应监听 keydown + compositionend 组合事件捕获 @ 触发时机,插入 contenteditable="false" 的提及节点并手动设置光标位置,下拉列表需基于光标坐标动态定位且挂载至 body。
contenteditable 捕捉 @ 符号的触发时刻仅仅监听 input 事件并不可靠,因为用户可能粘贴文字、通过方向键移动光标,或删除中间字符,从而使 @ 出现在不符合预期的位置。真正需要监听的是 keydown + compositionend 这一组合,重点识别按下 @ 键时光标位于行首,或其前一个位置为空白的瞬间。
操作建议:
立即学习前端免费学习笔记(深入);
keydown 内部判断 e.key === '@',随后通过 getSelection().getRangeAt(0).startOffset 取得光标位置,并向前检查一个字符是空格、换行还是开头compositionend,否则在中文输入法环境下按完 @ 不会触发,因为输入法会先发送合成事件)input 中执行匹配,因为它不能分辨用户输入 @ 与粘贴含 @ 字符串两种行为,容易错误开启下拉列表不能用纯文本拼接,也不能直接插入 <span> 后还指望光标正常进出——contenteditable 对嵌套内联元素的光标行为很敏感。正确做法是用 document.execCommand('insertHTML', false, ...) 或现代 getSelection().setBaseAndExtent() 配合 Range.insertNode() 插入带 data-mention-id 和 contenteditable="false" 的 <span>。
操作建议:
立即学习前端免费学习笔记(深入);
contenteditable="false",否则双击后会进入编辑状态并破坏语义tabindex="0" 以及 role="button",使键盘用户能够聚焦,并通过回车触发详情display: inline-flex + align-items: center,防止标签换行时被拆分white-space: nowrap,否则长昵称无法折行,还会溢出容器下拉列表不能依靠固定 top/left定位,而应在每次展示时计算光标相对于编辑区域的绝对坐标,再借助 getBoundingClientRect() 取得光标所在 Text 节点的末端位置,最后叠加滚动偏移。若直接写死 position: absolute; top: 200px ,遇到缩放、滚动或多行编辑时必然发生错位。
操作建议:
立即学习前端免费学习笔记(深入);
window.getSelection().getRangeAt(0).getClientRects() 获取最后一个 DOMRect,其 bottom 可以作为下拉列表显示的基准线body 下面(使用 createPortal 或者 document.body.appendChild),以免被父容器 overflow: hidden 截断scroll 以及 resize 事件以重新定位,同时加入 throttle 防抖,避免拖动滚动条时出现卡顿aria-activedescendant 实现键盘导航,不应只支持鼠标操作问题根源在于插入的提及节点属于 contenteditable="false",而紧邻它的文本节点依旧可编辑。浏览器对假内联块与真文本之间的光标合并机制并不统一:Chrome中光标可能卡在提及右侧,Safari则可能直接跳到下一行开头。
操作建议:
立即学习前端免费学习笔记(深入);
Range 把光标放至提及节点后面:先创建新的 Range,range.setStartAfter(mentionNode),然后再 selection.removeAllRanges() + selection.addRange(range)
input,判断光标附近是否出现两个空格,并自动 trim() 后重新设置光标<div><Mention /></div>),否则会增加额外的 DOM 层级,使光标异常更加严重输入法兼容才是最难处理的部分:中文用户输入完“@张”但尚未选人便按下空格时,需要判断这是输入法中止信号,而不是真实空格,因此必须结合 compositionstart/compositionend 以及 keydown 的 key 状态进行交叉判断。对于这个边界情况,90% 的开源 mention 库都处理得不够准确。