前端富文本基礎(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
關(guān)于 document.exexCommand 的更多命令,可參考 (https://developer.mozilla.org/zh-CN/docs/Web/API/Document/execCommand)


常用功能(字體樣式、插入圖片)演示
下圖挑選了幾個(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ì)