在本章節中,我們將介紹文件中的選取,以及表單欄位中的選取,例如 <input>
。
JavaScript 可以存取現有的選取,選取/取消選取 DOM 節點的全部或部分,從文件中移除選取的內容,將其包裝成標籤,等等。
您可以在章節結尾的「摘要」區段中找到一些常見任務的範例。也許這涵蓋了您目前的需要,但如果您閱讀全文,您將獲得更多收穫。
底層的 Range
和 Selection
物件很容易理解,然後您就不需要任何範例就能讓它們執行您想要的操作。
範圍
選取的基本概念是 範圍,它基本上是一對「邊界點」:範圍開始和範圍結束。
Range
物件是在沒有參數的情況下建立的
let range = new Range();
然後,我們可以使用 range.setStart(node, offset)
和 range.setEnd(node, offset)
來設定選取範圍。
正如你所猜測的,我們將進一步使用 Range
物件來進行選取,但首先讓我們建立幾個這樣的物件。
部分選取文字
有趣的是,這兩個方法中的第一個引數 node
可以是文字節點或元素節點,而第二個引數的意義取決於此。
如果 node
是文字節點,則 offset
必須是其文字中的位置。
例如,給定元素 <p>Hello</p>
,我們可以建立包含字母「ll」的範圍,如下所示
<p id="p">Hello</p>
<script>
let range = new Range();
range.setStart(p.firstChild, 2);
range.setEnd(p.firstChild, 4);
// toString of a range returns its content as text
console.log(range); // ll
</script>
這裡我們取 <p>
的第一個子節點(即文字節點),並指定其內部的文字位置
選取元素節點
或者,如果 node
是元素節點,則 offset
必須是子節點的編號。
這對於建立包含節點整體的範圍非常方便,而不是停在文字內部的某個地方。
例如,我們有一個更複雜的文件片段
<p id="p">Example: <i>italic</i> and <b>bold</b></p>
以下是其包含元素節點和文字節點的 DOM 結構
讓我們為 "範例:<i>斜體</i>"
建立一個範圍。
正如我們所看到的,這個片語恰好包含 <p>
的兩個子節點,索引為 0
和 1
-
起點的父節點為
<p>
,node
為0
。因此,我們可以將其設定為
range.setStart(p, 0)
。 -
終點的父節點也為
<p>
,node
為2
(它指定範圍直到,但不包含offset
)。因此,我們可以將其設定為
range.setEnd(p, 2)
。
以下是範例。如果你執行它,你可以看到文字被選取
<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
console.log(range); // Example: italic
// apply this range for document selection (explained later below)
document.getSelection().addRange(range);
</script>
這裡有一個更靈活的測試台,你可以在其中設定範圍的開始/結束數字並探索其他變體
<p id="p">Example: <i>italic</i> and <b>bold</b></p>
From <input id="start" type="number" value=1> – To <input id="end" type="number" value=4>
<button id="button">Click to select</button>
<script>
button.onclick = () => {
let range = new Range();
range.setStart(p, start.value);
range.setEnd(p, end.value);
// apply the selection, explained later below
document.getSelection().removeAllRanges();
document.getSelection().addRange(range);
};
</script>
例如,在同一個 <p>
中從偏移量 1
到 4
選取會給我們範圍 <i>斜體</i> 和 <b>粗體</b>
我們不必在 setStart
和 setEnd
中使用同一個節點。一個範圍可以跨越許多不相關的節點。重要的是,結束點在文件中必須在開始點之後。
選取更大的片段
讓我們在範例中進行更大的選取,如下所示
我們已經知道如何這樣做。我們只需要將開始和結束設定為文字節點中的相對偏移量即可。
我們需要建立一個範圍,它
- 從
<p>
第一個子節點中的位置 2 開始(取「範例:」的前兩個字母以外的所有字母) - 結束於
<b>
第一個子節點中的位置 3(取「粗體」的前三個字母,但不再取更多)
<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);
console.log(range); // ample: italic and bol
// use this range for selection (explained later)
window.getSelection().addRange(range);
</script>
正如你所看到的,建立我們想要的任何範圍都相當容易。
如果我們想取節點整體,我們可以在 setStart/setEnd
中傳遞元素。否則,我們可以在文字層級上工作。
範圍屬性
在上述範例中建立的範圍物件具有下列屬性
startContainer
、startOffset
– 起始節點和偏移量,- 在上述範例中:
<p>
內的第一個文字節點和2
。
- 在上述範例中:
endContainer
、endOffset
– 結束節點和偏移量,- 在上述範例中:
<b>
內的第一個文字節點和3
。
- 在上述範例中:
collapsed
– 布林值,如果範圍開始和結束於同一點(因此範圍內沒有內容),則為true
,- 在上述範例中:
false
- 在上述範例中:
commonAncestorContainer
– 範圍內所有節點最近的共用祖先,- 在上述範例中:
<p>
- 在上述範例中:
範圍選取方法
有許多便利的方法可操作範圍。
我們已經看過 setStart
和 setEnd
,以下是其他類似的函式。
設定範圍開始
setStart(node, offset)
在node
中的offset
位置設定開始setStartBefore(node)
在node
正前方設定開始setStartAfter(node)
在node
正後方設定開始
設定範圍結束(類似的方法)
setEnd(node, offset)
在node
中的offset
位置設定結束setEndBefore(node)
在node
正前方設定結束setEndAfter(node)
在node
正後方設定結束
技術上來說,setStart/setEnd
可以執行任何動作,但更多的方法提供了更多便利性。
在所有這些方法中,node
可以是文字節點或元素節點:對於文字節點,offset
會跳過這麼多個字元,而對於元素節點,則會跳過這麼多個子節點。
更多建立範圍的方法
selectNode(node)
設定範圍以選取整個node
selectNodeContents(node)
設定範圍以選取整個node
內容collapse(toStart)
如果toStart=true
,則設定結束等於開始,否則設定開始等於結束,因此範圍會縮小cloneRange()
建立一個具有相同開始/結束的新範圍
範圍編輯方法
建立範圍後,我們可以使用這些方法來操作其內容
deleteContents()
– 從文件中移除範圍內容extractContents()
– 從文件中移除範圍內容,並以 DocumentFragment 的形式傳回cloneContents()
– 複製範圍內容,並以 DocumentFragment 的形式傳回insertNode(node)
– 在範圍開頭將node
插入文件中surroundContents(node)
– 將node
包圍在範圍內容周圍。為此,範圍必須包含其內部所有元素的開啟和關閉標籤:沒有像<i>abc
這樣的部分範圍。
使用這些方法,我們基本上可以對選取的節點執行任何操作。
以下是查看它們實際運作的測試台
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) { console.log(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>
還有比較範圍的方法,但這些方法很少使用。當您需要時,請參閱 規格 或 MDN 手冊。
選取
Range
是用於管理選取範圍的通用物件。不過,建立 Range
並不表示我們會在螢幕上看到選取。
我們可以建立 Range
物件,並將它們傳遞出去,它們本身並不會在視覺上選取任何內容。
文件選取由 Selection
物件表示,可以作為 window.getSelection()
或 document.getSelection()
取得。選取可能包含零個或多個範圍。至少,選取 API 規格 是這麼說的。但在實務上,只有 Firefox 允許使用 Ctrl+click (Cmd+click 適用於 Mac) 在文件中選取多個範圍。
以下是在 Firefox 中建立 3 個範圍的選取的螢幕截圖
其他瀏覽器最多支援 1 個範圍。正如我們將看到的,有些 Selection
方法暗示可能有多個範圍,但同樣地,在 Firefox 以外的所有瀏覽器中,最多只有 1 個。
以下是一個小範例,顯示目前的選取(選取一些內容並按一下)為文字
選取屬性
如前所述,選取理論上可能包含多個範圍。我們可以使用下列方法取得這些範圍物件
getRangeAt(i)
- 取得第 i 個範圍,從0
開始。在 Firefox 以外的所有瀏覽器中,只使用0
。
此外,還有一些屬性通常提供更好的便利性。
與範圍類似,選取物件有一個開始,稱為「錨點」,以及一個結束,稱為「焦點」。
主要的選取屬性為
anchorNode
- 選取開始的節點,anchorOffset
- 選取在anchorNode
中開始的偏移量,focusNode
- 選取結束的節點,focusOffset
- 選取在focusNode
中結束的偏移量,isCollapsed
- 如果選取沒有選取任何內容(空範圍),或不存在,則為true
。rangeCount
- 選取中的範圍數量,在 Firefox 以外的所有瀏覽器中最多為1
。
選取錨點/焦點與 Range
開始/結束之間有重要的差異。
如我們所知,Range
物件的開始總是在結束之前。
對於選取,並非總是如此。
使用滑鼠選取內容可以朝兩個方向進行:從「左到右」或「右到左」。
換句話說,當按下滑鼠按鈕,然後在文件中向前移動時,其結束位置(焦點)會在開始位置(錨點)之後。
例如,如果使用者從「範例」開始,使用滑鼠選取到「斜體」
…但也可以反向進行相同的選取:從「斜體」開始到「範例」(反向),則其結束位置(焦點)會在開始位置(錨點)之前
選取事件
有一些事件可供追蹤選取
elem.onselectstart
– 當選取特別在元素elem
(或其內部)開始時。例如,當使用者在元素上按下滑鼠按鈕並開始移動指標時。- 防止預設動作會取消選取開始。因此,從此元素開始選取將變得不可能,但元素仍然可選取。訪客只需從其他地方開始選取即可。
document.onselectionchange
– 每當選取變更或開始時。- 請注意:此處理常式只能設定在
document
上,它會追蹤其中的所有選取。
- 請注意:此處理常式只能設定在
選取追蹤示範
以下是一個小示範。它會追蹤 document
上目前的選取,並顯示其邊界
<p id="p">Select me: <i>italic</i> and <b>bold</b></p>
From <input id="from" disabled> – To <input id="to" disabled>
<script>
document.onselectionchange = function() {
let selection = document.getSelection();
let {anchorNode, anchorOffset, focusNode, focusOffset} = selection;
// anchorNode and focusNode are text nodes usually
from.value = `${anchorNode?.data}, offset ${anchorOffset}`;
to.value = `${focusNode?.data}, offset ${focusOffset}`;
};
</script>
選取複製示範
有兩種方法可以複製選取的內容
- 我們可以使用
document.getSelection().toString()
將其取得為文字。 - 否則,若要複製完整的 DOM,例如,如果我們需要保留格式,可以使用
getRangeAt(...)
取得基礎範圍。Range
物件反過來有cloneContents()
方法,可以複製其內容並傳回DocumentFragment
物件,我們可以將其插入其他地方。
以下示範如何將選取的內容複製為文字和 DOM 節點
<p id="p">Select me: <i>italic</i> and <b>bold</b></p>
Cloned: <span id="cloned"></span>
<br>
As text: <span id="astext"></span>
<script>
document.onselectionchange = function() {
let selection = document.getSelection();
cloned.innerHTML = astext.innerHTML = "";
// Clone DOM nodes from ranges (we support multiselect here)
for (let i = 0; i < selection.rangeCount; i++) {
cloned.append(selection.getRangeAt(i).cloneContents());
}
// Get as text
astext.innerHTML += selection;
};
</script>
選取方法
我們可以透過新增/移除範圍來處理選取
getRangeAt(i)
- 取得第 i 個範圍,從0
開始。在 Firefox 以外的所有瀏覽器中,只使用0
。addRange(range)
– 將range
新增到選取。除了 Firefox 以外的所有瀏覽器都會忽略此呼叫,如果選取已經有相關聯的範圍。removeRange(range)
– 從選取中移除range
。removeAllRanges()
– 移除所有範圍。empty()
–removeAllRanges
的別名。
還有一些方便的方法可以直接處理選取範圍,而不需要中間的 Range
呼叫
collapse(node, offset)
– 以一個新的範圍取代選取的範圍,該範圍從指定的node
的位置offset
開始和結束。setPosition(node, offset)
–collapse
的別名。collapseToStart()
– 縮合(以一個空範圍取代)到選取開始collapseToEnd()
– 縮合到選取結束extend(node, offset)
– 將選取的焦點移到指定的node
的位置offset
setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset)
– 以指定的開始anchorNode/anchorOffset
和結束focusNode/focusOffset
取代選取範圍。它們之間的所有內容都會被選取。selectAllChildren(node)
– 選取node
的所有子節點。deleteFromDocument()
– 從文件中移除選取的內容。containsNode(node, allowPartialContainment = false)
– 檢查選取是否包含node
(如果第二個參數為true
,則為部分包含)
對於大多數任務,這些方法就夠用了,不需要存取底層的 Range
物件。
例如,選取段落 <p>
的全部內容
<p id="p">Select me: <i>italic</i> and <b>bold</b></p>
<script>
// select from 0th child of <p> to the last child
document.getSelection().setBaseAndExtent(p, 0, p, p.childNodes.length);
</script>
使用範圍選取相同內容
<p id="p">Select me: <i>italic</i> and <b>bold</b></p>
<script>
let range = new Range();
range.selectNodeContents(p); // or selectNode(p) to select the <p> tag too
document.getSelection().removeAllRanges(); // clear existing selection if any
document.getSelection().addRange(range);
</script>
如果文件選取已存在,請先使用 removeAllRanges()
清空它。然後再新增範圍。否則,除了 Firefox 之外的所有瀏覽器都會忽略新的範圍。
例外情況是某些選取方法,例如 setBaseAndExtent
,會取代現有的選取。
表單控制項中的選取
表單元素,例如 input
和 textarea
,提供 特殊的選取 API,不使用 Selection
或 Range
物件。由於輸入值是純文字,而不是 HTML,因此不需要這些物件,一切都簡單多了。
屬性
input.selectionStart
– 選取起點位置(可寫入),input.selectionEnd
– 選取終點位置(可寫入),input.selectionDirection
– 選取方向,其中之一:「forward」、「backward」或「none」(例如,如果使用滑鼠雙擊選取),
事件
input.onselect
– 當選取某個內容時觸發。
方法
-
input.select()
– 選取文字控制項中的所有內容(可以是textarea
,而不是input
), -
input.setSelectionRange(start, end, [direction])
– 將選取變更為從位置start
延伸到end
,在給定的方向(可選)。 -
input.setRangeText(replacement, [start], [end], [selectionMode])
– 使用新文字取代一段文字範圍。如果提供了選用的參數
start
和end
,則設定範圍的起點和終點,否則使用使用者的選取。最後一個參數
selectionMode
決定在文字被取代後如何設定選取。可能的數值為"select"
– 新插入的文字將會被選取。"start"
– 選取範圍會在插入的文字之前收合(游標會在文字之前)。"end"
– 選取範圍會在插入的文字之後收合(游標會在文字之後)。"保留"
– 嘗試保留選取。這是預設值。
現在讓我們看看這些方法的實際應用。
範例:追蹤選取
例如,這個程式碼使用 onselect
事件追蹤選取
<textarea id="area" style="width:80%;height:60px">
Selecting in this text updates values below.
</textarea>
<br>
From <input id="from" disabled> – To <input id="to" disabled>
<script>
area.onselect = function() {
from.value = area.selectionStart;
to.value = area.selectionEnd;
};
</script>
請注意
onselect
在選取某個項目時觸發,但選取移除時不會觸發。- 根據 規範,
document.onselectionchange
事件不應觸發表單控制項內的選取,因為這與document
選取和範圍無關。有些瀏覽器會產生它,但我們不應依賴它。
範例:移動游標
我們可以變更設定選取的 selectionStart
和 selectionEnd
。
一個重要的邊界情況是 selectionStart
和 selectionEnd
相等。這時就是游標位置。或者換句話說,當沒有選取任何內容時,選取會在游標位置處縮合。
因此,透過將 selectionStart
和 selectionEnd
設定為相同的值,我們可以移動游標。
例如
<textarea id="area" style="width:80%;height:60px">
Focus on me, the cursor will be at position 10.
</textarea>
<script>
area.onfocus = () => {
// zero delay setTimeout to run after browser "focus" action finishes
setTimeout(() => {
// we can set any selection
// if start=end, the cursor is exactly at that place
area.selectionStart = area.selectionEnd = 10;
});
};
</script>
範例:修改選取
若要修改選取的內容,我們可以使用 input.setRangeText()
方法。當然,我們可以讀取 selectionStart/End
,並根據選取的知識變更 value
的對應子字串,但 setRangeText
更強大,而且通常更方便。
這是一個有點複雜的方法。在其最簡單的單一參數形式中,它會取代使用者選取的範圍並移除選取。
例如,這裡使用者選取的內容會被 *...*
包圍
<input id="input" style="width:200px" value="Select here and click the button">
<button id="button">Wrap selection in stars *...*</button>
<script>
button.onclick = () => {
if (input.selectionStart == input.selectionEnd) {
return; // nothing is selected
}
let selected = input.value.slice(input.selectionStart, input.selectionEnd);
input.setRangeText(`*${selected}*`);
};
</script>
透過更多參數,我們可以設定範圍的 開始
和 結束
。
在這個範例中,我們在輸入文字中找到 "THIS"
,取代它並保持取代後的選取狀態
<input id="input" style="width:200px" value="Replace THIS in text">
<button id="button">Replace THIS</button>
<script>
button.onclick = () => {
let pos = input.value.indexOf("THIS");
if (pos >= 0) {
input.setRangeText("*THIS*", pos, pos + 4, "select");
input.focus(); // focus to make selection visible
}
};
</script>
範例:在游標處插入
如果沒有選取任何內容,或者我們在 setRangeText
中使用相等的 開始
和 結束
,則新文字會直接插入,不會移除任何內容。
我們也可以使用 setRangeText
在 "游標處"
插入一些內容。
這裡有一個按鈕會在游標位置插入 "HELLO"
,並將游標放在它之後。如果選取不為空,則會取代它(我們可以透過比較 selectionStart!=selectionEnd
來偵測它,並執行其他操作)
<input id="input" style="width:200px" value="Text Text Text Text Text">
<button id="button">Insert "HELLO" at cursor</button>
<script>
button.onclick = () => {
input.setRangeText("HELLO", input.selectionStart, input.selectionEnd, "end");
input.focus();
};
</script>
無法選取
要讓某個東西無法選取,有以下三種方法
-
使用 CSS 屬性
user-select: none
。<style> #elem { user-select: none; } </style> <div>Selectable <div id="elem">Unselectable</div> Selectable</div>
這無法讓選取從
elem
開始。但使用者可以在其他地方開始選取,並將elem
包含在其中。然後
elem
會變成document.getSelection()
的一部分,因此選取實際上會發生,但其內容通常會在複製貼上時被忽略。 -
在
onselectstart
或mousedown
事件中防止預設動作。<div>Selectable <div id="elem">Unselectable</div> Selectable</div> <script> elem.onselectstart = () => false; </script>
這會防止在
elem
上開始選取,但訪客可以在另一個元素上開始選取,然後延伸到elem
。當同一個動作上有另一個事件處理常式會觸發選取(例如
mousedown
)時,這很方便。因此我們停用選取以避免衝突,同時仍允許複製elem
內容。 -
我們還可以在選取發生後使用
document.getSelection().empty()
來清除選取。這很少使用,因為這會導致選取出現和消失時產生不需要的閃爍。
參考文獻
摘要
我們介紹了兩個不同的選取 API
- 對於文件:
Selection
和Range
物件。 - 對於
input
、textarea
:其他方法和屬性。
第二個 API 非常簡單,因為它使用文字。
最常用的範例可能是
- 取得選取
let selection = document.getSelection(); let cloned = /* element to clone the selected nodes to */; // then apply Range methods to selection.getRangeAt(0) // or, like here, to all ranges to support multi-select for (let i = 0; i < selection.rangeCount; i++) { cloned.append(selection.getRangeAt(i).cloneContents()); }
- 設定選取
let selection = document.getSelection(); // directly: selection.setBaseAndExtent(...from...to...); // or we can create a range and: selection.removeAllRanges(); selection.addRange(range);
最後,關於游標。在可編輯元素(例如 <textarea>
)中的游標位置總是位於選取的開始或結束處。我們可以使用它來取得游標位置或透過設定 elem.selectionStart
和 elem.selectionEnd
來移動游標。
留言
<code>
標籤;若要插入多行程式碼,請將它們包在<pre>
標籤中;若要插入 10 行以上的程式碼,請使用沙盒 (plnkr、jsbin、codepen…)