Merge branch 'master' into patch-1

This commit is contained in:
Miguel N. Galace 2020-09-23 18:44:06 +08:00 committed by GitHub
commit 482ca750a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
327 changed files with 3983 additions and 2566 deletions

View file

@ -2,11 +2,11 @@
The JavaScript language was initially created for web browsers. Since then it has evolved and become a language with many uses and platforms.
A platform may be a browser, or a web-server or another *host*, even a coffee machine. Each of them provides platform-specific functionality. The JavaScript specification calls that a *host environment*.
A platform may be a browser, or a web-server or another *host*, even a "smart" coffee machine, if it can run JavaScript. Each of them provides platform-specific functionality. The JavaScript specification calls that a *host environment*.
A host environment provides own objects and functions additional to the language core. Web browsers give a means to control web pages. Node.js provides server-side features, and so on.
Here's a bird's-eye view of what we have when JavaScript runs in a web-browser:
Here's a bird's-eye view of what we have when JavaScript runs in a web browser:
![](windowObjects.svg)
@ -49,9 +49,7 @@ document.body.style.background = "red";
setTimeout(() => document.body.style.background = "", 1000);
```
Here we used `document.body.style`, but there's much, much more. Properties and methods are described in the specification:
- **DOM Living Standard** at <https://dom.spec.whatwg.org>
Here we used `document.body.style`, but there's much, much more. Properties and methods are described in the specification: [DOM Living Standard](https://dom.spec.whatwg.org).
```smart header="DOM is not only for browsers"
The DOM specification explains the structure of a document and provides objects to manipulate it. There are non-browser instruments that use DOM too.
@ -60,9 +58,9 @@ For instance, server-side scripts that download HTML pages and process them can
```
```smart header="CSSOM for styling"
CSS rules and stylesheets are structured in a different way than HTML. There's a separate specification, [CSS Object Model (CSSOM)](https://www.w3.org/TR/cssom-1/), that explains how they are represented as objects, and how to read and write them.
There's also a separate specification, [CSS Object Model (CSSOM)](https://www.w3.org/TR/cssom-1/) for CSS rules and stylesheets, that explains how they are represented as objects, and how to read and write them.
CSSOM is used together with DOM when we modify style rules for the document. In practice though, CSSOM is rarely required, because usually CSS rules are static. We rarely need to add/remove CSS rules from JavaScript, but that's also possible.
CSSOM is used together with DOM when we modify style rules for the document. In practice though, CSSOM is rarely required, because we rarely need to modify CSS rules from JavaScript (usually we just add/remove CSS classes, not modify their CSS rules), but that's also possible.
```
## BOM (Browser Object Model)

View file

@ -201,7 +201,7 @@ The parent is available as `parentNode`.
For example:
```js
```js run
// parent of <body> is <html>
alert( document.body.parentNode === document.documentElement ); // true

View file

@ -103,7 +103,7 @@ Here we look for all `<li>` elements that are last children:
This method is indeed powerful, because any CSS selector can be used.
```smart header="Can use pseudo-classes as well"
Pseudo-classes in the CSS selector like `:hover` and `:active` are also supported. For instance, `document.querySelectorAll(':hover')` will return the collection with elements that the pointer is over now (in nesting order: from the outermost `<html>` to the most nested one).
Pseudo-classes in the CSS selector like `:hover` and `:active` are also supported. For instance, `document.querySelectorAll(':hover')` will return the collection with elements that the pointer is over now (in nesting order: from the outermost `<html>` to the most nested one).
```
## querySelector [#querySelector]
@ -178,7 +178,7 @@ So here we cover them mainly for completeness, while you can still find them in
- `elem.getElementsByTagName(tag)` looks for elements with the given tag and returns the collection of them. The `tag` parameter can also be a star `"*"` for "any tags".
- `elem.getElementsByClassName(className)` returns elements that have the given CSS class.
- `document.getElementsByName(name)` returns elements with the given `name` attribute, document-wide. very rarely used.
- `document.getElementsByName(name)` returns elements with the given `name` attribute, document-wide. Very rarely used.
For instance:
```js

View file

@ -20,7 +20,7 @@ The classes are:
- [EventTarget](https://dom.spec.whatwg.org/#eventtarget) -- is the root "abstract" class. Objects of that class are never created. It serves as a base, so that all DOM nodes support so-called "events", we'll study them later.
- [Node](http://dom.spec.whatwg.org/#interface-node) -- is also an "abstract" class, serving as a base for DOM nodes. It provides the core tree functionality: `parentNode`, `nextSibling`, `childNodes` and so on (they are getters). Objects of `Node` class are never created. But there are concrete node classes that inherit from it, namely: `Text` for text nodes, `Element` for element nodes and more exotic ones like `Comment` for comment nodes.
- [Element](http://dom.spec.whatwg.org/#interface-element) -- is a base class for DOM elements. It provides element-level navigation like `nextElementSibling`, `children` and searching methods like `getElementsByTagName`, `querySelector`. A browser supports not only HTML, but also XML and SVG. The `Element` class serves as a base for more specific classes: `SVGElement`, `XMLElement` and `HTMLElement`.
- [Element](http://dom.spec.whatwg.org/#interface-element) -- is a base class for DOM elements. It provides element-level navigation like `nextElementSibling`, `children` and searching methods like `getElementsByTagName`, `querySelector`. A browser supports not only HTML, but also XML and SVG. The `Element` class serves as a base for more specific classes: `SVGElement`, `XMLElement` and `HTMLElement`.
- [HTMLElement](https://html.spec.whatwg.org/multipage/dom.html#htmlelement) -- is finally the basic class for all HTML elements. It is inherited by concrete HTML elements:
- [HTMLInputElement](https://html.spec.whatwg.org/multipage/forms.html#htmlinputelement) -- the class for `<input>` elements,
- [HTMLBodyElement](https://html.spec.whatwg.org/multipage/semantics.html#htmlbodyelement) -- the class for `<body>` elements,
@ -36,7 +36,7 @@ It gets properties and methods as a superposition of (listed in inheritance orde
- `HTMLInputElement` -- this class provides input-specific properties,
- `HTMLElement` -- it provides common HTML element methods (and getters/setters),
- `Element` -- provides generic element methods,
- `Node` -- provides common DOM node properties,.
- `Node` -- provides common DOM node properties,
- `EventTarget` -- gives the support for events (to be covered),
- ...and finally it inherits from `Object`, so "plain object" methods like `hasOwnProperty` are also available.

View file

@ -1,9 +1,8 @@
The solution is short, yet may look a bit tricky, so here I provide it with extensive comments:
```js
let sortedRows = Array.from(table.tBodies[0].rows) // (1)
.sort((rowA, rowB) => rowA.cells[0].innerHTML > rowB.cells[0].innerHTML ? 1 : -1); // (2)
let sortedRows = Array.from(table.tBodies[0].rows) // 1
.sort((rowA, rowB) => rowA.cells[0].innerHTML.localeCompare(rowB.cells[0].innerHTML));
table.tBodies[0].append(...sortedRows); // (3)
```
@ -14,6 +13,6 @@ The step-by-step algorthm:
2. Then sort them comparing by the content of the first `<td>` (the name field).
3. Now insert nodes in the right order by `.append(...sortedRows)`.
Please note: we don't have to remove row elements, just "re-insert", they leave the old place automatically.
We don't have to remove row elements, just "re-insert", they leave the old place automatically.
Also note: even if the table HTML doesn't have `<tbody>`, the DOM structure always has it. So we must insert elements as `table.tBodes[0].append(...)`: a simple `table.append(...)` would fail.
P.S. In our case, there's an explicit `<tbody>` in the table, but even if HTML table doesn't have `<tbody>`, the DOM structure always has it.

View file

@ -1,37 +1,30 @@
<!DOCTYPE html>
<html>
<body>
<table id="table">
<tr>
<th>Name</th>
<th>Surname</th>
<th>Age</th>
</tr>
<tr>
<td>John</td>
<td>Smith</td>
<td>10</td>
</tr>
<tr>
<td>Pete</td>
<td>Brown</td>
<td>15</td>
</tr>
<tr>
<td>Ann</td>
<td>Lee</td>
<td>5</td>
</tr>
</table>
<table id="table">
<thead>
<tr>
<th>Name</th><th>Surname</th><th>Age</th>
</tr>
</thead>
<tbody>
<tr>
<td>John</td><td>Smith</td><td>10</td>
</tr>
<tr>
<td>Pete</td><td>Brown</td><td>15</td>
</tr>
<tr>
<td>Ann</td><td>Lee</td><td>5</td>
</tr>
<tr>
<td>...</td><td>...</td><td>...</td>
</tr>
</tbody>
</table>
<script>
let sortedRows = Array.from(table.rows)
.slice(1)
.sort((rowA, rowB) => rowA.cells[0].innerHTML > rowB.cells[0].innerHTML ? 1 : -1);
<script>
let sortedRows = Array.from(table.tBodies[0].rows)
.sort((rowA, rowB) => rowA.cells[0].innerHTML.localeCompare(rowB.cells[0].innerHTML));
table.tBodies[0].append(...sortedRows);
</script>
</body>
</html>
table.tBodies[0].append(...sortedRows);
</script>

View file

@ -1,33 +1,27 @@
<!DOCTYPE html>
<html>
<body>
<table id="table">
<tr>
<th>Name</th>
<th>Surname</th>
<th>Age</th>
</tr>
<tr>
<td>John</td>
<td>Smith</td>
<td>10</td>
</tr>
<tr>
<td>Pete</td>
<td>Brown</td>
<td>15</td>
</tr>
<tr>
<td>Ann</td>
<td>Lee</td>
<td>5</td>
</tr>
</table>
<table id="table">
<thead>
<tr>
<th>Name</th><th>Surname</th><th>Age</th>
</tr>
</thead>
<tbody>
<tr>
<td>John</td><td>Smith</td><td>10</td>
</tr>
<tr>
<td>Pete</td><td>Brown</td><td>15</td>
</tr>
<tr>
<td>Ann</td><td>Lee</td><td>5</td>
</tr>
<tr>
<td>...</td><td>...</td><td>...</td>
</tr>
</tbody>
</table>
<script>
// ... your code ...
</script>
</body>
</html>
<script>
// ... your code ...
</script>

View file

@ -6,33 +6,29 @@ importance: 5
There's a table:
```html run
<table>
<tr>
<th>Name</th>
<th>Surname</th>
<th>Age</th>
</tr>
<tr>
<td>John</td>
<td>Smith</td>
<td>10</td>
</tr>
<tr>
<td>Pete</td>
<td>Brown</td>
<td>15</td>
</tr>
<tr>
<td>Ann</td>
<td>Lee</td>
<td>5</td>
</tr>
<tr>
<td>...</td>
<td>...</td>
<td>...</td>
</tr>
<thead>
<tr>
<th>Name</th><th>Surname</th><th>Age</th>
</tr>
</thead>
<tbody>
<tr>
<td>John</td><td>Smith</td><td>10</td>
</tr>
<tr>
<td>Pete</td><td>Brown</td><td>15</td>
</tr>
<tr>
<td>Ann</td><td>Lee</td><td>5</td>
</tr>
<tr>
<td>...</td><td>...</td><td>...</td>
</tr>
</tbody>
</table>
```
There may be more rows in it.

View file

@ -3,7 +3,7 @@ We'll create the table as a string: `"<table>...</table>"`, and then assign it t
The algorithm:
1. Create the table header with `<th>` and weekday names.
1. Create the date object `d = new Date(year, month-1)`. That's the first day of `month` (taking into account that months in JavaScript start from `0`, not `1`).
2. First few cells till the first day of the month `d.getDay()` may be empty. Let's fill them in with `<td></td>`.
3. Increase the day in `d`: `d.setDate(d.getDate()+1)`. If `d.getMonth()` is not yet the next month, then add the new cell `<td>` to the calendar. If that's a Sunday, then add a newline <code>"&lt;/tr&gt;&lt;tr&gt;"</code>.
4. If the month has finished, but the table row is not yet full, add empty `<td>` into it, to make it square.
2. Create the date object `d = new Date(year, month-1)`. That's the first day of `month` (taking into account that months in JavaScript start from `0`, not `1`).
3. First few cells till the first day of the month `d.getDay()` may be empty. Let's fill them in with `<td></td>`.
4. Increase the day in `d`: `d.setDate(d.getDate()+1)`. If `d.getMonth()` is not yet the next month, then add the new cell `<td>` to the calendar. If that's a Sunday, then add a newline <code>"&lt;/tr&gt;&lt;tr&gt;"</code>.
5. If the month has finished, but the table row is not yet full, add empty `<td>` into it, to make it square.

View file

@ -28,7 +28,7 @@ Here's how it will look:
*/!*
```
That was an HTML example. Now let's create the same `div` with JavaScript (assuming that the styles are in the HTML or an external CSS file).
That was the HTML example. Now let's create the same `div` with JavaScript (assuming that the styles are in the HTML/CSS already).
## Creating an element
@ -48,21 +48,28 @@ To create DOM nodes, there are two methods:
let textNode = document.createTextNode('Here I am');
```
Most of the time we need to create element nodes, such as the `div` for the message.
### Creating the message
In our case the message is a `div` with `alert` class and the HTML in it:
Creating the message div takes 3 steps:
```js
// 1. Create <div> element
let div = document.createElement('div');
// 2. Set its class to "alert"
div.className = "alert";
// 3. Fill it with the content
div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";
```
We created the element, but as of now it's only in a variable. We can't see the element on the page, as it's not yet a part of the document.
We've created the element. But as of now it's only in a variable named `div`, not in the page yet. So we can't see it.
## Insertion methods
To make the `div` show up, we need to insert it somewhere into `document`. For instance, in `document.body`.
To make the `div` show up, we need to insert it somewhere into `document`. For instance, into `<body>` element, referenced by `document.body`.
There's a special method `append` for that: `document.body.append(div)`.
@ -90,14 +97,20 @@ Here's the full code:
</script>
```
This set of methods provides more ways to insert:
Here we called `append` on `document.body`, but we can call `append` method on any other element, to put another element into it. For instance, we can append something to `<div>` by calling `div.append(anotherElement)`.
- `node.append(...nodes or strings)` -- append nodes or strings at the end of `node`,
- `node.prepend(...nodes or strings)` -- insert nodes or strings at the beginning of `node`,
- `node.before(...nodes or strings)` - insert nodes or strings before `node`,
- `node.after(...nodes or strings)` - insert nodes or strings after `node`,
Here are more insertion methods, they specify different places where to insert:
- `node.append(...nodes or strings)` -- append nodes or strings *at the end* of `node`,
- `node.prepend(...nodes or strings)` -- insert nodes or strings *at the beginning* of `node`,
- `node.before(...nodes or strings)` - insert nodes or strings *before* `node`,
- `node.after(...nodes or strings)` - insert nodes or strings *after* `node`,
- `node.replaceWith(...nodes or strings)` - replaces `node` with the given nodes or strings.
Arguments of these methods are an arbitrary list of DOM nodes to insert, or text strings (that become text nodes automatically).
Let's see them in action.
Here's an example of using these methods to add items to a list and the text before/after it:
```html autorun
@ -121,7 +134,7 @@ Here's an example of using these methods to add items to a list and the text bef
</script>
```
Here's a visual picture what methods do:
Here's a visual picture of what the methods do:
![](before-prepend-append-after.svg)
@ -139,7 +152,7 @@ before
after
```
These methods can insert multiple lists of nodes and text pieces in a single call.
As said, these methods can insert multiple nodes and text pieces in a single call.
For instance, here a string and an element are inserted:
@ -150,7 +163,7 @@ For instance, here a string and an element are inserted:
</script>
```
All text is inserted *as text*.
Please note: the text is inserted "as text", not "as HTML", with proper escaping of characters such as `<`, `>`.
So the final HTML is:
@ -166,7 +179,7 @@ In other words, strings are inserted in a safe way, like `elem.textContent` does
So, these methods can only be used to insert DOM nodes or text pieces.
But what if we want to insert HTML "as html", with all tags and stuff working, like `elem.innerHTML`?
But what if we'd like to insert an HTML string "as html", with all tags and stuff working, in the same manner as `elem.innerHTML` does it?
## insertAdjacentHTML/Text/Element

View file

@ -249,7 +249,7 @@ For instance:
```smart header="Computed and resolved values"
There are two concepts in [CSS](https://drafts.csswg.org/cssom/#resolved-values):
1. A *computed* style value is the value after all CSS rules and CSS inheritance is applied, as the result of the CSS cascade. It can look like `height:1em` or `font-size:125%`.
1. A *computed* style value is the value after all CSS rules and CSS inheritance is applied, as the result of the CSS cascade. It can look like `height:1em` or `font-size:125%`.
2. A *resolved* style value is the one finally applied to the element. Values like `1em` or `125%` are relative. The browser takes the computed value and makes all units fixed and absolute, for instance: `height:20px` or `font-size:16px`. For geometry properties resolved values may have a floating point, like `width:50.5px`.
A long time ago `getComputedStyle` was created to get computed values, but it turned out that resolved values are much more convenient, and the standard changed.

View file

@ -9,7 +9,7 @@
background-color: #00FF00;
position: relative;
}
#ball {
position: absolute;
}
@ -20,7 +20,7 @@
<div id="field">
<img src="https://js.cx/clipart/ball.svg" id="ball"> . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
<img src="https://js.cx/clipart/ball.svg" height="40" width="40" id="ball"> . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
</div>
@ -38,4 +38,4 @@
</body>
</html>
</html>

View file

@ -24,17 +24,22 @@ ball.style.left = Math.round(field.clientWidth / 2 - ball.offsetWidth / 2) + 'px
ball.style.top = Math.round(field.clientHeight / 2 - ball.offsetHeight / 2) + 'px';
```
**Attention: the pitfall!**
Now the ball is finally centered.
````warn header="Attention: the pitfall!"
The code won't work reliably while `<img>` has no width/height:
```html
<img src="ball.png" id="ball">
```
````
When the browser does not know the width/height of an image (from tag attributes or CSS), then it assumes them to equal `0` until the image finishes loading.
After the first load browser usually caches the image, and on next loads it will have the size immediately. But on the first load the value of `ball.offsetWidth` is `0`. That leads to wrong coordinates.
So the value of `ball.offsetWidth` will be `0` until the image loads. That leads to wrong coordinates in the code above.
After the first load, the browser usually caches the image, and on reloads it will have the size immediately. But on the first load the value of `ball.offsetWidth` is `0`.
We should fix that by adding `width/height` to `<img>`:

View file

@ -26,7 +26,7 @@
<script>
// ball.offsetWidth=0 before image loaded!
// ball.offsetWidth=0 before image loaded!
// to fix: set width
ball.style.left = Math.round(field.clientWidth / 2 - ball.offsetWidth / 2) + 'px'
ball.style.top = Math.round(field.clientHeight / 2 - ball.offsetHeight / 2) + 'px'

View file

@ -10,7 +10,7 @@ Here's how the source document looks:
What are coordinates of the field center?
Calculate them and use to place the ball into the center of the field:
Calculate them and use to place the ball into the center of the green field:
[iframe src="solution" height=180]

View file

@ -211,7 +211,7 @@ If you click the element below, the code `elem.scrollTop += 10` executes. That m
<div onclick="this.scrollTop+=10" style="cursor:pointer;border:1px solid black;width:100px;height:80px;overflow:auto">Click<br>Me<br>1<br>2<br>3<br>4<br>5<br>6<br>7<br>8<br>9</div>
```
Setting `scrollTop` to `0` or `Infinity` will make the element scroll to the very top/bottom respectively.
Setting `scrollTop` to `0` or a big value, such as `1e9` will make the element scroll to the very top/bottom respectively.
````
## Don't take width/height from CSS

View file

@ -60,11 +60,11 @@ Why so? Better don't ask. These inconsistencies come from ancient times, not a "
## Get the current scroll [#page-scroll]
DOM elements have their current scroll state in `elem.scrollLeft/scrollTop`.
DOM elements have their current scroll state in their `scrollLeft/scrollTop` properties.
For document scroll, `document.documentElement.scrollLeft/scrollTop` works in most browsers, except older WebKit-based ones, like Safari (bug [5991](https://bugs.webkit.org/show_bug.cgi?id=5991)), where we should use `document.body` instead of `document.documentElement`.
Luckily, we don't have to remember these peculiarities at all, because the scroll is available in the special properties `window.pageXOffset/pageYOffset`:
Luckily, we don't have to remember these peculiarities at all, because the scroll is available in the special properties, `window.pageXOffset/pageYOffset`:
```js run
alert('Current scroll from the top: ' + window.pageYOffset);

View file

@ -1,6 +1,6 @@
# Outer corners
Outer corners are basically what we get from [elem.getBoundingClientRect()](https://developer.mozilla.org/en-US/docs/DOM/element.getBoundingClientRect).
Outer corners are basically what we get from [elem.getBoundingClientRect()](https://developer.mozilla.org/en-US/docs/DOM/element.getBoundingClientRect).
Coordinates of the upper-left corner `answer1` and the bottom-right corner `answer2`:

View file

@ -88,10 +88,10 @@ As you can see, `left/top` do not equal `x/y` in such case.
In practice though, `elem.getBoundingClientRect()` always returns positive width/height, here we mention negative `width/height` only for you to understand why these seemingly duplicate properties are not actually duplicates.
```
```warn header="Internet Explorer and Edge: no support for `x/y`"
Internet Explorer and Edge don't support `x/y` properties for historical reasons.
```warn header="Internet Explorer: no support for `x/y`"
Internet Explorer doesn't support `x/y` properties for historical reasons.
So we can either make a polywill (add getters in `DomRect.prototype`) or just use `top/left`, as they are always the same as `x/y` for positive `width/height`, in particular in the result of `elem.getBoundingClientRect()`.
So we can either make a polyfill (add getters in `DomRect.prototype`) or just use `top/left`, as they are always the same as `x/y` for positive `width/height`, in particular in the result of `elem.getBoundingClientRect()`.
```
```warn header="Coordinates right/bottom are different from CSS position properties"
@ -216,6 +216,8 @@ function getCoords(elem) {
return {
top: box.top + window.pageYOffset,
right: box.right + window.pageXOffset,
bottom: box.bottom + window.pageYOffset,
left: box.left + window.pageXOffset
};
}

View file

@ -11,13 +11,13 @@ Here's a list of the most useful DOM events, just to take a look at:
- `mousedown` / `mouseup` -- when the mouse button is pressed / released over an element.
- `mousemove` -- when the mouse is moved.
**Keyboard events:**
- `keydown` and `keyup` -- when a keyboard key is pressed and released.
**Form element events:**
- `submit` -- when the visitor submits a `<form>`.
- `focus` -- when the visitor focuses on an element, e.g. on an `<input>`.
**Keyboard events:**
- `keydown` and `keyup` -- when the visitor presses and then releases the button.
**Document events:**
- `DOMContentLoaded` -- when the HTML is loaded and processed, DOM is fully built.
@ -87,8 +87,6 @@ If the handler is assigned using an HTML-attribute then the browser reads it, cr
So this way is actually the same as the previous one.
**The handler is always in the DOM property: the HTML-attribute is just one of the ways to initialize it.**
These two code pieces work the same:
1. Only HTML:
@ -109,6 +107,8 @@ These two code pieces work the same:
</script>
```
In the first example, the HTML attribute is used to initialize the `button.onclick`, while in the second example -- the script, that's all the difference.
**As there's only one `onclick` property, we can't assign more than one event handler.**
In the example below adding a handler with JavaScript overwrites the existing handler:
@ -124,16 +124,6 @@ In the example below adding a handler with JavaScript overwrites the existing ha
</script>
```
By the way, we can assign an existing function as a handler directly:
```js
function sayThanks() {
alert('Thanks!');
}
elem.onclick = sayThanks;
```
To remove a handler -- assign `elem.onclick = null`.
## Accessing the element: this
@ -150,7 +140,17 @@ In the code below `button` shows its contents using `this.innerHTML`:
If you're starting to work with events -- please note some subtleties.
**The function should be assigned as `sayThanks`, not `sayThanks()`.**
We can set an existing function as a handler:
```js
function sayThanks() {
alert('Thanks!');
}
elem.onclick = sayThanks;
```
But be careful: the function should be assigned as `sayThanks`, not `sayThanks()`.
```js
// right
@ -160,7 +160,7 @@ button.onclick = sayThanks;
button.onclick = sayThanks();
```
If we add parentheses, `sayThanks()` -- is a function call. So the last line actually takes the *result* of the function execution, that is `undefined` (as the function returns nothing), and assigns it to `onclick`. That doesn't work.
If we add parentheses, then `sayThanks()` becomes is a function call. So the last line actually takes the *result* of the function execution, that is `undefined` (as the function returns nothing), and assigns it to `onclick`. That doesn't work.
...On the other hand, in the markup we do need the parentheses:
@ -168,21 +168,17 @@ If we add parentheses, `sayThanks()` -- is a function call. So the last line ac
<input type="button" id="button" onclick="sayThanks()">
```
The difference is easy to explain. When the browser reads the attribute, it creates a handler function with *body from its content*: `sayThanks()`.
The difference is easy to explain. When the browser reads the attribute, it creates a handler function with body from the attribute content.
So the markup generates this property:
```js
button.onclick = function() {
*!*
sayThanks(); // the attribute content
sayThanks(); // <-- the attribute content goes here
*/!*
};
```
**Use functions, not strings.**
The assignment `elem.onclick = "alert(1)"` would work too. It works for compatibility reasons, but is strongly not recommended.
**Don't use `setAttribute` for handlers.**
Such a call won't work:
@ -201,7 +197,7 @@ Assign a handler to `elem.onclick`, not `elem.ONCLICK`, because DOM properties a
The fundamental problem of the aforementioned ways to assign handlers -- we can't assign multiple handlers to one event.
For instance, one part of our code wants to highlight a button on click, and another one wants to show a message.
Let's say, one part of our code wants to highlight a button on click, and another one wants to show a message on the same click.
We'd like to assign two event handlers for that. But a new DOM property will overwrite the existing one:
@ -211,12 +207,12 @@ input.onclick = function() { alert(1); }
input.onclick = function() { alert(2); } // replaces the previous handler
```
Web-standard developers understood that long ago and suggested an alternative way of managing handlers using special methods `addEventListener` and `removeEventListener`. They are free of such a problem.
Developers of web standards understood that long ago and suggested an alternative way of managing handlers using special methods `addEventListener` and `removeEventListener`. They are free of such a problem.
The syntax to add a handler:
```js
element.addEventListener(event, handler[, options]);
element.addEventListener(event, handler, [options]);
```
`event`
@ -229,19 +225,18 @@ element.addEventListener(event, handler[, options]);
: An additional optional object with properties:
- `once`: if `true`, then the listener is automatically removed after it triggers.
- `capture`: the phase where to handle the event, to be covered later in the chapter <info:bubbling-and-capturing>. For historical reasons, `options` can also be `false/true`, that's the same as `{capture: false/true}`.
- `passive`: if `true`, then the handler will not `preventDefault()`, we'll cover that later in <info:default-browser-action>.
- `passive`: if `true`, then the handler will not call `preventDefault()`, we'll explain that later in <info:default-browser-action>.
To remove the handler, use `removeEventListener`:
```js
element.removeEventListener(event, handler[, options]);
element.removeEventListener(event, handler, [options]);
```
````warn header="Removal requires the same function"
To remove a handler we should pass exactly the same function as was assigned.
That doesn't work:
This doesn't work:
```js no-beautify
elem.addEventListener( "click" , () => alert('Thanks!'));
@ -249,7 +244,7 @@ elem.addEventListener( "click" , () => alert('Thanks!'));
elem.removeEventListener( "click", () => alert('Thanks!'));
```
The handler won't be removed, because `removeEventListener` gets another function -- with the same code, but that doesn't matter.
The handler won't be removed, because `removeEventListener` gets another function -- with the same code, but that doesn't matter, as it's a different function object.
Here's the right way:
@ -291,47 +286,33 @@ Multiple calls to `addEventListener` allow to add multiple handlers, like this:
As we can see in the example above, we can set handlers *both* using a DOM-property and `addEventListener`. But generally we use only one of these ways.
````warn header="For some events, handlers only work with `addEventListener`"
There exist events that can't be assigned via a DOM-property. Must use `addEventListener`.
There exist events that can't be assigned via a DOM-property. Only with `addEventListener`.
For instance, the event `transitionend` (CSS animation finished) is like that.
For instance, the `DOMContentLoaded` event, that triggers when the document is loaded and DOM is built.
Try the code below. In most browsers only the second handler works, not the first one.
```html run
<style>
input {
transition: width 1s;
width: 100px;
}
.wide {
width: 300px;
}
</style>
<input type="button" id="elem" onclick="this.classList.toggle('wide')" value="Click me">
<script>
elem.ontransitionend = function() {
alert("DOM property"); // doesn't work
};
*!*
elem.addEventListener("transitionend", function() {
alert("addEventListener"); // shows up when the animation finishes
});
*/!*
</script>
```js
// will never run
document.onDOMContentLoaded = function() {
alert("DOM built");
};
```
```js
// this way it works
document.addEventListener("DOMContentLoaded", function() {
alert("DOM built");
});
```
So `addEventListener` is more universal. Although, such events are an exception rather than the rule.
````
## Event object
To properly handle an event we'd want to know more about what's happened. Not just a "click" or a "keypress", but what were the pointer coordinates? Which key was pressed? And so on.
To properly handle an event we'd want to know more about what's happened. Not just a "click" or a "keydown", but what were the pointer coordinates? Which key was pressed? And so on.
When an event happens, the browser creates an *event object*, puts details into it and passes it as an argument to the handler.
Here's an example of getting mouse coordinates from the event object:
Here's an example of getting pointer coordinates from the event object:
```html run
<input type="button" value="Click me" id="elem">
@ -354,11 +335,11 @@ Some properties of `event` object:
: Element that handled the event. That's exactly the same as `this`, unless the handler is an arrow function, or its `this` is bound to something else, then we can get the element from `event.currentTarget`.
`event.clientX / event.clientY`
: Window-relative coordinates of the cursor, for mouse events.
: Window-relative coordinates of the cursor, for pointer events.
There are more properties. They depend on the event type, so we'll study them later when we come to different events in details.
There are more properties. Many of them depend on the event type: keyboard events have one set of properties, pointer events - another one, we'll study them later when we come to different events in details.
````smart header="The event object is also accessible from HTML"
````smart header="The event object is also available in HTML handlers"
If we assign a handler in HTML, we can also use the `event` object, like this:
```html autorun height=60
@ -380,15 +361,17 @@ For instance:
<button id="elem">Click me</button>
<script>
elem.addEventListener('click', {
let obj = {
handleEvent(event) {
alert(event.type + " at " + event.currentTarget);
}
});
};
elem.addEventListener('click', obj);
</script>
```
As we can see, when `addEventListener` receives an object as the handler, it calls `object.handleEvent(event)` in case of an event.
As we can see, when `addEventListener` receives an object as the handler, it calls `obj.handleEvent(event)` in case of an event.
We could also use a class for that:
@ -462,7 +445,7 @@ HTML attributes are used sparingly, because JavaScript in the middle of an HTML
DOM properties are ok to use, but we can't assign more than one handler of the particular event. In many cases that limitation is not pressing.
The last way is the most flexible, but it is also the longest to write. There are few events that only work with it, for instance `transtionend` and `DOMContentLoaded` (to be covered). Also `addEventListener` supports objects as event handlers. In that case the method `handleEvent` is called in case of the event.
The last way is the most flexible, but it is also the longest to write. There are few events that only work with it, for instance `transitionend` and `DOMContentLoaded` (to be covered). Also `addEventListener` supports objects as event handlers. In that case the method `handleEvent` is called in case of the event.
No matter how you assign the handler -- it gets an event object as the first argument. That object contains the details about what's happened.

View file

@ -181,7 +181,7 @@ The code sets click handlers on *every* element in the document to see which one
If you click on `<p>`, then the sequence is:
1. `HTML` -> `BODY` -> `FORM` -> `DIV` (capturing phase, the first listener):
2. `P` (target phrase, triggers two times, as we've set two listeners: capturing and bubbling)
2. `P` (target phase, triggers two times, as we've set two listeners: capturing and bubbling)
3. `DIV` -> `FORM` -> `BODY` -> `HTML` (bubbling phase, the second listener).
There's a property `event.eventPhase` that tells us the number of the phase on which the event was caught. But it's rarely used, because we usually know it in the handler.
@ -204,9 +204,9 @@ elem.addEventListener("click", e => alert(2));
When an event happens -- the most nested element where it happens gets labeled as the "target element" (`event.target`).
- Then the event moves down from the document root to `event.target`, calling handlers assigned with `addEventListener(...., true)` on the way (`true` is a shorthand for `{capture: true}`).
- Then the event moves down from the document root to `event.target`, calling handlers assigned with `addEventListener(..., true)` on the way (`true` is a shorthand for `{capture: true}`).
- Then handlers are called on the target element itself.
- Then the event bubbles up from `event.target` up to the root, calling handlers assigned using `on<event>` and `addEventListener` without the 3rd argument or with the 3rd argument `false/{capture:false}`.
- Then the event bubbles up from `event.target` to the root, calling handlers assigned using `on<event>` and `addEventListener` without the 3rd argument or with the 3rd argument `false/{capture:false}`.
Each handler can access `event` object properties:
@ -220,6 +220,6 @@ The capturing phase is used very rarely, usually we handle events on bubbling. A
In real world, when an accident happens, local authorities react first. They know best the area where it happened. Then higher-level authorities if needed.
The same for event handlers. The code that set the handler on a particular element knows maximum details about the element and what it does. A handler on a particular `<td>` may be suited for that exactly `<td>`, it knows everything about it, so it should get the chance first. Then its immediate parent also knows about the context, but a little bit less, and so on till the very top element that handles general concepts and runs the last.
The same for event handlers. The code that set the handler on a particular element knows maximum details about the element and what it does. A handler on a particular `<td>` may be suited for that exactly `<td>`, it knows everything about it, so it should get the chance first. Then its immediate parent also knows about the context, but a little bit less, and so on till the very top element that handles general concepts and runs the last one.
Bubbling and capturing lay the foundation for "event delegation" -- an extremely powerful event handling pattern that we study in the next chapter.

View file

@ -101,8 +101,8 @@ table.onclick = function(event) {
Explanations:
1. The method `elem.closest(selector)` returns the nearest ancestor that matches the selector. In our case we look for `<td>` on the way up from the source element.
2. If `event.target` is not inside any `<td>`, then the call returns `null`, and we don't have to do anything.
3. In case of nested tables, `event.target` may be a `<td>` lying outside of the current table. So we check if that's actually *our table's* `<td>`.
2. If `event.target` is not inside any `<td>`, then the call returns immediately, as there's nothing to do.
3. In case of nested tables, `event.target` may be a `<td>`, but lying outside of the current table. So we check if that's actually *our table's* `<td>`.
4. And, if it's so, then highlight it.
As the result, we have a fast, efficient highlighting code, that doesn't care about the total number of `<td>` in the table.
@ -121,7 +121,7 @@ The first idea may be to assign a separate handler to each button. But there's a
The handler reads the attribute and executes the method. Take a look at the working example:
```html autorun height=60 run
```html autorun height=60 run untrusted
<div id="menu">
<button data-action="save">Save</button>
<button data-action="load">Load</button>

View file

@ -113,7 +113,7 @@ The property `event.defaultPrevented` is `true` if the default action was preven
There's an interesting use case for it.
You remember in the chapter <info:bubbling-and-capturing> we talked about `event.stopPropagation()` and why stopping bubbling is bad?
You remember in the chapter <info:bubbling-and-capturing> we talked about `event.stopPropagation()` and why stopping bubbling is bad?
Sometimes we can use `event.defaultPrevented` instead, to signal other event handlers that the event was handled.

View file

@ -162,7 +162,7 @@ Besides, the event class describes "what kind of event" it is, and if the event
## event.preventDefault()
Many browser events have a "default action", such as nagivating to a link, starting a selection, and so on.
Many browser events have a "default action", such as navigating to a link, starting a selection, and so on.
For new, custom events, there are definitely no default browser actions, but a code that dispatches such event may have its own plans what to do after triggering the event.
@ -187,7 +187,6 @@ Any handler can listen for that event with `rabbit.addEventListener('hide',...)`
<button onclick="hide()">Hide()</button>
<script>
// hide() will be called automatically in 2 seconds
function hide() {
let event = new CustomEvent("hide", {
cancelable: true // without that flag preventDefault doesn't work
@ -211,13 +210,14 @@ Please note: the event must have the flag `cancelable: true`, otherwise the call
## Events-in-events are synchronous
Usually events are processed asynchronously. That is: if the browser is processing `onclick` and in the process a new event occurs, then it waits until the `onclick` processing is finished.
Usually events are processed in a queue. That is: if the browser is processing `onclick` and a new event occurs, e.g. mouse moved, then it's handling is queued up, corresponding `mousemove` handlers will be called after `onclick` processing is finished.
The exception is when one event is initiated from within another one.
The notable exception is when one event is initiated from within another one, e.g. using `dispatchEvent`. Such events are processed immediately: the new event handlers are called, and then the current event handling is resumed.
Then the control jumps to the nested event handler, and after it goes back.
For instance, in the code below the `menu-open` event is triggered during the `onclick`.
It's processed immediately, without waiting for `onclick` handler to end:
For instance, here the nested `menu-open` event is processed synchronously, during the `onclick`:
```html run autorun
<button id="menu">Menu (click me)</button>
@ -226,7 +226,6 @@ For instance, here the nested `menu-open` event is processed synchronously, duri
menu.onclick = function() {
alert(1);
// alert("nested")
menu.dispatchEvent(new CustomEvent("menu-open", {
bubbles: true
}));
@ -234,17 +233,20 @@ For instance, here the nested `menu-open` event is processed synchronously, duri
alert(2);
};
// triggers between 1 and 2
document.addEventListener('menu-open', () => alert('nested'));
</script>
```
```
The output order is: 1 -> nested -> 2.
Please note that the nested event `menu-open` fully bubbles up and is handled on the `document`. The propagation and handling of the nested event must be fully finished before the processing gets back to the outer code (`onclick`).
Please note that the nested event `menu-open` is caught on the `document`. The propagation and handling of the nested event is finished before the processing gets back to the outer code (`onclick`).
That's not only about `dispatchEvent`, there are other cases. JavaScript in an event handler can call methods that lead to other events -- they are too processed synchronously.
That's not only about `dispatchEvent`, there are other cases. If an event handler calls methods that trigger other events -- they are processed synchronously too, in a nested fashion.
If we don't like it, we can either put the `dispatchEvent` (or other event-triggering call) at the end of `onclick` or, maybe better, wrap it in zero-delay `setTimeout`:
Let's say we don't like it. We'd want `onclick` to be fully processed first, independently from `menu-open` or any other nested events.
Then we can either put the `dispatchEvent` (or another event-triggering call) at the end of `onclick` or, maybe better, wrap it in the zero-delay `setTimeout`:
```html run
<button id="menu">Menu (click me)</button>
@ -253,7 +255,6 @@ If we don't like it, we can either put the `dispatchEvent` (or other event-trigg
menu.onclick = function() {
alert(1);
// alert(2)
setTimeout(() => menu.dispatchEvent(new CustomEvent("menu-open", {
bubbles: true
})));
@ -263,7 +264,7 @@ If we don't like it, we can either put the `dispatchEvent` (or other event-trigg
document.addEventListener('menu-open', () => alert('nested'));
</script>
```
```
Now `dispatchEvent` runs asynchronously after the current code execution is finished, including `mouse.onclick`, so event handlers are totally separate.
@ -281,9 +282,9 @@ Other constructors of native events like `MouseEvent`, `KeyboardEvent` and so on
For custom events we should use `CustomEvent` constructor. It has an additional option named `detail`, we should assign the event-specific data to it. Then all handlers can access it as `event.detail`.
Despite the technical possibility to generate browser events like `click` or `keydown`, we should use with the great care.
Despite the technical possibility of generating browser events like `click` or `keydown`, we should use them with great care.
We shouldn't generate browser events as it's a hacky way to run handlers. That's a bad architecture most of the time.
We shouldn't generate browser events as it's a hacky way to run handlers. That's bad architecture most of the time.
Native events might be generated:

View file

@ -1,4 +1,4 @@
# Mouse events basics
# Mouse events
In this chapter we'll get into more details about mouse events and their properties.
@ -6,11 +6,7 @@ Please note: such events may come not only from "mouse devices", but are also fr
## Mouse event types
We can split mouse events into two categories: "simple" and "complex"
### Simple events
The most used simple events are:
We've already seen some of these events:
`mousedown/mouseup`
: Mouse button is clicked/released over an element.
@ -21,26 +17,22 @@ The most used simple events are:
`mousemove`
: Every mouse move over an element triggers that event.
`contextmenu`
: Triggers when opening a context menu is attempted. In the most common case, that happens when the right mouse button is pressed. Although, there are other ways to open a context menu, e.g. using a special keyboard key, so it's not exactly the mouse event.
...There are several other event types too, we'll cover them later.
### Complex events
`click`
: Triggers after `mousedown` and then `mouseup` over the same element if the left mouse button was used.
`dblclick`
: Triggers after a double click over an element.
: Triggers after two clicks on the same element within a short timeframe. Rarely used nowadays.
Complex events are made of simple ones, so in theory we could live without them. But they exist, and that's good, because they are convenient.
`contextmenu`
: Triggers when the right mouse button is pressed. There are other ways to open a context menu, e.g. using a special keyboard key, it triggers in that case also, so it's not exactly the mouse event.
### Events order
...There are several other events too, we'll cover them later.
An action may trigger multiple events.
## Events order
For instance, a click first triggers `mousedown`, when the button is pressed, then `mouseup` and `click` when it's released.
As you can see from the list above, a user action may trigger multiple events.
For instance, a left-button click first triggers `mousedown`, when the button is pressed, then `mouseup` and `click` when it's released.
In cases when a single action initiates multiple events, their order is fixed. That is, the handlers are called in the order `mousedown` -> `mouseup` -> `click`.
@ -49,26 +41,42 @@ Click the button below and you'll see the events. Try double-click too.
On the teststand below all mouse events are logged, and if there is more than a 1 second delay between them they are separated by a horizontal ruler.
Also we can see the `which` property that allows to detect the mouse button.
Also we can see the `button` property that allows to detect the mouse button, it's explained below.
<input onmousedown="return logMouse(event)" onmouseup="return logMouse(event)" onclick="return logMouse(event)" oncontextmenu="return logMouse(event)" ondblclick="return logMouse(event)" value="Click me with the right or the left mouse button" type="button"> <input onclick="logClear('test')" value="Clear" type="button"> <form id="testform" name="testform"> <textarea style="font-size:12px;height:150px;width:360px;"></textarea></form>
```
## Getting the button: which
## Mouse button
Click-related events always have the `which` property, which allows to get the exact mouse button.
Click-related events always have the `button` property, which allows to get the exact mouse button.
It is not used for `click` and `contextmenu` events, because the former happens only on left-click, and the latter -- only on right-click.
We usually don't use it for `click` and `contextmenu` events, because the former happens only on left-click, and the latter -- only on right-click.
But if we track `mousedown` and `mouseup`, then we need it, because these events trigger on any button, so `which` allows to distinguish between "right-mousedown" and "left-mousedown".
From the other hand, `mousedown` and `mouseup` handlers we may need `event.button`, because these events trigger on any button, so `button` allows to distinguish between "right-mousedown" and "left-mousedown".
There are the three possible values:
The possible values of `event.button` are:
- `event.which == 1` -- the left button
- `event.which == 2` - the middle button
- `event.which == 3` - the right button
| Button state | `event.button` |
|--------------|----------------|
| Left button (primary) | 0 |
| Middle button (auxiliary) | 1 |
| Right button (secondary) | 2 |
| X1 button (back) | 3 |
| X2 button (forward) | 4 |
The middle button is somewhat exotic right now and is very rarely used.
Most mouse devices only have the left and right buttons, so possible values are `0` or `2`. Touch devices also generate similar events when one taps on them.
Also there's `event.buttons` property that has all currently pressed buttons as an integer, one bit per button. In practice this property is very rarely used, you can find details at [MDN](mdn:/api/MouseEvent/buttons) if you ever need it.
```warn header="The outdated `event.which`"
Old code may use `event.which` property that's an old non-standard way of getting a button, with possible values:
- `event.which == 1` left button,
- `event.which == 2` middle button,
- `event.which == 3` right button.
As of now, `event.which` is deprecated, we shouldn't use it.
```
## Modifiers: shift, alt, ctrl and meta
@ -116,18 +124,25 @@ For JS-code it means that we should check `if (event.ctrlKey || event.metaKey)`.
```
```warn header="There are also mobile devices"
Keyboard combinations are good as an addition to the workflow. So that if the visitor has a
keyboard -- it works. And if their device doesn't have it -- then there should be another way to do the same.
Keyboard combinations are good as an addition to the workflow. So that if the visitor uses a keyboard -- they work.
But if their device doesn't have it -- then there should be a way to live without modifier keys.
```
## Coordinates: clientX/Y, pageX/Y
All mouse events have coordinates in two flavours:
All mouse events provide coordinates in two flavours:
1. Window-relative: `clientX` and `clientY`.
2. Document-relative: `pageX` and `pageY`.
For instance, if we have a window of the size 500x500, and the mouse is in the left-upper corner, then `clientX` and `clientY` are `0`. And if the mouse is in the center, then `clientX` and `clientY` are `250`, no matter what place in the document it is, how far the document was scrolled. They are similar to `position:fixed`.
We already covered the difference between them in the chapter <info:coordinates>.
In short, document-relative coordinates `pageX/Y` are counted from the left-upper corner of the document, and do not change when the page is scrolled, while `clientX/Y` are counted from the current window left-upper corner. When the page is scrolled, they change.
For instance, if we have a window of the size 500x500, and the mouse is in the left-upper corner, then `clientX` and `clientY` are `0`, no matter how the page is scrolled.
And if the mouse is in the center, then `clientX` and `clientY` are `250`, no matter what place in the document it is. They are similar to `position:fixed` in that aspect.
````online
Move the mouse over the input field to see `clientX/clientY` (the example is in the `iframe`, so coordinates are relative to that `iframe`):
@ -137,13 +152,11 @@ Move the mouse over the input field to see `clientX/clientY` (the example is in
```
````
Document-relative coordinates `pageX`, `pageY` are counted from the left-upper corner of the document, not the window. You can read more about coordinates in the chapter <info:coordinates>.
## Preventing selection on mousedown
## Disabling selection
Double mouse click has a side-effect that may be disturbing in some interfaces: it selects text.
Double mouse click has a side-effect that may be disturbing in some interfaces: it selects the text.
For instance, a double-click on the text below selects it in addition to our handler:
For instance, double-clicking on the text below selects it in addition to our handler:
```html autorun height=50
<span ondblclick="alert('dblclick')">Double-click me</span>
@ -186,7 +199,7 @@ Surely the user has access to HTML-source of the page, and can take the content
Mouse events have the following properties:
- Button: `which`.
- Button: `button`.
- Modifier keys (`true` if pressed): `altKey`, `ctrlKey`, `shiftKey` and `metaKey` (Mac).
- If you want to handle `key:Ctrl`, then don't forget Mac users, they usually use `key:Cmd`, so it's better to check `if (e.metaKey || e.ctrlKey)`.

View file

@ -25,7 +25,7 @@
function logMouse(e) {
let evt = e.type;
while (evt.length < 11) evt += ' ';
showmesg(evt + " which=" + e.which, 'test')
showmesg(evt + " button=" + e.button, 'test')
return false;
}

View file

@ -125,7 +125,7 @@ If there are some actions upon leaving the parent element, e.g. an animation run
To avoid it, we can check `relatedTarget` in the handler and, if the mouse is still inside the element, then ignore such event.
Alternatively we can use other events: `mouseenter` и `mouseleave`, that we'll be covering now, as they don't have such problems.
Alternatively we can use other events: `mouseenter` and `mouseleave`, that we'll be covering now, as they don't have such problems.
## Events mouseenter and mouseleave

View file

@ -4,9 +4,9 @@ Drag'n'Drop is a great interface solution. Taking something and dragging and dro
In the modern HTML standard there's a [section about Drag and Drop](https://html.spec.whatwg.org/multipage/interaction.html#dnd) with special events such as `dragstart`, `dragend`, and so on.
These events are useful in that they allow us to solve simple tasks easily. For instance, they allow us to handle the drag'n'drop of "external" files into the browser, so we can take a file in the OS file-manager and drop it into the browser window, thereby giving JavaScript access to its contents.
These events allow us to support special kinds of drag'n'drop, such as handling dragging a file from OS file-manager and dropping it into the browser window. Then JavaScript can access the contents of such files.
But native Drag Events also have limitations. For instance, we can't limit dragging by a certain area. Also we can't make it "horizontal" or "vertical" only. And there are other drag'n'drop tasks that can't be done using that API. Also, mobile device support for such events is almost non-existant.
But native Drag Events also have limitations. For instance, we can't prevent dragging from a certain area. Also we can't make the dragging "horizontal" or "vertical" only. And there are many other drag'n'drop tasks that can't be done using them. Also, mobile device support for such events is very weak.
So here we'll see how to implement Drag'n'Drop using mouse events.
@ -14,26 +14,23 @@ So here we'll see how to implement Drag'n'Drop using mouse events.
The basic Drag'n'Drop algorithm looks like this:
1. On `mousedown` - prepare the element for moving, if needed (maybe create a copy of it).
2. Then on `mousemove` move it by changing `left/top` and `position:absolute`.
3. On `mouseup` - perform all actions related to a finished Drag'n'Drop.
1. On `mousedown` - prepare the element for moving, if needed (maybe create a clone of it, add a class to it or whatever).
2. Then on `mousemove` move it by changing `left/top` with `position:absolute`.
3. On `mouseup` - perform all actions related to finishing the drag'n'drop.
These are the basics. Later we can extend it, for instance, by highlighting droppable (available for the drop) elements when hovering over them.
These are the basics. Later we'll see how to other features, such as highlighting current underlying elements while we drag over them.
Here's the algorithm for drag'n'drop of a ball:
Here's the implementation of dragging a ball:
```js
ball.onmousedown = function(event) { // (1) start the process
// (2) prepare to moving: make absolute and on top by z-index
ball.onmousedown = function(event) {
// (1) prepare to moving: make absolute and on top by z-index
ball.style.position = 'absolute';
ball.style.zIndex = 1000;
// move it out of any current parents directly into body
// to make it positioned relative to the body
document.body.append(ball);
// ...and put that absolutely positioned ball under the pointer
moveAt(event.pageX, event.pageY);
// centers the ball at (pageX, pageY) coordinates
function moveAt(pageX, pageY) {
@ -41,14 +38,17 @@ ball.onmousedown = function(event) { // (1) start the process
ball.style.top = pageY - ball.offsetHeight / 2 + 'px';
}
// move our absolutely positioned ball under the pointer
moveAt(event.pageX, event.pageY);
function onMouseMove(event) {
moveAt(event.pageX, event.pageY);
}
// (3) move the ball on mousemove
// (2) move the ball on mousemove
document.addEventListener('mousemove', onMouseMove);
// (4) drop the ball, remove unneeded handlers
// (3) drop the ball, remove unneeded handlers
ball.onmouseup = function() {
document.removeEventListener('mousemove', onMouseMove);
ball.onmouseup = null;
@ -64,10 +64,10 @@ Here's an example in action:
[iframe src="ball" height=230]
Try to drag'n'drop the mouse and you'll see such behavior.
Try to drag'n'drop with the mouse and you'll see such behavior.
```
That's because the browser has its own Drag'n'Drop for images and some other elements that runs automatically and conflicts with ours.
That's because the browser has its own drag'n'drop support for images and some other elements. It runs automatically and conflicts with ours.
To disable it:
@ -276,7 +276,7 @@ function onMouseMove(event) {
}
```
In the example below when the ball is dragged over the soccer gate, the gate is highlighted.
In the example below when the ball is dragged over the soccer goal, the goal is highlighted.
[codetabs height=250 src="ball4"]
@ -300,4 +300,4 @@ We can lay a lot on this foundation.
- We can use event delegation for `mousedown/up`. A large-area event handler that checks `event.target` can manage Drag'n'Drop for hundreds of elements.
- And so on.
There are frameworks that build architecture over it: `DragZone`, `Droppable`, `Draggable` and other classes. Most of them do the similar stuff to described above, so it should be easy to understand them now. Or roll our own, as you can see that's easy enough to do, sometimes easier than adapting a third-part solution.
There are frameworks that build architecture over it: `DragZone`, `Droppable`, `Draggable` and other classes. Most of them do the similar stuff to what's described above, so it should be easy to understand them now. Or roll your own, as you can see that that's easy enough to do, sometimes easier than adapting a third-party solution.

View file

@ -13,16 +13,13 @@
<script>
ball.onmousedown = function(event) { // (1) start the process
// (2) prepare to moving: make absolute and top by z-index
ball.onmousedown = function(event) {
ball.style.position = 'absolute';
ball.style.zIndex = 1000;
document.body.appendChild(ball);
// ...and put that absolutely positioned ball under the cursor
moveAt(event.pageX, event.pageY);
// centers the ball at (pageX, pageY) coordinates
function moveAt(pageX, pageY) {
ball.style.left = pageX - ball.offsetWidth / 2 + 'px';
ball.style.top = pageY - ball.offsetHeight / 2 + 'px';
@ -32,10 +29,8 @@
moveAt(event.pageX, event.pageY);
}
// (3) move the ball on mousemove
document.addEventListener('mousemove', onMouseMove);
// (4) drop the ball, remove unneeded handlers
ball.onmouseup = function() {
document.removeEventListener('mousemove', onMouseMove);
ball.onmouseup = null;

View file

@ -13,16 +13,13 @@
<script>
ball.onmousedown = function(event) { // (1) start the process
// (2) prepare to moving: make absolute and top by z-index
ball.onmousedown = function(event) {
ball.style.position = 'absolute';
ball.style.zIndex = 1000;
document.body.appendChild(ball);
// ...and put that absolutely positioned ball under the cursor
moveAt(event.pageX, event.pageY);
// centers the ball at (pageX, pageY) coordinates
function moveAt(pageX, pageY) {
ball.style.left = pageX - ball.offsetWidth / 2 + 'px';
ball.style.top = pageY - ball.offsetHeight / 2 + 'px';
@ -32,10 +29,8 @@
moveAt(event.pageX, event.pageY);
}
// (3) move the ball on mousemove
document.addEventListener('mousemove', onMouseMove);
// (4) drop the ball, remove unneeded handlers
ball.onmouseup = function() {
document.removeEventListener('mousemove', onMouseMove);
ball.onmouseup = null;

View file

@ -0,0 +1,248 @@
# Pointer events
Pointer events are a modern way to handle input from a variety of pointing devices, such as a mouse, a pen/stylus, a touchscreen, and so on.
## The brief history
Let's make a small overview, so that you understand the general picture and the place of Pointer Events among other event types.
- Long ago, in the past, there were only mouse events.
Then touch devices became widespread, phones and tablets in particular. For the existing scripts to work, they generated (and still generate) mouse events. For instance, tapping a touchscreen generates `mousedown`. So touch devices worked well with web pages.
But touch devices have more capabilities than a mouse. For example, it's possible to touch multiple points at once ("multi-touch"). Although, mouse events don't have necessary properties to handle such multi-touches.
- So touch events were introduced, such as `touchstart`, `touchend`, `touchmove`, that have touch-specific properties (we don't cover them in detail here, because pointer events are even better).
Still, it wasn't enough, as there are many other devices, such as pens, that have their own features. Also, writing code that listens for both touch and mouse events was cumbersome.
- To solve these issues, the new standard Pointer Events was introduced. It provides a single set of events for all kinds of pointing devices.
As of now, [Pointer Events Level 2](https://www.w3.org/TR/pointerevents2/) specification is supported in all major browsers, while the newer [Pointer Events Level 3](https://w3c.github.io/pointerevents/) is in the works and is mostly compartible with Pointer Events level 2.
Unless you develop for old browsers, such as Internet Explorer 10, or for Safari 12 or below, there's no point in using mouse or touch events any more -- we can switch to pointer events.
Then your code will work well with both touch and mouse devices.
That said, there are some important peculiarities that one should know in order to use Pointer Events correctly and avoid surprises. We'll make note of them in this article.
## Pointer event types
Pointer events are named similarly to mouse events:
| Pointer event | Similar mouse event |
|---------------|-------------|
| `pointerdown` | `mousedown` |
| `pointerup` | `mouseup` |
| `pointermove` | `mousemove` |
| `pointerover` | `mouseover` |
| `pointerout` | `mouseout` |
| `pointerenter` | `mouseenter` |
| `pointerleave` | `mouseleave` |
| `pointercancel` | - |
| `gotpointercapture` | - |
| `lostpointercapture` | - |
As we can see, for every `mouse<event>`, there's a `pointer<event>` that plays a similar role. Also there are 3 additional pointer events that don't have a corresponding `mouse...` counterpart, we'll explain them soon.
```smart header="Replacing `mouse<event>` with `pointer<event>` in our code"
We can replace `mouse<event>` events with `pointer<event>` in our code and expect things to continue working fine with mouse.
The support for touch devices will also "magically" improve. Although, we may need to add `touch-action: none` in some places in CSS. We'll cover it below in the section about `pointercancel`.
```
## Pointer event properties
Pointer events have the same properties as mouse events, such as `clientX/Y`, `target`, etc., plus some others:
- `pointerId` - the unique identifier of the pointer causing the event.
Browser-generated. Allows us to handle multiple pointers, such as a touchscreen with stylus and multi-touch (examples will follow).
- `pointerType` - the pointing device type. Must be a string, one of: "mouse", "pen" or "touch".
We can use this property to react differently on various pointer types.
- `isPrimary` - is `true` for the primary pointer (the first finger in multi-touch).
Some pointer devices measure contact area and pressure, e.g. for a finger on the touchscreen, there are additional properties for that:
- `width` - the width of the area where the pointer (e.g. a finger) touches the device. Where unsupported, e.g. for a mouse, it's always `1`.
- `height` - the height of the area where the pointer touches the device. Where unsupported, it's always `1`.
- `pressure` - the pressure of the pointer tip, in range from 0 to 1. For devices that don't support pressure must be either `0.5` (pressed) or `0`.
- `tangentialPressure` - the normalized tangential pressure.
- `tiltX`, `tiltY`, `twist` - pen-specific properties that describe how the pen is positioned relative the surface.
These properties aren't supported by most devices, so they are rarely used. You can find the details about them in the [specification](https://w3c.github.io/pointerevents/#pointerevent-interface) if needed.
## Multi-touch
One of the things that mouse events totally don't support is multi-touch: a user can touch in several places at once on their phone or tablet, or perform special gestures.
Pointer Events allow handling multi-touch with the help of the `pointerId` and `isPrimary` properties.
Here's what happens when a user touches a touchscreen in one place, then puts another finger somewhere else on it:
1. At the first finger touch:
- `pointerdown` with `isPrimary=true` and some `pointerId`.
2. For the second finger and more fingers (assuming the first one is still touching):
- `pointerdown` with `isPrimary=false` and a different `pointerId` for every finger.
Please note: the `pointerId` is assigned not to the whole device, but for each touching finger. If we use 5 fingers to simultaneously touch the screen, we have 5 `pointerdown` events, each with their respective coordinates and a different `pointerId`.
The events associated with the first finger always have `isPrimary=true`.
We can track multiple touching fingers using their `pointerId`. When the user moves and then removes a finger, we get `pointermove` and `pointerup` events with the same `pointerId` as we had in `pointerdown`.
```online
Here's the demo that logs `pointerdown` and `pointerup` events:
[iframe src="multitouch" edit height=200]
Please note: you must be using a touchscreen device, such as a phone or a tablet, to actually see the difference in `pointerId/isPrimary`. For single-touch devices, such as a mouse, there'll be always same `pointerId` with `isPrimary=true`, for all pointer events.
```
## Event: pointercancel
The `pointercancel` event fires when there's an ongoing pointer interaction, and then something happens that causes it to be aborted, so that no more pointer events are generated.
Such causes are:
- The pointer device hardware was physically disabled.
- The device orientation changed (tablet rotated).
- The browser decided to handle the interaction on its own, considering it a mouse gesture or zoom-and-pan action or something else.
We'll demonstrate `pointercancel` on a practical example to see how it affects us.
Let's say we're impelementing drag'n'drop for a ball, just as in the beginning of the article <info:mouse-drag-and-drop>.
Here is the flow of user actions and the corresponding events:
1) The user presses on an image, to start dragging
- `pointerdown` event fires
2) Then they start moving the pointer (thus dragging the image)
- `pointermove` fires, maybe several times
3) And then the surprise happens! The browser has native drag'n'drop support for images, that kicks in and takes over the drag'n'drop process, thus generating `pointercancel` event.
- The browser now handles drag'n'drop of the image on its own. The user may even drag the ball image out of the browser, into their Mail program or a File Manager.
- No more `pointermove` events for us.
So the issue is that the browser "hijacks" the interaction: `pointercancel` fires in the beginning of the "drag-and-drop" process, and no more `pointermove` events are generated.
```online
Here's the drag'n'drop demo with loggin of pointer events (only `up/down`, `move` and `cancel`) in the `textarea`:
[iframe src="ball" height=240 edit]
```
We'd like to implement the drag'n'drop on our own, so let's tell the browser not to take it over.
**Prevent the default browser action to avoid `pointercancel`.**
We need to do two things:
1. Prevent native drag'n'drop from happening:
- We can do this by setting `ball.ondragstart = () => false`, just as described in the article <info:mouse-drag-and-drop>.
- That works well for mouse events.
2. For touch devices, there are other touch-related browser actions (besides drag'n'drop). To avoid problems with them too:
- Prevent them by setting `#ball { touch-action: none }` in CSS.
- Then our code will start working on touch devices.
After we do that, the events will work as intended, the browser won't hijack the process and doesn't emit `pointercancel`.
```online
This demo adds these lines:
[iframe src="ball-2" height=240 edit]
As you can see, there's no `pointercancel` any more.
```
Now we can add the code to actually move the ball, and our drag'n'drop will work for mouse devices and touch devices.
## Pointer capturing
Pointer capturing is a special feature of pointer events.
The idea is very simple, but may seem quite odd at first, as nothing like that exists for any other event type.
The main method is:
- `elem.setPointerCapture(pointerId)` - binds events with the given `pointerId` to `elem`. After the call all pointer events with the same `pointerId` will have `elem` as the target (as if happened on `elem`), no matter where in document they really happened.
In other words, `elem.setPointerCapture(pointerId)` retargets all subsequent events with the given `pointerId` to `elem`.
The binding is removed:
- automatically when `pointerup` or `pointercancel` events occur,
- automatically when `elem` is removed from the document,
- when `elem.releasePointerCapture(pointerId)` is called.
**Pointer capturing can be used to simplify drag'n'drop kind of interactions.**
As an example, let's recall how one can implement a custom slider, described in the <info:mouse-drag-and-drop>.
We make a slider element with the strip and the "runner" (`thumb`) inside it.
Then it works like this:
1. The user presses on the slider `thumb` - `pointerdown` triggers.
2. Then they move the pointer - `pointermove` triggers, and we move the `thumb` along.
- ...As the pointer moves, it may leave the slider `thumb`: go above or below it. The `thumb` should move strictly horizontally, remaining aligned with the pointer.
So, to track all pointer movements, including when it goes above/below the `thumb`, we had to assign `pointermove` event handler on the whole `document`.
That solution looks a bit "dirty". One of the problems is that pointer movements around the document may cause side effects, trigger other event handlers, totally not related to the slider.
Pointer capturing provides a means to bind `pointermove` to `thumb` and avoid any such problems:
- We can call `thumb.setPointerCapture(event.pointerId)` in `pointerdown` handler,
- Then future pointer events until `pointerup/cancel` will be retargeted to `thumb`.
- When `pointerup` happens (dragging complete), the binding is removed automatically, we don't need to care about it.
So, even if the user moves the pointer around the whole document, events handlers will be called on `thumb`. Besides, coordinate properties of the event objects, such as `clientX/clientY` will still be correct - the capturing only affects `target/currentTarget`.
Here's the essential code:
```js
thumb.onpointerdown = function(event) {
// retarget all pointer events (until pointerup) to thumb
thumb.setPointerCapture(event.pointerId);
};
thumb.onpointermove = function(event) {
// moving the slider: listen on the thumb, as all pointer events are retargeted to it
let newLeft = event.clientX - slider.getBoundingClientRect().left;
thumb.style.left = newLeft + 'px';
};
// note: no need to call thumb.releasePointerCapture,
// it happens on pointerup automatically
```
```online
The full demo:
[iframe src="slider" height=100 edit]
```
At the end, pointer capturing gives us two benefits:
1. The code becomes cleaner as we don't need to add/remove handlers on the whole `document` any more. The binding is released automatically.
2. If there are any `pointermove` handlers in the document, they won't be accidentally triggered by the pointer while the user is dragging the slider.
### Pointer capturing events
There are two associated pointer events:
- `gotpointercapture` fires when an element uses `setPointerCapture` to enable capturing.
- `lostpointercapture` fires when the capture is released: either explicitly with `releasePointerCapture` call, or automatically on `pointerup`/`pointercancel`.
## Summary
Pointer events allow handling mouse, touch and pen events simultaneously, with a single piece of code.
Pointer events extend mouse events. We can replace `mouse` with `pointer` in event names and expect our code to continue working for mouse, with better support for other device types.
For drag'n'drops and complex touch interactions that the browser may decide to hijack and handle on its own - remember to cancel the default action on events and set `touch-events: none` in CSS for elements that we engage.
Additional abilities of pointer events are:
- Multi-touch support using `pointerId` and `isPrimary`.
- Device-specific properties, such as `pressure`, `width/height`, and others.
- Pointer capturing: we can retarget all pointer events to a specific element until `pointerup`/`pointercancel`.
As of now, pointer events are supported in all major browsers, so we can safely switch to them, especially if IE10- and Safari 12- are not needed. And even with those browsers, there are polyfills that enable the support of pointer events.

View file

@ -0,0 +1,38 @@
<!doctype html>
<body style="height: 200px">
<style>
#ball {
touch-action: none;
}
</style>
<p>Drag the ball.</p>
<img src="https://js.cx/clipart/ball.svg" style="cursor:pointer" width="40" height="40" id="ball">
<script>
ball.onpointerdown = log;
ball.onpointerup = log;
ball.onpointermove = log;
ball.onpointercancel = log;
ball.ondragstart = () => false;
let lastEventType;
let n = 1;
function log(event) {
if (lastEventType == event.type) {
n++;
text.value = text.value.replace(/.*\n$/, `${event.type} * ${n}\n`);
return;
}
lastEventType = event.type;
n = 1;
text.value += event.type + '\n';
text.scrollTop = 1e9;
}
</script>
<textarea id="text" style="display:block;width:300px;height:100px"></textarea>
</body>

View file

@ -0,0 +1,30 @@
<!doctype html>
<body style="height: 200px">
<p>Drag the ball.</p>
<img src="https://js.cx/clipart/ball.svg" style="cursor:pointer" width="40" height="40" id="ball">
<script>
ball.onpointerdown = log;
ball.onpointerup = log;
ball.onpointermove = log;
ball.onpointercancel = log;
let lastEventType;
let n = 1;
function log(event) {
if (lastEventType == event.type) {
n++;
text.value = text.value.replace(/.*\n$/, `${event.type} * ${n}\n`);
return;
}
lastEventType = event.type;
n = 1;
text.value += event.type + '\n';
text.scrollTop = 1e9;
}
</script>
<textarea id="text" style="display:block;width:300px;height:100px"></textarea>
</body>

View file

@ -0,0 +1,28 @@
<!doctype html>
<body>
<style>
#area {
border: 1px solid black;
width: 90%;
height: 180px;
cursor: pointer;
overflow: scroll;
user-select: none;
}
</style>
<div id="area">
Multi-touch here
</div>
<script>
document.onpointerdown = document.onpointerup = log;
function log(event) {
area.insertAdjacentHTML("beforeend", `
<div>${event.type} isPrimary=${event.isPrimary} pointerId=${event.pointerId}</div>
`)
area.scrollTop = 1e9;
}
</script>
</body>

View file

@ -0,0 +1,37 @@
<!DOCTYPE html>
<link rel="stylesheet" href="style.css">
<div id="slider" class="slider">
<div class="thumb"></div>
</div>
<script>
let thumb = slider.querySelector('.thumb');
let shiftX;
thumb.onpointerdown = function(event) {
event.preventDefault(); // prevent selection start (browser action)
shiftX = event.clientX - thumb.getBoundingClientRect().left;
thumb.setPointerCapture(event.pointerId);
};
thumb.onpointermove = function(event) {
let newLeft = event.clientX - shiftX - slider.getBoundingClientRect().left;
// if the pointer is out of slider => adjust left to be within the bounaries
if (newLeft < 0) {
newLeft = 0;
}
let rightEdge = slider.offsetWidth - thumb.offsetWidth;
if (newLeft > rightEdge) {
newLeft = rightEdge;
}
thumb.style.left = newLeft + 'px';
};
thumb.ondragstart = () => false;
</script>

View file

@ -0,0 +1,19 @@
.slider {
border-radius: 5px;
background: #E0E0E0;
background: linear-gradient(left top, #E0E0E0, #EEEEEE);
width: 310px;
height: 15px;
margin: 5px;
}
.thumb {
width: 10px;
height: 25px;
border-radius: 3px;
position: relative;
left: 10px;
top: -5px;
background: blue;
cursor: pointer;
}

View file

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Before After
Before After

View file

@ -55,11 +55,11 @@ function populate() {
// document bottom
let windowRelativeBottom = document.documentElement.getBoundingClientRect().bottom;
// if the user scrolled far enough (<100px to the end)
if (windowRelativeBottom < document.documentElement.clientHeight + 100) {
// let's add more data
document.body.insertAdjacentHTML("beforeend", `<p>Date: ${new Date()}</p>`);
}
// if the user hasn't scrolled far enough (>100px to the end)
if (windowRelativeBottom > document.documentElement.clientHeight + 100) break;
// let's add more data
document.body.insertAdjacentHTML("beforeend", `<p>Date: ${new Date()}</p>`);
}
}
```

View file

@ -1,6 +1,6 @@
# Scrolling
The `scroll` event allows to react on a page or element scrolling. There are quite a few good things we can do here.
The `scroll` event allows reacting to a page or element scrolling. There are quite a few good things we can do here.
For instance:
- Show/hide additional controls or information depending on where in the document the user is.
@ -34,4 +34,4 @@ If we add an event handler to these events and `event.preventDefault()` in it, t
There are many ways to initiate a scroll, so it's more reliable to use CSS, `overflow` property.
Here are few tasks that you can solve or look through to see the applications on `onscroll`.
Here are few tasks that you can solve or look through to see applications of `onscroll`.

View file

@ -125,8 +125,7 @@ That's usually not a problem, because we rarely change names of form elements.
## Backreference: element.form
For any element, the form is available as `element.form`. So a form references all elements, and elements
reference the form.
For any element, the form is available as `element.form`. So a form references all elements, and elements reference the form.
Here's the picture:

View file

@ -1,6 +1,6 @@
# Focusing: focus/blur
An element receives a focus when the user either clicks on it or uses the `key:Tab` key on the keyboard. There's also an `autofocus` HTML attribute that puts the focus into an element by default when a page loads and other means of getting a focus.
An element receives the focus when the user either clicks on it or uses the `key:Tab` key on the keyboard. There's also an `autofocus` HTML attribute that puts the focus onto an element by default when a page loads and other means of getting the focus.
Focusing on an element generally means: "prepare to accept the data here", so that's the moment when we can run the code to initialize the required functionality.
@ -18,7 +18,7 @@ Let's use them for validation of an input field.
In the example below:
- The `blur` handler checks if the field the email is entered, and if not -- shows an error.
- The `blur` handler checks if the field has an email entered, and if not -- shows an error.
- The `focus` handler hides the error message (on `blur` it will be checked again):
```html run autorun height=60
@ -108,7 +108,7 @@ By default many elements do not support focusing.
The list varies a bit between browsers, but one thing is always correct: `focus/blur` support is guaranteed for elements that a visitor can interact with: `<button>`, `<input>`, `<select>`, `<a>` and so on.
From the other hand, elements that exist to format something, such as `<div>`, `<span>`, `<table>` -- are unfocusable by default. The method `elem.focus()` doesn't work on them, and `focus/blur` events are never triggered.
On the other hand, elements that exist to format something, such as `<div>`, `<span>`, `<table>` -- are unfocusable by default. The method `elem.focus()` doesn't work on them, and `focus/blur` events are never triggered.
This can be changed using HTML-attribute `tabindex`.
@ -131,7 +131,7 @@ There are two special values:
For instance, here's a list. Click the first item and press `key:Tab`:
```html autorun no-beautify
Click the first item and press Tab. Keep track of the order. Please note that many subsequent Tabs can move the focus out of the iframe with the example.
Click the first item and press Tab. Keep track of the order. Please note that many subsequent Tabs can move the focus out of the iframe in the example.
<ul>
<li tabindex="1">One</li>
<li tabindex="0">Zero</li>
@ -216,7 +216,7 @@ So here's another working variant:
## Summary
Events `focus` and `blur` trigger on focusing/losing focus on the element.
Events `focus` and `blur` trigger on an element focusing/losing focus.
Their specials are:
- They do not bubble. Can use capturing state instead or `focusin/focusout`.

View file

@ -2,7 +2,7 @@
The lifecycle of an HTML page has three important events:
- `DOMContentLoaded` -- the browser fully loaded HTML, and the DOM tree is built, but external resources like pictures `<img>` and stylesheets may be not yet loaded.
- `DOMContentLoaded` -- the browser fully loaded HTML, and the DOM tree is built, but external resources like pictures `<img>` and stylesheets may not yet have loaded.
- `load` -- not only HTML is loaded, but also all the external resources: images, styles etc.
- `beforeunload/unload` -- the user is leaving the page.
@ -33,7 +33,7 @@ For instance:
function ready() {
alert('DOM is ready');
// image is not yet loaded (unless was cached), so the size is 0x0
// image is not yet loaded (unless it was cached), so the size is 0x0
alert(`Image size: ${img.offsetWidth}x${img.offsetHeight}`);
}
@ -85,7 +85,7 @@ External style sheets don't affect DOM, so `DOMContentLoaded` does not wait for
But there's a pitfall. If we have a script after the style, then that script must wait until the stylesheet loads:
```html
```html run
<link type="text/css" rel="stylesheet" href="style.css">
<script>
// the script doesn't not execute until the stylesheet is loaded
@ -108,13 +108,13 @@ So if `DOMContentLoaded` is postponed by long-loading scripts, then autofill als
## window.onload [#window-onload]
The `load` event on the `window` object triggers when the whole page is loaded including styles, images and other resources.
The `load` event on the `window` object triggers when the whole page is loaded including styles, images and other resources. This event is available via the `onload` property.
The example below correctly shows image sizes, because `window.onload` waits for all images:
```html run height=200 refresh
<script>
window.onload = function() {
window.onload = function() { // same as window.addEventListener('load', (event) => {
alert('Page loaded');
// image is loaded at this time
@ -157,7 +157,7 @@ When the `sendBeacon` request is finished, the browser probably has already left
There's also a `keepalive` flag for doing such "after-page-left" requests in [fetch](info:fetch) method for generic network requests. You can find more information in the chapter <info:fetch-api>.
If we want to cancel the transition to another page, we can't do it here. But we can use another event -- `onbeforeunload`.
If we want to cancel the transition to another page, we can't do it here. But we can use another event -- `onbeforeunload`.
## window.onbeforeunload [#window.onbeforeunload]
@ -173,7 +173,7 @@ window.onbeforeunload = function() {
};
```
For historical reasons, returning a non-empty string also counts as canceling the event. Some time ago browsers used show it as a message, but as the [modern specification](https://html.spec.whatwg.org/#unloading-documents) says, they shouldn't.
For historical reasons, returning a non-empty string also counts as canceling the event. Some time ago browsers used to show it as a message, but as the [modern specification](https://html.spec.whatwg.org/#unloading-documents) says, they shouldn't.
Here's an example:
@ -209,7 +209,7 @@ Like this:
function work() { /*...*/ }
if (document.readyState == 'loading') {
// loading yet, wait for the event
// still loading, wait for the event
document.addEventListener('DOMContentLoaded', work);
} else {
// DOM is ready!
@ -277,7 +277,7 @@ Page load events:
- Images and other resources may also still continue loading.
- The `load` event on `window` triggers when the page and all resources are loaded. We rarely use it, because there's usually no need to wait for so long.
- The `beforeunload` event on `window` triggers when the user wants to leave the page. If we cancel the event, browser asks whether the user really wants to leave (e.g we have unsaved changes).
- `The unload` event on `window` triggers when the user is finally leaving, in the handler we can only do simple things that do not involve delays or asking a user. Because of that limitation, it's rarely used. We can send out a network request with `navigator.sendBeacon`.
- The `unload` event on `window` triggers when the user is finally leaving, in the handler we can only do simple things that do not involve delays or asking a user. Because of that limitation, it's rarely used. We can send out a network request with `navigator.sendBeacon`.
- `document.readyState` is the current state of the document, changes can be tracked in the `readystatechange` event:
- `loading` -- the document is loading.
- `interactive` -- the document is parsed, happens at about the same time as `DOMContentLoaded`, but before it.

View file

@ -37,7 +37,7 @@ Luckily, there are two `<script>` attributes that solve the problem for us: `def
## defer
The `defer` attribute tells the browser that it should go on working with the page, and load the script "in background", then run the script when it loads.
The `defer` attribute tells the browser not to wait for the script. Instead, the browser will continue to process the HTML, build DOM. The script loads "in the background", and then runs when the DOM is fully built.
Here's the same example as above, but with `defer`:
@ -50,16 +50,18 @@ Here's the same example as above, but with `defer`:
<p>...content after script...</p>
```
- Scripts with `defer` never block the page.
- Scripts with `defer` always execute when the DOM is ready, but before `DOMContentLoaded` event.
In other words:
The following example demonstrates that:
- Scripts with `defer` never block the page.
- Scripts with `defer` always execute when the DOM is ready (but before `DOMContentLoaded` event).
The following example demonstrates the second part:
```html run height=100
<p>...content before scripts...</p>
<script>
document.addEventListener('DOMContentLoaded', () => alert("DOM ready after defer!")); // (2)
document.addEventListener('DOMContentLoaded', () => alert("DOM ready after defer!"));
</script>
<script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>
@ -68,40 +70,44 @@ The following example demonstrates that:
```
1. The page content shows up immediately.
2. `DOMContentLoaded` waits for the deferred script. It only triggers when the script `(2)` is downloaded and executed.
2. `DOMContentLoaded` event handler waits for the deferred script. It only triggers when the script is downloaded and executed.
Deferred scripts keep their relative order, just like regular scripts.
**Deferred scripts keep their relative order, just like regular scripts.**
So, if we have a long script first, and then a smaller one, then the latter one waits.
Let's say, we have two deferred scripts: the `long.js` and then `small.js`:
```html
<script defer src="https://javascript.info/article/script-async-defer/long.js"></script>
<script defer src="https://javascript.info/article/script-async-defer/small.js"></script>
```
```smart header="The small script downloads first, runs second"
Browsers scan the page for scripts and download them in parallel, to improve performance. So in the example above both scripts download in parallel. The `small.js` probably makes it first.
Browsers scan the page for scripts and download them in parallel, to improve performance. So in the example above both scripts download in parallel. The `small.js` probably finishes first.
But the specification requires scripts to execute in the document order, so it waits for `long.js` to execute.
```
...But the `defer` atribute, besides telling the browser "not to block", ensures that the relative order is kept. So even though `small.js` loads first, it still waits and runs after `long.js` executes.
That may be important for cases when we need to load a JavaScript library and then a script that depends on it.
```smart header="The `defer` attribute is only for external scripts"
The `defer` attribute is ignored if the `<script>` tag has no `src`.
```
## async
The `async` attribute is somewhat like `defer`. It also makes the script non-blocking. But it has important differences in the behavior.
The `async` attribute means that a script is completely independent:
- The page doesn't wait for async scripts, the contents are processed and displayed.
- The browser doesn't block on `async` scripts (like `defer`).
- Other scripts don't wait for `async` scripts, and `async` scripts don't wait for them.
- `DOMContentLoaded` and async scripts don't wait for each other:
- `DOMContentLoaded` may happen both before an async script (if an async script finishes loading after the page is complete)
- ...or after an async script (if an async script is short or was in HTTP-cache)
- Other scripts don't wait for `async` scripts, and `async` scripts don't wait for them.
In other words, `async` scripts load in the background and run when ready. The DOM and other scripts don't wait for them, and they don't wait for anything. A fully independent script that runs when loaded. As simple, at it can get, right?
So, if we have several `async` scripts, they may execute in any order. Whatever loads first -- runs first:
Here's an example similar to what we've seen with `defer`: two scripts `long.js` and `small.js`, but now with `async` instead of `defer`.
They don't wait for each other. Whatever loads first (probably `small.js`) -- runs first:
```html run height=100
<p>...content before scripts...</p>
@ -116,9 +122,9 @@ So, if we have several `async` scripts, they may execute in any order. Whatever
<p>...content after scripts...</p>
```
1. The page content shows up immediately: `async` doesn't block it.
2. `DOMContentLoaded` may happen both before and after `async`, no guarantees here.
3. Async scripts don't wait for each other. A smaller script `small.js` goes second, but probably loads before `long.js`, so runs first. That's called a "load-first" order.
- The page content shows up immediately: `async` doesn't block it.
- `DOMContentLoaded` may happen both before and after `async`, no guarantees here.
- A smaller script `small.js` goes second, but probably loads before `long.js`, so `small.js` runs first. Although, it might be that `long.js` loads first, if cached, then it runs first. In other words, async scripts run in the "load-first" order.
Async scripts are great when we integrate an independent third-party script into the page: counters, ads and so on, as they don't depend on our scripts, and our scripts shouldn't wait for them:
@ -127,10 +133,11 @@ Async scripts are great when we integrate an independent third-party script into
<script async src="https://google-analytics.com/analytics.js"></script>
```
## Dynamic scripts
There's one more important way of adding a script to the page.
We can also add a script dynamically using JavaScript:
We can create a script and append it to the document dynamically using JavaScript:
```js run
let script = document.createElement('script');
@ -146,20 +153,11 @@ That is:
- They don't wait for anything, nothing waits for them.
- The script that loads first -- runs first ("load-first" order).
This can be changed if we explicitly set `script.async=false`. Then scripts will be executed in the document order, just like `defer`.
```js run
let script = document.createElement('script');
script.src = "/article/script-async-defer/long.js";
*!*
script.async = false;
*/!*
document.body.append(script);
```
For example, here we add two scripts. Without `script.async=false` they would execute in load-first order (the `small.js` probably first). But with that flag the order is "as in the document":
In this example, `loadScript(src)` function adds a script and also sets `async` to `false`.
So `long.js` always runs first (as it's added first):
```js run
function loadScript(src) {
@ -174,6 +172,10 @@ loadScript("/article/script-async-defer/long.js");
loadScript("/article/script-async-defer/small.js");
```
Without `script.async=false`, scripts would execute in default, load-first order (the `small.js` probably first).
Again, as with the `defer`, the order matters if we'd like to load a library and then another script that depends on it.
## Summary
@ -186,12 +188,14 @@ But there are also essential differences between them:
| `async` | *Load-first order*. Their document order doesn't matter -- which loads first | Irrelevant. May load and execute while the document has not yet been fully downloaded. That happens if scripts are small or cached, and the document is long enough. |
| `defer` | *Document order* (as they go in the document). | Execute after the document is loaded and parsed (they wait if needed), right before `DOMContentLoaded`. |
In practice, `defer` is used for scripts that need the whole DOM and/or their relative execution order is important.
And `async` is used for independent scripts, like counters or ads. And their relative execution order does not matter.
```warn header="Page without scripts should be usable"
Please note that if you're using `defer`, then the page is visible *before* the script loads.
Please note: if you're using `defer` or `async`, then user will see the the page *before* the script loads.
So the user may read the page, but some graphical components are probably not ready yet.
In such case, some graphical components are probably not initialized yet.
There should be "loading" indications in the proper places, and disabled buttons should show as such, so the user can clearly see what's ready and what's not.
Don't forget to put "loading" indication and disable buttons that aren't functional yet. Let the user clearly see what he can do on the page, and what's still getting ready.
```
In practice, `defer` is used for scripts that need the whole DOM and/or their relative execution order is important. And `async` is used for independent scripts, like counters or ads. And their relative execution order does not matter.

View file

@ -107,7 +107,7 @@ That's for historical reasons.
There's a rule: scripts from one site can't access contents of the other site. So, e.g. a script at `https://facebook.com` can't read the user's mailbox at `https://gmail.com`.
Or, to be more precise, one origin (domain/port/protocol triplet) can't access the content from another one. So even if we have a subdomain, or just another port, these are different origins, no access to each other.
Or, to be more precise, one origin (domain/port/protocol triplet) can't access the content from another one. So even if we have a subdomain, or just another port, these are different origins with no access to each other.
This rule also affects resources from other domains.
@ -159,7 +159,7 @@ Details may vary depending on the browser, but the idea is the same: any informa
Why do we need error details?
There are many services (and we can build our own) that listen for global errors using `window.onerror`, save errors and provide an interface to access and analyze them. That's great, as we can see real errors, triggered by our users. But if a script comes from another origin, then there's no much information about errors in it, as we've just seen.
There are many services (and we can build our own) that listen for global errors using `window.onerror`, save errors and provide an interface to access and analyze them. That's great, as we can see real errors, triggered by our users. But if a script comes from another origin, then there's not much information about errors in it, as we've just seen.
Similar cross-origin policy (CORS) is enforced for other types of resources as well.
@ -169,7 +169,7 @@ There are three levels of cross-origin access:
1. **No `crossorigin` attribute** -- access prohibited.
2. **`crossorigin="anonymous"`** -- access allowed if the server responds with the header `Access-Control-Allow-Origin` with `*` or our origin. Browser does not send authorization information and cookies to remote server.
3. **`crossorigin="use-credentials"`** -- access allowed if the server sends back the header `Access-Control-Allow-Origin` with our origin and `Access-Control-Allow-Credentials: true`. Browser sends authorization information and cookies to remote server.
3. **`crossorigin="use-credentials"`** -- access allowed if the server sends back the header `Access-Control-Allow-Origin` with our origin and `Access-Control-Allow-Credentials: true`. Browser sends authorization information and cookies to remote server.
```smart
You can read more about cross-origin access in the chapter <info:fetch-crossorigin>. It describes the `fetch` method for network requests, but the policy is exactly the same.

View file

@ -1,7 +1,7 @@
# Mutation observer
`MutationObserver` is a built-in object that observes a DOM element and fires a callback in case of changes.
`MutationObserver` is a built-in object that observes a DOM element and fires a callback when it detects a change.
We'll first take a look at the syntax, and then explore a real-world use case, to see where such thing may be useful.
@ -128,16 +128,16 @@ Such snippet in an HTML markup looks like this:
...
```
Also we'll use a JavaScript highlighting library on our site, e.g. [Prism.js](https://prismjs.com/). A call to `Prism.highlightElem(pre)` examines the contents of such `pre` elements and adds into them special tags and styles for colored syntax highlighting, similar to what you see in examples here, at this page.
For better readability and at the same time, to beautify it, we'll be using a JavaScript syntax highlighting library on our site, like [Prism.js](https://prismjs.com/). To get syntax highlighting for above snippet in Prism, `Prism.highlightElem(pre)` is called, which examines the contents of such `pre` elements and adds special tags and styles for colored syntax highlighting into those elements, similar to what you see in examples here, on this page.
When exactly to run that highlighting method? We can do it on `DOMContentLoaded` event, or at the bottom of the page. At that moment we have our DOM ready, can search for elements `pre[class*="language"]` and call `Prism.highlightElem` on them:
When exactly should we run that highlighting method? Well, we can do it on `DOMContentLoaded` event, or put the script at the bottom of the page. The moment our DOM is ready, we can search for elements `pre[class*="language"]` and call `Prism.highlightElem` on them:
```js
// highlight all code snippets on the page
document.querySelectorAll('pre[class*="language"]').forEach(Prism.highlightElem);
```
Everything's simple so far, right? There are `<pre>` code snippets in HTML, we highlight them.
Everything's simple so far, right? We find code snippets in HTML and highlight them.
Now let's go on. Let's say we're going to dynamically fetch materials from a server. We'll study methods for that [later in the tutorial](info:fetch). For now it only matters that we fetch an HTML article from a webserver and display it on demand:
@ -162,13 +162,13 @@ snippets.forEach(Prism.highlightElem);
*/!*
```
...But imagine, we have many places in the code where we load contents: articles, quizzes, forum posts. Do we need to put the highlighting call everywhere? That's not very convenient, and also easy to forget.
...But, imagine if we have many places in the code where we load our content - articles, quizzes, forum posts, etc. Do we need to put the highlighting call everywhere, to highlight the code in content after loading? That's not very convenient.
And what if the content is loaded by a third-party module? E.g. we have a forum written by someone else, that loads contents dynamically, and we'd like to add syntax highlighting to it. No one likes to patch third-party scripts.
And what if the content is loaded by a third-party module? For example, we have a forum written by someone else, that loads content dynamically, and we'd like to add syntax highlighting to it. No one likes patching third-party scripts.
Luckily, there's another option.
We can use `MutationObserver` to automatically detect when code snippets are inserted in the page and highlight them.
We can use `MutationObserver` to automatically detect when code snippets are inserted into the page and highlight them.
So we'll handle the highlighting functionality in one place, relieving us from the need to integrate it.
@ -236,30 +236,37 @@ There's a method to stop observing the node:
- `observer.disconnect()` -- stops the observation.
When we stop the observing, it might be possible that some changes were not processed by the observer yet.
When we stop the observing, it might be possible that some changes were not yet processed by the observer. In such cases, we use
- `observer.takeRecords()` -- gets a list of unprocessed mutation records, those that happened, but the callback did not handle them.
- `observer.takeRecords()` -- gets a list of unprocessed mutation records - those that happened, but the callback has not handled them.
These methods can be used together, like this:
```js
// we'd like to stop tracking changes
observer.disconnect();
// handle unprocessed some mutations
// get a list of unprocessed mutations
// should be called before disconnecting,
// if you care about possibly unhandled recent mutations
let mutationRecords = observer.takeRecords();
// stop tracking changes
observer.disconnect();
...
```
```smart header="Records returned by `observer.takeRecords()` are removed from the processing queue"
The callback won't be called for records, returned by `observer.takeRecords()`.
```
```smart header="Garbage collection interaction"
Observers use weak references to nodes internally. That is: if a node is removed from DOM, and becomes unreachable, then it becomes garbage collected.
Observers use weak references to nodes internally. That is, if a node is removed from the DOM, and becomes unreachable, then it can be garbage collected.
The mere fact that a DOM node is observed doesn't prevent the garbage collection.
```
## Summary
`MutationObserver` can react on changes in DOM: attributes, added/removed elements, text content.
`MutationObserver` can react to changes in DOM - attributes, text content and adding/removing elements.
We can use it to track changes introduced by other parts of our code, as well as to integrate with third-party scripts.

View file

@ -8,7 +8,7 @@ libs:
In this chapter we'll cover selection in the document, as well as selection in form fields, such as `<input>`.
JavaScript can do get the existing selection, select/deselect both as a whole or partially, remove the selected part from the document, wrap it into a tag, and so on.
JavaScript can get the existing selection, select/deselect both as a whole or partially, remove the selected part from the document, wrap it into a tag, and so on.
You can get ready to use recipes at the end, in "Summary" section. But you'll get much more if you read the whole chapter. The underlying `Range` and `Selection` objects are easy to grasp, and then you'll need no recipes to make them do what you want.
@ -16,7 +16,7 @@ You can get ready to use recipes at the end, in "Summary" section. But you'll ge
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. If the parent node is an element element node, then the offset is a child number, for a text node it's the position in the text. Examples to follow.
Each point represented as a parent DOM node with the relative offset from its start. If the parent node is an element node, then the offset is a child number, for a text node it's the position in the text. Examples to follow.
Let's select something.
@ -95,8 +95,8 @@ Let's select `"Example: <i>italic</i>"`. That's two first children of `<p>` (cou
</script>
```
- `range.setStart(p, 0)` -- sets the start at the 0th child of `<p>` (that's a text node `"Example: "`).
- `range.setEnd(p, 2)` -- spans the range up to (but not including) 2nd child of `<p>` (that's a text node `" and "`, but as the end is not included, so the last selected node is `<i>`).
- `range.setStart(p, 0)` -- sets the start at the 0th child of `<p>` (that's the text node `"Example: "`).
- `range.setEnd(p, 2)` -- spans the range up to (but not including) 2nd child of `<p>` (that's the text node `" and "`, but as the end is not included, so the last selected node is `<i>`).
Here's a more flexible test stand where you try more variants:
@ -194,9 +194,9 @@ Others:
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)
- `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 `<i>abc`.
@ -259,7 +259,7 @@ Click buttons to run methods on the selection, "resetExample" to reset it.
</script>
```
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).
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](mdn:/api/Range).
## Selection
@ -318,8 +318,7 @@ There are events on to keep track of selection:
### Selection tracking demo
Here's a small demo that shows selection boundaries
dynamically as it changes:
Here's a small demo that shows selection boundaries dynamically as it changes:
```html run height=80
<p id="p">Select me: <i>italic</i> and <b>bold</b></p>
@ -387,7 +386,7 @@ Also, there are convenience methods to manipulate the selection range directly,
- `setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset)` - replace selection range with the given start `anchorNode/anchorOffset` and end `focusNode/focusOffset`. All content in-between them is selected.
- `selectAllChildren(node)` -- select all children of the `node`.
- `deleteFromDocument()` -- remove selected content from the document.
- `containsNode(node, allowPartialContainment = false)` -- checks whether the selection contains `node` (partically if the second argument is `true`)
- `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.
@ -551,7 +550,7 @@ If nothing is selected, or we use equal `start` and `end` in `setRangeText`, the
We can also insert something "at the cursor" using `setRangeText`.
Here's an button that inserts `"HELLO"` at the cursor position and puts the cursor immediately after it. If the selection is not empty, then it gets replaced (we can do detect in by comparing `selectionStart!=selectionEnd` and do something else instead):
Here's a button that inserts `"HELLO"` at the cursor position and puts the cursor immediately after it. If the selection is not empty, then it gets replaced (we can detect it by comparing `selectionStart!=selectionEnd` and do something else instead):
```html run autorun
<input id="input" style="width:200px" value="Text Text Text Text Text">
@ -583,7 +582,7 @@ To make something unselectable, there are three ways:
This doesn't allow the selection to start at `elem`. But the user may start the selection elsewhere and include `elem` into it.
Then `elem` will become a part of `document.getSelection()`, so the selection actully happens, but its content is usually ignored in copy-paste.
Then `elem` will become a part of `document.getSelection()`, so the selection actually happens, but its content is usually ignored in copy-paste.
2. Prevent default action in `onselectstart` or `mousedown` events.
@ -621,7 +620,7 @@ The second API is very simple, as it works with text.
The most used recipes are probably:
1. Getting the selection:
```js run
```js
let selection = document.getSelection();
let cloned = /* element to clone the selected nodes to */;
@ -632,8 +631,8 @@ The most used recipes are probably:
cloned.append(selection.getRangeAt(i).cloneContents());
}
```
2. Setting the selection
```js run
2. Setting the selection:
```js
let selection = document.getSelection();
// directly:

View file

@ -9,7 +9,7 @@ In this chapter we first cover theoretical details about how things work, and th
## Event Loop
The concept of *event loop* is very simple. There's an endless loop, when JavaScript engine waits for tasks, executes them and then sleeps waiting for more tasks.
The *event loop* concept is very simple. There's an endless loop, where the JavaScript engine waits for tasks, executes them and then sleeps, waiting for more tasks.
The general algorithm of the engine:
@ -17,7 +17,7 @@ The general algorithm of the engine:
- execute them, starting with the oldest task.
2. Sleep until a task appears, then go to 1.
That's a formalization for what we see when browsing a page. JavaScript engine does nothing most of the time, only runs if a script/handler/event activates.
That's a formalization for what we see when browsing a page. The JavaScript engine does nothing most of the time, it only runs if a script/handler/event activates.
Examples of tasks:
@ -41,22 +41,22 @@ Tasks from the queue are processed on "first come first served" basis. When
So far, quite simple, right?
Two more details:
1. Rendering never happens while the engine executes a task. Doesn't matter if the task takes a long time. Changes to DOM are painted only after the task is complete.
2. If a task takes too long, the browser can't do other tasks, process user events, so after a time it raises an alert like "Page Unresponsive" suggesting to kill the task with the whole page. That happens when there are a lot of complex calculations or a programming error leading to infinite loop.
1. Rendering never happens while the engine executes a task. It doesn't matter if the task takes a long time. Changes to the DOM are painted only after the task is complete.
2. If a task takes too long, the browser can't do other tasks, such as processing user events. So after a time, it raises an alert like "Page Unresponsive", suggesting killing the task with the whole page. That happens when there are a lot of complex calculations or a programming error leading to an infinite loop.
That was a theory. Now let's see how we can apply that knowledge.
That was the theory. Now let's see how we can apply that knowledge.
## Use-case 1: splitting CPU-hungry tasks
Let's say we have a CPU-hungry task.
For example, syntax-highlighting (used to colorize code examples on this page) is quite CPU-heavy. To highlight the code, it performs the analysis, creates many colored elements, adds them to the document -- for a big text that takes a lot of time.
For example, syntax-highlighting (used to colorize code examples on this page) is quite CPU-heavy. To highlight the code, it performs the analysis, creates many colored elements, adds them to the document -- for a large amount of text that takes a lot of time.
While the engine is busy with syntax highlighting, it can't do other DOM-related stuff, process user events, etc. It may even cause the browser to "hiccup" or even "hang" for a bit, which is unacceptable.
We can evade problems by splitting the big task into pieces. Highlight first 100 lines, then schedule `setTimeout` (with zero-delay) another 100 lines, and so on.
We can avoid problems by splitting the big task into pieces. Highlight first 100 lines, then schedule `setTimeout` (with zero-delay) for the next 100 lines, and so on.
To demonstrate the approach, for the sake of simplicity, instead of syntax-highlighting let's take a function that counts from `1` to `1000000000`.
To demonstrate this approach, for the sake of simplicity, instead of text-highlighting, let's take a function that counts from `1` to `1000000000`.
If you run the code below, the engine will "hang" for some time. For server-side JS that's clearly noticeable, and if you are running it in-browser, then try to click other buttons on the page -- you'll see that no other events get handled until the counting finishes.
@ -78,9 +78,9 @@ function count() {
count();
```
The browser may even show "the script takes too long" warning.
The browser may even show a "the script takes too long" warning.
Let's split the job using nested `setTimeout`:
Let's split the job using nested `setTimeout` calls:
```js run
let i = 0;
@ -113,13 +113,13 @@ A single run of `count` does a part of the job `(*)`, and then re-schedules itse
2. Second run counts: `i=1000001..2000000`.
3. ...and so on.
Now, if a new side task (e.g. `onclick` event) appears while the engine is busy executing part 1, it gets queued and then executes when part 1 finished, before the next part. Periodic returns to event loop between `count` executions provide just enough "air" for the JavaScript engine to do something else, to react on other user actions.
Now, if a new side task (e.g. `onclick` event) appears while the engine is busy executing part 1, it gets queued and then executes when part 1 finished, before the next part. Periodic returns to the event loop between `count` executions provide just enough "air" for the JavaScript engine to do something else, to react to other user actions.
The notable thing is that both variants -- with and without splitting the job by `setTimeout` -- are comparable in speed. There's no much difference in the overall counting time.
The notable thing is that both variants -- with and without splitting the job by `setTimeout` -- are comparable in speed. There's not much difference in the overall counting time.
To make them closer, let's make an improvement.
We'll move the scheduling in the beginning of the `count()`:
We'll move the scheduling to the beginning of the `count()`:
```js run
let i = 0;
@ -128,7 +128,7 @@ let start = Date.now();
function count() {
// move the scheduling at the beginning
// move the scheduling to the beginning
if (i < 1e9 - 1e6) {
setTimeout(count); // schedule the new call
}
@ -160,9 +160,9 @@ Finally, we've split a CPU-hungry task into parts - now it doesn't block the use
Another benefit of splitting heavy tasks for browser scripts is that we can show progress indication.
Usually the browser renders after the currently running code is complete. Doesn't matter if the task takes a long time. Changes to DOM are painted only after the task is finished.
As mentioned earlier, changes to DOM are painted only after the currently running task is completed, irrespective of how long it takes.
From one hand, that's great, because our function may create many elements, add them one-by-one to the document and change their styles -- the visitor won't see any "intermediate", unfinished state. An important thing, right?
On one hand, that's great, because our function may create many elements, add them one-by-one to the document and change their styles -- the visitor won't see any "intermediate", unfinished state. An important thing, right?
Here's the demo, the changes to `i` won't show up until the function finishes, so we'll see only the last value:
@ -238,7 +238,7 @@ menu.onclick = function() {
## Macrotasks and Microtasks
Along with *macrotasks*, described in this chapter, there exist *microtasks*, mentioned in the chapter <info:microtask-queue>.
Along with *macrotasks*, described in this chapter, there are *microtasks*, mentioned in the chapter <info:microtask-queue>.
Microtasks come solely from our code. They are usually created by promises: an execution of `.then/catch/finally` handler becomes a microtask. Microtasks are used "under the cover" of `await` as well, as it's another form of promise handling.
@ -263,11 +263,11 @@ What's going to be the order here?
2. `promise` shows second, because `.then` passes through the microtask queue, and runs after the current code.
3. `timeout` shows last, because it's a macrotask.
The richer event loop picture looks like this:
The richer event loop picture looks like this (order is from top to bottom, that is: the script first, then microtasks, rendering and so on):
![](eventLoop-full.svg)
**All microtasks are completed before any other event handling or rendering or any other macrotask takes place.**
All microtasks are completed before any other event handling or rendering or any other macrotask takes place.
That's important, as it guarantees that the application environment is basically the same (no mouse coordinate changes, no new network data, etc) between microtasks.
@ -303,7 +303,7 @@ Here's an example with "counting progress bar", similar to the one shown previou
## Summary
The more detailed algorithm of the event loop (though still simplified compare to the [specification](https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model)):
A more detailed event loop algorithm (though still simplified compared to the [specification](https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model)):
1. Dequeue and run the oldest task from the *macrotask* queue (e.g. "script").
2. Execute all *microtasks*:
@ -316,7 +316,7 @@ The more detailed algorithm of the event loop (though still simplified compare t
To schedule a new *macrotask*:
- Use zero delayed `setTimeout(f)`.
That may be used to split a big calculation-heavy task into pieces, for the browser to be able to react on user events and show progress between them.
That may be used to split a big calculation-heavy task into pieces, for the browser to be able to react to user events and show progress between them.
Also, used in event handlers to schedule an action after the event is fully handled (bubbling done).
@ -335,5 +335,5 @@ That's a way to run code in another, parallel thread.
Web Workers can exchange messages with the main process, but they have their own variables, and their own event loop.
Web Workers do not have access to DOM, so they are useful, mainly, for calculations, to use multiplle CPU cores simultaneously.
Web Workers do not have access to DOM, so they are useful, mainly, for calculations, to use multiple CPU cores simultaneously.
```

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Before After
Before After