一文入門富文字編輯器
簡介
富文字編輯器,能夠使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>
參考資料
- Document.execCommand
- Selection
- Range