Slate文档编辑器-Node节点与Path路径映射
Slate文档编辑器-Node节点与Path路径映射
在之前我们聊到了slate中的Decorator装饰器实现,装饰器可以为我们方便地在编辑器渲染调度时处理range的渲染,这在实现搜索替换、代码高亮等场景非常有用。那么在这篇文章中,我们聊一下Node节点与Path路径映射,这里的Node指的是渲染的节点对象,Path则是节点对象在当前JSON中的路径,即本文的重点是如何确定渲染出的节点处于文档数据定义中的位置。
关于slate文档编辑器项目的相关文章:
- 基于Slate构建文档编辑器
- Slate文档编辑器-WrapNode数据结构与操作变换
- Slate文档编辑器-TS类型扩展与节点类型检查
- Slate文档编辑器-Decorator装饰器渲染调度
- Slate文档编辑器-Node节点与Path路径映射
渲染与命令
在slate的文档中的03-defining-custom-elements一节中,我们可以看到我们可以看到slate中的Element节点是可以自定义渲染的,渲染的逻辑是需要我们根据props的element对象来判断类型,如果类型是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 = node
while (true) {
const parent = NODE_TO_PARENT.get(child)
if (parent == null) {
if (Editor.isEditor(child)) return path
else break
}
const i = NODE_TO_INDEX.get(child)
if (i == null) break
path.unshift(i)
child = parent
}
}
简单压缩了代码,在这里的实现是通过两个WeakMap非常巧妙地让我们可以取得节点的Path。那么这里就需要思考一个问题,为什么我们不直接在RenderProps直接将Path传递到渲染的方法中,而是非得需要每次都得重新查找而浪费一部分性能。实际上,如果我们只是渲染文档数据,那么自然是不会有问题的,然而我们通常是需要编辑文档的,在这个时候就会出现问题。举个例子,假设我们在[10]位置有一个表格,而此时我们在[6]位置上增添了1个空白行,那么此时我们的表格Path就应该是[11]了,然而由于我们实际上并没有编辑与表格相关的内容,所以我们本身也不应该刷新表格的相关内容,自然其Props就不会变化,此时我们如果直接取值的话,则会取到[10]而不是[11]。
那么同样的,即使我们用WeakMap记录Node与Path的对应关系,即使表格的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 Descendant
const 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 + Cell的element对象,在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)
);
}
但是通过这种方式来获取Node与Path节点的映射来获取位置就没有问题了嘛,高效的查找方案使得我们在这里必须依赖渲染后才可以得知节点最新的位置,也就是说当我们更新了节点对象后,如果此时立刻调用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#L29
useSlate();
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/5697
editor.event.trigger(EDITOR_EVENT.PAINT, {});
});
}
isNeedPaint.current = false;
});
return children as JSX.Element;
};
最后
在这里我们主要讨论了Node节点与Path路径映射,即如何确定渲染出的节点处于文档数据定义中的位置,这是slate中实现数据变更时的重要表达,特别是在仅使用选区无法实现的复杂操作中,并且还分析了slate源码来探究了相关问题的实现。那么在后面的文章中,我们延续当前提到的表格但单元格位置的查找,来聊聊表格模块的设计及交互。
每日一题
参考
- https://docs.slatejs.org/
- https://github.com/WindRunnerMax/DocEditor
- https://github.com/ianstormtaylor/slate/blob/25be3b/
Slate文档编辑器-Node节点与Path路径映射的更多相关文章
- 基于slate构建文档编辑器
基于slate构建文档编辑器 slate.js是一个完全可定制的框架,用于构建富文本编辑器,在这里我们使用slate.js构建专注于文档编辑的富文本编辑器. 描述 Github | Editor DE ...
- PowerDesigner(九)-模型文档编辑器(生成项目文档)(转)
模型文档编辑器 PowerDesigner的模型文档(Model Report)是基于模型的,面向项目的概览文档,提供了灵活,丰富的模型文档编辑界面,实现了设计,修改和输出模型文档的全过程. 模型文 ...
- 使用Swing实现简易而不简单的文档编辑器
本文通过Swing来实现文档简易而不简单的文档编辑器,该文档编辑器的功能包括: 设置字体样式:粗体,斜体,下划线,可扩展 设置字体:宋体,黑体,可扩展 设置字号:12,14,18,20,30,40, ...
- Linux_文档编辑器_简介
1. vi 2. vim 3. ubuntu 有一个 自己的图形化的 文档编辑器,用起来比较方便: gedit 4. 5.
- Web页面引入文档编辑器报风险
Web页面引入文档编辑器会报风险,则需要以下操作: <system.web> <httpRuntime requestValidationMode="2.0" / ...
- 在线HTML文档编辑器使用入门之图片上传与图片管理的实现
在线HTML文档编辑器使用入门之图片上传与图片管理的实现: 官方网址: http://kindeditor.net/demo.php 开发步骤: 1.开发中只需要导入选中的文件(通常在 webapp ...
- [Qt及Qt Quick开发实战精解] 第1章 多文档编辑器
这一章的例子是对<Qt Creator快速人门>基础应用篇各章节知识的综合应用, 也是一个规范的实例程序.之所以说其规范,是因为在这个程序中,我们对菜单什么时候可用/什么时候不可用.关 ...
- Swagger UI及 Swagger editor教程 API文档搭配 Node使用
swagger ui 是一个在线文档生成和测试的利器,目前发现最好用的.为啥好用呢?打开 demo,支持API自动生成同步的在线文档些文档可用于项目内部API审核方便测试人员了解 API这些文档可作为 ...
- js操作文档对象的节点
好吧,为了能让大家不至于睡着,我们先回顾先前面的东东吧~ 1.首先我们写了javaScriput的是一门弱类型的解释性的脚本语言:弱类型:我们的变量不用申明其具体的数据类型,在使用的时候浏览器根据其存 ...
- 添加swagger api文档到node服务
swagger,一款api测试工具,详细介绍参考官网:http://swagger.io/ ,这里主要记录下怎么将swagger api应用到我们的node服务中: 1.任意新建node api项目, ...
随机推荐
- Golang之数据库转换结构体工具table2struct
另外一个根据json生成对应结构体在线工具: https://mholt.github.io/json-to-go/ 安装: go get github.com/gohouse/converter 或 ...
- Linux命令之ncdu
简介 Ncdu - NCurses Disk Usage Ncdu 是一个带有 ncurses 接口的磁盘使用分析器. 它旨在在您没有完整图形设置可用的远程服务器上查找空间占用,但即使在常规桌面系统上 ...
- golang之基础语法
Go 是一种强类型语言. 这意味着你声明的每个变量都绑定到特定的数据类型,并且只接受与此类型匹配的值. Go 有四类数据类型: 基本类型:数字.字符串和布尔值 聚合类型:数组和结构 引用类型:指针.切 ...
- whisper v3 finetune 中文乱码问题的解决方案
最近学习了一下whisper的微调,主要是参考了github上的夜雨飘零大神项目.但是在操作中遇到了微调中文的时候出现了乱码的情况.以下是我这边对于微调过程中中文出现乱码情况的解决方案. 出现情况如下 ...
- 12C++循环结构-for循环(2)——教学
一.循环变量为字符型 (第32课 26个兄弟姐妹)参考视频1 试编一程序,按字典顺序输出26个字母. 流程图: 思考:先顺序输出26个小写英文字母,再逆序输出26个大写英文字母. 循环可以是递增型循环 ...
- 百度地图 自定义弹窗 InfoBox
infoBox文档地址: https://api.map.baidu.com/library/InfoBox/1.2/docs/symbols/BMapLib.InfoBox.html infobox ...
- Microsoft Build 2022 专家对话
Microsoft Build 2022 专家对话 Build 2022 专家对话地址:https://mybuild.microsoft.com/en-US/sessions/81056450-6f ...
- pip 安装 Caused by SSLError(SSLError(1, '[SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1123)'))
1.问题 1.1 问题截取 pip install redis -i https://pypi.tuna.tsinghua.edu.cn/simple Looking in indexes: http ...
- betterZip解压后压缩包会删除 zip文件解压后原压缩文件能不能删掉
https://www.betterzipcn.com/faq/better-ydeq.html 品牌型号:MacBook Book Air 系统:MacOS Mojave 10.14 软件版本:Be ...
- 如何在 Kubernetes Pod 和您的机器之间复制文件
在 Kubernetes Pod 中运行的容器是不需要手动交互的独立计算单元.有时您可能需要将文件复制到 Pod 的文件系统或从 Pod 的文件系统复制文件,这可能是因为您正在调试问题并希望存档存储在 ...