富文本編輯器,可以使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>
富文本中,文本範圍和選區是一個很是強大的功能,藉助於文本選區,咱們能夠對選中文本作一些自定義設置。核心是兩個對象,Selection
和Range
對象。用比較官方的說法是,Selection
對象,表示用戶選擇的文本範圍或光標的當前位置,Range
對象表示一個包含節點與文本節點的一部分的文檔片斷。簡單來講,Selection
是指頁面中,咱們鼠標選中的全部區域,Range
是指頁面中咱們鼠標選中的單個區域,屬於一對多的關係。好比,咱們要獲取當前頁面的選區對象,能夠調用var selection = window.getSelection()
,若是想要獲取到第一個文本選區信息,能夠調用var rang = selection.getRangeAt(0)
,獲取到選區文本信息,採用range.toString()
。
文本範圍與選區,一個比較經典的用法就是,富文本粘貼格式過濾。在咱們往富文本編輯器中複製文本時,會保留原文本的格式,若是咱們要去除複製的默認格式,只保留純文本,該如何操做呢?
博主在處理這個問題時,首先想到的是,能不能監聽粘貼事件(paste)
,在粘貼文本時,將剪切板內容替換掉。這一個裏面也是有坑的,粘貼時操做剪切板是不生效的。在實現功能需求時,最初採用的是正則匹配,去除HTML標籤。奈何文本格式五花八門,常常出現各類奇奇怪怪的字符,問題比較多,並且複製大文本時,頁面存在性能問題,這並非一種好的處理方式,直到後來真正理解了文本範圍與選區,才發現這個設置,真香。
富文本選區的處理邏輯大體思路以下:編輯器
示例代碼以下:性能
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>