初次接觸富文本編輯是在去年校招的時(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旷太。
一展懈、兩個(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();
了解了這兩個(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è)占位符。
// 按回車時(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)生換行占位符磅轻,需要特殊處理。
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> 占位符涤伐,因此需要判斷處理一下。
// 清空時(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)事件。
// 粘貼處理
_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ú)對(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ù)踩坑槽唾。丧枪。?(?>?<?)?
??ヽ(°▽°)ノ?