富文本 RTE 简介

相信很多人都使用过多种富文本编辑器,富文本编辑器(简称 RTE)是一种所见即所得的网页编辑器。最常见的比如论坛发帖编辑发布框、写作类网站用于编辑博客、用户交互的编辑器。

我们知道,可以让网页处于编辑状态可以通过以下三种形式

第一种:文本域。
文本域是网页中最常见的输入框,不过它只是文本框,只能输入文字,属于非所见即所得型编辑器。这种编辑器的实现原理很简单,用 textarea 元素就可以实现,假如要实现粗体、斜体、下划线、颜色字、图片的效果,只需在字的中间加上自定义标签即可,例如:<b>富文本编辑器</b><img src="https://www.google.com.hk/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png">;当然这些规则你得自己通过 js 进行定制。当 POST 提交后,再把这些标签转换为 html 标签。因为文本域本身不能有立竿见影的效果(所见即所得),并且也只是支持一些字符的输入,并不支持显示 html,因此它必然不是我们想要的。

第二种:添加 contenteditable 属性为 true 的 div。
contenteditable 这个属性连低版本的 ie 都支持,QQ 空间的心情发布框就是带有此属性的 div,因此我们可以想到用类似的方法做 RTE,但是本文介绍的这个 RTE 并不是利用 div 制作的,为什么?1. 当前文档的行为不会影响 iframe 中的元素。也就是说,当前页面定义的样式和 js 操作,对 iframe 中相同类名的元素是无效的。2. 对于文档操作,js 提供了一套现成的 execCommand 方法。倘若我们用 div 来开发 RTE,在没有现成方法的基础,那么就需要先提取选中的文字,可以参见我之前写的文字选中分享到” 简单实现,然后再对选中的文字对象做处理,处理完后再获取光标位置,最后插入到对应的位置,这当中所涉及到的方法兼容性都一塌糊涂,而且方法名都很长,谁搞谁知道。

第三种:设置了 designMode 为”on” 的 iframe。
目前用得最多、兼容性最好的还是 iframe 方式,使用 iframe 作为内容编辑区域。在网页中插入 iframe,那么 iframe 就可以看做是一个单独的文档,我们再把 iframe 的 designMode 属性设置为 on,结合 js 提供的 execCommand 方法。即可在这个文档中实现编辑、插入等设置功能,这也是本篇所要说的。

使用 iframe实现 HTML 制作 RTE

HTML 插入 iframe:

<iframe src="" frameborder="0"></iframe>

iframe 本身也是一个嵌套页面,它如何能够被编辑呢?这里有一些关键的属性,它们可以做到让 iframe 可以被编辑,换而言之,HTML 在线编辑器就是一个可编辑的 iframe。用 Javascript 把它设成可编辑:

iframe.contentWindow.document.designMode = "on";
iframe.contentWindow.document.contentEditable = true;

加粗、斜体、下划线、加链接等功能如何实现

浏览器已经提供了实现这些功能的接口 execCommand

iframe.contentWindow.document.execCommand(cmd, isDefaultShowUI, value);

这三个参数的意思分别是:

  • cmd:命令文本,要执行的命令名称。有好多,IE 的可以看这里,Firefox 的可以看这里
  • isDefaultShowUI:是否默认显示交互界面,比如加链接的时候,可以通过界面填入链接。不过这个参数存在兼容性问题,一般设为 false 将其禁用,并另外制作交互界面。在 Firefox 中若参数为 true 时可能会报错。
  • value:传入的值,参数为 null 或者字符串,表示浏览器是否应该为当前命令提供用户界面的一个布尔值和执行命令必须的一个值(如果不需要值,则传递 null)某些命令可以省略。

execCommand 的问题是,生成的代码可能不标准,比如在 IE 下,文字加粗用的是 b 标签而不是 strong 标签。

下表列出了开发中所用到的参数:

命令(第一个参数) 值(第三个参数) 说明
bold null 将选择的文字加粗
italic null 将选择的文字转换为斜体
underline null 将选择的文字加下划线
strikethrough null 将选择的文字加删除线
fontsize 1-7 将选择的文字修改为指定字体大小
justifyleft null 将选择的文字居左
justifycenter null 将选择的文字居中
justifyright null 将选择的文字居右
indent null 左缩进
outdent null 右缩进
insertorderedlist null 插入有序列表
insertunorderedlist null 插入无序列表
undo null 撤销
redo null 重复
removeformat null 将选择的文字清除格式
forecolor 颜色字符串 设置文档背景色
backcolor 颜色字符串 将选择的文字修改为指定颜色

倘若我们需要对 iframe 里的内容做特殊操作,比如预览编辑好的内容、或者切换到源码等,因为 document.execCommand 没有相关参数,那么 document.execCommand 就用不上了。此时就需要添加一些自定义方法,下表也列出了这些功能以及相关做法:

命令 说明 功能实现描述
insertimage 插入图片 通过 iframe 的 innerHTML 拼接
createlink 插入链接 通过 prompt 输入,插入到文档
inserttime 插入时间 获取时间对象,插入到文档
insertface 插入表情 通过 iframe 的 innerHTML 拼接
insertsymbol 插入特殊符号 通过 iframe 的 innerHTML 拼接
inserttable 插入表格 生成 table 结构,通过 iframe 的 innerHTML 拼接
save 保存当前内容 execCommand 中的 savesa 命令,其他浏览器给出提示
new 新建编辑内容 confirm 用户,确认后清空 iframe 的 innerHTML
preview 预览当前内容 将 iframe 内容复制给弹层,显示弹层
html 切换视图与源码 textarea 获取 iframe 的 html 源码,控制显示隐藏

我们对相应标签设置一个 data-command 或 self-command 属性,属性值为以上两个表格列出的命令。然后当用户点击这些标签时,获取 data-command(self-command)的属性值,执行相应的操作即可。

交互问题

用户不可能总是在编辑器中输入,比如加粗、插入图片等功能是通过按钮操作的。假设用户要加粗一段选中的文字,当他按了加粗按钮后,选区以及焦点也会跟着跑到那去,因此选区(选中的文字)丢失,操作也就无法完成;同理,插入图片时插入位置也会丢失。

也就是说,要保存最后出现在编辑器中的选区。我采取的方案是,当焦点在编辑器内的时候,用一个定时器(setInterval)定时获取当前选区。选区编程平时很少用,做起来也有很多兼容性问题,主要是参考微软的 MSDNTextRangeControlRange)和 Mozilla 的 MDCRangeSelection)了。

回车问题

在 IE 下,按回车是换段落,生成 <p>,但在 Firefox 下是换行,生成的是 < br>。要解决这个问题,就要监听 keydown 事件,如果检测到按键是回车,就插入 “<p></p>”。

获取标准的代码

如何获取编辑的内容呢?这个问题很简单,只要获取 iframe 页面 body 中的 innerHTML 就可以了:

var content = iframe.contentWindow.document.body.innerHTML;

然而,IE 下的 innerHTML 非常不标准:标签名是大写的,属性没有引号包起来,单标签也没有结束符…… 即便是 Firefox 下获取的代码,也有少量瑕疵。这个时候就要用正则表达式对代码进行标准化处理。

浏览器兼容性的处理

1. 低版本 ie 不支持新建窗口预览。
通常我们在做代码效果预览时,都是采用新建空白窗口,然后将文本域内容写入到新窗口中,即:

window.open().document.write(iframeDocument.body.innerHTML);

但是在低版本 ie 不支持此方法,于是采用当前页面弹框形式。

2. 低版本的 ie 中,点击 iframe 以外的元素,iframe 会失去焦点,回到 iframe 第一行最开始编辑的位置。
解决方案是给选择功能的元素加 unselectable 属性,并设置为 on。

ele[i].setAttribute("unselectable" , "on");

3. 低版本 ie 在 execCommand 方法中不支持三位数的颜色。
前面提到,iframe 文档中有这么一个方法:

iframeDocument.execCommand(commandType,false,value);

其中的 val 可以是字体的大小值,也可以为颜色值等。在处理 iframe 中的颜色(文字颜色、文字背景色)时,我一开始直接用我前面写的 colorTake.js 提取颜色,因为这个 js 文件获取的颜色值默认为三位,后来发现三位数值的颜色在低版本 ie 中根本无法生效,于是修改 colorTake.js 返回六位的数值。

4. 将 iframe 保存为页面。
在开发过程中发现,只有低版本的 ie 才支持 execCommand 的 saveas 命令,所以需要对其他浏览器做提示处理。

if(!+[1,]){
    iframeDocument.execCommand("saveas",false,"document.html");
}
else{
    alert("对不起,当前浏览器出于安全机制禁止保存,你可以尝试手动保存或使用IE浏览器!");
}

最后,我们的 Web 程序中经常使用 jQuery 作为基础类库,那就把上面的代码也改造为 jQuery 吧。代码如下:

<!DOCTYPE HTML>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>富文本编辑器</title>
    <script type="text/javascript" src="jquery.min.js"></script>
    <script type="text/javascript">
        $(function () {
            $d = $("#editor")[0].contentWindow.document; // IE、FF都兼容
            $d.designMode = "on";
            $d.contentEditable = true;
            $d.open();
            $d.close();
            $("body", $d).append("<div>A</div><div>B</div><div>C</div>");

            $('#insert_img').click(function () {
                // 在iframe中插入一张图片
                var img = '<img src="' + $('#path').val() + '" />';
                $("body", $d).append(img);
            });

            $('#preview').click(function () {
                // 获取iframe的body内容,用于显示或者插入到数据库
                // alert($('#editor').contents().find('body').html()); // 以警告框形式弹出查看
                $('#preview_area').html($('#editor').contents().find('body').html());

            });
        });

    </script>

</head>

<body>

    源码显示区
    <textarea id="txtYuan" style="width: 600px; height: 200px">
    </textarea>
    <p>
        实时编辑区
        <iframe id="editor" width="600px" height="200px" style="border: solid 1px;"></iframe>
    </p>
    <input type="text" id="path" value="https://www.google.com.hk/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png" />
    <input type="button" id="insert_img" value="插入图片" />
    <input type="button" id="preview" value="预览" />
    <input type="button" id="btnYuan" value="显示源码" />
    <input type="button" id="btnB" value="加粗/正常" />
    预览区
    <p style="border: 1px dashed #ccc;" id="preview_area"></p>

</body>
</html>

效果图如下:

是不是觉得很简单呢?来用下这款 富文本编辑器 吧。