native-document 1.0.95 → 1.0.98

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/{src/devtools/hrm → devtools}/ComponentRegistry.js +2 -2
  2. package/devtools/index.js +8 -0
  3. package/{src/devtools/plugin.js → devtools/plugin/dev-tools-plugin.js} +2 -2
  4. package/{src/devtools/hrm/nd-vite-hot-reload.js → devtools/transformers/nd-vite-devtools.js} +16 -6
  5. package/devtools/transformers/src/transformComponentForHrm.js +74 -0
  6. package/devtools/transformers/src/transformJsFile.js +9 -0
  7. package/devtools/transformers/src/utils.js +79 -0
  8. package/devtools/widget/Widget.js +48 -0
  9. package/devtools/widget/widget.css +81 -0
  10. package/devtools/widget.js +23 -0
  11. package/dist/native-document.components.min.js +1922 -1277
  12. package/dist/native-document.dev.js +1985 -1401
  13. package/dist/native-document.dev.js.map +1 -1
  14. package/dist/native-document.devtools.min.js +1 -1
  15. package/dist/native-document.min.js +1 -1
  16. package/docs/cache.md +1 -1
  17. package/docs/core-concepts.md +1 -1
  18. package/docs/native-document-element.md +51 -15
  19. package/docs/observables.md +310 -306
  20. package/docs/state-management.md +198 -193
  21. package/package.json +1 -1
  22. package/readme.md +1 -1
  23. package/src/core/data/ObservableChecker.js +2 -0
  24. package/src/core/data/ObservableItem.js +97 -0
  25. package/src/core/data/ObservableObject.js +182 -0
  26. package/src/core/data/Store.js +364 -34
  27. package/src/core/data/observable-helpers/object.js +2 -166
  28. package/src/core/utils/formatters.js +91 -0
  29. package/src/core/utils/localstorage.js +57 -0
  30. package/src/core/utils/validator.js +0 -2
  31. package/src/devtools.js +9 -0
  32. package/src/fetch/NativeFetch.js +5 -2
  33. package/types/observable.d.ts +71 -15
  34. package/types/plugins-manager.d.ts +1 -1
  35. package/types/store.d.ts +33 -6
  36. package/hrm.js +0 -7
  37. package/src/devtools/app/App.js +0 -66
  38. package/src/devtools/app/app.css +0 -0
  39. package/src/devtools/hrm/transformComponent.js +0 -129
  40. package/src/devtools/index.js +0 -18
  41. package/src/devtools/widget/DevToolsWidget.js +0 -26
  42. /package/{src/devtools/hrm → devtools/transformers/templates}/hrm.hook.template.js +0 -0
  43. /package/{src/devtools/hrm → devtools/transformers/templates}/hrm.orbservable.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$1 = 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
- * Creates an ObservableWhen that tracks whether an observable equals a specific value.
524
- *
525
- * @param {ObservableItem} observer - The observable to watch
526
- * @param {*} value - The value to compare against
527
- * @class ObservableWhen
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
- const invoke = function(fn, args, context) {
576
- if(context) {
577
- fn.apply(context, args);
578
- } else {
579
- fn(...args);
580
- }
581
- };
582
- /**
583
- *
584
- * @param {Function} fn
585
- * @param {number} delay
586
- * @param {{leading?:Boolean, trailing?:Boolean, debounce?:Boolean, check: Function}}options
587
- * @returns {(function(...[*]): void)|*}
588
- */
589
- const debounce = function(fn, delay, options = {}) {
590
- let timer = null;
591
- let lastArgs = null;
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
- return function(...args) {
594
- const context = options.context === true ? this : null;
595
- if(options.check) {
596
- options.check(...args);
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
- // debounce mode: reset the timer for each call
601
- clearTimeout(timer);
602
- timer = setTimeout(() => invoke(fn, lastArgs, context), delay);
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
- const nextTick = function(fn) {
607
- let pending = false;
608
- return function(...args) {
609
- if (pending) return;
610
- pending = true;
516
+ if(shouldRemove) {
517
+ data.disconnect();
518
+ node.nd?.remove();
519
+ }
520
+ },
611
521
 
612
- Promise.resolve().then(() => {
613
- fn.apply(this, args);
614
- pending = false;
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
- const deepClone = (value, onObservableFound) => {
620
- try {
621
- if(window.structuredClone !== undefined) {
622
- return window.structuredClone(value);
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
- } catch (e){}
550
+ },
625
551
 
626
- if (value === null || typeof value !== 'object') {
627
- return value;
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
- // Dates
631
- if (value instanceof Date) {
632
- return new Date(value.getTime());
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
- // Arrays
636
- if (Array.isArray(value)) {
637
- return value.map(item => deepClone(item));
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
- // Observables - keep the référence
641
- if (Validator.isObservable(value)) {
642
- onObservableFound && onObservableFound(value);
643
- return value;
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
- // Objects
647
- const cloned = {};
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
- * @param {*} value
659
- * @param {{ propagation: boolean, reset: boolean} | null} configs
660
- * @class ObservableItem
661
- */
662
- function ObservableItem(value, configs = null) {
663
- value = Validator.isObservable(value) ? value.val() : value;
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
- this.$previousValue = null;
666
- this.$currentValue = value;
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
- this.$firstListener = null;
669
- this.$listeners = null;
670
- this.$watchers = null;
631
+ off: (type, callback) => {
632
+ removeListener(type, callback);
633
+ }
634
+ };
635
+ }
636
+ };
671
637
 
672
- this.$memoryId = null;
638
+ DocumentObserver.observer = new MutationObserver(DocumentObserver.checkMutation);
639
+ DocumentObserver.observer.observe(document.body, {
640
+ childList: true,
641
+ subtree: true,
642
+ });
673
643
 
674
- if(configs) {
675
- this.configs = configs;
676
- if(configs.reset) {
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
- Object.defineProperty(ObservableItem.prototype, '$value', {
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
- ObservableItem.prototype.__$isObservable = true;
693
- const noneTrigger = function() {};
651
+ NDElement.prototype.valueOf = function() {
652
+ return this.$element;
653
+ };
694
654
 
695
- /**
696
- * Intercepts and transforms values before they are set on the observable.
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
- ObservableItem.prototype.triggerFirstListener = function(operations) {
712
- this.$firstListener(this.$currentValue, this.$previousValue, operations);
660
+ NDElement.prototype.refSelf = function(target, name) {
661
+ target[name] = this;
662
+ return this;
713
663
  };
714
664
 
715
- ObservableItem.prototype.triggerListeners = function(operations) {
716
- const $listeners = this.$listeners;
717
- const $previousValue = this.$previousValue;
718
- const $currentValue = this.$currentValue;
719
-
720
- for(let i = 0, length = $listeners.length; i < length; i++) {
721
- $listeners[i]($currentValue, $previousValue, operations);
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
- ObservableItem.prototype.triggerWatchers = function(operations) {
726
- const $watchers = this.$watchers;
727
- const $previousValue = this.$previousValue;
728
- const $currentValue = this.$currentValue;
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
- const $currentValueCallbacks = $watchers.get($currentValue);
731
- const $previousValueCallbacks = $watchers.get($previousValue);
732
- if($currentValueCallbacks) {
733
- $currentValueCallbacks(true, $previousValue, operations);
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($previousValueCallbacks) {
736
- $previousValueCallbacks(false, $currentValue, operations);
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
- ObservableItem.prototype.triggerAll = function(operations) {
741
- this.triggerWatchers(operations);
742
- this.triggerListeners(operations);
703
+ NDElement.prototype.mounted = function(callback) {
704
+ return this.lifecycle({ mounted: callback });
743
705
  };
744
706
 
745
- ObservableItem.prototype.triggerWatchersAndFirstListener = function(operations) {
746
- this.triggerWatchers(operations);
747
- this.triggerFirstListener(operations);
707
+ NDElement.prototype.unmounted = function(callback) {
708
+ return this.lifecycle({ unmounted: callback });
748
709
  };
749
710
 
750
- ObservableItem.prototype.assocTrigger = function() {
751
- this.$firstListener = null;
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
- ObservableItem.prototype.$updateWithNewValue = function(newValue) {
775
- newValue = newValue?.__$isObservable ? newValue.val() : newValue;
776
- if(this.$currentValue === newValue) {
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
- if (result !== undefined) {
793
- newValue = result;
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
- this.$updateWithNewValue(newValue);
738
+ DocumentObserver.beforeUnmount.get(el).set(id, callback);
739
+ return this;
797
740
  };
798
741
 
799
- /**
800
- * @param {*} data
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
- ObservableItem.prototype.set = ObservableItem.prototype.$basicSet;
808
-
809
- ObservableItem.prototype.val = function() {
810
- return this.$currentValue;
811
- };
746
+ NDElement.prototype.node = NDElement.prototype.htmlElement;
812
747
 
813
- ObservableItem.prototype.disconnectAll = function() {
814
- this.$listeners?.splice(0);
815
- this.$previousValue = null;
816
- this.$currentValue = null;
817
- if(this.$watchers) {
818
- for (const [_, watchValueList] of this.$watchers) {
819
- if(Validator.isArray(watchValueList)) {
820
- watchValueList.splice(0);
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
- this.$watchers?.clear();
825
- this.$listeners = null;
826
- this.$watchers = null;
827
- this.trigger = noneTrigger;
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
- * Registers a cleanup callback that will be executed when the observable is cleaned up.
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
- ObservableItem.prototype.cleanup = function() {
846
- if (this.$cleanupListeners) {
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 {Function} callback
860
- * @returns {(function(): void)}
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
- ObservableItem.prototype.subscribe = function(callback) {
863
- this.$listeners = this.$listeners ?? [];
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
- * Watches for a specific value and executes callback when the observable equals that value.
871
- * Creates a watcher that only triggers when the observable changes to the specified value.
788
+ * Extends the current NDElement instance with custom methods.
789
+ * Methods are bound to the instance and available for chaining.
872
790
  *
873
- * @param {*} value - The value to watch for
874
- * @param {(value) => void|ObservableItem} callback - Callback function or observable to set when value matches
791
+ * @param {Object} methods - Object containing method definitions
792
+ * @returns {this} The NDElement instance with added methods for chaining
875
793
  * @example
876
- * const status = Observable('idle');
877
- * status.on('loading', () => console.log('Started loading'));
878
- * status.on('error', isError); // Set another observable
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
- ObservableItem.prototype.on = function(value, callback) {
881
- this.$watchers = this.$watchers ?? new Map();
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
- let watchValueList = this.$watchers.get(value);
806
+ for (const name in methods) {
807
+ const method = methods[name];
884
808
 
885
- if(callback.__$isObservable) {
886
- callback = callback.set.bind(callback);
887
- }
809
+ if (typeof method !== 'function') {
810
+ console.warn(`⚠️ extends(): "${name}" is not a function, skipping`);
811
+ continue;
812
+ }
888
813
 
889
- if(!watchValueList) {
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.assocTrigger();
817
+ return this;
906
818
  };
907
819
 
908
820
  /**
909
- * Removes a watcher for a specific value. If no callback is provided, removes all watchers for that value.
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 {*} value - The value to stop watching
912
- * @param {Function} [callback] - Specific callback to remove. If omitted, removes all watchers for this value
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
- * const status = Observable('idle');
915
- * const handler = () => console.log('Loading');
916
- * status.on('loading', handler);
917
- * status.off('loading', handler); // Remove specific handler
918
- * status.off('loading'); // Remove all handlers for 'loading'
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
- ObservableItem.prototype.off = function(value, callback) {
921
- if(!this.$watchers) return;
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
- else if(watchValueList.length === 0) {
937
- this.$watchers?.delete(value);
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
- * Subscribes to the observable but automatically unsubscribes after the first time the predicate matches.
944
- *
945
- * @param {(value) => Boolean|any} predicate - Value to match or function that returns true when condition is met
946
- * @param {(value) => void} callback - Callback to execute when predicate matches, receives the matched value
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 handler = (val) => {
956
- if (fn(val)) {
957
- this.unsubscribe(handler);
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
- * Unsubscribe from an observable.
966
- * @param {Function} callback
967
- */
968
- ObservableItem.prototype.unsubscribe = function(callback) {
969
- if(!this.$listeners) return;
970
- const index = this.$listeners.indexOf(callback);
971
- if (index > -1) {
972
- this.$listeners.splice(index, 1);
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
- this.assocTrigger();
875
+
876
+ return NDElement;
975
877
  };
976
878
 
977
- /**
978
- * Create an Observable checker instance
979
- * @param callback
980
- * @returns {ObservableChecker}
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
- * Gets a property value from the observable's current value.
988
- * If the property is an observable, returns its value.
989
- *
990
- * @param {string|number} key - Property key to retrieve
991
- * @returns {*} The value of the property, unwrapped if it's an observable
992
- * @example
993
- * const user = Observable({ name: 'John', age: Observable(25) });
994
- * user.get('name'); // 'John'
995
- * user.get('age'); // 25 (unwrapped from observable)
996
- */
997
- ObservableItem.prototype.get = function(key) {
998
- const item = this.$currentValue[key];
999
- return Validator.isObservable(item) ? item.val() : item;
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 represents whether the observable equals a specific value.
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
- * @returns {ObservableWhen} An ObservableWhen instance that tracks when the observable equals the value
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
- ObservableItem.prototype.when = function(value) {
1015
- return new ObservableWhen(this, value);
1267
+ ObservableWhen.prototype.subscribe = function(callback) {
1268
+ return this.$observer.on(this.$target, callback);
1016
1269
  };
1017
1270
 
1018
1271
  /**
1019
- * Compares the observable's current value with another value or observable.
1272
+ * Returns true if the observable's current value equals the target value.
1020
1273
  *
1021
- * @param {*|ObservableItem} other - Value or observable to compare against
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
- ObservableItem.prototype.equals = function(other) {
1031
- if(Validator.isObservable(other)) {
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
- * Converts the observable's current value to a boolean.
1281
+ * Returns true if the observable's current value equals the target value.
1282
+ * Alias for val().
1039
1283
  *
1040
- * @returns {boolean} The boolean representation of the current value
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
- ObservableItem.prototype.toBool = function() {
1048
- return !!this.$currentValue;
1286
+ ObservableWhen.prototype.isMatch = ObservableWhen.prototype.val;
1287
+
1288
+ /**
1289
+ * Returns true if the observable's current value equals the target value.
1290
+ * Alias for val().
1291
+ *
1292
+ * @returns {boolean} True if observable value matches target value
1293
+ */
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
+ };
1049
1338
  };
1050
1339
 
1051
- /**
1052
- * Toggles the boolean value of the observable (false becomes true, true becomes false).
1053
- *
1054
- * @example
1055
- * const isOpen = Observable(false);
1056
- * isOpen.toggle(); // Now true
1057
- * isOpen.toggle(); // Now false
1058
- */
1059
- ObservableItem.prototype.toggle = function() {
1060
- this.set(!this.$currentValue);
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;
1061
1375
  };
1062
1376
 
1063
- /**
1064
- * Resets the observable to its initial value.
1065
- * Only works if the observable was created with { reset: true } config.
1066
- *
1067
- * @example
1068
- * const count = Observable(0, { reset: true });
1069
- * count.set(10);
1070
- * count.reset(); // Back to 0
1071
- */
1072
- ObservableItem.prototype.reset = function() {
1073
- if(!this.configs?.reset) {
1074
- return;
1377
+ const LocalStorage$1 = {
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;
1075
1410
  }
1076
- const resetValue = (Validator.isObject(this.$initialValue))
1077
- ? deepClone(this.$initialValue, (observable) => {
1078
- observable.reset();
1079
- })
1080
- : this.$initialValue;
1081
- this.set(resetValue);
1082
1411
  };
1083
1412
 
1084
- /**
1085
- * Returns a string representation of the observable's current value.
1086
- *
1087
- * @returns {string} String representation of the current value
1088
- */
1089
- ObservableItem.prototype.toString = function() {
1090
- return String(this.$currentValue);
1413
+ const $getFromStorage$1 = (key, value) => {
1414
+ if(!LocalStorage$1.has(key)) {
1415
+ return value;
1416
+ }
1417
+ switch (typeof value) {
1418
+ case 'object': return LocalStorage$1.getJson(key) ?? value;
1419
+ case 'boolean': return LocalStorage$1.getBool(key) ?? value;
1420
+ case 'number': return LocalStorage$1.getNumber(key) ?? value;
1421
+ default: return LocalStorage$1.get(key, value) ?? value;
1422
+ }
1091
1423
  };
1092
1424
 
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;
1425
+ const $saveToStorage$1 = (value) => {
1426
+ switch (typeof value) {
1427
+ case 'object': return LocalStorage$1.setJson;
1428
+ case 'boolean': return LocalStorage$1.setBool;
1429
+ default: return LocalStorage$1.set;
1430
+ }
1101
1431
  };
1102
1432
 
1103
- const DocumentObserver = {
1104
- mounted: new WeakMap(),
1105
- beforeUnmount: new WeakMap(),
1106
- mountedSupposedSize: 0,
1107
- unmounted: new WeakMap(),
1108
- unmountedSupposedSize: 0,
1109
- observer: null,
1433
+ const StoreFactory = function() {
1110
1434
 
1111
- executeMountedCallback(node) {
1112
- const data = DocumentObserver.mounted.get(node);
1113
- if(!data) {
1114
- return;
1115
- }
1116
- data.inDom = true;
1117
- if(!data.mounted) {
1118
- return;
1119
- }
1120
- if(Array.isArray(data.mounted)) {
1121
- for(const cb of data.mounted) {
1122
- cb(node);
1123
- }
1124
- return;
1125
- }
1126
- data.mounted(node);
1127
- },
1435
+ const $stores = new Map();
1436
+ const $followersCache = new Map();
1128
1437
 
1129
- executeUnmountedCallback(node) {
1130
- const data = DocumentObserver.unmounted.get(node);
1131
- if(!data) {
1132
- return;
1133
- }
1134
- data.inDom = false;
1135
- if(!data.unmounted) {
1136
- 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
+ );
1137
1448
  }
1449
+ return item;
1450
+ };
1138
1451
 
1139
- let shouldRemove = false;
1140
- if(Array.isArray(data.unmounted)) {
1141
- for(const cb of data.unmounted) {
1142
- if(cb(node) === true) {
1143
- shouldRemove = true;
1144
- }
1145
- }
1146
- } else {
1147
- shouldRemove = data.unmounted(node) === true;
1148
- }
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
+ };
1149
1466
 
1150
- if(shouldRemove) {
1151
- data.disconnect();
1152
- node.nd?.remove();
1467
+ const $createObservable = (value, options = {}) => {
1468
+ if(Array.isArray(value)) {
1469
+ return Observable$1.array(value, options);
1153
1470
  }
1154
- },
1471
+ if(typeof value === 'object') {
1472
+ return Observable$1.object(value, options);
1473
+ }
1474
+ return Observable$1(value, options);
1475
+ };
1155
1476
 
1156
- checkMutation: function(mutationsList) {
1157
- for(const mutation of mutationsList) {
1158
- if(DocumentObserver.mountedSupposedSize > 0) {
1159
- for(const node of mutation.addedNodes) {
1160
- DocumentObserver.executeMountedCallback(node);
1161
- if(!node.querySelectorAll) {
1162
- continue;
1163
- }
1164
- const children = node.querySelectorAll('[data--nd-mounted]');
1165
- for(const child of children) {
1166
- DocumentObserver.executeMountedCallback(child);
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
- if (DocumentObserver.unmountedSupposedSize > 0) {
1172
- for (const node of mutation.removedNodes) {
1173
- DocumentObserver.executeUnmountedCallback(node);
1174
- if(!node.querySelectorAll) {
1175
- continue;
1176
- }
1177
- const children = node.querySelectorAll('[data--nd-unmounted]');
1178
- for(const child of children) {
1179
- DocumentObserver.executeUnmountedCallback(child);
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
- * @param {HTMLElement} element
1188
- * @param {boolean} inDom
1189
- * @returns {{ disconnect: Function, mounted: Function, unmounted: Function, off: Function }}
1190
- */
1191
- watch: function(element, inDom = false) {
1192
- let mountedRegistered = false;
1193
- let unmountedRegistered = false;
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
- let data = {
1196
- inDom,
1197
- mounted: null,
1198
- unmounted: null,
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
- if (unmountedRegistered) {
1205
- DocumentObserver.unmounted.delete(element);
1206
- DocumentObserver.unmountedSupposedSize--;
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
- data = null;
1209
- }
1210
- };
1573
+ return depItem.observer;
1574
+ });
1211
1575
 
1212
- const addListener = (type, callback) => {
1213
- if (!data[type]) {
1214
- data[type] = callback;
1215
- return;
1576
+ // Create computed observable from dependency observers
1577
+ const observer = Observable$1.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 (!Array.isArray(data[type])) {
1218
- data[type] = [data[type], callback];
1219
- return;
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
- data[type].push(callback);
1222
- };
1613
+ item.observer.reset();
1614
+ },
1223
1615
 
1224
- const removeListener = (type, callback) => {
1225
- if(!data?.[type]) {
1226
- return;
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
+ );
1227
1633
  }
1228
- if(Array.isArray(data[type])) {
1229
- const index = data[type].indexOf(callback);
1230
- if(index > -1) {
1231
- data[type].splice(index, 1);
1232
- }
1233
- if(data[type].length === 1) {
1234
- data[type] = data[type][0];
1235
- }
1236
- if(data[type].length === 0) {
1237
- data[type] = null;
1238
- }
1634
+
1635
+ const { observer: originalObserver, subscribers } = item;
1636
+ const observerFollower = $createObservable(originalObserver.val());
1637
+
1638
+ const onStoreChange = value => observerFollower.set(value);
1639
+ const onFollowerChange = value => originalObserver.set(value);
1640
+
1641
+ originalObserver.subscribe(onStoreChange);
1642
+ observerFollower.subscribe(onFollowerChange);
1643
+
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.`);
1239
1719
  return;
1240
1720
  }
1241
- data[type] = null;
1242
- };
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';
1774
+ }
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$1(localstorage_key, value));
1782
+ const saver = $saveToStorage$1(value);
1243
1783
 
1244
- return {
1245
- disconnect: () => data?.disconnect(),
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$1(localstorage_key, value));
1790
+ const saver = $saveToStorage$1(value);
1791
+ observer.subscribe((val) => saver(localstorage_key, val));
1792
+
1793
+ const originalReset = observer.reset.bind(observer);
1794
+ observer.reset = () => {
1795
+ LocalStorage$1.remove(localstorage_key);
1796
+ originalReset();
1797
+ };
1246
1798
 
1247
- mounted: (callback) => {
1248
- addListener('mounted', callback);
1249
- DocumentObserver.mounted.set(element, data);
1250
- if (!mountedRegistered) {
1251
- DocumentObserver.mountedSupposedSize++;
1252
- mountedRegistered = true;
1253
- }
1254
- },
1799
+ return observer;
1800
+ }
1801
+ };
1255
1802
 
1256
- unmounted: (callback) => {
1257
- addListener('unmounted', callback);
1258
- DocumentObserver.unmounted.set(element, data);
1259
- if (!unmountedRegistered) {
1260
- DocumentObserver.unmountedSupposedSize++;
1261
- unmountedRegistered = true;
1262
- }
1263
- },
1264
1803
 
1265
- off: (type, callback) => {
1266
- removeListener(type, callback);
1804
+ return new Proxy($api, {
1805
+ get(target, prop) {
1806
+ if (typeof prop === 'symbol' || prop.startsWith('$') || prop in target) {
1807
+ return target[prop];
1267
1808
  }
1268
- };
1269
- }
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
+ });
1270
1827
  };
1271
1828
 
1272
- DocumentObserver.observer = new MutationObserver(DocumentObserver.checkMutation);
1273
- DocumentObserver.observer.observe(document.body, {
1274
- childList: true,
1275
- subtree: true,
1276
- });
1277
-
1278
- function NDElement(element) {
1279
- this.$element = element;
1280
- this.$observer = null;
1281
- }
1282
-
1283
- NDElement.prototype.__$isNDElement = true;
1829
+ const Store = StoreFactory();
1284
1830
 
1285
- NDElement.prototype.valueOf = function() {
1286
- return this.$element;
1287
- };
1831
+ Store.create('locale', 'fr');
1288
1832
 
1289
- NDElement.prototype.ref = function(target, name) {
1290
- target[name] = this.$element;
1291
- return this;
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
+ };
1292
1849
  };
1293
1850
 
1294
- NDElement.prototype.refSelf = function(target, name) {
1295
- target[name] = this;
1296
- return this;
1297
- };
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
+ },
1298
1897
 
1299
- NDElement.prototype.unmountChildren = function() {
1300
- let element = this.$element;
1301
- for(let i = 0, length = element.children.length; i < length; i++) {
1302
- let elementChildren = element.children[i];
1303
- if(!elementChildren.$ndProx) {
1304
- elementChildren.nd?.remove();
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);
1305
1902
  }
1306
- elementChildren = null;
1307
- }
1308
- element = null;
1309
- return this;
1310
- };
1903
+ return new Intl.DateTimeFormat(locale, { hour, minute, second }).format(new Date(value));
1904
+ },
1311
1905
 
1312
- NDElement.prototype.remove = function() {
1313
- let element = this.$element;
1314
- element.nd.unmountChildren();
1315
- element.$ndProx = null;
1316
- delete element.nd?.on?.prevent;
1317
- delete element.nd?.on;
1318
- delete element.nd;
1319
- element = null;
1320
- return this;
1321
- };
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
+ },
1322
1913
 
1323
- NDElement.prototype.lifecycle = function(states) {
1324
- this.$observer = this.$observer || DocumentObserver.watch(this.$element);
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
+ },
1325
1918
 
1326
- if(states.mounted) {
1327
- this.$element.setAttribute('data--nd-mounted', '1');
1328
- this.$observer.mounted(states.mounted);
1329
- }
1330
- if(states.unmounted) {
1331
- this.$element.setAttribute('data--nd-unmounted', '1');
1332
- this.$observer.unmounted(states.unmounted);
1333
- }
1334
- return this;
1919
+ plural: (value, locale, { singular, plural } = {}) => {
1920
+ const rule = new Intl.PluralRules(locale).select(value);
1921
+ return `${value} ${rule === 'one' ? singular : plural}`;
1922
+ },
1335
1923
  };
1336
1924
 
1337
- NDElement.prototype.mounted = function(callback) {
1338
- return this.lifecycle({ mounted: callback });
1339
- };
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;
1340
1933
 
1341
- NDElement.prototype.unmounted = function(callback) {
1342
- return this.lifecycle({ unmounted: callback });
1343
- };
1934
+ this.$previousValue = null;
1935
+ this.$currentValue = value;
1344
1936
 
1345
- NDElement.prototype.beforeUnmount = function(id, callback) {
1346
- const el = this.$element;
1937
+ this.$firstListener = null;
1938
+ this.$listeners = null;
1939
+ this.$watchers = null;
1347
1940
 
1348
- if(!DocumentObserver.beforeUnmount.has(el)) {
1349
- DocumentObserver.beforeUnmount.set(el, new Map());
1350
- const originalRemove = el.remove.bind(el);
1941
+ this.$memoryId = null;
1351
1942
 
1352
- let $isUnmounting = false;
1943
+ if(configs) {
1944
+ this.configs = configs;
1945
+ if(configs.reset) {
1946
+ this.$initialValue = Validator.isObject(value) ? deepClone(value) : value;
1947
+ }
1948
+ }
1949
+ }
1353
1950
 
1354
- el.remove = async () => {
1355
- if($isUnmounting) {
1356
- return;
1357
- }
1358
- $isUnmounting = true;
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
+ });
1359
1960
 
1360
- try {
1361
- const callbacks = DocumentObserver.beforeUnmount.get(el);
1362
- for (const cb of callbacks.values()) {
1363
- await cb.call(this, el);
1364
- }
1365
- } finally {
1366
- originalRemove();
1367
- $isUnmounting = false;
1368
- }
1369
- };
1370
- }
1961
+ ObservableItem.prototype.__$isObservable = true;
1962
+ const noneTrigger = function() {};
1371
1963
 
1372
- DocumentObserver.beforeUnmount.get(el).set(id, callback);
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
- NDElement.prototype.htmlElement = function() {
1377
- return this.$element;
1980
+ ObservableItem.prototype.triggerFirstListener = function(operations) {
1981
+ this.$firstListener(this.$currentValue, this.$previousValue, operations);
1378
1982
  };
1379
1983
 
1380
- NDElement.prototype.node = NDElement.prototype.htmlElement;
1984
+ ObservableItem.prototype.triggerListeners = function(operations) {
1985
+ const $listeners = this.$listeners;
1986
+ const $previousValue = this.$previousValue;
1987
+ const $currentValue = this.$currentValue;
1381
1988
 
1382
- NDElement.prototype.shadow = function(mode, style = null) {
1383
- const $element = this.$element;
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);
1992
+ };
1394
1993
 
1395
- return this;
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
+ }
1396
2007
  };
1397
2008
 
1398
- NDElement.prototype.openShadow = function(style = null) {
1399
- return this.shadow('open', style);
2009
+ ObservableItem.prototype.triggerAll = function(operations) {
2010
+ this.triggerWatchers(operations);
2011
+ this.triggerListeners(operations);
2012
+ };
2013
+
2014
+ ObservableItem.prototype.triggerWatchersAndFirstListener = function(operations) {
2015
+ this.triggerWatchers(operations);
2016
+ this.triggerFirstListener(operations);
2017
+ };
2018
+
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;
2024
+ }
2025
+ if(this.$listeners?.length) {
2026
+ if(this.$listeners.length === 1) {
2027
+ this.$firstListener = this.$listeners[0];
2028
+ this.trigger = this.triggerFirstListener;
2029
+ }
2030
+ else {
2031
+ this.trigger = this.triggerListeners;
2032
+ }
2033
+ return;
2034
+ }
2035
+ if(this.$watchers?.size) {
2036
+ this.trigger = this.triggerWatchers;
2037
+ return;
2038
+ }
2039
+ this.trigger = noneTrigger;
1400
2040
  };
2041
+ ObservableItem.prototype.trigger = noneTrigger;
1401
2042
 
1402
- NDElement.prototype.closedShadow = function(style = null) {
1403
- return this.shadow('closed', style);
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;
1404
2052
  };
1405
2053
 
1406
2054
  /**
1407
- * Attaches a template binding to the element by hydrating it with the specified method.
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);
2055
+ * @param {*} data
1415
2056
  */
1416
- NDElement.prototype.attach = function(methodName, bindingHydrator) {
1417
- bindingHydrator.$hydrate(this.$element, methodName);
1418
- return this.$element;
2057
+ ObservableItem.prototype.$setWithInterceptor = function(data) {
2058
+ let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
2059
+ const result = this.$interceptor(newValue, this.$currentValue);
2060
+
2061
+ if (result !== undefined) {
2062
+ newValue = result;
2063
+ }
2064
+
2065
+ this.$updateWithNewValue(newValue);
1419
2066
  };
1420
2067
 
1421
2068
  /**
1422
- * Extends the current NDElement instance with custom methods.
1423
- * Methods are bound to the instance and available for chaining.
1424
- *
1425
- * @param {Object} methods - Object containing method definitions
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'));
2069
+ * @param {*} data
1434
2070
  */
1435
- NDElement.prototype.with = function(methods) {
1436
- if (!methods || typeof methods !== 'object') {
1437
- throw new NativeDocumentError('extend() requires an object of methods');
1438
- }
2071
+ ObservableItem.prototype.$basicSet = function(data) {
2072
+ let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
2073
+ this.$updateWithNewValue(newValue);
2074
+ };
1439
2075
 
1440
- for (const name in methods) {
1441
- const method = methods[name];
2076
+ ObservableItem.prototype.set = ObservableItem.prototype.$basicSet;
1442
2077
 
1443
- if (typeof method !== 'function') {
1444
- console.warn(`⚠️ extends(): "${name}" is not a function, skipping`);
1445
- continue;
1446
- }
2078
+ ObservableItem.prototype.val = function() {
2079
+ return this.$currentValue;
2080
+ };
1447
2081
 
1448
- this[name] = method.bind(this);
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
+ }
2091
+ }
1449
2092
  }
1450
-
1451
- return this;
2093
+ this.$watchers?.clear();
2094
+ this.$listeners = null;
2095
+ this.$watchers = null;
2096
+ this.trigger = noneTrigger;
1452
2097
  };
1453
2098
 
1454
2099
  /**
1455
- * Extends the NDElement prototype with new methods available to all NDElement instances.
1456
- * Use this to add global methods to all NDElements.
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.
1457
2102
  *
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
2103
+ * @param {Function} callback - Cleanup function to execute on observable disposal
1461
2104
  * @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();
2105
+ * const obs = Observable(0);
2106
+ * obs.onCleanup(() => console.log('Cleaned up!'));
2107
+ * obs.cleanup(); // Logs: "Cleaned up!"
1470
2108
  */
1471
- NDElement.extend = function(methods) {
1472
- if (!methods || typeof methods !== 'object') {
1473
- throw new NativeDocumentError('NDElement.extend() requires an object of methods');
1474
- }
1475
-
1476
- if (Array.isArray(methods)) {
1477
- throw new NativeDocumentError('NDElement.extend() requires an object, not an array');
1478
- }
1479
-
1480
- const protectedMethods = new Set([
1481
- 'constructor', 'valueOf', '$element', '$observer',
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
- }
1490
-
1491
- const method = methods[name];
1492
-
1493
- if (typeof method !== 'function') {
1494
- DebugManager$1.warn('NDElement.extend', `"${name}" is not a function, skipping`);
1495
- continue;
1496
- }
1497
-
1498
- if (protectedMethods.has(name)) {
1499
- DebugManager$1.error('NDElement.extend', `Cannot override protected method "${name}"`);
1500
- throw new NativeDocumentError(`Cannot override protected method "${name}"`);
1501
- }
2109
+ ObservableItem.prototype.onCleanup = function(callback) {
2110
+ this.$cleanupListeners = this.$cleanupListeners ?? [];
2111
+ this.$cleanupListeners.push(callback);
2112
+ };
1502
2113
 
1503
- if (NDElement.prototype[name]) {
1504
- DebugManager$1.warn('NDElement.extend', `Overwriting existing prototype method "${name}"`);
2114
+ ObservableItem.prototype.cleanup = function() {
2115
+ if (this.$cleanupListeners) {
2116
+ for (let i = 0; i < this.$cleanupListeners.length; i++) {
2117
+ this.$cleanupListeners[i]();
1505
2118
  }
1506
-
1507
- NDElement.prototype[name] = method;
2119
+ this.$cleanupListeners = null;
1508
2120
  }
1509
-
1510
- return NDElement;
2121
+ MemoryManager.unregister(this.$memoryId);
2122
+ this.disconnectAll();
2123
+ delete this.$value;
1511
2124
  };
1512
2125
 
1513
- const COMMON_NODE_TYPES = {
1514
- ELEMENT: 1,
1515
- TEXT: 3,
1516
- COMMENT: 8,
1517
- DOCUMENT_FRAGMENT: 11
1518
- };
2126
+ /**
2127
+ *
2128
+ * @param {Function} callback
2129
+ * @returns {(function(): void)}
2130
+ */
2131
+ ObservableItem.prototype.subscribe = function(callback) {
2132
+ this.$listeners = this.$listeners ?? [];
1519
2133
 
1520
- const Validator = {
1521
- isObservable(value) {
1522
- return value?.__$isObservable;
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];
1596
- }
2134
+ this.$listeners.push(callback);
2135
+ this.assocTrigger();
2136
+ };
1597
2137
 
1598
- const invalid = children.filter(child => !this.isValidChild(child));
1599
- return invalid.length === 0;
1600
- },
1601
- validateChildren(children) {
1602
- if (!Array.isArray(children)) {
1603
- children = [children];
1604
- }
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();
1605
2151
 
1606
- const invalid = children.filter(child => !this.isValidChild(child));
1607
- if (invalid.length > 0) {
1608
- throw new NativeDocumentError(`Invalid children detected: ${invalid.map(i => typeof i).join(', ')}`);
1609
- }
2152
+ let watchValueList = this.$watchers.get(value);
1610
2153
 
1611
- return children;
1612
- },
1613
- /**
1614
- * Check if the data contains observables.
1615
- * @param {Array|Object} data
1616
- * @returns {boolean}
1617
- */
1618
- containsObservables(data) {
1619
- if(!data) {
1620
- return false;
1621
- }
1622
- return Validator.isObject(data)
1623
- && Object.values(data).some(value => Validator.isObservable(value));
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) {},
2154
+ if(callback.__$isObservable) {
2155
+ callback = callback.set.bind(callback);
2156
+ }
1637
2157
 
1638
- validateEventCallback(callback) {
1639
- if (typeof callback !== 'function') {
1640
- throw new NativeDocumentError('Event callback must be a function');
1641
- }
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);
1642
2172
  }
1643
- };
1644
2173
 
1645
- function Anchor(name, isUniqueChild = false) {
1646
- const anchorFragment = document.createDocumentFragment();
1647
- anchorFragment.__Anchor__ = true;
2174
+ this.assocTrigger();
2175
+ };
1648
2176
 
1649
- const anchorStart = document.createComment('Anchor Start : '+name);
1650
- const anchorEnd = document.createComment('/ Anchor End '+name);
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;
1651
2191
 
1652
- anchorFragment.appendChild(anchorStart);
1653
- anchorFragment.appendChild(anchorEnd);
2192
+ const watchValueList = this.$watchers.get(value);
2193
+ if(!watchValueList) return;
1654
2194
 
1655
- anchorFragment.nativeInsertBefore = anchorFragment.insertBefore;
1656
- anchorFragment.nativeAppendChild = anchorFragment.appendChild;
1657
- anchorFragment.nativeAppend = anchorFragment.append;
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
+ };
1658
2210
 
1659
- const isParentUniqueChild = (parent) => (isUniqueChild || (parent.firstChild === anchorStart && parent.lastChild === anchorEnd));
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;
1660
2223
 
1661
- const insertBefore = function(parent, child, target) {
1662
- const childElement = Validator.isElement(child) ? child : ElementCreator.getChild(child);
1663
- if(parent === anchorFragment) {
1664
- parent.nativeInsertBefore(childElement, target);
1665
- return;
1666
- }
1667
- if(isParentUniqueChild(parent) && target === anchorEnd) {
1668
- parent.append(childElement, target);
1669
- return;
2224
+ const handler = (val) => {
2225
+ if (fn(val)) {
2226
+ this.unsubscribe(handler);
2227
+ callback(val);
1670
2228
  }
1671
- parent.insertBefore(childElement, target);
1672
2229
  };
2230
+ this.subscribe(handler);
2231
+ };
1673
2232
 
1674
- anchorFragment.appendElement = function(child, before = null) {
1675
- const parentNode = anchorStart.parentNode;
1676
- const targetBefore = before || anchorEnd;
1677
- if(parentNode === anchorFragment) {
1678
- parentNode.nativeInsertBefore(child, targetBefore);
1679
- return;
1680
- }
1681
- parentNode?.insertBefore(child, targetBefore);
1682
- };
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
+ };
1683
2245
 
1684
- anchorFragment.appendChild = function(child, before = null) {
1685
- const parent = anchorEnd.parentNode;
1686
- if(!parent) {
1687
- DebugManager$1.error('Anchor', 'Anchor : parent not found', child);
1688
- return;
1689
- }
1690
- before = before ?? anchorEnd;
1691
- insertBefore(parent, child, before);
1692
- };
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
+ };
1693
2254
 
1694
- anchorFragment.append = function(...args ) {
1695
- return anchorFragment.appendChild(args);
1696
- };
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;
1697
2259
 
1698
- anchorFragment.removeChildren = async function() {
1699
- const parent = anchorEnd.parentNode;
1700
- if(parent === anchorFragment) {
1701
- return;
1702
- }
1703
- // if(isParentUniqueChild(parent)) {
1704
- // parent.replaceChildren(anchorStart, anchorEnd);
1705
- // return;
1706
- // }
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
+ };
1707
2275
 
1708
- let itemToRemove = anchorStart.nextSibling, tempItem;
1709
- const removes = [];
1710
- while(itemToRemove && itemToRemove !== anchorEnd) {
1711
- tempItem = itemToRemove.nextSibling;
1712
- removes.push(itemToRemove.remove());
1713
- itemToRemove = tempItem;
1714
- }
1715
- await Promise.all(removes);
1716
- };
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
+ };
1717
2291
 
1718
- anchorFragment.remove = async function() {
1719
- const parent = anchorEnd.parentNode;
1720
- if(parent === anchorFragment) {
1721
- return;
1722
- }
1723
- let itemToRemove = anchorStart.nextSibling, tempItem;
1724
- const allItemToRemove = [];
1725
- const removes = [];
1726
- while(itemToRemove && itemToRemove !== anchorEnd) {
1727
- tempItem = itemToRemove.nextSibling;
1728
- allItemToRemove.push(itemToRemove);
1729
- removes.push(itemToRemove.remove());
1730
- itemToRemove = tempItem;
1731
- }
1732
- await Promise.all(removes);
1733
- anchorFragment.nativeAppend(...allItemToRemove);
1734
- };
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
+ };
2310
+
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
+ };
1735
2324
 
1736
- anchorFragment.removeWithAnchors = async function() {
1737
- await anchorFragment.removeChildren();
1738
- anchorStart.remove();
1739
- anchorEnd.remove();
1740
- };
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
+ };
1741
2336
 
1742
- anchorFragment.replaceContent = async function(child) {
1743
- const childElement = Validator.isElement(child) ? child : ElementCreator.getChild(child);
1744
- const parent = anchorEnd.parentNode;
1745
- if(!parent) {
1746
- return;
1747
- }
1748
- // if(isParentUniqueChild(parent)) {
1749
- // parent.replaceChildren(anchorStart, childElement, anchorEnd);
1750
- // return;
1751
- // }
1752
- await anchorFragment.removeChildren();
1753
- parent.insertBefore(childElement, anchorEnd);
1754
- };
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
+ };
1755
2357
 
1756
- anchorFragment.setContent = anchorFragment.replaceContent;
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
+ };
1757
2366
 
1758
- anchorFragment.insertBefore = function(child, anchor = null) {
1759
- anchorFragment.appendChild(child, anchor);
1760
- };
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
+ };
1761
2376
 
1762
2377
 
1763
- anchorFragment.endElement = function() {
1764
- return anchorEnd;
1765
- };
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;
1766
2431
 
1767
- anchorFragment.startElement = function() {
1768
- return anchorStart;
1769
- };
1770
- anchorFragment.restore = function() {
1771
- anchorFragment.appendChild(anchorFragment);
1772
- };
1773
- anchorFragment.clear = anchorFragment.remove;
1774
- anchorFragment.detach = anchorFragment.remove;
2432
+ if (typeof type === 'function') {
2433
+ return new ObservableChecker(self, type);
2434
+ }
1775
2435
 
1776
- anchorFragment.getByIndex = function(index) {
1777
- let currentNode = anchorStart;
1778
- for(let i = 0; i <= index; i++) {
1779
- if(!currentNode.nextSibling) {
1780
- return null;
1781
- }
1782
- currentNode = currentNode.nextSibling;
1783
- }
1784
- return currentNode !== anchorStart ? currentNode : null;
1785
- };
2436
+ const formatter = Formatters[type];
2437
+ const localeObservable = Store.follow('locale');
1786
2438
 
1787
- return anchorFragment;
1788
- }
1789
- DocumentFragment.prototype.setAttribute = () => {};
2439
+ return Observable$1.computed(() => formatter(self.val(), localeObservable.val(), options),
2440
+ [self, localeObservable]
2441
+ );
2442
+ };
1790
2443
 
1791
- const BOOLEAN_ATTRIBUTES = new Set([
1792
- 'checked',
1793
- 'selected',
1794
- 'disabled',
1795
- 'readonly',
1796
- 'required',
1797
- 'autofocus',
1798
- 'multiple',
1799
- 'autocomplete',
1800
- 'hidden',
1801
- 'contenteditable',
1802
- 'spellcheck',
1803
- 'translate',
1804
- 'draggable',
1805
- 'async',
1806
- 'defer',
1807
- 'autoplay',
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
- ]);
2444
+ ObservableItem.prototype.persist = function(key, options = {}) {
2445
+ let value = $getFromStorage$1(key, this.$currentValue);
2446
+ if(options.get) {
2447
+ value = options.get(value);
2448
+ }
2449
+ this.set(value);
2450
+ const saver = $saveToStorage$1(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
  *
@@ -1828,17 +2461,17 @@ var NativeComponents = (function (exports) {
1828
2461
  * @returns {ObservableItem}
1829
2462
  * @constructor
1830
2463
  */
1831
- function Observable(value, configs = null) {
2464
+ function Observable$1(value, configs = null) {
1832
2465
  return new ObservableItem(value, configs);
1833
2466
  }
1834
2467
 
1835
- const $$1 = Observable;
2468
+ const $$1 = Observable$1;
1836
2469
 
1837
2470
  /**
1838
2471
  *
1839
2472
  * @param {string} propertyName
1840
2473
  */
1841
- Observable.useValueProperty = function(propertyName = 'value') {
2474
+ Observable$1.useValueProperty = function(propertyName = 'value') {
1842
2475
  Object.defineProperty(ObservableItem.prototype, propertyName, {
1843
2476
  get() {
1844
2477
  return this.$currentValue;
@@ -1856,7 +2489,7 @@ var NativeComponents = (function (exports) {
1856
2489
  * @param id
1857
2490
  * @returns {ObservableItem|null}
1858
2491
  */
1859
- Observable.getById = function(id) {
2492
+ Observable$1.getById = function(id) {
1860
2493
  const item = MemoryManager.getObservableById(parseInt(id));
1861
2494
  if(!item) {
1862
2495
  throw new NativeDocumentError('Observable.getById : No observable found with id ' + id);
@@ -1868,7 +2501,7 @@ var NativeComponents = (function (exports) {
1868
2501
  *
1869
2502
  * @param {ObservableItem} observable
1870
2503
  */
1871
- Observable.cleanup = function(observable) {
2504
+ Observable$1.cleanup = function(observable) {
1872
2505
  observable.cleanup();
1873
2506
  };
1874
2507
 
@@ -1877,7 +2510,7 @@ var NativeComponents = (function (exports) {
1877
2510
  * @param {Boolean} enable
1878
2511
  * @param {{interval:Boolean, threshold:number}} options
1879
2512
  */
1880
- Observable.autoCleanup = function(enable = false, options = {}) {
2513
+ Observable$1.autoCleanup = function(enable = false, options = {}) {
1881
2514
  if(!enable) {
1882
2515
  return;
1883
2516
  }
@@ -2635,7 +3268,7 @@ var NativeComponents = (function (exports) {
2635
3268
  String.prototype.use = function(args) {
2636
3269
  const value = this;
2637
3270
 
2638
- return Observable.computed(() => {
3271
+ return Observable$1.computed(() => {
2639
3272
  return value.replace(/\$\{(.*?)}/g, (match, key) => {
2640
3273
  const data = args[key];
2641
3274
  if(Validator.isObservable(data)) {
@@ -2655,7 +3288,7 @@ var NativeComponents = (function (exports) {
2655
3288
  return value;
2656
3289
  }
2657
3290
  const [_, id] = value.match(/\{\{#ObItem::\(([0-9]+)\)\}\}/);
2658
- return Observable.getById(id);
3291
+ return Observable$1.getById(id);
2659
3292
  });
2660
3293
  };
2661
3294
 
@@ -2928,7 +3561,7 @@ var NativeComponents = (function (exports) {
2928
3561
  }
2929
3562
  }
2930
3563
 
2931
- const viewArray = Observable.array();
3564
+ const viewArray = Observable$1.array();
2932
3565
 
2933
3566
  const filters = Object.entries(filterCallbacks);
2934
3567
  const updateView = () => {
@@ -3014,7 +3647,7 @@ var NativeComponents = (function (exports) {
3014
3647
  * items.push(4); // Triggers update
3015
3648
  * items.subscribe((arr) => console.log(arr));
3016
3649
  */
3017
- Observable.array = function(target = [], configs = null) {
3650
+ Observable$1.array = function(target = [], configs = null) {
3018
3651
  return new ObservableArray(target, configs);
3019
3652
  };
3020
3653
 
@@ -3023,8 +3656,8 @@ var NativeComponents = (function (exports) {
3023
3656
  * @param {Function} callback
3024
3657
  * @returns {Function}
3025
3658
  */
3026
- Observable.batch = function(callback) {
3027
- const $observer = Observable(0);
3659
+ Observable$1.batch = function(callback) {
3660
+ const $observer = Observable$1(0);
3028
3661
  const batch = function() {
3029
3662
  if(Validator.isAsyncFunction(callback)) {
3030
3663
  return (callback(...arguments)).then(() => {
@@ -3038,10 +3671,71 @@ var NativeComponents = (function (exports) {
3038
3671
  return batch;
3039
3672
  };
3040
3673
 
3041
- const ObservableObjectValue = function(data) {
3674
+ const ObservableObject = function(target, configs) {
3675
+ ObservableItem.call(this, target);
3676
+ this.$observables = {};
3677
+ this.configs = configs;
3678
+
3679
+ this.$load(target);
3680
+
3681
+ for(const name in target) {
3682
+ if(!Object.hasOwn(this, name)) {
3683
+ Object.defineProperty(this, name, {
3684
+ get: () => this.$observables[name],
3685
+ set: (value) => this.$observables[name].set(value)
3686
+ });
3687
+ }
3688
+ }
3689
+
3690
+ };
3691
+
3692
+ ObservableObject.prototype = Object.create(ObservableItem.prototype);
3693
+
3694
+ Object.defineProperty(ObservableObject, '$value', {
3695
+ get() {
3696
+ return this.val();
3697
+ },
3698
+ set(value) {
3699
+ this.set(value);
3700
+ }
3701
+ });
3702
+
3703
+ ObservableObject.prototype.__$isObservableObject = true;
3704
+ ObservableObject.prototype.__isProxy__ = true;
3705
+
3706
+ ObservableObject.prototype.$load = function(initialValue) {
3707
+ const configs = this.configs;
3708
+ for(const key in initialValue) {
3709
+ const itemValue = initialValue[key];
3710
+ if(Array.isArray(itemValue)) {
3711
+ if(configs?.deep !== false) {
3712
+ const mappedItemValue = itemValue.map(item => {
3713
+ if(Validator.isJson(item)) {
3714
+ return Observable$1.json(item, configs);
3715
+ }
3716
+ if(Validator.isArray(item)) {
3717
+ return Observable$1.array(item, configs);
3718
+ }
3719
+ return Observable$1(item, configs);
3720
+ });
3721
+ this.$observables[key] = Observable$1.array(mappedItemValue, configs);
3722
+ continue;
3723
+ }
3724
+ this.$observables[key] = Observable$1.array(itemValue, configs);
3725
+ continue;
3726
+ }
3727
+ if(Validator.isObservable(itemValue) || Validator.isProxy(itemValue)) {
3728
+ this.$observables[key] = itemValue;
3729
+ continue;
3730
+ }
3731
+ this.$observables[key] = Observable$1(itemValue, configs);
3732
+ }
3733
+ };
3734
+
3735
+ ObservableObject.prototype.val = function() {
3042
3736
  const result = {};
3043
- for(const key in data) {
3044
- const dataItem = data[key];
3737
+ for(const key in this.$observables) {
3738
+ const dataItem = this.$observables[key];
3045
3739
  if(Validator.isObservable(dataItem)) {
3046
3740
  let value = dataItem.val();
3047
3741
  if(Array.isArray(value)) {
@@ -3064,9 +3758,10 @@ var NativeComponents = (function (exports) {
3064
3758
  }
3065
3759
  return result;
3066
3760
  };
3761
+ ObservableObject.prototype.$val = ObservableObject.prototype.val;
3067
3762
 
3068
- const ObservableGet = function(target, property) {
3069
- const item = target[property];
3763
+ ObservableObject.prototype.get = function(property) {
3764
+ const item = this.$observables[property];
3070
3765
  if(Validator.isObservable(item)) {
3071
3766
  return item.val();
3072
3767
  }
@@ -3075,100 +3770,87 @@ var NativeComponents = (function (exports) {
3075
3770
  }
3076
3771
  return item;
3077
3772
  };
3773
+ ObservableObject.prototype.$get = ObservableObject.prototype.get;
3078
3774
 
3079
- /**
3080
- * Creates an observable proxy for an object where each property becomes an observable.
3081
- * Properties can be accessed directly or via getter methods.
3082
- *
3083
- * @param {Object} initialValue - Initial object value
3084
- * @param {Object|null} [configs=null] - Configuration options
3085
- * // @param {boolean} [configs.propagation=true] - Whether changes propagate to parent
3086
- * @param {boolean} [configs.deep=false] - Whether to make nested objects observable
3087
- * @param {boolean} [configs.reset=false] - Whether to enable reset() method
3088
- * @returns {ObservableProxy} A proxy where each property is an observable
3089
- * @example
3090
- * const user = Observable.init({
3091
- * name: 'John',
3092
- * age: 25,
3093
- * address: { city: 'NYC' }
3094
- * }, { deep: true });
3095
- *
3096
- * user.name.val(); // 'John'
3097
- * user.name.set('Jane');
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);
3775
+ ObservableObject.prototype.set = function(newData) {
3776
+ const data = Validator.isProxy(newData) ? newData.$value : newData;
3777
+ const configs = this.configs;
3778
+
3779
+ for(const key in data) {
3780
+ const targetItem = this.$observables[key];
3781
+ const newValueOrigin = newData[key];
3782
+ const newValue = data[key];
3783
+
3784
+ if(Validator.isObservable(targetItem)) {
3785
+ if(!Validator.isArray(newValue)) {
3786
+ targetItem.set(newValue);
3787
+ continue;
3788
+ }
3789
+ const firstElementFromOriginalValue = newValueOrigin.at(0);
3790
+ if(Validator.isObservable(firstElementFromOriginalValue) || Validator.isProxy(firstElementFromOriginalValue)) {
3791
+ const newValues = newValue.map(item => {
3792
+ if(Validator.isProxy(firstElementFromOriginalValue)) {
3793
+ return Observable$1.init(item, configs);
3113
3794
  }
3114
- return Observable(item, configs);
3795
+ return Observable$1(item, configs);
3115
3796
  });
3116
- data[key] = Observable.array(mappedItemValue, configs);
3797
+ targetItem.set(newValues);
3117
3798
  continue;
3118
3799
  }
3119
- data[key] = Observable.array(itemValue, configs);
3800
+ targetItem.set([...newValue]);
3120
3801
  continue;
3121
3802
  }
3122
- if(Validator.isObservable(itemValue) || Validator.isProxy(itemValue)) {
3123
- data[key] = itemValue;
3803
+ if(Validator.isProxy(targetItem)) {
3804
+ targetItem.update(newValue);
3124
3805
  continue;
3125
3806
  }
3126
- data[key] = Observable(itemValue, configs);
3807
+ this[key] = newValue;
3127
3808
  }
3809
+ };
3810
+ ObservableObject.prototype.$set = ObservableObject.prototype.set;
3811
+ ObservableObject.prototype.$updateWith = ObservableObject.prototype.set;
3128
3812
 
3129
- const $reset = () => {
3130
- for(const key in data) {
3131
- const item = data[key];
3132
- item.reset();
3133
- }
3134
- };
3813
+ ObservableObject.prototype.observables = function() {
3814
+ return Object.values(this.$observables);
3815
+ };
3816
+ ObservableObject.prototype.$observables = ObservableObject.prototype.observables;
3135
3817
 
3136
- const $val = () => ObservableObjectValue(data);
3818
+ ObservableObject.prototype.keys = function() {
3819
+ return Object.keys(this.$observables);
3820
+ };
3821
+ ObservableObject.prototype.$keys = ObservableObject.prototype.keys;
3822
+ ObservableObject.prototype.clone = function() {
3823
+ return Observable$1.init(this.val(), this.configs);
3824
+ };
3825
+ ObservableObject.prototype.$clone = ObservableObject.prototype.clone;
3826
+ ObservableObject.prototype.reset = function() {
3827
+ for(const key in this.$observables) {
3828
+ this.$observables[key].reset();
3829
+ }
3830
+ };
3831
+ ObservableObject.prototype.originalSubscribe = ObservableObject.prototype.subscribe;
3832
+ ObservableObject.prototype.subscribe = function(callback) {
3833
+ const observables = this.observables();
3834
+ const updatedValue = nextTick(() => {
3835
+ this.$currentValue = this.val();
3836
+ this.trigger();
3837
+ });
3137
3838
 
3138
- const $clone = () => Observable.init($val(), configs);
3839
+ this.originalSubscribe(callback);
3139
3840
 
3140
- const $updateWith = (values) => {
3141
- Observable.update(proxy, values);
3142
- };
3841
+ for(let i = 0, length = observables.length; i < length; i++) {
3842
+ const observable = observables[i];
3843
+ observable.subscribe(updatedValue);
3844
+ }
3845
+ };
3846
+ ObservableObject.prototype.configs = function() {
3847
+ return this.configs;
3848
+ };
3143
3849
 
3144
- const $get = (key) => ObservableGet(data, key);
3145
-
3146
- const proxy = new Proxy(data, {
3147
- get(target, property) {
3148
- if(property === '__isProxy__') { return true; }
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;
3168
- }
3169
- });
3850
+ ObservableObject.prototype.update = ObservableObject.prototype.set;
3170
3851
 
3171
- return proxy;
3852
+ Observable$1.init = function(initialValue, configs = null) {
3853
+ return new ObservableObject(initialValue, configs)
3172
3854
  };
3173
3855
 
3174
3856
  /**
@@ -3176,8 +3858,8 @@ var NativeComponents = (function (exports) {
3176
3858
  * @param {any[]} data
3177
3859
  * @return Proxy[]
3178
3860
  */
3179
- Observable.arrayOfObject = function(data) {
3180
- return data.map(item => Observable.object(item));
3861
+ Observable$1.arrayOfObject = function(data) {
3862
+ return data.map(item => Observable$1.object(item));
3181
3863
  };
3182
3864
 
3183
3865
  /**
@@ -3185,7 +3867,7 @@ var NativeComponents = (function (exports) {
3185
3867
  * @param {ObservableItem|Object<ObservableItem>} data
3186
3868
  * @returns {{}|*|null}
3187
3869
  */
3188
- Observable.value = function(data) {
3870
+ Observable$1.value = function(data) {
3189
3871
  if(Validator.isObservable(data)) {
3190
3872
  return data.val();
3191
3873
  }
@@ -3196,52 +3878,15 @@ var NativeComponents = (function (exports) {
3196
3878
  const result = [];
3197
3879
  for(let i = 0, length = data.length; i < length; i++) {
3198
3880
  const item = data[i];
3199
- result.push(Observable.value(item));
3881
+ result.push(Observable$1.value(item));
3200
3882
  }
3201
3883
  return result;
3202
3884
  }
3203
3885
  return data;
3204
3886
  };
3205
3887
 
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
- Observable.object = Observable.init;
3244
- Observable.json = Observable.init;
3888
+ Observable$1.object = Observable$1.init;
3889
+ Observable$1.json = Observable$1.init;
3245
3890
 
3246
3891
  /**
3247
3892
  * Creates a computed observable that automatically updates when its dependencies change.
@@ -3262,7 +3907,7 @@ var NativeComponents = (function (exports) {
3262
3907
  * const batch = Observable.batch(() => { ... });
3263
3908
  * const computed = Observable.computed(() => { ... }, batch);
3264
3909
  */
3265
- Observable.computed = function(callback, dependencies = []) {
3910
+ Observable$1.computed = function(callback, dependencies = []) {
3266
3911
  const initialValue = callback();
3267
3912
  const observable = new ObservableItem(initialValue);
3268
3913
  const updatedValue = nextTick(() => observable.set(callback()));
@@ -6931,10 +7576,10 @@ var NativeComponents = (function (exports) {
6931
7576
  this.$element = null;
6932
7577
  this.$configs = configs;
6933
7578
  this.$fields = new Map();
6934
- this.$submitting = Observable(false);
6935
- this.$errors = Observable(null);
6936
- this.$isDirty = Observable(false);
6937
- this.$isValid = Observable(false);
7579
+ this.$submitting = Observable$1(false);
7580
+ this.$errors = Observable$1(null);
7581
+ this.$isDirty = Observable$1(false);
7582
+ this.$isValid = Observable$1(false);
6938
7583
  }
6939
7584
 
6940
7585
  FormControl.defaultLayoutTemplate = null;
@@ -8255,7 +8900,7 @@ var NativeComponents = (function (exports) {
8255
8900
  defaultItem: null,
8256
8901
  value: (config?.data && Validator.isObservable(config.data))
8257
8902
  ? config.data
8258
- : Observable.array(config?.data || []),
8903
+ : Observable$1.array(config?.data || []),
8259
8904
  rules: null,
8260
8905
  layout: null,
8261
8906
  template: null,
@@ -8352,8 +8997,8 @@ var NativeComponents = (function (exports) {
8352
8997
  defaultItemData = Validator.isObservable(defaultItemData)
8353
8998
  ? defaultItemData
8354
8999
  : Validator.isObject(defaultItemData)
8355
- ? Observable.init(defaultItemData)
8356
- : Observable(defaultItemData);
9000
+ ? Observable$1.init(defaultItemData)
9001
+ : Observable$1(defaultItemData);
8357
9002
 
8358
9003
  this.$items = this.$items || new WeakMap();
8359
9004
 
@@ -10835,9 +11480,9 @@ var NativeComponents = (function (exports) {
10835
11480
  ...configs
10836
11481
  });
10837
11482
 
10838
- this.$currentPage = Observable(1);
10839
- this.$selectedRows = Observable.array();
10840
- this.$expandedRows = Observable.array();
11483
+ this.$currentPage = Observable$1(1);
11484
+ this.$selectedRows = Observable$1.array();
11485
+ this.$expandedRows = Observable$1.array();
10841
11486
  }
10842
11487
 
10843
11488
  DataTable.defaultToolbarTemplate = null;