一文入門富文本編輯器


簡介

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

參考資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM