我踩了富文本編輯的坑

初次接觸富文本編輯是在去年校招的時候,當時選了葡萄城校招編程中的一道,寫一個富文本編輯器。然後,我就寫了一個 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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章