aberdeen 1.0.5 → 1.0.7
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/dist/aberdeen.d.ts +3 -8
- package/dist/aberdeen.js +83 -78
- 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/package.json +2 -1
- package/src/aberdeen.ts +590 -400
- 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/html-to-aberdeen +0 -354
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>
|
|
84
|
+
if (time > 9) 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,26 @@ 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
|
+
|
|
566
|
+
sortedQueue?.remove(this); // This is very fast and O(1) when not queued
|
|
567
|
+
|
|
547
568
|
// Help garbage collection:
|
|
548
569
|
this.byIndex.clear();
|
|
549
570
|
setTimeout(() => {
|
|
550
571
|
// Unsure if this is a good idea. It takes time, but presumably makes things a lot easier for GC...
|
|
551
|
-
this.sortedSet.clear();
|
|
572
|
+
this.sortedSet.clear();
|
|
552
573
|
}, 1);
|
|
553
574
|
}
|
|
554
|
-
|
|
575
|
+
|
|
555
576
|
getLastNode(): Node | undefined {
|
|
556
|
-
for(
|
|
577
|
+
for (const scope of this.sortedSet) {
|
|
578
|
+
// Iterates starting at last child scope.
|
|
557
579
|
const node = scope.getActualLastNode();
|
|
558
580
|
if (node) return node;
|
|
559
581
|
}
|
|
@@ -564,7 +586,7 @@ class OnEachScope extends Scope {
|
|
|
564
586
|
class OnEachItemScope extends ContentScope {
|
|
565
587
|
sortKey: string | number | undefined; // When undefined, this scope is currently not showing in the list
|
|
566
588
|
public parentElement: Element;
|
|
567
|
-
|
|
589
|
+
|
|
568
590
|
constructor(
|
|
569
591
|
public parent: OnEachScope,
|
|
570
592
|
public itemIndex: any,
|
|
@@ -592,7 +614,7 @@ class OnEachItemScope extends ContentScope {
|
|
|
592
614
|
// of the sortedSet now (if we weren't already).
|
|
593
615
|
// This will do nothing and barely take any time of `this` is already part of the set:
|
|
594
616
|
this.parent.sortedSet.add(this);
|
|
595
|
-
|
|
617
|
+
|
|
596
618
|
const preScope = this.parent.sortedSet.prev(this);
|
|
597
619
|
// As preScope should have inserted itself as its first child, this should
|
|
598
620
|
// recursively call getPrecedingNode() on preScope in case it doesn't
|
|
@@ -610,7 +632,7 @@ class OnEachItemScope extends ContentScope {
|
|
|
610
632
|
getActualLastNode(): Node | undefined {
|
|
611
633
|
let child = this.lastChild;
|
|
612
634
|
|
|
613
|
-
while(child && child !== this) {
|
|
635
|
+
while (child && child !== this) {
|
|
614
636
|
if (child instanceof Node) return child;
|
|
615
637
|
const node = child.getLastNode();
|
|
616
638
|
if (node) return node;
|
|
@@ -621,7 +643,7 @@ class OnEachItemScope extends ContentScope {
|
|
|
621
643
|
queueRun() {
|
|
622
644
|
/* c8 ignore next */
|
|
623
645
|
if (currentScope !== ROOT_SCOPE) internalError(4);
|
|
624
|
-
|
|
646
|
+
|
|
625
647
|
// We're not calling `remove` here, as we don't want to remove ourselves from
|
|
626
648
|
// the sorted set. `redraw` will take care of that, if needed.
|
|
627
649
|
// Also, we can't use `getLastNode` here, as we've hacked it to return the
|
|
@@ -633,12 +655,12 @@ class OnEachItemScope extends ContentScope {
|
|
|
633
655
|
|
|
634
656
|
this.delete();
|
|
635
657
|
this.lastChild = this; // apply the hack (see constructor) again
|
|
636
|
-
|
|
658
|
+
|
|
637
659
|
topRedrawScope = this;
|
|
638
660
|
this.redraw();
|
|
639
661
|
topRedrawScope = undefined;
|
|
640
662
|
}
|
|
641
|
-
|
|
663
|
+
|
|
642
664
|
redraw() {
|
|
643
665
|
// Have the makeSortKey function return an ordering int/string/array.
|
|
644
666
|
|
|
@@ -646,22 +668,28 @@ class OnEachItemScope extends ContentScope {
|
|
|
646
668
|
// a wildcard subscription to delete/recreate any scopes when that changes.
|
|
647
669
|
// We ARE creating a proxy around the value though (in case its an object/array),
|
|
648
670
|
// so we'll have our own scope subscribe to changes on that.
|
|
649
|
-
const value: DatumType = optProxy(
|
|
671
|
+
const value: DatumType = optProxy(
|
|
672
|
+
(this.parent.target as any)[this.itemIndex],
|
|
673
|
+
);
|
|
650
674
|
|
|
651
675
|
// Since makeSortKey may get() the Store, we'll need to set currentScope first.
|
|
652
|
-
|
|
676
|
+
const savedScope = currentScope;
|
|
653
677
|
currentScope = this;
|
|
654
|
-
|
|
655
|
-
let sortKey
|
|
678
|
+
|
|
679
|
+
let sortKey: undefined | string | number;
|
|
656
680
|
try {
|
|
657
681
|
if (this.parent.makeSortKey) {
|
|
658
|
-
|
|
659
|
-
if (rawSortKey != null)
|
|
682
|
+
const rawSortKey = this.parent.makeSortKey(value, this.itemIndex);
|
|
683
|
+
if (rawSortKey != null)
|
|
684
|
+
sortKey =
|
|
685
|
+
rawSortKey instanceof Array
|
|
686
|
+
? rawSortKey.map(partToStr).join("")
|
|
687
|
+
: rawSortKey;
|
|
660
688
|
} else {
|
|
661
689
|
sortKey = this.itemIndex;
|
|
662
690
|
}
|
|
663
|
-
if (typeof sortKey ===
|
|
664
|
-
|
|
691
|
+
if (typeof sortKey === "number") sortKey = partToStr(sortKey);
|
|
692
|
+
|
|
665
693
|
if (this.sortKey !== sortKey) {
|
|
666
694
|
// If the sortKey is changed, make sure `this` is removed from the
|
|
667
695
|
// set before setting the new sortKey to it.
|
|
@@ -673,8 +701,8 @@ class OnEachItemScope extends ContentScope {
|
|
|
673
701
|
// in case no nodes are created. We'll do it just-in-time in `getPrecedingNode`.
|
|
674
702
|
|
|
675
703
|
if (sortKey != null) this.parent.renderer(value, this.itemIndex);
|
|
676
|
-
} catch(e) {
|
|
677
|
-
handleError(e, sortKey!=null);
|
|
704
|
+
} catch (e) {
|
|
705
|
+
handleError(e, sortKey != null);
|
|
678
706
|
}
|
|
679
707
|
|
|
680
708
|
currentScope = savedScope;
|
|
@@ -705,64 +733,89 @@ class OnEachItemScope extends ContentScope {
|
|
|
705
733
|
function addNode(node: Node) {
|
|
706
734
|
const parentEl = currentScope.parentElement;
|
|
707
735
|
const prevNode = currentScope.getInsertAfterNode();
|
|
708
|
-
parentEl.insertBefore(
|
|
736
|
+
parentEl.insertBefore(
|
|
737
|
+
node,
|
|
738
|
+
prevNode ? prevNode.nextSibling : parentEl.firstChild,
|
|
739
|
+
);
|
|
709
740
|
currentScope.lastChild = node;
|
|
710
741
|
}
|
|
711
742
|
|
|
712
|
-
|
|
713
743
|
/**
|
|
714
|
-
* This global is set during the execution of a `Scope.render`. It is used by
|
|
715
|
-
* functions like `$` and `clean`.
|
|
716
|
-
*/
|
|
744
|
+
* This global is set during the execution of a `Scope.render`. It is used by
|
|
745
|
+
* functions like `$` and `clean`.
|
|
746
|
+
*/
|
|
717
747
|
const ROOT_SCOPE = new RootScope();
|
|
718
748
|
let currentScope: ContentScope = ROOT_SCOPE;
|
|
719
749
|
|
|
720
750
|
/**
|
|
721
|
-
* A special Node observer index to subscribe to any value in the map changing.
|
|
722
|
-
*/
|
|
723
|
-
const ANY_SYMBOL = Symbol(
|
|
751
|
+
* A special Node observer index to subscribe to any value in the map changing.
|
|
752
|
+
*/
|
|
753
|
+
const ANY_SYMBOL = Symbol("any");
|
|
724
754
|
|
|
725
755
|
/**
|
|
726
756
|
* When our proxy objects need to lookup `obj[TARGET_SYMBOL]` it returns its
|
|
727
757
|
* target, to be used in our wrapped methods.
|
|
728
758
|
*/
|
|
729
|
-
const TARGET_SYMBOL = Symbol(
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
759
|
+
const TARGET_SYMBOL = Symbol("target");
|
|
760
|
+
|
|
761
|
+
const subscribers = new WeakMap<
|
|
762
|
+
TargetType,
|
|
763
|
+
Map<
|
|
764
|
+
any,
|
|
765
|
+
Set<Scope | ((index: any, newData: DatumType, oldData: DatumType) => void)>
|
|
766
|
+
>
|
|
767
|
+
>();
|
|
733
768
|
let peeking = 0; // When > 0, we're not subscribing to any changes
|
|
734
769
|
|
|
735
|
-
function subscribe(
|
|
770
|
+
function subscribe(
|
|
771
|
+
target: any,
|
|
772
|
+
index: symbol | string | number,
|
|
773
|
+
observer:
|
|
774
|
+
| Scope
|
|
775
|
+
| ((
|
|
776
|
+
index: any,
|
|
777
|
+
newData: DatumType,
|
|
778
|
+
oldData: DatumType,
|
|
779
|
+
) => void) = currentScope,
|
|
780
|
+
) {
|
|
736
781
|
if (observer === ROOT_SCOPE || peeking) return;
|
|
737
782
|
|
|
738
783
|
let byTarget = subscribers.get(target);
|
|
739
|
-
if (!byTarget) subscribers.set(target, byTarget = new Map());
|
|
784
|
+
if (!byTarget) subscribers.set(target, (byTarget = new Map()));
|
|
740
785
|
|
|
741
786
|
// No need to subscribe to specific keys if we're already subscribed to ANY
|
|
742
787
|
if (index !== ANY_SYMBOL && byTarget.get(ANY_SYMBOL)?.has(observer)) return;
|
|
743
788
|
|
|
744
789
|
let byIndex = byTarget.get(index);
|
|
745
|
-
if (!byIndex) byTarget.set(index, byIndex = new Set());
|
|
790
|
+
if (!byIndex) byTarget.set(index, (byIndex = new Set()));
|
|
746
791
|
|
|
747
792
|
if (byIndex.has(observer)) return;
|
|
748
793
|
|
|
749
794
|
byIndex.add(observer);
|
|
750
|
-
|
|
795
|
+
|
|
751
796
|
if (observer === currentScope) {
|
|
752
797
|
currentScope.cleaners.push(byIndex);
|
|
753
798
|
} else {
|
|
754
|
-
currentScope.cleaners.push(
|
|
799
|
+
currentScope.cleaners.push(() => {
|
|
755
800
|
byIndex.delete(observer);
|
|
756
801
|
});
|
|
757
802
|
}
|
|
758
803
|
}
|
|
759
804
|
|
|
760
|
-
export function onEach<T>(
|
|
761
|
-
|
|
805
|
+
export function onEach<T>(
|
|
806
|
+
target: Array<undefined | T>,
|
|
807
|
+
render: (value: T, index: number) => void,
|
|
808
|
+
makeKey?: (value: T, key: any) => SortKeyType,
|
|
809
|
+
): void;
|
|
810
|
+
export function onEach<K extends string | number | symbol, T>(
|
|
811
|
+
target: Record<K, undefined | T>,
|
|
812
|
+
render: (value: T, index: K) => void,
|
|
813
|
+
makeKey?: (value: T, key: K) => SortKeyType,
|
|
814
|
+
): void;
|
|
762
815
|
|
|
763
816
|
/**
|
|
764
817
|
* Reactively iterates over the items of an observable array or object, optionally rendering content for each item.
|
|
765
|
-
*
|
|
818
|
+
*
|
|
766
819
|
* Automatically updates when items are added, removed, or modified.
|
|
767
820
|
*
|
|
768
821
|
* @param target The observable array or object to iterate over. Values that are `undefined` are skipped.
|
|
@@ -796,7 +849,7 @@ export function onEach<K extends string|number|symbol,T>(target: Record<K,undefi
|
|
|
796
849
|
* $(`p:${user.name} (id=${user.id})`);
|
|
797
850
|
* }, (user) => [user.group, user.name]); // Sort by group, and within each group sort by name
|
|
798
851
|
* ```
|
|
799
|
-
*
|
|
852
|
+
*
|
|
800
853
|
* @example Iterating an object
|
|
801
854
|
* ```javascript
|
|
802
855
|
* const config = proxy({ theme: 'dark', fontSize: 14, showTips: true });
|
|
@@ -815,15 +868,20 @@ export function onEach<K extends string|number|symbol,T>(target: Record<K,undefi
|
|
|
815
868
|
* ```
|
|
816
869
|
* @see {@link invertString} To easily create keys for reverse sorting.
|
|
817
870
|
*/
|
|
818
|
-
export function onEach(
|
|
819
|
-
|
|
871
|
+
export function onEach(
|
|
872
|
+
target: TargetType,
|
|
873
|
+
render: (value: DatumType, index: any) => void,
|
|
874
|
+
makeKey?: (value: DatumType, key: any) => SortKeyType,
|
|
875
|
+
): void {
|
|
876
|
+
if (!target || typeof target !== "object")
|
|
877
|
+
throw new Error("onEach requires an object");
|
|
820
878
|
target = (target as any)[TARGET_SYMBOL] || target;
|
|
821
879
|
|
|
822
880
|
new OnEachScope(target, render, makeKey);
|
|
823
881
|
}
|
|
824
882
|
|
|
825
883
|
function isObjEmpty(obj: object): boolean {
|
|
826
|
-
for(
|
|
884
|
+
for (const k in obj) return false;
|
|
827
885
|
return true;
|
|
828
886
|
}
|
|
829
887
|
|
|
@@ -864,17 +922,24 @@ export function isEmpty(proxied: TargetType): boolean {
|
|
|
864
922
|
const scope = currentScope;
|
|
865
923
|
|
|
866
924
|
if (target instanceof Array) {
|
|
867
|
-
subscribe(
|
|
868
|
-
|
|
869
|
-
|
|
925
|
+
subscribe(
|
|
926
|
+
target,
|
|
927
|
+
"length",
|
|
928
|
+
(index: any, newData: DatumType, oldData: DatumType) => {
|
|
929
|
+
if (!newData !== !oldData) queue(scope);
|
|
930
|
+
},
|
|
931
|
+
);
|
|
870
932
|
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
933
|
}
|
|
934
|
+
const result = isObjEmpty(target);
|
|
935
|
+
subscribe(
|
|
936
|
+
target,
|
|
937
|
+
ANY_SYMBOL,
|
|
938
|
+
(index: any, newData: DatumType, oldData: DatumType) => {
|
|
939
|
+
if (result ? oldData === undefined : newData === undefined) queue(scope);
|
|
940
|
+
},
|
|
941
|
+
);
|
|
942
|
+
return result;
|
|
878
943
|
}
|
|
879
944
|
|
|
880
945
|
/** @private */
|
|
@@ -909,53 +974,62 @@ export interface ValueRef<T> {
|
|
|
909
974
|
* ```
|
|
910
975
|
*/
|
|
911
976
|
export function count(proxied: TargetType): ValueRef<number> {
|
|
912
|
-
if (proxied instanceof Array) return ref(proxied,
|
|
977
|
+
if (proxied instanceof Array) return ref(proxied, "length");
|
|
913
978
|
|
|
914
979
|
const target = (proxied as any)[TARGET_SYMBOL] || proxied;
|
|
915
980
|
let cnt = 0;
|
|
916
|
-
for(
|
|
917
|
-
|
|
981
|
+
for (const k in target) if (target[k] !== undefined) cnt++;
|
|
982
|
+
|
|
918
983
|
const result = proxy(cnt);
|
|
919
|
-
subscribe(
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
984
|
+
subscribe(
|
|
985
|
+
target,
|
|
986
|
+
ANY_SYMBOL,
|
|
987
|
+
(index: any, newData: DatumType, oldData: DatumType) => {
|
|
988
|
+
if (oldData === newData) {
|
|
989
|
+
} else if (oldData === undefined) result.value = ++cnt;
|
|
990
|
+
else if (newData === undefined) result.value = --cnt;
|
|
991
|
+
},
|
|
992
|
+
);
|
|
924
993
|
|
|
925
994
|
return result;
|
|
926
995
|
}
|
|
927
996
|
|
|
928
997
|
/** @internal */
|
|
929
|
-
export function defaultEmitHandler(
|
|
998
|
+
export function defaultEmitHandler(
|
|
999
|
+
target: TargetType,
|
|
1000
|
+
index: string | symbol | number,
|
|
1001
|
+
newData: DatumType,
|
|
1002
|
+
oldData: DatumType,
|
|
1003
|
+
) {
|
|
930
1004
|
// We're triggering for values changing from undefined to undefined, as this *may*
|
|
931
1005
|
// indicate a change from or to `[empty]` (such as `[,1][0]`).
|
|
932
1006
|
if (newData === oldData && newData !== undefined) return;
|
|
933
|
-
|
|
1007
|
+
|
|
934
1008
|
const byTarget = subscribers.get(target);
|
|
935
|
-
if (byTarget===undefined) return;
|
|
1009
|
+
if (byTarget === undefined) return;
|
|
936
1010
|
|
|
937
|
-
for(const what of [index, ANY_SYMBOL]) {
|
|
938
|
-
|
|
1011
|
+
for (const what of [index, ANY_SYMBOL]) {
|
|
1012
|
+
const byIndex = byTarget.get(what);
|
|
939
1013
|
if (byIndex) {
|
|
940
|
-
for(
|
|
941
|
-
if (typeof observer ===
|
|
942
|
-
else observer.onChange(index, newData, oldData)
|
|
1014
|
+
for (const observer of byIndex) {
|
|
1015
|
+
if (typeof observer === "function") observer(index, newData, oldData);
|
|
1016
|
+
else observer.onChange(index, newData, oldData);
|
|
943
1017
|
}
|
|
944
1018
|
}
|
|
945
1019
|
}
|
|
946
1020
|
}
|
|
947
1021
|
let emit = defaultEmitHandler;
|
|
948
1022
|
|
|
949
|
-
|
|
950
1023
|
const objectHandler: ProxyHandler<any> = {
|
|
951
1024
|
get(target: any, prop: any) {
|
|
952
|
-
if (prop===TARGET_SYMBOL) return target;
|
|
1025
|
+
if (prop === TARGET_SYMBOL) return target;
|
|
953
1026
|
subscribe(target, prop);
|
|
954
1027
|
return optProxy(target[prop]);
|
|
955
1028
|
},
|
|
956
1029
|
set(target: any, prop: any, newData: any) {
|
|
957
1030
|
// Make sure newData is unproxied
|
|
958
|
-
if (typeof newData ===
|
|
1031
|
+
if (typeof newData === "object" && newData)
|
|
1032
|
+
newData = (newData as any)[TARGET_SYMBOL] || newData;
|
|
959
1033
|
const oldData = target[prop];
|
|
960
1034
|
if (newData !== oldData) {
|
|
961
1035
|
target[prop] = newData;
|
|
@@ -979,32 +1053,33 @@ const objectHandler: ProxyHandler<any> = {
|
|
|
979
1053
|
ownKeys(target: any) {
|
|
980
1054
|
subscribe(target, ANY_SYMBOL);
|
|
981
1055
|
return Reflect.ownKeys(target);
|
|
982
|
-
}
|
|
1056
|
+
},
|
|
983
1057
|
};
|
|
984
1058
|
|
|
985
1059
|
function arraySet(target: any, prop: any, newData: any) {
|
|
986
1060
|
// Make sure newData is unproxied
|
|
987
|
-
if (typeof newData ===
|
|
1061
|
+
if (typeof newData === "object" && newData)
|
|
1062
|
+
newData = (newData as any)[TARGET_SYMBOL] || newData;
|
|
988
1063
|
const oldData = target[prop];
|
|
989
1064
|
if (newData !== oldData) {
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
if (prop ===
|
|
1065
|
+
const oldLength = target.length;
|
|
1066
|
+
|
|
1067
|
+
if (prop === "length") {
|
|
993
1068
|
target.length = newData;
|
|
994
1069
|
|
|
995
1070
|
// We only need to emit for shrinking, as growing just adds undefineds
|
|
996
|
-
for(let i=newData; i<oldLength; i++) {
|
|
1071
|
+
for (let i = newData; i < oldLength; i++) {
|
|
997
1072
|
emit(target, i, undefined, target[i]);
|
|
998
1073
|
}
|
|
999
1074
|
} else {
|
|
1000
|
-
const intProp = parseInt(prop)
|
|
1075
|
+
const intProp = Number.parseInt(prop);
|
|
1001
1076
|
if (intProp.toString() === prop) prop = intProp;
|
|
1002
1077
|
|
|
1003
1078
|
target[prop] = newData;
|
|
1004
1079
|
emit(target, prop, newData, oldData);
|
|
1005
1080
|
}
|
|
1006
1081
|
if (target.length !== oldLength) {
|
|
1007
|
-
emit(target,
|
|
1082
|
+
emit(target, "length", target.length, oldLength);
|
|
1008
1083
|
}
|
|
1009
1084
|
runImmediateQueue();
|
|
1010
1085
|
}
|
|
@@ -1013,40 +1088,66 @@ function arraySet(target: any, prop: any, newData: any) {
|
|
|
1013
1088
|
|
|
1014
1089
|
const arrayHandler: ProxyHandler<any[]> = {
|
|
1015
1090
|
get(target: any, prop: any) {
|
|
1016
|
-
if (prop===TARGET_SYMBOL) return target;
|
|
1091
|
+
if (prop === TARGET_SYMBOL) return target;
|
|
1017
1092
|
let subProp = prop;
|
|
1018
|
-
if (typeof prop !==
|
|
1019
|
-
const intProp = parseInt(prop);
|
|
1093
|
+
if (typeof prop !== "symbol") {
|
|
1094
|
+
const intProp = Number.parseInt(prop);
|
|
1020
1095
|
if (intProp.toString() === prop) subProp = intProp;
|
|
1021
1096
|
}
|
|
1022
1097
|
subscribe(target, subProp);
|
|
1023
1098
|
return optProxy(target[prop]);
|
|
1024
1099
|
},
|
|
1025
1100
|
set: arraySet,
|
|
1026
|
-
deleteProperty(target: any, prop: string|symbol) {
|
|
1101
|
+
deleteProperty(target: any, prop: string | symbol) {
|
|
1027
1102
|
return arraySet(target, prop, undefined);
|
|
1028
1103
|
},
|
|
1029
1104
|
};
|
|
1030
1105
|
|
|
1031
|
-
const proxyMap = new WeakMap<TargetType, /*Proxy*/TargetType>();
|
|
1106
|
+
const proxyMap = new WeakMap<TargetType, /*Proxy*/ TargetType>();
|
|
1032
1107
|
|
|
1033
1108
|
function optProxy(value: any): any {
|
|
1034
1109
|
// If value is a primitive type or already proxied, just return it
|
|
1035
|
-
if (
|
|
1110
|
+
if (
|
|
1111
|
+
typeof value !== "object" ||
|
|
1112
|
+
!value ||
|
|
1113
|
+
value[TARGET_SYMBOL] !== undefined
|
|
1114
|
+
) {
|
|
1036
1115
|
return value;
|
|
1037
1116
|
}
|
|
1038
1117
|
let proxied = proxyMap.get(value);
|
|
1039
|
-
if (proxied) return proxied // Only one proxy per target!
|
|
1040
|
-
|
|
1041
|
-
proxied = new Proxy(
|
|
1118
|
+
if (proxied) return proxied; // Only one proxy per target!
|
|
1119
|
+
|
|
1120
|
+
proxied = new Proxy(
|
|
1121
|
+
value,
|
|
1122
|
+
value instanceof Array ? arrayHandler : objectHandler,
|
|
1123
|
+
);
|
|
1042
1124
|
proxyMap.set(value, proxied as TargetType);
|
|
1043
1125
|
return proxied;
|
|
1044
1126
|
}
|
|
1045
1127
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1128
|
+
export function proxy<T extends DatumType>(
|
|
1129
|
+
target: Array<T>,
|
|
1130
|
+
): Array<
|
|
1131
|
+
T extends number
|
|
1132
|
+
? number
|
|
1133
|
+
: T extends string
|
|
1134
|
+
? string
|
|
1135
|
+
: T extends boolean
|
|
1136
|
+
? boolean
|
|
1137
|
+
: T
|
|
1138
|
+
>;
|
|
1048
1139
|
export function proxy<T extends object>(target: T): T;
|
|
1049
|
-
export function proxy<T extends DatumType>(
|
|
1140
|
+
export function proxy<T extends DatumType>(
|
|
1141
|
+
target: T,
|
|
1142
|
+
): ValueRef<
|
|
1143
|
+
T extends number
|
|
1144
|
+
? number
|
|
1145
|
+
: T extends string
|
|
1146
|
+
? string
|
|
1147
|
+
: T extends boolean
|
|
1148
|
+
? boolean
|
|
1149
|
+
: T
|
|
1150
|
+
>;
|
|
1050
1151
|
|
|
1051
1152
|
/**
|
|
1052
1153
|
* Creates a reactive proxy around the given data.
|
|
@@ -1087,7 +1188,7 @@ export function proxy<T extends DatumType>(target: T): ValueRef<T extends number
|
|
|
1087
1188
|
* observe(() => console.log(name.value)); // Subscribes to value
|
|
1088
1189
|
* setTimeout(() => name.value = 'UI', 2000); // Triggers the observe function
|
|
1089
1190
|
* ```
|
|
1090
|
-
*
|
|
1191
|
+
*
|
|
1091
1192
|
* @example Class instance
|
|
1092
1193
|
* ```typescript
|
|
1093
1194
|
* class Widget {
|
|
@@ -1102,7 +1203,9 @@ export function proxy<T extends DatumType>(target: T): ValueRef<T extends number
|
|
|
1102
1203
|
* ```
|
|
1103
1204
|
*/
|
|
1104
1205
|
export function proxy(target: TargetType): TargetType {
|
|
1105
|
-
|
|
1206
|
+
return optProxy(
|
|
1207
|
+
typeof target === "object" && target !== null ? target : { value: target },
|
|
1208
|
+
);
|
|
1106
1209
|
}
|
|
1107
1210
|
|
|
1108
1211
|
/**
|
|
@@ -1125,13 +1228,13 @@ export function proxy(target: TargetType): TargetType {
|
|
|
1125
1228
|
* $(() => console.log('proxied', userProxy.name));
|
|
1126
1229
|
* // The following will only ever log once, as we're not subscribing to any observable
|
|
1127
1230
|
* $(() => console.log('unproxied', rawUser.name));
|
|
1128
|
-
*
|
|
1231
|
+
*
|
|
1129
1232
|
* // This cause the first log to run again:
|
|
1130
1233
|
* setTimeout(() => userProxy.name += '!', 1000);
|
|
1131
|
-
*
|
|
1234
|
+
*
|
|
1132
1235
|
* // This doesn't cause any new logs:
|
|
1133
1236
|
* setTimeout(() => rawUser.name += '?', 2000);
|
|
1134
|
-
*
|
|
1237
|
+
*
|
|
1135
1238
|
* // Both userProxy and rawUser end up as `{name: 'Frank!?'}`
|
|
1136
1239
|
* setTimeout(() => {
|
|
1137
1240
|
* console.log('final proxied', userProxy)
|
|
@@ -1143,10 +1246,11 @@ export function unproxy<T>(target: T): T {
|
|
|
1143
1246
|
return target ? (target as any)[TARGET_SYMBOL] || target : target;
|
|
1144
1247
|
}
|
|
1145
1248
|
|
|
1146
|
-
|
|
1249
|
+
const onDestroyMap: WeakMap<Node, string | ((...args: any[]) => void) | true> =
|
|
1250
|
+
new WeakMap();
|
|
1147
1251
|
|
|
1148
1252
|
function destroyWithClass(element: Element, cls: string) {
|
|
1149
|
-
const classes = cls.split(
|
|
1253
|
+
const classes = cls.split(".").filter((c) => c);
|
|
1150
1254
|
element.classList.add(...classes);
|
|
1151
1255
|
setTimeout(() => element.remove(), 2000);
|
|
1152
1256
|
}
|
|
@@ -1203,7 +1307,7 @@ function destroyWithClass(element: Element, cls: string) {
|
|
|
1203
1307
|
* console.log(source.nested); // [1, 2, 3] (source was modified)
|
|
1204
1308
|
* ```
|
|
1205
1309
|
*/
|
|
1206
|
-
export function copy<T extends object>(dst: T, src: T
|
|
1310
|
+
export function copy<T extends object>(dst: T, src: Partial<T>, flags = 0) {
|
|
1207
1311
|
copyRecurse(dst, src, flags);
|
|
1208
1312
|
runImmediateQueue();
|
|
1209
1313
|
}
|
|
@@ -1216,14 +1320,14 @@ const COPY_EMIT = 64;
|
|
|
1216
1320
|
|
|
1217
1321
|
/**
|
|
1218
1322
|
* Clone an (optionally proxied) object or array.
|
|
1219
|
-
*
|
|
1323
|
+
*
|
|
1220
1324
|
* @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
|
|
1325
|
+
* @param flags
|
|
1222
1326
|
* - {@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
1327
|
* @template T - The type of the objects being copied.
|
|
1224
1328
|
* @returns A new unproxied array or object (of the same type as `src`), containing a deep (by default) copy of `src`.
|
|
1225
1329
|
*/
|
|
1226
|
-
export function clone<T extends object>(src: T, flags
|
|
1330
|
+
export function clone<T extends object>(src: T, flags = 0): T {
|
|
1227
1331
|
const dst = Object.create(Object.getPrototypeOf(src)) as T;
|
|
1228
1332
|
copyRecurse(dst, src, flags);
|
|
1229
1333
|
return dst;
|
|
@@ -1245,87 +1349,94 @@ function copyRecurse(dst: any, src: any, flags: number) {
|
|
|
1245
1349
|
if (currentScope !== ROOT_SCOPE && !peeking) flags |= COPY_SUBSCRIBE;
|
|
1246
1350
|
}
|
|
1247
1351
|
|
|
1248
|
-
if (flags©_SUBSCRIBE) subscribe(src, ANY_SYMBOL);
|
|
1249
|
-
|
|
1250
|
-
|
|
1352
|
+
if (flags & COPY_SUBSCRIBE) subscribe(src, ANY_SYMBOL);
|
|
1353
|
+
if (src instanceof Array) {
|
|
1354
|
+
if (!(dst instanceof Array))
|
|
1355
|
+
throw new Error("Cannot copy array into object");
|
|
1251
1356
|
const dstLen = dst.length;
|
|
1252
1357
|
const srcLen = src.length;
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1358
|
+
for (let i = 0; i < srcLen; i++) {
|
|
1359
|
+
copyValue(dst, src, i, flags);
|
|
1360
|
+
}
|
|
1256
1361
|
// Leaving additional values in the old array doesn't make sense
|
|
1257
1362
|
if (srcLen !== dstLen) {
|
|
1258
|
-
if (flags©_EMIT) {
|
|
1259
|
-
for(let i=srcLen; i<dstLen; i++) {
|
|
1363
|
+
if (flags & COPY_EMIT) {
|
|
1364
|
+
for (let i = srcLen; i < dstLen; i++) {
|
|
1260
1365
|
const old = dst[i];
|
|
1261
1366
|
dst[i] = undefined;
|
|
1262
1367
|
emit(dst, i, undefined, old);
|
|
1263
1368
|
}
|
|
1264
1369
|
dst.length = srcLen;
|
|
1265
|
-
emit(dst,
|
|
1370
|
+
emit(dst, "length", srcLen, dstLen);
|
|
1266
1371
|
} else {
|
|
1267
1372
|
dst.length = srcLen;
|
|
1268
1373
|
}
|
|
1269
1374
|
}
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1375
|
+
} else {
|
|
1376
|
+
for (const k in src) {
|
|
1377
|
+
copyValue(dst, src, k, flags);
|
|
1378
|
+
}
|
|
1274
1379
|
if (!(flags & MERGE)) {
|
|
1275
|
-
for(
|
|
1380
|
+
for (const k in dst) {
|
|
1276
1381
|
if (!(k in src)) {
|
|
1277
1382
|
const old = dst[k];
|
|
1278
1383
|
delete dst[k];
|
|
1279
|
-
if (flags©_EMIT && old !== undefined) {
|
|
1384
|
+
if (flags & COPY_EMIT && old !== undefined) {
|
|
1280
1385
|
emit(dst, k, undefined, old);
|
|
1281
1386
|
}
|
|
1282
1387
|
}
|
|
1283
1388
|
}
|
|
1284
1389
|
}
|
|
1285
|
-
|
|
1390
|
+
}
|
|
1286
1391
|
}
|
|
1287
1392
|
|
|
1288
1393
|
function copyValue(dst: any, src: any, index: any, flags: number) {
|
|
1289
|
-
|
|
1290
|
-
|
|
1394
|
+
const dstValue = dst[index];
|
|
1395
|
+
let srcValue = src[index];
|
|
1291
1396
|
if (srcValue !== dstValue) {
|
|
1292
|
-
if (
|
|
1397
|
+
if (
|
|
1398
|
+
srcValue &&
|
|
1399
|
+
dstValue &&
|
|
1400
|
+
typeof srcValue === "object" &&
|
|
1401
|
+
typeof dstValue === "object" &&
|
|
1402
|
+
(srcValue.constructor === dstValue.constructor ||
|
|
1403
|
+
(flags & MERGE && dstValue instanceof Array))
|
|
1404
|
+
) {
|
|
1293
1405
|
copyRecurse(dstValue, srcValue, flags);
|
|
1294
1406
|
return;
|
|
1295
1407
|
}
|
|
1296
|
-
|
|
1297
|
-
if (!(flags&SHALLOW) && srcValue && typeof srcValue ===
|
|
1408
|
+
|
|
1409
|
+
if (!(flags & SHALLOW) && srcValue && typeof srcValue === "object") {
|
|
1298
1410
|
// Create an empty object of the same type
|
|
1299
|
-
|
|
1411
|
+
const copy = Object.create(Object.getPrototypeOf(srcValue));
|
|
1300
1412
|
// Copy all properties to it. This doesn't need to emit anything
|
|
1301
1413
|
// and MERGE does not apply as this is a new branch.
|
|
1302
1414
|
copyRecurse(copy, srcValue, 0);
|
|
1303
1415
|
srcValue = copy;
|
|
1304
1416
|
}
|
|
1305
1417
|
const old = dst[index];
|
|
1306
|
-
if (flags&MERGE && srcValue == null) delete dst[index];
|
|
1418
|
+
if (flags & MERGE && srcValue == null) delete dst[index];
|
|
1307
1419
|
else dst[index] = srcValue;
|
|
1308
|
-
if (flags©_EMIT) emit(dst, index, srcValue, old)
|
|
1420
|
+
if (flags & COPY_EMIT) emit(dst, index, srcValue, old);
|
|
1309
1421
|
}
|
|
1310
1422
|
}
|
|
1311
1423
|
|
|
1312
|
-
|
|
1313
1424
|
interface RefTarget {
|
|
1314
|
-
proxy: TargetType
|
|
1315
|
-
index: any
|
|
1425
|
+
proxy: TargetType;
|
|
1426
|
+
index: any;
|
|
1316
1427
|
}
|
|
1317
1428
|
const refHandler: ProxyHandler<RefTarget> = {
|
|
1318
1429
|
get(target: RefTarget, prop: any) {
|
|
1319
|
-
if (prop===TARGET_SYMBOL) {
|
|
1430
|
+
if (prop === TARGET_SYMBOL) {
|
|
1320
1431
|
// Create a ref to the unproxied version of the target
|
|
1321
1432
|
return ref(unproxy(target.proxy), target.index);
|
|
1322
1433
|
}
|
|
1323
|
-
if (prop==="value") {
|
|
1434
|
+
if (prop === "value") {
|
|
1324
1435
|
return (target.proxy as any)[target.index];
|
|
1325
1436
|
}
|
|
1326
1437
|
},
|
|
1327
1438
|
set(target: any, prop: any, value: any) {
|
|
1328
|
-
if (prop==="value") {
|
|
1439
|
+
if (prop === "value") {
|
|
1329
1440
|
(target.proxy as any)[target.index] = value;
|
|
1330
1441
|
return true;
|
|
1331
1442
|
}
|
|
@@ -1333,7 +1444,6 @@ const refHandler: ProxyHandler<RefTarget> = {
|
|
|
1333
1444
|
},
|
|
1334
1445
|
};
|
|
1335
1446
|
|
|
1336
|
-
|
|
1337
1447
|
/**
|
|
1338
1448
|
* Creates a reactive reference (`{ value: T }`-like object) to a specific value
|
|
1339
1449
|
* within a proxied object or array.
|
|
@@ -1364,80 +1474,101 @@ const refHandler: ProxyHandler<RefTarget> = {
|
|
|
1364
1474
|
* text: ref(formData, 'color'),
|
|
1365
1475
|
* $color: ref(formData, 'color')
|
|
1366
1476
|
* });
|
|
1367
|
-
*
|
|
1477
|
+
*
|
|
1368
1478
|
* // Changes are actually stored in formData - this causes logs like `{color: "Blue", velocity 42}`
|
|
1369
1479
|
* $(() => console.log(formData))
|
|
1370
1480
|
* ```
|
|
1371
1481
|
*/
|
|
1372
|
-
export function ref<T extends TargetType, K extends keyof T>(
|
|
1373
|
-
|
|
1482
|
+
export function ref<T extends TargetType, K extends keyof T>(
|
|
1483
|
+
target: T,
|
|
1484
|
+
index: K,
|
|
1485
|
+
): ValueRef<T[K]> {
|
|
1486
|
+
return new Proxy({ proxy: target, index }, refHandler) as any as ValueRef<
|
|
1487
|
+
T[K]
|
|
1488
|
+
>;
|
|
1374
1489
|
}
|
|
1375
1490
|
|
|
1376
|
-
|
|
1377
1491
|
function applyBind(el: HTMLInputElement, target: any) {
|
|
1378
1492
|
let onProxyChange: () => void;
|
|
1379
1493
|
let onInputChange: () => void;
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
if (type ===
|
|
1494
|
+
const type = el.getAttribute("type");
|
|
1495
|
+
const value = unproxy(target).value;
|
|
1496
|
+
if (type === "checkbox") {
|
|
1383
1497
|
if (value === undefined) target.value = el.checked;
|
|
1384
|
-
onProxyChange = () =>
|
|
1385
|
-
|
|
1386
|
-
|
|
1498
|
+
onProxyChange = () => {
|
|
1499
|
+
el.checked = target.value;
|
|
1500
|
+
};
|
|
1501
|
+
onInputChange = () => {
|
|
1502
|
+
target.value = el.checked;
|
|
1503
|
+
};
|
|
1504
|
+
} else if (type === "radio") {
|
|
1387
1505
|
if (value === undefined && el.checked) target.value = el.value;
|
|
1388
|
-
onProxyChange = () =>
|
|
1506
|
+
onProxyChange = () => {
|
|
1507
|
+
el.checked = target.value === el.value;
|
|
1508
|
+
};
|
|
1389
1509
|
onInputChange = () => {
|
|
1390
1510
|
if (el.checked) target.value = el.value;
|
|
1391
|
-
}
|
|
1511
|
+
};
|
|
1392
1512
|
} else {
|
|
1393
|
-
onInputChange = () =>
|
|
1513
|
+
onInputChange = () => {
|
|
1514
|
+
target.value =
|
|
1515
|
+
type === "number" || type === "range"
|
|
1516
|
+
? el.value === ""
|
|
1517
|
+
? null
|
|
1518
|
+
: +el.value
|
|
1519
|
+
: el.value;
|
|
1520
|
+
};
|
|
1394
1521
|
if (value === undefined) onInputChange();
|
|
1395
1522
|
onProxyChange = () => {
|
|
1396
1523
|
el.value = target.value;
|
|
1397
|
-
|
|
1398
|
-
|
|
1524
|
+
// biome-ignore lint/suspicious/noDoubleEquals: it's fine for numbers to be casts to strings here
|
|
1525
|
+
if (el.tagName === "SELECT" && el.value != target.value)
|
|
1526
|
+
throw new Error(`SELECT has no '${target.value}' OPTION (yet)`);
|
|
1527
|
+
};
|
|
1399
1528
|
}
|
|
1400
1529
|
observe(onProxyChange);
|
|
1401
|
-
el.addEventListener(
|
|
1530
|
+
el.addEventListener("input", onInputChange);
|
|
1402
1531
|
clean(() => {
|
|
1403
|
-
el.removeEventListener(
|
|
1532
|
+
el.removeEventListener("input", onInputChange);
|
|
1404
1533
|
});
|
|
1405
1534
|
}
|
|
1406
1535
|
|
|
1407
|
-
const SPECIAL_PROPS: {[key: string]: (value: any) => void} = {
|
|
1408
|
-
create:
|
|
1536
|
+
const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
|
|
1537
|
+
create: (value: any) => {
|
|
1409
1538
|
const el = currentScope.parentElement;
|
|
1410
1539
|
if (currentScope !== topRedrawScope) return;
|
|
1411
|
-
if (typeof value ===
|
|
1540
|
+
if (typeof value === "function") {
|
|
1412
1541
|
value(el);
|
|
1413
1542
|
} else {
|
|
1414
|
-
const classes = value.split(
|
|
1543
|
+
const classes = value.split(".").filter((c: any) => c);
|
|
1415
1544
|
el.classList.add(...classes);
|
|
1416
|
-
(async
|
|
1545
|
+
(async () => {
|
|
1546
|
+
// attempt to prevent layout trashing
|
|
1417
1547
|
(el as HTMLElement).offsetHeight; // trigger layout
|
|
1418
1548
|
el.classList.remove(...classes);
|
|
1419
1549
|
})();
|
|
1420
1550
|
}
|
|
1421
1551
|
},
|
|
1422
|
-
destroy:
|
|
1552
|
+
destroy: (value: any) => {
|
|
1423
1553
|
const el = currentScope.parentElement;
|
|
1424
1554
|
onDestroyMap.set(el, value);
|
|
1425
1555
|
},
|
|
1426
|
-
html:
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1556
|
+
html: (value: any) => {
|
|
1557
|
+
const tmpParent = document.createElement(
|
|
1558
|
+
currentScope.parentElement.tagName,
|
|
1559
|
+
);
|
|
1560
|
+
tmpParent.innerHTML = `${value}`;
|
|
1561
|
+
while (tmpParent.firstChild) addNode(tmpParent.firstChild);
|
|
1430
1562
|
},
|
|
1431
|
-
text:
|
|
1563
|
+
text: (value: any) => {
|
|
1432
1564
|
addNode(document.createTextNode(value));
|
|
1433
1565
|
},
|
|
1434
|
-
element:
|
|
1435
|
-
if (!(value instanceof Node))
|
|
1566
|
+
element: (value: any) => {
|
|
1567
|
+
if (!(value instanceof Node))
|
|
1568
|
+
throw new Error(`Unexpected element-argument: ${JSON.parse(value)}`);
|
|
1436
1569
|
addNode(value);
|
|
1437
1570
|
},
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
|
|
1571
|
+
};
|
|
1441
1572
|
|
|
1442
1573
|
/**
|
|
1443
1574
|
* The core function for building reactive user interfaces in Aberdeen. It creates and inserts new DOM elements
|
|
@@ -1485,12 +1616,12 @@ const SPECIAL_PROPS: {[key: string]: (value: any) => void} = {
|
|
|
1485
1616
|
* $color: 'red'
|
|
1486
1617
|
* });
|
|
1487
1618
|
* ```
|
|
1488
|
-
*
|
|
1619
|
+
*
|
|
1489
1620
|
* @example Create Nested Elements
|
|
1490
1621
|
* ```typescript
|
|
1491
1622
|
* let inputElement: Element = $('label:Click me', 'input', {type: 'checkbox'});
|
|
1492
1623
|
* // You should usually not touch raw DOM elements, unless when integrating
|
|
1493
|
-
* // with non-Aberdeen code.
|
|
1624
|
+
* // with non-Aberdeen code.
|
|
1494
1625
|
* console.log('DOM element:', inputElement);
|
|
1495
1626
|
* ```
|
|
1496
1627
|
*
|
|
@@ -1525,62 +1656,71 @@ const SPECIAL_PROPS: {[key: string]: (value: any) => void} = {
|
|
|
1525
1656
|
* ```
|
|
1526
1657
|
*/
|
|
1527
1658
|
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1659
|
+
export function $(
|
|
1660
|
+
...args: (
|
|
1661
|
+
| string
|
|
1662
|
+
| null
|
|
1663
|
+
| undefined
|
|
1664
|
+
| false
|
|
1665
|
+
| (() => void)
|
|
1666
|
+
| Record<string, any>
|
|
1667
|
+
)[]
|
|
1668
|
+
): undefined | Element {
|
|
1669
|
+
let savedCurrentScope: undefined | ContentScope;
|
|
1670
|
+
let err: undefined | string;
|
|
1671
|
+
let result: undefined | Element;
|
|
1672
|
+
|
|
1673
|
+
for (let arg of args) {
|
|
1535
1674
|
if (arg == null || arg === false) continue;
|
|
1536
|
-
if (typeof arg ===
|
|
1537
|
-
let text
|
|
1538
|
-
|
|
1675
|
+
if (typeof arg === "string") {
|
|
1676
|
+
let text: undefined | string;
|
|
1677
|
+
let classes: undefined | string;
|
|
1678
|
+
const textPos = arg.indexOf(":");
|
|
1539
1679
|
if (textPos >= 0) {
|
|
1540
|
-
text = arg.substring(textPos+1);
|
|
1541
|
-
arg = arg.substring(0,textPos);
|
|
1680
|
+
text = arg.substring(textPos + 1);
|
|
1681
|
+
arg = arg.substring(0, textPos);
|
|
1542
1682
|
}
|
|
1543
|
-
const classPos = arg.indexOf(
|
|
1683
|
+
const classPos = arg.indexOf(".");
|
|
1544
1684
|
if (classPos >= 0) {
|
|
1545
|
-
classes = arg.substring(classPos+1);
|
|
1685
|
+
classes = arg.substring(classPos + 1);
|
|
1546
1686
|
arg = arg.substring(0, classPos);
|
|
1547
1687
|
}
|
|
1548
|
-
if (arg ===
|
|
1688
|
+
if (arg === "") {
|
|
1689
|
+
// Add text or classes to parent
|
|
1549
1690
|
if (text) addNode(document.createTextNode(text));
|
|
1550
1691
|
if (classes) {
|
|
1551
1692
|
const el = currentScope.parentElement;
|
|
1552
|
-
el.classList.add(...classes.split(
|
|
1693
|
+
el.classList.add(...classes.split("."));
|
|
1553
1694
|
if (!savedCurrentScope) {
|
|
1554
|
-
clean(() => el.classList.remove(...classes.split(
|
|
1695
|
+
clean(() => el.classList.remove(...classes.split(".")));
|
|
1555
1696
|
}
|
|
1556
1697
|
}
|
|
1557
|
-
} else if (arg.indexOf(
|
|
1698
|
+
} else if (arg.indexOf(" ") >= 0) {
|
|
1558
1699
|
err = `Tag '${arg}' cannot contain space`;
|
|
1559
1700
|
break;
|
|
1560
1701
|
} else {
|
|
1561
1702
|
result = document.createElement(arg);
|
|
1562
|
-
if (classes) result.className = classes.replaceAll(
|
|
1703
|
+
if (classes) result.className = classes.replaceAll(".", " ");
|
|
1563
1704
|
if (text) result.textContent = text;
|
|
1564
1705
|
addNode(result);
|
|
1565
1706
|
if (!savedCurrentScope) {
|
|
1566
1707
|
savedCurrentScope = currentScope;
|
|
1567
1708
|
}
|
|
1568
|
-
|
|
1709
|
+
const newScope = new ChainedScope(result, true);
|
|
1569
1710
|
newScope.lastChild = result.lastChild || undefined;
|
|
1570
1711
|
if (topRedrawScope === currentScope) topRedrawScope = newScope;
|
|
1571
1712
|
currentScope = newScope;
|
|
1572
1713
|
}
|
|
1573
|
-
}
|
|
1574
|
-
else if (typeof arg === 'object') {
|
|
1714
|
+
} else if (typeof arg === "object") {
|
|
1575
1715
|
if (arg.constructor !== Object) {
|
|
1576
1716
|
err = `Unexpected argument: ${arg}`;
|
|
1577
1717
|
break;
|
|
1578
1718
|
}
|
|
1579
|
-
for(const key in arg) {
|
|
1719
|
+
for (const key in arg) {
|
|
1580
1720
|
const val = arg[key];
|
|
1581
1721
|
applyArg(key, val);
|
|
1582
1722
|
}
|
|
1583
|
-
} else if (typeof arg ===
|
|
1723
|
+
} else if (typeof arg === "function") {
|
|
1584
1724
|
new RegularScope(currentScope.parentElement, arg);
|
|
1585
1725
|
} else {
|
|
1586
1726
|
err = `Unexpected argument: ${arg}`;
|
|
@@ -1649,71 +1789,88 @@ let cssCount = 0;
|
|
|
1649
1789
|
* color: "#107ab0",
|
|
1650
1790
|
* }
|
|
1651
1791
|
* }, true); // Pass true for global
|
|
1652
|
-
*
|
|
1792
|
+
*
|
|
1653
1793
|
* $('a:Styled link');
|
|
1654
1794
|
* ```
|
|
1655
1795
|
*/
|
|
1656
|
-
export function insertCss(style: object, global
|
|
1657
|
-
const prefix = global ? "" :
|
|
1658
|
-
|
|
1659
|
-
if (css) $(
|
|
1796
|
+
export function insertCss(style: object, global = false): string {
|
|
1797
|
+
const prefix = global ? "" : `.AbdStl${++cssCount}`;
|
|
1798
|
+
const css = styleToCss(style, prefix);
|
|
1799
|
+
if (css) $(`style:${css}`);
|
|
1660
1800
|
return prefix;
|
|
1661
1801
|
}
|
|
1662
1802
|
|
|
1663
1803
|
function styleToCss(style: object, prefix: string): string {
|
|
1664
|
-
let props =
|
|
1665
|
-
let rules =
|
|
1666
|
-
for(const kOr in style) {
|
|
1804
|
+
let props = "";
|
|
1805
|
+
let rules = "";
|
|
1806
|
+
for (const kOr in style) {
|
|
1667
1807
|
const v = (style as any)[kOr];
|
|
1668
|
-
for(const k of kOr.split(/, ?/g)) {
|
|
1669
|
-
if (v && typeof v ===
|
|
1670
|
-
if (k.startsWith(
|
|
1671
|
-
|
|
1808
|
+
for (const k of kOr.split(/, ?/g)) {
|
|
1809
|
+
if (v && typeof v === "object") {
|
|
1810
|
+
if (k.startsWith("@")) {
|
|
1811
|
+
// media queries
|
|
1812
|
+
rules += `${k}{\n${styleToCss(v, prefix)}}\n`;
|
|
1672
1813
|
} else {
|
|
1673
|
-
rules += styleToCss(
|
|
1814
|
+
rules += styleToCss(
|
|
1815
|
+
v,
|
|
1816
|
+
k.includes("&") ? k.replace(/&/g, prefix) : `${prefix} ${k}`,
|
|
1817
|
+
);
|
|
1674
1818
|
}
|
|
1675
1819
|
} else {
|
|
1676
|
-
props += k.replace(/[A-Z]/g, letter =>
|
|
1820
|
+
props += `${k.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}:${v};`;
|
|
1677
1821
|
}
|
|
1678
1822
|
}
|
|
1679
1823
|
}
|
|
1680
|
-
if (props) rules =
|
|
1824
|
+
if (props) rules = `${prefix.trimStart() || "*"}{${props}}\n${rules}`;
|
|
1681
1825
|
return rules;
|
|
1682
1826
|
}
|
|
1683
1827
|
|
|
1684
1828
|
function applyArg(key: string, value: any) {
|
|
1685
1829
|
const el = currentScope.parentElement;
|
|
1686
|
-
if (typeof value ===
|
|
1687
|
-
|
|
1688
|
-
|
|
1830
|
+
if (typeof value === "object" && value !== null && value[TARGET_SYMBOL]) {
|
|
1831
|
+
// Value is a proxy
|
|
1832
|
+
if (key === "bind") {
|
|
1833
|
+
applyBind(el as HTMLInputElement, value);
|
|
1689
1834
|
} else {
|
|
1690
|
-
new SetArgScope(el, key, value)
|
|
1835
|
+
new SetArgScope(el, key, value);
|
|
1691
1836
|
// SetArgScope will (repeatedly) call `applyArg` again with the actual value
|
|
1692
1837
|
}
|
|
1693
|
-
} else if (key[0] ===
|
|
1694
|
-
|
|
1838
|
+
} else if (key[0] === ".") {
|
|
1839
|
+
// CSS class(es)
|
|
1840
|
+
const classes = key.substring(1).split(".");
|
|
1695
1841
|
if (value) el.classList.add(...classes);
|
|
1696
1842
|
else el.classList.remove(...classes);
|
|
1697
|
-
} else if (key[0] ===
|
|
1843
|
+
} else if (key[0] === "$") {
|
|
1844
|
+
// Style
|
|
1698
1845
|
const name = key.substring(1);
|
|
1699
|
-
if (value==null || value===false) (el as any).style[name] =
|
|
1700
|
-
else (el as any).style[name] =
|
|
1701
|
-
} else if (value == null) {
|
|
1846
|
+
if (value == null || value === false) (el as any).style[name] = "";
|
|
1847
|
+
else (el as any).style[name] = `${value}`;
|
|
1848
|
+
} else if (value == null) {
|
|
1849
|
+
// Value left empty
|
|
1702
1850
|
// Do nothing
|
|
1703
|
-
} else if (key in SPECIAL_PROPS) {
|
|
1851
|
+
} else if (key in SPECIAL_PROPS) {
|
|
1852
|
+
// Special property
|
|
1704
1853
|
SPECIAL_PROPS[key](value);
|
|
1705
|
-
} else if (typeof value ===
|
|
1854
|
+
} else if (typeof value === "function") {
|
|
1855
|
+
// Event listener
|
|
1706
1856
|
el.addEventListener(key, value);
|
|
1707
1857
|
clean(() => el.removeEventListener(key, value));
|
|
1708
|
-
} else if (
|
|
1858
|
+
} else if (
|
|
1859
|
+
value === true ||
|
|
1860
|
+
value === false ||
|
|
1861
|
+
key === "value" ||
|
|
1862
|
+
key === "selectedIndex"
|
|
1863
|
+
) {
|
|
1864
|
+
// DOM property
|
|
1709
1865
|
(el as any)[key] = value;
|
|
1710
|
-
} else {
|
|
1866
|
+
} else {
|
|
1867
|
+
// HTML attribute
|
|
1711
1868
|
el.setAttribute(key, value);
|
|
1712
1869
|
}
|
|
1713
1870
|
}
|
|
1714
1871
|
|
|
1715
1872
|
function defaultOnError(error: Error) {
|
|
1716
|
-
console.error(
|
|
1873
|
+
console.error("Error while in Aberdeen render:", error);
|
|
1717
1874
|
return true;
|
|
1718
1875
|
}
|
|
1719
1876
|
let onError: (error: Error) => boolean | undefined = defaultOnError;
|
|
@@ -1749,7 +1906,7 @@ let onError: (error: Error) => boolean | undefined = defaultOnError;
|
|
|
1749
1906
|
*
|
|
1750
1907
|
* return false; // Suppress default console log and DOM error message
|
|
1751
1908
|
* });
|
|
1752
|
-
*
|
|
1909
|
+
*
|
|
1753
1910
|
* // Styling for our custom error message
|
|
1754
1911
|
* insertCss({
|
|
1755
1912
|
* '.error-message': {
|
|
@@ -1760,7 +1917,7 @@ let onError: (error: Error) => boolean | undefined = defaultOnError;
|
|
|
1760
1917
|
* padding: '2px 4px',
|
|
1761
1918
|
* }
|
|
1762
1919
|
* }, true); // global style
|
|
1763
|
-
*
|
|
1920
|
+
*
|
|
1764
1921
|
* // Cause an error within a render scope.
|
|
1765
1922
|
* $('div.box', () => {
|
|
1766
1923
|
* // Will cause our error handler to insert an error message within the box
|
|
@@ -1768,11 +1925,12 @@ let onError: (error: Error) => boolean | undefined = defaultOnError;
|
|
|
1768
1925
|
* })
|
|
1769
1926
|
* ```
|
|
1770
1927
|
*/
|
|
1771
|
-
export function setErrorHandler(
|
|
1928
|
+
export function setErrorHandler(
|
|
1929
|
+
handler?: (error: Error) => boolean | undefined,
|
|
1930
|
+
) {
|
|
1772
1931
|
onError = handler || defaultOnError;
|
|
1773
1932
|
}
|
|
1774
1933
|
|
|
1775
|
-
|
|
1776
1934
|
/**
|
|
1777
1935
|
* Gets the parent DOM `Element` where nodes created by {@link $} would currently be inserted.
|
|
1778
1936
|
*
|
|
@@ -1801,7 +1959,6 @@ export function getParentElement(): Element {
|
|
|
1801
1959
|
return currentScope.parentElement;
|
|
1802
1960
|
}
|
|
1803
1961
|
|
|
1804
|
-
|
|
1805
1962
|
/**
|
|
1806
1963
|
* Registers a cleanup function to be executed just before the current reactive scope
|
|
1807
1964
|
* is destroyed or redraws.
|
|
@@ -1826,14 +1983,14 @@ export function getParentElement(): Element {
|
|
|
1826
1983
|
* // we don't want to subscribe.
|
|
1827
1984
|
* peek(() => sum.value += item);
|
|
1828
1985
|
* // Clean gets called before each rerun for a certain item index
|
|
1829
|
-
* // No need for peek here, as the clean code doesn't run in an
|
|
1986
|
+
* // No need for peek here, as the clean code doesn't run in an
|
|
1830
1987
|
* // observe scope.
|
|
1831
1988
|
* clean(() => sum.value -= item);
|
|
1832
1989
|
* })
|
|
1833
|
-
*
|
|
1990
|
+
*
|
|
1834
1991
|
* // Show the sum
|
|
1835
1992
|
* $('h1', {text: sum});
|
|
1836
|
-
*
|
|
1993
|
+
*
|
|
1837
1994
|
* // Make random changes to the array
|
|
1838
1995
|
* const rnd = () => 0|(Math.random()*20);
|
|
1839
1996
|
* setInterval(() => myArray[rnd()] = rnd(), 1000);
|
|
@@ -1844,7 +2001,6 @@ export function clean(cleaner: () => void) {
|
|
|
1844
2001
|
currentScope.cleaners.push(cleaner);
|
|
1845
2002
|
}
|
|
1846
2003
|
|
|
1847
|
-
|
|
1848
2004
|
/**
|
|
1849
2005
|
* Creates a reactive scope that automatically re-executes the provided function
|
|
1850
2006
|
* whenever any proxied data (created by {@link proxy}) read during its last execution changes, storing
|
|
@@ -1876,9 +2032,9 @@ export function clean(cleaner: () => void) {
|
|
|
1876
2032
|
* });
|
|
1877
2033
|
* });
|
|
1878
2034
|
* ```
|
|
1879
|
-
*
|
|
2035
|
+
*
|
|
1880
2036
|
* ***Note*** that the above could just as easily be done using `$(func)` instead of `observe(func)`.
|
|
1881
|
-
*
|
|
2037
|
+
*
|
|
1882
2038
|
* @example Observation with return value
|
|
1883
2039
|
* ```typescript
|
|
1884
2040
|
* const counter = proxy(0);
|
|
@@ -1893,8 +2049,8 @@ export function clean(cleaner: () => void) {
|
|
|
1893
2049
|
* @overload
|
|
1894
2050
|
* @param func Func without a return value.
|
|
1895
2051
|
*/
|
|
1896
|
-
export function observe<T
|
|
1897
|
-
return
|
|
2052
|
+
export function observe<T>(func: () => T): ValueRef<T> {
|
|
2053
|
+
return new ResultScope<T>(currentScope.parentElement, func).result;
|
|
1898
2054
|
}
|
|
1899
2055
|
|
|
1900
2056
|
/**
|
|
@@ -1940,7 +2096,7 @@ export function immediateObserve(func: () => void) {
|
|
|
1940
2096
|
* Calls to {@link $} inside `func` will append nodes to `parentElement`.
|
|
1941
2097
|
* You can nest {@link observe} or other {@link $} scopes within `func`.
|
|
1942
2098
|
* Use {@link unmountAll} to clean up all mounted scopes and their DOM nodes.
|
|
1943
|
-
*
|
|
2099
|
+
*
|
|
1944
2100
|
* Mounting scopes happens reactively, meaning that if this function is called from within another
|
|
1945
2101
|
* ({@link observe} or {@link $} or {@link mount}) scope that gets cleaned up, so will the mount.
|
|
1946
2102
|
*
|
|
@@ -1951,7 +2107,7 @@ export function immediateObserve(func: () => void) {
|
|
|
1951
2107
|
* ```javascript
|
|
1952
2108
|
* // Create a pre-existing DOM structure (without Aberdeen)
|
|
1953
2109
|
* document.body.innerHTML = `<h3>Static content <span id="title-extra"></span></h3><div class="box" id="app-root"></div>`;
|
|
1954
|
-
*
|
|
2110
|
+
*
|
|
1955
2111
|
* import { mount, $, proxy } from 'aberdeen';
|
|
1956
2112
|
*
|
|
1957
2113
|
* const runTime = proxy(0);
|
|
@@ -1968,7 +2124,7 @@ export function immediateObserve(func: () => void) {
|
|
|
1968
2124
|
* }
|
|
1969
2125
|
* });
|
|
1970
2126
|
* ```
|
|
1971
|
-
*
|
|
2127
|
+
*
|
|
1972
2128
|
* Note how the inner mount behaves reactively as well, automatically unmounting when it's parent observer scope re-runs.
|
|
1973
2129
|
*/
|
|
1974
2130
|
|
|
@@ -2021,9 +2177,15 @@ export function peek<T>(func: () => T): T {
|
|
|
2021
2177
|
}
|
|
2022
2178
|
|
|
2023
2179
|
/** When using an object as `source`. */
|
|
2024
|
-
export function map<IN,OUT>(
|
|
2180
|
+
export function map<IN, OUT>(
|
|
2181
|
+
source: Record<string | symbol, IN>,
|
|
2182
|
+
func: (value: IN, index: string | symbol) => undefined | OUT,
|
|
2183
|
+
): Record<string | symbol, OUT>;
|
|
2025
2184
|
/** When using an array as `source`. */
|
|
2026
|
-
export function map<IN,OUT>(
|
|
2185
|
+
export function map<IN, OUT>(
|
|
2186
|
+
source: Array<IN>,
|
|
2187
|
+
func: (value: IN, index: number) => undefined | OUT,
|
|
2188
|
+
): Array<OUT>;
|
|
2027
2189
|
/**
|
|
2028
2190
|
* Reactively maps/filters items from a proxied source array or object to a new proxied array or object.
|
|
2029
2191
|
*
|
|
@@ -2067,24 +2229,34 @@ export function map<IN,OUT>(source: Array<IN>, func: (value: IN, index: number)
|
|
|
2067
2229
|
* // activeUserNames becomes proxy({ u1: 'Alice', u2: 'Bob', u3: 'Charlie' })
|
|
2068
2230
|
* ```
|
|
2069
2231
|
*/
|
|
2070
|
-
export function map(
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2232
|
+
export function map(
|
|
2233
|
+
source: any,
|
|
2234
|
+
func: (value: DatumType, key: any) => any,
|
|
2235
|
+
): any {
|
|
2236
|
+
const out = optProxy(source instanceof Array ? [] : {});
|
|
2237
|
+
onEach(source, (item: DatumType, key: symbol | string | number) => {
|
|
2238
|
+
const value = func(item, key);
|
|
2074
2239
|
if (value !== undefined) {
|
|
2075
2240
|
out[key] = value;
|
|
2076
2241
|
clean(() => {
|
|
2077
2242
|
delete out[key];
|
|
2078
|
-
})
|
|
2243
|
+
});
|
|
2079
2244
|
}
|
|
2080
|
-
})
|
|
2081
|
-
return out
|
|
2245
|
+
});
|
|
2246
|
+
return out;
|
|
2082
2247
|
}
|
|
2083
2248
|
|
|
2084
2249
|
/** When using an array as `source`. */
|
|
2085
|
-
export function multiMap<IN,OUT extends {[key: string|symbol]: DatumType}>(
|
|
2250
|
+
export function multiMap<IN, OUT extends { [key: string | symbol]: DatumType }>(
|
|
2251
|
+
source: Array<IN>,
|
|
2252
|
+
func: (value: IN, index: number) => OUT | undefined,
|
|
2253
|
+
): OUT;
|
|
2086
2254
|
/** When using an object as `source`. */
|
|
2087
|
-
export function multiMap<
|
|
2255
|
+
export function multiMap<
|
|
2256
|
+
K extends string | number | symbol,
|
|
2257
|
+
IN,
|
|
2258
|
+
OUT extends { [key: string | symbol]: DatumType },
|
|
2259
|
+
>(source: Record<K, IN>, func: (value: IN, index: K) => OUT | undefined): OUT;
|
|
2088
2260
|
/**
|
|
2089
2261
|
* Reactively maps items from a source proxy (array or object) to a target proxied object,
|
|
2090
2262
|
* where each source item can contribute multiple key-value pairs to the target.
|
|
@@ -2119,31 +2291,44 @@ export function multiMap<K extends string|number|symbol,IN,OUT extends {[key: st
|
|
|
2119
2291
|
* [item.id+item.id]: item.value*10,
|
|
2120
2292
|
* }));
|
|
2121
2293
|
* // itemsById is proxy({ a: 10, aa: 100, b: 20, bb: 200 })
|
|
2122
|
-
*
|
|
2294
|
+
*
|
|
2123
2295
|
* $(() => console.log(itemsById));
|
|
2124
2296
|
*
|
|
2125
2297
|
* items.push({ id: 'c', value: 30 });
|
|
2126
2298
|
* // itemsById becomes proxy({ a: 10, aa: 100, b: 20, bb: 200, c: 30, cc: 300 })
|
|
2127
2299
|
* ```
|
|
2128
2300
|
*/
|
|
2129
|
-
export function multiMap(
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2301
|
+
export function multiMap(
|
|
2302
|
+
source: any,
|
|
2303
|
+
func: (value: DatumType, key: any) => Record<string | symbol, DatumType>,
|
|
2304
|
+
): any {
|
|
2305
|
+
const out = optProxy({});
|
|
2306
|
+
onEach(source, (item: DatumType, key: symbol | string | number) => {
|
|
2307
|
+
const pairs = func(item, key);
|
|
2133
2308
|
if (pairs) {
|
|
2134
|
-
for(
|
|
2309
|
+
for (const key in pairs) out[key] = pairs[key];
|
|
2135
2310
|
clean(() => {
|
|
2136
|
-
for(
|
|
2137
|
-
})
|
|
2311
|
+
for (const key in pairs) delete out[key];
|
|
2312
|
+
});
|
|
2138
2313
|
}
|
|
2139
|
-
})
|
|
2140
|
-
return out
|
|
2314
|
+
});
|
|
2315
|
+
return out;
|
|
2141
2316
|
}
|
|
2142
2317
|
|
|
2143
2318
|
/** When using an object as `array`. */
|
|
2144
|
-
export function partition<OUT_K extends string|number|symbol, IN_V>(
|
|
2319
|
+
export function partition<OUT_K extends string | number | symbol, IN_V>(
|
|
2320
|
+
source: IN_V[],
|
|
2321
|
+
func: (value: IN_V, key: number) => undefined | OUT_K | OUT_K[],
|
|
2322
|
+
): Record<OUT_K, Record<number, IN_V>>;
|
|
2145
2323
|
/** When using an object as `source`. */
|
|
2146
|
-
export function partition<
|
|
2324
|
+
export function partition<
|
|
2325
|
+
IN_K extends string | number | symbol,
|
|
2326
|
+
OUT_K extends string | number | symbol,
|
|
2327
|
+
IN_V,
|
|
2328
|
+
>(
|
|
2329
|
+
source: Record<IN_K, IN_V>,
|
|
2330
|
+
func: (value: IN_V, key: IN_K) => undefined | OUT_K | OUT_K[],
|
|
2331
|
+
): Record<OUT_K, Record<IN_K, IN_V>>;
|
|
2147
2332
|
|
|
2148
2333
|
/**
|
|
2149
2334
|
* @overload
|
|
@@ -2189,10 +2374,10 @@ export function partition<IN_K extends string|number|symbol, OUT_K extends strin
|
|
|
2189
2374
|
* // Partition products by category. Output keys are categories (string).
|
|
2190
2375
|
* // Inner keys are original array indices (number).
|
|
2191
2376
|
* const productsByCategory = partition(products, (product) => product.category);
|
|
2192
|
-
*
|
|
2377
|
+
*
|
|
2193
2378
|
* // Reactively show the data structure
|
|
2194
2379
|
* dump(productsByCategory);
|
|
2195
|
-
*
|
|
2380
|
+
*
|
|
2196
2381
|
* // Make random changes to the categories, to show reactiveness
|
|
2197
2382
|
* setInterval(() => products[0|(Math.random()*3)].category = ['Snack','Fruit','Veg'][0|(Math.random()*3)], 2000);
|
|
2198
2383
|
* ```
|
|
@@ -2212,31 +2397,37 @@ export function partition<IN_K extends string|number|symbol, OUT_K extends strin
|
|
|
2212
2397
|
* console.log(usersByTag);
|
|
2213
2398
|
* ```
|
|
2214
2399
|
*/
|
|
2215
|
-
export function partition<
|
|
2216
|
-
|
|
2400
|
+
export function partition<
|
|
2401
|
+
IN_K extends string | number | symbol,
|
|
2402
|
+
OUT_K extends string | number | symbol,
|
|
2403
|
+
IN_V,
|
|
2404
|
+
>(
|
|
2405
|
+
source: Record<IN_K, IN_V>,
|
|
2406
|
+
func: (value: IN_V, key: IN_K) => undefined | OUT_K | OUT_K[],
|
|
2407
|
+
): Record<OUT_K, Record<IN_K, IN_V>> {
|
|
2408
|
+
const unproxiedOut = {} as Record<OUT_K, Record<IN_K, IN_V>>;
|
|
2217
2409
|
const out = proxy(unproxiedOut);
|
|
2218
2410
|
onEach(source, (item: IN_V, key: IN_K) => {
|
|
2219
|
-
|
|
2411
|
+
const rsp = func(item, key);
|
|
2220
2412
|
if (rsp != null) {
|
|
2221
2413
|
const buckets = rsp instanceof Array ? rsp : [rsp];
|
|
2222
2414
|
if (buckets.length) {
|
|
2223
|
-
for(
|
|
2415
|
+
for (const bucket of buckets) {
|
|
2224
2416
|
if (unproxiedOut[bucket]) out[bucket][key] = item;
|
|
2225
|
-
else out[bucket] = {[key]: item} as Record<IN_K, IN_V>;
|
|
2417
|
+
else out[bucket] = { [key]: item } as Record<IN_K, IN_V>;
|
|
2226
2418
|
}
|
|
2227
2419
|
clean(() => {
|
|
2228
|
-
for(
|
|
2420
|
+
for (const bucket of buckets) {
|
|
2229
2421
|
delete out[bucket][key];
|
|
2230
2422
|
if (isObjEmpty(unproxiedOut[bucket])) delete out[bucket];
|
|
2231
|
-
}
|
|
2232
|
-
})
|
|
2423
|
+
}
|
|
2424
|
+
});
|
|
2233
2425
|
}
|
|
2234
2426
|
}
|
|
2235
|
-
})
|
|
2427
|
+
});
|
|
2236
2428
|
return out;
|
|
2237
2429
|
}
|
|
2238
2430
|
|
|
2239
|
-
|
|
2240
2431
|
/**
|
|
2241
2432
|
* Renders a live, recursive dump of a proxied data structure (or any value)
|
|
2242
2433
|
* into the DOM at the current {@link $} insertion point.
|
|
@@ -2265,28 +2456,28 @@ export function partition<IN_K extends string|number|symbol, OUT_K extends strin
|
|
|
2265
2456
|
* ```
|
|
2266
2457
|
*/
|
|
2267
2458
|
export function dump<T>(data: T): T {
|
|
2268
|
-
if (data && typeof data ===
|
|
2269
|
-
$({text: data instanceof Array ? "<array>" : "<object>"});
|
|
2270
|
-
$(
|
|
2459
|
+
if (data && typeof data === "object") {
|
|
2460
|
+
$({ text: data instanceof Array ? "<array>" : "<object>" });
|
|
2461
|
+
$("ul", () => {
|
|
2271
2462
|
onEach(data as any, (value, key) => {
|
|
2272
|
-
$(
|
|
2273
|
-
dump(value)
|
|
2274
|
-
})
|
|
2275
|
-
})
|
|
2276
|
-
})
|
|
2463
|
+
$(`li:${JSON.stringify(key)}: `, () => {
|
|
2464
|
+
dump(value);
|
|
2465
|
+
});
|
|
2466
|
+
});
|
|
2467
|
+
});
|
|
2277
2468
|
} else {
|
|
2278
|
-
$({text: JSON.stringify(data)})
|
|
2469
|
+
$({ text: JSON.stringify(data) });
|
|
2279
2470
|
}
|
|
2280
|
-
return data
|
|
2471
|
+
return data;
|
|
2281
2472
|
}
|
|
2282
2473
|
|
|
2283
2474
|
/*
|
|
2284
|
-
* Helper functions
|
|
2285
|
-
*/
|
|
2475
|
+
* Helper functions
|
|
2476
|
+
*/
|
|
2286
2477
|
|
|
2287
2478
|
/* c8 ignore start */
|
|
2288
2479
|
function internalError(code: number): never {
|
|
2289
|
-
throw new Error(
|
|
2480
|
+
throw new Error(`Aberdeen internal error ${code}`);
|
|
2290
2481
|
}
|
|
2291
2482
|
/* c8 ignore end */
|
|
2292
2483
|
|
|
@@ -2297,7 +2488,7 @@ function handleError(e: any, showMessage: boolean) {
|
|
|
2297
2488
|
console.error(e);
|
|
2298
2489
|
}
|
|
2299
2490
|
try {
|
|
2300
|
-
if (showMessage) $(
|
|
2491
|
+
if (showMessage) $("div.aberdeen-error:Error");
|
|
2301
2492
|
} catch {
|
|
2302
2493
|
// Error while adding the error marker to the DOM. Apparently, we're in
|
|
2303
2494
|
// an awkward context. The error should already have been logged by
|
|
@@ -2306,7 +2497,15 @@ function handleError(e: any, showMessage: boolean) {
|
|
|
2306
2497
|
}
|
|
2307
2498
|
|
|
2308
2499
|
/** @internal */
|
|
2309
|
-
export function withEmitHandler(
|
|
2500
|
+
export function withEmitHandler(
|
|
2501
|
+
handler: (
|
|
2502
|
+
target: TargetType,
|
|
2503
|
+
index: any,
|
|
2504
|
+
newData: DatumType,
|
|
2505
|
+
oldData: DatumType,
|
|
2506
|
+
) => void,
|
|
2507
|
+
func: () => void,
|
|
2508
|
+
) {
|
|
2310
2509
|
const oldEmitHandler = emit;
|
|
2311
2510
|
emit = handler;
|
|
2312
2511
|
try {
|
|
@@ -2315,12 +2514,3 @@ export function withEmitHandler(handler: (target: TargetType, index: any, newDat
|
|
|
2315
2514
|
emit = oldEmitHandler;
|
|
2316
2515
|
}
|
|
2317
2516
|
}
|
|
2318
|
-
|
|
2319
|
-
// @ts-ignore
|
|
2320
|
-
// c8 ignore next
|
|
2321
|
-
if (!String.prototype.replaceAll) String.prototype.replaceAll = function(from, to) { return this.split(from).join(to) }
|
|
2322
|
-
declare global {
|
|
2323
|
-
interface String {
|
|
2324
|
-
replaceAll(from: string, to: string): string;
|
|
2325
|
-
}
|
|
2326
|
-
}
|