背景

公司需要我写一个编辑器能根据当前选择文字的光标处插入一个标签用作对文本阅读时的停顿。其实就是在 html 文本中插入一个标准的 html 标签。
最后将这个编辑器的内容中所有的 html 标签进行解析,最后转换成特定的 xml 标签即可。

前置的一些知识点介绍

  1. contenteditable 属性

他是 HTML 的属性,用作告诉给元素是否可被编辑。这是一切编辑器开发的开始!

注意:如果文本编辑器中的子元素没有显式设置 contenteditable ,那么子元素也是可以被编辑的, 如果不想被编辑就要设置 contenteditable=”false”

  1. Selection 对象

Selection 对象表示用户选择的文本范围或插入符号的当前位置。

其中 anchor(开始位置) 和 focus(结束位置) 概念是指在划选区时的 开始和结束 的位置。

常用 API

// 获取当前选中的 selection 对象,常用作获取光标位置。
const selection = window.getSelection();

// 获取当前选中的所有文字。
selection.toString();
// 获取选取对应 range 对象,在 chrome 下,index 只能填 0 .即:从 0 开始。
const range = selection.getRangeAt(index);
// 设置选中区域,选中某区域的某段节点上。
selection.addRange(range);

const range = document.createRange();
const span = document.querySelector("span");
range.selectNodeContents(span);
window.getSelection()?.addRange(range);
  1. Range 对象

解释源于 MDN

范围指的是文档中连续的一部分。一个范围包括整个节点,也可以包含节点的一部分,例如文本节点的一部分。用户通常下只能选择一个范围,但是有的时候用户也有可能选择多个范围(例如当用户按下 Control 按键并框选多个区域时,Chrome 中禁止了这个操作)。“范围”会被作为 Range 对象返回。Range 对象也能通过 DOM 创建、增加、删减。

常用的 API

// 克隆选区(仅仅只是浅克隆,里面的 node 对象还是直接引用),常用作保留当前的选区。
// 因为有时候用户可能会快速操作其他地方导致这里没法及时获取到的 range 。
const newRange = range.cloneRange();
// 其中的 range 有以下几个属性。

{
    // 表示 Range 的起始位置和终止位置是否相同
    collapsed: boolean,
    // 完整包含 startContainer 和 endContainer 的共同的父节点。
    commonAncestorContainer: node,
    // Range 终点所在的节点
    endContainer: node,
    // Range 终点所在的节点中的位置。
    endOffset: number,
    // 和end相反
    startContainer: node,
    // 和end相反
    startOffset: number,
}

有一个单词 Caret 。是 插入符 的意思,(就是一闪一闪的那个光标)

const range = document.createRange();
const referenceNode = document.querySelector("div");
// 快速选中整个节点的内容。如果是在 cloneRange 中就相当于是模拟。
range.selectNodeContents(referenceNode);
const range = document.createRange();
// 给选取插入节点
range.insertNode(node);
// 将光标定在 范围 的开始或结束的地方 true (开始) ,false (结束)
Range.collapse(boolean);
// 设置 Range 的开始位置。
// 要注意 startNode 的类型会影响开始位置的地方。详情看MDN
// https://developer.mozilla.org/zh-CN/docs/Web/API/Range/setStart
Range.setStart(startNode, startOffset);

// 设置 Range 根据 referenceNode 作为开始。
Range.setStartAfter(referenceNode);
// 只读属性,返回目标节点的共有祖先节点。
// 通常用作保存当前指定的 编辑器 ,而非其它的 input 的选中区。判断当前选中的是否为指定区
Range.commonAncestorContainer;
  1. DOMParser 浏览器内置解析 html 的对象

解析 Dom 这种 xml 语言如果只用 正则表达式 去匹配你会发现有很多棘手的问题去处理。

比如:单双闭合标签,双闭合标签中的其它标签,属性是否带有对应的值(如:

)。
这种时候,你的正则(即使是 GPT 生成的)都很难完美的去覆盖上述情形。(还不是全部)

所以这时候,原生浏览器的 DOMParse 就宛如是我们的救星!

讲一讲大概的使用流程。

const parser = new DOMParser();
// 返回一个 Document 对象。如下
/**
 * <html><head></head><body><div>Hello World!</div></body></html>
 */
const doc = parser.parseFromString("<div>Hello World!</div>", "text/html");

至此你就可以用原生的 Dom api 去操作 DOM 了,非常简单省心。

  1. 富文本中 dom 与 node 的区别

DOM

  1. DOM 代表 HTML 或 XML 文档的结构,使 JavaScript 能够动态操作网页的内容和结构。
  2. DOM 树 是由 Node 组成的层级结构,每个 HTML 元素、属性、文本等都是一个 Node。

node

  1. Node 是 DOM 元素的基类,HTML 元素、文本、属性、注释等都继承自 Node。

元素节点(Element Node):HTML 标签,如

,它们是 Element 类型的实例。

文本节点(Text Node):元素中的文本,如 Hello,它是 Text 类型的实例。

属性节点(Attribute Node):HTML 属性,如 class=”box”,它是 Attr 类型的实例。

注释节点(Comment Node):HTML 注释,如 ,它是 Comment 类型的实例。

总结:
一个 HTML 的结构化成 json 去理解

Dom 指的是整个 JSON 对象,而 Node 是指 JSON 中的每一个属性。

{
  "attr": "class",
  "tagName": "p",
  "children": ["..."]
}

具体的需求及对应的实现方式

  1. 插入一个 dom 节点。

如图:

  1. 获取当前光标和与之对应前后的 5 个字符。(尽可能,因为又可能会碰到其它的非 textNode)若超过有 5 个则追加省略号。

如图:

开发过程中遇到的一些坑以及对应的解决方案

  1. 使用 mvvm 框架对编辑器进行响应式渲染会导致的问题

响应式的渲染,由于编辑框内的光标在响应式数据更新后,重新渲染最终导致的位置发生重置。只需要更新响应式对象后修改回编辑框的 innerHtml 即可。

  1. 如何保存上一次光标的位置?

  2. 关于转换后的单双闭合标签保留如何用正则匹配?

  3. 如何解决从别的地方复制的文本在不保留样式的情况下到编辑器中?

  4. 如何快速获取编辑器中所有有效的字符?即:所有 textNode 中的内容。

总结



本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!