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

背景

在开发业务需求时,遇到一个富文本的功能开发。原本以为像富文本这种实现,业内肯定有很成熟的方案来实现。但只要涉及到功能的定制,就十分不易开发。本文是基于 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 方法,该方法会遍历所有创建的 富文本实例,把除用户点击的富文本外的所有实例,都设置成 展示状态,被点击的实例设置成 编辑状态

总结

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

相关推荐
懒大王爱吃狼37 分钟前
Python教程:python枚举类定义和使用
开发语言·前端·javascript·python·python基础·python编程·python书籍
待磨的钝刨2 小时前
【格式化查看JSON文件】coco的json文件内容都在一行如何按照json格式查看
开发语言·javascript·json
逐·風5 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#
Devil枫5 小时前
Vue 3 单元测试与E2E测试
前端·vue.js·单元测试
尚梦6 小时前
uni-app 封装刘海状态栏(适用小程序, h5, 头条小程序)
前端·小程序·uni-app
GIS程序媛—椰子6 小时前
【Vue 全家桶】6、vue-router 路由(更新中)
前端·vue.js
前端青山7 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
毕业设计制作和分享7 小时前
ssm《数据库系统原理》课程平台的设计与实现+vue
前端·数据库·vue.js·oracle·mybatis
从兄8 小时前
vue 使用docx-preview 预览替换文档内的特定变量
javascript·vue.js·ecmascript
清灵xmf9 小时前
在 Vue 中实现与优化轮询技术
前端·javascript·vue·轮询