diff --git a/1-js/02-first-steps/12-while-for/article.md b/1-js/02-first-steps/12-while-for/article.md index 992c21af6..6be50597d 100644 --- a/1-js/02-first-steps/12-while-for/article.md +++ b/1-js/02-first-steps/12-while-for/article.md @@ -308,8 +308,7 @@ for (let i = 0; i < 3; i++) { let input = prompt(`Value at coords (${i},${j})`, ''); - // what if I want to exit from here to Done (below)? - + // what if we want to exit from here to Done (below)? } } diff --git a/1-js/04-object-basics/01-object/article.md b/1-js/04-object-basics/01-object/article.md index f59ec0292..00706750c 100644 --- a/1-js/04-object-basics/01-object/article.md +++ b/1-js/04-object-basics/01-object/article.md @@ -146,6 +146,20 @@ let key = prompt("What do you want to know about the user?", "name"); alert( user[key] ); // John (if enter "name") ``` +<<<<<<< HEAD +======= +The dot notation cannot be used in a similar way: + +```js run +let user = { + name: "John", + age: 30 +}; + +let key = "name"; +alert( user.key ) // undefined +``` +>>>>>>> 852ee189170d9022f67ab6d387aeae76810b5923 ### Computed properties diff --git a/1-js/04-object-basics/03-symbol/article.md b/1-js/04-object-basics/03-symbol/article.md index 8323d6643..55599cccb 100644 --- a/1-js/04-object-basics/03-symbol/article.md +++ b/1-js/04-object-basics/03-symbol/article.md @@ -3,11 +3,11 @@ By specification, object property keys may be either of string type, or of symbol type. Not numbers, not booleans, only strings or symbols, these two types. -Till now we've only seen strings. Now let's see the advantages that symbols can give us. +Till now we've been using only strings. Now let's see the benefits that symbols can give us. ## Symbols -"Symbol" value represents a unique identifier. +A "symbol" represents a unique identifier. A value of this type can be created using `Symbol()`: @@ -52,7 +52,7 @@ alert(id); // TypeError: Cannot convert a Symbol value to a string That's a "language guard" against messing up, because strings and symbols are fundamentally different and should not occasionally convert one into another. -If we really want to show a symbol, we need to call `.toString()` on it, like here: +If we really want to show a symbol, we need to explicitly call `.toString()` on it, like here: ```js run let id = Symbol("id"); *!* @@ -60,7 +60,7 @@ alert(id.toString()); // Symbol(id), now it works */!* ``` -Or get `symbol.description` property to get the description only: +Or get `symbol.description` property to show the description only: ```js run let id = Symbol("id"); *!* @@ -74,13 +74,23 @@ alert(id.description); // id Symbols allow us to create "hidden" properties of an object, that no other part of code can occasionally access or overwrite. +<<<<<<< HEAD For instance, if we want to store an "identifier" for the object `user`, we can use a symbol as a key for it: +======= +For instance, if we're working with `user` objects, that belong to a third-party code. We'd like to add identifiers to them. + +Let's use a symbol key for it: +>>>>>>> 852ee189170d9022f67ab6d387aeae76810b5923 ```js run -let user = { name: "John" }; +let user = { // belongs to another code + name: "John" +}; + let id = Symbol("id"); -user[id] = "ID Value"; +user[id] = 1; + alert( user[id] ); // we can access the data using the symbol as the key ``` @@ -106,13 +116,13 @@ Now note that if we used a string `"id"` instead of a symbol for the same purpos ```js run let user = { name: "John" }; -// our script uses "id" property -user.id = "ID Value"; +// Our script uses "id" property +user.id = "Our id value"; -// ...if later another script the uses "id" for its purposes... +// ...Another script also wants "id" for its purposes... user.id = "Their id value" -// boom! overwritten! it did not mean to harm the colleague, but did it! +// Boom! overwritten by another script! ``` ### Symbols in a literal @@ -127,7 +137,7 @@ let id = Symbol("id"); let user = { name: "John", *!* - [id]: 123 // not just "id: 123" + [id]: 123 // not "id: 123" */!* }; ``` diff --git a/1-js/05-data-types/03-string/article.md b/1-js/05-data-types/03-string/article.md index e748d65f0..b7e619188 100644 --- a/1-js/05-data-types/03-string/article.md +++ b/1-js/05-data-types/03-string/article.md @@ -355,8 +355,8 @@ alert( "Hello".includes("Bye") ); // false The optional second argument of `str.includes` is the position to start searching from: ```js run -alert( "Midget".includes("id") ); // true -alert( "Midget".includes("id", 3) ); // false, from position 3 there is no "id" +alert( "Widget".includes("id") ); // true +alert( "Widget".includes("id", 3) ); // false, from position 3 there is no "id" ``` The methods [str.startsWith](mdn:js/String/startsWith) and [str.endsWith](mdn:js/String/endsWith) do exactly what they say: diff --git a/1-js/05-data-types/05-array-methods/9-shuffle/solution.md b/1-js/05-data-types/05-array-methods/9-shuffle/solution.md index a43715db8..31f7a2948 100644 --- a/1-js/05-data-types/05-array-methods/9-shuffle/solution.md +++ b/1-js/05-data-types/05-array-methods/9-shuffle/solution.md @@ -68,7 +68,13 @@ There are other good ways to do the task. For instance, there's a great algorith function shuffle(array) { for (let i = array.length - 1; i > 0; i--) { let j = Math.floor(Math.random() * (i + 1)); // random index from 0 to i - [array[i], array[j]] = [array[j], array[i]]; // swap elements + + // swap elements array[i] and array[j] + // we use "destructuring assignment" syntax to achieve that + // you'll find more details about that syntax in later chapters + // same can be written as: + // let t = array[i]; array[i] = array[j]; array[j] = t + [array[i], array[j]] = [array[j], array[i]]; } } ``` diff --git a/1-js/09-classes/07-mixins/article.md b/1-js/09-classes/07-mixins/article.md index 7b6d9ebad..2502c3982 100644 --- a/1-js/09-classes/07-mixins/article.md +++ b/1-js/09-classes/07-mixins/article.md @@ -2,13 +2,17 @@ In JavaScript we can only inherit from a single object. There can be only one `[[Prototype]]` for an object. And a class may extend only one other class. +<<<<<<< HEAD But sometimes that feels limiting. For instance, I have a class `StreetSweeper` and a class `Bicycle`, and want to make a `StreetSweepingBicycle`. +======= +But sometimes that feels limiting. For instance, we have a class `StreetSweeper` and a class `Bicycle`, and want to make their mix: a `StreetSweepingBicycle`. +>>>>>>> 852ee189170d9022f67ab6d387aeae76810b5923 Or, talking about programming, we have a class `Renderer` that implements templating and a class `EventEmitter` that implements event handling, and want to merge these functionalities together with a class `Page`, to make a page that can use templates and emit events. There's a concept that can help here, called "mixins". -As defined in Wikipedia, a [mixin](https://en.wikipedia.org/wiki/Mixin) is a class that contains methods for use by other classes without having to be the parent class of those other classes. +As defined in Wikipedia, a [mixin](https://en.wikipedia.org/wiki/Mixin) is a class containing methods that can be used by other classes without a need to inherit from it. In other words, a *mixin* provides methods that implement a certain behavior, but we do not use it alone, we use it to add the behavior to other classes. diff --git a/1-js/10-error-handling/1-try-catch/article.md b/1-js/10-error-handling/1-try-catch/article.md index 3f2e419bf..82941387a 100644 --- a/1-js/10-error-handling/1-try-catch/article.md +++ b/1-js/10-error-handling/1-try-catch/article.md @@ -302,7 +302,7 @@ try { *!* alert(e.name); // SyntaxError */!* - alert(e.message); // Unexpected token o in JSON at position 0 + alert(e.message); // Unexpected token o in JSON at position 2 } ``` diff --git a/1-js/11-async/01-callbacks/article.md b/1-js/11-async/01-callbacks/article.md index a9183c80f..742b621b7 100644 --- a/1-js/11-async/01-callbacks/article.md +++ b/1-js/11-async/01-callbacks/article.md @@ -222,6 +222,30 @@ As calls become more nested, the code becomes deeper and increasingly more diffi That's sometimes called "callback hell" or "pyramid of doom." + + ![](callback-hell.svg) The "pyramid" of nested calls grows to the right with every asynchronous action. Soon it spirals out of control. diff --git a/1-js/11-async/01-callbacks/callback-hell.svg b/1-js/11-async/01-callbacks/callback-hell.svg index 574d7dfbf..b13d3cd7a 100644 --- a/1-js/11-async/01-callbacks/callback-hell.svg +++ b/1-js/11-async/01-callbacks/callback-hell.svg @@ -1,3 +1,4 @@ +<<<<<<< HEAD @@ -347,4 +348,7 @@ - \ No newline at end of file + +======= + +>>>>>>> 852ee189170d9022f67ab6d387aeae76810b5923 diff --git a/1-js/11-async/04-promise-error-handling/article.md b/1-js/11-async/04-promise-error-handling/article.md index ca67202aa..7819a38c7 100644 --- a/1-js/11-async/04-promise-error-handling/article.md +++ b/1-js/11-async/04-promise-error-handling/article.md @@ -150,7 +150,11 @@ new Promise((resolve, reject) => { } }).then(function() { +<<<<<<< HEAD /* never runs here */ +======= + /* doesn't run here */ +>>>>>>> 852ee189170d9022f67ab6d387aeae76810b5923 }).catch(error => { // (**) alert(`The unknown error has occurred: ${error}`); @@ -266,7 +270,11 @@ new Promise(function() { In case of an error, the promise state becomes "rejected", and the execution should jump to the closest rejection handler. But there is no such handler in the examples above. So the error gets "stuck". +<<<<<<< HEAD In practice, just like with a regular unhandled errors, it means that something has terribly gone wrong, the script probably died. +======= +In practice, just like with regular unhandled errors in code, it means that something has terribly gone wrong. +>>>>>>> 852ee189170d9022f67ab6d387aeae76810b5923 Most JavaScript engines track such situations and generate a global error in that case. We can see it in the console. diff --git a/1-js/12-generators-iterators/1-generators/article.md b/1-js/12-generators-iterators/1-generators/article.md index f6fcfb00d..82e54edf9 100644 --- a/1-js/12-generators-iterators/1-generators/article.md +++ b/1-js/12-generators-iterators/1-generators/article.md @@ -243,10 +243,25 @@ That surely would require a `break` in `for..of`, otherwise the loop would repea Generator composition is a special feature of generators that allows to transparently "embed" generators in each other. +<<<<<<< HEAD For instance, we'd like to generate a sequence of: - digits `0..9` (character codes 48..57), - followed by alphabet letters `a..z` (character codes 65..90) - followed by uppercased letters `A..Z` (character codes 97..122) +======= +For instance, we have a function that generates a sequence of numbers: + +```js +function* generateSequence(start, end) { + for (let i = start; i <= end; i++) yield i; +} +``` + +Now we'd like to reuse it for generation of a more complex sequence: +- first, digits `0..9` (with character codes 48..57), +- followed by alphabet letters `A..Z` (character codes 65..90) +- followed by uppercased letters `a..z` (character codes 97..122) +>>>>>>> 852ee189170d9022f67ab6d387aeae76810b5923 Then we plan to create passwords by selecting characters from it (could add syntax characters as well), but need to generate the sequence first. diff --git a/1-js/12-generators-iterators/2-async-iterators-generators/article.md b/1-js/12-generators-iterators/2-async-iterators-generators/article.md index 45cf73938..5ff9b6493 100644 --- a/1-js/12-generators-iterators/2-async-iterators-generators/article.md +++ b/1-js/12-generators-iterators/2-async-iterators-generators/article.md @@ -280,7 +280,11 @@ for await (let commit of fetchCommits(repo)) { } ``` +<<<<<<< HEAD We'd like `fetchCommits` to get commits for us, making requests whenever needed. And let it care about all pagination stuff, for us it'll be a simple `for await..of`. +======= +We'd like to make a function `fetchCommits(repo)` that gets commits for us, making requests whenever needed. And let it care about all pagination stuff, for us it'll be a simple `for await..of`. +>>>>>>> 852ee189170d9022f67ab6d387aeae76810b5923 With async generators that's pretty easy to implement: @@ -358,4 +362,8 @@ In web-development we often meet streams of data, when it flows chunk-by-chunk. We could use async generators to process such data, but there's also another API called Streams, that may be more convenient, as it provides special interfaces to transform the data and to pass it from one stream to another (e.g. download from one place and immediately send elsewhere). But they are also more complex. +<<<<<<< HEAD Streams API not a part of JavaScript language standard. Streams and async generators complement each other, both are great ways to handle async data flows. +======= +Streams API is not a part of JavaScript language standard. +>>>>>>> 852ee189170d9022f67ab6d387aeae76810b5923 diff --git a/1-js/99-js-misc/01-proxy/03-observable/task.md b/1-js/99-js-misc/01-proxy/03-observable/task.md new file mode 100644 index 000000000..754d9f3bd --- /dev/null +++ b/1-js/99-js-misc/01-proxy/03-observable/task.md @@ -0,0 +1,27 @@ + +# Observable + +Create a function `makeObservable(target)` that "makes the object observable" by returning a proxy. + +Here's how it should work: + +```js run +function makeObservable(target) { + /* your code */ +} + +let user = {}; +user = makeObservable(user); + +user.observe((key, value) => { + alert(`SET ${key}=${value}`); +}); + +user.name = "John"; // alerts: SET name=John +``` + +In other words, an object returned by `makeObservable` is just like the original one, but also has the method `observe(handler)` that sets `handler` function to be called on any property change. + +Whenever a property changes, `handler(key, value)` is called with the name and value of the property. + +P.S. In this task, please only take care about writing to a property. Other operations can be implemented in a similar way. diff --git a/1-js/99-js-misc/01-proxy/article.md b/1-js/99-js-misc/01-proxy/article.md new file mode 100644 index 000000000..7a00ef663 --- /dev/null +++ b/1-js/99-js-misc/01-proxy/article.md @@ -0,0 +1,1030 @@ +# Proxy and Reflect + +A `Proxy` object wraps another object and intercepts operations, like reading/writing properties and others, optionally handling them on its own, or transparently allowing the object to handle them. + +Proxies are used in many libraries and some browser frameworks. We'll see many practical applications in this chapter. + +The syntax: + +```js +let proxy = new Proxy(target, handler) +``` + +- `target` -- is an object to wrap, can be anything, including functions. +- `handler` -- proxy configuration: an object with "traps": methods that intercept operations., e.g. `get` trap is for reading a property of `target`, `set` trap - for writing a property into `target`, etc. + +For operations on `proxy`, if there's a corresponding trap in `handler`, then it runs, and the proxy has a chance to handle it, otherwise the operation is performed on `target`. + +As a starting example, let's create a proxy without any traps: + +```js run +let target = {}; +let proxy = new Proxy(target, {}); // empty handler + +proxy.test = 5; // writing to proxy (1) +alert(target.test); // 5, the property appeared in target! + +alert(proxy.test); // 5, we can read it from proxy too (2) + +for(let key in proxy) alert(key); // test, iteration works (3) +``` + +As there are no traps, all operations on `proxy` are forwarded to `target`. + +1. A writing operation `proxy.test=` sets the value on `target`. +2. A reading operation `proxy.test` returns the value from `target`. +3. Iteration over `proxy` returns values from `target`. + +As we can see, without any traps, `proxy` is a transparent wrapper around `target`. + +![](proxy.svg) + +`Proxy` is a special "exotic object". It doesn't have own properties. With an empty `handler` it transparently forwards operations to `target`. + +To activate more capabilities, let's add traps. + +What can we intercept by them? + +For most operations on objects, there's a so-called "internal method" in JavaScript specificaiton, that describes on the lowest level, how it works. For instance, `[[Get]]` - the internal method to read a property, `[[Set]]` -- the internal method to write a property, and so on. These methods are only used in the specification, we can't them directly by name. + +Proxy traps inercept invocations of these methods. They are listed in [Proxy specification](https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots) and in the table below. + +For every internal method, there's a trap in this table: the name of the method that we can add to `handler` parameter of `new Proxy` to intercept the operation: + +| Internal Method | Handler Method | Triggers when... | +|-----------------|----------------|-------------| +| `[[Get]]` | `get` | reading a property | +| `[[Set]]` | `set` | writing to a property | +| `[[HasProperty]]` | `has` | `in` operator | +| `[[Delete]]` | `deleteProperty` | `delete` operator | +| `[[Call]]` | `apply` | function call | +| `[[Construct]]` | `construct` | `new` operator | +| `[[GetPrototypeOf]]` | `getPrototypeOf` | [Object.getPrototypeOf](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf) | +| `[[SetPrototypeOf]]` | `setPrototypeOf` | [Object.setPrototypeOf](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf) | +| `[[IsExtensible]]` | `isExtensible` | [Object.isExtensible](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isExtensible) | +| `[[PreventExtensions]]` | `preventExtensions` | [Object.preventExtensions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/preventExtensions) | +| `[[DefineOwnProperty]]` | `defineProperty` | [Object.defineProperty](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty), [Object.defineProperties](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties) | +| `[[GetOwnProperty]]` | `getOwnPropertyDescriptor` | [Object.getOwnPropertyDescriptor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor), `for..in`, `Object.keys/values/entries` | +| `[[OwnPropertyKeys]]` | `ownKeys` | [Object.getOwnPropertyNames](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames), [Object.getOwnPropertySymbols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertySymbols), `for..in`, `Object/keys/values/entries` | + +```warn header="Invariants" +JavaScript enforces some invariants -- conditions that must be fulfilled by internal methods and traps. + +Most of them are for return values: +- `[[Set]]` must return `true` if the value was written successfully, otherwise `false`. +- `[[Delete]]` must return `true` if the value was deleted successfully, otherwise `false`. +- ...and so on, we'll see more in examples below. + +There are some other invariants, like: +- `[[GetPrototypeOf]]`, applied to the proxy object must return the same value as `[[GetPrototypeOf]]` applied to the proxy object's target object. In other words, reading prototype of a proxy must always return the prototype of the target object. + +Traps can intercept these operations, but they must follow these rules. + +Invariants ensure correct and consistent behavior of language features. The full invariants list is in [the specification](https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots), you probably won't violate them, if not doing something weird. +``` + +Let's see how that works on practical examples. + +## Default value with "get" trap + +The most common traps are for reading/writing properties. + +To intercept the reading, the `handler` should have a method `get(target, property, receiver)`. + +It triggers when a property is read, with following arguments: + +- `target` -- is the target object, the one passed as the first argument to `new Proxy`, +- `property` -- property name, +- `receiver` -- if the target property is a getter, then `receiver` is the object that's going to be used as `this` in its call. Usually that's the `proxy` object itself (or an object that inherits from it, if we inherit from proxy). Right now we don't need this argument, will be explained in more details letter. + +Let's use `get` to implement default values for an object. + +We'll make a numeric array that returns return `0` for non-existant values. + +Usually when one tries to get a non-existing array item, they get `undefined`, but we'll wrap a regular array into proxy that traps reading and returns `0` if there's no such property: + +```js run +let numbers = [0, 1, 2]; + +numbers = new Proxy(numbers, { + get(target, prop) { + if (prop in target) { + return target[prop]; + } else { + return 0; // default value + } + } +}); + +*!* +alert( numbers[1] ); // 1 +alert( numbers[123] ); // 0 (no such item) +*/!* +``` + +As we can see, it's quite easy to do with `get` trap. + +We can use `Proxy` to implement any logic for "default" values. + +Imagine, we have a dictionary with phrases along with translations: + +```js run +let dictionary = { + 'Hello': 'Hola', + 'Bye': 'Adiós' +}; + +alert( dictionary['Hello'] ); // Hola +alert( dictionary['Welcome'] ); // undefined +``` + +Right now, if there's no phrase, reading from `dictionary` returns `undefined`. But in practice, leaving a phrase non-translated is usually better than `undefined`. So let's make it return a non-translated phrase in that case instead of `undefined`. + +To achieve that, we'll wrap `dictionary` in a proxy that intercepts reading operations: + +```js run +let dictionary = { + 'Hello': 'Hola', + 'Bye': 'Adiós' +}; + +dictionary = new Proxy(dictionary, { +*!* + get(target, phrase) { // intercept reading a property from dictionary +*/!* + if (phrase in target) { // if we have it in the dictionary + return target[phrase]; // return the translation + } else { + // otherwise, return the non-translated phrase + return phrase; + } + } +}); + +// Look up arbitrary phrases in the dictionary! +// At worst, they are not translated. +alert( dictionary['Hello'] ); // Hola +*!* +alert( dictionary['Welcome to Proxy']); // Welcome to Proxy (no translation) +*/!* +``` + +````smart +Please note how the proxy overwrites the variable: + +```js +dictionary = new Proxy(dictionary, ...); +``` + +The proxy should totally replace the target object everywhere. No one should ever reference the target object after it got proxied. Otherwise it's easy to mess up. +```` + +## Validation with "set" trap + +Let's say we want an array exclusively for numbers. If a value of another type is added, there should be an error. + +The `set` trap triggers when a property is written. + +`set(target, property, value, receiver)`: + +- `target` -- is the target object, the one passed as the first argument to `new Proxy`, +- `property` -- property name, +- `value` -- property value, +- `receiver` -- similar to `get` trap, matters only for setter properties. + +The `set` trap should return `true` if setting is successful, and `false` otherwise (triggers `TypeError`). + +Let's use it to validate new values: + +```js run +let numbers = []; + +numbers = new Proxy(numbers, { // (*) +*!* + set(target, prop, val) { // to intercept property writing +*/!* + if (typeof val == 'number') { + target[prop] = val; + return true; + } else { + return false; + } + } +}); + +numbers.push(1); // added successfully +numbers.push(2); // added successfully +alert("Length is: " + numbers.length); // 2 + +*!* +numbers.push("test"); // TypeError ('set' on proxy returned false) +*/!* + +alert("This line is never reached (error in the line above)"); +``` + +Please note: the built-in functionality of arrays is still working! Values are added by `push`. The `length` property auto-increases when values are added. Our proxy doesn't break anything. + +We don't have to override value-adding array methods like `push` and `unshift`, and so on, to add checks in there, because internally they use `[[Set]]` operation, that's intercepted by the proxy. + +So the code is clean and concise. + +```warn header="Don't forget to return `true`" +As said above, there are invariants to be held. + +For `set`, it must return `true` for a successful write. + +If we forget to do it or return any falsy value, the operation triggers `TypeError`. +``` + +## Iteration with "ownKeys" and "getOwnPropertyDescriptor" + +`Object.keys`, `for..in` loop and most other methods that iterate over object properties use `[[OwnPropertyKeys]]` internal method (intercepted by `ownKeys` trap) to get a list of properties. + +Such methods differ in details: +- `Object.getOwnPropertyNames(obj)` returns non-symbol keys. +- `Object.getOwnPropertySymbols(obj)` returns symbol keys. +- `Object.keys/values()` returns non-symbol keys/values with `enumerable` flag (property flags were explained in the chapter ). +- `for..in` loops over non-symbol keys with `enumerable` flag, and also prototype keys. + +...But all of them start with that list. + +In the example below we use `ownKeys` trap to make `for..in` loop over `user`, and also `Object.keys` and `Object.values`, to skip properties starting with an underscore `_`: + +```js run +let user = { + name: "John", + age: 30, + _password: "***" +}; + +user = new Proxy(user, { +*!* + ownKeys(target) { +*/!* + return Object.keys(target).filter(key => !key.startsWith('_')); + } +}); + +// "ownKeys" filters out _password +for(let key in user) alert(key); // name, then: age + +// same effect on these methods: +alert( Object.keys(user) ); // name,age +alert( Object.values(user) ); // John,30 +``` + +So far, it works. + +Although, if we return a key that doesn't exist in the object, `Object.keys` won't list it: + +```js run +let user = { }; + +user = new Proxy(user, { +*!* + ownKeys(target) { +*/!* + return ['a', 'b', 'c']; + } +}); + +alert( Object.keys(user) ); // +``` + +Why? The reason is simple: `Object.keys` returns only properties with `enumerable` flag. To check for it, it calls the internal method `[[GetOwnProperty]]` for every property to get [its descriptor](info:property-descriptors). And here, as there's no property, its descriptor is empty, no `enumerable` flag, so it's skipped. + +For `Object.keys` to return a property, we need it either exist in the object, with `enumerable` flag, or we can intercept calls to `[[GetOwnProperty]]` (the trap `getOwnPropertyDescriptor` does it), and return a descriptor with `enumerable: true`. + +Here's a working code: + +```js run +let user = { }; + +user = new Proxy(user, { + ownKeys(target) { // called once to get a list of properties + return ['a', 'b', 'c']; + }, + + getOwnPropertyDescriptor(target, prop) { // called for every property + return { + enumerable: true, + configurable: true + /* ...other flags, probable "value:..."" */ + }; + } + +}); + +alert( Object.keys(user) ); // a, b, c +``` + +Let's note once again: we only need to intercept `[[GetOwnProperty]]` if the property is absent in the object. + +## Protected properties with "deleteProperty" and other traps + +There's a widespread convention that properties and methods prefixed by an underscore `_` are internal. They shouldn't be accessed from outside the object. + +Technically, that's possible though: + +```js run +let user = { + name: "John", + _password: "secret" +}; + +alert(user._password); // secret +``` + +Let's use proxies to prevent any access to properties starting with `_`. + +We'll need the traps: +- `get` to throw an error when reading such property, +- `set` to throw an error when writing, +- `deleteProperty` to throw an error when deleting, +- `ownKeys` to exclude properties starting with `_` from `for..in` and methods like `Object.keys`. + +Here's the code: + +```js run +let user = { + name: "John", + _password: "***" +}; + +user = new Proxy(user, { +*!* + get(target, prop) { +*/!* + if (prop.startsWith('_')) { + throw new Error("Access denied"); + } + let value = target[prop]; + return (typeof value === 'function') ? value.bind(target) : value; // (*) + }, +*!* + set(target, prop, val) { // to intercept property writing +*/!* + if (prop.startsWith('_')) { + throw new Error("Access denied"); + } else { + target[prop] = val; + return true; + } + }, +*!* + deleteProperty(target, prop) { // to intercept property deletion +*/!* + if (prop.startsWith('_')) { + throw new Error("Access denied"); + } else { + delete target[prop]; + return true; + } + }, +*!* + ownKeys(target) { // to intercept property list +*/!* + return Object.keys(target).filter(key => !key.startsWith('_')); + } +}); + +// "get" doesn't allow to read _password +try { + alert(user._password); // Error: Access denied +} catch(e) { alert(e.message); } + +// "set" doesn't allow to write _password +try { + user._password = "test"; // Error: Access denied +} catch(e) { alert(e.message); } + +// "deleteProperty" doesn't allow to delete _password +try { + delete user._password; // Error: Access denied +} catch(e) { alert(e.message); } + +// "ownKeys" filters out _password +for(let key in user) alert(key); // name +``` + +Please note the important detail in `get` trap, in the line `(*)`: + +```js +get(target, prop) { + // ... + let value = target[prop]; +*!* + return (typeof value === 'function') ? value.bind(target) : value; // (*) +*/!* +} +``` + +Why do we need for a function to call `value.bind(target)`? + +The reason is that object methods, such as `user.checkPassword()`, must be able to access `_password`: + +```js +user = { + // ... + checkPassword(value) { + // object method must be able to read _password + return value === this._password; + } +} +``` + + +A call to `user.checkPassword()` call gets proxied `user` as `this` (the object before dot becomes `this`), so when it tries to access `this._password`, the `get` trap activates (it triggers on any property read) and throws an error. + +So we bind the context of object methods to the original object, `target`, in the line `(*)`. Then their future calls will use `target` as `this`, without any traps. + +That solution usually works, but isn't ideal, as a method may pass the unproxied object somewhere else, and then we'll get messed up: where's the original object, and where's the proxied one. + +Besides, an object may be proxied multiple times (multiple proxies may add different "tweaks" to the object), and if we pass an unwrapped object to a method, there may be unexpected consequences. + +So, such proxy shouldn't be used everywhere. + +```smart header="Private properties of a class" +Modern JavaScript engines natively support private properties in classes, prefixed with `#`. They are described in the chapter . No proxies required. + +Such properties have their own issues though. In particular, they are not inherited. +``` + +## "In range" with "has" trap + +Let's see more examples. + +We have a range object: + +```js +let range = { + start: 1, + end: 10 +}; +``` + +We'd like to use `in` operator to check that a number is in `range`. + +The `has` trap intercepts `in` calls. + +`has(target, property)` + +- `target` -- is the target object, passed as the first argument to `new Proxy`, +- `property` -- property name + +Here's the demo: + +```js run +let range = { + start: 1, + end: 10 +}; + +range = new Proxy(range, { +*!* + has(target, prop) { +*/!* + return prop >= target.start && prop <= target.end + } +}); + +*!* +alert(5 in range); // true +alert(50 in range); // false +*/!* +``` + +A nice syntactic sugar, isn't it? And very simple to implement. + +## Wrapping functions: "apply" + +We can wrap a proxy around a function as well. + +The `apply(target, thisArg, args)` trap handles calling a proxy as function: + +- `target` is the target object (function is an object in JavaScript), +- `thisArg` is the value of `this`. +- `args` is a list of arguments. + +For example, let's recall `delay(f, ms)` decorator, that we did in the chapter . + +In that chapter we did it without proxies. A call to `delay(f, ms)` returned a function that forwards all calls to `f` after `ms` milliseconds. + +Here's the previous, function-based implementation: + +```js run +function delay(f, ms) { + // return a wrapper that passes the call to f after the timeout + return function() { // (*) + setTimeout(() => f.apply(this, arguments), ms); + }; +} + +function sayHi(user) { + alert(`Hello, ${user}!`); +} + +// after this wrapping, calls to sayHi will be delayed for 3 seconds +sayHi = delay(sayHi, 3000); + +sayHi("John"); // Hello, John! (after 3 seconds) +``` + +As we've seen already, that mostly works. The wrapper function `(*)` performs the call after the timeout. + +But a wrapper function does not forward property read/write operations or anything else. After the wrapping, the access is lost to properties of the original functions, such as `name`, `length` and others: + +```js run +function delay(f, ms) { + return function() { + setTimeout(() => f.apply(this, arguments), ms); + }; +} + +function sayHi(user) { + alert(`Hello, ${user}!`); +} + +*!* +alert(sayHi.length); // 1 (function length is the arguments count in its declaration) +*/!* + +sayHi = delay(sayHi, 3000); + +*!* +alert(sayHi.length); // 0 (in the wrapper declaration, there are zero arguments) +*/!* +``` + +`Proxy` is much more powerful, as it forwards everything to the target object. + +Let's use `Proxy` instead of a wrapping function: + +```js run +function delay(f, ms) { + return new Proxy(f, { + apply(target, thisArg, args) { + setTimeout(() => target.apply(thisArg, args), ms); + } + }); +} + +function sayHi(user) { + alert(`Hello, ${user}!`); +} + +sayHi = delay(sayHi, 3000); + +*!* +alert(sayHi.length); // 1 (*) proxy forwards "get length" operation to the target +*/!* + +sayHi("John"); // Hello, John! (after 3 seconds) +``` + +The result is the same, but now not only calls, but all operations on the proxy are forwarded to the original function. So `sayHi.length` is returned correctly after the wrapping in the line `(*)`. + +We've got a "richer" wrapper. + +There exist other traps: the full list is in the beginning of this chapter. Their usage pattern is similar to the above. + +## Reflect + +`Reflect` is a built-in object that simplifies creation of `Proxy`. + +It was said previously that internal methods, such as `[[Get]]`, `[[Set]]` and others are specifiction only, they can't be called directly. + +`Reflect` object makes that somewhat possible. Its methods are minimal wrappers around the internal methods. + +Here are examples of operations and `Reflect` calls that do the same: + +| Operation | `Reflect` call | Internal method | +|-----------------|----------------|-------------| +| `obj[prop]` | `Reflect.get(obj, prop)` | `[[Get]]` | +| `obj[prop] = value` | `Reflect.set(obj, prop, value)` | `[[Set]]` | +| `delete obj[prop]` | `Reflect.deleteProperty(obj, prop)` | `[[HasProperty]]` | +| `new F(value)` | `Reflect.construct(F, value)` | `[[Construct]]` | +| ... | ... | ... | + +For example: + +```js run +let user = {}; + +Reflect.set(user, 'name', 'John'); + +alert(user.name); // John +``` + +In particular, `Reflect` allows to call operators (`new`, `delete`...) as functions (`Reflect.construct`, `Reflect.deleteProperty`, ...). That's an interesting capability, but here another thing is important. + +**For every internal method, trappable by `Proxy`, there's a corresponding method in `Reflect`, with the same name and arguments as `Proxy` trap.** + +So we can use `Reflect` to forward an operation to the original object. + +In this example both traps `get` and `set` transparently (as if they didn't exist) forward reading/writing operations to the object, showing a message: + +```js run +let user = { + name: "John", +}; + +user = new Proxy(user, { + get(target, prop, receiver) { + alert(`GET ${prop}`); +*!* + return Reflect.get(target, prop, receiver); // (1) +*/!* + }, + set(target, prop, val, receiver) { + alert(`SET ${prop}=${val}`); +*!* + return Reflect.set(target, prop, val, receiver); // (2) +*/!* + } +}); + +let name = user.name; // shows "GET name" +user.name = "Pete"; // shows "SET name=Pete" +``` + +Here: + +- `Reflect.get` reads an object property. +- `Reflect.set` writes an object property and returns `true` if successful, `false` otherwise. + +That is, everything's simple: if a trap wants to forward the call to the object, it's enough to call `Reflect.` with the same arguments. + +In most cases we can do the same without `Reflect`, for instance, reading a property `Reflect.get(target, prop, receiver)` can be replaced by `target[prop]`. There are important nuances though. + +### Proxying a getter + +Let's see an example that demonstrates why `Reflect.get` is better. And we'll also see why `get/set` have the fourth argument `receiver`, that we didn't use before. + +We have an object `user` with `_name` property and a getter for it. + +Here's a proxy around it: + +```js run +let user = { + _name: "Guest", + get name() { + return this._name; + } +}; + +*!* +let userProxy = new Proxy(user, { + get(target, prop, receiver) { + return target[prop]; + } +}); +*/!* + +alert(userProxy.name); // Guest +``` + +The `get` trap is "transparent" here, it returns the original property, and doesn't do anything else. That's enough for our example. + +Everything seems to be all right. But let's make the example a little bit more complex. + +After inheriting another object `admin` from `user`, we can observe the incorrect behavior: + +```js run +let user = { + _name: "Guest", + get name() { + return this._name; + } +}; + +let userProxy = new Proxy(user, { + get(target, prop, receiver) { + return target[prop]; // (*) target = user + } +}); + +*!* +let admin = { + __proto__: userProxy, + _name: "Admin" +}; + +// Expected: Admin +alert(admin.name); // outputs: Guest (?!?) +*/!* +``` + +Reading `admin.name` should return `"Admin"`, not `"Guest"`! + +What's the matter? Maybe we did something wrong with the inheritance? + +But if we remove the proxy, then everything will work as expected. + +The problem is actually in the proxy, in the line `(*)`. + +1. When we read `admin.name`, as `admin` object doesn't have such own property, the search goes to its prototype. +2. The prototype is `userProxy`. +3. When reading `name` property from the proxy, its `get` trap triggers and returns it from the original object as `target[prop]` in the line `(*)`. + + A call to `target[prop]`, when `prop` is a getter, runs its code in the context `this=target`. So the result is `this._name` from the original object `target`, that is: from `user`. + +To fix such situations, we need `receiver`, the third argument of `get` trap. It keeps the correct `this` to be passed to a getter. In our case that's `admin`. + +How to pass the context for a getter? For a regular function we could use `call/apply`, but that's a getter, it's not "called", just accessed. + +`Reflect.get` can do that. Everything will work right if we use it. + +Here's the corrected variant: + +```js run +let user = { + _name: "Guest", + get name() { + return this._name; + } +}; + +let userProxy = new Proxy(user, { + get(target, prop, receiver) { // receiver = admin +*!* + return Reflect.get(target, prop, receiver); // (*) +*/!* + } +}); + + +let admin = { + __proto__: userProxy, + _name: "Admin" +}; + +*!* +alert(admin.name); // Admin +*/!* +``` + +Now `receiver` that keeps a reference to the correct `this` (that is `admin`), is passed to the getter using `Reflect.get` in the line `(*)`. + +We can rewrite the trap even shorter: + +```js +get(target, prop, receiver) { + return Reflect.get(*!*...arguments*/!*); +} +``` + + +`Reflect` calls are named exactly the same way as traps and accept the same arguments. They were specifically designed this way. + +So, `return Reflect...` provides a safe no-brainer to forward the operation and make sure we don't forget anything related to that. + +## Proxy limitations + +Proxies provide a unique way to alter or tweak the behavior of the existing objects at the lowest level. Still, it's not perfect. There are limitations. + +### Built-in objects: Internal slots + +Many built-in objects, for example `Map`, `Set`, `Date`, `Promise` and others make use of so-called "internal slots". + +These are like properties, but reserved for internal, specification-only purposes. For instance, `Map` stores items in the internal slot `[[MapData]]`. Built-in methods access them directly, not via `[[Get]]/[[Set]]` internal methods. So `Proxy` can't intercept that. + +Why care? They are internal anyway! + +Well, here's the issue. After such built-in object gets proxied, the proxy doesn't have these internal slots, so built-in methods will fail. + +For example: + +```js run +let map = new Map(); + +let proxy = new Proxy(map, {}); + +*!* +proxy.set('test', 1); // Error +*/!* +``` + +Internally, a `Map` stores all data in its `[[MapData]]` internal slot. The proxy doesn't have such slot. The [built-in method `Map.prototype.set`](https://tc39.es/ecma262/#sec-map.prototype.set) method tries to access the internal property `this.[[MapData]]`, but because `this=proxy`, can't find it in `proxy` and just fails. + +Fortunately, there's a way to fix it: + +```js run +let map = new Map(); + +let proxy = new Proxy(map, { + get(target, prop, receiver) { + let value = Reflect.get(...arguments); +*!* + return typeof value == 'function' ? value.bind(target) : value; +*/!* + } +}); + +proxy.set('test', 1); +alert(proxy.get('test')); // 1 (works!) +``` + +Now it works fine, because `get` trap binds function properties, such as `map.set`, to the target object (`map`) itself. + +Unlike the previous example, the value of `this` inside `proxy.set(...)` will be not `proxy`, but the original `map`. So when the internal implementation of `set` tries to access `this.[[MapData]]` internal slot, it succeeds. + +```smart header="`Array` has no internal slots" +A notable exception: built-in `Array` doesn't use internal slots. That's for historical reasons, as it appeared so long ago. + +So there's no such problem when proxying an array. +``` + +### Private fields + +The similar thing happens with private class fields. + +For example, `getName()` method accesses the private `#name` property and breaks after proxying: + +```js run +class User { + #name = "Guest"; + + getName() { + return this.#name; + } +} + +let user = new User(); + +user = new Proxy(user, {}); + +*!* +alert(user.getName()); // Error +*/!* +``` + +The reason is that private fields are implemented using internal slots. JavaScript does not use `[[Get]]/[[Set]]` when accessing them. + +In the call `getName()` the value of `this` is the proxied `user`, and it doesn't have the slot with private fields. + +Once again, the solution with binding the method makes it work: + +```js run +class User { + #name = "Guest"; + + getName() { + return this.#name; + } +} + +let user = new User(); + +user = new Proxy(user, { + get(target, prop, receiver) { + let value = Reflect.get(...arguments); + return typeof value == 'function' ? value.bind(target) : value; + } +}); + +alert(user.getName()); // Guest +``` + +That said, the solution has drawbacks, explained previously: it exposes the original object to the method, potentially allowing it to be passed further and breaking other proxied functionality. + +### Proxy != target + +Proxy and the original object are different objects. That's natural, right? + +So if we use the original object as a key, and then proxy it, then the proxy can't be found: + +```js run +let allUsers = new Set(); + +class User { + constructor(name) { + this.name = name; + allUsers.add(this); + } +} + +let user = new User("John"); + +alert(allUsers.has(user)); // true + +user = new Proxy(user, {}); + +*!* +alert(allUsers.has(user)); // false +*/!* +``` + +As we can see, after proxying we can't find `user` in the set `allUsers`, because the proxy is a different object. + +```warn header="Proxies can't intercept a strict equality test `===`" +Proxies can intercept many operators, such as `new` (with `construct`), `in` (with `has`), `delete` (with `deleteProperty`) and so on. + +But there's no way to intercept a strict equality test for objects. An object is strictly equal to itself only, and no other value. + +So all operations and built-in classes that compare objects for equality will differentiate between the object and the proxy. No transparent replacement here. +``` + +## Revocable proxies + +A *revocable* proxy is a proxy that can be disabled. + +Let's say we have a resource, and would like to close access to it any moment. + +What we can do is to wrap it into a revocable proxy, without any traps. Such proxy will forward operations to object, and we can disable it at any moment. + +The syntax is: + +```js +let {proxy, revoke} = Proxy.revocable(target, handler) +``` + +The call returns an object with the `proxy` and `revoke` function to disable it. + +Here's an example: + +```js run +let object = { + data: "Valuable data" +}; + +let {proxy, revoke} = Proxy.revocable(object, {}); + +// pass the proxy somewhere instead of object... +alert(proxy.data); // Valuable data + +// later in our code +revoke(); + +// the proxy isn't working any more (revoked) +alert(proxy.data); // Error +``` + +A call to `revoke()` removes all internal references to the target object from the proxy, so they are no more connected. The target object can be garbage-collected after that. + +We can also store `revoke` in a `WeakMap`, to be able to easily find it by a proxy object: + +```js run +*!* +let revokes = new WeakMap(); +*/!* + +let object = { + data: "Valuable data" +}; + +let {proxy, revoke} = Proxy.revocable(object, {}); + +revokes.set(proxy, revoke); + +// ..later in our code.. +revoke = revokes.get(proxy); +revoke(); + +alert(proxy.data); // Error (revoked) +``` + +The benefit of such approach is that we don't have to carry `revoke` around. We can get it from the map by `proxy` when needeed. + +Using `WeakMap` instead of `Map` here, because it should not block garbage collection. If a proxy object becomes "unreachable" (e.g. no variable references it any more), `WeakMap` allows it to be wiped from memory together with its `revoke` that we won't need any more. + +## References + +- Specification: [Proxy](https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots). +- MDN: [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy). + +## Summary + +`Proxy` is a wrapper around an object, that forwards operations on it to the object, optionally trapping some of them. + +It can wrap any kind of object, including classes and functions. + +The syntax is: + +```js +let proxy = new Proxy(target, { + /* traps */ +}); +``` + +...Then we should use `proxy` everywhere instead of `target`. A proxy doesn't have its own properties or methods. It traps an operation if the trap is provided, otherwise forwards it to `target` object. + +We can trap: +- Reading (`get`), writing (`set`), deleting (`deleteProperty`) a property (even a non-existing one). +- Calling a function (`apply` trap). +- The `new` operator (`construct` trap). +- Many other operations (the full list is at the beginning of the article and in the [docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy)). + +That allows us to create "virtual" properties and methods, implement default values, observable objects, function decorators and so much more. + +We can also wrap an object multiple times in different proxies, decorating it with various aspects of functionality. + +The [Reflect](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect) API is designed to complement [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy). For any `Proxy` trap, there's a `Reflect` call with same arguments. We should use those to forward calls to target objects. + +Proxies have some limitations: + +- Built-in objects have "internal slots", access to those can't be proxied. See the workaround above. +- The same holds true for private class fields, as they are internally implemented using slots. So proxied method calls must have the target object as `this` to access them. +- Object equality tests `===` can't be intercepted. +- Performance: benchmarks depend on an engine, but generally accessing a property using a simplest proxy takes a few times longer. In practice that only matters for some "bottleneck" objects though. diff --git a/1-js/99-js-misc/01-proxy/proxy-inherit-admin.svg b/1-js/99-js-misc/01-proxy/proxy-inherit-admin.svg new file mode 100644 index 000000000..9ffe9a375 --- /dev/null +++ b/1-js/99-js-misc/01-proxy/proxy-inherit-admin.svg @@ -0,0 +1 @@ +_name: "Guest" name: getter_name: "Admin"user (proxied)original useradmin[[Prototype]] \ No newline at end of file diff --git a/1-js/99-js-misc/01-proxy/proxy-inherit.svg b/1-js/99-js-misc/01-proxy/proxy-inherit.svg new file mode 100644 index 000000000..510dcef1b --- /dev/null +++ b/1-js/99-js-misc/01-proxy/proxy-inherit.svg @@ -0,0 +1 @@ +_name: "Guest" name: getteruser (proxied)original user \ No newline at end of file diff --git a/1-js/99-js-misc/01-proxy/proxy.svg b/1-js/99-js-misc/01-proxy/proxy.svg new file mode 100644 index 000000000..8aa14bdb8 --- /dev/null +++ b/1-js/99-js-misc/01-proxy/proxy.svg @@ -0,0 +1 @@ +test: 5proxytargetget proxy.test5 \ No newline at end of file diff --git a/1-js/99-js-misc/02-eval/1-eval-calculator/task.md b/1-js/99-js-misc/02-eval/1-eval-calculator/task.md new file mode 100644 index 000000000..ece43ec9e --- /dev/null +++ b/1-js/99-js-misc/02-eval/1-eval-calculator/task.md @@ -0,0 +1,11 @@ +importance: 4 + +--- + +# Eval-calculator + +Create a calculator that prompts for an arithmetic expression and returns its result. + +There's no need to check the expression for correctness in this task. Just evaluate and return the result. + +[demo] diff --git a/1-js/99-js-misc/02-eval/article.md b/1-js/99-js-misc/02-eval/article.md new file mode 100644 index 000000000..73e8424d1 --- /dev/null +++ b/1-js/99-js-misc/02-eval/article.md @@ -0,0 +1,114 @@ +# Eval: run a code string + +The built-in `eval` function allows to execute a string of code. + +The syntax is: + +```js +let result = eval(code); +``` + +For example: + +```js run +let code = 'alert("Hello")'; +eval(code); // Hello +``` + +A string of code may be long, contain line breaks, function declarations, variables and so on. + +The result of `eval` is the result of the last statement. + +For example: +```js run +let value = eval('1+1'); +alert(value); // 2 +``` + +```js run +let value = eval('let i = 0; ++i'); +alert(value); // 1 +``` + +The eval'ed code is executed in the current lexical environment, so it can see outer variables: + +```js run no-beautify +let a = 1; + +function f() { + let a = 2; + +*!* + eval('alert(a)'); // 2 +*/!* +} + +f(); +``` + +It can change outer variables as well: + +```js untrusted refresh run +let x = 5; +eval("x = 10"); +alert(x); // 10, value modified +``` + +In strict mode, `eval` has its own lexical environment. So functions and variables, declared inside eval, are not visible outside: + +```js untrusted refresh run +// reminder: 'use strict' is enabled in runnable examples by default + +eval("let x = 5; function f() {}"); + +alert(typeof x); // undefined (no such variable) +// function f is also not visible +``` + +Without `use strict`, `eval` doesn't have its own lexical environment, so we would see `x` and `f` outside. + +## Using "eval" + +In modern programming `eval` is used very sparingly. It's often said that "eval is evil". + +The reason is simple: long, long time ago JavaScript was a much weaker language, many things could only be done with `eval`. But that time passed a decade ago. + +Right now, there's almost no reason to use `eval`. If someone is using it, there's a good chance they can replace it with a modern language construct or a [JavaScript Module](info:modules). + +Please note that its ability to access outer variables has side-effects. + +Code minifiers (tools used before JS gets to production, to compress it) replace local variables with shorter ones for optimization. That's usually safe, but not if `eval` is used, as it may reference them. So minifiers don't replace all local variables that might be visible from `eval`. That negatively affects code compression ratio. + +Using outer local variables inside `eval` is a bad programming practice, as it makes maintaining the code more difficult. + +There are two ways how to be totally safe from such problems. + +**If eval'ed code doesn't use outer variables, please call `eval` as `window.eval(...)`:** + +This way the code is executed in the global scope: + +```js untrusted refresh run +let x = 1; +{ + let x = 5; + window.eval('alert(x)'); // 1 (global variable) +} +``` + +**If eval'ed code needs local variables, change `eval` to `new Function` and pass them as arguments:** + +```js run +let f = new Function('a', 'alert(a)'); + +f(5); // 5 +``` + +The `new Function` construct is explained in the chapter . It creates a function from a string, also in the global scope. So it can't see local variables. But it's so much clearer to pass them explicitly as arguments, like in the example above. + +## Summary + +A call to `eval(code)` runs the string of code and returns the result of the last statement. +- Rarely used in modern JavaScript, as there's usually no need. +- Can access outer local variables. That's considered bad practice. +- Instead, to `eval` the code in the global scope, use `window.eval(code)`. +- Or, if your code needs some data from the outer scope, use `new Function` and pass it as arguments. diff --git a/1-js/99-js-misc/03-currying-partials/article.md b/1-js/99-js-misc/03-currying-partials/article.md new file mode 100644 index 000000000..02f9a510d --- /dev/null +++ b/1-js/99-js-misc/03-currying-partials/article.md @@ -0,0 +1,196 @@ +libs: + - lodash + +--- + +# Currying + +[Currying](https://en.wikipedia.org/wiki/Currying) is an advanced technique of working with functions. It's used not only in JavaScript, but in other languages as well. + +Currying is a transformation of functions that translates a function from callable as `f(a, b, c)` into callable as `f(a)(b)(c)`. + +Currying doesn't call a function. It just transforms it. + +Let's see an example first, to better understand what we're talking about, and then practical applications. + +We'll create a helper function `curry(f)` that performs currying for a two-argument `f`. In other words, `curry(f)` for two-argument `f(a, b)` translates it into a function that runs as `f(a)(b)`: + +```js run +*!* +function curry(f) { // curry(f) does the currying transform + return function(a) { + return function(b) { + return f(a, b); + }; + }; +} +*/!* + +// usage +function sum(a, b) { + return a + b; +} + +let carriedSum = curry(sum); + +alert( carriedSum(1)(2) ); // 3 +``` + +As you can see, the implementation is straightforward: it's just two wrappers. + +- The result of `curry(func)` is a wrapper `function(a)`. +- When it is called like `sum(1)`, the argument is saved in the Lexical Environment, and a new wrapper is returned `function(b)`. +- Then this wrapper is called with `2` as an argument, and it passes the call to the original `sum`. + +More advanced implementations of currying, such as [_.curry](https://lodash.com/docs#curry) from lodash library, return a wrapper that allows a function to be called both normally and partially: + +```js run +function sum(a, b) { + return a + b; +} + +let carriedSum = _.curry(sum); // using _.carry from lodash library + +alert( carriedSum(1, 2) ); // 3, still callable normally +alert( carriedSum(1)(2) ); // 3, called partially +``` + +## Currying? What for? + +To understand the benefits we need a worthy real-life example. + +For instance, we have the logging function `log(date, importance, message)` that formats and outputs the information. In real projects such functions have many useful features like sending logs over the network, here we'll just use `alert`: + +```js +function log(date, importance, message) { + alert(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`); +} +``` + +Let's curry it! + +```js +log = _.curry(log); +``` + +After that `log` work normally: + +```js +log(new Date(), "DEBUG", "some debug"); // log(a, b, c) +``` + +...But also works in the curried form: + +```js +log(new Date())("DEBUG")("some debug"); // log(a)(b)(c) +``` + +Now we can easily make a convenience function for current logs: + +```js +// logNow will be the partial of log with fixed first argument +let logNow = log(new Date()); + +// use it +logNow("INFO", "message"); // [HH:mm] INFO message +``` + +Now `logNow` is `log` with fixed first argument, in other words "partially applied function" or "partial" for short. + +We can go further and make a convenience function for current debug logs: + +```js +let debugNow = logNow("DEBUG"); + +debugNow("message"); // [HH:mm] DEBUG message +``` + +So: +1. We didn't lose anything after currying: `log` is still callable normally. +2. We can easily generate partial functions such as for today's logs. + +## Advanced curry implementation + +In case you'd like to get in details, here's the "advanced" curry implementation for multi-argument functions that we could use above. + +It's pretty short: + +```js +function curry(func) { + + return function curried(...args) { + if (args.length >= func.length) { + return func.apply(this, args); + } else { + return function(...args2) { + return curried.apply(this, args.concat(args2)); + } + } + }; + +} +``` + +Usage examples: + +```js +function sum(a, b, c) { + return a + b + c; +} + +let curriedSum = curry(sum); + +alert( curriedSum(1, 2, 3) ); // 6, still callable normally +alert( curriedSum(1)(2,3) ); // 6, currying of 1st arg +alert( curriedSum(1)(2)(3) ); // 6, full currying +``` + +The new `curry` may look complicated, but it's actually easy to understand. + +The result of `curry(func)` call is the wrapper `curried` that looks like this: + +```js +// func is the function to transform +function curried(...args) { + if (args.length >= func.length) { // (1) + return func.apply(this, args); + } else { + return function pass(...args2) { // (2) + return curried.apply(this, args.concat(args2)); + } + } +}; +``` + +When we run it, there are two `if` execution branches: + +1. Call now: if passed `args` count is the same as the original function has in its definition (`func.length`) or longer, then just pass the call to it. +2. Get a partial: otherwise, `func` is not called yet. Instead, another wrapper `pass` is returned, that will re-apply `curried` providing previous arguments together with the new ones. Then on a new call, again, we'll get either a new partial (if not enough arguments) or, finally, the result. + +For instance, let's see what happens in the case of `sum(a, b, c)`. Three arguments, so `sum.length = 3`. + +For the call `curried(1)(2)(3)`: + +1. The first call `curried(1)` remembers `1` in its Lexical Environment, and returns a wrapper `pass`. +2. The wrapper `pass` is called with `(2)`: it takes previous args (`1`), concatenates them with what it got `(2)` and calls `curried(1, 2)` with them together. As the argument count is still less than 3, `curry` returns `pass`. +3. The wrapper `pass` is called again with `(3)`, for the next call `pass(3)` takes previous args (`1`, `2`) and adds `3` to them, making the call `curried(1, 2, 3)` -- there are `3` arguments at last, they are given to the original function. + +If that's still not obvious, just trace the calls sequence in your mind or on the paper. + +```smart header="Fixed-length functions only" +The currying requires the function to have a fixed number of arguments. + +A function that uses rest parameters, such as `f(...args)`, can't be curried this way. +``` + +```smart header="A little more than currying" +By definition, currying should convert `sum(a, b, c)` into `sum(a)(b)(c)`. + +But most implementations of currying in JavaScript are advanced, as described: they also keep the function callable in the multi-argument variant. +``` + +## Summary + +*Currying* is a transform that makes `f(a,b,c)` callable as `f(a)(b)(c)`. JavaScript implementations usually both keep the function callable normally and return the partial if arguments count is not enough. + +Currying allows to easily get partials. As we've seen in the logging example: the universal function `log(date, importance, message)` after currying gives us partials when called with one argument like `log(date)` or two arguments `log(date, importance)`. diff --git a/2-ui/1-document/01-browser-environment/article.md b/2-ui/1-document/01-browser-environment/article.md index 0e123f581..974142761 100644 --- a/2-ui/1-document/01-browser-environment/article.md +++ b/2-ui/1-document/01-browser-environment/article.md @@ -94,11 +94,18 @@ if (confirm("Go to wikipedia?")) { Functions `alert/confirm/prompt` are also a part of BOM: they are directly not related to the document, but represent pure browser methods of communicating with the user. +<<<<<<< HEAD ```smart header="HTML specification" BOM is the part of the general [HTML specification](https://html.spec.whatwg.org). Yes, you heard that right. The HTML spec at is not only about the "HTML language" (tags, attributes), but also covers a bunch of objects, methods and browser-specific DOM extensions. That's "HTML in broad terms". +======= +```smart header="Specifications" +BOM is the part of the general [HTML specification](https://html.spec.whatwg.org). + +Yes, you heard that right. The HTML spec at is not only about the "HTML language" (tags, attributes), but also covers a bunch of objects, methods and browser-specific DOM extensions. That's "HTML in broad terms". Also, some parts have additional specs listed at . +>>>>>>> 852ee189170d9022f67ab6d387aeae76810b5923 ``` ## Summary diff --git a/2-ui/2-events/05-dispatch-events/article.md b/2-ui/2-events/05-dispatch-events/article.md index 2b13d2360..e0e9c58b7 100644 --- a/2-ui/2-events/05-dispatch-events/article.md +++ b/2-ui/2-events/05-dispatch-events/article.md @@ -2,9 +2,9 @@ We can not only assign handlers, but also generate events from JavaScript. -Custom events can be used to create "graphical components". For instance, a root element of the menu may trigger events telling what happens with the menu: `open` (menu open), `select` (an item is selected) and so on. +Custom events can be used to create "graphical components". For instance, a root element of our own JS-based menu may trigger events telling what happens with the menu: `open` (menu open), `select` (an item is selected) and so on. Another code may listen to the events and observe what's happening with the menu. -Also we can generate built-in events like `click`, `mousedown` etc, that may be good for testing. +We can generate not only completely new events, that we invent for our own purposes, but also built-in ones, such as `click`, `mousedown` etc. That may be helpful for automated testing. ## Event constructor @@ -27,9 +27,9 @@ Arguments: ## dispatchEvent -After an event object is created, we should "run" it on an element using the call `elem.dispatchEvent(event)`. +After an event object is created, we should "run" it on an element using the call `elem.dispatchEvent(event)`. -Then handlers react on it as if it were a regular built-in event. If the event was created with the `bubbles` flag, then it bubbles. +Then handlers react on it as if it were a regular browser event. If the event was created with the `bubbles` flag, then it bubbles. In the example below the `click` event is initiated in JavaScript. The handler works same way as if the button was clicked: @@ -125,11 +125,11 @@ alert(event.clientX); // undefined, the unknown property is ignored! Technically, we can work around that by assigning directly `event.clientX=100` after creation. So that's a matter of convenience and following the rules. Browser-generated events always have the right type. -The full list of properties for different UI events is in the specification, for instance [MouseEvent](https://www.w3.org/TR/uievents/#mouseevent). +The full list of properties for different UI events is in the specification, for instance, [MouseEvent](https://www.w3.org/TR/uievents/#mouseevent). ## Custom events -For our own, custom events like `"hello"` we should use `new CustomEvent`. Technically [CustomEvent](https://dom.spec.whatwg.org/#customevent) is the same as `Event`, with one exception. +For our own, completely new events types like `"hello"` we should use `new CustomEvent`. Technically [CustomEvent](https://dom.spec.whatwg.org/#customevent) is the same as `Event`, with one exception. In the second argument (object) we can add an additional property `detail` for any custom information that we want to pass with the event. @@ -154,25 +154,33 @@ For instance: The `detail` property can have any data. Technically we could live without, because we can assign any properties into a regular `new Event` object after its creation. But `CustomEvent` provides the special `detail` field for it to evade conflicts with other event properties. -The event class tells something about "what kind of event" it is, and if the event is custom, then we should use `CustomEvent` just to be clear about what it is. +Besides, the event class describes "what kind of event" it is, and if the event is custom, then we should use `CustomEvent` just to be clear about what it is. ## event.preventDefault() -We can call `event.preventDefault()` on a script-generated event if `cancelable:true` flag is specified. +Many browser events have a "default action", such as nagivating to a link, starting a selection, and so on. +<<<<<<< HEAD Of course, if the event has a non-standard name, then it's not known to the browser, and there's no "default browser action" for it. But the event-generating code may plan some actions after `dispatchEvent`. The call of `event.preventDefault()` is a way for the handler to send a signal that those actions shouldn't be performed. +======= +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. -In that case the call to `elem.dispatchEvent(event)` returns `false`. And the event-generating code knows that the processing shouldn't continue. +By calling `event.preventDefault()`, an event handler may send a signal that those actions should be canceled. +>>>>>>> 852ee189170d9022f67ab6d387aeae76810b5923 -For instance, in the example below there's a `hide()` function. It generates the `"hide"` event on the element `#rabbit`, notifying all interested parties that the rabbit is going to hide. +In that case the call to `elem.dispatchEvent(event)` returns `false`. And the code that dispatched it knows that it shouldn't continue. -A handler set by `rabbit.addEventListener('hide',...)` will learn about that and, if it wants, can prevent that action by calling `event.preventDefault()`. Then the rabbit won't hide: +Let's see a practical example - a hiding rabbit (could be a closing menu or something else). -```html run refresh +Below you can see a `#rabbit` and `hide()` function that dispatches `"hide"` event on it, to let all interested parties know that the rabbit is going to hide. + +Any handler can listen to that event with `rabbit.addEventListener('hide',...)` and, if needed, cancel the action using `event.preventDefault()`. Then the rabbit won't disappear: + +```html run refresh autorun
   |\   /|
    \|_|/
@@ -180,6 +188,7 @@ A handler set by `rabbit.addEventListener('hide',...)` will learn about that and
   =\_Y_/=
    {>o<}
 
+ ``` +Обратите внимание: событие должно иметь флаг `cancelable: true`, иначе вызов `event.preventDefault()` будет проигнорирован. ## Events-in-events are synchronous @@ -217,11 +223,10 @@ Then the control jumps to the nested event handler, and after it goes back. For instance, here the nested `menu-open` event is processed synchronously, during the `onclick`: -```html run +```html run autorun ``` -Please note that the nested event `menu-open` bubbles up and is handled on the `document`. The propagation of the nested event is fully finished before the processing gets back to the outer code (`onclick`). +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`). 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. +<<<<<<< HEAD If we don't like it, we can either put the `dispatchEvent` (or other event-triggering call) at the end of `onclick` or, if inconvenient, wrap it in `setTimeout(...,0)`: +======= +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`: +>>>>>>> 852ee189170d9022f67ab6d387aeae76810b5923 ```html run ``` +<<<<<<< HEAD +======= +Now `dispatchEvent` runs asynchronously after the current code execution is finished, including `mouse.onclick`, so event handlers are totally separate. + +The output order becomes: 1 -> 2 -> nested. + +>>>>>>> 852ee189170d9022f67ab6d387aeae76810b5923 ## Summary To generate an event, we first need to create an event object. The generic `Event(name, options)` constructor accepts an arbitrary event name and the `options` object with two properties: - - `bubbles: true` if the event should bubble. - - `cancelable: true` if the `event.preventDefault()` should work. +- `bubbles: true` if the event should bubble. +- `cancelable: true` if the `event.preventDefault()` should work. Other constructors of native events like `MouseEvent`, `KeyboardEvent` and so on accept properties specific to that event type. For instance, `clientX` for mouse events. diff --git a/2-ui/3-event-details/1-mouse-events-basics/01-selectable-list/task.md b/2-ui/3-event-details/1-mouse-events-basics/01-selectable-list/task.md index f358616ef..8d29134ff 100644 --- a/2-ui/3-event-details/1-mouse-events-basics/01-selectable-list/task.md +++ b/2-ui/3-event-details/1-mouse-events-basics/01-selectable-list/task.md @@ -14,4 +14,5 @@ The demo: [iframe border="1" src="solution" height=180] P.S. For this task we can assume that list items are text-only. No nested tags. + P.P.S. Prevent the native browser selection of the text on clicks. diff --git a/2-ui/3-event-details/1-mouse-events-basics/article.md b/2-ui/3-event-details/1-mouse-events-basics/article.md index dce1ad3de..14233fcca 100644 --- a/2-ui/3-event-details/1-mouse-events-basics/article.md +++ b/2-ui/3-event-details/1-mouse-events-basics/article.md @@ -1,9 +1,9 @@ # Mouse events basics -Mouse events come not only from "mouse manipulators", but are also emulated on touch devices, to make them compatible. - In this chapter we'll get into more details about mouse events and their properties. +Please note: such events may come not only from "mouse devices", but are also from other devices, such as phones and tablets, where they are emulated for compatibility. + ## Mouse event types We can split mouse events into two categories: "simple" and "complex" @@ -42,7 +42,7 @@ An action may trigger multiple events. For instance, a 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`. Events are handled in the same sequence: `onmouseup` finishes before `onclick` runs. +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`. ```online Click the button below and you'll see the events. Try double-click too. @@ -74,12 +74,14 @@ The middle button is somewhat exotic right now and is very rarely used. All mouse events include the information about pressed modifier keys. -The properties are: +Event properties: + +- `shiftKey`: `key:Shift` +- `altKey`: `key:Alt` (or `key:Opt` for Mac) +- `ctrlKey`: `key:Ctrl` +- `metaKey`: `key:Cmd` for Mac -- `shiftKey` -- `altKey` -- `ctrlKey` -- `metaKey` (`key:Cmd` for Mac) +They are `true` if the corresponding key was pressed during the event. For instance, the button below only works on `key:Alt+Shift`+click: @@ -98,15 +100,17 @@ For instance, the button below only works on `key:Alt+Shift`+click: ``` ```warn header="Attention: on Mac it's usually `Cmd` instead of `Ctrl`" -On Windows and Linux there are modifier keys `key:Alt`, `key:Shift` and `key:Ctrl`. On Mac there's one more: `key:Cmd`, it corresponds to the property `metaKey`. +On Windows and Linux there are modifier keys `key:Alt`, `key:Shift` and `key:Ctrl`. On Mac there's one more: `key:Cmd`, corresponding to the property `metaKey`. -In most cases when Windows/Linux uses `key:Ctrl`, on Mac people use `key:Cmd`. So where a Windows user presses `key:Ctrl+Enter` or `key:Ctrl+A`, a Mac user would press `key:Cmd+Enter` or `key:Cmd+A`, and so on, most apps use `key:Cmd` instead of `key:Ctrl`. +In most applications, when Windows/Linux uses `key:Ctrl`, on Mac `key:Cmd` is used. -So if we want to support combinations like `key:Ctrl`+click, then for Mac it makes sense to use `key:Cmd`+click. That's more comfortable for Mac users. +That is: where a Windows user presses `key:Ctrl+Enter` or `key:Ctrl+A`, a Mac user would press `key:Cmd+Enter` or `key:Cmd+A`, and so on. -Even if we'd like to force Mac users to `key:Ctrl`+click -- that's kind of difficult. The problem is: a left-click with `key:Ctrl` is interpreted as a *right-click* on Mac, and it generates the `contextmenu` event, not `click` like Windows/Linux. +So if we want to support combinations like `key:Ctrl`+click, then for Mac it makes sense to use `key:Cmd`+click. That's more comfortable for Mac users. -So if we want users of all operational systems to feel comfortable, then together with `ctrlKey` we should use `metaKey`. +Even if we'd like to force Mac users to `key:Ctrl`+click -- that's kind of difficult. The problem is: a left-click with `key:Ctrl` is interpreted as a *right-click* on MacOS, and it generates the `contextmenu` event, not `click` like Windows/Linux. + +So if we want users of all operational systems to feel comfortable, then together with `ctrlKey` we should check `metaKey`. For JS-code it means that we should check `if (event.ctrlKey || event.metaKey)`. ``` @@ -126,60 +130,38 @@ All mouse events have coordinates in two flavours: 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. They are similar to `position:fixed`. ````online -Move the mouse over the input field to see `clientX/clientY` (it's in the `iframe`, so coordinates are relative to that `iframe`): +Move the mouse over the input field to see `clientX/clientY` (the example is in the `iframe`, so coordinates are relative to that `iframe`): ```html autorun height=50 ``` ```` -Document-relative coordinates are counted from the left-upper corner of the document, not the window. -Coordinates `pageX`, `pageY` are similar to `position:absolute` on the document level. +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 . -You can read more about coordinates in the chapter . +## Disabling selection +<<<<<<< HEAD ## No selection on mousedown Mouse clicks have a side-effect that may be disturbing. A double click selects the text. If we want to handle click events ourselves, then the "extra" selection doesn't look good. +======= +Double mouse click has a side-effect that may be disturbing in some interfaces: it selects the text. +>>>>>>> 852ee189170d9022f67ab6d387aeae76810b5923 For instance, a double-click on the text below selects it in addition to our handler: ```html autorun height=50 -Double-click me +Double-click me ``` -There's a CSS way to stop the selection: the `user-select` property from [CSS UI Draft](https://www.w3.org/TR/css-ui-4/). +If one presses the left mouse button and, without releasing it, moves the mouse, that also makes the selection, often unwanted. -Most browsers support it with prefixes: +There are multiple ways to prevent the selection, that you can read in the chapter . -```html autorun height=50 - - -Before... - - Unselectable - -...After -``` - -Now if you double-click on "Unselectable", it doesn't get selected. Seems to work. - -...But there is a potential problem! The text became truly unselectable. Even if a user starts the selection from "Before" and ends with "After", the selection skips "Unselectable" part. Do we really want to make our text unselectable? - -Most of time, we don't. A user may have valid reasons to select the text, for copying or other needs. That may be inconvenient if we don't allow them to do it. So this solution is not that good. - -What we want is to prevent the selection on double-click, that's it. - -A text selection is the default browser action on `mousedown` event. So the alternative solution would be to handle `mousedown` and prevent it, like this: +In this particular case the most reasonable way is to prevent the browser action on `mousedown`. It prevents both these selections: ```html autorun height=50 Before... @@ -189,6 +171,7 @@ Before... ...After ``` +<<<<<<< HEAD Now the bold element is not selected on double clicks. The text inside it is still selectable. However, the selection should start not on the text itself, but before or after it. Usually that's fine though. @@ -205,12 +188,14 @@ Before... ...After ``` +======= +Now the bold element is not selected on double clicks, and pressing the left button on it won't start the selection. +>>>>>>> 852ee189170d9022f67ab6d387aeae76810b5923 -If you double-click on the bold element, then the selection appears and then is immediately removed. That doesn't look nice though. -```` +Please note: the text inside it is still selectable. However, the selection should start not on the text itself, but before or after it. Usually that's fine for users. ````smart header="Preventing copying" -If we want to disable selection to protect our content from copy-pasting, then we can use another event: `oncopy`. +If we want to disable selection to protect our page content from copy-pasting, then we can use another event: `oncopy`. ```html autorun height=80 no-beautify
@@ -221,7 +206,7 @@ If we want to disable selection to protect our content from copy-pasting, then w ``` If you try to copy a piece of text in the `
`, that won't work, because the default action `oncopy` is prevented. -Surely that can't stop the user from opening HTML-source, but not everyone knows how to do it. +Surely the user has access to HTML-source of the page, and can take the content from there, but not everyone knows how to do it. ```` ## Summary @@ -230,14 +215,20 @@ Mouse events have the following properties: - Button: `which`. - 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 use `key:Cmd`, so it's better to check `if (e.metaKey || e.ctrlKey)`. + - 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)`. - Window-relative coordinates: `clientX/clientY`. - Document-relative coordinates: `pageX/pageY`. +<<<<<<< HEAD It's also important to deal with text selection as an unwanted side-effect of clicks. There are several ways to do this, for instance: 1. The CSS-property `user-select:none` (with browser prefixes) completely disables text-selection. 2. Cancel the selection post-factum using `getSelection().removeAllRanges()`. 3. Handle `mousedown` and prevent the default action (usually the best). +======= +The default browser action of `mousedown` is text selection, if it's not good for the interface, then it should be prevented. + +In the next chapter we'll see more details about events that follow pointer movement and how to track element changes under it. +>>>>>>> 852ee189170d9022f67ab6d387aeae76810b5923 diff --git a/2-ui/3-event-details/1-mouse-events-basics/head.html b/2-ui/3-event-details/1-mouse-events-basics/head.html index 461f0e85b..f578fb7db 100644 --- a/2-ui/3-event-details/1-mouse-events-basics/head.html +++ b/2-ui/3-event-details/1-mouse-events-basics/head.html @@ -4,25 +4,28 @@ function showmesg(t, form) { - if (timer==0) timer = new Date() + if (timer == 0) { + timer = new Date(); + } + + let tm = new Date(); - let tm = new Date() - if (tm-timer > 300) { - t = '------------------------------\n'+t + if (tm - timer > 300) { + t = '------------------------------\n' + t; } - let area = document.forms[form+'form'].getElementsByTagName('textarea')[0] + let area = document.forms[form + 'form'].getElementsByTagName('textarea')[0]; area.value += t + '\n'; - area.scrollTop = area.scrollHeight + area.scrollTop = area.scrollHeight; - timer = tm + timer = tm; } function logMouse(e) { let evt = e.type; while (evt.length < 11) evt += ' '; - showmesg(evt+" which="+e.which, 'test') + showmesg(evt + " which=" + e.which, 'test') return false; } diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/1-behavior-nested-tooltip/task.md b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/1-behavior-nested-tooltip/task.md index 435bac0f8..c77aa0728 100644 --- a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/1-behavior-nested-tooltip/task.md +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/1-behavior-nested-tooltip/task.md @@ -4,10 +4,12 @@ importance: 5 # Improved tooltip behavior -Write JavaScript that shows a tooltip over an element with the attribute `data-tooltip`. +Write JavaScript that shows a tooltip over an element with the attribute `data-tooltip`. The value of this attribute should become the tooltip text. That's like the task , but here the annotated elements can be nested. The most deeply nested tooltip is shown. +Only one tooltip may show up at the same time. + For instance: ```html @@ -21,5 +23,3 @@ For instance: The result in iframe: [iframe src="solution" height=300 border=1] - -P.S. Hint: only one tooltip may show up at the same time. diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/solution.md b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/solution.md index c4af78b11..d50625f9e 100644 --- a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/solution.md +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/solution.md @@ -3,16 +3,16 @@ The algorithm looks simple: 1. Put `onmouseover/out` handlers on the element. Also can use `onmouseenter/leave` here, but they are less universal, won't work if we introduce delegation. 2. When a mouse cursor entered the element, start measuring the speed on `mousemove`. 3. If the speed is slow, then run `over`. -4. Later if we're out of the element, and `over` was executed, run `out`. +4. When we're going out of the element, and `over` was executed, run `out`. -The question is: "How to measure the speed?" +But how to measure the speed? -The first idea would be: to run our function every `100ms` and measure the distance between previous and new coordinates. If it's small, then the speed is small. +The first idea can be: run a function every `100ms` and measure the distance between previous and new coordinates. If it's small, then the speed is small. Unfortunately, there's no way to get "current mouse coordinates" in JavaScript. There's no function like `getCurrentMouseCoordinates()`. -The only way to get coordinates is to listen to mouse events, like `mousemove`. +The only way to get coordinates is to listen to mouse events, like `mousemove`, and take coordinates from the event object. -So we can set a handler on `mousemove` to track coordinates and remember them. Then we can compare them, once per `100ms`. +So let's set a handler on `mousemove` to track coordinates and remember them. And then compare them, once per `100ms`. P.S. Please note: the solution tests use `dispatchEvent` to see if the tooltip works right. diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/task.md b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/task.md index 3dfb7e9e1..e0ac375fc 100644 --- a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/task.md +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/task.md @@ -4,16 +4,21 @@ importance: 5 # "Smart" tooltip -Write a function that shows a tooltip over an element only if the visitor moves the mouse *over it*, but not *through it*. +Write a function that shows a tooltip over an element only if the visitor moves the mouse *to it*, but not *through it*. -In other words, if the visitor moves the mouse on the element and stopped -- show the tooltip. And if they just moved the mouse through fast, then no need, who wants extra blinking? +In other words, if the visitor moves the mouse to the element and stops there -- show the tooltip. And if they just moved the mouse through, then no need, who wants extra blinking? Technically, we can measure the mouse speed over the element, and if it's slow then we assume that it comes "over the element" and show the tooltip, if it's fast -- then we ignore it. -Make a universal object `new HoverIntent(options)` for it. With `options`: +Make a universal object `new HoverIntent(options)` for it. +Its `options`: - `elem` -- element to track. +<<<<<<< HEAD - `over` -- a function to call if the mouse is slowly moving the element. +======= +- `over` -- a function to call if the mouse came to the element: that is, it moves slowly or stopped over it. +>>>>>>> 852ee189170d9022f67ab6d387aeae76810b5923 - `out` -- a function to call when the mouse leaves the element (if `over` was called). An example of using such object for the tooltip: diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/article.md b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/article.md index ac20f84fa..3b12dbf26 100644 --- a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/article.md +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/article.md @@ -1,14 +1,18 @@ -# Moving: mouseover/out, mouseenter/leave +# Moving the mouse: mouseover/out, mouseenter/leave -Let's dive into more details about events that happen when mouse moves between elements. +Let's dive into more details about events that happen when the mouse moves between elements. -## Mouseover/mouseout, relatedTarget +## Events mouseover/mouseout, relatedTarget The `mouseover` event occurs when a mouse pointer comes over an element, and `mouseout` -- when it leaves. ![](mouseover-mouseout.svg) +<<<<<<< HEAD These events are special, because they have a `relatedTarget`. +======= +These events are special, because they have property `relatedTarget`. This property complements `target`. When a mouse leaves one element for another, one of them becomes `target`, and the other one - `relatedTarget`. +>>>>>>> 852ee189170d9022f67ab6d387aeae76810b5923 For `mouseover`: @@ -17,13 +21,18 @@ For `mouseover`: For `mouseout` the reverse: +<<<<<<< HEAD - `event.target` -- is the element that mouse left. - `event.relatedTarget` -- is the new under-the-pointer element (that mouse left for). +======= +- `event.target` -- is the element that the mouse left. +- `event.relatedTarget` -- is the new under-the-pointer element, that mouse left for (`target` -> `relatedTarget`). +>>>>>>> 852ee189170d9022f67ab6d387aeae76810b5923 ```online -In the example below each face feature is an element. When you move the mouse, you can see mouse events in the text area. +In the example below each face and its features are separate elements. When you move the mouse, you can see mouse events in the text area. -Each event has the information about where the element came and where it came from. +Each event has the information about both `target` and `relatedTarget`: [codetabs src="mouseoverout" height=280] ``` @@ -36,86 +45,111 @@ That's normal and just means that the mouse came not from another element, but f We should keep that possibility in mind when using `event.relatedTarget` in our code. If we access `event.relatedTarget.tagName`, then there will be an error. ``` -## Events frequency +## Skipping elements The `mousemove` event triggers when the mouse moves. But that doesn't mean that every pixel leads to an event. The browser checks the mouse position from time to time. And if it notices changes then triggers the events. -That means that if the visitor is moving the mouse very fast then DOM-elements may be skipped: +That means that if the visitor is moving the mouse very fast then some DOM-elements may be skipped: ![](mouseover-mouseout-over-elems.svg) If the mouse moves very fast from `#FROM` to `#TO` elements as painted above, then intermediate `
` (or some of them) may be skipped. The `mouseout` event may trigger on `#FROM` and then immediately `mouseover` on `#TO`. -In practice that's helpful, because if there may be many intermediate elements. We don't really want to process in and out of each one. +That's good for performance, because if there may be many intermediate elements. We don't really want to process in and out of each one. -On the other hand, we should keep in mind that we can't assume that the mouse slowly moves from one event to another. No, it can "jump". +On the other hand, we should keep in mind that the mouse pointer doesn't "visit" all elements along the way. It can "jump". -In particular it's possible that the cursor jumps right inside the middle of the page from out of the window. And `relatedTarget=null`, because it came from "nowhere": +In particular, it's possible that the pointer jumps right inside the middle of the page from out of the window. In that case `relatedTarget` is `null`, because it came from "nowhere": ![](mouseover-mouseout-from-outside.svg) -
-In case of a fast move, intermediate elements may trigger no events. But if the mouse enters the element (`mouseover`), when we're guaranteed to have `mouseout` when it leaves it. -
- ```online -Check it out "live" on a teststand below. +You can check it out "live" on a teststand below. -The HTML is two nested `
` elements. If you move the mouse fast over them, then there may be no events at all, or maybe only the red div triggers events, or maybe the green one. +Its HTML has two nested elements: the `
` is inside the `
`. If you move the mouse fast over them, then maybe only the child div triggers events, or maybe the parent one, or maybe there will be no events at all. -Also try to move the pointer over the red `div`, and then move it out quickly down through the green one. If the movement is fast enough then the parent element is ignored. +Also move the pointer into the child `div`, and then move it out quickly down through the parent one. If the movement is fast enough, then the parent element is ignored. The mouse will cross the parent element without noticing it. [codetabs height=360 src="mouseoverout-fast"] ``` -## "Extra" mouseout when leaving for a child +```smart header="If `mouseover` triggered, there must be `mouseout`" +In case of fast mouse movements, intermediate elements may be ignores, but one thing we know for sure: elements can be only skipped as a whole. + +If the pointer "officially" entered an element with `mouseover`, then upon leaving it we always get `mouseout`. +``` + +## Mouseout when leaving for a child -Imagine -- a mouse pointer entered an element. The `mouseover` triggered. Then the cursor goes into a child element. The interesting fact is that `mouseout` triggers in that case. The cursor is still in the element, but we have a `mouseout` from it! +An important feature of `mouseout` -- it triggers, when the pointer moves from an element to its descendant. + +Visually, the pointer is still on the element, but we get `mouseout`! ![](mouseover-to-child.svg) -That seems strange, but can be easily explained. +That looks strange, but can be easily explained. + +**According to the browser logic, the mouse cursor may be only over a *single* element at any time -- the most nested one and top by z-index.** -**According to the browser logic, the mouse cursor may be only over a *single* element at any time -- the most nested one (and top by z-index).** +So if it goes to another element (even a descendant), then it leaves the previous one. -So if it goes to another element (even a descendant), then it leaves the previous one. That simple. +Please note an important detail. -There's a funny consequence that we can see on the example below. +The `mouseover` event on a descendant bubbles up. So, if the parent element has such handler, it triggers. -The red `
` is nested inside the blue one. The blue `
` has `mouseover/out` handlers that log all events in the textarea below. +![](mouseover-bubble-nested.svg) -Try entering the blue element and then moving the mouse on the red one -- and watch the events: +```online +You can see that very well in the example below: `
` is inside the `
`. There are handlers on the parent that listen for `mouseover/out` events and output their details. + +If you move the mouse from the parent to the child, you see two events: `mouseout [target: parent]` (left the parent) and `mouseover [target: child]` (came to the child, bubbled). [codetabs height=360 src="mouseoverout-child"] +``` -1. On entering the blue one -- we get `mouseover [target: blue]`. -2. Then after moving from the blue to the red one -- we get `mouseout [target: blue]` (left the parent). -3. ...And immediately `mouseover [target: red]`. +When we move from a parent element to a child, then two handlers trigger on the parent element: `mouseout` and `mouseover`: -So, for a handler that does not take `target` into account, it looks like we left the parent in `mouseout` in `(2)` and returned back to it by `mouseover` in `(3)`. +```js +parent.onmouseout = function(event) { + /* event.target: parent element */ +}; +parent.onmouseover = function(event) { + /* event.target: child element (bubbled) */ +}; +``` -If we perform some actions on entering/leaving the element, then we'll get a lot of extra "false" runs. For simple stuff that may be unnoticeable. For complex things that may bring unwanted side-effects. +If the code inside the handlers doesn't look at `target`, then it might think that the mouse left the `parent` element, and then came back over it. But it's not the case! The mouse never left, it just moved to the child element. -We can fix it by using `mouseenter/mouseleave` events instead. +If there's some action upon leaving the element, e.g. animation runs, then such interpretation may bring unwanted side effects. + +To avoid it, we can check `relatedTarget` 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. ## Events mouseenter and mouseleave -Events `mouseenter/mouseleave` are like `mouseover/mouseout`. They also trigger when the mouse pointer enters/leaves the element. +Events `mouseenter/mouseleave` are like `mouseover/mouseout`. They trigger when the mouse pointer enters/leaves the element. -But there are two differences: +But there are two important differences: -1. Transitions inside the element are not counted. +1. Transitions inside the element, to/from descendants, are not counted. 2. Events `mouseenter/mouseleave` do not bubble. -These events are intuitively very clear. +These events are extremely simple. + +When the pointer enters an element -- `mouseenter` triggers. The exact location of the pointer inside the element or its descendants doesn't matter. -When the pointer enters an element -- the `mouseenter` triggers, and then doesn't matter where it goes while inside the element. The `mouseleave` event only triggers when the cursor leaves it. +When the pointer leaves an element -- `mouseleave` triggers. -If we make the same example, but put `mouseenter/mouseleave` on the blue `
`, and do the same -- we can see that events trigger only on entering and leaving the blue `
`. No extra events when going to the red one and back. Children are ignored. +```online +This example is similar to the one above, but now the top element has `mouseenter/mouseleave` instead of `mouseover/mouseout`. + +As you can see, the only generated events are the ones related to moving the pointer in and out of the top element. Nothing happens when the pointer goes to the child and back. Transitions between descendants are ignores [codetabs height=340 src="mouseleave"] +``` ## Event delegation @@ -123,16 +157,16 @@ Events `mouseenter/leave` are very simple and easy to use. But they do not bubbl Imagine we want to handle mouse enter/leave for table cells. And there are hundreds of cells. -The natural solution would be -- to set the handler on `` and process events there. But `mouseenter/leave` don't bubble. So if such event happens on `
`, then only a handler on that `` can catch it. +The natural solution would be -- to set the handler on `` and process events there. But `mouseenter/leave` don't bubble. So if such event happens on `
`, then only a handler on that `` is able to catch it. -Handlers for `mouseenter/leave` on `` only trigger on entering/leaving the whole table. It's impossible to get any information about transitions inside it. +Handlers for `mouseenter/leave` on `
` only trigger when the pointer enters/leaves the table as a whole. It's impossible to get any information about transitions inside it. -Not a problem -- let's use `mouseover/mouseout`. +So, let's use `mouseover/mouseout`. -A simple handler may look like this: +Let's start with simple handlers that highlight the element under mouse: ```js -// let's highlight cells under mouse +// let's highlight an element under the pointer table.onmouseover = function(event) { let target = event.target; target.style.background = 'pink'; @@ -145,41 +179,40 @@ table.onmouseout = function(event) { ``` ```online +Here they are in action. As the mouse travels across the elements of this table, the current one is highlighted: + [codetabs height=480 src="mouseenter-mouseleave-delegation"] ``` -These handlers work when going from any element to any inside the table. - -But we'd like to handle only transitions in and out of ` to another if (!currentElem) return; - // we're leaving the element -- where to? Maybe to a child element? + // we're leaving the element – where to? Maybe to a descendant? let relatedTarget = event.relatedTarget; - if (relatedTarget) { // possible: relatedTarget = null - while (relatedTarget) { - // go up the parent chain and check -- if we're still inside currentElem - // then that's an internal transition -- ignore it - if (relatedTarget == currentElem) return; - relatedTarget = relatedTarget.parentNode; - } + + while (relatedTarget) { + // go up the parent chain and check – if we're still inside currentElem + // then that's an internal transition – ignore it + if (relatedTarget == currentElem) return; + + relatedTarget = relatedTarget.parentNode; } - // we left the element. really. + // we left the
` as a whole. And highlight the cells as a whole. We don't want to handle transitions that happen between the children of ``. +In our case we'd like to handle transitions between table cells ``: entering a cell and leaving it. Other transitions, such as inside the cell or outside of any cells, don't interest us. Let's filter them out. -One of solutions: +Here's what we can do: -- Remember the currently highlighted `` in a variable. +- Remember the currently highlighted `` in a variable, let's call it `currentElem`. - On `mouseover` -- ignore the event if we're still inside the current ``. - On `mouseout` -- ignore if we didn't leave the current ``. -That filters out "extra" events when we are moving between the children of ``. +Here's an example of code that accounts for all possible situations: -```offline -The details are in the [full example](sandbox:mouseenter-mouseleave-delegation-2). -``` +[js src="mouseenter-mouseleave-delegation-2/script.js"] ```online Here's the full example with all details: [codetabs height=380 src="mouseenter-mouseleave-delegation-2"] -Try to move the cursor in and out of table cells and inside them. Fast or slow -- doesn't matter. Only `` as a whole is highlighted unlike the example before. +Try to move the cursor in and out of table cells and inside them. Fast or slow -- doesn't matter. Only `` as a whole is highlighted, unlike the example before. ``` - ## Summary We covered events `mouseover`, `mouseout`, `mousemove`, `mouseenter` and `mouseleave`. -Things that are good to note: +These things are good to note: + +- A fast mouse move may skip intermediate elements. +- Events `mouseover/out` and `mouseenter/leave` have an additional property: `relatedTarget`. That's the element that we are coming from/to, complementary to `target`. + +Events `mouseover/out` trigger even when we go from the parent element to a child element. The browser assumes that the mouse can be only over one element at one time -- the deepest one. -- A fast mouse move can make `mouseover, mousemove, mouseout` to skip intermediate elements. -- Events `mouseover/out` and `mouseenter/leave` have an additional target: `relatedTarget`. That's the element that we are coming from/to, complementary to `target`. -- Events `mouseover/out` trigger even when we go from the parent element to a child element. They assume that the mouse can be only over one element at one time -- the deepest one. -- Events `mouseenter/leave` do not bubble and do not trigger when the mouse goes to a child element. They only track whether the mouse comes inside and outside the element as a whole. +Events `mouseenter/leave` are different in that aspect: they only trigger when the mouse comes in and out the element as a whole. Also they do not bubble. diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation-2.view/script.js b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation-2.view/script.js index 9f6bf1b5b..27ae27b94 100755 --- a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation-2.view/script.js +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation-2.view/script.js @@ -2,16 +2,21 @@ let currentElem = null; table.onmouseover = function(event) { - if (currentElem) { - // before entering a new element, the mouse always leaves the previous one - // if we didn't leave yet, then we're still inside it, so can ignore the event - return; - } + // before entering a new element, the mouse always leaves the previous one + // if currentElem is set, we didn't leave the previous , + // that's a mouseover inside it, ignore the event + if (currentElem) return; let target = event.target.closest('td'); - if (!target || !table.contains(target)) return; - // yeah we're inside now + // we moved not into a - ignore + if (!target) return; + + // moved into , but outside of our table (possible in case of nested tables) + // ignore + if (!table.contains(target)) return; + + // hooray! we entered a new currentElem = target; target.style.background = 'pink'; }; @@ -19,20 +24,22 @@ table.onmouseover = function(event) { table.onmouseout = function(event) { // if we're outside of any now, then ignore the event + // that's probably a move inside the table, but out of , + // e.g. from
. really. currentElem.style.background = ''; currentElem = null; }; diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation-2.view/style.css b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation-2.view/style.css index 61e19e363..a5b1fd889 100755 --- a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation-2.view/style.css +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation-2.view/style.css @@ -16,6 +16,7 @@ vertical-align: bottom; padding-top: 5px; padding-bottom: 12px; + cursor: pointer; } #table .nw { @@ -62,4 +63,4 @@ #table .highlight { background: red; -} \ No newline at end of file +} diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation.view/style.css b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation.view/style.css index 61e19e363..a5b1fd889 100755 --- a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation.view/style.css +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation.view/style.css @@ -16,6 +16,7 @@ vertical-align: bottom; padding-top: 5px; padding-bottom: 12px; + cursor: pointer; } #table .nw { @@ -62,4 +63,4 @@ #table .highlight { background: red; -} \ No newline at end of file +} diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave-table.view/style.css b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave-table.view/style.css index 61e19e363..a5b1fd889 100755 --- a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave-table.view/style.css +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave-table.view/style.css @@ -16,6 +16,7 @@ vertical-align: bottom; padding-top: 5px; padding-bottom: 12px; + cursor: pointer; } #table .nw { @@ -62,4 +63,4 @@ #table .highlight { background: red; -} \ No newline at end of file +} diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave.view/index.html b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave.view/index.html index 87fc5c635..f26fdbdae 100755 --- a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave.view/index.html +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave.view/index.html @@ -1,15 +1,15 @@ - + - + -
-
+
parent +
child
diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave.view/script.js b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave.view/script.js index 7b0b70b19..bf64f2de7 100755 --- a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave.view/script.js +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave.view/script.js @@ -1,4 +1,5 @@ -function log(event) { - text.value += event.type + ' [target: ' + event.target.id + ']\n'; +function mouselog(event) { + let d = new Date(); + text.value += `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()} | ${event.type} [target: ${event.target.id}]\n`.replace(/(:|^)(\d\D)/, '$10$2'); text.scrollTop = text.scrollHeight; -} \ No newline at end of file +} diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave.view/style.css b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave.view/style.css index d4a759838..0d809f4e8 100755 --- a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave.view/style.css +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave.view/style.css @@ -1,21 +1,22 @@ -#blue { - background: blue; +#parent { + background: #99C0C3; width: 160px; - height: 160px; + height: 120px; position: relative; } -#red { - background: red; - width: 70px; - height: 70px; +#child { + background: #CFCE95; + width: 50%; + height: 50%; position: absolute; - left: 45px; - top: 45px; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); } -#text { +textarea { + height: 140px; + width: 300px; display: block; - height: 100px; - width: 400px; -} \ No newline at end of file +} diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-bubble-nested.svg b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-bubble-nested.svg new file mode 100644 index 000000000..f97e86b00 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-bubble-nested.svg @@ -0,0 +1 @@ +mouseoutmouseover#parent#child \ No newline at end of file diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-to-child.svg b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-to-child.svg index 860c6bfba..e36b81ca7 100644 --- a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-to-child.svg +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-to-child.svg @@ -1,3 +1,4 @@ +<<<<<<< HEAD @@ -22,4 +23,7 @@ - \ No newline at end of file + +======= +mouseoutmouseover#parent#child +>>>>>>> 852ee189170d9022f67ab6d387aeae76810b5923 diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-child.view/index.html b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-child.view/index.html index 20c96c0a7..b110a199d 100755 --- a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-child.view/index.html +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-child.view/index.html @@ -8,8 +8,8 @@ -
-
+
parent +
child
diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-child.view/script.js b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-child.view/script.js index 98a098150..bf64f2de7 100755 --- a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-child.view/script.js +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-child.view/script.js @@ -1,4 +1,5 @@ function mouselog(event) { - text.value += event.type + ' [target: ' + event.target.className + ']\n' - text.scrollTop = text.scrollHeight -} \ No newline at end of file + let d = new Date(); + text.value += `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()} | ${event.type} [target: ${event.target.id}]\n`.replace(/(:|^)(\d\D)/, '$10$2'); + text.scrollTop = text.scrollHeight; +} diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-child.view/style.css b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-child.view/style.css index 49e3f9d6d..0d809f4e8 100755 --- a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-child.view/style.css +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-child.view/style.css @@ -1,21 +1,22 @@ -.blue { - background: blue; +#parent { + background: #99C0C3; width: 160px; - height: 160px; + height: 120px; position: relative; } -.red { - background: red; - width: 100px; - height: 100px; +#child { + background: #CFCE95; + width: 50%; + height: 50%; position: absolute; - left: 30px; - top: 30px; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); } textarea { - height: 100px; - width: 400px; + height: 140px; + width: 300px; display: block; -} \ No newline at end of file +} diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-fast.view/index.html b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-fast.view/index.html index 1e38d693c..6124dd44a 100755 --- a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-fast.view/index.html +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-fast.view/index.html @@ -8,13 +8,11 @@ -
-
Test
+
parent +
child
- - - + diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-fast.view/script.js b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-fast.view/script.js index 63ec8cdcb..ff5041b95 100755 --- a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-fast.view/script.js +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-fast.view/script.js @@ -1,47 +1,47 @@ - green.onmouseover = green.onmouseout = green.onmousemove = handler; +parent.onmouseover = parent.onmouseout = parent.onmousemove = handler; - function handler(event) { - let type = event.type; - while (type < 11) type += ' '; +function handler(event) { + let type = event.type; + while (type < 11) type += ' '; - log(type + " target=" + event.target.id) - return false; - } + log(type + " target=" + event.target.id) + return false; +} - function clearText() { - text.value = ""; - lastMessage = ""; - } +function clearText() { + text.value = ""; + lastMessage = ""; +} - let lastMessageTime = 0; - let lastMessage = ""; - let repeatCounter = 1; +let lastMessageTime = 0; +let lastMessage = ""; +let repeatCounter = 1; - function log(message) { - if (lastMessageTime == 0) lastMessageTime = new Date(); +function log(message) { + if (lastMessageTime == 0) lastMessageTime = new Date(); - let time = new Date(); + let time = new Date(); - if (time - lastMessageTime > 500) { - message = '------------------------------\n' + message; - } + if (time - lastMessageTime > 500) { + message = '------------------------------\n' + message; + } - if (message === lastMessage) { - repeatCounter++; - if (repeatCounter == 2) { - text.value = text.value.trim() + ' x 2\n'; - } else { - text.value = text.value.slice(0, text.value.lastIndexOf('x') + 1) + repeatCounter + "\n"; - } + if (message === lastMessage) { + repeatCounter++; + if (repeatCounter == 2) { + text.value = text.value.trim() + ' x 2\n'; + } else { + text.value = text.value.slice(0, text.value.lastIndexOf('x') + 1) + repeatCounter + "\n"; + } - } else { - repeatCounter = 1; - text.value += message + "\n"; - } + } else { + repeatCounter = 1; + text.value += message + "\n"; + } - text.scrollTop = text.scrollHeight; + text.scrollTop = text.scrollHeight; - lastMessageTime = time; - lastMessage = message; - } \ No newline at end of file + lastMessageTime = time; + lastMessage = message; +} diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-fast.view/style.css b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-fast.view/style.css index e6ae1a2b0..0d809f4e8 100755 --- a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-fast.view/style.css +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-fast.view/style.css @@ -1,23 +1,22 @@ -#green { - height: 50px; +#parent { + background: #99C0C3; width: 160px; - background: green; + height: 120px; + position: relative; } -#red { - height: 20px; - width: 110px; - background: red; - color: white; - font-weight: bold; - padding: 5px; - text-align: center; - margin: 20px; +#child { + background: #CFCE95; + width: 50%; + height: 50%; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); } -#text { - font-size: 12px; - height: 200px; - width: 360px; +textarea { + height: 140px; + width: 300px; display: block; -} \ No newline at end of file +} diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout.view/script.js b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout.view/script.js index 8fa60cc2d..073ee7792 100755 --- a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout.view/script.js +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout.view/script.js @@ -7,9 +7,9 @@ function handler(event) { return el.className || el.tagName; } - log.value += event.type + ': ' + + log.value += event.type + ': ' + 'target=' + str(event.target) + - ', relatedTarget=' + str(event.relatedTarget) + "\n"; + ', relatedTarget=' + str(event.relatedTarget) + "\n"; log.scrollTop = log.scrollHeight; if (event.type == 'mouseover') { diff --git a/2-ui/3-event-details/4-mouse-drag-and-drop/1-slider/solution.md b/2-ui/3-event-details/4-mouse-drag-and-drop/1-slider/solution.md index 28e8cffa9..6d8878d4a 100644 --- a/2-ui/3-event-details/4-mouse-drag-and-drop/1-slider/solution.md +++ b/2-ui/3-event-details/4-mouse-drag-and-drop/1-slider/solution.md @@ -1,4 +1,5 @@ +As we can see from HTML/CSS, the slider is a `
` with a colored background, that contains a runner -- another `
` with `position:relative`. -We have a horizontal Drag'n'Drop here. +To position the runner we use `position:relative`, to provide the coordinates relative to its parent, here it's more convenient here than `position:absolute`. -To position the element we use `position:relative` and slider-relative coordinates for the thumb. Here it's more convenient here than `position:absolute`. +Then we implement horizontal-only Drag'n'Drop with limitation by width. diff --git a/2-ui/3-event-details/4-mouse-drag-and-drop/2-drag-heroes/solution.md b/2-ui/3-event-details/4-mouse-drag-and-drop/2-drag-heroes/solution.md index deb43b655..62cbdb9c5 100644 --- a/2-ui/3-event-details/4-mouse-drag-and-drop/2-drag-heroes/solution.md +++ b/2-ui/3-event-details/4-mouse-drag-and-drop/2-drag-heroes/solution.md @@ -1,5 +1,5 @@ -To drag the element we can use `position:fixed`, it makes coordinates easier to manage. At the end we should switch it back to `position:absolute`. +To drag the element we can use `position:fixed`, it makes coordinates easier to manage. At the end we should switch it back to `position:absolute` to lay the element into the document. -Then, when coordinates are at window top/bottom, we use `window.scrollTo` to scroll it. +When coordinates are at window top/bottom, we use `window.scrollTo` to scroll it. More details in the code, in comments. diff --git a/2-ui/3-event-details/4-mouse-drag-and-drop/2-drag-heroes/task.md b/2-ui/3-event-details/4-mouse-drag-and-drop/2-drag-heroes/task.md index 16add0cf8..91fbaa0f2 100644 --- a/2-ui/3-event-details/4-mouse-drag-and-drop/2-drag-heroes/task.md +++ b/2-ui/3-event-details/4-mouse-drag-and-drop/2-drag-heroes/task.md @@ -12,8 +12,8 @@ Requirements: - Use event delegation to track drag start: a single event handler on `document` for `mousedown`. - If elements are dragged to top/bottom window edges -- the page scrolls up/down to allow further dragging. -- There is no horizontal scroll. -- Draggable elements should never leave the window, even after swift mouse moves. +- There is no horizontal scroll (this makes the task a bit simpler, adding it is easy). +- Draggable elements or their parts should never leave the window, even after swift mouse moves. The demo is too big to fit it here, so here's the link. diff --git a/2-ui/3-event-details/4-mouse-drag-and-drop/article.md b/2-ui/3-event-details/4-mouse-drag-and-drop/article.md index 6f9238143..154375852 100644 --- a/2-ui/3-event-details/4-mouse-drag-and-drop/article.md +++ b/2-ui/3-event-details/4-mouse-drag-and-drop/article.md @@ -1,25 +1,28 @@ # Drag'n'Drop with mouse events -Drag'n'Drop is a great interface solution. Taking something, dragging and dropping is a clear and simple way to do many things, from copying and moving (see file managers) to ordering (drop into cart). +Drag'n'Drop is a great interface solution. Taking something, dragging and dropping is a clear and simple way to do many things, from copying and moving documents (as in file managers) to ordering (drop into cart). In the modern HTML standard there's a [section about Drag Events](https://html.spec.whatwg.org/multipage/interaction.html#dnd). They are interesting, because they allow to solve simple tasks easily, and also allow to handle 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. Then JavaScript gains access to its contents. +<<<<<<< HEAD But native Drag Events also have limitations. For instance, we can limit dragging by a certain area. Also we can't make it "horizontal" or "vertical" only. There are other drag'n'drop tasks that can't be implemented using that API. +======= +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. There are other drag'n'drop tasks that can't be done using that API. +>>>>>>> 852ee189170d9022f67ab6d387aeae76810b5923 -So here we'll see how to implement Drag'n'Drop using mouse events. Not that hard either. +Here we'll see how to implement Drag'n'Drop using mouse events. ## Drag'n'Drop algorithm The basic Drag'n'Drop algorithm looks like this: -1. Catch `mousedown` on a draggable element. -2. Prepare the element for moving (maybe create a copy of it or whatever). -3. Then on `mousemove` move it by changing `left/top` and `position:absolute`. -4. On `mouseup` (button release) -- perform all actions related to a finished Drag'n'Drop. +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. -These are the basics. We can extend it, for instance, by highlighting droppable (available for the drop) elements when hovering over them. +These are the basics. Later we can extend it, for instance, by highlighting droppable (available for the drop) elements when hovering over them. Here's the algorithm for drag'n'drop of a ball: @@ -32,7 +35,7 @@ ball.onmousedown = function(event) { // (1) start the process // 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 cursor + // ...and put that absolutely positioned ball under the pointer moveAt(event.pageX, event.pageY); @@ -65,7 +68,7 @@ Here's an example in action: [iframe src="ball" height=230] -Try to drag'n'drop the mouse and you'll see the strange behavior. +Try to drag'n'drop 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. @@ -88,7 +91,7 @@ In action: Another important aspect -- we track `mousemove` on `document`, not on `ball`. From the first sight it may seem that the mouse is always over the ball, and we can put `mousemove` on it. -But as we remember, `mousemove` triggers often, but not for every pixel. So after swift move the cursor can jump from the ball somewhere in the middle of document (or even outside of the window). +But as we remember, `mousemove` triggers often, but not for every pixel. So after swift move the pointer can jump from the ball somewhere in the middle of document (or even outside of the window). So we should listen on `document` to catch it. @@ -101,15 +104,21 @@ ball.style.left = pageX - ball.offsetWidth / 2 + 'px'; ball.style.top = pageY - ball.offsetHeight / 2 + 'px'; ``` +<<<<<<< HEAD Not bad, but there's a side-effect. To initiate the drag'n'drop can we `mousedown` anywhere on the ball. If do it at the edge, then the ball suddenly "jumps" to become centered. +======= +Not bad, but there's a side-effect. To initiate the drag'n'drop, we can `mousedown` anywhere on the ball. But if "take" it from its edge, then the ball suddenly "jumps" to become centered under the mouse pointer. +>>>>>>> 852ee189170d9022f67ab6d387aeae76810b5923 It would be better if we keep the initial shift of the element relative to the pointer. -For instance, if we start dragging by the edge of the ball, then the cursor should remain over the edge while dragging. +For instance, if we start dragging by the edge of the ball, then the pointer should remain over the edge while dragging. ![](ball_shift.svg) -1. When a visitor presses the button (`mousedown`) -- we can remember the distance from the cursor to the left-upper corner of the ball in variables `shiftX/shiftY`. We should keep that distance while dragging. +Let's update our algorithm: + +1. When a visitor presses the button (`mousedown`) - remember the distance from the pointer to the left-upper corner of the ball in variables `shiftX/shiftY`. We'll keep that distance while dragging. To get these shifts we can substract the coordinates: @@ -125,7 +134,7 @@ For instance, if we start dragging by the edge of the ball, then the cursor shou ```js // onmousemove - // ball has position:absoute + // у мяча ball стоит position:absoute ball.style.left = event.pageX - *!*shiftX*/!* + 'px'; ball.style.top = event.pageY - *!*shiftY*/!* + 'px'; ``` @@ -178,12 +187,13 @@ In action (inside `