简介
富文本编辑器,能够使web
页面像word
一样,实现对文本的编辑,通常应用在一些文本处理比较多的系统中。现在业界有很多成熟的富文本编辑器,比如功能齐全啊TinyMCE、轻量高效的wangEditor、百度出品的UEditor等。富文本编辑器很多,但是却很少思考如何从零开始,实现一个富文本编辑器。本文主要简述如何从零开始,实现一个简易的富文本编辑器。
基本使用
普通的HTML
标签,能够输入的通常只是表单,表单输入的是纯文本,不带格式的内容。富文本相对于表单,能够给输入文本内容增加一些自定义内容样式,比如加粗、字体颜色、背景...。富文本的实现,主要是给HTML标签,比如div
增加一个contenteditable
属性,拥有该属性的HTML
标签,就能够对该标签里的内容,实现自定义的编辑。最简单的富文本编辑器如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app" style="width: 200px;height: 200px;background-color: antiquewhite;" contenteditable='true'></div>
</body>
</html>
基本操作
富文本类似于Word,有很多操作文本选项,比如文本的加粗、添加背景颜色、段落缩进等,使用方式是命令式的,只需要执行document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)
,其中aCommandName
是命令名称,aShowDefaultUI
一个 Boolean
, 是否展示用户界面,一般为 false
。Mozilla 没有实现。aValueArgument
,额外参数,一般为null
。
基本操作命令
以下简单列举一些富文本操作命令,下面给出一些例子的简单使用
命令 | 值 | 说明 |
---|---|---|
backcolor | 颜色字符串 | 设置文档的背景颜色 |
bold | null | 将选择的文本加粗 |
createlink | URL字符串 | 将选择的文本转换成一个链接,指向指定的URL |
indent | null | 缩进文本 |
copy | null | 将选择的文本复制到剪切板 |
cut | null | 将选择文本剪切到剪切板 |
inserthorizontalrule | null | 在插入字符处插入一个hr元素 |
Example:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
<style>
html, body{
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
#app{
display: flex;
flex-direction: column;
justify-content: flex-start;
width: calc(100% - 100px);
height: calc(100% - 100px);
padding: 50px;
}
.operator-menu{
display: flex;
justify-content: flex-start;
align-items: center;
width: 100%;
min-height: 50px;
background-color: beige;
padding: 0 10px;
}
.edit-area{
width: 100%;
min-height: 600px;
background-color: blanchedalmond;
padding: 20px;
}
.operator-menu-item{
padding: 5px 10px;
background-color: cyan;
border-radius: 10px;
cursor: pointer;
margin: 0 5px;
}
</style>
</head>
<body>
<div id="app">
<div class="operator-menu">
<div class="operator-menu-item" data-fun='fontBold'>加粗</div>
<div class="operator-menu-item" data-fun='textIndent'>缩进</div>
<div class="operator-menu-item" data-fun='inserthorizontalrule'>插入分隔符</div>
<div class="operator-menu-item" data-fun='linkUrl'>链接百度</div>
</div>
<div class="edit-area" contenteditable="true"></div>
</div>
<script>
let operationItems = document.querySelector('.operator-menu')
// 事件监听采用mousedown,click事件会导致富文本编辑框失去焦点
operationItems.addEventListener('mousedown', function(e) {
let target = e.target
let funName = target.getAttribute('data-fun')
if (!window[funName]) return
window[funName]()
// 要阻止默认事件,否则富文本编辑框的选中区域会消失
e.preventDefault()
})
function fontBold () {
document.execCommand('bold')
}
function textIndent () {
document.execCommand('indent')
}
function inserthorizontalrule () {
document.execCommand('inserthorizontalrule')
}
function linkUrl () {
document.execCommand('createlink', null, 'www.baidu.com')
}
</script>
</body>
</html>
文本范围与选区
富文本中,文本范围和选区是一个非常强大的功能,借助于文本选区,我们可以对选中文本做一些自定义设置。核心是两个对象,Selection
和Range
对象。用比较官方的说法是,Selection
对象,表示用户选择的文本范围或光标的当前位置,Range
对象表示一个包含节点与文本节点的一部分的文档片段。简单来说,Selection
是指页面中,我们鼠标选中的所有区域,Range
是指页面中我们鼠标选中的单个区域,属于一对多的关系。比如,我们要获取当前页面的选区对象,可以调用var selection = window.getSelection()
,如果想要获取到第一个文本选区信息,可以调用var rang = selection.getRangeAt(0)
,获取到选区文本信息,采用range.toString()
。
文本范围与选区,一个比较经典的用法就是,富文本粘贴格式过滤。在我们往富文本编辑器中复制文本时,会保留原文本的格式,如果我们要去除复制的默认格式,只保留纯文本,该如何操作呢?
博主在处理这个问题时,首先想到的是,能不能监听粘贴事件(paste)
,在粘贴文本时,将剪切板内容替换掉。这一个里面也是有坑的,粘贴时操作剪切板是不生效的。在实现功能需求时,最初采用的是正则匹配,去除HTML标签。奈何文本格式五花八门,经常出现各种奇奇怪怪的字符,问题比较多,而且复制大文本时,页面存在性能问题,这并不是一种好的处理方式,直到后来真正理解了文本范围与选区,才发现这个设置,真香。
富文本选区的处理逻辑大致思路如下:
- 监听文本粘贴事件
- 阻止默认事件(阻止浏览器默认复制操作)
- 获取复制纯文本
- 获取页面文本选区
- 删除已选中文本选区
- 创建文本节点
- 将文本节点插入到选区中
- 将焦点移动到复制文本结尾
示例代码如下:
let $editArea = document.querySelector('.edit-area')
$editArea.addEventListener('paste', e => {
// 阻止默认的复制事件
e.preventDefault()
let txt = ''
let range = null
// 获取复制的文本
txt = e.clipboardData.getData('text/plain')
// 获取页面文本选区
range = window.getSelection().getRangeAt(0)
// 删除默认选中文本
range.deleteContents()
// 创建一个文本节点,用于替换选区文本
let pasteTxt = document.createTextNode(txt)
// 插入文本节点
range.insertNode(pasteTxt)
// 将焦点移动到复制文本结尾
range.collapse(false)
})
除此之外,还有很多操作可以借助于选区来实现,比如光标的定位、选中区域内容包裹其他样式等。
实现手动将光标定位到最后一个字符
function keepLastIndex(element) {
if (element && element.focus){
element.focus();
} else {
return
}
let range = document.createRange();
range.selectNodeContents(element);
range.collapse(false);
let sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
选中区域包裹其他样式
function addCode () {
let selection = window.getSelection()
// 暂时处理第一个选区
let range = selection.getRangeAt(0)
// 拷贝一份原始选中数据
let cloneNodes = range.cloneContents()
// 移除选区
range.deleteContents()
// 创建内容容器
let codeContainer = document.createElement('code')
codeContainer.appendChild(cloneNodes)
// 往选区内添加文本
range.insertNode(codeContainer)
}
附件
以下为测试代码
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
<!-- https://electronjs.org/docs/tutorial/security#csp-meta-tag -->
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
<style>
html, body{
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
#app{
display: flex;
flex-direction: column;
justify-content: flex-start;
width: calc(100% - 100px);
height: calc(100% - 100px);
padding: 50px;
}
.operator-menu{
display: flex;
justify-content: flex-start;
align-items: center;
width: 100%;
min-height: 50px;
background-color: beige;
padding: 0 10px;
}
.edit-area{
width: 100%;
min-height: 600px;
background-color: blanchedalmond;
padding: 20px;
}
.operator-menu-item{
padding: 5px 10px;
background-color: cyan;
border-radius: 10px;
cursor: pointer;
margin: 0 5px;
}
</style>
</head>
<body>
<div id="app">
<div class="operator-menu">
<div class="operator-menu-item" data-fun='fontBold'>加粗</div>
<div class="operator-menu-item" data-fun='textIndent'>缩进</div>
<div class="operator-menu-item" data-fun='inserthorizontalrule'>插入分隔符</div>
<div class="operator-menu-item" data-fun='linkUrl'>链接百度</div>
<div class="operator-menu-item" data-fun='addCode'>code</div>
</div>
<div class="edit-area" contenteditable="true"></div>
</div>
<script>
let operationItems = document.querySelector('.operator-menu')
// 事件监听采用mousedown,click事件会导致富文本编辑框失去焦点
operationItems.addEventListener('mousedown', function(e) {
let target = e.target
let funName = target.getAttribute('data-fun')
if (!funName) return
window[funName]()
// 要阻止默认事件,否则富文本编辑框的选中区域会消失
e.preventDefault()
})
let $editArea = document.querySelector('.edit-area')
$editArea.addEventListener('paste', e => {
// 阻止默认的复制事件
e.preventDefault()
let txt = ''
let range = null
// 获取复制的文本
txt = e.clipboardData.getData('text/plain')
// 获取页面文本选区
range = window.getSelection().getRangeAt(0)
// 删除默认选中文本
range.deleteContents()
// 创建一个文本节点,用于替换选区文本
let pasteTxt = document.createTextNode(txt)
// 插入文本节点
range.insertNode(pasteTxt)
// 将焦点移动到复制文本结尾
range.collapse(false)
keepLastIndex($editArea)
})
function fontBold () {
document.execCommand('bold')
}
function textIndent () {
document.execCommand('indent')
}
function inserthorizontalrule () {
document.execCommand('inserthorizontalrule')
}
function linkUrl () {
document.execCommand('createlink', null, 'www.baidu.com')
}
function addCode () {
let selection = window.getSelection()
// 暂时处理第一个选区
let range = selection.getRangeAt(0)
// 拷贝一份原始选中数据
let cloneNodes = range.cloneContents()
// 移除选区
range.deleteContents()
// 创建内容容器
let codeContainer = document.createElement('code')
codeContainer.appendChild(cloneNodes)
// 往选区内添加文本
range.insertNode(codeContainer)
}
function keepLastIndex(element) {
if (element && element.focus){
element.focus();
} else {
return
}
let range = document.createRange();
range.selectNodeContents(element);
range.collapse(false);
let sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
</script>
</body>
</html>