美文网首页
react-mentions 实例

react-mentions 实例

作者: 欢欣的膜笛 | 来源:发表于2021-11-09 23:01 被阅读0次

需求背景

  • 类似微博评论功能 @ 用户的功能
  • 列表中点击某项,将其插入文本框失焦处

实现

  • 插件:https://github.com/signavio/react-mentions
  • 已实现功能
    • plainText、rawText 格式均可自定义
    • 唤起字符,可自定义,默认 @
    • 文本框高度自适应
    • 整体高亮,中间不可插入,删除时为一个整体
    • 外部列表点击时,由于 plainText 和 rawText 不一致,重新计算实际的插入位置

npm install react-mentions --save

import React, { Component } from 'react';
import { render } from 'react-dom';
import './style.css';
import { MentionsInput, Mention } from 'react-mentions';

class App extends Component {
  constructor() {
    super();
    this.state = {
      caretPos: 0,
      value: '',
      mentions: null,
      users: [
        {
          _id: 1000,
          name: { first: 'John', last: 'Reynolds' },
        },
        {
          _id: 10001,
          name: { first: 'Holly', last: 'Reynolds' },
        },
        {
          _id: 100002,
          name: { first: 'Ryan', last: 'Williams' },
        },
      ],
    };

    this.expInputRef = React.createRef()
  }

  handleChange = (event, newValue, newPlainTextValue, mentions) => {
    this.setState({
      value: newValue,
      mentions,
    });
  };

  handleBlur = event => {
    event.persist()
    this.setState({ caretPos: event?.target?.selectionStart || 0 })
  }

  // 判断光标是否在复合指标之间,以及光标之前复合指标的个数
  getCursorInfo = (caretPos = 0, mentions = []) => {
    // 光标之前,复合指标,markup 比 displayTransform 多的字节数
    let byteNum = 0
    // 光标之前,复合指标个数
    let num = 0
    // 光标是否在复合指标之间
    let isMiddle = false

    mentions.some(({ plainTextIndex, display, id }) => {
      if (plainTextIndex < caretPos) {
        const strEndIndex = plainTextIndex + display.length
        if (strEndIndex < caretPos) {
          byteNum += String(id).length + 6
          num++
        }
        if (strEndIndex === caretPos) {
          byteNum += String(id).length + 6
          num++
          return true
        }
        if (strEndIndex > caretPos) {
          isMiddle = true
          return true
        }
      }
      if (plainTextIndex === caretPos) {
        return true
      }
    })

    return {
      byteNum,
      num,
      isMiddle,
    }
  }

  // `[${display}]`, id, `{{[${display}(${id})}}`)
  handleIndexSelect(display, id, str) {
    const { value = '', caretPos = 0, mentions = [] } = this.state
    const mentionObj = {
      display,
      id,
      index: caretPos,
      plainTextIndex: caretPos,
    }
    const plainTextCaretPos = caretPos + display.length

    if (!value?.trim() || !mentions?.length) {
      this.doInserIndex(str, value, caretPos, plainTextCaretPos)
      this.setState({
        mentions: [mentionObj],
      })
      return
    }

    const { byteNum, num, isMiddle } = this.getCursorInfo(caretPos, mentions)

    if (isMiddle) {
      alert('指标中间不能插入指标')
      return
    }
    const rawTextCaretPos = caretPos + byteNum
    mentionObj.index = rawTextCaretPos
    mentions.splice(num, 0, mentionObj)
    // 如果插入的指标,不是最后一个复合指标,需更新该指标之后的指标的 mention
    if (num + 1 < mentions.length) {
      for (let index = num + 1; index < mentions.length; index++) {
        const mention = mentions[index]
        mention.plainTextIndex += display.length
        mention.index += str.length
      }
    }
    this.doInserIndex(str, value, rawTextCaretPos, plainTextCaretPos)
    this.setState({
      mentions,
    })
  }

  doInserIndex = (str, value, rawTextCaretPos, plainTextCaretPos) => {
    this._expFocus()
    const newValue = this._insertStr(str, value, rawTextCaretPos)
    this.setState({
      value: newValue,
    })
    if (!this.expInputRef.current) {
      return
    }
    const $node = this.expInputRef.current
    this._setCaretPos($node, plainTextCaretPos)
  }

  _insertStr(source = '', target = '', pos) {
    const startPart = target.substring(0, pos)
    const endPart = target.substring(pos)
    return `${startPart}${source}${endPart}`
  }

  _setCaretPos($input, pos) {
    if (!$input) {
      return
    }
    setTimeout(() => {
      if ($input.createTextRange) {
        const range = $input.createTextRange()
        range.collapse(true)
        range.moveEnd('character', pos)
        range.moveStart('character', pos)
        range.select()
      } else if ($input.setSelectionRange) {
        $input.setSelectionRange(pos, pos)
      }
    }, 200)
  }

  _expFocus() {
    if (!this.expInputRef.current) {
      return
    }
    setTimeout(() => {
      const node = this.expInputRef.current
      node.focus()
    }, 200)
  }

  render() {
    const userMentionData = this.state.users.map((myUser) => ({
      id: myUser._id,
      display: `${myUser.name.first} ${myUser.name.last}`,
    }));

    return (
      <div>
        <p>Start editing to see some magic happen :)</p>
        <MentionsInput
          className="mentions"
          placeholder={`Type anything, use the @ symbol to tag other users.`}
          value={this.state.value}
          markup="{{[__display__](__id__)}}"
          allowSpaceInQuery
          displayTransform={(id, display) => `[${display}]`}
          inputRef={event => this.expInputRef.current = event}
          onChange={this.handleChange}
          onBlur={this.handleBlur}
        >
          <Mention
            type="index"
            trigger={/(?:^|.)(@([^.@]*))$/}
            data={userMentionData}
            className="mentions__mention"
          />
        </MentionsInput>

        <h3>The raw text is: {this.state.value}</h3>
        <ul className="index-list">
          {userMentionData.map(({ id, display }) => (
            <li key={id} onClick={() => this.handleIndexSelect(`[${display}]`, id, `{{[${display}](${id})}}`)}>
              {display}
            </li>
          ))}
        </ul>
      </div>
    )
  }
}

render(<App />, document.getElementById('root'));
.mentions {
  margin: 0;
  padding: 0;
  font-size: 14px;
  color: #60626b;
}

.mentions .mentions__control {
  min-height: 120px;
}

.mentions:focus-within .mentions__input {
  border-color: #5d95fc;
  outline: 0;
  box-shadow: 0 0 0 2px rgb(50 109 240 / 20%); 
}

.mentions .mentions__highlighter {
  padding: 4px 11px;
  line-height: 32px;
  border: 1px solid transparent;
  height: auto!important;
}

.mentions .mentions__input {
  padding: 4px 11px;
  min-height: 120px;
  line-height: 32px;
  outline: 0;
  border: 1px solid #dee0e8;
}

.mentions__mention {
  background-color: #d9e4ff;
}

.mentions__suggestions__list {
  width: 140px;
  line-height: 20px;
  color: #60626b;
  font-size: 12px;
  border: 1px solid #e8eaf2;
  box-shadow: 0px 2px 8px rgba(61, 67, 102, 0.148055);
  border-radius: 2px;
  background-color: #fff;
}

.mentions__suggestions__item {
  padding: 0 8px;
}

.mentions__suggestions__item:hover {
  background: #f4f6fc;
  color: #507ff2;
}

.mentions__suggestions__item--focused {
  background: #f4f6fc;
  color: #507ff2;
}

.index-list {
  padding: 0;
  margin: 0;
  width: 300px;
  border: 1px solid #e8eaf2;
  border-radius: 2px;
}

.index-list li {
  padding: 0 20px;
  margin: 0;
  list-style: none;
  line-height: 30px;
  cursor: pointer;
}

.index-list li:hover {
  background-color: #f4f6fc;
}

相关文章

  • react-mentions 实例

    需求背景 类似微博评论功能 @ 用户的功能 列表中点击某项,将其插入文本框失焦处 实现 插件:https://gi...

  • SQL C语言基本操作

    相关API 打开 实例 关闭 实例 获取错误消息 操作表 实例创建 实例插入 实例修改 实例删除 实例回调查询 非回调

  • Python-数据类型及其操作方法

    数字类型 代码实例: 字符串类型 代码实例: 列表 代码实例: 元组 代码实例 字典: 代码实例 集合 代码实例:

  • HTML基础-03

    HTML 标题 实例 HTML 段落 实例 HTML 链接 实例 HTML 图像 实例

  • Python 类属性、实例属性、类方法、实例方法

    1、实例属性 实例属性,就是赋给由类创建的实例的属性,实例属性属于它所属的实例,不同实例之间的实例属性可以不同。 ...

  • STL算法之常用拷贝和替换

    copy API 实例 replace API 实例 replace_if API 实例 swap API 实例

  • Vue 基础

    Vue 实例 1. Vue实例 2. 实例属性 3. 实例方法/数据 4. 实例方法/事件 5. 实例方法/生命周...

  • 类中的方法

    1.实例方法的调用方式 实例对象.实例方法() 类对象.实例方法(实例对象) 例如: class Student ...

  • C语言100例

    C 练习实例01C 练习实例02C 练习实例03C 练习实例04C 练习实例05C 练习实例06 C 练习实例07...

  • AWS云计算助手级架构师认证之EC2-实例购买类型

    在购买EC2实例的时候,这里有三种购买类型需要理解:按需实例,预留实例,计划实例,竞价实例。 按需实例: ...

网友评论

      本文标题:react-mentions 实例

      本文链接:https://www.haomeiwen.com/subject/bfovzltx.html