長期以來,做爲用戶我是富文本編輯器的使用者,做爲前端開發,我也只是富文本插件的使用者,對內部實現細節不甚瞭解,使用上也只停留在調用插件提供的API,實現一些業務邏輯。最近的項目,須要開發一個簡易富文本編輯器,也算是讓我有機會對其一窺究竟。javascript
咱們知道form表單中的input、textarea之類標籤是支持內容可編輯的,但並不支持富文本,若是在這些標籤裏粘貼帶格式的內容,會被去格式,只保留文本內容。若是想設置可編輯富文本,有兩種方式:html
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
常見的富文本編輯器插件,如wangEditor、百度的UEditor,都有各類豐富的菜單區域來設置編輯內容及格式,如常規的設置標題、文字加粗、超連接等,更勝者插入圖片、視頻及自定義的內容結構等,而實現這些功能的API就是document.execCommand()
,這個方法是與富文本編輯器進行交互的主要方式。segmentfault
語法:設計模式
bool = document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)
複製代碼
全部支持的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>
。
var canBold = document.queryCommandEnabled("bold");
複製代碼
var isBold = document.queryCommandState("bold");
複製代碼
可使用這個方法,來設置編輯器中加粗、斜體等按鈕的狀態。
Seletion對象是指用戶選中的文本範圍或鼠標的當前位置,經過window.getSelection()
來獲取該對象。
Seletion對象的屬性以下:
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)
複製代碼
若是往富文本編輯器裏粘貼內容,是會把內容的樣式也粘貼進來的,瀏覽器自動會把應用到某個標籤的樣式內聯到此標籤的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對象並進行上傳。步驟以下:
// 根據圖片的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