富文本编辑器初探

长期以来,做为用户我是富文本编辑器的使用者,做为前端开发,我也只是富文本插件的使用者,对内部实现细节不甚了解,使用上也只停留在调用插件提供的API,实现一些业务逻辑。最近的项目,须要开发一个简易富文本编辑器,也算是让我有机会对其一窥究竟。javascript

可编辑富文本的方式

咱们知道form表单中的input、textarea之类标签是支持内容可编辑的,但并不支持富文本,若是在这些标签里粘贴带格式的内容,会被去格式,只保留文本内容。若是想设置可编辑富文本,有两种方式:html

  • 嵌入空页面的iframe,并设置designMode属性值为“on”,这样整个文档就变得能够编辑。
<iframe
 name="richtext" src="blank.html"></iframe>

window.addEventListener("load"function (){
 frames("richtext").document.designMode = "on"
});
复制代码

须要在嵌入页面加载以后,动态设置iframe文档的designMode属性。前端

  • 使用contenteditable属性

该属性最先是由IE实现,且能够做用于页面中的任何标签,只须要在文档里给标签设置以上属性便可,无需嵌入iframe、设置js属性,因此这种方式也是目前富文本编辑器插件中更多采用的方式;java

<div class="editbox" id="richtext" contenteditable>
    <p></p>
    <p contenteditable="false"></p>   
 </>
复制代码

这样,此div元素中包含的内容就能够编辑了,固然也能够设置子元素(如第二个P元素)为不可编辑。经过js设置元素的该属性,也能够改变编辑模式:web

var elm = document.getElementById('richtext');
elm.contentEditable = 'true';

复制代码

contenteditable属性有三个可能的值:'true'表示打开编辑模式,'false'表示关闭,'inherit'表示从父元素继承此属性值。contenteditable属性兼容性较好,在主流浏览器包括IE以及目前大部分的移动端浏览器上,都获得支持。canvas

操做富文本

image

常见的富文本编辑器插件,如wangEditor、百度的UEditor,都有各类丰富的菜单区域来设置编辑内容及格式,如常规的设置标题、文字加粗、超连接等,更胜者插入图片、视频及自定义的内容结构等,而实现这些功能的API就是document.execCommand(),这个方法是与富文本编辑器进行交互的主要方式。segmentfault

语法设计模式

bool = document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)
复制代码
  • 返回值:布尔型,false表示操做不支持或未被启用
  • aCommandName,命令名称,如“bold”
  • aShowDefaultUI,是否为该命令提供用户界面,通常设为false,主流浏览器没实现该功能
  • aValueArgument,某些命令的额外参数(insertImage命令须要提供插入的图片的url)

全部支持的commands,可查阅MDN;其中,与剪贴板相关的命令(copy、cut、paste)各浏览器实现差别较大,使用时需关注浏览器差别。api

经常使用的命令举例:跨域

// 加粗
document.execCommand('bold', false, null);
// 超连接
document.execCommand('createlink', false, 'https://www.kaola.com');
// 格式化为h1标题
document.execCommand('formatblock', false, '<h1>');

复制代码

【注意】虽然全部浏览器都支持以上命令,但这些命令生成的html结构仍有差异。如bold命令,IE和Opera会使用<strong>标签包裹文本,而Safari和Chrome则使用<b>标签,firefox使用<span>

与命令相关的方法:
  • queryCommandEnabled 返回布尔值,用于检测是否能够针对当前选择的文本或当前光标位置执行某个命令;
var canBold = document.queryCommandEnabled("bold");
复制代码
  • queryCommandState 返回布尔值,用于判断当前选择的文本是否已经应用了指定的命令;
var isBold = document.queryCommandState("bold");
复制代码

可使用这个方法,来设置编辑器中加粗、斜体等按钮的状态。

  • queryCommandValue 用于获取执行某个命令时,传入的值(即execCommand()方法的第三方参数)

富文本选区 Seletion

Seletion对象是指用户选中的文本范围或鼠标的当前位置,经过window.getSelection()来获取该对象。

image

Seletion对象的属性以下:

  • anchorNode:选区起点所在节点;
  • anchorOffset:anchorNode中包含在选区内的字符数;
  • focusNode:选区终点所在节点;
  • focusOffset:focusNode中包含在选区内的字符数;
  • isCollapsed:boolean,选区的起点与终点是否重合,若是是,能够认为当前没有内容选中;
  • rangeCount:选区中包含的DOM范围的数量;
  • type:描述当前选区的类型

Selection对象的方法参阅MDN。这些方法在富文本编辑器插件里都是颇有用的方法,好比控制光标的方法collapse()、collapseToEnd()、collapseToStart(),能够设置插入内容以后光标的位置; 获取选区包含的文本的方法toString()getRangeAt(index)方法返回索引对应的选区中的DOM范围,即range对象

来看一个例子:

// 获取选区内容的位置
function  getSelPos () {
    let sel = window.getSelection()
    let rg = sel.getRangeAt(0)
    let elmRect = rg.getClientRects()[0]
    let editorRect = $('.j-editor')[0].getBoundingClientRect() // 编辑器容器
    let pos = {}
    if (elmRect) {
      // 选区内容居中位置距容器的左距离
      pos.x = elmRect.left - editorRect.left + elmRect.width / 2 
      pos.y = elmRect.top - editorRect.top
    }
    return pos
}

复制代码

上述方法,能够获取当前选区相对于编辑器容器的位置,能够用来设置在选区附近出现的工具条等。 想实时监测选区的变化,能够监听onselectionchange事件

// 高频事件,作好节流
document.onselectionchange = _.debounce(this.onSelect, 100)
复制代码

处理paste内容

若是往富文本编辑器里粘贴内容,是会把内容的样式也粘贴进来的,浏览器自动会把应用到某个标签的样式内联到此标签的style属性。但更多的时候咱们只是须要保留里面的部分格式,须要针对剪贴板中的内容进行过滤、格式化以及特定内容保留等。

editorElem.on('paste', event => {
        event.preventDefault();
        let clipboardData = event.clipboardData || event.originalEvent && event.originalEvent.clipboardData || {};
        let text = clipboardData.getData('text/plain');
        let html = clipboardData.getData('text/html');
    })

复制代码

经过侦听paste事件,能获取到事件对象上的clipboardData对象,获取粘贴的内容,能够经过getData方法获取剪切版上的纯文本或html结构。 有了html结构,就能够转成dom对象,针对处理了。默认状况下,剪贴板中的

下面重点说下对剪贴板中图片的处理。若是剪贴板中的网页元素包含图片,即img标签,若是直接粘贴到编辑器,该图片的连接地址是原网页所在的图片地址,这里就要考虑到,若是外链别人网站的图片,就有可能有朝一日这个图片不可用,因此仍是要放到自家的服务器上才放心。这里就涉及到,已知一张图片的可访问的连接地址,如何把该图片上传到本身的服务器上呢?

不考虑兼容性,给出一种可行的方案:经过canvas画布获取图片的数据,并将数据转为blob对象并进行上传。步骤以下:

  1. new一个Image对象,设置src属性为已知图片的url;
  2. 在图片对象的onload事件里,建立canvas画布,经过其toDataURL方法获取图片数据;
  3. 将图片数据转为一个blob对象,并调用图片上传接口上传该blob对象;
// 根据图片的url,上传图片
export function uploadImgWithUrl(imgUrl,  editor) {
    /**
     * 数据转blob对象
     */
    function dataToBlob(data) {
        var bytes = void 0;
        bytes = data.split(",")[0].indexOf("base64") >= 0 ? window.atob(data.split(",")[1]) : unescape(data.split(",")[1]);
        var paramType = data.split(",")[0].split(":")[1].split(";")[0];
        var uArr = new Uint8Array(bytes.length);
        for (let i=0; i < bytes.length; i++) {
            uArr[i] = bytes.charCodeAt(i);
        }
        return new Blob([uArr], {
            type: paramType,
            name: 'blob.png'
        });
    }

    var options = this;
    return new Promise(function(resolve, reject) {
        var img = new Image;
        img.setAttribute("crossOrigin", "anonymous");

        img.onload = function() {
            var canvas = document.createElement("canvas");
            canvas.width = img.width;
            canvas.height = img.height;
            canvas.getContext("2d").drawImage(img, 0, 0);
            var data = canvas.toDataURL("image/png");
            var blob = dataToBlob(data);
            
            blob.name = 'blob.jpg'
            // 调用编辑器的上传图片接口 or 也可自行实现一个图片上传方法
            editor.uploadImg.uploadImg([blob], function(result){
                if (result && result.body) {
                    var link = result.body.imageUrlList || [];
                    var item = {
                        resourceType: 'image',
                        imageUrl: link[0]
                    }
                    resolve(item);
                }
            })
        };
        img.onerror = function() {
            reject();
            Message.error('图片不容许跨域访问,请手动下载后添加')
        };
        imgUrl = -1 !== imgUrl.indexOf("?") ? imgUrl + "&time=" + (new Date).getTime() : imgUrl + "?time=" + (new Date).getTime();
        img.src = imgUrl;
    });
}

复制代码

以上是将在线的图片上传到服务器的一种解决方法,也在项目中进行了实践。对于剪贴板内存中的图片内容,能够经过getAsFile()方法来获取进而上传:

// 处理内存中的图片
if (clipboardData.items[0]) {
    let item = clipboardData.items[0]
    let type = item.type;
    let regResult = type.match(/image\/(.+)/)
    if (regResult) {
        let blob = item.getAsFile();
        // 调用编辑器的通用上传接口
        editor.uploadImg.uploadImg([blob])
    }
}

复制代码

最后

以上,只算上对富文本编辑器的基本知识点进行了初步的梳理,若是想本身造轮子,撸一个编辑器出来,须要解决的问题还有不少,能够看下知乎上的讨论为何都说富文本编辑器是天坑?,里面提到实现一个使人满意的编辑器须要各类填坑,以及良好的设计模式,路漫漫其修远兮……


参考文献

by lzf

尽可能关注网易考拉前端团队微信公众号

image
相关文章
相关标签/搜索