You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: 2-ui/99-ui-misc/02-selection-range/article.md
+115-50
Original file line number
Diff line number
Diff line change
@@ -26,18 +26,45 @@ let range = new Range();
26
26
27
27
Then we can set the selection boundaries using `range.setStart(node, offset)` and `range.setEnd(node, offset)`.
28
28
29
-
The first argument `node` can be either a text node or an element node. The meaning of the second argument depends on that:
29
+
As you might guess, further we'll use the `Range` objects for selection, but first let's create few such objects.
30
30
31
-
- If `node` is a text node, then `offset` must be the position in the text.
32
-
- If `node` is an element node, then `offset` must be the child number.
31
+
### Selecting the text partially
33
32
34
-
For example, let's create a range in this fragment:
33
+
The interesting thing is that the first argument `node` in both methods can be either a text node or an element node, and the meaning of the second argument depends on that.
34
+
35
+
**If `node` is a text node, then `offset` must be the position in its text.**
36
+
37
+
For example, given the element `<p>Hello</p>`, we can create the range containing the letters "ll" as follows:
38
+
39
+
```html run
40
+
<pid="p">Hello</p>
41
+
<script>
42
+
let range =newRange();
43
+
range.setStart(p.firstChild, 2);
44
+
range.setEnd(p.firstChild, 4);
45
+
46
+
// toString of a range returns its content as text
47
+
console.log(range); // ll
48
+
</script>
49
+
```
50
+
51
+
Here we take the first child of `<p>` (that's the text node) and specify the text positions inside it:
52
+
53
+

54
+
55
+
### Selecting element nodes
56
+
57
+
**Alternatively, if `node` is an element node, then `offset` must be the child number.**
58
+
59
+
That's handy for making ranges that contain nodes as a whole, not stop somewhere inside their text.
60
+
61
+
For example, we have a more complex document fragment:
35
62
36
63
```html autorun
37
64
<pid="p">Example: <i>italic</i> and <b>bold</b></p>
38
65
```
39
66
40
-
Here's its DOM structure:
67
+
Here's its DOM structure with both element and text nodes:
Let's make a range for `"Example: <i>italic</i>"`.
79
106
80
-
As we can see, this phrase consists of exactly the first and the second children of `<p>`:
107
+
As we can see, this phrase consists of exactly two children of `<p>`, with indexes `0` and `1`:
81
108
82
109

83
110
84
111
- The starting point has `<p>` as the parent `node`, and `0` as the offset.
112
+
113
+
So we can set it as `range.setStart(p, 0)`.
85
114
- The ending point also has `<p>` as the parent `node`, but `2` as the offset (it specifies the range up to, but not including `offset`).
86
115
87
-
Here's the demo, if you run it, you can see that the text gets selected:
116
+
So we can set it as `range.setEnd(p, 2)`.
117
+
118
+
Here's the demo. If you run it, you can see that the text gets selected:
88
119
89
120
```html run
90
121
<pid="p">Example: <i>italic</i> and <b>bold</b></p>
@@ -100,12 +131,12 @@ Here's the demo, if you run it, you can see that the text gets selected:
100
131
// toString of a range returns its content as text, without tags
101
132
console.log(range); // Example: italic
102
133
103
-
//let's apply this range for document selection (explained later)
134
+
// apply this range for document selection (explained later below)
104
135
document.getSelection().addRange(range);
105
136
</script>
106
137
```
107
138
108
-
Here's a more flexible test stand where you try more variants:
139
+
Here's a more flexible test stand where you can set range start/end numbers and explore other variants:
109
140
110
141
```html run autorun
111
142
<pid="p">Example: <i>italic</i> and <b>bold</b></p>
@@ -121,26 +152,28 @@ From <input id="start" type="number" value=1> – To <input id="end" type="numbe
121
152
range.setEnd(p, end.value);
122
153
*/!*
123
154
124
-
// apply the selection, explained later
155
+
// apply the selection, explained later below
125
156
document.getSelection().removeAllRanges();
126
157
document.getSelection().addRange(range);
127
158
};
128
159
</script>
129
160
```
130
161
131
-
E.g. selecting in the same `<p>` from offset `1` to `4` gives range `<i>italic</i> and <b>bold</b>`:
162
+
E.g. selecting in the same `<p>` from offset `1` to `4` gives us the range `<i>italic</i> and <b>bold</b>`:
132
163
133
164

134
165
135
-
We don't have to use the same node in `setStart` and `setEnd`. A range may span across many unrelated nodes. It's only important that the end is after the start.
166
+
```smart header="Starting and ending nodes can be different"
167
+
We don't have to use the same node in `setStart` and `setEnd`. A range may span across many unrelated nodes. It's only important that the end is after the start in the document.
168
+
```
136
169
137
-
### Selecting parts of text nodes
170
+
### Selecting a bigger fragment
138
171
139
-
Let's select the text partially, like this:
172
+
Let's make a bigger selection in our example, like this:
140
173
141
174

142
175
143
-
That's also possible, we just need to set the start and the end as a relative offset in text nodes.
176
+
We already know how to do that. We just need to set the start and the end as a relative offset in text nodes.
144
177
145
178
We need to create a range, that:
146
179
- starts from position 2 in `<p>` first child (taking all but two first letters of "Ex<b>ample:</b> ")
@@ -162,7 +195,13 @@ We need to create a range, that:
162
195
</script>
163
196
```
164
197
165
-
The range object has following properties:
198
+
As you can see, it's fairly easy to make a range of whatever we want.
199
+
200
+
If we'd like to take nodes as a whole, we can pass elements in `setStart/setEnd`. Otherwise, we can work on the text level.
201
+
202
+
## Range properties
203
+
204
+
The range object that we created in the example above has following properties:
166
205
167
206

168
207
@@ -175,10 +214,13 @@ The range object has following properties:
175
214
-`commonAncestorContainer` -- the nearest common ancestor of all nodes within the range,
176
215
- in the example above: `<p>`
177
216
178
-
## Range methods
217
+
218
+
## Range selection methods
179
219
180
220
There are many convenience methods to manipulate ranges.
181
221
222
+
We've already seen `setStart` and `setEnd`, here are other similar methods.
223
+
182
224
Set range start:
183
225
184
226
-`setStart(node, offset)` set start at: position `offset` in `node`
@@ -191,15 +233,19 @@ Set range end (similar methods):
191
233
-`setEndBefore(node)` set end at: right before `node`
192
234
-`setEndAfter(node)` set end at: right after `node`
193
235
194
-
**As it was demonstrated, `node` can be both a text or element node: for text nodes `offset` skips that many of characters, while for element nodes that many child nodes.**
236
+
Technically, `setStart/setEnd` can do anything, but more methods provide more convenience.
195
237
196
-
Others:
238
+
In all these methods, `node` can be both a text or element node: for text nodes `offset` skips that many of characters, while for element nodes that many child nodes.
239
+
240
+
Even more methods to create ranges:
197
241
-`selectNode(node)` set range to select the whole `node`
198
242
-`selectNodeContents(node)` set range to select the whole `node` contents
199
243
-`collapse(toStart)` if `toStart=true` set end=start, otherwise set start=end, thus collapsing the range
200
244
-`cloneRange()` creates a new range with the same start/end
201
245
202
-
To manipulate the content within the range:
246
+
## Range editing methods
247
+
248
+
Once the range is created, we can manipulate its content using these methods:
203
249
204
250
-`deleteContents()` -- remove range content from the document
205
251
-`extractContents()` -- remove range content from the document and return as [DocumentFragment](info:modifying-document#document-fragment)
@@ -271,7 +317,9 @@ There also exist methods to compare ranges, but these are rarely used. When you
271
317
272
318
## Selection
273
319
274
-
`Range` is a generic object for managing selection ranges. We may create `Range` objects, pass them around -- they do not visually select anything on their own.
320
+
`Range` is a generic object for managing selection ranges. Although, creating a `Range` doesn't mean that we see a selection on screen.
321
+
322
+
We may create `Range` objects, pass them around -- they do not visually select anything on their own.
275
323
276
324
The document selection is represented by `Selection` object, that can be obtained as `window.getSelection()` or `document.getSelection()`. A selection may include zero or more ranges. At least, the [Selection API specification](https://www.w3.org/TR/selection-api/) says so. In practice though, only Firefox allows to select multiple ranges in the document by using `key:Ctrl+click` (`key:Cmd+click` for Mac).
277
325
@@ -281,9 +329,19 @@ Here's a screenshot of a selection with 3 ranges, made in Firefox:
281
329
282
330
Other browsers support at maximum 1 range. As we'll see, some of `Selection` methods imply that there may be many ranges, but again, in all browsers except Firefox, there's at maximum 1.
283
331
332
+
Here's a small demo that shows the current selection (select something and click) as text:
Similar to a range, a selection has a start, called "anchor", and the end, called "focus".
338
+
As said, a selection may in theory contain multiple ranges. We can get these range objects using the method:
339
+
340
+
-`getRangeAt(i)` -- get i-th range, starting from `0`. In all browsers except Firefox, only `0` is used.
341
+
342
+
Also, there exist properties that often provide better convenience.
343
+
344
+
Similar to a range, a selection object has a start, called "anchor", and the end, called "focus".
287
345
288
346
The main selection properties are:
289
347
@@ -294,58 +352,65 @@ The main selection properties are:
294
352
-`isCollapsed` -- `true` if selection selects nothing (empty range), or doesn't exist.
295
353
-`rangeCount` -- count of ranges in the selection, maximum `1` in all browsers except Firefox.
296
354
297
-
````smart header="Usually, the selection end `focusNode` is after its start `anchorNode`, but it's not always the case"
298
-
There are many ways to select the content, depending on the user agent: mouse, hotkeys, taps on a mobile etc.
355
+
```smart header="Selection end/start vs Range"
356
+
357
+
There's an important differences of a selection anchor/focus compared with a `Range` start/end.
358
+
359
+
As we know, `Range` objects always have their start before the end.
299
360
300
-
Some of them, such as a mouse, allow the same selection can be created in two directions: "left-to-right" and "right-to-left".
361
+
For selections, that's not always the case.
301
362
302
-
If the start (anchor) of the selection goes in the document before the end (focus), this selection is said to have "forward" direction.
363
+
Selecting something with a mouse can be done in both directions: either "left-to-right" or "right-to-left".
364
+
365
+
In other words, when the mouse button is pressed, and then it moves forward in the document, then its end (focus) will be after its start (anchor).
303
366
304
367
E.g. if the user starts selecting with mouse and goes from "Example" to "italic":
305
368
306
369

307
370
308
-
Otherwise, if they go from the end of "italic" to "Example", the selection is directed "backward", its focus will be before the anchor:
371
+
...But the same selection could be done backwards: starting from "italic" to "Example" (backward direction), then its end (focus) will be before the start (anchor):
309
372
310
373

311
-
312
-
That's different from `Range` objects that are always directed forward: the range start can't be after its end.
313
-
````
374
+
```
314
375
315
376
## Selection events
316
377
317
378
There are events on to keep track of selection:
318
379
319
-
- `elem.onselectstart` -- when a selection starts on `elem`, e.g. the user starts moving mouse with pressed button.
320
-
- Preventing the default action makes the selection not start.
321
-
- `document.onselectionchange` -- whenever a selection changes.
322
-
- Please note: this handler can be set only on `document`.
380
+
-`elem.onselectstart` -- when a selection *starts* on speficially elemen `elem` (or inside it). For instance, when the user presses the mouse button on it and starts to move the pointer.
381
+
- Preventing the default action cancels the selection start. So starting a selection from this element becomes impossible, but the element is still selectable. The visitor just needs to start the selection from elsewhere.
382
+
-`document.onselectionchange` -- whenever a selection changes or starts.
383
+
- Please note: this handler can be set only on `document`, it tracks all selections in it.
323
384
324
385
### Selection tracking demo
325
386
326
-
Here's a small demo that shows selection boundaries dynamically as it changes:
387
+
Here's a small demo. It tracks the current selection on the `document` and shows its boundaries:
327
388
328
389
```html run height=80
329
390
<pid="p">Select me: <i>italic</i> and <b>bold</b></p>
330
391
331
392
From <inputid="from"disabled> – To <inputid="to"disabled>
332
393
<script>
333
394
document.onselectionchange=function() {
334
-
let {anchorNode, anchorOffset, focusNode, focusOffset} = document.getSelection();
There are two approaches to copying the selected content:
343
409
344
-
To get the whole selection:
345
-
- As text: just call `document.getSelection().toString()`.
346
-
- As DOM nodes: get the underlying ranges and call their `cloneContents()` method (only first range if we don't support Firefox multiselection).
410
+
1. We can use `document.getSelection().toString()` to get it as text.
411
+
2. Otherwise, to copy the full DOM, e.g. if we need to keep formatting, we can get the underlying ranges with`getRangesAt(...)`. A`Range` object, in turn, has `cloneContents()` method that clones its content and returns as `DocumentFragment` object, that we can insert elsewhere.
347
412
348
-
And here's the demo of getting the selection both as text and as DOM nodes:
413
+
Here's the demo of copying the selected content both as text and as DOM nodes:
349
414
350
415
```html run height=100
351
416
<p id="p">Select me: <i>italic</i> and <b>bold</b></p>
@@ -373,15 +438,15 @@ As text: <span id="astext"></span>
373
438
374
439
## Selection methods
375
440
376
-
Selection methods to add/remove ranges:
441
+
We can work with the selection by addding/removing ranges:
377
442
378
-
- `getRangeAt(i)` -- get i-th range, starting from `0`. In all browsers except firefox, only `0` is used.
443
+
- `getRangeAt(i)` -- get i-th range, starting from `0`. In all browsers except Firefox, only `0` is used.
379
444
- `addRange(range)` -- add `range` to selection. All browsers except Firefox ignore the call, if the selection already has an associated range.
380
445
- `removeRange(range)` -- remove `range` from the selection.
381
446
- `removeAllRanges()` -- remove all ranges.
382
447
- `empty()` -- alias to `removeAllRanges`.
383
448
384
-
Also, there are convenience methods to manipulate the selection range directly, without `Range`:
449
+
There are also convenience methods to manipulate the selection range directly, without intermediate `Range` calls:
385
450
386
451
- `collapse(node, offset)` -- replace selected range with a new one that starts and ends at the given `node`, at position `offset`.
387
452
- `setPosition(node, offset)` -- alias to `collapse`.
@@ -393,7 +458,7 @@ Also, there are convenience methods to manipulate the selection range directly,
393
458
- `deleteFromDocument()` -- remove selected content from the document.
394
459
- `containsNode(node, allowPartialContainment = false)` -- checks whether the selection contains `node` (partially if the second argument is `true`)
395
460
396
-
So, for many tasks we can call `Selection` methods, no need to access the underlying `Range` object.
461
+
For most tasks these methods are just fine, there's no need to access the underlying `Range` object.
397
462
398
463
For example, selecting the whole contents of the paragraph `<p>`:
399
464
@@ -420,10 +485,10 @@ The same thing using ranges:
420
485
</script>
421
486
```
422
487
423
-
```smart header="To select, remove the existing selection first"
424
-
If the selection already exists, empty it first with `removeAllRanges()`. And then add ranges. Otherwise, all browsers except Firefox ignore new ranges.
488
+
```smart header="To select something, remove the existing selection first"
489
+
If a document selection already exists, empty it first with `removeAllRanges()`. And then add ranges. Otherwise, all browsers except Firefox ignore new ranges.
425
490
426
-
The exception is some selection methods, that replace the existing selection, like `setBaseAndExtent`.
491
+
The exception is some selection methods, that replace the existing selection, such as`setBaseAndExtent`.
0 commit comments