diff --git a/1-js/11-async/07-microtask-queue/promiseQueue.png b/1-js/11-async/07-microtask-queue/promiseQueue.png index 7f4ca5f5..f8a972d3 100644 Binary files a/1-js/11-async/07-microtask-queue/promiseQueue.png and b/1-js/11-async/07-microtask-queue/promiseQueue.png differ diff --git a/1-js/11-async/07-microtask-queue/promiseQueue@2x.png b/1-js/11-async/07-microtask-queue/promiseQueue@2x.png index f1161783..f7b49e2b 100644 Binary files a/1-js/11-async/07-microtask-queue/promiseQueue@2x.png and b/1-js/11-async/07-microtask-queue/promiseQueue@2x.png differ diff --git a/1-js/99-js-misc/01-proxy/proxy-inherit.png b/1-js/99-js-misc/01-proxy/proxy-inherit.png index 84110b6d..c5f88bcc 100644 Binary files a/1-js/99-js-misc/01-proxy/proxy-inherit.png and b/1-js/99-js-misc/01-proxy/proxy-inherit.png differ diff --git a/2-ui/99-ui-misc/02-event-loop/promiseQueue.png b/2-ui/99-ui-misc/02-event-loop/promiseQueue.png deleted file mode 100644 index 7f4ca5f5..00000000 Binary files a/2-ui/99-ui-misc/02-event-loop/promiseQueue.png and /dev/null differ diff --git a/2-ui/99-ui-misc/02-event-loop/promiseQueue@2x.png b/2-ui/99-ui-misc/02-event-loop/promiseQueue@2x.png deleted file mode 100644 index f1161783..00000000 Binary files a/2-ui/99-ui-misc/02-event-loop/promiseQueue@2x.png and /dev/null differ diff --git a/2-ui/99-ui-misc/02-selection-range/article.md b/2-ui/99-ui-misc/02-selection-range/article.md new file mode 100644 index 00000000..3b145361 --- /dev/null +++ b/2-ui/99-ui-misc/02-selection-range/article.md @@ -0,0 +1,579 @@ +libs: + - d3 + - domtree + +--- + +# Text Selection and Range + +In this chapter we'll cover text selection. + +JavaScript can do everything with it: get the existing selection, select/deselect it or its parts, remove the selected part from the document, wrap it into a tag, and so on. + +You can get a few ready to use recipes at the end, in "Summary" section. But you'll get much more if you read on. The underlying `Range` and `Selection` objects are easy to grasp, and then you'll need no recipes to make them do what you want. + +## Range + +The basic concept of selection is [Range](https://dom.spec.whatwg.org/#ranges): basically, a pair of "boundary points": range start and range end. + +Each point represented as a parent DOM node with the relative offset from its start. For an element node, the offset is a child number, for a text node it's the position in the text. + +First, we can create a range (the constructor has no parameters): + +```js +let range = new Range(); +``` + +Then we can set the boundaries using `range.setStart(node, offset)` and `range.setEnd(node, offset)`. + +For example, consider this fragment of HTML: + +```html +
Example: italic and bold
+``` + +Here's its DOM structure, note that here text nodes are important for us: + + + + + +Let's select `"Example: italic"`. That's two first children of ``: + + + +```html run +
Example: italic and bold
+ + +``` + +- `range.setStart(p, 0)` -- sets the start at the 0th child of `` (that's a text node `"Example: "`). +- `range.setEnd(p, 2)` -- spans the range up to (but not including) 2nd child of `
` (that's a text node `" and "`, but as the end is not included, the last selected node is ``). + +Here's a more flexible test stand where you try more variants: + +```html run autorun +
Example: italic and bold
+ +From – To + + +``` + +E.g. selecting from `1` to `4` gives range `italic and bold`. + + + +We don't have to use the same node in `setStart` and `setEnd`. A range may span across many unrelated nodes. + +### Selecting parts of text nodes + +Let's select the text partially, like this: + + + +That's also possible, we just need to set the start and the end as a relative offset in text nodes. + +We need to create a range, that: +- starts from position 2 in `` first child (taking all but two first letters of "Example: ") +- ends at the position 3 in `` first child (taking first three letters of "bold"): + +```html run +
Example: italic and bold
+ + +``` + +The range object has following properties: + + + +- `startContainer`, `startOffset` -- node and offset of the start, + - in the example above: first text node inside `` and `2`. +- `endContainer`, `endOffset` -- node and offset of the end, + - in the example above: first text node inside `` and `3`. +- `collapsed` -- boolean, `true` if the range starts and ends on the same point (so there's no content inside the range), + - in the example above: `false` +- `commonAncestorContainer` -- the nearest common ancestor of all nodes within the range, + - in the example above: `
` + +## Range methods + +There are many convenience methods to manipulate ranges. + +Set range start: + +- `setStart(node, offset)` set start at: position `offset` in `node` +- `setStartBefore(node)` set start at: right before `node` +- `setStartAfter(node)` set start at: right after `node` + +Set range end (similar methods): + +- `setEnd(node, offset)` set end at: position `offset` in `node` +- `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.** + +Others: +- `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: + +- `deleteContents()` - remove range content from the document +- `extractContents()` - remove range content from the document and return as [DocumentFragment](info:modifying-document#document-fragment) +- `cloneContents()` - clone range content and return as [DocumentFragment](info:modifying-document#document-fragment) +- `insertNode(node)` -- insert `node` into the document at the beginning of the range +- `surroundContents(node)` -- wrap `node` around range content. For this to work, the range must contain both opening and closing tags for all elements inside it: no partial ranges like `abc`. + +With these methods we can do basically anything with selected nodes. + +Here's the test stand to see them in action: + +```html run autorun height=260 +Click buttons to run methods on the selection, "resetExample" to reset it. + +
Example: italic and bold
+ + + +``` + +There also exist methods to compare ranges, but these are rarely used. When you need them, please refer to the [spec](https://dom.spec.whatwg.org/#interface-range) or [MDN manual](https://developer.mozilla.org/en-US/docs/Web/API/Range). + + +## Selection + +`Range` is a generic object for managing selection ranges. We may create such 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). + +Here's a screenshot of a selection with 3 ranges, made in Firefox: + + + +Other browsers support at maximum 1 range per selection. 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. + +## Selection properties + +Similar to a range, a selection has a start, called "anchor", and the end, called "focus". + +The main selection properties are: + +- `anchorNode` -- the node where the selection starts, +- `anchorOffset` -- the offset in `anchorNode` where the selection starts, +- `focusNode` -- the node where the selection ends, +- `focusOffset` -- the offset in `focusNode` where the selection ends, +- `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="Selection end may be in the document before start" +There are many ways to select the content, depending on the user agent: mouse, hotkeys, taps on a mobile etc. + +Some of them, such as a mouse, allow the same selection can be created in two directions: "left-to-right" and "right-to-left". + +If the start (anchor) of the selection goes in the document before the end (focus), this selection is said to have "forward" direction. + +E.g. if the user starts selecting with mouse and goes from "Example" to "italic": + + + +Otherwise, if they go from the end of "italic" to "Example", the selection is directed "backward", its focus will be before the anchor: + + + +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. + - May trigger on any element. + - Preventing default action makes the selection not start. +- `document.onselectionchange` -- when a selection changes. + - Triggers only on `document`. + +## Selection tracking demo + +Here's a small demo that shows selection boundaries +dynamically as it changes: + +```html run height=80 +Select me: italic and bold
+ +From – To + +``` + +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). + +And here's the demo of getting the selection both as text and as DOM nodes: + +```html run height=100 +Select me: italic and bold
+ +Cloned: +Select me: italic and bold
+ + +``` + +The same thing using ranges: + +```html run +Select me: italic and bold
+ + +``` + +```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. + +The exception is some selection methods, that replace the existing selection, like `setBaseAndExtent`. +``` + +## Selection in form controls + +Form elements, such as `input` and `textarea` provide [API for selection in their values](https://html.spec.whatwg.org/#textFieldSelection). + +As the value is a pure text, not HTML, these methods to not use `Selection` or `Range` objects, they are much simpler. + +- `input.select()` -- selects everything in the text control, +- `input.selectionStart` -- position of selection start (writeable), +- `input.selectionEnd` -- position of selection start (writeable), +- `input.selectionDirection` -- direction, one of: "forward", "backward" or "none" (if e.g. selected with a double mouse click), +- `input.setSelectionRange(start, end, [direction])` -- change the selection to span from `start` till `end`, in the given direction (optional). + +To modify the content of the selection: + +- `input.setRangeText(replacement, [start], [end], [selectionMode])` -- replace a range of text with the new text. If the `start` and `end` arguments are not provided, the range is assumed to be the selection. + +The last argument, `selectionMode`, determines how the selection will be set after the text has been replaced. The possible values are: + +- `"select"` -- the newly inserted text will be selected. +- `"start"` -- the selection range collapses just before the inserted text. +- `"end"` -- the selection range collapses just after the inserted text. +- `"preserve"` -- attempts to preserve the selection. This is the default. + +For example, this code uses `onselect` event to track selection: + +```html run + +