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 就是算有历史渊源的一个。

详情就待明日分晓吧!


<<:  Day19 该如何发问问题?

>>:  Day 18 CSS <Sprite 精灵图>

Day 27. SSR 常见问题(2)

遍历 HTML tag 这时候你会需要使用Vue Server Test Utils暴露的另一个方法...

铁人赛 Day11 -- 一定要知道的 CSS (八) -- 想要设定个文字而已有那麽难吗?

前言 此篇虽然不是非常专业,但基本文字设定应该是没什麽问题啦 再谈谈设定文字之前要先知道文字有哪几种...

系统分析师的养成之路—案例分享(3)

其实写前一篇的案例主要是提醒後进们在处理收到的需求时,真的需要谨慎地确认,否则就会像我一样面临几乎打...

Kotlin Android 第24天,从 0 到 ML - TensorFlow Lite

前言: 人工智慧这个名词相信大家都一定常常听到看到,不管什麽产品都要是智能的,手机上更是有大量的运用...

【从零开始的Swift开发心路历程-Day22】天气预报App实作Part1

昨天我们完成简易订单系统後,今天要来练习一个新的挑战-串接API! 这次我们透过中央气象局开放资料平...