native-document 1.0.95 → 1.0.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{src/devtools/hrm → devtools}/ComponentRegistry.js +2 -2
- package/devtools/index.js +8 -0
- package/{src/devtools/plugin.js → devtools/plugin/dev-tools-plugin.js} +2 -2
- package/{src/devtools/hrm/nd-vite-hot-reload.js → devtools/transformers/nd-vite-devtools.js} +16 -6
- package/devtools/transformers/src/transformComponentForHrm.js +74 -0
- package/devtools/transformers/src/transformJsFile.js +9 -0
- package/devtools/transformers/src/utils.js +79 -0
- package/{src/devtools/hrm → devtools/transformers/templates}/hrm.orbservable.hook.template.js +8 -0
- package/devtools/widget/Widget.js +48 -0
- package/devtools/widget/widget.css +81 -0
- package/devtools/widget.js +23 -0
- package/dist/native-document.components.min.js +1953 -1245
- package/dist/native-document.dev.js +2022 -1375
- package/dist/native-document.dev.js.map +1 -1
- package/dist/native-document.devtools.min.js +1 -1
- package/dist/native-document.min.js +1 -1
- package/docs/cache.md +1 -1
- package/docs/core-concepts.md +1 -1
- package/docs/native-document-element.md +51 -15
- package/docs/observables.md +333 -315
- package/docs/state-management.md +198 -193
- package/package.json +1 -1
- package/readme.md +1 -1
- package/rollup.config.js +1 -1
- package/src/core/data/ObservableArray.js +67 -0
- package/src/core/data/ObservableChecker.js +2 -0
- package/src/core/data/ObservableItem.js +97 -0
- package/src/core/data/ObservableObject.js +183 -0
- package/src/core/data/Store.js +364 -34
- package/src/core/data/observable-helpers/object.js +2 -166
- package/src/core/utils/formatters.js +91 -0
- package/src/core/utils/localstorage.js +57 -0
- package/src/core/utils/validator.js +0 -2
- package/src/fetch/NativeFetch.js +5 -2
- package/types/observable.d.ts +73 -15
- package/types/plugins-manager.d.ts +1 -1
- package/types/store.d.ts +33 -6
- package/hrm.js +0 -7
- package/src/devtools/app/App.js +0 -66
- package/src/devtools/app/app.css +0 -0
- package/src/devtools/hrm/transformComponent.js +0 -129
- package/src/devtools/index.js +0 -18
- package/src/devtools/widget/DevToolsWidget.js +0 -26
- /package/{src/devtools/hrm → devtools/transformers/templates}/hrm.hook.template.js +0 -0
|
@@ -363,69 +363,16 @@ var NativeComponents = (function (exports) {
|
|
|
363
363
|
// });
|
|
364
364
|
};
|
|
365
365
|
|
|
366
|
-
let DebugManager = {};
|
|
366
|
+
let DebugManager$1 = {};
|
|
367
367
|
{
|
|
368
|
-
DebugManager = {
|
|
368
|
+
DebugManager$1 = {
|
|
369
369
|
log() {},
|
|
370
370
|
warn() {},
|
|
371
371
|
error() {},
|
|
372
372
|
disable() {}
|
|
373
373
|
};
|
|
374
374
|
}
|
|
375
|
-
var DebugManager
|
|
376
|
-
|
|
377
|
-
const MemoryManager = (function() {
|
|
378
|
-
|
|
379
|
-
let $nextObserverId = 0;
|
|
380
|
-
const $observables = new Map();
|
|
381
|
-
|
|
382
|
-
return {
|
|
383
|
-
/**
|
|
384
|
-
* Register an observable and return an id.
|
|
385
|
-
*
|
|
386
|
-
* @param {ObservableItem} observable
|
|
387
|
-
* @param {Function} getListeners
|
|
388
|
-
* @returns {number}
|
|
389
|
-
*/
|
|
390
|
-
register(observable) {
|
|
391
|
-
const id = ++$nextObserverId;
|
|
392
|
-
$observables.set(id, new WeakRef(observable));
|
|
393
|
-
return id;
|
|
394
|
-
},
|
|
395
|
-
unregister(id) {
|
|
396
|
-
$observables.delete(id);
|
|
397
|
-
},
|
|
398
|
-
getObservableById(id) {
|
|
399
|
-
return $observables.get(id)?.deref();
|
|
400
|
-
},
|
|
401
|
-
cleanup() {
|
|
402
|
-
for (const [_, weakObservableRef] of $observables) {
|
|
403
|
-
const observable = weakObservableRef.deref();
|
|
404
|
-
if (observable) {
|
|
405
|
-
observable.cleanup();
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
$observables.clear();
|
|
409
|
-
},
|
|
410
|
-
/**
|
|
411
|
-
* Clean observables that are not referenced anymore.
|
|
412
|
-
* @param {number} threshold
|
|
413
|
-
*/
|
|
414
|
-
cleanObservables(threshold) {
|
|
415
|
-
if($observables.size < threshold) return;
|
|
416
|
-
let cleanedCount = 0;
|
|
417
|
-
for (const [id, weakObservableRef] of $observables) {
|
|
418
|
-
if (!weakObservableRef.deref()) {
|
|
419
|
-
$observables.delete(id);
|
|
420
|
-
cleanedCount++;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
if (cleanedCount > 0) {
|
|
424
|
-
DebugManager$1.log('Memory Auto Clean', `🧹 Cleaned ${cleanedCount} orphaned observables`);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
};
|
|
428
|
-
}());
|
|
375
|
+
var DebugManager = DebugManager$1;
|
|
429
376
|
|
|
430
377
|
/**
|
|
431
378
|
*
|
|
@@ -519,1307 +466,1993 @@ var NativeComponents = (function (exports) {
|
|
|
519
466
|
return this.observable.cleanup();
|
|
520
467
|
};
|
|
521
468
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const ObservableWhen = function(observer, value) {
|
|
530
|
-
this.$target = value;
|
|
531
|
-
this.$observer = observer;
|
|
532
|
-
};
|
|
533
|
-
|
|
534
|
-
ObservableWhen.prototype.__$isObservableWhen = true;
|
|
535
|
-
|
|
536
|
-
/**
|
|
537
|
-
* Subscribes to changes in the match status (true when observable equals target value).
|
|
538
|
-
*
|
|
539
|
-
* @param {Function} callback - Function called with boolean indicating if values match
|
|
540
|
-
* @returns {Function} Unsubscribe function
|
|
541
|
-
* @example
|
|
542
|
-
* const status = Observable('idle');
|
|
543
|
-
* const isLoading = status.when('loading');
|
|
544
|
-
* isLoading.subscribe(active => console.log('Loading:', active));
|
|
545
|
-
*/
|
|
546
|
-
ObservableWhen.prototype.subscribe = function(callback) {
|
|
547
|
-
return this.$observer.on(this.$target, callback);
|
|
548
|
-
};
|
|
549
|
-
|
|
550
|
-
/**
|
|
551
|
-
* Returns true if the observable's current value equals the target value.
|
|
552
|
-
*
|
|
553
|
-
* @returns {boolean} True if observable value matches target value
|
|
554
|
-
*/
|
|
555
|
-
ObservableWhen.prototype.val = function() {
|
|
556
|
-
return this.$observer.$currentValue === this.$target;
|
|
557
|
-
};
|
|
558
|
-
|
|
559
|
-
/**
|
|
560
|
-
* Returns true if the observable's current value equals the target value.
|
|
561
|
-
* Alias for val().
|
|
562
|
-
*
|
|
563
|
-
* @returns {boolean} True if observable value matches target value
|
|
564
|
-
*/
|
|
565
|
-
ObservableWhen.prototype.isMatch = ObservableWhen.prototype.val;
|
|
566
|
-
|
|
567
|
-
/**
|
|
568
|
-
* Returns true if the observable's current value equals the target value.
|
|
569
|
-
* Alias for val().
|
|
570
|
-
*
|
|
571
|
-
* @returns {boolean} True if observable value matches target value
|
|
572
|
-
*/
|
|
573
|
-
ObservableWhen.prototype.isActive = ObservableWhen.prototype.val;
|
|
469
|
+
const DocumentObserver = {
|
|
470
|
+
mounted: new WeakMap(),
|
|
471
|
+
beforeUnmount: new WeakMap(),
|
|
472
|
+
mountedSupposedSize: 0,
|
|
473
|
+
unmounted: new WeakMap(),
|
|
474
|
+
unmountedSupposedSize: 0,
|
|
475
|
+
observer: null,
|
|
574
476
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
477
|
+
executeMountedCallback(node) {
|
|
478
|
+
const data = DocumentObserver.mounted.get(node);
|
|
479
|
+
if(!data) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
data.inDom = true;
|
|
483
|
+
if(!data.mounted) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if(Array.isArray(data.mounted)) {
|
|
487
|
+
for(const cb of data.mounted) {
|
|
488
|
+
cb(node);
|
|
489
|
+
}
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
data.mounted(node);
|
|
493
|
+
},
|
|
592
494
|
|
|
593
|
-
|
|
594
|
-
const
|
|
595
|
-
if(
|
|
596
|
-
|
|
495
|
+
executeUnmountedCallback(node) {
|
|
496
|
+
const data = DocumentObserver.unmounted.get(node);
|
|
497
|
+
if(!data) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
data.inDom = false;
|
|
501
|
+
if(!data.unmounted) {
|
|
502
|
+
return;
|
|
597
503
|
}
|
|
598
|
-
lastArgs = args;
|
|
599
504
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
505
|
+
let shouldRemove = false;
|
|
506
|
+
if(Array.isArray(data.unmounted)) {
|
|
507
|
+
for(const cb of data.unmounted) {
|
|
508
|
+
if(cb(node) === true) {
|
|
509
|
+
shouldRemove = true;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
} else {
|
|
513
|
+
shouldRemove = data.unmounted(node) === true;
|
|
514
|
+
}
|
|
605
515
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
516
|
+
if(shouldRemove) {
|
|
517
|
+
data.disconnect();
|
|
518
|
+
node.nd?.remove();
|
|
519
|
+
}
|
|
520
|
+
},
|
|
611
521
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
522
|
+
checkMutation: function(mutationsList) {
|
|
523
|
+
for(const mutation of mutationsList) {
|
|
524
|
+
if(DocumentObserver.mountedSupposedSize > 0) {
|
|
525
|
+
for(const node of mutation.addedNodes) {
|
|
526
|
+
DocumentObserver.executeMountedCallback(node);
|
|
527
|
+
if(!node.querySelectorAll) {
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
const children = node.querySelectorAll('[data--nd-mounted]');
|
|
531
|
+
for(const child of children) {
|
|
532
|
+
DocumentObserver.executeMountedCallback(child);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
618
536
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
537
|
+
if (DocumentObserver.unmountedSupposedSize > 0) {
|
|
538
|
+
for (const node of mutation.removedNodes) {
|
|
539
|
+
DocumentObserver.executeUnmountedCallback(node);
|
|
540
|
+
if(!node.querySelectorAll) {
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const children = node.querySelectorAll('[data--nd-unmounted]');
|
|
544
|
+
for(const child of children) {
|
|
545
|
+
DocumentObserver.executeUnmountedCallback(child);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
623
549
|
}
|
|
624
|
-
}
|
|
550
|
+
},
|
|
625
551
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
552
|
+
/**
|
|
553
|
+
* @param {HTMLElement} element
|
|
554
|
+
* @param {boolean} inDom
|
|
555
|
+
* @returns {{ disconnect: Function, mounted: Function, unmounted: Function, off: Function }}
|
|
556
|
+
*/
|
|
557
|
+
watch: function(element, inDom = false) {
|
|
558
|
+
let mountedRegistered = false;
|
|
559
|
+
let unmountedRegistered = false;
|
|
629
560
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
561
|
+
let data = {
|
|
562
|
+
inDom,
|
|
563
|
+
mounted: null,
|
|
564
|
+
unmounted: null,
|
|
565
|
+
disconnect: () => {
|
|
566
|
+
if (mountedRegistered) {
|
|
567
|
+
DocumentObserver.mounted.delete(element);
|
|
568
|
+
DocumentObserver.mountedSupposedSize--;
|
|
569
|
+
}
|
|
570
|
+
if (unmountedRegistered) {
|
|
571
|
+
DocumentObserver.unmounted.delete(element);
|
|
572
|
+
DocumentObserver.unmountedSupposedSize--;
|
|
573
|
+
}
|
|
574
|
+
data = null;
|
|
575
|
+
}
|
|
576
|
+
};
|
|
634
577
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
578
|
+
const addListener = (type, callback) => {
|
|
579
|
+
if (!data[type]) {
|
|
580
|
+
data[type] = callback;
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (!Array.isArray(data[type])) {
|
|
584
|
+
data[type] = [data[type], callback];
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
data[type].push(callback);
|
|
588
|
+
};
|
|
639
589
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
590
|
+
const removeListener = (type, callback) => {
|
|
591
|
+
if(!data?.[type]) {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if(Array.isArray(data[type])) {
|
|
595
|
+
const index = data[type].indexOf(callback);
|
|
596
|
+
if(index > -1) {
|
|
597
|
+
data[type].splice(index, 1);
|
|
598
|
+
}
|
|
599
|
+
if(data[type].length === 1) {
|
|
600
|
+
data[type] = data[type][0];
|
|
601
|
+
}
|
|
602
|
+
if(data[type].length === 0) {
|
|
603
|
+
data[type] = null;
|
|
604
|
+
}
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
data[type] = null;
|
|
608
|
+
};
|
|
645
609
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
for (const key in value) {
|
|
649
|
-
if (Object.hasOwn(value, key)) {
|
|
650
|
-
cloned[key] = deepClone(value[key]);
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
return cloned;
|
|
654
|
-
};
|
|
610
|
+
return {
|
|
611
|
+
disconnect: () => data?.disconnect(),
|
|
655
612
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
613
|
+
mounted: (callback) => {
|
|
614
|
+
addListener('mounted', callback);
|
|
615
|
+
DocumentObserver.mounted.set(element, data);
|
|
616
|
+
if (!mountedRegistered) {
|
|
617
|
+
DocumentObserver.mountedSupposedSize++;
|
|
618
|
+
mountedRegistered = true;
|
|
619
|
+
}
|
|
620
|
+
},
|
|
664
621
|
|
|
665
|
-
|
|
666
|
-
|
|
622
|
+
unmounted: (callback) => {
|
|
623
|
+
addListener('unmounted', callback);
|
|
624
|
+
DocumentObserver.unmounted.set(element, data);
|
|
625
|
+
if (!unmountedRegistered) {
|
|
626
|
+
DocumentObserver.unmountedSupposedSize++;
|
|
627
|
+
unmountedRegistered = true;
|
|
628
|
+
}
|
|
629
|
+
},
|
|
667
630
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
631
|
+
off: (type, callback) => {
|
|
632
|
+
removeListener(type, callback);
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
};
|
|
671
637
|
|
|
672
|
-
|
|
638
|
+
DocumentObserver.observer = new MutationObserver(DocumentObserver.checkMutation);
|
|
639
|
+
DocumentObserver.observer.observe(document.body, {
|
|
640
|
+
childList: true,
|
|
641
|
+
subtree: true,
|
|
642
|
+
});
|
|
673
643
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
this.$initialValue = Validator.isObject(value) ? deepClone(value) : value;
|
|
678
|
-
}
|
|
679
|
-
}
|
|
644
|
+
function NDElement(element) {
|
|
645
|
+
this.$element = element;
|
|
646
|
+
this.$observer = null;
|
|
680
647
|
}
|
|
681
648
|
|
|
682
|
-
|
|
683
|
-
get() {
|
|
684
|
-
return this.$currentValue;
|
|
685
|
-
},
|
|
686
|
-
set(value) {
|
|
687
|
-
this.set(value);
|
|
688
|
-
},
|
|
689
|
-
configurable: true,
|
|
690
|
-
});
|
|
649
|
+
NDElement.prototype.__$isNDElement = true;
|
|
691
650
|
|
|
692
|
-
|
|
693
|
-
|
|
651
|
+
NDElement.prototype.valueOf = function() {
|
|
652
|
+
return this.$element;
|
|
653
|
+
};
|
|
694
654
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
* The interceptor can modify the value or return undefined to use the original value.
|
|
698
|
-
*
|
|
699
|
-
* @param {(value) => any} callback - Interceptor function that receives (newValue, currentValue) and returns the transformed value or undefined
|
|
700
|
-
* @returns {ObservableItem} The observable instance for chaining
|
|
701
|
-
* @example
|
|
702
|
-
* const count = Observable(0);
|
|
703
|
-
* count.intercept((newVal, oldVal) => Math.max(0, newVal)); // Prevent negative values
|
|
704
|
-
*/
|
|
705
|
-
ObservableItem.prototype.intercept = function(callback) {
|
|
706
|
-
this.$interceptor = callback;
|
|
707
|
-
this.set = this.$setWithInterceptor;
|
|
655
|
+
NDElement.prototype.ref = function(target, name) {
|
|
656
|
+
target[name] = this.$element;
|
|
708
657
|
return this;
|
|
709
658
|
};
|
|
710
659
|
|
|
711
|
-
|
|
712
|
-
|
|
660
|
+
NDElement.prototype.refSelf = function(target, name) {
|
|
661
|
+
target[name] = this;
|
|
662
|
+
return this;
|
|
713
663
|
};
|
|
714
664
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
665
|
+
NDElement.prototype.unmountChildren = function() {
|
|
666
|
+
let element = this.$element;
|
|
667
|
+
for(let i = 0, length = element.children.length; i < length; i++) {
|
|
668
|
+
let elementChildren = element.children[i];
|
|
669
|
+
if(!elementChildren.$ndProx) {
|
|
670
|
+
elementChildren.nd?.remove();
|
|
671
|
+
}
|
|
672
|
+
elementChildren = null;
|
|
722
673
|
}
|
|
674
|
+
element = null;
|
|
675
|
+
return this;
|
|
723
676
|
};
|
|
724
677
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
678
|
+
NDElement.prototype.remove = function() {
|
|
679
|
+
let element = this.$element;
|
|
680
|
+
element.nd.unmountChildren();
|
|
681
|
+
element.$ndProx = null;
|
|
682
|
+
delete element.nd?.on?.prevent;
|
|
683
|
+
delete element.nd?.on;
|
|
684
|
+
delete element.nd;
|
|
685
|
+
element = null;
|
|
686
|
+
return this;
|
|
687
|
+
};
|
|
729
688
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
689
|
+
NDElement.prototype.lifecycle = function(states) {
|
|
690
|
+
this.$observer = this.$observer || DocumentObserver.watch(this.$element);
|
|
691
|
+
|
|
692
|
+
if(states.mounted) {
|
|
693
|
+
this.$element.setAttribute('data--nd-mounted', '1');
|
|
694
|
+
this.$observer.mounted(states.mounted);
|
|
734
695
|
}
|
|
735
|
-
if(
|
|
736
|
-
|
|
696
|
+
if(states.unmounted) {
|
|
697
|
+
this.$element.setAttribute('data--nd-unmounted', '1');
|
|
698
|
+
this.$observer.unmounted(states.unmounted);
|
|
737
699
|
}
|
|
700
|
+
return this;
|
|
738
701
|
};
|
|
739
702
|
|
|
740
|
-
|
|
741
|
-
this.
|
|
742
|
-
this.triggerListeners(operations);
|
|
703
|
+
NDElement.prototype.mounted = function(callback) {
|
|
704
|
+
return this.lifecycle({ mounted: callback });
|
|
743
705
|
};
|
|
744
706
|
|
|
745
|
-
|
|
746
|
-
this.
|
|
747
|
-
this.triggerFirstListener(operations);
|
|
707
|
+
NDElement.prototype.unmounted = function(callback) {
|
|
708
|
+
return this.lifecycle({ unmounted: callback });
|
|
748
709
|
};
|
|
749
710
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
if(this.$watchers?.size && this.$listeners?.length) {
|
|
753
|
-
this.trigger = (this.$listeners.length === 1) ? this.triggerWatchersAndFirstListener : this.triggerAll;
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
if(this.$listeners?.length) {
|
|
757
|
-
if(this.$listeners.length === 1) {
|
|
758
|
-
this.$firstListener = this.$listeners[0];
|
|
759
|
-
this.trigger = this.triggerFirstListener;
|
|
760
|
-
}
|
|
761
|
-
else {
|
|
762
|
-
this.trigger = this.triggerListeners;
|
|
763
|
-
}
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
766
|
-
if(this.$watchers?.size) {
|
|
767
|
-
this.trigger = this.triggerWatchers;
|
|
768
|
-
return;
|
|
769
|
-
}
|
|
770
|
-
this.trigger = noneTrigger;
|
|
771
|
-
};
|
|
772
|
-
ObservableItem.prototype.trigger = noneTrigger;
|
|
711
|
+
NDElement.prototype.beforeUnmount = function(id, callback) {
|
|
712
|
+
const el = this.$element;
|
|
773
713
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
return;
|
|
778
|
-
}
|
|
779
|
-
this.$previousValue = this.$currentValue;
|
|
780
|
-
this.$currentValue = newValue;
|
|
781
|
-
this.trigger();
|
|
782
|
-
this.$previousValue = null;
|
|
783
|
-
};
|
|
714
|
+
if(!DocumentObserver.beforeUnmount.has(el)) {
|
|
715
|
+
DocumentObserver.beforeUnmount.set(el, new Map());
|
|
716
|
+
const originalRemove = el.remove.bind(el);
|
|
784
717
|
|
|
785
|
-
|
|
786
|
-
* @param {*} data
|
|
787
|
-
*/
|
|
788
|
-
ObservableItem.prototype.$setWithInterceptor = function(data) {
|
|
789
|
-
let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
|
|
790
|
-
const result = this.$interceptor(newValue, this.$currentValue);
|
|
718
|
+
let $isUnmounting = false;
|
|
791
719
|
|
|
792
|
-
|
|
793
|
-
|
|
720
|
+
el.remove = async () => {
|
|
721
|
+
if($isUnmounting) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
$isUnmounting = true;
|
|
725
|
+
|
|
726
|
+
try {
|
|
727
|
+
const callbacks = DocumentObserver.beforeUnmount.get(el);
|
|
728
|
+
for (const cb of callbacks.values()) {
|
|
729
|
+
await cb.call(this, el);
|
|
730
|
+
}
|
|
731
|
+
} finally {
|
|
732
|
+
originalRemove();
|
|
733
|
+
$isUnmounting = false;
|
|
734
|
+
}
|
|
735
|
+
};
|
|
794
736
|
}
|
|
795
737
|
|
|
796
|
-
|
|
738
|
+
DocumentObserver.beforeUnmount.get(el).set(id, callback);
|
|
739
|
+
return this;
|
|
797
740
|
};
|
|
798
741
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
*/
|
|
802
|
-
ObservableItem.prototype.$basicSet = function(data) {
|
|
803
|
-
let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
|
|
804
|
-
this.$updateWithNewValue(newValue);
|
|
742
|
+
NDElement.prototype.htmlElement = function() {
|
|
743
|
+
return this.$element;
|
|
805
744
|
};
|
|
806
745
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
ObservableItem.prototype.val = function() {
|
|
810
|
-
return this.$currentValue;
|
|
811
|
-
};
|
|
746
|
+
NDElement.prototype.node = NDElement.prototype.htmlElement;
|
|
812
747
|
|
|
813
|
-
|
|
814
|
-
this.$
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
if(
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
}
|
|
822
|
-
}
|
|
748
|
+
NDElement.prototype.shadow = function(mode, style = null) {
|
|
749
|
+
const $element = this.$element;
|
|
750
|
+
const children = Array.from($element.childNodes);
|
|
751
|
+
const shadowRoot = $element.attachShadow({ mode });
|
|
752
|
+
if(style) {
|
|
753
|
+
const styleNode = document.createElement("style");
|
|
754
|
+
styleNode.textContent = style;
|
|
755
|
+
shadowRoot.appendChild(styleNode);
|
|
823
756
|
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
757
|
+
$element.append = shadowRoot.append.bind(shadowRoot);
|
|
758
|
+
$element.appendChild = shadowRoot.appendChild.bind(shadowRoot);
|
|
759
|
+
shadowRoot.append(...children);
|
|
760
|
+
|
|
761
|
+
return this;
|
|
828
762
|
};
|
|
829
763
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
* Useful for disposing resources, removing event listeners, or other cleanup tasks.
|
|
833
|
-
*
|
|
834
|
-
* @param {Function} callback - Cleanup function to execute on observable disposal
|
|
835
|
-
* @example
|
|
836
|
-
* const obs = Observable(0);
|
|
837
|
-
* obs.onCleanup(() => console.log('Cleaned up!'));
|
|
838
|
-
* obs.cleanup(); // Logs: "Cleaned up!"
|
|
839
|
-
*/
|
|
840
|
-
ObservableItem.prototype.onCleanup = function(callback) {
|
|
841
|
-
this.$cleanupListeners = this.$cleanupListeners ?? [];
|
|
842
|
-
this.$cleanupListeners.push(callback);
|
|
764
|
+
NDElement.prototype.openShadow = function(style = null) {
|
|
765
|
+
return this.shadow('open', style);
|
|
843
766
|
};
|
|
844
767
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
for (let i = 0; i < this.$cleanupListeners.length; i++) {
|
|
848
|
-
this.$cleanupListeners[i]();
|
|
849
|
-
}
|
|
850
|
-
this.$cleanupListeners = null;
|
|
851
|
-
}
|
|
852
|
-
MemoryManager.unregister(this.$memoryId);
|
|
853
|
-
this.disconnectAll();
|
|
854
|
-
delete this.$value;
|
|
768
|
+
NDElement.prototype.closedShadow = function(style = null) {
|
|
769
|
+
return this.shadow('closed', style);
|
|
855
770
|
};
|
|
856
771
|
|
|
857
772
|
/**
|
|
773
|
+
* Attaches a template binding to the element by hydrating it with the specified method.
|
|
858
774
|
*
|
|
859
|
-
* @param {
|
|
860
|
-
* @
|
|
775
|
+
* @param {string} methodName - Name of the hydration method to call
|
|
776
|
+
* @param {BindingHydrator} bindingHydrator - Template binding with $hydrate method
|
|
777
|
+
* @returns {HTMLElement} The underlying HTML element
|
|
778
|
+
* @example
|
|
779
|
+
* const onClick = $binder.attach((event, data) => console.log(data));
|
|
780
|
+
* element.nd.attach('onClick', onClick);
|
|
861
781
|
*/
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
this.$listeners.push(callback);
|
|
866
|
-
this.assocTrigger();
|
|
782
|
+
NDElement.prototype.attach = function(methodName, bindingHydrator) {
|
|
783
|
+
bindingHydrator.$hydrate(this.$element, methodName);
|
|
784
|
+
return this.$element;
|
|
867
785
|
};
|
|
868
786
|
|
|
869
787
|
/**
|
|
870
|
-
*
|
|
871
|
-
*
|
|
788
|
+
* Extends the current NDElement instance with custom methods.
|
|
789
|
+
* Methods are bound to the instance and available for chaining.
|
|
872
790
|
*
|
|
873
|
-
* @param {
|
|
874
|
-
* @
|
|
791
|
+
* @param {Object} methods - Object containing method definitions
|
|
792
|
+
* @returns {this} The NDElement instance with added methods for chaining
|
|
875
793
|
* @example
|
|
876
|
-
*
|
|
877
|
-
*
|
|
878
|
-
*
|
|
794
|
+
* element.nd.with({
|
|
795
|
+
* highlight() {
|
|
796
|
+
* this.$element.style.background = 'yellow';
|
|
797
|
+
* return this;
|
|
798
|
+
* }
|
|
799
|
+
* }).highlight().onClick(() => console.log('Clicked'));
|
|
879
800
|
*/
|
|
880
|
-
|
|
881
|
-
|
|
801
|
+
NDElement.prototype.with = function(methods) {
|
|
802
|
+
if (!methods || typeof methods !== 'object') {
|
|
803
|
+
throw new NativeDocumentError('extend() requires an object of methods');
|
|
804
|
+
}
|
|
882
805
|
|
|
883
|
-
|
|
806
|
+
for (const name in methods) {
|
|
807
|
+
const method = methods[name];
|
|
884
808
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
809
|
+
if (typeof method !== 'function') {
|
|
810
|
+
console.warn(`⚠️ extends(): "${name}" is not a function, skipping`);
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
888
813
|
|
|
889
|
-
|
|
890
|
-
watchValueList = callback;
|
|
891
|
-
this.$watchers.set(value, callback);
|
|
892
|
-
} else if(!Validator.isArray(watchValueList.list)) {
|
|
893
|
-
watchValueList = [watchValueList, callback];
|
|
894
|
-
callback = (value) => {
|
|
895
|
-
for(let i = 0, length = watchValueList.length; i < length; i++) {
|
|
896
|
-
watchValueList[i](value);
|
|
897
|
-
}
|
|
898
|
-
};
|
|
899
|
-
callback.list = watchValueList;
|
|
900
|
-
this.$watchers.set(value, callback);
|
|
901
|
-
} else {
|
|
902
|
-
watchValueList.list.push(callback);
|
|
814
|
+
this[name] = method.bind(this);
|
|
903
815
|
}
|
|
904
816
|
|
|
905
|
-
this
|
|
817
|
+
return this;
|
|
906
818
|
};
|
|
907
819
|
|
|
908
820
|
/**
|
|
909
|
-
*
|
|
821
|
+
* Extends the NDElement prototype with new methods available to all NDElement instances.
|
|
822
|
+
* Use this to add global methods to all NDElements.
|
|
910
823
|
*
|
|
911
|
-
* @param {
|
|
912
|
-
* @
|
|
824
|
+
* @param {Object} methods - Object containing method definitions to add to prototype
|
|
825
|
+
* @returns {typeof NDElement} The NDElement constructor
|
|
826
|
+
* @throws {NativeDocumentError} If methods is not an object or contains non-function values
|
|
913
827
|
* @example
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
*
|
|
918
|
-
*
|
|
828
|
+
* NDElement.extend({
|
|
829
|
+
* fadeIn() {
|
|
830
|
+
* this.$element.style.opacity = '1';
|
|
831
|
+
* return this;
|
|
832
|
+
* }
|
|
833
|
+
* });
|
|
834
|
+
* // Now all NDElements have .fadeIn() method
|
|
835
|
+
* Div().nd.fadeIn();
|
|
919
836
|
*/
|
|
920
|
-
|
|
921
|
-
if(!
|
|
922
|
-
|
|
923
|
-
const watchValueList = this.$watchers.get(value);
|
|
924
|
-
if(!watchValueList) return;
|
|
925
|
-
|
|
926
|
-
if(!callback || !Array.isArray(watchValueList.list)) {
|
|
927
|
-
this.$watchers?.delete(value);
|
|
928
|
-
this.assocTrigger();
|
|
929
|
-
return;
|
|
930
|
-
}
|
|
931
|
-
const index = watchValueList.indexOf(callback);
|
|
932
|
-
watchValueList?.splice(index, 1);
|
|
933
|
-
if(watchValueList.length === 1) {
|
|
934
|
-
this.$watchers.set(value, watchValueList[0]);
|
|
837
|
+
NDElement.extend = function(methods) {
|
|
838
|
+
if (!methods || typeof methods !== 'object') {
|
|
839
|
+
throw new NativeDocumentError('NDElement.extend() requires an object of methods');
|
|
935
840
|
}
|
|
936
|
-
|
|
937
|
-
|
|
841
|
+
|
|
842
|
+
if (Array.isArray(methods)) {
|
|
843
|
+
throw new NativeDocumentError('NDElement.extend() requires an object, not an array');
|
|
938
844
|
}
|
|
939
|
-
this.assocTrigger();
|
|
940
|
-
};
|
|
941
845
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
* @example
|
|
948
|
-
* const status = Observable('loading');
|
|
949
|
-
* status.once('ready', (val) => console.log('Ready!'));
|
|
950
|
-
* status.once(val => val === 'error', (val) => console.log('Error occurred'));
|
|
951
|
-
*/
|
|
952
|
-
ObservableItem.prototype.once = function(predicate, callback) {
|
|
953
|
-
const fn = typeof predicate === 'function' ? predicate : (v) => v === predicate;
|
|
846
|
+
const protectedMethods = new Set([
|
|
847
|
+
'constructor', 'valueOf', '$element', '$observer',
|
|
848
|
+
'ref', 'remove', 'cleanup', 'with', 'extend', 'attach',
|
|
849
|
+
'lifecycle', 'mounted', 'unmounted', 'unmountChildren'
|
|
850
|
+
]);
|
|
954
851
|
|
|
955
|
-
const
|
|
956
|
-
if (
|
|
957
|
-
|
|
958
|
-
callback(val);
|
|
852
|
+
for (const name in methods) {
|
|
853
|
+
if (!Object.hasOwn(methods, name)) {
|
|
854
|
+
continue;
|
|
959
855
|
}
|
|
960
|
-
};
|
|
961
|
-
this.subscribe(handler);
|
|
962
|
-
};
|
|
963
856
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
857
|
+
const method = methods[name];
|
|
858
|
+
|
|
859
|
+
if (typeof method !== 'function') {
|
|
860
|
+
DebugManager.warn('NDElement.extend', `"${name}" is not a function, skipping`);
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (protectedMethods.has(name)) {
|
|
865
|
+
DebugManager.error('NDElement.extend', `Cannot override protected method "${name}"`);
|
|
866
|
+
throw new NativeDocumentError(`Cannot override protected method "${name}"`);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (NDElement.prototype[name]) {
|
|
870
|
+
DebugManager.warn('NDElement.extend', `Overwriting existing prototype method "${name}"`);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
NDElement.prototype[name] = method;
|
|
973
874
|
}
|
|
974
|
-
|
|
875
|
+
|
|
876
|
+
return NDElement;
|
|
975
877
|
};
|
|
976
878
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
ObservableItem.prototype.check = function(callback) {
|
|
983
|
-
return new ObservableChecker(this, callback)
|
|
879
|
+
const COMMON_NODE_TYPES = {
|
|
880
|
+
ELEMENT: 1,
|
|
881
|
+
TEXT: 3,
|
|
882
|
+
COMMENT: 8,
|
|
883
|
+
DOCUMENT_FRAGMENT: 11
|
|
984
884
|
};
|
|
985
885
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
886
|
+
const Validator = {
|
|
887
|
+
isObservable(value) {
|
|
888
|
+
return value?.__$isObservable;
|
|
889
|
+
},
|
|
890
|
+
isTemplateBinding(value) {
|
|
891
|
+
return value?.__$isTemplateBinding;
|
|
892
|
+
},
|
|
893
|
+
isObservableWhenResult(value) {
|
|
894
|
+
return value && (value.__$isObservableWhen || (typeof value === 'object' && '$target' in value && '$observer' in value));
|
|
895
|
+
},
|
|
896
|
+
isArrayObservable(value) {
|
|
897
|
+
return value?.__$isObservableArray;
|
|
898
|
+
},
|
|
899
|
+
isProxy(value) {
|
|
900
|
+
return value?.__isProxy__
|
|
901
|
+
},
|
|
902
|
+
isObservableOrProxy(value) {
|
|
903
|
+
return Validator.isObservable(value) || Validator.isProxy(value);
|
|
904
|
+
},
|
|
905
|
+
isAnchor(value) {
|
|
906
|
+
return value?.__Anchor__
|
|
907
|
+
},
|
|
908
|
+
isObservableChecker(value) {
|
|
909
|
+
return value?.__$isObservableChecker || value instanceof ObservableChecker;
|
|
910
|
+
},
|
|
911
|
+
isArray(value) {
|
|
912
|
+
return Array.isArray(value);
|
|
913
|
+
},
|
|
914
|
+
isString(value) {
|
|
915
|
+
return typeof value === 'string';
|
|
916
|
+
},
|
|
917
|
+
isNumber(value) {
|
|
918
|
+
return typeof value === 'number';
|
|
919
|
+
},
|
|
920
|
+
isBoolean(value) {
|
|
921
|
+
return typeof value === 'boolean';
|
|
922
|
+
},
|
|
923
|
+
isFunction(value) {
|
|
924
|
+
return typeof value === 'function';
|
|
925
|
+
},
|
|
926
|
+
isAsyncFunction(value) {
|
|
927
|
+
return typeof value === 'function' && value.constructor.name === 'AsyncFunction';
|
|
928
|
+
},
|
|
929
|
+
isObject(value) {
|
|
930
|
+
return typeof value === 'object' && value !== null;
|
|
931
|
+
},
|
|
932
|
+
isJson(value) {
|
|
933
|
+
return !(typeof value !== 'object' || value === null || Array.isArray(value) || value.constructor.name !== 'Object')
|
|
934
|
+
},
|
|
935
|
+
isElement(value) {
|
|
936
|
+
return value && (
|
|
937
|
+
value.nodeType === COMMON_NODE_TYPES.ELEMENT ||
|
|
938
|
+
value.nodeType === COMMON_NODE_TYPES.TEXT ||
|
|
939
|
+
value.nodeType === COMMON_NODE_TYPES.DOCUMENT_FRAGMENT ||
|
|
940
|
+
value.nodeType === COMMON_NODE_TYPES.COMMENT
|
|
941
|
+
);
|
|
942
|
+
},
|
|
943
|
+
isFragment(value) {
|
|
944
|
+
return value?.nodeType === COMMON_NODE_TYPES.DOCUMENT_FRAGMENT;
|
|
945
|
+
},
|
|
946
|
+
isStringOrObservable(value) {
|
|
947
|
+
return this.isString(value) || this.isObservable(value);
|
|
948
|
+
},
|
|
949
|
+
isValidChild(child) {
|
|
950
|
+
return child === null ||
|
|
951
|
+
this.isElement(child) ||
|
|
952
|
+
this.isObservable(child) ||
|
|
953
|
+
this.isNDElement(child) ||
|
|
954
|
+
['string', 'number', 'boolean'].includes(typeof child);
|
|
955
|
+
},
|
|
956
|
+
isNDElement(child) {
|
|
957
|
+
return child?.__$isNDElement || child instanceof NDElement;
|
|
958
|
+
},
|
|
959
|
+
isValidChildren(children) {
|
|
960
|
+
if (!Array.isArray(children)) {
|
|
961
|
+
children = [children];
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const invalid = children.filter(child => !this.isValidChild(child));
|
|
965
|
+
return invalid.length === 0;
|
|
966
|
+
},
|
|
967
|
+
validateChildren(children) {
|
|
968
|
+
if (!Array.isArray(children)) {
|
|
969
|
+
children = [children];
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const invalid = children.filter(child => !this.isValidChild(child));
|
|
973
|
+
if (invalid.length > 0) {
|
|
974
|
+
throw new NativeDocumentError(`Invalid children detected: ${invalid.map(i => typeof i).join(', ')}`);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
return children;
|
|
978
|
+
},
|
|
979
|
+
/**
|
|
980
|
+
* Check if the data contains observables.
|
|
981
|
+
* @param {Array|Object} data
|
|
982
|
+
* @returns {boolean}
|
|
983
|
+
*/
|
|
984
|
+
containsObservables(data) {
|
|
985
|
+
if(!data) {
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
988
|
+
return Validator.isObject(data)
|
|
989
|
+
&& Object.values(data).some(value => Validator.isObservable(value));
|
|
990
|
+
},
|
|
991
|
+
/**
|
|
992
|
+
* Check if the data contains an observable reference.
|
|
993
|
+
* @param {string} data
|
|
994
|
+
* @returns {boolean}
|
|
995
|
+
*/
|
|
996
|
+
containsObservableReference(data) {
|
|
997
|
+
if(!data || typeof data !== 'string') {
|
|
998
|
+
return false;
|
|
999
|
+
}
|
|
1000
|
+
return /\{\{#ObItem::\([0-9]+\)\}\}/.test(data);
|
|
1001
|
+
},
|
|
1002
|
+
validateAttributes(attributes) {},
|
|
1003
|
+
|
|
1004
|
+
validateEventCallback(callback) {
|
|
1005
|
+
if (typeof callback !== 'function') {
|
|
1006
|
+
throw new NativeDocumentError('Event callback must be a function');
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1000
1009
|
};
|
|
1001
1010
|
|
|
1011
|
+
function Anchor(name, isUniqueChild = false) {
|
|
1012
|
+
const anchorFragment = document.createDocumentFragment();
|
|
1013
|
+
anchorFragment.__Anchor__ = true;
|
|
1014
|
+
|
|
1015
|
+
const anchorStart = document.createComment('Anchor Start : '+name);
|
|
1016
|
+
const anchorEnd = document.createComment('/ Anchor End '+name);
|
|
1017
|
+
|
|
1018
|
+
anchorFragment.appendChild(anchorStart);
|
|
1019
|
+
anchorFragment.appendChild(anchorEnd);
|
|
1020
|
+
|
|
1021
|
+
anchorFragment.nativeInsertBefore = anchorFragment.insertBefore;
|
|
1022
|
+
anchorFragment.nativeAppendChild = anchorFragment.appendChild;
|
|
1023
|
+
anchorFragment.nativeAppend = anchorFragment.append;
|
|
1024
|
+
|
|
1025
|
+
const isParentUniqueChild = (parent) => (isUniqueChild || (parent.firstChild === anchorStart && parent.lastChild === anchorEnd));
|
|
1026
|
+
|
|
1027
|
+
const insertBefore = function(parent, child, target) {
|
|
1028
|
+
const childElement = Validator.isElement(child) ? child : ElementCreator.getChild(child);
|
|
1029
|
+
if(parent === anchorFragment) {
|
|
1030
|
+
parent.nativeInsertBefore(childElement, target);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
if(isParentUniqueChild(parent) && target === anchorEnd) {
|
|
1034
|
+
parent.append(childElement, target);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
parent.insertBefore(childElement, target);
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
anchorFragment.appendElement = function(child, before = null) {
|
|
1041
|
+
const parentNode = anchorStart.parentNode;
|
|
1042
|
+
const targetBefore = before || anchorEnd;
|
|
1043
|
+
if(parentNode === anchorFragment) {
|
|
1044
|
+
parentNode.nativeInsertBefore(child, targetBefore);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
parentNode?.insertBefore(child, targetBefore);
|
|
1048
|
+
};
|
|
1049
|
+
|
|
1050
|
+
anchorFragment.appendChild = function(child, before = null) {
|
|
1051
|
+
const parent = anchorEnd.parentNode;
|
|
1052
|
+
if(!parent) {
|
|
1053
|
+
DebugManager.error('Anchor', 'Anchor : parent not found', child);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
before = before ?? anchorEnd;
|
|
1057
|
+
insertBefore(parent, child, before);
|
|
1058
|
+
};
|
|
1059
|
+
|
|
1060
|
+
anchorFragment.append = function(...args ) {
|
|
1061
|
+
return anchorFragment.appendChild(args);
|
|
1062
|
+
};
|
|
1063
|
+
|
|
1064
|
+
anchorFragment.removeChildren = async function() {
|
|
1065
|
+
const parent = anchorEnd.parentNode;
|
|
1066
|
+
if(parent === anchorFragment) {
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
// if(isParentUniqueChild(parent)) {
|
|
1070
|
+
// parent.replaceChildren(anchorStart, anchorEnd);
|
|
1071
|
+
// return;
|
|
1072
|
+
// }
|
|
1073
|
+
|
|
1074
|
+
let itemToRemove = anchorStart.nextSibling, tempItem;
|
|
1075
|
+
const removes = [];
|
|
1076
|
+
while(itemToRemove && itemToRemove !== anchorEnd) {
|
|
1077
|
+
tempItem = itemToRemove.nextSibling;
|
|
1078
|
+
removes.push(itemToRemove.remove());
|
|
1079
|
+
itemToRemove = tempItem;
|
|
1080
|
+
}
|
|
1081
|
+
await Promise.all(removes);
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
anchorFragment.remove = async function() {
|
|
1085
|
+
const parent = anchorEnd.parentNode;
|
|
1086
|
+
if(parent === anchorFragment) {
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
let itemToRemove = anchorStart.nextSibling, tempItem;
|
|
1090
|
+
const allItemToRemove = [];
|
|
1091
|
+
const removes = [];
|
|
1092
|
+
while(itemToRemove && itemToRemove !== anchorEnd) {
|
|
1093
|
+
tempItem = itemToRemove.nextSibling;
|
|
1094
|
+
allItemToRemove.push(itemToRemove);
|
|
1095
|
+
removes.push(itemToRemove.remove());
|
|
1096
|
+
itemToRemove = tempItem;
|
|
1097
|
+
}
|
|
1098
|
+
await Promise.all(removes);
|
|
1099
|
+
anchorFragment.nativeAppend(...allItemToRemove);
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
anchorFragment.removeWithAnchors = async function() {
|
|
1103
|
+
await anchorFragment.removeChildren();
|
|
1104
|
+
anchorStart.remove();
|
|
1105
|
+
anchorEnd.remove();
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
anchorFragment.replaceContent = async function(child) {
|
|
1109
|
+
const childElement = Validator.isElement(child) ? child : ElementCreator.getChild(child);
|
|
1110
|
+
const parent = anchorEnd.parentNode;
|
|
1111
|
+
if(!parent) {
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
// if(isParentUniqueChild(parent)) {
|
|
1115
|
+
// parent.replaceChildren(anchorStart, childElement, anchorEnd);
|
|
1116
|
+
// return;
|
|
1117
|
+
// }
|
|
1118
|
+
await anchorFragment.removeChildren();
|
|
1119
|
+
parent.insertBefore(childElement, anchorEnd);
|
|
1120
|
+
};
|
|
1121
|
+
|
|
1122
|
+
anchorFragment.setContent = anchorFragment.replaceContent;
|
|
1123
|
+
|
|
1124
|
+
anchorFragment.insertBefore = function(child, anchor = null) {
|
|
1125
|
+
anchorFragment.appendChild(child, anchor);
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
anchorFragment.endElement = function() {
|
|
1130
|
+
return anchorEnd;
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
anchorFragment.startElement = function() {
|
|
1134
|
+
return anchorStart;
|
|
1135
|
+
};
|
|
1136
|
+
anchorFragment.restore = function() {
|
|
1137
|
+
anchorFragment.appendChild(anchorFragment);
|
|
1138
|
+
};
|
|
1139
|
+
anchorFragment.clear = anchorFragment.remove;
|
|
1140
|
+
anchorFragment.detach = anchorFragment.remove;
|
|
1141
|
+
|
|
1142
|
+
anchorFragment.getByIndex = function(index) {
|
|
1143
|
+
let currentNode = anchorStart;
|
|
1144
|
+
for(let i = 0; i <= index; i++) {
|
|
1145
|
+
if(!currentNode.nextSibling) {
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
1148
|
+
currentNode = currentNode.nextSibling;
|
|
1149
|
+
}
|
|
1150
|
+
return currentNode !== anchorStart ? currentNode : null;
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
return anchorFragment;
|
|
1154
|
+
}
|
|
1155
|
+
DocumentFragment.prototype.setAttribute = () => {};
|
|
1156
|
+
|
|
1157
|
+
const BOOLEAN_ATTRIBUTES = new Set([
|
|
1158
|
+
'checked',
|
|
1159
|
+
'selected',
|
|
1160
|
+
'disabled',
|
|
1161
|
+
'readonly',
|
|
1162
|
+
'required',
|
|
1163
|
+
'autofocus',
|
|
1164
|
+
'multiple',
|
|
1165
|
+
'autocomplete',
|
|
1166
|
+
'hidden',
|
|
1167
|
+
'contenteditable',
|
|
1168
|
+
'spellcheck',
|
|
1169
|
+
'translate',
|
|
1170
|
+
'draggable',
|
|
1171
|
+
'async',
|
|
1172
|
+
'defer',
|
|
1173
|
+
'autoplay',
|
|
1174
|
+
'controls',
|
|
1175
|
+
'loop',
|
|
1176
|
+
'muted',
|
|
1177
|
+
'download',
|
|
1178
|
+
'reversed',
|
|
1179
|
+
'open',
|
|
1180
|
+
'default',
|
|
1181
|
+
'formnovalidate',
|
|
1182
|
+
'novalidate',
|
|
1183
|
+
'scoped',
|
|
1184
|
+
'itemscope',
|
|
1185
|
+
'allowfullscreen',
|
|
1186
|
+
'allowpaymentrequest',
|
|
1187
|
+
'playsinline'
|
|
1188
|
+
]);
|
|
1189
|
+
|
|
1190
|
+
const MemoryManager = (function() {
|
|
1191
|
+
|
|
1192
|
+
let $nextObserverId = 0;
|
|
1193
|
+
const $observables = new Map();
|
|
1194
|
+
|
|
1195
|
+
return {
|
|
1196
|
+
/**
|
|
1197
|
+
* Register an observable and return an id.
|
|
1198
|
+
*
|
|
1199
|
+
* @param {ObservableItem} observable
|
|
1200
|
+
* @param {Function} getListeners
|
|
1201
|
+
* @returns {number}
|
|
1202
|
+
*/
|
|
1203
|
+
register(observable) {
|
|
1204
|
+
const id = ++$nextObserverId;
|
|
1205
|
+
$observables.set(id, new WeakRef(observable));
|
|
1206
|
+
return id;
|
|
1207
|
+
},
|
|
1208
|
+
unregister(id) {
|
|
1209
|
+
$observables.delete(id);
|
|
1210
|
+
},
|
|
1211
|
+
getObservableById(id) {
|
|
1212
|
+
return $observables.get(id)?.deref();
|
|
1213
|
+
},
|
|
1214
|
+
cleanup() {
|
|
1215
|
+
for (const [_, weakObservableRef] of $observables) {
|
|
1216
|
+
const observable = weakObservableRef.deref();
|
|
1217
|
+
if (observable) {
|
|
1218
|
+
observable.cleanup();
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
$observables.clear();
|
|
1222
|
+
},
|
|
1223
|
+
/**
|
|
1224
|
+
* Clean observables that are not referenced anymore.
|
|
1225
|
+
* @param {number} threshold
|
|
1226
|
+
*/
|
|
1227
|
+
cleanObservables(threshold) {
|
|
1228
|
+
if($observables.size < threshold) return;
|
|
1229
|
+
let cleanedCount = 0;
|
|
1230
|
+
for (const [id, weakObservableRef] of $observables) {
|
|
1231
|
+
if (!weakObservableRef.deref()) {
|
|
1232
|
+
$observables.delete(id);
|
|
1233
|
+
cleanedCount++;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
if (cleanedCount > 0) {
|
|
1237
|
+
DebugManager.log('Memory Auto Clean', `🧹 Cleaned ${cleanedCount} orphaned observables`);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
}());
|
|
1242
|
+
|
|
1002
1243
|
/**
|
|
1003
|
-
* Creates an ObservableWhen that
|
|
1004
|
-
* Returns an object that can be subscribed to and will emit true/false.
|
|
1244
|
+
* Creates an ObservableWhen that tracks whether an observable equals a specific value.
|
|
1005
1245
|
*
|
|
1246
|
+
* @param {ObservableItem} observer - The observable to watch
|
|
1006
1247
|
* @param {*} value - The value to compare against
|
|
1007
|
-
* @
|
|
1248
|
+
* @class ObservableWhen
|
|
1249
|
+
*/
|
|
1250
|
+
const ObservableWhen = function(observer, value) {
|
|
1251
|
+
this.$target = value;
|
|
1252
|
+
this.$observer = observer;
|
|
1253
|
+
};
|
|
1254
|
+
|
|
1255
|
+
ObservableWhen.prototype.__$isObservableWhen = true;
|
|
1256
|
+
|
|
1257
|
+
/**
|
|
1258
|
+
* Subscribes to changes in the match status (true when observable equals target value).
|
|
1259
|
+
*
|
|
1260
|
+
* @param {Function} callback - Function called with boolean indicating if values match
|
|
1261
|
+
* @returns {Function} Unsubscribe function
|
|
1008
1262
|
* @example
|
|
1009
1263
|
* const status = Observable('idle');
|
|
1010
1264
|
* const isLoading = status.when('loading');
|
|
1011
1265
|
* isLoading.subscribe(active => console.log('Loading:', active));
|
|
1012
|
-
* status.set('loading'); // Logs: "Loading: true"
|
|
1013
1266
|
*/
|
|
1014
|
-
|
|
1015
|
-
return
|
|
1267
|
+
ObservableWhen.prototype.subscribe = function(callback) {
|
|
1268
|
+
return this.$observer.on(this.$target, callback);
|
|
1016
1269
|
};
|
|
1017
1270
|
|
|
1018
1271
|
/**
|
|
1019
|
-
*
|
|
1272
|
+
* Returns true if the observable's current value equals the target value.
|
|
1020
1273
|
*
|
|
1021
|
-
* @
|
|
1022
|
-
* @returns {boolean} True if values are equal
|
|
1023
|
-
* @example
|
|
1024
|
-
* const a = Observable(5);
|
|
1025
|
-
* const b = Observable(5);
|
|
1026
|
-
* a.equals(5); // true
|
|
1027
|
-
* a.equals(b); // true
|
|
1028
|
-
* a.equals(10); // false
|
|
1274
|
+
* @returns {boolean} True if observable value matches target value
|
|
1029
1275
|
*/
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
return this.$currentValue === other.$currentValue;
|
|
1033
|
-
}
|
|
1034
|
-
return this.$currentValue === other;
|
|
1276
|
+
ObservableWhen.prototype.val = function() {
|
|
1277
|
+
return this.$observer.$currentValue === this.$target;
|
|
1035
1278
|
};
|
|
1036
1279
|
|
|
1037
1280
|
/**
|
|
1038
|
-
*
|
|
1281
|
+
* Returns true if the observable's current value equals the target value.
|
|
1282
|
+
* Alias for val().
|
|
1039
1283
|
*
|
|
1040
|
-
* @returns {boolean}
|
|
1041
|
-
* @example
|
|
1042
|
-
* const count = Observable(0);
|
|
1043
|
-
* count.toBool(); // false
|
|
1044
|
-
* count.set(5);
|
|
1045
|
-
* count.toBool(); // true
|
|
1284
|
+
* @returns {boolean} True if observable value matches target value
|
|
1046
1285
|
*/
|
|
1047
|
-
|
|
1048
|
-
return !!this.$currentValue;
|
|
1049
|
-
};
|
|
1286
|
+
ObservableWhen.prototype.isMatch = ObservableWhen.prototype.val;
|
|
1050
1287
|
|
|
1051
1288
|
/**
|
|
1052
|
-
*
|
|
1289
|
+
* Returns true if the observable's current value equals the target value.
|
|
1290
|
+
* Alias for val().
|
|
1053
1291
|
*
|
|
1054
|
-
* @
|
|
1055
|
-
* const isOpen = Observable(false);
|
|
1056
|
-
* isOpen.toggle(); // Now true
|
|
1057
|
-
* isOpen.toggle(); // Now false
|
|
1292
|
+
* @returns {boolean} True if observable value matches target value
|
|
1058
1293
|
*/
|
|
1059
|
-
|
|
1060
|
-
|
|
1294
|
+
ObservableWhen.prototype.isActive = ObservableWhen.prototype.val;
|
|
1295
|
+
|
|
1296
|
+
const invoke = function(fn, args, context) {
|
|
1297
|
+
if(context) {
|
|
1298
|
+
fn.apply(context, args);
|
|
1299
|
+
} else {
|
|
1300
|
+
fn(...args);
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
/**
|
|
1304
|
+
*
|
|
1305
|
+
* @param {Function} fn
|
|
1306
|
+
* @param {number} delay
|
|
1307
|
+
* @param {{leading?:Boolean, trailing?:Boolean, debounce?:Boolean, check: Function}}options
|
|
1308
|
+
* @returns {(function(...[*]): void)|*}
|
|
1309
|
+
*/
|
|
1310
|
+
const debounce = function(fn, delay, options = {}) {
|
|
1311
|
+
let timer = null;
|
|
1312
|
+
let lastArgs = null;
|
|
1313
|
+
|
|
1314
|
+
return function(...args) {
|
|
1315
|
+
const context = options.context === true ? this : null;
|
|
1316
|
+
if(options.check) {
|
|
1317
|
+
options.check(...args);
|
|
1318
|
+
}
|
|
1319
|
+
lastArgs = args;
|
|
1320
|
+
|
|
1321
|
+
// debounce mode: reset the timer for each call
|
|
1322
|
+
clearTimeout(timer);
|
|
1323
|
+
timer = setTimeout(() => invoke(fn, lastArgs, context), delay);
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
|
|
1327
|
+
const nextTick = function(fn) {
|
|
1328
|
+
let pending = false;
|
|
1329
|
+
return function(...args) {
|
|
1330
|
+
if (pending) return;
|
|
1331
|
+
pending = true;
|
|
1332
|
+
|
|
1333
|
+
Promise.resolve().then(() => {
|
|
1334
|
+
fn.apply(this, args);
|
|
1335
|
+
pending = false;
|
|
1336
|
+
});
|
|
1337
|
+
};
|
|
1338
|
+
};
|
|
1339
|
+
|
|
1340
|
+
const deepClone = (value, onObservableFound) => {
|
|
1341
|
+
try {
|
|
1342
|
+
if(window.structuredClone !== undefined) {
|
|
1343
|
+
return window.structuredClone(value);
|
|
1344
|
+
}
|
|
1345
|
+
} catch (e){}
|
|
1346
|
+
|
|
1347
|
+
if (value === null || typeof value !== 'object') {
|
|
1348
|
+
return value;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// Dates
|
|
1352
|
+
if (value instanceof Date) {
|
|
1353
|
+
return new Date(value.getTime());
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// Arrays
|
|
1357
|
+
if (Array.isArray(value)) {
|
|
1358
|
+
return value.map(item => deepClone(item));
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// Observables - keep the référence
|
|
1362
|
+
if (Validator.isObservable(value)) {
|
|
1363
|
+
onObservableFound && onObservableFound(value);
|
|
1364
|
+
return value;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// Objects
|
|
1368
|
+
const cloned = {};
|
|
1369
|
+
for (const key in value) {
|
|
1370
|
+
if (Object.hasOwn(value, key)) {
|
|
1371
|
+
cloned[key] = deepClone(value[key]);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
return cloned;
|
|
1375
|
+
};
|
|
1376
|
+
|
|
1377
|
+
const LocalStorage = {
|
|
1378
|
+
getJson(key) {
|
|
1379
|
+
let value = localStorage.getItem(key);
|
|
1380
|
+
try {
|
|
1381
|
+
return JSON.parse(value);
|
|
1382
|
+
} catch (e) {
|
|
1383
|
+
throw new NativeDocumentError('invalid_json:'+key);
|
|
1384
|
+
}
|
|
1385
|
+
},
|
|
1386
|
+
getNumber(key) {
|
|
1387
|
+
return Number(this.get(key));
|
|
1388
|
+
},
|
|
1389
|
+
getBool(key) {
|
|
1390
|
+
const value = this.get(key);
|
|
1391
|
+
return value === 'true' || value === '1';
|
|
1392
|
+
},
|
|
1393
|
+
setJson(key, value) {
|
|
1394
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
1395
|
+
},
|
|
1396
|
+
setBool(key, value) {
|
|
1397
|
+
localStorage.setItem(key, value ? 'true' : 'false');
|
|
1398
|
+
},
|
|
1399
|
+
get(key, defaultValue = null) {
|
|
1400
|
+
return localStorage.getItem(key) || defaultValue;
|
|
1401
|
+
},
|
|
1402
|
+
set(key, value) {
|
|
1403
|
+
return localStorage.setItem(key, value);
|
|
1404
|
+
},
|
|
1405
|
+
remove(key) {
|
|
1406
|
+
localStorage.removeItem(key);
|
|
1407
|
+
},
|
|
1408
|
+
has(key) {
|
|
1409
|
+
return localStorage.getItem(key) != null;
|
|
1410
|
+
}
|
|
1061
1411
|
};
|
|
1062
1412
|
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
ObservableItem.prototype.reset = function() {
|
|
1073
|
-
if(!this.configs?.reset) {
|
|
1074
|
-
return;
|
|
1413
|
+
const $getFromStorage = (key, value) => {
|
|
1414
|
+
if(!LocalStorage.has(key)) {
|
|
1415
|
+
return value;
|
|
1416
|
+
}
|
|
1417
|
+
switch (typeof value) {
|
|
1418
|
+
case 'object': return LocalStorage.getJson(key) ?? value;
|
|
1419
|
+
case 'boolean': return LocalStorage.getBool(key) ?? value;
|
|
1420
|
+
case 'number': return LocalStorage.getNumber(key) ?? value;
|
|
1421
|
+
default: return LocalStorage.get(key, value) ?? value;
|
|
1075
1422
|
}
|
|
1076
|
-
const resetValue = (Validator.isObject(this.$initialValue))
|
|
1077
|
-
? deepClone(this.$initialValue, (observable) => {
|
|
1078
|
-
observable.reset();
|
|
1079
|
-
})
|
|
1080
|
-
: this.$initialValue;
|
|
1081
|
-
this.set(resetValue);
|
|
1082
1423
|
};
|
|
1083
1424
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
return String(this.$currentValue);
|
|
1425
|
+
const $saveToStorage = (value) => {
|
|
1426
|
+
switch (typeof value) {
|
|
1427
|
+
case 'object': return LocalStorage.setJson;
|
|
1428
|
+
case 'boolean': return LocalStorage.setBool;
|
|
1429
|
+
default: return LocalStorage.set;
|
|
1430
|
+
}
|
|
1091
1431
|
};
|
|
1092
1432
|
|
|
1093
|
-
|
|
1094
|
-
* Returns the primitive value of the observable (its current value).
|
|
1095
|
-
* Called automatically in type coercion contexts.
|
|
1096
|
-
*
|
|
1097
|
-
* @returns {*} The current value of the observable
|
|
1098
|
-
*/
|
|
1099
|
-
ObservableItem.prototype.valueOf = function() {
|
|
1100
|
-
return this.$currentValue;
|
|
1101
|
-
};
|
|
1433
|
+
const StoreFactory = function() {
|
|
1102
1434
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
beforeUnmount: new WeakMap(),
|
|
1106
|
-
mountedSupposedSize: 0,
|
|
1107
|
-
unmounted: new WeakMap(),
|
|
1108
|
-
unmountedSupposedSize: 0,
|
|
1109
|
-
observer: null,
|
|
1435
|
+
const $stores = new Map();
|
|
1436
|
+
const $followersCache = new Map();
|
|
1110
1437
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
for(const cb of data.mounted) {
|
|
1122
|
-
cb(node);
|
|
1123
|
-
}
|
|
1124
|
-
return;
|
|
1438
|
+
/**
|
|
1439
|
+
* Internal helper — retrieves a store entry or throws if not found.
|
|
1440
|
+
*/
|
|
1441
|
+
const $getStoreOrThrow = (method, name) => {
|
|
1442
|
+
const item = $stores.get(name);
|
|
1443
|
+
if (!item) {
|
|
1444
|
+
DebugManager.error('Store', `Store.${method}('${name}') : store not found. Did you call Store.create('${name}') first?`);
|
|
1445
|
+
throw new NativeDocumentError(
|
|
1446
|
+
`Store.${method}('${name}') : store not found.`
|
|
1447
|
+
);
|
|
1125
1448
|
}
|
|
1126
|
-
|
|
1127
|
-
}
|
|
1449
|
+
return item;
|
|
1450
|
+
};
|
|
1128
1451
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1452
|
+
/**
|
|
1453
|
+
* Internal helper — blocks write operations on a read-only observer.
|
|
1454
|
+
*/
|
|
1455
|
+
const $applyReadOnly = (observer, name, context) => {
|
|
1456
|
+
const readOnlyError = (method) => () => {
|
|
1457
|
+
DebugManager.error('Store', `Store.${context}('${name}') is read-only. '${method}()' is not allowed.`);
|
|
1458
|
+
throw new NativeDocumentError(
|
|
1459
|
+
`Store.${context}('${name}') is read-only.`
|
|
1460
|
+
);
|
|
1461
|
+
};
|
|
1462
|
+
observer.set = readOnlyError('set');
|
|
1463
|
+
observer.toggle = readOnlyError('toggle');
|
|
1464
|
+
observer.reset = readOnlyError('reset');
|
|
1465
|
+
};
|
|
1138
1466
|
|
|
1139
|
-
|
|
1140
|
-
if(Array.isArray(
|
|
1141
|
-
|
|
1142
|
-
if(cb(node) === true) {
|
|
1143
|
-
shouldRemove = true;
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
} else {
|
|
1147
|
-
shouldRemove = data.unmounted(node) === true;
|
|
1467
|
+
const $createObservable = (value, options = {}) => {
|
|
1468
|
+
if(Array.isArray(value)) {
|
|
1469
|
+
return Observable.array(value, options);
|
|
1148
1470
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
data.disconnect();
|
|
1152
|
-
node.nd?.remove();
|
|
1471
|
+
if(typeof value === 'object') {
|
|
1472
|
+
return Observable.object(value, options);
|
|
1153
1473
|
}
|
|
1154
|
-
|
|
1474
|
+
return Observable(value, options);
|
|
1475
|
+
};
|
|
1155
1476
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1477
|
+
const $api = {
|
|
1478
|
+
/**
|
|
1479
|
+
* Create a new state and return the observer.
|
|
1480
|
+
* Throws if a store with the same name already exists.
|
|
1481
|
+
*
|
|
1482
|
+
* @param {string} name
|
|
1483
|
+
* @param {*} value
|
|
1484
|
+
* @returns {ObservableItem}
|
|
1485
|
+
*/
|
|
1486
|
+
create(name, value) {
|
|
1487
|
+
if ($stores.has(name)) {
|
|
1488
|
+
DebugManager.warn('Store', `Store.create('${name}') : a store with this name already exists. Use Store.get('${name}') to retrieve it.`);
|
|
1489
|
+
throw new NativeDocumentError(
|
|
1490
|
+
`Store.create('${name}') : a store with this name already exists.`
|
|
1491
|
+
);
|
|
1169
1492
|
}
|
|
1493
|
+
const observer = $createObservable(value);
|
|
1494
|
+
$stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: false });
|
|
1495
|
+
return observer;
|
|
1496
|
+
},
|
|
1170
1497
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1498
|
+
/**
|
|
1499
|
+
* Create a new resettable state and return the observer.
|
|
1500
|
+
* The store can be reset to its initial value via Store.reset(name).
|
|
1501
|
+
* Throws if a store with the same name already exists.
|
|
1502
|
+
*
|
|
1503
|
+
* @param {string} name
|
|
1504
|
+
* @param {*} value
|
|
1505
|
+
* @returns {ObservableItem}
|
|
1506
|
+
*/
|
|
1507
|
+
createResettable(name, value) {
|
|
1508
|
+
if ($stores.has(name)) {
|
|
1509
|
+
DebugManager.warn('Store', `Store.createResettable('${name}') : a store with this name already exists.`);
|
|
1510
|
+
throw new NativeDocumentError(
|
|
1511
|
+
`Store.createResettable('${name}') : a store with this name already exists.`
|
|
1512
|
+
);
|
|
1182
1513
|
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1514
|
+
const observer = $createObservable(value, { reset: true });
|
|
1515
|
+
$stores.set(name, { observer, subscribers: new Set(), resettable: true, composed: false });
|
|
1516
|
+
return observer;
|
|
1517
|
+
},
|
|
1185
1518
|
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1519
|
+
/**
|
|
1520
|
+
* Create a computed store derived from other stores.
|
|
1521
|
+
* The value is automatically recalculated when any dependency changes.
|
|
1522
|
+
* This store is read-only — Store.use() and Store.set() will throw.
|
|
1523
|
+
* Throws if a store with the same name already exists.
|
|
1524
|
+
*
|
|
1525
|
+
* @param {string} name
|
|
1526
|
+
* @param {() => *} computation - Function that returns the computed value
|
|
1527
|
+
* @param {string[]} dependencies - Names of the stores to watch
|
|
1528
|
+
* @returns {ObservableItem}
|
|
1529
|
+
*
|
|
1530
|
+
* @example
|
|
1531
|
+
* Store.create('products', [{ id: 1, price: 10 }]);
|
|
1532
|
+
* Store.create('cart', [{ productId: 1, quantity: 2 }]);
|
|
1533
|
+
*
|
|
1534
|
+
* Store.createComposed('total', () => {
|
|
1535
|
+
* const products = Store.get('products').val();
|
|
1536
|
+
* const cart = Store.get('cart').val();
|
|
1537
|
+
* return cart.reduce((sum, item) => {
|
|
1538
|
+
* const product = products.find(p => p.id === item.productId);
|
|
1539
|
+
* return sum + (product.price * item.quantity);
|
|
1540
|
+
* }, 0);
|
|
1541
|
+
* }, ['products', 'cart']);
|
|
1542
|
+
*/
|
|
1543
|
+
createComposed(name, computation, dependencies) {
|
|
1544
|
+
if ($stores.has(name)) {
|
|
1545
|
+
DebugManager.warn('Store', `Store.createComposed('${name}') : a store with this name already exists.`);
|
|
1546
|
+
throw new NativeDocumentError(
|
|
1547
|
+
`Store.createComposed('${name}') : a store with this name already exists.`
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
if (typeof computation !== 'function') {
|
|
1551
|
+
throw new NativeDocumentError(
|
|
1552
|
+
`Store.createComposed('${name}') : computation must be a function.`
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1555
|
+
if (!Array.isArray(dependencies) || dependencies.length === 0) {
|
|
1556
|
+
throw new NativeDocumentError(
|
|
1557
|
+
`Store.createComposed('${name}') : dependencies must be a non-empty array of store names.`
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1194
1560
|
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
disconnect: () => {
|
|
1200
|
-
if (mountedRegistered) {
|
|
1201
|
-
DocumentObserver.mounted.delete(element);
|
|
1202
|
-
DocumentObserver.mountedSupposedSize--;
|
|
1561
|
+
// Resolve dependency observers
|
|
1562
|
+
const depObservers = dependencies.map(depName => {
|
|
1563
|
+
if(typeof depName !== 'string') {
|
|
1564
|
+
return depName;
|
|
1203
1565
|
}
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1566
|
+
const depItem = $stores.get(depName);
|
|
1567
|
+
if (!depItem) {
|
|
1568
|
+
DebugManager.error('Store', `Store.createComposed('${name}') : dependency '${depName}' not found. Create it first.`);
|
|
1569
|
+
throw new NativeDocumentError(
|
|
1570
|
+
`Store.createComposed('${name}') : dependency store '${depName}' not found.`
|
|
1571
|
+
);
|
|
1207
1572
|
}
|
|
1208
|
-
|
|
1209
|
-
}
|
|
1210
|
-
};
|
|
1573
|
+
return depItem.observer;
|
|
1574
|
+
});
|
|
1211
1575
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1576
|
+
// Create computed observable from dependency observers
|
|
1577
|
+
const observer = Observable.computed(computation, depObservers);
|
|
1578
|
+
|
|
1579
|
+
$stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: true });
|
|
1580
|
+
return observer;
|
|
1581
|
+
},
|
|
1582
|
+
|
|
1583
|
+
/**
|
|
1584
|
+
* Returns true if a store with the given name exists.
|
|
1585
|
+
*
|
|
1586
|
+
* @param {string} name
|
|
1587
|
+
* @returns {boolean}
|
|
1588
|
+
*/
|
|
1589
|
+
has(name) {
|
|
1590
|
+
return $stores.has(name);
|
|
1591
|
+
},
|
|
1592
|
+
|
|
1593
|
+
/**
|
|
1594
|
+
* Resets a resettable store to its initial value and notifies all subscribers.
|
|
1595
|
+
* Throws if the store was not created with createResettable().
|
|
1596
|
+
*
|
|
1597
|
+
* @param {string} name
|
|
1598
|
+
*/
|
|
1599
|
+
reset(name) {
|
|
1600
|
+
const item = $getStoreOrThrow('reset', name);
|
|
1601
|
+
if (item.composed) {
|
|
1602
|
+
DebugManager.error('Store', `Store.reset('${name}') : composed stores cannot be reset. Their value is derived from dependencies.`);
|
|
1603
|
+
throw new NativeDocumentError(
|
|
1604
|
+
`Store.reset('${name}') : composed stores cannot be reset.`
|
|
1605
|
+
);
|
|
1216
1606
|
}
|
|
1217
|
-
if (!
|
|
1218
|
-
|
|
1219
|
-
|
|
1607
|
+
if (!item.resettable) {
|
|
1608
|
+
DebugManager.error('Store', `Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`);
|
|
1609
|
+
throw new NativeDocumentError(
|
|
1610
|
+
`Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`
|
|
1611
|
+
);
|
|
1220
1612
|
}
|
|
1221
|
-
|
|
1222
|
-
}
|
|
1613
|
+
item.observer.reset();
|
|
1614
|
+
},
|
|
1223
1615
|
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1616
|
+
/**
|
|
1617
|
+
* Returns a two-way synchronized follower of the store.
|
|
1618
|
+
* Writing to the follower propagates the value back to the store and all its subscribers.
|
|
1619
|
+
* Throws if called on a composed store — use Store.follow() instead.
|
|
1620
|
+
* Call follower.destroy() or follower.dispose() to unsubscribe.
|
|
1621
|
+
*
|
|
1622
|
+
* @param {string} name
|
|
1623
|
+
* @returns {ObservableItem}
|
|
1624
|
+
*/
|
|
1625
|
+
use(name) {
|
|
1626
|
+
const item = $getStoreOrThrow('use', name);
|
|
1627
|
+
|
|
1628
|
+
if (item.composed) {
|
|
1629
|
+
DebugManager.error('Store', `Store.use('${name}') : composed stores are read-only. Use Store.follow('${name}') instead.`);
|
|
1630
|
+
throw new NativeDocumentError(
|
|
1631
|
+
`Store.use('${name}') : composed stores are read-only. Use Store.follow('${name}') instead.`
|
|
1632
|
+
);
|
|
1240
1633
|
}
|
|
1241
|
-
data[type] = null;
|
|
1242
|
-
};
|
|
1243
1634
|
|
|
1244
|
-
|
|
1245
|
-
|
|
1635
|
+
const { observer: originalObserver, subscribers } = item;
|
|
1636
|
+
const observerFollower = $createObservable(originalObserver.val());
|
|
1246
1637
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
DocumentObserver.mounted.set(element, data);
|
|
1250
|
-
if (!mountedRegistered) {
|
|
1251
|
-
DocumentObserver.mountedSupposedSize++;
|
|
1252
|
-
mountedRegistered = true;
|
|
1253
|
-
}
|
|
1254
|
-
},
|
|
1638
|
+
const onStoreChange = value => observerFollower.set(value);
|
|
1639
|
+
const onFollowerChange = value => originalObserver.set(value);
|
|
1255
1640
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
DocumentObserver.unmounted.set(element, data);
|
|
1259
|
-
if (!unmountedRegistered) {
|
|
1260
|
-
DocumentObserver.unmountedSupposedSize++;
|
|
1261
|
-
unmountedRegistered = true;
|
|
1262
|
-
}
|
|
1263
|
-
},
|
|
1641
|
+
originalObserver.subscribe(onStoreChange);
|
|
1642
|
+
observerFollower.subscribe(onFollowerChange);
|
|
1264
1643
|
|
|
1265
|
-
|
|
1266
|
-
|
|
1644
|
+
observerFollower.destroy = () => {
|
|
1645
|
+
originalObserver.unsubscribe(onStoreChange);
|
|
1646
|
+
observerFollower.unsubscribe(onFollowerChange);
|
|
1647
|
+
subscribers.delete(observerFollower);
|
|
1648
|
+
observerFollower.cleanup();
|
|
1649
|
+
};
|
|
1650
|
+
observerFollower.dispose = observerFollower.destroy;
|
|
1651
|
+
|
|
1652
|
+
subscribers.add(observerFollower);
|
|
1653
|
+
return observerFollower;
|
|
1654
|
+
},
|
|
1655
|
+
|
|
1656
|
+
/**
|
|
1657
|
+
* Returns a read-only follower of the store.
|
|
1658
|
+
* The follower reflects store changes but cannot write back to the store.
|
|
1659
|
+
* Any attempt to call .set(), .toggle() or .reset() will throw.
|
|
1660
|
+
* Call follower.destroy() or follower.dispose() to unsubscribe.
|
|
1661
|
+
*
|
|
1662
|
+
* @param {string} name
|
|
1663
|
+
* @returns {ObservableItem}
|
|
1664
|
+
*/
|
|
1665
|
+
follow(name) {
|
|
1666
|
+
const { observer: originalObserver, subscribers } = $getStoreOrThrow('follow', name);
|
|
1667
|
+
const observerFollower = $createObservable(originalObserver.val());
|
|
1668
|
+
|
|
1669
|
+
const onStoreChange = value => observerFollower.set(value);
|
|
1670
|
+
originalObserver.subscribe(onStoreChange);
|
|
1671
|
+
|
|
1672
|
+
$applyReadOnly(observerFollower, name, 'follow');
|
|
1673
|
+
|
|
1674
|
+
observerFollower.destroy = () => {
|
|
1675
|
+
originalObserver.unsubscribe(onStoreChange);
|
|
1676
|
+
subscribers.delete(observerFollower);
|
|
1677
|
+
observerFollower.cleanup();
|
|
1678
|
+
};
|
|
1679
|
+
observerFollower.dispose = observerFollower.destroy;
|
|
1680
|
+
|
|
1681
|
+
subscribers.add(observerFollower);
|
|
1682
|
+
return observerFollower;
|
|
1683
|
+
},
|
|
1684
|
+
|
|
1685
|
+
/**
|
|
1686
|
+
* Returns the raw store observer directly (no follower, no cleanup contract).
|
|
1687
|
+
* Use this for direct read access when you don't need to unsubscribe.
|
|
1688
|
+
* WARNING : mutations on this observer impact all subscribers immediately.
|
|
1689
|
+
*
|
|
1690
|
+
* @param {string} name
|
|
1691
|
+
* @returns {ObservableItem|null}
|
|
1692
|
+
*/
|
|
1693
|
+
get(name) {
|
|
1694
|
+
const item = $stores.get(name);
|
|
1695
|
+
if (!item) {
|
|
1696
|
+
DebugManager.warn('Store', `Store.get('${name}') : store not found.`);
|
|
1697
|
+
return null;
|
|
1698
|
+
}
|
|
1699
|
+
return item.observer;
|
|
1700
|
+
},
|
|
1701
|
+
|
|
1702
|
+
/**
|
|
1703
|
+
* @param {string} name
|
|
1704
|
+
* @returns {{ observer: ObservableItem, subscribers: Set } | null}
|
|
1705
|
+
*/
|
|
1706
|
+
getWithSubscribers(name) {
|
|
1707
|
+
return $stores.get(name) ?? null;
|
|
1708
|
+
},
|
|
1709
|
+
|
|
1710
|
+
/**
|
|
1711
|
+
* Destroys a store : cleans up the observer, destroys all followers, and removes the entry.
|
|
1712
|
+
*
|
|
1713
|
+
* @param {string} name
|
|
1714
|
+
*/
|
|
1715
|
+
delete(name) {
|
|
1716
|
+
const item = $stores.get(name);
|
|
1717
|
+
if (!item) {
|
|
1718
|
+
DebugManager.warn('Store', `Store.delete('${name}') : store not found, nothing to delete.`);
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
item.subscribers.forEach(follower => follower.destroy());
|
|
1722
|
+
item.subscribers.clear();
|
|
1723
|
+
item.observer.cleanup();
|
|
1724
|
+
$stores.delete(name);
|
|
1725
|
+
},
|
|
1726
|
+
/**
|
|
1727
|
+
* Creates an isolated store group with its own state namespace.
|
|
1728
|
+
* Each group is a fully independent StoreFactory instance —
|
|
1729
|
+
* no key conflicts, no shared state with the parent store.
|
|
1730
|
+
*
|
|
1731
|
+
* @param {string | ((group: ReturnType<typeof StoreFactory>) => void)} name - Group name for debugging, or setup callback if no name is provided
|
|
1732
|
+
* @param {((group: ReturnType<typeof StoreFactory>) => void)} [callback] - Setup function receiving the isolated store instance
|
|
1733
|
+
* @returns {ReturnType<typeof StoreFactory>}
|
|
1734
|
+
*
|
|
1735
|
+
* @example
|
|
1736
|
+
* // With name (recommended)
|
|
1737
|
+
* const EventStore = Store.group('events', (group) => {
|
|
1738
|
+
* group.create('catalog', []);
|
|
1739
|
+
* group.create('filters', { category: null, date: null });
|
|
1740
|
+
* group.createResettable('selected', null);
|
|
1741
|
+
* group.createComposed('filtered', () => {
|
|
1742
|
+
* const catalog = EventStore.get('catalog').val();
|
|
1743
|
+
* const filters = EventStore.get('filters').val();
|
|
1744
|
+
* return catalog.filter(event => {
|
|
1745
|
+
* if (filters.category && event.category !== filters.category) return false;
|
|
1746
|
+
* return true;
|
|
1747
|
+
* });
|
|
1748
|
+
* }, ['catalog', 'filters']);
|
|
1749
|
+
* });
|
|
1750
|
+
*
|
|
1751
|
+
* // Without name
|
|
1752
|
+
* const CartStore = Store.group((group) => {
|
|
1753
|
+
* group.create('items', []);
|
|
1754
|
+
* });
|
|
1755
|
+
*
|
|
1756
|
+
* // Usage
|
|
1757
|
+
* EventStore.use('catalog'); // two-way follower
|
|
1758
|
+
* EventStore.follow('filtered'); // read-only follower
|
|
1759
|
+
* EventStore.get('filters'); // raw observable
|
|
1760
|
+
*
|
|
1761
|
+
* // Cross-group composed
|
|
1762
|
+
* const OrderStore = Store.group('orders', (group) => {
|
|
1763
|
+
* group.createComposed('summary', () => {
|
|
1764
|
+
* const items = CartStore.get('items').val();
|
|
1765
|
+
* const events = EventStore.get('catalog').val();
|
|
1766
|
+
* return { items, events };
|
|
1767
|
+
* }, [CartStore.get('items'), EventStore.get('catalog')]);
|
|
1768
|
+
* });
|
|
1769
|
+
*/
|
|
1770
|
+
group(name, callback) {
|
|
1771
|
+
if (typeof name === 'function') {
|
|
1772
|
+
callback = name;
|
|
1773
|
+
name = 'anonymous';
|
|
1267
1774
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1775
|
+
const store = StoreFactory();
|
|
1776
|
+
callback && callback(store);
|
|
1777
|
+
return store;
|
|
1778
|
+
},
|
|
1779
|
+
createPersistent(name, value, localstorage_key) {
|
|
1780
|
+
localstorage_key = localstorage_key || name;
|
|
1781
|
+
const observer = this.create(name, $getFromStorage(localstorage_key, value));
|
|
1782
|
+
const saver = $saveToStorage(value);
|
|
1271
1783
|
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1784
|
+
observer.subscribe((val) => saver(localstorage_key, val));
|
|
1785
|
+
return observer;
|
|
1786
|
+
},
|
|
1787
|
+
createPersistentResettable(name, value, localstorage_key) {
|
|
1788
|
+
localstorage_key = localstorage_key || name;
|
|
1789
|
+
const observer = this.createResettable(name, $getFromStorage(localstorage_key, value));
|
|
1790
|
+
const saver = $saveToStorage(value);
|
|
1791
|
+
observer.subscribe((val) => saver(localstorage_key, val));
|
|
1792
|
+
|
|
1793
|
+
const originalReset = observer.reset.bind(observer);
|
|
1794
|
+
observer.reset = () => {
|
|
1795
|
+
LocalStorage.remove(localstorage_key);
|
|
1796
|
+
originalReset();
|
|
1797
|
+
};
|
|
1277
1798
|
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
}
|
|
1799
|
+
return observer;
|
|
1800
|
+
}
|
|
1801
|
+
};
|
|
1282
1802
|
|
|
1283
|
-
NDElement.prototype.__$isNDElement = true;
|
|
1284
1803
|
|
|
1285
|
-
|
|
1286
|
-
|
|
1804
|
+
return new Proxy($api, {
|
|
1805
|
+
get(target, prop) {
|
|
1806
|
+
if (typeof prop === 'symbol' || prop.startsWith('$') || prop in target) {
|
|
1807
|
+
return target[prop];
|
|
1808
|
+
}
|
|
1809
|
+
if (target.has(prop)) {
|
|
1810
|
+
if ($followersCache.has(prop)) {
|
|
1811
|
+
return $followersCache.get(prop);
|
|
1812
|
+
}
|
|
1813
|
+
const follower = target.follow(prop);
|
|
1814
|
+
$followersCache.set(prop, follower);
|
|
1815
|
+
return follower;
|
|
1816
|
+
}
|
|
1817
|
+
return undefined;
|
|
1818
|
+
},
|
|
1819
|
+
set(target, prop, value) {
|
|
1820
|
+
DebugManager.error('Store', `Forbidden: You cannot overwrite the store key '${String(prop)}'. Use .use('${String(prop)}').set(value) instead.`);
|
|
1821
|
+
throw new NativeDocumentError(`Store structure is immutable. Use .set() on the observable.`);
|
|
1822
|
+
},
|
|
1823
|
+
deleteProperty(target, prop) {
|
|
1824
|
+
throw new NativeDocumentError(`Store keys cannot be deleted.`);
|
|
1825
|
+
}
|
|
1826
|
+
});
|
|
1287
1827
|
};
|
|
1288
1828
|
|
|
1289
|
-
|
|
1290
|
-
target[name] = this.$element;
|
|
1291
|
-
return this;
|
|
1292
|
-
};
|
|
1829
|
+
const Store = StoreFactory();
|
|
1293
1830
|
|
|
1294
|
-
|
|
1295
|
-
target[name] = this;
|
|
1296
|
-
return this;
|
|
1297
|
-
};
|
|
1831
|
+
Store.create('locale', 'fr');
|
|
1298
1832
|
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1833
|
+
const $parseDateParts = (value, locale) => {
|
|
1834
|
+
const d = new Date(value);
|
|
1835
|
+
return {
|
|
1836
|
+
d,
|
|
1837
|
+
parts: new Intl.DateTimeFormat(locale, {
|
|
1838
|
+
year: 'numeric',
|
|
1839
|
+
month: 'long',
|
|
1840
|
+
day: '2-digit',
|
|
1841
|
+
hour: '2-digit',
|
|
1842
|
+
minute: '2-digit',
|
|
1843
|
+
second: '2-digit',
|
|
1844
|
+
}).formatToParts(d).reduce((acc, { type, value }) => {
|
|
1845
|
+
acc[type] = value;
|
|
1846
|
+
return acc;
|
|
1847
|
+
}, {})
|
|
1848
|
+
};
|
|
1310
1849
|
};
|
|
1311
1850
|
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1851
|
+
const $applyDatePattern = (pattern, d, parts) => {
|
|
1852
|
+
const pad = n => String(n).padStart(2, '0');
|
|
1853
|
+
return pattern
|
|
1854
|
+
.replace('YYYY', parts.year)
|
|
1855
|
+
.replace('YY', parts.year.slice(-2))
|
|
1856
|
+
.replace('MMMM', parts.month)
|
|
1857
|
+
.replace('MMM', parts.month.slice(0, 3))
|
|
1858
|
+
.replace('MM', pad(d.getMonth() + 1))
|
|
1859
|
+
.replace('DD', pad(d.getDate()))
|
|
1860
|
+
.replace('D', d.getDate())
|
|
1861
|
+
.replace('HH', parts.hour)
|
|
1862
|
+
.replace('mm', parts.minute)
|
|
1863
|
+
.replace('ss', parts.second);
|
|
1864
|
+
};
|
|
1865
|
+
|
|
1866
|
+
const Formatters = {
|
|
1867
|
+
|
|
1868
|
+
currency: (value, locale, { currency = 'XOF', notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
|
|
1869
|
+
new Intl.NumberFormat(locale, {
|
|
1870
|
+
style: 'currency',
|
|
1871
|
+
currency,
|
|
1872
|
+
notation,
|
|
1873
|
+
minimumFractionDigits,
|
|
1874
|
+
maximumFractionDigits
|
|
1875
|
+
}).format(value),
|
|
1876
|
+
|
|
1877
|
+
number: (value, locale, { notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
|
|
1878
|
+
new Intl.NumberFormat(locale, {
|
|
1879
|
+
notation,
|
|
1880
|
+
minimumFractionDigits,
|
|
1881
|
+
maximumFractionDigits
|
|
1882
|
+
}).format(value),
|
|
1883
|
+
|
|
1884
|
+
percent: (value, locale, { decimals = 1 } = {}) =>
|
|
1885
|
+
new Intl.NumberFormat(locale, {
|
|
1886
|
+
style: 'percent',
|
|
1887
|
+
maximumFractionDigits: decimals
|
|
1888
|
+
}).format(value),
|
|
1889
|
+
|
|
1890
|
+
date: (value, locale, { format, dateStyle = 'long' } = {}) => {
|
|
1891
|
+
if (format) {
|
|
1892
|
+
const { d, parts } = $parseDateParts(value, locale);
|
|
1893
|
+
return $applyDatePattern(format, d, parts);
|
|
1894
|
+
}
|
|
1895
|
+
return new Intl.DateTimeFormat(locale, { dateStyle }).format(new Date(value));
|
|
1896
|
+
},
|
|
1322
1897
|
|
|
1323
|
-
|
|
1324
|
-
|
|
1898
|
+
time: (value, locale, { format, hour = '2-digit', minute = '2-digit', second } = {}) => {
|
|
1899
|
+
if (format) {
|
|
1900
|
+
const { d, parts } = $parseDateParts(value, locale);
|
|
1901
|
+
return $applyDatePattern(format, d, parts);
|
|
1902
|
+
}
|
|
1903
|
+
return new Intl.DateTimeFormat(locale, { hour, minute, second }).format(new Date(value));
|
|
1904
|
+
},
|
|
1325
1905
|
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
}
|
|
1334
|
-
return this;
|
|
1335
|
-
};
|
|
1906
|
+
datetime: (value, locale, { format, dateStyle = 'long', hour = '2-digit', minute = '2-digit', second } = {}) => {
|
|
1907
|
+
if (format) {
|
|
1908
|
+
const { d, parts } = $parseDateParts(value, locale);
|
|
1909
|
+
return $applyDatePattern(format, d, parts);
|
|
1910
|
+
}
|
|
1911
|
+
return new Intl.DateTimeFormat(locale, { dateStyle, hour, minute, second }).format(new Date(value));
|
|
1912
|
+
},
|
|
1336
1913
|
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1914
|
+
relative: (value, locale, { unit = 'day', numeric = 'auto' } = {}) => {
|
|
1915
|
+
const diff = Math.round((value - Date.now()) / (1000 * 60 * 60 * 24));
|
|
1916
|
+
return new Intl.RelativeTimeFormat(locale, { numeric }).format(diff, unit);
|
|
1917
|
+
},
|
|
1340
1918
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1919
|
+
plural: (value, locale, { singular, plural } = {}) => {
|
|
1920
|
+
const rule = new Intl.PluralRules(locale).select(value);
|
|
1921
|
+
return `${value} ${rule === 'one' ? singular : plural}`;
|
|
1922
|
+
},
|
|
1343
1923
|
};
|
|
1344
1924
|
|
|
1345
|
-
|
|
1346
|
-
|
|
1925
|
+
/**
|
|
1926
|
+
*
|
|
1927
|
+
* @param {*} value
|
|
1928
|
+
* @param {{ propagation: boolean, reset: boolean} | null} configs
|
|
1929
|
+
* @class ObservableItem
|
|
1930
|
+
*/
|
|
1931
|
+
function ObservableItem(value, configs = null) {
|
|
1932
|
+
value = Validator.isObservable(value) ? value.val() : value;
|
|
1347
1933
|
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
const originalRemove = el.remove.bind(el);
|
|
1934
|
+
this.$previousValue = null;
|
|
1935
|
+
this.$currentValue = value;
|
|
1351
1936
|
|
|
1352
|
-
|
|
1937
|
+
this.$firstListener = null;
|
|
1938
|
+
this.$listeners = null;
|
|
1939
|
+
this.$watchers = null;
|
|
1353
1940
|
|
|
1354
|
-
|
|
1355
|
-
if($isUnmounting) {
|
|
1356
|
-
return;
|
|
1357
|
-
}
|
|
1358
|
-
$isUnmounting = true;
|
|
1941
|
+
this.$memoryId = null;
|
|
1359
1942
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
} finally {
|
|
1366
|
-
originalRemove();
|
|
1367
|
-
$isUnmounting = false;
|
|
1368
|
-
}
|
|
1369
|
-
};
|
|
1943
|
+
if(configs) {
|
|
1944
|
+
this.configs = configs;
|
|
1945
|
+
if(configs.reset) {
|
|
1946
|
+
this.$initialValue = Validator.isObject(value) ? deepClone(value) : value;
|
|
1947
|
+
}
|
|
1370
1948
|
}
|
|
1949
|
+
}
|
|
1371
1950
|
|
|
1372
|
-
|
|
1951
|
+
Object.defineProperty(ObservableItem.prototype, '$value', {
|
|
1952
|
+
get() {
|
|
1953
|
+
return this.$currentValue;
|
|
1954
|
+
},
|
|
1955
|
+
set(value) {
|
|
1956
|
+
this.set(value);
|
|
1957
|
+
},
|
|
1958
|
+
configurable: true,
|
|
1959
|
+
});
|
|
1960
|
+
|
|
1961
|
+
ObservableItem.prototype.__$isObservable = true;
|
|
1962
|
+
const noneTrigger = function() {};
|
|
1963
|
+
|
|
1964
|
+
/**
|
|
1965
|
+
* Intercepts and transforms values before they are set on the observable.
|
|
1966
|
+
* The interceptor can modify the value or return undefined to use the original value.
|
|
1967
|
+
*
|
|
1968
|
+
* @param {(value) => any} callback - Interceptor function that receives (newValue, currentValue) and returns the transformed value or undefined
|
|
1969
|
+
* @returns {ObservableItem} The observable instance for chaining
|
|
1970
|
+
* @example
|
|
1971
|
+
* const count = Observable(0);
|
|
1972
|
+
* count.intercept((newVal, oldVal) => Math.max(0, newVal)); // Prevent negative values
|
|
1973
|
+
*/
|
|
1974
|
+
ObservableItem.prototype.intercept = function(callback) {
|
|
1975
|
+
this.$interceptor = callback;
|
|
1976
|
+
this.set = this.$setWithInterceptor;
|
|
1373
1977
|
return this;
|
|
1374
1978
|
};
|
|
1375
1979
|
|
|
1376
|
-
|
|
1377
|
-
|
|
1980
|
+
ObservableItem.prototype.triggerFirstListener = function(operations) {
|
|
1981
|
+
this.$firstListener(this.$currentValue, this.$previousValue, operations);
|
|
1378
1982
|
};
|
|
1379
1983
|
|
|
1380
|
-
|
|
1984
|
+
ObservableItem.prototype.triggerListeners = function(operations) {
|
|
1985
|
+
const $listeners = this.$listeners;
|
|
1986
|
+
const $previousValue = this.$previousValue;
|
|
1987
|
+
const $currentValue = this.$currentValue;
|
|
1381
1988
|
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
const children = Array.from($element.childNodes);
|
|
1385
|
-
const shadowRoot = $element.attachShadow({ mode });
|
|
1386
|
-
if(style) {
|
|
1387
|
-
const styleNode = document.createElement("style");
|
|
1388
|
-
styleNode.textContent = style;
|
|
1389
|
-
shadowRoot.appendChild(styleNode);
|
|
1989
|
+
for(let i = 0, length = $listeners.length; i < length; i++) {
|
|
1990
|
+
$listeners[i]($currentValue, $previousValue, operations);
|
|
1390
1991
|
}
|
|
1391
|
-
$element.append = shadowRoot.append.bind(shadowRoot);
|
|
1392
|
-
$element.appendChild = shadowRoot.appendChild.bind(shadowRoot);
|
|
1393
|
-
shadowRoot.append(...children);
|
|
1394
|
-
|
|
1395
|
-
return this;
|
|
1396
1992
|
};
|
|
1397
1993
|
|
|
1398
|
-
|
|
1399
|
-
|
|
1994
|
+
ObservableItem.prototype.triggerWatchers = function(operations) {
|
|
1995
|
+
const $watchers = this.$watchers;
|
|
1996
|
+
const $previousValue = this.$previousValue;
|
|
1997
|
+
const $currentValue = this.$currentValue;
|
|
1998
|
+
|
|
1999
|
+
const $currentValueCallbacks = $watchers.get($currentValue);
|
|
2000
|
+
const $previousValueCallbacks = $watchers.get($previousValue);
|
|
2001
|
+
if($currentValueCallbacks) {
|
|
2002
|
+
$currentValueCallbacks(true, $previousValue, operations);
|
|
2003
|
+
}
|
|
2004
|
+
if($previousValueCallbacks) {
|
|
2005
|
+
$previousValueCallbacks(false, $currentValue, operations);
|
|
2006
|
+
}
|
|
1400
2007
|
};
|
|
1401
2008
|
|
|
1402
|
-
|
|
1403
|
-
|
|
2009
|
+
ObservableItem.prototype.triggerAll = function(operations) {
|
|
2010
|
+
this.triggerWatchers(operations);
|
|
2011
|
+
this.triggerListeners(operations);
|
|
1404
2012
|
};
|
|
1405
2013
|
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
* @param {string} methodName - Name of the hydration method to call
|
|
1410
|
-
* @param {BindingHydrator} bindingHydrator - Template binding with $hydrate method
|
|
1411
|
-
* @returns {HTMLElement} The underlying HTML element
|
|
1412
|
-
* @example
|
|
1413
|
-
* const onClick = $binder.attach((event, data) => console.log(data));
|
|
1414
|
-
* element.nd.attach('onClick', onClick);
|
|
1415
|
-
*/
|
|
1416
|
-
NDElement.prototype.attach = function(methodName, bindingHydrator) {
|
|
1417
|
-
bindingHydrator.$hydrate(this.$element, methodName);
|
|
1418
|
-
return this.$element;
|
|
2014
|
+
ObservableItem.prototype.triggerWatchersAndFirstListener = function(operations) {
|
|
2015
|
+
this.triggerWatchers(operations);
|
|
2016
|
+
this.triggerFirstListener(operations);
|
|
1419
2017
|
};
|
|
1420
2018
|
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
* @returns {this} The NDElement instance with added methods for chaining
|
|
1427
|
-
* @example
|
|
1428
|
-
* element.nd.with({
|
|
1429
|
-
* highlight() {
|
|
1430
|
-
* this.$element.style.background = 'yellow';
|
|
1431
|
-
* return this;
|
|
1432
|
-
* }
|
|
1433
|
-
* }).highlight().onClick(() => console.log('Clicked'));
|
|
1434
|
-
*/
|
|
1435
|
-
NDElement.prototype.with = function(methods) {
|
|
1436
|
-
if (!methods || typeof methods !== 'object') {
|
|
1437
|
-
throw new NativeDocumentError('extend() requires an object of methods');
|
|
2019
|
+
ObservableItem.prototype.assocTrigger = function() {
|
|
2020
|
+
this.$firstListener = null;
|
|
2021
|
+
if(this.$watchers?.size && this.$listeners?.length) {
|
|
2022
|
+
this.trigger = (this.$listeners.length === 1) ? this.triggerWatchersAndFirstListener : this.triggerAll;
|
|
2023
|
+
return;
|
|
1438
2024
|
}
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
if (typeof method !== 'function') {
|
|
1444
|
-
console.warn(`⚠️ extends(): "${name}" is not a function, skipping`);
|
|
1445
|
-
continue;
|
|
2025
|
+
if(this.$listeners?.length) {
|
|
2026
|
+
if(this.$listeners.length === 1) {
|
|
2027
|
+
this.$firstListener = this.$listeners[0];
|
|
2028
|
+
this.trigger = this.triggerFirstListener;
|
|
1446
2029
|
}
|
|
1447
|
-
|
|
1448
|
-
|
|
2030
|
+
else {
|
|
2031
|
+
this.trigger = this.triggerListeners;
|
|
2032
|
+
}
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
if(this.$watchers?.size) {
|
|
2036
|
+
this.trigger = this.triggerWatchers;
|
|
2037
|
+
return;
|
|
1449
2038
|
}
|
|
2039
|
+
this.trigger = noneTrigger;
|
|
2040
|
+
};
|
|
2041
|
+
ObservableItem.prototype.trigger = noneTrigger;
|
|
1450
2042
|
|
|
1451
|
-
|
|
2043
|
+
ObservableItem.prototype.$updateWithNewValue = function(newValue) {
|
|
2044
|
+
newValue = newValue?.__$isObservable ? newValue.val() : newValue;
|
|
2045
|
+
if(this.$currentValue === newValue) {
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
this.$previousValue = this.$currentValue;
|
|
2049
|
+
this.$currentValue = newValue;
|
|
2050
|
+
this.trigger();
|
|
2051
|
+
this.$previousValue = null;
|
|
1452
2052
|
};
|
|
1453
2053
|
|
|
1454
2054
|
/**
|
|
1455
|
-
*
|
|
1456
|
-
* Use this to add global methods to all NDElements.
|
|
1457
|
-
*
|
|
1458
|
-
* @param {Object} methods - Object containing method definitions to add to prototype
|
|
1459
|
-
* @returns {typeof NDElement} The NDElement constructor
|
|
1460
|
-
* @throws {NativeDocumentError} If methods is not an object or contains non-function values
|
|
1461
|
-
* @example
|
|
1462
|
-
* NDElement.extend({
|
|
1463
|
-
* fadeIn() {
|
|
1464
|
-
* this.$element.style.opacity = '1';
|
|
1465
|
-
* return this;
|
|
1466
|
-
* }
|
|
1467
|
-
* });
|
|
1468
|
-
* // Now all NDElements have .fadeIn() method
|
|
1469
|
-
* Div().nd.fadeIn();
|
|
2055
|
+
* @param {*} data
|
|
1470
2056
|
*/
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
}
|
|
2057
|
+
ObservableItem.prototype.$setWithInterceptor = function(data) {
|
|
2058
|
+
let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
|
|
2059
|
+
const result = this.$interceptor(newValue, this.$currentValue);
|
|
1475
2060
|
|
|
1476
|
-
if (
|
|
1477
|
-
|
|
2061
|
+
if (result !== undefined) {
|
|
2062
|
+
newValue = result;
|
|
1478
2063
|
}
|
|
1479
2064
|
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
'ref', 'remove', 'cleanup', 'with', 'extend', 'attach',
|
|
1483
|
-
'lifecycle', 'mounted', 'unmounted', 'unmountChildren'
|
|
1484
|
-
]);
|
|
1485
|
-
|
|
1486
|
-
for (const name in methods) {
|
|
1487
|
-
if (!Object.hasOwn(methods, name)) {
|
|
1488
|
-
continue;
|
|
1489
|
-
}
|
|
2065
|
+
this.$updateWithNewValue(newValue);
|
|
2066
|
+
};
|
|
1490
2067
|
|
|
1491
|
-
|
|
2068
|
+
/**
|
|
2069
|
+
* @param {*} data
|
|
2070
|
+
*/
|
|
2071
|
+
ObservableItem.prototype.$basicSet = function(data) {
|
|
2072
|
+
let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
|
|
2073
|
+
this.$updateWithNewValue(newValue);
|
|
2074
|
+
};
|
|
1492
2075
|
|
|
1493
|
-
|
|
1494
|
-
DebugManager$1.warn('NDElement.extend', `"${name}" is not a function, skipping`);
|
|
1495
|
-
continue;
|
|
1496
|
-
}
|
|
2076
|
+
ObservableItem.prototype.set = ObservableItem.prototype.$basicSet;
|
|
1497
2077
|
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
}
|
|
2078
|
+
ObservableItem.prototype.val = function() {
|
|
2079
|
+
return this.$currentValue;
|
|
2080
|
+
};
|
|
1502
2081
|
|
|
1503
|
-
|
|
1504
|
-
|
|
2082
|
+
ObservableItem.prototype.disconnectAll = function() {
|
|
2083
|
+
this.$listeners?.splice(0);
|
|
2084
|
+
this.$previousValue = null;
|
|
2085
|
+
this.$currentValue = null;
|
|
2086
|
+
if(this.$watchers) {
|
|
2087
|
+
for (const [_, watchValueList] of this.$watchers) {
|
|
2088
|
+
if(Validator.isArray(watchValueList)) {
|
|
2089
|
+
watchValueList.splice(0);
|
|
2090
|
+
}
|
|
1505
2091
|
}
|
|
1506
|
-
|
|
1507
|
-
NDElement.prototype[name] = method;
|
|
1508
2092
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
2093
|
+
this.$watchers?.clear();
|
|
2094
|
+
this.$listeners = null;
|
|
2095
|
+
this.$watchers = null;
|
|
2096
|
+
this.trigger = noneTrigger;
|
|
1511
2097
|
};
|
|
1512
2098
|
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
2099
|
+
/**
|
|
2100
|
+
* Registers a cleanup callback that will be executed when the observable is cleaned up.
|
|
2101
|
+
* Useful for disposing resources, removing event listeners, or other cleanup tasks.
|
|
2102
|
+
*
|
|
2103
|
+
* @param {Function} callback - Cleanup function to execute on observable disposal
|
|
2104
|
+
* @example
|
|
2105
|
+
* const obs = Observable(0);
|
|
2106
|
+
* obs.onCleanup(() => console.log('Cleaned up!'));
|
|
2107
|
+
* obs.cleanup(); // Logs: "Cleaned up!"
|
|
2108
|
+
*/
|
|
2109
|
+
ObservableItem.prototype.onCleanup = function(callback) {
|
|
2110
|
+
this.$cleanupListeners = this.$cleanupListeners ?? [];
|
|
2111
|
+
this.$cleanupListeners.push(callback);
|
|
1518
2112
|
};
|
|
1519
2113
|
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
isTemplateBinding(value) {
|
|
1525
|
-
return value?.__$isTemplateBinding;
|
|
1526
|
-
},
|
|
1527
|
-
isObservableWhenResult(value) {
|
|
1528
|
-
return value && (value.__$isObservableWhen || (typeof value === 'object' && '$target' in value && '$observer' in value));
|
|
1529
|
-
},
|
|
1530
|
-
isArrayObservable(value) {
|
|
1531
|
-
return value?.__$isObservableArray;
|
|
1532
|
-
},
|
|
1533
|
-
isProxy(value) {
|
|
1534
|
-
return value?.__isProxy__
|
|
1535
|
-
},
|
|
1536
|
-
isObservableOrProxy(value) {
|
|
1537
|
-
return Validator.isObservable(value) || Validator.isProxy(value);
|
|
1538
|
-
},
|
|
1539
|
-
isAnchor(value) {
|
|
1540
|
-
return value?.__Anchor__
|
|
1541
|
-
},
|
|
1542
|
-
isObservableChecker(value) {
|
|
1543
|
-
return value?.__$isObservableChecker || value instanceof ObservableChecker;
|
|
1544
|
-
},
|
|
1545
|
-
isArray(value) {
|
|
1546
|
-
return Array.isArray(value);
|
|
1547
|
-
},
|
|
1548
|
-
isString(value) {
|
|
1549
|
-
return typeof value === 'string';
|
|
1550
|
-
},
|
|
1551
|
-
isNumber(value) {
|
|
1552
|
-
return typeof value === 'number';
|
|
1553
|
-
},
|
|
1554
|
-
isBoolean(value) {
|
|
1555
|
-
return typeof value === 'boolean';
|
|
1556
|
-
},
|
|
1557
|
-
isFunction(value) {
|
|
1558
|
-
return typeof value === 'function';
|
|
1559
|
-
},
|
|
1560
|
-
isAsyncFunction(value) {
|
|
1561
|
-
return typeof value === 'function' && value.constructor.name === 'AsyncFunction';
|
|
1562
|
-
},
|
|
1563
|
-
isObject(value) {
|
|
1564
|
-
return typeof value === 'object' && value !== null;
|
|
1565
|
-
},
|
|
1566
|
-
isJson(value) {
|
|
1567
|
-
return !(typeof value !== 'object' || value === null || Array.isArray(value) || value.constructor.name !== 'Object')
|
|
1568
|
-
},
|
|
1569
|
-
isElement(value) {
|
|
1570
|
-
return value && (
|
|
1571
|
-
value.nodeType === COMMON_NODE_TYPES.ELEMENT ||
|
|
1572
|
-
value.nodeType === COMMON_NODE_TYPES.TEXT ||
|
|
1573
|
-
value.nodeType === COMMON_NODE_TYPES.DOCUMENT_FRAGMENT ||
|
|
1574
|
-
value.nodeType === COMMON_NODE_TYPES.COMMENT
|
|
1575
|
-
);
|
|
1576
|
-
},
|
|
1577
|
-
isFragment(value) {
|
|
1578
|
-
return value?.nodeType === COMMON_NODE_TYPES.DOCUMENT_FRAGMENT;
|
|
1579
|
-
},
|
|
1580
|
-
isStringOrObservable(value) {
|
|
1581
|
-
return this.isString(value) || this.isObservable(value);
|
|
1582
|
-
},
|
|
1583
|
-
isValidChild(child) {
|
|
1584
|
-
return child === null ||
|
|
1585
|
-
this.isElement(child) ||
|
|
1586
|
-
this.isObservable(child) ||
|
|
1587
|
-
this.isNDElement(child) ||
|
|
1588
|
-
['string', 'number', 'boolean'].includes(typeof child);
|
|
1589
|
-
},
|
|
1590
|
-
isNDElement(child) {
|
|
1591
|
-
return child?.__$isNDElement || child instanceof NDElement;
|
|
1592
|
-
},
|
|
1593
|
-
isValidChildren(children) {
|
|
1594
|
-
if (!Array.isArray(children)) {
|
|
1595
|
-
children = [children];
|
|
2114
|
+
ObservableItem.prototype.cleanup = function() {
|
|
2115
|
+
if (this.$cleanupListeners) {
|
|
2116
|
+
for (let i = 0; i < this.$cleanupListeners.length; i++) {
|
|
2117
|
+
this.$cleanupListeners[i]();
|
|
1596
2118
|
}
|
|
2119
|
+
this.$cleanupListeners = null;
|
|
2120
|
+
}
|
|
2121
|
+
MemoryManager.unregister(this.$memoryId);
|
|
2122
|
+
this.disconnectAll();
|
|
2123
|
+
delete this.$value;
|
|
2124
|
+
};
|
|
1597
2125
|
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
2126
|
+
/**
|
|
2127
|
+
*
|
|
2128
|
+
* @param {Function} callback
|
|
2129
|
+
* @returns {(function(): void)}
|
|
2130
|
+
*/
|
|
2131
|
+
ObservableItem.prototype.subscribe = function(callback) {
|
|
2132
|
+
this.$listeners = this.$listeners ?? [];
|
|
1605
2133
|
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
}
|
|
2134
|
+
this.$listeners.push(callback);
|
|
2135
|
+
this.assocTrigger();
|
|
2136
|
+
};
|
|
1610
2137
|
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
},
|
|
1625
|
-
/**
|
|
1626
|
-
* Check if the data contains an observable reference.
|
|
1627
|
-
* @param {string} data
|
|
1628
|
-
* @returns {boolean}
|
|
1629
|
-
*/
|
|
1630
|
-
containsObservableReference(data) {
|
|
1631
|
-
if(!data || typeof data !== 'string') {
|
|
1632
|
-
return false;
|
|
1633
|
-
}
|
|
1634
|
-
return /\{\{#ObItem::\([0-9]+\)\}\}/.test(data);
|
|
1635
|
-
},
|
|
1636
|
-
validateAttributes(attributes) {},
|
|
2138
|
+
/**
|
|
2139
|
+
* Watches for a specific value and executes callback when the observable equals that value.
|
|
2140
|
+
* Creates a watcher that only triggers when the observable changes to the specified value.
|
|
2141
|
+
*
|
|
2142
|
+
* @param {*} value - The value to watch for
|
|
2143
|
+
* @param {(value) => void|ObservableItem} callback - Callback function or observable to set when value matches
|
|
2144
|
+
* @example
|
|
2145
|
+
* const status = Observable('idle');
|
|
2146
|
+
* status.on('loading', () => console.log('Started loading'));
|
|
2147
|
+
* status.on('error', isError); // Set another observable
|
|
2148
|
+
*/
|
|
2149
|
+
ObservableItem.prototype.on = function(value, callback) {
|
|
2150
|
+
this.$watchers = this.$watchers ?? new Map();
|
|
1637
2151
|
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
2152
|
+
let watchValueList = this.$watchers.get(value);
|
|
2153
|
+
|
|
2154
|
+
if(callback.__$isObservable) {
|
|
2155
|
+
callback = callback.set.bind(callback);
|
|
1642
2156
|
}
|
|
1643
|
-
};
|
|
1644
2157
|
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
2158
|
+
if(!watchValueList) {
|
|
2159
|
+
watchValueList = callback;
|
|
2160
|
+
this.$watchers.set(value, callback);
|
|
2161
|
+
} else if(!Validator.isArray(watchValueList.list)) {
|
|
2162
|
+
watchValueList = [watchValueList, callback];
|
|
2163
|
+
callback = (value) => {
|
|
2164
|
+
for(let i = 0, length = watchValueList.length; i < length; i++) {
|
|
2165
|
+
watchValueList[i](value);
|
|
2166
|
+
}
|
|
2167
|
+
};
|
|
2168
|
+
callback.list = watchValueList;
|
|
2169
|
+
this.$watchers.set(value, callback);
|
|
2170
|
+
} else {
|
|
2171
|
+
watchValueList.list.push(callback);
|
|
2172
|
+
}
|
|
1648
2173
|
|
|
1649
|
-
|
|
1650
|
-
|
|
2174
|
+
this.assocTrigger();
|
|
2175
|
+
};
|
|
1651
2176
|
|
|
1652
|
-
|
|
1653
|
-
|
|
2177
|
+
/**
|
|
2178
|
+
* Removes a watcher for a specific value. If no callback is provided, removes all watchers for that value.
|
|
2179
|
+
*
|
|
2180
|
+
* @param {*} value - The value to stop watching
|
|
2181
|
+
* @param {Function} [callback] - Specific callback to remove. If omitted, removes all watchers for this value
|
|
2182
|
+
* @example
|
|
2183
|
+
* const status = Observable('idle');
|
|
2184
|
+
* const handler = () => console.log('Loading');
|
|
2185
|
+
* status.on('loading', handler);
|
|
2186
|
+
* status.off('loading', handler); // Remove specific handler
|
|
2187
|
+
* status.off('loading'); // Remove all handlers for 'loading'
|
|
2188
|
+
*/
|
|
2189
|
+
ObservableItem.prototype.off = function(value, callback) {
|
|
2190
|
+
if(!this.$watchers) return;
|
|
1654
2191
|
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
anchorFragment.nativeAppend = anchorFragment.append;
|
|
2192
|
+
const watchValueList = this.$watchers.get(value);
|
|
2193
|
+
if(!watchValueList) return;
|
|
1658
2194
|
|
|
1659
|
-
|
|
2195
|
+
if(!callback || !Array.isArray(watchValueList.list)) {
|
|
2196
|
+
this.$watchers?.delete(value);
|
|
2197
|
+
this.assocTrigger();
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
const index = watchValueList.indexOf(callback);
|
|
2201
|
+
watchValueList?.splice(index, 1);
|
|
2202
|
+
if(watchValueList.length === 1) {
|
|
2203
|
+
this.$watchers.set(value, watchValueList[0]);
|
|
2204
|
+
}
|
|
2205
|
+
else if(watchValueList.length === 0) {
|
|
2206
|
+
this.$watchers?.delete(value);
|
|
2207
|
+
}
|
|
2208
|
+
this.assocTrigger();
|
|
2209
|
+
};
|
|
1660
2210
|
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
2211
|
+
/**
|
|
2212
|
+
* Subscribes to the observable but automatically unsubscribes after the first time the predicate matches.
|
|
2213
|
+
*
|
|
2214
|
+
* @param {(value) => Boolean|any} predicate - Value to match or function that returns true when condition is met
|
|
2215
|
+
* @param {(value) => void} callback - Callback to execute when predicate matches, receives the matched value
|
|
2216
|
+
* @example
|
|
2217
|
+
* const status = Observable('loading');
|
|
2218
|
+
* status.once('ready', (val) => console.log('Ready!'));
|
|
2219
|
+
* status.once(val => val === 'error', (val) => console.log('Error occurred'));
|
|
2220
|
+
*/
|
|
2221
|
+
ObservableItem.prototype.once = function(predicate, callback) {
|
|
2222
|
+
const fn = typeof predicate === 'function' ? predicate : (v) => v === predicate;
|
|
1673
2223
|
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
parentNode.nativeInsertBefore(child, targetBefore);
|
|
1679
|
-
return;
|
|
2224
|
+
const handler = (val) => {
|
|
2225
|
+
if (fn(val)) {
|
|
2226
|
+
this.unsubscribe(handler);
|
|
2227
|
+
callback(val);
|
|
1680
2228
|
}
|
|
1681
|
-
parentNode?.insertBefore(child, targetBefore);
|
|
1682
2229
|
};
|
|
2230
|
+
this.subscribe(handler);
|
|
2231
|
+
};
|
|
1683
2232
|
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
2233
|
+
/**
|
|
2234
|
+
* Unsubscribe from an observable.
|
|
2235
|
+
* @param {Function} callback
|
|
2236
|
+
*/
|
|
2237
|
+
ObservableItem.prototype.unsubscribe = function(callback) {
|
|
2238
|
+
if(!this.$listeners) return;
|
|
2239
|
+
const index = this.$listeners.indexOf(callback);
|
|
2240
|
+
if (index > -1) {
|
|
2241
|
+
this.$listeners.splice(index, 1);
|
|
2242
|
+
}
|
|
2243
|
+
this.assocTrigger();
|
|
2244
|
+
};
|
|
1693
2245
|
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
2246
|
+
/**
|
|
2247
|
+
* Create an Observable checker instance
|
|
2248
|
+
* @param callback
|
|
2249
|
+
* @returns {ObservableChecker}
|
|
2250
|
+
*/
|
|
2251
|
+
ObservableItem.prototype.check = function(callback) {
|
|
2252
|
+
return new ObservableChecker(this, callback)
|
|
2253
|
+
};
|
|
1697
2254
|
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
}
|
|
1703
|
-
// if(isParentUniqueChild(parent)) {
|
|
1704
|
-
// parent.replaceChildren(anchorStart, anchorEnd);
|
|
1705
|
-
// return;
|
|
1706
|
-
// }
|
|
2255
|
+
ObservableItem.prototype.transform = ObservableItem.prototype.check;
|
|
2256
|
+
ObservableItem.prototype.pluck = ObservableItem.prototype.check;
|
|
2257
|
+
ObservableItem.prototype.is = ObservableItem.prototype.check;
|
|
2258
|
+
ObservableItem.prototype.select = ObservableItem.prototype.check;
|
|
1707
2259
|
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
2260
|
+
/**
|
|
2261
|
+
* Gets a property value from the observable's current value.
|
|
2262
|
+
* If the property is an observable, returns its value.
|
|
2263
|
+
*
|
|
2264
|
+
* @param {string|number} key - Property key to retrieve
|
|
2265
|
+
* @returns {*} The value of the property, unwrapped if it's an observable
|
|
2266
|
+
* @example
|
|
2267
|
+
* const user = Observable({ name: 'John', age: Observable(25) });
|
|
2268
|
+
* user.get('name'); // 'John'
|
|
2269
|
+
* user.get('age'); // 25 (unwrapped from observable)
|
|
2270
|
+
*/
|
|
2271
|
+
ObservableItem.prototype.get = function(key) {
|
|
2272
|
+
const item = this.$currentValue[key];
|
|
2273
|
+
return Validator.isObservable(item) ? item.val() : item;
|
|
2274
|
+
};
|
|
1717
2275
|
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
anchorFragment.nativeAppend(...allItemToRemove);
|
|
1734
|
-
};
|
|
2276
|
+
/**
|
|
2277
|
+
* Creates an ObservableWhen that represents whether the observable equals a specific value.
|
|
2278
|
+
* Returns an object that can be subscribed to and will emit true/false.
|
|
2279
|
+
*
|
|
2280
|
+
* @param {*} value - The value to compare against
|
|
2281
|
+
* @returns {ObservableWhen} An ObservableWhen instance that tracks when the observable equals the value
|
|
2282
|
+
* @example
|
|
2283
|
+
* const status = Observable('idle');
|
|
2284
|
+
* const isLoading = status.when('loading');
|
|
2285
|
+
* isLoading.subscribe(active => console.log('Loading:', active));
|
|
2286
|
+
* status.set('loading'); // Logs: "Loading: true"
|
|
2287
|
+
*/
|
|
2288
|
+
ObservableItem.prototype.when = function(value) {
|
|
2289
|
+
return new ObservableWhen(this, value);
|
|
2290
|
+
};
|
|
1735
2291
|
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
2292
|
+
/**
|
|
2293
|
+
* Compares the observable's current value with another value or observable.
|
|
2294
|
+
*
|
|
2295
|
+
* @param {*|ObservableItem} other - Value or observable to compare against
|
|
2296
|
+
* @returns {boolean} True if values are equal
|
|
2297
|
+
* @example
|
|
2298
|
+
* const a = Observable(5);
|
|
2299
|
+
* const b = Observable(5);
|
|
2300
|
+
* a.equals(5); // true
|
|
2301
|
+
* a.equals(b); // true
|
|
2302
|
+
* a.equals(10); // false
|
|
2303
|
+
*/
|
|
2304
|
+
ObservableItem.prototype.equals = function(other) {
|
|
2305
|
+
if(Validator.isObservable(other)) {
|
|
2306
|
+
return this.$currentValue === other.$currentValue;
|
|
2307
|
+
}
|
|
2308
|
+
return this.$currentValue === other;
|
|
2309
|
+
};
|
|
1741
2310
|
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
2311
|
+
/**
|
|
2312
|
+
* Converts the observable's current value to a boolean.
|
|
2313
|
+
*
|
|
2314
|
+
* @returns {boolean} The boolean representation of the current value
|
|
2315
|
+
* @example
|
|
2316
|
+
* const count = Observable(0);
|
|
2317
|
+
* count.toBool(); // false
|
|
2318
|
+
* count.set(5);
|
|
2319
|
+
* count.toBool(); // true
|
|
2320
|
+
*/
|
|
2321
|
+
ObservableItem.prototype.toBool = function() {
|
|
2322
|
+
return !!this.$currentValue;
|
|
2323
|
+
};
|
|
2324
|
+
|
|
2325
|
+
/**
|
|
2326
|
+
* Toggles the boolean value of the observable (false becomes true, true becomes false).
|
|
2327
|
+
*
|
|
2328
|
+
* @example
|
|
2329
|
+
* const isOpen = Observable(false);
|
|
2330
|
+
* isOpen.toggle(); // Now true
|
|
2331
|
+
* isOpen.toggle(); // Now false
|
|
2332
|
+
*/
|
|
2333
|
+
ObservableItem.prototype.toggle = function() {
|
|
2334
|
+
this.set(!this.$currentValue);
|
|
2335
|
+
};
|
|
1755
2336
|
|
|
1756
|
-
|
|
2337
|
+
/**
|
|
2338
|
+
* Resets the observable to its initial value.
|
|
2339
|
+
* Only works if the observable was created with { reset: true } config.
|
|
2340
|
+
*
|
|
2341
|
+
* @example
|
|
2342
|
+
* const count = Observable(0, { reset: true });
|
|
2343
|
+
* count.set(10);
|
|
2344
|
+
* count.reset(); // Back to 0
|
|
2345
|
+
*/
|
|
2346
|
+
ObservableItem.prototype.reset = function() {
|
|
2347
|
+
if(!this.configs?.reset) {
|
|
2348
|
+
return;
|
|
2349
|
+
}
|
|
2350
|
+
const resetValue = (Validator.isObject(this.$initialValue))
|
|
2351
|
+
? deepClone(this.$initialValue, (observable) => {
|
|
2352
|
+
observable.reset();
|
|
2353
|
+
})
|
|
2354
|
+
: this.$initialValue;
|
|
2355
|
+
this.set(resetValue);
|
|
2356
|
+
};
|
|
1757
2357
|
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
2358
|
+
/**
|
|
2359
|
+
* Returns a string representation of the observable's current value.
|
|
2360
|
+
*
|
|
2361
|
+
* @returns {string} String representation of the current value
|
|
2362
|
+
*/
|
|
2363
|
+
ObservableItem.prototype.toString = function() {
|
|
2364
|
+
return String(this.$currentValue);
|
|
2365
|
+
};
|
|
1761
2366
|
|
|
2367
|
+
/**
|
|
2368
|
+
* Returns the primitive value of the observable (its current value).
|
|
2369
|
+
* Called automatically in type coercion contexts.
|
|
2370
|
+
*
|
|
2371
|
+
* @returns {*} The current value of the observable
|
|
2372
|
+
*/
|
|
2373
|
+
ObservableItem.prototype.valueOf = function() {
|
|
2374
|
+
return this.$currentValue;
|
|
2375
|
+
};
|
|
1762
2376
|
|
|
1763
|
-
anchorFragment.endElement = function() {
|
|
1764
|
-
return anchorEnd;
|
|
1765
|
-
};
|
|
1766
2377
|
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
2378
|
+
/**
|
|
2379
|
+
* Creates a derived observable that formats the current value using Intl.
|
|
2380
|
+
* Automatically reacts to both value changes and locale changes (Store.__nd.locale).
|
|
2381
|
+
*
|
|
2382
|
+
* @param {string | Function} type - Format type or custom formatter function
|
|
2383
|
+
* @param {Object} [options={}] - Options passed to the formatter
|
|
2384
|
+
* @returns {ObservableItem<string>}
|
|
2385
|
+
*
|
|
2386
|
+
* @example
|
|
2387
|
+
* // Currency
|
|
2388
|
+
* price.format('currency') // "15 000 FCFA"
|
|
2389
|
+
* price.format('currency', { currency: 'EUR' }) // "15 000,00 €"
|
|
2390
|
+
* price.format('currency', { notation: 'compact' }) // "15 K FCFA"
|
|
2391
|
+
*
|
|
2392
|
+
* // Number
|
|
2393
|
+
* count.format('number') // "15 000"
|
|
2394
|
+
*
|
|
2395
|
+
* // Percent
|
|
2396
|
+
* rate.format('percent') // "15,0 %"
|
|
2397
|
+
* rate.format('percent', { decimals: 2 }) // "15,00 %"
|
|
2398
|
+
*
|
|
2399
|
+
* // Date
|
|
2400
|
+
* date.format('date') // "3 mars 2026"
|
|
2401
|
+
* date.format('date', { dateStyle: 'full' }) // "mardi 3 mars 2026"
|
|
2402
|
+
* date.format('date', { format: 'DD/MM/YYYY' }) // "03/03/2026"
|
|
2403
|
+
* date.format('date', { format: 'DD MMM YYYY' }) // "03 mar 2026"
|
|
2404
|
+
* date.format('date', { format: 'DD MMMM YYYY' }) // "03 mars 2026"
|
|
2405
|
+
*
|
|
2406
|
+
* // Time
|
|
2407
|
+
* date.format('time') // "20:30"
|
|
2408
|
+
* date.format('time', { second: '2-digit' }) // "20:30:00"
|
|
2409
|
+
* date.format('time', { format: 'HH:mm:ss' }) // "20:30:00"
|
|
2410
|
+
*
|
|
2411
|
+
* // Datetime
|
|
2412
|
+
* date.format('datetime') // "3 mars 2026, 20:30"
|
|
2413
|
+
* date.format('datetime', { dateStyle: 'full' }) // "mardi 3 mars 2026, 20:30"
|
|
2414
|
+
* date.format('datetime', { format: 'DD/MM/YYYY HH:mm' }) // "03/03/2026 20:30"
|
|
2415
|
+
*
|
|
2416
|
+
* // Relative
|
|
2417
|
+
* date.format('relative') // "dans 11 jours"
|
|
2418
|
+
* date.format('relative', { unit: 'month' }) // "dans 1 mois"
|
|
2419
|
+
*
|
|
2420
|
+
* // Plural
|
|
2421
|
+
* count.format('plural', { singular: 'billet', plural: 'billets' }) // "3 billets"
|
|
2422
|
+
*
|
|
2423
|
+
* // Custom formatter
|
|
2424
|
+
* price.format(value => `${value.toLocaleString()} FCFA`)
|
|
2425
|
+
*
|
|
2426
|
+
* // Reacts to locale changes automatically
|
|
2427
|
+
* Store.setLocale('en-US');
|
|
2428
|
+
*/
|
|
2429
|
+
ObservableItem.prototype.format = function(type, options = {}) {
|
|
2430
|
+
const self = this;
|
|
1775
2431
|
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
if(!currentNode.nextSibling) {
|
|
1780
|
-
return null;
|
|
1781
|
-
}
|
|
1782
|
-
currentNode = currentNode.nextSibling;
|
|
1783
|
-
}
|
|
1784
|
-
return currentNode !== anchorStart ? currentNode : null;
|
|
1785
|
-
};
|
|
2432
|
+
if (typeof type === 'function') {
|
|
2433
|
+
return new ObservableChecker(self, type);
|
|
2434
|
+
}
|
|
1786
2435
|
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
DocumentFragment.prototype.setAttribute = () => {};
|
|
2436
|
+
const formatter = Formatters[type];
|
|
2437
|
+
const localeObservable = Store.follow('locale');
|
|
1790
2438
|
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
'controls',
|
|
1809
|
-
'loop',
|
|
1810
|
-
'muted',
|
|
1811
|
-
'download',
|
|
1812
|
-
'reversed',
|
|
1813
|
-
'open',
|
|
1814
|
-
'default',
|
|
1815
|
-
'formnovalidate',
|
|
1816
|
-
'novalidate',
|
|
1817
|
-
'scoped',
|
|
1818
|
-
'itemscope',
|
|
1819
|
-
'allowfullscreen',
|
|
1820
|
-
'allowpaymentrequest',
|
|
1821
|
-
'playsinline'
|
|
1822
|
-
]);
|
|
2439
|
+
return Observable.computed(() => formatter(self.val(), localeObservable.val(), options),
|
|
2440
|
+
[self, localeObservable]
|
|
2441
|
+
);
|
|
2442
|
+
};
|
|
2443
|
+
|
|
2444
|
+
ObservableItem.prototype.persist = function(key, options = {}) {
|
|
2445
|
+
let value = $getFromStorage(key, this.$currentValue);
|
|
2446
|
+
if(options.get) {
|
|
2447
|
+
value = options.get(value);
|
|
2448
|
+
}
|
|
2449
|
+
this.set(value);
|
|
2450
|
+
const saver = $saveToStorage(this.$currentValue);
|
|
2451
|
+
this.subscribe((newValue) => {
|
|
2452
|
+
saver(key, options.set ? options.set(newValue) : newValue);
|
|
2453
|
+
});
|
|
2454
|
+
return this;
|
|
2455
|
+
};
|
|
1823
2456
|
|
|
1824
2457
|
/**
|
|
1825
2458
|
*
|
|
@@ -2999,6 +3632,68 @@ var NativeComponents = (function (exports) {
|
|
|
2999
3632
|
});
|
|
3000
3633
|
};
|
|
3001
3634
|
|
|
3635
|
+
ObservableArray.prototype.deepSubscribe = function(callback) {
|
|
3636
|
+
const updatedValue = nextTick(() => callback(this.val()));
|
|
3637
|
+
const $listeners = new WeakMap();
|
|
3638
|
+
|
|
3639
|
+
const bindItem = (item) => {
|
|
3640
|
+
if ($listeners.has(item)) {
|
|
3641
|
+
return;
|
|
3642
|
+
}
|
|
3643
|
+
if (item?.__$isObservableArray) {
|
|
3644
|
+
$listeners.set(item, item.deepSubscribe(updatedValue));
|
|
3645
|
+
return;
|
|
3646
|
+
}
|
|
3647
|
+
if (item?.__$isObservable) {
|
|
3648
|
+
item.subscribe(updatedValue);
|
|
3649
|
+
$listeners.set(item, () => item.unsubscribe(updatedValue));
|
|
3650
|
+
}
|
|
3651
|
+
};
|
|
3652
|
+
|
|
3653
|
+
const unbindItem = (item) => {
|
|
3654
|
+
const unsub = $listeners.get(item);
|
|
3655
|
+
if (unsub) {
|
|
3656
|
+
unsub();
|
|
3657
|
+
$listeners.delete(item);
|
|
3658
|
+
}
|
|
3659
|
+
};
|
|
3660
|
+
|
|
3661
|
+
this.$currentValue.forEach(bindItem);
|
|
3662
|
+
this.subscribe(updatedValue);
|
|
3663
|
+
|
|
3664
|
+
this.subscribe((items, _, operations) => {
|
|
3665
|
+
switch (operations?.action) {
|
|
3666
|
+
case 'push':
|
|
3667
|
+
case 'unshift':
|
|
3668
|
+
operations.args.forEach(bindItem);
|
|
3669
|
+
break;
|
|
3670
|
+
|
|
3671
|
+
case 'splice': {
|
|
3672
|
+
const [start, deleteCount, ...newItems] = operations.args;
|
|
3673
|
+
operations.result?.forEach(unbindItem);
|
|
3674
|
+
newItems.forEach(bindItem);
|
|
3675
|
+
break;
|
|
3676
|
+
}
|
|
3677
|
+
|
|
3678
|
+
case 'remove':
|
|
3679
|
+
unbindItem(operations.result);
|
|
3680
|
+
break;
|
|
3681
|
+
|
|
3682
|
+
case 'merge':
|
|
3683
|
+
operations.args.forEach(bindItem);
|
|
3684
|
+
break;
|
|
3685
|
+
|
|
3686
|
+
case 'clear':
|
|
3687
|
+
this.$currentValue.forEach(unbindItem);
|
|
3688
|
+
break;
|
|
3689
|
+
}
|
|
3690
|
+
});
|
|
3691
|
+
|
|
3692
|
+
return () => {
|
|
3693
|
+
this.$currentValue.forEach(unbindItem);
|
|
3694
|
+
};
|
|
3695
|
+
};
|
|
3696
|
+
|
|
3002
3697
|
/**
|
|
3003
3698
|
* Creates an observable array with reactive array methods.
|
|
3004
3699
|
* All mutations trigger updates automatically.
|
|
@@ -3038,10 +3733,71 @@ var NativeComponents = (function (exports) {
|
|
|
3038
3733
|
return batch;
|
|
3039
3734
|
};
|
|
3040
3735
|
|
|
3041
|
-
const
|
|
3736
|
+
const ObservableObject = function(target, configs) {
|
|
3737
|
+
ObservableItem.call(this, target);
|
|
3738
|
+
this.$observables = {};
|
|
3739
|
+
this.configs = configs;
|
|
3740
|
+
|
|
3741
|
+
this.$load(target);
|
|
3742
|
+
|
|
3743
|
+
for(const name in target) {
|
|
3744
|
+
if(!Object.hasOwn(this, name)) {
|
|
3745
|
+
Object.defineProperty(this, name, {
|
|
3746
|
+
get: () => this.$observables[name],
|
|
3747
|
+
set: (value) => this.$observables[name].set(value)
|
|
3748
|
+
});
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
|
|
3752
|
+
};
|
|
3753
|
+
|
|
3754
|
+
ObservableObject.prototype = Object.create(ObservableItem.prototype);
|
|
3755
|
+
|
|
3756
|
+
Object.defineProperty(ObservableObject, '$value', {
|
|
3757
|
+
get() {
|
|
3758
|
+
return this.val();
|
|
3759
|
+
},
|
|
3760
|
+
set(value) {
|
|
3761
|
+
this.set(value);
|
|
3762
|
+
}
|
|
3763
|
+
});
|
|
3764
|
+
|
|
3765
|
+
ObservableObject.prototype.__$isObservableObject = true;
|
|
3766
|
+
ObservableObject.prototype.__isProxy__ = true;
|
|
3767
|
+
|
|
3768
|
+
ObservableObject.prototype.$load = function(initialValue) {
|
|
3769
|
+
const configs = this.configs;
|
|
3770
|
+
for(const key in initialValue) {
|
|
3771
|
+
const itemValue = initialValue[key];
|
|
3772
|
+
if(Array.isArray(itemValue)) {
|
|
3773
|
+
if(configs?.deep !== false) {
|
|
3774
|
+
const mappedItemValue = itemValue.map(item => {
|
|
3775
|
+
if(Validator.isJson(item)) {
|
|
3776
|
+
return Observable.json(item, configs);
|
|
3777
|
+
}
|
|
3778
|
+
if(Validator.isArray(item)) {
|
|
3779
|
+
return Observable.array(item, configs);
|
|
3780
|
+
}
|
|
3781
|
+
return Observable(item, configs);
|
|
3782
|
+
});
|
|
3783
|
+
this.$observables[key] = Observable.array(mappedItemValue, configs);
|
|
3784
|
+
continue;
|
|
3785
|
+
}
|
|
3786
|
+
this.$observables[key] = Observable.array(itemValue, configs);
|
|
3787
|
+
continue;
|
|
3788
|
+
}
|
|
3789
|
+
if(Validator.isObservable(itemValue) || Validator.isProxy(itemValue)) {
|
|
3790
|
+
this.$observables[key] = itemValue;
|
|
3791
|
+
continue;
|
|
3792
|
+
}
|
|
3793
|
+
this.$observables[key] = (typeof itemValue === 'object') ? Observable.object(itemValue, configs) : Observable(itemValue, configs);
|
|
3794
|
+
}
|
|
3795
|
+
};
|
|
3796
|
+
|
|
3797
|
+
ObservableObject.prototype.val = function() {
|
|
3042
3798
|
const result = {};
|
|
3043
|
-
for(const key in
|
|
3044
|
-
const dataItem =
|
|
3799
|
+
for(const key in this.$observables) {
|
|
3800
|
+
const dataItem = this.$observables[key];
|
|
3045
3801
|
if(Validator.isObservable(dataItem)) {
|
|
3046
3802
|
let value = dataItem.val();
|
|
3047
3803
|
if(Array.isArray(value)) {
|
|
@@ -3064,9 +3820,10 @@ var NativeComponents = (function (exports) {
|
|
|
3064
3820
|
}
|
|
3065
3821
|
return result;
|
|
3066
3822
|
};
|
|
3823
|
+
ObservableObject.prototype.$val = ObservableObject.prototype.val;
|
|
3067
3824
|
|
|
3068
|
-
|
|
3069
|
-
const item =
|
|
3825
|
+
ObservableObject.prototype.get = function(property) {
|
|
3826
|
+
const item = this.$observables[property];
|
|
3070
3827
|
if(Validator.isObservable(item)) {
|
|
3071
3828
|
return item.val();
|
|
3072
3829
|
}
|
|
@@ -3075,100 +3832,88 @@ var NativeComponents = (function (exports) {
|
|
|
3075
3832
|
}
|
|
3076
3833
|
return item;
|
|
3077
3834
|
};
|
|
3835
|
+
ObservableObject.prototype.$get = ObservableObject.prototype.get;
|
|
3078
3836
|
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
* user.name = 'Jane X'
|
|
3099
|
-
* user.age.subscribe(val => console.log('Age:', val));
|
|
3100
|
-
*/
|
|
3101
|
-
Observable.init = function(initialValue, configs = null) {
|
|
3102
|
-
const data = {};
|
|
3103
|
-
for(const key in initialValue) {
|
|
3104
|
-
const itemValue = initialValue[key];
|
|
3105
|
-
if(Array.isArray(itemValue)) {
|
|
3106
|
-
if(configs?.deep !== false) {
|
|
3107
|
-
const mappedItemValue = itemValue.map(item => {
|
|
3108
|
-
if(Validator.isJson(item)) {
|
|
3109
|
-
return Observable.json(item, configs);
|
|
3110
|
-
}
|
|
3111
|
-
if(Validator.isArray(item)) {
|
|
3112
|
-
return Observable.array(item, configs);
|
|
3837
|
+
ObservableObject.prototype.set = function(newData) {
|
|
3838
|
+
const data = Validator.isProxy(newData) ? newData.$value : newData;
|
|
3839
|
+
const configs = this.configs;
|
|
3840
|
+
|
|
3841
|
+
for(const key in data) {
|
|
3842
|
+
const targetItem = this.$observables[key];
|
|
3843
|
+
const newValueOrigin = newData[key];
|
|
3844
|
+
const newValue = data[key];
|
|
3845
|
+
|
|
3846
|
+
if(Validator.isObservable(targetItem)) {
|
|
3847
|
+
if(!Validator.isArray(newValue)) {
|
|
3848
|
+
targetItem.set(newValue);
|
|
3849
|
+
continue;
|
|
3850
|
+
}
|
|
3851
|
+
const firstElementFromOriginalValue = newValueOrigin.at(0);
|
|
3852
|
+
if(Validator.isObservable(firstElementFromOriginalValue) || Validator.isProxy(firstElementFromOriginalValue)) {
|
|
3853
|
+
const newValues = newValue.map(item => {
|
|
3854
|
+
if(Validator.isProxy(firstElementFromOriginalValue)) {
|
|
3855
|
+
return Observable.init(item, configs);
|
|
3113
3856
|
}
|
|
3114
3857
|
return Observable(item, configs);
|
|
3115
3858
|
});
|
|
3116
|
-
|
|
3859
|
+
targetItem.set(newValues);
|
|
3117
3860
|
continue;
|
|
3118
3861
|
}
|
|
3119
|
-
|
|
3862
|
+
targetItem.set([...newValue]);
|
|
3120
3863
|
continue;
|
|
3121
3864
|
}
|
|
3122
|
-
if(Validator.
|
|
3123
|
-
|
|
3865
|
+
if(Validator.isProxy(targetItem)) {
|
|
3866
|
+
targetItem.update(newValue);
|
|
3124
3867
|
continue;
|
|
3125
3868
|
}
|
|
3126
|
-
|
|
3869
|
+
this[key] = newValue;
|
|
3127
3870
|
}
|
|
3871
|
+
};
|
|
3872
|
+
ObservableObject.prototype.$set = ObservableObject.prototype.set;
|
|
3873
|
+
ObservableObject.prototype.$updateWith = ObservableObject.prototype.set;
|
|
3128
3874
|
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
}
|
|
3134
|
-
};
|
|
3135
|
-
|
|
3136
|
-
const $val = () => ObservableObjectValue(data);
|
|
3875
|
+
ObservableObject.prototype.observables = function() {
|
|
3876
|
+
return Object.values(this.$observables);
|
|
3877
|
+
};
|
|
3878
|
+
ObservableObject.prototype.$observables = ObservableObject.prototype.observables;
|
|
3137
3879
|
|
|
3138
|
-
|
|
3880
|
+
ObservableObject.prototype.keys = function() {
|
|
3881
|
+
return Object.keys(this.$observables);
|
|
3882
|
+
};
|
|
3883
|
+
ObservableObject.prototype.$keys = ObservableObject.prototype.keys;
|
|
3884
|
+
ObservableObject.prototype.clone = function() {
|
|
3885
|
+
return Observable.init(this.val(), this.configs);
|
|
3886
|
+
};
|
|
3887
|
+
ObservableObject.prototype.$clone = ObservableObject.prototype.clone;
|
|
3888
|
+
ObservableObject.prototype.reset = function() {
|
|
3889
|
+
for(const key in this.$observables) {
|
|
3890
|
+
this.$observables[key].reset();
|
|
3891
|
+
}
|
|
3892
|
+
};
|
|
3893
|
+
ObservableObject.prototype.originalSubscribe = ObservableObject.prototype.subscribe;
|
|
3894
|
+
ObservableObject.prototype.subscribe = function(callback) {
|
|
3895
|
+
const observables = this.observables();
|
|
3896
|
+
const updatedValue = nextTick(() => this.trigger());
|
|
3139
3897
|
|
|
3140
|
-
|
|
3141
|
-
Observable.update(proxy, values);
|
|
3142
|
-
};
|
|
3898
|
+
this.originalSubscribe(callback);
|
|
3143
3899
|
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
if(property === '$value') { return $val() }
|
|
3150
|
-
if(property === 'get' || property === '$get') { return $get; }
|
|
3151
|
-
if(property === 'val' || property === '$val') { return $val; }
|
|
3152
|
-
if(property === 'set' || property === '$set' || property === '$updateWith') { return $updateWith; }
|
|
3153
|
-
if(property === 'observables' || property === '$observables') { return Object.values(target); }
|
|
3154
|
-
if(property === 'keys'|| property === '$keys') { return Object.keys(initialValue); }
|
|
3155
|
-
if(property === 'clone' || property === '$clone') { return $clone; }
|
|
3156
|
-
if(property === 'reset') { return $reset; }
|
|
3157
|
-
if(property === 'configs') { return configs; }
|
|
3158
|
-
return target[property];
|
|
3159
|
-
},
|
|
3160
|
-
set(target, prop, newValue) {
|
|
3161
|
-
if(target[prop] !== undefined) {
|
|
3162
|
-
Validator.isObservable(newValue)
|
|
3163
|
-
? target[prop].set(newValue.val())
|
|
3164
|
-
: target[prop].set(newValue);
|
|
3165
|
-
return true;
|
|
3166
|
-
}
|
|
3167
|
-
return true;
|
|
3900
|
+
for (let i = 0, length = observables.length; i < length; i++) {
|
|
3901
|
+
const observable = observables[i];
|
|
3902
|
+
if (observable.__$isObservableArray) {
|
|
3903
|
+
observable.deepSubscribe(updatedValue);
|
|
3904
|
+
continue
|
|
3168
3905
|
}
|
|
3169
|
-
|
|
3906
|
+
observable.subscribe(updatedValue);
|
|
3907
|
+
}
|
|
3908
|
+
};
|
|
3909
|
+
ObservableObject.prototype.configs = function() {
|
|
3910
|
+
return this.configs;
|
|
3911
|
+
};
|
|
3912
|
+
|
|
3913
|
+
ObservableObject.prototype.update = ObservableObject.prototype.set;
|
|
3170
3914
|
|
|
3171
|
-
|
|
3915
|
+
Observable.init = function(initialValue, configs = null) {
|
|
3916
|
+
return new ObservableObject(initialValue, configs)
|
|
3172
3917
|
};
|
|
3173
3918
|
|
|
3174
3919
|
/**
|
|
@@ -3203,43 +3948,6 @@ var NativeComponents = (function (exports) {
|
|
|
3203
3948
|
return data;
|
|
3204
3949
|
};
|
|
3205
3950
|
|
|
3206
|
-
|
|
3207
|
-
Observable.update = function($target, newData) {
|
|
3208
|
-
const data = Validator.isProxy(newData) ? newData.$value : newData;
|
|
3209
|
-
const configs = $target.configs;
|
|
3210
|
-
|
|
3211
|
-
for(const key in data) {
|
|
3212
|
-
const targetItem = $target[key];
|
|
3213
|
-
const newValueOrigin = newData[key];
|
|
3214
|
-
const newValue = data[key];
|
|
3215
|
-
|
|
3216
|
-
if(Validator.isObservable(targetItem)) {
|
|
3217
|
-
if(Validator.isArray(newValue)) {
|
|
3218
|
-
const firstElementFromOriginalValue = newValueOrigin.at(0);
|
|
3219
|
-
if(Validator.isObservable(firstElementFromOriginalValue) || Validator.isProxy(firstElementFromOriginalValue)) {
|
|
3220
|
-
const newValues = newValue.map(item => {
|
|
3221
|
-
if(Validator.isProxy(firstElementFromOriginalValue)) {
|
|
3222
|
-
return Observable.init(item, configs);
|
|
3223
|
-
}
|
|
3224
|
-
return Observable(item, configs);
|
|
3225
|
-
});
|
|
3226
|
-
targetItem.set(newValues);
|
|
3227
|
-
continue;
|
|
3228
|
-
}
|
|
3229
|
-
targetItem.set([...newValue]);
|
|
3230
|
-
continue;
|
|
3231
|
-
}
|
|
3232
|
-
targetItem.set(newValue);
|
|
3233
|
-
continue;
|
|
3234
|
-
}
|
|
3235
|
-
if(Validator.isProxy(targetItem)) {
|
|
3236
|
-
Observable.update(targetItem, newValue);
|
|
3237
|
-
continue;
|
|
3238
|
-
}
|
|
3239
|
-
$target[key] = newValue;
|
|
3240
|
-
}
|
|
3241
|
-
};
|
|
3242
|
-
|
|
3243
3951
|
Observable.object = Observable.init;
|
|
3244
3952
|
Observable.json = Observable.init;
|
|
3245
3953
|
|