native-document 1.0.116 → 1.0.118

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.
@@ -1,10 +1,10 @@
1
1
  var NativeDocument = (function (exports) {
2
2
  'use strict';
3
3
 
4
- let DebugManager = {};
4
+ let DebugManager$1 = {};
5
5
 
6
6
  {
7
- DebugManager = {
7
+ DebugManager$1 = {
8
8
  enabled: false,
9
9
 
10
10
  enable() {
@@ -35,7 +35,7 @@ var NativeDocument = (function (exports) {
35
35
  };
36
36
 
37
37
  }
38
- var DebugManager$1 = DebugManager;
38
+ var DebugManager = DebugManager$1;
39
39
 
40
40
  class NativeDocumentError extends Error {
41
41
  constructor(message, context = {}) {
@@ -313,10 +313,10 @@ var NativeDocument = (function (exports) {
313
313
  subtree: true,
314
314
  });
315
315
 
316
- let PluginsManager = null;
316
+ let PluginsManager$1 = null;
317
317
 
318
318
  {
319
- PluginsManager = (function() {
319
+ PluginsManager$1 = (function() {
320
320
 
321
321
  const $plugins = new Map();
322
322
  const $pluginByEvents = new Map();
@@ -382,7 +382,7 @@ var NativeDocument = (function (exports) {
382
382
  try{
383
383
  callback.call(plugin, ...data);
384
384
  } catch (error) {
385
- DebugManager$1.error('Plugin Manager', `Error in plugin ${plugin.$name} for event ${eventName}`, error);
385
+ DebugManager.error('Plugin Manager', `Error in plugin ${plugin.$name} for event ${eventName}`, error);
386
386
  }
387
387
  }
388
388
  }
@@ -391,12 +391,12 @@ var NativeDocument = (function (exports) {
391
391
  }());
392
392
  }
393
393
 
394
- var PluginsManager$1 = PluginsManager;
394
+ var PluginsManager = PluginsManager$1;
395
395
 
396
396
  function NDElement(element) {
397
397
  this.$element = element;
398
398
  {
399
- PluginsManager$1.emit('NDElementCreated', element, this);
399
+ PluginsManager.emit('NDElementCreated', element, this);
400
400
  }
401
401
  }
402
402
 
@@ -561,7 +561,7 @@ var NativeDocument = (function (exports) {
561
561
  }
562
562
  {
563
563
  if (this[name] && !this.$localExtensions.has(name)) {
564
- DebugManager$1.warn('NDElement.extend', `Method "${name}" already exists and will be overwritten`);
564
+ DebugManager.warn('NDElement.extend', `Method "${name}" already exists and will be overwritten`);
565
565
  }
566
566
  this.$localExtensions.set(name, method);
567
567
  }
@@ -612,23 +612,23 @@ var NativeDocument = (function (exports) {
612
612
  const method = methods[name];
613
613
 
614
614
  if (typeof method !== 'function') {
615
- DebugManager$1.warn('NDElement.extend', `"${name}" is not a function, skipping`);
615
+ DebugManager.warn('NDElement.extend', `"${name}" is not a function, skipping`);
616
616
  continue;
617
617
  }
618
618
 
619
619
  if (protectedMethods.has(name)) {
620
- DebugManager$1.error('NDElement.extend', `Cannot override protected method "${name}"`);
620
+ DebugManager.error('NDElement.extend', `Cannot override protected method "${name}"`);
621
621
  throw new NativeDocumentError(`Cannot override protected method "${name}"`);
622
622
  }
623
623
 
624
624
  if (NDElement.prototype[name]) {
625
- DebugManager$1.warn('NDElement.extend', `Overwriting existing prototype method "${name}"`);
625
+ DebugManager.warn('NDElement.extend', `Overwriting existing prototype method "${name}"`);
626
626
  }
627
627
 
628
628
  NDElement.prototype[name] = method;
629
629
  }
630
630
  {
631
- PluginsManager$1.emit('NDElementExtended', methods);
631
+ PluginsManager.emit('NDElementExtended', methods);
632
632
  }
633
633
 
634
634
  return NDElement;
@@ -779,7 +779,7 @@ var NativeDocument = (function (exports) {
779
779
  const foundReserved = Object.keys(attributes).filter(key => reserved.includes(key));
780
780
 
781
781
  if (foundReserved.length > 0) {
782
- DebugManager$1.warn('Validator', `Reserved attributes found: ${foundReserved.join(', ')}`);
782
+ DebugManager.warn('Validator', `Reserved attributes found: ${foundReserved.join(', ')}`);
783
783
  }
784
784
 
785
785
  return attributes;
@@ -866,7 +866,7 @@ var NativeDocument = (function (exports) {
866
866
  }
867
867
  }
868
868
  if (cleanedCount > 0) {
869
- DebugManager$1.log('Memory Auto Clean', `🧹 Cleaned ${cleanedCount} orphaned observables`);
869
+ DebugManager.log('Memory Auto Clean', `🧹 Cleaned ${cleanedCount} orphaned observables`);
870
870
  }
871
871
  }
872
872
  };
@@ -1001,6 +1001,97 @@ var NativeDocument = (function (exports) {
1001
1001
  return cloned;
1002
1002
  };
1003
1003
 
1004
+ const $parseDateParts = (value, locale) => {
1005
+ const d = new Date(value);
1006
+ return {
1007
+ d,
1008
+ parts: new Intl.DateTimeFormat(locale, {
1009
+ year: 'numeric',
1010
+ month: 'long',
1011
+ day: '2-digit',
1012
+ hour: '2-digit',
1013
+ minute: '2-digit',
1014
+ second: '2-digit',
1015
+ }).formatToParts(d).reduce((acc, { type, value }) => {
1016
+ acc[type] = value;
1017
+ return acc;
1018
+ }, {})
1019
+ };
1020
+ };
1021
+
1022
+ const $applyDatePattern = (pattern, d, parts) => {
1023
+ const pad = n => String(n).padStart(2, '0');
1024
+ return pattern
1025
+ .replace('YYYY', parts.year)
1026
+ .replace('YY', parts.year.slice(-2))
1027
+ .replace('MMMM', parts.month)
1028
+ .replace('MMM', parts.month.slice(0, 3))
1029
+ .replace('MM', pad(d.getMonth() + 1))
1030
+ .replace('DD', pad(d.getDate()))
1031
+ .replace('D', d.getDate())
1032
+ .replace('HH', parts.hour)
1033
+ .replace('mm', parts.minute)
1034
+ .replace('ss', parts.second);
1035
+ };
1036
+
1037
+ const Formatters = {
1038
+ currency: (value, locale, { currency = 'XOF', notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
1039
+ new Intl.NumberFormat(locale, {
1040
+ style: 'currency',
1041
+ currency,
1042
+ notation,
1043
+ minimumFractionDigits,
1044
+ maximumFractionDigits
1045
+ }).format(value),
1046
+
1047
+ number: (value, locale, { notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
1048
+ new Intl.NumberFormat(locale, {
1049
+ notation,
1050
+ minimumFractionDigits,
1051
+ maximumFractionDigits
1052
+ }).format(value),
1053
+
1054
+ percent: (value, locale, { decimals = 1 } = {}) =>
1055
+ new Intl.NumberFormat(locale, {
1056
+ style: 'percent',
1057
+ maximumFractionDigits: decimals
1058
+ }).format(value),
1059
+
1060
+ date: (value, locale, { format, dateStyle = 'long' } = {}) => {
1061
+ if (format) {
1062
+ const { d, parts } = $parseDateParts(value, locale);
1063
+ return $applyDatePattern(format, d, parts);
1064
+ }
1065
+ return new Intl.DateTimeFormat(locale, { dateStyle }).format(new Date(value));
1066
+ },
1067
+
1068
+ time: (value, locale, { format, hour = '2-digit', minute = '2-digit', second } = {}) => {
1069
+ if (format) {
1070
+ const { d, parts } = $parseDateParts(value, locale);
1071
+ return $applyDatePattern(format, d, parts);
1072
+ }
1073
+ return new Intl.DateTimeFormat(locale, { hour, minute, second }).format(new Date(value));
1074
+ },
1075
+
1076
+ datetime: (value, locale, { format, dateStyle = 'long', hour = '2-digit', minute = '2-digit', second } = {}) => {
1077
+ if (format) {
1078
+ const { d, parts } = $parseDateParts(value, locale);
1079
+ return $applyDatePattern(format, d, parts);
1080
+ }
1081
+ return new Intl.DateTimeFormat(locale, { dateStyle, hour, minute, second }).format(new Date(value));
1082
+ },
1083
+
1084
+ relative: (value, locale, { unit = 'day', numeric = 'auto' } = {}) => {
1085
+ const diff = Math.round((value - Date.now()) / (1000 * 60 * 60 * 24));
1086
+ return new Intl.RelativeTimeFormat(locale, { numeric }).format(diff, unit);
1087
+ },
1088
+
1089
+ plural: (value, locale, { singular, plural } = {}) => {
1090
+ const rule = new Intl.PluralRules(locale).select(value);
1091
+ return `${value} ${rule === 'one' ? singular : plural}`;
1092
+ },
1093
+ };
1094
+
1004
1095
  const LocalStorage = {
1005
1096
  getJson(key) {
1006
1097
  let value = localStorage.getItem(key);
@@ -1057,703 +1148,212 @@ var NativeDocument = (function (exports) {
1057
1148
  }
1058
1149
  };
1059
1150
 
1060
- const StoreFactory = function() {
1151
+ /**
1152
+ *
1153
+ * @param {*} value
1154
+ * @param {{ propagation: boolean, reset: boolean} | null} configs
1155
+ * @class ObservableItem
1156
+ */
1157
+ function ObservableItem(value, configs = null) {
1158
+ value = Validator.isObservable(value) ? value.val() : value;
1061
1159
 
1062
- const $stores = new Map();
1063
- const $followersCache = new Map();
1160
+ this.$previousValue = null;
1161
+ this.$currentValue = value;
1162
+ {
1163
+ this.$isCleanedUp = false;
1164
+ }
1064
1165
 
1065
- /**
1066
- * Internal helper — retrieves a store entry or throws if not found.
1067
- */
1068
- const $getStoreOrThrow = (method, name) => {
1069
- const item = $stores.get(name);
1070
- if (!item) {
1071
- DebugManager$1.error('Store', `Store.${method}('${name}') : store not found. Did you call Store.create('${name}') first?`);
1072
- throw new NativeDocumentError(
1073
- `Store.${method}('${name}') : store not found.`
1074
- );
1075
- }
1076
- return item;
1077
- };
1166
+ this.$firstListener = null;
1167
+ this.$listeners = null;
1168
+ this.$watchers = null;
1078
1169
 
1079
- /**
1080
- * Internal helper — blocks write operations on a read-only observer.
1081
- */
1082
- const $applyReadOnly = (observer, name, context) => {
1083
- const readOnlyError = (method) => () => {
1084
- DebugManager$1.error('Store', `Store.${context}('${name}') is read-only. '${method}()' is not allowed.`);
1085
- throw new NativeDocumentError(
1086
- `Store.${context}('${name}') is read-only.`
1087
- );
1088
- };
1089
- observer.set = readOnlyError('set');
1090
- observer.toggle = readOnlyError('toggle');
1091
- observer.reset = readOnlyError('reset');
1092
- };
1170
+ this.$memoryId = null;
1093
1171
 
1094
- const $createObservable = (value, options = {}) => {
1095
- if(Array.isArray(value)) {
1096
- return Observable.array(value, options);
1097
- }
1098
- if(typeof value === 'object') {
1099
- return Observable.object(value, options);
1172
+ if(configs) {
1173
+ this.configs = configs;
1174
+ if(configs.reset) {
1175
+ this.$initialValue = Validator.isObject(value) ? deepClone(value) : value;
1100
1176
  }
1101
- return Observable(value, options);
1102
- };
1177
+ }
1178
+ {
1179
+ PluginsManager.emit('CreateObservable', this);
1180
+ }
1181
+ }
1103
1182
 
1104
- const $api = {
1105
- /**
1106
- * Create a new state and return the observer.
1107
- * Throws if a store with the same name already exists.
1108
- *
1109
- * @param {string} name
1110
- * @param {*} value
1111
- * @returns {ObservableItem}
1112
- */
1113
- create(name, value) {
1114
- if ($stores.has(name)) {
1115
- DebugManager$1.warn('Store', `Store.create('${name}') : a store with this name already exists. Use Store.get('${name}') to retrieve it.`);
1116
- throw new NativeDocumentError(
1117
- `Store.create('${name}') : a store with this name already exists.`
1118
- );
1119
- }
1120
- const observer = $createObservable(value);
1121
- $stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: false });
1122
- return observer;
1123
- },
1183
+ Object.defineProperty(ObservableItem.prototype, '$value', {
1184
+ get() {
1185
+ return this.$currentValue;
1186
+ },
1187
+ set(value) {
1188
+ this.set(value);
1189
+ },
1190
+ configurable: true,
1191
+ });
1124
1192
 
1125
- /**
1126
- * Create a new resettable state and return the observer.
1127
- * The store can be reset to its initial value via Store.reset(name).
1128
- * Throws if a store with the same name already exists.
1129
- *
1130
- * @param {string} name
1131
- * @param {*} value
1132
- * @returns {ObservableItem}
1133
- */
1134
- createResettable(name, value) {
1135
- if ($stores.has(name)) {
1136
- DebugManager$1.warn('Store', `Store.createResettable('${name}') : a store with this name already exists.`);
1137
- throw new NativeDocumentError(
1138
- `Store.createResettable('${name}') : a store with this name already exists.`
1139
- );
1140
- }
1141
- const observer = $createObservable(value, { reset: true });
1142
- $stores.set(name, { observer, subscribers: new Set(), resettable: true, composed: false });
1143
- return observer;
1144
- },
1193
+ ObservableItem.prototype.__$isObservable = true;
1194
+ const noneTrigger = function() {};
1145
1195
 
1146
- /**
1147
- * Create a computed store derived from other stores.
1148
- * The value is automatically recalculated when any dependency changes.
1149
- * This store is read-only — Store.use() and Store.set() will throw.
1150
- * Throws if a store with the same name already exists.
1151
- *
1152
- * @param {string} name
1153
- * @param {() => *} computation - Function that returns the computed value
1154
- * @param {string[]} dependencies - Names of the stores to watch
1155
- * @returns {ObservableItem}
1156
- *
1157
- * @example
1158
- * Store.create('products', [{ id: 1, price: 10 }]);
1159
- * Store.create('cart', [{ productId: 1, quantity: 2 }]);
1160
- *
1161
- * Store.createComposed('total', () => {
1162
- * const products = Store.get('products').val();
1163
- * const cart = Store.get('cart').val();
1164
- * return cart.reduce((sum, item) => {
1165
- * const product = products.find(p => p.id === item.productId);
1166
- * return sum + (product.price * item.quantity);
1167
- * }, 0);
1168
- * }, ['products', 'cart']);
1169
- */
1170
- createComposed(name, computation, dependencies) {
1171
- if ($stores.has(name)) {
1172
- DebugManager$1.warn('Store', `Store.createComposed('${name}') : a store with this name already exists.`);
1173
- throw new NativeDocumentError(
1174
- `Store.createComposed('${name}') : a store with this name already exists.`
1175
- );
1176
- }
1177
- if (typeof computation !== 'function') {
1178
- throw new NativeDocumentError(
1179
- `Store.createComposed('${name}') : computation must be a function.`
1180
- );
1181
- }
1182
- if (!Array.isArray(dependencies) || dependencies.length === 0) {
1183
- throw new NativeDocumentError(
1184
- `Store.createComposed('${name}') : dependencies must be a non-empty array of store names.`
1185
- );
1186
- }
1196
+ /**
1197
+ * Intercepts and transforms values before they are set on the observable.
1198
+ * The interceptor can modify the value or return undefined to use the original value.
1199
+ *
1200
+ * @param {(value) => any} callback - Interceptor function that receives (newValue, currentValue) and returns the transformed value or undefined
1201
+ * @returns {ObservableItem} The observable instance for chaining
1202
+ * @example
1203
+ * const count = Observable(0);
1204
+ * count.intercept((newVal, oldVal) => Math.max(0, newVal)); // Prevent negative values
1205
+ */
1206
+ ObservableItem.prototype.intercept = function(callback) {
1207
+ this.$interceptor = callback;
1208
+ this.set = this.$setWithInterceptor;
1209
+ return this;
1210
+ };
1187
1211
 
1188
- // Resolve dependency observers
1189
- const depObservers = dependencies.map(depName => {
1190
- if(typeof depName !== 'string') {
1191
- return depName;
1192
- }
1193
- const depItem = $stores.get(depName);
1194
- if (!depItem) {
1195
- DebugManager$1.error('Store', `Store.createComposed('${name}') : dependency '${depName}' not found. Create it first.`);
1196
- throw new NativeDocumentError(
1197
- `Store.createComposed('${name}') : dependency store '${depName}' not found.`
1198
- );
1199
- }
1200
- return depItem.observer;
1201
- });
1202
-
1203
- // Create computed observable from dependency observers
1204
- const observer = Observable.computed(computation, depObservers);
1212
+ ObservableItem.prototype.triggerFirstListener = function(operations) {
1213
+ this.$firstListener(this.$currentValue, this.$previousValue, operations);
1214
+ };
1205
1215
 
1206
- $stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: true });
1207
- return observer;
1208
- },
1216
+ ObservableItem.prototype.triggerListeners = function(operations) {
1217
+ const $listeners = this.$listeners;
1218
+ const $previousValue = this.$previousValue;
1219
+ const $currentValue = this.$currentValue;
1209
1220
 
1210
- /**
1211
- * Returns true if a store with the given name exists.
1212
- *
1213
- * @param {string} name
1214
- * @returns {boolean}
1215
- */
1216
- has(name) {
1217
- return $stores.has(name);
1218
- },
1221
+ for(let i = 0, length = $listeners.length; i < length; i++) {
1222
+ $listeners[i]($currentValue, $previousValue, operations);
1223
+ }
1224
+ };
1219
1225
 
1220
- /**
1221
- * Resets a resettable store to its initial value and notifies all subscribers.
1222
- * Throws if the store was not created with createResettable().
1223
- *
1224
- * @param {string} name
1225
- */
1226
- reset(name) {
1227
- const item = $getStoreOrThrow('reset', name);
1228
- if (item.composed) {
1229
- DebugManager$1.error('Store', `Store.reset('${name}') : composed stores cannot be reset. Their value is derived from dependencies.`);
1230
- throw new NativeDocumentError(
1231
- `Store.reset('${name}') : composed stores cannot be reset.`
1232
- );
1233
- }
1234
- if (!item.resettable) {
1235
- DebugManager$1.error('Store', `Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`);
1236
- throw new NativeDocumentError(
1237
- `Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`
1238
- );
1239
- }
1240
- item.observer.reset();
1241
- },
1226
+ ObservableItem.prototype.triggerWatchers = function(operations) {
1227
+ const $watchers = this.$watchers;
1228
+ const $previousValue = this.$previousValue;
1229
+ const $currentValue = this.$currentValue;
1242
1230
 
1243
- /**
1244
- * Returns a two-way synchronized follower of the store.
1245
- * Writing to the follower propagates the value back to the store and all its subscribers.
1246
- * Throws if called on a composed store — use Store.follow() instead.
1247
- * Call follower.destroy() or follower.dispose() to unsubscribe.
1248
- *
1249
- * @param {string} name
1250
- * @returns {ObservableItem}
1251
- */
1252
- use(name) {
1253
- const item = $getStoreOrThrow('use', name);
1231
+ const $currentValueCallbacks = $watchers.get($currentValue);
1232
+ const $previousValueCallbacks = $watchers.get($previousValue);
1233
+ if($currentValueCallbacks) {
1234
+ $currentValueCallbacks(true, $previousValue, operations);
1235
+ }
1236
+ if($previousValueCallbacks) {
1237
+ $previousValueCallbacks(false, $currentValue, operations);
1238
+ }
1239
+ };
1254
1240
 
1255
- if (item.composed) {
1256
- DebugManager$1.error('Store', `Store.use('${name}') : composed stores are read-only. Use Store.follow('${name}') instead.`);
1257
- throw new NativeDocumentError(
1258
- `Store.use('${name}') : composed stores are read-only. Use Store.follow('${name}') instead.`
1259
- );
1260
- }
1241
+ ObservableItem.prototype.triggerAll = function(operations) {
1242
+ this.triggerWatchers(operations);
1243
+ this.triggerListeners(operations);
1244
+ };
1261
1245
 
1262
- const { observer: originalObserver, subscribers } = item;
1263
- const observerFollower = $createObservable(originalObserver.val());
1246
+ ObservableItem.prototype.triggerWatchersAndFirstListener = function(operations) {
1247
+ this.triggerWatchers(operations);
1248
+ this.triggerFirstListener(operations);
1249
+ };
1264
1250
 
1265
- const onStoreChange = value => observerFollower.set(value);
1266
- const onFollowerChange = value => originalObserver.set(value);
1251
+ ObservableItem.prototype.assocTrigger = function() {
1252
+ this.$firstListener = null;
1253
+ if(this.$watchers?.size && this.$listeners?.length) {
1254
+ this.trigger = (this.$listeners.length === 1) ? this.triggerWatchersAndFirstListener : this.triggerAll;
1255
+ return;
1256
+ }
1257
+ if(this.$listeners?.length) {
1258
+ if(this.$listeners.length === 1) {
1259
+ this.$firstListener = this.$listeners[0];
1260
+ this.trigger = this.$firstListener.length === 0 ? this.$firstListener : this.triggerFirstListener;
1261
+ }
1262
+ else {
1263
+ this.trigger = this.triggerListeners;
1264
+ }
1265
+ return;
1266
+ }
1267
+ if(this.$watchers?.size) {
1268
+ this.trigger = this.triggerWatchers;
1269
+ return;
1270
+ }
1271
+ this.trigger = noneTrigger;
1272
+ };
1273
+ ObservableItem.prototype.trigger = noneTrigger;
1267
1274
 
1268
- originalObserver.subscribe(onStoreChange);
1269
- observerFollower.subscribe(onFollowerChange);
1275
+ ObservableItem.prototype.$updateWithNewValue = function(newValue) {
1276
+ newValue = newValue?.__$isObservable ? newValue.val() : newValue;
1277
+ if(this.$currentValue === newValue) {
1278
+ return;
1279
+ }
1280
+ this.$previousValue = this.$currentValue;
1281
+ this.$currentValue = newValue;
1282
+ {
1283
+ PluginsManager.emit('ObservableBeforeChange', this);
1284
+ }
1285
+ this.trigger();
1286
+ this.$previousValue = null;
1287
+ {
1288
+ PluginsManager.emit('ObservableAfterChange', this);
1289
+ }
1290
+ };
1270
1291
 
1271
- observerFollower.destroy = () => {
1272
- originalObserver.unsubscribe(onStoreChange);
1273
- observerFollower.unsubscribe(onFollowerChange);
1274
- subscribers.delete(observerFollower);
1275
- observerFollower.cleanup();
1276
- };
1277
- observerFollower.dispose = observerFollower.destroy;
1292
+ /**
1293
+ * @param {*} data
1294
+ */
1295
+ ObservableItem.prototype.$setWithInterceptor = function(data) {
1296
+ let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
1297
+ const result = this.$interceptor(newValue, this.$currentValue);
1278
1298
 
1279
- subscribers.add(observerFollower);
1280
- return observerFollower;
1281
- },
1299
+ if (result !== undefined) {
1300
+ newValue = result;
1301
+ }
1282
1302
 
1283
- /**
1284
- * Returns a read-only follower of the store.
1285
- * The follower reflects store changes but cannot write back to the store.
1286
- * Any attempt to call .set(), .toggle() or .reset() will throw.
1287
- * Call follower.destroy() or follower.dispose() to unsubscribe.
1288
- *
1289
- * @param {string} name
1290
- * @returns {ObservableItem}
1291
- */
1292
- follow(name) {
1293
- const { observer: originalObserver, subscribers } = $getStoreOrThrow('follow', name);
1294
- const observerFollower = $createObservable(originalObserver.val());
1303
+ this.$updateWithNewValue(newValue);
1304
+ };
1295
1305
 
1296
- const onStoreChange = value => observerFollower.set(value);
1297
- originalObserver.subscribe(onStoreChange);
1306
+ /**
1307
+ * @param {*} data
1308
+ */
1309
+ ObservableItem.prototype.$basicSet = function(data) {
1310
+ let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
1311
+ this.$updateWithNewValue(newValue);
1312
+ };
1298
1313
 
1299
- $applyReadOnly(observerFollower, name, 'follow');
1314
+ ObservableItem.prototype.set = ObservableItem.prototype.$basicSet;
1300
1315
 
1301
- observerFollower.destroy = () => {
1302
- originalObserver.unsubscribe(onStoreChange);
1303
- subscribers.delete(observerFollower);
1304
- observerFollower.cleanup();
1305
- };
1306
- observerFollower.dispose = observerFollower.destroy;
1316
+ ObservableItem.prototype.val = function() {
1317
+ return this.$currentValue;
1318
+ };
1307
1319
 
1308
- subscribers.add(observerFollower);
1309
- return observerFollower;
1310
- },
1320
+ ObservableItem.prototype.disconnectAll = function() {
1321
+ this.$previousValue = null;
1322
+ this.$currentValue = null;
1323
+ this.$listeners = null;
1324
+ this.$watchers = null;
1325
+ this.trigger = noneTrigger;
1326
+ };
1311
1327
 
1312
- /**
1313
- * Returns the raw store observer directly (no follower, no cleanup contract).
1314
- * Use this for direct read access when you don't need to unsubscribe.
1315
- * WARNING : mutations on this observer impact all subscribers immediately.
1316
- *
1317
- * @param {string} name
1318
- * @returns {ObservableItem|null}
1319
- */
1320
- get(name) {
1321
- const item = $stores.get(name);
1322
- if (!item) {
1323
- DebugManager$1.warn('Store', `Store.get('${name}') : store not found.`);
1324
- return null;
1325
- }
1326
- return item.observer;
1327
- },
1328
+ /**
1329
+ * Registers a cleanup callback that will be executed when the observable is cleaned up.
1330
+ * Useful for disposing resources, removing event listeners, or other cleanup tasks.
1331
+ *
1332
+ * @param {Function} callback - Cleanup function to execute on observable disposal
1333
+ * @example
1334
+ * const obs = Observable(0);
1335
+ * obs.onCleanup(() => console.log('Cleaned up!'));
1336
+ * obs.cleanup(); // Logs: "Cleaned up!"
1337
+ */
1338
+ ObservableItem.prototype.onCleanup = function(callback) {
1339
+ this.$cleanupListeners = this.$cleanupListeners ?? [];
1340
+ this.$cleanupListeners.push(callback);
1341
+ };
1328
1342
 
1329
- /**
1330
- * @param {string} name
1331
- * @returns {{ observer: ObservableItem, subscribers: Set } | null}
1332
- */
1333
- getWithSubscribers(name) {
1334
- return $stores.get(name) ?? null;
1335
- },
1336
-
1337
- /**
1338
- * Destroys a store : cleans up the observer, destroys all followers, and removes the entry.
1339
- *
1340
- * @param {string} name
1341
- */
1342
- delete(name) {
1343
- const item = $stores.get(name);
1344
- if (!item) {
1345
- DebugManager$1.warn('Store', `Store.delete('${name}') : store not found, nothing to delete.`);
1346
- return;
1347
- }
1348
- item.subscribers.forEach(follower => follower.destroy());
1349
- item.subscribers.clear();
1350
- item.observer.cleanup();
1351
- $stores.delete(name);
1352
- },
1353
- /**
1354
- * Creates an isolated store group with its own state namespace.
1355
- * Each group is a fully independent StoreFactory instance —
1356
- * no key conflicts, no shared state with the parent store.
1357
- *
1358
- * @param {string | ((group: ReturnType<typeof StoreFactory>) => void)} name - Group name for debugging, or setup callback if no name is provided
1359
- * @param {((group: ReturnType<typeof StoreFactory>) => void)} [callback] - Setup function receiving the isolated store instance
1360
- * @returns {ReturnType<typeof StoreFactory>}
1361
- *
1362
- * @example
1363
- * // With name (recommended)
1364
- * const EventStore = Store.group('events', (group) => {
1365
- * group.create('catalog', []);
1366
- * group.create('filters', { category: null, date: null });
1367
- * group.createResettable('selected', null);
1368
- * group.createComposed('filtered', () => {
1369
- * const catalog = EventStore.get('catalog').val();
1370
- * const filters = EventStore.get('filters').val();
1371
- * return catalog.filter(event => {
1372
- * if (filters.category && event.category !== filters.category) return false;
1373
- * return true;
1374
- * });
1375
- * }, ['catalog', 'filters']);
1376
- * });
1377
- *
1378
- * // Without name
1379
- * const CartStore = Store.group((group) => {
1380
- * group.create('items', []);
1381
- * });
1382
- *
1383
- * // Usage
1384
- * EventStore.use('catalog'); // two-way follower
1385
- * EventStore.follow('filtered'); // read-only follower
1386
- * EventStore.get('filters'); // raw observable
1387
- *
1388
- * // Cross-group composed
1389
- * const OrderStore = Store.group('orders', (group) => {
1390
- * group.createComposed('summary', () => {
1391
- * const items = CartStore.get('items').val();
1392
- * const events = EventStore.get('catalog').val();
1393
- * return { items, events };
1394
- * }, [CartStore.get('items'), EventStore.get('catalog')]);
1395
- * });
1396
- */
1397
- group(name, callback) {
1398
- if (typeof name === 'function') {
1399
- callback = name;
1400
- name = 'anonymous';
1401
- }
1402
- const store = StoreFactory();
1403
- callback && callback(store);
1404
- return store;
1405
- },
1406
- createPersistent(name, value, localstorage_key) {
1407
- localstorage_key = localstorage_key || name;
1408
- const observer = this.create(name, $getFromStorage(localstorage_key, value));
1409
- const saver = $saveToStorage(value);
1410
-
1411
- observer.subscribe((val) => saver(localstorage_key, val));
1412
- return observer;
1413
- },
1414
- createPersistentResettable(name, value, localstorage_key) {
1415
- localstorage_key = localstorage_key || name;
1416
- const observer = this.createResettable(name, $getFromStorage(localstorage_key, value));
1417
- const saver = $saveToStorage(value);
1418
- observer.subscribe((val) => saver(localstorage_key, val));
1419
-
1420
- const originalReset = observer.reset.bind(observer);
1421
- observer.reset = () => {
1422
- LocalStorage.remove(localstorage_key);
1423
- originalReset();
1424
- };
1425
-
1426
- return observer;
1427
- }
1428
- };
1429
-
1430
-
1431
- return new Proxy($api, {
1432
- get(target, prop) {
1433
- if (typeof prop === 'symbol' || prop.startsWith('$') || prop in target) {
1434
- return target[prop];
1435
- }
1436
- if (target.has(prop)) {
1437
- if ($followersCache.has(prop)) {
1438
- return $followersCache.get(prop);
1439
- }
1440
- const follower = target.follow(prop);
1441
- $followersCache.set(prop, follower);
1442
- return follower;
1443
- }
1444
- return undefined;
1445
- },
1446
- set(target, prop, value) {
1447
- DebugManager$1.error('Store', `Forbidden: You cannot overwrite the store key '${String(prop)}'. Use .use('${String(prop)}').set(value) instead.`);
1448
- throw new NativeDocumentError(`Store structure is immutable. Use .set() on the observable.`);
1449
- },
1450
- deleteProperty(target, prop) {
1451
- throw new NativeDocumentError(`Store keys cannot be deleted.`);
1452
- }
1453
- });
1454
- };
1455
-
1456
- const Store = StoreFactory();
1457
-
1458
- Store.create('locale', navigator.language.split('-')[0] || 'en');
1459
-
1460
- const $parseDateParts = (value, locale) => {
1461
- const d = new Date(value);
1462
- return {
1463
- d,
1464
- parts: new Intl.DateTimeFormat(locale, {
1465
- year: 'numeric',
1466
- month: 'long',
1467
- day: '2-digit',
1468
- hour: '2-digit',
1469
- minute: '2-digit',
1470
- second: '2-digit',
1471
- }).formatToParts(d).reduce((acc, { type, value }) => {
1472
- acc[type] = value;
1473
- return acc;
1474
- }, {})
1475
- };
1476
- };
1477
-
1478
- const $applyDatePattern = (pattern, d, parts) => {
1479
- const pad = n => String(n).padStart(2, '0');
1480
- return pattern
1481
- .replace('YYYY', parts.year)
1482
- .replace('YY', parts.year.slice(-2))
1483
- .replace('MMMM', parts.month)
1484
- .replace('MMM', parts.month.slice(0, 3))
1485
- .replace('MM', pad(d.getMonth() + 1))
1486
- .replace('DD', pad(d.getDate()))
1487
- .replace('D', d.getDate())
1488
- .replace('HH', parts.hour)
1489
- .replace('mm', parts.minute)
1490
- .replace('ss', parts.second);
1491
- };
1492
-
1493
- const Formatters = {
1494
- currency: (value, locale, { currency = 'XOF', notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
1495
- new Intl.NumberFormat(locale, {
1496
- style: 'currency',
1497
- currency,
1498
- notation,
1499
- minimumFractionDigits,
1500
- maximumFractionDigits
1501
- }).format(value),
1502
-
1503
- number: (value, locale, { notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
1504
- new Intl.NumberFormat(locale, {
1505
- notation,
1506
- minimumFractionDigits,
1507
- maximumFractionDigits
1508
- }).format(value),
1509
-
1510
- percent: (value, locale, { decimals = 1 } = {}) =>
1511
- new Intl.NumberFormat(locale, {
1512
- style: 'percent',
1513
- maximumFractionDigits: decimals
1514
- }).format(value),
1515
-
1516
- date: (value, locale, { format, dateStyle = 'long' } = {}) => {
1517
- if (format) {
1518
- const { d, parts } = $parseDateParts(value, locale);
1519
- return $applyDatePattern(format, d, parts);
1520
- }
1521
- return new Intl.DateTimeFormat(locale, { dateStyle }).format(new Date(value));
1522
- },
1523
-
1524
- time: (value, locale, { format, hour = '2-digit', minute = '2-digit', second } = {}) => {
1525
- if (format) {
1526
- const { d, parts } = $parseDateParts(value, locale);
1527
- return $applyDatePattern(format, d, parts);
1528
- }
1529
- return new Intl.DateTimeFormat(locale, { hour, minute, second }).format(new Date(value));
1530
- },
1531
-
1532
- datetime: (value, locale, { format, dateStyle = 'long', hour = '2-digit', minute = '2-digit', second } = {}) => {
1533
- if (format) {
1534
- const { d, parts } = $parseDateParts(value, locale);
1535
- return $applyDatePattern(format, d, parts);
1536
- }
1537
- return new Intl.DateTimeFormat(locale, { dateStyle, hour, minute, second }).format(new Date(value));
1538
- },
1539
-
1540
- relative: (value, locale, { unit = 'day', numeric = 'auto' } = {}) => {
1541
- const diff = Math.round((value - Date.now()) / (1000 * 60 * 60 * 24));
1542
- return new Intl.RelativeTimeFormat(locale, { numeric }).format(diff, unit);
1543
- },
1544
-
1545
- plural: (value, locale, { singular, plural } = {}) => {
1546
- const rule = new Intl.PluralRules(locale).select(value);
1547
- return `${value} ${rule === 'one' ? singular : plural}`;
1548
- },
1549
- };
1550
-
1551
- /**
1552
- *
1553
- * @param {*} value
1554
- * @param {{ propagation: boolean, reset: boolean} | null} configs
1555
- * @class ObservableItem
1556
- */
1557
- function ObservableItem(value, configs = null) {
1558
- value = Validator.isObservable(value) ? value.val() : value;
1559
-
1560
- this.$previousValue = null;
1561
- this.$currentValue = value;
1562
- {
1563
- this.$isCleanedUp = false;
1564
- }
1565
-
1566
- this.$firstListener = null;
1567
- this.$listeners = null;
1568
- this.$watchers = null;
1569
-
1570
- this.$memoryId = null;
1571
-
1572
- if(configs) {
1573
- this.configs = configs;
1574
- if(configs.reset) {
1575
- this.$initialValue = Validator.isObject(value) ? deepClone(value) : value;
1576
- }
1577
- }
1578
- {
1579
- PluginsManager$1.emit('CreateObservable', this);
1580
- }
1581
- }
1582
-
1583
- Object.defineProperty(ObservableItem.prototype, '$value', {
1584
- get() {
1585
- return this.$currentValue;
1586
- },
1587
- set(value) {
1588
- this.set(value);
1589
- },
1590
- configurable: true,
1591
- });
1592
-
1593
- ObservableItem.prototype.__$isObservable = true;
1594
- const noneTrigger = function() {};
1595
-
1596
- /**
1597
- * Intercepts and transforms values before they are set on the observable.
1598
- * The interceptor can modify the value or return undefined to use the original value.
1599
- *
1600
- * @param {(value) => any} callback - Interceptor function that receives (newValue, currentValue) and returns the transformed value or undefined
1601
- * @returns {ObservableItem} The observable instance for chaining
1602
- * @example
1603
- * const count = Observable(0);
1604
- * count.intercept((newVal, oldVal) => Math.max(0, newVal)); // Prevent negative values
1605
- */
1606
- ObservableItem.prototype.intercept = function(callback) {
1607
- this.$interceptor = callback;
1608
- this.set = this.$setWithInterceptor;
1609
- return this;
1610
- };
1611
-
1612
- ObservableItem.prototype.triggerFirstListener = function(operations) {
1613
- this.$firstListener(this.$currentValue, this.$previousValue, operations);
1614
- };
1615
-
1616
- ObservableItem.prototype.triggerListeners = function(operations) {
1617
- const $listeners = this.$listeners;
1618
- const $previousValue = this.$previousValue;
1619
- const $currentValue = this.$currentValue;
1620
-
1621
- for(let i = 0, length = $listeners.length; i < length; i++) {
1622
- $listeners[i]($currentValue, $previousValue, operations);
1623
- }
1624
- };
1625
-
1626
- ObservableItem.prototype.triggerWatchers = function(operations) {
1627
- const $watchers = this.$watchers;
1628
- const $previousValue = this.$previousValue;
1629
- const $currentValue = this.$currentValue;
1630
-
1631
- const $currentValueCallbacks = $watchers.get($currentValue);
1632
- const $previousValueCallbacks = $watchers.get($previousValue);
1633
- if($currentValueCallbacks) {
1634
- $currentValueCallbacks(true, $previousValue, operations);
1635
- }
1636
- if($previousValueCallbacks) {
1637
- $previousValueCallbacks(false, $currentValue, operations);
1638
- }
1639
- };
1640
-
1641
- ObservableItem.prototype.triggerAll = function(operations) {
1642
- this.triggerWatchers(operations);
1643
- this.triggerListeners(operations);
1644
- };
1645
-
1646
- ObservableItem.prototype.triggerWatchersAndFirstListener = function(operations) {
1647
- this.triggerWatchers(operations);
1648
- this.triggerFirstListener(operations);
1649
- };
1650
-
1651
- ObservableItem.prototype.assocTrigger = function() {
1652
- this.$firstListener = null;
1653
- if(this.$watchers?.size && this.$listeners?.length) {
1654
- this.trigger = (this.$listeners.length === 1) ? this.triggerWatchersAndFirstListener : this.triggerAll;
1655
- return;
1656
- }
1657
- if(this.$listeners?.length) {
1658
- if(this.$listeners.length === 1) {
1659
- this.$firstListener = this.$listeners[0];
1660
- this.trigger = this.triggerFirstListener;
1661
- }
1662
- else {
1663
- this.trigger = this.triggerListeners;
1664
- }
1665
- return;
1666
- }
1667
- if(this.$watchers?.size) {
1668
- this.trigger = this.triggerWatchers;
1669
- return;
1670
- }
1671
- this.trigger = noneTrigger;
1672
- };
1673
- ObservableItem.prototype.trigger = noneTrigger;
1674
-
1675
- ObservableItem.prototype.$updateWithNewValue = function(newValue) {
1676
- newValue = newValue?.__$isObservable ? newValue.val() : newValue;
1677
- if(this.$currentValue === newValue) {
1678
- return;
1679
- }
1680
- this.$previousValue = this.$currentValue;
1681
- this.$currentValue = newValue;
1682
- {
1683
- PluginsManager$1.emit('ObservableBeforeChange', this);
1684
- }
1685
- this.trigger();
1686
- this.$previousValue = null;
1687
- {
1688
- PluginsManager$1.emit('ObservableAfterChange', this);
1689
- }
1690
- };
1691
-
1692
- /**
1693
- * @param {*} data
1694
- */
1695
- ObservableItem.prototype.$setWithInterceptor = function(data) {
1696
- let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
1697
- const result = this.$interceptor(newValue, this.$currentValue);
1698
-
1699
- if (result !== undefined) {
1700
- newValue = result;
1701
- }
1702
-
1703
- this.$updateWithNewValue(newValue);
1704
- };
1705
-
1706
- /**
1707
- * @param {*} data
1708
- */
1709
- ObservableItem.prototype.$basicSet = function(data) {
1710
- let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
1711
- this.$updateWithNewValue(newValue);
1712
- };
1713
-
1714
- ObservableItem.prototype.set = ObservableItem.prototype.$basicSet;
1715
-
1716
- ObservableItem.prototype.val = function() {
1717
- return this.$currentValue;
1718
- };
1719
-
1720
- ObservableItem.prototype.disconnectAll = function() {
1721
- this.$previousValue = null;
1722
- this.$currentValue = null;
1723
- this.$listeners = null;
1724
- this.$watchers = null;
1725
- this.trigger = noneTrigger;
1726
- };
1727
-
1728
- /**
1729
- * Registers a cleanup callback that will be executed when the observable is cleaned up.
1730
- * Useful for disposing resources, removing event listeners, or other cleanup tasks.
1731
- *
1732
- * @param {Function} callback - Cleanup function to execute on observable disposal
1733
- * @example
1734
- * const obs = Observable(0);
1735
- * obs.onCleanup(() => console.log('Cleaned up!'));
1736
- * obs.cleanup(); // Logs: "Cleaned up!"
1737
- */
1738
- ObservableItem.prototype.onCleanup = function(callback) {
1739
- this.$cleanupListeners = this.$cleanupListeners ?? [];
1740
- this.$cleanupListeners.push(callback);
1741
- };
1742
-
1743
- ObservableItem.prototype.cleanup = function() {
1744
- if (this.$cleanupListeners) {
1745
- for (let i = 0; i < this.$cleanupListeners.length; i++) {
1746
- this.$cleanupListeners[i]();
1747
- }
1748
- this.$cleanupListeners = null;
1749
- }
1750
- MemoryManager.unregister(this.$memoryId);
1751
- this.disconnectAll();
1752
- {
1753
- this.$isCleanedUp = true;
1754
- }
1755
- delete this.$value;
1756
- };
1343
+ ObservableItem.prototype.cleanup = function() {
1344
+ if (this.$cleanupListeners) {
1345
+ for (let i = 0; i < this.$cleanupListeners.length; i++) {
1346
+ this.$cleanupListeners[i]();
1347
+ }
1348
+ this.$cleanupListeners = null;
1349
+ }
1350
+ MemoryManager.unregister(this.$memoryId);
1351
+ this.disconnectAll();
1352
+ {
1353
+ this.$isCleanedUp = true;
1354
+ }
1355
+ delete this.$value;
1356
+ };
1757
1357
 
1758
1358
  /**
1759
1359
  *
@@ -1763,7 +1363,7 @@ var NativeDocument = (function (exports) {
1763
1363
  ObservableItem.prototype.subscribe = function(callback) {
1764
1364
  {
1765
1365
  if (this.$isCleanedUp) {
1766
- DebugManager$1.warn('Observable subscription', '⚠️ Attempted to subscribe to a cleaned up observable.');
1366
+ DebugManager.warn('Observable subscription', '⚠️ Attempted to subscribe to a cleaned up observable.');
1767
1367
  return;
1768
1368
  }
1769
1369
  if (typeof callback !== 'function') {
@@ -1775,7 +1375,7 @@ var NativeDocument = (function (exports) {
1775
1375
  this.$listeners.push(callback);
1776
1376
  this.assocTrigger();
1777
1377
  {
1778
- PluginsManager$1.emit('ObservableSubscribe', this);
1378
+ PluginsManager.emit('ObservableSubscribe', this);
1779
1379
  }
1780
1380
  };
1781
1381
 
@@ -1886,7 +1486,7 @@ var NativeDocument = (function (exports) {
1886
1486
  }
1887
1487
  this.assocTrigger();
1888
1488
  {
1889
- PluginsManager$1.emit('ObservableUnsubscribe', this);
1489
+ PluginsManager.emit('ObservableUnsubscribe', this);
1890
1490
  }
1891
1491
  };
1892
1492
 
@@ -2203,7 +1803,6 @@ var NativeDocument = (function (exports) {
2203
1803
  }
2204
1804
  element.classes.toggle(className, value);
2205
1805
  }
2206
- data = null;
2207
1806
  };
2208
1807
 
2209
1808
  /**
@@ -2362,7 +1961,7 @@ var NativeDocument = (function (exports) {
2362
1961
  Function.prototype.toNdElement = function () {
2363
1962
  const child = this;
2364
1963
  {
2365
- PluginsManager$1.emit('BeforeProcessComponent', child);
1964
+ PluginsManager.emit('BeforeProcessComponent', child);
2366
1965
  }
2367
1966
  return ElementCreator.getChild(child());
2368
1967
  };
@@ -2549,14 +2148,14 @@ var NativeDocument = (function (exports) {
2549
2148
  processChildren: (children, parent) => {
2550
2149
  if(children === null) return;
2551
2150
  {
2552
- PluginsManager$1.emit('BeforeProcessChildren', parent);
2151
+ PluginsManager.emit('BeforeProcessChildren', parent);
2553
2152
  }
2554
2153
  let child = ElementCreator.getChild(children);
2555
2154
  if(child) {
2556
2155
  parent.appendChild(child);
2557
2156
  }
2558
2157
  {
2559
- PluginsManager$1.emit('AfterProcessChildren', parent);
2158
+ PluginsManager.emit('AfterProcessChildren', parent);
2560
2159
  }
2561
2160
  },
2562
2161
  async safeRemove(element) {
@@ -2642,7 +2241,7 @@ var NativeDocument = (function (exports) {
2642
2241
  anchorFragment.appendChild = function(child, before = null) {
2643
2242
  const parent = anchorEnd.parentNode;
2644
2243
  if(!parent) {
2645
- DebugManager$1.error('Anchor', 'Anchor : parent not found', child);
2244
+ DebugManager.error('Anchor', 'Anchor : parent not found', child);
2646
2245
  return;
2647
2246
  }
2648
2247
  before = before ?? anchorEnd;
@@ -2905,16 +2504,16 @@ var NativeDocument = (function (exports) {
2905
2504
  "ContextMenu"
2906
2505
  ];
2907
2506
 
2908
- const property$1 = {
2507
+ const property = {
2909
2508
  configurable: true,
2910
2509
  get() {
2911
2510
  return new NDElement(this);
2912
2511
  }
2913
2512
  };
2914
2513
 
2915
- Object.defineProperty(HTMLElement.prototype, 'nd', property$1);
2514
+ Object.defineProperty(HTMLElement.prototype, 'nd', property);
2916
2515
 
2917
- Object.defineProperty(DocumentFragment.prototype, 'nd', property$1);
2516
+ Object.defineProperty(DocumentFragment.prototype, 'nd', property);
2918
2517
 
2919
2518
  Object.defineProperty(NDElement.prototype, 'nd', {
2920
2519
  configurable: true,
@@ -3267,35 +2866,78 @@ var NativeDocument = (function (exports) {
3267
2866
  }
3268
2867
  const steps = [];
3269
2868
  if(this.$ndMethods) {
3270
- steps.push((clonedNode, data) => {
3271
- for(const methodName in this.$ndMethods) {
2869
+ const methods = Object.keys(this.$ndMethods);
2870
+ if(methods.length === 1) {
2871
+ const methodName = methods[0];
2872
+ steps.push((clonedNode, data) => {
3272
2873
  clonedNode.nd[methodName](this.$ndMethods[methodName].bind(clonedNode, ...data));
3273
- }
3274
- });
2874
+ });
2875
+ } else {
2876
+ steps.push((clonedNode, data) => {
2877
+ const nd = clonedNode.nd;
2878
+ for(const methodName in this.$ndMethods) {
2879
+ nd[methodName](this.$ndMethods[methodName].bind(clonedNode, ...data));
2880
+ }
2881
+ });
2882
+ }
3275
2883
  }
3276
2884
  if(this.$classes) {
3277
2885
  const cache = {};
3278
- steps.push((clonedNode, data) => {
3279
- ElementCreator.processClassAttribute(clonedNode, buildProperties(cache, this.$classes, data));
3280
- });
2886
+ const keys = Object.keys(this.$classes);
2887
+
2888
+ if(keys.length === 1) {
2889
+ const key = keys[0];
2890
+ const callback = this.$classes[key];
2891
+ steps.push((clonedNode, data) => {
2892
+ cache[key] = callback.apply(null, data);
2893
+ ElementCreator.processClassAttribute(clonedNode, cache);
2894
+ });
2895
+ } else {
2896
+ steps.push((clonedNode, data) => {
2897
+ ElementCreator.processClassAttribute(clonedNode, buildProperties(cache, this.$classes, data));
2898
+ });
2899
+ }
3281
2900
  }
3282
2901
  if(this.$styles) {
3283
2902
  const cache = {};
3284
- steps.push((clonedNode, data) => {
3285
- ElementCreator.processStyleAttribute(clonedNode, buildProperties(cache, this.$styles, data));
3286
- });
2903
+ const keys = Object.keys(this.$styles);
2904
+
2905
+ if(keys.length === 1) {
2906
+ const key = keys[0];
2907
+ const callback = this.$styles[key];
2908
+ steps.push((clonedNode, data) => {
2909
+ cache[key] = callback.apply(null, data);
2910
+ ElementCreator.processStyleAttribute(clonedNode, cache);
2911
+ });
2912
+ } else {
2913
+ steps.push((clonedNode, data) => {
2914
+ ElementCreator.processStyleAttribute(clonedNode, buildProperties(cache, this.$styles, data));
2915
+ });
2916
+ }
3287
2917
  }
3288
2918
  if(this.$attrs) {
3289
2919
  const cache = {};
3290
- steps.push((clonedNode, data) => {
3291
- ElementCreator.processAttributes(clonedNode, buildProperties(cache, this.$attrs, data));
3292
- });
2920
+ const keys = Object.keys(this.$attrs);
2921
+
2922
+ if(keys.length === 1) {
2923
+ const key = keys[0];
2924
+ const callback = this.$attrs[key];
2925
+ steps.push((clonedNode, data) => {
2926
+ cache[key] = callback.apply(null, data);
2927
+ ElementCreator.processAttributes(clonedNode, cache);
2928
+ });
2929
+ } else {
2930
+ steps.push((clonedNode, data) => {
2931
+ ElementCreator.processAttributes(clonedNode, buildProperties(cache, this.$attrs, data));
2932
+ });
2933
+ }
3293
2934
  }
3294
2935
 
3295
2936
  const stepsCount = steps.length;
2937
+ const $element = this.$element;
3296
2938
 
3297
- this.cloneNode = function(data) {
3298
- const clonedNode = this.$element.cloneNode(false);
2939
+ this.cloneNode = (data) => {
2940
+ const clonedNode = $element.cloneNode(false);
3299
2941
  for(let i = 0; i < stepsCount; i++) {
3300
2942
  steps[i](clonedNode, data);
3301
2943
  }
@@ -3307,14 +2949,6 @@ var NativeDocument = (function (exports) {
3307
2949
  return this.$element.cloneNode(false);
3308
2950
  };
3309
2951
 
3310
- NodeCloner.prototype.cloneTextNodeByProperty = function(data) {
3311
- return createTextNode(data[0][this.$content]);
3312
- };
3313
-
3314
- NodeCloner.prototype.cloneTextNodeByCallback = function(data) {
3315
- return createTextNode(this.$content.apply(null, data));
3316
- };
3317
-
3318
2952
  NodeCloner.prototype.attach = function(methodName, callback) {
3319
2953
  this.$ndMethods = this.$ndMethods || {};
3320
2954
  this.$ndMethods[methodName] = callback;
@@ -3324,10 +2958,10 @@ var NativeDocument = (function (exports) {
3324
2958
  NodeCloner.prototype.text = function(value) {
3325
2959
  this.$content = value;
3326
2960
  if(typeof value === 'function') {
3327
- this.cloneNode = NodeCloner.prototype.cloneTextNodeByCallback;
2961
+ this.cloneNode = (data) => createTextNode(value.apply(null, data));
3328
2962
  return this;
3329
2963
  }
3330
- this.cloneNode = NodeCloner.prototype.cloneTextNodeByProperty;
2964
+ this.cloneNode = (data) => createTextNode(data[0][value]);
3331
2965
  return this;
3332
2966
  };
3333
2967
 
@@ -3386,8 +3020,7 @@ var NativeDocument = (function (exports) {
3386
3020
  $node.dynamicCloneNode = (data) => {
3387
3021
  const clonedNode = $node.nodeCloner.cloneNode(data);
3388
3022
  for(let i = 0; i < childNodesLength; i++) {
3389
- const child = childNodes[i].dynamicCloneNode(data);
3390
- clonedNode.appendChild(child);
3023
+ clonedNode.appendChild(childNodes[i].dynamicCloneNode(data));
3391
3024
  }
3392
3025
  return clonedNode;
3393
3026
  };
@@ -3395,8 +3028,7 @@ var NativeDocument = (function (exports) {
3395
3028
  $node.dynamicCloneNode = (data) => {
3396
3029
  const clonedNode = $node.cloneNode();
3397
3030
  for(let i = 0; i < childNodesLength; i++) {
3398
- const child = childNodes[i].dynamicCloneNode(data);
3399
- clonedNode.appendChild(child);
3031
+ clonedNode.appendChild(childNodes[i].dynamicCloneNode(data));
3400
3032
  }
3401
3033
  return clonedNode;
3402
3034
  };
@@ -3410,11 +3042,10 @@ var NativeDocument = (function (exports) {
3410
3042
  const binder = createTemplateCloner(this);
3411
3043
  $node = $fn(binder);
3412
3044
  if(!$node.nodeCloner) {
3413
- console.log('nodeCloner not found on :');
3414
3045
  $node.nodeCloner = new NodeCloner($node);
3415
3046
  }
3416
3047
  assignClonerToNode($node);
3417
- this.clone = (data) => $node.dynamicCloneNode(data);
3048
+ this.clone = $node.dynamicCloneNode;
3418
3049
  return $node.dynamicCloneNode(data);
3419
3050
  };
3420
3051
 
@@ -3466,10 +3097,9 @@ var NativeDocument = (function (exports) {
3466
3097
  let wrapper = (args) => {
3467
3098
  $cache = new TemplateCloner(fn);
3468
3099
 
3469
- wrapper = (args) => {
3470
- return $cache.clone(args);
3471
- };
3472
- return $cache.clone(args);
3100
+ const node = $cache.clone(args);
3101
+ wrapper = $cache.clone;
3102
+ return node;
3473
3103
  };
3474
3104
 
3475
3105
  if(fn.length < 2) {
@@ -4059,7 +3689,7 @@ var NativeDocument = (function (exports) {
4059
3689
 
4060
3690
  ObservableItem.call(this, target, configs);
4061
3691
  {
4062
- PluginsManager$1.emit('CreateObservableArray', this);
3692
+ PluginsManager.emit('CreateObservableArray', this);
4063
3693
  }
4064
3694
  };
4065
3695
 
@@ -4220,501 +3850,901 @@ var NativeDocument = (function (exports) {
4220
3850
  /**
4221
3851
  * Checks if the array is empty.
4222
3852
  *
4223
- * @returns {boolean} True if array has no elements
3853
+ * @returns {boolean} True if array has no elements
3854
+ * @example
3855
+ * const items = Observable.array([]);
3856
+ * items.isEmpty(); // true
3857
+ */
3858
+ ObservableArray.prototype.isEmpty = function() {
3859
+ return this.$currentValue.length === 0;
3860
+ };
3861
+
3862
+ /**
3863
+ * Triggers a populate operation with the current array, iteration count, and callback.
3864
+ * Used internally for rendering optimizations.
3865
+ *
3866
+ * @param {number} iteration - Iteration count for rendering
3867
+ * @param {Function} callback - Callback function for rendering items
3868
+ */
3869
+ ObservableArray.prototype.populateAndRender = function(iteration, callback) {
3870
+ this.trigger({ action: 'populate', args: [this.$currentValue, iteration, callback] });
3871
+ };
3872
+
3873
+
3874
+ /**
3875
+ * Creates a filtered view of the array based on predicates.
3876
+ * The filtered array updates automatically when source data or predicates change.
3877
+ *
3878
+ * @param {Object} predicates - Object mapping property names to filter conditions or functions
3879
+ * @returns {ObservableArray} A new observable array containing filtered items
3880
+ * @example
3881
+ * const users = Observable.array([
3882
+ * { name: 'John', age: 25 },
3883
+ * { name: 'Jane', age: 30 }
3884
+ * ]);
3885
+ * const adults = users.where({ age: (val) => val >= 18 });
3886
+ */
3887
+ ObservableArray.prototype.where = function(predicates) {
3888
+ const sourceArray = this;
3889
+ const observableDependencies = [sourceArray];
3890
+ const filterCallbacks = {};
3891
+
3892
+ for (const [key, rawPredicate] of Object.entries(predicates)) {
3893
+ const predicate = Validator.isObservable(rawPredicate) ? match(rawPredicate, false) : rawPredicate;
3894
+ if (predicate && typeof predicate === 'object' && 'callback' in predicate) {
3895
+ filterCallbacks[key] = predicate.callback;
3896
+
3897
+ if (predicate.dependencies) {
3898
+ const deps = Array.isArray(predicate.dependencies)
3899
+ ? predicate.dependencies
3900
+ : [predicate.dependencies];
3901
+ observableDependencies.push.apply(observableDependencies, deps);
3902
+ }
3903
+ } else if(typeof predicate === 'function') {
3904
+ filterCallbacks[key] = predicate;
3905
+ } else {
3906
+ filterCallbacks[key] = (value) => value === predicate;
3907
+ }
3908
+ }
3909
+
3910
+ const viewArray = Observable.array();
3911
+
3912
+ const filters = Object.entries(filterCallbacks);
3913
+ const updateView = () => {
3914
+ const filtered = sourceArray.val().filter(item => {
3915
+ for (const [key, callback] of filters) {
3916
+ if(key === '_') {
3917
+ if (!callback(item)) return false;
3918
+ } else {
3919
+ if (!callback(item[key])) return false;
3920
+ }
3921
+ }
3922
+ return true;
3923
+ });
3924
+
3925
+ viewArray.set(filtered);
3926
+ };
3927
+
3928
+ observableDependencies.forEach(dep => dep.subscribe(updateView));
3929
+
3930
+ updateView();
3931
+
3932
+ return viewArray;
3933
+ };
3934
+
3935
+ /**
3936
+ * Creates a filtered view where at least one of the specified fields matches the filter.
3937
+ *
3938
+ * @param {Array<string>} fields - Array of field names to check
3939
+ * @param {FilterResult} filter - Filter condition with callback and dependencies
3940
+ * @returns {ObservableArray} A new observable array containing filtered items
3941
+ * @example
3942
+ * const products = Observable.array([
3943
+ * { name: 'Apple', category: 'Fruit' },
3944
+ * { name: 'Carrot', category: 'Vegetable' }
3945
+ * ]);
3946
+ * const searchTerm = Observable('App');
3947
+ * const filtered = products.whereSome(['name', 'category'], match(searchTerm));
3948
+ */
3949
+ ObservableArray.prototype.whereSome = function(fields, filter) {
3950
+ return this.where({
3951
+ _: {
3952
+ dependencies: filter.dependencies,
3953
+ callback: (item) => fields.some(field => filter.callback(item[field]))
3954
+ }
3955
+ });
3956
+ };
3957
+
3958
+ /**
3959
+ * Creates a filtered view where all specified fields match the filter.
3960
+ *
3961
+ * @param {Array<string>} fields - Array of field names to check
3962
+ * @param {FilterResult} filter - Filter condition with callback and dependencies
3963
+ * @returns {ObservableArray} A new observable array containing filtered items
3964
+ * @example
3965
+ * const items = Observable.array([
3966
+ * { status: 'active', verified: true },
3967
+ * { status: 'active', verified: false }
3968
+ * ]);
3969
+ * const activeFilter = equals('active');
3970
+ * const filtered = items.whereEvery(['status', 'verified'], activeFilter);
3971
+ */
3972
+ ObservableArray.prototype.whereEvery = function(fields, filter) {
3973
+ return this.where({
3974
+ _: {
3975
+ dependencies: filter.dependencies,
3976
+ callback: (item) => fields.every(field => filter.callback(item[field]))
3977
+ }
3978
+ });
3979
+ };
3980
+
3981
+ ObservableArray.prototype.deepSubscribe = function(callback) {
3982
+ const updatedValue = nextTick(() => callback(this.val()));
3983
+ const $listeners = new WeakMap();
3984
+
3985
+ const bindItem = (item) => {
3986
+ if ($listeners.has(item)) {
3987
+ return;
3988
+ }
3989
+ if (item?.__$isObservableArray) {
3990
+ $listeners.set(item, item.deepSubscribe(updatedValue));
3991
+ return;
3992
+ }
3993
+ if (item?.__$isObservable) {
3994
+ item.subscribe(updatedValue);
3995
+ $listeners.set(item, () => item.unsubscribe(updatedValue));
3996
+ }
3997
+ };
3998
+
3999
+ const unbindItem = (item) => {
4000
+ const unsub = $listeners.get(item);
4001
+ if (unsub) {
4002
+ unsub();
4003
+ $listeners.delete(item);
4004
+ }
4005
+ };
4006
+
4007
+ this.$currentValue.forEach(bindItem);
4008
+ this.subscribe(updatedValue);
4009
+
4010
+ this.subscribe((items, _, operations) => {
4011
+ switch (operations?.action) {
4012
+ case 'push':
4013
+ case 'unshift':
4014
+ operations.args.forEach(bindItem);
4015
+ break;
4016
+
4017
+ case 'splice': {
4018
+ const [start, deleteCount, ...newItems] = operations.args;
4019
+ operations.result?.forEach(unbindItem);
4020
+ newItems.forEach(bindItem);
4021
+ break;
4022
+ }
4023
+
4024
+ case 'remove':
4025
+ unbindItem(operations.result);
4026
+ break;
4027
+
4028
+ case 'merge':
4029
+ operations.args.forEach(bindItem);
4030
+ break;
4031
+
4032
+ case 'clear':
4033
+ this.$currentValue.forEach(unbindItem);
4034
+ break;
4035
+ }
4036
+ });
4037
+
4038
+ return () => {
4039
+ this.$currentValue.forEach(unbindItem);
4040
+ };
4041
+ };
4042
+
4043
+ /**
4044
+ * Creates an observable array with reactive array methods.
4045
+ * All mutations trigger updates automatically.
4046
+ *
4047
+ * @param {Array} [target=[]] - Initial array value
4048
+ * @param {Object|null} [configs=null] - Configuration options
4049
+ * // @param {boolean} [configs.propagation=true] - Whether to propagate changes to parent observables
4050
+ * // @param {boolean} [configs.deep=false] - Whether to make nested objects observable
4051
+ * @param {boolean} [configs.reset=false] - Whether to store initial value for reset()
4052
+ * @returns {ObservableArray} An observable array with reactive methods
4224
4053
  * @example
4225
- * const items = Observable.array([]);
4226
- * items.isEmpty(); // true
4054
+ * const items = Observable.array([1, 2, 3]);
4055
+ * items.push(4); // Triggers update
4056
+ * items.subscribe((arr) => console.log(arr));
4227
4057
  */
4228
- ObservableArray.prototype.isEmpty = function() {
4229
- return this.$currentValue.length === 0;
4058
+ Observable.array = function(target = [], configs = null) {
4059
+ return new ObservableArray(target, configs);
4230
4060
  };
4231
4061
 
4232
4062
  /**
4233
- * Triggers a populate operation with the current array, iteration count, and callback.
4234
- * Used internally for rendering optimizations.
4235
4063
  *
4236
- * @param {number} iteration - Iteration count for rendering
4237
- * @param {Function} callback - Callback function for rendering items
4064
+ * @param {Function} callback
4065
+ * @returns {Function}
4238
4066
  */
4239
- ObservableArray.prototype.populateAndRender = function(iteration, callback) {
4240
- this.trigger({ action: 'populate', args: [this.$currentValue, iteration, callback] });
4067
+ Observable.batch = function(callback) {
4068
+ const $observer = Observable(0);
4069
+ const batch = function() {
4070
+ if(Validator.isAsyncFunction(callback)) {
4071
+ return (callback(...arguments)).then(() => {
4072
+ $observer.trigger();
4073
+ }).catch(error => { throw error; });
4074
+ }
4075
+ callback(...arguments);
4076
+ $observer.trigger();
4077
+ };
4078
+ batch.$observer = $observer;
4079
+ return batch;
4241
4080
  };
4242
4081
 
4082
+ const ObservableObject = function(target, configs) {
4083
+ ObservableItem.call(this, target);
4084
+ this.$observables = {};
4085
+ this.configs = configs;
4243
4086
 
4244
- /**
4245
- * Creates a filtered view of the array based on predicates.
4246
- * The filtered array updates automatically when source data or predicates change.
4247
- *
4248
- * @param {Object} predicates - Object mapping property names to filter conditions or functions
4249
- * @returns {ObservableArray} A new observable array containing filtered items
4250
- * @example
4251
- * const users = Observable.array([
4252
- * { name: 'John', age: 25 },
4253
- * { name: 'Jane', age: 30 }
4254
- * ]);
4255
- * const adults = users.where({ age: (val) => val >= 18 });
4256
- */
4257
- ObservableArray.prototype.where = function(predicates) {
4258
- const sourceArray = this;
4259
- const observableDependencies = [sourceArray];
4260
- const filterCallbacks = {};
4087
+ this.$load(target);
4261
4088
 
4262
- for (const [key, rawPredicate] of Object.entries(predicates)) {
4263
- const predicate = Validator.isObservable(rawPredicate) ? match(rawPredicate, false) : rawPredicate;
4264
- if (predicate && typeof predicate === 'object' && 'callback' in predicate) {
4265
- filterCallbacks[key] = predicate.callback;
4089
+ for(const name in target) {
4090
+ if(!Object.hasOwn(this, name)) {
4091
+ Object.defineProperty(this, name, {
4092
+ get: () => this.$observables[name],
4093
+ set: (value) => this.$observables[name].set(value)
4094
+ });
4095
+ }
4096
+ }
4266
4097
 
4267
- if (predicate.dependencies) {
4268
- const deps = Array.isArray(predicate.dependencies)
4269
- ? predicate.dependencies
4270
- : [predicate.dependencies];
4271
- observableDependencies.push.apply(observableDependencies, deps);
4098
+ };
4099
+
4100
+ ObservableObject.prototype = Object.create(ObservableItem.prototype);
4101
+
4102
+ Object.defineProperty(ObservableObject, '$value', {
4103
+ get() {
4104
+ return this.val();
4105
+ },
4106
+ set(value) {
4107
+ this.set(value);
4108
+ }
4109
+ });
4110
+
4111
+ ObservableObject.prototype.__$isObservableObject = true;
4112
+ ObservableObject.prototype.__isProxy__ = true;
4113
+
4114
+ ObservableObject.prototype.$load = function(initialValue) {
4115
+ const configs = this.configs;
4116
+ for(const key in initialValue) {
4117
+ const itemValue = initialValue[key];
4118
+ if(Array.isArray(itemValue)) {
4119
+ if(configs?.deep !== false) {
4120
+ const mappedItemValue = itemValue.map(item => {
4121
+ if(Validator.isJson(item)) {
4122
+ return Observable.json(item, configs);
4123
+ }
4124
+ if(Validator.isArray(item)) {
4125
+ return Observable.array(item, configs);
4126
+ }
4127
+ return Observable(item, configs);
4128
+ });
4129
+ this.$observables[key] = Observable.array(mappedItemValue, configs);
4130
+ continue;
4272
4131
  }
4273
- } else if(typeof predicate === 'function') {
4274
- filterCallbacks[key] = predicate;
4132
+ this.$observables[key] = Observable.array(itemValue, configs);
4133
+ continue;
4134
+ }
4135
+ if(Validator.isObservable(itemValue) || Validator.isProxy(itemValue)) {
4136
+ this.$observables[key] = itemValue;
4137
+ continue;
4138
+ }
4139
+ this.$observables[key] = (typeof itemValue === 'object') ? Observable.object(itemValue, configs) : Observable(itemValue, configs);
4140
+ }
4141
+ };
4142
+
4143
+ ObservableObject.prototype.val = function() {
4144
+ const result = {};
4145
+ for(const key in this.$observables) {
4146
+ const dataItem = this.$observables[key];
4147
+ if(Validator.isObservable(dataItem)) {
4148
+ let value = dataItem.val();
4149
+ if(Array.isArray(value)) {
4150
+ value = value.map(item => {
4151
+ if(Validator.isObservable(item)) {
4152
+ return item.val();
4153
+ }
4154
+ if(Validator.isProxy(item)) {
4155
+ return item.$value;
4156
+ }
4157
+ return item;
4158
+ });
4159
+ }
4160
+ result[key] = value;
4161
+ } else if(Validator.isProxy(dataItem)) {
4162
+ result[key] = dataItem.$value;
4275
4163
  } else {
4276
- filterCallbacks[key] = (value) => value === predicate;
4164
+ result[key] = dataItem;
4277
4165
  }
4278
4166
  }
4167
+ return result;
4168
+ };
4169
+ ObservableObject.prototype.$val = ObservableObject.prototype.val;
4279
4170
 
4280
- const viewArray = Observable.array();
4171
+ ObservableObject.prototype.get = function(property) {
4172
+ const item = this.$observables[property];
4173
+ if(Validator.isObservable(item)) {
4174
+ return item.val();
4175
+ }
4176
+ if(Validator.isProxy(item)) {
4177
+ return item.$value;
4178
+ }
4179
+ return item;
4180
+ };
4181
+ ObservableObject.prototype.$get = ObservableObject.prototype.get;
4281
4182
 
4282
- const filters = Object.entries(filterCallbacks);
4283
- const updateView = () => {
4284
- const filtered = sourceArray.val().filter(item => {
4285
- for (const [key, callback] of filters) {
4286
- if(key === '_') {
4287
- if (!callback(item)) return false;
4288
- } else {
4289
- if (!callback(item[key])) return false;
4290
- }
4183
+ ObservableObject.prototype.set = function(newData) {
4184
+ const data = Validator.isProxy(newData) ? newData.$value : newData;
4185
+ const configs = this.configs;
4186
+
4187
+ for(const key in data) {
4188
+ const targetItem = this.$observables[key];
4189
+ const newValueOrigin = newData[key];
4190
+ const newValue = data[key];
4191
+
4192
+ if(Validator.isObservable(targetItem)) {
4193
+ if(!Validator.isArray(newValue)) {
4194
+ targetItem.set(newValue);
4195
+ continue;
4291
4196
  }
4292
- return true;
4293
- });
4197
+ const firstElementFromOriginalValue = newValueOrigin.at(0);
4198
+ if(Validator.isObservable(firstElementFromOriginalValue) || Validator.isProxy(firstElementFromOriginalValue)) {
4199
+ const newValues = newValue.map(item => {
4200
+ if(Validator.isProxy(firstElementFromOriginalValue)) {
4201
+ return Observable.init(item, configs);
4202
+ }
4203
+ return Observable(item, configs);
4204
+ });
4205
+ targetItem.set(newValues);
4206
+ continue;
4207
+ }
4208
+ targetItem.set([...newValue]);
4209
+ continue;
4210
+ }
4211
+ if(Validator.isProxy(targetItem)) {
4212
+ targetItem.update(newValue);
4213
+ continue;
4214
+ }
4215
+ this[key] = newValue;
4216
+ }
4217
+ };
4218
+ ObservableObject.prototype.$set = ObservableObject.prototype.set;
4219
+ ObservableObject.prototype.$updateWith = ObservableObject.prototype.set;
4294
4220
 
4295
- viewArray.set(filtered);
4296
- };
4221
+ ObservableObject.prototype.observables = function() {
4222
+ return Object.values(this.$observables);
4223
+ };
4224
+ ObservableObject.prototype.$observables = ObservableObject.prototype.observables;
4297
4225
 
4298
- observableDependencies.forEach(dep => dep.subscribe(updateView));
4226
+ ObservableObject.prototype.keys = function() {
4227
+ return Object.keys(this.$observables);
4228
+ };
4229
+ ObservableObject.prototype.$keys = ObservableObject.prototype.keys;
4230
+ ObservableObject.prototype.clone = function() {
4231
+ return Observable.init(this.val(), this.configs);
4232
+ };
4233
+ ObservableObject.prototype.$clone = ObservableObject.prototype.clone;
4234
+ ObservableObject.prototype.reset = function() {
4235
+ for(const key in this.$observables) {
4236
+ this.$observables[key].reset();
4237
+ }
4238
+ };
4239
+ ObservableObject.prototype.originalSubscribe = ObservableObject.prototype.subscribe;
4240
+ ObservableObject.prototype.subscribe = function(callback) {
4241
+ const observables = this.observables();
4242
+ const updatedValue = nextTick(() => this.trigger());
4299
4243
 
4300
- updateView();
4244
+ this.originalSubscribe(callback);
4301
4245
 
4302
- return viewArray;
4246
+ for (let i = 0, length = observables.length; i < length; i++) {
4247
+ const observable = observables[i];
4248
+ if (observable.__$isObservableArray) {
4249
+ observable.deepSubscribe(updatedValue);
4250
+ continue
4251
+ }
4252
+ observable.subscribe(updatedValue);
4253
+ }
4254
+ };
4255
+ ObservableObject.prototype.configs = function() {
4256
+ return this.configs;
4257
+ };
4258
+
4259
+ ObservableObject.prototype.update = ObservableObject.prototype.set;
4260
+
4261
+ Observable.init = function(initialValue, configs = null) {
4262
+ return new ObservableObject(initialValue, configs)
4263
+ };
4264
+
4265
+ /**
4266
+ *
4267
+ * @param {any[]} data
4268
+ * @return Proxy[]
4269
+ */
4270
+ Observable.arrayOfObject = function(data) {
4271
+ return data.map(item => Observable.object(item));
4303
4272
  };
4304
4273
 
4305
4274
  /**
4306
- * Creates a filtered view where at least one of the specified fields matches the filter.
4307
- *
4308
- * @param {Array<string>} fields - Array of field names to check
4309
- * @param {FilterResult} filter - Filter condition with callback and dependencies
4310
- * @returns {ObservableArray} A new observable array containing filtered items
4311
- * @example
4312
- * const products = Observable.array([
4313
- * { name: 'Apple', category: 'Fruit' },
4314
- * { name: 'Carrot', category: 'Vegetable' }
4315
- * ]);
4316
- * const searchTerm = Observable('App');
4317
- * const filtered = products.whereSome(['name', 'category'], match(searchTerm));
4275
+ * Get the value of an observable or an object of observables.
4276
+ * @param {ObservableItem|Object<ObservableItem>} data
4277
+ * @returns {{}|*|null}
4318
4278
  */
4319
- ObservableArray.prototype.whereSome = function(fields, filter) {
4320
- return this.where({
4321
- _: {
4322
- dependencies: filter.dependencies,
4323
- callback: (item) => fields.some(field => filter.callback(item[field]))
4279
+ Observable.value = function(data) {
4280
+ if(Validator.isObservable(data)) {
4281
+ return data.val();
4282
+ }
4283
+ if(Validator.isProxy(data)) {
4284
+ return data.$value;
4285
+ }
4286
+ if(Validator.isArray(data)) {
4287
+ const result = [];
4288
+ for(let i = 0, length = data.length; i < length; i++) {
4289
+ const item = data[i];
4290
+ result.push(Observable.value(item));
4324
4291
  }
4325
- });
4292
+ return result;
4293
+ }
4294
+ return data;
4326
4295
  };
4327
4296
 
4297
+ Observable.object = Observable.init;
4298
+ Observable.json = Observable.init;
4299
+
4328
4300
  /**
4329
- * Creates a filtered view where all specified fields match the filter.
4301
+ * Creates a computed observable that automatically updates when its dependencies change.
4302
+ * The callback is re-executed whenever any dependency observable changes.
4330
4303
  *
4331
- * @param {Array<string>} fields - Array of field names to check
4332
- * @param {FilterResult} filter - Filter condition with callback and dependencies
4333
- * @returns {ObservableArray} A new observable array containing filtered items
4304
+ * @param {Function} callback - Function that returns the computed value
4305
+ * @param {Array<ObservableItem|ObservableChecker|ObservableProxy>|Function} [dependencies=[]] - Array of observables to watch, or batch function
4306
+ * @returns {ObservableItem} A new observable that updates automatically
4334
4307
  * @example
4335
- * const items = Observable.array([
4336
- * { status: 'active', verified: true },
4337
- * { status: 'active', verified: false }
4338
- * ]);
4339
- * const activeFilter = equals('active');
4340
- * const filtered = items.whereEvery(['status', 'verified'], activeFilter);
4341
- */
4342
- ObservableArray.prototype.whereEvery = function(fields, filter) {
4343
- return this.where({
4344
- _: {
4345
- dependencies: filter.dependencies,
4346
- callback: (item) => fields.every(field => filter.callback(item[field]))
4347
- }
4348
- });
4349
- };
4350
-
4351
- ObservableArray.prototype.deepSubscribe = function(callback) {
4352
- const updatedValue = nextTick(() => callback(this.val()));
4353
- const $listeners = new WeakMap();
4308
+ * const firstName = Observable('John');
4309
+ * const lastName = Observable('Doe');
4310
+ * const fullName = Observable.computed(
4311
+ * () => `${firstName.val()} ${lastName.val()}`,
4312
+ * [firstName, lastName]
4313
+ * );
4314
+ *
4315
+ * // With batch function
4316
+ * const batch = Observable.batch(() => { ... });
4317
+ * const computed = Observable.computed(() => { ... }, batch);
4318
+ */
4319
+ Observable.computed = function(callback, dependencies = []) {
4320
+ const initialValue = callback();
4321
+ const observable = new ObservableItem(initialValue);
4322
+ const updatedValue = nextTick(() => observable.set(callback()));
4323
+ {
4324
+ PluginsManager.emit('CreateObservableComputed', observable, dependencies);
4325
+ }
4354
4326
 
4355
- const bindItem = (item) => {
4356
- if ($listeners.has(item)) {
4357
- return;
4358
- }
4359
- if (item?.__$isObservableArray) {
4360
- $listeners.set(item, item.deepSubscribe(updatedValue));
4361
- return;
4362
- }
4363
- if (item?.__$isObservable) {
4364
- item.subscribe(updatedValue);
4365
- $listeners.set(item, () => item.unsubscribe(updatedValue));
4327
+ if(Validator.isFunction(dependencies)) {
4328
+ if(!Validator.isObservable(dependencies.$observer)) {
4329
+ throw new NativeDocumentError('Observable.computed : dependencies must be valid batch function');
4366
4330
  }
4367
- };
4331
+ dependencies.$observer.subscribe(updatedValue);
4332
+ return observable;
4333
+ }
4368
4334
 
4369
- const unbindItem = (item) => {
4370
- const unsub = $listeners.get(item);
4371
- if (unsub) {
4372
- unsub();
4373
- $listeners.delete(item);
4335
+ dependencies.forEach(dependency => {
4336
+ if(Validator.isProxy(dependency)) {
4337
+ dependency.$observables.forEach((observable) => {
4338
+ observable.subscribe(updatedValue);
4339
+ });
4340
+ return;
4374
4341
  }
4375
- };
4376
-
4377
- this.$currentValue.forEach(bindItem);
4378
- this.subscribe(updatedValue);
4379
-
4380
- this.subscribe((items, _, operations) => {
4381
- switch (operations?.action) {
4382
- case 'push':
4383
- case 'unshift':
4384
- operations.args.forEach(bindItem);
4385
- break;
4342
+ dependency.subscribe(updatedValue);
4343
+ });
4386
4344
 
4387
- case 'splice': {
4388
- const [start, deleteCount, ...newItems] = operations.args;
4389
- operations.result?.forEach(unbindItem);
4390
- newItems.forEach(bindItem);
4391
- break;
4392
- }
4345
+ return observable;
4346
+ };
4393
4347
 
4394
- case 'remove':
4395
- unbindItem(operations.result);
4396
- break;
4348
+ const StoreFactory = function() {
4397
4349
 
4398
- case 'merge':
4399
- operations.args.forEach(bindItem);
4400
- break;
4350
+ const $stores = new Map();
4351
+ const $followersCache = new Map();
4401
4352
 
4402
- case 'clear':
4403
- this.$currentValue.forEach(unbindItem);
4404
- break;
4353
+ /**
4354
+ * Internal helper — retrieves a store entry or throws if not found.
4355
+ */
4356
+ const $getStoreOrThrow = (method, name) => {
4357
+ const item = $stores.get(name);
4358
+ if (!item) {
4359
+ DebugManager.error('Store', `Store.${method}('${name}') : store not found. Did you call Store.create('${name}') first?`);
4360
+ throw new NativeDocumentError(
4361
+ `Store.${method}('${name}') : store not found.`
4362
+ );
4405
4363
  }
4406
- });
4407
-
4408
- return () => {
4409
- this.$currentValue.forEach(unbindItem);
4364
+ return item;
4410
4365
  };
4411
- };
4412
4366
 
4413
- /**
4414
- * Creates an observable array with reactive array methods.
4415
- * All mutations trigger updates automatically.
4416
- *
4417
- * @param {Array} [target=[]] - Initial array value
4418
- * @param {Object|null} [configs=null] - Configuration options
4419
- * // @param {boolean} [configs.propagation=true] - Whether to propagate changes to parent observables
4420
- * // @param {boolean} [configs.deep=false] - Whether to make nested objects observable
4421
- * @param {boolean} [configs.reset=false] - Whether to store initial value for reset()
4422
- * @returns {ObservableArray} An observable array with reactive methods
4423
- * @example
4424
- * const items = Observable.array([1, 2, 3]);
4425
- * items.push(4); // Triggers update
4426
- * items.subscribe((arr) => console.log(arr));
4427
- */
4428
- Observable.array = function(target = [], configs = null) {
4429
- return new ObservableArray(target, configs);
4430
- };
4367
+ /**
4368
+ * Internal helper blocks write operations on a read-only observer.
4369
+ */
4370
+ const $applyReadOnly = (observer, name, context) => {
4371
+ const readOnlyError = (method) => () => {
4372
+ DebugManager.error('Store', `Store.${context}('${name}') is read-only. '${method}()' is not allowed.`);
4373
+ throw new NativeDocumentError(
4374
+ `Store.${context}('${name}') is read-only.`
4375
+ );
4376
+ };
4377
+ observer.set = readOnlyError('set');
4378
+ observer.toggle = readOnlyError('toggle');
4379
+ observer.reset = readOnlyError('reset');
4380
+ };
4431
4381
 
4432
- /**
4433
- *
4434
- * @param {Function} callback
4435
- * @returns {Function}
4436
- */
4437
- Observable.batch = function(callback) {
4438
- const $observer = Observable(0);
4439
- const batch = function() {
4440
- if(Validator.isAsyncFunction(callback)) {
4441
- return (callback(...arguments)).then(() => {
4442
- $observer.trigger();
4443
- }).catch(error => { throw error; });
4382
+ const $createObservable = (value, options = {}) => {
4383
+ if(Array.isArray(value)) {
4384
+ return Observable.array(value, options);
4444
4385
  }
4445
- callback(...arguments);
4446
- $observer.trigger();
4386
+ if(typeof value === 'object') {
4387
+ return Observable.object(value, options);
4388
+ }
4389
+ return Observable(value, options);
4447
4390
  };
4448
- batch.$observer = $observer;
4449
- return batch;
4450
- };
4451
4391
 
4452
- const ObservableObject = function(target, configs) {
4453
- ObservableItem.call(this, target);
4454
- this.$observables = {};
4455
- this.configs = configs;
4392
+ const $api = {
4393
+ /**
4394
+ * Create a new state and return the observer.
4395
+ * Throws if a store with the same name already exists.
4396
+ *
4397
+ * @param {string} name
4398
+ * @param {*} value
4399
+ * @returns {ObservableItem}
4400
+ */
4401
+ create(name, value) {
4402
+ if ($stores.has(name)) {
4403
+ DebugManager.warn('Store', `Store.create('${name}') : a store with this name already exists. Use Store.get('${name}') to retrieve it.`);
4404
+ throw new NativeDocumentError(
4405
+ `Store.create('${name}') : a store with this name already exists.`
4406
+ );
4407
+ }
4408
+ const observer = $createObservable(value);
4409
+ $stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: false });
4410
+ return observer;
4411
+ },
4412
+
4413
+ /**
4414
+ * Create a new resettable state and return the observer.
4415
+ * The store can be reset to its initial value via Store.reset(name).
4416
+ * Throws if a store with the same name already exists.
4417
+ *
4418
+ * @param {string} name
4419
+ * @param {*} value
4420
+ * @returns {ObservableItem}
4421
+ */
4422
+ createResettable(name, value) {
4423
+ if ($stores.has(name)) {
4424
+ DebugManager.warn('Store', `Store.createResettable('${name}') : a store with this name already exists.`);
4425
+ throw new NativeDocumentError(
4426
+ `Store.createResettable('${name}') : a store with this name already exists.`
4427
+ );
4428
+ }
4429
+ const observer = $createObservable(value, { reset: true });
4430
+ $stores.set(name, { observer, subscribers: new Set(), resettable: true, composed: false });
4431
+ return observer;
4432
+ },
4456
4433
 
4457
- this.$load(target);
4434
+ /**
4435
+ * Create a computed store derived from other stores.
4436
+ * The value is automatically recalculated when any dependency changes.
4437
+ * This store is read-only — Store.use() and Store.set() will throw.
4438
+ * Throws if a store with the same name already exists.
4439
+ *
4440
+ * @param {string} name
4441
+ * @param {() => *} computation - Function that returns the computed value
4442
+ * @param {string[]} dependencies - Names of the stores to watch
4443
+ * @returns {ObservableItem}
4444
+ *
4445
+ * @example
4446
+ * Store.create('products', [{ id: 1, price: 10 }]);
4447
+ * Store.create('cart', [{ productId: 1, quantity: 2 }]);
4448
+ *
4449
+ * Store.createComposed('total', () => {
4450
+ * const products = Store.get('products').val();
4451
+ * const cart = Store.get('cart').val();
4452
+ * return cart.reduce((sum, item) => {
4453
+ * const product = products.find(p => p.id === item.productId);
4454
+ * return sum + (product.price * item.quantity);
4455
+ * }, 0);
4456
+ * }, ['products', 'cart']);
4457
+ */
4458
+ createComposed(name, computation, dependencies) {
4459
+ if ($stores.has(name)) {
4460
+ DebugManager.warn('Store', `Store.createComposed('${name}') : a store with this name already exists.`);
4461
+ throw new NativeDocumentError(
4462
+ `Store.createComposed('${name}') : a store with this name already exists.`
4463
+ );
4464
+ }
4465
+ if (typeof computation !== 'function') {
4466
+ throw new NativeDocumentError(
4467
+ `Store.createComposed('${name}') : computation must be a function.`
4468
+ );
4469
+ }
4470
+ if (!Array.isArray(dependencies) || dependencies.length === 0) {
4471
+ throw new NativeDocumentError(
4472
+ `Store.createComposed('${name}') : dependencies must be a non-empty array of store names.`
4473
+ );
4474
+ }
4458
4475
 
4459
- for(const name in target) {
4460
- if(!Object.hasOwn(this, name)) {
4461
- Object.defineProperty(this, name, {
4462
- get: () => this.$observables[name],
4463
- set: (value) => this.$observables[name].set(value)
4476
+ // Resolve dependency observers
4477
+ const depObservers = dependencies.map(depName => {
4478
+ if(typeof depName !== 'string') {
4479
+ return depName;
4480
+ }
4481
+ const depItem = $stores.get(depName);
4482
+ if (!depItem) {
4483
+ DebugManager.error('Store', `Store.createComposed('${name}') : dependency '${depName}' not found. Create it first.`);
4484
+ throw new NativeDocumentError(
4485
+ `Store.createComposed('${name}') : dependency store '${depName}' not found.`
4486
+ );
4487
+ }
4488
+ return depItem.observer;
4464
4489
  });
4465
- }
4466
- }
4467
-
4468
- };
4469
4490
 
4470
- ObservableObject.prototype = Object.create(ObservableItem.prototype);
4491
+ // Create computed observable from dependency observers
4492
+ const observer = Observable.computed(computation, depObservers);
4471
4493
 
4472
- Object.defineProperty(ObservableObject, '$value', {
4473
- get() {
4474
- return this.val();
4475
- },
4476
- set(value) {
4477
- this.set(value);
4478
- }
4479
- });
4494
+ $stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: true });
4495
+ return observer;
4496
+ },
4480
4497
 
4481
- ObservableObject.prototype.__$isObservableObject = true;
4482
- ObservableObject.prototype.__isProxy__ = true;
4498
+ /**
4499
+ * Returns true if a store with the given name exists.
4500
+ *
4501
+ * @param {string} name
4502
+ * @returns {boolean}
4503
+ */
4504
+ has(name) {
4505
+ return $stores.has(name);
4506
+ },
4483
4507
 
4484
- ObservableObject.prototype.$load = function(initialValue) {
4485
- const configs = this.configs;
4486
- for(const key in initialValue) {
4487
- const itemValue = initialValue[key];
4488
- if(Array.isArray(itemValue)) {
4489
- if(configs?.deep !== false) {
4490
- const mappedItemValue = itemValue.map(item => {
4491
- if(Validator.isJson(item)) {
4492
- return Observable.json(item, configs);
4493
- }
4494
- if(Validator.isArray(item)) {
4495
- return Observable.array(item, configs);
4496
- }
4497
- return Observable(item, configs);
4498
- });
4499
- this.$observables[key] = Observable.array(mappedItemValue, configs);
4500
- continue;
4508
+ /**
4509
+ * Resets a resettable store to its initial value and notifies all subscribers.
4510
+ * Throws if the store was not created with createResettable().
4511
+ *
4512
+ * @param {string} name
4513
+ */
4514
+ reset(name) {
4515
+ const item = $getStoreOrThrow('reset', name);
4516
+ if (item.composed) {
4517
+ DebugManager.error('Store', `Store.reset('${name}') : composed stores cannot be reset. Their value is derived from dependencies.`);
4518
+ throw new NativeDocumentError(
4519
+ `Store.reset('${name}') : composed stores cannot be reset.`
4520
+ );
4501
4521
  }
4502
- this.$observables[key] = Observable.array(itemValue, configs);
4503
- continue;
4504
- }
4505
- if(Validator.isObservable(itemValue) || Validator.isProxy(itemValue)) {
4506
- this.$observables[key] = itemValue;
4507
- continue;
4508
- }
4509
- this.$observables[key] = (typeof itemValue === 'object') ? Observable.object(itemValue, configs) : Observable(itemValue, configs);
4510
- }
4511
- };
4522
+ if (!item.resettable) {
4523
+ DebugManager.error('Store', `Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`);
4524
+ throw new NativeDocumentError(
4525
+ `Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`
4526
+ );
4527
+ }
4528
+ item.observer.reset();
4529
+ },
4512
4530
 
4513
- ObservableObject.prototype.val = function() {
4514
- const result = {};
4515
- for(const key in this.$observables) {
4516
- const dataItem = this.$observables[key];
4517
- if(Validator.isObservable(dataItem)) {
4518
- let value = dataItem.val();
4519
- if(Array.isArray(value)) {
4520
- value = value.map(item => {
4521
- if(Validator.isObservable(item)) {
4522
- return item.val();
4523
- }
4524
- if(Validator.isProxy(item)) {
4525
- return item.$value;
4526
- }
4527
- return item;
4528
- });
4531
+ /**
4532
+ * Returns a two-way synchronized follower of the store.
4533
+ * Writing to the follower propagates the value back to the store and all its subscribers.
4534
+ * Throws if called on a composed store — use Store.follow() instead.
4535
+ * Call follower.destroy() or follower.dispose() to unsubscribe.
4536
+ *
4537
+ * @param {string} name
4538
+ * @returns {ObservableItem}
4539
+ */
4540
+ use(name) {
4541
+ const item = $getStoreOrThrow('use', name);
4542
+
4543
+ if (item.composed) {
4544
+ DebugManager.error('Store', `Store.use('${name}') : composed stores are read-only. Use Store.follow('${name}') instead.`);
4545
+ throw new NativeDocumentError(
4546
+ `Store.use('${name}') : composed stores are read-only. Use Store.follow('${name}') instead.`
4547
+ );
4529
4548
  }
4530
- result[key] = value;
4531
- } else if(Validator.isProxy(dataItem)) {
4532
- result[key] = dataItem.$value;
4533
- } else {
4534
- result[key] = dataItem;
4535
- }
4536
- }
4537
- return result;
4538
- };
4539
- ObservableObject.prototype.$val = ObservableObject.prototype.val;
4540
4549
 
4541
- ObservableObject.prototype.get = function(property) {
4542
- const item = this.$observables[property];
4543
- if(Validator.isObservable(item)) {
4544
- return item.val();
4545
- }
4546
- if(Validator.isProxy(item)) {
4547
- return item.$value;
4548
- }
4549
- return item;
4550
- };
4551
- ObservableObject.prototype.$get = ObservableObject.prototype.get;
4550
+ const { observer: originalObserver, subscribers } = item;
4551
+ const observerFollower = $createObservable(originalObserver.val());
4552
4552
 
4553
- ObservableObject.prototype.set = function(newData) {
4554
- const data = Validator.isProxy(newData) ? newData.$value : newData;
4555
- const configs = this.configs;
4553
+ const onStoreChange = value => observerFollower.set(value);
4554
+ const onFollowerChange = value => originalObserver.set(value);
4556
4555
 
4557
- for(const key in data) {
4558
- const targetItem = this.$observables[key];
4559
- const newValueOrigin = newData[key];
4560
- const newValue = data[key];
4556
+ originalObserver.subscribe(onStoreChange);
4557
+ observerFollower.subscribe(onFollowerChange);
4561
4558
 
4562
- if(Validator.isObservable(targetItem)) {
4563
- if(!Validator.isArray(newValue)) {
4564
- targetItem.set(newValue);
4565
- continue;
4566
- }
4567
- const firstElementFromOriginalValue = newValueOrigin.at(0);
4568
- if(Validator.isObservable(firstElementFromOriginalValue) || Validator.isProxy(firstElementFromOriginalValue)) {
4569
- const newValues = newValue.map(item => {
4570
- if(Validator.isProxy(firstElementFromOriginalValue)) {
4571
- return Observable.init(item, configs);
4572
- }
4573
- return Observable(item, configs);
4574
- });
4575
- targetItem.set(newValues);
4576
- continue;
4577
- }
4578
- targetItem.set([...newValue]);
4579
- continue;
4580
- }
4581
- if(Validator.isProxy(targetItem)) {
4582
- targetItem.update(newValue);
4583
- continue;
4584
- }
4585
- this[key] = newValue;
4586
- }
4587
- };
4588
- ObservableObject.prototype.$set = ObservableObject.prototype.set;
4589
- ObservableObject.prototype.$updateWith = ObservableObject.prototype.set;
4559
+ observerFollower.destroy = () => {
4560
+ originalObserver.unsubscribe(onStoreChange);
4561
+ observerFollower.unsubscribe(onFollowerChange);
4562
+ subscribers.delete(observerFollower);
4563
+ observerFollower.cleanup();
4564
+ };
4565
+ observerFollower.dispose = observerFollower.destroy;
4590
4566
 
4591
- ObservableObject.prototype.observables = function() {
4592
- return Object.values(this.$observables);
4593
- };
4594
- ObservableObject.prototype.$observables = ObservableObject.prototype.observables;
4567
+ subscribers.add(observerFollower);
4568
+ return observerFollower;
4569
+ },
4570
+
4571
+ /**
4572
+ * Returns a read-only follower of the store.
4573
+ * The follower reflects store changes but cannot write back to the store.
4574
+ * Any attempt to call .set(), .toggle() or .reset() will throw.
4575
+ * Call follower.destroy() or follower.dispose() to unsubscribe.
4576
+ *
4577
+ * @param {string} name
4578
+ * @returns {ObservableItem}
4579
+ */
4580
+ follow(name) {
4581
+ const { observer: originalObserver, subscribers } = $getStoreOrThrow('follow', name);
4582
+ const observerFollower = $createObservable(originalObserver.val());
4595
4583
 
4596
- ObservableObject.prototype.keys = function() {
4597
- return Object.keys(this.$observables);
4598
- };
4599
- ObservableObject.prototype.$keys = ObservableObject.prototype.keys;
4600
- ObservableObject.prototype.clone = function() {
4601
- return Observable.init(this.val(), this.configs);
4602
- };
4603
- ObservableObject.prototype.$clone = ObservableObject.prototype.clone;
4604
- ObservableObject.prototype.reset = function() {
4605
- for(const key in this.$observables) {
4606
- this.$observables[key].reset();
4607
- }
4608
- };
4609
- ObservableObject.prototype.originalSubscribe = ObservableObject.prototype.subscribe;
4610
- ObservableObject.prototype.subscribe = function(callback) {
4611
- const observables = this.observables();
4612
- const updatedValue = nextTick(() => this.trigger());
4584
+ const onStoreChange = value => observerFollower.set(value);
4585
+ originalObserver.subscribe(onStoreChange);
4613
4586
 
4614
- this.originalSubscribe(callback);
4587
+ $applyReadOnly(observerFollower, name, 'follow');
4615
4588
 
4616
- for (let i = 0, length = observables.length; i < length; i++) {
4617
- const observable = observables[i];
4618
- if (observable.__$isObservableArray) {
4619
- observable.deepSubscribe(updatedValue);
4620
- continue
4621
- }
4622
- observable.subscribe(updatedValue);
4623
- }
4624
- };
4625
- ObservableObject.prototype.configs = function() {
4626
- return this.configs;
4627
- };
4589
+ observerFollower.destroy = () => {
4590
+ originalObserver.unsubscribe(onStoreChange);
4591
+ subscribers.delete(observerFollower);
4592
+ observerFollower.cleanup();
4593
+ };
4594
+ observerFollower.dispose = observerFollower.destroy;
4628
4595
 
4629
- ObservableObject.prototype.update = ObservableObject.prototype.set;
4596
+ subscribers.add(observerFollower);
4597
+ return observerFollower;
4598
+ },
4630
4599
 
4631
- Observable.init = function(initialValue, configs = null) {
4632
- return new ObservableObject(initialValue, configs)
4633
- };
4600
+ /**
4601
+ * Returns the raw store observer directly (no follower, no cleanup contract).
4602
+ * Use this for direct read access when you don't need to unsubscribe.
4603
+ * WARNING : mutations on this observer impact all subscribers immediately.
4604
+ *
4605
+ * @param {string} name
4606
+ * @returns {ObservableItem|null}
4607
+ */
4608
+ get(name) {
4609
+ const item = $stores.get(name);
4610
+ if (!item) {
4611
+ DebugManager.warn('Store', `Store.get('${name}') : store not found.`);
4612
+ return null;
4613
+ }
4614
+ return item.observer;
4615
+ },
4634
4616
 
4635
- /**
4636
- *
4637
- * @param {any[]} data
4638
- * @return Proxy[]
4639
- */
4640
- Observable.arrayOfObject = function(data) {
4641
- return data.map(item => Observable.object(item));
4642
- };
4617
+ /**
4618
+ * @param {string} name
4619
+ * @returns {{ observer: ObservableItem, subscribers: Set } | null}
4620
+ */
4621
+ getWithSubscribers(name) {
4622
+ return $stores.get(name) ?? null;
4623
+ },
4643
4624
 
4644
- /**
4645
- * Get the value of an observable or an object of observables.
4646
- * @param {ObservableItem|Object<ObservableItem>} data
4647
- * @returns {{}|*|null}
4648
- */
4649
- Observable.value = function(data) {
4650
- if(Validator.isObservable(data)) {
4651
- return data.val();
4652
- }
4653
- if(Validator.isProxy(data)) {
4654
- return data.$value;
4655
- }
4656
- if(Validator.isArray(data)) {
4657
- const result = [];
4658
- for(let i = 0, length = data.length; i < length; i++) {
4659
- const item = data[i];
4660
- result.push(Observable.value(item));
4661
- }
4662
- return result;
4663
- }
4664
- return data;
4665
- };
4625
+ /**
4626
+ * Destroys a store : cleans up the observer, destroys all followers, and removes the entry.
4627
+ *
4628
+ * @param {string} name
4629
+ */
4630
+ delete(name) {
4631
+ const item = $stores.get(name);
4632
+ if (!item) {
4633
+ DebugManager.warn('Store', `Store.delete('${name}') : store not found, nothing to delete.`);
4634
+ return;
4635
+ }
4636
+ item.subscribers.forEach(follower => follower.destroy());
4637
+ item.subscribers.clear();
4638
+ item.observer.cleanup();
4639
+ $stores.delete(name);
4640
+ },
4641
+ /**
4642
+ * Creates an isolated store group with its own state namespace.
4643
+ * Each group is a fully independent StoreFactory instance —
4644
+ * no key conflicts, no shared state with the parent store.
4645
+ *
4646
+ * @param {string | ((group: ReturnType<typeof StoreFactory>) => void)} name - Group name for debugging, or setup callback if no name is provided
4647
+ * @param {((group: ReturnType<typeof StoreFactory>) => void)} [callback] - Setup function receiving the isolated store instance
4648
+ * @returns {ReturnType<typeof StoreFactory>}
4649
+ *
4650
+ * @example
4651
+ * // With name (recommended)
4652
+ * const EventStore = Store.group('events', (group) => {
4653
+ * group.create('catalog', []);
4654
+ * group.create('filters', { category: null, date: null });
4655
+ * group.createResettable('selected', null);
4656
+ * group.createComposed('filtered', () => {
4657
+ * const catalog = EventStore.get('catalog').val();
4658
+ * const filters = EventStore.get('filters').val();
4659
+ * return catalog.filter(event => {
4660
+ * if (filters.category && event.category !== filters.category) return false;
4661
+ * return true;
4662
+ * });
4663
+ * }, ['catalog', 'filters']);
4664
+ * });
4665
+ *
4666
+ * // Without name
4667
+ * const CartStore = Store.group((group) => {
4668
+ * group.create('items', []);
4669
+ * });
4670
+ *
4671
+ * // Usage
4672
+ * EventStore.use('catalog'); // two-way follower
4673
+ * EventStore.follow('filtered'); // read-only follower
4674
+ * EventStore.get('filters'); // raw observable
4675
+ *
4676
+ * // Cross-group composed
4677
+ * const OrderStore = Store.group('orders', (group) => {
4678
+ * group.createComposed('summary', () => {
4679
+ * const items = CartStore.get('items').val();
4680
+ * const events = EventStore.get('catalog').val();
4681
+ * return { items, events };
4682
+ * }, [CartStore.get('items'), EventStore.get('catalog')]);
4683
+ * });
4684
+ */
4685
+ group(name, callback) {
4686
+ if (typeof name === 'function') {
4687
+ callback = name;
4688
+ name = 'anonymous';
4689
+ }
4690
+ const store = StoreFactory();
4691
+ callback && callback(store);
4692
+ return store;
4693
+ },
4694
+ createPersistent(name, value, localstorage_key) {
4695
+ localstorage_key = localstorage_key || name;
4696
+ const observer = this.create(name, $getFromStorage(localstorage_key, value));
4697
+ const saver = $saveToStorage(value);
4666
4698
 
4667
- Observable.object = Observable.init;
4668
- Observable.json = Observable.init;
4699
+ observer.subscribe((val) => saver(localstorage_key, val));
4700
+ return observer;
4701
+ },
4702
+ createPersistentResettable(name, value, localstorage_key) {
4703
+ localstorage_key = localstorage_key || name;
4704
+ const observer = this.createResettable(name, $getFromStorage(localstorage_key, value));
4705
+ const saver = $saveToStorage(value);
4706
+ observer.subscribe((val) => saver(localstorage_key, val));
4669
4707
 
4670
- /**
4671
- * Creates a computed observable that automatically updates when its dependencies change.
4672
- * The callback is re-executed whenever any dependency observable changes.
4673
- *
4674
- * @param {Function} callback - Function that returns the computed value
4675
- * @param {Array<ObservableItem|ObservableChecker|ObservableProxy>|Function} [dependencies=[]] - Array of observables to watch, or batch function
4676
- * @returns {ObservableItem} A new observable that updates automatically
4677
- * @example
4678
- * const firstName = Observable('John');
4679
- * const lastName = Observable('Doe');
4680
- * const fullName = Observable.computed(
4681
- * () => `${firstName.val()} ${lastName.val()}`,
4682
- * [firstName, lastName]
4683
- * );
4684
- *
4685
- * // With batch function
4686
- * const batch = Observable.batch(() => { ... });
4687
- * const computed = Observable.computed(() => { ... }, batch);
4688
- */
4689
- Observable.computed = function(callback, dependencies = []) {
4690
- const initialValue = callback();
4691
- const observable = new ObservableItem(initialValue);
4692
- const updatedValue = nextTick(() => observable.set(callback()));
4693
- {
4694
- PluginsManager$1.emit('CreateObservableComputed', observable, dependencies);
4695
- }
4708
+ const originalReset = observer.reset.bind(observer);
4709
+ observer.reset = () => {
4710
+ LocalStorage.remove(localstorage_key);
4711
+ originalReset();
4712
+ };
4696
4713
 
4697
- if(Validator.isFunction(dependencies)) {
4698
- if(!Validator.isObservable(dependencies.$observer)) {
4699
- throw new NativeDocumentError('Observable.computed : dependencies must be valid batch function');
4714
+ return observer;
4700
4715
  }
4701
- dependencies.$observer.subscribe(updatedValue);
4702
- return observable;
4703
- }
4716
+ };
4704
4717
 
4705
- dependencies.forEach(dependency => {
4706
- if(Validator.isProxy(dependency)) {
4707
- dependency.$observables.forEach((observable) => {
4708
- observable.subscribe(updatedValue);
4709
- });
4710
- return;
4718
+
4719
+ return new Proxy($api, {
4720
+ get(target, prop) {
4721
+ if (typeof prop === 'symbol' || prop.startsWith('$') || prop in target) {
4722
+ return target[prop];
4723
+ }
4724
+ if (target.has(prop)) {
4725
+ if ($followersCache.has(prop)) {
4726
+ return $followersCache.get(prop);
4727
+ }
4728
+ const follower = target.follow(prop);
4729
+ $followersCache.set(prop, follower);
4730
+ return follower;
4731
+ }
4732
+ return undefined;
4733
+ },
4734
+ set(target, prop, value) {
4735
+ DebugManager.error('Store', `Forbidden: You cannot overwrite the store key '${String(prop)}'. Use .use('${String(prop)}').set(value) instead.`);
4736
+ throw new NativeDocumentError(`Store structure is immutable. Use .set() on the observable.`);
4737
+ },
4738
+ deleteProperty(target, prop) {
4739
+ throw new NativeDocumentError(`Store keys cannot be deleted.`);
4711
4740
  }
4712
- dependency.subscribe(updatedValue);
4713
4741
  });
4714
-
4715
- return observable;
4716
4742
  };
4717
4743
 
4744
+ const Store = StoreFactory();
4745
+
4746
+ Store.create('locale', navigator.language.split('-')[0] || 'en');
4747
+
4718
4748
  /**
4719
4749
  * Renders a list of items from an observable array or object, automatically updating when data changes.
4720
4750
  * Efficiently manages DOM updates by tracking items with keys.
@@ -4790,7 +4820,7 @@ var NativeDocument = (function (exports) {
4790
4820
  }
4791
4821
  cache.set(keyId, { keyId, isNew: true, child: new WeakRef(child), indexObserver});
4792
4822
  } catch (e) {
4793
- DebugManager$1.error('ForEach', `Error creating element for key ${keyId}` , e);
4823
+ DebugManager.error('ForEach', `Error creating element for key ${keyId}` , e);
4794
4824
  throw e;
4795
4825
  }
4796
4826
  return keyId;
@@ -5180,7 +5210,7 @@ var NativeDocument = (function (exports) {
5180
5210
  */
5181
5211
  const ShowIf = function(condition, child, { comment = null, shouldKeepInCache = true} = {}) {
5182
5212
  if(!(Validator.isObservable(condition)) && !Validator.isObservableWhenResult(condition)) {
5183
- return DebugManager$1.warn('ShowIf', "ShowIf : condition must be an Observable / "+comment, condition);
5213
+ return DebugManager.warn('ShowIf', "ShowIf : condition must be an Observable / "+comment, condition);
5184
5214
  }
5185
5215
  const element = Anchor('Show if : '+(comment || ''));
5186
5216
 
@@ -6599,7 +6629,7 @@ var NativeDocument = (function (exports) {
6599
6629
  window.history.pushState({ name: route.name(), params, path}, route.name() || path , path);
6600
6630
  this.handleRouteChange(route, params, query, path);
6601
6631
  } catch (e) {
6602
- DebugManager$1.error('HistoryRouter', 'Error in pushState', e);
6632
+ DebugManager.error('HistoryRouter', 'Error in pushState', e);
6603
6633
  }
6604
6634
  };
6605
6635
  /**
@@ -6612,7 +6642,7 @@ var NativeDocument = (function (exports) {
6612
6642
  window.history.replaceState({ name: route.name(), params, path}, route.name() || path , path);
6613
6643
  this.handleRouteChange(route, params, {}, path);
6614
6644
  } catch(e) {
6615
- DebugManager$1.error('HistoryRouter', 'Error in replaceState', e);
6645
+ DebugManager.error('HistoryRouter', 'Error in replaceState', e);
6616
6646
  }
6617
6647
  };
6618
6648
  this.forward = function() {
@@ -6639,7 +6669,7 @@ var NativeDocument = (function (exports) {
6639
6669
  }
6640
6670
  this.handleRouteChange(route, params, query, path);
6641
6671
  } catch(e) {
6642
- DebugManager$1.error('HistoryRouter', 'Error in popstate event', e);
6672
+ DebugManager.error('HistoryRouter', 'Error in popstate event', e);
6643
6673
  }
6644
6674
  });
6645
6675
  const { route, params, query, path } = this.resolve(defaultPath || (window.location.pathname+window.location.search));
@@ -6864,7 +6894,7 @@ var NativeDocument = (function (exports) {
6864
6894
  listener(request);
6865
6895
  next && next(request);
6866
6896
  } catch (e) {
6867
- DebugManager$1.warn('Route Listener', 'Error in listener:', e);
6897
+ DebugManager.warn('Route Listener', 'Error in listener:', e);
6868
6898
  }
6869
6899
  }
6870
6900
  };
@@ -7042,7 +7072,7 @@ var NativeDocument = (function (exports) {
7042
7072
  */
7043
7073
  Router.create = function(options, callback) {
7044
7074
  if(!Validator.isFunction(callback)) {
7045
- DebugManager$1.error('Router', 'Callback must be a function');
7075
+ DebugManager.error('Router', 'Callback must be a function');
7046
7076
  throw new RouterError('Callback must be a function');
7047
7077
  }
7048
7078
  const router = new Router(options);
@@ -7246,7 +7276,7 @@ var NativeDocument = (function (exports) {
7246
7276
  exports.HtmlElementWrapper = HtmlElementWrapper;
7247
7277
  exports.NDElement = NDElement;
7248
7278
  exports.Observable = Observable;
7249
- exports.PluginsManager = PluginsManager$1;
7279
+ exports.PluginsManager = PluginsManager;
7250
7280
  exports.SingletonView = SingletonView;
7251
7281
  exports.Store = Store;
7252
7282
  exports.StoreFactory = StoreFactory;