JS Range HTML文檔/文字內(nèi)容選中、庫及應用介紹
本文的內(nèi)容基本上是基于“區(qū)域范圍對象(Range objects)”這個概念來說的。這個玩意,可以讓你選擇HTML文檔的任意部分,并可以拿這些選擇的信息做你想做的事情。其中,最常見的Range是用戶用鼠標選擇的內(nèi)容(user selection)。
本文有不少篇幅就是講如何將用戶的這種選擇轉(zhuǎn)換為W3C Range或Microsoft Text Range對象。
二、什么是Range?
所謂"Range",是指HTML文檔中任意一段內(nèi)容。一個Range的起始點和結束點位置任意,甚至起始點和結束點可以是一樣的(也就是空Range)。最常見的Range是用戶文本選擇范圍(user text selection)。當用戶選擇了頁面上的某一段文字后,你就可以把這個選擇轉(zhuǎn)為Range。當然,你也可以直接用程序定義Range。
例如下面這個模樣的例子:
2011-04-12
負責調(diào)查切爾諾貝利核事故對人與環(huán)境造成影響的俄科學家亞布羅科夫博士指出,因福島核電站使用的燃料較切爾諾貝利核電站多,且有反應堆使用了含有高毒性的钚的燃料,因此"福島核電站事故可能會比切爾諾貝利帶來更嚴重的后果"。
上面選中狀態(tài)的那些文字就可以轉(zhuǎn)換成Range對象
(下面會詳細講述)。通過Range對象
你可以找到Range
的起始點和結束點,如果你實在有心,還可以刪除或是復制這些內(nèi)容,或是用其他文字替換,甚至是簡單的HTML。
上面的例子可以說是最簡單的Range對象
的例子,因為其只包含了文字。而實際上,Range對象
也是可以包含HTML代碼內(nèi)容的,例如下面這個示例:
<time>2011-04-12</time>
<p>據(jù)日本廣播協(xié)會電視臺12日報道,日本經(jīng)濟產(chǎn)業(yè)省原子能安全保安院決定將福島第一核電站核泄漏事故等級提高至7級。這使日本核泄漏事故等級與蘇聯(lián)切爾諾貝利核電站核泄漏事故等級相同。</p>
<p>負責調(diào)查切爾諾貝利核事故對人與環(huán)境造成影響的俄科學家亞布羅科夫博士指出,因福島核電站使用的燃料較切爾諾貝利核電站多,且有反應堆使用了含有高毒性的钚的燃料,因此"福島核電站事故可能會比切爾諾貝利帶來更嚴重的后果"。</p>
同樣的,Range對象
被創(chuàng)建,且包含HTML,現(xiàn)在的問題是選擇的內(nèi)容正好跨過了楚河和漢界(跨標簽),如果就單純的論選擇的內(nèi)容的話,應該如下:
泄漏事故等級與蘇聯(lián)切爾諾貝利核電站核泄漏事故等級相同。</p> <p>負責調(diào)查切爾諾貝
顯然,上面的HTML屬于1級殘廢,基本無效。然而幸運的是,所有的瀏覽器都會自動調(diào)整HTML片段使其有效,就像變成下面這樣:
<p>泄漏事故等級與蘇聯(lián)切爾諾貝利核電站核泄漏事故等級相同。</p> <p>負責調(diào)查切爾諾貝</p>
可以看到,瀏覽器自動補全了一定數(shù)目的HTML來讓Range
有效。如果你復制或是移動Range
,你所復制或移動的HTML內(nèi)容一定是有效的。
三、瀏覽器的兼容性
在真正操刀JavaScript之前我們需要大致知道Range對象
的瀏覽器兼容性情況。實際上,問題是比較麻煩的,因為至少有3種類似Range對象
,且你有必要全部理解。先展示詳細的兼容性情況表:
支持:不支持:
部分支持:
1. W3C Range
Explorer 6/7 | Firefox 2 | Safari 1.3 | Opera 9 | |
---|---|---|---|---|
cloneContents() | ![]() |
![]() |
![]() |
![]() |
cloneRange() | ![]() |
![]() |
![]() |
![]() |
collapse() | tbd | tbd | tbd | tbd |
collapsed | ![]() |
![]() |
![]() |
![]() |
commonAncestorContainer | ![]() |
![]() |
![]() |
![]() |
compareBoundaryPoints() | ![]() |
![]() |
![]() |
![]() |
comparePoint() – Mozilla 擴展 | ![]() |
![]() |
![]() |
![]() |
createContextualFragment() – Mozilla 擴展 | ![]() |
![]() |
![]() |
![]() |
deleteContents() | ![]() |
![]() |
![]() |
![]() |
detach() | ![]() |
![]() |
![]() |
![]() |
endContainer | ![]() |
![]() |
![]() |
![]() |
endOffset | ![]() |
![]() |
![]() |
![]() |
extractContents() | ![]() |
![]() |
![]() |
![]() |
insertNode() | ![]() |
![]() |
![]() |
![]() |
isPointInRange() – Mozilla 擴展 | ![]() |
![]() |
![]() |
![]() |
selectNode() | ![]() |
![]() |
![]() |
![]() |
selectNodeContents() | ![]() |
![]() |
![]() |
![]() |
setEnd() | ![]() |
![]() |
![]() |
![]() |
setEndAfter() | ![]() |
![]() |
![]() |
![]() |
setEndBefore() | ![]() |
![]() |
![]() |
![]() |
setStart() | ![]() |
![]() |
![]() |
![]() |
setStartAfter() | ![]() |
![]() |
![]() |
![]() |
setStartBefore() | ![]() |
![]() |
![]() |
![]() |
startContainer | ![]() |
![]() |
![]() |
![]() |
startOffset | ![]() |
![]() |
![]() |
![]() |
surroundContents() | ![]() |
![]() |
![]() |
![]() |
說明:cloneContents()
的用法類似docFrag = rangeObject.cloneContents()
,Range對象
內(nèi)容被克隆同時被添加到文檔片段上,并返回自身。但是在Safari下有個問題,即如果選擇范圍是空,將會返回null
而不是空的文檔片段??梢酝ㄟ^類似docFrag = rangeObject.cloneContents() || document.createDocumentFragment()
這樣的代碼修復。
deleteContents()
處,Range
內(nèi)容會被永久刪除,無返回值。
endContainer
指用戶選擇內(nèi)容結尾處的容器節(jié)點。通常是文本節(jié)點。
extractContents()
用法docFrag = rangeObject.extractContents()
。從DOM樹上剪切Range對象
并返回文檔片段。該片段可以粘貼到頁面上。
startContainer
指用戶選擇內(nèi)容起始處的容器節(jié)點。通常是文本節(jié)點。
startOffset
在Opera瀏覽器下,在選擇內(nèi)容為空的時候返回0
。
2. Mozilla Selection
Explorer 6/7 | Firefox 2 | Safari 1.3 | Opera 9 | |
---|---|---|---|---|
addRange() | ![]() |
![]() |
![]() |
![]() |
anchorNode | ![]() |
![]() |
![]() |
![]() |
anchorOffset | ![]() |
![]() |
![]() |
![]() |
collapse() | tbd | tbd | tbd | tbd |
collapseToEnd() | ![]() |
![]() |
![]() |
![]() |
collapseToStart() | ![]() |
![]() |
![]() |
![]() |
containsNode() | ![]() |
![]() |
![]() |
![]() |
deleteFromDocument() | ![]() |
![]() |
![]() |
![]() |
extend() | ![]() |
![]() |
![]() |
![]() |
focusNode | ![]() |
![]() |
![]() |
![]() |
focusOffset | ![]() |
![]() |
![]() |
![]() |
getRangeAt() | ![]() |
![]() |
![]() |
![]() |
isCollapsed | ![]() |
![]() |
![]() |
![]() |
rangeCount | ![]() |
![]() |
![]() |
![]() |
removeAllRanges() | ![]() |
![]() |
![]() |
![]() |
removeRange() | ![]() |
![]() |
![]() |
![]() |
selectAllChildren() | ![]() |
![]() |
![]() |
![]() |
selectionLanguageChange() | ![]() |
![]() |
![]() |
![]() |
說明:anchorNode
用法為userSelection.anchorNode
。指用戶選擇內(nèi)容起始處的容器節(jié)點。通常是文本節(jié)點。
anchorNode
在Opera瀏覽器下,在選擇內(nèi)容為空的時候返回0
。
focusNode
用法為userSelection.focusNode
。指用戶選擇內(nèi)容結尾處的容器節(jié)點。通常是文本節(jié)點。
focusOffset
在Opera瀏覽器下,在選擇內(nèi)容為空的時候返回0
。
getRangeAt()
用法為rangeObject = userSelection.getRangeAt(0),作用是將
Mozilla Selection
轉(zhuǎn)換為W3C Range
。
3. Microsoft TextRange
Explorer 6/7 | Firefox 2 | Safari 1.3 | Opera 9 | |
---|---|---|---|---|
boundingHeight | ![]() |
![]() |
![]() |
![]() |
boundingLeft | ![]() |
![]() |
![]() |
![]() |
boundingTop | ![]() |
![]() |
![]() |
![]() |
boundingWidth | ![]() |
![]() |
![]() |
![]() |
collapse() | tbd | tbd | tbd | tbd |
compareEndPoints() | ![]() |
![]() |
![]() |
![]() |
duplicate() | ![]() |
![]() |
![]() |
![]() |
expand() | ![]() |
![]() |
![]() |
![]() |
findText() | ![]() |
![]() |
![]() |
![]() |
htmlText | ![]() |
![]() |
![]() |
![]() |
move() | ![]() |
![]() |
![]() |
![]() |
moveEnd() | ![]() |
![]() |
![]() |
![]() |
moveStart() | ![]() |
![]() |
![]() |
![]() |
moveToElementText() | ![]() |
![]() |
![]() |
![]() |
moveToPoint() | ![]() |
![]() |
![]() |
![]() |
offsetLeft | ![]() |
![]() |
![]() |
![]() |
offsetTop | ![]() |
![]() |
![]() |
![]() |
parentElement() | ![]() |
![]() |
![]() |
![]() |
pasteHTML() | ![]() |
![]() |
![]() |
![]() |
scrollIntoView() | ![]() |
![]() |
![]() |
![]() |
select() | ![]() |
![]() |
![]() |
![]() |
text | ![]() |
![]() |
![]() |
![]() |
說明:htmlText
用法為htmlString = userSelection.htmlText
。返回字符串,為TextRange
的HTML內(nèi)容,相當于innerHTML
。只讀。
pasteHTML()
,當粘貼HTML到一個文本節(jié)點時,該文本節(jié)點自動分隔。
text
用法為string = userSelection.text
。返回字符串,為TextRange
的文本內(nèi)容,相當于innerText
??勺x/寫。
4. 總的兼容性
Explorer 6/7 | Firefox 2 | Safari 1.3 | Opera 9 | |
---|---|---|---|---|
W3C Range | ![]() |
![]() |
![]() |
![]() |
Mozilla Selection | ![]() |
![]() |
![]() |
![]() |
Microsoft Text Range | ![]() |
![]() |
![]() |
![]() |
說明:
W3C Range對象
是唯一官方指定。基本上其是將Range
作為包含DOM的文檔片段。Mozilla Selection對象
顯得有些多余,其存在是為了向后兼容Netscape 4。其類似于W3C Range對象
,也是基于DOM樹的。Microsoft Text Range對象
跟上面兩個就是郭德綱和玄彬的區(qū)別了,因為其是基于字符串的。事實上,Text Range
包含的字符串是很難一下子跳變成DOM節(jié)點的。
總的來說,Mozilla Selection對象
就是個打醬油的命,僅有的閃光點能夠直接將用戶選擇任何內(nèi)容變成完全Range對象
以及一些額外的方法或是屬性可以向后兼容Netscape 4。但是不幸的是除了IE瀏覽器外的其他瀏覽器都支持此Selection對象
。
四、獲得用戶選擇內(nèi)容
婆婆媽媽的解釋就免了,直接看相關代碼:
var userSelection;
if (window.getSelection) { //現(xiàn)代瀏覽器
userSelection = window.getSelection();
} else if (document.selection) { //IE瀏覽器 考慮到Opera,應該放在后面
userSelection = document.selection.createRange();
}
由于兼容性的問題,IE瀏覽器吃IE的包子,其他瀏覽器吃Mozilla的饅頭。
在Mozilla、Safari、Opera下上面的userSelection是個Selection對象,而在IE下則是Text Range對象。這種差異會影響到你后面的腳本:Internet Explorer的Text Ranges完全不同于Mozilla的Selection或是W3C的Range對象,你需要分別為IE和其他瀏覽器寫兩套不同的腳本。
需要注意腳本書寫的順序:Mozilla Selection需放在前面。原因在于Opera支持兩種對象,如果你使用window.getSelection()去讀取用戶選擇的內(nèi)容,Opera會創(chuàng)建一個Selection對象;而使用document.selection則會創(chuàng)建一個Text Range對象。
因為Opera支持Mozilla Selection和W3C Range非常好,但是其對Microsoft Text Range的支持卻差強人意。所以顯然優(yōu)先考慮標準瀏覽器,即使用window.getSelection()。
五、userSelection的內(nèi)容
userSelection變量現(xiàn)在的內(nèi)容要么是Mozilla Selection要么就是Microsoft Text Range對象。因此它允許訪問定義在對象上的全部方法和屬性。
Mozilla Selection對象包含用戶選擇的文本內(nèi)容,如下操作:
alert(userSelection)
雖然格式并不是字符串,但是在現(xiàn)代瀏覽器下還是會彈出類似下面的內(nèi)容:
泄漏事故等級與蘇聯(lián)切爾諾貝利核電站核泄漏事故等級相同。負責調(diào)查切爾諾貝
為了從Microsoft Text Range 對象上獲得同樣的信息,你需要使用userSelection.text。為了讀取旋轉(zhuǎn)的文字,可以使用類似下面代碼:
var selectedText = userSelection;
if (userSelection.text) {
selectedText = userSelection.text;
}
現(xiàn)在selectedText就包含了用戶所選中的文字了。
您可以狠狠地點擊這里:獲取用戶所選文字demo
例如在IE7瀏覽器下,選中一段文字再點擊demo頁面上的測試按鈕,就會有類似下面的彈出內(nèi)容:

六、從Selection對象創(chuàng)建Range對象
在IE瀏覽器下,userSelection是Text Range,在現(xiàn)代瀏覽器下,userSelection仍然是Selection對象,要想同樣創(chuàng)建和Selection對象內(nèi)容一樣的Range對象可以使用類似下面代碼:
var getRangeObject = function(selectionObject) {
if (selectionObject.getRangeAt)
return selectionObject.getRangeAt(0);
else { // 較老版本Safari!
var range = document.createRange();
range.setStart(selectionObject.anchorNode,selectionObject.anchorOffset);
range.setEnd(selectionObject.focusNode,selectionObject.focusOffset);
return range;
}
}
var rangeObject = getRangeObject(userSelection);
理想情況下,我們通過Selection對象的getRangeAt()方法就可以得到W3C Range對象。此方法可以返回給定索引值的range對象。通常情況下,在JavaScript中第一個Range的索引值是0。
使用程序創(chuàng)建Range
Safari 1.3不支持getRangeAt(),因此我們要想兼顧此瀏覽器,需要使用其他的方法創(chuàng)建新的Range對象。顯然,顯示創(chuàng)建一個對象:
var range = document.createRange();
上面的一行代碼創(chuàng)建了一個空的Range,為了插入內(nèi)容,我們需要通過setStart()和setEnd()方法定義起止點。
這兩個方法需要兩個參數(shù):
1. Range起止的DOM節(jié)點
2. Range起止的文本偏移。該偏移指選中文字第一個字符和最后一個字符在文本節(jié)點中的位置。
setStart()的兩個參數(shù)屬性為startContainer和startOffset;setEnd()兩個參數(shù)屬性為endContainer和endOffset。
以下面這個例子舉例:
<p>男人,即使到了50歲,也千萬不要碰超過26歲還沒有結婚的女人。她可以是離婚,喪偶等等的,但是絕對不能是沒有結婚。超過了26歲沒有結婚,這種女人一般心理變態(tài),不然就是有嚴重問題。市場很少犯錯。即使它犯了錯,那被你撿到寶的概率也很小。</p>
<p>婚姻市場未來的變化將會是很有趣的問題,而且對未來大陸經(jīng)濟的走勢也有舉足輕重的影響,對于行業(yè)的分布,經(jīng)濟的整體效率有決定性的影響。</p>
<ol>
<li>為什么是26這個準確的數(shù)字?</li>
<li>找罵帖</li>
<li>言論是對的,在100年前,lz穿越了而已。</li>
</ol>此處Range開始于第二個<p>節(jié)點,結束與第一個<li>節(jié)點。(通常文本節(jié)點的第一個字符的索引是0。)
由于<p>節(jié)點處的文字偏移值是8, <li>節(jié)點處的偏移是5,因而有:
var startP = [the p node];
var endLi = [the second li node];
range.setStart(startP, 8);
range.setEnd(endLi, 5);
讀取起止選中內(nèi)容
上面提到了setStart(startContainer, startOffset)以及setEnd(endContainer, endOffset)??紤]到實際情況,你很難準確知道用戶選擇的文字的起始位置,所以,上面一板一眼賦予偏移值的方法顯然有很大的局限性。好在任何(看參見上面的兼容性表格)Range對象有4個屬性是用來定義選擇內(nèi)容起止點的,這4個屬性與Selection對象相似,但是卻是不同的名稱:anchorNode/anchorOffset定義選擇的起始,focusNode/focusOffset定義結束。
因此,上面的腳本創(chuàng)建選區(qū)可以使用如下代碼實現(xiàn):
range.setStart(selectionObject.anchorNode,selectionObject.anchorOffset);
range.setEnd(selectionObject.focusNode,selectionObject.focusOffset);
Safari的多慮
現(xiàn)在已經(jīng)是2011年了,釋小龍都有緋聞了,Safari 5已經(jīng)出來好些日子了。所以,如果僅僅是為了兼顧低版本的Safari而去使用程序創(chuàng)建Range,我覺得是一點必要都沒有。尤其在我們這個神奇的國度上,首先使用Safari就少,低版本的就少之又少,Safari老早就已經(jīng)支持getRangeAt()了,Chrome瀏覽器也是如此。
您可以狠狠地點擊這里:Safari下getRangeAt測試demo
選擇demo頁面中的任意一部分文字,然后點擊測試按鈕,在較新版本的Safari瀏覽器下就會出現(xiàn)類似下圖的結果:

所以,在當前環(huán)境下,要想將Selection對象轉(zhuǎn)換成Range對象,直接如下代碼就OK了(完整版):
var userSelection, rangeObject;
if (window.getSelection) {
//現(xiàn)代瀏覽器
userSelection = window.getSelection();
} else if (document.selection) {
//IE瀏覽器 考慮到Opera,應該放在后面
userSelection = document.selection.createRange();
}
//Range對象
rangeObject = userSelection;
if (userSelection.getRangeAt) {
//現(xiàn)代瀏覽器
rangeObject = userSelection.getRangeAt(0);
}
七、rangy – JavaScript Range&Selection庫
項目地址:http://code.google.com/p/rangy/
就在幾天前,rangy更新到了版本1.1,作者還新更新了四五個示意的頁面,展示了相關的API,方法和屬性等。雖然如此,由于實例較少,還是讓人很難知道此JavaScript庫如何使用。這里就舉幾個簡單的例子示意下。//zxx:此插件非壓縮達115K,個人覺得有些龐大,在實際項目中的應用價值不大

示例1,獲取用戶選中的文字:
您可以狠狠地點擊這里:rangy獲取用戶選中文字demo
選中部分文字點擊按鈕,會有如下彈出(截自Firefox3.6):

相關JavaScript代碼如下:
var sel = rangy.getSelection();
alert(sel.toString());
示例2,給選中文字添加背景
您可以狠狠地點擊這里:文字選中添加背景圖demo
選中頁面上一段文字,然后失去焦點,就會看到文字后面有了個美女背景圖,如下截圖,截自IE7瀏覽器:

完整JavaScript代碼如下:
<script type="text/javascript" src="http://www.zhangxinxu.com/study/201104/rangy/rangy-core.js"></script>
<script type="text/javascript" src="http://www.zhangxinxu.com/study/201104/rangy/rangy-cssclassapplier.js"></script>
<script>
var cssApplier;
window.onload = function() {
rangy.init();
cssApplier = rangy.createCssClassApplier("selectClass", true);
document.body.onmouseup = function() {
cssApplier.toggleSelection();
};
};
</script>
如果您看到下面的文字,可能是由于在其他網(wǎng)站或是RSS中閱讀本文,本文原地址:http://www.zhangxinxu.com/wordpress/?p=1591,本文作者:張鑫旭,來自張鑫旭-鑫空間-鑫生活,訪問原出處更多優(yōu)秀技術文章。
八、實際的應用
微博之插入話題
差不多去年這個時候,自己折騰過JS 文本域光標處添加文字并選中的內(nèi)容,也是拿的新浪微博示例的,文章是“新浪微博插入話題后部分文字選中的js實現(xiàn)”,但是去年這篇文章多實現(xiàn)的話題插入效果是比較弱的:
1. 選中普通文字不能作為話題插入
2. 話題只能插在文本域最后二不是光標處
3. 默認文字的話題可以重復插入
所以,趁這個機會,正好把微博之插入話題這個功能完善下。
您可以狠狠地點擊這里:微博插入話題的效果實現(xiàn)demo
歡迎輸入內(nèi)容,點擊測試。大致會有類似下面的效果(截自Chrome):

源代碼有些高度,為了節(jié)約篇幅,這里就不展示出來了,您可以在demo頁面中看到完整的CSS/HTML/JS代碼。不過JS部分半封裝,您要是有興趣可以在外面包裹一個函數(shù)使其插件化,我是懶得再去折騰了。
九、結語相關
對于Range
相關的知識即使到現(xiàn)在都是半生不熟的,所以文章的內(nèi)容更多的算是翻譯性質(zhì)的內(nèi)容。自己并沒有從深入理解的基礎上很淺顯地剖析相關知識點,文章很多地方會顯得不怎么通俗易懂。
文中多展示的Range
等兼容性表格的數(shù)據(jù)都是N年前的,還是Safari 1.3時代的數(shù)據(jù),老的牙都掉了,實用價值大打折扣,不過可以告知的是先前現(xiàn)代瀏覽器所不支持的個別屬性現(xiàn)早就支持了。
跌跌撞撞,滾滾爬爬。文章難免有表述不準確的地方,歡迎指正。也歡迎提交相關的腳本的bug。
- Introduction to Range
- W3C DOM Compatibility – Range
- rangy – A cross-browser JavaScript range and selection library
- Reveal a Background Image upon Text Selection
原創(chuàng)文章,轉(zhuǎn)載請注明來自張鑫旭-鑫空間-鑫生活
相關文章
深入解析JavaScript中函數(shù)的Currying柯里化
這篇文章主要介紹了JavaScript中函數(shù)的Currying柯里化,Currying 的重要意義在于可以把函數(shù)完全變成"接受一個參數(shù)、返回一個值"的固定形式,需要的朋友可以參考下2016-03-03