美文网首页javaScript 相关
我踩了富文本编辑的坑

我踩了富文本编辑的坑

作者: 一个笑点低的妹纸 | 来源:发表于2018-04-03 18:10 被阅读852次

    初次接触富文本编辑是在去年校招的时候,当时选了葡萄城校招编程中的一道,写一个富文本编辑器。然后,我就写了一个 demo:textEditor,实现了一些很简单的功能。最近,工作上有了富文本编辑的需求,正好趁此机会,可以好好研究一下了,有意思的同时也将寄几带入了深坑。

    WangEditor 算是目前做的比较好的开源的富文本编辑器,阅读它的源码真的是解决了我很多问题呢,感谢大神~~以下是对自己踩坑的记录,项目背景是仿网易七鱼访客端IM。

    仿网易七鱼聊天室
    一、两个主要对象

    对于富文本编辑器的操作,主要关注 2 个对象:Selection 和 Range。

    • Selection 对象代表页面中的文本选区。一般是由用户拖拽鼠标选中文字或图片等其他元素而产生。(copy)
    • Range 对象表示包含节点的文档片段,字面意思来讲表示文档中一个或多个范围。(copy)
    // 生成 Selection 对象
    window.getSelection();
    // 获得选中的文本
    window.getSelection().toString();
    // 获得 Range 对象,会有多个
    window.getSelection().getRangeAt(0);
    // 查看 Range 对象的个数
    window.getSelection().rangeCount;
    // 创建 Range 对象
    document.createRange();
    
    控制台log

    了解了这两个对象的获取,那么在操作富文本编辑器时最主要的保存选区的代码就容易理解了:

    // 保存选区(记录光标位置)
    saveRange: function() {
        const selection = window.getSelection();
        let range;
    
        if (selection.getRangeAt && selection.rangeCount) {
            range = selection.getRangeAt(0);
        } else {
            range = window.createRange();
        }
    
        this._currRange = range;
    }
    

    在富文本编辑器中进行操作时,需要实时地对选区进行保存。保存选区的作用是为了后续恢复选区。

    // 恢复选区
    restoreRange: function() {
        const selection = window.getSelection();
        selection.removeAllRanges();
        selection.addRange(this._currRange);
    }
    

    保存选区和恢复选区在富文本操作中很重要,因为有可能编辑器失去焦点时,页面的选区已经变化了(比如点击Emoji表情,这时候选区已经不在编辑器中了)。因此,在编辑器中的操作,无论是鼠标点击、键盘输入还是表情插入之后,都需要对选区进行实时保存,这样才能保证后续在正确的光标位置处进行插入。

    二、实时保存选区:键盘鼠标事件处理
    // 实时保存选区
    _saveRangeRealTime() {
        this.editor.addEventListener('keyup', (e) => this.saveRange());
        this.editor.addEventListener('click', (e) => this.saveRange());
    }
    

    WangEditor 对于鼠标操作监听了 mousedown、mouseup、mouseleave,我暂时好像没有用到这个,具体可以去参考它的代码。

    三、回车处理

    聊天室有“回车发送消息的”需求,这里需要在keydown时阻止回车默认事件,否则,在发送时会产生一个占位符。

    不阻止回车默认事件
    阻止回车默认事件
    // 按回车时的处理
    _enterKeyHandle() {
        const onEnter = this.config.onEnter;  // 回车后的回调函数
    
        this.editor.addEventListener('keydown', (e) => {
            if (e.keyCode === 13 && onEnter) {
                e.preventDefault(); // 防止回车换行
            }
        });
    
        this.editor.addEventListener('keyup', (e) => {
            if (e.keyCode === 13 && onEnter) {
                onEnter();
            }
        });
    },
    
    四、自定义快捷键换行

    如果还想实现“换行”的功能呢?(Enter?Ctrl + Enter?Alt + Enter?)

    • 像上面的代码,如果不传 onEnter 函数,那么回车就能换行;
    • 如果不想要回车换行,那么就需要自定义快捷键实现换行,比如常用的“Ctrl + Enter” 或“Alt + Enter”换行。

    进一步修改上面回车处理的代码,如下:

    // 按回车时的处理、自定义换行
    _enterKeyHandle() {
        const onEnter = this.config.onEnter;  // 回车后的回调函数
        const brKey = this.config.brKey;    // 自定义换行键:e.ctrlKey or e.altKey
    
        this.editor.addEventListener('keydown', (e) => {
            if (e.keyCode === 13 && onEnter && !e[brKey]) {
                e.preventDefault(); // 防止回车换行
            }
        });
    
        this.editor.addEventListener('keyup', (e) => {
            if (e.keyCode === 13) {
                if (e[brKey]) {
                    this.appendBr();  // 人工换行,自行实现 ☟
                } else {
                    onEnter && onEnter();
                }
            }            
        });
    }
    

    【注意】:IE 和 Firefox 实现换行时会产生换行占位符,需要特殊处理。

    正常Chome下换行输入 IE下换行输入 Firefox下换行输入
    appendBr() {
        let oBr = document.createElement('p');
        oBr.innerHTML = '<br>';
        this.editor.appendChild(oBr);
    
        //设置输入焦点
        var o = this.editor.lastChild.firstChild;
        var range = document.createRange();
        range.selectNodeContents(this.editor);
        range.collapse(false);
        range.setEndAfter(o);
        range.setStartAfter(o);
        this._currRange = range;
        this.restoreRange();
    
        // 兼容FF和IE
        if (browserType() == 'FF' || browserType() == 'IE') {
            for (var i = 0, len = this.editor.childNodes.length; i < len; i++) {
                var child = this.editor.childNodes[i];
                if (child.innerHTML == '<br>' || child.innerHTML == '<br></br>') {
                    child.innerHTML = '';
                }
            }
        }
    }
    

    所以,这段兼容的代码,就是人为的对 DOM 进行了操作。。╮(╯▽╰)╭

    五、清空处理

    Firefox 中按 DEL 键删除时,会产生 <br> 占位符,因此需要判断处理一下。

    Firefox下删除内容之后产生 <br>
    // 清空时的处理
    _clearHandle() {
        this.editor.addEventListener('keyup', (e) => {
            let txtHtml = this.editor.innerHTML;
            if (e.keyCode === 8 && (txtHtml === '' || txtHtml === '<br>')) {    // 最后剩下一个空行,就不再删除了
                this.editor.innerHTML = ''; 
            }
        });
    }
    

    注意,这里需要监听删除键的 keyup 事件,这样才能获得正确的编辑器内的文本,如果在 keydown 时监听,就会滞后一步。

    六、粘贴处理

    实现粘贴功能,也需要阻止浏览器的默认事件。

    不阻止浏览器默认事件
    // 粘贴处理
    _pasteHandle() {
        this.editor.addEventListener('paste', (e) => {
            let plainText = event.clipboardData.getData('text/plain');
            e.preventDefault(); // 阻止默认行为,使用 execCommand 的粘贴命令
            this.insertText(plainText);
        });
    },
    
    insertText(text) {
        this.restoreRange();
        const range = this._currRange;
        
        if (document.queryCommandSupported('insertText')) {
            // W3C
            document.execCommand('insertText', false, text);
        } else if (range.insertNode) {
            // IE
            let newNode = document.createElement('div');
            newNode.innerText = text;
            range.insertNode(newNode.childNodes[0]);
            range.collapse(false);  // IE 下把光标定位到最后
        }
    }
    
    六、插入 HTML(如 Emoji 表情)
    网易七鱼表情插入

    如图,网易七鱼对 emoji 表情插入的处理方式,是构造了1个 <img src="" title="[]" alt="[]" /> 标签,我们看到的 emoji 其实就是个存储在 CDN 上的图片,也只有富文本编辑器能这么搞。

    // 插入html
    insertHTML: function(html) {
        this.restoreRange();
        const range = this._currRange;
        
        if (document.queryCommandSupported('insertHTML')) {
            // W3C
            document.execCommand('insertHtml', false, html)
        } else if (range.insertNode) {
            // IE
            let newNode = document.createElement('div');
            newNode.innerHTML = html;
            range.insertNode(newNode.childNodes[0]);
            range.collapse(false);  // IE 下把光标定位到最后
        }
    
        this.saveRange();
    } 
    

    IM 进行 websocket 通讯的时候,不能把整个 img 标签传给服务器,需要对它进行转换,如转成对应的 title([可爱]),要不然传输字节数会很大。。请叫我小太阳:)

    后续继续踩坑。。٩(๑>◡<๑)۶

    ✿✿ヽ(°▽°)ノ✿

    ☂ 参考

    相关文章

      网友评论

        本文标题:我踩了富文本编辑的坑

        本文链接:https://www.haomeiwen.com/subject/wxqtzxtx.html