native-document 1.0.95 → 1.0.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) 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/{src/devtools/hrm → devtools/transformers/templates}/hrm.orbservable.hook.template.js +8 -0
  9. package/devtools/widget/Widget.js +48 -0
  10. package/devtools/widget/widget.css +81 -0
  11. package/devtools/widget.js +23 -0
  12. package/dist/native-document.components.min.js +1953 -1245
  13. package/dist/native-document.dev.js +2022 -1375
  14. package/dist/native-document.dev.js.map +1 -1
  15. package/dist/native-document.devtools.min.js +1 -1
  16. package/dist/native-document.min.js +1 -1
  17. package/docs/cache.md +1 -1
  18. package/docs/core-concepts.md +1 -1
  19. package/docs/native-document-element.md +51 -15
  20. package/docs/observables.md +333 -315
  21. package/docs/state-management.md +198 -193
  22. package/package.json +1 -1
  23. package/readme.md +1 -1
  24. package/rollup.config.js +1 -1
  25. package/src/core/data/ObservableArray.js +67 -0
  26. package/src/core/data/ObservableChecker.js +2 -0
  27. package/src/core/data/ObservableItem.js +97 -0
  28. package/src/core/data/ObservableObject.js +183 -0
  29. package/src/core/data/Store.js +364 -34
  30. package/src/core/data/observable-helpers/object.js +2 -166
  31. package/src/core/utils/formatters.js +91 -0
  32. package/src/core/utils/localstorage.js +57 -0
  33. package/src/core/utils/validator.js +0 -2
  34. package/src/fetch/NativeFetch.js +5 -2
  35. package/types/observable.d.ts +73 -15
  36. package/types/plugins-manager.d.ts +1 -1
  37. package/types/store.d.ts +33 -6
  38. package/hrm.js +0 -7
  39. package/src/devtools/app/App.js +0 -66
  40. package/src/devtools/app/app.css +0 -0
  41. package/src/devtools/hrm/transformComponent.js +0 -129
  42. package/src/devtools/index.js +0 -18
  43. package/src/devtools/widget/DevToolsWidget.js +0 -26
  44. /package/{src/devtools/hrm → devtools/transformers/templates}/hrm.hook.template.js +0 -0
@@ -363,69 +363,16 @@ var NativeComponents = (function (exports) {
363
363
  // });
364
364
  };
365
365
 
366
- let DebugManager = {};
366
+ let DebugManager$1 = {};
367
367
  {
368
- DebugManager = {
368
+ DebugManager$1 = {
369
369
  log() {},
370
370
  warn() {},
371
371
  error() {},
372
372
  disable() {}
373
373
  };
374
374
  }
375
- var DebugManager$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;
1049
- };
1286
+ ObservableWhen.prototype.isMatch = ObservableWhen.prototype.val;
1050
1287
 
1051
1288
  /**
1052
- * Toggles the boolean value of the observable (false becomes true, true becomes false).
1289
+ * Returns true if the observable's current value equals the target value.
1290
+ * Alias for val().
1053
1291
  *
1054
- * @example
1055
- * const isOpen = Observable(false);
1056
- * isOpen.toggle(); // Now true
1057
- * isOpen.toggle(); // Now false
1292
+ * @returns {boolean} True if observable value matches target value
1058
1293
  */
1059
- ObservableItem.prototype.toggle = function() {
1060
- this.set(!this.$currentValue);
1294
+ ObservableWhen.prototype.isActive = ObservableWhen.prototype.val;
1295
+
1296
+ const invoke = function(fn, args, context) {
1297
+ if(context) {
1298
+ fn.apply(context, args);
1299
+ } else {
1300
+ fn(...args);
1301
+ }
1302
+ };
1303
+ /**
1304
+ *
1305
+ * @param {Function} fn
1306
+ * @param {number} delay
1307
+ * @param {{leading?:Boolean, trailing?:Boolean, debounce?:Boolean, check: Function}}options
1308
+ * @returns {(function(...[*]): void)|*}
1309
+ */
1310
+ const debounce = function(fn, delay, options = {}) {
1311
+ let timer = null;
1312
+ let lastArgs = null;
1313
+
1314
+ return function(...args) {
1315
+ const context = options.context === true ? this : null;
1316
+ if(options.check) {
1317
+ options.check(...args);
1318
+ }
1319
+ lastArgs = args;
1320
+
1321
+ // debounce mode: reset the timer for each call
1322
+ clearTimeout(timer);
1323
+ timer = setTimeout(() => invoke(fn, lastArgs, context), delay);
1324
+ }
1325
+ };
1326
+
1327
+ const nextTick = function(fn) {
1328
+ let pending = false;
1329
+ return function(...args) {
1330
+ if (pending) return;
1331
+ pending = true;
1332
+
1333
+ Promise.resolve().then(() => {
1334
+ fn.apply(this, args);
1335
+ pending = false;
1336
+ });
1337
+ };
1338
+ };
1339
+
1340
+ const deepClone = (value, onObservableFound) => {
1341
+ try {
1342
+ if(window.structuredClone !== undefined) {
1343
+ return window.structuredClone(value);
1344
+ }
1345
+ } catch (e){}
1346
+
1347
+ if (value === null || typeof value !== 'object') {
1348
+ return value;
1349
+ }
1350
+
1351
+ // Dates
1352
+ if (value instanceof Date) {
1353
+ return new Date(value.getTime());
1354
+ }
1355
+
1356
+ // Arrays
1357
+ if (Array.isArray(value)) {
1358
+ return value.map(item => deepClone(item));
1359
+ }
1360
+
1361
+ // Observables - keep the référence
1362
+ if (Validator.isObservable(value)) {
1363
+ onObservableFound && onObservableFound(value);
1364
+ return value;
1365
+ }
1366
+
1367
+ // Objects
1368
+ const cloned = {};
1369
+ for (const key in value) {
1370
+ if (Object.hasOwn(value, key)) {
1371
+ cloned[key] = deepClone(value[key]);
1372
+ }
1373
+ }
1374
+ return cloned;
1375
+ };
1376
+
1377
+ const LocalStorage = {
1378
+ getJson(key) {
1379
+ let value = localStorage.getItem(key);
1380
+ try {
1381
+ return JSON.parse(value);
1382
+ } catch (e) {
1383
+ throw new NativeDocumentError('invalid_json:'+key);
1384
+ }
1385
+ },
1386
+ getNumber(key) {
1387
+ return Number(this.get(key));
1388
+ },
1389
+ getBool(key) {
1390
+ const value = this.get(key);
1391
+ return value === 'true' || value === '1';
1392
+ },
1393
+ setJson(key, value) {
1394
+ localStorage.setItem(key, JSON.stringify(value));
1395
+ },
1396
+ setBool(key, value) {
1397
+ localStorage.setItem(key, value ? 'true' : 'false');
1398
+ },
1399
+ get(key, defaultValue = null) {
1400
+ return localStorage.getItem(key) || defaultValue;
1401
+ },
1402
+ set(key, value) {
1403
+ return localStorage.setItem(key, value);
1404
+ },
1405
+ remove(key) {
1406
+ localStorage.removeItem(key);
1407
+ },
1408
+ has(key) {
1409
+ return localStorage.getItem(key) != null;
1410
+ }
1061
1411
  };
1062
1412
 
1063
- /**
1064
- * 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;
1413
+ const $getFromStorage = (key, value) => {
1414
+ if(!LocalStorage.has(key)) {
1415
+ return value;
1416
+ }
1417
+ switch (typeof value) {
1418
+ case 'object': return LocalStorage.getJson(key) ?? value;
1419
+ case 'boolean': return LocalStorage.getBool(key) ?? value;
1420
+ case 'number': return LocalStorage.getNumber(key) ?? value;
1421
+ default: return LocalStorage.get(key, value) ?? value;
1075
1422
  }
1076
- const resetValue = (Validator.isObject(this.$initialValue))
1077
- ? deepClone(this.$initialValue, (observable) => {
1078
- observable.reset();
1079
- })
1080
- : this.$initialValue;
1081
- this.set(resetValue);
1082
1423
  };
1083
1424
 
1084
- /**
1085
- * 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);
1425
+ const $saveToStorage = (value) => {
1426
+ switch (typeof value) {
1427
+ case 'object': return LocalStorage.setJson;
1428
+ case 'boolean': return LocalStorage.setBool;
1429
+ default: return LocalStorage.set;
1430
+ }
1091
1431
  };
1092
1432
 
1093
- /**
1094
- * Returns the primitive value of the observable (its current value).
1095
- * Called automatically in type coercion contexts.
1096
- *
1097
- * @returns {*} The current value of the observable
1098
- */
1099
- ObservableItem.prototype.valueOf = function() {
1100
- return this.$currentValue;
1101
- };
1433
+ const StoreFactory = function() {
1102
1434
 
1103
- const DocumentObserver = {
1104
- mounted: new WeakMap(),
1105
- beforeUnmount: new WeakMap(),
1106
- mountedSupposedSize: 0,
1107
- unmounted: new WeakMap(),
1108
- unmountedSupposedSize: 0,
1109
- observer: null,
1435
+ const $stores = new Map();
1436
+ const $followersCache = new Map();
1110
1437
 
1111
- 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;
1438
+ /**
1439
+ * Internal helper — retrieves a store entry or throws if not found.
1440
+ */
1441
+ const $getStoreOrThrow = (method, name) => {
1442
+ const item = $stores.get(name);
1443
+ if (!item) {
1444
+ DebugManager.error('Store', `Store.${method}('${name}') : store not found. Did you call Store.create('${name}') first?`);
1445
+ throw new NativeDocumentError(
1446
+ `Store.${method}('${name}') : store not found.`
1447
+ );
1125
1448
  }
1126
- data.mounted(node);
1127
- },
1449
+ return item;
1450
+ };
1128
1451
 
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;
1137
- }
1452
+ /**
1453
+ * Internal helper — blocks write operations on a read-only observer.
1454
+ */
1455
+ const $applyReadOnly = (observer, name, context) => {
1456
+ const readOnlyError = (method) => () => {
1457
+ DebugManager.error('Store', `Store.${context}('${name}') is read-only. '${method}()' is not allowed.`);
1458
+ throw new NativeDocumentError(
1459
+ `Store.${context}('${name}') is read-only.`
1460
+ );
1461
+ };
1462
+ observer.set = readOnlyError('set');
1463
+ observer.toggle = readOnlyError('toggle');
1464
+ observer.reset = readOnlyError('reset');
1465
+ };
1138
1466
 
1139
- 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;
1467
+ const $createObservable = (value, options = {}) => {
1468
+ if(Array.isArray(value)) {
1469
+ return Observable.array(value, options);
1148
1470
  }
1149
-
1150
- if(shouldRemove) {
1151
- data.disconnect();
1152
- node.nd?.remove();
1471
+ if(typeof value === 'object') {
1472
+ return Observable.object(value, options);
1153
1473
  }
1154
- },
1474
+ return Observable(value, options);
1475
+ };
1155
1476
 
1156
- 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.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;
1227
- }
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
- }
1239
- 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
+ );
1240
1633
  }
1241
- data[type] = null;
1242
- };
1243
1634
 
1244
- return {
1245
- disconnect: () => data?.disconnect(),
1635
+ const { observer: originalObserver, subscribers } = item;
1636
+ const observerFollower = $createObservable(originalObserver.val());
1246
1637
 
1247
- mounted: (callback) => {
1248
- addListener('mounted', callback);
1249
- DocumentObserver.mounted.set(element, data);
1250
- if (!mountedRegistered) {
1251
- DocumentObserver.mountedSupposedSize++;
1252
- mountedRegistered = true;
1253
- }
1254
- },
1638
+ const onStoreChange = value => observerFollower.set(value);
1639
+ const onFollowerChange = value => originalObserver.set(value);
1255
1640
 
1256
- unmounted: (callback) => {
1257
- addListener('unmounted', callback);
1258
- DocumentObserver.unmounted.set(element, data);
1259
- if (!unmountedRegistered) {
1260
- DocumentObserver.unmountedSupposedSize++;
1261
- unmountedRegistered = true;
1262
- }
1263
- },
1641
+ originalObserver.subscribe(onStoreChange);
1642
+ observerFollower.subscribe(onFollowerChange);
1264
1643
 
1265
- off: (type, callback) => {
1266
- removeListener(type, callback);
1644
+ observerFollower.destroy = () => {
1645
+ originalObserver.unsubscribe(onStoreChange);
1646
+ observerFollower.unsubscribe(onFollowerChange);
1647
+ subscribers.delete(observerFollower);
1648
+ observerFollower.cleanup();
1649
+ };
1650
+ observerFollower.dispose = observerFollower.destroy;
1651
+
1652
+ subscribers.add(observerFollower);
1653
+ return observerFollower;
1654
+ },
1655
+
1656
+ /**
1657
+ * Returns a read-only follower of the store.
1658
+ * The follower reflects store changes but cannot write back to the store.
1659
+ * Any attempt to call .set(), .toggle() or .reset() will throw.
1660
+ * Call follower.destroy() or follower.dispose() to unsubscribe.
1661
+ *
1662
+ * @param {string} name
1663
+ * @returns {ObservableItem}
1664
+ */
1665
+ follow(name) {
1666
+ const { observer: originalObserver, subscribers } = $getStoreOrThrow('follow', name);
1667
+ const observerFollower = $createObservable(originalObserver.val());
1668
+
1669
+ const onStoreChange = value => observerFollower.set(value);
1670
+ originalObserver.subscribe(onStoreChange);
1671
+
1672
+ $applyReadOnly(observerFollower, name, 'follow');
1673
+
1674
+ observerFollower.destroy = () => {
1675
+ originalObserver.unsubscribe(onStoreChange);
1676
+ subscribers.delete(observerFollower);
1677
+ observerFollower.cleanup();
1678
+ };
1679
+ observerFollower.dispose = observerFollower.destroy;
1680
+
1681
+ subscribers.add(observerFollower);
1682
+ return observerFollower;
1683
+ },
1684
+
1685
+ /**
1686
+ * Returns the raw store observer directly (no follower, no cleanup contract).
1687
+ * Use this for direct read access when you don't need to unsubscribe.
1688
+ * WARNING : mutations on this observer impact all subscribers immediately.
1689
+ *
1690
+ * @param {string} name
1691
+ * @returns {ObservableItem|null}
1692
+ */
1693
+ get(name) {
1694
+ const item = $stores.get(name);
1695
+ if (!item) {
1696
+ DebugManager.warn('Store', `Store.get('${name}') : store not found.`);
1697
+ return null;
1698
+ }
1699
+ return item.observer;
1700
+ },
1701
+
1702
+ /**
1703
+ * @param {string} name
1704
+ * @returns {{ observer: ObservableItem, subscribers: Set } | null}
1705
+ */
1706
+ getWithSubscribers(name) {
1707
+ return $stores.get(name) ?? null;
1708
+ },
1709
+
1710
+ /**
1711
+ * Destroys a store : cleans up the observer, destroys all followers, and removes the entry.
1712
+ *
1713
+ * @param {string} name
1714
+ */
1715
+ delete(name) {
1716
+ const item = $stores.get(name);
1717
+ if (!item) {
1718
+ DebugManager.warn('Store', `Store.delete('${name}') : store not found, nothing to delete.`);
1719
+ return;
1720
+ }
1721
+ item.subscribers.forEach(follower => follower.destroy());
1722
+ item.subscribers.clear();
1723
+ item.observer.cleanup();
1724
+ $stores.delete(name);
1725
+ },
1726
+ /**
1727
+ * Creates an isolated store group with its own state namespace.
1728
+ * Each group is a fully independent StoreFactory instance —
1729
+ * no key conflicts, no shared state with the parent store.
1730
+ *
1731
+ * @param {string | ((group: ReturnType<typeof StoreFactory>) => void)} name - Group name for debugging, or setup callback if no name is provided
1732
+ * @param {((group: ReturnType<typeof StoreFactory>) => void)} [callback] - Setup function receiving the isolated store instance
1733
+ * @returns {ReturnType<typeof StoreFactory>}
1734
+ *
1735
+ * @example
1736
+ * // With name (recommended)
1737
+ * const EventStore = Store.group('events', (group) => {
1738
+ * group.create('catalog', []);
1739
+ * group.create('filters', { category: null, date: null });
1740
+ * group.createResettable('selected', null);
1741
+ * group.createComposed('filtered', () => {
1742
+ * const catalog = EventStore.get('catalog').val();
1743
+ * const filters = EventStore.get('filters').val();
1744
+ * return catalog.filter(event => {
1745
+ * if (filters.category && event.category !== filters.category) return false;
1746
+ * return true;
1747
+ * });
1748
+ * }, ['catalog', 'filters']);
1749
+ * });
1750
+ *
1751
+ * // Without name
1752
+ * const CartStore = Store.group((group) => {
1753
+ * group.create('items', []);
1754
+ * });
1755
+ *
1756
+ * // Usage
1757
+ * EventStore.use('catalog'); // two-way follower
1758
+ * EventStore.follow('filtered'); // read-only follower
1759
+ * EventStore.get('filters'); // raw observable
1760
+ *
1761
+ * // Cross-group composed
1762
+ * const OrderStore = Store.group('orders', (group) => {
1763
+ * group.createComposed('summary', () => {
1764
+ * const items = CartStore.get('items').val();
1765
+ * const events = EventStore.get('catalog').val();
1766
+ * return { items, events };
1767
+ * }, [CartStore.get('items'), EventStore.get('catalog')]);
1768
+ * });
1769
+ */
1770
+ group(name, callback) {
1771
+ if (typeof name === 'function') {
1772
+ callback = name;
1773
+ name = 'anonymous';
1267
1774
  }
1268
- };
1269
- }
1270
- };
1775
+ const store = StoreFactory();
1776
+ callback && callback(store);
1777
+ return store;
1778
+ },
1779
+ createPersistent(name, value, localstorage_key) {
1780
+ localstorage_key = localstorage_key || name;
1781
+ const observer = this.create(name, $getFromStorage(localstorage_key, value));
1782
+ const saver = $saveToStorage(value);
1271
1783
 
1272
- DocumentObserver.observer = new MutationObserver(DocumentObserver.checkMutation);
1273
- DocumentObserver.observer.observe(document.body, {
1274
- childList: true,
1275
- subtree: true,
1276
- });
1784
+ observer.subscribe((val) => saver(localstorage_key, val));
1785
+ return observer;
1786
+ },
1787
+ createPersistentResettable(name, value, localstorage_key) {
1788
+ localstorage_key = localstorage_key || name;
1789
+ const observer = this.createResettable(name, $getFromStorage(localstorage_key, value));
1790
+ const saver = $saveToStorage(value);
1791
+ observer.subscribe((val) => saver(localstorage_key, val));
1792
+
1793
+ const originalReset = observer.reset.bind(observer);
1794
+ observer.reset = () => {
1795
+ LocalStorage.remove(localstorage_key);
1796
+ originalReset();
1797
+ };
1277
1798
 
1278
- function NDElement(element) {
1279
- this.$element = element;
1280
- this.$observer = null;
1281
- }
1799
+ return observer;
1800
+ }
1801
+ };
1282
1802
 
1283
- NDElement.prototype.__$isNDElement = true;
1284
1803
 
1285
- NDElement.prototype.valueOf = function() {
1286
- return this.$element;
1804
+ return new Proxy($api, {
1805
+ get(target, prop) {
1806
+ if (typeof prop === 'symbol' || prop.startsWith('$') || prop in target) {
1807
+ return target[prop];
1808
+ }
1809
+ if (target.has(prop)) {
1810
+ if ($followersCache.has(prop)) {
1811
+ return $followersCache.get(prop);
1812
+ }
1813
+ const follower = target.follow(prop);
1814
+ $followersCache.set(prop, follower);
1815
+ return follower;
1816
+ }
1817
+ return undefined;
1818
+ },
1819
+ set(target, prop, value) {
1820
+ DebugManager.error('Store', `Forbidden: You cannot overwrite the store key '${String(prop)}'. Use .use('${String(prop)}').set(value) instead.`);
1821
+ throw new NativeDocumentError(`Store structure is immutable. Use .set() on the observable.`);
1822
+ },
1823
+ deleteProperty(target, prop) {
1824
+ throw new NativeDocumentError(`Store keys cannot be deleted.`);
1825
+ }
1826
+ });
1287
1827
  };
1288
1828
 
1289
- NDElement.prototype.ref = function(target, name) {
1290
- target[name] = this.$element;
1291
- return this;
1292
- };
1829
+ const Store = StoreFactory();
1293
1830
 
1294
- NDElement.prototype.refSelf = function(target, name) {
1295
- target[name] = this;
1296
- return this;
1297
- };
1831
+ Store.create('locale', 'fr');
1298
1832
 
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();
1305
- }
1306
- elementChildren = null;
1307
- }
1308
- element = null;
1309
- 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
+ };
1310
1849
  };
1311
1850
 
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
- };
1851
+ const $applyDatePattern = (pattern, d, parts) => {
1852
+ const pad = n => String(n).padStart(2, '0');
1853
+ return pattern
1854
+ .replace('YYYY', parts.year)
1855
+ .replace('YY', parts.year.slice(-2))
1856
+ .replace('MMMM', parts.month)
1857
+ .replace('MMM', parts.month.slice(0, 3))
1858
+ .replace('MM', pad(d.getMonth() + 1))
1859
+ .replace('DD', pad(d.getDate()))
1860
+ .replace('D', d.getDate())
1861
+ .replace('HH', parts.hour)
1862
+ .replace('mm', parts.minute)
1863
+ .replace('ss', parts.second);
1864
+ };
1865
+
1866
+ const Formatters = {
1867
+
1868
+ currency: (value, locale, { currency = 'XOF', notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
1869
+ new Intl.NumberFormat(locale, {
1870
+ style: 'currency',
1871
+ currency,
1872
+ notation,
1873
+ minimumFractionDigits,
1874
+ maximumFractionDigits
1875
+ }).format(value),
1876
+
1877
+ number: (value, locale, { notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
1878
+ new Intl.NumberFormat(locale, {
1879
+ notation,
1880
+ minimumFractionDigits,
1881
+ maximumFractionDigits
1882
+ }).format(value),
1883
+
1884
+ percent: (value, locale, { decimals = 1 } = {}) =>
1885
+ new Intl.NumberFormat(locale, {
1886
+ style: 'percent',
1887
+ maximumFractionDigits: decimals
1888
+ }).format(value),
1889
+
1890
+ date: (value, locale, { format, dateStyle = 'long' } = {}) => {
1891
+ if (format) {
1892
+ const { d, parts } = $parseDateParts(value, locale);
1893
+ return $applyDatePattern(format, d, parts);
1894
+ }
1895
+ return new Intl.DateTimeFormat(locale, { dateStyle }).format(new Date(value));
1896
+ },
1322
1897
 
1323
- NDElement.prototype.lifecycle = function(states) {
1324
- this.$observer = this.$observer || DocumentObserver.watch(this.$element);
1898
+ time: (value, locale, { format, hour = '2-digit', minute = '2-digit', second } = {}) => {
1899
+ if (format) {
1900
+ const { d, parts } = $parseDateParts(value, locale);
1901
+ return $applyDatePattern(format, d, parts);
1902
+ }
1903
+ return new Intl.DateTimeFormat(locale, { hour, minute, second }).format(new Date(value));
1904
+ },
1325
1905
 
1326
- 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;
1335
- };
1906
+ datetime: (value, locale, { format, dateStyle = 'long', hour = '2-digit', minute = '2-digit', second } = {}) => {
1907
+ if (format) {
1908
+ const { d, parts } = $parseDateParts(value, locale);
1909
+ return $applyDatePattern(format, d, parts);
1910
+ }
1911
+ return new Intl.DateTimeFormat(locale, { dateStyle, hour, minute, second }).format(new Date(value));
1912
+ },
1336
1913
 
1337
- NDElement.prototype.mounted = function(callback) {
1338
- return this.lifecycle({ mounted: callback });
1339
- };
1914
+ relative: (value, locale, { unit = 'day', numeric = 'auto' } = {}) => {
1915
+ const diff = Math.round((value - Date.now()) / (1000 * 60 * 60 * 24));
1916
+ return new Intl.RelativeTimeFormat(locale, { numeric }).format(diff, unit);
1917
+ },
1340
1918
 
1341
- NDElement.prototype.unmounted = function(callback) {
1342
- return this.lifecycle({ unmounted: callback });
1919
+ plural: (value, locale, { singular, plural } = {}) => {
1920
+ const rule = new Intl.PluralRules(locale).select(value);
1921
+ return `${value} ${rule === 'one' ? singular : plural}`;
1922
+ },
1343
1923
  };
1344
1924
 
1345
- NDElement.prototype.beforeUnmount = function(id, callback) {
1346
- const el = this.$element;
1925
+ /**
1926
+ *
1927
+ * @param {*} value
1928
+ * @param {{ propagation: boolean, reset: boolean} | null} configs
1929
+ * @class ObservableItem
1930
+ */
1931
+ function ObservableItem(value, configs = null) {
1932
+ value = Validator.isObservable(value) ? value.val() : value;
1347
1933
 
1348
- if(!DocumentObserver.beforeUnmount.has(el)) {
1349
- DocumentObserver.beforeUnmount.set(el, new Map());
1350
- const originalRemove = el.remove.bind(el);
1934
+ this.$previousValue = null;
1935
+ this.$currentValue = value;
1351
1936
 
1352
- let $isUnmounting = false;
1937
+ this.$firstListener = null;
1938
+ this.$listeners = null;
1939
+ this.$watchers = null;
1353
1940
 
1354
- el.remove = async () => {
1355
- if($isUnmounting) {
1356
- return;
1357
- }
1358
- $isUnmounting = true;
1941
+ this.$memoryId = null;
1359
1942
 
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
- };
1943
+ if(configs) {
1944
+ this.configs = configs;
1945
+ if(configs.reset) {
1946
+ this.$initialValue = Validator.isObject(value) ? deepClone(value) : value;
1947
+ }
1370
1948
  }
1949
+ }
1371
1950
 
1372
- DocumentObserver.beforeUnmount.get(el).set(id, callback);
1951
+ Object.defineProperty(ObservableItem.prototype, '$value', {
1952
+ get() {
1953
+ return this.$currentValue;
1954
+ },
1955
+ set(value) {
1956
+ this.set(value);
1957
+ },
1958
+ configurable: true,
1959
+ });
1960
+
1961
+ ObservableItem.prototype.__$isObservable = true;
1962
+ const noneTrigger = function() {};
1963
+
1964
+ /**
1965
+ * Intercepts and transforms values before they are set on the observable.
1966
+ * The interceptor can modify the value or return undefined to use the original value.
1967
+ *
1968
+ * @param {(value) => any} callback - Interceptor function that receives (newValue, currentValue) and returns the transformed value or undefined
1969
+ * @returns {ObservableItem} The observable instance for chaining
1970
+ * @example
1971
+ * const count = Observable(0);
1972
+ * count.intercept((newVal, oldVal) => Math.max(0, newVal)); // Prevent negative values
1973
+ */
1974
+ ObservableItem.prototype.intercept = function(callback) {
1975
+ this.$interceptor = callback;
1976
+ this.set = this.$setWithInterceptor;
1373
1977
  return this;
1374
1978
  };
1375
1979
 
1376
- 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);
1394
-
1395
- return this;
1396
1992
  };
1397
1993
 
1398
- NDElement.prototype.openShadow = function(style = null) {
1399
- return this.shadow('open', style);
1994
+ ObservableItem.prototype.triggerWatchers = function(operations) {
1995
+ const $watchers = this.$watchers;
1996
+ const $previousValue = this.$previousValue;
1997
+ const $currentValue = this.$currentValue;
1998
+
1999
+ const $currentValueCallbacks = $watchers.get($currentValue);
2000
+ const $previousValueCallbacks = $watchers.get($previousValue);
2001
+ if($currentValueCallbacks) {
2002
+ $currentValueCallbacks(true, $previousValue, operations);
2003
+ }
2004
+ if($previousValueCallbacks) {
2005
+ $previousValueCallbacks(false, $currentValue, operations);
2006
+ }
1400
2007
  };
1401
2008
 
1402
- NDElement.prototype.closedShadow = function(style = null) {
1403
- return this.shadow('closed', style);
2009
+ ObservableItem.prototype.triggerAll = function(operations) {
2010
+ this.triggerWatchers(operations);
2011
+ this.triggerListeners(operations);
1404
2012
  };
1405
2013
 
1406
- /**
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);
1415
- */
1416
- NDElement.prototype.attach = function(methodName, bindingHydrator) {
1417
- bindingHydrator.$hydrate(this.$element, methodName);
1418
- return this.$element;
2014
+ ObservableItem.prototype.triggerWatchersAndFirstListener = function(operations) {
2015
+ this.triggerWatchers(operations);
2016
+ this.triggerFirstListener(operations);
1419
2017
  };
1420
2018
 
1421
- /**
1422
- * 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'));
1434
- */
1435
- NDElement.prototype.with = function(methods) {
1436
- if (!methods || typeof methods !== 'object') {
1437
- throw new NativeDocumentError('extend() requires an object of methods');
2019
+ ObservableItem.prototype.assocTrigger = function() {
2020
+ this.$firstListener = null;
2021
+ if(this.$watchers?.size && this.$listeners?.length) {
2022
+ this.trigger = (this.$listeners.length === 1) ? this.triggerWatchersAndFirstListener : this.triggerAll;
2023
+ return;
1438
2024
  }
1439
-
1440
- for (const name in methods) {
1441
- const method = methods[name];
1442
-
1443
- if (typeof method !== 'function') {
1444
- console.warn(`⚠️ extends(): "${name}" is not a function, skipping`);
1445
- continue;
2025
+ if(this.$listeners?.length) {
2026
+ if(this.$listeners.length === 1) {
2027
+ this.$firstListener = this.$listeners[0];
2028
+ this.trigger = this.triggerFirstListener;
1446
2029
  }
1447
-
1448
- this[name] = method.bind(this);
2030
+ else {
2031
+ this.trigger = this.triggerListeners;
2032
+ }
2033
+ return;
2034
+ }
2035
+ if(this.$watchers?.size) {
2036
+ this.trigger = this.triggerWatchers;
2037
+ return;
1449
2038
  }
2039
+ this.trigger = noneTrigger;
2040
+ };
2041
+ ObservableItem.prototype.trigger = noneTrigger;
1450
2042
 
1451
- return this;
2043
+ ObservableItem.prototype.$updateWithNewValue = function(newValue) {
2044
+ newValue = newValue?.__$isObservable ? newValue.val() : newValue;
2045
+ if(this.$currentValue === newValue) {
2046
+ return;
2047
+ }
2048
+ this.$previousValue = this.$currentValue;
2049
+ this.$currentValue = newValue;
2050
+ this.trigger();
2051
+ this.$previousValue = null;
1452
2052
  };
1453
2053
 
1454
2054
  /**
1455
- * Extends the NDElement prototype with new methods available to all NDElement instances.
1456
- * Use this to add global methods to all NDElements.
1457
- *
1458
- * @param {Object} methods - Object containing method definitions to add to prototype
1459
- * @returns {typeof NDElement} The NDElement constructor
1460
- * @throws {NativeDocumentError} If methods is not an object or contains non-function values
1461
- * @example
1462
- * NDElement.extend({
1463
- * fadeIn() {
1464
- * this.$element.style.opacity = '1';
1465
- * return this;
1466
- * }
1467
- * });
1468
- * // Now all NDElements have .fadeIn() method
1469
- * Div().nd.fadeIn();
2055
+ * @param {*} data
1470
2056
  */
1471
- NDElement.extend = function(methods) {
1472
- if (!methods || typeof methods !== 'object') {
1473
- throw new NativeDocumentError('NDElement.extend() requires an object of methods');
1474
- }
2057
+ ObservableItem.prototype.$setWithInterceptor = function(data) {
2058
+ let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
2059
+ const result = this.$interceptor(newValue, this.$currentValue);
1475
2060
 
1476
- if (Array.isArray(methods)) {
1477
- throw new NativeDocumentError('NDElement.extend() requires an object, not an array');
2061
+ if (result !== undefined) {
2062
+ newValue = result;
1478
2063
  }
1479
2064
 
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
- }
2065
+ this.$updateWithNewValue(newValue);
2066
+ };
1490
2067
 
1491
- const method = methods[name];
2068
+ /**
2069
+ * @param {*} data
2070
+ */
2071
+ ObservableItem.prototype.$basicSet = function(data) {
2072
+ let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
2073
+ this.$updateWithNewValue(newValue);
2074
+ };
1492
2075
 
1493
- if (typeof method !== 'function') {
1494
- DebugManager$1.warn('NDElement.extend', `"${name}" is not a function, skipping`);
1495
- continue;
1496
- }
2076
+ ObservableItem.prototype.set = ObservableItem.prototype.$basicSet;
1497
2077
 
1498
- 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
- }
2078
+ ObservableItem.prototype.val = function() {
2079
+ return this.$currentValue;
2080
+ };
1502
2081
 
1503
- if (NDElement.prototype[name]) {
1504
- DebugManager$1.warn('NDElement.extend', `Overwriting existing prototype method "${name}"`);
2082
+ ObservableItem.prototype.disconnectAll = function() {
2083
+ this.$listeners?.splice(0);
2084
+ this.$previousValue = null;
2085
+ this.$currentValue = null;
2086
+ if(this.$watchers) {
2087
+ for (const [_, watchValueList] of this.$watchers) {
2088
+ if(Validator.isArray(watchValueList)) {
2089
+ watchValueList.splice(0);
2090
+ }
1505
2091
  }
1506
-
1507
- NDElement.prototype[name] = method;
1508
2092
  }
1509
-
1510
- return NDElement;
2093
+ this.$watchers?.clear();
2094
+ this.$listeners = null;
2095
+ this.$watchers = null;
2096
+ this.trigger = noneTrigger;
1511
2097
  };
1512
2098
 
1513
- const COMMON_NODE_TYPES = {
1514
- ELEMENT: 1,
1515
- TEXT: 3,
1516
- COMMENT: 8,
1517
- DOCUMENT_FRAGMENT: 11
2099
+ /**
2100
+ * Registers a cleanup callback that will be executed when the observable is cleaned up.
2101
+ * Useful for disposing resources, removing event listeners, or other cleanup tasks.
2102
+ *
2103
+ * @param {Function} callback - Cleanup function to execute on observable disposal
2104
+ * @example
2105
+ * const obs = Observable(0);
2106
+ * obs.onCleanup(() => console.log('Cleaned up!'));
2107
+ * obs.cleanup(); // Logs: "Cleaned up!"
2108
+ */
2109
+ ObservableItem.prototype.onCleanup = function(callback) {
2110
+ this.$cleanupListeners = this.$cleanupListeners ?? [];
2111
+ this.$cleanupListeners.push(callback);
1518
2112
  };
1519
2113
 
1520
- 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];
2114
+ ObservableItem.prototype.cleanup = function() {
2115
+ if (this.$cleanupListeners) {
2116
+ for (let i = 0; i < this.$cleanupListeners.length; i++) {
2117
+ this.$cleanupListeners[i]();
1596
2118
  }
2119
+ this.$cleanupListeners = null;
2120
+ }
2121
+ MemoryManager.unregister(this.$memoryId);
2122
+ this.disconnectAll();
2123
+ delete this.$value;
2124
+ };
1597
2125
 
1598
- 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
- }
2126
+ /**
2127
+ *
2128
+ * @param {Function} callback
2129
+ * @returns {(function(): void)}
2130
+ */
2131
+ ObservableItem.prototype.subscribe = function(callback) {
2132
+ this.$listeners = this.$listeners ?? [];
1605
2133
 
1606
- 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
- }
2134
+ this.$listeners.push(callback);
2135
+ this.assocTrigger();
2136
+ };
1610
2137
 
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) {},
2138
+ /**
2139
+ * Watches for a specific value and executes callback when the observable equals that value.
2140
+ * Creates a watcher that only triggers when the observable changes to the specified value.
2141
+ *
2142
+ * @param {*} value - The value to watch for
2143
+ * @param {(value) => void|ObservableItem} callback - Callback function or observable to set when value matches
2144
+ * @example
2145
+ * const status = Observable('idle');
2146
+ * status.on('loading', () => console.log('Started loading'));
2147
+ * status.on('error', isError); // Set another observable
2148
+ */
2149
+ ObservableItem.prototype.on = function(value, callback) {
2150
+ this.$watchers = this.$watchers ?? new Map();
1637
2151
 
1638
- validateEventCallback(callback) {
1639
- if (typeof callback !== 'function') {
1640
- throw new NativeDocumentError('Event callback must be a function');
1641
- }
2152
+ let watchValueList = this.$watchers.get(value);
2153
+
2154
+ if(callback.__$isObservable) {
2155
+ callback = callback.set.bind(callback);
1642
2156
  }
1643
- };
1644
2157
 
1645
- function Anchor(name, isUniqueChild = false) {
1646
- const anchorFragment = document.createDocumentFragment();
1647
- anchorFragment.__Anchor__ = true;
2158
+ if(!watchValueList) {
2159
+ watchValueList = callback;
2160
+ this.$watchers.set(value, callback);
2161
+ } else if(!Validator.isArray(watchValueList.list)) {
2162
+ watchValueList = [watchValueList, callback];
2163
+ callback = (value) => {
2164
+ for(let i = 0, length = watchValueList.length; i < length; i++) {
2165
+ watchValueList[i](value);
2166
+ }
2167
+ };
2168
+ callback.list = watchValueList;
2169
+ this.$watchers.set(value, callback);
2170
+ } else {
2171
+ watchValueList.list.push(callback);
2172
+ }
1648
2173
 
1649
- const anchorStart = document.createComment('Anchor Start : '+name);
1650
- const anchorEnd = document.createComment('/ Anchor End '+name);
2174
+ this.assocTrigger();
2175
+ };
1651
2176
 
1652
- anchorFragment.appendChild(anchorStart);
1653
- anchorFragment.appendChild(anchorEnd);
2177
+ /**
2178
+ * Removes a watcher for a specific value. If no callback is provided, removes all watchers for that value.
2179
+ *
2180
+ * @param {*} value - The value to stop watching
2181
+ * @param {Function} [callback] - Specific callback to remove. If omitted, removes all watchers for this value
2182
+ * @example
2183
+ * const status = Observable('idle');
2184
+ * const handler = () => console.log('Loading');
2185
+ * status.on('loading', handler);
2186
+ * status.off('loading', handler); // Remove specific handler
2187
+ * status.off('loading'); // Remove all handlers for 'loading'
2188
+ */
2189
+ ObservableItem.prototype.off = function(value, callback) {
2190
+ if(!this.$watchers) return;
1654
2191
 
1655
- anchorFragment.nativeInsertBefore = anchorFragment.insertBefore;
1656
- anchorFragment.nativeAppendChild = anchorFragment.appendChild;
1657
- anchorFragment.nativeAppend = anchorFragment.append;
2192
+ const watchValueList = this.$watchers.get(value);
2193
+ if(!watchValueList) return;
1658
2194
 
1659
- const isParentUniqueChild = (parent) => (isUniqueChild || (parent.firstChild === anchorStart && parent.lastChild === anchorEnd));
2195
+ if(!callback || !Array.isArray(watchValueList.list)) {
2196
+ this.$watchers?.delete(value);
2197
+ this.assocTrigger();
2198
+ return;
2199
+ }
2200
+ const index = watchValueList.indexOf(callback);
2201
+ watchValueList?.splice(index, 1);
2202
+ if(watchValueList.length === 1) {
2203
+ this.$watchers.set(value, watchValueList[0]);
2204
+ }
2205
+ else if(watchValueList.length === 0) {
2206
+ this.$watchers?.delete(value);
2207
+ }
2208
+ this.assocTrigger();
2209
+ };
1660
2210
 
1661
- 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;
1670
- }
1671
- parent.insertBefore(childElement, target);
1672
- };
2211
+ /**
2212
+ * Subscribes to the observable but automatically unsubscribes after the first time the predicate matches.
2213
+ *
2214
+ * @param {(value) => Boolean|any} predicate - Value to match or function that returns true when condition is met
2215
+ * @param {(value) => void} callback - Callback to execute when predicate matches, receives the matched value
2216
+ * @example
2217
+ * const status = Observable('loading');
2218
+ * status.once('ready', (val) => console.log('Ready!'));
2219
+ * status.once(val => val === 'error', (val) => console.log('Error occurred'));
2220
+ */
2221
+ ObservableItem.prototype.once = function(predicate, callback) {
2222
+ const fn = typeof predicate === 'function' ? predicate : (v) => v === predicate;
1673
2223
 
1674
- 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;
2224
+ const handler = (val) => {
2225
+ if (fn(val)) {
2226
+ this.unsubscribe(handler);
2227
+ callback(val);
1680
2228
  }
1681
- parentNode?.insertBefore(child, targetBefore);
1682
2229
  };
2230
+ this.subscribe(handler);
2231
+ };
1683
2232
 
1684
- 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
- };
2233
+ /**
2234
+ * Unsubscribe from an observable.
2235
+ * @param {Function} callback
2236
+ */
2237
+ ObservableItem.prototype.unsubscribe = function(callback) {
2238
+ if(!this.$listeners) return;
2239
+ const index = this.$listeners.indexOf(callback);
2240
+ if (index > -1) {
2241
+ this.$listeners.splice(index, 1);
2242
+ }
2243
+ this.assocTrigger();
2244
+ };
1693
2245
 
1694
- anchorFragment.append = function(...args ) {
1695
- return anchorFragment.appendChild(args);
1696
- };
2246
+ /**
2247
+ * Create an Observable checker instance
2248
+ * @param callback
2249
+ * @returns {ObservableChecker}
2250
+ */
2251
+ ObservableItem.prototype.check = function(callback) {
2252
+ return new ObservableChecker(this, callback)
2253
+ };
1697
2254
 
1698
- 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
- // }
2255
+ ObservableItem.prototype.transform = ObservableItem.prototype.check;
2256
+ ObservableItem.prototype.pluck = ObservableItem.prototype.check;
2257
+ ObservableItem.prototype.is = ObservableItem.prototype.check;
2258
+ ObservableItem.prototype.select = ObservableItem.prototype.check;
1707
2259
 
1708
- 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
- };
2260
+ /**
2261
+ * Gets a property value from the observable's current value.
2262
+ * If the property is an observable, returns its value.
2263
+ *
2264
+ * @param {string|number} key - Property key to retrieve
2265
+ * @returns {*} The value of the property, unwrapped if it's an observable
2266
+ * @example
2267
+ * const user = Observable({ name: 'John', age: Observable(25) });
2268
+ * user.get('name'); // 'John'
2269
+ * user.get('age'); // 25 (unwrapped from observable)
2270
+ */
2271
+ ObservableItem.prototype.get = function(key) {
2272
+ const item = this.$currentValue[key];
2273
+ return Validator.isObservable(item) ? item.val() : item;
2274
+ };
1717
2275
 
1718
- 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
- };
2276
+ /**
2277
+ * Creates an ObservableWhen that represents whether the observable equals a specific value.
2278
+ * Returns an object that can be subscribed to and will emit true/false.
2279
+ *
2280
+ * @param {*} value - The value to compare against
2281
+ * @returns {ObservableWhen} An ObservableWhen instance that tracks when the observable equals the value
2282
+ * @example
2283
+ * const status = Observable('idle');
2284
+ * const isLoading = status.when('loading');
2285
+ * isLoading.subscribe(active => console.log('Loading:', active));
2286
+ * status.set('loading'); // Logs: "Loading: true"
2287
+ */
2288
+ ObservableItem.prototype.when = function(value) {
2289
+ return new ObservableWhen(this, value);
2290
+ };
1735
2291
 
1736
- anchorFragment.removeWithAnchors = async function() {
1737
- await anchorFragment.removeChildren();
1738
- anchorStart.remove();
1739
- anchorEnd.remove();
1740
- };
2292
+ /**
2293
+ * Compares the observable's current value with another value or observable.
2294
+ *
2295
+ * @param {*|ObservableItem} other - Value or observable to compare against
2296
+ * @returns {boolean} True if values are equal
2297
+ * @example
2298
+ * const a = Observable(5);
2299
+ * const b = Observable(5);
2300
+ * a.equals(5); // true
2301
+ * a.equals(b); // true
2302
+ * a.equals(10); // false
2303
+ */
2304
+ ObservableItem.prototype.equals = function(other) {
2305
+ if(Validator.isObservable(other)) {
2306
+ return this.$currentValue === other.$currentValue;
2307
+ }
2308
+ return this.$currentValue === other;
2309
+ };
1741
2310
 
1742
- 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
- };
2311
+ /**
2312
+ * Converts the observable's current value to a boolean.
2313
+ *
2314
+ * @returns {boolean} The boolean representation of the current value
2315
+ * @example
2316
+ * const count = Observable(0);
2317
+ * count.toBool(); // false
2318
+ * count.set(5);
2319
+ * count.toBool(); // true
2320
+ */
2321
+ ObservableItem.prototype.toBool = function() {
2322
+ return !!this.$currentValue;
2323
+ };
2324
+
2325
+ /**
2326
+ * Toggles the boolean value of the observable (false becomes true, true becomes false).
2327
+ *
2328
+ * @example
2329
+ * const isOpen = Observable(false);
2330
+ * isOpen.toggle(); // Now true
2331
+ * isOpen.toggle(); // Now false
2332
+ */
2333
+ ObservableItem.prototype.toggle = function() {
2334
+ this.set(!this.$currentValue);
2335
+ };
1755
2336
 
1756
- anchorFragment.setContent = anchorFragment.replaceContent;
2337
+ /**
2338
+ * Resets the observable to its initial value.
2339
+ * Only works if the observable was created with { reset: true } config.
2340
+ *
2341
+ * @example
2342
+ * const count = Observable(0, { reset: true });
2343
+ * count.set(10);
2344
+ * count.reset(); // Back to 0
2345
+ */
2346
+ ObservableItem.prototype.reset = function() {
2347
+ if(!this.configs?.reset) {
2348
+ return;
2349
+ }
2350
+ const resetValue = (Validator.isObject(this.$initialValue))
2351
+ ? deepClone(this.$initialValue, (observable) => {
2352
+ observable.reset();
2353
+ })
2354
+ : this.$initialValue;
2355
+ this.set(resetValue);
2356
+ };
1757
2357
 
1758
- anchorFragment.insertBefore = function(child, anchor = null) {
1759
- anchorFragment.appendChild(child, anchor);
1760
- };
2358
+ /**
2359
+ * Returns a string representation of the observable's current value.
2360
+ *
2361
+ * @returns {string} String representation of the current value
2362
+ */
2363
+ ObservableItem.prototype.toString = function() {
2364
+ return String(this.$currentValue);
2365
+ };
1761
2366
 
2367
+ /**
2368
+ * Returns the primitive value of the observable (its current value).
2369
+ * Called automatically in type coercion contexts.
2370
+ *
2371
+ * @returns {*} The current value of the observable
2372
+ */
2373
+ ObservableItem.prototype.valueOf = function() {
2374
+ return this.$currentValue;
2375
+ };
1762
2376
 
1763
- anchorFragment.endElement = function() {
1764
- return anchorEnd;
1765
- };
1766
2377
 
1767
- 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;
2378
+ /**
2379
+ * Creates a derived observable that formats the current value using Intl.
2380
+ * Automatically reacts to both value changes and locale changes (Store.__nd.locale).
2381
+ *
2382
+ * @param {string | Function} type - Format type or custom formatter function
2383
+ * @param {Object} [options={}] - Options passed to the formatter
2384
+ * @returns {ObservableItem<string>}
2385
+ *
2386
+ * @example
2387
+ * // Currency
2388
+ * price.format('currency') // "15 000 FCFA"
2389
+ * price.format('currency', { currency: 'EUR' }) // "15 000,00 €"
2390
+ * price.format('currency', { notation: 'compact' }) // "15 K FCFA"
2391
+ *
2392
+ * // Number
2393
+ * count.format('number') // "15 000"
2394
+ *
2395
+ * // Percent
2396
+ * rate.format('percent') // "15,0 %"
2397
+ * rate.format('percent', { decimals: 2 }) // "15,00 %"
2398
+ *
2399
+ * // Date
2400
+ * date.format('date') // "3 mars 2026"
2401
+ * date.format('date', { dateStyle: 'full' }) // "mardi 3 mars 2026"
2402
+ * date.format('date', { format: 'DD/MM/YYYY' }) // "03/03/2026"
2403
+ * date.format('date', { format: 'DD MMM YYYY' }) // "03 mar 2026"
2404
+ * date.format('date', { format: 'DD MMMM YYYY' }) // "03 mars 2026"
2405
+ *
2406
+ * // Time
2407
+ * date.format('time') // "20:30"
2408
+ * date.format('time', { second: '2-digit' }) // "20:30:00"
2409
+ * date.format('time', { format: 'HH:mm:ss' }) // "20:30:00"
2410
+ *
2411
+ * // Datetime
2412
+ * date.format('datetime') // "3 mars 2026, 20:30"
2413
+ * date.format('datetime', { dateStyle: 'full' }) // "mardi 3 mars 2026, 20:30"
2414
+ * date.format('datetime', { format: 'DD/MM/YYYY HH:mm' }) // "03/03/2026 20:30"
2415
+ *
2416
+ * // Relative
2417
+ * date.format('relative') // "dans 11 jours"
2418
+ * date.format('relative', { unit: 'month' }) // "dans 1 mois"
2419
+ *
2420
+ * // Plural
2421
+ * count.format('plural', { singular: 'billet', plural: 'billets' }) // "3 billets"
2422
+ *
2423
+ * // Custom formatter
2424
+ * price.format(value => `${value.toLocaleString()} FCFA`)
2425
+ *
2426
+ * // Reacts to locale changes automatically
2427
+ * Store.setLocale('en-US');
2428
+ */
2429
+ ObservableItem.prototype.format = function(type, options = {}) {
2430
+ const self = this;
1775
2431
 
1776
- 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
- };
2432
+ if (typeof type === 'function') {
2433
+ return new ObservableChecker(self, type);
2434
+ }
1786
2435
 
1787
- return anchorFragment;
1788
- }
1789
- DocumentFragment.prototype.setAttribute = () => {};
2436
+ const formatter = Formatters[type];
2437
+ const localeObservable = Store.follow('locale');
1790
2438
 
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
- ]);
2439
+ return Observable.computed(() => formatter(self.val(), localeObservable.val(), options),
2440
+ [self, localeObservable]
2441
+ );
2442
+ };
2443
+
2444
+ ObservableItem.prototype.persist = function(key, options = {}) {
2445
+ let value = $getFromStorage(key, this.$currentValue);
2446
+ if(options.get) {
2447
+ value = options.get(value);
2448
+ }
2449
+ this.set(value);
2450
+ const saver = $saveToStorage(this.$currentValue);
2451
+ this.subscribe((newValue) => {
2452
+ saver(key, options.set ? options.set(newValue) : newValue);
2453
+ });
2454
+ return this;
2455
+ };
1823
2456
 
1824
2457
  /**
1825
2458
  *
@@ -2999,6 +3632,68 @@ var NativeComponents = (function (exports) {
2999
3632
  });
3000
3633
  };
3001
3634
 
3635
+ ObservableArray.prototype.deepSubscribe = function(callback) {
3636
+ const updatedValue = nextTick(() => callback(this.val()));
3637
+ const $listeners = new WeakMap();
3638
+
3639
+ const bindItem = (item) => {
3640
+ if ($listeners.has(item)) {
3641
+ return;
3642
+ }
3643
+ if (item?.__$isObservableArray) {
3644
+ $listeners.set(item, item.deepSubscribe(updatedValue));
3645
+ return;
3646
+ }
3647
+ if (item?.__$isObservable) {
3648
+ item.subscribe(updatedValue);
3649
+ $listeners.set(item, () => item.unsubscribe(updatedValue));
3650
+ }
3651
+ };
3652
+
3653
+ const unbindItem = (item) => {
3654
+ const unsub = $listeners.get(item);
3655
+ if (unsub) {
3656
+ unsub();
3657
+ $listeners.delete(item);
3658
+ }
3659
+ };
3660
+
3661
+ this.$currentValue.forEach(bindItem);
3662
+ this.subscribe(updatedValue);
3663
+
3664
+ this.subscribe((items, _, operations) => {
3665
+ switch (operations?.action) {
3666
+ case 'push':
3667
+ case 'unshift':
3668
+ operations.args.forEach(bindItem);
3669
+ break;
3670
+
3671
+ case 'splice': {
3672
+ const [start, deleteCount, ...newItems] = operations.args;
3673
+ operations.result?.forEach(unbindItem);
3674
+ newItems.forEach(bindItem);
3675
+ break;
3676
+ }
3677
+
3678
+ case 'remove':
3679
+ unbindItem(operations.result);
3680
+ break;
3681
+
3682
+ case 'merge':
3683
+ operations.args.forEach(bindItem);
3684
+ break;
3685
+
3686
+ case 'clear':
3687
+ this.$currentValue.forEach(unbindItem);
3688
+ break;
3689
+ }
3690
+ });
3691
+
3692
+ return () => {
3693
+ this.$currentValue.forEach(unbindItem);
3694
+ };
3695
+ };
3696
+
3002
3697
  /**
3003
3698
  * Creates an observable array with reactive array methods.
3004
3699
  * All mutations trigger updates automatically.
@@ -3038,10 +3733,71 @@ var NativeComponents = (function (exports) {
3038
3733
  return batch;
3039
3734
  };
3040
3735
 
3041
- const ObservableObjectValue = function(data) {
3736
+ const ObservableObject = function(target, configs) {
3737
+ ObservableItem.call(this, target);
3738
+ this.$observables = {};
3739
+ this.configs = configs;
3740
+
3741
+ this.$load(target);
3742
+
3743
+ for(const name in target) {
3744
+ if(!Object.hasOwn(this, name)) {
3745
+ Object.defineProperty(this, name, {
3746
+ get: () => this.$observables[name],
3747
+ set: (value) => this.$observables[name].set(value)
3748
+ });
3749
+ }
3750
+ }
3751
+
3752
+ };
3753
+
3754
+ ObservableObject.prototype = Object.create(ObservableItem.prototype);
3755
+
3756
+ Object.defineProperty(ObservableObject, '$value', {
3757
+ get() {
3758
+ return this.val();
3759
+ },
3760
+ set(value) {
3761
+ this.set(value);
3762
+ }
3763
+ });
3764
+
3765
+ ObservableObject.prototype.__$isObservableObject = true;
3766
+ ObservableObject.prototype.__isProxy__ = true;
3767
+
3768
+ ObservableObject.prototype.$load = function(initialValue) {
3769
+ const configs = this.configs;
3770
+ for(const key in initialValue) {
3771
+ const itemValue = initialValue[key];
3772
+ if(Array.isArray(itemValue)) {
3773
+ if(configs?.deep !== false) {
3774
+ const mappedItemValue = itemValue.map(item => {
3775
+ if(Validator.isJson(item)) {
3776
+ return Observable.json(item, configs);
3777
+ }
3778
+ if(Validator.isArray(item)) {
3779
+ return Observable.array(item, configs);
3780
+ }
3781
+ return Observable(item, configs);
3782
+ });
3783
+ this.$observables[key] = Observable.array(mappedItemValue, configs);
3784
+ continue;
3785
+ }
3786
+ this.$observables[key] = Observable.array(itemValue, configs);
3787
+ continue;
3788
+ }
3789
+ if(Validator.isObservable(itemValue) || Validator.isProxy(itemValue)) {
3790
+ this.$observables[key] = itemValue;
3791
+ continue;
3792
+ }
3793
+ this.$observables[key] = (typeof itemValue === 'object') ? Observable.object(itemValue, configs) : Observable(itemValue, configs);
3794
+ }
3795
+ };
3796
+
3797
+ ObservableObject.prototype.val = function() {
3042
3798
  const result = {};
3043
- for(const key in data) {
3044
- const dataItem = data[key];
3799
+ for(const key in this.$observables) {
3800
+ const dataItem = this.$observables[key];
3045
3801
  if(Validator.isObservable(dataItem)) {
3046
3802
  let value = dataItem.val();
3047
3803
  if(Array.isArray(value)) {
@@ -3064,9 +3820,10 @@ var NativeComponents = (function (exports) {
3064
3820
  }
3065
3821
  return result;
3066
3822
  };
3823
+ ObservableObject.prototype.$val = ObservableObject.prototype.val;
3067
3824
 
3068
- const ObservableGet = function(target, property) {
3069
- const item = target[property];
3825
+ ObservableObject.prototype.get = function(property) {
3826
+ const item = this.$observables[property];
3070
3827
  if(Validator.isObservable(item)) {
3071
3828
  return item.val();
3072
3829
  }
@@ -3075,100 +3832,88 @@ var NativeComponents = (function (exports) {
3075
3832
  }
3076
3833
  return item;
3077
3834
  };
3835
+ ObservableObject.prototype.$get = ObservableObject.prototype.get;
3078
3836
 
3079
- /**
3080
- * 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);
3837
+ ObservableObject.prototype.set = function(newData) {
3838
+ const data = Validator.isProxy(newData) ? newData.$value : newData;
3839
+ const configs = this.configs;
3840
+
3841
+ for(const key in data) {
3842
+ const targetItem = this.$observables[key];
3843
+ const newValueOrigin = newData[key];
3844
+ const newValue = data[key];
3845
+
3846
+ if(Validator.isObservable(targetItem)) {
3847
+ if(!Validator.isArray(newValue)) {
3848
+ targetItem.set(newValue);
3849
+ continue;
3850
+ }
3851
+ const firstElementFromOriginalValue = newValueOrigin.at(0);
3852
+ if(Validator.isObservable(firstElementFromOriginalValue) || Validator.isProxy(firstElementFromOriginalValue)) {
3853
+ const newValues = newValue.map(item => {
3854
+ if(Validator.isProxy(firstElementFromOriginalValue)) {
3855
+ return Observable.init(item, configs);
3113
3856
  }
3114
3857
  return Observable(item, configs);
3115
3858
  });
3116
- data[key] = Observable.array(mappedItemValue, configs);
3859
+ targetItem.set(newValues);
3117
3860
  continue;
3118
3861
  }
3119
- data[key] = Observable.array(itemValue, configs);
3862
+ targetItem.set([...newValue]);
3120
3863
  continue;
3121
3864
  }
3122
- if(Validator.isObservable(itemValue) || Validator.isProxy(itemValue)) {
3123
- data[key] = itemValue;
3865
+ if(Validator.isProxy(targetItem)) {
3866
+ targetItem.update(newValue);
3124
3867
  continue;
3125
3868
  }
3126
- data[key] = Observable(itemValue, configs);
3869
+ this[key] = newValue;
3127
3870
  }
3871
+ };
3872
+ ObservableObject.prototype.$set = ObservableObject.prototype.set;
3873
+ ObservableObject.prototype.$updateWith = ObservableObject.prototype.set;
3128
3874
 
3129
- const $reset = () => {
3130
- for(const key in data) {
3131
- const item = data[key];
3132
- item.reset();
3133
- }
3134
- };
3135
-
3136
- const $val = () => ObservableObjectValue(data);
3875
+ ObservableObject.prototype.observables = function() {
3876
+ return Object.values(this.$observables);
3877
+ };
3878
+ ObservableObject.prototype.$observables = ObservableObject.prototype.observables;
3137
3879
 
3138
- const $clone = () => Observable.init($val(), configs);
3880
+ ObservableObject.prototype.keys = function() {
3881
+ return Object.keys(this.$observables);
3882
+ };
3883
+ ObservableObject.prototype.$keys = ObservableObject.prototype.keys;
3884
+ ObservableObject.prototype.clone = function() {
3885
+ return Observable.init(this.val(), this.configs);
3886
+ };
3887
+ ObservableObject.prototype.$clone = ObservableObject.prototype.clone;
3888
+ ObservableObject.prototype.reset = function() {
3889
+ for(const key in this.$observables) {
3890
+ this.$observables[key].reset();
3891
+ }
3892
+ };
3893
+ ObservableObject.prototype.originalSubscribe = ObservableObject.prototype.subscribe;
3894
+ ObservableObject.prototype.subscribe = function(callback) {
3895
+ const observables = this.observables();
3896
+ const updatedValue = nextTick(() => this.trigger());
3139
3897
 
3140
- const $updateWith = (values) => {
3141
- Observable.update(proxy, values);
3142
- };
3898
+ this.originalSubscribe(callback);
3143
3899
 
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;
3900
+ for (let i = 0, length = observables.length; i < length; i++) {
3901
+ const observable = observables[i];
3902
+ if (observable.__$isObservableArray) {
3903
+ observable.deepSubscribe(updatedValue);
3904
+ continue
3168
3905
  }
3169
- });
3906
+ observable.subscribe(updatedValue);
3907
+ }
3908
+ };
3909
+ ObservableObject.prototype.configs = function() {
3910
+ return this.configs;
3911
+ };
3912
+
3913
+ ObservableObject.prototype.update = ObservableObject.prototype.set;
3170
3914
 
3171
- return proxy;
3915
+ Observable.init = function(initialValue, configs = null) {
3916
+ return new ObservableObject(initialValue, configs)
3172
3917
  };
3173
3918
 
3174
3919
  /**
@@ -3203,43 +3948,6 @@ var NativeComponents = (function (exports) {
3203
3948
  return data;
3204
3949
  };
3205
3950
 
3206
-
3207
- Observable.update = function($target, newData) {
3208
- const data = Validator.isProxy(newData) ? newData.$value : newData;
3209
- const configs = $target.configs;
3210
-
3211
- for(const key in data) {
3212
- const targetItem = $target[key];
3213
- const newValueOrigin = newData[key];
3214
- const newValue = data[key];
3215
-
3216
- if(Validator.isObservable(targetItem)) {
3217
- if(Validator.isArray(newValue)) {
3218
- const firstElementFromOriginalValue = newValueOrigin.at(0);
3219
- if(Validator.isObservable(firstElementFromOriginalValue) || Validator.isProxy(firstElementFromOriginalValue)) {
3220
- const newValues = newValue.map(item => {
3221
- if(Validator.isProxy(firstElementFromOriginalValue)) {
3222
- return Observable.init(item, configs);
3223
- }
3224
- return Observable(item, configs);
3225
- });
3226
- targetItem.set(newValues);
3227
- continue;
3228
- }
3229
- targetItem.set([...newValue]);
3230
- continue;
3231
- }
3232
- targetItem.set(newValue);
3233
- continue;
3234
- }
3235
- if(Validator.isProxy(targetItem)) {
3236
- Observable.update(targetItem, newValue);
3237
- continue;
3238
- }
3239
- $target[key] = newValue;
3240
- }
3241
- };
3242
-
3243
3951
  Observable.object = Observable.init;
3244
3952
  Observable.json = Observable.init;
3245
3953