用vue实现markdown编辑器的思路
以下均只考虑在chrome 64版本下
目录
- 编辑器的实现方式及原理
- 从剪切板上传图片遇到的坑
编辑器的实现方式及原理
1.实现方式
- 扩展textarea
- github的issue如此实现,本博客也是
- 特点:涉及的api少且简单,能满足基本需求;编辑时样式不可调;无法做行号高亮等
- 利用div设置
contenteditable
- 复杂的富文本基本都是基于此实现,例如github的readme编辑器
- 涉及
selection
和range
对象
2.富文本需要实现的基本功能
- 选中tab缩进(向后或向前)
- 代码及代码块快捷键
- 粘贴图片上传
- 超链接
3.实现原理
**1.在textarea中 ** ,value就是一个字符串,所有的操作都是基于这个字符串去做切割与拼接。涉及到的api:
// 获取选中的字符
window.getSelection().toString()
// 获取选中字符的起始位置
dom.selectionStart
dom.selectionEnd
// 根据起始位置和方向设置字符选中
dom.setSelectionRange(start, end, selectionDirection) // 控制字符的选择的方向
在写技术型文章时,缩进是最常用的,那有了以上api,如何实现?
我们可以假设一段代码如下:
js
// \s代表空格 \n代表换行
var obj = {\n
\s\sname: 'jmingzi',\n
\s\sage: ''\n
}
首先我们要获取到选中的字符 ,即selectedString
当我们向后缩进这块代码时,实际上就是在每一行前加2个tab,利用replace
。
indent = ' '
indent + selectedString.replace(/\n/g, '\n' + indent)
当我们向前缩进时,实际上也是去掉每一行前的2个tab,同理
// 替换行首2个tab
result = selectedString.replace(/^\s{2}/, '')
// 替换剩余换行前的tab
result = result.replace(/\n\s{2}/g, '\n')
最后,我们需要保持选中的代码块,即可。
dom.setSelectionRange(
selectionStart,
selectionStart + result.length
)
需要注意的是,在vue中,利用:value
绑定的textarea值,设置setSelectionRange
时,需要等到视图更新后。
2.利用div,需要了解的知识点:
- selection和range,利用selection可以获取当前选中的字符或range对象,利用range对象可以选取节点,增加选取的范围。
// selection很显然代表选中的范围,它是一个集合
// range代表html片段
// selection 对象可以包含多个range,因为在一个页面中可以选中多个range
- execCommand
参考
- Introduction to Range
- selection 和 range 介绍
- JavaScript标准Selection操作
- 复文本框神器execCommand
- 利用 javascript 实现富文本编辑器
3.还有一种方式,就是利用textarea和div重合,在textarea中编写文本,覆盖一层div,利用
pointer-event: none;
来使textarea可以继续编辑,在渲染后与textarea重合,从而达到高亮效果,唯一需要注意的是,滚动的时候需要同步。
从剪切板上传图片遇到的坑
有2种方式监听到paste
粘贴事件:
- 全局window上
- 某一节点上
document
、body
、div
等
事件中有一个clipboardData
属性,在chrome
中:
clipboardData
包含
属性 | 类型 | 说明 |
---|---|---|
dropEffect | String | none |
effectAllowed | String | uninitialized |
files | FileList | 和input获取图片数据时的结构一样,为File对象的集合;当且仅当复制的为文件时才有值。 |
items | DataTransferItemList | 复制的对象列表,包含了3个方法add remove clear |
types | Array | ['Files', 'String'] |
File对象
DataTransferItemList对象
你会发现在控制台打印
console.log(e.clipboardData.items)
看到的永远是空的,像这样
{
length: 0,
__proto__: DataTransferItemList
}
但是又能看到一些信息,比如这样
DataTransferItemList {0: DataTransferItem, 1: DataTransferItem, 2: DataTransferItem, length: 3}
说明在某种情况下控制台取不到该属性?所以我们得循环取值(注意它并不是iterator对象,不可直接遍历),每一个item由如下组成 其中每个item包含了3个方法,我们就可以对复制的内容作更改删除操作。
针对file,我们可以直接从FileList
对象中获取,也可以用getAsFile
来获取,都是得到的File
对象。对于File对象,可以用FileReader
对象来读取
js
const rdr = new FileReader()
rdr.onloadend = () => {
this.upload(rdr.result)
}
rdr.readAsDataURL(File)
针对string,有getAsString
方法,返回的结果用回调来接收。
最后,利用vue的事件处理,我们可以很简单的组合按键事件
// 输入
@input="update"
// 粘贴
@paste="paste"
// tab键
@keydown.9.exact.prevent="tab($event, true)"
// tab缩进
@keydown.shift.exact.9.prevent="tab($event, false)"
这样,我们就可以在chrome
下实现markdown编辑器的一系列基础功能。
参考
最后实现的编辑器看起来是这样的