记录使用 hooks 实现 mention 组件的历程
September 20, 2019
记录使用 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 何时隐藏
- 当
measureText
中包含空格时 - 当选中
MeasureList
中的某一个 item 时「点击或 Enter」 - 当按下 ESC 键时
- 当光标的左侧无
measurePrefix
时
注意
按下 ESC 键时,此时 measureText
中不会包含空格,因此依然满足 measuring
为 true
的情况,所以需要对 ESC 进行特殊处理:当 measuring
为 true
时按下 ESC 键时我会将 isESC
置为 true
,而当 isESC
为 true
时 measuring
则至为 false
。isESC
会在下次普通的 keyUp
事件中重新置为 false