前言

印象中,富文本编辑器一直是一个高大上的玩意儿,想实现一个应该也挺难的。既然如此,那必须得学啊,这篇文章就是记录如何实现一个超简易的富文本编辑器。

实现方式

实现富文本编辑器通常来说有两种方式:

给元素设置 contenteditable=’true’

1
2
3
<div contenteditable="true">
设置 contenteditable='true' 后,该元素内的内容就是可以编辑了。
</div>

设置 iframe 的 designMode 为 ‘on’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<iframe style="width: 500px;height: 330px;"></iframe>
<script>
var ifr = document.getElementsByTagName('iframe')[0]
function printText() {
// 输出内容
console.log(ifr.contentWindow.document.body.innerHTML)
}
function init() {
var doc = ifr.contentWindow.document
doc.designMode = 'on'
doc.body.innerHTML = '<div>设置 iframe 的 designMode</div>'
}
init()
</script>

现在大多数都使用 contenteditable=’true’ 方法来实现富文本编辑器,我这里也采用这种方式

document​.exec​Command

定义:当一个HTML文档切换到设计模式时,document暴露 execCommand 方法,该方法允许运行命令来操纵可编辑内容区域的元素。
使用:document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)
参数:

  • aCommandName 命令的名称,具体可看命令列表
  • aShowDefaultUI 布尔值,是否展示用户界面,一般为 false。
  • aValueArgument 额外的命令参数,默认为 null。

可以使用 document​.query​Command​State(commandName) 来检测是否支持命令

命令列表

详细的可以查看这里,这里只列一些常见的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
backColor
修改文档的背景颜色。在styleWithCss模式下,则只影响容器元素的背景颜色。这需要一个<color> 类型的字符串值作为参数传入。注意,IE浏览器用这个设置文字的背景颜色。

bold
开启或关闭选中文字或插入点的粗体字效果。IE浏览器使用 <strong>标签,而不是<b>标签。

createLink
将选中内容创建为一个锚链接。这个命令需要一个hrefURI字符串作为参数值传入。URI必须包含至少一个字符,例如一个空格。(浏览器会创建一个空链接)

copy
拷贝当前选中内容到剪贴板。启用这个功能的条件因浏览器不同而不同,而且不同时期,其启用条件也不尽相同。使用之前请检查浏览器兼容表,以确定是否可用。

cut
剪贴当前选中的文字并复制到剪贴板。启用这个功能的条件因浏览器不同而不同,而且不同时期,其启用条件也不尽相同。使用之前请检查浏览器兼容表,以确定是否可用。

delete
删除选中部分.

fontName
在插入点或者选中文字部分修改字体名称. 需要提供一个字体名称字符串 (例如:"Arial")作为参数。

fontSize
在插入点或者选中文字部分修改字体大小. 需要提供一个HTML字体尺寸 (1-7) 作为参数。

foreColor
在插入点或者选中文字部分修改字体颜色. 需要提供一个颜色值字符串作为参数。

heading
添加一个标题标签在光标处或者所选文字上。 需要提供 aValueArgument 参数 (例如. "H1", "H6"). (IE 和 Safari不支持)

italic
在光标插入点开启或关闭斜体字。 (Internet Explorer 使用 EM 标签,而不是 I )

justifyCenter、justifyFull、justifyLeft、justifyRight
对光标插入位置或者所选内容进行文字居中、文本对齐、左对齐、右对齐。

selectAll
选中编辑区里的全部内容。

redo
重做被撤销的操作。

undo
撤销最近执行的命令。

到这里正式开始撸

HTML部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Editor</title>
</head>
<style>
* { margin: 0; padding: 0; }
html, body { height: 100%; width: 100%; }
ul { list-style: none; }
.wrap-editor{ width: 800px; height: 500px; border: 1px solid #ccc; position: fixed;
top: 0; left: 0; right: 0; bottom: 0; margin: auto; overflow: hidden; }
.wrap-editor .menu{ width: 100%; height: 35px; border-bottom: 1px solid #ccc;
cursor: pointer; font-size: 0; line-height: 35px; }
.wrap-editor .menu li{ display: inline-block; font-size: 14px; padding: 0 10px; }
.wrap-editor .editor{ width: 100%; height: 465px; outline: 0; overflow: auto; padding: 8px; box-sizing: border-box; }
</style>
<body>
<div class="wrap-editor">
<!-- 按钮区域 -->
<ul class="menu">
<li class="bold">B</li>
</ul>
<!-- 编辑区域 -->
<div class="editor" contenteditable="true">
</div>
</div>
</body>
<script src="./main.js"></script>
</html>

接下来是Js部分
一步步来,实现最基本的功能先:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var editor = document.querySelector('.editor') // 编辑器
var bold = document.querySelector('.bold') // 加粗按钮

editor.innerHTML = '请输入内容' // 给编辑器一个初始内容

// 执行命令
function execCommand(commandName, value) {
value = value ? value : null
document.execCommand(commandName, false, value)
}

bold.onclick = function (e) {
execCommand('bold')
}

这段代码十分简单,连我之前也没想到会如此简单。
接着我们点击加粗按钮,这里会遇到一个坑无论怎么操作都无法加粗选择的文字,而且编辑区还失去了焦点
这个问题也很好解释,因为我们点击的按钮的文字也是可以选择的,点击按钮时所选区域自然就清空了,当然会出现没有加粗与失去焦点的问题啦。

下面是我所知道的几种解决方法:

  1. 在加粗按钮上阻止 onmousedown 的默认事件:

    1
    <li class="bold" onmousedown="event.preventDefault();">B</li>
  2. 在按钮区域添加CSS user-select: none;
    这种方式存在兼容问题,如果不考虑低版本浏览器则可以使用。

    1
    2
    3
    .wrap-editor .menu{
    user-select: none;
    }
  3. 缓存所选区域,点击事件触发后,手动调用API重新选中文字:
    监听editor区域的onkeyup、onmouseup、onmouseout等操作,在这些操作后缓存editor中的选中区域,点击事件触发后,手动调用API重新选中文字。
    这种方法下面会讲。

缓存用户所选区域

  • 在键盘、鼠标、移出编辑区等操作后,缓存当前所选区域
  • 点击按钮后恢复所选区域、focus()编辑器,之后执行 document.execCommand 修改内容

具体用法见 Window​.get​Selection
修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
var editor = document.querySelector('.editor') // 编辑器
var bold = document.querySelector('.bold') // 加粗按钮
editor.innerHTML = '请输入内容' // 给编辑器一个初始内容

// 执行命令
function execCommand(commandName, value) {
value = value ? value : null
document.execCommand(commandName, false, value)
}

// 定义一个变量
var selectedRange

// 保存选择区域
function saveSelection() {
var sel = window.getSelection()
if (sel.getRangeAt && sel.rangeCount) {
selectedRange = sel.getRangeAt(0)
}
}

// 恢复选择区域
function restoreSelection() {
var selection = window.getSelection()
if (selectedRange) {
try {
selection.removeAllRanges()
} catch (ex) {
document.body.createTextRange().select()
document.selection.empty()
}
selection.addRange(selectedRange)
}
}

// 在editor内做了鼠标键盘操作就保存一下选择区域
editor.onmouseup = editor.onkeyup = editor.onmouseout = function () {
saveSelection();
}

bold.onclick = function (e) {
restoreSelection() // 恢复选择区域
editor.focus() // focus一下
execCommand('bold')
saveSelection() // 再保存一下选择区域
}

如何插入图片

使用 FileReader 将图片读为 base64 格式,调用对应 API 将图片插入编辑区。

1
document.execCommand('insertImage', false, imgUrl)

将图片读为 base64 格式

部分HTML:

1
<input class="inster-img" type="file" />

部分Js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 将图片读为base64格式
function imgToBase64(file) {
return new Promise((resolve, reject) => {
var reader = new FileReader()
reader.readAsDataURL(file);
reader.onload = function () {
resolve(this.result)
}
})
}

var insterImg = document.querySelector('.inster-img')
insterImg.onchange = function (e) {
restoreSelection() // 恢复选择区域
editor.focus() // focus一下

var file = e.target.files[0]
imgToBase64(file).then((val) => {
var base64Url = val
execCommand('insertImage', base64Url)
saveSelection() // 再保存一下选择区域
})
}

到此学习如何实现一个简易富文本编辑器的目的已经达到了,这里就不再写了
下面放上简易编辑器的完整的代码,写的比较随意⊙﹏⊙‖∣,将就看吧:

完整代码

参考

bootstrap-wysiwyg