利用 JavaScript 实现富文本编辑器
在近期的项目中,我需要开发一个兼容 PC 和移动端的富文本编辑器,并且包含一些特殊的定制功能。经过对现有 JavaScript 富文本编辑器的调研,发现大多数适用于桌面端的工具,如 UEditor,但很少有适用于移动端的。然而,由于我们不打算考虑过多的兼容性问题,所以决定自研一个轻量级的富文本编辑器。本文将介绍如何实现富文本编辑器,并分享在不同浏览器和设备之间遇到的一些问题及解决方案。
准备阶段
现代浏览器已经提供了许多 API 支持 HTML 的富文本编辑功能,因此我们不需要从零开始实现全部内容。
contenteditable="true"
首先,我们需要让一个 div
成为可编辑状态,只需添加 contenteditable="true"
属性即可。
<div contenteditable="true" id="rich-editor"></div>
在这样的 div
中插入的任何节点默认都是可编辑的。如果想插入不可编辑的节点,则需要指定插入节点的属性为 contenteditable="false"
。
光标操作
作为富文本编辑器,开发者需要能够控制光标的状态和位置信息。浏览器提供了 selection
对象和 range
对象来操作光标。
selection
对象
Selection
对象表示用户选择的文本范围或插入符号的当前位置。通常情况下,我们不会直接操作 selection
对象,而是通过操作与之对应的 range
对象来控制用户的选择区域。
获取 selection
对象的方式如下:
let selection = window.getSelection();
通过 selection
对象可以获得用户的 range
对象:
let range = selection.getRangeAt(0);
range
对象表示文档中的一段内容,包括起始节点和结束节点。操作 range
对象是控制光标的重点。
操作 range
对象
range
对象提供了一系列方法来操作光标位置,例如 setStart()
、setEnd()
、collapse()
等。通过这些方法,我们可以精确地控制光标的位置和选择范围。例如,将光标移动到指定位置:
let range = window.getSelection().getRangeAt(0);
let textEle = range.commonAncestorContainer;
range.setStart(range.startContainer, textEle.length);
range.setEnd(range.endContainer, textEle.length);
修改光标位置
有时需要强制修改光标位置,可以通过重新创建一个 range
对象,并删除所有现有的 ranges
:
function resetRange(startContainer, startOffset, endContainer, endOffset) {
let selection = window.getSelection();
selection.removeAllRanges();
let range = document.createRange();
range.setStart(startContainer, startOffset);
range.setEnd(endContainer, endOffset);
selection.addRange(range);
}
修改文本格式
实现富文本编辑器的关键在于能够修改文本格式。DOM 提供了 document.execCommand
方法,该方法允许运行命令来操作可编辑区域的内容。常见的命令包括加粗、斜体、文本颜色、列表等。
bool = document.execCommand(aCommandName, aShowDefaultUI, aValueArgument);
例如,加粗文本的命令如下:
document.execCommand('bold', false, null);
实战开始,填坑的旅途
在开发过程中,我们遇到了一些浏览器默认行为的问题,需要进行修正。
回车换行
在可编辑框中输入内容并回车换行后,浏览器生成的节点结构可能与预期不符。为了解决这个问题,可以在初始化时向 div
中插入一个 <p><br></p>
元素,并在内容清空后重新加入该结构。
插入 ul
和 ol
位置错误
调用 document.execCommand("insertUnorderedList", false, null)
来插入列表时,新的列表可能会被插入到 <p>
标签中。为此,我们需要在调用该命令前做一次修正:
function adjustList() {
let lists = document.querySelectorAll("ol, ul");
for (let i = 0; i < lists.length; i++) {
let ele = lists[i];
let parentNode = ele.parentNode;
if (parentNode.tagName === 'P' && parentNode.lastChild === parentNode.firstChild) {
parentNode.insertAdjacentElement('beforebegin', ele);
parentNode.remove();
}
}
}
插入分割线
插入 <hr>
标签后,需要在其后追加一个空的文本节点或 <p><br></p>
,并将光标定位在其中:
function insertHr() {
let range = document.createRange();
let hr = document.createElement('hr');
range.insertNode(hr);
range.setEndAfter(hr);
range.setStartAfter(hr);
}
移动端优化
在移动端,富文本编辑器的问题主要集中在光标和键盘上。
自动获取焦点
在 iOS 中,为了安全考虑,代码无法自动获取焦点,需要用户点击。可以通过以下方法在 WebView
中移除这一限制:
[self.appWebView setKeyboardDisplayRequiresUserAction:NO];
处理 paste
粘贴
在富文本编辑器中,粘贴内容时默认会保留格式。为了更好地控制粘贴内容,可以监听 paste
事件,并使用 clipboardData
对象获取剪贴板中的文本或 HTML:
let plainText = event.clipboardData.getData('text/plain');
let plainHTML = event.clipboardData.getData('text/html');
其他功能
在实际项目中,还可能遇到如待办列表、附件卡片、Markdown 切换等需求。了解了 range
对象的操作后,这些问题都可以轻松解决。