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

-...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() {