默認(rèn)插入
元素
(
標(biāo)簽用來(lái)占位,有內(nèi)容輸入后會(huì)自動(dòng)刪除)。這樣以后每次回車產(chǎn)生的新內(nèi)容都會(huì)被
元素包裹起來(lái)(在可編輯狀態(tài)下,回車換行產(chǎn)生的新結(jié)構(gòu)會(huì)默認(rèn)拷貝之前的內(nèi)容,包裹節(jié)點(diǎn),類名等各種內(nèi)容)。
我們還需要監(jiān)聽 keyUp 事件下 event. === 8 刪除鍵。當(dāng)編輯器中內(nèi)容全被清空后(鍵也會(huì)把
標(biāo)簽刪除),要重新加入
標(biāo)簽,并把光標(biāo)定位在里面。
插入 ul 和 ol 位置錯(cuò)誤
當(dāng)我們調(diào)用 .("", false, null) 來(lái)插入一個(gè)列表的時(shí)候,新的列表會(huì)被插入
標(biāo)簽中。
image
為此我們需要每次調(diào)用該命令前做一次修正,參考代碼如下:
function adjustList() {
let lists = document.querySelectorAll("ol, ul");
for (let i = 0; i < lists.length; i++) {
let ele = lists[i]; // ol
let parentNode = ele.parentNode;
if (parentNode.tagName === 'P' && parentNode.lastChild === parentNode.firstChild) {
parentNode.insertAdjacentElement('beforebegin', ele);
parentNode.remove()
}
}
}
這里有個(gè)附帶的小問(wèn)題,我試圖在 維護(hù)這樣的編輯器結(jié)構(gòu)(默認(rèn)是沒有
標(biāo)簽的)。效果在 下運(yùn)行很好。但是在 中,回車永遠(yuǎn)不會(huì)產(chǎn)生新的 標(biāo)簽,這樣就是去了該有的列表效果。
插入分割線
調(diào)用 .('', false, null); 會(huì)插入一個(gè)標(biāo)簽。然而產(chǎn)生的效果卻是這樣的:
image
光標(biāo)和的效果一致了。為此要判斷當(dāng)前光標(biāo)是否在 里面,如果是則在 后面追加一個(gè)空的文本節(jié)點(diǎn) #text 不是的話追加
。然后將光標(biāo)定位在里面,可用如下方式查找。
/**
* 查找父元素
* @param {String} root
* @param {String | Array} name
*/
function findParentByTagName(root, name) {
let parent = root;
if (typeof name === "string") {
name = [name];

}
while (name.indexOf(parent.nodeName.toLowerCase()) === -1 && parent.nodeName !== "BODY" && parent.nodeName !== "HTML") {
parent = parent.parentNode;
}
return parent.nodeName === "BODY" || parent.nodeName === "HTML" ? null : parent;
},
插入鏈接
調(diào)用 .('', false, url); 方法我們可以插入一個(gè) url 鏈接,但是該方法不支持插入指定文字的鏈接。同時(shí)對(duì)已經(jīng)有鏈接的位置可以反復(fù)插入新的鏈接。為此我們需要重寫此方法。
function insertLink(url, title) {
let selection = document.getSelection(),
range = selection.getRangeAt(0);
if(range.collapsed) {
let start = range.startContainer,
parent = Util.findParentByTagName(start, 'a');
if(parent) {
parent.setAttribute('src', url);
}else {
this.insertHTML(`${title}`);
}
}else {
document.execCommand('createLink', false, url);
}
}
設(shè)置 h1 ~ h6 標(biāo)題
瀏覽器沒有現(xiàn)成的方法,但我們可以借助 .('', false, tag), 來(lái)實(shí)現(xiàn),代碼如下:
function setHeading(heading) {
let formatTag = heading,
formatBlock = document.queryCommandValue("formatBlock");
if (formatBlock.length > 0 && formatBlock.toLowerCase() === formatTag) {
document.execCommand('formatBlock', false, ``);

} else {
document.execCommand('formatBlock', false, ``);
}
}
插入定制內(nèi)容
當(dāng)編輯器上傳或加載附件的時(shí)候,要插入能夠展示附件的
節(jié)點(diǎn)卡片到編輯中。這里我們借助 .('', false, html); 來(lái)插入內(nèi)容。為了防止div被編輯,要設(shè)置 ="false"哦。
處理 paste 粘貼
在富文本編輯器中,粘貼效果默認(rèn)采用如下規(guī)則:
如果是帶有格式的文本,則保留格式(格式會(huì)被轉(zhuǎn)換成html標(biāo)簽的形式)粘貼圖文混排的內(nèi)容,圖片可以顯示,src 為圖片真實(shí)地址。通過(guò)復(fù)制圖片來(lái)進(jìn)行粘貼的時(shí)候,不能粘入內(nèi)容粘貼其他格式內(nèi)容,不能粘入內(nèi)容
為了能夠控制粘貼的內(nèi)容,我們監(jiān)聽 paste 事件。該事件的 event 對(duì)象中會(huì)包含一個(gè) 剪切板對(duì)象。我們可以利用該對(duì)象的 方法來(lái)獲得帶有格式和不帶格式的內(nèi)容,如下。
let plainText = event.clipboardData.getData('text/plain'); // 無(wú)格式文本
let plainHTML = event.clipboardData.getData('text/html'); // 有格式文本
之后調(diào)用 .('', false, ); 或 .('', false, ; 來(lái)重寫編輯上的paste效果。
然而對(duì)于規(guī)則 3 ,上述方案就無(wú)法處理了。這里我們要引入 event..items。這是一個(gè)數(shù)組包含了所有剪切板中的內(nèi)容對(duì)象。比如你復(fù)制了一張圖片來(lái)粘貼,那么 event..items 的長(zhǎng)度就為2:
items[0] 為圖片的名稱,items[0].kind 為 ‘’, items[0].type 為 ‘text/plain’ 或 ‘text/html’。獲取內(nèi)容方式如下:
items[0].getAsString(str => {
// 處理 str 即可
})
items[1] 為圖片的二進(jìn)制數(shù)據(jù),items[1].kind 為’file’, items[1].type 為圖片的格式。想要獲取里面的內(nèi)容,我們就需要?jiǎng)?chuàng)建 對(duì)象了。示例代碼如下:
let file = items[1].getAsFile();
// file.size 為文件大小
let reader = new FileReader();
reader.onload = function() {
// reader.result 為文件內(nèi)容,就可以做上傳操作了
}
if(/image/.test(item.type)) {
reader.readAsDataURL(file); // 讀取為 base64 格式
}
處理完圖片,那么對(duì)于復(fù)制粘貼其他格式內(nèi)容會(huì)怎么樣呢?在 mac 中,如果你復(fù)制一個(gè)磁盤文件,event..items 的長(zhǎng)度為 2。 items[0] 依然為文件名,然而 items[1] 則為圖片了,沒錯(cuò),是文件的縮略圖。
輸入法處理
當(dāng)使用輸入發(fā)的時(shí)候,有時(shí)候會(huì)發(fā)生一些意想不到的事情。 比如百度輸入法可以輸入一張本地圖片,為此我們需要監(jiān)聽輸入法產(chǎn)生的內(nèi)容做處理。這里通過(guò)如下兩個(gè)事件處理:
修復(fù)移動(dòng)端的問(wèn)題
在移動(dòng)端,富文本編輯器的問(wèn)題主要集中在光標(biāo)和鍵盤上面。我這里介紹幾個(gè)比較大的坑。
自動(dòng)獲取焦點(diǎn)
如果想讓我們的編輯器自動(dòng)獲得焦點(diǎn),彈出軟鍵盤,可以利用 focus() 方法。然而在 ios 下,死活沒有結(jié)果。這主要是因?yàn)?ios 中,為了安全考慮不允許代碼獲得焦點(diǎn)。只能通過(guò)用戶交互點(diǎn)擊才可以。還好,這一限制可以去除:
[self.appWebView setKeyboardDisplayRequiresUserAction:NO]
iOS 下回車換行,滾動(dòng)條不會(huì)自動(dòng)滾動(dòng)
在 iOS 下,當(dāng)我們回車換行的時(shí)候,滾動(dòng)條并不會(huì)隨著滾動(dòng)下去。這樣光標(biāo)就可能被鍵盤擋住,體驗(yàn)不好。為了解決這一問(wèn)題,我們就需要監(jiān)聽 事件,觸發(fā)時(shí),計(jì)算每次光標(biāo)編輯器頂端距離,之后再調(diào)用 .() 即可解決。問(wèn)題在于我們要如何計(jì)算當(dāng)前光標(biāo)的位置,如果僅是計(jì)算光標(biāo)所在父元素的位置很有可能出現(xiàn)偏差(多行文本計(jì)算不準(zhǔn))。我們可以通過(guò)創(chuàng)建一個(gè)臨時(shí) 元素查到光標(biāo)位置,計(jì)算元素的位置即可。代碼如下:
function getCaretYPosition() {
let sel = window.getSelection(),
range = sel.getRangeAt(0);
let span = document.createElement('span');
range.collapse(false);
range.insertNode(span);
var topPosition = span.offsetTop;
span.parentNode.removeChild(span);
return topPosition;
}
正當(dāng)我開心的時(shí)候,安卓端反應(yīng),編輯器越編輯越卡。什么鬼?我在 上線檢查了一下,發(fā)現(xiàn) 函數(shù)一直在運(yùn)行,不管有沒有操作。
在逐一排查的時(shí)候發(fā)現(xiàn)了這么一個(gè)事實(shí)。range. 函數(shù)同樣觸發(fā) 事件。這樣就形成了一個(gè)死循環(huán)。這個(gè)死循環(huán)在 中就不會(huì)產(chǎn)生,只出現(xiàn)在 中,為此我們就需要加上瀏覽器類型判斷了。
鍵盤彈起遮擋輸入部分
網(wǎng)上對(duì)于這個(gè)問(wèn)題主要的方案就是,設(shè)置定時(shí)器。局限與前端,確實(shí)只能這采用這樣笨笨的解決。最后我們讓 iOS 同學(xué)在鍵盤彈出的時(shí)候,將 高度減去軟鍵盤高度就解決了。
CGFloat webviewY = 64.0 + self.noteSourceView.height;
self.appWebView.frame = CGRectMake(0, webviewY, BDScreenWidth, BDScreenHeight - webviewY - height);
插入圖片失敗
在移動(dòng)端,通過(guò)調(diào)用 來(lái)喚起相冊(cè)選擇圖片。之后調(diào)用 函數(shù)來(lái)向編輯器插入圖片。然而,插入圖片一直失敗。最后發(fā)現(xiàn)是因?yàn)樵? 下,如果編輯器失去了焦點(diǎn),那么 和 range 對(duì)象將銷毀。因此調(diào)用 時(shí),并不能獲得光標(biāo)所在位置,因此失敗。為此需要增加,() 和 () 函數(shù)。當(dāng)頁(yè)面失去焦點(diǎn)的時(shí)候記錄 range 信息,插入圖片前恢復(fù) range 信息。
backupRange() {
let selection = window.getSelection();
let range = selection.getRangeAt(0);
this.currentSelection = {
"startContainer": range.startContainer,
"startOffset": range.startOffset,
"endContainer": range.endContainer,
"endOffset": range.endOffset
}
}

restoreRange() {
if (this.currentSelection) {
let selection = window.getSelection();
selection.removeAllRanges();
let range = document.createRange();
range.setStart(this.currentSelection.startContainer, this.currentSelection.startOffset);
range.setEnd(this.currentSelection.endContainer, this.currentSelection.endOffset);
// 向選區(qū)中添加一個(gè)區(qū)域
selection.addRange(range);
}
}
在 中,失去焦點(diǎn)并不會(huì)清除 對(duì)象和 range 對(duì)象,這樣我們輕輕松松一個(gè) focus() 就搞定了。
重要問(wèn)題就這么多,限于篇幅限制其他的問(wèn)題省略了。總體來(lái)說(shuō),填坑花了開發(fā)的大部分時(shí)間。
其他功能
基礎(chǔ)功能修修補(bǔ)補(bǔ)以后,實(shí)際項(xiàng)目中有可能遇到一些其他的需求移動(dòng)web 富文本編輯器,比如當(dāng)前光標(biāo)所在文字內(nèi)容狀態(tài)啊,圖片拖拽放大啊,待辦列表功能,附件卡片等功能啊,切換等等。在了解了js 富文本的種種坑之后,range 對(duì)象的操作之后,相信這些問(wèn)題你都可以輕松解決。這里最后提幾個(gè)做擴(kuò)展功能時(shí)候遇到的有去的問(wèn)題。
回車換行帶格式
前面已經(jīng)說(shuō)過(guò)了,富文本編輯器的機(jī)制就是這樣,當(dāng)你回車換行的時(shí)候新產(chǎn)生的內(nèi)容和之前的格式一模一樣。如果我們利用 .card 類來(lái)定義了一個(gè)卡片內(nèi)容,那么換行產(chǎn)生的新的段落都將含有 .card 類且結(jié)構(gòu)也是直接 copy 過(guò)來(lái)的。我們想要屏蔽這種機(jī)制,于是嘗試在 的階段做處理(如果在 keyup 階段處理用戶體驗(yàn)不好)。然而,并沒有什么用,因?yàn)橛脩糇远x的 事件要在 瀏覽器富文本的默認(rèn) 事件之前觸發(fā),這樣你就做不了任何處理。
為此我們?yōu)檫@類特殊的個(gè)體都添加一個(gè) 屬性,添加在 上的內(nèi)容是不會(huì)被copy下來(lái)的。這樣以后就可以區(qū)分出來(lái)了,從而做對(duì)應(yīng)的處理。
獲取當(dāng)前光標(biāo)所在處樣式
這里主要是考慮 下劃線,刪除線之類的樣式,這些樣式都是用標(biāo)簽類描述的移動(dòng)web 富文本編輯器,所以要遍歷標(biāo)簽層級(jí)。直接上代碼:
function getCaretStyle() {
let selection = window.getSelection(),
range = selection.getRangeAt(0);
aimEle = range.commonAncestorContainer,
tempEle = null;
let tags = ["U", "I", "B", "STRIKE"],
result = [];
if(aimEle.nodeType === 3) {
aimEle = aimEle.parentNode;
}
tempEle = aimEle;
while(block.indexOf(tempEle.nodeName.toLowerCase()) === -1) {
if(tags.indexOf(tempEle.nodeName) !== -1) {
result.push(tempEle.nodeName);

}
tempEle = tempEle.parentNode;
}
let viewStyle = {
"italic": result.indexOf("I") !== -1 ? true : false,
"underline": result.indexOf("U") !== -1 ? true : false,
"bold": result.indexOf("B") !== -1 ? true : false,
"strike": result.indexOf("STRIKE") !== -1 ? true : false
}
let styles = window.getComputedStyle(aimEle, null);
viewStyle.fontSize = styles["fontSize"],
viewStyle.color = styles["color"],
viewStyle.fontWeight = styles["fontWeight"],
viewStyle.fontStyle = styles["fontStyle"],
viewStyle.textDecoration = styles["textDecoration"];
viewStyle.isH1 = Util.findParentByTagName(aimEle, "h1") ? true : false;
viewStyle.isH2 = Util.findParentByTagName(aimEle, "h2") ? true : false;
viewStyle.isP = Util.findParentByTagName(aimEle, "p") ? true : false;
viewStyle.isUl = Util.findParentByTagName(aimEle, "ul") ? true : false;
viewStyle.isOl = Util.findParentByTagName(aimEle, "ol") ? true : false;
return viewStyle;
}
|
# 最后說(shuō)一句
該項(xiàng)目目前提測(cè)中,所以呢,一但發(fā)現(xiàn)有意思的坑,我會(huì)及時(shí)補(bǔ)充的。
# 參考內(nèi)容
* [MDN – document.execCommand](https://developer.mozilla.org/zh-CN/docs/Web/API/Document/execCommand)
* [MDN – selection](https://developer.mozilla.org/zh-CN/docs/Web/API/Selection)
* [MDN – range](https://developer.mozilla.org/zh-CN/docs/Web/API/Range)
* [input 事件兼容處理以及中文輸入法優(yōu)化](http://frontenddev.org/article/compatible-with-processing-and-chinese-input-method-to-optimize-the-input-events.html)
* [js獲取剪切板內(nèi)容,js控制圖片粘貼](https://segmentfault.com/a/1190000004288686)
* [iOS UIWebView 全屬性詳解](http://blog.csdn.net/ll845876425/article/details/51884736)