java实现给选中文字添加样式,天坑之路:用js给选中文字添加样式
前言
本例基于react,但是實際上就是用原生js做的。兼容性做到了IE9,但是按照這個思路做是可以做到IE8甚至更低的。
需求與最初的思路
當我拿到這個需求的時候以為很簡單,就是可以給頁面上的文章做記號,比如添加個下劃線,或者背景涂色做成熒光筆的樣子。
因為只需要兼容IE9,所以window.getSelection是支持的。(IE8及以下有其它的獲取選中的方法)
那么思路就是選中文本,點擊添加下劃線后,通過 window.getSelection.getRangeAt(0) 拿到選中的文本對象,獲取到文本后,通過文本對象的 surroundContents 方法來將文本替換為帶有class的元素。
初步的實現
思路很簡單,代碼同樣也很簡單。
CSS代碼:.custom-underline{??border-bottom:?1px?solid?#f00;??font-style:?normal;
}.nite-writer-pen{??background-color:?lightgreen;??border-radius:?5px;??box-shadow:?0?0?10px?lightgreen;??font-style:?normal;
}
JS代碼:/**
*?用元素替換被選中的文本
*/var?replaceSelectedStrByEle?=?function(className){??var?selecter?=?window.getSelection();??var?selectStr?=?selecter.toString();??if?(selectStr.trim?!=?"")?{????var?rang?=?selecter.getRangeAt(0);????var?ele?=?document.createElement("i");
ele.className?=?className;
ele.textContent?=?selectStr
rang.surroundContents(ele);
}
}
replaceSelectedStrByEle('nite-writer-pen');
天坑出現
上面的思路實在是過于簡單,如果是一個很簡單的元素,那么這種做法是沒有問題的。
但是我們的文章的html結構一般都沒有這么簡單,比如對于以下情況:
道可道,非常道。
名可名,非常名
如果在頁面上我選中的操作如下:
那么上面的代碼實現就會出現BUG,對于這種跨元素選中的情況,想當然的用元素去替換文本是沒用的。
如果你想得更多,比如跨多個元素選中,以及選中元素為更為復雜的html結構,你就會發現這是一個多大的坑。
html結構有多復雜,這個坑就有多深。
思路的僵局與寫輪眼
其實天坑也不是完全沒有路走,在跨多個元素選中的過程中,我想給選中的內容加樣式,那么就需要獲取到所有選中的文本節點,并且批量替換成元素。
但是 window.getSelection.getRangeAt(0) 獲取到的range對象只能獲取到最開始選中的節點和最后選中的節點的。
那么接下來通過選中的最開始的節點和最后的節點獲取到所有的文本節點。
思路就是這么個思路,但是實現起來是很復雜的。
面臨深坑,肯定不可能硬剛。
畢竟我已經不是當年頭鐵的愣頭青了,做項目是有時間和精力成本的,如果要填掉這個坑,那么加班是不可避免的,最重要的是在有限時間內填的這個坑可能還有各種BUG和兼容性問題。
對待這種天坑,一般就給需求來個做不了三連了。
但是這把我想贏,畢竟這個東西看起來確實簡單。
在外人的眼里,如此之簡單,分分鐘搞定的事情。這都做不了,我還怎么在前端的圈子里繼續劃水?
所以我要動用程序員的入門技——寫輪眼。
然而百度、谷歌無效,根本沒有這個解決方案,有的全是些我最初的簡單實現。
但是回憶我們以前見到的各種網頁應用與場景,很容易就能想到上面的這種操作我們是見過的。
那就是從遠古IE時代就已經出現的各種富文本編輯器組件。
目標確認,百度的ueditor,這波我要贏。
分析ueditor與復制
上github兩三下拿到ueditor源碼,開始讀源碼分析代碼。
中間過程不再多說,精簡代碼,除去一些不需要的代碼和兼容性處理后,拿到了五個文件:browser.js (瀏覽器版本判斷,用于做兼容性處理)
domUtils.js (dom操作)
dtd.js (節點的類型與元素判斷)
Range.js (封裝的選中范圍對象)
utils.js (工具類)
即使精簡后,代碼也不少,大概兩三千行。不過其中還有很多注釋,壓縮后體積并不大。
由于代碼比較多,這里就不全部展示了,文章最后會給出github的地址。
這里只給出最后的使用代碼:/**
*?添加下劃線
*/addUnderline?=?()?=>?{??this.replaceSelectedStrByEle(styles['custom-underline'])
}/**
*?啟用熒光筆
*/enableNiteWriterPen?=?()?=>?{??this.replaceSelectedStrByEle(styles['nite-writer-pen'])
}/**
*?用元素替換被選中的文本
*/replaceSelectedStrByEle?=?(className)?=>?{??var?getRange?=?()?=>?{????var?me?=?window;????var?range?=?new?Range(me.document);????var?sel?=?window.getSelection();????if?(sel?&&?sel.rangeCount)?{??????var?firstRange?=?sel.getRangeAt(0);??????var?lastRange?=?sel.getRangeAt(sel.rangeCount?-?1);
range.setStart(firstRange.startContainer,?firstRange.startOffset)
.setEnd(lastRange.endContainer,?lastRange.endOffset);
}????return?range
}??var?range?=?getRange();
range.applyInlineStyle('i',?{????class:?className
});
range.select();
}
使用起來還是比較簡單的。
對i元素的處理做的一些修改
如果我們選中的是已經被包裹在i元素中的一段文本,那么調用后會發現并沒有加上class屬性。
這是因為富文本編輯器和我們的需求不一樣,通過操作想把選中文本變為i元素,而文本外面本來就是i元素了,自然不會進行剩下的操作。
在填充元素的最后會有一個mergeToParent的操作,他會在填充元素的標簽和其父級元素的標簽一樣后將元素替換為文本。if?(parent.tagName?==?node.tagName?||?parent.tagName?==?"A")?{??//...}
那么這里我們要修改源碼加上一個判斷if?((parent.tagName?==?node.tagName?&&?parent.className?==?node.className)?||?parent.tagName?==?"A")?{??//...}
至于其它的邏輯保持不變即可。
為支持回退操作做的一些修改
這里getRange拿到的對象range在選中內容并替換樣式后依然可以使用。
可以調用range.removeInlineStyle('i')
移除之前添加的樣式。
也就是說這里如果使用一個命令模式之類的,是可以實現回退操作的。
不過這里還是有一個坑,就是removeInlineStyle會移除掉選中內容中所有的i元素,于是我修改了
Range.js中removeInlineStyle這個方法,多加了一個className參數,每次去掉i元素時都會判斷是否參數等于className。
然后我們調用時就是range.removeInlineStyle('i',styles['nite-writer-pen'])
總結
作為一個暗藏天坑的小需求,搞定之后其實還挺有成就感的。
粗略閱讀了源碼后才發現如果自己做會有多坑,基本上沒個三五天下不來,并且在多掉了N根頭發后仍然會發現到處都是考慮不周和各種BUG。
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的java实现给选中文字添加样式,天坑之路:用js给选中文字添加样式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: jquery和php怎么链接地址,jQu
- 下一篇: mysql cpu占用率过高,MySQL