aberdeen 0.2.2 → 0.2.4

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