作者:宇文涛
一些概念与误解
什么是富文本?
微软公司开发用来多平台阅读编辑的一种文件类型,RTF文档(Rich Text Forma),类似于word,现在多数指对文字可进行编辑排版的工具,如vscode也可以称为富文本
前端所说的富文本指的是?
统一是web端的富文本编辑器,也具备多端能力主要是网页本身具备多端运行能力
常用的富文本编辑器
- 百度 UEditor
- quilljs
- wangEditor
- Google Doc 与 腾讯文档
富文本编辑器的类型
类型 | 描述 | 代表 | 优劣 |
---|---|---|---|
L0 | 1. 基于浏览器的contenteditable属性完成富文本输入框 |
- 使用document.execCommand操作命令完成排版 | 轻量级编辑器 典型代表:wangEditor,UEditor | 优:短时间内快速研发 劣:可定制空间非常有限 | | L1 | 1. 基于浏览器的contentditable属性富文本输入框
- 自主实现操作命令完成排版 | 典型代表:draft.js(初始化了一个编辑区)、TinyMCE等 | 优:在浏览器的基础上,能满足大部分业务 劣:无法突破浏览器本身的排版效果,性能与HTML的局限问题 | | L2 | 1. 自主实现富文本输入框
- 只依赖少量浏览器API | Google Doc,腾讯文档, | 优:都自己实现,可控度都掌握在开发者手里 劣:技术难度大 |
此次分享仅仅针对 L0与L1做一次技术分享
关于contenteditable 属性
全局属性[所有html元素属性] contenteditable 是一个枚举属性,表示元素是否可被用户编辑。 如果可以,浏览器会修改元素的部件以允许编辑, 默认继承父元素的当前属性值
-
注意事项
- 该属性是一个枚举属性,而非布尔属性。这意味着必须显式设置其值为
true
、false
或空字符串中的一个,并且不允许简写为<label contenteditable>Example Label</label>
正确的用法是<label contenteditable="true">Example Label</label>
。
- 该属性是一个枚举属性,而非布尔属性。这意味着必须显式设置其值为
-
oninput事件
- 通过事件对象可获取编辑区域的Dom对象,从而拿到innerHtml 与innerText
举例
import React, { useState, useCallback } from "react";
function App() {
const [isEdit, setIsEdit] = useState(true);
const [htmlIfo, setHtmlIfo] = useState(null);
const onEdit = useCallback((e) => {
const target = e.target;
setHtmlIfo(target.innerHTML);
}, []);
return (
<div
className="demoBox"
contentEditable={isEdit}
suppressContentEditableWarning
onInput={onEdit}
>
<h2>我是标题</h2>
<p>我是段落</p>
<p>我是图片</p>
</div>
);
}
export default App;
关于 document.execCommand 的JS 方法
document.execCommand的介绍
- execcommand文档
- 设计模式
document.designMode
控制整个文档是否可编辑。
- selection对象
- 对象表示用户选择的文本范围或插入符号的当前位置。它代表页面中的文本选区,可能横跨多个元素。
举例
import React, { useState, useCallback } from "react";
const commandMapData = [
{
name: "背景色",
commandKey: "backColor",
value: "#ccc",
},
{
name: "字体色",
commandKey: "foreColor",
value: "red",
},
{
name: "字体加粗",
commandKey: "bold",
},
{
name: "删除",
commandKey: "delete",
},
{
name: "撤销",
commandKey: "undo",
},
{
name: "恢复",
commandKey: "redo",
},
];
function App() {
const [isEdit, setIsEdit] = useState(true);
const [htmlIfo, setHtmlIfo] = useState(null);
const onEdit = useCallback((e) => {
const target = e.target;
setHtmlIfo(target.innerHTML);
}, []);
// 执行指令
const actionCommand = useCallback((e) => {
const key = e.target.dataset.key;
const index = commandMapData.findIndex((res) => res.commandKey === key);
document.execCommand(key, false, commandMapData[index].value);
}, []);
return (
<div>
<div>
{commandMapData.map((res) => (
<button
key={res.commandKey}
data-key={res.commandKey}
onClick={actionCommand}
>
{res.name}
</button>
))}
</div>
<div
className="demoBox"
contentEditable={isEdit}
suppressContentEditableWarning
onInput={onEdit}
>
<h2>我是标题</h2>
<p>我是段落</p>
<p>我是图片</p>
</div>
</div>
);
}
export default App;
document.execCommand的局限性
- 兼容性问题,MDN已经显示主流浏览器废弃了这个api
- 但是现在firefox还是chrom 等主流浏览器都还是支持的,所以废弃态度更像是不维护,不修复
- 缺乏扩展性,一些业务BUG
- undo与 redo的全局影响
- 排版受限,排版必须按照指定命令来执行
- 等等
业界富文本组件在做什么?
既然有浏览器行业标准API,那么那些富文本编辑器代码是在干什么?
- 处理 document.execCommand 的兼容问题
- 如 insertHTML 和 increaseFontSize 不支持IE浏览器
- 解决方案,利用selection找到光标位置,然后插入需要处理的数据
- 如 insertHTML 和 increaseFontSize 不支持IE浏览器
- 处理 document.execCommand 的业务逻辑BUG
- 如undo与 redo对整个页面进行操作
- 需要自己写一个栈数据,对用户操作记载,然后undo和redo
- 如undo与 redo对整个页面进行操作
- 新的富文本利用selection和Range这两个属性重写 execCommand
- 下面会详细说
- 富文本功能的插件化
- 功能可配置化
- 处理 用户操作的负面问题
- 如xss攻击预防
- 字符串的正则检查,数据过滤
- 如复制粘贴去除无用节点(更多是从word文件复制过来)
- 正则检查,数据过滤
- 如图片插入上传与复制上传处理
- 插入上传利用 execCommand 可以做到,允许嵌入在线图片链接的
- 静态图片上传
- 复制上传需要监听 paste 事件,存在多种情况
- 单独复制图片
- 拿到 clipboardData 数据 然后进行处理
- 和文本一块复制图片(HTML格式)
- 复制图片为本地路径
- 暂无解决方案
- 复制图片为在线路径
- 在线路径 利用正侧查找 img 标签,单独请求链接 然后处理
- 复制图片为本地路径
- 单独复制图片
- 如xss攻击预防
关于Selection 与 Range
均为实验属性
Selection 对象
Selection
对象表示用户选择的文本范围或插入符号的当前位置。它代表页面中的文本选区,可能横跨多个元素。文本选区由用户拖拽鼠标经过文字而产生。
所说的横跨多个元素,用户操作无法完成,必须JS进行选区添加,场景就是 多人协同修改文档,显示光标位置
Range对象
表示一个包含节点与文本节点的一部分的文档片段
可以说selection对象有选区,那么就会有range对象
尝试实现execCommand 功能
已现有DEMO 为例,进行功能实现
-
功能
- 背景色
- 字体色
- 加粗
- 撤销与恢复
-
方案
- 获取光标选择区
- 对光标选择区进行编辑
- undo与redo写一个数据操作栈,然后完成操作
-
难点?
- 关于Range 范围的嵌套Dom处理
- 关于undo与redo的光标位置处理
- ...
-
现有demo地址, richTextDemo