上一篇我們有提到上圖這些畫了黃框的 files ,是我們在建立 editor 與操作 editor value 時主要會使用到的概念。
同時介紹了 slate document model 裡需要用到的 concepts : text 、 element。
緊接著今天我們就要把範圍擴大到整個 Data-Model 的層級了,也就是要納入 editor 與 node 這兩個概念。
一樣一個一個來,我們先從 editor.ts 開始,裡面主要定義了三種 type 分別是: Selection
、 Editor
、 NodeMatch
Selection
export type BaseSelection = Range | null
export type Selection = ExtendedType<'Selection', BaseSelection>
如其名,主要是拿來紀錄編輯器裡使用者當前的反白區塊,對 DOM 有一定熟悉程度的讀者就會知道,slate 裡的 Selection
與 Range
這兩個概念完全就是來自於 Web api 所提供的 Selection
、 Range
這兩個 object ,連 slate 的 Range
裡頭的 anchor
、 focus
也完全是在模仿瀏覽器 Selection
裡的 anchorNode
、 focusNode
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 valueselection
紀錄編輯器當前反白區域operations
紀錄一組 FLUSHING 內觸發過的 operations listmarks
提供了一個暫存空間,儲存文字節點內除了 text
屬性之外的自定義屬性資料,會在下次插入文字時賦予它們這些屬性資料。除此之外它也同時提供 overridable 的 behaviors :
onChange
在 Day10 介紹過了,負責通知 view layer 的 render
isInline
與 isVoid
又是兩個從 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 的章節會再詳細介紹。
經過了 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 之下的全貌。
是不是看起來就跟 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 就如我們先前介紹過的一樣,是 Editor
、 Element
、 Text
的 union ,Descendant 與 Ancestor 則各自為『可以成為子層節點』與『可以成為父層節點』的 types union ,它們是為了定義父/子層這兩種概念的集合而存在的,回頭看一下 前一篇 介紹的 Element
type
export interface BaseElement {
children: Descendant[]
}
這種定義方式的用意就是在描述『 children
property 相對應的 type 是子層的 type 集合』。
Node
、 Descendant
、 Ancestor
這三種 types 會經常在我們開發 slate 時被使用到,尤其是 Descendant ,因為太常出現需要針對『子層集合』這個概念進行操作的情境了,撇除掉開發者自行開發,就連在 method apis 裡面也經常會看到他們的身影。
在 node.ts 裡還有額外定義的 NodeEntry
、 NodeProps
這兩種 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 )的,這又會是另一場硬仗了,一樣明天再見真章吧!