Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit cebefb4

Browse files
authoredMay 4, 2024
Merge pull request #186 from javascript-tutorial/merge-upstream
Merge upstream
2 parents e597b6c + 237a30d commit cebefb4

File tree

37 files changed

+1939
-169
lines changed

37 files changed

+1939
-169
lines changed
 

‎1-js/02-first-steps/04-variables/article.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ let имя = '...';
205205
let 我 = '...';
206206
```
207207
208-
Նման անունները վավեր են, սակայն ամենուր ընդունված է օգտագործել անգլերենը: Նույնիսկ եթե փոքր սկրիպտ եք գրում, այն հնարավոր է կարդան նաև այլազգի ծրագրավորողները:
208+
Այստեղ սխալ չկա։ Նման անունները վավեր են, սակայն ամենուր ընդունված է օգտագործել անգլերենը: Նույնիսկ եթե փոքր սկրիպտ եք գրում, այն հնարավոր է կարդան նաև այլազգի ծրագրավորողները:
209209
````
210210

211211
````warn header="Վերապահված անուններ (reserved names)"
@@ -264,7 +264,7 @@ myBirthday = '01.01.2001'; // error, can't reassign the constant!
264264
265265
### Մեծատառ հաստատուններ
266266
267-
Ընդունված է օգտագործել հաստատունները դժվար հիշվող արժեքների համար, որոնք հայտնի են նախօրոք:
267+
Ընդունված է հաստատունները օգտագործել դժվար հիշվող արժեքների համար, որոնք հայտնի են նախօրոք:
268268
269269
Նման հաստատունները գրվում են մեծատառ և ընդգծիկներով (underscores):
270270
@@ -289,15 +289,15 @@ alert(color); // #FF7F00
289289
290290
Եկեք հասկանանք երբ է պետք հաստատունները գրել մեծատառերով, իսկ երբ ոչ:
291291
292-
«Հաստատունն» ուղղակի փոփոխական է, որի արժեքը չի փոխվում: Այունամենայնիվ կան հաստատուններ, որոնց արժեքը հայտնի է մինչև սկրիպը գործարկելը (ինչպես, օրինակ, վերոնշյալ գույները) և կան հաստատուններ, որոնց արժեքը հայտնի է դառնում միայն սկրիպտի աշխատանքի ընթացքում և այն չի փոխվում սկզբնական վերագրումից հետո:
292+
«Հաստատունն» ուղղակի փոփոխական է, որի արժեքը չի փոխվում: Կան հաստատուններ, որոնց արժեքը հայտնի է մինչև սկրիպտը գործարկելը (ինչպես, օրինակ, վերոնշյալ գույները) և կան հաստատուններ, որոնց արժեքը հայտնի է դառնում միայն սկրիպտի աշխատանքի ընթացքում և այն չի փոխվում սկզբնական վերագրումից հետո:
293293
294294
Օրինակ՝
295295
296296
```js
297297
const pageLoadTime = /* time taken by a webpage to load */;
298298
```
299299
300-
`pageLoadTime`-ի արժեքը հայտնի չէ, քանի դեռ էջն ամբողջությամբ չի բեռնվել, սակայն այն հաստատուն է, քանի որ արժեքը չի փոխվում:
300+
`pageLoadTime`-ի արժեքը հայտնի չէ, քանի դեռ էջն ամբողջությամբ չի բեռնվել, այդ իսկ պատճառով այն մեծատառ չէ։ Սակայն այն հաստատուն է, քանի որ վերագրելուց հետո արժեքը չի փոխվում:
301301
302302
Այլ կերպ ասած՝ մեծատառերով գրված հաստատունները օգտագործվում են միայն որպես «կոշտ կոդավորված» արժեքների փոխանուններ:
303303

‎1-js/03-code-quality/06-polyfills/article.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Teams behind JavaScript engines have their own ideas about what to implement fir
77

88
So it's quite common for an engine to implement only part of the standard.
99

10-
A good page to see the current state of support for language features is <https://kangax.github.io/compat-table/es6/> (it's big, we have a lot to study yet).
10+
A good page to see the current state of support for language features is <https://compat-table.github.io/compat-table/es6/> (it's big, we have a lot to study yet).
1111

1212
As programmers, we'd like to use most recent features. The more good stuff - the better!
1313

@@ -85,7 +85,7 @@ Just don't forget to use a transpiler (if using modern syntax or operators) and
8585
For example, later when you're familiar with JavaScript, you can setup a code build system based on [webpack](https://webpack.js.org/) with the [babel-loader](https://github.com/babel/babel-loader) plugin.
8686
8787
Good resources that show the current state of support for various features:
88-
- <https://kangax.github.io/compat-table/es6/> - for pure JavaScript.
88+
- <https://compat-table.github.io/compat-table/es6/> - for pure JavaScript.
8989
- <https://caniuse.com/> - for browser-related functions.
9090
9191
P.S. Google Chrome is usually the most up-to-date with language features, try it if a tutorial demo fails. Most tutorial demos work with any modern browser though.

‎1-js/05-data-types/03-string/3-truncate/task.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ The result of the function should be the truncated (if needed) string.
1111
For instance:
1212

1313
```js
14-
truncate("What I'd like to tell on this topic is:", 20) = "What I'd like to te…"
14+
truncate("What I'd like to tell on this topic is:", 20) == "What I'd like to te…"
1515

16-
truncate("Hi everyone!", 20) = "Hi everyone!"
16+
truncate("Hi everyone!", 20) == "Hi everyone!"
1717
```

‎1-js/05-data-types/10-destructuring-assignment/article.md‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@ The two most used data structures in JavaScript are `Object` and `Array`.
55
- Objects allow us to create a single entity that stores data items by key.
66
- Arrays allow us to gather data items into an ordered list.
77

8-
Although, when we pass those to a function, it may need not be an object/array as a whole. It may need individual pieces.
8+
However, when we pass these to a function, we may not need all of it. The function might only require certain elements or properties.
99

1010
*Destructuring assignment* is a special syntax that allows us to "unpack" arrays or objects into a bunch of variables, as sometimes that's more convenient.
1111

12-
Destructuring also works great with complex functions that have a lot of parameters, default values, and so on. Soon we'll see that.
12+
Destructuring also works well with complex functions that have a lot of parameters, default values, and so on. Soon we'll see that.
1313

1414
## Array destructuring
1515

1616
Here's an example of how an array is destructured into variables:
1717

1818
```js
19-
// we have an array with the name and surname
19+
// we have an array with a name and surname
2020
let arr = ["John", "Smith"]
2121

2222
*!*
@@ -40,10 +40,10 @@ alert(firstName); // John
4040
alert(surname); // Smith
4141
```
4242

43-
As you can see, the syntax is simple. There are several peculiar details though. Let's see more examples, to better understand it.
43+
As you can see, the syntax is simple. There are several peculiar details though. Let's see more examples to understand it better.
4444

4545
````smart header="\"Destructuring\" does not mean \"destructive\"."
46-
It's called "destructuring assignment," because it "destructurizes" by copying items into variables. But the array itself is not modified.
46+
It's called "destructuring assignment," because it "destructurizes" by copying items into variables. However, the array itself is not modified.
4747
4848
It's just a shorter way to write:
4949
```js
@@ -65,7 +65,7 @@ let [firstName, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic
6565
alert( title ); // Consul
6666
```
6767
68-
In the code above, the second element of the array is skipped, the third one is assigned to `title`, and the rest of the array items is also skipped (as there are no variables for them).
68+
In the code above, the second element of the array is skipped, the third one is assigned to `title`, and the rest of the array items are also skipped (as there are no variables for them).
6969
````
7070

7171
````smart header="Works with any iterable on the right-side"
@@ -95,17 +95,17 @@ alert(user.surname); // Smith
9595
````
9696

9797
````smart header="Looping with .entries()"
98-
In the previous chapter we saw the [Object.entries(obj)](mdn:js/Object/entries) method.
98+
In the previous chapter, we saw the [Object.entries(obj)](mdn:js/Object/entries) method.
9999
100-
We can use it with destructuring to loop over keys-and-values of an object:
100+
We can use it with destructuring to loop over the keys-and-values of an object:
101101
102102
```js run
103103
let user = {
104104
name: "John",
105105
age: 30
106106
};
107107
108-
// loop over keys-and-values
108+
// loop over the keys-and-values
109109
*!*
110110
for (let [key, value] of Object.entries(user)) {
111111
*/!*
@@ -169,7 +169,7 @@ If we'd like also to gather all that follows -- we can add one more parameter th
169169
let [name1, name2, *!*...rest*/!*] = ["Julius", "Caesar", *!*"Consul", "of the Roman Republic"*/!*];
170170
171171
*!*
172-
// rest is array of items, starting from the 3rd one
172+
// rest is an array of items, starting from the 3rd one
173173
alert(rest[0]); // Consul
174174
alert(rest[1]); // of the Roman Republic
175175
alert(rest.length); // 2
@@ -187,7 +187,7 @@ let [name1, name2, *!*...titles*/!*] = ["Julius", "Caesar", "Consul", "of the Ro
187187

188188
### Default values
189189

190-
If the array is shorter than the list of variables at the left, there'll be no errors. Absent values are considered undefined:
190+
If the array is shorter than the list of variables on the left, there will be no errors. Absent values are considered undefined:
191191

192192
```js run
193193
*!*
@@ -418,7 +418,7 @@ alert( title ); // Menu
418418
419419
## Nested destructuring
420420
421-
If an object or an array contain other nested objects and arrays, we can use more complex left-side patterns to extract deeper portions.
421+
If an object or an array contains other nested objects and arrays, we can use more complex left-side patterns to extract deeper portions.
422422
423423
In the code below `options` has another object in the property `size` and an array in the property `items`. The pattern on the left side of the assignment has the same structure to extract values from them:
424424
@@ -461,7 +461,7 @@ Note that there are no variables for `size` and `items`, as we take their conten
461461
462462
There are times when a function has many parameters, most of which are optional. That's especially true for user interfaces. Imagine a function that creates a menu. It may have a width, a height, a title, items list and so on.
463463
464-
Here's a bad way to write such function:
464+
Here's a bad way to write such a function:
465465
466466
```js
467467
function showMenu(title = "Untitled", width = 200, height = 100, items = []) {

‎1-js/05-data-types/12-json/article.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ To decode a JSON-string, we need another method named [JSON.parse](mdn:js/JSON/p
405405

406406
The syntax:
407407
```js
408-
let value = JSON.parse(str, [reviver]);
408+
let value = JSON.parse(str[, reviver]);
409409
```
410410

411411
str

‎1-js/08-prototypes/04-prototype-methods/article.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ The only usage of `__proto__`, that's not frowned upon, is as a property when cr
1414

1515
Although, there's a special method for this too:
1616

17-
- [Object.create(proto, [descriptors])](mdn:js/Object/create) -- creates an empty object with given `proto` as `[[Prototype]]` and optional property descriptors.
17+
- [Object.create(proto[, descriptors])](mdn:js/Object/create) -- creates an empty object with given `proto` as `[[Prototype]]` and optional property descriptors.
1818

1919
For instance:
2020

@@ -201,7 +201,7 @@ alert(Object.keys(chineseDictionary)); // hello,bye
201201
- To create an object with the given prototype, use:
202202

203203
- literal syntax: `{ __proto__: ... }`, allows to specify multiple properties
204-
- or [Object.create(proto, [descriptors])](mdn:js/Object/create), allows to specify property descriptors.
204+
- or [Object.create(proto[, descriptors])](mdn:js/Object/create), allows to specify property descriptors.
205205

206206
The `Object.create` provides an easy way to shallow-copy an object with all descriptors:
207207

‎1-js/13-modules/02-import-export/article.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,9 @@ Well, there are few reasons.
9797
2. Explicit list of imports gives better overview of the code structure: what is used and where. It makes code support and refactoring easier.
9898

9999
```smart header="Don't be afraid to import too much"
100-
Modern build tools, such as [webpack](https://webpack.js.org/) and others, bundle modules together and optimize them to speedup loading. They also removed unused imports.
100+
Modern build tools, such as [webpack](https://webpack.js.org/) and others, bundle modules together and optimize them to speedup loading. They also remove unused imports.
101101
102-
For instance, if you `import * as library` from a huge code library, and then use only few methods, then unused ones [will not be included](https://github.com/webpack/webpack/tree/main/examples/harmony-unused#examplejs) into the optimzed bundle.
102+
For instance, if you `import * as library` from a huge code library, and then use only few methods, then unused ones [will not be included](https://github.com/webpack/webpack/tree/main/examples/harmony-unused#examplejs) into the optimized bundle.
103103
```
104104

105105
## Import "as"

‎1-js/99-js-misc/07-weakref-finalizationregistry/article.md‎

Lines changed: 483 additions & 0 deletions
Large diffs are not rendered by default.
55.1 KB
Loading
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
.app {
2+
display: flex;
3+
flex-direction: column;
4+
gap: 16px;
5+
}
6+
7+
.start-messages {
8+
width: fit-content;
9+
}
10+
11+
.window {
12+
width: 100%;
13+
border: 2px solid #464154;
14+
overflow: hidden;
15+
}
16+
17+
.window__header {
18+
position: sticky;
19+
padding: 8px;
20+
display: flex;
21+
justify-content: space-between;
22+
align-items: center;
23+
background-color: #736e7e;
24+
}
25+
26+
.window__title {
27+
margin: 0;
28+
font-size: 24px;
29+
font-weight: 700;
30+
color: white;
31+
letter-spacing: 1px;
32+
}
33+
34+
.window__button {
35+
padding: 4px;
36+
background: #4f495c;
37+
outline: none;
38+
border: 2px solid #464154;
39+
color: white;
40+
font-size: 16px;
41+
cursor: pointer;
42+
}
43+
44+
.window__body {
45+
height: 250px;
46+
padding: 16px;
47+
overflow: scroll;
48+
background-color: #736e7e33;
49+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<!DOCTYPE HTML>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="utf-8">
6+
<link rel="stylesheet" href="index.css">
7+
<title>WeakRef DOM Logger</title>
8+
</head>
9+
10+
<body>
11+
12+
<div class="app">
13+
<button class="start-messages">Start sending messages</button>
14+
<div class="window">
15+
<div class="window__header">
16+
<p class="window__title">Messages:</p>
17+
<button class="window__button">Close</button>
18+
</div>
19+
<div class="window__body">
20+
No messages.
21+
</div>
22+
</div>
23+
</div>
24+
25+
26+
<script type="module" src="index.js"></script>
27+
</body>
28+
</html>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const startMessagesBtn = document.querySelector('.start-messages'); // (1)
2+
const closeWindowBtn = document.querySelector('.window__button'); // (2)
3+
const windowElementRef = new WeakRef(document.querySelector(".window__body")); // (3)
4+
5+
startMessagesBtn.addEventListener('click', () => { // (4)
6+
startMessages(windowElementRef);
7+
startMessagesBtn.disabled = true;
8+
});
9+
10+
closeWindowBtn.addEventListener('click', () => document.querySelector(".window__body").remove()); // (5)
11+
12+
13+
const startMessages = (element) => {
14+
const timerId = setInterval(() => { // (6)
15+
if (element.deref()) { // (7)
16+
const payload = document.createElement("p");
17+
payload.textContent = `Message: System status OK: ${new Date().toLocaleTimeString()}`;
18+
element.deref().append(payload);
19+
} else { // (8)
20+
alert("The element has been deleted."); // (9)
21+
clearInterval(timerId);
22+
}
23+
}, 1000);
24+
};
Lines changed: 32 additions & 0 deletions
Loading
Lines changed: 33 additions & 0 deletions
Loading

‎1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry-03.svg‎

Lines changed: 75 additions & 0 deletions
Loading

‎1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry-04.svg‎

Lines changed: 77 additions & 0 deletions
Loading

‎1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry-05.svg‎

Lines changed: 103 additions & 0 deletions
Loading
1.18 MB
Loading
816 KB
Loading
1.03 MB
Loading
300 KB
Loading
1.48 MB
Loading
338 KB
Loading
1.48 MB
Loading
334 KB
Loading
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
:root {
2+
--mineralGreen: 60, 98, 85;
3+
--viridianGreen: 97, 135, 110;
4+
--swampGreen: 166, 187, 141;
5+
--fallGreen: 234, 231, 177;
6+
--brinkPink: #FA7070;
7+
--silverChalice: 178, 178, 178;
8+
--white: 255, 255, 255;
9+
--black: 0, 0, 0;
10+
11+
--topBarHeight: 64px;
12+
--itemPadding: 32px;
13+
--containerGap: 8px;
14+
}
15+
16+
@keyframes zoom-in {
17+
0% {
18+
transform: scale(1, 1);
19+
}
20+
21+
100% {
22+
transform: scale(1.30, 1.30);
23+
}
24+
}
25+
26+
body, html {
27+
margin: 0;
28+
padding: 0;
29+
}
30+
31+
.app {
32+
min-height: 100vh;
33+
background-color: rgba(var(--viridianGreen), 0.5);
34+
}
35+
36+
.header {
37+
height: var(--topBarHeight);
38+
padding: 0 24px;
39+
display: flex;
40+
justify-content: space-between;
41+
align-items: center;
42+
background-color: rgba(var(--mineralGreen), 1);
43+
}
44+
45+
.header-text {
46+
color: white;
47+
}
48+
49+
.container {
50+
display: flex;
51+
gap: 24px;
52+
padding: var(--itemPadding);
53+
}
54+
55+
.item {
56+
width: 50%;
57+
}
58+
59+
.item--scrollable {
60+
overflow-y: scroll;
61+
height: calc(100vh - var(--topBarHeight) - (var(--itemPadding) * 2));
62+
}
63+
64+
.thumbnails-container {
65+
display: flex;
66+
flex-wrap: wrap;
67+
gap: 8px;
68+
justify-content: center;
69+
align-items: center;
70+
}
71+
72+
.thumbnail-item {
73+
width: calc(25% - var(--containerGap));
74+
cursor: pointer;
75+
position: relative;
76+
}
77+
78+
.thumbnail-item:hover {
79+
z-index: 1;
80+
animation: zoom-in 0.1s forwards;
81+
}
82+
83+
.thumbnail-item--selected {
84+
outline: 3px solid rgba(var(--fallGreen), 1);
85+
outline-offset: -3px;
86+
}
87+
88+
.badge {
89+
width: 16px;
90+
height: 16px;
91+
display: flex;
92+
justify-content: center;
93+
align-items: center;
94+
padding: 4px;
95+
position: absolute;
96+
right: 8px;
97+
bottom: 8px;
98+
border-radius: 50%;
99+
border: 2px solid rgba(var(--fallGreen), 1);
100+
background-color: rgba(var(--swampGreen), 1);
101+
}
102+
103+
.check {
104+
display: inline-block;
105+
transform: rotate(45deg);
106+
border-bottom: 2px solid white;
107+
border-right: 2px solid white;
108+
width: 6px;
109+
height: 12px;
110+
}
111+
112+
.img {
113+
width: 100%;
114+
height: 100%;
115+
object-fit: cover;
116+
}
117+
118+
.actions {
119+
display: flex;
120+
flex-wrap: wrap;
121+
justify-content: center;
122+
align-content: center;
123+
padding: 0 0 16px 0;
124+
gap: 8px;
125+
}
126+
127+
.select {
128+
padding: 16px;
129+
cursor: pointer;
130+
font-weight: 700;
131+
color: rgba(var(--black), 1);
132+
border: 2px solid rgba(var(--swampGreen), 0.5);
133+
background-color: rgba(var(--swampGreen), 1);
134+
}
135+
136+
.select:disabled {
137+
cursor: not-allowed;
138+
background-color: rgba(var(--silverChalice), 1);
139+
color: rgba(var(--black), 0.5);
140+
border: 2px solid rgba(var(--black), 0.25);
141+
}
142+
143+
.btn {
144+
outline: none;
145+
padding: 16px;
146+
cursor: pointer;
147+
font-weight: 700;
148+
color: rgba(var(--black), 1);
149+
border: 2px solid rgba(var(--black), 0.5);
150+
}
151+
152+
.btn--primary {
153+
background-color: rgba(var(--mineralGreen), 1);
154+
}
155+
156+
.btn--primary:hover:not([disabled]) {
157+
background-color: rgba(var(--mineralGreen), 0.85);
158+
}
159+
160+
.btn--secondary {
161+
background-color: rgba(var(--viridianGreen), 0.5);
162+
}
163+
164+
.btn--secondary:hover:not([disabled]) {
165+
background-color: rgba(var(--swampGreen), 0.25);
166+
}
167+
168+
.btn--success {
169+
background-color: rgba(var(--fallGreen), 1);
170+
}
171+
172+
.btn--success:hover:not([disabled]) {
173+
background-color: rgba(var(--fallGreen), 0.85);
174+
}
175+
176+
.btn:disabled {
177+
cursor: not-allowed;
178+
background-color: rgba(var(--silverChalice), 1);
179+
color: rgba(var(--black), 0.5);
180+
border: 2px solid rgba(var(--black), 0.25);
181+
}
182+
183+
.previewContainer {
184+
margin-bottom: 16px;
185+
display: flex;
186+
width: 100%;
187+
height: 40vh;
188+
overflow: scroll;
189+
border: 3px solid rgba(var(--black), 1);
190+
}
191+
192+
.previewContainer--disabled {
193+
background-color: rgba(var(--black), 0.1);
194+
cursor: not-allowed;
195+
}
196+
197+
.canvas {
198+
margin: auto;
199+
display: none;
200+
}
201+
202+
.canvas--ready {
203+
display: block;
204+
}
205+
206+
.spinnerContainer {
207+
display: flex;
208+
gap: 8px;
209+
flex-direction: column;
210+
align-content: center;
211+
align-items: center;
212+
margin: auto;
213+
}
214+
215+
.spinnerContainer--hidden {
216+
display: none;
217+
}
218+
219+
.spinnerText {
220+
margin: 0;
221+
color: rgba(var(--mineralGreen), 1);
222+
}
223+
224+
.spinner {
225+
display: inline-block;
226+
width: 50px;
227+
height: 50px;
228+
margin: auto;
229+
border: 3px solid rgba(var(--mineralGreen), 0.3);
230+
border-radius: 50%;
231+
border-top-color: rgba(var(--mineralGreen), 0.9);
232+
animation: spin 1s ease-in-out infinite;
233+
}
234+
235+
@keyframes spin {
236+
to {
237+
transform: rotate(360deg);
238+
}
239+
}
240+
241+
.loggerContainer {
242+
display: flex;
243+
flex-direction: column;
244+
gap: 8px;
245+
padding: 0 8px 8px 8px;
246+
width: 100%;
247+
min-height: 30vh;
248+
max-height: 30vh;
249+
overflow: scroll;
250+
border-left: 3px solid rgba(var(--black), 0.25);
251+
}
252+
253+
.logger-title {
254+
display: flex;
255+
align-items: center;
256+
padding: 8px;
257+
position: sticky;
258+
height: 40px;
259+
min-height: 40px;
260+
top: 0;
261+
left: 0;
262+
background-color: rgba(var(--viridianGreen), 1);
263+
font-size: 24px;
264+
font-weight: 700;
265+
margin: 0;
266+
}
267+
268+
.logger-item {
269+
font-size: 14px;
270+
padding: 8px;
271+
border: 2px solid #5a5a5a;
272+
color: white;
273+
}
274+
275+
.logger--primary {
276+
background-color: #13315a;
277+
}
278+
279+
.logger--success {
280+
background-color: #385a4e;
281+
}
282+
283+
.logger--error {
284+
background-color: #5a1a24;
285+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<!DOCTYPE HTML>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="utf-8">
6+
<link rel="stylesheet" href="index.css">
7+
<title>Photo Library Collage</title>
8+
</head>
9+
10+
<body>
11+
12+
<div class="app">
13+
<header class="header">
14+
<h1 class="header-text">
15+
Photo Library Collage
16+
</h1>
17+
</header>
18+
<div class="container">
19+
<div class="item item--scrollable">
20+
<!--Thumbnails-->
21+
<div class="thumbnails-container"></div>
22+
</div>
23+
<div class="item">
24+
<div>
25+
<div class=actions>
26+
<select class="select"></select>
27+
<button class="btn btn--primary btn-create-collage"> Create collage </button>
28+
<button class="btn btn--secondary btn-start-over"> Start over </button>
29+
<button class="btn btn--success btn-download" onClick={downloadCollage}> Download </button>
30+
</div>
31+
<div class="previewContainer">
32+
<div class="spinnerContainer spinnerContainer--hidden">
33+
<div class="spinner"></div>
34+
<p class="spinnerText"></p>
35+
</div>
36+
<canvas class="canvas"></canvas>
37+
</div>
38+
<div class="loggerContainer">
39+
<p class="logger-title">Logger:</p>
40+
</div>
41+
</div>
42+
</div>
43+
</div>
44+
</div>
45+
46+
47+
<script type="module" src="index.js"></script>
48+
</body>
49+
</html>
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import {
2+
createImageFile,
3+
loadImage,
4+
weakRefCache,
5+
LAYOUTS,
6+
images,
7+
THUMBNAIL_PARAMS,
8+
stateObj,
9+
} from "./utils.js";
10+
11+
export const state = new Proxy(stateObj, {
12+
set(target, property, value) {
13+
const previousValue = target[property];
14+
15+
target[property] = value;
16+
17+
if (previousValue !== value) {
18+
handleStateChange(target);
19+
}
20+
21+
return true;
22+
},
23+
});
24+
25+
// Elements.
26+
const thumbnailsContainerEl = document.querySelector(".thumbnails-container");
27+
const selectEl = document.querySelector(".select");
28+
const previewContainerEl = document.querySelector(".previewContainer");
29+
const canvasEl = document.querySelector(".canvas");
30+
const createCollageBtn = document.querySelector(".btn-create-collage");
31+
const startOverBtn = document.querySelector(".btn-start-over");
32+
const downloadBtn = document.querySelector(".btn-download");
33+
const spinnerContainerEl = document.querySelector(".spinnerContainer");
34+
const spinnerTextEl = document.querySelector(".spinnerText");
35+
const loggerContainerEl = document.querySelector(".loggerContainer");
36+
37+
// Renders.
38+
// Render thumbnails previews.
39+
images.forEach((img) => {
40+
const thumbnail = document.createElement("div");
41+
thumbnail.classList.add("thumbnail-item");
42+
43+
thumbnail.innerHTML = `
44+
<img src='${img.img}?${THUMBNAIL_PARAMS}' class="img">
45+
`;
46+
47+
thumbnail.addEventListener("click", (e) => handleSelection(e, img));
48+
49+
thumbnailsContainerEl.appendChild(thumbnail);
50+
});
51+
// Render layouts select.
52+
LAYOUTS.forEach((layout) => {
53+
const option = document.createElement("option");
54+
option.value = JSON.stringify(layout);
55+
option.innerHTML = layout.name;
56+
selectEl.appendChild(option);
57+
});
58+
59+
const handleStateChange = (state) => {
60+
if (state.loading) {
61+
selectEl.disabled = true;
62+
createCollageBtn.disabled = true;
63+
startOverBtn.disabled = true;
64+
downloadBtn.disabled = true;
65+
previewContainerEl.classList.add("previewContainer--disabled");
66+
spinnerContainerEl.classList.remove("spinnerContainer--hidden");
67+
spinnerTextEl.innerText = "Loading...";
68+
canvasEl.classList.remove("canvas--ready");
69+
} else if (!state.loading) {
70+
selectEl.disabled = false;
71+
createCollageBtn.disabled = false;
72+
startOverBtn.disabled = false;
73+
downloadBtn.disabled = false;
74+
previewContainerEl.classList.remove("previewContainer--disabled");
75+
spinnerContainerEl.classList.add("spinnerContainer--hidden");
76+
canvasEl.classList.add("canvas--ready");
77+
}
78+
79+
if (!state.selectedImages.size) {
80+
createCollageBtn.disabled = true;
81+
document.querySelectorAll(".badge").forEach((item) => item.remove());
82+
} else if (state.selectedImages.size && !state.loading) {
83+
createCollageBtn.disabled = false;
84+
}
85+
86+
if (!state.collageRendered) {
87+
downloadBtn.disabled = true;
88+
} else if (state.collageRendered) {
89+
downloadBtn.disabled = false;
90+
}
91+
};
92+
handleStateChange(state);
93+
94+
const handleSelection = (e, imgName) => {
95+
const imgEl = e.currentTarget;
96+
97+
imgEl.classList.toggle("thumbnail-item--selected");
98+
99+
if (state.selectedImages.has(imgName)) {
100+
state.selectedImages.delete(imgName);
101+
state.selectedImages = new Set(state.selectedImages);
102+
imgEl.querySelector(".badge")?.remove();
103+
} else {
104+
state.selectedImages = new Set(state.selectedImages.add(imgName));
105+
106+
const badge = document.createElement("div");
107+
badge.classList.add("badge");
108+
badge.innerHTML = `
109+
<div class="check" />
110+
`;
111+
imgEl.prepend(badge);
112+
}
113+
};
114+
115+
// Make a wrapper function.
116+
let getCachedImage;
117+
(async () => {
118+
getCachedImage = await weakRefCache(loadImage);
119+
})();
120+
121+
const calculateGridRows = (blobsLength) =>
122+
Math.ceil(blobsLength / state.currentLayout.columns);
123+
124+
const drawCollage = (images) => {
125+
state.drawing = true;
126+
127+
let context = canvasEl.getContext("2d");
128+
129+
/**
130+
* Calculate canvas dimensions based on the current layout.
131+
* */
132+
context.canvas.width =
133+
state.currentLayout.itemWidth * state.currentLayout.columns;
134+
context.canvas.height =
135+
calculateGridRows(images.length) * state.currentLayout.itemHeight;
136+
137+
let currentRow = 0;
138+
let currentCanvasDx = 0;
139+
let currentCanvasDy = 0;
140+
141+
for (let i = 0; i < images.length; i++) {
142+
/**
143+
* Get current row of the collage.
144+
* */
145+
if (i % state.currentLayout.columns === 0) {
146+
currentRow += 1;
147+
currentCanvasDx = 0;
148+
149+
if (currentRow > 1) {
150+
currentCanvasDy += state.currentLayout.itemHeight;
151+
}
152+
}
153+
154+
context.drawImage(
155+
images[i],
156+
0,
157+
0,
158+
images[i].width,
159+
images[i].height,
160+
currentCanvasDx,
161+
currentCanvasDy,
162+
state.currentLayout.itemWidth,
163+
state.currentLayout.itemHeight,
164+
);
165+
166+
currentCanvasDx += state.currentLayout.itemWidth;
167+
}
168+
169+
state.drawing = false;
170+
state.collageRendered = true;
171+
};
172+
173+
const createCollage = async () => {
174+
state.loading = true;
175+
176+
const images = [];
177+
178+
for (const image of state.selectedImages.values()) {
179+
const blobImage = await getCachedImage(image.img);
180+
181+
const url = URL.createObjectURL(blobImage);
182+
const img = await createImageFile(url);
183+
184+
images.push(img);
185+
URL.revokeObjectURL(url);
186+
}
187+
188+
state.loading = false;
189+
190+
drawCollage(images);
191+
};
192+
193+
/**
194+
* Clear all settled data to start over.
195+
* */
196+
const startOver = () => {
197+
state.selectedImages = new Set();
198+
state.collageRendered = false;
199+
const context = canvasEl.getContext("2d");
200+
context.clearRect(0, 0, canvasEl.width, canvasEl.height);
201+
202+
document
203+
.querySelectorAll(".thumbnail-item--selected")
204+
.forEach((item) => item.classList.remove("thumbnail-item--selected"));
205+
206+
loggerContainerEl.innerHTML = '<p class="logger-title">Logger:</p>';
207+
};
208+
209+
const downloadCollage = () => {
210+
const date = new Date();
211+
const fileName = `Collage-${date.getDay()}-${date.getMonth()}-${date.getFullYear()}.png`;
212+
const img = canvasEl.toDataURL("image/png");
213+
const link = document.createElement("a");
214+
link.download = fileName;
215+
link.href = img;
216+
link.click();
217+
link.remove();
218+
};
219+
220+
const changeLayout = ({ target }) => {
221+
state.currentLayout = JSON.parse(target.value);
222+
};
223+
224+
// Listeners.
225+
selectEl.addEventListener("change", changeLayout);
226+
createCollageBtn.addEventListener("click", createCollage);
227+
startOverBtn.addEventListener("click", startOver);
228+
downloadBtn.addEventListener("click", downloadCollage);
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
const loggerContainerEl = document.querySelector(".loggerContainer");
2+
3+
export const images = [
4+
{
5+
img: "https://images.unsplash.com/photo-1471357674240-e1a485acb3e1",
6+
},
7+
{
8+
img: "https://images.unsplash.com/photo-1589118949245-7d38baf380d6",
9+
},
10+
{
11+
img: "https://images.unsplash.com/photo-1527631746610-bca00a040d60",
12+
},
13+
{
14+
img: "https://images.unsplash.com/photo-1500835556837-99ac94a94552",
15+
},
16+
{
17+
img: "https://images.unsplash.com/photo-1503220317375-aaad61436b1b",
18+
},
19+
{
20+
img: "https://images.unsplash.com/photo-1501785888041-af3ef285b470",
21+
},
22+
{
23+
img: "https://images.unsplash.com/photo-1528543606781-2f6e6857f318",
24+
},
25+
{
26+
img: "https://images.unsplash.com/photo-1523906834658-6e24ef2386f9",
27+
},
28+
{
29+
img: "https://images.unsplash.com/photo-1539635278303-d4002c07eae3",
30+
},
31+
{
32+
img: "https://images.unsplash.com/photo-1533105079780-92b9be482077",
33+
},
34+
{
35+
img: "https://images.unsplash.com/photo-1516483638261-f4dbaf036963",
36+
},
37+
{
38+
img: "https://images.unsplash.com/photo-1502791451862-7bd8c1df43a7",
39+
},
40+
{
41+
img: "https://plus.unsplash.com/premium_photo-1663047367140-91adf819d007",
42+
},
43+
{
44+
img: "https://images.unsplash.com/photo-1506197603052-3cc9c3a201bd",
45+
},
46+
{
47+
img: "https://images.unsplash.com/photo-1517760444937-f6397edcbbcd",
48+
},
49+
{
50+
img: "https://images.unsplash.com/photo-1518684079-3c830dcef090",
51+
},
52+
{
53+
img: "https://images.unsplash.com/photo-1505832018823-50331d70d237",
54+
},
55+
{
56+
img: "https://images.unsplash.com/photo-1524850011238-e3d235c7d4c9",
57+
},
58+
{
59+
img: "https://plus.unsplash.com/premium_photo-1661277758451-b5053309eea1",
60+
},
61+
{
62+
img: "https://images.unsplash.com/photo-1541410965313-d53b3c16ef17",
63+
},
64+
{
65+
img: "https://images.unsplash.com/photo-1528702748617-c64d49f918af",
66+
},
67+
{
68+
img: "https://images.unsplash.com/photo-1502003148287-a82ef80a6abc",
69+
},
70+
{
71+
img: "https://plus.unsplash.com/premium_photo-1661281272544-5204ea3a481a",
72+
},
73+
{
74+
img: "https://images.unsplash.com/photo-1503457574462-bd27054394c1",
75+
},
76+
{
77+
img: "https://images.unsplash.com/photo-1499363536502-87642509e31b",
78+
},
79+
{
80+
img: "https://images.unsplash.com/photo-1551918120-9739cb430c6d",
81+
},
82+
{
83+
img: "https://plus.unsplash.com/premium_photo-1661382219642-43e54f7e81d7",
84+
},
85+
{
86+
img: "https://images.unsplash.com/photo-1497262693247-aa258f96c4f5",
87+
},
88+
{
89+
img: "https://images.unsplash.com/photo-1525254134158-4fd5fdd45793",
90+
},
91+
{
92+
img: "https://plus.unsplash.com/premium_photo-1661274025419-4c54107d5c48",
93+
},
94+
{
95+
img: "https://images.unsplash.com/photo-1553697388-94e804e2f0f6",
96+
},
97+
{
98+
img: "https://images.unsplash.com/photo-1574260031597-bcd9eb192b4f",
99+
},
100+
{
101+
img: "https://images.unsplash.com/photo-1536323760109-ca8c07450053",
102+
},
103+
{
104+
img: "https://images.unsplash.com/photo-1527824404775-dce343118ebc",
105+
},
106+
{
107+
img: "https://images.unsplash.com/photo-1612278675615-7b093b07772d",
108+
},
109+
{
110+
img: "https://images.unsplash.com/photo-1522010675502-c7b3888985f6",
111+
},
112+
{
113+
img: "https://images.unsplash.com/photo-1501555088652-021faa106b9b",
114+
},
115+
{
116+
img: "https://plus.unsplash.com/premium_photo-1669223469435-27e091439169",
117+
},
118+
{
119+
img: "https://images.unsplash.com/photo-1506012787146-f92b2d7d6d96",
120+
},
121+
{
122+
img: "https://images.unsplash.com/photo-1511739001486-6bfe10ce785f",
123+
},
124+
{
125+
img: "https://images.unsplash.com/photo-1553342385-111fd6bc6ab3",
126+
},
127+
{
128+
img: "https://images.unsplash.com/photo-1516546453174-5e1098a4b4af",
129+
},
130+
{
131+
img: "https://images.unsplash.com/photo-1527142879-95b61a0b8226",
132+
},
133+
{
134+
img: "https://images.unsplash.com/photo-1520466809213-7b9a56adcd45",
135+
},
136+
{
137+
img: "https://images.unsplash.com/photo-1516939884455-1445c8652f83",
138+
},
139+
{
140+
img: "https://images.unsplash.com/photo-1545389336-cf090694435e",
141+
},
142+
{
143+
img: "https://plus.unsplash.com/premium_photo-1669223469455-b7b734c838f4",
144+
},
145+
{
146+
img: "https://images.unsplash.com/photo-1454391304352-2bf4678b1a7a",
147+
},
148+
{
149+
img: "https://images.unsplash.com/photo-1433838552652-f9a46b332c40",
150+
},
151+
{
152+
img: "https://images.unsplash.com/photo-1506125840744-167167210587",
153+
},
154+
{
155+
img: "https://images.unsplash.com/photo-1522199873717-bc67b1a5e32b",
156+
},
157+
{
158+
img: "https://images.unsplash.com/photo-1495904786722-d2b5a19a8535",
159+
},
160+
{
161+
img: "https://images.unsplash.com/photo-1614094082869-cd4e4b2905c7",
162+
},
163+
{
164+
img: "https://images.unsplash.com/photo-1474755032398-4b0ed3b2ae5c",
165+
},
166+
{
167+
img: "https://images.unsplash.com/photo-1501554728187-ce583db33af7",
168+
},
169+
{
170+
img: "https://images.unsplash.com/photo-1515859005217-8a1f08870f59",
171+
},
172+
{
173+
img: "https://images.unsplash.com/photo-1531141445733-14c2eb7d4c1f",
174+
},
175+
{
176+
img: "https://images.unsplash.com/photo-1500259783852-0ca9ce8a64dc",
177+
},
178+
{
179+
img: "https://images.unsplash.com/photo-1510662145379-13537db782dc",
180+
},
181+
{
182+
img: "https://images.unsplash.com/photo-1573790387438-4da905039392",
183+
},
184+
{
185+
img: "https://images.unsplash.com/photo-1512757776214-26d36777b513",
186+
},
187+
{
188+
img: "https://images.unsplash.com/photo-1518855706573-84de4022b69b",
189+
},
190+
{
191+
img: "https://images.unsplash.com/photo-1500049242364-5f500807cdd7",
192+
},
193+
{
194+
img: "https://images.unsplash.com/photo-1528759335187-3b683174c86a",
195+
},
196+
];
197+
export const THUMBNAIL_PARAMS = "w=240&h=240&fit=crop&auto=format";
198+
199+
// Console styles.
200+
export const CONSOLE_BASE_STYLES = [
201+
"font-size: 12px",
202+
"padding: 4px",
203+
"border: 2px solid #5a5a5a",
204+
"color: white",
205+
].join(";");
206+
export const CONSOLE_PRIMARY = [
207+
CONSOLE_BASE_STYLES,
208+
"background-color: #13315a",
209+
].join(";");
210+
export const CONSOLE_SUCCESS = [
211+
CONSOLE_BASE_STYLES,
212+
"background-color: #385a4e",
213+
].join(";");
214+
export const CONSOLE_ERROR = [
215+
CONSOLE_BASE_STYLES,
216+
"background-color: #5a1a24",
217+
].join(";");
218+
219+
// Layouts.
220+
export const LAYOUT_4_COLUMNS = {
221+
name: "Layout 4 columns",
222+
columns: 4,
223+
itemWidth: 240,
224+
itemHeight: 240,
225+
};
226+
export const LAYOUT_8_COLUMNS = {
227+
name: "Layout 8 columns",
228+
columns: 8,
229+
itemWidth: 240,
230+
itemHeight: 240,
231+
};
232+
export const LAYOUTS = [LAYOUT_4_COLUMNS, LAYOUT_8_COLUMNS];
233+
234+
export const createImageFile = async (src) =>
235+
new Promise((resolve, reject) => {
236+
const img = new Image();
237+
img.src = src;
238+
img.onload = () => resolve(img);
239+
img.onerror = () => reject(new Error("Failed to construct image."));
240+
});
241+
242+
export const loadImage = async (url) => {
243+
try {
244+
const response = await fetch(url);
245+
if (!response.ok) {
246+
throw new Error(String(response.status));
247+
}
248+
249+
return await response.blob();
250+
} catch (e) {
251+
console.log(`%cFETCHED_FAILED: ${e}`, CONSOLE_ERROR);
252+
}
253+
};
254+
255+
export const weakRefCache = (fetchImg) => {
256+
const imgCache = new Map();
257+
const registry = new FinalizationRegistry(({ imgName, size, type }) => {
258+
const cachedImg = imgCache.get(imgName);
259+
if (cachedImg && !cachedImg.deref()) {
260+
imgCache.delete(imgName);
261+
console.log(
262+
`%cCLEANED_IMAGE: Url: ${imgName}, Size: ${size}, Type: ${type}`,
263+
CONSOLE_ERROR,
264+
);
265+
266+
const logEl = document.createElement("div");
267+
logEl.classList.add("logger-item", "logger--error");
268+
logEl.innerHTML = `CLEANED_IMAGE: Url: ${imgName}, Size: ${size}, Type: ${type}`;
269+
loggerContainerEl.appendChild(logEl);
270+
loggerContainerEl.scrollTop = loggerContainerEl.scrollHeight;
271+
}
272+
});
273+
274+
return async (imgName) => {
275+
const cachedImg = imgCache.get(imgName);
276+
277+
if (cachedImg?.deref() !== undefined) {
278+
console.log(
279+
`%cCACHED_IMAGE: Url: ${imgName}, Size: ${cachedImg.size}, Type: ${cachedImg.type}`,
280+
CONSOLE_SUCCESS,
281+
);
282+
283+
const logEl = document.createElement("div");
284+
logEl.classList.add("logger-item", "logger--success");
285+
logEl.innerHTML = `CACHED_IMAGE: Url: ${imgName}, Size: ${cachedImg.size}, Type: ${cachedImg.type}`;
286+
loggerContainerEl.appendChild(logEl);
287+
loggerContainerEl.scrollTop = loggerContainerEl.scrollHeight;
288+
289+
return cachedImg?.deref();
290+
}
291+
292+
const newImg = await fetchImg(imgName);
293+
console.log(
294+
`%cFETCHED_IMAGE: Url: ${imgName}, Size: ${newImg.size}, Type: ${newImg.type}`,
295+
CONSOLE_PRIMARY,
296+
);
297+
298+
const logEl = document.createElement("div");
299+
logEl.classList.add("logger-item", "logger--primary");
300+
logEl.innerHTML = `FETCHED_IMAGE: Url: ${imgName}, Size: ${newImg.size}, Type: ${newImg.type}`;
301+
loggerContainerEl.appendChild(logEl);
302+
loggerContainerEl.scrollTop = loggerContainerEl.scrollHeight;
303+
304+
imgCache.set(imgName, new WeakRef(newImg));
305+
registry.register(newImg, {
306+
imgName,
307+
size: newImg.size,
308+
type: newImg.type,
309+
});
310+
311+
return newImg;
312+
};
313+
};
314+
315+
export const stateObj = {
316+
loading: false,
317+
drawing: true,
318+
collageRendered: false,
319+
currentLayout: LAYOUTS[0],
320+
selectedImages: new Set(),
321+
};

‎2-ui/3-event-details/6-pointer-events/article.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ Here is the flow of user actions and the corresponding events:
126126
So the issue is that the browser "hijacks" the interaction: `pointercancel` fires in the beginning of the "drag-and-drop" process, and no more `pointermove` events are generated.
127127

128128
```online
129-
Here's the drag'n'drop demo with loggin of pointer events (only `up/down`, `move` and `cancel`) in the `textarea`:
129+
Here's the drag'n'drop demo with logging of pointer events (only `up/down`, `move` and `cancel`) in the `textarea`:
130130
131131
[iframe src="ball" height=240 edit]
132132
```

‎2-ui/4-forms-controls/3-events-change-input/article.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ The clipboard is a "global" OS-level thing. A user may switch between various ap
9595

9696
So most browsers allow seamless read/write access to the clipboard only in the scope of certain user actions, such as copying/pasting etc.
9797

98-
It's forbidden to generate "custom" clipboard events with `dispatchEvent` in all browsers except Firefox. And even if we manage to dispatch such event, the specification clearly states that such "syntetic" events must not provide access to the clipboard.
98+
It's forbidden to generate "custom" clipboard events with `dispatchEvent` in all browsers except Firefox. And even if we manage to dispatch such event, the specification clearly states that such "synthetic" events must not provide access to the clipboard.
9999

100100
Even if someone decides to save `event.clipboardData` in an event handler, and then access it later -- it won't work.
101101

‎6-data-storage/01-cookie/article.md‎

Lines changed: 82 additions & 79 deletions
Large diffs are not rendered by default.

‎6-data-storage/03-indexeddb/article.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ That power is usually excessive for traditional client-server apps. IndexedDB is
1616

1717
The native interface to IndexedDB, described in the specification <https://www.w3.org/TR/IndexedDB>, is event-based.
1818

19-
We can also use `async/await` with the help of a promise-based wrapper, like <https://github.com/jakearchibald/idb>. That's pretty convenient, but the wrapper is not perfect, it can't replace events for all cases. So we'll start with events, and then, after we gain an understanding of IndexedDb, we'll use the wrapper.
19+
We can also use `async/await` with the help of a promise-based wrapper, like <https://github.com/jakearchibald/idb>. That's pretty convenient, but the wrapper is not perfect, it can't replace events for all cases. So we'll start with events, and then, after we gain an understanding of IndexedDB, we'll use the wrapper.
2020

2121
```smart header="Where's the data?"
2222
Technically, the data is usually stored in the visitor's home directory, along with browser settings, extensions, etc.

‎7-animation/2-css-animations/article.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ The `opacity` property also never triggers Layout (also skips Paint in Mozilla G
463463

464464
Paring `transform` with `opacity` can usually solve most of our needs, providing fluid, good-looking animations.
465465

466-
For example, here clicking on the `#boat` element adds the class with `transform: translateX(300)` and `opacity: 0`, thus making it move `300px` to the right and disappear:
466+
For example, here clicking on the `#boat` element adds the class with `transform: translateX(300px)` and `opacity: 0`, thus making it move `300px` to the right and disappear:
467467

468468
```html run height=260 autorun no-beautify
469469
<img src="https://js.cx/clipart/boat.png" id="boat">

‎7-animation/3-js-animation/article.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,4 +452,4 @@ Surely we could improve it, add more bells and whistles, but JavaScript animatio
452452

453453
JavaScript animations can use any timing function. We covered a lot of examples and transformations to make them even more versatile. Unlike CSS, we are not limited to Bezier curves here.
454454

455-
The same is about `draw`: we can animate anything, not just CSS properties.
455+
The same is true about `draw`: we can animate anything, not just CSS properties.

‎9-regular-expressions/11-regexp-groups/article.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,9 @@ alert(results[0]); // <h1>,h1 (1st tag)
209209
alert(results[1]); // <h2>,h2 (2nd tag)
210210
```
211211

212-
As we can see, the first difference is very important, as demonstrated in the line `(*)`. We can't get the match as `results[0]`, because that object isn't pseudoarray. We can turn it into a real `Array` using `Array.from`. There are more details about pseudoarrays and iterables in the article <info:iterable>.
212+
As we can see, the first difference is very important, as demonstrated in the line `(*)`. We can't get the match as `results[0]`, because that object is a pseudoarray. We can turn it into a real `Array` using `Array.from`. There are more details about pseudoarrays and iterables in the article <info:iterable>.
213213

214-
There's no need in `Array.from` if we're looping over results:
214+
There's no need for `Array.from` if we're looping over results:
215215

216216
```js run
217217
let results = '<h1> <h2>'.matchAll(/<(.*?)>/gi);

‎LICENSE.md‎

Lines changed: 37 additions & 57 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.