高级JavaScript。如何用JavaScript手撸一个富文本编辑器?

148 阅读8分钟

要素过多建议收藏

- 富文本编辑

基本的技术就是在空白 HTML 文件中嵌入一个

iframe。通过 designMode 属性,可以将这个空白文档变成可以编辑的,实际编辑的则是元素

的 HTML。designMode 属性有两个可能的值:"off"(默认值)和"on"。设置为"on"时,整个文档

都会变成可以编辑的(显示插入光标),从而可以像使用文字处理程序一样编辑文本,通过键盘将文本

标记为粗体、斜体,等等。

作为 iframe 源的是一个非常简单的空白 HTML 页面。下面是一个例子:

<!DOCTYPE html>
<html>
	<head>
		<title>Blank Page for Rich Text Editing</title>
	</head>
	<body>
	</body>
</html>

这个页面会像其他任何页面一样加载到 iframe 里。为了可以编辑,必须将文档的 designMode 属

性设置为"on"。不过,只有在文档完全加载之后才可以设置。在这个包含页面内,需要使用 onload

事件处理程序在适当时机设置 designMode,如下面的例子所示:

<iframe name="richedit" style="height: 100px; width: 100px"></iframe> 
<script> 
 window.addEventListener("load", () => { 
 frames["richedit"].document.designMode = "on"; 
 }); 
</script>

以上代码加载之后,可以在页面上看到一个类似文本框的区域。这个框的样式具有网页默认样式,

不过可以通过 CSS 调整。

- 使用 contenteditable

还有一种处理富文本的方式,也是 IE 最早实现的,即指定 contenteditable 属性。可以给页面

中的任何元素指定 contenteditable 属性,然后该元素会立即被用户编辑。这种方式更受欢迎,因为

不需要额外的 iframe、空页面和 JavaScript,只给元素添加一个 contenteditable 属性即可,比如:

元素中包含的任何文本都会自动被编辑,元素本身类似于元素。通过设置

contentEditable 属性,也可以随时切换元素的可编辑状态:

let div = document.getElementById("richedit");

richedit.contentEditable = "true";

contentEditable 属性有 3 个可能的值:"true"表示开启,"false"表示关闭,"inherit"表示

继承父元素的设置(因为在 contenteditable 元素内部会创建和删除元素)。IE、Firefox、Chrome、

Safari 和 Opera 及所有主流移动浏览器都支持 contentEditable 属性。

- 与富文本交互

与富文本编辑器交互的主要方法是使用 document.execCommand()。这个方法在文档上执行既定

的命令,可以实现大多数格式化任务。document.execCommand()可以接收 3 个参数:要执行的命令、

表示浏览器是否为命令提供用户界面的布尔值和执行命令必需的值(如果不需要则为 null)。为跨浏览

器兼容,第二个参数应该始终为 false,因为 Firefox 会在其为 true 时抛出错误。

不同浏览器支持的命令也不一样。下表列出了最常用的命令。

​编辑

​编辑

剪贴板相关的命令与浏览器关系密切。虽然这些命令并不都可以通过 document.execCommand()

使用,但相应的键盘快捷键都是可以用的。

这些命令可以用于修改内嵌窗格(iframe)中富文本区域的外观,如下面的例子所示:

// 在内嵌窗格中切换粗体文本样式

frames["richedit"].document.execCommand("bold", false, null);

// 在内嵌窗格中切换斜体文本样式

frames["richedit"].document.execCommand("italic", false, null);

// 在内嵌窗格中创建指向 www.wrox.com 的链接

frames["richedit"].document.execCommand("createlink", false,

"www.wrox.com");

// 在内嵌窗格中为内容添加

标签

frames["richedit"].document.execCommand("formatblock", false, "

");

同样的方法也可以用于页面中添加了 contenteditable 属性的元素,只不过要使用当前窗口而不

是内嵌窗格中的 document 对象:

// 切换粗体文本样式

document.execCommand("bold", false, null);

// 切换斜体文本样式

document.execCommand("italic", false, null);

// 创建指向 www.wrox.com 的链接

document.execCommand("createlink", false, "www.wrox.com");

// 为内容添加

标签

document.execCommand("formatblock", false, "

");

注意,即使命令是所有浏览器都支持的,命令生成的 HTML 通常差别也很大。例如,为选中文本

应用 bold 命令在 IE 和 Opera 中会使用标签,在 Safari 和 Chrome 中会使用标签,而在Firefox 中会使用标签。在富文本编辑中,不能依赖浏览器生成的 HTML,因为命令实现和格式 转换都是通过 innerHTML 完成的。

还有与命令相关的其他一些方法。第一个方法是 queryCommandEnabled(),此方法用于确定对当

前选中文本或光标所在位置是否可以执行相关命令。它只接收一个参数,即要检查的命令名。如果可编 辑区可以执行该命令就返回 true,否则返回 false。来看下面的例子:

let result = frames["richedit"].document.queryCommandEnabled("bold");

以上代码在当前选区可以执行"bold"命令时返回 true。不过要注意,queryCommandEnabled()

返回 true 并不代表允许执行相关命令,只代表当前选区适合执行相关命令。在 Firefox 中,

queryCommandEnabled("cut")即使默认不允许剪切也会返回 true。

另一个方法 queryCommandState()用于确定相关命令是否应用到了当前文本选区。例如,要确定

当前选区的文本是否为粗体,可以这样:

let isBold = frames["richedit"].document.queryCommandState("bold");

如果之前给文本选区应用过"bold"命令,则以上代码返回 true。全功能富文本编辑器可以利用这

个方法更新粗体、斜体等按钮。

最后一个方法是 queryCommandValue(),此方法可以返回执行命令时使用的值(即前面示例的

execCommand()中的第三个参数)。如果对一段选中文本应用了值为 7 的"fontsize"命令,则如下代 码会返回 7:

let fontSize = frames["richedit"].document.queryCommandValue("fontsize");

这个方法可用于确定如何将命令应用于文本选区,从而进一步决定是否需要执行下一个命令。

- 富文件选择

在内嵌窗格中使用 getSelection()方法,可以获得富文本编辑器的选区。这个方法暴露在

document 和 window 对象上,返回表示当前选中文本的 Selection 对象。每个 Selection 对象都拥

有以下属性。

 anchorNode:选区开始的节点。

 anchorOffset:在 anchorNode 中,从开头到选区开始跳过的字符数。

 focusNode:选区结束的节点。

 focusOffset:focusNode 中包含在选区内的字符数。

 isCollapsed:布尔值,表示选区起点和终点是否在同一个地方。

 rangeCount:选区中包含的 DOM 范围数量。

Selection 的属性并没有包含很多有用的信息。好在它的以下方法提供了更多信息,并允许操作

选区。

 addRange(

range

):把给定的 DOM 范围添加到选区。

 collapse(

node, offset

):将选区折叠到给定节点中给定的文本偏移处。

 collapseToEnd():将选区折叠到终点。

 collapseToStart():将选区折叠到起点。

 containsNode(

node

):确定给定节点是否包含在选区中。

 deleteFromDocument():从文档中删除选区文本。与执行 execCommand("delete", false,

null)命令结果相同。

 extend(

node, offset

):通过将 focusNode 和 focusOffset 移动到指定值来扩展选区。

 getRangeAt(

index

):返回选区中指定索引处的 DOM 范围。

 removeAllRanges():从选区中移除所有 DOM 范围。这实际上会移除选区,因为选区中至少

要包含一个范围。

 removeRange(

range

):从选区中移除指定的 DOM 范围。

 selectAllChildren(

node

):清除选区并选择给定节点的所有子节点。

 toString():返回选区中的文本内容。

Selection 对象的这个方法极其强大,充分利用了 DOM 范围来管理选区。操纵 DOM 范围可以实

现比 execCommand()更细粒度的控制,因为可以直接对选中文本的 DOM 内容进行操作。来看下面的 例子:

let selection = frames["richedit"].getSelection(); 
// 取得选中的文本
let selectedText = selection.toString(); 
// 取得表示选区的范围
let range = selection.getRangeAt(0); 
// 高亮选中的文本
let span = frames["richedit"].document.createElement("span"); 
span.style.backgroundColor = "yellow"; 
range.surroundContents(span);

以上代码会在富文本编辑器中给选中文本添加黄色高亮背景。实现方式是在默认选区使用 DOM 范

围,用 surroundContents()方法给选中文本添加背景为黄色的标签。

getSelection()方法在 HTML5 中进行了标准化,IE9 以及 Firefox、Safari、Chrome 和 Opera 的所

有现代版本中都实现了这个方法。

IE8 及更早版本不支持 DOM 范围,不过它们允许通过专有的 selection 对象操作选中的文本。如

本章前面所讨论的,这个 selection 对象是 document 的属性。要取得富文本编辑器中选中的文本, 必须先创建一个文本范围,然后再访问其 text 属性:

let range = frames["richedit"].document.selection.createRange(); 
let selectedText = range.text;

使用 IE 文本范围执行 HTML 操作不像使用 DOM 范围那么可靠,不过也是可以做到的。要实现与

使用 DOM 范围一样的高亮效果,可以组合使用 htmlText 属性和 pasteHTML()方法:

let range = frames["richedit"].document.selection.createRange(); 
range.pasteHTML( 
 '<span style="background-color:yellow">${range.htmlText}</span>');

以上代码使用 htmlText 取得了当前选区的 HTML,然后用一个标签将其包围起来并通过

pasteHTML()再把它插入选区中。