我踩了富文本編輯的坑

初次接觸富文本編輯是在去年校招的時(shí)候讨跟,當(dāng)時(shí)選了葡萄城校招編程中的一道,寫(xiě)一個(gè)富文本編輯器鄙煤。然后晾匠,我就寫(xiě)了一個(gè) demo:textEditor,實(shí)現(xiàn)了一些很簡(jiǎn)單的功能梯刚。最近凉馆,工作上有了富文本編輯的需求,正好趁此機(jī)會(huì)亡资,可以好好研究一下了澜共,有意思的同時(shí)也將寄幾帶入了深坑。

WangEditor 算是目前做的比較好的開(kāi)源的富文本編輯器沟于,閱讀它的源碼真的是解決了我很多問(wèn)題呢咳胃,感謝大神~~以下是對(duì)自己踩坑的記錄,項(xiàng)目背景是仿網(wǎng)易七魚(yú)訪客端IM旷太。

仿網(wǎng)易七魚(yú)聊天室
一展懈、兩個(gè)主要對(duì)象

對(duì)于富文本編輯器的操作销睁,主要關(guān)注 2 個(gè)對(duì)象:Selection 和 Range。

  • Selection 對(duì)象代表頁(yè)面中的文本選區(qū)存崖。一般是由用戶拖拽鼠標(biāo)選中文字或圖片等其他元素而產(chǎn)生冻记。(copy)
  • Range 對(duì)象表示包含節(jié)點(diǎn)的文檔片段,字面意思來(lái)講表示文檔中一個(gè)或多個(gè)范圍来惧。(copy)
// 生成 Selection 對(duì)象
window.getSelection();
// 獲得選中的文本
window.getSelection().toString();
// 獲得 Range 對(duì)象冗栗,會(huì)有多個(gè)
window.getSelection().getRangeAt(0);
// 查看 Range 對(duì)象的個(gè)數(shù)
window.getSelection().rangeCount;
// 創(chuàng)建 Range 對(duì)象
document.createRange();
控制臺(tái)log

了解了這兩個(gè)對(duì)象的獲取,那么在操作富文本編輯器時(shí)最主要的保存選區(qū)的代碼就容易理解了:

// 保存選區(qū)(記錄光標(biāo)位置)
saveRange: function() {
    const selection = window.getSelection();
    let range;

    if (selection.getRangeAt && selection.rangeCount) {
        range = selection.getRangeAt(0);
    } else {
        range = window.createRange();
    }

    this._currRange = range;
}

在富文本編輯器中進(jìn)行操作時(shí)供搀,需要實(shí)時(shí)地對(duì)選區(qū)進(jìn)行保存隅居。保存選區(qū)的作用是為了后續(xù)恢復(fù)選區(qū)。

// 恢復(fù)選區(qū)
restoreRange: function() {
    const selection = window.getSelection();
    selection.removeAllRanges();
    selection.addRange(this._currRange);
}

保存選區(qū)和恢復(fù)選區(qū)在富文本操作中很重要葛虐,因?yàn)橛锌赡芫庉嬈魇ソ裹c(diǎn)時(shí)胎源,頁(yè)面的選區(qū)已經(jīng)變化了(比如點(diǎn)擊Emoji表情,這時(shí)候選區(qū)已經(jīng)不在編輯器中了)屿脐。因此涕蚤,在編輯器中的操作,無(wú)論是鼠標(biāo)點(diǎn)擊的诵、鍵盤(pán)輸入還是表情插入之后万栅,都需要對(duì)選區(qū)進(jìn)行實(shí)時(shí)保存,這樣才能保證后續(xù)在正確的光標(biāo)位置處進(jìn)行插入西疤。

二烦粒、實(shí)時(shí)保存選區(qū):鍵盤(pán)鼠標(biāo)事件處理
// 實(shí)時(shí)保存選區(qū)
_saveRangeRealTime() {
    this.editor.addEventListener('keyup', (e) => this.saveRange());
    this.editor.addEventListener('click', (e) => this.saveRange());
}

WangEditor 對(duì)于鼠標(biāo)操作監(jiān)聽(tīng)了 mousedown、mouseup瘪阁、mouseleave撒遣,我暫時(shí)好像沒(méi)有用到這個(gè),具體可以去參考它的代碼管跺。

三义黎、回車處理

聊天室有“回車發(fā)送消息的”需求,這里需要在keydown時(shí)阻止回車默認(rèn)事件豁跑,否則廉涕,在發(fā)送時(shí)會(huì)產(chǎn)生一個(gè)占位符。

不阻止回車默認(rèn)事件

阻止回車默認(rèn)事件
// 按回車時(shí)的處理
_enterKeyHandle() {
    const onEnter = this.config.onEnter;  // 回車后的回調(diào)函數(shù)

    this.editor.addEventListener('keydown', (e) => {
        if (e.keyCode === 13 && onEnter) {
            e.preventDefault(); // 防止回車換行
        }
    });

    this.editor.addEventListener('keyup', (e) => {
        if (e.keyCode === 13 && onEnter) {
            onEnter();
        }
    });
},
四艇拍、自定義快捷鍵換行

如果還想實(shí)現(xiàn)“換行”的功能呢狐蜕?(Enter?Ctrl + Enter卸夕?Alt + Enter层释?)

  • 像上面的代碼,如果不傳 onEnter 函數(shù)快集,那么回車就能換行贡羔;
  • 如果不想要回車換行廉白,那么就需要自定義快捷鍵實(shí)現(xiàn)換行,比如常用的“Ctrl + Enter” 或“Alt + Enter”換行乖寒。

進(jìn)一步修改上面回車處理的代碼猴蹂,如下:

// 按回車時(shí)的處理、自定義換行
_enterKeyHandle() {
    const onEnter = this.config.onEnter;  // 回車后的回調(diào)函數(shù)
    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();  // 人工換行楣嘁,自行實(shí)現(xiàn) ?
            } else {
                onEnter && onEnter();
            }
        }            
    });
}

【注意】:IE 和 Firefox 實(shí)現(xiàn)換行時(shí)會(huì)產(chǎn)生換行占位符磅轻,需要特殊處理。

正常Chome下?lián)Q行輸入
IE下?lián)Q行輸入
Firefox下?lián)Q行輸入
appendBr() {
    let oBr = document.createElement('p');
    oBr.innerHTML = '<br>';
    this.editor.appendChild(oBr);

    //設(shè)置輸入焦點(diǎn)
    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 = '';
            }
        }
    }
}

所以逐虚,這段兼容的代碼聋溜,就是人為的對(duì) DOM 進(jìn)行了操作。叭爱。╮(╯▽╰)╭

五勤婚、清空處理

Firefox 中按 DEL 鍵刪除時(shí),會(huì)產(chǎn)生 <br> 占位符涤伐,因此需要判斷處理一下。

Firefox下刪除內(nèi)容之后產(chǎn)生 <br>
// 清空時(shí)的處理
_clearHandle() {
    this.editor.addEventListener('keyup', (e) => {
        let txtHtml = this.editor.innerHTML;
        if (e.keyCode === 8 && (txtHtml === '' || txtHtml === '<br>')) {    // 最后剩下一個(gè)空行缨称,就不再刪除了
            this.editor.innerHTML = ''; 
        }
    });
}

注意凝果,這里需要監(jiān)聽(tīng)刪除鍵的 keyup 事件,這樣才能獲得正確的編輯器內(nèi)的文本睦尽,如果在 keydown 時(shí)監(jiān)聽(tīng)器净,就會(huì)滯后一步。

六当凡、粘貼處理

實(shí)現(xiàn)粘貼功能山害,也需要阻止瀏覽器的默認(rèn)事件。

不阻止瀏覽器默認(rèn)事件
// 粘貼處理
_pasteHandle() {
    this.editor.addEventListener('paste', (e) => {
        let plainText = event.clipboardData.getData('text/plain');
        e.preventDefault(); // 阻止默認(rèn)行為沿量,使用 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 下把光標(biāo)定位到最后
    }
}
六浪慌、插入 HTML(如 Emoji 表情)
網(wǎng)易七魚(yú)表情插入

如圖,網(wǎng)易七魚(yú)對(duì) emoji 表情插入的處理方式朴则,是構(gòu)造了1個(gè) <img src="" title="[]" alt="[]" /> 標(biāo)簽权纤,我們看到的 emoji 其實(shí)就是個(gè)存儲(chǔ)在 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 下把光標(biāo)定位到最后
    }

    this.saveRange();
} 

IM 進(jìn)行 websocket 通訊的時(shí)候汹想,不能把整個(gè) img 標(biāo)簽傳給服務(wù)器,需要對(duì)它進(jìn)行轉(zhuǎn)換撤蚊,如轉(zhuǎn)成對(duì)應(yīng)的 title([可愛(ài)])古掏,要不然傳輸字節(jié)數(shù)會(huì)很大。侦啸。請(qǐng)叫我小太陽(yáng):)

后續(xù)繼續(xù)踩坑槽唾。丧枪。?(?>?<?)?

??ヽ(°▽°)ノ?

? 參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末挂绰,一起剝皮案震驚了整個(gè)濱河市屎篱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌葵蒂,老刑警劉巖交播,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異践付,居然都是意外死亡秦士,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)永高,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)隧土,“玉大人,你說(shuō)我怎么就攤上這事命爬〔芸” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵饲宛,是天一觀的道長(zhǎng)皆愉。 經(jīng)常有香客問(wèn)我,道長(zhǎng)艇抠,這世上最難降的妖魔是什么幕庐? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮家淤,結(jié)果婚禮上异剥,老公的妹妹穿的比我還像新娘。我一直安慰自己媒鼓,他們只是感情好届吁,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著绿鸣,像睡著了一般疚沐。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上潮模,一...
    開(kāi)封第一講書(shū)人閱讀 49,144評(píng)論 1 285
  • 那天亮蛔,我揣著相機(jī)與錄音,去河邊找鬼擎厢。 笑死究流,一個(gè)胖子當(dāng)著我的面吹牛辣吃,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播芬探,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼神得,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了偷仿?” 一聲冷哼從身側(cè)響起哩簿,我...
    開(kāi)封第一講書(shū)人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎酝静,沒(méi)想到半個(gè)月后节榜,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡别智,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年宗苍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片薄榛。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡讳窟,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出敞恋,到底是詐尸還是另有隱情挪钓,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布耳舅,位于F島的核電站,受9級(jí)特大地震影響倚评,放射性物質(zhì)發(fā)生泄漏浦徊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一天梧、第九天 我趴在偏房一處隱蔽的房頂上張望盔性。 院中可真熱鬧,春花似錦呢岗、人聲如沸冕香。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)悉尾。三九已至,卻和暖如春挫酿,著一層夾襖步出監(jiān)牢的瞬間构眯,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工早龟, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留惫霸,地道東北人猫缭。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像壹店,于是被迫代替她去往敵國(guó)和親猜丹。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345