一文入門富文本編輯器

簡介

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

文本范圍與選區

富文本中,文本范圍和選區是一個非常強大的功能,借助于文本選區,我們可以對選中文本做一些自定義設置。核心是兩個對象,SelectionRange對象。用比較官方的說法是,Selection對象,表示用戶選擇的文本范圍或光標的當前位置Range對象表示一個包含節點與文本節點的一部分的文檔片段。簡單來說,Selection是指頁面中,我們鼠標選中的所有區域,Range是指頁面中我們鼠標選中的單個區域,屬于一對多的關系。比如,我們要獲取當前頁面的選區對象,可以調用var selection = window.getSelection(),如果想要獲取到第一個文本選區信息,可以調用var rang = selection.getRangeAt(0),獲取到選區文本信息,采用range.toString()
文本范圍與選區,一個比較經典的用法就是,富文本粘貼格式過濾。在我們往富文本編輯器中復制文本時,會保留原文本的格式,如果我們要去除復制的默認格式,只保留純文本,該如何操作呢?
博主在處理這個問題時,首先想到的是,能不能監聽粘貼事件(paste),在粘貼文本時,將剪切板內容替換掉。這一個里面也是有坑的,粘貼時操作剪切板是不生效的。在實現功能需求時,最初采用的是正則匹配,去除HTML標簽。奈何文本格式五花八門,經常出現各種奇奇怪怪的字符,問題比較多,而且復制大文本時,頁面存在性能問題,這并不是一種好的處理方式,直到后來真正理解了文本范圍與選區,才發現這個設置,真香。
富文本選區的處理邏輯大致思路如下:

  1. 監聽文本粘貼事件
  2. 阻止默認事件(阻止瀏覽器默認復制操作)
  3. 獲取復制純文本
  4. 獲取頁面文本選區
  5. 刪除已選中文本選區
  6. 創建文本節點
  7. 將文本節點插入到選區中
  8. 將焦點移動到復制文本結尾

示例代碼如下:

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>

參考資料

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容