我踩了富文本编辑的坑

初次接触富文本编辑是在去年校招的时候,当时选了葡萄城校招编程中的一道,写一个富文本编辑器。然后,我就写了一个 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([可爱]),要不然传输字节数会很大。。请叫我小太阳:)

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

✿✿ヽ(°▽°)ノ✿

☂ 参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342