この章では、ドキュメント内の選択と、<input>
などのフォームフィールドでの選択について説明します。
JavaScript は、既存の選択範囲にアクセスしたり、DOM ノード全体または一部を選択/選択解除したり、選択したコンテンツをドキュメントから削除したり、タグで囲んだりすることができます。
一般的なタスクのレシピは、章の最後にある「まとめ」セクションにあります。おそらくそれで現在のニーズは満たされるでしょうが、全文を読めばもっと多くのことが得られます。
基礎となる Range
オブジェクトと Selection
オブジェクトは理解しやすく、それらを理解すれば、レシピがなくても思い通りに操作できます。
Range
選択の基本概念は Range で、これは本質的に「境界点」のペア、つまり Range の開始点と終了点です。
Range
オブジェクトはパラメータなしで作成されます
let range = new Range();
その後、range.setStart(node, offset)
と range.setEnd(node, offset)
を使用して選択範囲を設定できます。
ご想像のとおり、さらに Range
オブジェクトを選択に使用しますが、まずはそのようなオブジェクトをいくつか作成してみましょう。
テキストの部分的な選択
興味深いのは、両方のメソッドの最初の引数 node
がテキストノードまたは要素ノードのいずれかであり、2 番目の引数の意味がそれに依存することです。
node
がテキストノードの場合、offset
はそのテキスト内の位置でなければなりません。
たとえば、要素 <p>Hello</p>
が与えられた場合、文字「ll」を含む Range を次のように作成できます
<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
は子の番号でなければなりません。
これは、テキストの途中で停止するのではなく、ノード全体を含む Range を作成するのに便利です。
たとえば、より複雑なドキュメントフラグメントがあるとします
<p id="p">Example: <i>italic</i> and <b>bold</b></p>
要素ノードとテキストノードの両方を含む DOM 構造は次のとおりです
"Example: <i>italic</i>"
の Range を作成してみましょう。
見てのとおり、この句は <p>
のちょうど 2 つの子で構成されており、インデックスは 0
と 1
です
-
開始点は、親
node
として<p>
を、オフセットとして0
を持ちます。そのため、
range.setStart(p, 0)
として設定できます。 -
終了点も親
node
として<p>
を持ちますが、オフセットは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>
これは、Range の開始/終了番号を設定し、他のバリアントを探索できる、より柔軟なテストスタンドです
<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>italic</i> and <b>bold</b>
という Range が得られます
setStart
と setEnd
で同じノードを使用する必要はありません。Range は、多くの無関係なノードにまたがることができます。ドキュメント内で終了が開始の後にあることだけが重要です。
より大きなフラグメントの選択
例では、次のようにより大きな選択範囲を作成してみましょう
その方法はすでにわかっています。開始と終了をテキストノードの相対オフセットとして設定するだけです。
作成する必要がある Range は、
<p>
の最初の子の 2 番目の位置から開始します("Example: " の最初の 2 文字を除くすべてを取得します)<b>
の最初の子の 3 番目の位置で終了します("bold" の最初の 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>
見てのとおり、必要な範囲の Range を作成するのは非常に簡単です。
ノード全体を取得したい場合は、setStart/setEnd
に要素を渡すことができます。そうでない場合は、テキストレベルで作業できます。
Range プロパティ
上記の例で作成した Range オブジェクトには、次のプロパティがあります
startContainer
、startOffset
– 開始のノードとオフセット、- 上記の例では:
<p>
内の最初のテキストノードと2
です。
- 上記の例では:
endContainer
、endOffset
– 終了のノードとオフセット、- 上記の例では:
<b>
内の最初のテキストノードと3
です。
- 上記の例では:
collapsed
– ブール値。Range の開始と終了が同じ点にある場合(つまり、Range 内にコンテンツがない場合)はtrue
です。- 上記の例では:
false
- 上記の例では:
commonAncestorContainer
– Range 内のすべてのノードの最も近い共通の祖先、- 上記の例では:
<p>
- 上記の例では:
Range 選択メソッド
Range を操作するための便利なメソッドが多数あります。
setStart
と setEnd
はすでに見てきました。他にも同様のメソッドがあります。
Range の開始の設定
setStart(node, offset)
開始を次の場所に設定します:node
内のoffset
の位置setStartBefore(node)
開始を次の場所に設定します:node
の直前setStartAfter(node)
開始を次の場所に設定します:node
の直後
Range の終了の設定(同様のメソッド)
setEnd(node, offset)
終了を次の場所に設定します:node
内のoffset
の位置setEndBefore(node)
終了を次の場所に設定します:node
の直前setEndAfter(node)
終了を次の場所に設定します:node
の直後
技術的には、setStart/setEnd
は何でもできますが、より多くのメソッドはより多くの利便性を提供します。
これらのすべてのメソッドで、node
はテキストノードまたは要素ノードのいずれかになります。テキストノードの場合、offset
はその文字数をスキップしますが、要素ノードの場合はその子ノード数をスキップします。
Range を作成するためのさらに多くのメソッド
selectNode(node)
node
全体を選択するように Range を設定しますselectNodeContents(node)
node
のコンテンツ全体を選択するように Range を設定しますcollapse(toStart)
toStart=true
の場合は end=start を設定し、そうでない場合は start=end を設定して、Range を折りたたみますcloneRange()
同じ開始/終了を持つ新しい Range を作成します
Range 編集メソッド
Range が作成されると、次のメソッドを使用してそのコンテンツを操作できます
deleteContents()
– ドキュメントから Range のコンテンツを削除しますextractContents()
– ドキュメントから Range のコンテンツを削除し、DocumentFragment として返しますcloneContents()
– Range のコンテンツを複製し、DocumentFragment として返しますinsertNode(node)
– Range の先頭にnode
をドキュメントに挿入しますsurroundContents(node)
– Range のコンテンツをnode
で囲みます。これが機能するためには、Range にその中のすべての要素の開始タグと終了タグの両方が含まれている必要があります。<i>abc
のような部分的な Range は使用できません。
これらのメソッドを使用すると、基本的に選択したノードに対して何でもできます。
これが、それらを実際に確認するためのテストスタンドです
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>
Range を比較するためのメソッドも存在しますが、これらはめったに使用されません。必要な場合は、仕様書 または MDN マニュアル を参照してください。
Selection
Range
は、選択範囲を管理するための汎用オブジェクトです。ただし、Range
を作成しても、画面に選択範囲が表示されるわけではありません。
Range
オブジェクトを作成して渡すことができますが、それだけでは視覚的に何も選択されません。
ドキュメントの選択範囲は Selection
オブジェクトで表され、window.getSelection()
または document.getSelection()
として取得できます。選択範囲には、ゼロ個以上の Range が含まれる場合があります。少なくとも、Selection API 仕様 ではそのように述べられています。ただし実際には、Firefox のみ Ctrl+クリック (Mac の場合は Cmd+クリック) を使用してドキュメント内の複数の Range を選択できます。
これは、Firefox で作成された 3 つの Range を持つ選択範囲のスクリーンショットです
他のブラウザは最大 1 つの Range をサポートしています。後述するように、一部の Selection
メソッドは複数の Range があることを暗示していますが、繰り返しますが、Firefox を除くすべてのブラウザでは最大 1 つです。
これは、現在の選択範囲(何かを選択してクリック)をテキストとして表示する小さなデモです
Selection プロパティ
前述のとおり、選択範囲には理論的には複数の Range が含まれる場合があります。これらの Range オブジェクトは、次のメソッドを使用して取得できます
getRangeAt(i)
–0
から始まる i 番目の Range を取得します。Firefox を除くすべてのブラウザでは、0
のみを使用します。
また、より便利なプロパティも存在します。
Range と同様に、選択オブジェクトには「アンカー」と呼ばれる開始と「フォーカス」と呼ばれる終了があります。
主な Selection プロパティは次のとおりです
anchorNode
– 選択範囲が開始されるノード、anchorOffset
– 選択範囲が開始されるanchorNode
内のオフセット、focusNode
– 選択範囲が終了するノード、focusOffset
– 選択範囲が終了するfocusNode
内のオフセット、isCollapsed
– 選択範囲が何も選択していない(空の Range)場合、または存在しない場合はtrue
です。rangeCount
– 選択範囲内の Range の数。Firefox を除くすべてのブラウザでは最大1
です。
選択範囲のアンカー/フォーカスと Range
の開始/終了には重要な違いがあります。
ご存知のとおり、Range
オブジェクトは常に開始が終了の前にあります。
選択範囲の場合、必ずしもそうとは限りません。
マウスで何かを選択すると、「左から右」または「右から左」のどちらの方向でも実行できます。
言い換えれば、マウスボタンが押されて、ドキュメント内で前方に移動すると、その終了(フォーカス)は開始(アンカー)の後になります。
たとえば、ユーザーがマウスで選択を開始し、「Example」から「italic」に移動した場合
…しかし、同じ選択を逆方向に実行することもできます。「italic」から「Example」に開始する(逆方向)と、その終了(フォーカス)は開始(アンカー)の前にあります
Selection イベント
選択範囲を追跡するためのイベントがあります
elem.onselectstart
– 要素elem
上(またはその内部)で specifically 選択が*開始*されたとき。たとえば、ユーザーがマウスボタンを押してポインタを動かし始めたときです。- デフォルトアクションを防止すると、選択の開始がキャンセルされます。そのため、この要素から選択を開始することは不可能になりますが、要素は選択可能なままです。訪問者は、他の場所から選択を開始する必要があります。
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>
選択コピーのデモ
選択したコンテンツをコピーするには、2つのアプローチがあります。
document.getSelection().toString()
を使用して、テキストとして取得できます。- または、フォーマットを維持する必要がある場合など、DOM全体をコピーするには、
getRangeAt(...)
で基礎となる範囲を取得できます。Range
オブジェクトには、コンテンツを複製してDocumentFragment
オブジェクトとして返すcloneContents()
メソッドがあり、それを他の場所に挿入できます。
選択したコンテンツをテキストと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)
–0
から始まる i 番目の Range を取得します。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
が含まれているかどうかを確認します(2番目の引数が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
などのフォーム要素は、Selection
オブジェクトやRange
オブジェクトを使用せずに、選択のための特別なAPIを提供します。入力値はHTMLではなくプレーンテキストであるため、そのようなオブジェクトは必要なく、すべてがはるかに単純です。
プロパティ
input.selectionStart
– 選択開始位置(書き込み可能)、input.selectionEnd
– 選択終了位置(書き込み可能)、input.selectionDirection
– 選択方向。"forward"、"backward"、または"none"(たとえば、マウスをダブルクリックして選択した場合)のいずれかです。
イベント
input.onselect
– 何かが選択されたときにトリガーされます。
メソッド
-
input.select()
– テキストコントロール(input
の代わりにtextarea
も可)内のすべてを選択します。 -
input.setSelectionRange(start, end, [direction])
– 選択範囲を、指定された方向(オプション)で、位置start
からend
までに変更します。 -
input.setRangeText(replacement, [start], [end], [selectionMode])
– テキストの範囲を新しいテキストに置き換えます。オプションの引数
start
とend
を指定した場合、範囲の開始と終了が設定されます。そうでない場合は、ユーザーの選択範囲が使用されます。最後の引数
selectionMode
は、テキストが置き換えられた後に選択範囲をどのように設定するかを決定します。可能な値は次のとおりです。"select"
– 新しく挿入されたテキストが選択されます。"start"
– 選択範囲は、挿入されたテキストの直前で折りたたまれます(カーソルは直前に配置されます)。"end"
– 選択範囲は、挿入されたテキストの直後で折りたたまれます(カーソルは直後に配置されます)。"preserve"
– 選択範囲を維持しようとします。これがデフォルトです。
では、これらのメソッドの動作を見てみましょう。
例:選択の追跡
たとえば、次のコードは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` はより強力で、多くの場合より便利です。
これはやや複雑なメソッドです。最も単純な1つの引数の形式では、ユーザーが選択した範囲を置き換え、選択範囲を削除します。
たとえば、ここではユーザーの選択範囲が `*...*` で囲まれます。
<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>
より多くの引数を使用すると、範囲の `start` と `end` を設定できます。
この例では、入力テキストで `"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` で等しい `start` と `end` を使用した場合、新しいテキストが挿入されるだけで、何も削除されません。
`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>
選択不可にする
何かを選択不可にするには、3つの方法があります。
-
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()` を使用して、選択を事後的にクリアすることもできます。これは、選択が表示されたり消えたりするため、不要な点滅が発生するため、めったに使用されません。
参考資料
まとめ
選択のための2つの異なる API について説明しました。
- ドキュメントの場合: `Selection` オブジェクトと `Range` オブジェクト。
- `input`、`textarea` の場合: 追加のメソッドとプロパティ。
2番目の 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` を設定することでカーソルを移動したりするために使用できます。
コメント