Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React Hooks 实现 Mention 组件 #21

Open
MLuminary opened this issue Sep 24, 2019 · 0 comments
Open

React Hooks 实现 Mention 组件 #21

MLuminary opened this issue Sep 24, 2019 · 0 comments

Comments

@MLuminary
Copy link
Owner

MLuminary commented Sep 24, 2019

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

原文链接

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant