diff --git a/2-ui/99-ui-misc/02-selection-range/article.md b/2-ui/99-ui-misc/02-selection-range/article.md index b53d4177..6284e02e 100644 --- a/2-ui/99-ui-misc/02-selection-range/article.md +++ b/2-ui/99-ui-misc/02-selection-range/article.md @@ -26,18 +26,45 @@ let range = new Range(); Then we can set the selection boundaries using `range.setStart(node, offset)` and `range.setEnd(node, offset)`. -The first argument `node` can be either a text node or an element node. The meaning of the second argument depends on that: +As you might guess, further we'll use the `Range` objects for selection, but first let's create few such objects. -- If `node` is a text node, then `offset` must be the position in the text. -- If `node` is an element node, then `offset` must be the child number. +### Selecting the text partially -For example, let's create a range in this fragment: +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. + +**If `node` is a text node, then `offset` must be the position in its text.** + +For example, given the element `

Hello

`, we can create the range containing the letters "ll" as follows: + +```html run +

Hello

+ +``` + +Here we take the first child of `

` (that's the text node) and specify the text positions inside it: + +![](range-hello-1.svg) + +### Selecting element nodes + +**Alternatively, if `node` is an element node, then `offset` must be the child number.** + +That's handy for making ranges that contain nodes as a whole, not stop somewhere inside their text. + +For example, we have a more complex document fragment: ```html autorun

Example: italic and bold

``` -Here's its DOM structure: +Here's its DOM structure with both element and text nodes:
@@ -77,14 +104,18 @@ drawHtmlTree(selectPDomtree, 'div.select-p-domtree', 690, 320); Let's make a range for `"Example: italic"`. -As we can see, this phrase consists of exactly the first and the second children of `

`: +As we can see, this phrase consists of exactly two children of `

`, with indexes `0` and `1`: ![](range-example-p-0-1.svg) - The starting point has `

` as the parent `node`, and `0` as the offset. + + So we can set it as `range.setStart(p, 0)`. - The ending point also has `

` as the parent `node`, but `2` as the offset (it specifies the range up to, but not including `offset`). -Here's the demo, if you run it, you can see that the text gets selected: + So we can set it as `range.setEnd(p, 2)`. + +Here's the demo. If you run it, you can see that the text gets selected: ```html run

Example: italic and bold

@@ -100,12 +131,12 @@ Here's the demo, if you run it, you can see that the text gets selected: // toString of a range returns its content as text, without tags console.log(range); // Example: italic - // let's apply this range for document selection (explained later) + // apply this range for document selection (explained later below) document.getSelection().addRange(range); ``` -Here's a more flexible test stand where you try more variants: +Here's a more flexible test stand where you can set range start/end numbers and explore other variants: ```html run autorun

Example: italic and bold

@@ -121,26 +152,28 @@ From – To ` first child (taking all but two first letters of "Example: ") @@ -162,7 +195,13 @@ We need to create a range, that: ``` -The range object has following properties: +As you can see, it's fairly easy to make a range of whatever we want. + +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. + +## Range properties + +The range object that we created in the example above has following properties: ![](range-example-p-2-b-3-range.svg) @@ -175,10 +214,13 @@ The range object has following properties: - `commonAncestorContainer` -- the nearest common ancestor of all nodes within the range, - in the example above: `

` -## Range methods + +## Range selection methods There are many convenience methods to manipulate ranges. +We've already seen `setStart` and `setEnd`, here are other similar methods. + Set range start: - `setStart(node, offset)` set start at: position `offset` in `node` @@ -191,15 +233,19 @@ Set range end (similar methods): - `setEndBefore(node)` set end at: right before `node` - `setEndAfter(node)` set end at: right after `node` -**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.** +Technically, `setStart/setEnd` can do anything, but more methods provide more convenience. -Others: +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. + +Even more methods to create ranges: - `selectNode(node)` set range to select the whole `node` - `selectNodeContents(node)` set range to select the whole `node` contents - `collapse(toStart)` if `toStart=true` set end=start, otherwise set start=end, thus collapsing the range - `cloneRange()` creates a new range with the same start/end -To manipulate the content within the range: +## Range editing methods + +Once the range is created, we can manipulate its content using these methods: - `deleteContents()` -- remove range content from the document - `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 ## Selection -`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. +`Range` is a generic object for managing selection ranges. Although, creating a `Range` doesn't mean that we see a selection on screen. + +We may create `Range` objects, pass them around -- they do not visually select anything on their own. 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). @@ -281,9 +329,19 @@ Here's a screenshot of a selection with 3 ranges, made in Firefox: 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. +Here's a small demo that shows the current selection (select something and click) as text: + + + ## Selection properties -Similar to a range, a selection has a start, called "anchor", and the end, called "focus". +As said, a selection may in theory contain multiple ranges. We can get these range objects using the method: + +- `getRangeAt(i)` -- get i-th range, starting from `0`. In all browsers except Firefox, only `0` is used. + +Also, there exist properties that often provide better convenience. + +Similar to a range, a selection object has a start, called "anchor", and the end, called "focus". The main selection properties are: @@ -294,36 +352,39 @@ The main selection properties are: - `isCollapsed` -- `true` if selection selects nothing (empty range), or doesn't exist. - `rangeCount` -- count of ranges in the selection, maximum `1` in all browsers except Firefox. -````smart header="Usually, the selection end `focusNode` is after its start `anchorNode`, but it's not always the case" -There are many ways to select the content, depending on the user agent: mouse, hotkeys, taps on a mobile etc. +```smart header="Selection end/start vs Range" -Some of them, such as a mouse, allow the same selection can be created in two directions: "left-to-right" and "right-to-left". +There's an important differences of a selection anchor/focus compared with a `Range` start/end. -If the start (anchor) of the selection goes in the document before the end (focus), this selection is said to have "forward" direction. +As we know, `Range` objects always have their start before the end. + +For selections, that's not always the case. + +Selecting something with a mouse can be done in both directions: either "left-to-right" or "right-to-left". + +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). E.g. if the user starts selecting with mouse and goes from "Example" to "italic": ![](selection-direction-forward.svg) -Otherwise, if they go from the end of "italic" to "Example", the selection is directed "backward", its focus will be before the anchor: +...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): ![](selection-direction-backward.svg) - -That's different from `Range` objects that are always directed forward: the range start can't be after its end. -```` +``` ## Selection events There are events on to keep track of selection: -- `elem.onselectstart` -- when a selection starts on `elem`, e.g. the user starts moving mouse with pressed button. - - Preventing the default action makes the selection not start. -- `document.onselectionchange` -- whenever a selection changes. - - Please note: this handler can be set only on `document`. +- `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. + - 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. +- `document.onselectionchange` -- whenever a selection changes or starts. + - Please note: this handler can be set only on `document`, it tracks all selections in it. ### Selection tracking demo -Here's a small demo that shows selection boundaries dynamically as it changes: +Here's a small demo. It tracks the current selection on the `document` and shows its boundaries: ```html run height=80

Select me: italic and bold

@@ -331,21 +392,25 @@ Here's a small demo that shows selection boundaries dynamically as it changes: From – To ``` -### Selection getting demo +### Selection copying demo -To get the whole selection: -- As text: just call `document.getSelection().toString()`. -- As DOM nodes: get the underlying ranges and call their `cloneContents()` method (only first range if we don't support Firefox multiselection). +There are two approaches to copying the selected content: -And here's the demo of getting the selection both as text and as DOM nodes: +1. We can use `document.getSelection().toString()` to get it as text. +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. + +Here's the demo of copying the selected content both as text and as DOM nodes: ```html run height=100

Select me: italic and bold

@@ -373,15 +438,15 @@ As text: ## Selection methods -Selection methods to add/remove ranges: +We can work with the selection by addding/removing ranges: -- `getRangeAt(i)` -- get i-th range, starting from `0`. In all browsers except firefox, only `0` is used. +- `getRangeAt(i)` -- get i-th range, starting from `0`. In all browsers except Firefox, only `0` is used. - `addRange(range)` -- add `range` to selection. All browsers except Firefox ignore the call, if the selection already has an associated range. - `removeRange(range)` -- remove `range` from the selection. - `removeAllRanges()` -- remove all ranges. - `empty()` -- alias to `removeAllRanges`. -Also, there are convenience methods to manipulate the selection range directly, without `Range`: +There are also convenience methods to manipulate the selection range directly, without intermediate `Range` calls: - `collapse(node, offset)` -- replace selected range with a new one that starts and ends at the given `node`, at position `offset`. - `setPosition(node, offset)` -- alias to `collapse`. @@ -393,7 +458,7 @@ Also, there are convenience methods to manipulate the selection range directly, - `deleteFromDocument()` -- remove selected content from the document. - `containsNode(node, allowPartialContainment = false)` -- checks whether the selection contains `node` (partially if the second argument is `true`) -So, for many tasks we can call `Selection` methods, no need to access the underlying `Range` object. +For most tasks these methods are just fine, there's no need to access the underlying `Range` object. For example, selecting the whole contents of the paragraph `

`: @@ -420,10 +485,10 @@ The same thing using ranges: ``` -```smart header="To select, remove the existing selection first" -If the selection already exists, empty it first with `removeAllRanges()`. And then add ranges. Otherwise, all browsers except Firefox ignore new ranges. +```smart header="To select something, remove the existing selection first" +If a document selection already exists, empty it first with `removeAllRanges()`. And then add ranges. Otherwise, all browsers except Firefox ignore new ranges. -The exception is some selection methods, that replace the existing selection, like `setBaseAndExtent`. +The exception is some selection methods, that replace the existing selection, such as `setBaseAndExtent`. ``` ## Selection in form controls diff --git a/2-ui/99-ui-misc/02-selection-range/range-hello-1.svg b/2-ui/99-ui-misc/02-selection-range/range-hello-1.svg new file mode 100644 index 00000000..90054cef --- /dev/null +++ b/2-ui/99-ui-misc/02-selection-range/range-hello-1.svg @@ -0,0 +1 @@ +<p>Hello</p>p.firstChild \ No newline at end of file diff --git a/figures.sketch b/figures.sketch index 29ba3da1..aa3ecfca 100644 Binary files a/figures.sketch and b/figures.sketch differ