公司要作一个笔记模块,须要用到富文本编辑器。以前有耳闻富文本编辑器是天坑。知乎-为何说富文本编辑器是个天坑? 在试过了市面上主流的编辑器后,发现或多或少都不符合要求。主要有如下问题:javascript
还好开发时间比较富足,因而决定在vue-html5-editor基础上二次开发,最后完成上线的做品,呼唤star✨ 🙋 Github:my-vue-editorhtml
web端实现富文本编辑器主要有2个套路:前端
contenteditable
属性结合document.execCommand API实现,好比国外的CKEditor、百度的UEditor、优秀的后起之秀wangEditor。selection
、视图渲染等一切。好比Google Doc、有道云笔记、基于electron
开发的VS Code。这里咱们很理智的选择了第一种实现方式。先简单介绍下编辑器很重要的几个概念:vue
Range
翻译过来是范围,幅度的意思,与数学上的“区间”这以概念相似。浏览器提供的Range
对象用来描述DOM树中的一段连续的范围。html5
startContainer
,startOffset
描述Range
的起始处,endContainer
,endOffset
描述Range
的结尾处。当一个Range
的起始处和结尾处是同一个位置时,该Range
就处于collapsed
状态。java
Selection
(选区)管理整个页面当前的Range
及Range
的绘制。当Selection
中的Range
处于collapsed
状态时,便是平常所说的光标。光标实际上是Selection
的一种特殊状态。node
浏览器原生为咱们提供了一些对Range
内节点进行富文本操做的方法,这些方法都是经过document.execCommand
调用。git
bool = document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)
复制代码
好比github
// 向当前插入点插入一个p标签。
document.execCommand('insertHTML', false, '<p></p>')
// 将框选部分字体变为绿色,若是是collapsep状态则接下来输入的文字为绿色
document.execCommand('foreColor', false, '#00ff00')
复制代码
咱们编辑器的架都是围绕这两个概念展开的:web
Range
对象,并在须要时能够调用的机制。document.execCommand
是用来操纵选区HTML结构的,可是原生提供的方法的逻辑大多数都不彻底符合咱们的须要,或者存在兼容性问题。因此咱们封装咱们本身的构造函数Command
用来操纵富文本,不一样的按钮点击后就会实例化相应的Command
并执行相关操做。对于第一点,只须要定义一个保存,一个设置方法。
// 保存当前Range
function saveCurrentRange () {
// 获取selection对象
const selection = window.getSelection ? window.getSelection() : document.getSelection()
if (!selection.rangeCount) {
return
}
const content = this.$refs.content
for (let i = 0; i < selection.rangeCount; i++) {
// 从selection中获取第一个Range对象
const range = selection.getRangeAt(0)
let start = range.startContainer
let end = range.endContainer
// 兼容IE11 node.contains(textNode) 永远 return false的bug
start = start.nodeType === Node.TEXT_NODE ? start.parentNode : start
end = end.nodeType === Node.TEXT_NODE ? end.parentNode : end
if (content.contains(start) && content.contains(end)) {
// Range对象被保存在this.range
this.range = range
break
}
}
}
// 设置Range对象
function restoreSelection () {
// 首先获取selection对象并清除当前的Range
const selection = window.getSelection ? window.getSelection() : document.getSelection()
selection.removeAllRanges()
// 从this.range中得到保存的Range设置为Selection的Range对象
if (this.range) {
selection.addRange(this.range)
} else {
// 若是以前没有保存Range则新建一个
const content = this.$refs.content
const row = RH.prototype.newRow({br: true})
const range = document.createRange()
content.appendChild(row)
range.setStart(row, 0)
range.setEnd(row, 0)
selection.addRange(range)
this.range = range
}
}
复制代码
有了这两个方法,咱们只须要为编辑器的内容区域注册mouseup
keyup
mouseout
事件监听来实时执行saveCurrentRange
,当点击按钮后在实例化Command
前执行restoreSelection
。
对于第二点,封装execCommand
方法很好理解,好比我要实现"缩进indent"的功能,document.execCommand
就提供了indent
这个参数能够直接使用,当Range处于ul>li,中执行indent
会让ul嵌套ul,变成ul>ul>li,多个缩进就执行多个嵌套。这知足咱们的须要。
// 缩进前
<ul>
<li>当前光标位置</li>
</ul>
// 缩进后
<ul>
<ul>
<li>当前光标位置</li>
</ul>
</ul>
复制代码
可是当Range处于通常的块级元素中,执行indent
会让块级元素外面嵌套blockquote
元素,咱们想经过在块级元素上增长margin-left
来处理通常块级元素的缩进。
// 缩进前
<p>当前光标位置</p>
// 缩进后
<blockquote>
<p>当前光标位置</p>
</blockquote>
// 咱们但愿的状况
<p style='margin-left: 8%;'>当前光标位置</p>
复制代码
咱们只须要封装execCommand
方法,当其参数为indent
时,执行对应封装好的indent
方法,判断Range
是处于列表元素仍是其余块级元素中分别对待就行。 这里之因此要采用构造函数而不是普通函数的形式,是由于全部原生的execCommand
方法,当执行时浏览器内部会对该contenteditable区域维护一个undo栈和一个redo栈,使得每个修改行为能够撤销和重作。
咱们封装的方法覆写了原生的方法,就会破坏undo/redo栈的连续性,致使撤销和重作出错或失效。因此咱们须要在每一个Command
实例上保存执行前编辑器区域的DOM结构(快照)和执行后编辑器区域的DOM结构(快照),并把这个实例推入相应的undo/redo栈。当咱们执行撤销和重作操做时只须要从相应的栈中取出保存的快照恢复到内容区域便可。 因此你发现啦,undo
和redo
也是两个须要重写的Command
。
到这里一个富文本编辑器的雏形就出来了,咱们只须要在这个基础上不断完善咱们的Command
,再处理须要过滤的样式、多端数据结构同步、各类浏览器的兼容性等一个又一个坑就能作出功能丰富的编辑器啦。👏👏👏😄