aberdeen 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -6
- package/dist/aberdeen.d.ts +3 -8
- package/dist/aberdeen.js +87 -80
- package/dist/aberdeen.js.map +4 -4
- package/dist/prediction.d.ts +2 -2
- package/dist/prediction.js +21 -22
- package/dist/prediction.js.map +3 -3
- package/dist/route.d.ts +2 -2
- package/dist/route.js +29 -15
- package/dist/route.js.map +3 -3
- package/dist/transitions.d.ts +14 -14
- package/dist/transitions.js +19 -6
- package/dist/transitions.js.map +3 -3
- package/dist-min/aberdeen.js +5 -5
- package/dist-min/aberdeen.js.map +4 -4
- package/dist-min/prediction.js +2 -2
- package/dist-min/prediction.js.map +3 -3
- package/dist-min/route.js +2 -2
- package/dist-min/route.js.map +3 -3
- package/dist-min/transitions.js +2 -2
- package/dist-min/transitions.js.map +3 -3
- package/html-to-aberdeen +354 -0
- package/package.json +3 -2
- package/src/aberdeen.ts +591 -401
- package/src/helpers/reverseSortedSet.ts +187 -178
- package/src/prediction.ts +73 -55
- package/src/route.ts +115 -97
- package/src/transitions.ts +49 -37
package/src/aberdeen.ts
CHANGED
|
@@ -1,30 +1,36 @@
|
|
|
1
1
|
import { ReverseSortedSet } from "./helpers/reverseSortedSet.js";
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
|
-
* QueueRunner
|
|
5
|
-
*
|
|
6
|
-
* `queue()`d runners are executed on the next timer tick, by order of their
|
|
7
|
-
* `prio` values.
|
|
8
|
-
*/
|
|
4
|
+
* QueueRunner
|
|
5
|
+
*
|
|
6
|
+
* `queue()`d runners are executed on the next timer tick, by order of their
|
|
7
|
+
* `prio` values.
|
|
8
|
+
*/
|
|
9
9
|
interface QueueRunner {
|
|
10
10
|
prio: number; // Higher values have higher priority
|
|
11
11
|
queueRun(): void;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
let sortedQueue: ReverseSortedSet<QueueRunner> | undefined; // When set, a runQueue is scheduled or currently running.
|
|
15
|
-
let runQueueDepth = 0 // Incremented when a queue event causes another queue event to be added. Reset when queue is empty. Throw when >= 42 to break (infinite) recursion.
|
|
16
|
-
let topRedrawScope: Scope | undefined // The scope that triggered the current redraw. Elements drawn at this scope level may trigger 'create' animations.
|
|
15
|
+
let runQueueDepth = 0; // Incremented when a queue event causes another queue event to be added. Reset when queue is empty. Throw when >= 42 to break (infinite) recursion.
|
|
16
|
+
let topRedrawScope: Scope | undefined; // The scope that triggered the current redraw. Elements drawn at this scope level may trigger 'create' animations.
|
|
17
17
|
|
|
18
18
|
/** @internal */
|
|
19
|
-
export type TargetType = any[] | {[key: string]: any};
|
|
19
|
+
export type TargetType = any[] | { [key: string]: any };
|
|
20
20
|
/** @internal */
|
|
21
|
-
export type DatumType =
|
|
21
|
+
export type DatumType =
|
|
22
|
+
| TargetType
|
|
23
|
+
| boolean
|
|
24
|
+
| number
|
|
25
|
+
| string
|
|
26
|
+
| null
|
|
27
|
+
| undefined;
|
|
22
28
|
|
|
23
29
|
function queue(runner: QueueRunner) {
|
|
24
30
|
if (!sortedQueue) {
|
|
25
|
-
sortedQueue = new ReverseSortedSet<QueueRunner>(
|
|
31
|
+
sortedQueue = new ReverseSortedSet<QueueRunner>("prio");
|
|
26
32
|
setTimeout(runQueue, 0);
|
|
27
|
-
} else if (!(runQueueDepth&1)) {
|
|
33
|
+
} else if (!(runQueueDepth & 1)) {
|
|
28
34
|
runQueueDepth++; // Make it uneven
|
|
29
35
|
if (runQueueDepth > 98) {
|
|
30
36
|
throw new Error("Too many recursive updates from observes");
|
|
@@ -50,7 +56,7 @@ function queue(runner: QueueRunner) {
|
|
|
50
56
|
* @example
|
|
51
57
|
* ```typescript
|
|
52
58
|
* const data = proxy("before");
|
|
53
|
-
*
|
|
59
|
+
*
|
|
54
60
|
* $({text: data});
|
|
55
61
|
* console.log(1, document.body.innerHTML); // before
|
|
56
62
|
*
|
|
@@ -60,57 +66,61 @@ function queue(runner: QueueRunner) {
|
|
|
60
66
|
* // Normally, the DOM update would happen after a timeout.
|
|
61
67
|
* // But this causes an immediate update:
|
|
62
68
|
* runQueue();
|
|
63
|
-
*
|
|
69
|
+
*
|
|
64
70
|
* console.log(2, document.body.innerHTML); // after
|
|
65
71
|
* ```
|
|
66
72
|
*/
|
|
67
73
|
export function runQueue(): void {
|
|
68
74
|
let time = Date.now();
|
|
69
|
-
while(sortedQueue) {
|
|
75
|
+
while (sortedQueue) {
|
|
70
76
|
const runner = sortedQueue.fetchLast();
|
|
71
77
|
if (!runner) break;
|
|
72
|
-
if (runQueueDepth&1) runQueueDepth++; // Make it even
|
|
78
|
+
if (runQueueDepth & 1) runQueueDepth++; // Make it even
|
|
73
79
|
runner.queueRun();
|
|
74
80
|
}
|
|
75
81
|
sortedQueue = undefined;
|
|
76
82
|
runQueueDepth = 0;
|
|
77
83
|
time = Date.now() - time;
|
|
78
|
-
if (time>1) console.debug(`Aberdeen queue took ${time}ms`);
|
|
84
|
+
if (time > 1) console.debug(`Aberdeen queue took ${time}ms`);
|
|
79
85
|
}
|
|
80
86
|
|
|
81
|
-
|
|
82
87
|
/**
|
|
83
|
-
* A sort key, as used by {@link onEach}, is a value that determines the order of items. It can
|
|
88
|
+
* A sort key, as used by {@link onEach}, is a value that determines the order of items. It can
|
|
84
89
|
* be a number, string, or an array of numbers/strings. The sort key is used to sort items
|
|
85
90
|
* based on their values. The sort key can also be `undefined`, which indicates that the item
|
|
86
91
|
* should be ignored.
|
|
87
92
|
* @private
|
|
88
93
|
*/
|
|
89
|
-
export type SortKeyType = number | string | Array<number|string> | undefined;
|
|
94
|
+
export type SortKeyType = number | string | Array<number | string> | undefined;
|
|
90
95
|
|
|
91
96
|
/**
|
|
92
|
-
* Given an integer number or a string, this function returns a string that can be concatenated
|
|
93
|
-
* with other strings to create a composed sort key, that follows natural number ordering.
|
|
94
|
-
*/
|
|
95
|
-
function partToStr(part: number|string): string {
|
|
96
|
-
if (typeof part ===
|
|
97
|
-
return part
|
|
98
|
-
}
|
|
99
|
-
let result =
|
|
97
|
+
* Given an integer number or a string, this function returns a string that can be concatenated
|
|
98
|
+
* with other strings to create a composed sort key, that follows natural number ordering.
|
|
99
|
+
*/
|
|
100
|
+
function partToStr(part: number | string): string {
|
|
101
|
+
if (typeof part === "string") {
|
|
102
|
+
return `${part}\x01`; // end-of-string
|
|
103
|
+
}
|
|
104
|
+
let result = "";
|
|
100
105
|
let num = Math.abs(Math.round(part));
|
|
101
106
|
const negative = part < 0;
|
|
102
|
-
while(num > 0) {
|
|
107
|
+
while (num > 0) {
|
|
103
108
|
/*
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
result += String.fromCharCode(
|
|
109
|
+
* We're reserving a few character codes:
|
|
110
|
+
* 0 - for compatibility
|
|
111
|
+
* 1 - separator between string array items
|
|
112
|
+
* 65535 - for compatibility
|
|
113
|
+
*/
|
|
114
|
+
result += String.fromCharCode(
|
|
115
|
+
negative ? 65534 - (num % 65533) : 2 + (num % 65533),
|
|
116
|
+
);
|
|
110
117
|
num = Math.floor(num / 65533);
|
|
111
118
|
}
|
|
112
119
|
// Prefix the number of digits, counting down from 128 for negative and up for positive
|
|
113
|
-
return
|
|
120
|
+
return (
|
|
121
|
+
String.fromCharCode(128 + (negative ? -result.length : result.length)) +
|
|
122
|
+
result
|
|
123
|
+
);
|
|
114
124
|
}
|
|
115
125
|
|
|
116
126
|
/**
|
|
@@ -141,14 +151,13 @@ function partToStr(part: number|string): string {
|
|
|
141
151
|
* @see {@link onEach} for usage with sorting.
|
|
142
152
|
*/
|
|
143
153
|
export function invertString(input: string): string {
|
|
144
|
-
let result =
|
|
154
|
+
let result = "";
|
|
145
155
|
for (let i = 0; i < input.length; i++) {
|
|
146
156
|
result += String.fromCodePoint(65535 - input.charCodeAt(i));
|
|
147
157
|
}
|
|
148
158
|
return result;
|
|
149
159
|
}
|
|
150
160
|
|
|
151
|
-
|
|
152
161
|
// Each new scope gets a lower prio than all scopes before it, by decrementing
|
|
153
162
|
// this counter.
|
|
154
163
|
let lastPrio = 0;
|
|
@@ -161,7 +170,7 @@ abstract class Scope implements QueueRunner {
|
|
|
161
170
|
|
|
162
171
|
abstract onChange(index: any, newData: DatumType, oldData: DatumType): void;
|
|
163
172
|
abstract queueRun(): void;
|
|
164
|
-
|
|
173
|
+
|
|
165
174
|
abstract getLastNode(): Node | undefined;
|
|
166
175
|
abstract getPrecedingNode(): Node | undefined;
|
|
167
176
|
abstract delete(): void;
|
|
@@ -187,9 +196,11 @@ abstract class Scope implements QueueRunner {
|
|
|
187
196
|
abstract class ContentScope extends Scope {
|
|
188
197
|
// The list of clean functions to be called when this scope is cleaned. These can
|
|
189
198
|
// be for child scopes, subscriptions as well as `clean(..)` hooks.
|
|
190
|
-
cleaners: Array<{delete: (scope: Scope) => void} | (() => void)>;
|
|
199
|
+
cleaners: Array<{ delete: (scope: Scope) => void } | (() => void)>;
|
|
191
200
|
|
|
192
|
-
constructor(
|
|
201
|
+
constructor(
|
|
202
|
+
cleaners: Array<{ delete: (scope: Scope) => void } | (() => void)> = [],
|
|
203
|
+
) {
|
|
193
204
|
super();
|
|
194
205
|
this.cleaners = cleaners;
|
|
195
206
|
}
|
|
@@ -197,7 +208,7 @@ abstract class ContentScope extends Scope {
|
|
|
197
208
|
lastChild: Node | Scope | undefined;
|
|
198
209
|
|
|
199
210
|
// Should be subclassed in most cases..
|
|
200
|
-
redraw() {}
|
|
211
|
+
redraw() {}
|
|
201
212
|
|
|
202
213
|
abstract parentElement: Element;
|
|
203
214
|
|
|
@@ -210,8 +221,8 @@ abstract class ContentScope extends Scope {
|
|
|
210
221
|
* It is called `delete`, so that the list of cleaners can also contain `Set`s.
|
|
211
222
|
*/
|
|
212
223
|
delete(/* ignore observer argument */) {
|
|
213
|
-
for(
|
|
214
|
-
if (typeof cleaner ===
|
|
224
|
+
for (const cleaner of this.cleaners) {
|
|
225
|
+
if (typeof cleaner === "function") cleaner();
|
|
215
226
|
else cleaner.delete(this); // pass in observer argument, in case `cleaner` is a `Set`
|
|
216
227
|
}
|
|
217
228
|
this.cleaners.length = 0;
|
|
@@ -220,13 +231,13 @@ abstract class ContentScope extends Scope {
|
|
|
220
231
|
// To prepare for a redraw or to help GC when we're being removed:
|
|
221
232
|
this.lastChild = undefined;
|
|
222
233
|
}
|
|
223
|
-
|
|
234
|
+
|
|
224
235
|
queueRun() {
|
|
225
236
|
this.remove();
|
|
226
|
-
|
|
227
|
-
topRedrawScope = this
|
|
237
|
+
|
|
238
|
+
topRedrawScope = this;
|
|
228
239
|
this.redraw();
|
|
229
|
-
topRedrawScope = undefined
|
|
240
|
+
topRedrawScope = undefined;
|
|
230
241
|
}
|
|
231
242
|
|
|
232
243
|
getInsertAfterNode() {
|
|
@@ -242,7 +253,6 @@ abstract class ContentScope extends Scope {
|
|
|
242
253
|
}
|
|
243
254
|
}
|
|
244
255
|
|
|
245
|
-
|
|
246
256
|
class ChainedScope extends ContentScope {
|
|
247
257
|
// The node or scope right before this scope that has the same `parentElement`.
|
|
248
258
|
public prevSibling: Node | Scope | undefined;
|
|
@@ -251,7 +261,7 @@ class ChainedScope extends ContentScope {
|
|
|
251
261
|
// The parent DOM element we'll add our child nodes to.
|
|
252
262
|
public parentElement: Element,
|
|
253
263
|
// When true, we share our 'cleaners' list with the parent scope.
|
|
254
|
-
useParentCleaners
|
|
264
|
+
useParentCleaners = false,
|
|
255
265
|
) {
|
|
256
266
|
super(useParentCleaners ? currentScope.cleaners : []);
|
|
257
267
|
if (parentElement === currentScope.parentElement) {
|
|
@@ -268,25 +278,25 @@ class ChainedScope extends ContentScope {
|
|
|
268
278
|
getPrecedingNode(): Node | undefined {
|
|
269
279
|
return findLastNodeInPrevSiblings(this.prevSibling);
|
|
270
280
|
}
|
|
271
|
-
|
|
281
|
+
|
|
272
282
|
getChildPrevSibling() {
|
|
273
283
|
return this.lastChild || this.prevSibling;
|
|
274
284
|
}
|
|
275
285
|
}
|
|
276
286
|
|
|
277
287
|
/**
|
|
278
|
-
* @internal
|
|
279
|
-
* A `RegularScope` is created with a `render` function that is run initially,
|
|
280
|
-
* and again when any of the `Store`s that this function reads are changed. Any
|
|
281
|
-
* DOM elements that is given a `render` function for its contents has its own scope.
|
|
282
|
-
* The `Scope` manages the position in the DOM tree elements created by `render`
|
|
283
|
-
* are inserted at. Before a rerender, all previously created elements are removed
|
|
284
|
-
* and the `clean` functions for the scope and all sub-scopes are called.
|
|
285
|
-
*/
|
|
288
|
+
* @internal
|
|
289
|
+
* A `RegularScope` is created with a `render` function that is run initially,
|
|
290
|
+
* and again when any of the `Store`s that this function reads are changed. Any
|
|
291
|
+
* DOM elements that is given a `render` function for its contents has its own scope.
|
|
292
|
+
* The `Scope` manages the position in the DOM tree elements created by `render`
|
|
293
|
+
* are inserted at. Before a rerender, all previously created elements are removed
|
|
294
|
+
* and the `clean` functions for the scope and all sub-scopes are called.
|
|
295
|
+
*/
|
|
286
296
|
class RegularScope extends ChainedScope {
|
|
287
297
|
constructor(
|
|
288
298
|
parentElement: Element,
|
|
289
|
-
// The function that will be reactively called. Elements it creates using `$` are
|
|
299
|
+
// The function that will be reactively called. Elements it creates using `$` are
|
|
290
300
|
// added to the appropriate position within `parentElement`.
|
|
291
301
|
public renderer: () => any,
|
|
292
302
|
) {
|
|
@@ -297,11 +307,11 @@ class RegularScope extends ChainedScope {
|
|
|
297
307
|
}
|
|
298
308
|
|
|
299
309
|
redraw() {
|
|
300
|
-
|
|
310
|
+
const savedScope = currentScope;
|
|
301
311
|
currentScope = this;
|
|
302
312
|
try {
|
|
303
313
|
this.renderer();
|
|
304
|
-
} catch(e) {
|
|
314
|
+
} catch (e) {
|
|
305
315
|
// Throw the error async, so the rest of the rendering can continue
|
|
306
316
|
handleError(e, true);
|
|
307
317
|
}
|
|
@@ -309,7 +319,6 @@ class RegularScope extends ChainedScope {
|
|
|
309
319
|
}
|
|
310
320
|
}
|
|
311
321
|
|
|
312
|
-
|
|
313
322
|
class RootScope extends ContentScope {
|
|
314
323
|
parentElement = document.body;
|
|
315
324
|
getPrecedingNode(): Node | undefined {
|
|
@@ -321,12 +330,12 @@ class MountScope extends ContentScope {
|
|
|
321
330
|
constructor(
|
|
322
331
|
// The parent DOM element we'll add our child nodes to
|
|
323
332
|
public parentElement: Element,
|
|
324
|
-
// The function that
|
|
333
|
+
// The function that
|
|
325
334
|
public renderer: () => any,
|
|
326
335
|
) {
|
|
327
336
|
super();
|
|
328
337
|
this.redraw();
|
|
329
|
-
currentScope.cleaners.push(this)
|
|
338
|
+
currentScope.cleaners.push(this);
|
|
330
339
|
}
|
|
331
340
|
|
|
332
341
|
redraw() {
|
|
@@ -350,16 +359,18 @@ class MountScope extends ContentScope {
|
|
|
350
359
|
}
|
|
351
360
|
}
|
|
352
361
|
|
|
353
|
-
|
|
354
362
|
// Remove node and all its preceding siblings (uptil and excluding preNode)
|
|
355
363
|
// from the DOM, using onDestroy if applicable.
|
|
356
|
-
function removeNodes(
|
|
357
|
-
|
|
364
|
+
function removeNodes(
|
|
365
|
+
node: Node | null | undefined,
|
|
366
|
+
preNode: Node | null | undefined,
|
|
367
|
+
) {
|
|
368
|
+
while (node && node !== preNode) {
|
|
358
369
|
const prevNode: Node | null = node.previousSibling;
|
|
359
|
-
|
|
370
|
+
const onDestroy = onDestroyMap.get(node);
|
|
360
371
|
if (onDestroy && node instanceof Element) {
|
|
361
372
|
if (onDestroy !== true) {
|
|
362
|
-
if (typeof onDestroy ===
|
|
373
|
+
if (typeof onDestroy === "function") {
|
|
363
374
|
onDestroy(node);
|
|
364
375
|
} else {
|
|
365
376
|
destroyWithClass(node, onDestroy);
|
|
@@ -369,7 +380,7 @@ function removeNodes(node: Node | null | undefined, preNode: Node | null | undef
|
|
|
369
380
|
}
|
|
370
381
|
// Ignore the deleting element
|
|
371
382
|
} else {
|
|
372
|
-
(node as Element|Text).remove();
|
|
383
|
+
(node as Element | Text).remove();
|
|
373
384
|
}
|
|
374
385
|
node = prevNode;
|
|
375
386
|
}
|
|
@@ -377,14 +388,15 @@ function removeNodes(node: Node | null | undefined, preNode: Node | null | undef
|
|
|
377
388
|
|
|
378
389
|
// Get a reference to the last node within `sibling` or any of its preceding siblings.
|
|
379
390
|
// If a `Node` is given, that node is returned.
|
|
380
|
-
function findLastNodeInPrevSiblings(
|
|
391
|
+
function findLastNodeInPrevSiblings(
|
|
392
|
+
sibling: Node | Scope | undefined,
|
|
393
|
+
): Node | undefined {
|
|
381
394
|
if (!sibling || sibling instanceof Node) return sibling;
|
|
382
395
|
return sibling.getLastNode() || sibling.getPrecedingNode();
|
|
383
396
|
}
|
|
384
397
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
public result: ValueRef<T> = optProxy({value: undefined});
|
|
398
|
+
class ResultScope<T> extends ChainedScope {
|
|
399
|
+
public result: ValueRef<T> = optProxy({ value: undefined });
|
|
388
400
|
|
|
389
401
|
constructor(
|
|
390
402
|
parentElement: Element,
|
|
@@ -396,11 +408,11 @@ class ResultScope<T extends DatumType | void> extends ChainedScope {
|
|
|
396
408
|
}
|
|
397
409
|
|
|
398
410
|
redraw() {
|
|
399
|
-
|
|
411
|
+
const savedScope = currentScope;
|
|
400
412
|
currentScope = this;
|
|
401
413
|
try {
|
|
402
414
|
this.result.value = this.renderer();
|
|
403
|
-
} catch(e) {
|
|
415
|
+
} catch (e) {
|
|
404
416
|
// Throw the error async, so the rest of the rendering can continue
|
|
405
417
|
handleError(e, true);
|
|
406
418
|
}
|
|
@@ -410,28 +422,27 @@ class ResultScope<T extends DatumType | void> extends ChainedScope {
|
|
|
410
422
|
|
|
411
423
|
/**
|
|
412
424
|
* A `Scope` subclass optimized for reactively setting just a single element property
|
|
413
|
-
* based on a proxied reference.
|
|
425
|
+
* based on a proxied reference.
|
|
414
426
|
*/
|
|
415
427
|
|
|
416
428
|
class SetArgScope extends ChainedScope {
|
|
417
429
|
constructor(
|
|
418
430
|
parentElement: Element,
|
|
419
431
|
public key: string,
|
|
420
|
-
public target: {value: DatumType},
|
|
432
|
+
public target: { value: DatumType },
|
|
421
433
|
) {
|
|
422
434
|
super(parentElement);
|
|
423
435
|
this.redraw();
|
|
424
436
|
}
|
|
425
437
|
redraw() {
|
|
426
|
-
|
|
438
|
+
const savedScope = currentScope;
|
|
427
439
|
currentScope = this;
|
|
428
|
-
applyArg(this.key, this.target.value)
|
|
440
|
+
applyArg(this.key, this.target.value);
|
|
429
441
|
currentScope = savedScope;
|
|
430
442
|
}
|
|
431
443
|
}
|
|
432
444
|
|
|
433
|
-
|
|
434
|
-
let immediateQueue: ReverseSortedSet<Scope> = new ReverseSortedSet('prio');
|
|
445
|
+
let immediateQueue: ReverseSortedSet<Scope> = new ReverseSortedSet("prio");
|
|
435
446
|
|
|
436
447
|
class ImmediateScope extends RegularScope {
|
|
437
448
|
onChange(index: any, newData: DatumType, oldData: DatumType) {
|
|
@@ -441,16 +452,20 @@ class ImmediateScope extends RegularScope {
|
|
|
441
452
|
|
|
442
453
|
let immediateQueueRunning = false;
|
|
443
454
|
function runImmediateQueue() {
|
|
444
|
-
for
|
|
455
|
+
for (
|
|
456
|
+
let count = 0;
|
|
457
|
+
!immediateQueue.isEmpty() && !immediateQueueRunning;
|
|
458
|
+
count++
|
|
459
|
+
) {
|
|
445
460
|
if (count > 42) {
|
|
446
461
|
immediateQueue.clear();
|
|
447
462
|
throw new Error("Too many immediate-mode recursive updates");
|
|
448
463
|
}
|
|
449
464
|
immediateQueueRunning = true;
|
|
450
|
-
|
|
451
|
-
immediateQueue = new ReverseSortedSet(
|
|
465
|
+
const copy = immediateQueue;
|
|
466
|
+
immediateQueue = new ReverseSortedSet("prio");
|
|
452
467
|
try {
|
|
453
|
-
for(const scope of copy) {
|
|
468
|
+
for (const scope of copy) {
|
|
454
469
|
// On exception, the exception will be bubbled up to the call site, discarding any
|
|
455
470
|
// remaining immediate scopes from the queue. This behavior is perhaps debatable,
|
|
456
471
|
// but getting a synchronous exception at the call site can be very helpful.
|
|
@@ -462,34 +477,37 @@ function runImmediateQueue() {
|
|
|
462
477
|
}
|
|
463
478
|
}
|
|
464
479
|
|
|
465
|
-
|
|
466
480
|
/** @internal */
|
|
467
481
|
class OnEachScope extends Scope {
|
|
482
|
+
// biome-ignore lint/correctness/noInvalidUseBeforeDeclaration: circular, as currentScope is initialized with a Scope
|
|
468
483
|
parentElement: Element = currentScope.parentElement;
|
|
469
484
|
prevSibling: Node | Scope | undefined;
|
|
470
485
|
|
|
471
486
|
/** The data structure we are iterating */
|
|
472
487
|
target: TargetType;
|
|
473
|
-
|
|
488
|
+
|
|
474
489
|
/** All item scopes, by array index or object key. This is used for removing an item scope when its value
|
|
475
490
|
* disappears, and calling all subscope cleaners. */
|
|
476
|
-
byIndex: Map<any,OnEachItemScope> = new Map();
|
|
491
|
+
byIndex: Map<any, OnEachItemScope> = new Map();
|
|
477
492
|
|
|
478
493
|
/** The reverse-ordered list of item scopes, not including those for which makeSortKey returned undefined. */
|
|
479
|
-
sortedSet: ReverseSortedSet<OnEachItemScope> = new ReverseSortedSet(
|
|
494
|
+
sortedSet: ReverseSortedSet<OnEachItemScope> = new ReverseSortedSet(
|
|
495
|
+
"sortKey",
|
|
496
|
+
);
|
|
480
497
|
|
|
481
498
|
/** Indexes that have been created/removed and need to be handled in the next `queueRun`. */
|
|
482
499
|
changedIndexes: Set<any> = new Set();
|
|
483
|
-
|
|
500
|
+
|
|
484
501
|
constructor(
|
|
485
502
|
proxy: TargetType,
|
|
486
503
|
/** A function that renders an item */
|
|
487
|
-
public renderer: (value: DatumType, key: any
|
|
504
|
+
public renderer: (value: DatumType, key: any) => void,
|
|
488
505
|
/** A function returning a number/string/array that defines the position of an item */
|
|
489
506
|
public makeSortKey?: (value: DatumType, key: any) => SortKeyType,
|
|
490
507
|
) {
|
|
491
508
|
super();
|
|
492
|
-
const target: TargetType = this.target =
|
|
509
|
+
const target: TargetType = (this.target =
|
|
510
|
+
(proxy as any)[TARGET_SYMBOL] || proxy);
|
|
493
511
|
|
|
494
512
|
subscribe(target, ANY_SYMBOL, this);
|
|
495
513
|
this.prevSibling = currentScope.getChildPrevSibling();
|
|
@@ -499,13 +517,13 @@ class OnEachScope extends Scope {
|
|
|
499
517
|
|
|
500
518
|
// Do _addChild() calls for initial items
|
|
501
519
|
if (target instanceof Array) {
|
|
502
|
-
for(let i=0; i<target.length; i++) {
|
|
503
|
-
if (target[i]!==undefined) {
|
|
520
|
+
for (let i = 0; i < target.length; i++) {
|
|
521
|
+
if (target[i] !== undefined) {
|
|
504
522
|
new OnEachItemScope(this, i, false);
|
|
505
523
|
}
|
|
506
524
|
}
|
|
507
525
|
} else {
|
|
508
|
-
for(const key in target) {
|
|
526
|
+
for (const key in target) {
|
|
509
527
|
if (target[key] !== undefined) {
|
|
510
528
|
new OnEachItemScope(this, key, false);
|
|
511
529
|
}
|
|
@@ -516,16 +534,17 @@ class OnEachScope extends Scope {
|
|
|
516
534
|
getPrecedingNode(): Node | undefined {
|
|
517
535
|
return findLastNodeInPrevSiblings(this.prevSibling);
|
|
518
536
|
}
|
|
519
|
-
|
|
537
|
+
|
|
520
538
|
onChange(index: any, newData: DatumType, oldData: DatumType) {
|
|
521
|
-
if (!(this.target instanceof Array) || typeof index ===
|
|
539
|
+
if (!(this.target instanceof Array) || typeof index === "number")
|
|
540
|
+
this.changedIndexes.add(index);
|
|
522
541
|
queue(this);
|
|
523
542
|
}
|
|
524
|
-
|
|
543
|
+
|
|
525
544
|
queueRun() {
|
|
526
|
-
|
|
545
|
+
const indexes = this.changedIndexes;
|
|
527
546
|
this.changedIndexes = new Set();
|
|
528
|
-
for(
|
|
547
|
+
for (const index of indexes) {
|
|
529
548
|
const oldScope = this.byIndex.get(index);
|
|
530
549
|
if (oldScope) oldScope.remove();
|
|
531
550
|
|
|
@@ -537,23 +556,24 @@ class OnEachScope extends Scope {
|
|
|
537
556
|
}
|
|
538
557
|
topRedrawScope = undefined;
|
|
539
558
|
}
|
|
540
|
-
|
|
559
|
+
|
|
541
560
|
delete() {
|
|
542
561
|
// Propagate to all our subscopes
|
|
543
562
|
for (const scope of this.byIndex.values()) {
|
|
544
563
|
scope.delete();
|
|
545
564
|
}
|
|
546
|
-
|
|
565
|
+
|
|
547
566
|
// Help garbage collection:
|
|
548
567
|
this.byIndex.clear();
|
|
549
568
|
setTimeout(() => {
|
|
550
569
|
// Unsure if this is a good idea. It takes time, but presumably makes things a lot easier for GC...
|
|
551
|
-
this.sortedSet.clear();
|
|
570
|
+
this.sortedSet.clear();
|
|
552
571
|
}, 1);
|
|
553
572
|
}
|
|
554
|
-
|
|
573
|
+
|
|
555
574
|
getLastNode(): Node | undefined {
|
|
556
|
-
for(
|
|
575
|
+
for (const scope of this.sortedSet) {
|
|
576
|
+
// Iterates starting at last child scope.
|
|
557
577
|
const node = scope.getActualLastNode();
|
|
558
578
|
if (node) return node;
|
|
559
579
|
}
|
|
@@ -564,7 +584,7 @@ class OnEachScope extends Scope {
|
|
|
564
584
|
class OnEachItemScope extends ContentScope {
|
|
565
585
|
sortKey: string | number | undefined; // When undefined, this scope is currently not showing in the list
|
|
566
586
|
public parentElement: Element;
|
|
567
|
-
|
|
587
|
+
|
|
568
588
|
constructor(
|
|
569
589
|
public parent: OnEachScope,
|
|
570
590
|
public itemIndex: any,
|
|
@@ -592,7 +612,7 @@ class OnEachItemScope extends ContentScope {
|
|
|
592
612
|
// of the sortedSet now (if we weren't already).
|
|
593
613
|
// This will do nothing and barely take any time of `this` is already part of the set:
|
|
594
614
|
this.parent.sortedSet.add(this);
|
|
595
|
-
|
|
615
|
+
|
|
596
616
|
const preScope = this.parent.sortedSet.prev(this);
|
|
597
617
|
// As preScope should have inserted itself as its first child, this should
|
|
598
618
|
// recursively call getPrecedingNode() on preScope in case it doesn't
|
|
@@ -610,7 +630,7 @@ class OnEachItemScope extends ContentScope {
|
|
|
610
630
|
getActualLastNode(): Node | undefined {
|
|
611
631
|
let child = this.lastChild;
|
|
612
632
|
|
|
613
|
-
while(child && child !== this) {
|
|
633
|
+
while (child && child !== this) {
|
|
614
634
|
if (child instanceof Node) return child;
|
|
615
635
|
const node = child.getLastNode();
|
|
616
636
|
if (node) return node;
|
|
@@ -621,7 +641,7 @@ class OnEachItemScope extends ContentScope {
|
|
|
621
641
|
queueRun() {
|
|
622
642
|
/* c8 ignore next */
|
|
623
643
|
if (currentScope !== ROOT_SCOPE) internalError(4);
|
|
624
|
-
|
|
644
|
+
|
|
625
645
|
// We're not calling `remove` here, as we don't want to remove ourselves from
|
|
626
646
|
// the sorted set. `redraw` will take care of that, if needed.
|
|
627
647
|
// Also, we can't use `getLastNode` here, as we've hacked it to return the
|
|
@@ -633,12 +653,12 @@ class OnEachItemScope extends ContentScope {
|
|
|
633
653
|
|
|
634
654
|
this.delete();
|
|
635
655
|
this.lastChild = this; // apply the hack (see constructor) again
|
|
636
|
-
|
|
656
|
+
|
|
637
657
|
topRedrawScope = this;
|
|
638
658
|
this.redraw();
|
|
639
659
|
topRedrawScope = undefined;
|
|
640
660
|
}
|
|
641
|
-
|
|
661
|
+
|
|
642
662
|
redraw() {
|
|
643
663
|
// Have the makeSortKey function return an ordering int/string/array.
|
|
644
664
|
|
|
@@ -646,22 +666,28 @@ class OnEachItemScope extends ContentScope {
|
|
|
646
666
|
// a wildcard subscription to delete/recreate any scopes when that changes.
|
|
647
667
|
// We ARE creating a proxy around the value though (in case its an object/array),
|
|
648
668
|
// so we'll have our own scope subscribe to changes on that.
|
|
649
|
-
const value: DatumType = optProxy(
|
|
669
|
+
const value: DatumType = optProxy(
|
|
670
|
+
(this.parent.target as any)[this.itemIndex],
|
|
671
|
+
);
|
|
650
672
|
|
|
651
673
|
// Since makeSortKey may get() the Store, we'll need to set currentScope first.
|
|
652
|
-
|
|
674
|
+
const savedScope = currentScope;
|
|
653
675
|
currentScope = this;
|
|
654
|
-
|
|
655
|
-
let sortKey
|
|
676
|
+
|
|
677
|
+
let sortKey: undefined | string | number;
|
|
656
678
|
try {
|
|
657
679
|
if (this.parent.makeSortKey) {
|
|
658
|
-
|
|
659
|
-
if (rawSortKey != null)
|
|
680
|
+
const rawSortKey = this.parent.makeSortKey(value, this.itemIndex);
|
|
681
|
+
if (rawSortKey != null)
|
|
682
|
+
sortKey =
|
|
683
|
+
rawSortKey instanceof Array
|
|
684
|
+
? rawSortKey.map(partToStr).join("")
|
|
685
|
+
: rawSortKey;
|
|
660
686
|
} else {
|
|
661
687
|
sortKey = this.itemIndex;
|
|
662
688
|
}
|
|
663
|
-
if (typeof sortKey ===
|
|
664
|
-
|
|
689
|
+
if (typeof sortKey === "number") sortKey = partToStr(sortKey);
|
|
690
|
+
|
|
665
691
|
if (this.sortKey !== sortKey) {
|
|
666
692
|
// If the sortKey is changed, make sure `this` is removed from the
|
|
667
693
|
// set before setting the new sortKey to it.
|
|
@@ -673,8 +699,8 @@ class OnEachItemScope extends ContentScope {
|
|
|
673
699
|
// in case no nodes are created. We'll do it just-in-time in `getPrecedingNode`.
|
|
674
700
|
|
|
675
701
|
if (sortKey != null) this.parent.renderer(value, this.itemIndex);
|
|
676
|
-
} catch(e) {
|
|
677
|
-
handleError(e, sortKey!=null);
|
|
702
|
+
} catch (e) {
|
|
703
|
+
handleError(e, sortKey != null);
|
|
678
704
|
}
|
|
679
705
|
|
|
680
706
|
currentScope = savedScope;
|
|
@@ -705,64 +731,89 @@ class OnEachItemScope extends ContentScope {
|
|
|
705
731
|
function addNode(node: Node) {
|
|
706
732
|
const parentEl = currentScope.parentElement;
|
|
707
733
|
const prevNode = currentScope.getInsertAfterNode();
|
|
708
|
-
parentEl.insertBefore(
|
|
734
|
+
parentEl.insertBefore(
|
|
735
|
+
node,
|
|
736
|
+
prevNode ? prevNode.nextSibling : parentEl.firstChild,
|
|
737
|
+
);
|
|
709
738
|
currentScope.lastChild = node;
|
|
710
739
|
}
|
|
711
740
|
|
|
712
|
-
|
|
713
741
|
/**
|
|
714
|
-
* This global is set during the execution of a `Scope.render`. It is used by
|
|
715
|
-
* functions like `$` and `clean`.
|
|
716
|
-
*/
|
|
742
|
+
* This global is set during the execution of a `Scope.render`. It is used by
|
|
743
|
+
* functions like `$` and `clean`.
|
|
744
|
+
*/
|
|
717
745
|
const ROOT_SCOPE = new RootScope();
|
|
718
746
|
let currentScope: ContentScope = ROOT_SCOPE;
|
|
719
747
|
|
|
720
748
|
/**
|
|
721
|
-
* A special Node observer index to subscribe to any value in the map changing.
|
|
722
|
-
*/
|
|
723
|
-
const ANY_SYMBOL = Symbol(
|
|
749
|
+
* A special Node observer index to subscribe to any value in the map changing.
|
|
750
|
+
*/
|
|
751
|
+
const ANY_SYMBOL = Symbol("any");
|
|
724
752
|
|
|
725
753
|
/**
|
|
726
754
|
* When our proxy objects need to lookup `obj[TARGET_SYMBOL]` it returns its
|
|
727
755
|
* target, to be used in our wrapped methods.
|
|
728
756
|
*/
|
|
729
|
-
const TARGET_SYMBOL = Symbol(
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
757
|
+
const TARGET_SYMBOL = Symbol("target");
|
|
758
|
+
|
|
759
|
+
const subscribers = new WeakMap<
|
|
760
|
+
TargetType,
|
|
761
|
+
Map<
|
|
762
|
+
any,
|
|
763
|
+
Set<Scope | ((index: any, newData: DatumType, oldData: DatumType) => void)>
|
|
764
|
+
>
|
|
765
|
+
>();
|
|
733
766
|
let peeking = 0; // When > 0, we're not subscribing to any changes
|
|
734
767
|
|
|
735
|
-
function subscribe(
|
|
768
|
+
function subscribe(
|
|
769
|
+
target: any,
|
|
770
|
+
index: symbol | string | number,
|
|
771
|
+
observer:
|
|
772
|
+
| Scope
|
|
773
|
+
| ((
|
|
774
|
+
index: any,
|
|
775
|
+
newData: DatumType,
|
|
776
|
+
oldData: DatumType,
|
|
777
|
+
) => void) = currentScope,
|
|
778
|
+
) {
|
|
736
779
|
if (observer === ROOT_SCOPE || peeking) return;
|
|
737
780
|
|
|
738
781
|
let byTarget = subscribers.get(target);
|
|
739
|
-
if (!byTarget) subscribers.set(target, byTarget = new Map());
|
|
782
|
+
if (!byTarget) subscribers.set(target, (byTarget = new Map()));
|
|
740
783
|
|
|
741
784
|
// No need to subscribe to specific keys if we're already subscribed to ANY
|
|
742
785
|
if (index !== ANY_SYMBOL && byTarget.get(ANY_SYMBOL)?.has(observer)) return;
|
|
743
786
|
|
|
744
787
|
let byIndex = byTarget.get(index);
|
|
745
|
-
if (!byIndex) byTarget.set(index, byIndex = new Set());
|
|
788
|
+
if (!byIndex) byTarget.set(index, (byIndex = new Set()));
|
|
746
789
|
|
|
747
790
|
if (byIndex.has(observer)) return;
|
|
748
791
|
|
|
749
792
|
byIndex.add(observer);
|
|
750
|
-
|
|
793
|
+
|
|
751
794
|
if (observer === currentScope) {
|
|
752
795
|
currentScope.cleaners.push(byIndex);
|
|
753
796
|
} else {
|
|
754
|
-
currentScope.cleaners.push(
|
|
797
|
+
currentScope.cleaners.push(() => {
|
|
755
798
|
byIndex.delete(observer);
|
|
756
799
|
});
|
|
757
800
|
}
|
|
758
801
|
}
|
|
759
802
|
|
|
760
|
-
export function onEach<T>(
|
|
761
|
-
|
|
803
|
+
export function onEach<T>(
|
|
804
|
+
target: Array<undefined | T>,
|
|
805
|
+
render: (value: T, index: number) => void,
|
|
806
|
+
makeKey?: (value: T, key: any) => SortKeyType,
|
|
807
|
+
): void;
|
|
808
|
+
export function onEach<K extends string | number | symbol, T>(
|
|
809
|
+
target: Record<K, undefined | T>,
|
|
810
|
+
render: (value: T, index: K) => void,
|
|
811
|
+
makeKey?: (value: T, key: K) => SortKeyType,
|
|
812
|
+
): void;
|
|
762
813
|
|
|
763
814
|
/**
|
|
764
815
|
* Reactively iterates over the items of an observable array or object, optionally rendering content for each item.
|
|
765
|
-
*
|
|
816
|
+
*
|
|
766
817
|
* Automatically updates when items are added, removed, or modified.
|
|
767
818
|
*
|
|
768
819
|
* @param target The observable array or object to iterate over. Values that are `undefined` are skipped.
|
|
@@ -796,7 +847,7 @@ export function onEach<K extends string|number|symbol,T>(target: Record<K,undefi
|
|
|
796
847
|
* $(`p:${user.name} (id=${user.id})`);
|
|
797
848
|
* }, (user) => [user.group, user.name]); // Sort by group, and within each group sort by name
|
|
798
849
|
* ```
|
|
799
|
-
*
|
|
850
|
+
*
|
|
800
851
|
* @example Iterating an object
|
|
801
852
|
* ```javascript
|
|
802
853
|
* const config = proxy({ theme: 'dark', fontSize: 14, showTips: true });
|
|
@@ -815,15 +866,20 @@ export function onEach<K extends string|number|symbol,T>(target: Record<K,undefi
|
|
|
815
866
|
* ```
|
|
816
867
|
* @see {@link invertString} To easily create keys for reverse sorting.
|
|
817
868
|
*/
|
|
818
|
-
export function onEach(
|
|
819
|
-
|
|
869
|
+
export function onEach(
|
|
870
|
+
target: TargetType,
|
|
871
|
+
render: (value: DatumType, index: any) => void,
|
|
872
|
+
makeKey?: (value: DatumType, key: any) => SortKeyType,
|
|
873
|
+
): void {
|
|
874
|
+
if (!target || typeof target !== "object")
|
|
875
|
+
throw new Error("onEach requires an object");
|
|
820
876
|
target = (target as any)[TARGET_SYMBOL] || target;
|
|
821
877
|
|
|
822
878
|
new OnEachScope(target, render, makeKey);
|
|
823
879
|
}
|
|
824
880
|
|
|
825
881
|
function isObjEmpty(obj: object): boolean {
|
|
826
|
-
for(
|
|
882
|
+
for (const k in obj) return false;
|
|
827
883
|
return true;
|
|
828
884
|
}
|
|
829
885
|
|
|
@@ -864,17 +920,24 @@ export function isEmpty(proxied: TargetType): boolean {
|
|
|
864
920
|
const scope = currentScope;
|
|
865
921
|
|
|
866
922
|
if (target instanceof Array) {
|
|
867
|
-
subscribe(
|
|
868
|
-
|
|
869
|
-
|
|
923
|
+
subscribe(
|
|
924
|
+
target,
|
|
925
|
+
"length",
|
|
926
|
+
(index: any, newData: DatumType, oldData: DatumType) => {
|
|
927
|
+
if (!newData !== !oldData) queue(scope);
|
|
928
|
+
},
|
|
929
|
+
);
|
|
870
930
|
return !target.length;
|
|
871
|
-
} else {
|
|
872
|
-
const result = isObjEmpty(target);
|
|
873
|
-
subscribe(target, ANY_SYMBOL, function(index: any, newData: DatumType, oldData: DatumType) {
|
|
874
|
-
if (result ? oldData===undefined : newData===undefined) queue(scope);
|
|
875
|
-
});
|
|
876
|
-
return result;
|
|
877
931
|
}
|
|
932
|
+
const result = isObjEmpty(target);
|
|
933
|
+
subscribe(
|
|
934
|
+
target,
|
|
935
|
+
ANY_SYMBOL,
|
|
936
|
+
(index: any, newData: DatumType, oldData: DatumType) => {
|
|
937
|
+
if (result ? oldData === undefined : newData === undefined) queue(scope);
|
|
938
|
+
},
|
|
939
|
+
);
|
|
940
|
+
return result;
|
|
878
941
|
}
|
|
879
942
|
|
|
880
943
|
/** @private */
|
|
@@ -909,53 +972,62 @@ export interface ValueRef<T> {
|
|
|
909
972
|
* ```
|
|
910
973
|
*/
|
|
911
974
|
export function count(proxied: TargetType): ValueRef<number> {
|
|
912
|
-
if (proxied instanceof Array) return ref(proxied,
|
|
975
|
+
if (proxied instanceof Array) return ref(proxied, "length");
|
|
913
976
|
|
|
914
977
|
const target = (proxied as any)[TARGET_SYMBOL] || proxied;
|
|
915
978
|
let cnt = 0;
|
|
916
|
-
for(
|
|
917
|
-
|
|
979
|
+
for (const k in target) if (target[k] !== undefined) cnt++;
|
|
980
|
+
|
|
918
981
|
const result = proxy(cnt);
|
|
919
|
-
subscribe(
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
982
|
+
subscribe(
|
|
983
|
+
target,
|
|
984
|
+
ANY_SYMBOL,
|
|
985
|
+
(index: any, newData: DatumType, oldData: DatumType) => {
|
|
986
|
+
if (oldData === newData) {
|
|
987
|
+
} else if (oldData === undefined) result.value = ++cnt;
|
|
988
|
+
else if (newData === undefined) result.value = --cnt;
|
|
989
|
+
},
|
|
990
|
+
);
|
|
924
991
|
|
|
925
992
|
return result;
|
|
926
993
|
}
|
|
927
994
|
|
|
928
995
|
/** @internal */
|
|
929
|
-
export function defaultEmitHandler(
|
|
996
|
+
export function defaultEmitHandler(
|
|
997
|
+
target: TargetType,
|
|
998
|
+
index: string | symbol | number,
|
|
999
|
+
newData: DatumType,
|
|
1000
|
+
oldData: DatumType,
|
|
1001
|
+
) {
|
|
930
1002
|
// We're triggering for values changing from undefined to undefined, as this *may*
|
|
931
1003
|
// indicate a change from or to `[empty]` (such as `[,1][0]`).
|
|
932
1004
|
if (newData === oldData && newData !== undefined) return;
|
|
933
|
-
|
|
1005
|
+
|
|
934
1006
|
const byTarget = subscribers.get(target);
|
|
935
|
-
if (byTarget===undefined) return;
|
|
1007
|
+
if (byTarget === undefined) return;
|
|
936
1008
|
|
|
937
|
-
for(const what of [index, ANY_SYMBOL]) {
|
|
938
|
-
|
|
1009
|
+
for (const what of [index, ANY_SYMBOL]) {
|
|
1010
|
+
const byIndex = byTarget.get(what);
|
|
939
1011
|
if (byIndex) {
|
|
940
|
-
for(
|
|
941
|
-
if (typeof observer ===
|
|
942
|
-
else observer.onChange(index, newData, oldData)
|
|
1012
|
+
for (const observer of byIndex) {
|
|
1013
|
+
if (typeof observer === "function") observer(index, newData, oldData);
|
|
1014
|
+
else observer.onChange(index, newData, oldData);
|
|
943
1015
|
}
|
|
944
1016
|
}
|
|
945
1017
|
}
|
|
946
1018
|
}
|
|
947
1019
|
let emit = defaultEmitHandler;
|
|
948
1020
|
|
|
949
|
-
|
|
950
1021
|
const objectHandler: ProxyHandler<any> = {
|
|
951
1022
|
get(target: any, prop: any) {
|
|
952
|
-
if (prop===TARGET_SYMBOL) return target;
|
|
1023
|
+
if (prop === TARGET_SYMBOL) return target;
|
|
953
1024
|
subscribe(target, prop);
|
|
954
1025
|
return optProxy(target[prop]);
|
|
955
1026
|
},
|
|
956
1027
|
set(target: any, prop: any, newData: any) {
|
|
957
1028
|
// Make sure newData is unproxied
|
|
958
|
-
if (typeof newData ===
|
|
1029
|
+
if (typeof newData === "object" && newData)
|
|
1030
|
+
newData = (newData as any)[TARGET_SYMBOL] || newData;
|
|
959
1031
|
const oldData = target[prop];
|
|
960
1032
|
if (newData !== oldData) {
|
|
961
1033
|
target[prop] = newData;
|
|
@@ -979,32 +1051,33 @@ const objectHandler: ProxyHandler<any> = {
|
|
|
979
1051
|
ownKeys(target: any) {
|
|
980
1052
|
subscribe(target, ANY_SYMBOL);
|
|
981
1053
|
return Reflect.ownKeys(target);
|
|
982
|
-
}
|
|
1054
|
+
},
|
|
983
1055
|
};
|
|
984
1056
|
|
|
985
1057
|
function arraySet(target: any, prop: any, newData: any) {
|
|
986
1058
|
// Make sure newData is unproxied
|
|
987
|
-
if (typeof newData ===
|
|
1059
|
+
if (typeof newData === "object" && newData)
|
|
1060
|
+
newData = (newData as any)[TARGET_SYMBOL] || newData;
|
|
988
1061
|
const oldData = target[prop];
|
|
989
1062
|
if (newData !== oldData) {
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
if (prop ===
|
|
1063
|
+
const oldLength = target.length;
|
|
1064
|
+
|
|
1065
|
+
if (prop === "length") {
|
|
993
1066
|
target.length = newData;
|
|
994
1067
|
|
|
995
1068
|
// We only need to emit for shrinking, as growing just adds undefineds
|
|
996
|
-
for(let i=newData; i<oldLength; i++) {
|
|
1069
|
+
for (let i = newData; i < oldLength; i++) {
|
|
997
1070
|
emit(target, i, undefined, target[i]);
|
|
998
1071
|
}
|
|
999
1072
|
} else {
|
|
1000
|
-
const intProp = parseInt(prop)
|
|
1073
|
+
const intProp = Number.parseInt(prop);
|
|
1001
1074
|
if (intProp.toString() === prop) prop = intProp;
|
|
1002
1075
|
|
|
1003
1076
|
target[prop] = newData;
|
|
1004
1077
|
emit(target, prop, newData, oldData);
|
|
1005
1078
|
}
|
|
1006
1079
|
if (target.length !== oldLength) {
|
|
1007
|
-
emit(target,
|
|
1080
|
+
emit(target, "length", target.length, oldLength);
|
|
1008
1081
|
}
|
|
1009
1082
|
runImmediateQueue();
|
|
1010
1083
|
}
|
|
@@ -1013,40 +1086,66 @@ function arraySet(target: any, prop: any, newData: any) {
|
|
|
1013
1086
|
|
|
1014
1087
|
const arrayHandler: ProxyHandler<any[]> = {
|
|
1015
1088
|
get(target: any, prop: any) {
|
|
1016
|
-
if (prop===TARGET_SYMBOL) return target;
|
|
1089
|
+
if (prop === TARGET_SYMBOL) return target;
|
|
1017
1090
|
let subProp = prop;
|
|
1018
|
-
if (typeof prop !==
|
|
1019
|
-
const intProp = parseInt(prop);
|
|
1091
|
+
if (typeof prop !== "symbol") {
|
|
1092
|
+
const intProp = Number.parseInt(prop);
|
|
1020
1093
|
if (intProp.toString() === prop) subProp = intProp;
|
|
1021
1094
|
}
|
|
1022
1095
|
subscribe(target, subProp);
|
|
1023
1096
|
return optProxy(target[prop]);
|
|
1024
1097
|
},
|
|
1025
1098
|
set: arraySet,
|
|
1026
|
-
deleteProperty(target: any, prop: string|symbol) {
|
|
1099
|
+
deleteProperty(target: any, prop: string | symbol) {
|
|
1027
1100
|
return arraySet(target, prop, undefined);
|
|
1028
1101
|
},
|
|
1029
1102
|
};
|
|
1030
1103
|
|
|
1031
|
-
const proxyMap = new WeakMap<TargetType, /*Proxy*/TargetType>();
|
|
1104
|
+
const proxyMap = new WeakMap<TargetType, /*Proxy*/ TargetType>();
|
|
1032
1105
|
|
|
1033
1106
|
function optProxy(value: any): any {
|
|
1034
1107
|
// If value is a primitive type or already proxied, just return it
|
|
1035
|
-
if (
|
|
1108
|
+
if (
|
|
1109
|
+
typeof value !== "object" ||
|
|
1110
|
+
!value ||
|
|
1111
|
+
value[TARGET_SYMBOL] !== undefined
|
|
1112
|
+
) {
|
|
1036
1113
|
return value;
|
|
1037
1114
|
}
|
|
1038
1115
|
let proxied = proxyMap.get(value);
|
|
1039
|
-
if (proxied) return proxied // Only one proxy per target!
|
|
1040
|
-
|
|
1041
|
-
proxied = new Proxy(
|
|
1116
|
+
if (proxied) return proxied; // Only one proxy per target!
|
|
1117
|
+
|
|
1118
|
+
proxied = new Proxy(
|
|
1119
|
+
value,
|
|
1120
|
+
value instanceof Array ? arrayHandler : objectHandler,
|
|
1121
|
+
);
|
|
1042
1122
|
proxyMap.set(value, proxied as TargetType);
|
|
1043
1123
|
return proxied;
|
|
1044
1124
|
}
|
|
1045
1125
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1126
|
+
export function proxy<T extends DatumType>(
|
|
1127
|
+
target: Array<T>,
|
|
1128
|
+
): Array<
|
|
1129
|
+
T extends number
|
|
1130
|
+
? number
|
|
1131
|
+
: T extends string
|
|
1132
|
+
? string
|
|
1133
|
+
: T extends boolean
|
|
1134
|
+
? boolean
|
|
1135
|
+
: T
|
|
1136
|
+
>;
|
|
1048
1137
|
export function proxy<T extends object>(target: T): T;
|
|
1049
|
-
export function proxy<T extends DatumType>(
|
|
1138
|
+
export function proxy<T extends DatumType>(
|
|
1139
|
+
target: T,
|
|
1140
|
+
): ValueRef<
|
|
1141
|
+
T extends number
|
|
1142
|
+
? number
|
|
1143
|
+
: T extends string
|
|
1144
|
+
? string
|
|
1145
|
+
: T extends boolean
|
|
1146
|
+
? boolean
|
|
1147
|
+
: T
|
|
1148
|
+
>;
|
|
1050
1149
|
|
|
1051
1150
|
/**
|
|
1052
1151
|
* Creates a reactive proxy around the given data.
|
|
@@ -1087,7 +1186,7 @@ export function proxy<T extends DatumType>(target: T): ValueRef<T extends number
|
|
|
1087
1186
|
* observe(() => console.log(name.value)); // Subscribes to value
|
|
1088
1187
|
* setTimeout(() => name.value = 'UI', 2000); // Triggers the observe function
|
|
1089
1188
|
* ```
|
|
1090
|
-
*
|
|
1189
|
+
*
|
|
1091
1190
|
* @example Class instance
|
|
1092
1191
|
* ```typescript
|
|
1093
1192
|
* class Widget {
|
|
@@ -1102,7 +1201,9 @@ export function proxy<T extends DatumType>(target: T): ValueRef<T extends number
|
|
|
1102
1201
|
* ```
|
|
1103
1202
|
*/
|
|
1104
1203
|
export function proxy(target: TargetType): TargetType {
|
|
1105
|
-
|
|
1204
|
+
return optProxy(
|
|
1205
|
+
typeof target === "object" && target !== null ? target : { value: target },
|
|
1206
|
+
);
|
|
1106
1207
|
}
|
|
1107
1208
|
|
|
1108
1209
|
/**
|
|
@@ -1125,13 +1226,13 @@ export function proxy(target: TargetType): TargetType {
|
|
|
1125
1226
|
* $(() => console.log('proxied', userProxy.name));
|
|
1126
1227
|
* // The following will only ever log once, as we're not subscribing to any observable
|
|
1127
1228
|
* $(() => console.log('unproxied', rawUser.name));
|
|
1128
|
-
*
|
|
1229
|
+
*
|
|
1129
1230
|
* // This cause the first log to run again:
|
|
1130
1231
|
* setTimeout(() => userProxy.name += '!', 1000);
|
|
1131
|
-
*
|
|
1232
|
+
*
|
|
1132
1233
|
* // This doesn't cause any new logs:
|
|
1133
1234
|
* setTimeout(() => rawUser.name += '?', 2000);
|
|
1134
|
-
*
|
|
1235
|
+
*
|
|
1135
1236
|
* // Both userProxy and rawUser end up as `{name: 'Frank!?'}`
|
|
1136
1237
|
* setTimeout(() => {
|
|
1137
1238
|
* console.log('final proxied', userProxy)
|
|
@@ -1143,10 +1244,11 @@ export function unproxy<T>(target: T): T {
|
|
|
1143
1244
|
return target ? (target as any)[TARGET_SYMBOL] || target : target;
|
|
1144
1245
|
}
|
|
1145
1246
|
|
|
1146
|
-
|
|
1247
|
+
const onDestroyMap: WeakMap<Node, string | ((...args: any[]) => void) | true> =
|
|
1248
|
+
new WeakMap();
|
|
1147
1249
|
|
|
1148
1250
|
function destroyWithClass(element: Element, cls: string) {
|
|
1149
|
-
const classes = cls.split(
|
|
1251
|
+
const classes = cls.split(".").filter((c) => c);
|
|
1150
1252
|
element.classList.add(...classes);
|
|
1151
1253
|
setTimeout(() => element.remove(), 2000);
|
|
1152
1254
|
}
|
|
@@ -1203,7 +1305,7 @@ function destroyWithClass(element: Element, cls: string) {
|
|
|
1203
1305
|
* console.log(source.nested); // [1, 2, 3] (source was modified)
|
|
1204
1306
|
* ```
|
|
1205
1307
|
*/
|
|
1206
|
-
export function copy<T extends object>(dst: T, src: T
|
|
1308
|
+
export function copy<T extends object>(dst: T, src: Partial<T>, flags = 0) {
|
|
1207
1309
|
copyRecurse(dst, src, flags);
|
|
1208
1310
|
runImmediateQueue();
|
|
1209
1311
|
}
|
|
@@ -1216,14 +1318,14 @@ const COPY_EMIT = 64;
|
|
|
1216
1318
|
|
|
1217
1319
|
/**
|
|
1218
1320
|
* Clone an (optionally proxied) object or array.
|
|
1219
|
-
*
|
|
1321
|
+
*
|
|
1220
1322
|
* @param src The object or array to clone. If it is proxied, `clone` will subscribe to any changes to the (nested) data structure.
|
|
1221
|
-
* @param flags
|
|
1323
|
+
* @param flags
|
|
1222
1324
|
* - {@link SHALLOW}: Performs a shallow clone, meaning that only the top-level array or object will be copied, while object/array values will just be references to the original data in `src`.
|
|
1223
1325
|
* @template T - The type of the objects being copied.
|
|
1224
1326
|
* @returns A new unproxied array or object (of the same type as `src`), containing a deep (by default) copy of `src`.
|
|
1225
1327
|
*/
|
|
1226
|
-
export function clone<T extends object>(src: T, flags
|
|
1328
|
+
export function clone<T extends object>(src: T, flags = 0): T {
|
|
1227
1329
|
const dst = Object.create(Object.getPrototypeOf(src)) as T;
|
|
1228
1330
|
copyRecurse(dst, src, flags);
|
|
1229
1331
|
return dst;
|
|
@@ -1245,87 +1347,94 @@ function copyRecurse(dst: any, src: any, flags: number) {
|
|
|
1245
1347
|
if (currentScope !== ROOT_SCOPE && !peeking) flags |= COPY_SUBSCRIBE;
|
|
1246
1348
|
}
|
|
1247
1349
|
|
|
1248
|
-
if (flags©_SUBSCRIBE) subscribe(src, ANY_SYMBOL);
|
|
1249
|
-
|
|
1250
|
-
|
|
1350
|
+
if (flags & COPY_SUBSCRIBE) subscribe(src, ANY_SYMBOL);
|
|
1351
|
+
if (src instanceof Array) {
|
|
1352
|
+
if (!(dst instanceof Array))
|
|
1353
|
+
throw new Error("Cannot copy array into object");
|
|
1251
1354
|
const dstLen = dst.length;
|
|
1252
1355
|
const srcLen = src.length;
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1356
|
+
for (let i = 0; i < srcLen; i++) {
|
|
1357
|
+
copyValue(dst, src, i, flags);
|
|
1358
|
+
}
|
|
1256
1359
|
// Leaving additional values in the old array doesn't make sense
|
|
1257
1360
|
if (srcLen !== dstLen) {
|
|
1258
|
-
if (flags©_EMIT) {
|
|
1259
|
-
for(let i=srcLen; i<dstLen; i++) {
|
|
1361
|
+
if (flags & COPY_EMIT) {
|
|
1362
|
+
for (let i = srcLen; i < dstLen; i++) {
|
|
1260
1363
|
const old = dst[i];
|
|
1261
1364
|
dst[i] = undefined;
|
|
1262
1365
|
emit(dst, i, undefined, old);
|
|
1263
1366
|
}
|
|
1264
1367
|
dst.length = srcLen;
|
|
1265
|
-
emit(dst,
|
|
1368
|
+
emit(dst, "length", srcLen, dstLen);
|
|
1266
1369
|
} else {
|
|
1267
1370
|
dst.length = srcLen;
|
|
1268
1371
|
}
|
|
1269
1372
|
}
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1373
|
+
} else {
|
|
1374
|
+
for (const k in src) {
|
|
1375
|
+
copyValue(dst, src, k, flags);
|
|
1376
|
+
}
|
|
1274
1377
|
if (!(flags & MERGE)) {
|
|
1275
|
-
for(
|
|
1378
|
+
for (const k in dst) {
|
|
1276
1379
|
if (!(k in src)) {
|
|
1277
1380
|
const old = dst[k];
|
|
1278
1381
|
delete dst[k];
|
|
1279
|
-
if (flags©_EMIT && old !== undefined) {
|
|
1382
|
+
if (flags & COPY_EMIT && old !== undefined) {
|
|
1280
1383
|
emit(dst, k, undefined, old);
|
|
1281
1384
|
}
|
|
1282
1385
|
}
|
|
1283
1386
|
}
|
|
1284
1387
|
}
|
|
1285
|
-
|
|
1388
|
+
}
|
|
1286
1389
|
}
|
|
1287
1390
|
|
|
1288
1391
|
function copyValue(dst: any, src: any, index: any, flags: number) {
|
|
1289
|
-
|
|
1290
|
-
|
|
1392
|
+
const dstValue = dst[index];
|
|
1393
|
+
let srcValue = src[index];
|
|
1291
1394
|
if (srcValue !== dstValue) {
|
|
1292
|
-
if (
|
|
1395
|
+
if (
|
|
1396
|
+
srcValue &&
|
|
1397
|
+
dstValue &&
|
|
1398
|
+
typeof srcValue === "object" &&
|
|
1399
|
+
typeof dstValue === "object" &&
|
|
1400
|
+
(srcValue.constructor === dstValue.constructor ||
|
|
1401
|
+
(flags & MERGE && dstValue instanceof Array))
|
|
1402
|
+
) {
|
|
1293
1403
|
copyRecurse(dstValue, srcValue, flags);
|
|
1294
1404
|
return;
|
|
1295
1405
|
}
|
|
1296
|
-
|
|
1297
|
-
if (!(flags&SHALLOW) && srcValue && typeof srcValue ===
|
|
1406
|
+
|
|
1407
|
+
if (!(flags & SHALLOW) && srcValue && typeof srcValue === "object") {
|
|
1298
1408
|
// Create an empty object of the same type
|
|
1299
|
-
|
|
1409
|
+
const copy = Object.create(Object.getPrototypeOf(srcValue));
|
|
1300
1410
|
// Copy all properties to it. This doesn't need to emit anything
|
|
1301
1411
|
// and MERGE does not apply as this is a new branch.
|
|
1302
1412
|
copyRecurse(copy, srcValue, 0);
|
|
1303
1413
|
srcValue = copy;
|
|
1304
1414
|
}
|
|
1305
1415
|
const old = dst[index];
|
|
1306
|
-
if (flags&MERGE && srcValue == null) delete dst[index];
|
|
1416
|
+
if (flags & MERGE && srcValue == null) delete dst[index];
|
|
1307
1417
|
else dst[index] = srcValue;
|
|
1308
|
-
if (flags©_EMIT) emit(dst, index, srcValue, old)
|
|
1418
|
+
if (flags & COPY_EMIT) emit(dst, index, srcValue, old);
|
|
1309
1419
|
}
|
|
1310
1420
|
}
|
|
1311
1421
|
|
|
1312
|
-
|
|
1313
1422
|
interface RefTarget {
|
|
1314
|
-
proxy: TargetType
|
|
1315
|
-
index: any
|
|
1423
|
+
proxy: TargetType;
|
|
1424
|
+
index: any;
|
|
1316
1425
|
}
|
|
1317
1426
|
const refHandler: ProxyHandler<RefTarget> = {
|
|
1318
1427
|
get(target: RefTarget, prop: any) {
|
|
1319
|
-
if (prop===TARGET_SYMBOL) {
|
|
1428
|
+
if (prop === TARGET_SYMBOL) {
|
|
1320
1429
|
// Create a ref to the unproxied version of the target
|
|
1321
1430
|
return ref(unproxy(target.proxy), target.index);
|
|
1322
1431
|
}
|
|
1323
|
-
if (prop==="value") {
|
|
1432
|
+
if (prop === "value") {
|
|
1324
1433
|
return (target.proxy as any)[target.index];
|
|
1325
1434
|
}
|
|
1326
1435
|
},
|
|
1327
1436
|
set(target: any, prop: any, value: any) {
|
|
1328
|
-
if (prop==="value") {
|
|
1437
|
+
if (prop === "value") {
|
|
1329
1438
|
(target.proxy as any)[target.index] = value;
|
|
1330
1439
|
return true;
|
|
1331
1440
|
}
|
|
@@ -1333,7 +1442,6 @@ const refHandler: ProxyHandler<RefTarget> = {
|
|
|
1333
1442
|
},
|
|
1334
1443
|
};
|
|
1335
1444
|
|
|
1336
|
-
|
|
1337
1445
|
/**
|
|
1338
1446
|
* Creates a reactive reference (`{ value: T }`-like object) to a specific value
|
|
1339
1447
|
* within a proxied object or array.
|
|
@@ -1364,78 +1472,101 @@ const refHandler: ProxyHandler<RefTarget> = {
|
|
|
1364
1472
|
* text: ref(formData, 'color'),
|
|
1365
1473
|
* $color: ref(formData, 'color')
|
|
1366
1474
|
* });
|
|
1367
|
-
*
|
|
1475
|
+
*
|
|
1368
1476
|
* // Changes are actually stored in formData - this causes logs like `{color: "Blue", velocity 42}`
|
|
1369
1477
|
* $(() => console.log(formData))
|
|
1370
1478
|
* ```
|
|
1371
1479
|
*/
|
|
1372
|
-
export function ref<T extends TargetType, K extends keyof T>(
|
|
1373
|
-
|
|
1480
|
+
export function ref<T extends TargetType, K extends keyof T>(
|
|
1481
|
+
target: T,
|
|
1482
|
+
index: K,
|
|
1483
|
+
): ValueRef<T[K]> {
|
|
1484
|
+
return new Proxy({ proxy: target, index }, refHandler) as any as ValueRef<
|
|
1485
|
+
T[K]
|
|
1486
|
+
>;
|
|
1374
1487
|
}
|
|
1375
1488
|
|
|
1376
|
-
|
|
1377
|
-
function applyBind(_el: Element, target: any) {
|
|
1378
|
-
const el = _el as HTMLInputElement;
|
|
1489
|
+
function applyBind(el: HTMLInputElement, target: any) {
|
|
1379
1490
|
let onProxyChange: () => void;
|
|
1380
1491
|
let onInputChange: () => void;
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
if (type ===
|
|
1492
|
+
const type = el.getAttribute("type");
|
|
1493
|
+
const value = unproxy(target).value;
|
|
1494
|
+
if (type === "checkbox") {
|
|
1384
1495
|
if (value === undefined) target.value = el.checked;
|
|
1385
|
-
onProxyChange = () =>
|
|
1386
|
-
|
|
1387
|
-
|
|
1496
|
+
onProxyChange = () => {
|
|
1497
|
+
el.checked = target.value;
|
|
1498
|
+
};
|
|
1499
|
+
onInputChange = () => {
|
|
1500
|
+
target.value = el.checked;
|
|
1501
|
+
};
|
|
1502
|
+
} else if (type === "radio") {
|
|
1388
1503
|
if (value === undefined && el.checked) target.value = el.value;
|
|
1389
|
-
onProxyChange = () =>
|
|
1504
|
+
onProxyChange = () => {
|
|
1505
|
+
el.checked = target.value === el.value;
|
|
1506
|
+
};
|
|
1390
1507
|
onInputChange = () => {
|
|
1391
1508
|
if (el.checked) target.value = el.value;
|
|
1392
|
-
}
|
|
1509
|
+
};
|
|
1393
1510
|
} else {
|
|
1394
|
-
onInputChange = () =>
|
|
1511
|
+
onInputChange = () => {
|
|
1512
|
+
target.value =
|
|
1513
|
+
type === "number" || type === "range"
|
|
1514
|
+
? el.value === ""
|
|
1515
|
+
? null
|
|
1516
|
+
: +el.value
|
|
1517
|
+
: el.value;
|
|
1518
|
+
};
|
|
1395
1519
|
if (value === undefined) onInputChange();
|
|
1396
|
-
onProxyChange = () =>
|
|
1520
|
+
onProxyChange = () => {
|
|
1521
|
+
el.value = target.value;
|
|
1522
|
+
// biome-ignore lint/suspicious/noDoubleEquals: it's fine for numbers to be casts to strings here
|
|
1523
|
+
if (el.tagName === "SELECT" && el.value != target.value)
|
|
1524
|
+
throw new Error(`SELECT has no '${target.value}' OPTION (yet)`);
|
|
1525
|
+
};
|
|
1397
1526
|
}
|
|
1398
1527
|
observe(onProxyChange);
|
|
1399
|
-
el.addEventListener(
|
|
1528
|
+
el.addEventListener("input", onInputChange);
|
|
1400
1529
|
clean(() => {
|
|
1401
|
-
el.removeEventListener(
|
|
1530
|
+
el.removeEventListener("input", onInputChange);
|
|
1402
1531
|
});
|
|
1403
1532
|
}
|
|
1404
1533
|
|
|
1405
|
-
const SPECIAL_PROPS: {[key: string]: (value: any) => void} = {
|
|
1406
|
-
create:
|
|
1534
|
+
const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
|
|
1535
|
+
create: (value: any) => {
|
|
1407
1536
|
const el = currentScope.parentElement;
|
|
1408
1537
|
if (currentScope !== topRedrawScope) return;
|
|
1409
|
-
if (typeof value ===
|
|
1538
|
+
if (typeof value === "function") {
|
|
1410
1539
|
value(el);
|
|
1411
1540
|
} else {
|
|
1412
|
-
const classes = value.split(
|
|
1541
|
+
const classes = value.split(".").filter((c: any) => c);
|
|
1413
1542
|
el.classList.add(...classes);
|
|
1414
|
-
(async
|
|
1543
|
+
(async () => {
|
|
1544
|
+
// attempt to prevent layout trashing
|
|
1415
1545
|
(el as HTMLElement).offsetHeight; // trigger layout
|
|
1416
1546
|
el.classList.remove(...classes);
|
|
1417
1547
|
})();
|
|
1418
1548
|
}
|
|
1419
1549
|
},
|
|
1420
|
-
destroy:
|
|
1550
|
+
destroy: (value: any) => {
|
|
1421
1551
|
const el = currentScope.parentElement;
|
|
1422
1552
|
onDestroyMap.set(el, value);
|
|
1423
1553
|
},
|
|
1424
|
-
html:
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1554
|
+
html: (value: any) => {
|
|
1555
|
+
const tmpParent = document.createElement(
|
|
1556
|
+
currentScope.parentElement.tagName,
|
|
1557
|
+
);
|
|
1558
|
+
tmpParent.innerHTML = `${value}`;
|
|
1559
|
+
while (tmpParent.firstChild) addNode(tmpParent.firstChild);
|
|
1428
1560
|
},
|
|
1429
|
-
text:
|
|
1561
|
+
text: (value: any) => {
|
|
1430
1562
|
addNode(document.createTextNode(value));
|
|
1431
1563
|
},
|
|
1432
|
-
element:
|
|
1433
|
-
if (!(value instanceof Node))
|
|
1564
|
+
element: (value: any) => {
|
|
1565
|
+
if (!(value instanceof Node))
|
|
1566
|
+
throw new Error(`Unexpected element-argument: ${JSON.parse(value)}`);
|
|
1434
1567
|
addNode(value);
|
|
1435
1568
|
},
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
|
|
1569
|
+
};
|
|
1439
1570
|
|
|
1440
1571
|
/**
|
|
1441
1572
|
* The core function for building reactive user interfaces in Aberdeen. It creates and inserts new DOM elements
|
|
@@ -1483,12 +1614,12 @@ const SPECIAL_PROPS: {[key: string]: (value: any) => void} = {
|
|
|
1483
1614
|
* $color: 'red'
|
|
1484
1615
|
* });
|
|
1485
1616
|
* ```
|
|
1486
|
-
*
|
|
1617
|
+
*
|
|
1487
1618
|
* @example Create Nested Elements
|
|
1488
1619
|
* ```typescript
|
|
1489
1620
|
* let inputElement: Element = $('label:Click me', 'input', {type: 'checkbox'});
|
|
1490
1621
|
* // You should usually not touch raw DOM elements, unless when integrating
|
|
1491
|
-
* // with non-Aberdeen code.
|
|
1622
|
+
* // with non-Aberdeen code.
|
|
1492
1623
|
* console.log('DOM element:', inputElement);
|
|
1493
1624
|
* ```
|
|
1494
1625
|
*
|
|
@@ -1523,62 +1654,71 @@ const SPECIAL_PROPS: {[key: string]: (value: any) => void} = {
|
|
|
1523
1654
|
* ```
|
|
1524
1655
|
*/
|
|
1525
1656
|
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1657
|
+
export function $(
|
|
1658
|
+
...args: (
|
|
1659
|
+
| string
|
|
1660
|
+
| null
|
|
1661
|
+
| undefined
|
|
1662
|
+
| false
|
|
1663
|
+
| (() => void)
|
|
1664
|
+
| Record<string, any>
|
|
1665
|
+
)[]
|
|
1666
|
+
): undefined | Element {
|
|
1667
|
+
let savedCurrentScope: undefined | ContentScope;
|
|
1668
|
+
let err: undefined | string;
|
|
1669
|
+
let result: undefined | Element;
|
|
1670
|
+
|
|
1671
|
+
for (let arg of args) {
|
|
1533
1672
|
if (arg == null || arg === false) continue;
|
|
1534
|
-
if (typeof arg ===
|
|
1535
|
-
let text
|
|
1536
|
-
|
|
1673
|
+
if (typeof arg === "string") {
|
|
1674
|
+
let text: undefined | string;
|
|
1675
|
+
let classes: undefined | string;
|
|
1676
|
+
const textPos = arg.indexOf(":");
|
|
1537
1677
|
if (textPos >= 0) {
|
|
1538
|
-
text = arg.substring(textPos+1);
|
|
1539
|
-
arg = arg.substring(0,textPos);
|
|
1678
|
+
text = arg.substring(textPos + 1);
|
|
1679
|
+
arg = arg.substring(0, textPos);
|
|
1540
1680
|
}
|
|
1541
|
-
const classPos = arg.indexOf(
|
|
1681
|
+
const classPos = arg.indexOf(".");
|
|
1542
1682
|
if (classPos >= 0) {
|
|
1543
|
-
classes = arg.substring(classPos+1);
|
|
1683
|
+
classes = arg.substring(classPos + 1);
|
|
1544
1684
|
arg = arg.substring(0, classPos);
|
|
1545
1685
|
}
|
|
1546
|
-
if (arg ===
|
|
1686
|
+
if (arg === "") {
|
|
1687
|
+
// Add text or classes to parent
|
|
1547
1688
|
if (text) addNode(document.createTextNode(text));
|
|
1548
1689
|
if (classes) {
|
|
1549
1690
|
const el = currentScope.parentElement;
|
|
1550
|
-
el.classList.add(...classes.split(
|
|
1691
|
+
el.classList.add(...classes.split("."));
|
|
1551
1692
|
if (!savedCurrentScope) {
|
|
1552
|
-
clean(() => el.classList.remove(...classes.split(
|
|
1693
|
+
clean(() => el.classList.remove(...classes.split(".")));
|
|
1553
1694
|
}
|
|
1554
1695
|
}
|
|
1555
|
-
} else if (arg.indexOf(
|
|
1696
|
+
} else if (arg.indexOf(" ") >= 0) {
|
|
1556
1697
|
err = `Tag '${arg}' cannot contain space`;
|
|
1557
1698
|
break;
|
|
1558
1699
|
} else {
|
|
1559
1700
|
result = document.createElement(arg);
|
|
1560
|
-
if (classes) result.className = classes.replaceAll(
|
|
1701
|
+
if (classes) result.className = classes.replaceAll(".", " ");
|
|
1561
1702
|
if (text) result.textContent = text;
|
|
1562
1703
|
addNode(result);
|
|
1563
1704
|
if (!savedCurrentScope) {
|
|
1564
1705
|
savedCurrentScope = currentScope;
|
|
1565
1706
|
}
|
|
1566
|
-
|
|
1707
|
+
const newScope = new ChainedScope(result, true);
|
|
1567
1708
|
newScope.lastChild = result.lastChild || undefined;
|
|
1568
1709
|
if (topRedrawScope === currentScope) topRedrawScope = newScope;
|
|
1569
1710
|
currentScope = newScope;
|
|
1570
1711
|
}
|
|
1571
|
-
}
|
|
1572
|
-
else if (typeof arg === 'object') {
|
|
1712
|
+
} else if (typeof arg === "object") {
|
|
1573
1713
|
if (arg.constructor !== Object) {
|
|
1574
1714
|
err = `Unexpected argument: ${arg}`;
|
|
1575
1715
|
break;
|
|
1576
1716
|
}
|
|
1577
|
-
for(const key in arg) {
|
|
1717
|
+
for (const key in arg) {
|
|
1578
1718
|
const val = arg[key];
|
|
1579
1719
|
applyArg(key, val);
|
|
1580
1720
|
}
|
|
1581
|
-
} else if (typeof arg ===
|
|
1721
|
+
} else if (typeof arg === "function") {
|
|
1582
1722
|
new RegularScope(currentScope.parentElement, arg);
|
|
1583
1723
|
} else {
|
|
1584
1724
|
err = `Unexpected argument: ${arg}`;
|
|
@@ -1647,71 +1787,88 @@ let cssCount = 0;
|
|
|
1647
1787
|
* color: "#107ab0",
|
|
1648
1788
|
* }
|
|
1649
1789
|
* }, true); // Pass true for global
|
|
1650
|
-
*
|
|
1790
|
+
*
|
|
1651
1791
|
* $('a:Styled link');
|
|
1652
1792
|
* ```
|
|
1653
1793
|
*/
|
|
1654
|
-
export function insertCss(style: object, global
|
|
1655
|
-
const prefix = global ? "" :
|
|
1656
|
-
|
|
1657
|
-
if (css) $(
|
|
1794
|
+
export function insertCss(style: object, global = false): string {
|
|
1795
|
+
const prefix = global ? "" : `.AbdStl${++cssCount}`;
|
|
1796
|
+
const css = styleToCss(style, prefix);
|
|
1797
|
+
if (css) $(`style:${css}`);
|
|
1658
1798
|
return prefix;
|
|
1659
1799
|
}
|
|
1660
1800
|
|
|
1661
1801
|
function styleToCss(style: object, prefix: string): string {
|
|
1662
|
-
let props =
|
|
1663
|
-
let rules =
|
|
1664
|
-
for(const kOr in style) {
|
|
1802
|
+
let props = "";
|
|
1803
|
+
let rules = "";
|
|
1804
|
+
for (const kOr in style) {
|
|
1665
1805
|
const v = (style as any)[kOr];
|
|
1666
|
-
for(const k of kOr.split(/, ?/g)) {
|
|
1667
|
-
if (v && typeof v ===
|
|
1668
|
-
if (k.startsWith(
|
|
1669
|
-
|
|
1806
|
+
for (const k of kOr.split(/, ?/g)) {
|
|
1807
|
+
if (v && typeof v === "object") {
|
|
1808
|
+
if (k.startsWith("@")) {
|
|
1809
|
+
// media queries
|
|
1810
|
+
rules += `${k}{\n${styleToCss(v, prefix)}}\n`;
|
|
1670
1811
|
} else {
|
|
1671
|
-
rules += styleToCss(
|
|
1812
|
+
rules += styleToCss(
|
|
1813
|
+
v,
|
|
1814
|
+
k.includes("&") ? k.replace(/&/g, prefix) : `${prefix} ${k}`,
|
|
1815
|
+
);
|
|
1672
1816
|
}
|
|
1673
1817
|
} else {
|
|
1674
|
-
props += k.replace(/[A-Z]/g, letter =>
|
|
1818
|
+
props += `${k.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}:${v};`;
|
|
1675
1819
|
}
|
|
1676
1820
|
}
|
|
1677
1821
|
}
|
|
1678
|
-
if (props) rules =
|
|
1822
|
+
if (props) rules = `${prefix.trimStart() || "*"}{${props}}\n${rules}`;
|
|
1679
1823
|
return rules;
|
|
1680
1824
|
}
|
|
1681
1825
|
|
|
1682
1826
|
function applyArg(key: string, value: any) {
|
|
1683
1827
|
const el = currentScope.parentElement;
|
|
1684
|
-
if (typeof value ===
|
|
1685
|
-
|
|
1686
|
-
|
|
1828
|
+
if (typeof value === "object" && value !== null && value[TARGET_SYMBOL]) {
|
|
1829
|
+
// Value is a proxy
|
|
1830
|
+
if (key === "bind") {
|
|
1831
|
+
applyBind(el as HTMLInputElement, value);
|
|
1687
1832
|
} else {
|
|
1688
|
-
new SetArgScope(el, key, value)
|
|
1833
|
+
new SetArgScope(el, key, value);
|
|
1689
1834
|
// SetArgScope will (repeatedly) call `applyArg` again with the actual value
|
|
1690
1835
|
}
|
|
1691
|
-
} else if (key[0] ===
|
|
1692
|
-
|
|
1836
|
+
} else if (key[0] === ".") {
|
|
1837
|
+
// CSS class(es)
|
|
1838
|
+
const classes = key.substring(1).split(".");
|
|
1693
1839
|
if (value) el.classList.add(...classes);
|
|
1694
1840
|
else el.classList.remove(...classes);
|
|
1695
|
-
} else if (key[0] ===
|
|
1841
|
+
} else if (key[0] === "$") {
|
|
1842
|
+
// Style
|
|
1696
1843
|
const name = key.substring(1);
|
|
1697
|
-
if (value==null || value===false) (el as any).style[name] =
|
|
1698
|
-
else (el as any).style[name] =
|
|
1699
|
-
} else if (value == null) {
|
|
1844
|
+
if (value == null || value === false) (el as any).style[name] = "";
|
|
1845
|
+
else (el as any).style[name] = `${value}`;
|
|
1846
|
+
} else if (value == null) {
|
|
1847
|
+
// Value left empty
|
|
1700
1848
|
// Do nothing
|
|
1701
|
-
} else if (key in SPECIAL_PROPS) {
|
|
1849
|
+
} else if (key in SPECIAL_PROPS) {
|
|
1850
|
+
// Special property
|
|
1702
1851
|
SPECIAL_PROPS[key](value);
|
|
1703
|
-
} else if (typeof value ===
|
|
1852
|
+
} else if (typeof value === "function") {
|
|
1853
|
+
// Event listener
|
|
1704
1854
|
el.addEventListener(key, value);
|
|
1705
1855
|
clean(() => el.removeEventListener(key, value));
|
|
1706
|
-
} else if (
|
|
1856
|
+
} else if (
|
|
1857
|
+
value === true ||
|
|
1858
|
+
value === false ||
|
|
1859
|
+
key === "value" ||
|
|
1860
|
+
key === "selectedIndex"
|
|
1861
|
+
) {
|
|
1862
|
+
// DOM property
|
|
1707
1863
|
(el as any)[key] = value;
|
|
1708
|
-
} else {
|
|
1864
|
+
} else {
|
|
1865
|
+
// HTML attribute
|
|
1709
1866
|
el.setAttribute(key, value);
|
|
1710
1867
|
}
|
|
1711
1868
|
}
|
|
1712
1869
|
|
|
1713
1870
|
function defaultOnError(error: Error) {
|
|
1714
|
-
console.error(
|
|
1871
|
+
console.error("Error while in Aberdeen render:", error);
|
|
1715
1872
|
return true;
|
|
1716
1873
|
}
|
|
1717
1874
|
let onError: (error: Error) => boolean | undefined = defaultOnError;
|
|
@@ -1747,7 +1904,7 @@ let onError: (error: Error) => boolean | undefined = defaultOnError;
|
|
|
1747
1904
|
*
|
|
1748
1905
|
* return false; // Suppress default console log and DOM error message
|
|
1749
1906
|
* });
|
|
1750
|
-
*
|
|
1907
|
+
*
|
|
1751
1908
|
* // Styling for our custom error message
|
|
1752
1909
|
* insertCss({
|
|
1753
1910
|
* '.error-message': {
|
|
@@ -1758,7 +1915,7 @@ let onError: (error: Error) => boolean | undefined = defaultOnError;
|
|
|
1758
1915
|
* padding: '2px 4px',
|
|
1759
1916
|
* }
|
|
1760
1917
|
* }, true); // global style
|
|
1761
|
-
*
|
|
1918
|
+
*
|
|
1762
1919
|
* // Cause an error within a render scope.
|
|
1763
1920
|
* $('div.box', () => {
|
|
1764
1921
|
* // Will cause our error handler to insert an error message within the box
|
|
@@ -1766,11 +1923,12 @@ let onError: (error: Error) => boolean | undefined = defaultOnError;
|
|
|
1766
1923
|
* })
|
|
1767
1924
|
* ```
|
|
1768
1925
|
*/
|
|
1769
|
-
export function setErrorHandler(
|
|
1926
|
+
export function setErrorHandler(
|
|
1927
|
+
handler?: (error: Error) => boolean | undefined,
|
|
1928
|
+
) {
|
|
1770
1929
|
onError = handler || defaultOnError;
|
|
1771
1930
|
}
|
|
1772
1931
|
|
|
1773
|
-
|
|
1774
1932
|
/**
|
|
1775
1933
|
* Gets the parent DOM `Element` where nodes created by {@link $} would currently be inserted.
|
|
1776
1934
|
*
|
|
@@ -1799,7 +1957,6 @@ export function getParentElement(): Element {
|
|
|
1799
1957
|
return currentScope.parentElement;
|
|
1800
1958
|
}
|
|
1801
1959
|
|
|
1802
|
-
|
|
1803
1960
|
/**
|
|
1804
1961
|
* Registers a cleanup function to be executed just before the current reactive scope
|
|
1805
1962
|
* is destroyed or redraws.
|
|
@@ -1824,14 +1981,14 @@ export function getParentElement(): Element {
|
|
|
1824
1981
|
* // we don't want to subscribe.
|
|
1825
1982
|
* peek(() => sum.value += item);
|
|
1826
1983
|
* // Clean gets called before each rerun for a certain item index
|
|
1827
|
-
* // No need for peek here, as the clean code doesn't run in an
|
|
1984
|
+
* // No need for peek here, as the clean code doesn't run in an
|
|
1828
1985
|
* // observe scope.
|
|
1829
1986
|
* clean(() => sum.value -= item);
|
|
1830
1987
|
* })
|
|
1831
|
-
*
|
|
1988
|
+
*
|
|
1832
1989
|
* // Show the sum
|
|
1833
1990
|
* $('h1', {text: sum});
|
|
1834
|
-
*
|
|
1991
|
+
*
|
|
1835
1992
|
* // Make random changes to the array
|
|
1836
1993
|
* const rnd = () => 0|(Math.random()*20);
|
|
1837
1994
|
* setInterval(() => myArray[rnd()] = rnd(), 1000);
|
|
@@ -1842,7 +1999,6 @@ export function clean(cleaner: () => void) {
|
|
|
1842
1999
|
currentScope.cleaners.push(cleaner);
|
|
1843
2000
|
}
|
|
1844
2001
|
|
|
1845
|
-
|
|
1846
2002
|
/**
|
|
1847
2003
|
* Creates a reactive scope that automatically re-executes the provided function
|
|
1848
2004
|
* whenever any proxied data (created by {@link proxy}) read during its last execution changes, storing
|
|
@@ -1874,9 +2030,9 @@ export function clean(cleaner: () => void) {
|
|
|
1874
2030
|
* });
|
|
1875
2031
|
* });
|
|
1876
2032
|
* ```
|
|
1877
|
-
*
|
|
2033
|
+
*
|
|
1878
2034
|
* ***Note*** that the above could just as easily be done using `$(func)` instead of `observe(func)`.
|
|
1879
|
-
*
|
|
2035
|
+
*
|
|
1880
2036
|
* @example Observation with return value
|
|
1881
2037
|
* ```typescript
|
|
1882
2038
|
* const counter = proxy(0);
|
|
@@ -1891,8 +2047,8 @@ export function clean(cleaner: () => void) {
|
|
|
1891
2047
|
* @overload
|
|
1892
2048
|
* @param func Func without a return value.
|
|
1893
2049
|
*/
|
|
1894
|
-
export function observe<T
|
|
1895
|
-
return
|
|
2050
|
+
export function observe<T>(func: () => T): ValueRef<T> {
|
|
2051
|
+
return new ResultScope<T>(currentScope.parentElement, func).result;
|
|
1896
2052
|
}
|
|
1897
2053
|
|
|
1898
2054
|
/**
|
|
@@ -1938,7 +2094,7 @@ export function immediateObserve(func: () => void) {
|
|
|
1938
2094
|
* Calls to {@link $} inside `func` will append nodes to `parentElement`.
|
|
1939
2095
|
* You can nest {@link observe} or other {@link $} scopes within `func`.
|
|
1940
2096
|
* Use {@link unmountAll} to clean up all mounted scopes and their DOM nodes.
|
|
1941
|
-
*
|
|
2097
|
+
*
|
|
1942
2098
|
* Mounting scopes happens reactively, meaning that if this function is called from within another
|
|
1943
2099
|
* ({@link observe} or {@link $} or {@link mount}) scope that gets cleaned up, so will the mount.
|
|
1944
2100
|
*
|
|
@@ -1949,7 +2105,7 @@ export function immediateObserve(func: () => void) {
|
|
|
1949
2105
|
* ```javascript
|
|
1950
2106
|
* // Create a pre-existing DOM structure (without Aberdeen)
|
|
1951
2107
|
* document.body.innerHTML = `<h3>Static content <span id="title-extra"></span></h3><div class="box" id="app-root"></div>`;
|
|
1952
|
-
*
|
|
2108
|
+
*
|
|
1953
2109
|
* import { mount, $, proxy } from 'aberdeen';
|
|
1954
2110
|
*
|
|
1955
2111
|
* const runTime = proxy(0);
|
|
@@ -1966,7 +2122,7 @@ export function immediateObserve(func: () => void) {
|
|
|
1966
2122
|
* }
|
|
1967
2123
|
* });
|
|
1968
2124
|
* ```
|
|
1969
|
-
*
|
|
2125
|
+
*
|
|
1970
2126
|
* Note how the inner mount behaves reactively as well, automatically unmounting when it's parent observer scope re-runs.
|
|
1971
2127
|
*/
|
|
1972
2128
|
|
|
@@ -2019,9 +2175,15 @@ export function peek<T>(func: () => T): T {
|
|
|
2019
2175
|
}
|
|
2020
2176
|
|
|
2021
2177
|
/** When using an object as `source`. */
|
|
2022
|
-
export function map<IN,OUT>(
|
|
2178
|
+
export function map<IN, OUT>(
|
|
2179
|
+
source: Record<string | symbol, IN>,
|
|
2180
|
+
func: (value: IN, index: string | symbol) => undefined | OUT,
|
|
2181
|
+
): Record<string | symbol, OUT>;
|
|
2023
2182
|
/** When using an array as `source`. */
|
|
2024
|
-
export function map<IN,OUT>(
|
|
2183
|
+
export function map<IN, OUT>(
|
|
2184
|
+
source: Array<IN>,
|
|
2185
|
+
func: (value: IN, index: number) => undefined | OUT,
|
|
2186
|
+
): Array<OUT>;
|
|
2025
2187
|
/**
|
|
2026
2188
|
* Reactively maps/filters items from a proxied source array or object to a new proxied array or object.
|
|
2027
2189
|
*
|
|
@@ -2065,24 +2227,34 @@ export function map<IN,OUT>(source: Array<IN>, func: (value: IN, index: number)
|
|
|
2065
2227
|
* // activeUserNames becomes proxy({ u1: 'Alice', u2: 'Bob', u3: 'Charlie' })
|
|
2066
2228
|
* ```
|
|
2067
2229
|
*/
|
|
2068
|
-
export function map(
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2230
|
+
export function map(
|
|
2231
|
+
source: any,
|
|
2232
|
+
func: (value: DatumType, key: any) => any,
|
|
2233
|
+
): any {
|
|
2234
|
+
const out = optProxy(source instanceof Array ? [] : {});
|
|
2235
|
+
onEach(source, (item: DatumType, key: symbol | string | number) => {
|
|
2236
|
+
const value = func(item, key);
|
|
2072
2237
|
if (value !== undefined) {
|
|
2073
2238
|
out[key] = value;
|
|
2074
2239
|
clean(() => {
|
|
2075
2240
|
delete out[key];
|
|
2076
|
-
})
|
|
2241
|
+
});
|
|
2077
2242
|
}
|
|
2078
|
-
})
|
|
2079
|
-
return out
|
|
2243
|
+
});
|
|
2244
|
+
return out;
|
|
2080
2245
|
}
|
|
2081
2246
|
|
|
2082
2247
|
/** When using an array as `source`. */
|
|
2083
|
-
export function multiMap<IN,OUT extends {[key: string|symbol]: DatumType}>(
|
|
2248
|
+
export function multiMap<IN, OUT extends { [key: string | symbol]: DatumType }>(
|
|
2249
|
+
source: Array<IN>,
|
|
2250
|
+
func: (value: IN, index: number) => OUT | undefined,
|
|
2251
|
+
): OUT;
|
|
2084
2252
|
/** When using an object as `source`. */
|
|
2085
|
-
export function multiMap<
|
|
2253
|
+
export function multiMap<
|
|
2254
|
+
K extends string | number | symbol,
|
|
2255
|
+
IN,
|
|
2256
|
+
OUT extends { [key: string | symbol]: DatumType },
|
|
2257
|
+
>(source: Record<K, IN>, func: (value: IN, index: K) => OUT | undefined): OUT;
|
|
2086
2258
|
/**
|
|
2087
2259
|
* Reactively maps items from a source proxy (array or object) to a target proxied object,
|
|
2088
2260
|
* where each source item can contribute multiple key-value pairs to the target.
|
|
@@ -2117,31 +2289,44 @@ export function multiMap<K extends string|number|symbol,IN,OUT extends {[key: st
|
|
|
2117
2289
|
* [item.id+item.id]: item.value*10,
|
|
2118
2290
|
* }));
|
|
2119
2291
|
* // itemsById is proxy({ a: 10, aa: 100, b: 20, bb: 200 })
|
|
2120
|
-
*
|
|
2292
|
+
*
|
|
2121
2293
|
* $(() => console.log(itemsById));
|
|
2122
2294
|
*
|
|
2123
2295
|
* items.push({ id: 'c', value: 30 });
|
|
2124
2296
|
* // itemsById becomes proxy({ a: 10, aa: 100, b: 20, bb: 200, c: 30, cc: 300 })
|
|
2125
2297
|
* ```
|
|
2126
2298
|
*/
|
|
2127
|
-
export function multiMap(
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2299
|
+
export function multiMap(
|
|
2300
|
+
source: any,
|
|
2301
|
+
func: (value: DatumType, key: any) => Record<string | symbol, DatumType>,
|
|
2302
|
+
): any {
|
|
2303
|
+
const out = optProxy({});
|
|
2304
|
+
onEach(source, (item: DatumType, key: symbol | string | number) => {
|
|
2305
|
+
const pairs = func(item, key);
|
|
2131
2306
|
if (pairs) {
|
|
2132
|
-
for(
|
|
2307
|
+
for (const key in pairs) out[key] = pairs[key];
|
|
2133
2308
|
clean(() => {
|
|
2134
|
-
for(
|
|
2135
|
-
})
|
|
2309
|
+
for (const key in pairs) delete out[key];
|
|
2310
|
+
});
|
|
2136
2311
|
}
|
|
2137
|
-
})
|
|
2138
|
-
return out
|
|
2312
|
+
});
|
|
2313
|
+
return out;
|
|
2139
2314
|
}
|
|
2140
2315
|
|
|
2141
2316
|
/** When using an object as `array`. */
|
|
2142
|
-
export function partition<OUT_K extends string|number|symbol, IN_V>(
|
|
2317
|
+
export function partition<OUT_K extends string | number | symbol, IN_V>(
|
|
2318
|
+
source: IN_V[],
|
|
2319
|
+
func: (value: IN_V, key: number) => undefined | OUT_K | OUT_K[],
|
|
2320
|
+
): Record<OUT_K, Record<number, IN_V>>;
|
|
2143
2321
|
/** When using an object as `source`. */
|
|
2144
|
-
export function partition<
|
|
2322
|
+
export function partition<
|
|
2323
|
+
IN_K extends string | number | symbol,
|
|
2324
|
+
OUT_K extends string | number | symbol,
|
|
2325
|
+
IN_V,
|
|
2326
|
+
>(
|
|
2327
|
+
source: Record<IN_K, IN_V>,
|
|
2328
|
+
func: (value: IN_V, key: IN_K) => undefined | OUT_K | OUT_K[],
|
|
2329
|
+
): Record<OUT_K, Record<IN_K, IN_V>>;
|
|
2145
2330
|
|
|
2146
2331
|
/**
|
|
2147
2332
|
* @overload
|
|
@@ -2187,10 +2372,10 @@ export function partition<IN_K extends string|number|symbol, OUT_K extends strin
|
|
|
2187
2372
|
* // Partition products by category. Output keys are categories (string).
|
|
2188
2373
|
* // Inner keys are original array indices (number).
|
|
2189
2374
|
* const productsByCategory = partition(products, (product) => product.category);
|
|
2190
|
-
*
|
|
2375
|
+
*
|
|
2191
2376
|
* // Reactively show the data structure
|
|
2192
2377
|
* dump(productsByCategory);
|
|
2193
|
-
*
|
|
2378
|
+
*
|
|
2194
2379
|
* // Make random changes to the categories, to show reactiveness
|
|
2195
2380
|
* setInterval(() => products[0|(Math.random()*3)].category = ['Snack','Fruit','Veg'][0|(Math.random()*3)], 2000);
|
|
2196
2381
|
* ```
|
|
@@ -2210,31 +2395,37 @@ export function partition<IN_K extends string|number|symbol, OUT_K extends strin
|
|
|
2210
2395
|
* console.log(usersByTag);
|
|
2211
2396
|
* ```
|
|
2212
2397
|
*/
|
|
2213
|
-
export function partition<
|
|
2214
|
-
|
|
2398
|
+
export function partition<
|
|
2399
|
+
IN_K extends string | number | symbol,
|
|
2400
|
+
OUT_K extends string | number | symbol,
|
|
2401
|
+
IN_V,
|
|
2402
|
+
>(
|
|
2403
|
+
source: Record<IN_K, IN_V>,
|
|
2404
|
+
func: (value: IN_V, key: IN_K) => undefined | OUT_K | OUT_K[],
|
|
2405
|
+
): Record<OUT_K, Record<IN_K, IN_V>> {
|
|
2406
|
+
const unproxiedOut = {} as Record<OUT_K, Record<IN_K, IN_V>>;
|
|
2215
2407
|
const out = proxy(unproxiedOut);
|
|
2216
2408
|
onEach(source, (item: IN_V, key: IN_K) => {
|
|
2217
|
-
|
|
2409
|
+
const rsp = func(item, key);
|
|
2218
2410
|
if (rsp != null) {
|
|
2219
2411
|
const buckets = rsp instanceof Array ? rsp : [rsp];
|
|
2220
2412
|
if (buckets.length) {
|
|
2221
|
-
for(
|
|
2413
|
+
for (const bucket of buckets) {
|
|
2222
2414
|
if (unproxiedOut[bucket]) out[bucket][key] = item;
|
|
2223
|
-
else out[bucket] = {[key]: item} as Record<IN_K, IN_V>;
|
|
2415
|
+
else out[bucket] = { [key]: item } as Record<IN_K, IN_V>;
|
|
2224
2416
|
}
|
|
2225
2417
|
clean(() => {
|
|
2226
|
-
for(
|
|
2418
|
+
for (const bucket of buckets) {
|
|
2227
2419
|
delete out[bucket][key];
|
|
2228
2420
|
if (isObjEmpty(unproxiedOut[bucket])) delete out[bucket];
|
|
2229
|
-
}
|
|
2230
|
-
})
|
|
2421
|
+
}
|
|
2422
|
+
});
|
|
2231
2423
|
}
|
|
2232
2424
|
}
|
|
2233
|
-
})
|
|
2425
|
+
});
|
|
2234
2426
|
return out;
|
|
2235
2427
|
}
|
|
2236
2428
|
|
|
2237
|
-
|
|
2238
2429
|
/**
|
|
2239
2430
|
* Renders a live, recursive dump of a proxied data structure (or any value)
|
|
2240
2431
|
* into the DOM at the current {@link $} insertion point.
|
|
@@ -2263,28 +2454,28 @@ export function partition<IN_K extends string|number|symbol, OUT_K extends strin
|
|
|
2263
2454
|
* ```
|
|
2264
2455
|
*/
|
|
2265
2456
|
export function dump<T>(data: T): T {
|
|
2266
|
-
if (data && typeof data ===
|
|
2267
|
-
$({text: data instanceof Array ? "<array>" : "<object>"});
|
|
2268
|
-
$(
|
|
2457
|
+
if (data && typeof data === "object") {
|
|
2458
|
+
$({ text: data instanceof Array ? "<array>" : "<object>" });
|
|
2459
|
+
$("ul", () => {
|
|
2269
2460
|
onEach(data as any, (value, key) => {
|
|
2270
|
-
$(
|
|
2271
|
-
dump(value)
|
|
2272
|
-
})
|
|
2273
|
-
})
|
|
2274
|
-
})
|
|
2461
|
+
$(`li:${JSON.stringify(key)}: `, () => {
|
|
2462
|
+
dump(value);
|
|
2463
|
+
});
|
|
2464
|
+
});
|
|
2465
|
+
});
|
|
2275
2466
|
} else {
|
|
2276
|
-
$({text: JSON.stringify(data)})
|
|
2467
|
+
$({ text: JSON.stringify(data) });
|
|
2277
2468
|
}
|
|
2278
|
-
return data
|
|
2469
|
+
return data;
|
|
2279
2470
|
}
|
|
2280
2471
|
|
|
2281
2472
|
/*
|
|
2282
|
-
* Helper functions
|
|
2283
|
-
*/
|
|
2473
|
+
* Helper functions
|
|
2474
|
+
*/
|
|
2284
2475
|
|
|
2285
2476
|
/* c8 ignore start */
|
|
2286
2477
|
function internalError(code: number): never {
|
|
2287
|
-
throw new Error(
|
|
2478
|
+
throw new Error(`Aberdeen internal error ${code}`);
|
|
2288
2479
|
}
|
|
2289
2480
|
/* c8 ignore end */
|
|
2290
2481
|
|
|
@@ -2295,7 +2486,7 @@ function handleError(e: any, showMessage: boolean) {
|
|
|
2295
2486
|
console.error(e);
|
|
2296
2487
|
}
|
|
2297
2488
|
try {
|
|
2298
|
-
if (showMessage) $(
|
|
2489
|
+
if (showMessage) $("div.aberdeen-error:Error");
|
|
2299
2490
|
} catch {
|
|
2300
2491
|
// Error while adding the error marker to the DOM. Apparently, we're in
|
|
2301
2492
|
// an awkward context. The error should already have been logged by
|
|
@@ -2304,7 +2495,15 @@ function handleError(e: any, showMessage: boolean) {
|
|
|
2304
2495
|
}
|
|
2305
2496
|
|
|
2306
2497
|
/** @internal */
|
|
2307
|
-
export function withEmitHandler(
|
|
2498
|
+
export function withEmitHandler(
|
|
2499
|
+
handler: (
|
|
2500
|
+
target: TargetType,
|
|
2501
|
+
index: any,
|
|
2502
|
+
newData: DatumType,
|
|
2503
|
+
oldData: DatumType,
|
|
2504
|
+
) => void,
|
|
2505
|
+
func: () => void,
|
|
2506
|
+
) {
|
|
2308
2507
|
const oldEmitHandler = emit;
|
|
2309
2508
|
emit = handler;
|
|
2310
2509
|
try {
|
|
@@ -2313,12 +2512,3 @@ export function withEmitHandler(handler: (target: TargetType, index: any, newDat
|
|
|
2313
2512
|
emit = oldEmitHandler;
|
|
2314
2513
|
}
|
|
2315
2514
|
}
|
|
2316
|
-
|
|
2317
|
-
// @ts-ignore
|
|
2318
|
-
// c8 ignore next
|
|
2319
|
-
if (!String.prototype.replaceAll) String.prototype.replaceAll = function(from, to) { return this.split(from).join(to) }
|
|
2320
|
-
declare global {
|
|
2321
|
-
interface String {
|
|
2322
|
-
replaceAll(from: string, to: string): string;
|
|
2323
|
-
}
|
|
2324
|
-
}
|