如果你做過wysiwyg這樣的app,一個很讓人頭疼的問題是如何保證執行bold,italic等格式化操作后保持先前鼠標所在的位置。要好好的解決這個問題,就必須將Selection和Range的api搞搞清楚。
https://javascript.info/selection-range
Selection and Range
js可以獲得當前的選中區域信息,可以選擇或者去選擇部分或者全部內容,清楚document中的選中部分,使用一個心的tag來進行包裹等操作。所有這些操作的基石就是Selction和Range這兩個api.
Range
選擇區的基本概念是Range:它是一對邊界點組成,分別定義range的start和end.
每一個端點都是以相對於父DOM Node的offset這些信息來表達的point。如果父親node是一個element element node,那么offset就是child的number號,兒對於text node,則是在text中的位置。我們以例子來說明,我們以選中某些內容為例:
首先,我們可以創建一個range:
let range = new Range();
然后我們可以通過使用 range.setStart(node, offset), range.setEnd(node, offset) 這兩個api函數來設定range的邊界,比如,如果我們的html代碼如下:
<p id="p">Example: <i>italic</i> and <b>bold</b></p>
其對應的dom樹如下:

我們來選擇 Example: <i>italic</i> 這一部分內容。它實際上是p這個父元素的前面兩個兒子節點(包含text node)

我們來看實際的代碼:
<p id="p">Example: <i>italic</i> and <b>bold</b></p> <script> let range = new Range(); range.setStart(p, 0); range.setEnd(p, 2); // toString of a range returns its content as text (without tags) alert(range); // Example: italic // apply this range for document selection (explained later)
document.getSelection().removeAllRanges();
document.getSelection().addRange(range); </script>
- range.setStart(p,0)- 設定該選擇范圍是p父元素的第0個child節點(也就是一個text node: Example: )
- range.setEnd(p,2)-指定該range將延展到p父元素的第2個child(也就是" and "這個text node),但是注意這里是不包含額,也就是說實際上是到第1個child,因此也就是 i 節點
需要注意的是我們實際上不需要在setStart和setEnd調用中使用同一個參考node節點,一個范圍可能延展涵蓋到多個不相關的節點。唯一需要注意的是end必須是在start的后面
選中text nodes的部分,而非全部
假設我們想像下面的情況來做選中操作:

這也可以使用代碼輕松實現,我們需要做的是設定start和end時使用相對於text nodes的offset位置就好了。
我們需要先創建一個range:
1. range的start是p父親元素的first child的position 2,也就是"ample:"
2.range的end則是b父親元素的position 3,也就是"bol"
<p id="p">Example: <i>italic</i> and <b>bold</b></p> <script> let range = new Range(); range.setStart(p.firstChild, 2); range.setEnd(p.querySelector('b').firstChild, 3); alert(range); // ample: italic and bol // use this range for selection (explained later) document.getSelection().removeAllRanges();?? window.getSelection().addRange(range); </script>
這時,range屬性如下圖取值:

- startContainer,startOffset-分別指定start點的node和相對於該node的offset,本例中是p節點的首個text node子節點,以及第2個position
- endContainer,endOffset-分別指定end點的node和offset,本例中是b節點的首個text node子節點,以及position 3
- collapsed - 布爾值,如果star和end point都指向了同一個point的話為true,也意味着在該range中沒有內容被選中,本例中取值為false
- commonAncestorContainer - 在本range中所有節點的最近的共同祖先節點,本例中為p節點
Range的方法methods
range對象有很多有用的方法用於操作range:
設定range的start:
setEnd(node, offset) set end at: position offset in node
setEndBefore(node) set end at: right before node
setEndAfter(node) set end at: right after node
正如前面演示的那樣,node可以是一個text或者element node,對於text node, offset意思是忽略幾個字符,而如果是element node,則指忽略多少個child nodes
其他的方法:
selectNode(node)set range to select the wholenodeselectNodeContents(node)set range to select the wholenodecontentscollapse(toStart)iftoStart=trueset end=start, otherwise set start=end, thus collapsing the rangecloneRange()creates a new range with the same start/end
用於操作range的內容的方法:
deleteContents()– remove range content from the documentextractContents()– remove range content from the document and return as DocumentFragmentcloneContents()– clone range content and return as DocumentFragmentinsertNode(node)– insertnodeinto the document at the beginning of the rangesurroundContents(node)– wrapnodearound range content. For this to work, the range must contain both opening and closing tags for all elements inside it: no partial ranges like<i>abc.
有了這些有用的方法,我們就可以基本上針對選中的nodes做任何事情了,看下面一個比價復雜的例子:
Click buttons to run methods on the selection, "resetExample" to reset it. <p id="p">Example: <i>italic</i> and <b>bold</b></p> <p id="result"></p> <script> let range = new Range(); // Each demonstrated method is represented here: let methods = { deleteContents() { range.deleteContents() }, extractContents() { let content = range.extractContents(); result.innerHTML = ""; result.append("extracted: ", content); }, cloneContents() { let content = range.cloneContents(); result.innerHTML = ""; result.append("cloned: ", content); }, insertNode() { let newNode = document.createElement('u'); newNode.innerHTML = "NEW NODE"; range.insertNode(newNode); }, surroundContents() { let newNode = document.createElement('u'); try { range.surroundContents(newNode); } catch(e) { alert(e) } }, resetExample() { p.innerHTML = `Example: <i>italic</i> and <b>bold</b>`; result.innerHTML = ""; range.setStart(p.firstChild, 2); range.setEnd(p.querySelector('b').firstChild, 3); window.getSelection().removeAllRanges(); window.getSelection().addRange(range); } }; for(let method in methods) { document.write(`<div><button onclick="methods.${method}()">${method}</button></div>`); } methods.resetExample(); </script>
除此之外,還有一些很少使用的用於比較range的api,https://developer.mozilla.org/en-US/docs/Web/API/Range
Selection
Range是一個用於管理selection ranges的通用對象。我們可以創建這些range對象,然后傳遞給dom api
document的selection是由Selection對象來表征的,這可以通過 window.getSelection()或者document.getSelection() 來獲得。
一個selection可以包括0個或者多個ranges,但是在實際使用中,僅僅firefox允許選中多個ranges,這需要通過ctrl+click來實現,比如下圖:

selection對象的屬性
和range類似,一個selection也有start,被稱為"anchor",和一個end,被稱為"focus",主要的屬性如下:
anchorNode– the node where the selection starts,anchorOffset– the offset inanchorNodewhere the selection starts,focusNode– the node where the selection ends,focusOffset– the offset infocusNodewhere the selection ends,isCollapsed–trueif selection selects nothing (empty range), or doesn’t exist.rangeCount– count of ranges in the selection, maximum1in all browsers except Firefox.
selection events
1. elem.onselectstart -當一個selection從elem這個元素開始發生時,比如用戶當按下左鍵同時拖動鼠標時就會發生該事件。需要注意的是,如果elem被prevent default時,不發生該事件
2. document.onselectionchange,這個事件只能在document上發生,只要有selection發生變化就會觸發該事件
看以下代碼

selection的常用methods:
getRangeAt(i)– get i-th range, starting from0. In all browsers except firefox, only0is used.addRange(range)– addrangeto selection. All browsers except Firefox ignore the call, if the selection already has an associated range.removeRange(range)– removerangefrom the selection.removeAllRanges()– remove all ranges.empty()– alias toremoveAllRanges
以下方法無需操作底層的range對象就可以直接完成對應的功能:
collapse(node, offset)– replace selected range with a new one that starts and ends at the givennode, at positionoffset.setPosition(node, offset)– alias tocollapse.collapseToStart()– collapse (replace with an empty range) to selection start,collapseToEnd()– collapse to selection end,extend(node, offset)– move focus of the selection to the givennode, positionoffset,setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset)– replace selection range with the given startanchorNode/anchorOffsetand endfocusNode/focusOffset. All content in-between them is selected.selectAllChildren(node)– select all children of thenode.deleteFromDocument()– remove selected content from the document.containsNode(node, allowPartialContainment = false)– checks whether the selection containsnode(partically if the second argument istrue)
我們再來看看以下例子代碼及其效果:

Selection in form controls
Form元素,比如input, textarea則提供了更多的api用於selection操作和處理,而沒有selection或者說range對象。由於input的value僅僅是text,而非html,因此也沒有必要提供這些selection和range對象,事情會變得更加簡單。
input.selectionStart– position of selection start (writeable),input.selectionEnd– position of selection start (writeable),input.selectionDirection– selection direction, one of: “forward”, “backward” or “none” (if e.g. selected with a double mouse click)
input.onselect – triggers when something is selected.
