Slate文档编辑器-TS范例扩展与节点范例查抄
在之前我们基于slate实现的文档编辑器探讨了WrapNode数据布局与操作变更,重要是对于嵌套范例的数据布局范例需要关注的Normalize与Transformers,那么接下来我们更专注于文档编辑器的数据布局计划,聊聊基于slate实现的文档编辑器范例系统。
- 在线编辑: https://windrunnermax.github.io/DocEditor
- 开源地址: https://github.com/WindRunnerMax/DocEditor
关于slate文档编辑器项目的相关文章:
- 基于Slate构建文档编辑器
- Slate文档编辑器-WrapNode数据布局与操作变更
- Slate文档编辑器-TS范例扩展与节点范例查抄
- Slate文档编辑器-Decorator装饰器渲染调理
- Slate文档编辑器-Node节点与Path路径映射
TS范例扩展
当我们使用TS引入slate时,可能会发现调用createEditor实例化编辑器时,并没有提供范型的定义范例传入,那么这是不是意味着slate无法在TS中定义范例,而是只能在处理属性时采用as的情势强制范例断言。那么作为成熟的编辑器引擎显然是不能这样的,对于富文本编辑器来说,能够在TS中定义范例是非常重要的,我们可以将富文本明白为带着属性的文本,如果属性的定义不明确,那么维护起来可能会变得越来越困难,而自己维护富文本就充斥着各种标题,所以维护范例的定义是很有须要的。
在研究slate的范例扩展之前,我们需要先看看TypeScript提供的declare module以及interface声明,我们可能经常会碰到对type和interface定义的接口范例的区别,实际上除了type可以定义联合范例以外,interface和type还有一个比较重要的区别就是interface可以被合并,也就是可以将定义的接口进行扩展。而type在同一个模块内是无法重新定义范例声明的。
- interface A { a: string }
- interface A { b: number }
- const a: A = { a: "", b: 1 }
- type B = { a: string }
- // type B = { b: number }
- const b = { a: "" }
复制代码 当我们开端了解时可能会觉得interface和type的区别不大几乎可以平替,但是在实际应用中interface的合并特性是非常重要的,特别是在declare module的场景下,我们可以用于为模块扩展声明范例,也就是为已经有范例定义的库增长额外范例定义。
- declare module "some-library" {
- export interface A {
- a: number;
- }
- export function fn(): void;
- }
复制代码 那么在这里我们就可以通过declare module + interface的合并特性来扩展模块的范例定义了,这也就是slate实际上定义的类扩展方式,我们可以通过为slate模块扩展范例的方式,来注入我们需要的基本范例,而我们的基本范例都是使用interface关键字定义的,这样就表现我们的范例是可以不停通过declare module的方式进行扩展的。还有一点需要特别关注,当我们实现了declare module的范例扩展时,我们就可以按需加载范例的扩展,也就是说只有当实际引用到我们定义好的范例时,才会加载对应的范例声明,做到按需扩展范例。
- // packages/delta/src/interface.ts
- import type { BaseEditor } from "slate";
- declare module "slate" {
- interface CustomTypes {
- Editor: BaseEditor;
- Element: BlockElement;
- Text: TextElement;
- }
- }
- export interface BlockElement {
- text?: never;
- children: BaseNode[];
- [key: string]: unknown;
- }
- export interface TextElement {
- text: string;
- children?: never;
- [key: string]: unknown;
- }
复制代码 实际上我是非常保举将范例定义独立抽离出单独的文件来定义的,否则当我们在IDE中使用跳转到范例定义的功能查看完整slate的范例文件时,发现其跳转的位置是我们上述定义的文件位置,同理当我们将slate及其相关模块独立抽离为单独的包时,会发现范例的跳转变得并不那么方便,即使我们的目标不是扩展范例的定义也会被跳转到我们declare module的文件位置。因此当我们将其抽离出来之后,声明文件单独作为独立的模块处理,这样就不会存在定位标题了,例如下面就是我们声明代码块格式的范例定义。
- // packages/plugin/src/codeblock/types/index.ts
- declare module "doc-editor-delta/dist/interface" {
- interface BlockElement {
- [CODE_BLOCK_KEY]?: boolean;
- [CODE_BLOCK_CONFIG]?: { language: string };
- }
- interface TextElement {
- [CODE_BLOCK_TYPE]?: string;
- }
- }
- export const CODE_BLOCK_KEY = "code-block";
- export const CODE_BLOCK_TYPE = "code-block-type";
- export const CODE_BLOCK_CONFIG = "code-block-config";
复制代码 节点范例查抄
那么在TS方面我们将范例扩展好之后,我们还需要关注实际的节点范例判断,毕竟TS只是帮我们实现了静态范例查抄,在实际编译为Js运行在欣赏器中时,通常是不会有实际额外代码注入的,而在我们平常使用的过程中是显着需要这些范例的判断的,例如通常我们需要查抄当前的选区节点是否准确位于Text节点上,以便对用户的操作做出对应的相应。
那么我们就可以梳理一下在slate当中的节点范例,实际上根据slate导出的CustomTypes我们可以确定最基本的范例就只有Element和Text,而我更习惯将其重定名为BlockElement和TextElement。而实际上由于我们的业务复杂性,我们通常还需要扩展出InlineElement和TextBlockElement两个范例。
那么我们起首来看BlockElement,这是slate中定义的基本块元素,例如我们的代码块最外层的嵌套布局需要是BlockElement,而实际上由于可能存在的复杂嵌套布局,例如表格中嵌套分栏布局,分栏布局中继续嵌套代码块布局等,所以我们可以将其明白为BlockElement是嵌套所需的布局即可,而判断节点范例是否是BlockElement的方式则可以直接调理Editor.isBlock方法即可。
- export interface BlockElement {
- text?: never;
- children: BaseNode[];
- [key: string]: unknown;
- }
- export const isBlock = (editor: Editor, node: Node | null): node is BlockElement => {
- if (!node) return false;
- return Editor.isBlock(editor, node);
- };
- // https://github.com/ianstormtaylor/slate/blob/25be3b/packages/slate/src/interfaces/editor.ts#L590
- export const Editor = {
- isBlock(editor: Editor, value: any): value is Element {
- return Element.isElement(value) && !editor.isInline(value)
- }
- }
复制代码 TextElement被定义为文本节点,需要注意的是这里同样也是可以到场属性的,在slate中被称为marker,无论什么节点范例的嵌套,数据布局即树形布局的叶子节点肯定需要是文本节点,而针对于TextElement的判断则同样可以调理Text上的isText方法。
- export interface TextElement {
- text: string;
- children?: never;
- [key: string]: unknown;
- }
- export const isText = (node: Node): node is TextElement => Text.isText(node);
- // https://github.com/ianstormtaylor/slate/blob/25be3b/packages/slate/src/interfaces/text.ts#L53
- export const Text = {
- isText(value: any): value is Text {
- return isPlainObject(value) && typeof value.text === 'string'
- },
- }
复制代码 如果细致看slate判断BlockElement的条件,我们可以发现除了调用Element.isElement方法外,还判断了editor.isInline方法,由于slate中InlineElement是一种特别的BlockElement,我们同样需要为其独立实现范例判断,而判断InlineElement的方式则需要由editor.isInline方法来实现,这部分实际上是由我们在with的时候需要定义好的。
- export interface InlineElement {
- text?: never;
- children: TextElement[];
- [key: string]: unknown;
- }
- export const isInline = (editor: Editor, node:Node): node is InlineElement => {
- return editor.isInline(node);
- }
复制代码 在业务中通常我们还需要判断文本段落实际渲染的节点TextBlockElement,这个判断同样非常重要,由于这里决定了当前的节点是我们实际要渲染的段落了,这在数据转换等场景中非常有用,当我们需要二次解析数据的时候,当碰到文本段落时,我们就可以确定当前的块级布局解析可以竣事了,接下来可以构建段落的内容组合了。而由于段落中可能存在TextElement和InlineElement两种节点,我们的判断就需要将两种情况都思量到,在规范的条件下我们通常只需要判断首个节点是否符合条件即可,而在开发模式下我们可以尝试对比全部节点的布局来判断并且校验脏数据。
- export interface TextBlockElement {
- text?: never;
- children: (TextElement | InlineElement)[];
- [key: string]: unknown;
- }
- export const isTextBlock = (editor: Editor, node: Node): node is TextBlockElement => {
- if (!isBlock(editor, node)) return false;
- const firstNode = node.children[0];
- const result = firstNode && (isText(firstNode) || isInline(editor, firstNode));
- if (process.env.NODE_ENV === "development") {
- const strictInspection = node.children.every(child => isText(firstNode) || isInline(editor, firstNode));
- if (result !== strictInspection) {
- console.error("Fatal Error: Text Block Check Fail", node);
- }
- }
- return result;
- };
复制代码 最后
在这里我们更专注于文档编辑器的数据布局计划,聊聊基于slate实现的文档编辑器范例系统。在slate中还有许多额外的概念和操作需要关注,例如Range、Operation、Editor、Element、Path等,那么在后边的文章中我们就重要聊一聊在slate中Path的表达,以及在React中是如何控制其内容表达与准确维护Path路径与Element内容渲染的,并且我们还可以聊一聊表格模块的计划与实现。
Blog
- https://github.com/WindRunnerMax/EveryDay
复制代码 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |