前端富文本基礎(chǔ)及實(shí)現(xiàn)
前端富文本基礎(chǔ)及實(shí)現(xiàn)
https://www.zoo.team/article/rich-text
前言
在日常生活中我們會(huì)經(jīng)常接觸到各種各樣的文檔格式和形式,其中富文本在文檔格式中扮演了重要角色。對(duì)于前端而言,富文本產(chǎn)品也層出不窮,其應(yīng)用也越來(lái)越廣。
這篇文章將會(huì)為大家介紹前端富文本的一些基礎(chǔ)知識(shí)以及簡(jiǎn)單的實(shí)現(xiàn)思路。
什么是富文本
純文本就是用純文字編輯器編寫(xiě),輸入什么就是什么的文檔,只包含字符。
富文本對(duì)應(yīng)的是富文本格式(Rich Text Format),即 RTF 格式,又稱(chēng)多文本格式,是由微軟公司開(kāi)發(fā)的跨平臺(tái)文檔格式。除字符外還有豐富的樣式。doc,docx,rtf,pdf 等都是富文本格式的文件類(lèi)型。
如圖所示:
前端中的富文本
前端富文本通過(guò) html 的各個(gè)元素配合各種樣式(一般是內(nèi)聯(lián)樣式)實(shí)現(xiàn)。
例如:圖片富文本編輯器中的富文本,是由紅色框中帶有語(yǔ)義化標(biāo)簽和內(nèi)聯(lián)樣式的 html 渲染實(shí)現(xiàn)的。通過(guò)富文本編輯器,即可實(shí)現(xiàn)富文本的編寫(xiě)、展示。
目前常見(jiàn)的前端富文本編輯器有 tinymce,UEeditor,draft 等。
文章下文將會(huì)講述實(shí)現(xiàn)前端富文本編輯器的一些基礎(chǔ)知識(shí)和步驟。
富文本輸入模式實(shí)現(xiàn)
實(shí)現(xiàn)前端富文本編輯器首先要實(shí)現(xiàn)文本輸入,一般常用兩種方式實(shí)現(xiàn)。
iframe
第一種方式是使用 iframe 標(biāo)簽。
在空白的 HTML 文檔中嵌入一個(gè) iframe,并將 designMode 屬性設(shè)置為 on,文檔就會(huì)變成可編輯的,實(shí)際編輯的則是 iframe 內(nèi)的 body 元素。文檔變成可編輯后,就可以像使用文字處理程序一樣編輯文本。
效果如圖:
元素設(shè)置 contenteditable
第二種方式是使用 contenteditable 屬性指定 HTML 文檔中的元素。該方式是 IE 最早實(shí)現(xiàn)的。使用方式是在一個(gè)元素上添加 contenteditable 屬性并設(shè)置為 true,然后該元素會(huì)立即被用戶編輯。
此種方式通常會(huì)和 autocapitalize(首字母自動(dòng)大寫(xiě)屬性)、spellcheck(檢查元素的拼寫(xiě)錯(cuò)誤,實(shí)驗(yàn)功能)等屬性共同使用以提升體驗(yàn)。
效果如圖:
兩者特點(diǎn)
兩種方式都可以實(shí)現(xiàn)編輯模式,并且這種編輯模式與 textarea 不同,其內(nèi)部會(huì)用塊級(jí)元素(默認(rèn)為 div 元素)做換行處理,最終體現(xiàn)在 dom 結(jié)構(gòu)中。
兩者不同的是:iframe 方式可做到樣式隔離,內(nèi)部樣式與外部樣式不存在污染與沖突( tinymce 實(shí)現(xiàn)方式);元素設(shè)置 contentEditable 的方式( wangEditor 等實(shí)現(xiàn)方式)則和其他元素一樣受到頁(yè)面 css 作用。個(gè)人認(rèn)為兩者沒(méi)有優(yōu)劣之分,開(kāi)發(fā)者根據(jù)自身需求選擇即可。
富文件選區(qū)
富文本編輯中我們?cè)谶M(jìn)行編輯時(shí)首先會(huì)先選擇一塊文本區(qū)域(即選區(qū)),比如選擇一段文字并進(jìn)行字體加粗等操作,那么選區(qū)本身包含了哪些信息呢,下面為大家簡(jiǎn)單介紹一下。
Selection 對(duì)象表示用戶選擇的文本范圍或插入符號(hào)的當(dāng)前位置。它代表頁(yè)面中的文本選區(qū),可能橫跨多個(gè)元素。文本選區(qū)由用戶拖拽鼠標(biāo)經(jīng)過(guò)文字而產(chǎn)生。調(diào)用 window.getSelection()(https://developer.mozilla.org/zh-CN/docs/Web/API/Window/getSelection) 可得到此對(duì)象,其內(nèi)部常用屬性如下:
anchorNode
返回選中區(qū)域?qū)?yīng)的節(jié)點(diǎn)
anchorOffset
返回選中區(qū)域的起始下標(biāo),需要注意起始下標(biāo)會(huì)根據(jù)左右方向選擇的次序不同來(lái)展示不同的下標(biāo)。如果 anchorNode 是字符串則對(duì)應(yīng)文字下標(biāo),anchorNode 是元素,則對(duì)應(yīng)選中區(qū)域?qū)?yīng)它之前的同級(jí)節(jié)點(diǎn)的數(shù)目。
focusNode
返回選中區(qū)域終點(diǎn)所在的節(jié)點(diǎn)。
focusOffset
與 anchorOffset 類(lèi)似,如果是 focusNode 是字符串,則對(duì)應(yīng)最后一個(gè)選中的字符所在的位置,focusOffset 是元素,則對(duì)應(yīng)選中區(qū)域?qū)?yīng)同級(jí)節(jié)點(diǎn)的總數(shù)。
rangeCount
返回選中的區(qū)域所對(duì)應(yīng)的連續(xù)的范圍內(nèi)的數(shù)量。
type
返回選中區(qū)域所對(duì)應(yīng)的類(lèi)別是連續(xù) (Range),還是同一個(gè)位置的 (Caret)。
我們常通過(guò) anchorNode 與 anchorOffset 屬性判斷選區(qū)起始位置,通過(guò) focusNode 與 focusOffset 屬性判斷選區(qū)終止位置。
選區(qū)示例
如圖:anchorNode 為選區(qū)起始位置所在節(jié)點(diǎn)("政采云"文本節(jié)點(diǎn)),focusNode 為選區(qū)結(jié)束位置所在節(jié)點(diǎn)("ZOO" 文本節(jié)點(diǎn)),anchorOffset 與 focusOffset 分別為起始位置的 index,通過(guò)此信息可得到選區(qū)范圍,此時(shí) Selection 對(duì)象 type 為 Range。
光標(biāo)示例(起始位置是同一個(gè)位置的選區(qū))
如圖:anchorNode 與 focusNode 為同一節(jié)點(diǎn) ("ZOO" 文本節(jié)點(diǎn)),anchorOffset 與 focusOffset 指向節(jié)點(diǎn)同一處,通過(guò)此信息可得到光標(biāo)位置,此時(shí) Selection 對(duì)象 type 為 Caret。
用途
刪除、替換選區(qū)內(nèi)容&插入操作
Selection 對(duì)象有 deleteFromDocument(https://developer.mozilla.org/zh-CN/docs/Web/API/Selection/deleteFromDocument) 方法,可以在編輯區(qū)域刪除選區(qū)內(nèi)容。如想刪除后插入,可獲取新的 Selection 對(duì)象,利用此時(shí)位置所在 dom 元素的方法插入對(duì)應(yīng)的文字、元素。
效果如圖:
插入邏輯代碼如下:
const insert = () => {
// 刪除所選內(nèi)容
window.getSelection().deleteFromDocument()
const selection = window.getSelection()
// 刪除后選取的起始位置就是插入位置,由 anchorNode 及 anchorOffset 確定
const { anchorNode, anchorOffset } = selection
// anchorNode 分為兩種情況,一種是文本節(jié)點(diǎn),另一種是其他類(lèi)型節(jié)點(diǎn),處理邏輯不同
if (anchorNode.nodeType === 3) {
const string = anchorNode.nodeValue
// anchorNode 為文本節(jié)點(diǎn)時(shí),需要將內(nèi)部字符串與索要插入的內(nèi)容拼接
anchorNode.nodeValue = (string.substring(0, anchorOffset) + '??' + string.substring(anchorOffset, Infinity))
} else {
const newNode = document.createElement('span')
newNode.innerText = '??'
// anchorNode 為其他類(lèi)型節(jié)點(diǎn)時(shí),需要根據(jù) anchorOffset 在 anchorNode 中插入片元素
anchorNode.insertBefore(newNode, anchorNode.childNodes[anchorOffset])
}
}
//也可根據(jù) Selection 提供的原生方法實(shí)現(xiàn)
const insert2 = () => {
lastRange = window.getSelection().getRangeAt(0);
const newNode = document.createElement('span');
newNode.textContent = '??'
lastRange.deleteContents()
lastRange.insertNode(newNode)
}
關(guān)于選區(qū)的更多用途,可參考選區(qū)屬性和方法進(jìn)行靈活實(shí)現(xiàn):https://developer.mozilla.org/zh-CN/docs/Web/API/Selection#methods
富文本工具欄實(shí)現(xiàn)
根據(jù)前文介紹的方法實(shí)現(xiàn)輸入功能后,我們即實(shí)現(xiàn)了純文本編輯的功能,那么如何進(jìn)一步實(shí)現(xiàn)富文本編輯呢?
document 提供了 execCommand() 方法,該方法會(huì)影響使用 designMode 或 contentEditable 屬性實(shí)現(xiàn)可編輯區(qū)域的元素。方法說(shuō)明如下所示:
document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)
aCommandName
一個(gè) DOMString(https://developer.mozilla.org/zh-CN/docs/conflicting/Web/JavaScript/Reference/Global_Objects/String_6fa58bba0570d663099f0ae7ae8883ab) ,命令的名稱(chēng)。可用命令列表請(qǐng)參閱 命令 (https://developer.mozilla.org/zh-CN/docs/Web/API/Document/execCommand#%E5%91%BD%E4%BB%A4) 。
aShowDefaultUI
一個(gè) Boolean(https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Boolean), 是否展示用戶界面,一般為 false。Mozilla 沒(méi)有實(shí)現(xiàn)。
aValueArgument
一些命令(例如 insertImage)需要額外的參數(shù)(insertImage 需要提供插入 image 的 url),默認(rèn)為 null。
該方法執(zhí)行后,會(huì)返回 boolean 值,如果是 false,表示操作不被支持或未被啟用。
不同瀏覽器支持的命令也不一樣。下方標(biāo)列出了最常用的命令。
命令 | 作用 | 可選值 |
---|---|---|
backColor | 設(shè)置文檔背景顏色。在 styleWithCss 模式下,則只影響容器元素的背景顏色。 | 顏色值字符串(IE 使用這個(gè)命令設(shè)置文本背景色) |
bold | 切換選中文本的粗體樣式 | null |
createLink | 將選中內(nèi)容轉(zhuǎn)換為指向給定 URL的鏈接 | URL 鏈接值,至少包含一個(gè)字符 |
fontSize | 將選中文本改為指定字體大小 | 提供 HTML 字體尺寸 (1-7) |
foreColor | 將選中文本改為指定顏色 | 顏色值字符串 |
formatBlock | 將選中文本包含在指定的 HTML標(biāo)簽中 | 提供 HTML 標(biāo)簽,如 |
insertImage | 在光標(biāo)位置插入圖片 | 圖片的 URL 鏈接 |
insertParagraph | 在光標(biāo)位置插入 元素 | null |
italic | 切換選中文本的斜體樣式 | null |
styleWithCSS | 用這個(gè)取代 useCSS 命令。切換使用 HTML tags 還是 CSS 來(lái)生成標(biāo)記。 | Boolean 值,false 使用CSS,true 使用 HTML |
常用功能(字體樣式、插入圖片)演示
下圖挑選了幾個(gè)常用命令(加粗、斜體、改變字體顏色、插入圖片)作為演示:
代碼示例如下:
// 加粗
const bold = (val) => {
document.execCommand('StyleWithCSS', true, true)
document.execCommand('Bold', false, val)
}
// 斜體
const italic = (val) => {
document.execCommand('StyleWithCSS', true, true)
document.execCommand('italic', false, val)
}
// 改變字體顏色
const changeColor = (val = '#ff0000') => {
document.execCommand('StyleWithCSS', true, true)
document.execCommand('foreColor', false, val)
}
// 插入圖片
const insertImage = (val = 'https://avatar-static.segmentfault.com/339/131/3391311562-5d5653daaad5f_huge256') => {
document.execCommand('StyleWithCSS', true, true)
document.execCommand('insertImage', false, val)
}
富文本數(shù)據(jù)收集存儲(chǔ)與回填
富文本容器的 innerHTML 即是富文本數(shù)據(jù)。
編輯區(qū)域可通過(guò)獲取編輯元素的 innerHTML 拿到對(duì)應(yīng)富文本數(shù)據(jù),存入數(shù)據(jù)庫(kù)。
網(wǎng)絡(luò)請(qǐng)求的富文本數(shù)據(jù)設(shè)置為富文本容器的 innerHTML,即可展示富文本內(nèi)容。
下列圖片可簡(jiǎn)單表明:
結(jié)尾(附 Demo)
根據(jù)本文介紹內(nèi)容我們依次了解了前端富文本的概念、輸入模式實(shí)現(xiàn)、選區(qū)的信息及應(yīng)用、富文本工具欄的實(shí)現(xiàn)和富文本數(shù)據(jù)收集回填。將這些內(nèi)容匯總即可實(shí)現(xiàn)一個(gè)簡(jiǎn)單的前端富文本編輯器。
下方附上本文內(nèi)容匯總的代碼 demo ,內(nèi)含基于 iframe 和 div 元素分別實(shí)現(xiàn)的富文本編輯器,功能簡(jiǎn)單,供讀者參考。讀者可根據(jù)文章內(nèi)容進(jìn)行拓展實(shí)現(xiàn)自己的前端富文本編輯器。
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
.rt-container {
height: 200px;
width: 500px;
padding: 10px;
overflow: auto;
}
</style>
<body>
--------------------------------------------------------------<br />
<button onclick="bold()">粗體</button>
<button onclick="italic()">斜體</button>
<button onclick="changeColor()">改變顏色</button>
<button onclick="insertImage()">插入圖片</button>
<button onclick="insert()">插入字符(表情)</button><br />
元素設(shè)置contenteditable<br />
--------------------------------------------------------------<br />
// 元素設(shè)置 contenteditable 方式
<div class="rt-container" contenteditable="true">政采云前端團(tuán)隊(duì)</div>
------------------------------------------------------------------<br />
<button onclick="boldIframe()">iframe粗體</button><br />
iframe設(shè)置designMode<br />
// iframe 設(shè)置 designMode 方式
<iframe class="rt-container" name="editor"></iframe><br />
------------------------------------------------------------------
<div>政采云<span>前端</span>團(tuán)隊(duì)<img src="https://avatar-static.segmentfault.com/339/131/3391311562-5d5653daaad5f_huge256"
width="32" height="32">
<div>ZOO</div>TEAM
</div>
</body>
<script>
window.addEventListener("load", () => {
frames["editor"].document.designMode = "on";
});
const bold = (val) => {
document.execCommand('StyleWithCSS', true, true)
document.execCommand('Bold', false, val)
}
const italic = (val) => {
document.execCommand('StyleWithCSS', true, true)
document.execCommand('italic', false, val)
}
const changeColor = (val = '#ff0000') => {
document.execCommand('StyleWithCSS', true, true)
document.execCommand('foreColor', false, val)
}
const insertImage = (val = 'https://avatar-static.segmentfault.com/339/131/3391311562-5d5653daaad5f_huge256') => {
document.execCommand('StyleWithCSS', true, true)
document.execCommand('insertImage', false, val)
}
const boldIframe = (val) => {
frames["editor"].document.execCommand('StyleWithCSS', true, true)
frames["editor"].document.execCommand('Bold', false, val)
}
const insert = () => {
window.getSelection().deleteFromDocument()
const selection = window.getSelection()
const { anchorNode, anchorOffset } = selection
if (anchorNode.nodeType === 3) {
const string = anchorNode.nodeValue
anchorNode.nodeValue = (string.substring(0, anchorOffset) + '??' + string.substring(anchorOffset, Infinity))
} else {
const newNode = document.createElement('span')
newNode.innerText = '??'
anchorNode.insertBefore(newNode, anchorNode.childNodes[anchorOffset])
}
}
const insert2 = () => {
lastRange = window.getSelection().getRangeAt(0);
const newNode = document.createElement('span');
newNode.textContent = '??'
lastRange.deleteContents()
lastRange.insertNode(newNode)
}
</script>
</html>
效果如圖:
作者:頁(yè)航
歡迎關(guān)注微信公眾號(hào) :政采云前端團(tuán)隊(duì)