一文入門富文本編輯器

簡介

富文本編輯器,可以使web頁面像word同樣,實現對文本的編輯,一般應用在一些文本處理比較多的系統中。如今業界有不少成熟的富文本編輯器,好比功能齊全啊TinyMCE、輕量高效的wangEditor、百度出品的UEditor等。富文本編輯器不少,可是卻不多思考如何從零開始,實現一個富文本編輯器。本文主要簡述如何從零開始,實現一個簡易的富文本編輯器。html

基本使用

普通的HTML標籤,可以輸入的一般只是表單,表單輸入的是純文本,不帶格式的內容。富文本相對於表單,可以給輸入文本內容增長一些自定義內容樣式,好比加粗、字體顏色、背景...。富文本的實現,主要是給HTML標籤,好比div增長一個contenteditable屬性,擁有該屬性的HTML標籤,就可以對該標籤裏的內容,實現自定義的編輯。最簡單的富文本編輯器以下:web

<!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瀏覽器

基本操做命令

如下簡單列舉一些富文本操做命令,下面給出一些例子的簡單使用app

命令 說明
backcolor 顏色字符串 設置文檔的背景顏色
bold null 將選擇的文本加粗
createlink URL字符串 將選擇的文本轉換成一個連接,指向指定的URL
indent null 縮進文本
copy null 將選擇的文本複製到剪切板
cut null 將選擇文本剪切到剪切板
inserthorizontalrule null 在插入字符處插入一個hr元素

Example:electron

<!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>

參考資料

相關文章
相關標籤/搜索