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
@@ -37,59 +37,6 @@ var NativeDocument = (function (exports) {
37
37
  }
38
38
  var DebugManager = DebugManager$1;
39
39
 
40
- const MemoryManager = (function() {
41
-
42
- let $nextObserverId = 0;
43
- const $observables = new Map();
44
-
45
- return {
46
- /**
47
- * Register an observable and return an id.
48
- *
49
- * @param {ObservableItem} observable
50
- * @param {Function} getListeners
51
- * @returns {number}
52
- */
53
- register(observable) {
54
- const id = ++$nextObserverId;
55
- $observables.set(id, new WeakRef(observable));
56
- return id;
57
- },
58
- unregister(id) {
59
- $observables.delete(id);
60
- },
61
- getObservableById(id) {
62
- return $observables.get(id)?.deref();
63
- },
64
- cleanup() {
65
- for (const [_, weakObservableRef] of $observables) {
66
- const observable = weakObservableRef.deref();
67
- if (observable) {
68
- observable.cleanup();
69
- }
70
- }
71
- $observables.clear();
72
- },
73
- /**
74
- * Clean observables that are not referenced anymore.
75
- * @param {number} threshold
76
- */
77
- cleanObservables(threshold) {
78
- if($observables.size < threshold) return;
79
- let cleanedCount = 0;
80
- for (const [id, weakObservableRef] of $observables) {
81
- if (!weakObservableRef.deref()) {
82
- $observables.delete(id);
83
- cleanedCount++;
84
- }
85
- }
86
- if (cleanedCount > 0) {
87
- DebugManager.log('Memory Auto Clean', `🧹 Cleaned ${cleanedCount} orphaned observables`);
88
- }
89
- }
90
- };
91
- }());
92
-
93
40
  class NativeDocumentError extends Error {
94
41
  constructor(message, context = {}) {
95
42
  super(message);
@@ -191,6 +138,181 @@ var NativeDocument = (function (exports) {
191
138
  return this.observable.cleanup();
192
139
  };
193
140
 
141
+ const DocumentObserver = {
142
+ mounted: new WeakMap(),
143
+ beforeUnmount: new WeakMap(),
144
+ mountedSupposedSize: 0,
145
+ unmounted: new WeakMap(),
146
+ unmountedSupposedSize: 0,
147
+ observer: null,
148
+
149
+ executeMountedCallback(node) {
150
+ const data = DocumentObserver.mounted.get(node);
151
+ if(!data) {
152
+ return;
153
+ }
154
+ data.inDom = true;
155
+ if(!data.mounted) {
156
+ return;
157
+ }
158
+ if(Array.isArray(data.mounted)) {
159
+ for(const cb of data.mounted) {
160
+ cb(node);
161
+ }
162
+ return;
163
+ }
164
+ data.mounted(node);
165
+ },
166
+
167
+ executeUnmountedCallback(node) {
168
+ const data = DocumentObserver.unmounted.get(node);
169
+ if(!data) {
170
+ return;
171
+ }
172
+ data.inDom = false;
173
+ if(!data.unmounted) {
174
+ return;
175
+ }
176
+
177
+ let shouldRemove = false;
178
+ if(Array.isArray(data.unmounted)) {
179
+ for(const cb of data.unmounted) {
180
+ if(cb(node) === true) {
181
+ shouldRemove = true;
182
+ }
183
+ }
184
+ } else {
185
+ shouldRemove = data.unmounted(node) === true;
186
+ }
187
+
188
+ if(shouldRemove) {
189
+ data.disconnect();
190
+ node.nd?.remove();
191
+ }
192
+ },
193
+
194
+ checkMutation: function(mutationsList) {
195
+ for(const mutation of mutationsList) {
196
+ if(DocumentObserver.mountedSupposedSize > 0) {
197
+ for(const node of mutation.addedNodes) {
198
+ DocumentObserver.executeMountedCallback(node);
199
+ if(!node.querySelectorAll) {
200
+ continue;
201
+ }
202
+ const children = node.querySelectorAll('[data--nd-mounted]');
203
+ for(const child of children) {
204
+ DocumentObserver.executeMountedCallback(child);
205
+ }
206
+ }
207
+ }
208
+
209
+ if (DocumentObserver.unmountedSupposedSize > 0) {
210
+ for (const node of mutation.removedNodes) {
211
+ DocumentObserver.executeUnmountedCallback(node);
212
+ if(!node.querySelectorAll) {
213
+ continue;
214
+ }
215
+ const children = node.querySelectorAll('[data--nd-unmounted]');
216
+ for(const child of children) {
217
+ DocumentObserver.executeUnmountedCallback(child);
218
+ }
219
+ }
220
+ }
221
+ }
222
+ },
223
+
224
+ /**
225
+ * @param {HTMLElement} element
226
+ * @param {boolean} inDom
227
+ * @returns {{ disconnect: Function, mounted: Function, unmounted: Function, off: Function }}
228
+ */
229
+ watch: function(element, inDom = false) {
230
+ let mountedRegistered = false;
231
+ let unmountedRegistered = false;
232
+
233
+ let data = {
234
+ inDom,
235
+ mounted: null,
236
+ unmounted: null,
237
+ disconnect: () => {
238
+ if (mountedRegistered) {
239
+ DocumentObserver.mounted.delete(element);
240
+ DocumentObserver.mountedSupposedSize--;
241
+ }
242
+ if (unmountedRegistered) {
243
+ DocumentObserver.unmounted.delete(element);
244
+ DocumentObserver.unmountedSupposedSize--;
245
+ }
246
+ data = null;
247
+ }
248
+ };
249
+
250
+ const addListener = (type, callback) => {
251
+ if (!data[type]) {
252
+ data[type] = callback;
253
+ return;
254
+ }
255
+ if (!Array.isArray(data[type])) {
256
+ data[type] = [data[type], callback];
257
+ return;
258
+ }
259
+ data[type].push(callback);
260
+ };
261
+
262
+ const removeListener = (type, callback) => {
263
+ if(!data?.[type]) {
264
+ return;
265
+ }
266
+ if(Array.isArray(data[type])) {
267
+ const index = data[type].indexOf(callback);
268
+ if(index > -1) {
269
+ data[type].splice(index, 1);
270
+ }
271
+ if(data[type].length === 1) {
272
+ data[type] = data[type][0];
273
+ }
274
+ if(data[type].length === 0) {
275
+ data[type] = null;
276
+ }
277
+ return;
278
+ }
279
+ data[type] = null;
280
+ };
281
+
282
+ return {
283
+ disconnect: () => data?.disconnect(),
284
+
285
+ mounted: (callback) => {
286
+ addListener('mounted', callback);
287
+ DocumentObserver.mounted.set(element, data);
288
+ if (!mountedRegistered) {
289
+ DocumentObserver.mountedSupposedSize++;
290
+ mountedRegistered = true;
291
+ }
292
+ },
293
+
294
+ unmounted: (callback) => {
295
+ addListener('unmounted', callback);
296
+ DocumentObserver.unmounted.set(element, data);
297
+ if (!unmountedRegistered) {
298
+ DocumentObserver.unmountedSupposedSize++;
299
+ unmountedRegistered = true;
300
+ }
301
+ },
302
+
303
+ off: (type, callback) => {
304
+ removeListener(type, callback);
305
+ }
306
+ };
307
+ }
308
+ };
309
+
310
+ DocumentObserver.observer = new MutationObserver(DocumentObserver.checkMutation);
311
+ DocumentObserver.observer.observe(document.body, {
312
+ childList: true,
313
+ subtree: true,
314
+ });
315
+
194
316
  let PluginsManager$1 = null;
195
317
 
196
318
  {
@@ -271,1385 +393,1898 @@ var NativeDocument = (function (exports) {
271
393
 
272
394
  var PluginsManager = PluginsManager$1;
273
395
 
274
- /**
275
- * Creates an ObservableWhen that tracks whether an observable equals a specific value.
276
- *
277
- * @param {ObservableItem} observer - The observable to watch
278
- * @param {*} value - The value to compare against
279
- * @class ObservableWhen
280
- */
281
- const ObservableWhen = function(observer, value) {
282
- this.$target = value;
283
- this.$observer = observer;
396
+ function NDElement(element) {
397
+ this.$element = element;
398
+ this.$observer = null;
399
+ {
400
+ PluginsManager.emit('NDElementCreated', element, this);
401
+ }
402
+ }
403
+
404
+ NDElement.prototype.__$isNDElement = true;
405
+
406
+ NDElement.prototype.valueOf = function() {
407
+ return this.$element;
284
408
  };
285
409
 
286
- ObservableWhen.prototype.__$isObservableWhen = true;
410
+ NDElement.prototype.ref = function(target, name) {
411
+ target[name] = this.$element;
412
+ return this;
413
+ };
287
414
 
288
- /**
289
- * Subscribes to changes in the match status (true when observable equals target value).
290
- *
291
- * @param {Function} callback - Function called with boolean indicating if values match
292
- * @returns {Function} Unsubscribe function
293
- * @example
294
- * const status = Observable('idle');
295
- * const isLoading = status.when('loading');
296
- * isLoading.subscribe(active => console.log('Loading:', active));
297
- */
298
- ObservableWhen.prototype.subscribe = function(callback) {
299
- return this.$observer.on(this.$target, callback);
415
+ NDElement.prototype.refSelf = function(target, name) {
416
+ target[name] = this;
417
+ return this;
300
418
  };
301
419
 
302
- /**
303
- * Returns true if the observable's current value equals the target value.
304
- *
305
- * @returns {boolean} True if observable value matches target value
306
- */
307
- ObservableWhen.prototype.val = function() {
308
- return this.$observer.$currentValue === this.$target;
420
+ NDElement.prototype.unmountChildren = function() {
421
+ let element = this.$element;
422
+ for(let i = 0, length = element.children.length; i < length; i++) {
423
+ let elementChildren = element.children[i];
424
+ if(!elementChildren.$ndProx) {
425
+ elementChildren.nd?.remove();
426
+ }
427
+ elementChildren = null;
428
+ }
429
+ element = null;
430
+ return this;
309
431
  };
310
432
 
311
- /**
312
- * Returns true if the observable's current value equals the target value.
313
- * Alias for val().
314
- *
315
- * @returns {boolean} True if observable value matches target value
316
- */
317
- ObservableWhen.prototype.isMatch = ObservableWhen.prototype.val;
433
+ NDElement.prototype.remove = function() {
434
+ let element = this.$element;
435
+ element.nd.unmountChildren();
436
+ element.$ndProx = null;
437
+ delete element.nd?.on?.prevent;
438
+ delete element.nd?.on;
439
+ delete element.nd;
440
+ element = null;
441
+ return this;
442
+ };
318
443
 
319
- /**
320
- * Returns true if the observable's current value equals the target value.
321
- * Alias for val().
322
- *
323
- * @returns {boolean} True if observable value matches target value
324
- */
325
- ObservableWhen.prototype.isActive = ObservableWhen.prototype.val;
444
+ NDElement.prototype.lifecycle = function(states) {
445
+ this.$observer = this.$observer || DocumentObserver.watch(this.$element);
326
446
 
327
- const nextTick = function(fn) {
328
- let pending = false;
329
- return function(...args) {
330
- if (pending) return;
331
- pending = true;
447
+ if(states.mounted) {
448
+ this.$element.setAttribute('data--nd-mounted', '1');
449
+ this.$observer.mounted(states.mounted);
450
+ }
451
+ if(states.unmounted) {
452
+ this.$element.setAttribute('data--nd-unmounted', '1');
453
+ this.$observer.unmounted(states.unmounted);
454
+ }
455
+ return this;
456
+ };
332
457
 
333
- Promise.resolve().then(() => {
334
- fn.apply(this, args);
335
- pending = false;
336
- });
337
- };
458
+ NDElement.prototype.mounted = function(callback) {
459
+ return this.lifecycle({ mounted: callback });
338
460
  };
339
461
 
340
- /**
341
- *
342
- * @param {*} item
343
- * @param {string|null} defaultKey
344
- * @param {?Function} key
345
- * @returns {*}
346
- */
347
- const getKey = (item, defaultKey, key) => {
348
- if (Validator.isString(key)) {
349
- const val = Validator.isObservable(item) ? item.val() : item;
350
- const result = val?.[key];
351
- return Validator.isObservable(result) ? result.val() : (result ?? defaultKey);
352
- }
462
+ NDElement.prototype.unmounted = function(callback) {
463
+ return this.lifecycle({ unmounted: callback });
464
+ };
353
465
 
354
- if (Validator.isFunction(key)) {
355
- return key(item, defaultKey);
466
+ NDElement.prototype.beforeUnmount = function(id, callback) {
467
+ const el = this.$element;
468
+
469
+ if(!DocumentObserver.beforeUnmount.has(el)) {
470
+ DocumentObserver.beforeUnmount.set(el, new Map());
471
+ const originalRemove = el.remove.bind(el);
472
+
473
+ let $isUnmounting = false;
474
+
475
+ el.remove = async () => {
476
+ if($isUnmounting) {
477
+ return;
478
+ }
479
+ $isUnmounting = true;
480
+
481
+ try {
482
+ const callbacks = DocumentObserver.beforeUnmount.get(el);
483
+ for (const cb of callbacks.values()) {
484
+ await cb.call(this, el);
485
+ }
486
+ } finally {
487
+ originalRemove();
488
+ $isUnmounting = false;
489
+ }
490
+ };
356
491
  }
357
492
 
358
- const val = Validator.isObservable(item) ? item.val() : item;
359
- return val ?? defaultKey;
493
+ DocumentObserver.beforeUnmount.get(el).set(id, callback);
494
+ return this;
360
495
  };
361
496
 
362
- const trim = function(str, char) {
363
- return str.replace(new RegExp(`^[${char}]+|[${char}]+$`, 'g'), '');
497
+ NDElement.prototype.htmlElement = function() {
498
+ return this.$element;
364
499
  };
365
500
 
366
- const deepClone = (value, onObservableFound) => {
367
- try {
368
- if(window.structuredClone !== undefined) {
369
- return window.structuredClone(value);
370
- }
371
- } catch (e){}
372
-
373
- if (value === null || typeof value !== 'object') {
374
- return value;
375
- }
501
+ NDElement.prototype.node = NDElement.prototype.htmlElement;
376
502
 
377
- // Dates
378
- if (value instanceof Date) {
379
- return new Date(value.getTime());
503
+ NDElement.prototype.shadow = function(mode, style = null) {
504
+ const $element = this.$element;
505
+ const children = Array.from($element.childNodes);
506
+ const shadowRoot = $element.attachShadow({ mode });
507
+ if(style) {
508
+ const styleNode = document.createElement("style");
509
+ styleNode.textContent = style;
510
+ shadowRoot.appendChild(styleNode);
380
511
  }
512
+ $element.append = shadowRoot.append.bind(shadowRoot);
513
+ $element.appendChild = shadowRoot.appendChild.bind(shadowRoot);
514
+ shadowRoot.append(...children);
381
515
 
382
- // Arrays
383
- if (Array.isArray(value)) {
384
- return value.map(item => deepClone(item));
385
- }
516
+ return this;
517
+ };
386
518
 
387
- // Observables - keep the référence
388
- if (Validator.isObservable(value)) {
389
- onObservableFound && onObservableFound(value);
390
- return value;
391
- }
519
+ NDElement.prototype.openShadow = function(style = null) {
520
+ return this.shadow('open', style);
521
+ };
392
522
 
393
- // Objects
394
- const cloned = {};
395
- for (const key in value) {
396
- if (Object.hasOwn(value, key)) {
397
- cloned[key] = deepClone(value[key]);
398
- }
399
- }
400
- return cloned;
523
+ NDElement.prototype.closedShadow = function(style = null) {
524
+ return this.shadow('closed', style);
401
525
  };
402
526
 
403
527
  /**
528
+ * Attaches a template binding to the element by hydrating it with the specified method.
404
529
  *
405
- * @param {*} value
406
- * @param {{ propagation: boolean, reset: boolean} | null} configs
407
- * @class ObservableItem
530
+ * @param {string} methodName - Name of the hydration method to call
531
+ * @param {BindingHydrator} bindingHydrator - Template binding with $hydrate method
532
+ * @returns {HTMLElement} The underlying HTML element
533
+ * @example
534
+ * const onClick = $binder.attach((event, data) => console.log(data));
535
+ * element.nd.attach('onClick', onClick);
408
536
  */
409
- function ObservableItem(value, configs = null) {
410
- value = Validator.isObservable(value) ? value.val() : value;
537
+ NDElement.prototype.attach = function(methodName, bindingHydrator) {
538
+ bindingHydrator.$hydrate(this.$element, methodName);
539
+ return this.$element;
540
+ };
411
541
 
412
- this.$previousValue = null;
413
- this.$currentValue = value;
542
+ /**
543
+ * Extends the current NDElement instance with custom methods.
544
+ * Methods are bound to the instance and available for chaining.
545
+ *
546
+ * @param {Object} methods - Object containing method definitions
547
+ * @returns {this} The NDElement instance with added methods for chaining
548
+ * @example
549
+ * element.nd.with({
550
+ * highlight() {
551
+ * this.$element.style.background = 'yellow';
552
+ * return this;
553
+ * }
554
+ * }).highlight().onClick(() => console.log('Clicked'));
555
+ */
556
+ NDElement.prototype.with = function(methods) {
557
+ if (!methods || typeof methods !== 'object') {
558
+ throw new NativeDocumentError('extend() requires an object of methods');
559
+ }
414
560
  {
415
- this.$isCleanedUp = false;
561
+ if (!this.$localExtensions) {
562
+ this.$localExtensions = new Map();
563
+ }
416
564
  }
417
565
 
418
- this.$firstListener = null;
419
- this.$listeners = null;
420
- this.$watchers = null;
421
-
422
- this.$memoryId = null;
566
+ for (const name in methods) {
567
+ const method = methods[name];
423
568
 
424
- if(configs) {
425
- this.configs = configs;
426
- if(configs.reset) {
427
- this.$initialValue = Validator.isObject(value) ? deepClone(value) : value;
569
+ if (typeof method !== 'function') {
570
+ console.warn(`⚠️ extends(): "${name}" is not a function, skipping`);
571
+ continue;
572
+ }
573
+ {
574
+ if (this[name] && !this.$localExtensions.has(name)) {
575
+ DebugManager.warn('NDElement.extend', `Method "${name}" already exists and will be overwritten`);
576
+ }
577
+ this.$localExtensions.set(name, method);
428
578
  }
429
- }
430
- {
431
- PluginsManager.emit('CreateObservable', this);
432
- }
433
- }
434
579
 
435
- Object.defineProperty(ObservableItem.prototype, '$value', {
436
- get() {
437
- return this.$currentValue;
438
- },
439
- set(value) {
440
- this.set(value);
441
- },
442
- configurable: true,
443
- });
580
+ this[name] = method.bind(this);
581
+ }
444
582
 
445
- ObservableItem.prototype.__$isObservable = true;
446
- const noneTrigger = function() {};
583
+ return this;
584
+ };
447
585
 
448
586
  /**
449
- * Intercepts and transforms values before they are set on the observable.
450
- * The interceptor can modify the value or return undefined to use the original value.
587
+ * Extends the NDElement prototype with new methods available to all NDElement instances.
588
+ * Use this to add global methods to all NDElements.
451
589
  *
452
- * @param {(value) => any} callback - Interceptor function that receives (newValue, currentValue) and returns the transformed value or undefined
453
- * @returns {ObservableItem} The observable instance for chaining
590
+ * @param {Object} methods - Object containing method definitions to add to prototype
591
+ * @returns {typeof NDElement} The NDElement constructor
592
+ * @throws {NativeDocumentError} If methods is not an object or contains non-function values
454
593
  * @example
455
- * const count = Observable(0);
456
- * count.intercept((newVal, oldVal) => Math.max(0, newVal)); // Prevent negative values
594
+ * NDElement.extend({
595
+ * fadeIn() {
596
+ * this.$element.style.opacity = '1';
597
+ * return this;
598
+ * }
599
+ * });
600
+ * // Now all NDElements have .fadeIn() method
601
+ * Div().nd.fadeIn();
457
602
  */
458
- ObservableItem.prototype.intercept = function(callback) {
459
- this.$interceptor = callback;
460
- this.set = this.$setWithInterceptor;
461
- return this;
462
- };
463
-
464
- ObservableItem.prototype.triggerFirstListener = function(operations) {
465
- this.$firstListener(this.$currentValue, this.$previousValue, operations);
466
- };
467
-
468
- ObservableItem.prototype.triggerListeners = function(operations) {
469
- const $listeners = this.$listeners;
470
- const $previousValue = this.$previousValue;
471
- const $currentValue = this.$currentValue;
603
+ NDElement.extend = function(methods) {
604
+ if (!methods || typeof methods !== 'object') {
605
+ throw new NativeDocumentError('NDElement.extend() requires an object of methods');
606
+ }
472
607
 
473
- for(let i = 0, length = $listeners.length; i < length; i++) {
474
- $listeners[i]($currentValue, $previousValue, operations);
608
+ if (Array.isArray(methods)) {
609
+ throw new NativeDocumentError('NDElement.extend() requires an object, not an array');
475
610
  }
476
- };
477
611
 
478
- ObservableItem.prototype.triggerWatchers = function(operations) {
479
- const $watchers = this.$watchers;
480
- const $previousValue = this.$previousValue;
481
- const $currentValue = this.$currentValue;
612
+ const protectedMethods = new Set([
613
+ 'constructor', 'valueOf', '$element', '$observer',
614
+ 'ref', 'remove', 'cleanup', 'with', 'extend', 'attach',
615
+ 'lifecycle', 'mounted', 'unmounted', 'unmountChildren'
616
+ ]);
482
617
 
483
- const $currentValueCallbacks = $watchers.get($currentValue);
484
- const $previousValueCallbacks = $watchers.get($previousValue);
485
- if($currentValueCallbacks) {
486
- $currentValueCallbacks(true, $previousValue, operations);
487
- }
488
- if($previousValueCallbacks) {
489
- $previousValueCallbacks(false, $currentValue, operations);
490
- }
491
- };
618
+ for (const name in methods) {
619
+ if (!Object.hasOwn(methods, name)) {
620
+ continue;
621
+ }
492
622
 
493
- ObservableItem.prototype.triggerAll = function(operations) {
494
- this.triggerWatchers(operations);
495
- this.triggerListeners(operations);
496
- };
623
+ const method = methods[name];
497
624
 
498
- ObservableItem.prototype.triggerWatchersAndFirstListener = function(operations) {
499
- this.triggerWatchers(operations);
500
- this.triggerFirstListener(operations);
501
- };
625
+ if (typeof method !== 'function') {
626
+ DebugManager.warn('NDElement.extend', `"${name}" is not a function, skipping`);
627
+ continue;
628
+ }
502
629
 
503
- ObservableItem.prototype.assocTrigger = function() {
504
- this.$firstListener = null;
505
- if(this.$watchers?.size && this.$listeners?.length) {
506
- this.trigger = (this.$listeners.length === 1) ? this.triggerWatchersAndFirstListener : this.triggerAll;
507
- return;
508
- }
509
- if(this.$listeners?.length) {
510
- if(this.$listeners.length === 1) {
511
- this.$firstListener = this.$listeners[0];
512
- this.trigger = this.triggerFirstListener;
630
+ if (protectedMethods.has(name)) {
631
+ DebugManager.error('NDElement.extend', `Cannot override protected method "${name}"`);
632
+ throw new NativeDocumentError(`Cannot override protected method "${name}"`);
513
633
  }
514
- else {
515
- this.trigger = this.triggerListeners;
634
+
635
+ if (NDElement.prototype[name]) {
636
+ DebugManager.warn('NDElement.extend', `Overwriting existing prototype method "${name}"`);
516
637
  }
517
- return;
518
- }
519
- if(this.$watchers?.size) {
520
- this.trigger = this.triggerWatchers;
521
- return;
522
- }
523
- this.trigger = noneTrigger;
524
- };
525
- ObservableItem.prototype.trigger = noneTrigger;
526
638
 
527
- ObservableItem.prototype.$updateWithNewValue = function(newValue) {
528
- newValue = newValue?.__$isObservable ? newValue.val() : newValue;
529
- if(this.$currentValue === newValue) {
530
- return;
531
- }
532
- this.$previousValue = this.$currentValue;
533
- this.$currentValue = newValue;
534
- {
535
- PluginsManager.emit('ObservableBeforeChange', this);
639
+ NDElement.prototype[name] = method;
536
640
  }
537
- this.trigger();
538
- this.$previousValue = null;
539
641
  {
540
- PluginsManager.emit('ObservableAfterChange', this);
541
- }
542
- };
543
-
544
- /**
545
- * @param {*} data
546
- */
547
- ObservableItem.prototype.$setWithInterceptor = function(data) {
548
- let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
549
- const result = this.$interceptor(newValue, this.$currentValue);
550
-
551
- if (result !== undefined) {
552
- newValue = result;
642
+ PluginsManager.emit('NDElementExtended', methods);
553
643
  }
554
644
 
555
- this.$updateWithNewValue(newValue);
645
+ return NDElement;
556
646
  };
557
647
 
558
- /**
559
- * @param {*} data
560
- */
561
- ObservableItem.prototype.$basicSet = function(data) {
562
- let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
563
- this.$updateWithNewValue(newValue);
648
+ const COMMON_NODE_TYPES = {
649
+ ELEMENT: 1,
650
+ TEXT: 3,
651
+ COMMENT: 8,
652
+ DOCUMENT_FRAGMENT: 11
564
653
  };
565
654
 
566
- ObservableItem.prototype.set = ObservableItem.prototype.$basicSet;
655
+ const Validator = {
656
+ isObservable(value) {
657
+ return value?.__$isObservable;
658
+ },
659
+ isTemplateBinding(value) {
660
+ return value?.__$isTemplateBinding;
661
+ },
662
+ isObservableWhenResult(value) {
663
+ return value && (value.__$isObservableWhen || (typeof value === 'object' && '$target' in value && '$observer' in value));
664
+ },
665
+ isArrayObservable(value) {
666
+ return value?.__$isObservableArray;
667
+ },
668
+ isProxy(value) {
669
+ return value?.__isProxy__
670
+ },
671
+ isObservableOrProxy(value) {
672
+ return Validator.isObservable(value) || Validator.isProxy(value);
673
+ },
674
+ isAnchor(value) {
675
+ return value?.__Anchor__
676
+ },
677
+ isObservableChecker(value) {
678
+ return value?.__$isObservableChecker || value instanceof ObservableChecker;
679
+ },
680
+ isArray(value) {
681
+ return Array.isArray(value);
682
+ },
683
+ isString(value) {
684
+ return typeof value === 'string';
685
+ },
686
+ isNumber(value) {
687
+ return typeof value === 'number';
688
+ },
689
+ isBoolean(value) {
690
+ return typeof value === 'boolean';
691
+ },
692
+ isFunction(value) {
693
+ return typeof value === 'function';
694
+ },
695
+ isAsyncFunction(value) {
696
+ return typeof value === 'function' && value.constructor.name === 'AsyncFunction';
697
+ },
698
+ isObject(value) {
699
+ return typeof value === 'object' && value !== null;
700
+ },
701
+ isJson(value) {
702
+ return !(typeof value !== 'object' || value === null || Array.isArray(value) || value.constructor.name !== 'Object')
703
+ },
704
+ isElement(value) {
705
+ return value && (
706
+ value.nodeType === COMMON_NODE_TYPES.ELEMENT ||
707
+ value.nodeType === COMMON_NODE_TYPES.TEXT ||
708
+ value.nodeType === COMMON_NODE_TYPES.DOCUMENT_FRAGMENT ||
709
+ value.nodeType === COMMON_NODE_TYPES.COMMENT
710
+ );
711
+ },
712
+ isFragment(value) {
713
+ return value?.nodeType === COMMON_NODE_TYPES.DOCUMENT_FRAGMENT;
714
+ },
715
+ isStringOrObservable(value) {
716
+ return this.isString(value) || this.isObservable(value);
717
+ },
718
+ isValidChild(child) {
719
+ return child === null ||
720
+ this.isElement(child) ||
721
+ this.isObservable(child) ||
722
+ this.isNDElement(child) ||
723
+ ['string', 'number', 'boolean'].includes(typeof child);
724
+ },
725
+ isNDElement(child) {
726
+ return child?.__$isNDElement || child instanceof NDElement;
727
+ },
728
+ isValidChildren(children) {
729
+ if (!Array.isArray(children)) {
730
+ children = [children];
731
+ }
567
732
 
568
- ObservableItem.prototype.val = function() {
569
- return this.$currentValue;
570
- };
733
+ const invalid = children.filter(child => !this.isValidChild(child));
734
+ return invalid.length === 0;
735
+ },
736
+ validateChildren(children) {
737
+ if (!Array.isArray(children)) {
738
+ children = [children];
739
+ }
571
740
 
572
- ObservableItem.prototype.disconnectAll = function() {
573
- this.$listeners?.splice(0);
574
- this.$previousValue = null;
575
- this.$currentValue = null;
576
- if(this.$watchers) {
577
- for (const [_, watchValueList] of this.$watchers) {
578
- if(Validator.isArray(watchValueList)) {
579
- watchValueList.splice(0);
580
- }
741
+ const invalid = children.filter(child => !this.isValidChild(child));
742
+ if (invalid.length > 0) {
743
+ throw new NativeDocumentError(`Invalid children detected: ${invalid.map(i => typeof i).join(', ')}`);
581
744
  }
582
- }
583
- this.$watchers?.clear();
584
- this.$listeners = null;
585
- this.$watchers = null;
586
- this.trigger = noneTrigger;
587
- };
588
745
 
589
- /**
590
- * Registers a cleanup callback that will be executed when the observable is cleaned up.
591
- * Useful for disposing resources, removing event listeners, or other cleanup tasks.
592
- *
593
- * @param {Function} callback - Cleanup function to execute on observable disposal
594
- * @example
595
- * const obs = Observable(0);
596
- * obs.onCleanup(() => console.log('Cleaned up!'));
597
- * obs.cleanup(); // Logs: "Cleaned up!"
598
- */
599
- ObservableItem.prototype.onCleanup = function(callback) {
600
- this.$cleanupListeners = this.$cleanupListeners ?? [];
601
- this.$cleanupListeners.push(callback);
602
- };
603
-
604
- ObservableItem.prototype.cleanup = function() {
605
- if (this.$cleanupListeners) {
606
- for (let i = 0; i < this.$cleanupListeners.length; i++) {
607
- this.$cleanupListeners[i]();
746
+ return children;
747
+ },
748
+ /**
749
+ * Check if the data contains observables.
750
+ * @param {Array|Object} data
751
+ * @returns {boolean}
752
+ */
753
+ containsObservables(data) {
754
+ if(!data) {
755
+ return false;
608
756
  }
609
- this.$cleanupListeners = null;
610
- }
611
- MemoryManager.unregister(this.$memoryId);
612
- this.disconnectAll();
613
- {
614
- this.$isCleanedUp = true;
615
- }
616
- delete this.$value;
617
- };
618
-
619
- /**
620
- *
621
- * @param {Function} callback
622
- * @returns {(function(): void)}
623
- */
624
- ObservableItem.prototype.subscribe = function(callback) {
625
- {
626
- if (this.$isCleanedUp) {
627
- DebugManager.warn('Observable subscription', '⚠️ Attempted to subscribe to a cleaned up observable.');
628
- return;
757
+ return Validator.isObject(data)
758
+ && Object.values(data).some(value => Validator.isObservable(value));
759
+ },
760
+ /**
761
+ * Check if the data contains an observable reference.
762
+ * @param {string} data
763
+ * @returns {boolean}
764
+ */
765
+ containsObservableReference(data) {
766
+ if(!data || typeof data !== 'string') {
767
+ return false;
629
768
  }
769
+ return /\{\{#ObItem::\([0-9]+\)\}\}/.test(data);
770
+ },
771
+ validateAttributes(attributes) {},
772
+
773
+ validateEventCallback(callback) {
630
774
  if (typeof callback !== 'function') {
631
- throw new NativeDocumentError('Callback must be a function');
775
+ throw new NativeDocumentError('Event callback must be a function');
632
776
  }
633
777
  }
634
- this.$listeners = this.$listeners ?? [];
635
-
636
- this.$listeners.push(callback);
637
- this.assocTrigger();
638
- {
639
- PluginsManager.emit('ObservableSubscribe', this);
640
- }
641
778
  };
779
+ {
780
+ Validator.validateAttributes = function(attributes) {
781
+ if (!attributes || typeof attributes !== 'object') {
782
+ return attributes;
783
+ }
642
784
 
643
- /**
644
- * Watches for a specific value and executes callback when the observable equals that value.
645
- * Creates a watcher that only triggers when the observable changes to the specified value.
646
- *
647
- * @param {*} value - The value to watch for
648
- * @param {(value) => void|ObservableItem} callback - Callback function or observable to set when value matches
649
- * @example
650
- * const status = Observable('idle');
651
- * status.on('loading', () => console.log('Started loading'));
652
- * status.on('error', isError); // Set another observable
653
- */
654
- ObservableItem.prototype.on = function(value, callback) {
655
- this.$watchers = this.$watchers ?? new Map();
785
+ const reserved = [];
786
+ const foundReserved = Object.keys(attributes).filter(key => reserved.includes(key));
656
787
 
657
- let watchValueList = this.$watchers.get(value);
788
+ if (foundReserved.length > 0) {
789
+ DebugManager.warn('Validator', `Reserved attributes found: ${foundReserved.join(', ')}`);
790
+ }
658
791
 
659
- if(callback.__$isObservable) {
660
- callback = callback.set.bind(callback);
661
- }
792
+ return attributes;
793
+ };
794
+ }
662
795
 
663
- if(!watchValueList) {
664
- watchValueList = callback;
665
- this.$watchers.set(value, callback);
666
- } else if(!Validator.isArray(watchValueList.list)) {
667
- watchValueList = [watchValueList, callback];
668
- callback = (value) => {
669
- for(let i = 0, length = watchValueList.length; i < length; i++) {
670
- watchValueList[i](value);
671
- }
672
- };
673
- callback.list = watchValueList;
674
- this.$watchers.set(value, callback);
675
- } else {
676
- watchValueList.list.push(callback);
677
- }
796
+ function Anchor(name, isUniqueChild = false) {
797
+ const anchorFragment = document.createDocumentFragment();
798
+ anchorFragment.__Anchor__ = true;
678
799
 
679
- this.assocTrigger();
680
- };
800
+ const anchorStart = document.createComment('Anchor Start : '+name);
801
+ const anchorEnd = document.createComment('/ Anchor End '+name);
681
802
 
682
- /**
683
- * Removes a watcher for a specific value. If no callback is provided, removes all watchers for that value.
684
- *
685
- * @param {*} value - The value to stop watching
686
- * @param {Function} [callback] - Specific callback to remove. If omitted, removes all watchers for this value
687
- * @example
688
- * const status = Observable('idle');
689
- * const handler = () => console.log('Loading');
690
- * status.on('loading', handler);
691
- * status.off('loading', handler); // Remove specific handler
692
- * status.off('loading'); // Remove all handlers for 'loading'
693
- */
694
- ObservableItem.prototype.off = function(value, callback) {
695
- if(!this.$watchers) return;
803
+ anchorFragment.appendChild(anchorStart);
804
+ anchorFragment.appendChild(anchorEnd);
696
805
 
697
- const watchValueList = this.$watchers.get(value);
698
- if(!watchValueList) return;
806
+ anchorFragment.nativeInsertBefore = anchorFragment.insertBefore;
807
+ anchorFragment.nativeAppendChild = anchorFragment.appendChild;
808
+ anchorFragment.nativeAppend = anchorFragment.append;
699
809
 
700
- if(!callback || !Array.isArray(watchValueList.list)) {
701
- this.$watchers?.delete(value);
702
- this.assocTrigger();
703
- return;
704
- }
705
- const index = watchValueList.indexOf(callback);
706
- watchValueList?.splice(index, 1);
707
- if(watchValueList.length === 1) {
708
- this.$watchers.set(value, watchValueList[0]);
709
- }
710
- else if(watchValueList.length === 0) {
711
- this.$watchers?.delete(value);
712
- }
713
- this.assocTrigger();
714
- };
810
+ const isParentUniqueChild = (parent) => (isUniqueChild || (parent.firstChild === anchorStart && parent.lastChild === anchorEnd));
715
811
 
716
- /**
717
- * Subscribes to the observable but automatically unsubscribes after the first time the predicate matches.
718
- *
719
- * @param {(value) => Boolean|any} predicate - Value to match or function that returns true when condition is met
720
- * @param {(value) => void} callback - Callback to execute when predicate matches, receives the matched value
721
- * @example
722
- * const status = Observable('loading');
723
- * status.once('ready', (val) => console.log('Ready!'));
724
- * status.once(val => val === 'error', (val) => console.log('Error occurred'));
725
- */
726
- ObservableItem.prototype.once = function(predicate, callback) {
727
- const fn = typeof predicate === 'function' ? predicate : (v) => v === predicate;
812
+ const insertBefore = function(parent, child, target) {
813
+ const childElement = Validator.isElement(child) ? child : ElementCreator.getChild(child);
814
+ if(parent === anchorFragment) {
815
+ parent.nativeInsertBefore(childElement, target);
816
+ return;
817
+ }
818
+ if(isParentUniqueChild(parent) && target === anchorEnd) {
819
+ parent.append(childElement, target);
820
+ return;
821
+ }
822
+ parent.insertBefore(childElement, target);
823
+ };
728
824
 
729
- const handler = (val) => {
730
- if (fn(val)) {
731
- this.unsubscribe(handler);
732
- callback(val);
825
+ anchorFragment.appendElement = function(child, before = null) {
826
+ const parentNode = anchorStart.parentNode;
827
+ const targetBefore = before || anchorEnd;
828
+ if(parentNode === anchorFragment) {
829
+ parentNode.nativeInsertBefore(child, targetBefore);
830
+ return;
733
831
  }
832
+ parentNode?.insertBefore(child, targetBefore);
734
833
  };
735
- this.subscribe(handler);
736
- };
737
834
 
738
- /**
739
- * Unsubscribe from an observable.
740
- * @param {Function} callback
741
- */
742
- ObservableItem.prototype.unsubscribe = function(callback) {
743
- if(!this.$listeners) return;
744
- const index = this.$listeners.indexOf(callback);
745
- if (index > -1) {
746
- this.$listeners.splice(index, 1);
747
- }
748
- this.assocTrigger();
749
- {
750
- PluginsManager.emit('ObservableUnsubscribe', this);
751
- }
752
- };
835
+ anchorFragment.appendChild = function(child, before = null) {
836
+ const parent = anchorEnd.parentNode;
837
+ if(!parent) {
838
+ DebugManager.error('Anchor', 'Anchor : parent not found', child);
839
+ return;
840
+ }
841
+ before = before ?? anchorEnd;
842
+ insertBefore(parent, child, before);
843
+ };
753
844
 
754
- /**
755
- * Create an Observable checker instance
756
- * @param callback
757
- * @returns {ObservableChecker}
758
- */
759
- ObservableItem.prototype.check = function(callback) {
760
- return new ObservableChecker(this, callback)
761
- };
845
+ anchorFragment.append = function(...args ) {
846
+ return anchorFragment.appendChild(args);
847
+ };
762
848
 
763
- /**
764
- * Gets a property value from the observable's current value.
765
- * If the property is an observable, returns its value.
766
- *
767
- * @param {string|number} key - Property key to retrieve
768
- * @returns {*} The value of the property, unwrapped if it's an observable
769
- * @example
770
- * const user = Observable({ name: 'John', age: Observable(25) });
771
- * user.get('name'); // 'John'
772
- * user.get('age'); // 25 (unwrapped from observable)
773
- */
774
- ObservableItem.prototype.get = function(key) {
775
- const item = this.$currentValue[key];
776
- return Validator.isObservable(item) ? item.val() : item;
777
- };
849
+ anchorFragment.removeChildren = async function() {
850
+ const parent = anchorEnd.parentNode;
851
+ if(parent === anchorFragment) {
852
+ return;
853
+ }
854
+ // if(isParentUniqueChild(parent)) {
855
+ // parent.replaceChildren(anchorStart, anchorEnd);
856
+ // return;
857
+ // }
778
858
 
859
+ let itemToRemove = anchorStart.nextSibling, tempItem;
860
+ const removes = [];
861
+ while(itemToRemove && itemToRemove !== anchorEnd) {
862
+ tempItem = itemToRemove.nextSibling;
863
+ removes.push(itemToRemove.remove());
864
+ itemToRemove = tempItem;
865
+ }
866
+ await Promise.all(removes);
867
+ };
868
+
869
+ anchorFragment.remove = async function() {
870
+ const parent = anchorEnd.parentNode;
871
+ if(parent === anchorFragment) {
872
+ return;
873
+ }
874
+ let itemToRemove = anchorStart.nextSibling, tempItem;
875
+ const allItemToRemove = [];
876
+ const removes = [];
877
+ while(itemToRemove && itemToRemove !== anchorEnd) {
878
+ tempItem = itemToRemove.nextSibling;
879
+ allItemToRemove.push(itemToRemove);
880
+ removes.push(itemToRemove.remove());
881
+ itemToRemove = tempItem;
882
+ }
883
+ await Promise.all(removes);
884
+ anchorFragment.nativeAppend(...allItemToRemove);
885
+ };
886
+
887
+ anchorFragment.removeWithAnchors = async function() {
888
+ await anchorFragment.removeChildren();
889
+ anchorStart.remove();
890
+ anchorEnd.remove();
891
+ };
892
+
893
+ anchorFragment.replaceContent = async function(child) {
894
+ const childElement = Validator.isElement(child) ? child : ElementCreator.getChild(child);
895
+ const parent = anchorEnd.parentNode;
896
+ if(!parent) {
897
+ return;
898
+ }
899
+ // if(isParentUniqueChild(parent)) {
900
+ // parent.replaceChildren(anchorStart, childElement, anchorEnd);
901
+ // return;
902
+ // }
903
+ await anchorFragment.removeChildren();
904
+ parent.insertBefore(childElement, anchorEnd);
905
+ };
906
+
907
+ anchorFragment.setContent = anchorFragment.replaceContent;
908
+
909
+ anchorFragment.insertBefore = function(child, anchor = null) {
910
+ anchorFragment.appendChild(child, anchor);
911
+ };
912
+
913
+
914
+ anchorFragment.endElement = function() {
915
+ return anchorEnd;
916
+ };
917
+
918
+ anchorFragment.startElement = function() {
919
+ return anchorStart;
920
+ };
921
+ anchorFragment.restore = function() {
922
+ anchorFragment.appendChild(anchorFragment);
923
+ };
924
+ anchorFragment.clear = anchorFragment.remove;
925
+ anchorFragment.detach = anchorFragment.remove;
926
+
927
+ anchorFragment.getByIndex = function(index) {
928
+ let currentNode = anchorStart;
929
+ for(let i = 0; i <= index; i++) {
930
+ if(!currentNode.nextSibling) {
931
+ return null;
932
+ }
933
+ currentNode = currentNode.nextSibling;
934
+ }
935
+ return currentNode !== anchorStart ? currentNode : null;
936
+ };
937
+
938
+ return anchorFragment;
939
+ }
779
940
  /**
780
- * Creates an ObservableWhen that represents whether the observable equals a specific value.
781
- * Returns an object that can be subscribed to and will emit true/false.
782
941
  *
942
+ * @param {HTMLElement|DocumentFragment|Text|String|Array} children
943
+ * @param {{ parent?: HTMLElement, name?: String}} configs
944
+ * @returns {DocumentFragment}
945
+ */
946
+ function createPortal(children, { parent, name = 'unnamed' } = {}) {
947
+ const anchor = Anchor('Portal '+name);
948
+ anchor.appendChild(ElementCreator.getChild(children));
949
+
950
+ (parent || document.body).appendChild(anchor);
951
+ return anchor;
952
+ }
953
+
954
+ DocumentFragment.prototype.setAttribute = () => {};
955
+
956
+ const BOOLEAN_ATTRIBUTES = new Set([
957
+ 'checked',
958
+ 'selected',
959
+ 'disabled',
960
+ 'readonly',
961
+ 'required',
962
+ 'autofocus',
963
+ 'multiple',
964
+ 'autocomplete',
965
+ 'hidden',
966
+ 'contenteditable',
967
+ 'spellcheck',
968
+ 'translate',
969
+ 'draggable',
970
+ 'async',
971
+ 'defer',
972
+ 'autoplay',
973
+ 'controls',
974
+ 'loop',
975
+ 'muted',
976
+ 'download',
977
+ 'reversed',
978
+ 'open',
979
+ 'default',
980
+ 'formnovalidate',
981
+ 'novalidate',
982
+ 'scoped',
983
+ 'itemscope',
984
+ 'allowfullscreen',
985
+ 'allowpaymentrequest',
986
+ 'playsinline'
987
+ ]);
988
+
989
+ const MemoryManager = (function() {
990
+
991
+ let $nextObserverId = 0;
992
+ const $observables = new Map();
993
+
994
+ return {
995
+ /**
996
+ * Register an observable and return an id.
997
+ *
998
+ * @param {ObservableItem} observable
999
+ * @param {Function} getListeners
1000
+ * @returns {number}
1001
+ */
1002
+ register(observable) {
1003
+ const id = ++$nextObserverId;
1004
+ $observables.set(id, new WeakRef(observable));
1005
+ return id;
1006
+ },
1007
+ unregister(id) {
1008
+ $observables.delete(id);
1009
+ },
1010
+ getObservableById(id) {
1011
+ return $observables.get(id)?.deref();
1012
+ },
1013
+ cleanup() {
1014
+ for (const [_, weakObservableRef] of $observables) {
1015
+ const observable = weakObservableRef.deref();
1016
+ if (observable) {
1017
+ observable.cleanup();
1018
+ }
1019
+ }
1020
+ $observables.clear();
1021
+ },
1022
+ /**
1023
+ * Clean observables that are not referenced anymore.
1024
+ * @param {number} threshold
1025
+ */
1026
+ cleanObservables(threshold) {
1027
+ if($observables.size < threshold) return;
1028
+ let cleanedCount = 0;
1029
+ for (const [id, weakObservableRef] of $observables) {
1030
+ if (!weakObservableRef.deref()) {
1031
+ $observables.delete(id);
1032
+ cleanedCount++;
1033
+ }
1034
+ }
1035
+ if (cleanedCount > 0) {
1036
+ DebugManager.log('Memory Auto Clean', `🧹 Cleaned ${cleanedCount} orphaned observables`);
1037
+ }
1038
+ }
1039
+ };
1040
+ }());
1041
+
1042
+ /**
1043
+ * Creates an ObservableWhen that tracks whether an observable equals a specific value.
1044
+ *
1045
+ * @param {ObservableItem} observer - The observable to watch
783
1046
  * @param {*} value - The value to compare against
784
- * @returns {ObservableWhen} An ObservableWhen instance that tracks when the observable equals the value
785
- * @example
786
- * const status = Observable('idle');
787
- * const isLoading = status.when('loading');
788
- * isLoading.subscribe(active => console.log('Loading:', active));
789
- * status.set('loading'); // Logs: "Loading: true"
1047
+ * @class ObservableWhen
790
1048
  */
791
- ObservableItem.prototype.when = function(value) {
792
- return new ObservableWhen(this, value);
1049
+ const ObservableWhen = function(observer, value) {
1050
+ this.$target = value;
1051
+ this.$observer = observer;
793
1052
  };
794
1053
 
1054
+ ObservableWhen.prototype.__$isObservableWhen = true;
1055
+
795
1056
  /**
796
- * Compares the observable's current value with another value or observable.
1057
+ * Subscribes to changes in the match status (true when observable equals target value).
797
1058
  *
798
- * @param {*|ObservableItem} other - Value or observable to compare against
799
- * @returns {boolean} True if values are equal
1059
+ * @param {Function} callback - Function called with boolean indicating if values match
1060
+ * @returns {Function} Unsubscribe function
800
1061
  * @example
801
- * const a = Observable(5);
802
- * const b = Observable(5);
803
- * a.equals(5); // true
804
- * a.equals(b); // true
805
- * a.equals(10); // false
1062
+ * const status = Observable('idle');
1063
+ * const isLoading = status.when('loading');
1064
+ * isLoading.subscribe(active => console.log('Loading:', active));
806
1065
  */
807
- ObservableItem.prototype.equals = function(other) {
808
- if(Validator.isObservable(other)) {
809
- return this.$currentValue === other.$currentValue;
810
- }
811
- return this.$currentValue === other;
1066
+ ObservableWhen.prototype.subscribe = function(callback) {
1067
+ return this.$observer.on(this.$target, callback);
812
1068
  };
813
1069
 
814
1070
  /**
815
- * Converts the observable's current value to a boolean.
1071
+ * Returns true if the observable's current value equals the target value.
816
1072
  *
817
- * @returns {boolean} The boolean representation of the current value
818
- * @example
819
- * const count = Observable(0);
820
- * count.toBool(); // false
821
- * count.set(5);
822
- * count.toBool(); // true
1073
+ * @returns {boolean} True if observable value matches target value
823
1074
  */
824
- ObservableItem.prototype.toBool = function() {
825
- return !!this.$currentValue;
1075
+ ObservableWhen.prototype.val = function() {
1076
+ return this.$observer.$currentValue === this.$target;
826
1077
  };
827
1078
 
828
1079
  /**
829
- * Toggles the boolean value of the observable (false becomes true, true becomes false).
1080
+ * Returns true if the observable's current value equals the target value.
1081
+ * Alias for val().
830
1082
  *
831
- * @example
832
- * const isOpen = Observable(false);
833
- * isOpen.toggle(); // Now true
834
- * isOpen.toggle(); // Now false
1083
+ * @returns {boolean} True if observable value matches target value
835
1084
  */
836
- ObservableItem.prototype.toggle = function() {
837
- this.set(!this.$currentValue);
838
- };
1085
+ ObservableWhen.prototype.isMatch = ObservableWhen.prototype.val;
839
1086
 
840
1087
  /**
841
- * Resets the observable to its initial value.
842
- * Only works if the observable was created with { reset: true } config.
1088
+ * Returns true if the observable's current value equals the target value.
1089
+ * Alias for val().
843
1090
  *
844
- * @example
845
- * const count = Observable(0, { reset: true });
846
- * count.set(10);
847
- * count.reset(); // Back to 0
1091
+ * @returns {boolean} True if observable value matches target value
848
1092
  */
849
- ObservableItem.prototype.reset = function() {
850
- if(!this.configs?.reset) {
851
- return;
852
- }
853
- const resetValue = (Validator.isObject(this.$initialValue))
854
- ? deepClone(this.$initialValue, (observable) => {
855
- observable.reset();
856
- })
857
- : this.$initialValue;
858
- this.set(resetValue);
859
- };
1093
+ ObservableWhen.prototype.isActive = ObservableWhen.prototype.val;
860
1094
 
861
- /**
862
- * Returns a string representation of the observable's current value.
863
- *
864
- * @returns {string} String representation of the current value
865
- */
866
- ObservableItem.prototype.toString = function() {
867
- return String(this.$currentValue);
1095
+ const nextTick = function(fn) {
1096
+ let pending = false;
1097
+ return function(...args) {
1098
+ if (pending) return;
1099
+ pending = true;
1100
+
1101
+ Promise.resolve().then(() => {
1102
+ fn.apply(this, args);
1103
+ pending = false;
1104
+ });
1105
+ };
868
1106
  };
869
1107
 
870
1108
  /**
871
- * Returns the primitive value of the observable (its current value).
872
- * Called automatically in type coercion contexts.
873
1109
  *
874
- * @returns {*} The current value of the observable
1110
+ * @param {*} item
1111
+ * @param {string|null} defaultKey
1112
+ * @param {?Function} key
1113
+ * @returns {*}
875
1114
  */
876
- ObservableItem.prototype.valueOf = function() {
877
- return this.$currentValue;
1115
+ const getKey = (item, defaultKey, key) => {
1116
+ if (Validator.isString(key)) {
1117
+ const val = Validator.isObservable(item) ? item.val() : item;
1118
+ const result = val?.[key];
1119
+ return Validator.isObservable(result) ? result.val() : (result ?? defaultKey);
1120
+ }
1121
+
1122
+ if (Validator.isFunction(key)) {
1123
+ return key(item, defaultKey);
1124
+ }
1125
+
1126
+ const val = Validator.isObservable(item) ? item.val() : item;
1127
+ return val ?? defaultKey;
878
1128
  };
879
1129
 
880
- const DocumentObserver = {
881
- mounted: new WeakMap(),
882
- beforeUnmount: new WeakMap(),
883
- mountedSupposedSize: 0,
884
- unmounted: new WeakMap(),
885
- unmountedSupposedSize: 0,
886
- observer: null,
1130
+ const trim = function(str, char) {
1131
+ return str.replace(new RegExp(`^[${char}]+|[${char}]+$`, 'g'), '');
1132
+ };
887
1133
 
888
- executeMountedCallback(node) {
889
- const data = DocumentObserver.mounted.get(node);
890
- if(!data) {
891
- return;
892
- }
893
- data.inDom = true;
894
- if(!data.mounted) {
895
- return;
896
- }
897
- if(Array.isArray(data.mounted)) {
898
- for(const cb of data.mounted) {
899
- cb(node);
900
- }
901
- return;
1134
+ const deepClone = (value, onObservableFound) => {
1135
+ try {
1136
+ if(window.structuredClone !== undefined) {
1137
+ return window.structuredClone(value);
902
1138
  }
903
- data.mounted(node);
904
- },
1139
+ } catch (e){}
905
1140
 
906
- executeUnmountedCallback(node) {
907
- const data = DocumentObserver.unmounted.get(node);
908
- if(!data) {
909
- return;
910
- }
911
- data.inDom = false;
912
- if(!data.unmounted) {
913
- return;
914
- }
1141
+ if (value === null || typeof value !== 'object') {
1142
+ return value;
1143
+ }
915
1144
 
916
- let shouldRemove = false;
917
- if(Array.isArray(data.unmounted)) {
918
- for(const cb of data.unmounted) {
919
- if(cb(node) === true) {
920
- shouldRemove = true;
921
- }
922
- }
923
- } else {
924
- shouldRemove = data.unmounted(node) === true;
1145
+ // Dates
1146
+ if (value instanceof Date) {
1147
+ return new Date(value.getTime());
1148
+ }
1149
+
1150
+ // Arrays
1151
+ if (Array.isArray(value)) {
1152
+ return value.map(item => deepClone(item));
1153
+ }
1154
+
1155
+ // Observables - keep the référence
1156
+ if (Validator.isObservable(value)) {
1157
+ onObservableFound && onObservableFound(value);
1158
+ return value;
1159
+ }
1160
+
1161
+ // Objects
1162
+ const cloned = {};
1163
+ for (const key in value) {
1164
+ if (Object.hasOwn(value, key)) {
1165
+ cloned[key] = deepClone(value[key]);
925
1166
  }
1167
+ }
1168
+ return cloned;
1169
+ };
926
1170
 
927
- if(shouldRemove) {
928
- data.disconnect();
929
- node.nd?.remove();
1171
+ const LocalStorage = {
1172
+ getJson(key) {
1173
+ let value = localStorage.getItem(key);
1174
+ try {
1175
+ return JSON.parse(value);
1176
+ } catch (e) {
1177
+ throw new NativeDocumentError('invalid_json:'+key);
930
1178
  }
931
1179
  },
1180
+ getNumber(key) {
1181
+ return Number(this.get(key));
1182
+ },
1183
+ getBool(key) {
1184
+ const value = this.get(key);
1185
+ return value === 'true' || value === '1';
1186
+ },
1187
+ setJson(key, value) {
1188
+ localStorage.setItem(key, JSON.stringify(value));
1189
+ },
1190
+ setBool(key, value) {
1191
+ localStorage.setItem(key, value ? 'true' : 'false');
1192
+ },
1193
+ get(key, defaultValue = null) {
1194
+ return localStorage.getItem(key) || defaultValue;
1195
+ },
1196
+ set(key, value) {
1197
+ return localStorage.setItem(key, value);
1198
+ },
1199
+ remove(key) {
1200
+ localStorage.removeItem(key);
1201
+ },
1202
+ has(key) {
1203
+ return localStorage.getItem(key) != null;
1204
+ }
1205
+ };
932
1206
 
933
- checkMutation: function(mutationsList) {
934
- for(const mutation of mutationsList) {
935
- if(DocumentObserver.mountedSupposedSize > 0) {
936
- for(const node of mutation.addedNodes) {
937
- DocumentObserver.executeMountedCallback(node);
938
- if(!node.querySelectorAll) {
939
- continue;
940
- }
941
- const children = node.querySelectorAll('[data--nd-mounted]');
942
- for(const child of children) {
943
- DocumentObserver.executeMountedCallback(child);
944
- }
945
- }
946
- }
1207
+ const $getFromStorage = (key, value) => {
1208
+ if(!LocalStorage.has(key)) {
1209
+ return value;
1210
+ }
1211
+ switch (typeof value) {
1212
+ case 'object': return LocalStorage.getJson(key) ?? value;
1213
+ case 'boolean': return LocalStorage.getBool(key) ?? value;
1214
+ case 'number': return LocalStorage.getNumber(key) ?? value;
1215
+ default: return LocalStorage.get(key, value) ?? value;
1216
+ }
1217
+ };
947
1218
 
948
- if (DocumentObserver.unmountedSupposedSize > 0) {
949
- for (const node of mutation.removedNodes) {
950
- DocumentObserver.executeUnmountedCallback(node);
951
- if(!node.querySelectorAll) {
952
- continue;
953
- }
954
- const children = node.querySelectorAll('[data--nd-unmounted]');
955
- for(const child of children) {
956
- DocumentObserver.executeUnmountedCallback(child);
957
- }
958
- }
959
- }
960
- }
961
- },
1219
+ const $saveToStorage = (value) => {
1220
+ switch (typeof value) {
1221
+ case 'object': return LocalStorage.setJson;
1222
+ case 'boolean': return LocalStorage.setBool;
1223
+ default: return LocalStorage.set;
1224
+ }
1225
+ };
1226
+
1227
+ const StoreFactory = function() {
1228
+
1229
+ const $stores = new Map();
1230
+ const $followersCache = new Map();
962
1231
 
963
1232
  /**
964
- * @param {HTMLElement} element
965
- * @param {boolean} inDom
966
- * @returns {{ disconnect: Function, mounted: Function, unmounted: Function, off: Function }}
1233
+ * Internal helper — retrieves a store entry or throws if not found.
967
1234
  */
968
- watch: function(element, inDom = false) {
969
- let mountedRegistered = false;
970
- let unmountedRegistered = false;
1235
+ const $getStoreOrThrow = (method, name) => {
1236
+ const item = $stores.get(name);
1237
+ if (!item) {
1238
+ DebugManager.error('Store', `Store.${method}('${name}') : store not found. Did you call Store.create('${name}') first?`);
1239
+ throw new NativeDocumentError(
1240
+ `Store.${method}('${name}') : store not found.`
1241
+ );
1242
+ }
1243
+ return item;
1244
+ };
971
1245
 
972
- let data = {
973
- inDom,
974
- mounted: null,
975
- unmounted: null,
976
- disconnect: () => {
977
- if (mountedRegistered) {
978
- DocumentObserver.mounted.delete(element);
979
- DocumentObserver.mountedSupposedSize--;
980
- }
981
- if (unmountedRegistered) {
982
- DocumentObserver.unmounted.delete(element);
983
- DocumentObserver.unmountedSupposedSize--;
984
- }
985
- data = null;
986
- }
1246
+ /**
1247
+ * Internal helper — blocks write operations on a read-only observer.
1248
+ */
1249
+ const $applyReadOnly = (observer, name, context) => {
1250
+ const readOnlyError = (method) => () => {
1251
+ DebugManager.error('Store', `Store.${context}('${name}') is read-only. '${method}()' is not allowed.`);
1252
+ throw new NativeDocumentError(
1253
+ `Store.${context}('${name}') is read-only.`
1254
+ );
987
1255
  };
1256
+ observer.set = readOnlyError('set');
1257
+ observer.toggle = readOnlyError('toggle');
1258
+ observer.reset = readOnlyError('reset');
1259
+ };
988
1260
 
989
- const addListener = (type, callback) => {
990
- if (!data[type]) {
991
- data[type] = callback;
992
- return;
1261
+ const $createObservable = (value, options = {}) => {
1262
+ if(Array.isArray(value)) {
1263
+ return Observable.array(value, options);
1264
+ }
1265
+ if(typeof value === 'object') {
1266
+ return Observable.object(value, options);
1267
+ }
1268
+ return Observable(value, options);
1269
+ };
1270
+
1271
+ const $api = {
1272
+ /**
1273
+ * Create a new state and return the observer.
1274
+ * Throws if a store with the same name already exists.
1275
+ *
1276
+ * @param {string} name
1277
+ * @param {*} value
1278
+ * @returns {ObservableItem}
1279
+ */
1280
+ create(name, value) {
1281
+ if ($stores.has(name)) {
1282
+ DebugManager.warn('Store', `Store.create('${name}') : a store with this name already exists. Use Store.get('${name}') to retrieve it.`);
1283
+ throw new NativeDocumentError(
1284
+ `Store.create('${name}') : a store with this name already exists.`
1285
+ );
993
1286
  }
994
- if (!Array.isArray(data[type])) {
995
- data[type] = [data[type], callback];
996
- return;
1287
+ const observer = $createObservable(value);
1288
+ $stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: false });
1289
+ return observer;
1290
+ },
1291
+
1292
+ /**
1293
+ * Create a new resettable state and return the observer.
1294
+ * The store can be reset to its initial value via Store.reset(name).
1295
+ * Throws if a store with the same name already exists.
1296
+ *
1297
+ * @param {string} name
1298
+ * @param {*} value
1299
+ * @returns {ObservableItem}
1300
+ */
1301
+ createResettable(name, value) {
1302
+ if ($stores.has(name)) {
1303
+ DebugManager.warn('Store', `Store.createResettable('${name}') : a store with this name already exists.`);
1304
+ throw new NativeDocumentError(
1305
+ `Store.createResettable('${name}') : a store with this name already exists.`
1306
+ );
997
1307
  }
998
- data[type].push(callback);
999
- };
1308
+ const observer = $createObservable(value, { reset: true });
1309
+ $stores.set(name, { observer, subscribers: new Set(), resettable: true, composed: false });
1310
+ return observer;
1311
+ },
1000
1312
 
1001
- const removeListener = (type, callback) => {
1002
- if(!data?.[type]) {
1003
- return;
1313
+ /**
1314
+ * Create a computed store derived from other stores.
1315
+ * The value is automatically recalculated when any dependency changes.
1316
+ * This store is read-only — Store.use() and Store.set() will throw.
1317
+ * Throws if a store with the same name already exists.
1318
+ *
1319
+ * @param {string} name
1320
+ * @param {() => *} computation - Function that returns the computed value
1321
+ * @param {string[]} dependencies - Names of the stores to watch
1322
+ * @returns {ObservableItem}
1323
+ *
1324
+ * @example
1325
+ * Store.create('products', [{ id: 1, price: 10 }]);
1326
+ * Store.create('cart', [{ productId: 1, quantity: 2 }]);
1327
+ *
1328
+ * Store.createComposed('total', () => {
1329
+ * const products = Store.get('products').val();
1330
+ * const cart = Store.get('cart').val();
1331
+ * return cart.reduce((sum, item) => {
1332
+ * const product = products.find(p => p.id === item.productId);
1333
+ * return sum + (product.price * item.quantity);
1334
+ * }, 0);
1335
+ * }, ['products', 'cart']);
1336
+ */
1337
+ createComposed(name, computation, dependencies) {
1338
+ if ($stores.has(name)) {
1339
+ DebugManager.warn('Store', `Store.createComposed('${name}') : a store with this name already exists.`);
1340
+ throw new NativeDocumentError(
1341
+ `Store.createComposed('${name}') : a store with this name already exists.`
1342
+ );
1004
1343
  }
1005
- if(Array.isArray(data[type])) {
1006
- const index = data[type].indexOf(callback);
1007
- if(index > -1) {
1008
- data[type].splice(index, 1);
1009
- }
1010
- if(data[type].length === 1) {
1011
- data[type] = data[type][0];
1344
+ if (typeof computation !== 'function') {
1345
+ throw new NativeDocumentError(
1346
+ `Store.createComposed('${name}') : computation must be a function.`
1347
+ );
1348
+ }
1349
+ if (!Array.isArray(dependencies) || dependencies.length === 0) {
1350
+ throw new NativeDocumentError(
1351
+ `Store.createComposed('${name}') : dependencies must be a non-empty array of store names.`
1352
+ );
1353
+ }
1354
+
1355
+ // Resolve dependency observers
1356
+ const depObservers = dependencies.map(depName => {
1357
+ if(typeof depName !== 'string') {
1358
+ return depName;
1012
1359
  }
1013
- if(data[type].length === 0) {
1014
- data[type] = null;
1360
+ const depItem = $stores.get(depName);
1361
+ if (!depItem) {
1362
+ DebugManager.error('Store', `Store.createComposed('${name}') : dependency '${depName}' not found. Create it first.`);
1363
+ throw new NativeDocumentError(
1364
+ `Store.createComposed('${name}') : dependency store '${depName}' not found.`
1365
+ );
1015
1366
  }
1016
- return;
1017
- }
1018
- data[type] = null;
1019
- };
1367
+ return depItem.observer;
1368
+ });
1020
1369
 
1021
- return {
1022
- disconnect: () => data?.disconnect(),
1370
+ // Create computed observable from dependency observers
1371
+ const observer = Observable.computed(computation, depObservers);
1023
1372
 
1024
- mounted: (callback) => {
1025
- addListener('mounted', callback);
1026
- DocumentObserver.mounted.set(element, data);
1027
- if (!mountedRegistered) {
1028
- DocumentObserver.mountedSupposedSize++;
1029
- mountedRegistered = true;
1030
- }
1031
- },
1373
+ $stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: true });
1374
+ return observer;
1375
+ },
1032
1376
 
1033
- unmounted: (callback) => {
1034
- addListener('unmounted', callback);
1035
- DocumentObserver.unmounted.set(element, data);
1036
- if (!unmountedRegistered) {
1037
- DocumentObserver.unmountedSupposedSize++;
1038
- unmountedRegistered = true;
1039
- }
1040
- },
1377
+ /**
1378
+ * Returns true if a store with the given name exists.
1379
+ *
1380
+ * @param {string} name
1381
+ * @returns {boolean}
1382
+ */
1383
+ has(name) {
1384
+ return $stores.has(name);
1385
+ },
1041
1386
 
1042
- off: (type, callback) => {
1043
- removeListener(type, callback);
1387
+ /**
1388
+ * Resets a resettable store to its initial value and notifies all subscribers.
1389
+ * Throws if the store was not created with createResettable().
1390
+ *
1391
+ * @param {string} name
1392
+ */
1393
+ reset(name) {
1394
+ const item = $getStoreOrThrow('reset', name);
1395
+ if (item.composed) {
1396
+ DebugManager.error('Store', `Store.reset('${name}') : composed stores cannot be reset. Their value is derived from dependencies.`);
1397
+ throw new NativeDocumentError(
1398
+ `Store.reset('${name}') : composed stores cannot be reset.`
1399
+ );
1044
1400
  }
1045
- };
1046
- }
1047
- };
1048
-
1049
- DocumentObserver.observer = new MutationObserver(DocumentObserver.checkMutation);
1050
- DocumentObserver.observer.observe(document.body, {
1051
- childList: true,
1052
- subtree: true,
1053
- });
1401
+ if (!item.resettable) {
1402
+ DebugManager.error('Store', `Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`);
1403
+ throw new NativeDocumentError(
1404
+ `Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`
1405
+ );
1406
+ }
1407
+ item.observer.reset();
1408
+ },
1054
1409
 
1055
- function NDElement(element) {
1056
- this.$element = element;
1057
- this.$observer = null;
1058
- {
1059
- PluginsManager.emit('NDElementCreated', element, this);
1060
- }
1061
- }
1410
+ /**
1411
+ * Returns a two-way synchronized follower of the store.
1412
+ * Writing to the follower propagates the value back to the store and all its subscribers.
1413
+ * Throws if called on a composed store — use Store.follow() instead.
1414
+ * Call follower.destroy() or follower.dispose() to unsubscribe.
1415
+ *
1416
+ * @param {string} name
1417
+ * @returns {ObservableItem}
1418
+ */
1419
+ use(name) {
1420
+ const item = $getStoreOrThrow('use', name);
1062
1421
 
1063
- NDElement.prototype.__$isNDElement = true;
1422
+ if (item.composed) {
1423
+ DebugManager.error('Store', `Store.use('${name}') : composed stores are read-only. Use Store.follow('${name}') instead.`);
1424
+ throw new NativeDocumentError(
1425
+ `Store.use('${name}') : composed stores are read-only. Use Store.follow('${name}') instead.`
1426
+ );
1427
+ }
1064
1428
 
1065
- NDElement.prototype.valueOf = function() {
1066
- return this.$element;
1067
- };
1429
+ const { observer: originalObserver, subscribers } = item;
1430
+ const observerFollower = $createObservable(originalObserver.val());
1068
1431
 
1069
- NDElement.prototype.ref = function(target, name) {
1070
- target[name] = this.$element;
1071
- return this;
1072
- };
1432
+ const onStoreChange = value => observerFollower.set(value);
1433
+ const onFollowerChange = value => originalObserver.set(value);
1073
1434
 
1074
- NDElement.prototype.refSelf = function(target, name) {
1075
- target[name] = this;
1076
- return this;
1077
- };
1435
+ originalObserver.subscribe(onStoreChange);
1436
+ observerFollower.subscribe(onFollowerChange);
1078
1437
 
1079
- NDElement.prototype.unmountChildren = function() {
1080
- let element = this.$element;
1081
- for(let i = 0, length = element.children.length; i < length; i++) {
1082
- let elementChildren = element.children[i];
1083
- if(!elementChildren.$ndProx) {
1084
- elementChildren.nd?.remove();
1085
- }
1086
- elementChildren = null;
1087
- }
1088
- element = null;
1089
- return this;
1090
- };
1438
+ observerFollower.destroy = () => {
1439
+ originalObserver.unsubscribe(onStoreChange);
1440
+ observerFollower.unsubscribe(onFollowerChange);
1441
+ subscribers.delete(observerFollower);
1442
+ observerFollower.cleanup();
1443
+ };
1444
+ observerFollower.dispose = observerFollower.destroy;
1091
1445
 
1092
- NDElement.prototype.remove = function() {
1093
- let element = this.$element;
1094
- element.nd.unmountChildren();
1095
- element.$ndProx = null;
1096
- delete element.nd?.on?.prevent;
1097
- delete element.nd?.on;
1098
- delete element.nd;
1099
- element = null;
1100
- return this;
1101
- };
1446
+ subscribers.add(observerFollower);
1447
+ return observerFollower;
1448
+ },
1102
1449
 
1103
- NDElement.prototype.lifecycle = function(states) {
1104
- this.$observer = this.$observer || DocumentObserver.watch(this.$element);
1450
+ /**
1451
+ * Returns a read-only follower of the store.
1452
+ * The follower reflects store changes but cannot write back to the store.
1453
+ * Any attempt to call .set(), .toggle() or .reset() will throw.
1454
+ * Call follower.destroy() or follower.dispose() to unsubscribe.
1455
+ *
1456
+ * @param {string} name
1457
+ * @returns {ObservableItem}
1458
+ */
1459
+ follow(name) {
1460
+ const { observer: originalObserver, subscribers } = $getStoreOrThrow('follow', name);
1461
+ const observerFollower = $createObservable(originalObserver.val());
1105
1462
 
1106
- if(states.mounted) {
1107
- this.$element.setAttribute('data--nd-mounted', '1');
1108
- this.$observer.mounted(states.mounted);
1109
- }
1110
- if(states.unmounted) {
1111
- this.$element.setAttribute('data--nd-unmounted', '1');
1112
- this.$observer.unmounted(states.unmounted);
1113
- }
1114
- return this;
1115
- };
1463
+ const onStoreChange = value => observerFollower.set(value);
1464
+ originalObserver.subscribe(onStoreChange);
1116
1465
 
1117
- NDElement.prototype.mounted = function(callback) {
1118
- return this.lifecycle({ mounted: callback });
1119
- };
1466
+ $applyReadOnly(observerFollower, name, 'follow');
1120
1467
 
1121
- NDElement.prototype.unmounted = function(callback) {
1122
- return this.lifecycle({ unmounted: callback });
1123
- };
1468
+ observerFollower.destroy = () => {
1469
+ originalObserver.unsubscribe(onStoreChange);
1470
+ subscribers.delete(observerFollower);
1471
+ observerFollower.cleanup();
1472
+ };
1473
+ observerFollower.dispose = observerFollower.destroy;
1124
1474
 
1125
- NDElement.prototype.beforeUnmount = function(id, callback) {
1126
- const el = this.$element;
1475
+ subscribers.add(observerFollower);
1476
+ return observerFollower;
1477
+ },
1127
1478
 
1128
- if(!DocumentObserver.beforeUnmount.has(el)) {
1129
- DocumentObserver.beforeUnmount.set(el, new Map());
1130
- const originalRemove = el.remove.bind(el);
1479
+ /**
1480
+ * Returns the raw store observer directly (no follower, no cleanup contract).
1481
+ * Use this for direct read access when you don't need to unsubscribe.
1482
+ * WARNING : mutations on this observer impact all subscribers immediately.
1483
+ *
1484
+ * @param {string} name
1485
+ * @returns {ObservableItem|null}
1486
+ */
1487
+ get(name) {
1488
+ const item = $stores.get(name);
1489
+ if (!item) {
1490
+ DebugManager.warn('Store', `Store.get('${name}') : store not found.`);
1491
+ return null;
1492
+ }
1493
+ return item.observer;
1494
+ },
1131
1495
 
1132
- let $isUnmounting = false;
1496
+ /**
1497
+ * @param {string} name
1498
+ * @returns {{ observer: ObservableItem, subscribers: Set } | null}
1499
+ */
1500
+ getWithSubscribers(name) {
1501
+ return $stores.get(name) ?? null;
1502
+ },
1133
1503
 
1134
- el.remove = async () => {
1135
- if($isUnmounting) {
1504
+ /**
1505
+ * Destroys a store : cleans up the observer, destroys all followers, and removes the entry.
1506
+ *
1507
+ * @param {string} name
1508
+ */
1509
+ delete(name) {
1510
+ const item = $stores.get(name);
1511
+ if (!item) {
1512
+ DebugManager.warn('Store', `Store.delete('${name}') : store not found, nothing to delete.`);
1136
1513
  return;
1137
1514
  }
1138
- $isUnmounting = true;
1515
+ item.subscribers.forEach(follower => follower.destroy());
1516
+ item.subscribers.clear();
1517
+ item.observer.cleanup();
1518
+ $stores.delete(name);
1519
+ },
1520
+ /**
1521
+ * Creates an isolated store group with its own state namespace.
1522
+ * Each group is a fully independent StoreFactory instance —
1523
+ * no key conflicts, no shared state with the parent store.
1524
+ *
1525
+ * @param {string | ((group: ReturnType<typeof StoreFactory>) => void)} name - Group name for debugging, or setup callback if no name is provided
1526
+ * @param {((group: ReturnType<typeof StoreFactory>) => void)} [callback] - Setup function receiving the isolated store instance
1527
+ * @returns {ReturnType<typeof StoreFactory>}
1528
+ *
1529
+ * @example
1530
+ * // With name (recommended)
1531
+ * const EventStore = Store.group('events', (group) => {
1532
+ * group.create('catalog', []);
1533
+ * group.create('filters', { category: null, date: null });
1534
+ * group.createResettable('selected', null);
1535
+ * group.createComposed('filtered', () => {
1536
+ * const catalog = EventStore.get('catalog').val();
1537
+ * const filters = EventStore.get('filters').val();
1538
+ * return catalog.filter(event => {
1539
+ * if (filters.category && event.category !== filters.category) return false;
1540
+ * return true;
1541
+ * });
1542
+ * }, ['catalog', 'filters']);
1543
+ * });
1544
+ *
1545
+ * // Without name
1546
+ * const CartStore = Store.group((group) => {
1547
+ * group.create('items', []);
1548
+ * });
1549
+ *
1550
+ * // Usage
1551
+ * EventStore.use('catalog'); // two-way follower
1552
+ * EventStore.follow('filtered'); // read-only follower
1553
+ * EventStore.get('filters'); // raw observable
1554
+ *
1555
+ * // Cross-group composed
1556
+ * const OrderStore = Store.group('orders', (group) => {
1557
+ * group.createComposed('summary', () => {
1558
+ * const items = CartStore.get('items').val();
1559
+ * const events = EventStore.get('catalog').val();
1560
+ * return { items, events };
1561
+ * }, [CartStore.get('items'), EventStore.get('catalog')]);
1562
+ * });
1563
+ */
1564
+ group(name, callback) {
1565
+ if (typeof name === 'function') {
1566
+ callback = name;
1567
+ name = 'anonymous';
1568
+ }
1569
+ const store = StoreFactory();
1570
+ callback && callback(store);
1571
+ return store;
1572
+ },
1573
+ createPersistent(name, value, localstorage_key) {
1574
+ localstorage_key = localstorage_key || name;
1575
+ const observer = this.create(name, $getFromStorage(localstorage_key, value));
1576
+ const saver = $saveToStorage(value);
1139
1577
 
1140
- try {
1141
- const callbacks = DocumentObserver.beforeUnmount.get(el);
1142
- for (const cb of callbacks.values()) {
1143
- await cb.call(this, el);
1578
+ observer.subscribe((val) => saver(localstorage_key, val));
1579
+ return observer;
1580
+ },
1581
+ createPersistentResettable(name, value, localstorage_key) {
1582
+ localstorage_key = localstorage_key || name;
1583
+ const observer = this.createResettable(name, $getFromStorage(localstorage_key, value));
1584
+ const saver = $saveToStorage(value);
1585
+ observer.subscribe((val) => saver(localstorage_key, val));
1586
+
1587
+ const originalReset = observer.reset.bind(observer);
1588
+ observer.reset = () => {
1589
+ LocalStorage.remove(localstorage_key);
1590
+ originalReset();
1591
+ };
1592
+
1593
+ return observer;
1594
+ }
1595
+ };
1596
+
1597
+
1598
+ return new Proxy($api, {
1599
+ get(target, prop) {
1600
+ if (typeof prop === 'symbol' || prop.startsWith('$') || prop in target) {
1601
+ return target[prop];
1602
+ }
1603
+ if (target.has(prop)) {
1604
+ if ($followersCache.has(prop)) {
1605
+ return $followersCache.get(prop);
1144
1606
  }
1145
- } finally {
1146
- originalRemove();
1147
- $isUnmounting = false;
1607
+ const follower = target.follow(prop);
1608
+ $followersCache.set(prop, follower);
1609
+ return follower;
1148
1610
  }
1149
- };
1150
- }
1151
-
1152
- DocumentObserver.beforeUnmount.get(el).set(id, callback);
1153
- return this;
1611
+ return undefined;
1612
+ },
1613
+ set(target, prop, value) {
1614
+ DebugManager.error('Store', `Forbidden: You cannot overwrite the store key '${String(prop)}'. Use .use('${String(prop)}').set(value) instead.`);
1615
+ throw new NativeDocumentError(`Store structure is immutable. Use .set() on the observable.`);
1616
+ },
1617
+ deleteProperty(target, prop) {
1618
+ throw new NativeDocumentError(`Store keys cannot be deleted.`);
1619
+ }
1620
+ });
1154
1621
  };
1155
1622
 
1156
- NDElement.prototype.htmlElement = function() {
1157
- return this.$element;
1158
- };
1623
+ const Store = StoreFactory();
1159
1624
 
1160
- NDElement.prototype.node = NDElement.prototype.htmlElement;
1625
+ Store.create('locale', 'fr');
1161
1626
 
1162
- NDElement.prototype.shadow = function(mode, style = null) {
1163
- const $element = this.$element;
1164
- const children = Array.from($element.childNodes);
1165
- const shadowRoot = $element.attachShadow({ mode });
1166
- if(style) {
1167
- const styleNode = document.createElement("style");
1168
- styleNode.textContent = style;
1169
- shadowRoot.appendChild(styleNode);
1170
- }
1171
- $element.append = shadowRoot.append.bind(shadowRoot);
1172
- $element.appendChild = shadowRoot.appendChild.bind(shadowRoot);
1173
- shadowRoot.append(...children);
1627
+ const $parseDateParts = (value, locale) => {
1628
+ const d = new Date(value);
1629
+ return {
1630
+ d,
1631
+ parts: new Intl.DateTimeFormat(locale, {
1632
+ year: 'numeric',
1633
+ month: 'long',
1634
+ day: '2-digit',
1635
+ hour: '2-digit',
1636
+ minute: '2-digit',
1637
+ second: '2-digit',
1638
+ }).formatToParts(d).reduce((acc, { type, value }) => {
1639
+ acc[type] = value;
1640
+ return acc;
1641
+ }, {})
1642
+ };
1643
+ };
1644
+
1645
+ const $applyDatePattern = (pattern, d, parts) => {
1646
+ const pad = n => String(n).padStart(2, '0');
1647
+ return pattern
1648
+ .replace('YYYY', parts.year)
1649
+ .replace('YY', parts.year.slice(-2))
1650
+ .replace('MMMM', parts.month)
1651
+ .replace('MMM', parts.month.slice(0, 3))
1652
+ .replace('MM', pad(d.getMonth() + 1))
1653
+ .replace('DD', pad(d.getDate()))
1654
+ .replace('D', d.getDate())
1655
+ .replace('HH', parts.hour)
1656
+ .replace('mm', parts.minute)
1657
+ .replace('ss', parts.second);
1658
+ };
1659
+
1660
+ const Formatters = {
1661
+
1662
+ currency: (value, locale, { currency = 'XOF', notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
1663
+ new Intl.NumberFormat(locale, {
1664
+ style: 'currency',
1665
+ currency,
1666
+ notation,
1667
+ minimumFractionDigits,
1668
+ maximumFractionDigits
1669
+ }).format(value),
1670
+
1671
+ number: (value, locale, { notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
1672
+ new Intl.NumberFormat(locale, {
1673
+ notation,
1674
+ minimumFractionDigits,
1675
+ maximumFractionDigits
1676
+ }).format(value),
1677
+
1678
+ percent: (value, locale, { decimals = 1 } = {}) =>
1679
+ new Intl.NumberFormat(locale, {
1680
+ style: 'percent',
1681
+ maximumFractionDigits: decimals
1682
+ }).format(value),
1683
+
1684
+ date: (value, locale, { format, dateStyle = 'long' } = {}) => {
1685
+ if (format) {
1686
+ const { d, parts } = $parseDateParts(value, locale);
1687
+ return $applyDatePattern(format, d, parts);
1688
+ }
1689
+ return new Intl.DateTimeFormat(locale, { dateStyle }).format(new Date(value));
1690
+ },
1174
1691
 
1175
- return this;
1176
- };
1692
+ time: (value, locale, { format, hour = '2-digit', minute = '2-digit', second } = {}) => {
1693
+ if (format) {
1694
+ const { d, parts } = $parseDateParts(value, locale);
1695
+ return $applyDatePattern(format, d, parts);
1696
+ }
1697
+ return new Intl.DateTimeFormat(locale, { hour, minute, second }).format(new Date(value));
1698
+ },
1177
1699
 
1178
- NDElement.prototype.openShadow = function(style = null) {
1179
- return this.shadow('open', style);
1180
- };
1700
+ datetime: (value, locale, { format, dateStyle = 'long', hour = '2-digit', minute = '2-digit', second } = {}) => {
1701
+ if (format) {
1702
+ const { d, parts } = $parseDateParts(value, locale);
1703
+ return $applyDatePattern(format, d, parts);
1704
+ }
1705
+ return new Intl.DateTimeFormat(locale, { dateStyle, hour, minute, second }).format(new Date(value));
1706
+ },
1181
1707
 
1182
- NDElement.prototype.closedShadow = function(style = null) {
1183
- return this.shadow('closed', style);
1708
+ relative: (value, locale, { unit = 'day', numeric = 'auto' } = {}) => {
1709
+ const diff = Math.round((value - Date.now()) / (1000 * 60 * 60 * 24));
1710
+ return new Intl.RelativeTimeFormat(locale, { numeric }).format(diff, unit);
1711
+ },
1712
+
1713
+ plural: (value, locale, { singular, plural } = {}) => {
1714
+ const rule = new Intl.PluralRules(locale).select(value);
1715
+ return `${value} ${rule === 'one' ? singular : plural}`;
1716
+ },
1184
1717
  };
1185
1718
 
1186
1719
  /**
1187
- * Attaches a template binding to the element by hydrating it with the specified method.
1188
1720
  *
1189
- * @param {string} methodName - Name of the hydration method to call
1190
- * @param {BindingHydrator} bindingHydrator - Template binding with $hydrate method
1191
- * @returns {HTMLElement} The underlying HTML element
1192
- * @example
1193
- * const onClick = $binder.attach((event, data) => console.log(data));
1194
- * element.nd.attach('onClick', onClick);
1721
+ * @param {*} value
1722
+ * @param {{ propagation: boolean, reset: boolean} | null} configs
1723
+ * @class ObservableItem
1195
1724
  */
1196
- NDElement.prototype.attach = function(methodName, bindingHydrator) {
1197
- bindingHydrator.$hydrate(this.$element, methodName);
1198
- return this.$element;
1199
- };
1725
+ function ObservableItem(value, configs = null) {
1726
+ value = Validator.isObservable(value) ? value.val() : value;
1200
1727
 
1201
- /**
1202
- * Extends the current NDElement instance with custom methods.
1203
- * Methods are bound to the instance and available for chaining.
1204
- *
1205
- * @param {Object} methods - Object containing method definitions
1206
- * @returns {this} The NDElement instance with added methods for chaining
1207
- * @example
1208
- * element.nd.with({
1209
- * highlight() {
1210
- * this.$element.style.background = 'yellow';
1211
- * return this;
1212
- * }
1213
- * }).highlight().onClick(() => console.log('Clicked'));
1214
- */
1215
- NDElement.prototype.with = function(methods) {
1216
- if (!methods || typeof methods !== 'object') {
1217
- throw new NativeDocumentError('extend() requires an object of methods');
1218
- }
1728
+ this.$previousValue = null;
1729
+ this.$currentValue = value;
1219
1730
  {
1220
- if (!this.$localExtensions) {
1221
- this.$localExtensions = new Map();
1222
- }
1731
+ this.$isCleanedUp = false;
1223
1732
  }
1224
1733
 
1225
- for (const name in methods) {
1226
- const method = methods[name];
1734
+ this.$firstListener = null;
1735
+ this.$listeners = null;
1736
+ this.$watchers = null;
1227
1737
 
1228
- if (typeof method !== 'function') {
1229
- console.warn(`⚠️ extends(): "${name}" is not a function, skipping`);
1230
- continue;
1231
- }
1232
- {
1233
- if (this[name] && !this.$localExtensions.has(name)) {
1234
- DebugManager.warn('NDElement.extend', `Method "${name}" already exists and will be overwritten`);
1235
- }
1236
- this.$localExtensions.set(name, method);
1237
- }
1738
+ this.$memoryId = null;
1238
1739
 
1239
- this[name] = method.bind(this);
1740
+ if(configs) {
1741
+ this.configs = configs;
1742
+ if(configs.reset) {
1743
+ this.$initialValue = Validator.isObject(value) ? deepClone(value) : value;
1744
+ }
1745
+ }
1746
+ {
1747
+ PluginsManager.emit('CreateObservable', this);
1240
1748
  }
1749
+ }
1241
1750
 
1242
- return this;
1243
- };
1751
+ Object.defineProperty(ObservableItem.prototype, '$value', {
1752
+ get() {
1753
+ return this.$currentValue;
1754
+ },
1755
+ set(value) {
1756
+ this.set(value);
1757
+ },
1758
+ configurable: true,
1759
+ });
1760
+
1761
+ ObservableItem.prototype.__$isObservable = true;
1762
+ const noneTrigger = function() {};
1244
1763
 
1245
1764
  /**
1246
- * Extends the NDElement prototype with new methods available to all NDElement instances.
1247
- * Use this to add global methods to all NDElements.
1765
+ * Intercepts and transforms values before they are set on the observable.
1766
+ * The interceptor can modify the value or return undefined to use the original value.
1248
1767
  *
1249
- * @param {Object} methods - Object containing method definitions to add to prototype
1250
- * @returns {typeof NDElement} The NDElement constructor
1251
- * @throws {NativeDocumentError} If methods is not an object or contains non-function values
1768
+ * @param {(value) => any} callback - Interceptor function that receives (newValue, currentValue) and returns the transformed value or undefined
1769
+ * @returns {ObservableItem} The observable instance for chaining
1252
1770
  * @example
1253
- * NDElement.extend({
1254
- * fadeIn() {
1255
- * this.$element.style.opacity = '1';
1256
- * return this;
1257
- * }
1258
- * });
1259
- * // Now all NDElements have .fadeIn() method
1260
- * Div().nd.fadeIn();
1771
+ * const count = Observable(0);
1772
+ * count.intercept((newVal, oldVal) => Math.max(0, newVal)); // Prevent negative values
1261
1773
  */
1262
- NDElement.extend = function(methods) {
1263
- if (!methods || typeof methods !== 'object') {
1264
- throw new NativeDocumentError('NDElement.extend() requires an object of methods');
1265
- }
1774
+ ObservableItem.prototype.intercept = function(callback) {
1775
+ this.$interceptor = callback;
1776
+ this.set = this.$setWithInterceptor;
1777
+ return this;
1778
+ };
1266
1779
 
1267
- if (Array.isArray(methods)) {
1268
- throw new NativeDocumentError('NDElement.extend() requires an object, not an array');
1780
+ ObservableItem.prototype.triggerFirstListener = function(operations) {
1781
+ this.$firstListener(this.$currentValue, this.$previousValue, operations);
1782
+ };
1783
+
1784
+ ObservableItem.prototype.triggerListeners = function(operations) {
1785
+ const $listeners = this.$listeners;
1786
+ const $previousValue = this.$previousValue;
1787
+ const $currentValue = this.$currentValue;
1788
+
1789
+ for(let i = 0, length = $listeners.length; i < length; i++) {
1790
+ $listeners[i]($currentValue, $previousValue, operations);
1269
1791
  }
1792
+ };
1270
1793
 
1271
- const protectedMethods = new Set([
1272
- 'constructor', 'valueOf', '$element', '$observer',
1273
- 'ref', 'remove', 'cleanup', 'with', 'extend', 'attach',
1274
- 'lifecycle', 'mounted', 'unmounted', 'unmountChildren'
1275
- ]);
1794
+ ObservableItem.prototype.triggerWatchers = function(operations) {
1795
+ const $watchers = this.$watchers;
1796
+ const $previousValue = this.$previousValue;
1797
+ const $currentValue = this.$currentValue;
1276
1798
 
1277
- for (const name in methods) {
1278
- if (!Object.hasOwn(methods, name)) {
1279
- continue;
1280
- }
1799
+ const $currentValueCallbacks = $watchers.get($currentValue);
1800
+ const $previousValueCallbacks = $watchers.get($previousValue);
1801
+ if($currentValueCallbacks) {
1802
+ $currentValueCallbacks(true, $previousValue, operations);
1803
+ }
1804
+ if($previousValueCallbacks) {
1805
+ $previousValueCallbacks(false, $currentValue, operations);
1806
+ }
1807
+ };
1281
1808
 
1282
- const method = methods[name];
1809
+ ObservableItem.prototype.triggerAll = function(operations) {
1810
+ this.triggerWatchers(operations);
1811
+ this.triggerListeners(operations);
1812
+ };
1283
1813
 
1284
- if (typeof method !== 'function') {
1285
- DebugManager.warn('NDElement.extend', `"${name}" is not a function, skipping`);
1286
- continue;
1287
- }
1814
+ ObservableItem.prototype.triggerWatchersAndFirstListener = function(operations) {
1815
+ this.triggerWatchers(operations);
1816
+ this.triggerFirstListener(operations);
1817
+ };
1288
1818
 
1289
- if (protectedMethods.has(name)) {
1290
- DebugManager.error('NDElement.extend', `Cannot override protected method "${name}"`);
1291
- throw new NativeDocumentError(`Cannot override protected method "${name}"`);
1819
+ ObservableItem.prototype.assocTrigger = function() {
1820
+ this.$firstListener = null;
1821
+ if(this.$watchers?.size && this.$listeners?.length) {
1822
+ this.trigger = (this.$listeners.length === 1) ? this.triggerWatchersAndFirstListener : this.triggerAll;
1823
+ return;
1824
+ }
1825
+ if(this.$listeners?.length) {
1826
+ if(this.$listeners.length === 1) {
1827
+ this.$firstListener = this.$listeners[0];
1828
+ this.trigger = this.triggerFirstListener;
1292
1829
  }
1293
-
1294
- if (NDElement.prototype[name]) {
1295
- DebugManager.warn('NDElement.extend', `Overwriting existing prototype method "${name}"`);
1830
+ else {
1831
+ this.trigger = this.triggerListeners;
1296
1832
  }
1833
+ return;
1834
+ }
1835
+ if(this.$watchers?.size) {
1836
+ this.trigger = this.triggerWatchers;
1837
+ return;
1838
+ }
1839
+ this.trigger = noneTrigger;
1840
+ };
1841
+ ObservableItem.prototype.trigger = noneTrigger;
1297
1842
 
1298
- NDElement.prototype[name] = method;
1843
+ ObservableItem.prototype.$updateWithNewValue = function(newValue) {
1844
+ newValue = newValue?.__$isObservable ? newValue.val() : newValue;
1845
+ if(this.$currentValue === newValue) {
1846
+ return;
1299
1847
  }
1848
+ this.$previousValue = this.$currentValue;
1849
+ this.$currentValue = newValue;
1300
1850
  {
1301
- PluginsManager.emit('NDElementExtended', methods);
1851
+ PluginsManager.emit('ObservableBeforeChange', this);
1852
+ }
1853
+ this.trigger();
1854
+ this.$previousValue = null;
1855
+ {
1856
+ PluginsManager.emit('ObservableAfterChange', this);
1302
1857
  }
1858
+ };
1303
1859
 
1304
- return NDElement;
1860
+ /**
1861
+ * @param {*} data
1862
+ */
1863
+ ObservableItem.prototype.$setWithInterceptor = function(data) {
1864
+ let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
1865
+ const result = this.$interceptor(newValue, this.$currentValue);
1866
+
1867
+ if (result !== undefined) {
1868
+ newValue = result;
1869
+ }
1870
+
1871
+ this.$updateWithNewValue(newValue);
1305
1872
  };
1306
1873
 
1307
- function TemplateBinding(hydrate) {
1308
- this.$hydrate = hydrate;
1309
- }
1874
+ /**
1875
+ * @param {*} data
1876
+ */
1877
+ ObservableItem.prototype.$basicSet = function(data) {
1878
+ let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
1879
+ this.$updateWithNewValue(newValue);
1880
+ };
1310
1881
 
1311
- TemplateBinding.prototype.__$isTemplateBinding = true;
1882
+ ObservableItem.prototype.set = ObservableItem.prototype.$basicSet;
1312
1883
 
1313
- const COMMON_NODE_TYPES = {
1314
- ELEMENT: 1,
1315
- TEXT: 3,
1316
- COMMENT: 8,
1317
- DOCUMENT_FRAGMENT: 11
1884
+ ObservableItem.prototype.val = function() {
1885
+ return this.$currentValue;
1318
1886
  };
1319
1887
 
1320
- const Validator = {
1321
- isObservable(value) {
1322
- return value?.__$isObservable;
1323
- },
1324
- isTemplateBinding(value) {
1325
- return value?.__$isTemplateBinding;
1326
- },
1327
- isObservableWhenResult(value) {
1328
- return value && (value.__$isObservableWhen || (typeof value === 'object' && '$target' in value && '$observer' in value));
1329
- },
1330
- isArrayObservable(value) {
1331
- return value?.__$isObservableArray;
1332
- },
1333
- isProxy(value) {
1334
- return value?.__isProxy__
1335
- },
1336
- isObservableOrProxy(value) {
1337
- return Validator.isObservable(value) || Validator.isProxy(value);
1338
- },
1339
- isAnchor(value) {
1340
- return value?.__Anchor__
1341
- },
1342
- isObservableChecker(value) {
1343
- return value?.__$isObservableChecker || value instanceof ObservableChecker;
1344
- },
1345
- isArray(value) {
1346
- return Array.isArray(value);
1347
- },
1348
- isString(value) {
1349
- return typeof value === 'string';
1350
- },
1351
- isNumber(value) {
1352
- return typeof value === 'number';
1353
- },
1354
- isBoolean(value) {
1355
- return typeof value === 'boolean';
1356
- },
1357
- isFunction(value) {
1358
- return typeof value === 'function';
1359
- },
1360
- isAsyncFunction(value) {
1361
- return typeof value === 'function' && value.constructor.name === 'AsyncFunction';
1362
- },
1363
- isObject(value) {
1364
- return typeof value === 'object' && value !== null;
1365
- },
1366
- isJson(value) {
1367
- return !(typeof value !== 'object' || value === null || Array.isArray(value) || value.constructor.name !== 'Object')
1368
- },
1369
- isElement(value) {
1370
- return value && (
1371
- value.nodeType === COMMON_NODE_TYPES.ELEMENT ||
1372
- value.nodeType === COMMON_NODE_TYPES.TEXT ||
1373
- value.nodeType === COMMON_NODE_TYPES.DOCUMENT_FRAGMENT ||
1374
- value.nodeType === COMMON_NODE_TYPES.COMMENT
1375
- );
1376
- },
1377
- isFragment(value) {
1378
- return value?.nodeType === COMMON_NODE_TYPES.DOCUMENT_FRAGMENT;
1379
- },
1380
- isStringOrObservable(value) {
1381
- return this.isString(value) || this.isObservable(value);
1382
- },
1383
- isValidChild(child) {
1384
- return child === null ||
1385
- this.isElement(child) ||
1386
- this.isObservable(child) ||
1387
- this.isNDElement(child) ||
1388
- ['string', 'number', 'boolean'].includes(typeof child);
1389
- },
1390
- isNDElement(child) {
1391
- return child?.__$isNDElement || child instanceof NDElement;
1392
- },
1393
- isValidChildren(children) {
1394
- if (!Array.isArray(children)) {
1395
- children = [children];
1888
+ ObservableItem.prototype.disconnectAll = function() {
1889
+ this.$listeners?.splice(0);
1890
+ this.$previousValue = null;
1891
+ this.$currentValue = null;
1892
+ if(this.$watchers) {
1893
+ for (const [_, watchValueList] of this.$watchers) {
1894
+ if(Validator.isArray(watchValueList)) {
1895
+ watchValueList.splice(0);
1896
+ }
1396
1897
  }
1898
+ }
1899
+ this.$watchers?.clear();
1900
+ this.$listeners = null;
1901
+ this.$watchers = null;
1902
+ this.trigger = noneTrigger;
1903
+ };
1397
1904
 
1398
- const invalid = children.filter(child => !this.isValidChild(child));
1399
- return invalid.length === 0;
1400
- },
1401
- validateChildren(children) {
1402
- if (!Array.isArray(children)) {
1403
- children = [children];
1404
- }
1905
+ /**
1906
+ * Registers a cleanup callback that will be executed when the observable is cleaned up.
1907
+ * Useful for disposing resources, removing event listeners, or other cleanup tasks.
1908
+ *
1909
+ * @param {Function} callback - Cleanup function to execute on observable disposal
1910
+ * @example
1911
+ * const obs = Observable(0);
1912
+ * obs.onCleanup(() => console.log('Cleaned up!'));
1913
+ * obs.cleanup(); // Logs: "Cleaned up!"
1914
+ */
1915
+ ObservableItem.prototype.onCleanup = function(callback) {
1916
+ this.$cleanupListeners = this.$cleanupListeners ?? [];
1917
+ this.$cleanupListeners.push(callback);
1918
+ };
1405
1919
 
1406
- const invalid = children.filter(child => !this.isValidChild(child));
1407
- if (invalid.length > 0) {
1408
- throw new NativeDocumentError(`Invalid children detected: ${invalid.map(i => typeof i).join(', ')}`);
1920
+ ObservableItem.prototype.cleanup = function() {
1921
+ if (this.$cleanupListeners) {
1922
+ for (let i = 0; i < this.$cleanupListeners.length; i++) {
1923
+ this.$cleanupListeners[i]();
1409
1924
  }
1925
+ this.$cleanupListeners = null;
1926
+ }
1927
+ MemoryManager.unregister(this.$memoryId);
1928
+ this.disconnectAll();
1929
+ {
1930
+ this.$isCleanedUp = true;
1931
+ }
1932
+ delete this.$value;
1933
+ };
1410
1934
 
1411
- return children;
1412
- },
1413
- /**
1414
- * Check if the data contains observables.
1415
- * @param {Array|Object} data
1416
- * @returns {boolean}
1417
- */
1418
- containsObservables(data) {
1419
- if(!data) {
1420
- return false;
1421
- }
1422
- return Validator.isObject(data)
1423
- && Object.values(data).some(value => Validator.isObservable(value));
1424
- },
1425
- /**
1426
- * Check if the data contains an observable reference.
1427
- * @param {string} data
1428
- * @returns {boolean}
1429
- */
1430
- containsObservableReference(data) {
1431
- if(!data || typeof data !== 'string') {
1432
- return false;
1935
+ /**
1936
+ *
1937
+ * @param {Function} callback
1938
+ * @returns {(function(): void)}
1939
+ */
1940
+ ObservableItem.prototype.subscribe = function(callback) {
1941
+ {
1942
+ if (this.$isCleanedUp) {
1943
+ DebugManager.warn('Observable subscription', '⚠️ Attempted to subscribe to a cleaned up observable.');
1944
+ return;
1433
1945
  }
1434
- return /\{\{#ObItem::\([0-9]+\)\}\}/.test(data);
1435
- },
1436
- validateAttributes(attributes) {},
1437
-
1438
- validateEventCallback(callback) {
1439
1946
  if (typeof callback !== 'function') {
1440
- throw new NativeDocumentError('Event callback must be a function');
1947
+ throw new NativeDocumentError('Callback must be a function');
1441
1948
  }
1442
1949
  }
1443
- };
1444
- {
1445
- Validator.validateAttributes = function(attributes) {
1446
- if (!attributes || typeof attributes !== 'object') {
1447
- return attributes;
1448
- }
1950
+ this.$listeners = this.$listeners ?? [];
1449
1951
 
1450
- const reserved = [];
1451
- const foundReserved = Object.keys(attributes).filter(key => reserved.includes(key));
1952
+ this.$listeners.push(callback);
1953
+ this.assocTrigger();
1954
+ {
1955
+ PluginsManager.emit('ObservableSubscribe', this);
1956
+ }
1957
+ };
1452
1958
 
1453
- if (foundReserved.length > 0) {
1454
- DebugManager.warn('Validator', `Reserved attributes found: ${foundReserved.join(', ')}`);
1455
- }
1959
+ /**
1960
+ * Watches for a specific value and executes callback when the observable equals that value.
1961
+ * Creates a watcher that only triggers when the observable changes to the specified value.
1962
+ *
1963
+ * @param {*} value - The value to watch for
1964
+ * @param {(value) => void|ObservableItem} callback - Callback function or observable to set when value matches
1965
+ * @example
1966
+ * const status = Observable('idle');
1967
+ * status.on('loading', () => console.log('Started loading'));
1968
+ * status.on('error', isError); // Set another observable
1969
+ */
1970
+ ObservableItem.prototype.on = function(value, callback) {
1971
+ this.$watchers = this.$watchers ?? new Map();
1456
1972
 
1457
- return attributes;
1458
- };
1459
- }
1973
+ let watchValueList = this.$watchers.get(value);
1460
1974
 
1461
- function Anchor(name, isUniqueChild = false) {
1462
- const anchorFragment = document.createDocumentFragment();
1463
- anchorFragment.__Anchor__ = true;
1975
+ if(callback.__$isObservable) {
1976
+ callback = callback.set.bind(callback);
1977
+ }
1464
1978
 
1465
- const anchorStart = document.createComment('Anchor Start : '+name);
1466
- const anchorEnd = document.createComment('/ Anchor End '+name);
1979
+ if(!watchValueList) {
1980
+ watchValueList = callback;
1981
+ this.$watchers.set(value, callback);
1982
+ } else if(!Validator.isArray(watchValueList.list)) {
1983
+ watchValueList = [watchValueList, callback];
1984
+ callback = (value) => {
1985
+ for(let i = 0, length = watchValueList.length; i < length; i++) {
1986
+ watchValueList[i](value);
1987
+ }
1988
+ };
1989
+ callback.list = watchValueList;
1990
+ this.$watchers.set(value, callback);
1991
+ } else {
1992
+ watchValueList.list.push(callback);
1993
+ }
1467
1994
 
1468
- anchorFragment.appendChild(anchorStart);
1469
- anchorFragment.appendChild(anchorEnd);
1995
+ this.assocTrigger();
1996
+ };
1470
1997
 
1471
- anchorFragment.nativeInsertBefore = anchorFragment.insertBefore;
1472
- anchorFragment.nativeAppendChild = anchorFragment.appendChild;
1473
- anchorFragment.nativeAppend = anchorFragment.append;
1998
+ /**
1999
+ * Removes a watcher for a specific value. If no callback is provided, removes all watchers for that value.
2000
+ *
2001
+ * @param {*} value - The value to stop watching
2002
+ * @param {Function} [callback] - Specific callback to remove. If omitted, removes all watchers for this value
2003
+ * @example
2004
+ * const status = Observable('idle');
2005
+ * const handler = () => console.log('Loading');
2006
+ * status.on('loading', handler);
2007
+ * status.off('loading', handler); // Remove specific handler
2008
+ * status.off('loading'); // Remove all handlers for 'loading'
2009
+ */
2010
+ ObservableItem.prototype.off = function(value, callback) {
2011
+ if(!this.$watchers) return;
1474
2012
 
1475
- const isParentUniqueChild = (parent) => (isUniqueChild || (parent.firstChild === anchorStart && parent.lastChild === anchorEnd));
2013
+ const watchValueList = this.$watchers.get(value);
2014
+ if(!watchValueList) return;
1476
2015
 
1477
- const insertBefore = function(parent, child, target) {
1478
- const childElement = Validator.isElement(child) ? child : ElementCreator.getChild(child);
1479
- if(parent === anchorFragment) {
1480
- parent.nativeInsertBefore(childElement, target);
1481
- return;
1482
- }
1483
- if(isParentUniqueChild(parent) && target === anchorEnd) {
1484
- parent.append(childElement, target);
1485
- return;
1486
- }
1487
- parent.insertBefore(childElement, target);
1488
- };
2016
+ if(!callback || !Array.isArray(watchValueList.list)) {
2017
+ this.$watchers?.delete(value);
2018
+ this.assocTrigger();
2019
+ return;
2020
+ }
2021
+ const index = watchValueList.indexOf(callback);
2022
+ watchValueList?.splice(index, 1);
2023
+ if(watchValueList.length === 1) {
2024
+ this.$watchers.set(value, watchValueList[0]);
2025
+ }
2026
+ else if(watchValueList.length === 0) {
2027
+ this.$watchers?.delete(value);
2028
+ }
2029
+ this.assocTrigger();
2030
+ };
1489
2031
 
1490
- anchorFragment.appendElement = function(child, before = null) {
1491
- const parentNode = anchorStart.parentNode;
1492
- const targetBefore = before || anchorEnd;
1493
- if(parentNode === anchorFragment) {
1494
- parentNode.nativeInsertBefore(child, targetBefore);
1495
- return;
1496
- }
1497
- parentNode?.insertBefore(child, targetBefore);
1498
- };
2032
+ /**
2033
+ * Subscribes to the observable but automatically unsubscribes after the first time the predicate matches.
2034
+ *
2035
+ * @param {(value) => Boolean|any} predicate - Value to match or function that returns true when condition is met
2036
+ * @param {(value) => void} callback - Callback to execute when predicate matches, receives the matched value
2037
+ * @example
2038
+ * const status = Observable('loading');
2039
+ * status.once('ready', (val) => console.log('Ready!'));
2040
+ * status.once(val => val === 'error', (val) => console.log('Error occurred'));
2041
+ */
2042
+ ObservableItem.prototype.once = function(predicate, callback) {
2043
+ const fn = typeof predicate === 'function' ? predicate : (v) => v === predicate;
1499
2044
 
1500
- anchorFragment.appendChild = function(child, before = null) {
1501
- const parent = anchorEnd.parentNode;
1502
- if(!parent) {
1503
- DebugManager.error('Anchor', 'Anchor : parent not found', child);
1504
- return;
2045
+ const handler = (val) => {
2046
+ if (fn(val)) {
2047
+ this.unsubscribe(handler);
2048
+ callback(val);
1505
2049
  }
1506
- before = before ?? anchorEnd;
1507
- insertBefore(parent, child, before);
1508
2050
  };
2051
+ this.subscribe(handler);
2052
+ };
1509
2053
 
1510
- anchorFragment.append = function(...args ) {
1511
- return anchorFragment.appendChild(args);
1512
- };
2054
+ /**
2055
+ * Unsubscribe from an observable.
2056
+ * @param {Function} callback
2057
+ */
2058
+ ObservableItem.prototype.unsubscribe = function(callback) {
2059
+ if(!this.$listeners) return;
2060
+ const index = this.$listeners.indexOf(callback);
2061
+ if (index > -1) {
2062
+ this.$listeners.splice(index, 1);
2063
+ }
2064
+ this.assocTrigger();
2065
+ {
2066
+ PluginsManager.emit('ObservableUnsubscribe', this);
2067
+ }
2068
+ };
1513
2069
 
1514
- anchorFragment.removeChildren = async function() {
1515
- const parent = anchorEnd.parentNode;
1516
- if(parent === anchorFragment) {
1517
- return;
1518
- }
1519
- // if(isParentUniqueChild(parent)) {
1520
- // parent.replaceChildren(anchorStart, anchorEnd);
1521
- // return;
1522
- // }
2070
+ /**
2071
+ * Create an Observable checker instance
2072
+ * @param callback
2073
+ * @returns {ObservableChecker}
2074
+ */
2075
+ ObservableItem.prototype.check = function(callback) {
2076
+ return new ObservableChecker(this, callback)
2077
+ };
1523
2078
 
1524
- let itemToRemove = anchorStart.nextSibling, tempItem;
1525
- const removes = [];
1526
- while(itemToRemove && itemToRemove !== anchorEnd) {
1527
- tempItem = itemToRemove.nextSibling;
1528
- removes.push(itemToRemove.remove());
1529
- itemToRemove = tempItem;
1530
- }
1531
- await Promise.all(removes);
1532
- };
2079
+ ObservableItem.prototype.transform = ObservableItem.prototype.check;
2080
+ ObservableItem.prototype.pluck = ObservableItem.prototype.check;
2081
+ ObservableItem.prototype.is = ObservableItem.prototype.check;
2082
+ ObservableItem.prototype.select = ObservableItem.prototype.check;
1533
2083
 
1534
- anchorFragment.remove = async function() {
1535
- const parent = anchorEnd.parentNode;
1536
- if(parent === anchorFragment) {
1537
- return;
1538
- }
1539
- let itemToRemove = anchorStart.nextSibling, tempItem;
1540
- const allItemToRemove = [];
1541
- const removes = [];
1542
- while(itemToRemove && itemToRemove !== anchorEnd) {
1543
- tempItem = itemToRemove.nextSibling;
1544
- allItemToRemove.push(itemToRemove);
1545
- removes.push(itemToRemove.remove());
1546
- itemToRemove = tempItem;
1547
- }
1548
- await Promise.all(removes);
1549
- anchorFragment.nativeAppend(...allItemToRemove);
1550
- };
2084
+ /**
2085
+ * Gets a property value from the observable's current value.
2086
+ * If the property is an observable, returns its value.
2087
+ *
2088
+ * @param {string|number} key - Property key to retrieve
2089
+ * @returns {*} The value of the property, unwrapped if it's an observable
2090
+ * @example
2091
+ * const user = Observable({ name: 'John', age: Observable(25) });
2092
+ * user.get('name'); // 'John'
2093
+ * user.get('age'); // 25 (unwrapped from observable)
2094
+ */
2095
+ ObservableItem.prototype.get = function(key) {
2096
+ const item = this.$currentValue[key];
2097
+ return Validator.isObservable(item) ? item.val() : item;
2098
+ };
1551
2099
 
1552
- anchorFragment.removeWithAnchors = async function() {
1553
- await anchorFragment.removeChildren();
1554
- anchorStart.remove();
1555
- anchorEnd.remove();
1556
- };
2100
+ /**
2101
+ * Creates an ObservableWhen that represents whether the observable equals a specific value.
2102
+ * Returns an object that can be subscribed to and will emit true/false.
2103
+ *
2104
+ * @param {*} value - The value to compare against
2105
+ * @returns {ObservableWhen} An ObservableWhen instance that tracks when the observable equals the value
2106
+ * @example
2107
+ * const status = Observable('idle');
2108
+ * const isLoading = status.when('loading');
2109
+ * isLoading.subscribe(active => console.log('Loading:', active));
2110
+ * status.set('loading'); // Logs: "Loading: true"
2111
+ */
2112
+ ObservableItem.prototype.when = function(value) {
2113
+ return new ObservableWhen(this, value);
2114
+ };
1557
2115
 
1558
- anchorFragment.replaceContent = async function(child) {
1559
- const childElement = Validator.isElement(child) ? child : ElementCreator.getChild(child);
1560
- const parent = anchorEnd.parentNode;
1561
- if(!parent) {
1562
- return;
1563
- }
1564
- // if(isParentUniqueChild(parent)) {
1565
- // parent.replaceChildren(anchorStart, childElement, anchorEnd);
1566
- // return;
1567
- // }
1568
- await anchorFragment.removeChildren();
1569
- parent.insertBefore(childElement, anchorEnd);
1570
- };
2116
+ /**
2117
+ * Compares the observable's current value with another value or observable.
2118
+ *
2119
+ * @param {*|ObservableItem} other - Value or observable to compare against
2120
+ * @returns {boolean} True if values are equal
2121
+ * @example
2122
+ * const a = Observable(5);
2123
+ * const b = Observable(5);
2124
+ * a.equals(5); // true
2125
+ * a.equals(b); // true
2126
+ * a.equals(10); // false
2127
+ */
2128
+ ObservableItem.prototype.equals = function(other) {
2129
+ if(Validator.isObservable(other)) {
2130
+ return this.$currentValue === other.$currentValue;
2131
+ }
2132
+ return this.$currentValue === other;
2133
+ };
1571
2134
 
1572
- anchorFragment.setContent = anchorFragment.replaceContent;
2135
+ /**
2136
+ * Converts the observable's current value to a boolean.
2137
+ *
2138
+ * @returns {boolean} The boolean representation of the current value
2139
+ * @example
2140
+ * const count = Observable(0);
2141
+ * count.toBool(); // false
2142
+ * count.set(5);
2143
+ * count.toBool(); // true
2144
+ */
2145
+ ObservableItem.prototype.toBool = function() {
2146
+ return !!this.$currentValue;
2147
+ };
1573
2148
 
1574
- anchorFragment.insertBefore = function(child, anchor = null) {
1575
- anchorFragment.appendChild(child, anchor);
1576
- };
2149
+ /**
2150
+ * Toggles the boolean value of the observable (false becomes true, true becomes false).
2151
+ *
2152
+ * @example
2153
+ * const isOpen = Observable(false);
2154
+ * isOpen.toggle(); // Now true
2155
+ * isOpen.toggle(); // Now false
2156
+ */
2157
+ ObservableItem.prototype.toggle = function() {
2158
+ this.set(!this.$currentValue);
2159
+ };
1577
2160
 
2161
+ /**
2162
+ * Resets the observable to its initial value.
2163
+ * Only works if the observable was created with { reset: true } config.
2164
+ *
2165
+ * @example
2166
+ * const count = Observable(0, { reset: true });
2167
+ * count.set(10);
2168
+ * count.reset(); // Back to 0
2169
+ */
2170
+ ObservableItem.prototype.reset = function() {
2171
+ if(!this.configs?.reset) {
2172
+ return;
2173
+ }
2174
+ const resetValue = (Validator.isObject(this.$initialValue))
2175
+ ? deepClone(this.$initialValue, (observable) => {
2176
+ observable.reset();
2177
+ })
2178
+ : this.$initialValue;
2179
+ this.set(resetValue);
2180
+ };
1578
2181
 
1579
- anchorFragment.endElement = function() {
1580
- return anchorEnd;
1581
- };
2182
+ /**
2183
+ * Returns a string representation of the observable's current value.
2184
+ *
2185
+ * @returns {string} String representation of the current value
2186
+ */
2187
+ ObservableItem.prototype.toString = function() {
2188
+ return String(this.$currentValue);
2189
+ };
1582
2190
 
1583
- anchorFragment.startElement = function() {
1584
- return anchorStart;
1585
- };
1586
- anchorFragment.restore = function() {
1587
- anchorFragment.appendChild(anchorFragment);
1588
- };
1589
- anchorFragment.clear = anchorFragment.remove;
1590
- anchorFragment.detach = anchorFragment.remove;
2191
+ /**
2192
+ * Returns the primitive value of the observable (its current value).
2193
+ * Called automatically in type coercion contexts.
2194
+ *
2195
+ * @returns {*} The current value of the observable
2196
+ */
2197
+ ObservableItem.prototype.valueOf = function() {
2198
+ return this.$currentValue;
2199
+ };
1591
2200
 
1592
- anchorFragment.getByIndex = function(index) {
1593
- let currentNode = anchorStart;
1594
- for(let i = 0; i <= index; i++) {
1595
- if(!currentNode.nextSibling) {
1596
- return null;
1597
- }
1598
- currentNode = currentNode.nextSibling;
1599
- }
1600
- return currentNode !== anchorStart ? currentNode : null;
1601
- };
1602
2201
 
1603
- return anchorFragment;
1604
- }
1605
2202
  /**
2203
+ * Creates a derived observable that formats the current value using Intl.
2204
+ * Automatically reacts to both value changes and locale changes (Store.__nd.locale).
2205
+ *
2206
+ * @param {string | Function} type - Format type or custom formatter function
2207
+ * @param {Object} [options={}] - Options passed to the formatter
2208
+ * @returns {ObservableItem<string>}
2209
+ *
2210
+ * @example
2211
+ * // Currency
2212
+ * price.format('currency') // "15 000 FCFA"
2213
+ * price.format('currency', { currency: 'EUR' }) // "15 000,00 €"
2214
+ * price.format('currency', { notation: 'compact' }) // "15 K FCFA"
2215
+ *
2216
+ * // Number
2217
+ * count.format('number') // "15 000"
2218
+ *
2219
+ * // Percent
2220
+ * rate.format('percent') // "15,0 %"
2221
+ * rate.format('percent', { decimals: 2 }) // "15,00 %"
2222
+ *
2223
+ * // Date
2224
+ * date.format('date') // "3 mars 2026"
2225
+ * date.format('date', { dateStyle: 'full' }) // "mardi 3 mars 2026"
2226
+ * date.format('date', { format: 'DD/MM/YYYY' }) // "03/03/2026"
2227
+ * date.format('date', { format: 'DD MMM YYYY' }) // "03 mar 2026"
2228
+ * date.format('date', { format: 'DD MMMM YYYY' }) // "03 mars 2026"
2229
+ *
2230
+ * // Time
2231
+ * date.format('time') // "20:30"
2232
+ * date.format('time', { second: '2-digit' }) // "20:30:00"
2233
+ * date.format('time', { format: 'HH:mm:ss' }) // "20:30:00"
2234
+ *
2235
+ * // Datetime
2236
+ * date.format('datetime') // "3 mars 2026, 20:30"
2237
+ * date.format('datetime', { dateStyle: 'full' }) // "mardi 3 mars 2026, 20:30"
2238
+ * date.format('datetime', { format: 'DD/MM/YYYY HH:mm' }) // "03/03/2026 20:30"
2239
+ *
2240
+ * // Relative
2241
+ * date.format('relative') // "dans 11 jours"
2242
+ * date.format('relative', { unit: 'month' }) // "dans 1 mois"
1606
2243
  *
1607
- * @param {HTMLElement|DocumentFragment|Text|String|Array} children
1608
- * @param {{ parent?: HTMLElement, name?: String}} configs
1609
- * @returns {DocumentFragment}
2244
+ * // Plural
2245
+ * count.format('plural', { singular: 'billet', plural: 'billets' }) // "3 billets"
2246
+ *
2247
+ * // Custom formatter
2248
+ * price.format(value => `${value.toLocaleString()} FCFA`)
2249
+ *
2250
+ * // Reacts to locale changes automatically
2251
+ * Store.setLocale('en-US');
1610
2252
  */
1611
- function createPortal(children, { parent, name = 'unnamed' } = {}) {
1612
- const anchor = Anchor('Portal '+name);
1613
- anchor.appendChild(ElementCreator.getChild(children));
2253
+ ObservableItem.prototype.format = function(type, options = {}) {
2254
+ const self = this;
1614
2255
 
1615
- (parent || document.body).appendChild(anchor);
1616
- return anchor;
1617
- }
2256
+ if (typeof type === 'function') {
2257
+ return new ObservableChecker(self, type);
2258
+ }
1618
2259
 
1619
- DocumentFragment.prototype.setAttribute = () => {};
2260
+ {
2261
+ if (!Formatters[type]) {
2262
+ throw new NativeDocumentError(
2263
+ `Observable.format : unknown type '${type}'. Available : ${Object.keys(Formatters).join(', ')}.`
2264
+ );
2265
+ }
2266
+ }
1620
2267
 
1621
- const BOOLEAN_ATTRIBUTES = new Set([
1622
- 'checked',
1623
- 'selected',
1624
- 'disabled',
1625
- 'readonly',
1626
- 'required',
1627
- 'autofocus',
1628
- 'multiple',
1629
- 'autocomplete',
1630
- 'hidden',
1631
- 'contenteditable',
1632
- 'spellcheck',
1633
- 'translate',
1634
- 'draggable',
1635
- 'async',
1636
- 'defer',
1637
- 'autoplay',
1638
- 'controls',
1639
- 'loop',
1640
- 'muted',
1641
- 'download',
1642
- 'reversed',
1643
- 'open',
1644
- 'default',
1645
- 'formnovalidate',
1646
- 'novalidate',
1647
- 'scoped',
1648
- 'itemscope',
1649
- 'allowfullscreen',
1650
- 'allowpaymentrequest',
1651
- 'playsinline'
1652
- ]);
2268
+ const formatter = Formatters[type];
2269
+ const localeObservable = Store.follow('locale');
2270
+
2271
+ return Observable.computed(() => formatter(self.val(), localeObservable.val(), options),
2272
+ [self, localeObservable]
2273
+ );
2274
+ };
2275
+
2276
+ ObservableItem.prototype.persist = function(key, options = {}) {
2277
+ let value = $getFromStorage(key, this.$currentValue);
2278
+ if(options.get) {
2279
+ value = options.get(value);
2280
+ }
2281
+ this.set(value);
2282
+ const saver = $saveToStorage(this.$currentValue);
2283
+ this.subscribe((newValue) => {
2284
+ saver(key, options.set ? options.set(newValue) : newValue);
2285
+ });
2286
+ return this;
2287
+ };
1653
2288
 
1654
2289
  /**
1655
2290
  *
@@ -1853,6 +2488,12 @@ var NativeDocument = (function (exports) {
1853
2488
  return element;
1854
2489
  }
1855
2490
 
2491
+ function TemplateBinding(hydrate) {
2492
+ this.$hydrate = hydrate;
2493
+ }
2494
+
2495
+ TemplateBinding.prototype.__$isTemplateBinding = true;
2496
+
1856
2497
  String.prototype.toNdElement = function () {
1857
2498
  const formattedChild = this.resolveObservableTemplate ? this.resolveObservableTemplate() : this;
1858
2499
  if(Validator.isString(formattedChild)) {
@@ -3640,6 +4281,68 @@ var NativeDocument = (function (exports) {
3640
4281
  });
3641
4282
  };
3642
4283
 
4284
+ ObservableArray.prototype.deepSubscribe = function(callback) {
4285
+ const updatedValue = nextTick(() => callback(this.val()));
4286
+ const $listeners = new WeakMap();
4287
+
4288
+ const bindItem = (item) => {
4289
+ if ($listeners.has(item)) {
4290
+ return;
4291
+ }
4292
+ if (item?.__$isObservableArray) {
4293
+ $listeners.set(item, item.deepSubscribe(updatedValue));
4294
+ return;
4295
+ }
4296
+ if (item?.__$isObservable) {
4297
+ item.subscribe(updatedValue);
4298
+ $listeners.set(item, () => item.unsubscribe(updatedValue));
4299
+ }
4300
+ };
4301
+
4302
+ const unbindItem = (item) => {
4303
+ const unsub = $listeners.get(item);
4304
+ if (unsub) {
4305
+ unsub();
4306
+ $listeners.delete(item);
4307
+ }
4308
+ };
4309
+
4310
+ this.$currentValue.forEach(bindItem);
4311
+ this.subscribe(updatedValue);
4312
+
4313
+ this.subscribe((items, _, operations) => {
4314
+ switch (operations?.action) {
4315
+ case 'push':
4316
+ case 'unshift':
4317
+ operations.args.forEach(bindItem);
4318
+ break;
4319
+
4320
+ case 'splice': {
4321
+ const [start, deleteCount, ...newItems] = operations.args;
4322
+ operations.result?.forEach(unbindItem);
4323
+ newItems.forEach(bindItem);
4324
+ break;
4325
+ }
4326
+
4327
+ case 'remove':
4328
+ unbindItem(operations.result);
4329
+ break;
4330
+
4331
+ case 'merge':
4332
+ operations.args.forEach(bindItem);
4333
+ break;
4334
+
4335
+ case 'clear':
4336
+ this.$currentValue.forEach(unbindItem);
4337
+ break;
4338
+ }
4339
+ });
4340
+
4341
+ return () => {
4342
+ this.$currentValue.forEach(unbindItem);
4343
+ };
4344
+ };
4345
+
3643
4346
  /**
3644
4347
  * Creates an observable array with reactive array methods.
3645
4348
  * All mutations trigger updates automatically.
@@ -3679,10 +4382,71 @@ var NativeDocument = (function (exports) {
3679
4382
  return batch;
3680
4383
  };
3681
4384
 
3682
- const ObservableObjectValue = function(data) {
4385
+ const ObservableObject = function(target, configs) {
4386
+ ObservableItem.call(this, target);
4387
+ this.$observables = {};
4388
+ this.configs = configs;
4389
+
4390
+ this.$load(target);
4391
+
4392
+ for(const name in target) {
4393
+ if(!Object.hasOwn(this, name)) {
4394
+ Object.defineProperty(this, name, {
4395
+ get: () => this.$observables[name],
4396
+ set: (value) => this.$observables[name].set(value)
4397
+ });
4398
+ }
4399
+ }
4400
+
4401
+ };
4402
+
4403
+ ObservableObject.prototype = Object.create(ObservableItem.prototype);
4404
+
4405
+ Object.defineProperty(ObservableObject, '$value', {
4406
+ get() {
4407
+ return this.val();
4408
+ },
4409
+ set(value) {
4410
+ this.set(value);
4411
+ }
4412
+ });
4413
+
4414
+ ObservableObject.prototype.__$isObservableObject = true;
4415
+ ObservableObject.prototype.__isProxy__ = true;
4416
+
4417
+ ObservableObject.prototype.$load = function(initialValue) {
4418
+ const configs = this.configs;
4419
+ for(const key in initialValue) {
4420
+ const itemValue = initialValue[key];
4421
+ if(Array.isArray(itemValue)) {
4422
+ if(configs?.deep !== false) {
4423
+ const mappedItemValue = itemValue.map(item => {
4424
+ if(Validator.isJson(item)) {
4425
+ return Observable.json(item, configs);
4426
+ }
4427
+ if(Validator.isArray(item)) {
4428
+ return Observable.array(item, configs);
4429
+ }
4430
+ return Observable(item, configs);
4431
+ });
4432
+ this.$observables[key] = Observable.array(mappedItemValue, configs);
4433
+ continue;
4434
+ }
4435
+ this.$observables[key] = Observable.array(itemValue, configs);
4436
+ continue;
4437
+ }
4438
+ if(Validator.isObservable(itemValue) || Validator.isProxy(itemValue)) {
4439
+ this.$observables[key] = itemValue;
4440
+ continue;
4441
+ }
4442
+ this.$observables[key] = (typeof itemValue === 'object') ? Observable.object(itemValue, configs) : Observable(itemValue, configs);
4443
+ }
4444
+ };
4445
+
4446
+ ObservableObject.prototype.val = function() {
3683
4447
  const result = {};
3684
- for(const key in data) {
3685
- const dataItem = data[key];
4448
+ for(const key in this.$observables) {
4449
+ const dataItem = this.$observables[key];
3686
4450
  if(Validator.isObservable(dataItem)) {
3687
4451
  let value = dataItem.val();
3688
4452
  if(Array.isArray(value)) {
@@ -3705,9 +4469,10 @@ var NativeDocument = (function (exports) {
3705
4469
  }
3706
4470
  return result;
3707
4471
  };
4472
+ ObservableObject.prototype.$val = ObservableObject.prototype.val;
3708
4473
 
3709
- const ObservableGet = function(target, property) {
3710
- const item = target[property];
4474
+ ObservableObject.prototype.get = function(property) {
4475
+ const item = this.$observables[property];
3711
4476
  if(Validator.isObservable(item)) {
3712
4477
  return item.val();
3713
4478
  }
@@ -3716,100 +4481,88 @@ var NativeDocument = (function (exports) {
3716
4481
  }
3717
4482
  return item;
3718
4483
  };
4484
+ ObservableObject.prototype.$get = ObservableObject.prototype.get;
3719
4485
 
3720
- /**
3721
- * Creates an observable proxy for an object where each property becomes an observable.
3722
- * Properties can be accessed directly or via getter methods.
3723
- *
3724
- * @param {Object} initialValue - Initial object value
3725
- * @param {Object|null} [configs=null] - Configuration options
3726
- * // @param {boolean} [configs.propagation=true] - Whether changes propagate to parent
3727
- * @param {boolean} [configs.deep=false] - Whether to make nested objects observable
3728
- * @param {boolean} [configs.reset=false] - Whether to enable reset() method
3729
- * @returns {ObservableProxy} A proxy where each property is an observable
3730
- * @example
3731
- * const user = Observable.init({
3732
- * name: 'John',
3733
- * age: 25,
3734
- * address: { city: 'NYC' }
3735
- * }, { deep: true });
3736
- *
3737
- * user.name.val(); // 'John'
3738
- * user.name.set('Jane');
3739
- * user.name = 'Jane X'
3740
- * user.age.subscribe(val => console.log('Age:', val));
3741
- */
3742
- Observable.init = function(initialValue, configs = null) {
3743
- const data = {};
3744
- for(const key in initialValue) {
3745
- const itemValue = initialValue[key];
3746
- if(Array.isArray(itemValue)) {
3747
- if(configs?.deep !== false) {
3748
- const mappedItemValue = itemValue.map(item => {
3749
- if(Validator.isJson(item)) {
3750
- return Observable.json(item, configs);
3751
- }
3752
- if(Validator.isArray(item)) {
3753
- return Observable.array(item, configs);
4486
+ ObservableObject.prototype.set = function(newData) {
4487
+ const data = Validator.isProxy(newData) ? newData.$value : newData;
4488
+ const configs = this.configs;
4489
+
4490
+ for(const key in data) {
4491
+ const targetItem = this.$observables[key];
4492
+ const newValueOrigin = newData[key];
4493
+ const newValue = data[key];
4494
+
4495
+ if(Validator.isObservable(targetItem)) {
4496
+ if(!Validator.isArray(newValue)) {
4497
+ targetItem.set(newValue);
4498
+ continue;
4499
+ }
4500
+ const firstElementFromOriginalValue = newValueOrigin.at(0);
4501
+ if(Validator.isObservable(firstElementFromOriginalValue) || Validator.isProxy(firstElementFromOriginalValue)) {
4502
+ const newValues = newValue.map(item => {
4503
+ if(Validator.isProxy(firstElementFromOriginalValue)) {
4504
+ return Observable.init(item, configs);
3754
4505
  }
3755
4506
  return Observable(item, configs);
3756
4507
  });
3757
- data[key] = Observable.array(mappedItemValue, configs);
4508
+ targetItem.set(newValues);
3758
4509
  continue;
3759
4510
  }
3760
- data[key] = Observable.array(itemValue, configs);
4511
+ targetItem.set([...newValue]);
3761
4512
  continue;
3762
4513
  }
3763
- if(Validator.isObservable(itemValue) || Validator.isProxy(itemValue)) {
3764
- data[key] = itemValue;
4514
+ if(Validator.isProxy(targetItem)) {
4515
+ targetItem.update(newValue);
3765
4516
  continue;
3766
4517
  }
3767
- data[key] = Observable(itemValue, configs);
4518
+ this[key] = newValue;
3768
4519
  }
4520
+ };
4521
+ ObservableObject.prototype.$set = ObservableObject.prototype.set;
4522
+ ObservableObject.prototype.$updateWith = ObservableObject.prototype.set;
3769
4523
 
3770
- const $reset = () => {
3771
- for(const key in data) {
3772
- const item = data[key];
3773
- item.reset();
3774
- }
3775
- };
3776
-
3777
- const $val = () => ObservableObjectValue(data);
3778
-
3779
- const $clone = () => Observable.init($val(), configs);
4524
+ ObservableObject.prototype.observables = function() {
4525
+ return Object.values(this.$observables);
4526
+ };
4527
+ ObservableObject.prototype.$observables = ObservableObject.prototype.observables;
3780
4528
 
3781
- const $updateWith = (values) => {
3782
- Observable.update(proxy, values);
3783
- };
4529
+ ObservableObject.prototype.keys = function() {
4530
+ return Object.keys(this.$observables);
4531
+ };
4532
+ ObservableObject.prototype.$keys = ObservableObject.prototype.keys;
4533
+ ObservableObject.prototype.clone = function() {
4534
+ return Observable.init(this.val(), this.configs);
4535
+ };
4536
+ ObservableObject.prototype.$clone = ObservableObject.prototype.clone;
4537
+ ObservableObject.prototype.reset = function() {
4538
+ for(const key in this.$observables) {
4539
+ this.$observables[key].reset();
4540
+ }
4541
+ };
4542
+ ObservableObject.prototype.originalSubscribe = ObservableObject.prototype.subscribe;
4543
+ ObservableObject.prototype.subscribe = function(callback) {
4544
+ const observables = this.observables();
4545
+ const updatedValue = nextTick(() => this.trigger());
3784
4546
 
3785
- const $get = (key) => ObservableGet(data, key);
4547
+ this.originalSubscribe(callback);
3786
4548
 
3787
- const proxy = new Proxy(data, {
3788
- get(target, property) {
3789
- if(property === '__isProxy__') { return true; }
3790
- if(property === '$value') { return $val() }
3791
- if(property === 'get' || property === '$get') { return $get; }
3792
- if(property === 'val' || property === '$val') { return $val; }
3793
- if(property === 'set' || property === '$set' || property === '$updateWith') { return $updateWith; }
3794
- if(property === 'observables' || property === '$observables') { return Object.values(target); }
3795
- if(property === 'keys'|| property === '$keys') { return Object.keys(initialValue); }
3796
- if(property === 'clone' || property === '$clone') { return $clone; }
3797
- if(property === 'reset') { return $reset; }
3798
- if(property === 'configs') { return configs; }
3799
- return target[property];
3800
- },
3801
- set(target, prop, newValue) {
3802
- if(target[prop] !== undefined) {
3803
- Validator.isObservable(newValue)
3804
- ? target[prop].set(newValue.val())
3805
- : target[prop].set(newValue);
3806
- return true;
3807
- }
3808
- return true;
4549
+ for (let i = 0, length = observables.length; i < length; i++) {
4550
+ const observable = observables[i];
4551
+ if (observable.__$isObservableArray) {
4552
+ observable.deepSubscribe(updatedValue);
4553
+ continue
3809
4554
  }
3810
- });
4555
+ observable.subscribe(updatedValue);
4556
+ }
4557
+ };
4558
+ ObservableObject.prototype.configs = function() {
4559
+ return this.configs;
4560
+ };
4561
+
4562
+ ObservableObject.prototype.update = ObservableObject.prototype.set;
3811
4563
 
3812
- return proxy;
4564
+ Observable.init = function(initialValue, configs = null) {
4565
+ return new ObservableObject(initialValue, configs)
3813
4566
  };
3814
4567
 
3815
4568
  /**
@@ -3844,43 +4597,6 @@ var NativeDocument = (function (exports) {
3844
4597
  return data;
3845
4598
  };
3846
4599
 
3847
-
3848
- Observable.update = function($target, newData) {
3849
- const data = Validator.isProxy(newData) ? newData.$value : newData;
3850
- const configs = $target.configs;
3851
-
3852
- for(const key in data) {
3853
- const targetItem = $target[key];
3854
- const newValueOrigin = newData[key];
3855
- const newValue = data[key];
3856
-
3857
- if(Validator.isObservable(targetItem)) {
3858
- if(Validator.isArray(newValue)) {
3859
- const firstElementFromOriginalValue = newValueOrigin.at(0);
3860
- if(Validator.isObservable(firstElementFromOriginalValue) || Validator.isProxy(firstElementFromOriginalValue)) {
3861
- const newValues = newValue.map(item => {
3862
- if(Validator.isProxy(firstElementFromOriginalValue)) {
3863
- return Observable.init(item, configs);
3864
- }
3865
- return Observable(item, configs);
3866
- });
3867
- targetItem.set(newValues);
3868
- continue;
3869
- }
3870
- targetItem.set([...newValue]);
3871
- continue;
3872
- }
3873
- targetItem.set(newValue);
3874
- continue;
3875
- }
3876
- if(Validator.isProxy(targetItem)) {
3877
- Observable.update(targetItem, newValue);
3878
- continue;
3879
- }
3880
- $target[key] = newValue;
3881
- }
3882
- };
3883
-
3884
4600
  Observable.object = Observable.init;
3885
4601
  Observable.json = Observable.init;
3886
4602
 
@@ -3932,79 +4648,6 @@ var NativeDocument = (function (exports) {
3932
4648
  return observable;
3933
4649
  };
3934
4650
 
3935
- const Store = (function() {
3936
-
3937
- const $stores = new Map();
3938
-
3939
- return {
3940
- /**
3941
- * Create a new state follower and return it.
3942
- * @param {string} name
3943
- * @returns {ObservableItem}
3944
- */
3945
- use(name) {
3946
- const {observer: originalObserver, subscribers } = $stores.get(name);
3947
- const observerFollower = Observable(originalObserver.val());
3948
- const unSubscriber = originalObserver.subscribe(value => observerFollower.set(value));
3949
- const updaterUnsubscriber = observerFollower.subscribe(value => originalObserver.set(value));
3950
- observerFollower.destroy = () => {
3951
- unSubscriber();
3952
- updaterUnsubscriber();
3953
- observerFollower.cleanup();
3954
- };
3955
- subscribers.add(observerFollower);
3956
-
3957
- return observerFollower;
3958
- },
3959
- /**
3960
- * @param {string} name
3961
- * @returns {ObservableItem}
3962
- */
3963
- follow(name) {
3964
- return this.use(name);
3965
- },
3966
- /**
3967
- * Create a new state and return the observer.
3968
- * @param {string} name
3969
- * @param {*} value
3970
- * @returns {ObservableItem}
3971
- */
3972
- create(name, value) {
3973
- const observer = Observable(value);
3974
- $stores.set(name, { observer, subscribers: new Set()});
3975
- return observer;
3976
- },
3977
- /**
3978
- * Get the observer for a state.
3979
- * @param {string} name
3980
- * @returns {null|ObservableItem}
3981
- */
3982
- get(name) {
3983
- const item = $stores.get(name);
3984
- return item ? item.observer : null;
3985
- },
3986
- /**
3987
- *
3988
- * @param {string} name
3989
- * @returns {{observer: ObservableItem, subscribers: Set}}
3990
- */
3991
- getWithSubscribers(name) {
3992
- return $stores.get(name);
3993
- },
3994
- /**
3995
- * Delete a state.
3996
- * @param {string} name
3997
- */
3998
- delete(name) {
3999
- const item = $stores.get(name);
4000
- if(!item) return;
4001
- item.observer.cleanup();
4002
- item.subscribers.forEach(follower => follower.destroy());
4003
- item.observer.clear();
4004
- }
4005
- };
4006
- }());
4007
-
4008
4651
  /**
4009
4652
  * Renders a list of items from an observable array or object, automatically updating when data changes.
4010
4653
  * Efficiently manages DOM updates by tracking items with keys.
@@ -6430,11 +7073,14 @@ var NativeDocument = (function (exports) {
6430
7073
  configs.body = params;
6431
7074
  }
6432
7075
  else {
6433
- configs.headers['Content-Type'] = 'application/json';
6434
7076
  if(method !== 'GET') {
7077
+ configs.headers['Content-Type'] = 'application/json';
6435
7078
  configs.body = JSON.stringify(params);
6436
7079
  } else {
6437
- configs.params = params;
7080
+ const queryString = new URLSearchParams(params).toString();
7081
+ if (queryString) {
7082
+ endpoint = endpoint + (endpoint.includes('?') ? '&' : '?') + queryString;
7083
+ }
6438
7084
  }
6439
7085
  }
6440
7086
  }
@@ -6505,6 +7151,7 @@ var NativeDocument = (function (exports) {
6505
7151
  exports.PluginsManager = PluginsManager;
6506
7152
  exports.SingletonView = SingletonView;
6507
7153
  exports.Store = Store;
7154
+ exports.StoreFactory = StoreFactory;
6508
7155
  exports.TemplateCloner = TemplateCloner;
6509
7156
  exports.Validator = Validator;
6510
7157
  exports.autoMemoize = autoMemoize;