# 富文本基础概念篇
Table of Contents
常用富文本编辑器梳理
| 编辑器名称 | 网址 | 主要介绍 |
| TinyMCE | https://www.tiny.cloud/ | 易用、功能强大、所见即所得、开源可商用 |
| CKEditor | https://ckeditor.com/ | 开箱即用 |
| UEEditor | https://github.com/fex-team/ueditor | 百度 FEX-team |
| AI Editor | https://aieditor.dev/zh/ | ai集成, 简单友好 |
| slatejs | https://www.slatejs.org/examples/richtext | 完全可定制 |
| Lexical | https://lexical.dev/ | 扁平化数据管理 |
| quill | https://quilljs.com/ | |
| Editor-kit | https://editor-kit.web.bytedance.net/docs/quick-start | 飞书富文本 |
| Tiptap | https://tiptap.dev/ |
JS富文本常见API
Contenteditable
MDN:https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/contenteditable
基本功能:表示元素是否可被用户编辑,是实现编辑器的最基本属性
可选属性值:
true或空字符串,表示元素是可编辑的。false表示元素不是可编辑的。plaintext-only表示元素的原始文本是可编辑的,但富文本格式会被禁用。
execCommand(deprecated)
MDN 文档中介绍,execCommand是一种为富文本编辑设计的API。它可以在网页文档中执行各种文本操作命令,让开发者轻松实现加粗、斜体、复制和插入链接等功能。
这个API曾经非常流行,但随着网络技术的发展,它逐渐被淘汰,现在MDN已将其标记为⚠️废弃,不再建议使用。
不再使用的原因:
- 兼容性问题:现代浏览器对
execCommand的支持越来越少,不同浏览器的表现也不一致。 - 功能有限:无法满足复杂的格式和个性化需求
替代方法:通过直接操作Selection和Range对DOM进行控制,以实现自定义的文本编辑功能
Selection
1. 关键属性
| 属性 | 说明 |
|---|---|
anchorNode | 选中的起始节点(鼠标按下时的节点) |
anchorOffset | 起始节点内的偏移量(比如文本节点的字符位置,元素节点的子节点索引) |
focusNode | 选中的结束节点(鼠标松开时的节点) |
focusOffset | 结束节点内的偏移量 |
isCollapsed | 是否「折叠」(即没有选中任何内容,只有光标) |
rangeCount | 包含的 Range 数量 |
相关术语
- 锚点(anchor):锚点指的是一个选区的起始点。当鼠标框选某个区域时,鼠标点击的那一瞬间锚点的位置即确定了,拖动时不会变化。
- 焦点(focus):选区的焦点是选区的终点,当鼠标框选某个区域时,鼠标松开的一瞬间焦点即确定了,焦点位置随拖动变化。
- 范围(range):范围指文档中连续的一部分。一个范围可能包含整个节点,也可能只包含某个节点的一部分。用户一般只能选择一个返回,但是也有可能会选择多个范围,例如使用
Control+Click选择多个区域(Chrome 禁止了这个操作)。「范围」会被作为Range对象返回。
2. 常用方法
selection.collapse(node, offset):将选中折叠到某个位置(设置光标);例:把光标移到段落第 5 个字符后:const p = document.querySelector('p');selection.collapse(p.firstChild, 5); // p.firstChild 是文本节点selection.selectAllChildren(node):选中某个元素的所有子节点;例:选中 div 内的所有内容:const div = document.querySelector('div');selection.selectAllChildren(div);selection.deleteFromDocument():删除所有选中的内容;例:删除用户选中的文本:selection.deleteFromDocument();
Range
- 创建Range
通过 document.createRange() 创建空 Range,再用方法定义范围:
const range = document.createRange();- 常用方法:定义范围
range.setStart(node, offset) + range.setEnd(node, offset)
:精准定义范围;
例:选中文本节点的第 0 到 5 个字符:
const textNode = document.querySelector('p').firstChild;range.setStart(textNode, 0); // 从第0个字符开始range.setEnd(textNode, 5); // 到第5个字符结束range.selectNode(node):选中整个节点(包括节点本身);
const btn = document.querySelector('button');range.selectNode(btn);range.selectNodeContents(node):选中节点的所有子内容(不包括节点本身);例:选中 div 内的文本(不包括 div 标签):
const div = document.querySelector('div');range.selectNodeContents(div);- 常用方法:操作范围内容
这部分是实战重点,比如「加粗选中文本」「插入内容到光标」都依赖这些方法:
| 方法 | 说明 |
|---|---|
range.surroundContents(wrapper) | 用 wrapper 包裹范围内容(要求范围不跨节点边界,否则报错) |
range.extractContents() | 提取范围内容为 DocumentFragment(从 DOM 中移除原内容) |
range.deleteContents() | 删除范围内容(不返回) |
range.insertNode(node) | 在范围起始位置插入节点 |
range.cloneRange() | 复制 Range(修改副本不影响原 Range) |
示例代码
- 例子一:把选中的文本「加粗」(富文本核心操作)
function boldSelectedText() { const selection = window.getSelection(); if (selection.isCollapsed) return; // 没有选中内容,直接返回
const range = selection.getRangeAt(0); const bold = document.createElement('b');
try { // 尝试直接包裹(要求范围不跨节点) range.surroundContents(bold); } catch (e) { // 跨节点时,提取内容再包裹(比如选了一个 span 的一部分) const fragment = range.extractContents(); bold.appendChild(fragment); range.insertNode(bold); }
// 保持选中状态(否则加粗后选中会消失) selection.removeAllRanges(); const newRange = document.createRange(); newRange.selectNodeContents(bold); selection.addRange(newRange);}
// 绑定按钮点击事件document.querySelector('#bold-btn').addEventListener('click', boldSelectedText);- 例子二:在光标位置插入文本(比如「@提及」功能)
function insertTextAtCursor(text) { const selection = window.getSelection(); if (!selection.rangeCount) return;
const range = selection.getRangeAt(0); const textNode = document.createTextNode(text);
if (range.collapsed) { // 光标状态:直接插入到光标位置 range.insertNode(textNode); // 移动光标到插入文本之后 range.setStartAfter(textNode); range.collapse(true); } else { // 选中状态:替换选中内容 range.deleteContents(); range.insertNode(textNode); range.collapse(false); // 光标移到文本末尾 }
// 更新 Selection selection.removeAllRanges(); selection.addRange(range);}
// 调用:在光标位置插入 "@test"insertTextAtCursor('@test');注意事项
- DOM 修改后,Range 会失效:如果删除了 Range 中的节点(比如
range.startContainer),原 Range 会变成「无效状态」,需要重新获取。 surroundContents的限制:该方法要求 Range 不能「部分选中」节点(比如选了一个<span>的前半部分和<div>的后半部分),否则会抛出错误。此时用extractContents()+insertNode()替代。兼容性:
- 现代浏览器(Chrome/Edge/Firefox/Safari)完全支持;
- IE 浏览器用
document.selection(已淘汰,无需兼容)。
- Selection 是「实时的」:用户改变选中内容时,
window.getSelection()返回的对象会自动更新,不需要重新获取。