iT邦幫忙

2021 iThome 鐵人賽

DAY 12
0
Modern Web

深入 slate.js x 一起打造專屬的富文字編輯器吧!系列 第 12

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 )的,這又會是另一場硬仗了,一樣明天再見真章吧!


上一篇
Day 11. slate × Interfaces × Document-Model
下一篇
Day 13. slate × Interfaces × Positioning
系列文
深入 slate.js x 一起打造專屬的富文字編輯器吧!30

尚未有邦友留言

立即登入留言