初次接觸富文本編輯是在去年校招的時候,當時選了葡萄城校招編程中的一道,寫一個富文本編輯器。然後,我就寫了一個 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([可愛]),要不然傳輸字節數會很大。。請叫我小太陽:)
後續繼續踩坑。。٩(๑>◡<๑)۶
✿✿ヽ(°▽°)ノ✿
☂ 參考
- 使用 js 選中文本——selection 和 range 介紹
- JavaScript標準Selection操作
- XML DOM - Range 對象
- 利用 javascript 實現富文本編輯器
- js按ctrl+enter換行,讓可編輯的div換行,並獲取輸入焦點光標
- 用js判斷瀏覽器類型及IE具體各版本,支持IE11及Edge瀏覽器
- 知乎div編輯器,@功能的光標定位問題
- JavaScript 獲取輸入時的光標位置及場景問題
轉自https://www.jianshu.com/p/50c433ec1c32