Lexical编辑器聚焦HidingNode显示致Selection光标偏移求解
解决方案
这个需求完全可实现,你之前的偏移补偿思路走不通的核心原因是:靠字符数换算像素位置的逻辑,在非等宽字体场景下本身就不成立,不需要在这个方向继续耗时间。
问题根源
你之前拿不到正确点击位置,是因为绑定的事件回调执行时机晚于浏览器原生光标定位+节点重排流程:当点击事件冒泡到你绑定的逻辑时,HidingNode已经从display: none切换为显示状态,完成了布局偏移,此时不管是读原生Selection还是算坐标,拿到的都是错位后的结果。
实现步骤
核心思路是在DOM发生重排前的捕获阶段拦截点击事件,提前拿到真实点击位置,再手动控制节点显示和光标定位,完全绕开浏览器默认的错位定位逻辑:
- 给编辑器根节点绑定
mousedown事件,开启捕获模式(useCapture: true),回调第一时间调用event.preventDefault()阻止浏览器默认的光标定位行为。此时编辑器还处于失焦状态,所有HidingNode都处于隐藏状态不参与布局,不存在位置偏移。 - 在同一个回调里,直接用浏览器原生API传入点击坐标,取出当前状态下真实的光标位置:现代浏览器用
document.caretPositionFromPoint(e.clientX, e.clientY),旧版webkit内核浏览器兼容用document.caretRangeFromPoint(e.clientX, e.clientY),拿到的节点和偏移量就是用户点击瞬间的真实目标。 - 等
HidingNode切换为显示、DOM重排完成后(用requestAnimationFrame等一帧即可),通过Lexical的API把选区手动设置到你提前存好的目标位置,不要用浏览器默认生成的选区。 - 键盘聚焦(Tab切换、代码调用focus)的场景不需要做这个处理,直接在失焦时缓存最后一次的选区位置,聚焦时恢复即可,不会出现点击错位的问题。如果需要支持触屏端,再加个捕获阶段的
touchstart事件,逻辑和mousedown完全一致。
核心代码示例(基于@lexical/react)
把下面这个自定义插件插入到你的LexicalComposer子组件里即可:
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { $getNearestNodeFromDOMNode, $setSelection, $isTextNode } from 'lexical'; import { useEffect } from 'react'; // 替换成你项目中HidingNode判断方法的实际引入路径 import { $isHidingNode } from './nodes/HidingNode'; export default function CursorPositionFixPlugin() { const [editor] = useLexicalComposerContext(); useEffect(() => { const editorRoot = editor.getRootElement(); if (!editorRoot) return; const handlePointerDown = (e) => { // 只响应左键点击/单指触摸 if (e.type === 'mousedown' && e.button !== 0) return; // 阻止默认的光标定位逻辑,避免错位 e.preventDefault(); // 获取点击坐标 const clientX = e.clientX ?? e.touches?.[0]?.clientX; const clientY = e.clientY ?? e.touches?.[0]?.clientY; if (clientX === undefined || clientY === undefined) return; // 此时HidingNode仍处于隐藏状态,直接取真实光标位置 const caretPos = document.caretPositionFromPoint(clientX, clientY) || document.caretRangeFromPoint(clientX, clientY); if (!caretPos) return; const { offsetNode, offset } = caretPos; // 等下一帧DOM更新(HidingNode显示完成)后手动设置选区 requestAnimationFrame(() => { editor.update(() => { const targetNode = $getNearestNodeFromDOMNode(offsetNode); if (!targetNode) return; // 文本节点直接定位到对应偏移 if ($isTextNode(targetNode) && !$isHidingNode(targetNode)) { const selection = targetNode.select(offset, offset); $setSelection(selection); } }); }); }; // 捕获阶段绑定事件,优先于所有内部逻辑执行 editorRoot.addEventListener('mousedown', handlePointerDown, true); editorRoot.addEventListener('touchstart', handlePointerDown, { capture: true, passive: false }); return () => { editorRoot.removeEventListener('mousedown', handlePointerDown, true); editorRoot.removeEventListener('touchstart', handlePointerDown, { capture: true }); }; }, [editor]); return null; }
补充说明
接入后可以删掉你之前写的字符数偏移补偿逻辑,这套方案和字体宽度无关,不管是等宽还是非等宽字体都能准确定位,不会出现位置偏移。
内容的提问来源于stack exchange,提问作者Michal Kurz
相关产品推荐
相关产品推荐

