我踩了富文本编辑的坑

93 篇文章 2 订阅

初次接触富文本编辑是在去年校招的时候,当时选了葡萄城校招编程中的一道,写一个富文本编辑器。然后,我就写了一个 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.jianshu.com/p/50c433ec1c32

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值