富文本編輯器初探

長期以來,做爲用戶我是富文本編輯器的使用者,做爲前端開發,我也只是富文本插件的使用者,對內部實現細節不甚瞭解,使用上也只停留在調用插件提供的API,實現一些業務邏輯。最近的項目,須要開發一個簡易富文本編輯器,也算是讓我有機會對其一窺究竟。javascript

可編輯富文本的方式

咱們知道form表單中的input、textarea之類標籤是支持內容可編輯的,但並不支持富文本,若是在這些標籤裏粘貼帶格式的內容,會被去格式,只保留文本內容。若是想設置可編輯富文本,有兩種方式:html

  • 嵌入空頁面的iframe,並設置designMode屬性值爲「on」,這樣整個文檔就變得能夠編輯。
<iframe
 name="richtext" src="blank.html"></iframe>

window.addEventListener("load"function (){
 frames("richtext").document.designMode = "on"
});
複製代碼

須要在嵌入頁面加載以後,動態設置iframe文檔的designMode屬性。前端

  • 使用contenteditable屬性

該屬性最先是由IE實現,且能夠做用於頁面中的任何標籤,只須要在文檔裏給標籤設置以上屬性便可,無需嵌入iframe、設置js屬性,因此這種方式也是目前富文本編輯器插件中更多采用的方式;java

<div class="editbox" id="richtext" contenteditable>
    <p></p>
    <p contenteditable="false"></p>   
 </>
複製代碼

這樣,此div元素中包含的內容就能夠編輯了,固然也能夠設置子元素(如第二個P元素)爲不可編輯。經過js設置元素的該屬性,也能夠改變編輯模式:web

var elm = document.getElementById('richtext');
elm.contentEditable = 'true';

複製代碼

contenteditable屬性有三個可能的值:'true'表示打開編輯模式,'false'表示關閉,'inherit'表示從父元素繼承此屬性值。contenteditable屬性兼容性較好,在主流瀏覽器包括IE以及目前大部分的移動端瀏覽器上,都獲得支持。canvas

操做富文本

image

常見的富文本編輯器插件,如wangEditor、百度的UEditor,都有各類豐富的菜單區域來設置編輯內容及格式,如常規的設置標題、文字加粗、超連接等,更勝者插入圖片、視頻及自定義的內容結構等,而實現這些功能的API就是document.execCommand(),這個方法是與富文本編輯器進行交互的主要方式。segmentfault

語法設計模式

bool = document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)
複製代碼
  • 返回值:布爾型,false表示操做不支持或未被啓用
  • aCommandName,命令名稱,如「bold」
  • aShowDefaultUI,是否爲該命令提供用戶界面,通常設爲false,主流瀏覽器沒實現該功能
  • aValueArgument,某些命令的額外參數(insertImage命令須要提供插入的圖片的url)

全部支持的commands,可查閱MDN;其中,與剪貼板相關的命令(copy、cut、paste)各瀏覽器實現差別較大,使用時需關注瀏覽器差別。api

經常使用的命令舉例:跨域

// 加粗
document.execCommand('bold', false, null);
// 超連接
document.execCommand('createlink', false, 'https://www.kaola.com');
// 格式化爲h1標題
document.execCommand('formatblock', false, '<h1>');

複製代碼

【注意】雖然全部瀏覽器都支持以上命令,但這些命令生成的html結構仍有差異。如bold命令,IE和Opera會使用<strong>標籤包裹文本,而Safari和Chrome則使用<b>標籤,firefox使用<span>

與命令相關的方法:
  • queryCommandEnabled 返回布爾值,用於檢測是否能夠針對當前選擇的文本或當前光標位置執行某個命令;
var canBold = document.queryCommandEnabled("bold");
複製代碼
  • queryCommandState 返回布爾值,用於判斷當前選擇的文本是否已經應用了指定的命令;
var isBold = document.queryCommandState("bold");
複製代碼

可使用這個方法,來設置編輯器中加粗、斜體等按鈕的狀態。

  • queryCommandValue 用於獲取執行某個命令時,傳入的值(即execCommand()方法的第三方參數)

富文本選區 Seletion

Seletion對象是指用戶選中的文本範圍或鼠標的當前位置,經過window.getSelection()來獲取該對象。

image

Seletion對象的屬性以下:

  • anchorNode:選區起點所在節點;
  • anchorOffset:anchorNode中包含在選區內的字符數;
  • focusNode:選區終點所在節點;
  • focusOffset:focusNode中包含在選區內的字符數;
  • isCollapsed:boolean,選區的起點與終點是否重合,若是是,能夠認爲當前沒有內容選中;
  • rangeCount:選區中包含的DOM範圍的數量;
  • type:描述當前選區的類型

Selection對象的方法參閱MDN。這些方法在富文本編輯器插件裏都是頗有用的方法,好比控制光標的方法collapse()、collapseToEnd()、collapseToStart(),能夠設置插入內容以後光標的位置; 獲取選區包含的文本的方法toString()getRangeAt(index)方法返回索引對應的選區中的DOM範圍,即range對象

來看一個例子:

// 獲取選區內容的位置
function  getSelPos () {
    let sel = window.getSelection()
    let rg = sel.getRangeAt(0)
    let elmRect = rg.getClientRects()[0]
    let editorRect = $('.j-editor')[0].getBoundingClientRect() // 編輯器容器
    let pos = {}
    if (elmRect) {
      // 選區內容居中位置距容器的左距離
      pos.x = elmRect.left - editorRect.left + elmRect.width / 2 
      pos.y = elmRect.top - editorRect.top
    }
    return pos
}

複製代碼

上述方法,能夠獲取當前選區相對於編輯器容器的位置,能夠用來設置在選區附近出現的工具條等。 想實時監測選區的變化,能夠監聽onselectionchange事件

// 高頻事件,作好節流
document.onselectionchange = _.debounce(this.onSelect, 100)
複製代碼

處理paste內容

若是往富文本編輯器裏粘貼內容,是會把內容的樣式也粘貼進來的,瀏覽器自動會把應用到某個標籤的樣式內聯到此標籤的style屬性。但更多的時候咱們只是須要保留裏面的部分格式,須要針對剪貼板中的內容進行過濾、格式化以及特定內容保留等。

editorElem.on('paste', event => {
        event.preventDefault();
        let clipboardData = event.clipboardData || event.originalEvent && event.originalEvent.clipboardData || {};
        let text = clipboardData.getData('text/plain');
        let html = clipboardData.getData('text/html');
    })

複製代碼

經過偵聽paste事件,能獲取到事件對象上的clipboardData對象,獲取粘貼的內容,能夠經過getData方法獲取剪切版上的純文本或html結構。 有了html結構,就能夠轉成dom對象,針對處理了。默認狀況下,剪貼板中的

下面重點說下對剪貼板中圖片的處理。若是剪貼板中的網頁元素包含圖片,即img標籤,若是直接粘貼到編輯器,該圖片的連接地址是原網頁所在的圖片地址,這裏就要考慮到,若是外鏈別人網站的圖片,就有可能有朝一日這個圖片不可用,因此仍是要放到自家的服務器上才放心。這裏就涉及到,已知一張圖片的可訪問的連接地址,如何把該圖片上傳到本身的服務器上呢?

不考慮兼容性,給出一種可行的方案:經過canvas畫布獲取圖片的數據,並將數據轉爲blob對象並進行上傳。步驟以下:

  1. new一個Image對象,設置src屬性爲已知圖片的url;
  2. 在圖片對象的onload事件裏,建立canvas畫布,經過其toDataURL方法獲取圖片數據;
  3. 將圖片數據轉爲一個blob對象,並調用圖片上傳接口上傳該blob對象;
// 根據圖片的url,上傳圖片
export function uploadImgWithUrl(imgUrl,  editor) {
    /**
     * 數據轉blob對象
     */
    function dataToBlob(data) {
        var bytes = void 0;
        bytes = data.split(",")[0].indexOf("base64") >= 0 ? window.atob(data.split(",")[1]) : unescape(data.split(",")[1]);
        var paramType = data.split(",")[0].split(":")[1].split(";")[0];
        var uArr = new Uint8Array(bytes.length);
        for (let i=0; i < bytes.length; i++) {
            uArr[i] = bytes.charCodeAt(i);
        }
        return new Blob([uArr], {
            type: paramType,
            name: 'blob.png'
        });
    }

    var options = this;
    return new Promise(function(resolve, reject) {
        var img = new Image;
        img.setAttribute("crossOrigin", "anonymous");

        img.onload = function() {
            var canvas = document.createElement("canvas");
            canvas.width = img.width;
            canvas.height = img.height;
            canvas.getContext("2d").drawImage(img, 0, 0);
            var data = canvas.toDataURL("image/png");
            var blob = dataToBlob(data);
            
            blob.name = 'blob.jpg'
            // 調用編輯器的上傳圖片接口 or 也可自行實現一個圖片上傳方法
            editor.uploadImg.uploadImg([blob], function(result){
                if (result && result.body) {
                    var link = result.body.imageUrlList || [];
                    var item = {
                        resourceType: 'image',
                        imageUrl: link[0]
                    }
                    resolve(item);
                }
            })
        };
        img.onerror = function() {
            reject();
            Message.error('圖片不容許跨域訪問,請手動下載後添加')
        };
        imgUrl = -1 !== imgUrl.indexOf("?") ? imgUrl + "&time=" + (new Date).getTime() : imgUrl + "?time=" + (new Date).getTime();
        img.src = imgUrl;
    });
}

複製代碼

以上是將在線的圖片上傳到服務器的一種解決方法,也在項目中進行了實踐。對於剪貼板內存中的圖片內容,能夠經過getAsFile()方法來獲取進而上傳:

// 處理內存中的圖片
if (clipboardData.items[0]) {
    let item = clipboardData.items[0]
    let type = item.type;
    let regResult = type.match(/image\/(.+)/)
    if (regResult) {
        let blob = item.getAsFile();
        // 調用編輯器的通用上傳接口
        editor.uploadImg.uploadImg([blob])
    }
}

複製代碼

最後

以上,只算上對富文本編輯器的基本知識點進行了初步的梳理,若是想本身造輪子,擼一個編輯器出來,須要解決的問題還有不少,能夠看下知乎上的討論爲何都說富文本編輯器是天坑?,裏面提到實現一個使人滿意的編輯器須要各類填坑,以及良好的設計模式,路漫漫其修遠兮……


參考文獻

by lzf

儘可能關注網易考拉前端團隊微信公衆號

image
相關文章
相關標籤/搜索