前言
Level 0(不知道为啥从零开始)是编辑器的起始阶段,代表旧一代的编辑器的实现
Level 1 第二阶段,是在第一阶段发展过来的,有一定的先进性,也引入了主流的一些编程思想,对于富文本内容有一定的抽象
Level 2 第三阶段,完全不依赖浏览器的编辑能力,独立的实现光标和排版
搜了一下掘金,发现富文本编辑器的文章实在是太少了,于是一时兴起,准备写一个系列,从Level 0的编辑器写到Level 2的编辑器。
从Level 0到Level 2,个人理解就是将富文本编辑器的控制权一步步由浏览器控制,变成由开发者控制。
最简单的富文本编辑器
<div contenteditable></div>
利用浏览器的能力,我们便拥有了一个最简单的富文本编辑器,而且可以通过浏览器自带的能力,如document.execCommand
,达到一些加粗、斜体的目的。
本章节的内容就是去写一个简单的Level 0的富文本编辑器
一个简单的编辑器
由于简单,我们先按如下的图来划分模块
编辑器核心
首先我们来定义主类Editor
,思考一个编辑器需要哪些基础api,初步定义了【获取编辑器内容】和【设置编辑器内容】两个api。
// editor.ts
interface IEditorProps {
container: HTMLElement;
editable: boolean; // 编辑器是否可编辑
createToolbar?: (editor: Editor) => HTMLElement[];
}
class Editor {
// 编辑器主体编辑区域
private body: HTMLELement;
constructor(props: IEditorProps) {
// ...
}
// 获取富文本内容
getContent() {
return this.body.innerHTML;
}
// 设置富文本内容
setContent(html: string) {
this.body.innerHTML = html;
}
// 焦点是否在编辑器内
isFocus() {
return (
document.activeElement === this.body ||
this.body!.contains(document.activeElement)
);
}
// 对document.execCommand封装一层,使得如果window selection在编辑器外,调用execCommand仍然有效
execCommand(commandId, showUI?, value?) {
if (!this.isFocus()) {
this.selection.setSelection();
}
document.execCommand(commandId, showUI, value);
}
}
编辑器选区
选区表达了当前编辑器选中的文本范围或者光标所在位置,选区的表达是Range
,目前采用的是浏览器原生的表达 developer.mozilla.org/zh-CN/docs/…
// selection.ts
interface ISelectionProps {
body: HTMLElement;
}
export class Selection {
private range: Range | null = null;
constructor (private props: ISelectionProps) {
document.addEventListener('selectionchange', this._handleSelectionChange);
}
private _handleSelectionChange = () => {
// ...
}
setSelection = (range?: Range) => {
// ...
}
destroy() {
document.removeEventListener('selectionchange', this._handleSelectionChange);
}
}
工具栏
目前采用的方式是传入一个createToolbar
方法来创建工具栏,但是这可能并不是一个非常合适的方式,更好的工具栏模块设计还在进一步思考中。
当前的设计中,createToolbar
会返回一个HTMLElement
数组,那么可能有人会问,为什么不是直接传入这个HTMLElement
数组,而且传入一个函数,反而多了一层。最主要的原因是为了扩展,虽然现在工具栏使用的api基本上是document.execCommand
,但并不是完全与当前的Editor
实例无关了,在某些时候,可能需要调用到编辑器内部的api。因此需要把当前的Editor
实例传递进去。
目前内置了这三个工具栏按钮
工具栏按钮的坑
举个例子,加粗,当我们选中文字时,点击加粗按钮,预期是选中的文字加粗了。
我们的做法是在加粗按钮上绑定click
事件,然而这样做之后,就会发现文字并没有加粗。
boldBtn.addEventListener('click', () => {
document.execCommand('bold');
});
原因是当我们点击事件执行时,windows的选区已经不在选中的文本上了,此时选区如图所示
解决方案1
将editor
内保存的选区重新设置回去。但这种方案有个缺点,即选中的蓝色区域会闪一下。
const selection = window.getSelection();
if (!selection) {
return;
}
selection.removeAllRanges();
// range即原先的选区
selection.addRange(range);
解决方案2
我们录一个profile,发现在mousedown
事件之后,触发了编辑区域的blur
事件,那问题就简单了,我们阻止掉mousedown
事件的默认行为即可。
boldBtn.addEventListener('mousedown', (e) => {
e.preventDefault();
});
结语
第一篇文章到此就结束了,可以看到这个简单编辑器还是存在很多缺点。
举一些例子
-
编辑器内容完全是由浏览器控制的,不同浏览器可能还不一样,容易发生不符合预期的行为
-
选区的描述还是原生的
Range
,不符合直觉。比如说 我们正常的思维可能是我的光标在某一行的某一列,而不是现在的某个元素的某个偏移位置 -
缺少了很多功能,比如插入图片、插入视频等。
-
难以扩展,假设开发者想要在 编辑器内插入一个
iframe
,那么他就必须去改动底层的代码。