Skip to content

Commit 33f8b4c

Browse files
committed
selection improvements
1 parent d4b3c13 commit 33f8b4c

File tree

3 files changed

+116
-50
lines changed

3 files changed

+116
-50
lines changed

Diff for: 2-ui/99-ui-misc/02-selection-range/article.md

+115-50
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,45 @@ let range = new Range();
2626

2727
Then we can set the selection boundaries using `range.setStart(node, offset)` and `range.setEnd(node, offset)`.
2828

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.
3030

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
3332

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+
<p id="p">Hello</p>
41+
<script>
42+
let range = new Range();
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+
![](range-hello-1.svg)
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:
3562

3663
```html autorun
3764
<p id="p">Example: <i>italic</i> and <b>bold</b></p>
3865
```
3966

40-
Here's its DOM structure:
67+
Here's its DOM structure with both element and text nodes:
4168

4269
<div class="select-p-domtree"></div>
4370

@@ -77,14 +104,18 @@ drawHtmlTree(selectPDomtree, 'div.select-p-domtree', 690, 320);
77104

78105
Let's make a range for `"Example: <i>italic</i>"`.
79106

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`:
81108

82109
![](range-example-p-0-1.svg)
83110

84111
- 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)`.
85114
- 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`).
86115

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:
88119

89120
```html run
90121
<p id="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:
100131
// toString of a range returns its content as text, without tags
101132
console.log(range); // Example: italic
102133
103-
// let's apply this range for document selection (explained later)
134+
// apply this range for document selection (explained later below)
104135
document.getSelection().addRange(range);
105136
</script>
106137
```
107138

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:
109140

110141
```html run autorun
111142
<p id="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
121152
range.setEnd(p, end.value);
122153
*/!*
123154
124-
// apply the selection, explained later
155+
// apply the selection, explained later below
125156
document.getSelection().removeAllRanges();
126157
document.getSelection().addRange(range);
127158
};
128159
</script>
129160
```
130161

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>`:
132163

133164
![](range-example-p-1-3.svg)
134165

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+
```
136169

137-
### Selecting parts of text nodes
170+
### Selecting a bigger fragment
138171

139-
Let's select the text partially, like this:
172+
Let's make a bigger selection in our example, like this:
140173

141174
![](range-example-p-2-b-3.svg)
142175

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.
144177

145178
We need to create a range, that:
146179
- 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:
162195
</script>
163196
```
164197

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:
166205

167206
![](range-example-p-2-b-3-range.svg)
168207

@@ -175,10 +214,13 @@ The range object has following properties:
175214
- `commonAncestorContainer` -- the nearest common ancestor of all nodes within the range,
176215
- in the example above: `<p>`
177216

178-
## Range methods
217+
218+
## Range selection methods
179219

180220
There are many convenience methods to manipulate ranges.
181221

222+
We've already seen `setStart` and `setEnd`, here are other similar methods.
223+
182224
Set range start:
183225

184226
- `setStart(node, offset)` set start at: position `offset` in `node`
@@ -191,15 +233,19 @@ Set range end (similar methods):
191233
- `setEndBefore(node)` set end at: right before `node`
192234
- `setEndAfter(node)` set end at: right after `node`
193235

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.
195237

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:
197241
- `selectNode(node)` set range to select the whole `node`
198242
- `selectNodeContents(node)` set range to select the whole `node` contents
199243
- `collapse(toStart)` if `toStart=true` set end=start, otherwise set start=end, thus collapsing the range
200244
- `cloneRange()` creates a new range with the same start/end
201245

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:
203249

204250
- `deleteContents()` -- remove range content from the document
205251
- `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
271317

272318
## Selection
273319

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.
275323

276324
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).
277325

@@ -281,9 +329,19 @@ Here's a screenshot of a selection with 3 ranges, made in Firefox:
281329

282330
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.
283331

332+
Here's a small demo that shows the current selection (select something and click) as text:
333+
334+
<button onclick="alert(document.getSelection())">alert(document.getSelection())</button>
335+
284336
## Selection properties
285337

286-
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".
287345

288346
The main selection properties are:
289347

@@ -294,58 +352,65 @@ The main selection properties are:
294352
- `isCollapsed` -- `true` if selection selects nothing (empty range), or doesn't exist.
295353
- `rangeCount` -- count of ranges in the selection, maximum `1` in all browsers except Firefox.
296354

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.
299360
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.
301362
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).
303366
304367
E.g. if the user starts selecting with mouse and goes from "Example" to "italic":
305368
306369
![](selection-direction-forward.svg)
307370
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):
309372
310373
![](selection-direction-backward.svg)
311-
312-
That's different from `Range` objects that are always directed forward: the range start can't be after its end.
313-
````
374+
```
314375

315376
## Selection events
316377

317378
There are events on to keep track of selection:
318379

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.
323384

324385
### Selection tracking demo
325386

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:
327388

328389
```html run height=80
329390
<p id="p">Select me: <i>italic</i> and <b>bold</b></p>
330391

331392
From <input id="from" disabled> – To <input id="to" disabled>
332393
<script>
333394
document.onselectionchange = function() {
334-
let {anchorNode, anchorOffset, focusNode, focusOffset} = document.getSelection();
395+
let selection = document.getSelection();
335396
336-
from.value = `${anchorNode && anchorNode.data}:${anchorOffset}`;
337-
to.value = `${focusNode && focusNode.data}:${focusOffset}`;
397+
let {anchorNode, anchorOffset, focusNode, focusOffset} = selection;
398+
399+
// anchorNode and focusNode are text nodes usually
400+
from.value = `${anchorNode?.data}, offset ${anchorOffset}`;
401+
to.value = `${focusNode?.data}, offset ${focusOffset}`;
338402
};
339403
</script>
340404
```
341405
342-
### Selection getting demo
406+
### Selection copying demo
407+
408+
There are two approaches to copying the selected content:
343409
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.
347412
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:
349414
350415
```html run height=100
351416
<p id="p">Select me: <i>italic</i> and <b>bold</b></p>
@@ -373,15 +438,15 @@ As text: <span id="astext"></span>
373438
374439
## Selection methods
375440
376-
Selection methods to add/remove ranges:
441+
We can work with the selection by addding/removing ranges:
377442
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.
379444
- `addRange(range)` -- add `range` to selection. All browsers except Firefox ignore the call, if the selection already has an associated range.
380445
- `removeRange(range)` -- remove `range` from the selection.
381446
- `removeAllRanges()` -- remove all ranges.
382447
- `empty()` -- alias to `removeAllRanges`.
383448
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:
385450
386451
- `collapse(node, offset)` -- replace selected range with a new one that starts and ends at the given `node`, at position `offset`.
387452
- `setPosition(node, offset)` -- alias to `collapse`.
@@ -393,7 +458,7 @@ Also, there are convenience methods to manipulate the selection range directly,
393458
- `deleteFromDocument()` -- remove selected content from the document.
394459
- `containsNode(node, allowPartialContainment = false)` -- checks whether the selection contains `node` (partially if the second argument is `true`)
395460
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.
397462
398463
For example, selecting the whole contents of the paragraph `<p>`:
399464
@@ -420,10 +485,10 @@ The same thing using ranges:
420485
</script>
421486
```
422487
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.
425490
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`.
427492
```
428493
429494
## Selection in form controls

Diff for: 2-ui/99-ui-misc/02-selection-range/range-hello-1.svg

+1
Loading

Diff for: figures.sketch

-581 KB
Binary file not shown.

0 commit comments

Comments
 (0)