react-mentions 实例

2021-11-09  本文已影响0人  欢欣的膜笛

需求背景

实现

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;
}
上一篇 下一篇

猜你喜欢

热点阅读