iT邦幫忙

2021 iThome 鐵人賽

DAY 11
0
Modern Web

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

Day 11. slate × Interfaces × Document-Model

https://ithelp.ithome.com.tw/upload/images/20210926/20139359tu5MSeiweV.png

接下來的篇章我們會把目光聚焦於 interfaces/ 這個目錄底下的內容,想確認 slate package 完整的 src directory 的讀者們可以回到 上一篇 做確認。

slate Interfaces


我們首先來為它底下的 files 做點簡單的分類與介紹:

https://ithelp.ithome.com.tw/upload/images/20210926/20139359by7pwbU5II.png

除了 custom-types.ts 是專門定義與處理 type extension 之外,其他的 file 都各自代表著一組 concept ,也就是上圖畫了紅線的部分。它們各自同時擁有:定義這組 concept 相關的 types 、這組 concept 提供給開發者使用的 method apis。

我們用一個名叫 Example 的範例來講解 concept file 裡基本的 code-structure :

export type ExampleType = {
	// type declaration, use "interface" to declare it if it's extendable
}

export interface ExampleInterface {
	// Concept method apis interface declaration
}

export const Example: ExampleInterface = {
	// Concept methods implementation
}

如果 type 被定義為 extendable 的話,我們則能看到額外引用 custom-types.ts 裡的 ExtendedType type utility 去擴充其基本 BaseType 的 type 。

type 來 type 去的是想亂死誰,直接上個範例最清楚,一樣用 Example 來介紹。

import { ExtendedType } from './custom-types';

export interface BaseExample {
  // type declaration
}

export type Example = ExtendedType<'Example', BaseExmaple>

要注意其實上面的範例放進 ExtendedType 是不過關的,因為 ExtendedType 吃得第一個 string generic 是有用一個 Union type 限制住的,詳情我們後續在 custom-type 的篇章會再深入講解,目前只是先讓讀者們有個概念而已。


接著我們看到有被黃框的 files ,這些是我們在建立 editor 與操作 editor value 時主要會使用到的概念,我們接下來會花不少的篇幅,拿官方提供的註解當溫開水配著服用,依序介紹裡面的 types ,而各個 files 裡提供的 method apis 因為數量眾多又雜又亂,一條一條介紹完另一個 30 天就又過去了所以我們就不在這邊細說,筆者的時間跟心態都算充裕的話再另外整理成 reference 提供給各位吧~

那麼就如標題所言,我們首先會針對 slate 的 document model 所需要用到的 concepts 做介紹,分別是: text 、 element。

https://ithelp.ithome.com.tw/upload/images/20210926/20139359GBkAbKAyk2.png

text.ts


這裡面就只有一個 Extendable 的 Text type 而已,光看名字應該也能很直覺的反應出它代表的就是 editor 裡的『文字資料』。

/**
 * `Text` objects represent the nodes that contain the actual text content of a
 * Slate document along with any formatting properties. They are always leaf
 * nodes in the document tree as they cannot contain any children.
 */

export interface BaseText {
  text: string
}

export type Text = ExtendedType<'Text', BaseText>

Text type 只規定了其中的內容物必須要有一個類別為 string 的 text property ,開發者可以依照自己的喜好定義其他的 properties ,就拿我們在 Day3 舉過官方範例來看看。

下圖只要有被紅圈圈到的 Object 都各為一組 Text

https://ithelp.ithome.com.tw/upload/images/20210926/20139359zAomlQFvmw.png

再來看看它的 module declaration

type CustomText = {
  bold?: boolean
  italic?: boolean
  code?: boolean
  text: string
}

// slate module "Text" declaration
declare module 'slate' {
  interface CustomTypes {
    Text: CustomText
  }
}

把定義好的 module 引入就能使用自定義的 CustomText 隨意開發了,就是這麼輕鬆寫意。


Text 提供的 method apis 裡有一個特別值得提到的 method 是 decorations()

/**
 * Get the leaves for a text node given decorations.
 */
decorations(node: Text, decorations: Range[]): Text[] {
    ... // method implementation
}

Range 的概念我們會在 下一篇 介紹,目前讀者先知道它是 Slate 中描述一段可以橫跨不同的 Text 甚至 Element 的『字元集範圍』就好,一個 Range 裡面會同時有 anchor 與 focus 這兩個 properties 代表這個 range 的起始字元位置以及結束字元位置。

當我們丟給 decorations 合法的 Text 以及 range list ,也就是這些 Ranges 的範圍是正確的位於該 Text 內時,它會動態地幫我們依照這些 Range 拆成一組 Text list。

const text = { text: 'This is a text example.' };
const ranges = [{
    anchor: { path: [0, 0], offset: 5 },
    focus: { path: [0, 0], offset: 7 },
}];

/**
returns: [
    { text: 'This ' },
    { text: 'is' },
    { text: ' a text example.' },
] */
Text.decorations(text, ranges);

當然你也可以放上自定義的 Text property

const text = { text: 'This is text example2.' };
const ranges = [{
    anchor: { path: [0, 0], offset: 5 },
    focus: { path: [0, 0], offset: 7 },
    bold: true,
}];

/**
returns: [
    { text: 'This ' },
    { text: 'is', bold: true },
    { text: ' text example2.' },
] */
Text.decorations(text, ranges);

嗯?幹嘛多此一舉?直接修改 document model 裡的 value 不就好了嗎?


筆者一開始也很困惑於這個功能存在的意義,確實直接修改 value 就能達到一模一樣的效果了,後來在官方提供的 Search Highlighting example 裡找到了答案:

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a2cd3927-6885-4be1-b0cd-b7f025568edc/ezgif.com-gif-maker拷貝.gif

它很適合用於開發 「修改的頻率高,幅度大,卻只修正 Text properties 而不會更改到文字的內容或其他 value 的功能」。正如其名 decorations ,它的工用就像是在整個工程的尾端輕輕的裝飾文字的屬性,因為不會去更動到 editor value ,所以可以省略掉許多更新整個 value tree 龐雜的運算,因此把它放在 view layer 的淺層,動態地計算 decorate 後的 Text ,會讓速度整個快上非常非常多。(讀者可以自己去實驗看看 XD


element.ts


裡面主要定義的 type 也只有一個 Extendable 的 Element type 而已,slate 的 document model 就是由 ElementText 這兩種 types 所組成的,相對於 Text 所代表的文字資料, Element 代表 editor 裡的元件資料

/**
 * `Element` objects are a type of node in a Slate document that contain other
 * element nodes or text nodes. They can be either "blocks" or "inlines"
 * depending on the Slate editor's configuration.
 */

export interface BaseElement {
  children: Descendant[]
}

export type Element = ExtendedType<'Element', BaseElement>

Element type 只規定了其中的內容物必須要有一個類別為 children 的 Descendant[] property ,開發者一樣可以依照自己的喜好定義其他的 properties 。

Descendant type 象徵著 slate document tree 裡的子節點,我們會在之後的 node concept 章節 詳細介紹,我們現在先偷瞄一眼 Descendant type 的定義

/**
 * 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

可以看到 ElementDescendant 這兩個 type 是彼此關聯的,在 Elementchildren property 裡再放上另一個 Element 也是沒問題的,這也是為什麼開發者可以很直觀地透過 slate 建立擁有巢狀結構的 feature ,只要照著 type 的規範走 slate 完全不在乎你的 document tree 長什麼樣子,你愛怎麼巢就怎麼巢。

另外我們也可以在 Element type 的註解裡看到,開發者也能針對各個不同的 elements 定義 blocksinlines 屬性,這部分因為牽涉到 slate editor 的 config 定義,我們就放到後面介紹 editor concept 的篇章吧!

element.ts 裡還有一個附加的 type 是 ElementEntry ,它主要是用在進行 iterate 相關操作的用途上,關於 iterate 我們之後會在整理出一篇文章跟各位分享分享。

哇今天的內容不少誒!來稍微整理一下:我們首先對 interfaces/ 這個目錄底下的 files 做了個簡單的分類 → 定義畫了 紅線 的 concept files 裡的 code structure → 分別介紹了 Document model 裡會出現的 Text type 以及 decorations methods 、 Element type 以及它與 Descendant type 的關係


感謝你的整理,今天的篇幅有那麼一點長,請讀者花些時間消化一下,緊接著到下一篇我們要介紹 slate 是如何實現 document value 的『定位』這件事的。

咱們一樣下一篇見啦~


上一篇
Day 10. slate × 架構藍圖
下一篇
Day 12. slate × Interfaces × Data-Model
系列文
深入 slate.js x 一起打造專屬的富文字編輯器吧!30

尚未有邦友留言

立即登入留言