Slate文档编辑器-WrapNode数据结构与操作变换
Slate文档编辑器-WrapNode数据结构与操作变换
在之前我们聊到了一些关于slate
富文本引擎的基本概念,并且对基于slate
实现文档编辑器的一些插件化能力设计、类型拓展、具体方案等作了探讨,那么接下来我们更专注于文档编辑器的细节,由浅入深聊聊文档编辑器的相关能力设计。
关于slate
文档编辑器项目的相关文章:
Normalize
在slate
中数据结构的规整是比较麻烦的事情,特别是对于需要嵌套的结构来说,例如在本项目中存在的Quote
和List
,那么在规整数据结构的时候就有着多种方案,同样以这两组数据结构为例,每个Wrap
必须有相应的Pair
的结构嵌套,那么对于数据结构就有如下的方案。实际上我觉得对于这类问题是很难解决的,嵌套的数据结构对于增删改查都没有那么高效,因此在缺乏最佳实践相关的输入情况下,也只能不断摸索。
首先是复用当前的块结构,也就是说Quote Key
和List Key
都是平级的,同样的其Pair Key
也都复用起来,这样的好处是不会出现太多的层级嵌套关系,对于内容的查找和相关处理会简单很多。但是同样也会出现问题,如果在Quote
和List
不配齐的情况下,也就是说其并不是完全等同关系的情况下,就会需要存在Pair
不对应Wrap
的情况,此时就很难保证Normalize
,因为我们是需要可预测的结构。
{
"quote-wrap": true,
"list-wrap": true,
children: [
{ "quote-pair": true, "list-pair": 1, children: [/* ... */] },
{ "quote-pair": true, "list-pair": 2, children: [/* ... */] },
{ "quote-pair": true, children: [/* ... */] },
{ "quote-pair": true, "list-pair": 1, children: [/* ... */] },
{ "quote-pair": true, "list-pair": 2, children: [/* ... */] },
]
}
那么如果我们不对内容做很复杂的控制,在slate
中使用默认行为进行处理,那么其数据结构表达会出现如下的情况,在这种情况下数据结构是可预测的,那么Normalize
就不成问题,而且由于这是其默认行为,不会有太多的操作数据处理需要关注。但是问题也比较明显,这种情况下数据虽然是可预测的,但是处理起来特别麻烦,当我们维护对应关系时,必须要递归处理所有子节点,在特别多层次的嵌套情况下,这个计算量就颇显复杂了,如果在支持表格等结构的情况下,就变得更加难以控制。
{
"quote-wrap": true,
children: [
{
"list-wrap": true,
children: [
{ "quote-pair": true, "list-pair": 1, children: [/* ... */] },
{ "quote-pair": true, "list-pair": 2, children: [/* ... */] },
]
},
{ "quote-pair": true, children: [/* ... */] },
{ "quote-pair": true, children: [/* ... */] },
]
}
那么这个数据结构实际上也并不是很完善,其最大的问题是wrap - pair
的间隔太大,这样的处理方式就会出现比较多的边界问题,举个比较极端的例子,假设我们最外层存在引用块,在引用块中又嵌套了表格,表格中又嵌套了高亮块,高亮块中又嵌套了引用块,这种情况下我们的wrap
需要传递N
多层才能匹配到pair
,这种情况下影响最大的就是Normalize
,我们需要有非常深层次的DFS
处理才行,处理起来不仅需要耗费性能深度遍历,还容易由于处理不好造成很多问题。
那么在这种情况下,我们可以尽可能简化层级的嵌套,也就是说我们需要避免wrap - pair
的间隔问题,那么很明显我们直接严格规定wrap
的所有children
必须是pair
,在这种情况下我们做Normalize
就简单了很多,只需要在wrap
的情况下遍历其子节点以及在pair
的情况下检查其父节点即可。当然这种方案也不是没有缺点,这让我们对于数据的操作精确性有着更严格的要求,因为在这里我们不会走默认行为,而是全部需要自己控制,特别是所有的嵌套关系以及边界都需要严格定义,这对编辑器行为的设计也有更高的要求。
{
"quote-wrap": true,
children: [
{
"list-wrap": true,
"quote-pair": true,
children: [
{ "list-pair": 1, children: [/* ... */] },
{ "list-pair": 2, children: [/* ... */] },
{ "list-pair": 3, children: [/* ... */] },
]
},
{ "quote-pair": true, children: [/* ... */] },
{ "quote-pair": true, children: [/* ... */] },
{ "quote-pair": true, children: [/* ... */] },
]
}
那么为什么说数据结构会变得复杂了起来,就以上述的结构为例,假如我们将list-pair: 2
这个节点解除了list-wrap
节点的嵌套结构,那么我们就需要将节点变为如下的类型,我们可以发现这里的结构差别会比较大,除了除了将list-wrap
分割成了两份之外,我们还需要处理其他list-pair
的有序列表索引值更新,这里要做的操作就比较多了,因此我们如果想实现比较通用的Schema
就需要更多的设计和规范。
而在这里最容易忽略的一点是,我们需要为原本的list-pair: 2
这个节点加入"quote-pair": true
,因为此时该行变成了quote-wrap
的子元素,总结起来也就是我们需要将原本在list-wrap
的属性再复制一份给到list-pair: 2
中来保持正确的嵌套结构。那么为什么不是借助normalize
来被动添加而是要主动复制呢,原因很简单,如果是quote-pair
的话还好,如果是被动处理则直接设置为true
就可以了,但是如果是list-pair
来实现的话,我们无法得知这个值的数据结构应该是什么样子的,这个实现则只能归于插件的normalize
来实现了。
{
"quote-wrap": true,
children: [
{
"list-wrap": true,
"quote-pair": true,
children: [
{ "list-pair": 1, children: [/* ... */] },
]
},
{ "quote-pair": true, children: [/* ... */] },
{
"list-wrap": true,
"quote-pair": true,
children: [
{ "list-pair": 1, children: [/* ... */] },
]
},
{ "quote-pair": true, children: [/* ... */] },
{ "quote-pair": true, children: [/* ... */] },
{ "quote-pair": true, children: [/* ... */] },
]
}
Transformers
前边也提到了,在嵌套的数据结构中是存在默认行为的,而在之前由于一直遵守着默认行为所以并没有发现太多的数据处理方面的问题,然而当将数据结构改变之后,就发现了很多时候数据结构并不那么容易控制。先前在处理SetBlock
的时候通常我都会通过match
参数匹配Block
类型的节点,因为在默认行为的情况下这个处理通常不会出什么问题。
然而在变更数据结构的过程中,处理Normalize
的时候就出现了问题,在块元素的匹配上其表现与预期的并不一致,这样就导致其处理的数据一直无法正常处理,Normalize
也就无法完成直至抛出异常。在这里主要是其迭代顺序与我预期的不一致造成的问题,例如在DEMO
页上执行[...Editor.nodes(editor, {at: [9, 1, 0] })]
,其返回的结果是由顶Editor
至底Node
,当然这里还会包括范围内的所有Leaf
节点相当于是Range
。
[] Editor
[9] Wrap
[9, 1] List
[9, 1, 9] Line
[9, 1, 0] Text
实际上在这种情况下如果按照原本的Path.equals(path, at)
是不会出现问题的,在这里就是之前太依赖其默认行为了,这也就导致了对于数据的精确性把控太差,我们对数据的处理应该是需要有可预期性的,而不是依赖默认行为。此外,slate
的文档还是太过于简练了,很多细节都没有提及,在这种情况下还是需要去阅读源码才会对数据处理有更好的理解,例如在这里看源码让我了解到了每次做操作都会取Range
所有符合条件的元素进行match
,在一次调用中可能会发生多次Op
调度。
此外,因为这次的处理主要是对于嵌套元素的支持,所以在这里还发现了unwrapNodes
或者说相关数据处理的特性,当我调用unwrapNodes
时仅at
传入的值不一样,分别是A-[3, 1, 0]
和B-[3, 1, 0, 0]
,这里有一个关键点是在匹配的时候我们都是严格等于[3, 1, 0]
,但是调用结果却是不一样的,在A
中[3, 1, 0]
所有元素都被unwrap
了,而B
中仅[3, 1, 0, 0]
被unwrap
了,在这里我们能够保证的是match
结果是完全一致的,那么问题就出在了at
上。此时如果不理解slate
数据操作的模型的话,就必须要去看源码了,在读源码的时候我们可以发现其会存在Range.intersection
帮我们缩小了范围,所以在这里at
的值就会影响到最终的结果。
unwrapNodes(editor, { match: (_, p) => Path.equals(p, [3, 1, 0]), at: [3, 1, 0] }); // A
unwrapNodes(editor, { match: (_, p) => Path.equals(p, [3, 1, 0]), at: [3, 1, 0, 0] }); // B
上边这个问题也就意味着我们所有的数据都不应该乱传,我们应该非常明确地知道我们要操作的数据及其结构。其实前边还提到一个问题,就是多级嵌套的情况很难处理,这其中实际上涉及了一个编辑边界情况,使得数据的维护就变得复杂了起来。举个例子,加入此时我们有个表格嵌套了比较多的Cell
,如果我们是多实例的Cell
结构,此时我们筛选出Editor
实例之后处理任何数据都不会影响其他的Editor
实例,而如果我们此时是JSON
嵌套表达的结构,我们就可能存在超过操作边界而影响到其他数据特别是父级数据结构的情况。所以我们对于边界条件的处理也必须要关注到,也就是前边提到的我们需要非常明确要处理的数据结构,明确划分操作节点与范围。
{
children: [
{
BLOCK_EDGE: true, // 块结构边界
children: [
{ children: [/* ... */] },
{ children: [/* ... */] },
]
},
{ children: [/* ... */] },
{ children: [/* ... */] },
]
}
此外,在线上已有页面中调试代码可能是个难题,特别是在editor
并没有暴露给window
的情况下,想要直接获得编辑器实例则需要在本地复现线上环境,在这种情况下我们可以借助React
会将Fiber
实际写在DOM
节点的特性,通过DOM
节点直接取得Editor
实例,不过原生的slate
使用了大量的WeakMap
来存储数据,在这种情况下暂时没有很好的解决办法,除非editor
实际引用了此类对象或者拥有其实例,否则就只能通过debug
打断点,然后将对象在调试的过程中暂储为全局变量使用了。
const el = document.querySelector(`[data-slate-editor="true"]`);
const key = Object.keys(el).find(it => it.startsWith("__react"));
const editor = el[key].child.memoizedProps.node;
最后
在这里我们聊到了WrapNode
数据结构与操作变换,主要是对于嵌套类型的数据结构需要关注的内容,而实际上节点的类型还可以分为很多种,我们在大范围上可以有BlockNode
、TextBlockNode
、TextNode
,在BlockNode
中我们又可以划分出BaseNode
、WrapNode
、PairNode
、InlineBlockNode
、VoidNode
、InstanceNode
等,因此文中叙述的内容还是属于比较基本的,在slate
中还有很多额外的概念和操作需要关注,例如Range
、Operation
、Editor
、Element
、Path
等。那么在后边的文章中我们就主要聊一聊在slate
中Path
的表达,以及在React
中是如何控制其内容表达与正确维护Path
路径与Element
内容渲染的。
Slate文档编辑器-WrapNode数据结构与操作变换的更多相关文章
- 基于slate构建文档编辑器
基于slate构建文档编辑器 slate.js是一个完全可定制的框架,用于构建富文本编辑器,在这里我们使用slate.js构建专注于文档编辑的富文本编辑器. 描述 Github | Editor DE ...
- Web页面引入文档编辑器报风险
Web页面引入文档编辑器会报风险,则需要以下操作: <system.web> <httpRuntime requestValidationMode="2.0" / ...
- [Qt及Qt Quick开发实战精解] 第1章 多文档编辑器
这一章的例子是对<Qt Creator快速人门>基础应用篇各章节知识的综合应用, 也是一个规范的实例程序.之所以说其规范,是因为在这个程序中,我们对菜单什么时候可用/什么时候不可用.关 ...
- 基于DevExpress实现对PDF、Word、Excel文档的预览及操作处理
http://www.cnblogs.com/wuhuacong/p/4175266.html 在一般的管理系统模块里面,越来越多的设计到一些常用文档的上传保存操作,其中如PDF.Word.Excel ...
- Linux_文档编辑器_简介
1. vi 2. vim 3. ubuntu 有一个 自己的图形化的 文档编辑器,用起来比较方便: gedit 4. 5.
- PowerDesigner(九)-模型文档编辑器(生成项目文档)(转)
模型文档编辑器 PowerDesigner的模型文档(Model Report)是基于模型的,面向项目的概览文档,提供了灵活,丰富的模型文档编辑界面,实现了设计,修改和输出模型文档的全过程. 模型文 ...
- 使用Swing实现简易而不简单的文档编辑器
本文通过Swing来实现文档简易而不简单的文档编辑器,该文档编辑器的功能包括: 设置字体样式:粗体,斜体,下划线,可扩展 设置字体:宋体,黑体,可扩展 设置字号:12,14,18,20,30,40, ...
- 配置允许匿名用户登录访问vsftpd服务,进行文档的上传下载、文档的新建删除等操作
centos7环境下 临时关闭防火墙 #systemctl stop firewalld 临时关闭selinux #setenforce 0 安装ftp服务 #yum install vsftpd - ...
- 在线HTML文档编辑器使用入门之图片上传与图片管理的实现
在线HTML文档编辑器使用入门之图片上传与图片管理的实现: 官方网址: http://kindeditor.net/demo.php 开发步骤: 1.开发中只需要导入选中的文件(通常在 webapp ...
- MongoDB 文档的查询和插入操作
MongoDB是文档型数据库,有一些专门的术语,和关系型DB相似,但也有差异,例如,Collection类似于关系型DB的Table,document类似于row,key/value pair类似于c ...
随机推荐
- Go 匿名字段介绍
在 Go 语言中,结构体(struct)是一种用于将多个不同类型的数据组合在一起的数据结构.你提到的语法: type RiderNode struct { service.SimpleService ...
- pikachu靶场 越权(水平越权+垂直越权)
水平越权 A用户和B用户属于同一级别用户,但各自不能操作对方个人信息.A用户如果越权操作B用户个人信息的情况称为水行越权操作 三个用户 lucy/lili/kobe 密码都为123456 随便登录其 ...
- CM3和ARM7的差异
此文章由文心一言生成,引用请标注作者:文心一言CM3通常指的是Cortex-M3,它是ARM公司设计的一种基于ARMv7-M架构的32位处理器内核,主要用于嵌入式系统.而ARM7则是ARM公司早期设计 ...
- 性能、成本与 POSIX 兼容性比较: JuiceFS vs EFS vs FSx for Lustre
JuiceFS 是一款为云环境设计的分布式高性能文件系统.Amazon EFS 易于使用且可伸缩,适用于多种应用.Amazon FSx for Lustre 则是面向处理快速和大规模数据工作负载的高性 ...
- sicp每日一题[1.44]
Exercise 1.44 The idea of smoothing a function is an important concept in signal processing. If f is ...
- SpringBoot 基于注解实现接口的代理Bean注入
SpringBoot 基于注解实现接口的代理Bean注入 在springboot加载时需自己手动将接口的代理bean注入到spring容器中,这样在service层注入该接口类型即可, 1.在Spri ...
- Naming Conversion & Case Style 命名规范
前言 写代码有 2 个点很重要 第一是表达 (不要词不达意) 要达到这点, 就要多参考其它人如何表达. 第二是一致性 (一样的东西就用一样的写法) 要达到这点就要建立规范 以前的笔记 命名规范 nam ...
- ArgoWorkflow教程(五)---Workflow 的多种触发模式:手动、定时任务与事件触发
上一篇我们分析了argo-workflow 中的 archive,包括 流水线GC.流水线归档.日志归档等功能.本篇主要分析 Workflow 中的几种触发方式,包括手动触发.定时触发.Event 事 ...
- [OI] 指针与迭代器
取地址与解引用 一般来说,我们有一个取地址符 & 可以返回该变量的地址. int main(){ int a; cout<<&a; } 0x6ffe1c 如果我们现在有一个 ...
- Android UsbDeviceManager 代码分析
USBDeviceManager是一个Android系统中用于管理USB设备的类,它是系统服务之一.其主要功能是控制USB设备的连接和断开,以及管理USB设备的权限和状态.下面是对USBDeviceM ...