初次接触富文本编辑是在去年校招的时候,当时选了葡萄城校招编程中的一道,写一个富文本编辑器。然后,我就写了一个 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();
了解了这两个对象的获取,那么在操作富文本编辑器时最主要的保存选区的代码就容易理解了:
// 保存选区(记录光标位置)
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 实现换行时会产生换行占位符,需要特殊处理。
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> 占位符,因此需要判断处理一下。
// 清空时的处理
_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([可爱]),要不然传输字节数会很大。。请叫我小太阳:)
后续继续踩坑。。٩(๑>◡<๑)۶
✿✿ヽ(°▽°)ノ✿