Day 12. slate × Interfaces × Data-Model

https://ithelp.ithome.com.tw/upload/images/20210927/20139359poAFmlkmdh.png

上一篇我们有提到上图这些画了黄框的 files ,是我们在建立 editor 与操作 editor value 时主要会使用到的概念。

同时介绍了 slate document model 里需要用到的 concepts : text 、 element。

紧接着今天我们就要把范围扩大到整个 Data-Model 的层级了,也就是要纳入 editor 与 node 这两个概念。

https://ithelp.ithome.com.tw/upload/images/20210927/20139359fZrLNqhBNb.png

一样一个一个来,我们先从 editor.ts 开始,里面主要定义了三种 type 分别是: SelectionEditorNodeMatch

  • Selection

    export type BaseSelection = Range | null
    
    export type Selection = ExtendedType<'Selection', BaseSelection>
    

    如其名,主要是拿来纪录编辑器里使用者当前的反白区块,对 DOM 有一定熟悉程度的读者就会知道,slate 里的 SelectionRange 这两个概念完全就是来自於 Web api 所提供的 SelectionRange 这两个 object ,连 slate 的 Range 里头的 anchorfocus 也完全是在模仿浏览器 Selection 里的 anchorNodefocusNode properties ,这边提供个 MDN 连结给读者,有兴趣可以上前查看,看完这段觉得一头雾水的读者也不用担心,关於 slate 的 Range 我们在 下一篇 就会详细介绍到它了,这边先知道 Selection 是拿来纪录 user 反白的区域即可。

  • NodeMatch

    /**
     * A helper type for narrowing matched nodes with a predicate.
     */
    
    export type NodeMatch<T extends Node> =
      | ((node: Node, path: Path) => node is T)
      | ((node: Node, path: Path) => boolean)
    

    其实作者留给它的注释就写地蛮清楚的了,它是用来让开发者比对 Node 的 function type ,它在 library 中只出现在具有迭代性质的 method apis 里的 match option ,slate 会透过开发者传入的 match function 来判断当前迭代到的 Node 是否要跳过计算,藉此来提升运算的速度。

  • Editor

    /**
     * The `Editor` interface stores all the state of a Slate editor. It is extended
     * by plugins that wish to add their own helpers and implement new behaviors.
     */
    
    export interface BaseEditor {
      children: Descendant[]
      selection: Selection
      operations: Operation[]
      marks: Omit<Text, 'text'> | null
    
      // Schema-specific node behaviors.
      isInline: (element: Element) => boolean
      isVoid: (element: Element) => boolean
      normalizeNode: (entry: NodeEntry) => void
      onChange: () => void
    
      // Overrideable core actions.
      addMark: (key: string, value: any) => void
      apply: (operation: Operation) => void
      deleteBackward: (unit: 'character' | 'word' | 'line' | 'block') => void
      deleteForward: (unit: 'character' | 'word' | 'line' | 'block') => void
      deleteFragment: (direction?: 'forward' | 'backward') => void
      getFragment: () => Descendant[]
      insertBreak: () => void
      insertFragment: (fragment: Node[]) => void
      insertNode: (node: Node) => void
      insertText: (text: string) => void
      removeMark: (key: string) => void
    }
    
    export type Editor = ExtendedType<'Editor', BaseEditor>
    

    代表的就是编辑器本身,主要的 Data model 有四大项:

    • children 纪录 document-model value
    • selection 纪录编辑器当前反白区域
    • operations 纪录一组 FLUSHING 内触发过的 operations list
    • marks 提供了一个暂存空间,储存文字节点内除了 text 属性之外的自定义属性资料,会在下次插入文字时赋予它们这些属性资料。

    除此之外它也同时提供 overridable 的 behaviors :

    • onChangeDay10 介绍过了,负责通知 view layer 的 render

    • isInlineisVoid 又是两个从 DOM 的 inline element 以及 void element 借用过来的概念了,来看一下 create-editor.ts 里面是如何实现这两个函式的

      isInline: () => false,
      isVoid: () => false,
      

      蛤?这也太短了吧!


      是的没错就是这麽短!这两个函式的功用是预设让开发者去撰写判断传入的 element 是否为 inline-element 或 void-element 的函式,而这两种属性的预设值皆为 false ,开发者可以自行依照自己的需求去更改它的定义。

      const { isInline } = editor
      
      editor.isInline = element => {
        return element.type === 'link' ? true : isInline(element)
      }
      
    • normalizeNode

      这个 method 的 code 实作细节我们会在後面 Operation 的篇章再做介绍,目前先来看看官方文件上 normalizeNode 的 override 范例:

      const { normalizeNode } = editor
      
      editor.normalizeNode = entry => {
        const [node, path] = entry
      
        if (Element.isElement(node) && node.type === 'link') {
          // ...
          return
        }
      
        normalizeNode(entry)
      }
      

      其实 slate 提供的 override example 都像上图的范例一样,把原本定义好的函式当作 callback function 使用。


    接着就要来收回我在 Day8 时挖给自己的坑了 XD

    // Overrideable core actions.
      addMark: (key: string, value: any) => void
      apply: (operation: Operation) => void
      deleteBackward: (unit: 'character' | 'word' | 'line' | 'block') => void
      deleteForward: (unit: 'character' | 'word' | 'line' | 'block') => void
      deleteFragment: (direction?: 'forward' | 'backward') => void
      getFragment: () => Descendant[]
      insertBreak: () => void
      insertFragment: (fragment: Node[]) => void
      insertNode: (node: Node) => void
      insertText: (text: string) => void
      removeMark: (key: string) => void
    

    剩余的这些 core actions 除了 apply 以外,其他的实质上都是我们当时提到的新版 slate 的 Commands ,我们来看一下 editor.ts 里的 method apis 里面的 code 就会了解了

    export const Editor: EditorInterface = {
    	// other methods...
    	addMark(editor: Editor, key: string, value: any): void {
        editor.addMark(key, value)
      },
    	deleteBackward(
        editor: Editor,
        options: {
          unit?: 'character' | 'word' | 'line' | 'block'
        } = {}
      ): void {
        const { unit = 'character' } = options
        editor.deleteBackward(unit)
      },
    	deleteForward(
        editor: Editor,
        options: {
          unit?: 'character' | 'word' | 'line' | 'block'
        } = {}
      ): void {
        const { unit = 'character' } = options
        editor.deleteForward(unit)
      },
    	// ...and so on
    }
    

    editor 里的 method apis 的数量是远超过上面所列举的内容的,不过只要是负责操纵 editor value 的 methods ,都只是去 call editor 里同名的 core actions ,这边只是帮你多包一层依赖注入而已。

    而这就是新版 slate 的 "Command" 的真面目,开发者想以 plugin 的方式自定义或 override editor 的 core actions 也可以,自定义 method api 或 override 里头的 methods 也随便你,它就是提供给你两个 object 随你操纵,重点依旧是你如何操作 Transforms 与 Operations 。

    apply 呢?

    它是我们主要操纵 Operations 的地方,这部分我们等到 Transforms 与 Operations 的章节会再详细介绍。


node.ts


经过了 text 、 element 、 editor 这一连串的概念与范例轰炸以後,相信大家越来越能感受到 slate 追求与 DOM 相似这件事所谓何事了,而最後的这个 node.ts 可以说是整个 slate 里最重要的一个 concept ,更是他们追求相似於 DOM 这件事的集大成。

slate editor 将顶层的 Editor 、 中间的 Element 容器、底层的 Text 这三种 interfaces 视为个别的 Nodes ,并由他们来组成一整组的 slate node tree ,同时它的巢状架构让一个节点可以拥有无限个子节点,如果我们把一组 editor 里的其余 properties 拔掉,只留 document model 的话就会长得如下

const editor = {
  children: [
    {
      type: 'paragraph',
      children: [
        {
          text: 'A line of text!',
        },
      ],
    },
  ],
  // ...the editor has other properties too.
}

Slate Document 也没有限制只能存在一组 Editor node ,因此我们也可以用下图来诠释 node concept 在 Slate Document 之下的全貌。

https://ithelp.ithome.com.tw/upload/images/20210927/20139359Ssqq6czhIU.png

是不是看起来就跟 HTML DOM tree 一模一样呢?

接着就直接来看看 node.ts 里面定义了哪些 types

/**
 * The `Node` union type represents all of the different types of nodes that
 * occur in a Slate document tree.
 */
export type Node = Editor | Element | Text

/**
 * The `Descendant` union type represents nodes that are descendants in the
 * tree. It is returned as a convenience in certain cases to narrow a value
 * further than the more generic `Node` union.
 */

export type Descendant = Element | Text

/**
 * The `Ancestor` union type represents nodes that are ancestors in the tree.
 * It is returned as a convenience in certain cases to narrow a value further
 * than the more generic `Node` union.
 */

export type Ancestor = Editor | Element

Node type 就如我们先前介绍过的一样,是 EditorElementText 的 union ,Descendant 与 Ancestor 则各自为『可以成为子层节点』与『可以成为父层节点』的 types union ,它们是为了定义父/子层这两种概念的集合而存在的,回头看一下 前一篇 介绍的 Element type

export interface BaseElement {
  children: Descendant[]
}

这种定义方式的用意就是在描述『 children property 相对应的 type 是子层的 type 集合』。

NodeDescendantAncestor 这三种 types 会经常在我们开发 slate 时被使用到,尤其是 Descendant ,因为太常出现需要针对『子层集合』这个概念进行操作的情境了,撇除掉开发者自行开发,就连在 method apis 里面也经常会看到他们的身影。


在 node.ts 里还有额外定义的 NodeEntryNodeProps 这两种 types

/**
 * `NodeEntry` objects are returned when iterating over the nodes in a Slate
 * document tree. They consist of the node and its `Path` relative to the root
 * node in the document.
 */

export type NodeEntry<T extends Node = Node> = [T, Path]

/**
 * Convenience type for returning the props of a node.
 */
export type NodeProps =
  | Omit<Editor, 'children'>
  | Omit<Element, 'children'>
  | Omit<Text, 'text'>
  • NodeEntry 跟我们前一篇提到过的 ElementEntry type 的性质上很相近,大多都是用在 iterate 的功用上,详细内容我们一样放到之後再讨论

  • NodeProps 搭配 node.ts method apis 里的 extractProps method 让我们取得传入的 node 其余自定义的 properties

    /**
     * Extract props from a Node.
     */
    
    extractProps(node: Node): NodeProps {
      if (Element.isAncestor(node)) {
        const { children, ...properties } = node
    
        return properties
      } else {
        const { text, ...properties } = node
    
        return properties
      }
    },
    

让我再试着来统整一次今天的内容吧!
我们先从 editor.ts 的内容开始,提到了 selection 的源头是来自於 Web api 的 Selection 概念。介绍了 NodeMatch 的用途与常用在 method apis 里的 match options 分别介绍了 editor 里的各个 properties 与 methods ,再回头补足了 core actions 与 Command 之间的关系。
最後介绍了集大成 node.ts 的 Node union type 以及 NodeProps
然後 ... 就没有然後了,就两个 files 的内容而言要吸收的概念也不少呢!


毕竟 Data-Model 也算是 slate 的重点之一呢,要想开始使用 slate 做开发,这篇的内容绝对算是最基本的入门门槛。

紧接着下一篇我们要来介绍 /Interface 里最後的重点,也就是 slate 是如何去做文字定位( Position )的,这又会是另一场硬仗了,一样明天再见真章吧!


<<:  day12 轻松一下,用 coroutine 接个 restful api

>>:  [Day27] 超萌❤ 教你用Python画天竺鼠车车逗女友开心!

[神经机器翻译理论与实作] 你只需要专注力(I): Attention Mechanism

前言 Google 翻译团队在2016年发表了重要文章《Google’s Neural Machin...

前端工程日记 30日 名片设计

如图 pancode: div 设计成 各种形状 三角形。五角形 六角形 的方法 制作参考引用 ht...

Day10

第六章函数与递回,强调的是函式原型(function prototype)又称为函式宣告(func...

29 | WordPress 区块编辑器 | 本次教学单元总结:

感谢大家花宝贵的时间阅读这系列的文章,由於篇幅有限,其实还有很多主题无法尽录,不过希望阅读过後,大...

第 01 天 小试身手由简入深 ( leetcode 001 )

https://leetcode.com/problems/two-sum/description...