簡介
富文本編輯器,能夠使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>