经常在做企业网站的管理系统的时候需要用到富文本编辑器,之前基本上都是直接去 npm 或者 github 上面搜找一些排名考前或者 readme 写的好的库,直接拿来用。万变不离其宗,是时候探索下本质了。
要想实现富文本需要开启“编辑”的能力,系统提供了一个 api:contenteditable 允许我们对内容进行编辑。下面是来自 MDN 的官方解释。
The contenteditable global attribute is an enumerated attribute indicating if the element should be editable by the user. If so, the browser modifies its widget to allow editing.
The attribute must take one of the following values:
- true or the empty string, which indicates that the element must be editable;
- false, which indicates that the element must not be editable.
If this attribute is not set, its default value is inherited from its parent element.
contenteditable 的值设置为 true 或者空字符串""
允许内容被编辑,false 则代表不可被编辑。
它不仅可以作用在 textarea、div、甚至是网页所见的都可以进行编辑。所以利用这点儿你可以做一些坏事情 ,比如修改教务处网页上的成绩单和绩点分数,修改天气预报的温度走势情况,反手修改某一天的温度为 66度。
想想看,你在输入框里面输入了一段文字,你点击上面的加粗按钮如何实现?MDN 告诉我们有个 api 可以满足需求。当元素进入编辑模式的时候,document 对象暴露出一个 execCommand 方法去操纵当前的可编辑区域 。看看下方 MDN 给出的解释。
When an HTML document has been switched to designMode, its document object exposes an execCommand method to run commands that manipulate the current editable region, such as form inputs or contentEditable elements.
document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)
A Boolean that is false if the command is unsupported or disabled.
aCommandName:命令名称。比如加粗、下划线、无序列表、段落、H1等等
aShowDefaultUI:布尔值。是否展示默认的样式,一般为 false
aValueArgument:一些命令所需要的额外参数,比如 insertImage 插入图片所需要的图片 url
完整的命令和各个浏览器的支持情况可以查看 MDN。
在执行 document.execCommand 的时候需要知道对谁在什么范围内执行命令。这里有一个选区的概念,也就是 Selection,用来表示用户选择的范围。(说明:用户不选中任何内容,也就是只有一闪一闪的光标的情况也算是一种特殊的选中)。
一个页面包含多个选中区域(Firefox) 支持。所以 Selection 可以看作是 Range 对象的集合。通常情况下我们一般只存在一个选中的区域,所以 document.getSelection().getRangeAt(0)
就可以拿到当前选区的信息。
Range 对象请看下图
上面说到光标也是一个特殊的选区,当 endOffset 和 startOffset 相等的时候,collapsed 属性就为 true。
通过 document.getSelection().getRangeAt(0) 就可以获取到选区的信息,那么可以将当前选区保存下来,等到需要的时候再拿出来并展示。Selection 对象还有几个开放的方法(addRange、collapse、collapseToEnd、collapseToStart)可以操纵光标(比如插入文字后光标的位置)。
动手做一个简易的富文本编辑器吧。(不想写一个 Vue 或者 React 工程,拿最简单的 html 撸一个吧)
思路:
下面贴出代码
<html>
<head>
<title>富文本title>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<style>
.commandZone {
margin: 20px;
margin-bottom: 0px;
background: burlywood;
}
.editor {
border: 1px solid gray;
margin: 0px 20px 20px 20px;
height: 300px;
}
.btn {
margin: 10px 20px;
color: black;
font-size: 20px;
line-height: 20px;
display: inline;
}
style>
head>
<body>
<div class="commandZone">
<button id="paragraphBtn" class="btn">段落button>
<select name="hstyle" id="hstyle">
<option value="1">h1option>
<option value="2">h2option>
<option value="3">h3option>
<option value="4">h4option>
<option value="5">h5option>
<option value="h6">h6option>
select>
<button id="boldBtn" class="btn">加粗button>
<button id="undoBtn" class="btn">后退button>
<button id="redoBtn" class="btn">前进button>
<button id="insertHorizontalRuleBtn" class="btn">水平线button>
<button id="insertUnorderedListBtn" class="btn">无序列表button>
<button id="createLinkBtn" class="btn">插入链接button>
<button id="insertImageBtn" class="btn">插入图片button>
div>
<div class="editor" contenteditable="true">div>
body>
<script>
var hStyle = ''
;
document.getElementById('hstyle').onchange = function () {
var optionSelectedIndex = document.getElementsByTagName('option');
hStyle = optionSelectedIndex[document.getElementById('hstyle').selectedIndex].innerHTML;
execEditorCommand('formatBlock', hStyle);
}
function execEditorCommand(name, args = null) {
document.execCommand(name, false, args);
}
document.getElementById('boldBtn').onclick = function () {
execEditorCommand('bold', null);
}
document.getElementById('insertHorizontalRuleBtn').onclick = function () {
execEditorCommand('insertHorizontalRule', null);
}
document.getElementById('insertUnorderedListBtn').onclick = function () {
execEditorCommand('insertUnorderedList', null);
}
document.getElementById('undoBtn').onclick = function () {
execEditorCommand('undo', null);
}
document.getElementById('redoBtn').onclick = function () {
execEditorCommand('redo', null);
}
document.getElementById('paragraphBtn').onclick = function () {
execEditorCommand('formatBlock', ''
);
}
document.getElementById('createLinkBtn').onclick = function () {
let link = window.prompt('请输入链接地址');
execEditorCommand('createLink', link);
}
document.getElementById('insertImageBtn').onclick = function () {
let image = window.prompt('请输入图片地址');
execEditorCommand('insertImage', image);
}
script>
html>