diff --git a/2-ui/1-document/09-size-and-scroll/2-scrollbar-width/task.md b/2-ui/1-document/09-size-and-scroll/2-scrollbar-width/task.md index 67787894..b39004cb 100644 --- a/2-ui/1-document/09-size-and-scroll/2-scrollbar-width/task.md +++ b/2-ui/1-document/09-size-and-scroll/2-scrollbar-width/task.md @@ -6,6 +6,6 @@ importance: 3 Write the code that returns the width of a standard scrollbar. -For Windows it usually varies between `12px` and `20px`. If the browser doesn't reserves any space for it, then it may be `0px`. +For Windows it usually varies between `12px` and `20px`. If the browser doesn't reserve any space for it (the scrollbar is half-translucent over the text, also happens), then it may be `0px`. P.S. The code should work for any HTML document, do not depend on its content. diff --git a/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/solution.md b/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/solution.md index 69dec23c..eaf7a02a 100644 --- a/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/solution.md +++ b/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/solution.md @@ -34,9 +34,7 @@ The code won't work reliably while `` has no width/height: 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. -In real life 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. +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. We should fix that by adding `width/height` to ``: diff --git a/2-ui/1-document/09-size-and-scroll/article.md b/2-ui/1-document/09-size-and-scroll/article.md index 774857f3..84151069 100644 --- a/2-ui/1-document/09-size-and-scroll/article.md +++ b/2-ui/1-document/09-size-and-scroll/article.md @@ -38,18 +38,18 @@ The picture above demonstrates the most complex case when the element has a scro So, without scrollbar the content width would be `300px`, but if the scrollbar is `16px` wide (the width may vary between devices and browsers) then only `300 - 16 = 284px` remains, and we should take it into account. That's why examples from this chapter assume that there's a scrollbar. If there's no scrollbar, then things are just a bit simpler. ``` -```smart header="The `padding-bottom` may be filled with text" -Usually paddings are shown empty on illustrations, but if there's a lot of text in the element and it overflows, then browsers show the "overflowing" text at `padding-bottom`, so you can see that in examples. But the padding is still there, unless specified otherwise. +```smart header="The `padding-bottom` area may be filled with text" +Usually paddings are shown empty on illustrations, but if there's a lot of text in the element and it overflows, then browsers show the "overflowing" text at `padding-bottom`, so you can see that in examples. Still, the padding is set in further examples, unless explicitly specified otherwise. ``` ## Geometry -Element properties that provide width, height and other geometry are always numbers. They are assumed to be in pixels. - Here's the overall picture: ![](metric-all.png) +Values of these properties are technically numbers, but these numbers are "of pixels", so these are pixel measurements. + They are many properties, it's difficult to fit them all in the single picture, but their values are simple and easy to understand. Let's start exploring them from the outside of the element. @@ -58,13 +58,15 @@ Let's start exploring them from the outside of the element. These properties are rarely needed, but still they are the "most outer" geometry properties, so we'll start with them. -The `offsetParent` is the nearest ancestor that is: +The `offsetParent` is the nearest ancestor, that browser uses for calculating coordinates during rendering. + +That's the nearest ancestor, that satisfies following conditions: 1. CSS-positioned (`position` is `absolute`, `relative`, `fixed` or `sticky`), 2. or ``, ``, ``, 2. or ``. -In most practical cases we can use `offsetParent` to get the nearest CSS-positioned ancestor. And `offsetLeft/offsetTop` provide x/y coordinates relative to its upper-left corner. +In most practical cases `offsetParent` is exactly the nearest ancestor, that is CSS-positioned. And `offsetLeft/offsetTop` provide x/y coordinates relative to its upper-left corner. In the example below the inner `
` has `
` as `offsetParent` and `offsetLeft/offsetTop` shifts from its upper-left corner (`180`): @@ -103,12 +105,12 @@ For our sample element: - `offsetWidth = 390` -- the outer width, can be calculated as inner CSS-width (`300px`) plus paddings (`2 * 20px`) and borders (`2 * 25px`). - `offsetHeight = 290` -- the outer height. -````smart header="Geometry properties for not shown elements are zero/null" -Geometry properties are calculated only for shown elements. +````smart header="Geometry properties for not displayed elements are zero/null" +Geometry properties are calculated only for displayed elements. -If an element (or any of its ancestors) has `display:none` or is not in the document, then all geometry properties are zero or `null` depending on what it is. +If an element (or any of its ancestors) has `display:none` or is not in the document, then all geometry properties are zero (or `null` if that's `offsetParent`). -For example, `offsetParent` is `null`, and `offsetWidth`, `offsetHeight` are `0`. +For example, `offsetParent` is `null`, and `offsetWidth`, `offsetHeight` are `0` when we created an element, but haven't inserted it into the document yet, or it (or it's ancestor) has `display:none`. We can use this to check if an element is hidden, like this: @@ -134,7 +136,7 @@ In our example: ![](metric-client-left-top.png) -...But to be precise -- they are not borders, but relative coordinates of the inner side from the outer side. +...But to be precise -- these propeerties are not border width/height, but rather relative coordinates of the inner side from the outer side. What's the difference? @@ -215,11 +217,11 @@ Setting `scrollTop` to `0` or `Infinity` will make the element scroll to the ver ## Don't take width/height from CSS -We've just covered geometry properties of DOM elements. They are normally used to get widths, heights and calculate distances. +We've just covered geometry properties of DOM elements, that can be used to get widths, heights and calculate distances. But as we know from the chapter , we can read CSS-height and width using `getComputedStyle`. -So why not to read the width of an element like this? +So why not to read the width of an element with `getComputedStyle`, like this? ```js run let elem = document.body; @@ -269,7 +271,7 @@ Elements have the following geometry properties: - `offsetWidth/offsetHeight` -- "outer" width/height of an element including borders. - `clientLeft/clientTop` -- the distance from the upper-left outer corner to its upper-left inner corner. For left-to-right OS they are always the widths of left/top borders. For right-to-left OS the vertical scrollbar is on the left so `clientLeft` includes its width too. - `clientWidth/clientHeight` -- the width/height of the content including paddings, but without the scrollbar. -- `scrollWidth/scrollHeight` -- the width/height of the content including the scrolled out parts. Also includes paddings, but not the scrollbar. -- `scrollLeft/scrollTop` -- width/height of the scrolled out part of the element, starting from its upper-left corner. +- `scrollWidth/scrollHeight` -- the width/height of the content, just like `clientWidth/clientHeight`, but also include scrolled-out, invisible part of the element. +- `scrollLeft/scrollTop` -- width/height of the scrolled out upper part of the element, starting from its upper-left corner. All properties are read-only except `scrollLeft/scrollTop`. They make the browser scroll the element if changed. diff --git a/6-data-storage/03-indexeddb/article.md b/6-data-storage/03-indexeddb/article.md index 4194bd15..69f0f84b 100644 --- a/6-data-storage/03-indexeddb/article.md +++ b/6-data-storage/03-indexeddb/article.md @@ -35,18 +35,18 @@ We can have many databases with different names, but all of them exist within th After the call, we need to listen to events on `openRequest` object: - `success`: database is ready, there's the "database object" in `openRequest.result`, that we should use it for further calls. -- `error`: open failed. -- `upgradeneeded`: database version is outdated (see below). +- `error`: opening failed. +- `upgradeneeded`: database is ready, but its version is outdated (see below). **IndexedDB has a built-in mechanism of "schema versioning", absent in server-side databases.** -Unlike server-side databases, IndexedDB is client-side, in the browser, so we don't have the data at hands. But when we publish a new version of our app, we may need to update the database. +Unlike server-side databases, IndexedDB is client-side, the data is stored in the browser, so we, developers, don't have direct access to it. But when we publish a new version of our app, we may need to update the database. If the local database version is less than specified in `open`, then a special event `upgradeneeded` is triggered, and we can compare versions and upgrade data structures as needed. The event also triggers when the database did not exist yet, so we can perform initialization. -For instance, when we first publish our app, we open it with version `1` and perform the initialization in `upgradeneeded` handler: +When we first publish our app, we open it with version `1` and perform the initialization in `upgradeneeded` handler: ```js let openRequest = indexedDB.open("store", *!*1*/!*); @@ -71,10 +71,10 @@ When we publish the 2nd version: ```js let openRequest = indexedDB.open("store", *!*2*/!*); -// check the existing database version, do the updates if needed: openRequest.onupgradeneeded = function() { + // the existing database version is less than 2 (or it doesn't exist) let db = openRequest.result; - switch(db.version) { // existing (old) db version + switch(db.version) { // existing db version case 0: // version 0 means that the client had no database // perform initialization @@ -85,6 +85,8 @@ openRequest.onupgradeneeded = function() { }; ``` +So, in `openRequest.onupgradeneeded` we update the database. Soon we'll see how it's done. And then, only if its handler finishes without errors, `openRequest.onsuccess` triggers. + After `openRequest.onsuccess` we have the database object in `openRequest.result`, that we'll use for further operations. To delete a database: @@ -94,9 +96,67 @@ let deleteRequest = indexedDB.deleteDatabase(name) // deleteRequest.onsuccess/onerror tracks the result ``` +### Opening an old version + +Now what if we try to open a database with a lower version than the current one? +E.g. the existing DB version is 3, and we try to `open(...2)`. That's simple: `openRequest.onerror` triggers. + +Such thing may happen if the visitor loaded an outdated code, e.g. from a proxy cache. We should check `db.version`, suggest him to reload the page, and also make sure that our caching policy is correct. + +### Multi-page update problem + +As we're talking about versioning, let's tackle a small related problem. + +Let's say, a visitor opened our site in a browser tab, with database version 1. + +Then we rolled out an update, and the same visitor opens our site in another tab. So there are two tabs, both with our site, but one has an open connection with DB version 1, while the other one attempts to update it in `upgradeneeded` handler. + +The problem is that a database is shared between two tabs, as that's the same site, same origin. And it can't be both version 1 and 2. To perform the update to version 2, all connections to version 1 must be closed. + +In order to organize that, there's `versionchange` event on an open database object. We should listen to it, as it lets us know that the version is about to change, so that we should close the database (and probably suggest the visitor to reload the page, to load the updated code). + +If we don't close it, then the second connection will be blocked with `blocked` event instead of `success`. + +Here's the code to work around it, it has two minor additions: + +```js +let openRequest = indexedDB.open("store", 2); + +openRequest.onupgradeneeded = ...; +openRequest.onerror = ...; + +openRequest.onsuccess = function() { + let db = openRequest.result; + + *!* + db.onversionchange = function() { + db.close(); + alert("Your database is outdated, please reload the page.") + }; + */!* + + // ...the db is ready, use it... +}; + +*!* +openRequest.onblocked = function() { + // there's another open connection to same database + // and it wasn't closed by db.onversionchange listener +}; +*/!* +``` + +We do two things: + +1. Add `db.onversionchange` listener after a successful opening, to close the old database. +2. Add `openRequest.onblocked` listener to handle the case when an old connection wasn't closed. Normally, this doesn't happen if we close it in `db.onversionchange`. + +Alternatively, we can just do nothing in `db.onversionchange` and let the new connection be blocked with a proper message. That's up to us really. ## Object store +To store stomething in IndexedDB, we need an *object store*. + An object store is a core concept of IndexedDB. Counterparts in other databases are called "tables" or "collections". It's where the data is stored. A database may have multiple stores: one for users, another one for goods, etc. Despite being named an "object store", primitives can be stored too. @@ -146,12 +206,12 @@ To perform database version upgrade, there are two main approaches: 1. We can implement per-version upgrade functions: from 1 to 2, from 2 to 3, from 3 to 4 etc. Then, in `upgradeneeded` we can compare versions (e.g. old 2, now 4) and run per-version upgrades step by step, for every intermediate version (2 to 3, then 3 to 4). 2. Or we can just examine the database: get a list of existing object stores as `db.objectStoreNames`. That object is a [DOMStringList](https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#domstringlist) that provides `contains(name)` method to check for existance. And then we can do updates depending on what exists and what doesn't. -For small databases the second path may be simpler. +For small databases the second variant may be simpler. Here's the demo of the second approach: ```js -let openRequest = indexedDB.open("db", 1); +let openRequest = indexedDB.open("db", 2); // create/upgrade the database without version checks openRequest.onupgradeneeded = function() {