react-mentions 實例

需求背景

  • 類似微博評論功能 @ 用戶的功能
  • 列表中點擊某項溯职,將其插入文本框失焦處

實現(xiàn)

  • 插件:https://github.com/signavio/react-mentions
  • 已實現(xiàn)功能
    • plainText膊升、rawText 格式均可自定義
    • 喚起字符封寞,可自定義墓塌,默認(rèn) @
    • 文本框高度自適應(yīng)
    • 整體高亮需五,中間不可插入护桦,刪除時為一個整體
    • 外部列表點擊時廊宪,由于 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 })
  }

  // 判斷光標(biāo)是否在復(fù)合指標(biāo)之間懈玻,以及光標(biāo)之前復(fù)合指標(biāo)的個數(shù)
  getCursorInfo = (caretPos = 0, mentions = []) => {
    // 光標(biāo)之前,復(fù)合指標(biāo)乾颁,markup 比 displayTransform 多的字節(jié)數(shù)
    let byteNum = 0
    // 光標(biāo)之前涂乌,復(fù)合指標(biāo)個數(shù)
    let num = 0
    // 光標(biāo)是否在復(fù)合指標(biāo)之間
    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('指標(biāo)中間不能插入指標(biāo)')
      return
    }
    const rawTextCaretPos = caretPos + byteNum
    mentionObj.index = rawTextCaretPos
    mentions.splice(num, 0, mentionObj)
    // 如果插入的指標(biāo),不是最后一個復(fù)合指標(biāo)英岭,需更新該指標(biāo)之后的指標(biāo)的 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;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末湾盒,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子诅妹,更是在濱河造成了極大的恐慌罚勾,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吭狡,死亡現(xiàn)場離奇詭異荧库,居然都是意外死亡,警方通過查閱死者的電腦和手機赵刑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門分衫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人般此,你說我怎么就攤上這事蚪战。” “怎么了铐懊?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵邀桑,是天一觀的道長。 經(jīng)常有香客問我科乎,道長壁畸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮捏萍,結(jié)果婚禮上太抓,老公的妹妹穿的比我還像新娘。我一直安慰自己令杈,他們只是感情好走敌,可當(dāng)我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著逗噩,像睡著了一般掉丽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上异雁,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天捶障,我揣著相機與錄音,去河邊找鬼纲刀。 笑死项炼,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的柑蛇。 我是一名探鬼主播芥挣,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼耻台!你這毒婦竟也來了空免?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤盆耽,失蹤者是張志新(化名)和其女友劉穎蹋砚,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體摄杂,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡坝咐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了析恢。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片墨坚。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖映挂,靈堂內(nèi)的尸體忽然破棺而出泽篮,到底是詐尸還是另有隱情,我是刑警寧澤柑船,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布帽撑,位于F島的核電站,受9級特大地震影響鞍时,放射性物質(zhì)發(fā)生泄漏亏拉。R本人自食惡果不足惜扣蜻,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望及塘。 院中可真熱鬧莽使,春花似錦、人聲如沸磷蛹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽味咳。三九已至,卻和暖如春檬嘀,著一層夾襖步出監(jiān)牢的瞬間槽驶,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工鸳兽, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留掂铐,地道東北人。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓揍异,卻偏偏與公主長得像全陨,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子衷掷,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容