精准定位:解决富文本编辑器光标问题

背景

在开发业务需求时,遇到一个富文本的功能开发。原本以为像富文本这种实现,业内肯定有很成熟的方案来实现。但只要涉及到功能的定制,就十分不易开发。本文是基于 Slate 开发的富文本编辑器,将详细介绍如何解决 鼠标光标在富文本编辑器中定位富文本编辑器多实例管理 的问题。

准备

可以参考 Slate 文档 开发基础的富文本编辑器功能

bash 复制代码
yarn add slate slate-react

需求描述

  1. 富文本在使用中,分两种状态 编辑状态纯展示状态,是有两种不同的 UI
  2. 当用户点击 纯展示状态 下的文字时,编辑器要转换成 编辑状态,且光标要自动定位到点击的文字上
  3. 同一页面会有多个富文本实例,但始终 只能有一个实例处于编辑状态

分析

  1. 展示两种不同的 UI 很好实现,无非就是写个逻辑判断 { canEditor ? <编辑状态 UI /> : <展示状态 UI />}
  2. 得记录鼠标的位置,在切换编辑器状态后,根据记录的位置,手动更新光标的位置
  3. 需要用到类组件,并维护一个实例队列,遍历确定每个实例的状态

解决方案

光标定位

为了解决鼠标光标在富文本编辑器中定位的问题,我们首先需要记录用户点击或选中文本时的位置信息。然后,在 编辑状态 下,将鼠标光标移动到正确的位置。

记录鼠标选择的位置信息

展示状态 下,用户点击文本时,我们需要记录鼠标选择的位置信息。这里,我们使用 recordSelection 方法来实现这个功能:

ts 复制代码
// 获取鼠标选择的位置信息
const getSelectionRecord = useCallback((): SelectionRecord => {
    if (edit) return window.__selectionRecord;
    return selectionRecordRef.current;
}, [edit]);

 // 获取当前的Selection
const getSelection = useCallback(() => {
    if (edit) return (document.querySelector('#iframe-root') as HTMLIFrameElement)?.contentWindow?.getSelection();
    return window.getSelection();
}, [edit]);

const recordSelection = useCallback(() => {
  const selection = getSelection();
  const { anchorNode, anchorOffset } = selection || {};
  if (!anchorNode?.parentNode) {
    setSelectionRecord(null);
    return;
  }
  let node = anchorNode.parentNode as Node;
  let index = 0;
  while ((node = node.previousSibling as Node) !== null) index += 1;
  setSelectionRecord([index, anchorOffset || 0]);
}, [getSelection, setSelectionRecord]);

recordSelection 方法中,我们首先获取当前的Selection对象。然后,从Selection对象中获取 anchorNodeanchorOffset 属性。anchorNode 表示鼠标选择的起始节点,anchorOffset 表示鼠标选择的起始位置相对于 anchorNode 的偏移量。

接下来,我们遍历 anchorNode 的所有前一个兄弟节点,计算出 anchorNode 在其父节点中的索引(index)。最后,我们将 indexanchorOffset 作为鼠标选择的位置信息,存储到 selectionRecordRef 中。

更新鼠标光标到富文本编辑器中

在切换成 编辑状态 时,我们需要将鼠标光标移动到正确的位置。这里,我们使用 updateMouseCursor 方法来实现这个功能:

ts 复制代码
const updateMouseCursor = useCallback(() => {
  setTimeout(() => {
    const selectionRecord = getSelectionRecord();
    if (!selectionRecord) return;
    const [index, anchorOffset] = selectionRecord;
    const pElement = richTextRef.current?.querySelectorAll('.cloud-sidebar__rich-text__editor-wrap>div:not(.cloud-sidebar__rich-text__editor-placeholder)>p',)?.[index];
    const text = pElement?.querySelector('span>span>span')?.childNodes?.[0];
    if (!text) return;
    const selection = getSelection();
    selection?.collapse(text, Math.min(anchorOffset, (text as Text).length));
  });
}, [getSelectionRecord, getSelection]);

updateMouseCursor 方法中,我们首先获取之前记录的鼠标选择的位置信息(selectionRecord)。然后,根据 indexanchorOffset,找到富文本编辑器中对应的文本节点(text)。

接下来,我们获取当前的Selection对象,并使用collapse方法将鼠标光标移动到text节点的 anchorOffset 位置。这样,当用户开始编辑文本时,操作将应用在正确的位置。

需要注意的是,我们在 updateMouseCursor 方法中使用了 setTimeout。这是因为在某些情况下,浏览器可能需要一点时间来渲染富文本编辑器。通过将 updateMouseCursor 放入事件循环的下一轮,我们确保了富文本编辑器 切换状态 的渲染已经完成,从而可以正确地定位鼠标光标。

整合到富文本编辑器

接下来,我们需要将上述方法整合到富文本编辑器中。首先,在用户点击 展示状态 时,我们调用 recordSelection 方法记录鼠标选择的位置信息。然后,设置 canEditor 状态为 true (切换成 编辑状态),并调用 changeInteractive 方法。

ts 复制代码
const onClick = () => {
  setCanEditor(true);
  recordSelection();
  changeInteractive();
};

useEffect中,我们根据showRichText状态决定是否显示富文本编辑器。当showRichTexttrue时,我们调用updateMouseCursor方法将鼠标光标移动到正确的位置。

ts 复制代码
useEffect(() => {
  onChangeStatus?.(showRichText ? 'input' : 'default');
  if (showRichText) updateMouseCursor();
  return () => onChangeStatus('default');
}, [showRichText]);

这样,我们就实现了鼠标光标在富文本编辑器中的定位功能。

只能有一个实例处于编辑状态

我们通过 函数式组件 实现了富文本编辑器,但是如果涉及到多个富文本的状态切换,还是需要借助 类组件 的方式,结构类似于:

ts 复制代码
class RichTextClass extends React.Component {
    render() {
        <RichText {...props} />
    }
}

我们需要在类组件中定义一个实例数组,每创建一个富文本编辑器,都要把组件实例push到数组中

ts 复制代码
static instanceList: ToolbarRichText[] = [];

componentDidMount() {
    RichTextClass.instanceList.push(this);
}

再定义一个 change 方法,用于感知 函数式组件 的状态,是 编辑状态 还是 展示状态

ts 复制代码
changeInteractive = () => {
    RichTextClass.instanceList.forEach((instance) => {
      const interactive = instance === this ? 'focus' : 'blur';
      if (interactive === instance.state.interactive) return;
      instance.setState({ interactive });
    });
};

interactive 属性会透传给函数式的编辑器组件,用于控制状态的切换

ts 复制代码
class RichTextClass extends React.Component {
    render() {
        <RichText
          {...props}
          interactive={interactive}
          changeInteractive={changeInteractive}
        />
    }
}

所以整个流程是:当用户第一次点击富文本编辑器组件时,触发 changeInteractive 方法,该方法会遍历所有创建的 富文本实例,把除用户点击的富文本外的所有实例,都设置成 展示状态,被点击的实例设置成 编辑状态

总结

虽然本文提供的解决方案可能较为简单,但它提供了一个基本思路。在实际项目中,大家可能需要根据具体需求和浏览器兼容性进行相应的调整。

相关推荐
燃先生._.23 分钟前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖1 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235241 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240252 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar2 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人3 小时前
前端知识补充—CSS
前端·css
GISer_Jing3 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试
m0_748245523 小时前
吉利前端、AI面试
前端·面试·职场和发展
理想不理想v3 小时前
webpack最基础的配置
前端·webpack·node.js