native-document 1.0.117 → 1.0.119

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$1 = {};
4
+ let DebugManager$2 = {};
5
5
 
6
6
  {
7
- DebugManager$1 = {
7
+ DebugManager$2 = {
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 = DebugManager$1;
38
+ var DebugManager$1 = DebugManager$2;
39
39
 
40
40
  class NativeDocumentError extends Error {
41
41
  constructor(message, context = {}) {
@@ -382,7 +382,7 @@ var NativeDocument = (function (exports) {
382
382
  try{
383
383
  callback.call(plugin, ...data);
384
384
  } catch (error) {
385
- DebugManager.error('Plugin Manager', `Error in plugin ${plugin.$name} for event ${eventName}`, error);
385
+ DebugManager$1.error('Plugin Manager', `Error in plugin ${plugin.$name} for event ${eventName}`, error);
386
386
  }
387
387
  }
388
388
  }
@@ -561,7 +561,7 @@ var NativeDocument = (function (exports) {
561
561
  }
562
562
  {
563
563
  if (this[name] && !this.$localExtensions.has(name)) {
564
- DebugManager.warn('NDElement.extend', `Method "${name}" already exists and will be overwritten`);
564
+ DebugManager$1.warn('NDElement.extend', `Method "${name}" already exists and will be overwritten`);
565
565
  }
566
566
  this.$localExtensions.set(name, method);
567
567
  }
@@ -612,17 +612,17 @@ var NativeDocument = (function (exports) {
612
612
  const method = methods[name];
613
613
 
614
614
  if (typeof method !== 'function') {
615
- DebugManager.warn('NDElement.extend', `"${name}" is not a function, skipping`);
615
+ DebugManager$1.warn('NDElement.extend', `"${name}" is not a function, skipping`);
616
616
  continue;
617
617
  }
618
618
 
619
619
  if (protectedMethods.has(name)) {
620
- DebugManager.error('NDElement.extend', `Cannot override protected method "${name}"`);
620
+ DebugManager$1.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.warn('NDElement.extend', `Overwriting existing prototype method "${name}"`);
625
+ DebugManager$1.warn('NDElement.extend', `Overwriting existing prototype method "${name}"`);
626
626
  }
627
627
 
628
628
  NDElement.prototype[name] = method;
@@ -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.warn('Validator', `Reserved attributes found: ${foundReserved.join(', ')}`);
782
+ DebugManager$1.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.log('Memory Auto Clean', `🧹 Cleaned ${cleanedCount} orphaned observables`);
869
+ DebugManager$1.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.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.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
- };
1103
-
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.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
- },
1177
+ }
1178
+ {
1179
+ PluginsManager.emit('CreateObservable', this);
1180
+ }
1181
+ }
1124
1182
 
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.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
- },
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
+ });
1145
1192
 
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.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
- }
1193
+ ObservableItem.prototype.__$isObservable = true;
1194
+ const noneTrigger = function() {};
1187
1195
 
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.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
- });
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
+ };
1202
1211
 
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.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.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.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.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.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.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.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.emit('ObservableBeforeChange', this);
1684
- }
1685
- this.trigger();
1686
- this.$previousValue = null;
1687
- {
1688
- PluginsManager.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.warn('Observable subscription', '⚠️ Attempted to subscribe to a cleaned up observable.');
1366
+ DebugManager$1.warn('Observable subscription', '⚠️ Attempted to subscribe to a cleaned up observable.');
1767
1367
  return;
1768
1368
  }
1769
1369
  if (typeof callback !== 'function') {
@@ -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
  /**
@@ -2323,6 +1922,10 @@ var NativeDocument = (function (exports) {
2323
1922
  return ElementCreator.createStaticTextNode(null, this);
2324
1923
  };
2325
1924
 
1925
+ Number.prototype.toNdElement = function () {
1926
+ return ElementCreator.createStaticTextNode(null, this.toString());
1927
+ };
1928
+
2326
1929
  Element.prototype.toNdElement = function () {
2327
1930
  return this;
2328
1931
  };
@@ -2576,30 +2179,125 @@ var NativeDocument = (function (exports) {
2576
2179
  } while (child.toNdElement);
2577
2180
  }
2578
2181
 
2579
- return ElementCreator.createStaticTextNode(null, child);
2580
- },
2182
+ return ElementCreator.createStaticTextNode(null, child);
2183
+ },
2184
+ /**
2185
+ *
2186
+ * @param {HTMLElement} element
2187
+ * @param {Object} attributes
2188
+ */
2189
+ processAttributes: (element, attributes) => {
2190
+ if (attributes) {
2191
+ AttributesWrapper(element, attributes);
2192
+ }
2193
+ },
2194
+ /**
2195
+ *
2196
+ * @param {HTMLElement} element
2197
+ * @param {Object} attributes
2198
+ */
2199
+ processAttributesDirect: AttributesWrapper,
2200
+ processClassAttribute: bindClassAttribute,
2201
+ processStyleAttribute: bindStyleAttribute,
2202
+ };
2203
+
2204
+ function AnchorWithSentinel(name) {
2205
+ const instance = Reflect.construct(DocumentFragment, [], AnchorWithSentinel);
2206
+ const sentinel = document.createComment((name || '') + ' Anchor Sentinel');
2207
+ const events = {};
2208
+
2209
+ instance.appendChild(sentinel);
2210
+
2211
+ const observer = new MutationObserver(() => {
2212
+ if (sentinel.parentNode !== instance && !(sentinel.parentNode instanceof DocumentFragment)) {
2213
+ events.connected && events.connected(sentinel.parentNode);
2214
+ }
2215
+ });
2216
+
2217
+ observer.observe(document, { childList: true, subtree: true });
2218
+
2219
+
2220
+ instance.$sentinel = sentinel;
2221
+ instance.$observer = observer;
2222
+ instance.$events = events;
2223
+
2224
+ return instance;
2225
+ }
2226
+
2227
+ AnchorWithSentinel.prototype = Object.create(DocumentFragment.prototype);
2228
+ AnchorWithSentinel.prototype.constructor = AnchorWithSentinel;
2229
+
2230
+ AnchorWithSentinel.prototype.onConnected = function(callback) {
2231
+ this.$events.connected = callback;
2232
+ return this;
2233
+ };
2234
+
2235
+ AnchorWithSentinel.prototype.onConnectedOnce = function(callback) {
2236
+ this.$events.connected = (parent) => {
2237
+ callback(parent);
2238
+ this.$observer.disconnect();
2239
+ this.$events.connectedOnce = null;
2240
+ };
2241
+ };
2242
+
2243
+ function oneChildAnchorOverwriting(anchor, parent) {
2244
+
2245
+ anchor.remove = () => {
2246
+ anchor.append.apply(anchor, parent.childNodes);
2247
+ };
2248
+
2249
+ anchor.appendChild = (child) => {
2250
+ child = Validator.isElement(child) ? child : ElementCreator.getChild(child);
2251
+ parent.appendChild(child);
2252
+ };
2253
+
2254
+ anchor.appendElement = anchor.appendChild;
2255
+
2256
+ anchor.removeChildren = () => {
2257
+ parent.replaceChildren();
2258
+ };
2259
+
2260
+ anchor.replaceContent = function(content) {
2261
+ const child = Validator.isElement(content) ? content : ElementCreator.getChild(content);
2262
+ parent.replaceChildren(child);
2263
+ };
2264
+ anchor.setContent = anchor.replaceContent;
2265
+
2266
+ anchor.insertBefore = (child, anchor) => {
2267
+ child = Validator.isElement(child) ? child : ElementCreator.getChild(child);
2268
+ parent.insertBefore(child, anchor);
2269
+ };
2270
+ anchor.appendChildBefore = anchor.insertBefore;
2271
+
2272
+ anchor.clear = anchor.remove;
2273
+ anchor.detach = anchor.remove;
2274
+
2275
+ anchor.replaceChildren = function() {
2276
+ parent.replaceChildren(...arguments);
2277
+ };
2278
+
2279
+ anchor.getByIndex = (index) => {
2280
+ return parent.childNodes[index];
2281
+ };
2282
+ }
2283
+
2284
+ function Anchor(name, isUniqueChild = false) {
2285
+ const anchorFragment = new AnchorWithSentinel(name);
2286
+
2581
2287
  /**
2582
- *
2583
- * @param {HTMLElement} element
2584
- * @param {Object} attributes
2288
+ * State :
2289
+ * 1. Not injected in the DOM
2290
+ * 2. Injected in the DOM and should be the only child of parent
2291
+ * 3. Injected in the DOM and the parent may have other children
2585
2292
  */
2586
- processAttributes: (element, attributes) => {
2587
- if (attributes) {
2588
- AttributesWrapper(element, attributes);
2293
+
2294
+ anchorFragment.onConnectedOnce((parent) => {
2295
+ if(isUniqueChild) {
2296
+ console.log('Lets overwrite some functions with parent ', parent);
2297
+ oneChildAnchorOverwriting(anchorFragment, parent);
2589
2298
  }
2590
- },
2591
- /**
2592
- *
2593
- * @param {HTMLElement} element
2594
- * @param {Object} attributes
2595
- */
2596
- processAttributesDirect: AttributesWrapper,
2597
- processClassAttribute: bindClassAttribute,
2598
- processStyleAttribute: bindStyleAttribute,
2599
- };
2299
+ });
2600
2300
 
2601
- function Anchor(name, isUniqueChild = false) {
2602
- const anchorFragment = document.createDocumentFragment();
2603
2301
  anchorFragment.__Anchor__ = true;
2604
2302
 
2605
2303
  const anchorStart = document.createComment('Anchor Start : '+name);
@@ -2613,8 +2311,7 @@ var NativeDocument = (function (exports) {
2613
2311
  anchorFragment.nativeAppend = anchorFragment.append;
2614
2312
 
2615
2313
  const isParentUniqueChild = isUniqueChild
2616
- ? () => true
2617
- : (parent) => (parent.firstChild === anchorStart && parent.lastChild === anchorEnd);
2314
+ ? () => true: (parent) => (parent.firstChild === anchorStart && parent.lastChild === anchorEnd);
2618
2315
 
2619
2316
  const insertBefore = function(parent, child, target) {
2620
2317
  const childElement = Validator.isElement(child) ? child : ElementCreator.getChild(child);
@@ -2629,14 +2326,13 @@ var NativeDocument = (function (exports) {
2629
2326
  parent.insertBefore(childElement, target);
2630
2327
  };
2631
2328
 
2632
- anchorFragment.appendElement = function(child, before = null) {
2329
+ anchorFragment.appendElement = function(child) {
2633
2330
  const parentNode = anchorStart.parentNode;
2634
- const targetBefore = before || anchorEnd;
2635
2331
  if(parentNode === anchorFragment) {
2636
- parentNode.nativeInsertBefore(child, targetBefore);
2332
+ parentNode.nativeInsertBefore(child, anchorEnd);
2637
2333
  return;
2638
2334
  }
2639
- parentNode?.insertBefore(child, targetBefore);
2335
+ parentNode.insertBefore(child, anchorEnd);
2640
2336
  };
2641
2337
 
2642
2338
  anchorFragment.appendChild = function(child, before = null) {
@@ -2649,8 +2345,8 @@ var NativeDocument = (function (exports) {
2649
2345
  insertBefore(parent, child, before);
2650
2346
  };
2651
2347
 
2652
- anchorFragment.append = function(...args ) {
2653
- return anchorFragment.appendChild(args);
2348
+ anchorFragment.append = function() {
2349
+ return anchorFragment.appendChild(Array.from(arguments));
2654
2350
  };
2655
2351
 
2656
2352
  anchorFragment.removeChildren = function() {
@@ -2677,6 +2373,7 @@ var NativeDocument = (function (exports) {
2677
2373
  return;
2678
2374
  }
2679
2375
  if(isParentUniqueChild(parent)) {
2376
+ anchorFragment.append.apply(anchorFragment, parent.childNodes);
2680
2377
  parent.replaceChildren(anchorStart, anchorEnd);
2681
2378
  return;
2682
2379
  }
@@ -2721,9 +2418,11 @@ var NativeDocument = (function (exports) {
2721
2418
  anchorFragment.startElement = function() {
2722
2419
  return anchorStart;
2723
2420
  };
2421
+
2724
2422
  anchorFragment.restore = function() {
2725
2423
  anchorFragment.appendChild(anchorFragment);
2726
2424
  };
2425
+
2727
2426
  anchorFragment.clear = anchorFragment.remove;
2728
2427
  anchorFragment.detach = anchorFragment.remove;
2729
2428
 
@@ -3174,9 +2873,10 @@ var NativeDocument = (function (exports) {
3174
2873
  * @returns {Text}
3175
2874
  */
3176
2875
  const createTextNode = (value) => {
3177
- return (Validator.isObservable(value))
3178
- ? ElementCreator.createObservableNode(null, value)
3179
- : ElementCreator.createStaticTextNode(null, value);
2876
+ if(value) {
2877
+ return value.toNdElement();
2878
+ }
2879
+ return ElementCreator.createTextNode();
3180
2880
  };
3181
2881
 
3182
2882
 
@@ -3267,29 +2967,72 @@ var NativeDocument = (function (exports) {
3267
2967
  }
3268
2968
  const steps = [];
3269
2969
  if(this.$ndMethods) {
3270
- steps.push((clonedNode, data) => {
3271
- for(const methodName in this.$ndMethods) {
3272
- clonedNode.nd[methodName](this.$ndMethods[methodName].bind(clonedNode, ...data));
3273
- }
3274
- });
2970
+ const methods = Object.keys(this.$ndMethods);
2971
+ if(methods.length === 1) {
2972
+ const methodName = methods[0];
2973
+ const callback = this.$ndMethods[methodName];
2974
+ steps.push((clonedNode, data) => {
2975
+ clonedNode.nd[methodName](callback.bind(clonedNode, ...data));
2976
+ });
2977
+ } else {
2978
+ steps.push((clonedNode, data) => {
2979
+ const nd = clonedNode.nd;
2980
+ for(const methodName in this.$ndMethods) {
2981
+ nd[methodName](this.$ndMethods[methodName].bind(clonedNode, ...data));
2982
+ }
2983
+ });
2984
+ }
3275
2985
  }
3276
2986
  if(this.$classes) {
3277
2987
  const cache = {};
3278
- steps.push((clonedNode, data) => {
3279
- ElementCreator.processClassAttribute(clonedNode, buildProperties(cache, this.$classes, data));
3280
- });
2988
+ const keys = Object.keys(this.$classes);
2989
+
2990
+ if(keys.length === 1) {
2991
+ const key = keys[0];
2992
+ const callback = this.$classes[key];
2993
+ steps.push((clonedNode, data) => {
2994
+ cache[key] = callback.apply(null, data);
2995
+ ElementCreator.processClassAttribute(clonedNode, cache);
2996
+ });
2997
+ } else {
2998
+ steps.push((clonedNode, data) => {
2999
+ ElementCreator.processClassAttribute(clonedNode, buildProperties(cache, this.$classes, data));
3000
+ });
3001
+ }
3281
3002
  }
3282
3003
  if(this.$styles) {
3283
3004
  const cache = {};
3284
- steps.push((clonedNode, data) => {
3285
- ElementCreator.processStyleAttribute(clonedNode, buildProperties(cache, this.$styles, data));
3286
- });
3005
+ const keys = Object.keys(this.$styles);
3006
+
3007
+ if(keys.length === 1) {
3008
+ const key = keys[0];
3009
+ const callback = this.$styles[key];
3010
+ steps.push((clonedNode, data) => {
3011
+ cache[key] = callback.apply(null, data);
3012
+ ElementCreator.processStyleAttribute(clonedNode, cache);
3013
+ });
3014
+ } else {
3015
+ steps.push((clonedNode, data) => {
3016
+ ElementCreator.processStyleAttribute(clonedNode, buildProperties(cache, this.$styles, data));
3017
+ });
3018
+ }
3287
3019
  }
3288
3020
  if(this.$attrs) {
3289
3021
  const cache = {};
3290
- steps.push((clonedNode, data) => {
3291
- ElementCreator.processAttributes(clonedNode, buildProperties(cache, this.$attrs, data));
3292
- });
3022
+ const keys = Object.keys(this.$attrs);
3023
+
3024
+ if(keys.length === 1) {
3025
+ const key = keys[0];
3026
+ const callback = this.$attrs[key];
3027
+ steps.push((clonedNode, data) => {
3028
+ cache[key] = callback.apply(null, data);
3029
+ ElementCreator.processAttributes(clonedNode, cache);
3030
+ });
3031
+ } else {
3032
+ steps.push((clonedNode, data) => {
3033
+ ElementCreator.processAttributes(clonedNode, buildProperties(cache, this.$attrs, data));
3034
+ });
3035
+ }
3293
3036
  }
3294
3037
 
3295
3038
  const stepsCount = steps.length;
@@ -3379,8 +3122,7 @@ var NativeDocument = (function (exports) {
3379
3122
  $node.dynamicCloneNode = (data) => {
3380
3123
  const clonedNode = $node.nodeCloner.cloneNode(data);
3381
3124
  for(let i = 0; i < childNodesLength; i++) {
3382
- const child = childNodes[i].dynamicCloneNode(data);
3383
- clonedNode.appendChild(child);
3125
+ clonedNode.appendChild(childNodes[i].dynamicCloneNode(data));
3384
3126
  }
3385
3127
  return clonedNode;
3386
3128
  };
@@ -3388,8 +3130,7 @@ var NativeDocument = (function (exports) {
3388
3130
  $node.dynamicCloneNode = (data) => {
3389
3131
  const clonedNode = $node.cloneNode();
3390
3132
  for(let i = 0; i < childNodesLength; i++) {
3391
- const child = childNodes[i].dynamicCloneNode(data);
3392
- clonedNode.appendChild(child);
3133
+ clonedNode.appendChild(childNodes[i].dynamicCloneNode(data));
3393
3134
  }
3394
3135
  return clonedNode;
3395
3136
  };
@@ -4221,491 +3962,891 @@ var NativeDocument = (function (exports) {
4221
3962
  };
4222
3963
 
4223
3964
  /**
4224
- * Triggers a populate operation with the current array, iteration count, and callback.
4225
- * Used internally for rendering optimizations.
3965
+ * Triggers a populate operation with the current array, iteration count, and callback.
3966
+ * Used internally for rendering optimizations.
3967
+ *
3968
+ * @param {number} iteration - Iteration count for rendering
3969
+ * @param {Function} callback - Callback function for rendering items
3970
+ */
3971
+ ObservableArray.prototype.populateAndRender = function(iteration, callback) {
3972
+ this.trigger({ action: 'populate', args: [this.$currentValue, iteration, callback] });
3973
+ };
3974
+
3975
+
3976
+ /**
3977
+ * Creates a filtered view of the array based on predicates.
3978
+ * The filtered array updates automatically when source data or predicates change.
3979
+ *
3980
+ * @param {Object} predicates - Object mapping property names to filter conditions or functions
3981
+ * @returns {ObservableArray} A new observable array containing filtered items
3982
+ * @example
3983
+ * const users = Observable.array([
3984
+ * { name: 'John', age: 25 },
3985
+ * { name: 'Jane', age: 30 }
3986
+ * ]);
3987
+ * const adults = users.where({ age: (val) => val >= 18 });
3988
+ */
3989
+ ObservableArray.prototype.where = function(predicates) {
3990
+ const sourceArray = this;
3991
+ const observableDependencies = [sourceArray];
3992
+ const filterCallbacks = {};
3993
+
3994
+ for (const [key, rawPredicate] of Object.entries(predicates)) {
3995
+ const predicate = Validator.isObservable(rawPredicate) ? match(rawPredicate, false) : rawPredicate;
3996
+ if (predicate && typeof predicate === 'object' && 'callback' in predicate) {
3997
+ filterCallbacks[key] = predicate.callback;
3998
+
3999
+ if (predicate.dependencies) {
4000
+ const deps = Array.isArray(predicate.dependencies)
4001
+ ? predicate.dependencies
4002
+ : [predicate.dependencies];
4003
+ observableDependencies.push.apply(observableDependencies, deps);
4004
+ }
4005
+ } else if(typeof predicate === 'function') {
4006
+ filterCallbacks[key] = predicate;
4007
+ } else {
4008
+ filterCallbacks[key] = (value) => value === predicate;
4009
+ }
4010
+ }
4011
+
4012
+ const viewArray = Observable.array();
4013
+
4014
+ const filters = Object.entries(filterCallbacks);
4015
+ const updateView = () => {
4016
+ const filtered = sourceArray.val().filter(item => {
4017
+ for (const [key, callback] of filters) {
4018
+ if(key === '_') {
4019
+ if (!callback(item)) return false;
4020
+ } else {
4021
+ if (!callback(item[key])) return false;
4022
+ }
4023
+ }
4024
+ return true;
4025
+ });
4026
+
4027
+ viewArray.set(filtered);
4028
+ };
4029
+
4030
+ observableDependencies.forEach(dep => dep.subscribe(updateView));
4031
+
4032
+ updateView();
4033
+
4034
+ return viewArray;
4035
+ };
4036
+
4037
+ /**
4038
+ * Creates a filtered view where at least one of the specified fields matches the filter.
4039
+ *
4040
+ * @param {Array<string>} fields - Array of field names to check
4041
+ * @param {FilterResult} filter - Filter condition with callback and dependencies
4042
+ * @returns {ObservableArray} A new observable array containing filtered items
4043
+ * @example
4044
+ * const products = Observable.array([
4045
+ * { name: 'Apple', category: 'Fruit' },
4046
+ * { name: 'Carrot', category: 'Vegetable' }
4047
+ * ]);
4048
+ * const searchTerm = Observable('App');
4049
+ * const filtered = products.whereSome(['name', 'category'], match(searchTerm));
4050
+ */
4051
+ ObservableArray.prototype.whereSome = function(fields, filter) {
4052
+ return this.where({
4053
+ _: {
4054
+ dependencies: filter.dependencies,
4055
+ callback: (item) => fields.some(field => filter.callback(item[field]))
4056
+ }
4057
+ });
4058
+ };
4059
+
4060
+ /**
4061
+ * Creates a filtered view where all specified fields match the filter.
4062
+ *
4063
+ * @param {Array<string>} fields - Array of field names to check
4064
+ * @param {FilterResult} filter - Filter condition with callback and dependencies
4065
+ * @returns {ObservableArray} A new observable array containing filtered items
4066
+ * @example
4067
+ * const items = Observable.array([
4068
+ * { status: 'active', verified: true },
4069
+ * { status: 'active', verified: false }
4070
+ * ]);
4071
+ * const activeFilter = equals('active');
4072
+ * const filtered = items.whereEvery(['status', 'verified'], activeFilter);
4073
+ */
4074
+ ObservableArray.prototype.whereEvery = function(fields, filter) {
4075
+ return this.where({
4076
+ _: {
4077
+ dependencies: filter.dependencies,
4078
+ callback: (item) => fields.every(field => filter.callback(item[field]))
4079
+ }
4080
+ });
4081
+ };
4082
+
4083
+ ObservableArray.prototype.deepSubscribe = function(callback) {
4084
+ const updatedValue = nextTick(() => callback(this.val()));
4085
+ const $listeners = new WeakMap();
4086
+
4087
+ const bindItem = (item) => {
4088
+ if ($listeners.has(item)) {
4089
+ return;
4090
+ }
4091
+ if (item?.__$isObservableArray) {
4092
+ $listeners.set(item, item.deepSubscribe(updatedValue));
4093
+ return;
4094
+ }
4095
+ if (item?.__$isObservable) {
4096
+ item.subscribe(updatedValue);
4097
+ $listeners.set(item, () => item.unsubscribe(updatedValue));
4098
+ }
4099
+ };
4100
+
4101
+ const unbindItem = (item) => {
4102
+ const unsub = $listeners.get(item);
4103
+ if (unsub) {
4104
+ unsub();
4105
+ $listeners.delete(item);
4106
+ }
4107
+ };
4108
+
4109
+ this.$currentValue.forEach(bindItem);
4110
+ this.subscribe(updatedValue);
4111
+
4112
+ this.subscribe((items, _, operations) => {
4113
+ switch (operations?.action) {
4114
+ case 'push':
4115
+ case 'unshift':
4116
+ operations.args.forEach(bindItem);
4117
+ break;
4118
+
4119
+ case 'splice': {
4120
+ const [start, deleteCount, ...newItems] = operations.args;
4121
+ operations.result?.forEach(unbindItem);
4122
+ newItems.forEach(bindItem);
4123
+ break;
4124
+ }
4125
+
4126
+ case 'remove':
4127
+ unbindItem(operations.result);
4128
+ break;
4129
+
4130
+ case 'merge':
4131
+ operations.args.forEach(bindItem);
4132
+ break;
4133
+
4134
+ case 'clear':
4135
+ this.$currentValue.forEach(unbindItem);
4136
+ break;
4137
+ }
4138
+ });
4139
+
4140
+ return () => {
4141
+ this.$currentValue.forEach(unbindItem);
4142
+ };
4143
+ };
4144
+
4145
+ /**
4146
+ * Creates an observable array with reactive array methods.
4147
+ * All mutations trigger updates automatically.
4148
+ *
4149
+ * @param {Array} [target=[]] - Initial array value
4150
+ * @param {Object|null} [configs=null] - Configuration options
4151
+ * // @param {boolean} [configs.propagation=true] - Whether to propagate changes to parent observables
4152
+ * // @param {boolean} [configs.deep=false] - Whether to make nested objects observable
4153
+ * @param {boolean} [configs.reset=false] - Whether to store initial value for reset()
4154
+ * @returns {ObservableArray} An observable array with reactive methods
4155
+ * @example
4156
+ * const items = Observable.array([1, 2, 3]);
4157
+ * items.push(4); // Triggers update
4158
+ * items.subscribe((arr) => console.log(arr));
4159
+ */
4160
+ Observable.array = function(target = [], configs = null) {
4161
+ return new ObservableArray(target, configs);
4162
+ };
4163
+
4164
+ /**
4226
4165
  *
4227
- * @param {number} iteration - Iteration count for rendering
4228
- * @param {Function} callback - Callback function for rendering items
4166
+ * @param {Function} callback
4167
+ * @returns {Function}
4229
4168
  */
4230
- ObservableArray.prototype.populateAndRender = function(iteration, callback) {
4231
- this.trigger({ action: 'populate', args: [this.$currentValue, iteration, callback] });
4169
+ Observable.batch = function(callback) {
4170
+ const $observer = Observable(0);
4171
+ const batch = function() {
4172
+ if(Validator.isAsyncFunction(callback)) {
4173
+ return (callback(...arguments)).then(() => {
4174
+ $observer.trigger();
4175
+ }).catch(error => { throw error; });
4176
+ }
4177
+ callback(...arguments);
4178
+ $observer.trigger();
4179
+ };
4180
+ batch.$observer = $observer;
4181
+ return batch;
4232
4182
  };
4233
4183
 
4184
+ const ObservableObject = function(target, configs) {
4185
+ ObservableItem.call(this, target);
4186
+ this.$observables = {};
4187
+ this.configs = configs;
4234
4188
 
4235
- /**
4236
- * Creates a filtered view of the array based on predicates.
4237
- * The filtered array updates automatically when source data or predicates change.
4238
- *
4239
- * @param {Object} predicates - Object mapping property names to filter conditions or functions
4240
- * @returns {ObservableArray} A new observable array containing filtered items
4241
- * @example
4242
- * const users = Observable.array([
4243
- * { name: 'John', age: 25 },
4244
- * { name: 'Jane', age: 30 }
4245
- * ]);
4246
- * const adults = users.where({ age: (val) => val >= 18 });
4247
- */
4248
- ObservableArray.prototype.where = function(predicates) {
4249
- const sourceArray = this;
4250
- const observableDependencies = [sourceArray];
4251
- const filterCallbacks = {};
4189
+ this.$load(target);
4252
4190
 
4253
- for (const [key, rawPredicate] of Object.entries(predicates)) {
4254
- const predicate = Validator.isObservable(rawPredicate) ? match(rawPredicate, false) : rawPredicate;
4255
- if (predicate && typeof predicate === 'object' && 'callback' in predicate) {
4256
- filterCallbacks[key] = predicate.callback;
4191
+ for(const name in target) {
4192
+ if(!Object.hasOwn(this, name)) {
4193
+ Object.defineProperty(this, name, {
4194
+ get: () => this.$observables[name],
4195
+ set: (value) => this.$observables[name].set(value)
4196
+ });
4197
+ }
4198
+ }
4257
4199
 
4258
- if (predicate.dependencies) {
4259
- const deps = Array.isArray(predicate.dependencies)
4260
- ? predicate.dependencies
4261
- : [predicate.dependencies];
4262
- observableDependencies.push.apply(observableDependencies, deps);
4200
+ };
4201
+
4202
+ ObservableObject.prototype = Object.create(ObservableItem.prototype);
4203
+
4204
+ Object.defineProperty(ObservableObject, '$value', {
4205
+ get() {
4206
+ return this.val();
4207
+ },
4208
+ set(value) {
4209
+ this.set(value);
4210
+ }
4211
+ });
4212
+
4213
+ ObservableObject.prototype.__$isObservableObject = true;
4214
+ ObservableObject.prototype.__isProxy__ = true;
4215
+
4216
+ ObservableObject.prototype.$load = function(initialValue) {
4217
+ const configs = this.configs;
4218
+ for(const key in initialValue) {
4219
+ const itemValue = initialValue[key];
4220
+ if(Array.isArray(itemValue)) {
4221
+ if(configs?.deep !== false) {
4222
+ const mappedItemValue = itemValue.map(item => {
4223
+ if(Validator.isJson(item)) {
4224
+ return Observable.json(item, configs);
4225
+ }
4226
+ if(Validator.isArray(item)) {
4227
+ return Observable.array(item, configs);
4228
+ }
4229
+ return Observable(item, configs);
4230
+ });
4231
+ this.$observables[key] = Observable.array(mappedItemValue, configs);
4232
+ continue;
4263
4233
  }
4264
- } else if(typeof predicate === 'function') {
4265
- filterCallbacks[key] = predicate;
4234
+ this.$observables[key] = Observable.array(itemValue, configs);
4235
+ continue;
4236
+ }
4237
+ if(Validator.isObservable(itemValue) || Validator.isProxy(itemValue)) {
4238
+ this.$observables[key] = itemValue;
4239
+ continue;
4240
+ }
4241
+ this.$observables[key] = (typeof itemValue === 'object') ? Observable.object(itemValue, configs) : Observable(itemValue, configs);
4242
+ }
4243
+ };
4244
+
4245
+ ObservableObject.prototype.val = function() {
4246
+ const result = {};
4247
+ for(const key in this.$observables) {
4248
+ const dataItem = this.$observables[key];
4249
+ if(Validator.isObservable(dataItem)) {
4250
+ let value = dataItem.val();
4251
+ if(Array.isArray(value)) {
4252
+ value = value.map(item => {
4253
+ if(Validator.isObservable(item)) {
4254
+ return item.val();
4255
+ }
4256
+ if(Validator.isProxy(item)) {
4257
+ return item.$value;
4258
+ }
4259
+ return item;
4260
+ });
4261
+ }
4262
+ result[key] = value;
4263
+ } else if(Validator.isProxy(dataItem)) {
4264
+ result[key] = dataItem.$value;
4266
4265
  } else {
4267
- filterCallbacks[key] = (value) => value === predicate;
4266
+ result[key] = dataItem;
4268
4267
  }
4269
4268
  }
4269
+ return result;
4270
+ };
4271
+ ObservableObject.prototype.$val = ObservableObject.prototype.val;
4270
4272
 
4271
- const viewArray = Observable.array();
4273
+ ObservableObject.prototype.get = function(property) {
4274
+ const item = this.$observables[property];
4275
+ if(Validator.isObservable(item)) {
4276
+ return item.val();
4277
+ }
4278
+ if(Validator.isProxy(item)) {
4279
+ return item.$value;
4280
+ }
4281
+ return item;
4282
+ };
4283
+ ObservableObject.prototype.$get = ObservableObject.prototype.get;
4272
4284
 
4273
- const filters = Object.entries(filterCallbacks);
4274
- const updateView = () => {
4275
- const filtered = sourceArray.val().filter(item => {
4276
- for (const [key, callback] of filters) {
4277
- if(key === '_') {
4278
- if (!callback(item)) return false;
4279
- } else {
4280
- if (!callback(item[key])) return false;
4281
- }
4285
+ ObservableObject.prototype.set = function(newData) {
4286
+ const data = Validator.isProxy(newData) ? newData.$value : newData;
4287
+ const configs = this.configs;
4288
+
4289
+ for(const key in data) {
4290
+ const targetItem = this.$observables[key];
4291
+ const newValueOrigin = newData[key];
4292
+ const newValue = data[key];
4293
+
4294
+ if(Validator.isObservable(targetItem)) {
4295
+ if(!Validator.isArray(newValue)) {
4296
+ targetItem.set(newValue);
4297
+ continue;
4282
4298
  }
4283
- return true;
4284
- });
4299
+ const firstElementFromOriginalValue = newValueOrigin.at(0);
4300
+ if(Validator.isObservable(firstElementFromOriginalValue) || Validator.isProxy(firstElementFromOriginalValue)) {
4301
+ const newValues = newValue.map(item => {
4302
+ if(Validator.isProxy(firstElementFromOriginalValue)) {
4303
+ return Observable.init(item, configs);
4304
+ }
4305
+ return Observable(item, configs);
4306
+ });
4307
+ targetItem.set(newValues);
4308
+ continue;
4309
+ }
4310
+ targetItem.set([...newValue]);
4311
+ continue;
4312
+ }
4313
+ if(Validator.isProxy(targetItem)) {
4314
+ targetItem.update(newValue);
4315
+ continue;
4316
+ }
4317
+ this[key] = newValue;
4318
+ }
4319
+ };
4320
+ ObservableObject.prototype.$set = ObservableObject.prototype.set;
4321
+ ObservableObject.prototype.$updateWith = ObservableObject.prototype.set;
4285
4322
 
4286
- viewArray.set(filtered);
4287
- };
4323
+ ObservableObject.prototype.observables = function() {
4324
+ return Object.values(this.$observables);
4325
+ };
4326
+ ObservableObject.prototype.$observables = ObservableObject.prototype.observables;
4288
4327
 
4289
- observableDependencies.forEach(dep => dep.subscribe(updateView));
4328
+ ObservableObject.prototype.keys = function() {
4329
+ return Object.keys(this.$observables);
4330
+ };
4331
+ ObservableObject.prototype.$keys = ObservableObject.prototype.keys;
4332
+ ObservableObject.prototype.clone = function() {
4333
+ return Observable.init(this.val(), this.configs);
4334
+ };
4335
+ ObservableObject.prototype.$clone = ObservableObject.prototype.clone;
4336
+ ObservableObject.prototype.reset = function() {
4337
+ for(const key in this.$observables) {
4338
+ this.$observables[key].reset();
4339
+ }
4340
+ };
4341
+ ObservableObject.prototype.originalSubscribe = ObservableObject.prototype.subscribe;
4342
+ ObservableObject.prototype.subscribe = function(callback) {
4343
+ const observables = this.observables();
4344
+ const updatedValue = nextTick(() => this.trigger());
4290
4345
 
4291
- updateView();
4346
+ this.originalSubscribe(callback);
4292
4347
 
4293
- return viewArray;
4348
+ for (let i = 0, length = observables.length; i < length; i++) {
4349
+ const observable = observables[i];
4350
+ if (observable.__$isObservableArray) {
4351
+ observable.deepSubscribe(updatedValue);
4352
+ continue
4353
+ }
4354
+ observable.subscribe(updatedValue);
4355
+ }
4356
+ };
4357
+ ObservableObject.prototype.configs = function() {
4358
+ return this.configs;
4359
+ };
4360
+
4361
+ ObservableObject.prototype.update = ObservableObject.prototype.set;
4362
+
4363
+ Observable.init = function(initialValue, configs = null) {
4364
+ return new ObservableObject(initialValue, configs)
4294
4365
  };
4295
4366
 
4296
4367
  /**
4297
- * Creates a filtered view where at least one of the specified fields matches the filter.
4298
4368
  *
4299
- * @param {Array<string>} fields - Array of field names to check
4300
- * @param {FilterResult} filter - Filter condition with callback and dependencies
4301
- * @returns {ObservableArray} A new observable array containing filtered items
4302
- * @example
4303
- * const products = Observable.array([
4304
- * { name: 'Apple', category: 'Fruit' },
4305
- * { name: 'Carrot', category: 'Vegetable' }
4306
- * ]);
4307
- * const searchTerm = Observable('App');
4308
- * const filtered = products.whereSome(['name', 'category'], match(searchTerm));
4309
- */
4310
- ObservableArray.prototype.whereSome = function(fields, filter) {
4311
- return this.where({
4312
- _: {
4313
- dependencies: filter.dependencies,
4314
- callback: (item) => fields.some(field => filter.callback(item[field]))
4315
- }
4316
- });
4369
+ * @param {any[]} data
4370
+ * @return Proxy[]
4371
+ */
4372
+ Observable.arrayOfObject = function(data) {
4373
+ return data.map(item => Observable.object(item));
4317
4374
  };
4318
4375
 
4319
4376
  /**
4320
- * Creates a filtered view where all specified fields match the filter.
4321
- *
4322
- * @param {Array<string>} fields - Array of field names to check
4323
- * @param {FilterResult} filter - Filter condition with callback and dependencies
4324
- * @returns {ObservableArray} A new observable array containing filtered items
4325
- * @example
4326
- * const items = Observable.array([
4327
- * { status: 'active', verified: true },
4328
- * { status: 'active', verified: false }
4329
- * ]);
4330
- * const activeFilter = equals('active');
4331
- * const filtered = items.whereEvery(['status', 'verified'], activeFilter);
4377
+ * Get the value of an observable or an object of observables.
4378
+ * @param {ObservableItem|Object<ObservableItem>} data
4379
+ * @returns {{}|*|null}
4332
4380
  */
4333
- ObservableArray.prototype.whereEvery = function(fields, filter) {
4334
- return this.where({
4335
- _: {
4336
- dependencies: filter.dependencies,
4337
- callback: (item) => fields.every(field => filter.callback(item[field]))
4381
+ Observable.value = function(data) {
4382
+ if(Validator.isObservable(data)) {
4383
+ return data.val();
4384
+ }
4385
+ if(Validator.isProxy(data)) {
4386
+ return data.$value;
4387
+ }
4388
+ if(Validator.isArray(data)) {
4389
+ const result = [];
4390
+ for(let i = 0, length = data.length; i < length; i++) {
4391
+ const item = data[i];
4392
+ result.push(Observable.value(item));
4338
4393
  }
4339
- });
4394
+ return result;
4395
+ }
4396
+ return data;
4340
4397
  };
4341
4398
 
4342
- ObservableArray.prototype.deepSubscribe = function(callback) {
4343
- const updatedValue = nextTick(() => callback(this.val()));
4344
- const $listeners = new WeakMap();
4399
+ Observable.object = Observable.init;
4400
+ Observable.json = Observable.init;
4345
4401
 
4346
- const bindItem = (item) => {
4347
- if ($listeners.has(item)) {
4348
- return;
4349
- }
4350
- if (item?.__$isObservableArray) {
4351
- $listeners.set(item, item.deepSubscribe(updatedValue));
4352
- return;
4353
- }
4354
- if (item?.__$isObservable) {
4355
- item.subscribe(updatedValue);
4356
- $listeners.set(item, () => item.unsubscribe(updatedValue));
4357
- }
4358
- };
4402
+ /**
4403
+ * Creates a computed observable that automatically updates when its dependencies change.
4404
+ * The callback is re-executed whenever any dependency observable changes.
4405
+ *
4406
+ * @param {Function} callback - Function that returns the computed value
4407
+ * @param {Array<ObservableItem|ObservableChecker|ObservableProxy>|Function} [dependencies=[]] - Array of observables to watch, or batch function
4408
+ * @returns {ObservableItem} A new observable that updates automatically
4409
+ * @example
4410
+ * const firstName = Observable('John');
4411
+ * const lastName = Observable('Doe');
4412
+ * const fullName = Observable.computed(
4413
+ * () => `${firstName.val()} ${lastName.val()}`,
4414
+ * [firstName, lastName]
4415
+ * );
4416
+ *
4417
+ * // With batch function
4418
+ * const batch = Observable.batch(() => { ... });
4419
+ * const computed = Observable.computed(() => { ... }, batch);
4420
+ */
4421
+ Observable.computed = function(callback, dependencies = []) {
4422
+ const initialValue = callback();
4423
+ const observable = new ObservableItem(initialValue);
4424
+ const updatedValue = nextTick(() => observable.set(callback()));
4425
+ {
4426
+ PluginsManager.emit('CreateObservableComputed', observable, dependencies);
4427
+ }
4359
4428
 
4360
- const unbindItem = (item) => {
4361
- const unsub = $listeners.get(item);
4362
- if (unsub) {
4363
- unsub();
4364
- $listeners.delete(item);
4429
+ if(Validator.isFunction(dependencies)) {
4430
+ if(!Validator.isObservable(dependencies.$observer)) {
4431
+ throw new NativeDocumentError('Observable.computed : dependencies must be valid batch function');
4365
4432
  }
4366
- };
4367
-
4368
- this.$currentValue.forEach(bindItem);
4369
- this.subscribe(updatedValue);
4433
+ dependencies.$observer.subscribe(updatedValue);
4434
+ return observable;
4435
+ }
4370
4436
 
4371
- this.subscribe((items, _, operations) => {
4372
- switch (operations?.action) {
4373
- case 'push':
4374
- case 'unshift':
4375
- operations.args.forEach(bindItem);
4376
- break;
4437
+ dependencies.forEach(dependency => {
4438
+ if(Validator.isProxy(dependency)) {
4439
+ dependency.$observables.forEach((observable) => {
4440
+ observable.subscribe(updatedValue);
4441
+ });
4442
+ return;
4443
+ }
4444
+ dependency.subscribe(updatedValue);
4445
+ });
4377
4446
 
4378
- case 'splice': {
4379
- const [start, deleteCount, ...newItems] = operations.args;
4380
- operations.result?.forEach(unbindItem);
4381
- newItems.forEach(bindItem);
4382
- break;
4383
- }
4447
+ return observable;
4448
+ };
4384
4449
 
4385
- case 'remove':
4386
- unbindItem(operations.result);
4387
- break;
4450
+ const StoreFactory = function() {
4388
4451
 
4389
- case 'merge':
4390
- operations.args.forEach(bindItem);
4391
- break;
4452
+ const $stores = new Map();
4453
+ const $followersCache = new Map();
4392
4454
 
4393
- case 'clear':
4394
- this.$currentValue.forEach(unbindItem);
4395
- break;
4455
+ /**
4456
+ * Internal helper — retrieves a store entry or throws if not found.
4457
+ */
4458
+ const $getStoreOrThrow = (method, name) => {
4459
+ const item = $stores.get(name);
4460
+ if (!item) {
4461
+ DebugManager$1.error('Store', `Store.${method}('${name}') : store not found. Did you call Store.create('${name}') first?`);
4462
+ throw new NativeDocumentError(
4463
+ `Store.${method}('${name}') : store not found.`
4464
+ );
4396
4465
  }
4397
- });
4398
-
4399
- return () => {
4400
- this.$currentValue.forEach(unbindItem);
4466
+ return item;
4401
4467
  };
4402
- };
4403
4468
 
4404
- /**
4405
- * Creates an observable array with reactive array methods.
4406
- * All mutations trigger updates automatically.
4407
- *
4408
- * @param {Array} [target=[]] - Initial array value
4409
- * @param {Object|null} [configs=null] - Configuration options
4410
- * // @param {boolean} [configs.propagation=true] - Whether to propagate changes to parent observables
4411
- * // @param {boolean} [configs.deep=false] - Whether to make nested objects observable
4412
- * @param {boolean} [configs.reset=false] - Whether to store initial value for reset()
4413
- * @returns {ObservableArray} An observable array with reactive methods
4414
- * @example
4415
- * const items = Observable.array([1, 2, 3]);
4416
- * items.push(4); // Triggers update
4417
- * items.subscribe((arr) => console.log(arr));
4418
- */
4419
- Observable.array = function(target = [], configs = null) {
4420
- return new ObservableArray(target, configs);
4421
- };
4469
+ /**
4470
+ * Internal helper blocks write operations on a read-only observer.
4471
+ */
4472
+ const $applyReadOnly = (observer, name, context) => {
4473
+ const readOnlyError = (method) => () => {
4474
+ DebugManager$1.error('Store', `Store.${context}('${name}') is read-only. '${method}()' is not allowed.`);
4475
+ throw new NativeDocumentError(
4476
+ `Store.${context}('${name}') is read-only.`
4477
+ );
4478
+ };
4479
+ observer.set = readOnlyError('set');
4480
+ observer.toggle = readOnlyError('toggle');
4481
+ observer.reset = readOnlyError('reset');
4482
+ };
4422
4483
 
4423
- /**
4424
- *
4425
- * @param {Function} callback
4426
- * @returns {Function}
4427
- */
4428
- Observable.batch = function(callback) {
4429
- const $observer = Observable(0);
4430
- const batch = function() {
4431
- if(Validator.isAsyncFunction(callback)) {
4432
- return (callback(...arguments)).then(() => {
4433
- $observer.trigger();
4434
- }).catch(error => { throw error; });
4484
+ const $createObservable = (value, options = {}) => {
4485
+ if(Array.isArray(value)) {
4486
+ return Observable.array(value, options);
4435
4487
  }
4436
- callback(...arguments);
4437
- $observer.trigger();
4488
+ if(typeof value === 'object') {
4489
+ return Observable.object(value, options);
4490
+ }
4491
+ return Observable(value, options);
4438
4492
  };
4439
- batch.$observer = $observer;
4440
- return batch;
4441
- };
4442
4493
 
4443
- const ObservableObject = function(target, configs) {
4444
- ObservableItem.call(this, target);
4445
- this.$observables = {};
4446
- this.configs = configs;
4494
+ const $api = {
4495
+ /**
4496
+ * Create a new state and return the observer.
4497
+ * Throws if a store with the same name already exists.
4498
+ *
4499
+ * @param {string} name
4500
+ * @param {*} value
4501
+ * @returns {ObservableItem}
4502
+ */
4503
+ create(name, value) {
4504
+ if ($stores.has(name)) {
4505
+ DebugManager$1.warn('Store', `Store.create('${name}') : a store with this name already exists. Use Store.get('${name}') to retrieve it.`);
4506
+ throw new NativeDocumentError(
4507
+ `Store.create('${name}') : a store with this name already exists.`
4508
+ );
4509
+ }
4510
+ const observer = $createObservable(value);
4511
+ $stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: false });
4512
+ return observer;
4513
+ },
4447
4514
 
4448
- this.$load(target);
4515
+ /**
4516
+ * Create a new resettable state and return the observer.
4517
+ * The store can be reset to its initial value via Store.reset(name).
4518
+ * Throws if a store with the same name already exists.
4519
+ *
4520
+ * @param {string} name
4521
+ * @param {*} value
4522
+ * @returns {ObservableItem}
4523
+ */
4524
+ createResettable(name, value) {
4525
+ if ($stores.has(name)) {
4526
+ DebugManager$1.warn('Store', `Store.createResettable('${name}') : a store with this name already exists.`);
4527
+ throw new NativeDocumentError(
4528
+ `Store.createResettable('${name}') : a store with this name already exists.`
4529
+ );
4530
+ }
4531
+ const observer = $createObservable(value, { reset: true });
4532
+ $stores.set(name, { observer, subscribers: new Set(), resettable: true, composed: false });
4533
+ return observer;
4534
+ },
4535
+
4536
+ /**
4537
+ * Create a computed store derived from other stores.
4538
+ * The value is automatically recalculated when any dependency changes.
4539
+ * This store is read-only — Store.use() and Store.set() will throw.
4540
+ * Throws if a store with the same name already exists.
4541
+ *
4542
+ * @param {string} name
4543
+ * @param {() => *} computation - Function that returns the computed value
4544
+ * @param {string[]} dependencies - Names of the stores to watch
4545
+ * @returns {ObservableItem}
4546
+ *
4547
+ * @example
4548
+ * Store.create('products', [{ id: 1, price: 10 }]);
4549
+ * Store.create('cart', [{ productId: 1, quantity: 2 }]);
4550
+ *
4551
+ * Store.createComposed('total', () => {
4552
+ * const products = Store.get('products').val();
4553
+ * const cart = Store.get('cart').val();
4554
+ * return cart.reduce((sum, item) => {
4555
+ * const product = products.find(p => p.id === item.productId);
4556
+ * return sum + (product.price * item.quantity);
4557
+ * }, 0);
4558
+ * }, ['products', 'cart']);
4559
+ */
4560
+ createComposed(name, computation, dependencies) {
4561
+ if ($stores.has(name)) {
4562
+ DebugManager$1.warn('Store', `Store.createComposed('${name}') : a store with this name already exists.`);
4563
+ throw new NativeDocumentError(
4564
+ `Store.createComposed('${name}') : a store with this name already exists.`
4565
+ );
4566
+ }
4567
+ if (typeof computation !== 'function') {
4568
+ throw new NativeDocumentError(
4569
+ `Store.createComposed('${name}') : computation must be a function.`
4570
+ );
4571
+ }
4572
+ if (!Array.isArray(dependencies) || dependencies.length === 0) {
4573
+ throw new NativeDocumentError(
4574
+ `Store.createComposed('${name}') : dependencies must be a non-empty array of store names.`
4575
+ );
4576
+ }
4449
4577
 
4450
- for(const name in target) {
4451
- if(!Object.hasOwn(this, name)) {
4452
- Object.defineProperty(this, name, {
4453
- get: () => this.$observables[name],
4454
- set: (value) => this.$observables[name].set(value)
4578
+ // Resolve dependency observers
4579
+ const depObservers = dependencies.map(depName => {
4580
+ if(typeof depName !== 'string') {
4581
+ return depName;
4582
+ }
4583
+ const depItem = $stores.get(depName);
4584
+ if (!depItem) {
4585
+ DebugManager$1.error('Store', `Store.createComposed('${name}') : dependency '${depName}' not found. Create it first.`);
4586
+ throw new NativeDocumentError(
4587
+ `Store.createComposed('${name}') : dependency store '${depName}' not found.`
4588
+ );
4589
+ }
4590
+ return depItem.observer;
4455
4591
  });
4456
- }
4457
- }
4458
-
4459
- };
4460
4592
 
4461
- ObservableObject.prototype = Object.create(ObservableItem.prototype);
4593
+ // Create computed observable from dependency observers
4594
+ const observer = Observable.computed(computation, depObservers);
4462
4595
 
4463
- Object.defineProperty(ObservableObject, '$value', {
4464
- get() {
4465
- return this.val();
4466
- },
4467
- set(value) {
4468
- this.set(value);
4469
- }
4470
- });
4596
+ $stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: true });
4597
+ return observer;
4598
+ },
4471
4599
 
4472
- ObservableObject.prototype.__$isObservableObject = true;
4473
- ObservableObject.prototype.__isProxy__ = true;
4600
+ /**
4601
+ * Returns true if a store with the given name exists.
4602
+ *
4603
+ * @param {string} name
4604
+ * @returns {boolean}
4605
+ */
4606
+ has(name) {
4607
+ return $stores.has(name);
4608
+ },
4474
4609
 
4475
- ObservableObject.prototype.$load = function(initialValue) {
4476
- const configs = this.configs;
4477
- for(const key in initialValue) {
4478
- const itemValue = initialValue[key];
4479
- if(Array.isArray(itemValue)) {
4480
- if(configs?.deep !== false) {
4481
- const mappedItemValue = itemValue.map(item => {
4482
- if(Validator.isJson(item)) {
4483
- return Observable.json(item, configs);
4484
- }
4485
- if(Validator.isArray(item)) {
4486
- return Observable.array(item, configs);
4487
- }
4488
- return Observable(item, configs);
4489
- });
4490
- this.$observables[key] = Observable.array(mappedItemValue, configs);
4491
- continue;
4610
+ /**
4611
+ * Resets a resettable store to its initial value and notifies all subscribers.
4612
+ * Throws if the store was not created with createResettable().
4613
+ *
4614
+ * @param {string} name
4615
+ */
4616
+ reset(name) {
4617
+ const item = $getStoreOrThrow('reset', name);
4618
+ if (item.composed) {
4619
+ DebugManager$1.error('Store', `Store.reset('${name}') : composed stores cannot be reset. Their value is derived from dependencies.`);
4620
+ throw new NativeDocumentError(
4621
+ `Store.reset('${name}') : composed stores cannot be reset.`
4622
+ );
4492
4623
  }
4493
- this.$observables[key] = Observable.array(itemValue, configs);
4494
- continue;
4495
- }
4496
- if(Validator.isObservable(itemValue) || Validator.isProxy(itemValue)) {
4497
- this.$observables[key] = itemValue;
4498
- continue;
4499
- }
4500
- this.$observables[key] = (typeof itemValue === 'object') ? Observable.object(itemValue, configs) : Observable(itemValue, configs);
4501
- }
4502
- };
4624
+ if (!item.resettable) {
4625
+ DebugManager$1.error('Store', `Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`);
4626
+ throw new NativeDocumentError(
4627
+ `Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`
4628
+ );
4629
+ }
4630
+ item.observer.reset();
4631
+ },
4503
4632
 
4504
- ObservableObject.prototype.val = function() {
4505
- const result = {};
4506
- for(const key in this.$observables) {
4507
- const dataItem = this.$observables[key];
4508
- if(Validator.isObservable(dataItem)) {
4509
- let value = dataItem.val();
4510
- if(Array.isArray(value)) {
4511
- value = value.map(item => {
4512
- if(Validator.isObservable(item)) {
4513
- return item.val();
4514
- }
4515
- if(Validator.isProxy(item)) {
4516
- return item.$value;
4517
- }
4518
- return item;
4519
- });
4633
+ /**
4634
+ * Returns a two-way synchronized follower of the store.
4635
+ * Writing to the follower propagates the value back to the store and all its subscribers.
4636
+ * Throws if called on a composed store — use Store.follow() instead.
4637
+ * Call follower.destroy() or follower.dispose() to unsubscribe.
4638
+ *
4639
+ * @param {string} name
4640
+ * @returns {ObservableItem}
4641
+ */
4642
+ use(name) {
4643
+ const item = $getStoreOrThrow('use', name);
4644
+
4645
+ if (item.composed) {
4646
+ DebugManager$1.error('Store', `Store.use('${name}') : composed stores are read-only. Use Store.follow('${name}') instead.`);
4647
+ throw new NativeDocumentError(
4648
+ `Store.use('${name}') : composed stores are read-only. Use Store.follow('${name}') instead.`
4649
+ );
4520
4650
  }
4521
- result[key] = value;
4522
- } else if(Validator.isProxy(dataItem)) {
4523
- result[key] = dataItem.$value;
4524
- } else {
4525
- result[key] = dataItem;
4526
- }
4527
- }
4528
- return result;
4529
- };
4530
- ObservableObject.prototype.$val = ObservableObject.prototype.val;
4531
4651
 
4532
- ObservableObject.prototype.get = function(property) {
4533
- const item = this.$observables[property];
4534
- if(Validator.isObservable(item)) {
4535
- return item.val();
4536
- }
4537
- if(Validator.isProxy(item)) {
4538
- return item.$value;
4539
- }
4540
- return item;
4541
- };
4542
- ObservableObject.prototype.$get = ObservableObject.prototype.get;
4652
+ const { observer: originalObserver, subscribers } = item;
4653
+ const observerFollower = $createObservable(originalObserver.val());
4543
4654
 
4544
- ObservableObject.prototype.set = function(newData) {
4545
- const data = Validator.isProxy(newData) ? newData.$value : newData;
4546
- const configs = this.configs;
4655
+ const onStoreChange = value => observerFollower.set(value);
4656
+ const onFollowerChange = value => originalObserver.set(value);
4547
4657
 
4548
- for(const key in data) {
4549
- const targetItem = this.$observables[key];
4550
- const newValueOrigin = newData[key];
4551
- const newValue = data[key];
4658
+ originalObserver.subscribe(onStoreChange);
4659
+ observerFollower.subscribe(onFollowerChange);
4552
4660
 
4553
- if(Validator.isObservable(targetItem)) {
4554
- if(!Validator.isArray(newValue)) {
4555
- targetItem.set(newValue);
4556
- continue;
4557
- }
4558
- const firstElementFromOriginalValue = newValueOrigin.at(0);
4559
- if(Validator.isObservable(firstElementFromOriginalValue) || Validator.isProxy(firstElementFromOriginalValue)) {
4560
- const newValues = newValue.map(item => {
4561
- if(Validator.isProxy(firstElementFromOriginalValue)) {
4562
- return Observable.init(item, configs);
4563
- }
4564
- return Observable(item, configs);
4565
- });
4566
- targetItem.set(newValues);
4567
- continue;
4568
- }
4569
- targetItem.set([...newValue]);
4570
- continue;
4571
- }
4572
- if(Validator.isProxy(targetItem)) {
4573
- targetItem.update(newValue);
4574
- continue;
4575
- }
4576
- this[key] = newValue;
4577
- }
4578
- };
4579
- ObservableObject.prototype.$set = ObservableObject.prototype.set;
4580
- ObservableObject.prototype.$updateWith = ObservableObject.prototype.set;
4661
+ observerFollower.destroy = () => {
4662
+ originalObserver.unsubscribe(onStoreChange);
4663
+ observerFollower.unsubscribe(onFollowerChange);
4664
+ subscribers.delete(observerFollower);
4665
+ observerFollower.cleanup();
4666
+ };
4667
+ observerFollower.dispose = observerFollower.destroy;
4581
4668
 
4582
- ObservableObject.prototype.observables = function() {
4583
- return Object.values(this.$observables);
4584
- };
4585
- ObservableObject.prototype.$observables = ObservableObject.prototype.observables;
4669
+ subscribers.add(observerFollower);
4670
+ return observerFollower;
4671
+ },
4586
4672
 
4587
- ObservableObject.prototype.keys = function() {
4588
- return Object.keys(this.$observables);
4589
- };
4590
- ObservableObject.prototype.$keys = ObservableObject.prototype.keys;
4591
- ObservableObject.prototype.clone = function() {
4592
- return Observable.init(this.val(), this.configs);
4593
- };
4594
- ObservableObject.prototype.$clone = ObservableObject.prototype.clone;
4595
- ObservableObject.prototype.reset = function() {
4596
- for(const key in this.$observables) {
4597
- this.$observables[key].reset();
4598
- }
4599
- };
4600
- ObservableObject.prototype.originalSubscribe = ObservableObject.prototype.subscribe;
4601
- ObservableObject.prototype.subscribe = function(callback) {
4602
- const observables = this.observables();
4603
- const updatedValue = nextTick(() => this.trigger());
4673
+ /**
4674
+ * Returns a read-only follower of the store.
4675
+ * The follower reflects store changes but cannot write back to the store.
4676
+ * Any attempt to call .set(), .toggle() or .reset() will throw.
4677
+ * Call follower.destroy() or follower.dispose() to unsubscribe.
4678
+ *
4679
+ * @param {string} name
4680
+ * @returns {ObservableItem}
4681
+ */
4682
+ follow(name) {
4683
+ const { observer: originalObserver, subscribers } = $getStoreOrThrow('follow', name);
4684
+ const observerFollower = $createObservable(originalObserver.val());
4604
4685
 
4605
- this.originalSubscribe(callback);
4686
+ const onStoreChange = value => observerFollower.set(value);
4687
+ originalObserver.subscribe(onStoreChange);
4606
4688
 
4607
- for (let i = 0, length = observables.length; i < length; i++) {
4608
- const observable = observables[i];
4609
- if (observable.__$isObservableArray) {
4610
- observable.deepSubscribe(updatedValue);
4611
- continue
4612
- }
4613
- observable.subscribe(updatedValue);
4614
- }
4615
- };
4616
- ObservableObject.prototype.configs = function() {
4617
- return this.configs;
4618
- };
4689
+ $applyReadOnly(observerFollower, name, 'follow');
4619
4690
 
4620
- ObservableObject.prototype.update = ObservableObject.prototype.set;
4691
+ observerFollower.destroy = () => {
4692
+ originalObserver.unsubscribe(onStoreChange);
4693
+ subscribers.delete(observerFollower);
4694
+ observerFollower.cleanup();
4695
+ };
4696
+ observerFollower.dispose = observerFollower.destroy;
4621
4697
 
4622
- Observable.init = function(initialValue, configs = null) {
4623
- return new ObservableObject(initialValue, configs)
4624
- };
4698
+ subscribers.add(observerFollower);
4699
+ return observerFollower;
4700
+ },
4625
4701
 
4626
- /**
4627
- *
4628
- * @param {any[]} data
4629
- * @return Proxy[]
4630
- */
4631
- Observable.arrayOfObject = function(data) {
4632
- return data.map(item => Observable.object(item));
4633
- };
4702
+ /**
4703
+ * Returns the raw store observer directly (no follower, no cleanup contract).
4704
+ * Use this for direct read access when you don't need to unsubscribe.
4705
+ * WARNING : mutations on this observer impact all subscribers immediately.
4706
+ *
4707
+ * @param {string} name
4708
+ * @returns {ObservableItem|null}
4709
+ */
4710
+ get(name) {
4711
+ const item = $stores.get(name);
4712
+ if (!item) {
4713
+ DebugManager$1.warn('Store', `Store.get('${name}') : store not found.`);
4714
+ return null;
4715
+ }
4716
+ return item.observer;
4717
+ },
4634
4718
 
4635
- /**
4636
- * Get the value of an observable or an object of observables.
4637
- * @param {ObservableItem|Object<ObservableItem>} data
4638
- * @returns {{}|*|null}
4639
- */
4640
- Observable.value = function(data) {
4641
- if(Validator.isObservable(data)) {
4642
- return data.val();
4643
- }
4644
- if(Validator.isProxy(data)) {
4645
- return data.$value;
4646
- }
4647
- if(Validator.isArray(data)) {
4648
- const result = [];
4649
- for(let i = 0, length = data.length; i < length; i++) {
4650
- const item = data[i];
4651
- result.push(Observable.value(item));
4652
- }
4653
- return result;
4654
- }
4655
- return data;
4656
- };
4719
+ /**
4720
+ * @param {string} name
4721
+ * @returns {{ observer: ObservableItem, subscribers: Set } | null}
4722
+ */
4723
+ getWithSubscribers(name) {
4724
+ return $stores.get(name) ?? null;
4725
+ },
4657
4726
 
4658
- Observable.object = Observable.init;
4659
- Observable.json = Observable.init;
4727
+ /**
4728
+ * Destroys a store : cleans up the observer, destroys all followers, and removes the entry.
4729
+ *
4730
+ * @param {string} name
4731
+ */
4732
+ delete(name) {
4733
+ const item = $stores.get(name);
4734
+ if (!item) {
4735
+ DebugManager$1.warn('Store', `Store.delete('${name}') : store not found, nothing to delete.`);
4736
+ return;
4737
+ }
4738
+ item.subscribers.forEach(follower => follower.destroy());
4739
+ item.subscribers.clear();
4740
+ item.observer.cleanup();
4741
+ $stores.delete(name);
4742
+ },
4743
+ /**
4744
+ * Creates an isolated store group with its own state namespace.
4745
+ * Each group is a fully independent StoreFactory instance —
4746
+ * no key conflicts, no shared state with the parent store.
4747
+ *
4748
+ * @param {string | ((group: ReturnType<typeof StoreFactory>) => void)} name - Group name for debugging, or setup callback if no name is provided
4749
+ * @param {((group: ReturnType<typeof StoreFactory>) => void)} [callback] - Setup function receiving the isolated store instance
4750
+ * @returns {ReturnType<typeof StoreFactory>}
4751
+ *
4752
+ * @example
4753
+ * // With name (recommended)
4754
+ * const EventStore = Store.group('events', (group) => {
4755
+ * group.create('catalog', []);
4756
+ * group.create('filters', { category: null, date: null });
4757
+ * group.createResettable('selected', null);
4758
+ * group.createComposed('filtered', () => {
4759
+ * const catalog = EventStore.get('catalog').val();
4760
+ * const filters = EventStore.get('filters').val();
4761
+ * return catalog.filter(event => {
4762
+ * if (filters.category && event.category !== filters.category) return false;
4763
+ * return true;
4764
+ * });
4765
+ * }, ['catalog', 'filters']);
4766
+ * });
4767
+ *
4768
+ * // Without name
4769
+ * const CartStore = Store.group((group) => {
4770
+ * group.create('items', []);
4771
+ * });
4772
+ *
4773
+ * // Usage
4774
+ * EventStore.use('catalog'); // two-way follower
4775
+ * EventStore.follow('filtered'); // read-only follower
4776
+ * EventStore.get('filters'); // raw observable
4777
+ *
4778
+ * // Cross-group composed
4779
+ * const OrderStore = Store.group('orders', (group) => {
4780
+ * group.createComposed('summary', () => {
4781
+ * const items = CartStore.get('items').val();
4782
+ * const events = EventStore.get('catalog').val();
4783
+ * return { items, events };
4784
+ * }, [CartStore.get('items'), EventStore.get('catalog')]);
4785
+ * });
4786
+ */
4787
+ group(name, callback) {
4788
+ if (typeof name === 'function') {
4789
+ callback = name;
4790
+ name = 'anonymous';
4791
+ }
4792
+ const store = StoreFactory();
4793
+ callback && callback(store);
4794
+ return store;
4795
+ },
4796
+ createPersistent(name, value, localstorage_key) {
4797
+ localstorage_key = localstorage_key || name;
4798
+ const observer = this.create(name, $getFromStorage(localstorage_key, value));
4799
+ const saver = $saveToStorage(value);
4660
4800
 
4661
- /**
4662
- * Creates a computed observable that automatically updates when its dependencies change.
4663
- * The callback is re-executed whenever any dependency observable changes.
4664
- *
4665
- * @param {Function} callback - Function that returns the computed value
4666
- * @param {Array<ObservableItem|ObservableChecker|ObservableProxy>|Function} [dependencies=[]] - Array of observables to watch, or batch function
4667
- * @returns {ObservableItem} A new observable that updates automatically
4668
- * @example
4669
- * const firstName = Observable('John');
4670
- * const lastName = Observable('Doe');
4671
- * const fullName = Observable.computed(
4672
- * () => `${firstName.val()} ${lastName.val()}`,
4673
- * [firstName, lastName]
4674
- * );
4675
- *
4676
- * // With batch function
4677
- * const batch = Observable.batch(() => { ... });
4678
- * const computed = Observable.computed(() => { ... }, batch);
4679
- */
4680
- Observable.computed = function(callback, dependencies = []) {
4681
- const initialValue = callback();
4682
- const observable = new ObservableItem(initialValue);
4683
- const updatedValue = nextTick(() => observable.set(callback()));
4684
- {
4685
- PluginsManager.emit('CreateObservableComputed', observable, dependencies);
4686
- }
4801
+ observer.subscribe((val) => saver(localstorage_key, val));
4802
+ return observer;
4803
+ },
4804
+ createPersistentResettable(name, value, localstorage_key) {
4805
+ localstorage_key = localstorage_key || name;
4806
+ const observer = this.createResettable(name, $getFromStorage(localstorage_key, value));
4807
+ const saver = $saveToStorage(value);
4808
+ observer.subscribe((val) => saver(localstorage_key, val));
4687
4809
 
4688
- if(Validator.isFunction(dependencies)) {
4689
- if(!Validator.isObservable(dependencies.$observer)) {
4690
- throw new NativeDocumentError('Observable.computed : dependencies must be valid batch function');
4810
+ const originalReset = observer.reset.bind(observer);
4811
+ observer.reset = () => {
4812
+ LocalStorage.remove(localstorage_key);
4813
+ originalReset();
4814
+ };
4815
+
4816
+ return observer;
4691
4817
  }
4692
- dependencies.$observer.subscribe(updatedValue);
4693
- return observable;
4694
- }
4818
+ };
4695
4819
 
4696
- dependencies.forEach(dependency => {
4697
- if(Validator.isProxy(dependency)) {
4698
- dependency.$observables.forEach((observable) => {
4699
- observable.subscribe(updatedValue);
4700
- });
4701
- return;
4820
+
4821
+ return new Proxy($api, {
4822
+ get(target, prop) {
4823
+ if (typeof prop === 'symbol' || prop.startsWith('$') || prop in target) {
4824
+ return target[prop];
4825
+ }
4826
+ if (target.has(prop)) {
4827
+ if ($followersCache.has(prop)) {
4828
+ return $followersCache.get(prop);
4829
+ }
4830
+ const follower = target.follow(prop);
4831
+ $followersCache.set(prop, follower);
4832
+ return follower;
4833
+ }
4834
+ return undefined;
4835
+ },
4836
+ set(target, prop, value) {
4837
+ DebugManager$1.error('Store', `Forbidden: You cannot overwrite the store key '${String(prop)}'. Use .use('${String(prop)}').set(value) instead.`);
4838
+ throw new NativeDocumentError(`Store structure is immutable. Use .set() on the observable.`);
4839
+ },
4840
+ deleteProperty(target, prop) {
4841
+ throw new NativeDocumentError(`Store keys cannot be deleted.`);
4702
4842
  }
4703
- dependency.subscribe(updatedValue);
4704
4843
  });
4705
-
4706
- return observable;
4707
4844
  };
4708
4845
 
4846
+ const Store = StoreFactory();
4847
+
4848
+ Store.create('locale', navigator.language.split('-')[0] || 'en');
4849
+
4709
4850
  /**
4710
4851
  * Renders a list of items from an observable array or object, automatically updating when data changes.
4711
4852
  * Efficiently manages DOM updates by tracking items with keys.
@@ -4781,7 +4922,7 @@ var NativeDocument = (function (exports) {
4781
4922
  }
4782
4923
  cache.set(keyId, { keyId, isNew: true, child: new WeakRef(child), indexObserver});
4783
4924
  } catch (e) {
4784
- DebugManager.error('ForEach', `Error creating element for key ${keyId}` , e);
4925
+ DebugManager$1.error('ForEach', `Error creating element for key ${keyId}` , e);
4785
4926
  throw e;
4786
4927
  }
4787
4928
  return keyId;
@@ -4972,21 +5113,23 @@ var NativeDocument = (function (exports) {
4972
5113
  };
4973
5114
 
4974
5115
 
4975
- const cleanCache = (items) => {
4976
- if(!isIndexRequired) {
4977
- cache.clear();
4978
- return;
4979
- }
4980
- if(configs.shouldKeepItemsInCache) {
4981
- return;
4982
- }
4983
- for (const [itemAsKey, _] of cache.entries()) {
4984
- if(items && items.includes(itemAsKey)) {
4985
- continue;
5116
+ let cleanCache;
5117
+
5118
+ if(!isIndexRequired) {
5119
+ cleanCache = cache.clear.bind(cache);
5120
+ }
5121
+ else if(configs.shouldKeepItemsInCache) {
5122
+ cleanCache = () => {};
5123
+ } else {
5124
+ cleanCache = (items) => {
5125
+ for (const [itemAsKey, _] of cache.entries()) {
5126
+ if(items && items.includes(itemAsKey)) {
5127
+ continue;
5128
+ }
5129
+ removeCacheItem(itemAsKey, false);
4986
5130
  }
4987
- removeCacheItem(itemAsKey, false);
4988
- }
4989
- };
5131
+ };
5132
+ }
4990
5133
 
4991
5134
  const removeByItem = (item, fragment) => {
4992
5135
  const cacheItem = cache.get(item);
@@ -5006,7 +5149,7 @@ var NativeDocument = (function (exports) {
5006
5149
  };
5007
5150
 
5008
5151
  const Actions = {
5009
- toFragment(items){
5152
+ toFragment: (items) =>{
5010
5153
  const fragment = document.createDocumentFragment();
5011
5154
  for(let i = 0, length = items.length; i < length; i++) {
5012
5155
  fragment.appendChild(buildItem(items[i], lastNumberOfItems));
@@ -5014,14 +5157,14 @@ var NativeDocument = (function (exports) {
5014
5157
  }
5015
5158
  return fragment;
5016
5159
  },
5017
- add(items) {
5160
+ add: (items) => {
5018
5161
  element.appendElement(Actions.toFragment(items));
5019
5162
  },
5020
- replace(items) {
5163
+ replace: (items) => {
5021
5164
  clear(items);
5022
5165
  Actions.add(items);
5023
5166
  },
5024
- reOrder(items) {
5167
+ reOrder: (items) => {
5025
5168
  let child = null;
5026
5169
  const fragment = document.createDocumentFragment();
5027
5170
  for(const item of items) {
@@ -5033,14 +5176,14 @@ var NativeDocument = (function (exports) {
5033
5176
  child = null;
5034
5177
  element.appendElement(fragment, blockEnd);
5035
5178
  },
5036
- removeOne(element, index) {
5179
+ removeOne: (element, index) => {
5037
5180
  removeCacheItem(element, true);
5038
5181
  },
5039
5182
  clear,
5040
- merge(items) {
5183
+ merge: (items) => {
5041
5184
  Actions.add(items);
5042
5185
  },
5043
- push(items) {
5186
+ push: (items) => {
5044
5187
  let delay = 0;
5045
5188
  if(configs.pushDelay) {
5046
5189
  delay = configs.pushDelay(items) ?? 0;
@@ -5048,7 +5191,7 @@ var NativeDocument = (function (exports) {
5048
5191
 
5049
5192
  Actions.add(items, delay);
5050
5193
  },
5051
- populate([target, iteration, callback]) {
5194
+ populate: ([target, iteration, callback]) => {
5052
5195
  const fragment = document.createDocumentFragment();
5053
5196
  for (let i = 0; i < iteration; i++) {
5054
5197
  const data = callback(i);
@@ -5059,10 +5202,10 @@ var NativeDocument = (function (exports) {
5059
5202
  element.appendChild(fragment);
5060
5203
  fragment.replaceChildren();
5061
5204
  },
5062
- unshift(values){
5205
+ unshift: (values) => {
5063
5206
  element.insertBefore(Actions.toFragment(values), blockStart.nextSibling);
5064
5207
  },
5065
- splice(args, deleted) {
5208
+ splice: (args, deleted) => {
5066
5209
  const [start, deleteCount, ...values] = args;
5067
5210
  let elementBeforeFirst = null;
5068
5211
  const garbageFragment = document.createDocumentFragment();
@@ -5089,22 +5232,22 @@ var NativeDocument = (function (exports) {
5089
5232
  }
5090
5233
 
5091
5234
  },
5092
- reverse(_, reversed) {
5235
+ reverse: (_, reversed) => {
5093
5236
  Actions.reOrder(reversed);
5094
5237
  },
5095
- sort(_, sorted) {
5238
+ sort: (_, sorted) => {
5096
5239
  Actions.reOrder(sorted);
5097
5240
  },
5098
- remove(_, deleted) {
5241
+ remove: (_, deleted)=> {
5099
5242
  Actions.removeOne(deleted);
5100
5243
  },
5101
- pop(_, deleted) {
5244
+ pop: (_, deleted) => {
5102
5245
  Actions.removeOne(deleted);
5103
5246
  },
5104
- shift(_, deleted) {
5247
+ shift: (_, deleted) => {
5105
5248
  Actions.removeOne(deleted);
5106
5249
  },
5107
- swap(args, elements) {
5250
+ swap: (args, elements) => {
5108
5251
  const parent = blockEnd.parentNode;
5109
5252
 
5110
5253
  let childA = getItemChild(elements[0]);
@@ -5171,7 +5314,7 @@ var NativeDocument = (function (exports) {
5171
5314
  */
5172
5315
  const ShowIf = function(condition, child, { comment = null, shouldKeepInCache = true} = {}) {
5173
5316
  if(!(Validator.isObservable(condition)) && !Validator.isObservableWhenResult(condition)) {
5174
- return DebugManager.warn('ShowIf', "ShowIf : condition must be an Observable / "+comment, condition);
5317
+ return DebugManager$1.warn('ShowIf', "ShowIf : condition must be an Observable / "+comment, condition);
5175
5318
  }
5176
5319
  const element = Anchor('Show if : '+(comment || ''));
5177
5320
 
@@ -6590,7 +6733,7 @@ var NativeDocument = (function (exports) {
6590
6733
  window.history.pushState({ name: route.name(), params, path}, route.name() || path , path);
6591
6734
  this.handleRouteChange(route, params, query, path);
6592
6735
  } catch (e) {
6593
- DebugManager.error('HistoryRouter', 'Error in pushState', e);
6736
+ DebugManager$1.error('HistoryRouter', 'Error in pushState', e);
6594
6737
  }
6595
6738
  };
6596
6739
  /**
@@ -6603,7 +6746,7 @@ var NativeDocument = (function (exports) {
6603
6746
  window.history.replaceState({ name: route.name(), params, path}, route.name() || path , path);
6604
6747
  this.handleRouteChange(route, params, {}, path);
6605
6748
  } catch(e) {
6606
- DebugManager.error('HistoryRouter', 'Error in replaceState', e);
6749
+ DebugManager$1.error('HistoryRouter', 'Error in replaceState', e);
6607
6750
  }
6608
6751
  };
6609
6752
  this.forward = function() {
@@ -6630,7 +6773,7 @@ var NativeDocument = (function (exports) {
6630
6773
  }
6631
6774
  this.handleRouteChange(route, params, query, path);
6632
6775
  } catch(e) {
6633
- DebugManager.error('HistoryRouter', 'Error in popstate event', e);
6776
+ DebugManager$1.error('HistoryRouter', 'Error in popstate event', e);
6634
6777
  }
6635
6778
  });
6636
6779
  const { route, params, query, path } = this.resolve(defaultPath || (window.location.pathname+window.location.search));
@@ -6855,7 +6998,7 @@ var NativeDocument = (function (exports) {
6855
6998
  listener(request);
6856
6999
  next && next(request);
6857
7000
  } catch (e) {
6858
- DebugManager.warn('Route Listener', 'Error in listener:', e);
7001
+ DebugManager$1.warn('Route Listener', 'Error in listener:', e);
6859
7002
  }
6860
7003
  }
6861
7004
  };
@@ -7033,7 +7176,7 @@ var NativeDocument = (function (exports) {
7033
7176
  */
7034
7177
  Router.create = function(options, callback) {
7035
7178
  if(!Validator.isFunction(callback)) {
7036
- DebugManager.error('Router', 'Callback must be a function');
7179
+ DebugManager$1.error('Router', 'Callback must be a function');
7037
7180
  throw new RouterError('Callback must be a function');
7038
7181
  }
7039
7182
  const router = new Router(options);