Slate文档编辑器-Node节点与Path路径映射

news/2025/1/20 10:36:04/文章来源:https://www.cnblogs.com/WindrunnerMax/p/18680883

Slate文档编辑器-Node节点与Path路径映射

在之前我们聊到了slate中的Decorator装饰器实现,装饰器可以为我们方便地在编辑器渲染调度时处理range的渲染,这在实现搜索替换、代码高亮等场景非常有用。那么在这篇文章中,我们聊一下Node节点与Path路径映射,这里的Node指的是渲染的节点对象,Path则是节点对象在当前JSON中的路径,即本文的重点是如何确定渲染出的节点处于文档数据定义中的位置。

  • 在线编辑: https://windrunnermax.github.io/DocEditor
  • 开源地址: https://github.com/WindRunnerMax/DocEditor

关于slate文档编辑器项目的相关文章:

  • 基于Slate构建文档编辑器
  • Slate文档编辑器-WrapNode数据结构与操作变换
  • Slate文档编辑器-TS类型扩展与节点类型检查
  • Slate文档编辑器-Decorator装饰器渲染调度
  • Slate文档编辑器-Node节点与Path路径映射

渲染与命令

slate的文档中的03-defining-custom-elements一节中,我们可以看到我们可以看到slate中的Element节点是可以自定义渲染的,渲染的逻辑是需要我们根据propselement对象来判断类型,如果类型是code的话那就要渲染我们预定义好的CodeElement组件,否则渲染DefaultElement组件,这里的type是我们预设的init数据结构值,是数据结构的形式约定。

// https://docs.slatejs.org/walkthroughs/03-defining-custom-elements
const App = () => {const [editor] = useState(() => withReact(createEditor()))// Define a rendering function based on the element passed to `props`. We use// `useCallback` here to memoize the function for subsequent renders.const renderElement = useCallback(props => {switch (props.element.type) {case 'code':return <CodeElement {...props} />default:return <DefaultElement {...props} />}}, [])return (<Slate editor={editor} initialValue={initialValue}><Editable// Pass in the `renderElement` function.renderElement={renderElement}/></Slate>)
}

那么这里的渲染自然是不会有什么问题,我们的编辑器实际上必然不仅仅是要渲染内容,执行命令来变更文档结构/内容也是非常重要的事情。那么在05-executing-commands中一节中,我们可以看到对于文本内容加粗与代码块的切换分别是执行了addMark/removeMark以及Transforms.setNodes的函数来执行的。

// https://docs.slatejs.org/walkthroughs/05-executing-commands
toggleBoldMark(editor) {const isActive = CustomEditor.isBoldMarkActive(editor)if (isActive) {Editor.removeMark(editor, 'bold')} else {Editor.addMark(editor, 'bold', true)}
}toggleCodeBlock(editor) {const isActive = CustomEditor.isCodeBlockActive(editor)Transforms.setNodes(editor,{ type: isActive ? null : 'code' },{ match: n => Editor.isBlock(editor, n) })
}

路径映射

在上述的例子中看起来并没有什么问题,似乎我们对于编辑器基础的节点渲染与变更执行都已经完备了。然而,这里我们却可能忽略一个问题,为什么我们执行命令的时候slate可以知道我们要操作的是哪个节点,这是个很有趣的问题。如果将上述的例子运行起来的话,就可以发现我们直接执行上述操作非常依赖与光标的位置,这是因为在默认参数缺省的情况下就是取的选区位置来执行变更操作。这对于普通的节点渲染自然是没有问题的,但是当我们想实现比较复杂的模块或者交互时,例如表格模块与图片的异步上传等场景时,这可能并不足以让我们完成这些功能。

我们的文档编辑器当然并不是特别简单的场景,那么如果我们需要深入实现编辑器的复杂操作时,完全依赖选区来执行操作显然不够现实,例如我们希望在在代码块元素下面插入一个空行,由于选区必须要在Text节点上,我们不能直接操作选区到Node节点上,这种实现就不能直接依靠选区来完成。以及在单元格中得知当前处于表格的位置也不是件易事,因为此时的渲染调度是由框架来实现的,我们无法直接获取parent的数据对象。那么经常使用slate的同学都知道,无论是RenderElementProps还是RenderLeafProps在渲染的时候,除了attributes以及children等数据之外,是没有Path数据的传递的。

export interface RenderElementProps {children: any;element: Element;attributes: {// ...};
}
export interface RenderLeafProps {children: any;leaf: Text;text: Text;attributes: {// ...};
}

这个问题实际上不光在富文本编辑器中会出现,在重前端编辑的场景下都有可能会出现,例如低代码编辑器中。其共性是我们通常都会使用插件化的形式来实现编辑器,那么此时渲染的节点不是我们直接写的组件,而是由核心层与插件自行调度渲染的内容,单个定义的组件会被渲染N次,那么我们如果需要操作组件的数据,就需要知道到底是要更新哪个位置的数据对象,即在渲染的组件中如何得知我此时处在数据对象的什么位置。诚然对每个渲染的对象都定义id是个可行的方案,但是这样就必须要迭代整个对象来查找位置,我们在这里的实现则更加高效。

那么我们对于数据操作的时候Path是非常重要的,在平时的交互处理中,我们使用editor.selection就可以满足大部分功能了。然而很多情况下单纯用selection来处理要操作的目标Path是有些捉襟见肘的。那么此时在传递的数据结构中我们可以看到与Path最相关的数据就是element/text值了,那么此时我们可以比较轻松地记起在ReactEditor中存在findPath方法,可以让我们通过Node来查找对应的Path

// https://github.com/ianstormtaylor/slate/blob/25be3b/packages/slate-react/src/plugin/react-editor.ts#L90
findPath(editor: ReactEditor, node: Node): Path {const path: Path = []let child = nodewhile (true) {const parent = NODE_TO_PARENT.get(child)if (parent == null) {if (Editor.isEditor(child))   return pathelse break}const i = NODE_TO_INDEX.get(child)if (i == null) breakpath.unshift(i)child = parent}
}

简单压缩了代码,在这里的实现是通过两个WeakMap非常巧妙地让我们可以取得节点的Path。那么这里就需要思考一个问题,为什么我们不直接在RenderProps直接将Path传递到渲染的方法中,而是非得需要每次都得重新查找而浪费一部分性能。实际上,如果我们只是渲染文档数据,那么自然是不会有问题的,然而我们通常是需要编辑文档的,在这个时候就会出现问题。举个例子,假设我们在[10]位置有一个表格,而此时我们在[6]位置上增添了1个空白行,那么此时我们的表格Path就应该是[11]了,然而由于我们实际上并没有编辑与表格相关的内容,所以我们本身也不应该刷新表格的相关内容,自然其Props就不会变化,此时我们如果直接取值的话,则会取到[10]而不是[11]

那么同样的,即使我们用WeakMap记录NodePath的对应关系,即使表格的Node实际并没有变化,我们也无法很轻松地迭代所有的节点去更新其Path。因此我们就可以基于这个方法,在需要的时候查找即可。那么新的问题又来了,既然前边我们提到了不会更新表格相关的内容,那么应该如何更新其index的值呢,在这里就是另一个巧妙的方法了,在每次由于数据变化导致渲染的时候,我们同样会向上更新其所有的父节点,这点和immutable的模型是一致的,那么此时我们就可以更新所有影响到的索引值了。

那么如何避免其他节点的更新呢,很明显我们可以根据key去控制这个行为,对于相同的节点赋予唯一的id即可。另外在这里可以看出,useChildren是定义为Hooks的,那么其调用次数必定不会低,而在这里每次组件render都会存在findPath调用,所以这里倒也不需要太过于担心这个方法的性能问题,因为这里的迭代次数是由我们的层级决定的,通常我们都不会有太多层级的嵌套,所以性能方面还是可控的。

// https://github.com/ianstormtaylor/slate/blob/25be3b/packages/slate-react/src/hooks/use-children.tsx#L90
const path = ReactEditor.findPath(editor, node)
const children = []
for (let i = 0; i < node.children.length; i++) {const p = path.concat(i)const n = node.children[i] as Descendantconst key = ReactEditor.findKey(editor, n)// ...if (Element.isElement(n)) {children.push(<SelectedContext.Provider key={`provider-${key.id}`} value={!!sel}><ElementComponent /></SelectedContext.Provider>)} else {children.push(<TextComponent />)}NODE_TO_INDEX.set(n, i)NODE_TO_PARENT.set(n, node)
}

我们也可以借助这个概念来处理表格,当我们需要实现表格节点的复杂交互时,可以发现很难确定渲染节点的[RowIndex, ColIndex],即当前单元格在表格中的位置,我们需要这些信息来实现单元格选择和调整大小等功能。使用ReactEditor.findPath可以使用基于Node获取最新的Path,但是当数据嵌套层级较多时,例如表格中嵌套表格,这里就有很多不必要”的迭代。实际上两层就可以满足需求,但是使用ReactEditor.findPath会一直迭代到Editor Node,这在频繁触发的操作例如Resize中可能会导致一些性能问题。

而如果借助这个概念,我们就同样可以实现两个WeakMap,在最顶层节点即Table节点渲染时将映射关系建立好,此时就可以完全迭代Tr + Cellelement对象,在immutable的支持下,我们就可以得到当前单元格的索引值。当然在后期的slate中这两个WeakMap已经导出,不需要我们自行建立映射关系,只需要将其取出即可。

// https://github.com/ianstormtaylor/slate/pull/5657
export const Table: FC = () => {useMemo(() => {const table = context.element;table.children.forEach((tr, index) => {NODE_TO_PARENT.set(tr, table);NODE_TO_INDEX.set(tr, index);tr.children &&tr.children.forEach((cell, index) => {NODE_TO_PARENT.set(cell, tr);NODE_TO_INDEX.set(cell, index);});});}, [context.element]);
}export const Cell: FC = () => {const parent = NODE_TO_PARENT.get(context.element);console.log("RowIndex - CellIndex",NODE_TO_INDEX.get(parent!),NODE_TO_INDEX.get(context.element));
}

但是通过这种方式来获取NodePath节点的映射来获取位置就没有问题了嘛,高效的查找方案使得我们在这里必须依赖渲染后才可以得知节点最新的位置,也就是说当我们更新了节点对象后,如果此时立刻调用findPath方法是无法得到最新的Path的,因为此时的渲染行为是异步的。那么如果需要的话此时就必须要迭代整个数据对象来获取Path,当然我觉得这里倒是没有迭代整个对象的必要,在使用Transforms更改内容后,我们不应该立即获取路径值,而是等到React完成渲染后再进行下一步。这样我们可以按顺序执行相关操作,由于slate中没有额外的异步操作,我们可以轻松地在<Editable />useEffect中确定当前渲染何时完成。

export const WithContext: FC<{ editor: EditorKit }> = props => {const { editor, children } = props;const isNeedPaint = useRef(true);// 保证每次触发 Apply 时都会重新渲染// https://github.com/ianstormtaylor/slate/blob/25be3b/packages/slate-react/src/components/slate.tsx#L29useSlate();useEffect(() => {const onContentChange = () => {isNeedPaint.current = true;};editor.event.on(EDITOR_EVENT.CONTENT_CHANGE, onContentChange, 1);return () => {editor.event.off(EDITOR_EVENT.CONTENT_CHANGE, onContentChange);};}, [editor]);useEffect(() => {if (isNeedPaint.current) {Promise.resolve().then(() => {// https://github.com/ianstormtaylor/slate/issues/5697editor.event.trigger(EDITOR_EVENT.PAINT, {});});}isNeedPaint.current = false;});return children as JSX.Element;
};

最后

在这里我们主要讨论了Node节点与Path路径映射,即如何确定渲染出的节点处于文档数据定义中的位置,这是slate中实现数据变更时的重要表达,特别是在仅使用选区无法实现的复杂操作中,并且还分析了slate源码来探究了相关问题的实现。那么在后面的文章中,我们延续当前提到的表格但单元格位置的查找,来聊聊表格模块的设计及交互。

每日一题

  • https://github.com/WindRunnerMax/EveryDay

参考

  • https://docs.slatejs.org/
  • https://github.com/WindRunnerMax/DocEditor
  • https://github.com/ianstormtaylor/slate/blob/25be3b/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/872026.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue指令v-cloak,v-text,v-html,v-pre

1.v-cloak 配合css使用,用于解决加载过慢(复杂逻辑处理闪烁问题)编译时元素会自动加上属性v-cloak,编译结束后会自动消失 csscss[v-cloak]{display: none }html<div v-cloak> xxx</div> 2.v-html & v-text & v-pre 引用https://blog.csdn.net/weixin_5…

IDEA如何将代码进行注释

前言 大家好,我是小徐啊。我们在使用IDEA开发Java应用的时候,一般都是需要写注释的,这些注释帮助我们和别人更好的理解代码的含义,可以说是必不可少的。在使用IDEA开发时,其实是可以快捷的进行代码注释的,而不用手动去注释,那么IDEA该如何进行代码注释呢? 如何进行代码…

完整的 c 语言用到的基础知识总结

作者: baron 博客: baron-z.cn用于记录 c 语言基础知识,如果没有特别说明本文的所有代码编译环境为 gcc 编译器编译,学习资料来自狄泰软件学院可在淘宝购买学习一、基本数据类型 1、数据类型含义 固定内存大小的别名作用 创建变量c语言数据类型表Type Storage size Valu…

seqtk 生信工具的安装与使用

001、安装git clone https://github.com/lh3/seqtk.git cd seqtk/ make ./seqtk | head -n 3 002、

[Tools] SWC Intro

SWC SWC 英文全称为 Speedy Web Compiler,翻译成中文为“快速网页编译器”。 官网地址:https://swc.rs/来看一下官方的介绍:SWC is an extensible Rust-based platform for the next generation of fast developer tools. Its used by tools like Next.js, Parcel, and Deno…

数据迁移丨借助 AI 从 PostgreSQL 到 GreatSQL

数据迁移丨借助 AI 从 PostgreSQL 到 GreatSQL本文将介绍如何从 PostgreSQL 到 GreatSQL 的数据迁移,并运用 AI 协助迁移更加方便。迁移的方式有很多,例如:pg_dump:导出SQL文件,修改后导入 GreatSQL 数据库。 COPY:导出txt文本文件,导入 GreatSQL 数据库。 pg2mysql:从…

圆方树学习笔记

元方树。 下文除特殊强调外,所有图皆为无向图。 引入割点:在图中,删除某个点后,导致图不再连通的点。 点双连通:在一张图中,取两个点 \(u\)、\(v\),无论删去哪个点(除 \(u\)、\(v\) 自身外),\(u\)、\(v\) 都能连通,我们就说 \(u\) 和 \(v\) 点双连通。 点双连通分量…

WordPress产品导入后内容出现乱码,以及附属一些别的功能

效果图如下 该插件附带了一个可以把产品描述里面的超链接给去掉,以及有的产品图片点击会在地址栏上面显示图片的路径,在该插件可以进行关闭,并且替换成一个模态窗,还有对产品邮费展示进行了处理,到金额到达包邮的时候,别的邮费进行隐藏 下面是该插件源码目录结构duola …

如何利用甘特图进行高效管理?——附应用工具

通过快速创建甘特图并合理利用其进行项目管理,可以显著提升项目管理的效率和质量。同时,不断优化甘特图和项目管理流程也是实现持续改进的关键。甘特图通过横轴表示时间,纵轴表示任务,每个任务的开始和结束时间通过横向条形表示。甘特图可以帮助管理者对项目中的各个任务进…

互联网(internet)的基本组成

一般而言,互联网被认为由三个部分组成: Edge NetworkDevices and Endpoints: This part consists of all the devices at the periphery of the network that users directly interact with. It includes personal computers, laptops, smartphones, tablets, and various In…

WebRTC 笔记

目录通话建立流程特别提醒代码创建 RTCPeerConnection 对象获取本地摄像头/麦克风创建发起方会话描述对象(createOffer)连接的远程对等方属性(setRemoteDescription)建立一条最优的连接方式 WebRTC 允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(…