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

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.mzph.cn/pingmian/67773.shtml

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

相关文章

麒麟系统中删除权限不够的文件方法

在麒麟系统中删除权限不够的文件&#xff0c;可以尝试以下几种方法&#xff1a; 通过修改文件权限删除 打开终端&#xff1a;点击左下角的“终端”图标&#xff0c;或者通过搜索功能找到并打开终端 。定位文件&#xff1a;使用cd命令切换到文件所在的目录 。修改文件权限&…

Kotlin语言的正则表达式

Kotlin语言中的正则表达式 引言 正则表达式作为一种强大的文本处理工具&#xff0c;广泛应用于字符串匹配、数据验证、文本搜索等场景。在Kotlin语言中&#xff0c;正则表达式的应用同样得到了广泛关注。Kotlin不仅具备与Java相同的正则表达式功能优势&#xff0c;还提供了更…

Flask简介与安装以及实现一个糕点店的简单流程

目录 1. Flask简介 1.1 Flask的核心特点 1.2 Flask的基本结构 1.3 Flask的常见用法 1.3.1 创建Flask应用 1.3.2 路由和视图函数 1.3.3 动态URL参数 1.3.4 使用模板 1.4 Flask的优点 1.5 总结 2. Flask 环境创建 2.1 创建虚拟环境 2.2 激活虚拟环境 1.3 安装Flask…

RFID系统安全认证协议及防碰撞算法研究(RFID Security)

目录 1.摘要 2.引言 3.前人研究成果 3.1 RFID系统协议模型 3.2 RFID系统安全认证协议分类 3.3 RFID安全认证协议及其研究 3.3.1 超轻量级安全认证协议及其研究 3.3.2 轻量级安全认证协议及其研究 3.3.2 中量级安全认证协议及其研究 3.3.3 重量级安全认证协议及其研究…

Docker 实现MySQL 主从复制

一、拉取镜像 docker pull mysql:5.7相关命令&#xff1a; 查看镜像&#xff1a;docker images 二、启动镜像 启动mysql01、02容器&#xff1a; docker run -d -p 3310:3306 -v /root/mysql/node-1/config:/etc/mysql/ -v /root/mysql/node-1/data:/var/lib/mysql -e MYS…

Spring MVC:设置响应

目录 引言 1. 返回静态页面 1.1 Spring 默认扫描路径 1.2 RestController 1.2.1 Controller > 返回页面 1.2.2 ResponseBody 2. 返回 HTML 2.1 RequestMapping 2.1.1 produces(修改响应的 Content-Type) 2.1.2 其他属性 3. 返回 JSON 4. 设置状态码 4.1 HttpSer…

在Windows/Linux/MacOS C++程序中打印崩溃调用栈和局部变量信息

打印崩溃调用栈和局部变量信息的方法有所不同。以下是针对 Windows、Linux 和 MacOS 的示例代码。 Windows 在 Windows 上&#xff0c;可以使用 Windows API 来捕获异常并打印调用栈。 #include <windows.h> #include <DbgHelp.h> #include <stdio.h> #in…

基于python+Django+mysql鲜花水果销售商城网站系统设计与实现

博主介绍&#xff1a;黄菊华老师《Vue.js入门与商城开发实战》《微信小程序商城开发》图书作者&#xff0c;CSDN博客专家&#xff0c;在线教育专家&#xff0c;CSDN钻石讲师&#xff1b;专注大学生毕业设计教育、辅导。 所有项目都配有从入门到精通的基础知识视频课程&#xff…

提示词的艺术----AI Prompt撰写指南(个人用)

提示词的艺术 写在前面 制定提示词就像是和朋友聊天一样&#xff0c;要求我们能够清楚地表达问题。通过这个过程&#xff0c;一方面要不断练习提高自己地表达能力&#xff0c;另一方面还要锻炼自己使用更准确精炼的语言提出问题的能力。 什么样的提示词有用&#xff1f; 有…

MySQL管理事务处理

目录 1、事务处理是什么 2、控制事务处理 &#xff08;1&#xff09;事务的开始和结束 &#xff08;2&#xff09;回滚事务 &#xff08;3&#xff09;使用COMMIT &#xff08;4&#xff09;使用保留点 &#xff08;5&#xff09;结合存储过程的完整事务例子 3、小结 …

oneplus3t-lineageos-16.1编译-android9,

oneplus3t-lineageos-16.1编译-android9 oneplus3t 前提 救砖线刷 OnePlus3t android9 OTA卡刷 OnePlus3t android9 APatch root debian11(标准GNU工具集) arm 工具盘(chroot 风格rootfs, 含有 比如sshd 、gdb) : tinan/eadb.git 本仓库开发已经完毕,使用请直接从4.2开始…

Linux网络 序列化与反序列化

概念 序列化&#xff08;Serialization&#xff09;是将对象的状态信息转换为可以存储或传输的形式的过程。以下是关于序列化与反序列化的介绍&#xff1a; 序列化&#xff1a;将对象的状态信息转换为可以存储或传输的格式&#xff0c;通常是字节序列或文本格式。反序列化&am…

使用 spring boot 2.5.6 版本时缺少 jvm 配置项

2.5.6我正在使用带有版本和springfox-boot-starter版本的Spring Boot 项目3.0.0。我的项目还包括一个WebSecurityConfig扩展WebSecurityConfigurerAdapter并实现WebMvcConfigurer的类。但是&#xff0c;我面临的问题是指标在端点jvm_memory_usage_after_gc_percent中不可见/act…

python在财务领域的应用

财务岗位在处理数据时&#xff0c;经常会遇到一些复杂的场景&#xff0c;Excel 虽然功能强大&#xff0c;但在某些情况下可能无法高效或灵活地解决问题。以下是一些常见的、需要用编程&#xff08;如 Python、R 或 SQL&#xff09;来解决的数据问题&#xff1a; 1. 大规模数据处…

ZooKeeper 中的 ZAB 一致性协议与 Zookeeper 设计目的、使用场景、相关概念(数据模型、myid、事务 ID、版本、监听器、ACL、角色)

参考Zookeeper 介绍——设计目的、使用场景、相关概念&#xff08;数据模型、myid、事务 ID、版本、监听器、ACL、角色&#xff09; ZooKeeper 设计目的、特性、使用场景 ZooKeeper 的四个设计目标ZooKeeper 可以保证如下分布式一致性特性ZooKeeper 是一个典型的分布式数据一致…

Objective-C语言的数据类型

Objective-C数据类型详解 Objective-C是一种面向对象的编程语言&#xff0c;主要用于macOS和iOS应用程序的开发。作为C语言的超集&#xff0c;Objective-C继承了C语言的基本数据类型&#xff0c;同时也引入了一些独特的特性。本文将对Objective-C的各种数据类型进行详细的介绍…

Spring Boot自动配置原理:如何实现零配置启动

引言 在现代软件开发中&#xff0c;Spring 框架已经成为 Java 开发领域不可或缺的一部分。而 Spring Boot 的出现&#xff0c;更是为 Spring 应用的开发带来了革命性的变化。Spring Boot 的核心优势之一就是它的“自动配置”能力&#xff0c;它极大地简化了 Spring 应用的配置…

大模型GUI系列论文阅读 DAY2续2:《使用指令微调基础模型的多模态网页导航》

摘要 自主网页导航的进展一直受到以下因素的阻碍&#xff1a; 依赖于数十亿次的探索性交互&#xff08;通常采用在线强化学习&#xff09;&#xff0c;依赖于特定领域的模型设计&#xff0c;难以利用丰富的跨领域数据进行泛化。 在本研究中&#xff0c;我们探讨了基于视觉-语…

在视频汇聚平台EasyNVR平台中使用RTSP拉流的具体步骤

之前有用户反馈&#xff0c;在EasyNVR平台中添加Pull时使用海康设备的RTSP流地址无法播放。经过研发的优化及一系列严谨的验证流程&#xff0c;我们已确认优化后的EasyNVR平台&#xff0c;通过Pull方式添加海康设备的RTSP流已经能够正常播放。以下是具体的操作步骤&#xff1a;…

Debezium日常分享系列之:对于从Oracle数据库进行快照的性能优化

Debezium日常分享系列之&#xff1a;对于从Oracle数据库进行快照的性能优化 源数据库Kafka Connect监控测试结果 源数据库 Oracle 19c&#xff0c;本地&#xff0c;CDB数据库主机的I/O带宽为6 GB/s&#xff0c;由此主机上运行的所有数据库共享临时表空间由42个文件组成&#x…