iT邦幫忙

2021 iThome 鐵人賽

DAY 13
0
Modern Web

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

Day 13. slate × Interfaces × Positioning

https://ithelp.ithome.com.tw/upload/images/20210928/20139359PxGFG9amLY.png

緊接著這一篇要來探討 slate 是如何實現『定位』這件事的。

在開發以 text 文字資料模型為主的功能時,有辦法精準地定位到特定的文字位置,甚至是文字集合絕對是基本中的基本。

讓我們先來舉幾個簡單的例子讓讀者先體會一下『定位』這件事的重要性:

  1. 範例一:使用者當前關注位置

    https://s3-us-west-2.amazonaws.com/secure.notion-static.com/25a0062e-9537-42d4-abac-724e69738810/ezgif.com-gif-maker_(2).gif

    比如說像上圖這種功能,在進行任何的屬性更動之前我們都必須先定義清楚『使用者當前的關注位置』,而這通常會是以當前光標( caret )的位置來決定,也因此有一個能夠清楚定位且紀錄下當前光標位置,而不是依賴於瀏覽器提供的 api 就顯得格外重要。

  2. 範例二:Toolbar (浮動工具欄)

    https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3105cf29-6ee8-46d2-b4f6-eae1d111d16c/ezgif.com-gif-maker_(1).gif

    這幾乎是現代我們常看到的富文本編輯器幾乎一定會出現的功能。

    在開發這項功能時有一項決定性的要素:我們必須要知道使用者反白的文字位置。先有這項資訊我們才有辦法進行後續的處理,像是定位工具欄在畫面上的位置以及執行工具欄的功能時如何更新資料 ... 等等。

    這類的針對特定『區間』而非『單一文字』的功能在富文本編輯器裡非常的常見,於是 slate 當然也有必要提供實現這項需求的概念給開發者使用。

其實在 slate 裡,與定位相關的概念很大一部份也是來自於 HTML DOM 的 Selection 與 Range 這兩個 object ,這讓我們在學習上變得更加直觀一點,只是 slate 又額外提供了 path 與 point 這兩個概念讓開發者也能做到絕對定位這件事,以及方便開發者使用的 location 裡的內容,我們就由簡入繁依序介紹吧!

https://ithelp.ithome.com.tw/upload/images/20210928/20139359QlfDmcxveQ.png

path.ts


path 是其中最基本的概念,在這個 file 裡面就只有一個非 extendable 的 Path type 與他提供的 methods 。

/**
 * `Path` arrays are a list of indexes that describe a node's exact position in
 * a Slate node tree. Although they are usually relative to the root `Editor`
 * object, they can be relative to any `Node` object.
 */

export type Path = number[]

Path 的用途是拿來指向 Slate node tree 裡,相對於自定義 root 節點的特定節點,而通常 root 節點代表最頂端的 Editor 節點。』

可以說點人話嗎?你都說這是最基本的觀念了,要這樣打擊人的信心?


抱歉抱歉,雖然乍看之下超級像在背書,但這就是對 Path 這個 type 最精確的解釋了。

我們在上一篇有介紹過了 slate node tree ,那 Path type 就是負責定位特定的 node 在 node tree 裡的絕對位置,我們直接來看看以 Editor 為 root 的範例。

const editor = {
    children: [
        // Path: [0]
        {
            type: 'paragraph',
            children: [
                // Path: [0, 0]
                {
                    text: 'A line of text!',
                },
                // Path: [0, 1]
                {
                    text: 'Another line of text!',
                    bold: true,
                },
            ],
            },
		// Path: [1]
		{
			type: 'paragraph',
			children: [
				// Path: [1, 0]
				{
					text: 'A line of text!',
				},
			],
		},
    ],
    // ...other properties in editor
}

上例裡的註釋( Path: [...] )代表位於它正下方的 node 以 editor 為 root 的 Path 結果,基本上在 slate 裡一切只要與『定位』這件事相關的內容都一定會與 Path 這項資料扯上邊,它在定義上代表的是 node 在整個 Slate node tree 的絕對位置,所以一般來說他是以 Editor 作為 root node (根節點)。但其實我們在使用上也能將它視為與『任意』節點的相對位置。

像是我們以 Node method api 裡的 get method 為例:

/**
 * Get the descendant node referred to by a specific path. If the path is an
 * empty array, it refers to the root node itself.
 */

get(root: Node, path: Path): Node {
  // ... Implementation
},

這個 method 會以 root 而非直接以 Editor 作為根節點,計算出傳入的 path 所指向的節點並回傳。

point.ts


point 是從 path 延伸出來的概念,它負責的工作就是定位單一文字 character 的位置,這個 file 裡有一個主要的 extendable Point type 以及協助 iterate 的 PointEntry type ,後者我們一樣會在之後的篇章一併整理介紹

/**
 * `Point` objects refer to a specific location in a text node in a Slate
 * document. Its path refers to the location of the node in the tree, and its
 * offset refers to the distance into the node's string of text. Points can
 * only refer to `Text` nodes.
 */

export interface BasePoint {
  path: Path
  offset: number
}

export type Point = ExtendedType<'Point', BasePoint>

先用 path property ,代表該 char 屬於哪一個 node ,再用 offset property 指出該 char 為該 text node 裡的第幾個字,我們一樣來舉個例子幫助理解。

const editor = {
    children: [
        {
            type: 'paragraph',
            children: [
                {
                    // The point of the character "!" is { path: [0, 0], offset: 14 }
                    text: 'A line of text!',
                },
				{
					// The point of the character "!" is { path: [0, 1], offset: 20 }
					text: 'Another line of text!',
					bold: true,
				},
            ],
        },
		{
			type: 'paragraph',
			children: [
				{
					// The point of the character "!" is { path: [1, 0], offset: 14 }
					text: 'A line of text!',
				},
			],
		},
    ],
    // ...other properties in editor
}

range.ts


最後的 range 又是從 point 延伸而來的概念,在這個 file 裡就只有一個 extandable 的 Range type ,之前也有提到過 Range 代表的就是一串『文字集合』,最常被應用到的時機點就是在 User selection ,也就是常見的反白的概念。

/**
 * `Range` objects are a set of points that refer to a specific span of a Slate
 * document. They can define a span inside a single node or a can span across
 * multiple nodes.
 */

export interface BaseRange {
  anchor: Point
  focus: Point
}

export type Range = ExtendedType<'Range', BaseRange>

我們在之前也有提到過 slate range 的概念其實就是來自於 DOM 的 Range object,從 anchorfocus 這兩個 properties 就可以看出這點了, anchor 代表這串文字集合開始的 Pointfocus 代表結束的 Point

既然都說它代表一個文字集合了,那它理所當然也有 Expand (展開)與 Collapse (收合)的概念,我們用幾個簡單的 selection 範例來解釋。

https://ithelp.ithome.com.tw/upload/images/20210928/20139359TDnOAScPc8.png

上圖的反白區域所代表的 range 為:

{
	anchor: {
		path: [0, 0],
		offset: 0,	
	},
	focus: {
		path: [0, 0],
		offset: 13,
	},
}

https://ithelp.ithome.com.tw/upload/images/20210928/20139359mDwLkuNcy1.png

上圖的紅線所標示的位置就是當前光標的位置,而它的 range 為:

{
	anchor: {
		path: [0, 0],
		offset: 0,
	},
	focus: {
		path: [0, 0],
		offset: 0,
	},
}

上面這三個 files : path 、 point 、 range 裡的內容就是 slate 的定位主要會使用,需要了解的概念,最後剩下的 location 是為了輔助開發所建立的,分別可以在裡面看到 LocationSpan 這兩種 types

/**
 * The `Location` interface is a union of the ways to refer to a specific
 * location in a Slate document: paths, points or ranges.
 *
 * Methods will often accept a `Location` instead of requiring only a `Path`,
 * `Point` or `Range`. This eliminates the need for developers to manage
 * converting between the different interfaces in their own code base.
 */

export type Location = Path | Point | Range

/**
 * The `Span` interface is a low-level way to refer to locations in nodes
 * without using `Point` which requires leaf text nodes to be present.
 */

export type Span = [Path, Path]
  • Location

    一個集合了 PathPointRange 的 Union type ,它的存在完全就只是為了方便開發而已。

    slate 將它視為開發者與一切 document position 功能相關的溝通介面,也因此 slate 提供的 method apis 裡只要與定位相關的功能,只要不是特別鎖定於某個 type 所用,幾乎都會支援 Location 並在 method 裡面針對不同 type 的情境做處理。

  • Span

    最後的 Span 是一個沒有 Text node 版本的 range ,使用情境就是在以 Element node 為單位的節點集合。


到此為止我們終於將 Interface/ 裡的主要 concepts 都介紹完了!了解了這三篇介紹的 concepts 後就過了最~基本使用 slate 的門檻,不過在 interface/ 底下還有提供其他好用的 concepts 給開發者使用,像是下一篇要借介紹的 refs concept 就是算有歷史淵源的一個。

詳情就待明日分曉吧!


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

尚未有邦友留言

立即登入留言