19

深入 Slate.js - HTML 中的富文本

 3 years ago
source link: https://zhuanlan.zhihu.com/p/324332585
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文是「深入 Slate.js」系列的第 4 篇内容 - Slate.js 简介。

22QNjej.jpg!mobile

Slate.js 的数据结构设计大量参考了 HTML 中对于 DOM 的设计,因此,在了解 Slate.js 是怎么设计富文本数据模型之前,我们先回顾下 HTML 中的富文本是怎么设计的。

前文中我们提到,Web 富文本,其实就是一段 HTML 内容,它由两个部分组成:

  • 节点(Node) :节点容纳了我们能看到的富文本内容,富文本容器也是一个节点,容纳了其他节点
  • 选区(Selection) :当前选中的区域,如果区域的起点和终点重合,那看到就是一个光标

节点

设计富文本中的节点,我们需要考虑:

  • 有哪些类型的节点
  • 如何为节点绑定额外数据
  • 节点与节点间的关系如何
eeeQzi2.jpg!mobile

节点类型

在 HTML 现行规范中,节点(Node)被定义为一个 Node Interface,它是一个抽象基类,继承自 Node Interface 的类将具备访问节点属性、修改节点结构的能力。

对于一个网页来说,继承自 Node 的Document 就被用来表示网页内容:

7BFRRnm.jpg!mobile

在这颗 DOM 树中,挂载了不同类型的、实现了Element Interface 的节点,从而拥有了样式和尺寸:

JrAVjav.jpg!mobile
Element.classList
Element.styles
Element.clientHeight
Element.getBoundingClientRect()
Element.getComputedStyle()

我们常见的 divpspan 等都继承自HTMLElement(它继承自 Element)。在 HTML 5 之前,HTMLElement 被分为了:

  • 块级元素(Block Level Element) :块级元素占据了父容器的整个空间,视觉上就是形成了一个 “块”。文档中每新增一个块,首先要新增一个容纳这个块的行。
  • 行内元素(Inline Level Element) :行内元素只占据了内容所需要的空间。
vmq6bu7.jpg!mobile

而在 HTML 5 之后,HTMLElement 的按照「内容」进行了更细的分类:

qimymae.jpg!mobile

节点文本

在 HTML 规范中,Text Interface 用来表示 Element 的文本内容,它继承自CharacterData ,后者描述了 Node 所包含的字符:

fIZ7Rfb.jpg!mobile

我们可以通过 Document.createTextNode() 向节点中插入多个 Text,并通过 Node.textContent 访问节点的文本:

const element = document.createElement('div');

element.appendChild(document.createTextNode('1 '));
element.appendChild(document.createTextNode('2 '));
element.appendChild(document.createTextNode('3 '));

element.childNodes; // NodeList [text, text, text]
element.textContent; // 1 2 3

在上面这个例子中,我们创建了多个相邻的文本节点,这些相邻的节点可以通过 Node.normalize() 进行合并:

element.normalize();

element.childNodes; // NodeList [text]
element.textContent; // 1 2 3

Void Element(空节点) ,也被称为 Empty Element,是一类特殊的 HTML Element,它不允许包含任何的内容。诸如 <input /><link /> 等都是 Void Element,尝试注入内容到 Void Element 是无效的:

<p>
  <input value="12345">Content</input>
  <img src="https://this-is-an-image.jpg">Image</img>
</p>

节点数据

一个节点,除了可以含有文本内容,还能绑定一些额外数据。例如我们需要为图片节点设置图片源和尺寸信息,HTML img tag 为此提供了 srcwidthheight 等属性。更一般地,我们可以通过 data-* 设置节点的数据:

<pre
  id="code-block"
  data-syntax="javascript"
  data-theme="dracula"
/>

再通过 HTMLElement.dataset.xxx 访问这些数据:

const code = document.querySelector("code-block");

code.dataset.syntax; // javascript
code.dataset.theme; // dracula

节点关系

一篇 HTML 文档,内部被一棵 DOM 树所表示,DOM 树中的每个节点又是一个 DOM 子树,树节点就存在「祖先、孩子、兄弟」的关系。HTML 规范中,通过 Node Interface 定义了访问当前节点的父节点、孩子节点以及兄弟节点的方式:

Node.parentNode;
Node.childNodes;
Node.previousSibling;
Node.nextSibling;

选区

HTML 的中的选区,包含了两个部分:

  • Selection:用户选中的内容
  • Range:DOM 中表示内容区间的数据结构
6faABrY.jpg!mobile

Selection

选区(Selection)表示的是当前用户选中的内容:

auiuiaY.jpg!mobile

描述一个选区需要有:

  • 选区的起点 :起点反映了选区从哪个节点,哪个偏移位置开始
  • 选区的终点 :终点反映了选区从哪个节点,哪个偏移位置结束

因此,在 HTML 中,一个 Selection 对象就包含下面这些属性:

anchorNode
anchorOffset
focusNode
focusOffset
isCollapsed

选区的方向则可以通过 anchor、focus 的位置判定,若 focus 落在 anchor 之后,为向后选择,若落在 anchor 之前,则为向前选择,二者重叠时,即选区被折叠,用户视觉上看到的就是一个闪烁的光标。

调用 window.getSelection() 可以获得当前窗口的选区。

Range

在 DOM 内部,被选中的内容又是怎么表示呢?HTML 标准为此定义了Range:一个 Range 对象描述了一个包含若干 DOM 节点的「文档区间」。

q6ryEnZ.jpg!mobile

Range 同样需要确定起点和终点,其含有的属性类似于 Selection 对象的属性:

collapsed
commonAncestorContainer
startContainer
startOffset
endContainer
endOffset

总结

本节分析了 HTML 中富文本内容的组成和实现,一个富文本需要包含节点和选区两个部分,节点是一棵 DOM 树,树中的节点各有类型;选区则分为了 Selection 和 Range,前者表示了用户选中的内容,后者则是这些内容的 DOM 表达。

对于选区,要多说一句的是,Selection 最早是作为 Netscape 浏览器的特性被引入,之后又被 Firefox 的 Gecko 引擎所实现,最后陆续被其他浏览器所实现。Netscape 将 Selection 实现为了由多个 Range 所构成,这看似合乎语义,一个选区应当可以包含多个文档片段。假设用户想同时选中一个表格的若干列,这样的设计就是必要的,但是开发者因此要应付许许多多边界问题。

因此,现在的浏览器在实现 Selection 时,不再允许一个 Selection 包含多个 Range,考虑到向后兼容 API,还是为 Selection 支持了 removeRange()getRangeAt() 等方法,只是开发者在使用这些 API 时,一般都只能传入 0 作为 index,代表操作第一个也是唯一一个 Range 对象。

了解了 HTML 中是怎么表示富文本之后,接下来我们看看 Slate.js 又是怎么设计它的富文本数据模型的。

参考资料

如果你对协同文档技术感兴趣,也可以加入下面的群(钉钉/微信),和我们一同讨论。

https:// qr.dingtalk.com/action/ joingroup?code=v1,k1,FSwHUda2hGA0PvtY34qY68ZWAC9bRkUAvN/NH83ovJ8= <br> https:// weixin.qq.com/g/AQYAAEk 4qxSX1JWBg6LJzUXNlEGIyR70uhBLCLptE5D12sRogH7CHAGaQQn-FMav (二维码自动识别)

也欢迎关注本账号,我们每周都会更新~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK