Skip to content

React Hooks 实现 Mention 组件 #21

@MLuminary

Description

@MLuminary

Mention

记录使用 hooks 实现 mention 组件的历程 💻

State

measuring // 测量
measureLocation // 需要定位的位置
measureText // 用于搜索的本文
measurePrefix // 呼出 List 的符号 例如 '@'
isESC // 使用 ESC 关闭
activeIndex // 用于键盘事件中上下移动搜索的列表

Props

type Props = {
  setValue: (value: string) => void
  value: string
  trigger: string // '@'
  onSearch: (text: string) => void // 传递 MeasureText
  textareaRef?: React.RefObject<HTMLTextAreaElement> | null
  style?: React.CSSProperties
  className?: string
  onSelect?: (params: ItemType) => void
  MenuClassName?: string
  mountIn?: () => HTMLElement
  loading?: boolean
} & Omit<HTMLTextareaProps, 'onSelect'>

MeasureList 何时出现

MeasureList 在这仅代表呼出的搜索结果列表,很多人可能首先想到的是应该要确定 measureList 出现的位置,但其实我们只需要在 measureList 要出现的时候再计算其位置就可以,所以首先必须要确定 measureList 何时出现

规则:

  • 当光标左侧有 measurePrefix 存在并且之间没有空格时, 也就是 measureText 不含有空格

  • isESC 为 false 时「下文详细解释」

获取 MeasureText

// 获取光标左侧的文本 
textareaValue.slice(0, textAreaRef.current!.selectionStart)
// 获取距离光标最近的 MeasurePrefix 的位置, 也就是需要定位的位置
const lastMeasurePrefixLocation = selectionStartText.lastIndexOf(measurePrefix)
// 如果 lastMeasurePrefixLocation 不等于 -1 的情况下
// 此时 mesurePrefix 位置与光标位置之间的文本即为 measurePrefix
const measureText = selectionStartText.slice(lastMeasurePrefixLocation + measurePrefix.length)

当满足以上规则时 measuring 为 true

MeasurePrefix 的定位

当 measureList 出现时,需要用 measurePrefix 此时的位置来对 measureList 进行定位

在 textarea 中做定位相对困难,因此我们采用 div 来模拟 textarea 中的文本情况去实现 measurePrefix 的定位

首先需要将 textarea 中影响文本样式的 style 应用到 div 中去

useEffect(() => {
    if (textAreaRef.current) {
      const textareaCssProperties = getComputedStyle(textAreaRef.current!)
      const style = {
        lineHeight: textareaCssProperties.lineHeight!,
        fontSize: textareaCssProperties.fontSize!,
      }
      setTextAreaStyle(style)
    }
  }, [textAreaRef.current && textAreaRef.current.style, props.style])

模拟的 div 如下

const measureContent = `
	<div style={textAreaStyle} className=${classnames(props.className, Styles['measure-content'])}>
		<span dangerouslySetInnerHTML={{ __html: escape(value.slice(0, measureLocation)) }} />
		<span ref={measurePrefixRef}>@</span>
		<span dangerouslySetInnerHTML={{ __html: escape(value.slice( measureLocation)) }} />
	</div>`
// escape 函数主要是用来处理回车造成的文本换行,如果不转换成 <br> 的话,在 div 中文本会显示成一行,定位就会有错误。
const escape = function (text: string) {
    return text.replace(/<|>|`|"|&/g, '?').replace(/\r\n|\r|\n/g, '<br>')
}

模拟 div 的样式需要额外添加一些属性来使 div 完全模拟 textarea 的行为,当然 text-area-autosize 的行为也支持

.measure-content
  position absolute
  top 0
  right 0
  bottom 0
  left 0
  z-index -1
  overflow scroll
  white-space pre-wrap
  word-break break-all
  opacity 0

注意其共同的父 div 需要添加 position: relative

MeasureList 何时隐藏

  1. 当 measureText 中包含空格时

  2. 当选中 MeasureList 中的某一个 item 时「点击或 Enter」

  3. 当按下 ESC 键时

  4. 当光标的左侧无 measurePrefix 时

注意

按下 ESC 键时,此时 measureText 中不会包含空格,因此依然满足 measuring 为 true 的情况,所以需要对 ESC 进行特殊处理:当 measuring 为 true 时按下 ESC 键时我会将 isESC 置为 true,而当 isESC 为 true 时 measuring 则至为 false。isESC 会在下次普通的 keyUp 事件中重新置为 false

原文链接

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions