如果你做過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 wholenode
selectNodeContents(node)
set range to select the wholenode
contentscollapse(toStart)
iftoStart=true
set 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)
– insertnode
into the document at the beginning of the rangesurroundContents(node)
– wrapnode
around 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 inanchorNode
where the selection starts,focusNode
– the node where the selection ends,focusOffset
– the offset infocusNode
where the selection ends,isCollapsed
–true
if selection selects nothing (empty range), or doesn’t exist.rangeCount
– count of ranges in the selection, maximum1
in 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, only0
is used.addRange(range)
– addrange
to selection. All browsers except Firefox ignore the call, if the selection already has an associated range.removeRange(range)
– removerange
from 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/anchorOffset
and 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.