aberdeen 0.1.2 → 0.2.1

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.
Files changed (58) hide show
  1. package/.github/workflows/deploy.yml +43 -0
  2. package/.vscode/launch.json +23 -0
  3. package/README.md +11 -5
  4. package/{dist → dist-min}/aberdeen.d.ts +28 -153
  5. package/dist-min/aberdeen.js +2 -0
  6. package/dist-min/aberdeen.js.map +1 -0
  7. package/dist-min/prediction.d.ts +29 -0
  8. package/dist-min/prediction.js +2 -0
  9. package/dist-min/prediction.js.map +1 -0
  10. package/dist-min/route.d.ts +16 -0
  11. package/dist-min/route.js +2 -0
  12. package/dist-min/route.js.map +1 -0
  13. package/dist-min/transitions.d.ts +18 -0
  14. package/dist-min/transitions.js +2 -0
  15. package/dist-min/transitions.js.map +1 -0
  16. package/examples/input/index.html +8 -0
  17. package/examples/input/input.css +56 -0
  18. package/examples/input/input.js +66 -0
  19. package/examples/list/index.html +7 -0
  20. package/examples/list/list.js +47 -0
  21. package/examples/router/index.html +8 -0
  22. package/examples/router/page-home.js +12 -0
  23. package/examples/router/page-list.js +35 -0
  24. package/examples/router/page-settings.js +6 -0
  25. package/examples/router/router.js +76 -0
  26. package/examples/router/style.css +88 -0
  27. package/examples/tic-tac-toe/index.html +8 -0
  28. package/examples/tic-tac-toe/tic-tac-toe.css +50 -0
  29. package/examples/tic-tac-toe/tic-tac-toe.js +90 -0
  30. package/package.json +35 -27
  31. package/src/aberdeen.ts +2037 -0
  32. package/src/prediction.ts +117 -0
  33. package/src/route.ts +121 -0
  34. package/src/transitions.ts +73 -0
  35. package/tests/_fakedom.js +255 -0
  36. package/tests/_init.js +81 -0
  37. package/tests/array.js +109 -0
  38. package/tests/binding.js +106 -0
  39. package/tests/browsers.js +22 -0
  40. package/tests/clean.js +26 -0
  41. package/tests/count.js +105 -0
  42. package/tests/create.js +92 -0
  43. package/tests/destroy.js +270 -0
  44. package/tests/dom.js +219 -0
  45. package/tests/errors.js +114 -0
  46. package/tests/immediate.js +87 -0
  47. package/tests/map.js +76 -0
  48. package/tests/objmap.js +40 -0
  49. package/tests/onEach.js +392 -0
  50. package/tests/prediction.js +97 -0
  51. package/tests/props.js +49 -0
  52. package/tests/schedule.js +44 -0
  53. package/tests/scope.js +277 -0
  54. package/tests/sort.js +105 -0
  55. package/tests/store.js +254 -0
  56. package/tsconfig.json +67 -0
  57. package/dist/aberdeen.js +0 -1842
  58. package/dist/aberdeen.min.js +0 -1
package/dist/aberdeen.js DELETED
@@ -1,1842 +0,0 @@
1
- let queueArray = [];
2
- let queueSet = new Set();
3
- let queueOrdered = true;
4
- let runQueueDepth = 0;
5
- let queueIndex;
6
- let recordingPatch;
7
- function queue(runner) {
8
- if (queueSet.has(runner))
9
- return;
10
- if (runQueueDepth > 42) {
11
- throw new Error("Too many recursive updates from observes");
12
- }
13
- if (!queueArray.length) {
14
- setTimeout(runQueue, 0);
15
- }
16
- else if (runner.queueOrder < queueArray[queueArray.length - 1].queueOrder) {
17
- queueOrdered = false;
18
- }
19
- queueArray.push(runner);
20
- queueSet.add(runner);
21
- }
22
- function runQueue() {
23
- onCreateEnabled = true;
24
- for (queueIndex = 0; queueIndex < queueArray.length;) {
25
- // Sort queue if new unordered items have been added since last time.
26
- if (!queueOrdered) {
27
- queueArray.splice(0, queueIndex);
28
- queueIndex = 0;
29
- // Order queued observers by depth, lowest first.
30
- queueArray.sort((a, b) => a.queueOrder - b.queueOrder);
31
- queueOrdered = true;
32
- }
33
- // Process the rest of what's currently in the queue.
34
- let batchEndIndex = queueArray.length;
35
- for (; queueIndex < batchEndIndex && queueOrdered; queueIndex++) {
36
- let runner = queueArray[queueIndex];
37
- queueSet.delete(runner);
38
- runner.queueRun();
39
- }
40
- // If new items have been added to the queue while processing the previous
41
- // batch, we'll need to run this loop again.
42
- runQueueDepth++;
43
- }
44
- queueArray.length = 0;
45
- queueIndex = undefined;
46
- runQueueDepth = 0;
47
- onCreateEnabled = false;
48
- }
49
- let scheduleOrder = 1000;
50
- /**
51
- * Schedule a DOM read operation to be executed in Aberdeen's internal task queue.
52
- *
53
- * This function is used to batch DOM read operations together, avoiding unnecessary
54
- * layout recalculations and improving browser performance. A DOM read operation should
55
- * only *read* from the DOM, such as measuring element dimensions or retrieving computed styles.
56
- *
57
- * By batching DOM reads separately from DOM writes, this prevents the browser from
58
- * interleaving layout reads and writes, which can force additional layout recalculations.
59
- * This helps reduce visual glitches and flashes by ensuring the browser doesn't render
60
- * intermediate DOM states during updates.
61
- *
62
- * Unlike `setTimeout` or `requestAnimationFrame`, this mechanism ensures that DOM read
63
- * operations happen before any DOM writes in the same queue cycle, minimizing layout thrashing.
64
- *
65
- * @param func The function to be executed as a DOM read operation.
66
- */
67
- export function scheduleDomReader(func) {
68
- let order = (queueIndex != null && queueIndex < queueArray.length && queueArray[queueIndex].queueOrder >= 1000) ? ((queueArray[queueIndex].queueOrder + 1) & (~1)) : 1000;
69
- queue({ queueOrder: order, queueRun: func });
70
- }
71
- /**
72
- * Schedule a DOM write operation to be executed in Aberdeen's internal task queue.
73
- *
74
- * This function is used to batch DOM write operations together, avoiding unnecessary
75
- * layout recalculations and improving browser performance. A DOM write operation should
76
- * only *write* to the DOM, such as modifying element properties or applying styles.
77
- *
78
- * By batching DOM writes separately from DOM reads, this prevents the browser from
79
- * interleaving layout reads and writes, which can force additional layout recalculations.
80
- * This helps reduce visual glitches and flashes by ensuring the browser doesn't render
81
- * intermediate DOM states during updates.
82
- *
83
- * Unlike `setTimeout` or `requestAnimationFrame`, this mechanism ensures that DOM write
84
- * operations happen after all DOM reads in the same queue cycle, minimizing layout thrashing.
85
- *
86
- * @param func The function to be executed as a DOM write operation.
87
- */
88
- export function scheduleDomWriter(func) {
89
- let order = (queueIndex != null && queueIndex < queueArray.length && queueArray[queueIndex].queueOrder >= 1000) ? (queueArray[queueIndex].queueOrder | 1) : 1001;
90
- queue({ queueOrder: order, queueRun: func });
91
- }
92
- /**
93
- * Given an integer number, a string or an array of these, this function returns a string that can be used
94
- * to compare items in a natural sorting order. So `[3, 'ab']` should be smaller than `[3, 'ac']`.
95
- * The resulting string is guaranteed to never be empty.
96
- */
97
- function sortKeyToString(key) {
98
- if (key instanceof Array) {
99
- return key.map(partToStr).join('');
100
- }
101
- else {
102
- return partToStr(key);
103
- }
104
- }
105
- function partToStr(part) {
106
- if (typeof part === 'string') {
107
- return part + '\x01';
108
- }
109
- else {
110
- let result = numToString(Math.abs(Math.round(part)), part < 0);
111
- // Prefix the number of digits, counting down from 128 for negative and up for positive
112
- return String.fromCharCode(128 + (part > 0 ? result.length : -result.length)) + result;
113
- }
114
- }
115
- function numToString(num, neg) {
116
- let result = '';
117
- while (num > 0) {
118
- /*
119
- * We're reserving a few character codes:
120
- * 0 - for compatibility
121
- * 1 - separator between array items
122
- * 65535 - for compatibility
123
- */
124
- result += String.fromCharCode(neg ? 65535 - (num % 65533) : 2 + (num % 65533));
125
- num = Math.floor(num / 65533);
126
- }
127
- return result;
128
- }
129
- /*
130
- * Scope
131
- *
132
- * A `Scope` is created with a `render` function that is run initially,
133
- * and again when any of the `Store`s that this function reads are changed. Any
134
- * DOM elements that is given a `render` function for its contents has its own scope.
135
- * The `Scope` manages the position in the DOM tree elements created by `render`
136
- * are inserted at. Before a rerender, all previously created elements are removed
137
- * and the `clean` functions for the scope and all sub-scopes are called.
138
- */
139
- class Scope {
140
- constructor(parentElement, precedingSibling, queueOrder) {
141
- // The list of clean functions to be called when this scope is cleaned. These can
142
- // be for child scopes, subscriptions as well as `clean(..)` hooks.
143
- this.cleaners = [];
144
- // Set to true after the scope has been cleaned, causing any spurious reruns to
145
- // be ignored.
146
- this.isDead = false;
147
- this.parentElement = parentElement;
148
- this.precedingSibling = precedingSibling;
149
- this.queueOrder = queueOrder;
150
- }
151
- // Get a reference to the last Node preceding this Scope, or undefined if there is none
152
- findPrecedingNode(stopAt = undefined) {
153
- let cur = this;
154
- let pre;
155
- while ((pre = cur.precedingSibling) && pre !== stopAt) {
156
- if (pre instanceof Node)
157
- return pre;
158
- let node = pre.findLastNode();
159
- if (node)
160
- return node;
161
- cur = pre;
162
- }
163
- }
164
- // Get a reference to the last Node within this scope and parentElement
165
- findLastNode() {
166
- if (this.lastChild) {
167
- if (this.lastChild instanceof Node)
168
- return this.lastChild;
169
- else
170
- return this.lastChild.findLastNode() || this.lastChild.findPrecedingNode(this.precedingSibling);
171
- }
172
- }
173
- addNode(node) {
174
- if (!this.parentElement)
175
- throw new ScopeError(true);
176
- let prevNode = this.findLastNode() || this.findPrecedingNode();
177
- this.parentElement.insertBefore(node, prevNode ? prevNode.nextSibling : this.parentElement.firstChild);
178
- this.lastChild = node;
179
- }
180
- remove() {
181
- if (this.parentElement) {
182
- let lastNode = this.findLastNode();
183
- if (lastNode) {
184
- // at least one DOM node to be removed
185
- let nextNode = this.findPrecedingNode();
186
- nextNode = (nextNode ? nextNode.nextSibling : this.parentElement.firstChild);
187
- this.lastChild = undefined;
188
- // Keep removing DOM nodes starting at our first node, until we encounter the last node
189
- while (true) {
190
- /* istanbul ignore next */
191
- if (!nextNode)
192
- return internalError(1);
193
- const node = nextNode;
194
- nextNode = node.nextSibling || undefined;
195
- let onDestroy = onDestroyMap.get(node);
196
- if (onDestroy && node instanceof Element) {
197
- if (onDestroy !== true) {
198
- if (typeof onDestroy === 'function') {
199
- onDestroy(node);
200
- }
201
- else {
202
- destroyWithClass(node, onDestroy);
203
- }
204
- // This causes the element to be ignored from this function from now on:
205
- onDestroyMap.set(node, true);
206
- }
207
- // Ignore the deleting element
208
- }
209
- else {
210
- this.parentElement.removeChild(node);
211
- }
212
- if (node === lastNode)
213
- break;
214
- }
215
- }
216
- }
217
- // run cleaners
218
- this._clean();
219
- }
220
- _clean() {
221
- this.isDead = true;
222
- for (let cleaner of this.cleaners) {
223
- cleaner._clean(this);
224
- }
225
- this.cleaners.length = 0;
226
- }
227
- onChange(index, newData, oldData) {
228
- queue(this);
229
- }
230
- }
231
- class SimpleScope extends Scope {
232
- constructor(parentElement, precedingSibling, queueOrder, renderer) {
233
- super(parentElement, precedingSibling, queueOrder);
234
- this.renderer = renderer;
235
- }
236
- queueRun() {
237
- /* istanbul ignore next */
238
- if (currentScope) {
239
- internalError(2);
240
- }
241
- if (this.isDead)
242
- return;
243
- this.remove();
244
- this.isDead = false;
245
- this.update();
246
- }
247
- update() {
248
- let savedScope = currentScope;
249
- currentScope = this;
250
- try {
251
- this.renderer();
252
- }
253
- catch (e) {
254
- // Throw the error async, so the rest of the rendering can continue
255
- handleError(e);
256
- }
257
- currentScope = savedScope;
258
- }
259
- }
260
- class IsEmptyObserver {
261
- constructor(scope, collection, triggerCount) {
262
- this.scope = scope;
263
- this.collection = collection;
264
- this.triggerCount = triggerCount;
265
- this.count = collection.getCount();
266
- collection.addObserver(ANY_INDEX, this);
267
- scope.cleaners.push(this);
268
- }
269
- onChange(index, newData, oldData) {
270
- if (newData === undefined) {
271
- // oldData is guaranteed not to be undefined
272
- if (this.triggerCount || !--this.count)
273
- queue(this.scope);
274
- }
275
- else if (oldData === undefined) {
276
- if (this.triggerCount || !this.count++)
277
- queue(this.scope);
278
- }
279
- }
280
- _clean() {
281
- this.collection.removeObserver(ANY_INDEX, this);
282
- }
283
- }
284
- class OnEachScope extends Scope {
285
- constructor(parentElement, precedingSibling, queueOrder, collection, renderer, makeSortKey) {
286
- super(parentElement, precedingSibling, queueOrder);
287
- /** The ordered list of currently item scopes */
288
- this.byPosition = [];
289
- /** The item scopes in a Map by index */
290
- this.byIndex = new Map();
291
- /** Indexes that have been created/removed and need to be handled in the next `queueRun` */
292
- this.newIndexes = new Set();
293
- this.removedIndexes = new Set();
294
- this.collection = collection;
295
- this.renderer = renderer;
296
- this.makeSortKey = makeSortKey;
297
- }
298
- // toString(): string {
299
- // return `OnEachScope(collection=${this.collection})`
300
- // }
301
- onChange(index, newData, oldData) {
302
- if (oldData === undefined) {
303
- if (this.removedIndexes.has(index)) {
304
- this.removedIndexes.delete(index);
305
- }
306
- else {
307
- this.newIndexes.add(index);
308
- queue(this);
309
- }
310
- }
311
- else if (newData === undefined) {
312
- if (this.newIndexes.has(index)) {
313
- this.newIndexes.delete(index);
314
- }
315
- else {
316
- this.removedIndexes.add(index);
317
- queue(this);
318
- }
319
- }
320
- }
321
- queueRun() {
322
- if (this.isDead)
323
- return;
324
- let indexes = this.removedIndexes;
325
- this.removedIndexes = new Set();
326
- indexes.forEach(index => {
327
- this.removeChild(index);
328
- });
329
- indexes = this.newIndexes;
330
- this.newIndexes = new Set();
331
- indexes.forEach(index => {
332
- this.addChild(index);
333
- });
334
- }
335
- _clean() {
336
- super._clean();
337
- this.collection.observers.delete(this);
338
- for (const [index, scope] of this.byIndex) {
339
- scope._clean();
340
- }
341
- // Help garbage collection:
342
- this.byPosition.length = 0;
343
- this.byIndex.clear();
344
- }
345
- renderInitial() {
346
- /* istanbul ignore next */
347
- if (!currentScope) {
348
- return internalError(3);
349
- }
350
- let parentScope = currentScope;
351
- this.collection.iterateIndexes(this);
352
- currentScope = parentScope;
353
- }
354
- addChild(itemIndex) {
355
- let scope = new OnEachItemScope(this.parentElement, undefined, this.queueOrder + 1, this, itemIndex);
356
- this.byIndex.set(itemIndex, scope);
357
- scope.update();
358
- // We're not adding a cleaner here, as we'll be calling them from our _clean function
359
- }
360
- removeChild(itemIndex) {
361
- let scope = this.byIndex.get(itemIndex);
362
- /* istanbul ignore next */
363
- if (!scope) {
364
- return internalError(6);
365
- }
366
- scope.remove();
367
- this.byIndex.delete(itemIndex);
368
- this.removeFromPosition(scope);
369
- }
370
- findPosition(sortStr) {
371
- // In case of duplicate `sortStr`s, this will return the first match.
372
- let items = this.byPosition;
373
- let min = 0, max = items.length;
374
- // Fast-path for elements that are already ordered (as is the case when working with arrays ordered by index)
375
- if (!max || sortStr > items[max - 1].sortStr)
376
- return max;
377
- // Binary search for the insert position
378
- while (min < max) {
379
- let mid = (min + max) >> 1;
380
- if (items[mid].sortStr < sortStr) {
381
- min = mid + 1;
382
- }
383
- else {
384
- max = mid;
385
- }
386
- }
387
- return min;
388
- }
389
- insertAtPosition(child) {
390
- let pos = this.findPosition(child.sortStr);
391
- this.byPosition.splice(pos, 0, child);
392
- // Based on the position in the list, set the precedingSibling for the new Scope
393
- // and for the next sibling.
394
- let nextSibling = this.byPosition[pos + 1];
395
- if (nextSibling) {
396
- child.precedingSibling = nextSibling.precedingSibling;
397
- nextSibling.precedingSibling = child;
398
- }
399
- else {
400
- child.precedingSibling = this.lastChild || this.precedingSibling;
401
- this.lastChild = child;
402
- }
403
- }
404
- removeFromPosition(child) {
405
- if (child.sortStr === '')
406
- return;
407
- let pos = this.findPosition(child.sortStr);
408
- while (true) {
409
- if (this.byPosition[pos] === child) {
410
- // Yep, this is the right scope
411
- this.byPosition.splice(pos, 1);
412
- if (pos < this.byPosition.length) {
413
- let nextSibling = this.byPosition[pos];
414
- /* istanbul ignore next */
415
- if (!nextSibling)
416
- return internalError(8);
417
- /* istanbul ignore next */
418
- if (nextSibling.precedingSibling !== child)
419
- return internalError(13);
420
- nextSibling.precedingSibling = child.precedingSibling;
421
- }
422
- else {
423
- /* istanbul ignore next */
424
- if (child !== this.lastChild)
425
- return internalError(12);
426
- this.lastChild = child.precedingSibling === this.precedingSibling ? undefined : child.precedingSibling;
427
- }
428
- return;
429
- }
430
- // There may be another Scope with the same sortStr
431
- /* istanbul ignore next */
432
- if (++pos >= this.byPosition.length || this.byPosition[pos].sortStr !== child.sortStr) {
433
- return internalError(5);
434
- }
435
- }
436
- }
437
- }
438
- class OnEachItemScope extends Scope {
439
- constructor(parentElement, precedingSibling, queueOrder, parent, itemIndex) {
440
- super(parentElement, precedingSibling, queueOrder);
441
- this.sortStr = "";
442
- this.parent = parent;
443
- this.itemIndex = itemIndex;
444
- }
445
- // toString(): string {
446
- // return `OnEachItemScope(itemIndex=${this.itemIndex} parentElement=${this.parentElement} parent=${this.parent} precedingSibling=${this.precedingSibling} lastChild=${this.lastChild})`
447
- // }
448
- queueRun() {
449
- /* istanbul ignore next */
450
- if (currentScope) {
451
- internalError(4);
452
- }
453
- if (this.isDead)
454
- return;
455
- this.remove();
456
- this.isDead = false;
457
- this.update();
458
- }
459
- update() {
460
- // Have the makeSortKey function return an ordering int/string/array.
461
- // Since makeSortKey may get() the Store, we'll need to set currentScope first.
462
- let savedScope = currentScope;
463
- currentScope = this;
464
- let itemStore = new Store(this.parent.collection, this.itemIndex);
465
- let sortKey;
466
- try {
467
- sortKey = this.parent.makeSortKey(itemStore);
468
- }
469
- catch (e) {
470
- handleError(e);
471
- }
472
- let oldSortStr = this.sortStr;
473
- let newSortStr = sortKey == null ? '' : sortKeyToString(sortKey);
474
- if (oldSortStr !== '' && oldSortStr !== newSortStr) {
475
- this.parent.removeFromPosition(this);
476
- }
477
- this.sortStr = newSortStr;
478
- if (newSortStr !== '') {
479
- if (newSortStr !== oldSortStr) {
480
- this.parent.insertAtPosition(this);
481
- }
482
- try {
483
- this.parent.renderer(itemStore);
484
- }
485
- catch (e) {
486
- handleError(e);
487
- }
488
- }
489
- currentScope = savedScope;
490
- }
491
- }
492
- /**
493
- * This global is set during the execution of a `Scope.render`. It is used by
494
- * functions like `node`, `text` and `clean`.
495
- */
496
- let currentScope;
497
- /**
498
- * A special Node observer index to subscribe to any value in the map changing.
499
- */
500
- const ANY_INDEX = {};
501
- class ObsCollection {
502
- constructor() {
503
- this.observers = new Map();
504
- }
505
- // toString(): string {
506
- // return JSON.stringify(peek(() => this.getRecursive(3)))
507
- // }
508
- addObserver(index, observer) {
509
- observer = observer;
510
- let obsSet = this.observers.get(index);
511
- if (obsSet) {
512
- if (obsSet.has(observer))
513
- return false;
514
- obsSet.add(observer);
515
- }
516
- else {
517
- this.observers.set(index, new Set([observer]));
518
- }
519
- return true;
520
- }
521
- removeObserver(index, observer) {
522
- let obsSet = this.observers.get(index);
523
- obsSet.delete(observer);
524
- }
525
- emitChange(index, newData, oldData) {
526
- if (recordingPatch) {
527
- addToPatch(recordingPatch, this, index, newData, oldData);
528
- }
529
- else {
530
- let obsSet = this.observers.get(index);
531
- if (obsSet)
532
- obsSet.forEach(observer => observer.onChange(index, newData, oldData));
533
- obsSet = this.observers.get(ANY_INDEX);
534
- if (obsSet)
535
- obsSet.forEach(observer => observer.onChange(index, newData, oldData));
536
- }
537
- }
538
- _clean(observer) {
539
- this.removeObserver(ANY_INDEX, observer);
540
- }
541
- setIndex(index, newValue, deleteMissing) {
542
- const curData = this.rawGet(index);
543
- if (!(curData instanceof ObsCollection) || newValue instanceof Store || !curData.merge(newValue, deleteMissing)) {
544
- let newData = valueToData(newValue);
545
- if (newData !== curData) {
546
- this.rawSet(index, newData);
547
- this.emitChange(index, newData, curData);
548
- }
549
- }
550
- }
551
- }
552
- class ObsArray extends ObsCollection {
553
- constructor() {
554
- super(...arguments);
555
- this.data = [];
556
- }
557
- getType() {
558
- return "array";
559
- }
560
- getRecursive(depth) {
561
- if (currentScope) {
562
- if (this.addObserver(ANY_INDEX, currentScope)) {
563
- currentScope.cleaners.push(this);
564
- }
565
- }
566
- let result = [];
567
- for (let i = 0; i < this.data.length; i++) {
568
- let v = this.data[i];
569
- result.push(v instanceof ObsCollection ? (depth ? v.getRecursive(depth - 1) : new Store(this, i)) : v);
570
- }
571
- return result;
572
- }
573
- rawGet(index) {
574
- return this.data[index];
575
- }
576
- rawSet(index, newData) {
577
- if (index !== (0 | index) || index < 0 || index > 999999) {
578
- throw new Error(`Invalid array index ${JSON.stringify(index)}`);
579
- }
580
- this.data[index] = newData;
581
- // Remove trailing `undefined`s
582
- while (this.data.length > 0 && this.data[this.data.length - 1] === undefined) {
583
- this.data.pop();
584
- }
585
- }
586
- merge(newValue, deleteMissing) {
587
- if (!(newValue instanceof Array)) {
588
- return false;
589
- }
590
- // newValue is an array
591
- for (let i = 0; i < newValue.length; i++) {
592
- this.setIndex(i, newValue[i], deleteMissing);
593
- }
594
- if (deleteMissing && this.data.length > newValue.length) {
595
- for (let i = newValue.length; i < this.data.length; i++) {
596
- let old = this.data[i];
597
- if (old !== undefined) {
598
- this.emitChange(i, undefined, old);
599
- }
600
- }
601
- this.data.length = newValue.length;
602
- }
603
- return true;
604
- }
605
- iterateIndexes(scope) {
606
- for (let i = 0; i < this.data.length; i++) {
607
- if (this.data[i] !== undefined) {
608
- scope.addChild(i);
609
- }
610
- }
611
- }
612
- normalizeIndex(index) {
613
- if (typeof index === 'number')
614
- return index;
615
- if (typeof index === 'string') {
616
- // Convert to int
617
- let num = 0 | index;
618
- // Check if the number is still the same after conversion
619
- if (index.length && num == index)
620
- return index;
621
- }
622
- throw new Error(`Invalid array index ${JSON.stringify(index)}`);
623
- }
624
- getCount() {
625
- return this.data.length;
626
- }
627
- }
628
- class ObsMap extends ObsCollection {
629
- constructor() {
630
- super(...arguments);
631
- this.data = new Map();
632
- }
633
- getType() {
634
- return "map";
635
- }
636
- getRecursive(depth) {
637
- if (currentScope) {
638
- if (this.addObserver(ANY_INDEX, currentScope)) {
639
- currentScope.cleaners.push(this);
640
- }
641
- }
642
- let result = new Map();
643
- this.data.forEach((v, k) => {
644
- result.set(k, (v instanceof ObsCollection) ? (depth ? v.getRecursive(depth - 1) : new Store(this, k)) : v);
645
- });
646
- return result;
647
- }
648
- rawGet(index) {
649
- return this.data.get(index);
650
- }
651
- rawSet(index, newData) {
652
- if (newData === undefined) {
653
- this.data.delete(index);
654
- }
655
- else {
656
- this.data.set(index, newData);
657
- }
658
- }
659
- merge(newValue, deleteMissing) {
660
- if (!(newValue instanceof Map)) {
661
- return false;
662
- }
663
- // Walk the pairs of the new value map
664
- newValue.forEach((v, k) => {
665
- this.setIndex(k, v, deleteMissing);
666
- });
667
- if (deleteMissing) {
668
- this.data.forEach((v, k) => {
669
- if (!newValue.has(k))
670
- this.setIndex(k, undefined, false);
671
- });
672
- }
673
- return true;
674
- }
675
- iterateIndexes(scope) {
676
- this.data.forEach((_, itemIndex) => {
677
- scope.addChild(itemIndex);
678
- });
679
- }
680
- normalizeIndex(index) {
681
- return index;
682
- }
683
- getCount() {
684
- return this.data.size;
685
- }
686
- }
687
- class ObsObject extends ObsMap {
688
- getType() {
689
- return "object";
690
- }
691
- getRecursive(depth) {
692
- if (currentScope) {
693
- if (this.addObserver(ANY_INDEX, currentScope)) {
694
- currentScope.cleaners.push(this);
695
- }
696
- }
697
- let result = {};
698
- this.data.forEach((v, k) => {
699
- result[k] = (v instanceof ObsCollection) ? (depth ? v.getRecursive(depth - 1) : new Store(this, k)) : v;
700
- });
701
- return result;
702
- }
703
- merge(newValue, deleteMissing) {
704
- if (!newValue || newValue.constructor !== Object) {
705
- return false;
706
- }
707
- // Walk the pairs of the new value object
708
- for (let k in newValue) {
709
- this.setIndex(k, newValue[k], deleteMissing);
710
- }
711
- if (deleteMissing) {
712
- this.data.forEach((v, k) => {
713
- if (!newValue.hasOwnProperty(k))
714
- this.setIndex(k, undefined, false);
715
- });
716
- }
717
- return true;
718
- }
719
- normalizeIndex(index) {
720
- let type = typeof index;
721
- if (type === 'string')
722
- return index;
723
- if (type === 'number')
724
- return '' + index;
725
- throw new Error(`Invalid object index ${JSON.stringify(index)}`);
726
- }
727
- getCount() {
728
- let cnt = 0;
729
- for (let key of this.data)
730
- cnt++;
731
- return cnt;
732
- }
733
- }
734
- /**
735
- * A data store that automatically subscribes the current scope to updates
736
- * whenever data is read from it.
737
- *
738
- * Supported data types are: `string`, `number`, `boolean`, `undefined`, `null`,
739
- * `Array`, `object` and `Map`. The latter three will always have `Store` objects as
740
- * values, creating a tree of `Store`-objects.
741
- */
742
- export class Store {
743
- constructor(value = undefined, index = undefined) {
744
- if (index === undefined) {
745
- this.collection = new ObsArray();
746
- this.idx = 0;
747
- if (value !== undefined) {
748
- this.collection.rawSet(0, valueToData(value));
749
- }
750
- }
751
- else {
752
- if (!(value instanceof ObsCollection)) {
753
- throw new Error("1st parameter should be an ObsCollection if the 2nd is also given");
754
- }
755
- this.collection = value;
756
- this.idx = index;
757
- }
758
- }
759
- /**
760
- *
761
- * @returns The index for this Store within its parent collection. This will be a `number`
762
- * when the parent collection is an array, a `string` when it's an object, or any data type
763
- * when it's a `Map`.
764
- *
765
- * @example
766
- * ```
767
- * let store = new Store({x: 123})
768
- * let subStore = store.ref('x')
769
- * assert(subStore.get() === 123)
770
- * assert(subStore.index() === 'x') // <----
771
- * ```
772
- */
773
- index() {
774
- return this.idx;
775
- }
776
- /** @internal */
777
- _clean(scope) {
778
- this.collection.removeObserver(this.idx, scope);
779
- }
780
- /**
781
- * @returns Resolves `path` and then retrieves the value that is there, subscribing
782
- * to all read `Store` values. If `path` does not exist, `undefined` is returned.
783
- * @param path - Any path terms to resolve before retrieving the value.
784
- * @example
785
- * ```
786
- * let store = new Store({a: {b: {c: {d: 42}}}})
787
- * assert('a' in store.get())
788
- * assert(store.get('a', 'b') === {c: {d: 42}})
789
- * ```
790
- */
791
- get(...path) {
792
- return this.query({ path });
793
- }
794
- /**
795
- * Like {@link Store.get}, but doesn't subscribe to changes.
796
- */
797
- peek(...path) {
798
- return this.query({ path, peek: true });
799
- }
800
- /**
801
- * @returns Like {@link Store.get}, but throws a `TypeError` if the resulting value is not of type `number`.
802
- * Using this instead of just {@link Store.get} is especially useful from within TypeScript.
803
- */
804
- getNumber(...path) { return this.query({ path, type: 'number' }); }
805
- /**
806
- * @returns Like {@link Store.get}, but throws a `TypeError` if the resulting value is not of type `string`.
807
- * Using this instead of just {@link Store.get} is especially useful from within TypeScript.
808
- */
809
- getString(...path) { return this.query({ path, type: 'string' }); }
810
- /**
811
- * @returns Like {@link Store.get}, but throws a `TypeError` if the resulting value is not of type `boolean`.
812
- * Using this instead of just {@link Store.get} is especially useful from within TypeScript.
813
- */
814
- getBoolean(...path) { return this.query({ path, type: 'boolean' }); }
815
- /**
816
- * @returns Like {@link Store.get}, but throws a `TypeError` if the resulting value is not of type `function`.
817
- * Using this instead of just {@link Store.get} is especially useful from within TypeScript.
818
- */
819
- getFunction(...path) { return this.query({ path, type: 'function' }); }
820
- /**
821
- * @returns Like {@link Store.get}, but throws a `TypeError` if the resulting value is not of type `array`.
822
- * Using this instead of just {@link Store.get} is especially useful from within TypeScript.
823
- */
824
- getArray(...path) { return this.query({ path, type: 'array' }); }
825
- /**
826
- * @returns Like {@link Store.get}, but throws a `TypeError` if the resulting value is not of type `object`.
827
- * Using this instead of just {@link Store.get} is especially useful from within TypeScript.
828
- */
829
- getObject(...path) { return this.query({ path, type: 'object' }); }
830
- /**
831
- * @returns Like {@link Store.get}, but throws a `TypeError` if the resulting value is not of type `map`.
832
- * Using this instead of just {@link Store.get} is especially useful from within TypeScript.
833
- */
834
- getMap(...path) { return this.query({ path, type: 'map' }); }
835
- /**
836
- * Like {@link Store.get}, but the first parameter is the default value (returned when the Store
837
- * contains `undefined`). This default value is also used to determine the expected type,
838
- * and to throw otherwise.
839
- *
840
- * @example
841
- * ```
842
- * let store = {x: 42}
843
- * assert(getOr(99, 'x') == 42)
844
- * assert(getOr(99, 'y') == 99)
845
- * getOr('hello', x') # throws TypeError (because 42 is not a string)
846
- * ```
847
- */
848
- getOr(defaultValue, ...path) {
849
- let type = typeof defaultValue;
850
- if (type === 'object') {
851
- if (defaultValue instanceof Map)
852
- type = 'map';
853
- else if (defaultValue instanceof Array)
854
- type = 'array';
855
- }
856
- return this.query({ type, defaultValue, path });
857
- }
858
- /** Retrieve a value, subscribing to all read `Store` values. This is a more flexible
859
- * form of the {@link Store.get} and {@link Store.peek} methods.
860
- *
861
- * @returns The resulting value, or `undefined` if the `path` does not exist.
862
- */
863
- query(opts) {
864
- if (opts.peek && currentScope) {
865
- let savedScope = currentScope;
866
- currentScope = undefined;
867
- let result = this.query(opts);
868
- currentScope = savedScope;
869
- return result;
870
- }
871
- let store = opts.path && opts.path.length ? this.ref(...opts.path) : this;
872
- let value = store._observe();
873
- if (opts.type && (value !== undefined || opts.defaultValue === undefined)) {
874
- let type = (value instanceof ObsCollection) ? value.getType() : (value === null ? "null" : typeof value);
875
- if (type !== opts.type)
876
- throw new TypeError(`Expecting ${opts.type} but got ${type}`);
877
- }
878
- if (value instanceof ObsCollection) {
879
- return value.getRecursive(opts.depth == null ? -1 : opts.depth - 1);
880
- }
881
- return value === undefined ? opts.defaultValue : value;
882
- }
883
- /**
884
- * Checks if the specified collection is empty, and subscribes the current scope to changes of the emptiness of this collection.
885
- *
886
- * @param path Any path terms to resolve before retrieving the value.
887
- * @returns When the specified collection is not empty `true` is returned. If it is empty or if the value is undefined, `false` is returned.
888
- * @throws When the value is not a collection and not undefined, an Error will be thrown.
889
- */
890
- isEmpty(...path) {
891
- let store = this.ref(...path);
892
- let value = store._observe();
893
- if (value instanceof ObsCollection) {
894
- if (currentScope) {
895
- let observer = new IsEmptyObserver(currentScope, value, false);
896
- return !observer.count;
897
- }
898
- else {
899
- return !value.getCount();
900
- }
901
- }
902
- else if (value === undefined) {
903
- return true;
904
- }
905
- else {
906
- throw new Error(`isEmpty() expects a collection or undefined, but got ${JSON.stringify(value)}`);
907
- }
908
- }
909
- /**
910
- * Returns the number of items in the specified collection, and subscribes the current scope to changes in this count.
911
- *
912
- * @param path Any path terms to resolve before retrieving the value.
913
- * @returns The number of items contained in the collection, or 0 if the value is undefined.
914
- * @throws When the value is not a collection and not undefined, an Error will be thrown.
915
- */
916
- count(...path) {
917
- let store = this.ref(...path);
918
- let value = store._observe();
919
- if (value instanceof ObsCollection) {
920
- if (currentScope) {
921
- let observer = new IsEmptyObserver(currentScope, value, true);
922
- return observer.count;
923
- }
924
- else {
925
- return value.getCount();
926
- }
927
- }
928
- else if (value === undefined) {
929
- return 0;
930
- }
931
- else {
932
- throw new Error(`count() expects a collection or undefined, but got ${JSON.stringify(value)}`);
933
- }
934
- }
935
- /**
936
- * Returns a strings describing the type of the store value, subscribing to changes of this type.
937
- * Note: this currently also subscribes to changes of primitive values, so changing a value from 3 to 4
938
- * would cause the scope to be rerun. This is not great, and may change in the future. This caveat does
939
- * not apply to changes made *inside* an object, `Array` or `Map`.
940
- *
941
- * @param path Any path terms to resolve before retrieving the value.
942
- * @returns Possible options: "undefined", "null", "boolean", "number", "string", "function", "array", "map" or "object".
943
- */
944
- getType(...path) {
945
- let store = this.ref(...path);
946
- let value = store._observe();
947
- return (value instanceof ObsCollection) ? value.getType() : (value === null ? "null" : typeof value);
948
- }
949
- /**
950
- * Sets the value to the last given argument. Any earlier argument are a Store-path that is first
951
- * resolved/created using {@link Store.makeRef}.
952
- *
953
- * When a `Store` is passed in as the value, its value will be copied (subscribing to changes). In
954
- * case the value is an object, an `Array` or a `Map`, a *reference* to that data structure will
955
- * be created, so that changes made through one `Store` will be reflected through the other. Be
956
- * carefull not to create loops in your `Store` tree that way, as that would cause any future
957
- * call to {@link Store.get} to throw a `RangeError` (Maximum call stack size exceeded.)
958
- *
959
- * If you intent to make a copy instead of a reference, call {@link Store.get} on the origin `Store`.
960
- *
961
- *
962
- * @example
963
- * ```
964
- * let store = new Store() // Value is `undefined`
965
- *
966
- * store.set('x', 6) // Causes the store to become an object
967
- * assert(store.get() == {x: 6})
968
- *
969
- * store.set('a', 'b', 'c', 'd') // Create parent path as objects
970
- * assert(store.get() == {x: 6, a: {b: {c: 'd'}}})
971
- *
972
- * store.set(42) // Overwrites all of the above
973
- * assert(store.get() == 42)
974
- *
975
- * store.set('x', 6) // Throw Error (42 is not a collection)
976
- * ```
977
- */
978
- set(...pathAndValue) {
979
- let newValue = pathAndValue.pop();
980
- let store = this.makeRef(...pathAndValue);
981
- store.collection.setIndex(store.idx, newValue, true);
982
- }
983
- /**
984
- * Sets the `Store` to the given `mergeValue`, but without deleting any pre-existing
985
- * items when a collection overwrites a similarly typed collection. This results in
986
- * a deep merge.
987
- *
988
- * @example
989
- * ```
990
- * let store = new Store({a: {x: 1}})
991
- * store.merge({a: {y: 2}, b: 3})
992
- * assert(store.get() == {a: {x: 1, y: 2}, b: 3})
993
- * ```
994
- */
995
- merge(...pathAndValue) {
996
- let mergeValue = pathAndValue.pop();
997
- let store = this.makeRef(...pathAndValue);
998
- store.collection.setIndex(store.idx, mergeValue, false);
999
- }
1000
- /**
1001
- * Sets the value for the store to `undefined`, which causes it to be omitted from the map (or array, if it's at the end)
1002
- *
1003
- * @example
1004
- * ```
1005
- * let store = new Store({a: 1, b: 2})
1006
- * store.delete('a')
1007
- * assert(store.get() == {b: 2})
1008
- *
1009
- * store = new Store(['a','b','c'])
1010
- * store.delete(1)
1011
- * assert(store.get() == ['a', undefined, 'c'])
1012
- * store.delete(2)
1013
- * assert(store.get() == ['a'])
1014
- * ```
1015
- */
1016
- delete(...path) {
1017
- let store = this.makeRef(...path);
1018
- store.collection.setIndex(store.idx, undefined, true);
1019
- }
1020
- /**
1021
- * Pushes a value to the end of the Array that is at the specified path in the store.
1022
- * If that store path is `undefined`, an Array is created first.
1023
- * The last argument is the value to be added, any earlier arguments indicate the path.
1024
- *
1025
- * @example
1026
- * ```
1027
- * let store = new Store()
1028
- * store.push(3) // Creates the array
1029
- * store.push(6)
1030
- * assert(store.get() == [3,6])
1031
- *
1032
- * store = new Store({myArray: [1,2]})
1033
- * store.push('myArray', 3)
1034
- * assert(store.get() == {myArray: [1,2,3]})
1035
- * ```
1036
- */
1037
- push(...pathAndValue) {
1038
- let newValue = pathAndValue.pop();
1039
- let store = this.makeRef(...pathAndValue);
1040
- let obsArray = store.collection.rawGet(store.idx);
1041
- if (obsArray === undefined) {
1042
- obsArray = new ObsArray();
1043
- store.collection.setIndex(store.idx, obsArray, true);
1044
- }
1045
- else if (!(obsArray instanceof ObsArray)) {
1046
- throw new Error(`push() is only allowed for an array or undefined (which would become an array)`);
1047
- }
1048
- let newData = valueToData(newValue);
1049
- let pos = obsArray.data.length;
1050
- obsArray.data.push(newData);
1051
- obsArray.emitChange(pos, newData, undefined);
1052
- return pos;
1053
- }
1054
- /**
1055
- * {@link Store.peek} the current value, pass it through `func`, and {@link Store.set} the resulting
1056
- * value.
1057
- * @param func The function transforming the value.
1058
- */
1059
- modify(func) {
1060
- this.set(func(this.query({ peek: true })));
1061
- }
1062
- /**
1063
- * Return a `Store` deeper within the tree by resolving the given `path`,
1064
- * subscribing to every level.
1065
- * In case `undefined` is encountered while resolving the path, a newly
1066
- * created `Store` containing `undefined` is returned. In that case, the
1067
- * `Store`'s {@link Store.isDetached} method will return `true`.
1068
- * In case something other than a collection is encountered, an error is thrown.
1069
- */
1070
- ref(...path) {
1071
- let store = this;
1072
- for (let i = 0; i < path.length; i++) {
1073
- let value = store._observe();
1074
- if (value instanceof ObsCollection) {
1075
- store = new Store(value, value.normalizeIndex(path[i]));
1076
- }
1077
- else {
1078
- if (value !== undefined)
1079
- throw new Error(`Value ${JSON.stringify(value)} is not a collection (nor undefined) in step ${i} of $(${JSON.stringify(path)})`);
1080
- return new DetachedStore();
1081
- }
1082
- }
1083
- return store;
1084
- }
1085
- /**
1086
- * Similar to `ref()`, but instead of returning `undefined`, new objects are created when
1087
- * a path does not exist yet. An error is still thrown when the path tries to index an invalid
1088
- * type.
1089
- * Unlike `ref`, `makeRef` does *not* subscribe to the path levels, as it is intended to be
1090
- * a write-only operation.
1091
- *
1092
- * @example
1093
- * ```
1094
- * let store = new Store() // Value is `undefined`
1095
- *
1096
- * let ref = store.makeRef('a', 'b', 'c')
1097
- * assert(store.get() == {a: {b: {}}}
1098
- *
1099
- * ref.set(42)
1100
- * assert(store.get() == {a: {b: {c: 42}}}
1101
- *
1102
- * ref.makeRef('d') // Throw Error (42 is not a collection)
1103
- * ```
1104
- */
1105
- makeRef(...path) {
1106
- let store = this;
1107
- for (let i = 0; i < path.length; i++) {
1108
- let value = store.collection.rawGet(store.idx);
1109
- if (!(value instanceof ObsCollection)) {
1110
- if (value !== undefined)
1111
- throw new Error(`Value ${JSON.stringify(value)} is not a collection (nor undefined) in step ${i} of $(${JSON.stringify(path)})`);
1112
- value = new ObsObject();
1113
- store.collection.rawSet(store.idx, value);
1114
- store.collection.emitChange(store.idx, value, undefined);
1115
- }
1116
- store = new Store(value, value.normalizeIndex(path[i]));
1117
- }
1118
- return store;
1119
- }
1120
- /** @internal */
1121
- _observe() {
1122
- if (currentScope) {
1123
- if (this.collection.addObserver(this.idx, currentScope)) {
1124
- currentScope.cleaners.push(this);
1125
- }
1126
- }
1127
- return this.collection.rawGet(this.idx);
1128
- }
1129
- /**
1130
- * Iterate the specified collection (Array, Map or object), running the given code block for each item.
1131
- * When items are added to the collection at some later point, the code block will be ran for them as well.
1132
- * When an item is removed, the {@link Store.clean} handlers left by its code block are executed.
1133
- *
1134
- *
1135
- *
1136
- * @param pathAndFuncs
1137
- */
1138
- onEach(...pathAndFuncs) {
1139
- let makeSortKey = defaultMakeSortKey;
1140
- let renderer = pathAndFuncs.pop();
1141
- if (typeof pathAndFuncs[pathAndFuncs.length - 1] === 'function' && (typeof renderer === 'function' || renderer == null)) {
1142
- if (renderer != null)
1143
- makeSortKey = renderer;
1144
- renderer = pathAndFuncs.pop();
1145
- }
1146
- if (typeof renderer !== 'function')
1147
- throw new Error(`onEach() expects a render function as its last argument but got ${JSON.stringify(renderer)}`);
1148
- if (!currentScope)
1149
- throw new ScopeError(false);
1150
- let store = this.ref(...pathAndFuncs);
1151
- let val = store._observe();
1152
- if (val instanceof ObsCollection) {
1153
- // Subscribe to changes using the specialized OnEachScope
1154
- let onEachScope = new OnEachScope(currentScope.parentElement, currentScope.lastChild || currentScope.precedingSibling, currentScope.queueOrder + 1, val, renderer, makeSortKey);
1155
- val.addObserver(ANY_INDEX, onEachScope);
1156
- currentScope.cleaners.push(onEachScope);
1157
- currentScope.lastChild = onEachScope;
1158
- onEachScope.renderInitial();
1159
- }
1160
- else if (val !== undefined) {
1161
- throw new Error(`onEach() attempted on a value that is neither a collection nor undefined`);
1162
- }
1163
- }
1164
- /**
1165
- * Applies a filter/map function on each item within the `Store`'s collection,
1166
- * and reactively manages the returned `Map` `Store` to hold any results.
1167
- *
1168
- * @param func - Function that transform the given store into an output value or
1169
- * `undefined` in case this value should be skipped:
1170
- *
1171
- * @returns - A map `Store` with the values returned by `func` and the corresponding
1172
- * keys from the original map, array or object `Store`.
1173
- *
1174
- * When items disappear from the `Store` or are changed in a way that `func` depends
1175
- * upon, the resulting items are removed from the output `Store` as well. When multiple
1176
- * input items produce the same output keys, this may lead to unexpected results.
1177
- */
1178
- map(func) {
1179
- let out = new Store(new Map());
1180
- this.onEach((item) => {
1181
- let value = func(item);
1182
- if (value !== undefined) {
1183
- let key = item.index();
1184
- out.set(key, value);
1185
- clean(() => {
1186
- out.delete(key);
1187
- });
1188
- }
1189
- });
1190
- return out;
1191
- }
1192
- /**
1193
- * Applies a filter/map function on each item within the `Store`'s collection,
1194
- * each of which can deliver any number of key/value pairs, and reactively manages the
1195
- * returned map `Store` to hold any results.
1196
- *
1197
- * @param func - Function that transform the given store into output values
1198
- * that can take one of the following forms:
1199
- * - an `Object` or a `Map`: Each key/value pair will be added to the output `Store`.
1200
- * - anything else: No key/value pairs are added to the output `Store`.
1201
- *
1202
- * @returns - A map `Store` with the key/value pairs returned by all `func` invocations.
1203
- *
1204
- * When items disappear from the `Store` or are changed in a way that `func` depends
1205
- * upon, the resulting items are removed from the output `Store` as well. When multiple
1206
- * input items produce the same output keys, this may lead to unexpected results.
1207
- */
1208
- multiMap(func) {
1209
- let out = new Store(new Map());
1210
- this.onEach((item) => {
1211
- let result = func(item);
1212
- let keys;
1213
- if (result.constructor === Object) {
1214
- for (let key in result) {
1215
- out.set(key, result[key]);
1216
- }
1217
- keys = Object.keys(result);
1218
- }
1219
- else if (result instanceof Map) {
1220
- result.forEach((value, key) => {
1221
- out.set(key, value);
1222
- });
1223
- keys = [...result.keys()];
1224
- }
1225
- else {
1226
- return;
1227
- }
1228
- if (keys.length) {
1229
- clean(() => {
1230
- for (let key of keys) {
1231
- out.delete(key);
1232
- }
1233
- });
1234
- }
1235
- });
1236
- return out;
1237
- }
1238
- /**
1239
- * @returns Returns `true` when the `Store` was created by {@link Store.ref}ing a path that
1240
- * does not exist.
1241
- */
1242
- isDetached() { return false; }
1243
- /*
1244
- * Dump a live view of the `Store` tree as HTML text, `ul` and `li` nodes at
1245
- * the current mount position. Meant for debugging purposes.
1246
- */
1247
- dump() {
1248
- let type = this.getType();
1249
- if (type === 'array' || type === 'object' || type === 'map') {
1250
- text('<' + type + '>');
1251
- node('ul', () => {
1252
- this.onEach((sub) => {
1253
- node('li', () => {
1254
- text(JSON.stringify(sub.index()) + ': ');
1255
- sub.dump();
1256
- });
1257
- });
1258
- });
1259
- }
1260
- else {
1261
- text(JSON.stringify(this.get()));
1262
- }
1263
- }
1264
- }
1265
- class DetachedStore extends Store {
1266
- isDetached() { return true; }
1267
- }
1268
- let onCreateEnabled = false;
1269
- let onDestroyMap = new WeakMap();
1270
- function destroyWithClass(element, cls) {
1271
- element.classList.add(cls);
1272
- setTimeout(() => element.remove(), 2000);
1273
- }
1274
- /**
1275
- * Create a new DOM element, and insert it into the DOM at the position held by the current scope.
1276
- * @param tag - The tag of the element to be created and optionally dot-separated class names. For example: `h1` or `p.intro.has_avatar`.
1277
- * @param rest - The other arguments are flexible and interpreted based on their types:
1278
- * - `string`: Used as textContent for the element.
1279
- * - `object`: Used as attributes, properties or event listeners for the element. See {@link Store.prop} on how the distinction is made and to read about a couple of special keys.
1280
- * - `function`: The render function used to draw the scope of the element. This function gets its own `Scope`, so that if any `Store` it reads changes, it will redraw by itself.
1281
- * - `Store`: Presuming `tag` is `"input"`, `"textarea"` or `"select"`, create a two-way binding between this `Store` value and the input element. The initial value of the input will be set to the initial value of the `Store`, or the other way around if the `Store` holds `undefined`. After that, the `Store` will be updated when the input changes and vice versa.
1282
- * @example
1283
- * node('aside.editorial', 'Yada yada yada....', () => {
1284
- * node('a', {href: '/bio'}, () => {
1285
- * node('img.author', {src: '/me.jpg', alt: 'The author'})
1286
- * })
1287
- * })
1288
- */
1289
- export function node(tag = "", ...rest) {
1290
- if (!currentScope)
1291
- throw new ScopeError(true);
1292
- let el;
1293
- if (tag instanceof Element) {
1294
- el = tag;
1295
- }
1296
- else {
1297
- let pos = tag.indexOf('.');
1298
- let classes;
1299
- if (pos >= 0) {
1300
- classes = tag.substr(pos + 1);
1301
- tag = tag.substr(0, pos);
1302
- }
1303
- el = document.createElement(tag || 'div');
1304
- if (classes) {
1305
- // @ts-ignore (replaceAll is polyfilled)
1306
- el.className = classes.replaceAll('.', ' ');
1307
- }
1308
- }
1309
- currentScope.addNode(el);
1310
- for (let item of rest) {
1311
- let type = typeof item;
1312
- if (type === 'function') {
1313
- let scope = new SimpleScope(el, undefined, currentScope.queueOrder + 1, item);
1314
- if (onCreateEnabled) {
1315
- onCreateEnabled = false;
1316
- scope.update();
1317
- onCreateEnabled = true;
1318
- }
1319
- else {
1320
- scope.update();
1321
- }
1322
- // Add it to our list of cleaners. Even if `scope` currently has
1323
- // no cleaners, it may get them in a future refresh.
1324
- currentScope.cleaners.push(scope);
1325
- }
1326
- else if (type === 'string' || type === 'number') {
1327
- el.textContent = item;
1328
- }
1329
- else if (type === 'object' && item && item.constructor === Object) {
1330
- for (let k in item) {
1331
- applyProp(el, k, item[k]);
1332
- }
1333
- }
1334
- else if (item instanceof Store) {
1335
- bindInput(el, item);
1336
- }
1337
- else if (item != null) {
1338
- throw new Error(`Unexpected argument ${JSON.stringify(item)}`);
1339
- }
1340
- }
1341
- }
1342
- /**
1343
- * Convert an HTML string to one or more DOM elements, and add them to the current DOM scope.
1344
- * @param html - The HTML string. For example `"<section><h2>Test</h2><p>Info..</p></section>"`.
1345
- */
1346
- export function html(html) {
1347
- if (!currentScope || !currentScope.parentElement)
1348
- throw new ScopeError(true);
1349
- let tmpParent = document.createElement(currentScope.parentElement.tagName);
1350
- tmpParent.innerHTML = '' + html;
1351
- while (tmpParent.firstChild) {
1352
- currentScope.addNode(tmpParent.firstChild);
1353
- }
1354
- }
1355
- function bindInput(el, store) {
1356
- let onStoreChange;
1357
- let onInputChange;
1358
- let type = el.getAttribute('type');
1359
- let value = store.query({ peek: true });
1360
- if (type === 'checkbox') {
1361
- if (value === undefined)
1362
- store.set(el.checked);
1363
- onStoreChange = value => el.checked = value;
1364
- onInputChange = () => store.set(el.checked);
1365
- }
1366
- else if (type === 'radio') {
1367
- if (value === undefined && el.checked)
1368
- store.set(el.value);
1369
- onStoreChange = value => el.checked = (value === el.value);
1370
- onInputChange = () => {
1371
- if (el.checked)
1372
- store.set(el.value);
1373
- };
1374
- }
1375
- else {
1376
- onInputChange = () => store.set(type === 'number' || type === 'range' ? (el.value === '' ? null : +el.value) : el.value);
1377
- if (value === undefined)
1378
- onInputChange();
1379
- onStoreChange = value => {
1380
- if (el.value !== value)
1381
- el.value = value;
1382
- };
1383
- }
1384
- observe(() => {
1385
- onStoreChange(store.get());
1386
- });
1387
- el.addEventListener('input', onInputChange);
1388
- clean(() => {
1389
- el.removeEventListener('input', onInputChange);
1390
- });
1391
- }
1392
- /**
1393
- * Add a text node at the current Scope position.
1394
- */
1395
- export function text(text) {
1396
- if (!currentScope)
1397
- throw new ScopeError(true);
1398
- if (text == null)
1399
- return;
1400
- currentScope.addNode(document.createTextNode(text));
1401
- }
1402
- export function prop(name, value = undefined) {
1403
- if (!currentScope || !currentScope.parentElement)
1404
- throw new ScopeError(true);
1405
- if (typeof name === 'object') {
1406
- for (let k in name) {
1407
- applyProp(currentScope.parentElement, k, name[k]);
1408
- }
1409
- }
1410
- else {
1411
- applyProp(currentScope.parentElement, name, value);
1412
- }
1413
- }
1414
- /**
1415
- * Return the browser Element that `node()`s would be rendered to at this point.
1416
- * NOTE: Manually changing the DOM is not recommended in most cases. There is
1417
- * usually a better, declarative way. Although there are no hard guarantees on
1418
- * how your changes interact with Aberdeen, in most cases results will not be
1419
- * terribly surprising. Be careful within the parent element of onEach() though.
1420
- */
1421
- export function getParentElement() {
1422
- if (!currentScope || !currentScope.parentElement)
1423
- throw new ScopeError(true);
1424
- return currentScope.parentElement;
1425
- }
1426
- /**
1427
- * Register a function that is to be executed right before the current reactive scope
1428
- * disappears or redraws.
1429
- * @param clean - The function to be executed.
1430
- */
1431
- export function clean(clean) {
1432
- if (!currentScope)
1433
- throw new ScopeError(false);
1434
- currentScope.cleaners.push({ _clean: clean });
1435
- }
1436
- /**
1437
- * Reactively run a function, meaning the function will rerun when any `Store` that was read
1438
- * during its execution is updated.
1439
- * Calls to `observe` can be nested, such that changes to `Store`s read by the inner function do
1440
- * no cause the outer function to rerun.
1441
- *
1442
- * @param func - The function to be (repeatedly) executed.
1443
- * @example
1444
- * ```
1445
- * let number = new Store(0)
1446
- * let doubled = new Store()
1447
- * setInterval(() => number.set(0|Math.random()*100)), 1000)
1448
- *
1449
- * observe(() => {
1450
- * doubled.set(number.get() * 2)
1451
- * })
1452
- *
1453
- * observe(() => {
1454
- * console.log(doubled.get())
1455
- * })
1456
- */
1457
- export function observe(func) {
1458
- mount(undefined, func);
1459
- }
1460
- /**
1461
- * Like {@link Store.observe}, but allow the function to create DOM elements using {@link Store.node}.
1462
-
1463
- * @param func - The function to be (repeatedly) executed, possibly adding DOM elements to `parentElement`.
1464
- * @param parentElement - A DOM element that will be used as the parent element for calls to `node`.
1465
- *
1466
- * @example
1467
- * ```
1468
- * let store = new Store(0)
1469
- * setInterval(() => store.modify(v => v+1), 1000)
1470
- *
1471
- * mount(document.body, () => {
1472
- * node('h2', `${store.get()} seconds have passed`)
1473
- * })
1474
- * ```
1475
- *
1476
- * An example nesting {@link Store.observe} within `mount`:
1477
- * ```
1478
- * let selected = new Store(0)
1479
- * let colors = new Store(new Map())
1480
- *
1481
- * mount(document.body, () => {
1482
- * // This function will never rerun (as it does not read any `Store`s)
1483
- * node('button', '<<', {click: () => selected.modify(n => n-1)})
1484
- * node('button', '>>', {click: () => selected.modify(n => n+1)})
1485
- *
1486
- * observe(() => {
1487
- * // This will rerun whenever `selected` changes, recreating the <h2> and <input>.
1488
- * node('h2', '#'+selected.get())
1489
- * node('input', {type: 'color', value: '#ffffff'}, colors.ref(selected.get()))
1490
- * })
1491
- *
1492
- * observe(() => {
1493
- * // This function will rerun when `selected` or the selected color changes.
1494
- * // It will change the <body> background-color.
1495
- * prop({style: {backgroundColor: colors.get(selected.get()) || 'white'}})
1496
- * })
1497
- * })
1498
- * ```
1499
- */
1500
- export function mount(parentElement, func) {
1501
- let scope;
1502
- if (parentElement || !currentScope) {
1503
- scope = new SimpleScope(parentElement, undefined, 0, func);
1504
- }
1505
- else {
1506
- scope = new SimpleScope(currentScope.parentElement, currentScope.lastChild || currentScope.precedingSibling, currentScope.queueOrder + 1, func);
1507
- currentScope.lastChild = scope;
1508
- }
1509
- // Do the initial run
1510
- scope.update();
1511
- // Add it to our list of cleaners. Even if `scope` currently has
1512
- // no cleaners, it may get them in a future refresh.
1513
- if (currentScope) {
1514
- currentScope.cleaners.push(scope);
1515
- }
1516
- }
1517
- /** Runs the given function, while not subscribing the current scope when reading {@link Store.Store} values.
1518
- *
1519
- * @param func Function to be executed immediately.
1520
- * @returns Whatever `func()` returns.
1521
- * @example
1522
- * ```
1523
- * import {Store, peek, text} from aberdeen
1524
- *
1525
- * let store = new Store(['a', 'b', 'c'])
1526
- *
1527
- * mount(document.body, () => {
1528
- * // Prevent rerender when store changes
1529
- * let msg = peek(() => `Store has ${store.count()} elements, and the first is ${store.get(0)}`))
1530
- * text(msg)
1531
- * })
1532
- * ```
1533
- *
1534
- * In the above example `store.get(0)` could be replaced with `store.peek(0)` to achieve the
1535
- * same result without `peek()` wrapping everything. There is no non-subscribing equivalent
1536
- * for `count()` however.
1537
- */
1538
- export function peek(func) {
1539
- let savedScope = currentScope;
1540
- currentScope = undefined;
1541
- try {
1542
- return func();
1543
- }
1544
- finally {
1545
- currentScope = savedScope;
1546
- }
1547
- }
1548
- /*
1549
- * Helper functions
1550
- */
1551
- function applyProp(el, prop, value) {
1552
- if (prop === 'create') {
1553
- if (onCreateEnabled) {
1554
- if (typeof value === 'function') {
1555
- value(el);
1556
- }
1557
- else {
1558
- el.classList.add(value);
1559
- setTimeout(function () { el.classList.remove(value); }, 0);
1560
- }
1561
- }
1562
- }
1563
- else if (prop === 'destroy') {
1564
- onDestroyMap.set(el, value);
1565
- }
1566
- else if (typeof value === 'function') {
1567
- // Set an event listener; remove it again on clean.
1568
- el.addEventListener(prop, value);
1569
- clean(() => el.removeEventListener(prop, value));
1570
- }
1571
- else if (prop === 'value' || prop === 'className' || prop === 'selectedIndex' || value === true || value === false) {
1572
- // All boolean values and a few specific keys should be set as a property
1573
- el[prop] = value;
1574
- }
1575
- else if (prop === 'text') {
1576
- // `text` is set as textContent
1577
- el.textContent = value;
1578
- }
1579
- else if ((prop === 'class' || prop === 'className') && typeof value === 'object') {
1580
- // Allow setting classes using an object where the keys are the names and
1581
- // the values are booleans stating whether to set or remove.
1582
- for (let name in value) {
1583
- if (value[name])
1584
- el.classList.add(name);
1585
- else
1586
- el.classList.remove(name);
1587
- }
1588
- }
1589
- else if (prop === 'style' && typeof value === 'object') {
1590
- // `style` can receive an object
1591
- Object.assign(el.style, value);
1592
- }
1593
- else {
1594
- // Everything else is an HTML attribute
1595
- el.setAttribute(prop, value);
1596
- }
1597
- }
1598
- function valueToData(value) {
1599
- if (typeof value !== "object" || !value) {
1600
- // Simple data types
1601
- return value;
1602
- }
1603
- else if (value instanceof Store) {
1604
- // When a Store is passed pointing at a collection, a reference
1605
- // is made to that collection.
1606
- return value._observe();
1607
- }
1608
- else if (value instanceof Map) {
1609
- let result = new ObsMap();
1610
- value.forEach((v, k) => {
1611
- let d = valueToData(v);
1612
- if (d !== undefined)
1613
- result.rawSet(k, d);
1614
- });
1615
- return result;
1616
- }
1617
- else if (value instanceof Array) {
1618
- let result = new ObsArray();
1619
- for (let i = 0; i < value.length; i++) {
1620
- let d = valueToData(value[i]);
1621
- if (d !== undefined)
1622
- result.rawSet(i, d);
1623
- }
1624
- return result;
1625
- }
1626
- else if (value.constructor === Object) {
1627
- // A plain (literal) object
1628
- let result = new ObsObject();
1629
- for (let k in value) {
1630
- let d = valueToData(value[k]);
1631
- if (d !== undefined)
1632
- result.rawSet(k, d);
1633
- }
1634
- return result;
1635
- }
1636
- else {
1637
- // Any other type of object (including ObsCollection)
1638
- return value;
1639
- }
1640
- }
1641
- function defaultMakeSortKey(store) {
1642
- return store.index();
1643
- }
1644
- /* istanbul ignore next */
1645
- function internalError(code) {
1646
- let error = new Error("Aberdeen internal error " + code);
1647
- setTimeout(() => { throw error; }, 0);
1648
- }
1649
- function handleError(e) {
1650
- // Throw the error async, so the rest of the rendering can continue
1651
- setTimeout(() => { throw e; }, 0);
1652
- }
1653
- class ScopeError extends Error {
1654
- constructor(mount) {
1655
- super(`Operation not permitted outside of ${mount ? "a mount" : "an observe"}() scope`);
1656
- }
1657
- }
1658
- const FADE_TIME = 400;
1659
- const GROW_SHRINK_TRANSITION = `margin ${FADE_TIME}ms ease-out, transform ${FADE_TIME}ms ease-out`;
1660
- function getGrowShrinkProps(el) {
1661
- const parentStyle = el.parentElement ? getComputedStyle(el.parentElement) : {};
1662
- const isHorizontal = parentStyle.display === 'flex' && (parentStyle.flexDirection || '').startsWith('row');
1663
- return isHorizontal ?
1664
- { marginLeft: `-${el.offsetWidth / 2}px`, marginRight: `-${el.offsetWidth / 2}px`, transform: "scaleX(0)" } :
1665
- { marginBottom: `-${el.offsetHeight / 2}px`, marginTop: `-${el.offsetHeight / 2}px`, transform: "scaleY(0)" };
1666
- }
1667
- /** Do a grow transition for the given element. This is meant to be used as a
1668
- * handler for the `create` property.
1669
- *
1670
- * @param el The element to transition.
1671
- *
1672
- * The transition doesn't look great for table elements, and may have problems
1673
- * for other specific cases as well.
1674
- */
1675
- export function grow(el) {
1676
- // This timeout is to await all other elements having been added to the Dom
1677
- scheduleDomReader(() => {
1678
- // Make the element size 0 using transforms and negative margins.
1679
- // This causes a browser layout, as we're querying el.offset<>.
1680
- let props = getGrowShrinkProps(el);
1681
- // The timeout is in order to batch all reads and then all writes when there
1682
- // are multiple simultaneous grow transitions.
1683
- scheduleDomWriter(() => {
1684
- Object.assign(el.style, props);
1685
- // This timeout is to combine multiple transitions into a single browser layout
1686
- scheduleDomReader(() => {
1687
- // Make sure the layouting has been performed, to cause transitions to trigger
1688
- el.offsetHeight;
1689
- scheduleDomWriter(() => {
1690
- // Do the transitions
1691
- el.style.transition = GROW_SHRINK_TRANSITION;
1692
- for (let prop in props)
1693
- el.style[prop] = "";
1694
- setTimeout(() => {
1695
- // Reset the element to a clean state
1696
- el.style.transition = "";
1697
- }, FADE_TIME);
1698
- });
1699
- });
1700
- });
1701
- });
1702
- }
1703
- /** Do a shrink transition for the given element, and remove it from the DOM
1704
- * afterwards. This is meant to be used as a handler for the `destroy` property.
1705
- *
1706
- * @param el The element to transition and remove.
1707
- *
1708
- * The transition doesn't look great for table elements, and may have problems
1709
- * for other specific cases as well.
1710
- */
1711
- export function shrink(el) {
1712
- scheduleDomReader(() => {
1713
- const props = getGrowShrinkProps(el);
1714
- // The timeout is in order to batch all reads and then all writes when there
1715
- // are multiple simultaneous shrink transitions.
1716
- scheduleDomWriter(() => {
1717
- el.style.transition = GROW_SHRINK_TRANSITION;
1718
- Object.assign(el.style, props);
1719
- setTimeout(() => el.remove(), FADE_TIME);
1720
- });
1721
- });
1722
- }
1723
- function recordPatch(func) {
1724
- if (recordingPatch)
1725
- throw new Error(`already recording a patch`);
1726
- recordingPatch = new Map();
1727
- try {
1728
- func();
1729
- }
1730
- catch (e) {
1731
- recordingPatch = undefined;
1732
- throw e;
1733
- }
1734
- const result = recordingPatch;
1735
- recordingPatch = undefined;
1736
- return result;
1737
- }
1738
- function addToPatch(patch, collection, index, newData, oldData) {
1739
- let collectionMap = patch.get(collection);
1740
- if (!collectionMap) {
1741
- collectionMap = new Map();
1742
- patch.set(collection, collectionMap);
1743
- }
1744
- let prev = collectionMap.get(index);
1745
- if (prev)
1746
- oldData = prev[1];
1747
- if (newData === oldData)
1748
- collectionMap.delete(index);
1749
- else
1750
- collectionMap.set(index, [newData, oldData]);
1751
- }
1752
- function emitPatch(patch) {
1753
- for (let [collection, collectionMap] of patch) {
1754
- for (let [index, [newData, oldData]] of collectionMap) {
1755
- collection.emitChange(index, newData, oldData);
1756
- }
1757
- }
1758
- }
1759
- function mergePatch(target, source, reverse = false) {
1760
- for (let [collection, collectionMap] of source) {
1761
- for (let [index, [newData, oldData]] of collectionMap) {
1762
- addToPatch(target, collection, index, reverse ? oldData : newData, reverse ? newData : oldData);
1763
- }
1764
- }
1765
- }
1766
- function silentlyApplyPatch(patch, force = false) {
1767
- for (let [collection, collectionMap] of patch) {
1768
- for (let [index, [newData, oldData]] of collectionMap) {
1769
- let actualData = collection.rawGet(index);
1770
- if (actualData !== oldData) {
1771
- if (force)
1772
- handleError(new Error(`Applying invalid patch: data ${actualData} is unequal to expected old data ${oldData} for index ${index}`));
1773
- else
1774
- return false;
1775
- }
1776
- }
1777
- }
1778
- for (let [collection, collectionMap] of patch) {
1779
- for (let [index, [newData, oldData]] of collectionMap) {
1780
- collection.rawSet(index, newData);
1781
- }
1782
- }
1783
- return true;
1784
- }
1785
- const appliedPredictions = [];
1786
- /**
1787
- * Run the provided function, while treating all changes to Observables as predictions,
1788
- * meaning they will be reverted when changes come back from the server (or some other
1789
- * async source).
1790
- * @param predictFunc The function to run. It will generally modify some Observables
1791
- * to immediately reflect state (as closely as possible) that we expect the server
1792
- * to communicate back to us later on.
1793
- * @returns A `Patch` object. Don't modify it. This is only meant to be passed to `applyCanon`.
1794
- */
1795
- export function applyPrediction(predictFunc) {
1796
- let patch = recordPatch(predictFunc);
1797
- appliedPredictions.push(patch);
1798
- emitPatch(patch);
1799
- return patch;
1800
- }
1801
- /**
1802
- * Temporarily revert all outstanding predictions, optionally run the provided function
1803
- * (which will generally make authoritative changes to the data based on a server response),
1804
- * and then attempt to reapply the predictions on top of the new canonical state, dropping
1805
- * any predictions that can no longer be applied cleanly (the data has been modified) or
1806
- * that were specified in `dropPredictions`.
1807
- *
1808
- * All of this is done such that redraws are only triggered if the overall effect is an
1809
- * actual change to an `Observable`.
1810
- * @param canonFunc The function to run without any predictions applied. This will typically
1811
- * make authoritative changes to the data, based on a server response.
1812
- * @param dropPredictions An optional list of predictions (as returned by `applyPrediction`)
1813
- * to undo. Typically, when a server response for a certain request is being handled,
1814
- * you'd want to drop the prediction that was done for that request.
1815
- */
1816
- export function applyCanon(canonFunc, dropPredictions = []) {
1817
- let resultPatch = new Map();
1818
- for (let prediction of appliedPredictions)
1819
- mergePatch(resultPatch, prediction, true);
1820
- silentlyApplyPatch(resultPatch, true);
1821
- for (let prediction of dropPredictions) {
1822
- let pos = appliedPredictions.indexOf(prediction);
1823
- if (pos >= 0)
1824
- appliedPredictions.splice(pos, 1);
1825
- }
1826
- if (canonFunc)
1827
- mergePatch(resultPatch, recordPatch(canonFunc));
1828
- for (let idx = 0; idx < appliedPredictions.length; idx++) {
1829
- if (silentlyApplyPatch(appliedPredictions[idx])) {
1830
- mergePatch(resultPatch, appliedPredictions[idx]);
1831
- }
1832
- else {
1833
- appliedPredictions.splice(idx, 1);
1834
- idx--;
1835
- }
1836
- }
1837
- emitPatch(resultPatch);
1838
- }
1839
- // @ts-ignore
1840
- // istanbul ignore next
1841
- if (!String.prototype.replaceAll)
1842
- String.prototype.replaceAll = function (from, to) { return this.split(from).join(to); };