native-document 1.0.116 → 1.0.118
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/native-document.components.min.js +1141 -1107
- package/dist/native-document.dev.js +1187 -1157
- package/dist/native-document.dev.js.map +1 -1
- package/dist/native-document.min.js +1 -1
- package/package.json +1 -1
- package/src/core/data/ObservableItem.js +1 -2
- package/src/core/wrappers/AttributesWrapper.js +0 -1
- package/src/core/wrappers/prototypes/bind-class-extensions.js +1 -2
- package/src/core/wrappers/template-cloner/NodeCloner.js +60 -25
- package/src/core/wrappers/template-cloner/TemplateCloner.js +6 -10
|
@@ -1221,6 +1221,97 @@ var NativeComponents = (function (exports) {
|
|
|
1221
1221
|
return cloned;
|
|
1222
1222
|
};
|
|
1223
1223
|
|
|
1224
|
+
const $parseDateParts = (value, locale) => {
|
|
1225
|
+
const d = new Date(value);
|
|
1226
|
+
return {
|
|
1227
|
+
d,
|
|
1228
|
+
parts: new Intl.DateTimeFormat(locale, {
|
|
1229
|
+
year: 'numeric',
|
|
1230
|
+
month: 'long',
|
|
1231
|
+
day: '2-digit',
|
|
1232
|
+
hour: '2-digit',
|
|
1233
|
+
minute: '2-digit',
|
|
1234
|
+
second: '2-digit',
|
|
1235
|
+
}).formatToParts(d).reduce((acc, { type, value }) => {
|
|
1236
|
+
acc[type] = value;
|
|
1237
|
+
return acc;
|
|
1238
|
+
}, {})
|
|
1239
|
+
};
|
|
1240
|
+
};
|
|
1241
|
+
|
|
1242
|
+
const $applyDatePattern = (pattern, d, parts) => {
|
|
1243
|
+
const pad = n => String(n).padStart(2, '0');
|
|
1244
|
+
return pattern
|
|
1245
|
+
.replace('YYYY', parts.year)
|
|
1246
|
+
.replace('YY', parts.year.slice(-2))
|
|
1247
|
+
.replace('MMMM', parts.month)
|
|
1248
|
+
.replace('MMM', parts.month.slice(0, 3))
|
|
1249
|
+
.replace('MM', pad(d.getMonth() + 1))
|
|
1250
|
+
.replace('DD', pad(d.getDate()))
|
|
1251
|
+
.replace('D', d.getDate())
|
|
1252
|
+
.replace('HH', parts.hour)
|
|
1253
|
+
.replace('mm', parts.minute)
|
|
1254
|
+
.replace('ss', parts.second);
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
const Formatters = {
|
|
1258
|
+
currency: (value, locale, { currency = 'XOF', notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
|
|
1259
|
+
new Intl.NumberFormat(locale, {
|
|
1260
|
+
style: 'currency',
|
|
1261
|
+
currency,
|
|
1262
|
+
notation,
|
|
1263
|
+
minimumFractionDigits,
|
|
1264
|
+
maximumFractionDigits
|
|
1265
|
+
}).format(value),
|
|
1266
|
+
|
|
1267
|
+
number: (value, locale, { notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
|
|
1268
|
+
new Intl.NumberFormat(locale, {
|
|
1269
|
+
notation,
|
|
1270
|
+
minimumFractionDigits,
|
|
1271
|
+
maximumFractionDigits
|
|
1272
|
+
}).format(value),
|
|
1273
|
+
|
|
1274
|
+
percent: (value, locale, { decimals = 1 } = {}) =>
|
|
1275
|
+
new Intl.NumberFormat(locale, {
|
|
1276
|
+
style: 'percent',
|
|
1277
|
+
maximumFractionDigits: decimals
|
|
1278
|
+
}).format(value),
|
|
1279
|
+
|
|
1280
|
+
date: (value, locale, { format, dateStyle = 'long' } = {}) => {
|
|
1281
|
+
if (format) {
|
|
1282
|
+
const { d, parts } = $parseDateParts(value, locale);
|
|
1283
|
+
return $applyDatePattern(format, d, parts);
|
|
1284
|
+
}
|
|
1285
|
+
return new Intl.DateTimeFormat(locale, { dateStyle }).format(new Date(value));
|
|
1286
|
+
},
|
|
1287
|
+
|
|
1288
|
+
time: (value, locale, { format, hour = '2-digit', minute = '2-digit', second } = {}) => {
|
|
1289
|
+
if (format) {
|
|
1290
|
+
const { d, parts } = $parseDateParts(value, locale);
|
|
1291
|
+
return $applyDatePattern(format, d, parts);
|
|
1292
|
+
}
|
|
1293
|
+
return new Intl.DateTimeFormat(locale, { hour, minute, second }).format(new Date(value));
|
|
1294
|
+
},
|
|
1295
|
+
|
|
1296
|
+
datetime: (value, locale, { format, dateStyle = 'long', hour = '2-digit', minute = '2-digit', second } = {}) => {
|
|
1297
|
+
if (format) {
|
|
1298
|
+
const { d, parts } = $parseDateParts(value, locale);
|
|
1299
|
+
return $applyDatePattern(format, d, parts);
|
|
1300
|
+
}
|
|
1301
|
+
return new Intl.DateTimeFormat(locale, { dateStyle, hour, minute, second }).format(new Date(value));
|
|
1302
|
+
},
|
|
1303
|
+
|
|
1304
|
+
relative: (value, locale, { unit = 'day', numeric = 'auto' } = {}) => {
|
|
1305
|
+
const diff = Math.round((value - Date.now()) / (1000 * 60 * 60 * 24));
|
|
1306
|
+
return new Intl.RelativeTimeFormat(locale, { numeric }).format(diff, unit);
|
|
1307
|
+
},
|
|
1308
|
+
|
|
1309
|
+
plural: (value, locale, { singular, plural } = {}) => {
|
|
1310
|
+
const rule = new Intl.PluralRules(locale).select(value);
|
|
1311
|
+
return `${value} ${rule === 'one' ? singular : plural}`;
|
|
1312
|
+
},
|
|
1313
|
+
};
|
|
1314
|
+
|
|
1224
1315
|
const LocalStorage = {
|
|
1225
1316
|
getJson(key) {
|
|
1226
1317
|
let value = localStorage.getItem(key);
|
|
@@ -1277,696 +1368,205 @@ var NativeComponents = (function (exports) {
|
|
|
1277
1368
|
}
|
|
1278
1369
|
};
|
|
1279
1370
|
|
|
1280
|
-
|
|
1371
|
+
/**
|
|
1372
|
+
*
|
|
1373
|
+
* @param {*} value
|
|
1374
|
+
* @param {{ propagation: boolean, reset: boolean} | null} configs
|
|
1375
|
+
* @class ObservableItem
|
|
1376
|
+
*/
|
|
1377
|
+
function ObservableItem(value, configs = null) {
|
|
1378
|
+
value = Validator.isObservable(value) ? value.val() : value;
|
|
1281
1379
|
|
|
1282
|
-
|
|
1283
|
-
|
|
1380
|
+
this.$previousValue = null;
|
|
1381
|
+
this.$currentValue = value;
|
|
1284
1382
|
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
const $getStoreOrThrow = (method, name) => {
|
|
1289
|
-
const item = $stores.get(name);
|
|
1290
|
-
if (!item) {
|
|
1291
|
-
DebugManager.error('Store', `Store.${method}('${name}') : store not found. Did you call Store.create('${name}') first?`);
|
|
1292
|
-
throw new NativeDocumentError(
|
|
1293
|
-
`Store.${method}('${name}') : store not found.`
|
|
1294
|
-
);
|
|
1295
|
-
}
|
|
1296
|
-
return item;
|
|
1297
|
-
};
|
|
1383
|
+
this.$firstListener = null;
|
|
1384
|
+
this.$listeners = null;
|
|
1385
|
+
this.$watchers = null;
|
|
1298
1386
|
|
|
1299
|
-
|
|
1300
|
-
* Internal helper — blocks write operations on a read-only observer.
|
|
1301
|
-
*/
|
|
1302
|
-
const $applyReadOnly = (observer, name, context) => {
|
|
1303
|
-
const readOnlyError = (method) => () => {
|
|
1304
|
-
DebugManager.error('Store', `Store.${context}('${name}') is read-only. '${method}()' is not allowed.`);
|
|
1305
|
-
throw new NativeDocumentError(
|
|
1306
|
-
`Store.${context}('${name}') is read-only.`
|
|
1307
|
-
);
|
|
1308
|
-
};
|
|
1309
|
-
observer.set = readOnlyError('set');
|
|
1310
|
-
observer.toggle = readOnlyError('toggle');
|
|
1311
|
-
observer.reset = readOnlyError('reset');
|
|
1312
|
-
};
|
|
1387
|
+
this.$memoryId = null;
|
|
1313
1388
|
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
if(typeof value === 'object') {
|
|
1319
|
-
return Observable.object(value, options);
|
|
1389
|
+
if(configs) {
|
|
1390
|
+
this.configs = configs;
|
|
1391
|
+
if(configs.reset) {
|
|
1392
|
+
this.$initialValue = Validator.isObject(value) ? deepClone(value) : value;
|
|
1320
1393
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1323
1396
|
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
create(name, value) {
|
|
1334
|
-
if ($stores.has(name)) {
|
|
1335
|
-
DebugManager.warn('Store', `Store.create('${name}') : a store with this name already exists. Use Store.get('${name}') to retrieve it.`);
|
|
1336
|
-
throw new NativeDocumentError(
|
|
1337
|
-
`Store.create('${name}') : a store with this name already exists.`
|
|
1338
|
-
);
|
|
1339
|
-
}
|
|
1340
|
-
const observer = $createObservable(value);
|
|
1341
|
-
$stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: false });
|
|
1342
|
-
return observer;
|
|
1343
|
-
},
|
|
1397
|
+
Object.defineProperty(ObservableItem.prototype, '$value', {
|
|
1398
|
+
get() {
|
|
1399
|
+
return this.$currentValue;
|
|
1400
|
+
},
|
|
1401
|
+
set(value) {
|
|
1402
|
+
this.set(value);
|
|
1403
|
+
},
|
|
1404
|
+
configurable: true,
|
|
1405
|
+
});
|
|
1344
1406
|
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
* The store can be reset to its initial value via Store.reset(name).
|
|
1348
|
-
* Throws if a store with the same name already exists.
|
|
1349
|
-
*
|
|
1350
|
-
* @param {string} name
|
|
1351
|
-
* @param {*} value
|
|
1352
|
-
* @returns {ObservableItem}
|
|
1353
|
-
*/
|
|
1354
|
-
createResettable(name, value) {
|
|
1355
|
-
if ($stores.has(name)) {
|
|
1356
|
-
DebugManager.warn('Store', `Store.createResettable('${name}') : a store with this name already exists.`);
|
|
1357
|
-
throw new NativeDocumentError(
|
|
1358
|
-
`Store.createResettable('${name}') : a store with this name already exists.`
|
|
1359
|
-
);
|
|
1360
|
-
}
|
|
1361
|
-
const observer = $createObservable(value, { reset: true });
|
|
1362
|
-
$stores.set(name, { observer, subscribers: new Set(), resettable: true, composed: false });
|
|
1363
|
-
return observer;
|
|
1364
|
-
},
|
|
1407
|
+
ObservableItem.prototype.__$isObservable = true;
|
|
1408
|
+
const noneTrigger = function() {};
|
|
1365
1409
|
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
* Store.createComposed('total', () => {
|
|
1382
|
-
* const products = Store.get('products').val();
|
|
1383
|
-
* const cart = Store.get('cart').val();
|
|
1384
|
-
* return cart.reduce((sum, item) => {
|
|
1385
|
-
* const product = products.find(p => p.id === item.productId);
|
|
1386
|
-
* return sum + (product.price * item.quantity);
|
|
1387
|
-
* }, 0);
|
|
1388
|
-
* }, ['products', 'cart']);
|
|
1389
|
-
*/
|
|
1390
|
-
createComposed(name, computation, dependencies) {
|
|
1391
|
-
if ($stores.has(name)) {
|
|
1392
|
-
DebugManager.warn('Store', `Store.createComposed('${name}') : a store with this name already exists.`);
|
|
1393
|
-
throw new NativeDocumentError(
|
|
1394
|
-
`Store.createComposed('${name}') : a store with this name already exists.`
|
|
1395
|
-
);
|
|
1396
|
-
}
|
|
1397
|
-
if (typeof computation !== 'function') {
|
|
1398
|
-
throw new NativeDocumentError(
|
|
1399
|
-
`Store.createComposed('${name}') : computation must be a function.`
|
|
1400
|
-
);
|
|
1401
|
-
}
|
|
1402
|
-
if (!Array.isArray(dependencies) || dependencies.length === 0) {
|
|
1403
|
-
throw new NativeDocumentError(
|
|
1404
|
-
`Store.createComposed('${name}') : dependencies must be a non-empty array of store names.`
|
|
1405
|
-
);
|
|
1406
|
-
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Intercepts and transforms values before they are set on the observable.
|
|
1412
|
+
* The interceptor can modify the value or return undefined to use the original value.
|
|
1413
|
+
*
|
|
1414
|
+
* @param {(value) => any} callback - Interceptor function that receives (newValue, currentValue) and returns the transformed value or undefined
|
|
1415
|
+
* @returns {ObservableItem} The observable instance for chaining
|
|
1416
|
+
* @example
|
|
1417
|
+
* const count = Observable(0);
|
|
1418
|
+
* count.intercept((newVal, oldVal) => Math.max(0, newVal)); // Prevent negative values
|
|
1419
|
+
*/
|
|
1420
|
+
ObservableItem.prototype.intercept = function(callback) {
|
|
1421
|
+
this.$interceptor = callback;
|
|
1422
|
+
this.set = this.$setWithInterceptor;
|
|
1423
|
+
return this;
|
|
1424
|
+
};
|
|
1407
1425
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
return depName;
|
|
1412
|
-
}
|
|
1413
|
-
const depItem = $stores.get(depName);
|
|
1414
|
-
if (!depItem) {
|
|
1415
|
-
DebugManager.error('Store', `Store.createComposed('${name}') : dependency '${depName}' not found. Create it first.`);
|
|
1416
|
-
throw new NativeDocumentError(
|
|
1417
|
-
`Store.createComposed('${name}') : dependency store '${depName}' not found.`
|
|
1418
|
-
);
|
|
1419
|
-
}
|
|
1420
|
-
return depItem.observer;
|
|
1421
|
-
});
|
|
1426
|
+
ObservableItem.prototype.triggerFirstListener = function(operations) {
|
|
1427
|
+
this.$firstListener(this.$currentValue, this.$previousValue, operations);
|
|
1428
|
+
};
|
|
1422
1429
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1430
|
+
ObservableItem.prototype.triggerListeners = function(operations) {
|
|
1431
|
+
const $listeners = this.$listeners;
|
|
1432
|
+
const $previousValue = this.$previousValue;
|
|
1433
|
+
const $currentValue = this.$currentValue;
|
|
1425
1434
|
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1435
|
+
for(let i = 0, length = $listeners.length; i < length; i++) {
|
|
1436
|
+
$listeners[i]($currentValue, $previousValue, operations);
|
|
1437
|
+
}
|
|
1438
|
+
};
|
|
1429
1439
|
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
* @returns {boolean}
|
|
1435
|
-
*/
|
|
1436
|
-
has(name) {
|
|
1437
|
-
return $stores.has(name);
|
|
1438
|
-
},
|
|
1440
|
+
ObservableItem.prototype.triggerWatchers = function(operations) {
|
|
1441
|
+
const $watchers = this.$watchers;
|
|
1442
|
+
const $previousValue = this.$previousValue;
|
|
1443
|
+
const $currentValue = this.$currentValue;
|
|
1439
1444
|
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
DebugManager.error('Store', `Store.reset('${name}') : composed stores cannot be reset. Their value is derived from dependencies.`);
|
|
1450
|
-
throw new NativeDocumentError(
|
|
1451
|
-
`Store.reset('${name}') : composed stores cannot be reset.`
|
|
1452
|
-
);
|
|
1453
|
-
}
|
|
1454
|
-
if (!item.resettable) {
|
|
1455
|
-
DebugManager.error('Store', `Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`);
|
|
1456
|
-
throw new NativeDocumentError(
|
|
1457
|
-
`Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`
|
|
1458
|
-
);
|
|
1459
|
-
}
|
|
1460
|
-
item.observer.reset();
|
|
1461
|
-
},
|
|
1445
|
+
const $currentValueCallbacks = $watchers.get($currentValue);
|
|
1446
|
+
const $previousValueCallbacks = $watchers.get($previousValue);
|
|
1447
|
+
if($currentValueCallbacks) {
|
|
1448
|
+
$currentValueCallbacks(true, $previousValue, operations);
|
|
1449
|
+
}
|
|
1450
|
+
if($previousValueCallbacks) {
|
|
1451
|
+
$previousValueCallbacks(false, $currentValue, operations);
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1462
1454
|
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
* Call follower.destroy() or follower.dispose() to unsubscribe.
|
|
1468
|
-
*
|
|
1469
|
-
* @param {string} name
|
|
1470
|
-
* @returns {ObservableItem}
|
|
1471
|
-
*/
|
|
1472
|
-
use(name) {
|
|
1473
|
-
const item = $getStoreOrThrow('use', name);
|
|
1455
|
+
ObservableItem.prototype.triggerAll = function(operations) {
|
|
1456
|
+
this.triggerWatchers(operations);
|
|
1457
|
+
this.triggerListeners(operations);
|
|
1458
|
+
};
|
|
1474
1459
|
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
);
|
|
1480
|
-
}
|
|
1460
|
+
ObservableItem.prototype.triggerWatchersAndFirstListener = function(operations) {
|
|
1461
|
+
this.triggerWatchers(operations);
|
|
1462
|
+
this.triggerFirstListener(operations);
|
|
1463
|
+
};
|
|
1481
1464
|
|
|
1482
|
-
|
|
1483
|
-
|
|
1465
|
+
ObservableItem.prototype.assocTrigger = function() {
|
|
1466
|
+
this.$firstListener = null;
|
|
1467
|
+
if(this.$watchers?.size && this.$listeners?.length) {
|
|
1468
|
+
this.trigger = (this.$listeners.length === 1) ? this.triggerWatchersAndFirstListener : this.triggerAll;
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
if(this.$listeners?.length) {
|
|
1472
|
+
if(this.$listeners.length === 1) {
|
|
1473
|
+
this.$firstListener = this.$listeners[0];
|
|
1474
|
+
this.trigger = this.$firstListener.length === 0 ? this.$firstListener : this.triggerFirstListener;
|
|
1475
|
+
}
|
|
1476
|
+
else {
|
|
1477
|
+
this.trigger = this.triggerListeners;
|
|
1478
|
+
}
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
if(this.$watchers?.size) {
|
|
1482
|
+
this.trigger = this.triggerWatchers;
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
this.trigger = noneTrigger;
|
|
1486
|
+
};
|
|
1487
|
+
ObservableItem.prototype.trigger = noneTrigger;
|
|
1484
1488
|
|
|
1485
|
-
|
|
1486
|
-
|
|
1489
|
+
ObservableItem.prototype.$updateWithNewValue = function(newValue) {
|
|
1490
|
+
newValue = newValue?.__$isObservable ? newValue.val() : newValue;
|
|
1491
|
+
if(this.$currentValue === newValue) {
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
this.$previousValue = this.$currentValue;
|
|
1495
|
+
this.$currentValue = newValue;
|
|
1496
|
+
this.trigger();
|
|
1497
|
+
this.$previousValue = null;
|
|
1498
|
+
};
|
|
1487
1499
|
|
|
1488
|
-
|
|
1489
|
-
|
|
1500
|
+
/**
|
|
1501
|
+
* @param {*} data
|
|
1502
|
+
*/
|
|
1503
|
+
ObservableItem.prototype.$setWithInterceptor = function(data) {
|
|
1504
|
+
let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
|
|
1505
|
+
const result = this.$interceptor(newValue, this.$currentValue);
|
|
1490
1506
|
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
subscribers.delete(observerFollower);
|
|
1495
|
-
observerFollower.cleanup();
|
|
1496
|
-
};
|
|
1497
|
-
observerFollower.dispose = observerFollower.destroy;
|
|
1507
|
+
if (result !== undefined) {
|
|
1508
|
+
newValue = result;
|
|
1509
|
+
}
|
|
1498
1510
|
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
},
|
|
1511
|
+
this.$updateWithNewValue(newValue);
|
|
1512
|
+
};
|
|
1502
1513
|
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
* @returns {ObservableItem}
|
|
1511
|
-
*/
|
|
1512
|
-
follow(name) {
|
|
1513
|
-
const { observer: originalObserver, subscribers } = $getStoreOrThrow('follow', name);
|
|
1514
|
-
const observerFollower = $createObservable(originalObserver.val());
|
|
1514
|
+
/**
|
|
1515
|
+
* @param {*} data
|
|
1516
|
+
*/
|
|
1517
|
+
ObservableItem.prototype.$basicSet = function(data) {
|
|
1518
|
+
let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
|
|
1519
|
+
this.$updateWithNewValue(newValue);
|
|
1520
|
+
};
|
|
1515
1521
|
|
|
1516
|
-
|
|
1517
|
-
originalObserver.subscribe(onStoreChange);
|
|
1522
|
+
ObservableItem.prototype.set = ObservableItem.prototype.$basicSet;
|
|
1518
1523
|
|
|
1519
|
-
|
|
1524
|
+
ObservableItem.prototype.val = function() {
|
|
1525
|
+
return this.$currentValue;
|
|
1526
|
+
};
|
|
1520
1527
|
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1528
|
+
ObservableItem.prototype.disconnectAll = function() {
|
|
1529
|
+
this.$previousValue = null;
|
|
1530
|
+
this.$currentValue = null;
|
|
1531
|
+
this.$listeners = null;
|
|
1532
|
+
this.$watchers = null;
|
|
1533
|
+
this.trigger = noneTrigger;
|
|
1534
|
+
};
|
|
1527
1535
|
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1536
|
+
/**
|
|
1537
|
+
* Registers a cleanup callback that will be executed when the observable is cleaned up.
|
|
1538
|
+
* Useful for disposing resources, removing event listeners, or other cleanup tasks.
|
|
1539
|
+
*
|
|
1540
|
+
* @param {Function} callback - Cleanup function to execute on observable disposal
|
|
1541
|
+
* @example
|
|
1542
|
+
* const obs = Observable(0);
|
|
1543
|
+
* obs.onCleanup(() => console.log('Cleaned up!'));
|
|
1544
|
+
* obs.cleanup(); // Logs: "Cleaned up!"
|
|
1545
|
+
*/
|
|
1546
|
+
ObservableItem.prototype.onCleanup = function(callback) {
|
|
1547
|
+
this.$cleanupListeners = this.$cleanupListeners ?? [];
|
|
1548
|
+
this.$cleanupListeners.push(callback);
|
|
1549
|
+
};
|
|
1531
1550
|
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
DebugManager.warn('Store', `Store.get('${name}') : store not found.`);
|
|
1544
|
-
return null;
|
|
1545
|
-
}
|
|
1546
|
-
return item.observer;
|
|
1547
|
-
},
|
|
1551
|
+
ObservableItem.prototype.cleanup = function() {
|
|
1552
|
+
if (this.$cleanupListeners) {
|
|
1553
|
+
for (let i = 0; i < this.$cleanupListeners.length; i++) {
|
|
1554
|
+
this.$cleanupListeners[i]();
|
|
1555
|
+
}
|
|
1556
|
+
this.$cleanupListeners = null;
|
|
1557
|
+
}
|
|
1558
|
+
MemoryManager.unregister(this.$memoryId);
|
|
1559
|
+
this.disconnectAll();
|
|
1560
|
+
delete this.$value;
|
|
1561
|
+
};
|
|
1548
1562
|
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
/**
|
|
1558
|
-
* Destroys a store : cleans up the observer, destroys all followers, and removes the entry.
|
|
1559
|
-
*
|
|
1560
|
-
* @param {string} name
|
|
1561
|
-
*/
|
|
1562
|
-
delete(name) {
|
|
1563
|
-
const item = $stores.get(name);
|
|
1564
|
-
if (!item) {
|
|
1565
|
-
DebugManager.warn('Store', `Store.delete('${name}') : store not found, nothing to delete.`);
|
|
1566
|
-
return;
|
|
1567
|
-
}
|
|
1568
|
-
item.subscribers.forEach(follower => follower.destroy());
|
|
1569
|
-
item.subscribers.clear();
|
|
1570
|
-
item.observer.cleanup();
|
|
1571
|
-
$stores.delete(name);
|
|
1572
|
-
},
|
|
1573
|
-
/**
|
|
1574
|
-
* Creates an isolated store group with its own state namespace.
|
|
1575
|
-
* Each group is a fully independent StoreFactory instance —
|
|
1576
|
-
* no key conflicts, no shared state with the parent store.
|
|
1577
|
-
*
|
|
1578
|
-
* @param {string | ((group: ReturnType<typeof StoreFactory>) => void)} name - Group name for debugging, or setup callback if no name is provided
|
|
1579
|
-
* @param {((group: ReturnType<typeof StoreFactory>) => void)} [callback] - Setup function receiving the isolated store instance
|
|
1580
|
-
* @returns {ReturnType<typeof StoreFactory>}
|
|
1581
|
-
*
|
|
1582
|
-
* @example
|
|
1583
|
-
* // With name (recommended)
|
|
1584
|
-
* const EventStore = Store.group('events', (group) => {
|
|
1585
|
-
* group.create('catalog', []);
|
|
1586
|
-
* group.create('filters', { category: null, date: null });
|
|
1587
|
-
* group.createResettable('selected', null);
|
|
1588
|
-
* group.createComposed('filtered', () => {
|
|
1589
|
-
* const catalog = EventStore.get('catalog').val();
|
|
1590
|
-
* const filters = EventStore.get('filters').val();
|
|
1591
|
-
* return catalog.filter(event => {
|
|
1592
|
-
* if (filters.category && event.category !== filters.category) return false;
|
|
1593
|
-
* return true;
|
|
1594
|
-
* });
|
|
1595
|
-
* }, ['catalog', 'filters']);
|
|
1596
|
-
* });
|
|
1597
|
-
*
|
|
1598
|
-
* // Without name
|
|
1599
|
-
* const CartStore = Store.group((group) => {
|
|
1600
|
-
* group.create('items', []);
|
|
1601
|
-
* });
|
|
1602
|
-
*
|
|
1603
|
-
* // Usage
|
|
1604
|
-
* EventStore.use('catalog'); // two-way follower
|
|
1605
|
-
* EventStore.follow('filtered'); // read-only follower
|
|
1606
|
-
* EventStore.get('filters'); // raw observable
|
|
1607
|
-
*
|
|
1608
|
-
* // Cross-group composed
|
|
1609
|
-
* const OrderStore = Store.group('orders', (group) => {
|
|
1610
|
-
* group.createComposed('summary', () => {
|
|
1611
|
-
* const items = CartStore.get('items').val();
|
|
1612
|
-
* const events = EventStore.get('catalog').val();
|
|
1613
|
-
* return { items, events };
|
|
1614
|
-
* }, [CartStore.get('items'), EventStore.get('catalog')]);
|
|
1615
|
-
* });
|
|
1616
|
-
*/
|
|
1617
|
-
group(name, callback) {
|
|
1618
|
-
if (typeof name === 'function') {
|
|
1619
|
-
callback = name;
|
|
1620
|
-
name = 'anonymous';
|
|
1621
|
-
}
|
|
1622
|
-
const store = StoreFactory();
|
|
1623
|
-
callback && callback(store);
|
|
1624
|
-
return store;
|
|
1625
|
-
},
|
|
1626
|
-
createPersistent(name, value, localstorage_key) {
|
|
1627
|
-
localstorage_key = localstorage_key || name;
|
|
1628
|
-
const observer = this.create(name, $getFromStorage(localstorage_key, value));
|
|
1629
|
-
const saver = $saveToStorage(value);
|
|
1630
|
-
|
|
1631
|
-
observer.subscribe((val) => saver(localstorage_key, val));
|
|
1632
|
-
return observer;
|
|
1633
|
-
},
|
|
1634
|
-
createPersistentResettable(name, value, localstorage_key) {
|
|
1635
|
-
localstorage_key = localstorage_key || name;
|
|
1636
|
-
const observer = this.createResettable(name, $getFromStorage(localstorage_key, value));
|
|
1637
|
-
const saver = $saveToStorage(value);
|
|
1638
|
-
observer.subscribe((val) => saver(localstorage_key, val));
|
|
1639
|
-
|
|
1640
|
-
const originalReset = observer.reset.bind(observer);
|
|
1641
|
-
observer.reset = () => {
|
|
1642
|
-
LocalStorage.remove(localstorage_key);
|
|
1643
|
-
originalReset();
|
|
1644
|
-
};
|
|
1645
|
-
|
|
1646
|
-
return observer;
|
|
1647
|
-
}
|
|
1648
|
-
};
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
return new Proxy($api, {
|
|
1652
|
-
get(target, prop) {
|
|
1653
|
-
if (typeof prop === 'symbol' || prop.startsWith('$') || prop in target) {
|
|
1654
|
-
return target[prop];
|
|
1655
|
-
}
|
|
1656
|
-
if (target.has(prop)) {
|
|
1657
|
-
if ($followersCache.has(prop)) {
|
|
1658
|
-
return $followersCache.get(prop);
|
|
1659
|
-
}
|
|
1660
|
-
const follower = target.follow(prop);
|
|
1661
|
-
$followersCache.set(prop, follower);
|
|
1662
|
-
return follower;
|
|
1663
|
-
}
|
|
1664
|
-
return undefined;
|
|
1665
|
-
},
|
|
1666
|
-
set(target, prop, value) {
|
|
1667
|
-
DebugManager.error('Store', `Forbidden: You cannot overwrite the store key '${String(prop)}'. Use .use('${String(prop)}').set(value) instead.`);
|
|
1668
|
-
throw new NativeDocumentError(`Store structure is immutable. Use .set() on the observable.`);
|
|
1669
|
-
},
|
|
1670
|
-
deleteProperty(target, prop) {
|
|
1671
|
-
throw new NativeDocumentError(`Store keys cannot be deleted.`);
|
|
1672
|
-
}
|
|
1673
|
-
});
|
|
1674
|
-
};
|
|
1675
|
-
|
|
1676
|
-
const Store = StoreFactory();
|
|
1677
|
-
|
|
1678
|
-
Store.create('locale', navigator.language.split('-')[0] || 'en');
|
|
1679
|
-
|
|
1680
|
-
const $parseDateParts = (value, locale) => {
|
|
1681
|
-
const d = new Date(value);
|
|
1682
|
-
return {
|
|
1683
|
-
d,
|
|
1684
|
-
parts: new Intl.DateTimeFormat(locale, {
|
|
1685
|
-
year: 'numeric',
|
|
1686
|
-
month: 'long',
|
|
1687
|
-
day: '2-digit',
|
|
1688
|
-
hour: '2-digit',
|
|
1689
|
-
minute: '2-digit',
|
|
1690
|
-
second: '2-digit',
|
|
1691
|
-
}).formatToParts(d).reduce((acc, { type, value }) => {
|
|
1692
|
-
acc[type] = value;
|
|
1693
|
-
return acc;
|
|
1694
|
-
}, {})
|
|
1695
|
-
};
|
|
1696
|
-
};
|
|
1697
|
-
|
|
1698
|
-
const $applyDatePattern = (pattern, d, parts) => {
|
|
1699
|
-
const pad = n => String(n).padStart(2, '0');
|
|
1700
|
-
return pattern
|
|
1701
|
-
.replace('YYYY', parts.year)
|
|
1702
|
-
.replace('YY', parts.year.slice(-2))
|
|
1703
|
-
.replace('MMMM', parts.month)
|
|
1704
|
-
.replace('MMM', parts.month.slice(0, 3))
|
|
1705
|
-
.replace('MM', pad(d.getMonth() + 1))
|
|
1706
|
-
.replace('DD', pad(d.getDate()))
|
|
1707
|
-
.replace('D', d.getDate())
|
|
1708
|
-
.replace('HH', parts.hour)
|
|
1709
|
-
.replace('mm', parts.minute)
|
|
1710
|
-
.replace('ss', parts.second);
|
|
1711
|
-
};
|
|
1712
|
-
|
|
1713
|
-
const Formatters = {
|
|
1714
|
-
currency: (value, locale, { currency = 'XOF', notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
|
|
1715
|
-
new Intl.NumberFormat(locale, {
|
|
1716
|
-
style: 'currency',
|
|
1717
|
-
currency,
|
|
1718
|
-
notation,
|
|
1719
|
-
minimumFractionDigits,
|
|
1720
|
-
maximumFractionDigits
|
|
1721
|
-
}).format(value),
|
|
1722
|
-
|
|
1723
|
-
number: (value, locale, { notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
|
|
1724
|
-
new Intl.NumberFormat(locale, {
|
|
1725
|
-
notation,
|
|
1726
|
-
minimumFractionDigits,
|
|
1727
|
-
maximumFractionDigits
|
|
1728
|
-
}).format(value),
|
|
1729
|
-
|
|
1730
|
-
percent: (value, locale, { decimals = 1 } = {}) =>
|
|
1731
|
-
new Intl.NumberFormat(locale, {
|
|
1732
|
-
style: 'percent',
|
|
1733
|
-
maximumFractionDigits: decimals
|
|
1734
|
-
}).format(value),
|
|
1735
|
-
|
|
1736
|
-
date: (value, locale, { format, dateStyle = 'long' } = {}) => {
|
|
1737
|
-
if (format) {
|
|
1738
|
-
const { d, parts } = $parseDateParts(value, locale);
|
|
1739
|
-
return $applyDatePattern(format, d, parts);
|
|
1740
|
-
}
|
|
1741
|
-
return new Intl.DateTimeFormat(locale, { dateStyle }).format(new Date(value));
|
|
1742
|
-
},
|
|
1743
|
-
|
|
1744
|
-
time: (value, locale, { format, hour = '2-digit', minute = '2-digit', second } = {}) => {
|
|
1745
|
-
if (format) {
|
|
1746
|
-
const { d, parts } = $parseDateParts(value, locale);
|
|
1747
|
-
return $applyDatePattern(format, d, parts);
|
|
1748
|
-
}
|
|
1749
|
-
return new Intl.DateTimeFormat(locale, { hour, minute, second }).format(new Date(value));
|
|
1750
|
-
},
|
|
1751
|
-
|
|
1752
|
-
datetime: (value, locale, { format, dateStyle = 'long', hour = '2-digit', minute = '2-digit', second } = {}) => {
|
|
1753
|
-
if (format) {
|
|
1754
|
-
const { d, parts } = $parseDateParts(value, locale);
|
|
1755
|
-
return $applyDatePattern(format, d, parts);
|
|
1756
|
-
}
|
|
1757
|
-
return new Intl.DateTimeFormat(locale, { dateStyle, hour, minute, second }).format(new Date(value));
|
|
1758
|
-
},
|
|
1759
|
-
|
|
1760
|
-
relative: (value, locale, { unit = 'day', numeric = 'auto' } = {}) => {
|
|
1761
|
-
const diff = Math.round((value - Date.now()) / (1000 * 60 * 60 * 24));
|
|
1762
|
-
return new Intl.RelativeTimeFormat(locale, { numeric }).format(diff, unit);
|
|
1763
|
-
},
|
|
1764
|
-
|
|
1765
|
-
plural: (value, locale, { singular, plural } = {}) => {
|
|
1766
|
-
const rule = new Intl.PluralRules(locale).select(value);
|
|
1767
|
-
return `${value} ${rule === 'one' ? singular : plural}`;
|
|
1768
|
-
},
|
|
1769
|
-
};
|
|
1770
|
-
|
|
1771
|
-
/**
|
|
1772
|
-
*
|
|
1773
|
-
* @param {*} value
|
|
1774
|
-
* @param {{ propagation: boolean, reset: boolean} | null} configs
|
|
1775
|
-
* @class ObservableItem
|
|
1776
|
-
*/
|
|
1777
|
-
function ObservableItem(value, configs = null) {
|
|
1778
|
-
value = Validator.isObservable(value) ? value.val() : value;
|
|
1779
|
-
|
|
1780
|
-
this.$previousValue = null;
|
|
1781
|
-
this.$currentValue = value;
|
|
1782
|
-
|
|
1783
|
-
this.$firstListener = null;
|
|
1784
|
-
this.$listeners = null;
|
|
1785
|
-
this.$watchers = null;
|
|
1786
|
-
|
|
1787
|
-
this.$memoryId = null;
|
|
1788
|
-
|
|
1789
|
-
if(configs) {
|
|
1790
|
-
this.configs = configs;
|
|
1791
|
-
if(configs.reset) {
|
|
1792
|
-
this.$initialValue = Validator.isObject(value) ? deepClone(value) : value;
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
Object.defineProperty(ObservableItem.prototype, '$value', {
|
|
1798
|
-
get() {
|
|
1799
|
-
return this.$currentValue;
|
|
1800
|
-
},
|
|
1801
|
-
set(value) {
|
|
1802
|
-
this.set(value);
|
|
1803
|
-
},
|
|
1804
|
-
configurable: true,
|
|
1805
|
-
});
|
|
1806
|
-
|
|
1807
|
-
ObservableItem.prototype.__$isObservable = true;
|
|
1808
|
-
const noneTrigger = function() {};
|
|
1809
|
-
|
|
1810
|
-
/**
|
|
1811
|
-
* Intercepts and transforms values before they are set on the observable.
|
|
1812
|
-
* The interceptor can modify the value or return undefined to use the original value.
|
|
1813
|
-
*
|
|
1814
|
-
* @param {(value) => any} callback - Interceptor function that receives (newValue, currentValue) and returns the transformed value or undefined
|
|
1815
|
-
* @returns {ObservableItem} The observable instance for chaining
|
|
1816
|
-
* @example
|
|
1817
|
-
* const count = Observable(0);
|
|
1818
|
-
* count.intercept((newVal, oldVal) => Math.max(0, newVal)); // Prevent negative values
|
|
1819
|
-
*/
|
|
1820
|
-
ObservableItem.prototype.intercept = function(callback) {
|
|
1821
|
-
this.$interceptor = callback;
|
|
1822
|
-
this.set = this.$setWithInterceptor;
|
|
1823
|
-
return this;
|
|
1824
|
-
};
|
|
1825
|
-
|
|
1826
|
-
ObservableItem.prototype.triggerFirstListener = function(operations) {
|
|
1827
|
-
this.$firstListener(this.$currentValue, this.$previousValue, operations);
|
|
1828
|
-
};
|
|
1829
|
-
|
|
1830
|
-
ObservableItem.prototype.triggerListeners = function(operations) {
|
|
1831
|
-
const $listeners = this.$listeners;
|
|
1832
|
-
const $previousValue = this.$previousValue;
|
|
1833
|
-
const $currentValue = this.$currentValue;
|
|
1834
|
-
|
|
1835
|
-
for(let i = 0, length = $listeners.length; i < length; i++) {
|
|
1836
|
-
$listeners[i]($currentValue, $previousValue, operations);
|
|
1837
|
-
}
|
|
1838
|
-
};
|
|
1839
|
-
|
|
1840
|
-
ObservableItem.prototype.triggerWatchers = function(operations) {
|
|
1841
|
-
const $watchers = this.$watchers;
|
|
1842
|
-
const $previousValue = this.$previousValue;
|
|
1843
|
-
const $currentValue = this.$currentValue;
|
|
1844
|
-
|
|
1845
|
-
const $currentValueCallbacks = $watchers.get($currentValue);
|
|
1846
|
-
const $previousValueCallbacks = $watchers.get($previousValue);
|
|
1847
|
-
if($currentValueCallbacks) {
|
|
1848
|
-
$currentValueCallbacks(true, $previousValue, operations);
|
|
1849
|
-
}
|
|
1850
|
-
if($previousValueCallbacks) {
|
|
1851
|
-
$previousValueCallbacks(false, $currentValue, operations);
|
|
1852
|
-
}
|
|
1853
|
-
};
|
|
1854
|
-
|
|
1855
|
-
ObservableItem.prototype.triggerAll = function(operations) {
|
|
1856
|
-
this.triggerWatchers(operations);
|
|
1857
|
-
this.triggerListeners(operations);
|
|
1858
|
-
};
|
|
1859
|
-
|
|
1860
|
-
ObservableItem.prototype.triggerWatchersAndFirstListener = function(operations) {
|
|
1861
|
-
this.triggerWatchers(operations);
|
|
1862
|
-
this.triggerFirstListener(operations);
|
|
1863
|
-
};
|
|
1864
|
-
|
|
1865
|
-
ObservableItem.prototype.assocTrigger = function() {
|
|
1866
|
-
this.$firstListener = null;
|
|
1867
|
-
if(this.$watchers?.size && this.$listeners?.length) {
|
|
1868
|
-
this.trigger = (this.$listeners.length === 1) ? this.triggerWatchersAndFirstListener : this.triggerAll;
|
|
1869
|
-
return;
|
|
1870
|
-
}
|
|
1871
|
-
if(this.$listeners?.length) {
|
|
1872
|
-
if(this.$listeners.length === 1) {
|
|
1873
|
-
this.$firstListener = this.$listeners[0];
|
|
1874
|
-
this.trigger = this.triggerFirstListener;
|
|
1875
|
-
}
|
|
1876
|
-
else {
|
|
1877
|
-
this.trigger = this.triggerListeners;
|
|
1878
|
-
}
|
|
1879
|
-
return;
|
|
1880
|
-
}
|
|
1881
|
-
if(this.$watchers?.size) {
|
|
1882
|
-
this.trigger = this.triggerWatchers;
|
|
1883
|
-
return;
|
|
1884
|
-
}
|
|
1885
|
-
this.trigger = noneTrigger;
|
|
1886
|
-
};
|
|
1887
|
-
ObservableItem.prototype.trigger = noneTrigger;
|
|
1888
|
-
|
|
1889
|
-
ObservableItem.prototype.$updateWithNewValue = function(newValue) {
|
|
1890
|
-
newValue = newValue?.__$isObservable ? newValue.val() : newValue;
|
|
1891
|
-
if(this.$currentValue === newValue) {
|
|
1892
|
-
return;
|
|
1893
|
-
}
|
|
1894
|
-
this.$previousValue = this.$currentValue;
|
|
1895
|
-
this.$currentValue = newValue;
|
|
1896
|
-
this.trigger();
|
|
1897
|
-
this.$previousValue = null;
|
|
1898
|
-
};
|
|
1899
|
-
|
|
1900
|
-
/**
|
|
1901
|
-
* @param {*} data
|
|
1902
|
-
*/
|
|
1903
|
-
ObservableItem.prototype.$setWithInterceptor = function(data) {
|
|
1904
|
-
let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
|
|
1905
|
-
const result = this.$interceptor(newValue, this.$currentValue);
|
|
1906
|
-
|
|
1907
|
-
if (result !== undefined) {
|
|
1908
|
-
newValue = result;
|
|
1909
|
-
}
|
|
1910
|
-
|
|
1911
|
-
this.$updateWithNewValue(newValue);
|
|
1912
|
-
};
|
|
1913
|
-
|
|
1914
|
-
/**
|
|
1915
|
-
* @param {*} data
|
|
1916
|
-
*/
|
|
1917
|
-
ObservableItem.prototype.$basicSet = function(data) {
|
|
1918
|
-
let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
|
|
1919
|
-
this.$updateWithNewValue(newValue);
|
|
1920
|
-
};
|
|
1921
|
-
|
|
1922
|
-
ObservableItem.prototype.set = ObservableItem.prototype.$basicSet;
|
|
1923
|
-
|
|
1924
|
-
ObservableItem.prototype.val = function() {
|
|
1925
|
-
return this.$currentValue;
|
|
1926
|
-
};
|
|
1927
|
-
|
|
1928
|
-
ObservableItem.prototype.disconnectAll = function() {
|
|
1929
|
-
this.$previousValue = null;
|
|
1930
|
-
this.$currentValue = null;
|
|
1931
|
-
this.$listeners = null;
|
|
1932
|
-
this.$watchers = null;
|
|
1933
|
-
this.trigger = noneTrigger;
|
|
1934
|
-
};
|
|
1935
|
-
|
|
1936
|
-
/**
|
|
1937
|
-
* Registers a cleanup callback that will be executed when the observable is cleaned up.
|
|
1938
|
-
* Useful for disposing resources, removing event listeners, or other cleanup tasks.
|
|
1939
|
-
*
|
|
1940
|
-
* @param {Function} callback - Cleanup function to execute on observable disposal
|
|
1941
|
-
* @example
|
|
1942
|
-
* const obs = Observable(0);
|
|
1943
|
-
* obs.onCleanup(() => console.log('Cleaned up!'));
|
|
1944
|
-
* obs.cleanup(); // Logs: "Cleaned up!"
|
|
1945
|
-
*/
|
|
1946
|
-
ObservableItem.prototype.onCleanup = function(callback) {
|
|
1947
|
-
this.$cleanupListeners = this.$cleanupListeners ?? [];
|
|
1948
|
-
this.$cleanupListeners.push(callback);
|
|
1949
|
-
};
|
|
1950
|
-
|
|
1951
|
-
ObservableItem.prototype.cleanup = function() {
|
|
1952
|
-
if (this.$cleanupListeners) {
|
|
1953
|
-
for (let i = 0; i < this.$cleanupListeners.length; i++) {
|
|
1954
|
-
this.$cleanupListeners[i]();
|
|
1955
|
-
}
|
|
1956
|
-
this.$cleanupListeners = null;
|
|
1957
|
-
}
|
|
1958
|
-
MemoryManager.unregister(this.$memoryId);
|
|
1959
|
-
this.disconnectAll();
|
|
1960
|
-
delete this.$value;
|
|
1961
|
-
};
|
|
1962
|
-
|
|
1963
|
-
/**
|
|
1964
|
-
*
|
|
1965
|
-
* @param {Function} callback
|
|
1966
|
-
* @returns {(function(): void)}
|
|
1967
|
-
*/
|
|
1968
|
-
ObservableItem.prototype.subscribe = function(callback) {
|
|
1969
|
-
this.$listeners = this.$listeners ?? [];
|
|
1563
|
+
/**
|
|
1564
|
+
*
|
|
1565
|
+
* @param {Function} callback
|
|
1566
|
+
* @returns {(function(): void)}
|
|
1567
|
+
*/
|
|
1568
|
+
ObservableItem.prototype.subscribe = function(callback) {
|
|
1569
|
+
this.$listeners = this.$listeners ?? [];
|
|
1970
1570
|
|
|
1971
1571
|
this.$listeners.push(callback);
|
|
1972
1572
|
this.assocTrigger();
|
|
@@ -2384,7 +1984,6 @@ var NativeComponents = (function (exports) {
|
|
|
2384
1984
|
}
|
|
2385
1985
|
element.classes.toggle(className, value);
|
|
2386
1986
|
}
|
|
2387
|
-
data = null;
|
|
2388
1987
|
};
|
|
2389
1988
|
|
|
2390
1989
|
/**
|
|
@@ -3298,35 +2897,78 @@ var NativeComponents = (function (exports) {
|
|
|
3298
2897
|
}
|
|
3299
2898
|
const steps = [];
|
|
3300
2899
|
if(this.$ndMethods) {
|
|
3301
|
-
|
|
3302
|
-
|
|
2900
|
+
const methods = Object.keys(this.$ndMethods);
|
|
2901
|
+
if(methods.length === 1) {
|
|
2902
|
+
const methodName = methods[0];
|
|
2903
|
+
steps.push((clonedNode, data) => {
|
|
3303
2904
|
clonedNode.nd[methodName](this.$ndMethods[methodName].bind(clonedNode, ...data));
|
|
3304
|
-
}
|
|
3305
|
-
}
|
|
2905
|
+
});
|
|
2906
|
+
} else {
|
|
2907
|
+
steps.push((clonedNode, data) => {
|
|
2908
|
+
const nd = clonedNode.nd;
|
|
2909
|
+
for(const methodName in this.$ndMethods) {
|
|
2910
|
+
nd[methodName](this.$ndMethods[methodName].bind(clonedNode, ...data));
|
|
2911
|
+
}
|
|
2912
|
+
});
|
|
2913
|
+
}
|
|
3306
2914
|
}
|
|
3307
2915
|
if(this.$classes) {
|
|
3308
2916
|
const cache = {};
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
2917
|
+
const keys = Object.keys(this.$classes);
|
|
2918
|
+
|
|
2919
|
+
if(keys.length === 1) {
|
|
2920
|
+
const key = keys[0];
|
|
2921
|
+
const callback = this.$classes[key];
|
|
2922
|
+
steps.push((clonedNode, data) => {
|
|
2923
|
+
cache[key] = callback.apply(null, data);
|
|
2924
|
+
ElementCreator.processClassAttribute(clonedNode, cache);
|
|
2925
|
+
});
|
|
2926
|
+
} else {
|
|
2927
|
+
steps.push((clonedNode, data) => {
|
|
2928
|
+
ElementCreator.processClassAttribute(clonedNode, buildProperties(cache, this.$classes, data));
|
|
2929
|
+
});
|
|
2930
|
+
}
|
|
3312
2931
|
}
|
|
3313
2932
|
if(this.$styles) {
|
|
3314
2933
|
const cache = {};
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
2934
|
+
const keys = Object.keys(this.$styles);
|
|
2935
|
+
|
|
2936
|
+
if(keys.length === 1) {
|
|
2937
|
+
const key = keys[0];
|
|
2938
|
+
const callback = this.$styles[key];
|
|
2939
|
+
steps.push((clonedNode, data) => {
|
|
2940
|
+
cache[key] = callback.apply(null, data);
|
|
2941
|
+
ElementCreator.processStyleAttribute(clonedNode, cache);
|
|
2942
|
+
});
|
|
2943
|
+
} else {
|
|
2944
|
+
steps.push((clonedNode, data) => {
|
|
2945
|
+
ElementCreator.processStyleAttribute(clonedNode, buildProperties(cache, this.$styles, data));
|
|
2946
|
+
});
|
|
2947
|
+
}
|
|
3318
2948
|
}
|
|
3319
2949
|
if(this.$attrs) {
|
|
3320
2950
|
const cache = {};
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
2951
|
+
const keys = Object.keys(this.$attrs);
|
|
2952
|
+
|
|
2953
|
+
if(keys.length === 1) {
|
|
2954
|
+
const key = keys[0];
|
|
2955
|
+
const callback = this.$attrs[key];
|
|
2956
|
+
steps.push((clonedNode, data) => {
|
|
2957
|
+
cache[key] = callback.apply(null, data);
|
|
2958
|
+
ElementCreator.processAttributes(clonedNode, cache);
|
|
2959
|
+
});
|
|
2960
|
+
} else {
|
|
2961
|
+
steps.push((clonedNode, data) => {
|
|
2962
|
+
ElementCreator.processAttributes(clonedNode, buildProperties(cache, this.$attrs, data));
|
|
2963
|
+
});
|
|
2964
|
+
}
|
|
3324
2965
|
}
|
|
3325
2966
|
|
|
3326
2967
|
const stepsCount = steps.length;
|
|
2968
|
+
const $element = this.$element;
|
|
3327
2969
|
|
|
3328
|
-
this.cloneNode =
|
|
3329
|
-
const clonedNode =
|
|
2970
|
+
this.cloneNode = (data) => {
|
|
2971
|
+
const clonedNode = $element.cloneNode(false);
|
|
3330
2972
|
for(let i = 0; i < stepsCount; i++) {
|
|
3331
2973
|
steps[i](clonedNode, data);
|
|
3332
2974
|
}
|
|
@@ -3338,14 +2980,6 @@ var NativeComponents = (function (exports) {
|
|
|
3338
2980
|
return this.$element.cloneNode(false);
|
|
3339
2981
|
};
|
|
3340
2982
|
|
|
3341
|
-
NodeCloner.prototype.cloneTextNodeByProperty = function(data) {
|
|
3342
|
-
return createTextNode(data[0][this.$content]);
|
|
3343
|
-
};
|
|
3344
|
-
|
|
3345
|
-
NodeCloner.prototype.cloneTextNodeByCallback = function(data) {
|
|
3346
|
-
return createTextNode(this.$content.apply(null, data));
|
|
3347
|
-
};
|
|
3348
|
-
|
|
3349
2983
|
NodeCloner.prototype.attach = function(methodName, callback) {
|
|
3350
2984
|
this.$ndMethods = this.$ndMethods || {};
|
|
3351
2985
|
this.$ndMethods[methodName] = callback;
|
|
@@ -3355,10 +2989,10 @@ var NativeComponents = (function (exports) {
|
|
|
3355
2989
|
NodeCloner.prototype.text = function(value) {
|
|
3356
2990
|
this.$content = value;
|
|
3357
2991
|
if(typeof value === 'function') {
|
|
3358
|
-
this.cloneNode =
|
|
2992
|
+
this.cloneNode = (data) => createTextNode(value.apply(null, data));
|
|
3359
2993
|
return this;
|
|
3360
2994
|
}
|
|
3361
|
-
this.cloneNode =
|
|
2995
|
+
this.cloneNode = (data) => createTextNode(data[0][value]);
|
|
3362
2996
|
return this;
|
|
3363
2997
|
};
|
|
3364
2998
|
|
|
@@ -3648,500 +3282,900 @@ var NativeComponents = (function (exports) {
|
|
|
3648
3282
|
};
|
|
3649
3283
|
|
|
3650
3284
|
/**
|
|
3651
|
-
* Checks if the array is empty.
|
|
3285
|
+
* Checks if the array is empty.
|
|
3286
|
+
*
|
|
3287
|
+
* @returns {boolean} True if array has no elements
|
|
3288
|
+
* @example
|
|
3289
|
+
* const items = Observable.array([]);
|
|
3290
|
+
* items.isEmpty(); // true
|
|
3291
|
+
*/
|
|
3292
|
+
ObservableArray.prototype.isEmpty = function() {
|
|
3293
|
+
return this.$currentValue.length === 0;
|
|
3294
|
+
};
|
|
3295
|
+
|
|
3296
|
+
/**
|
|
3297
|
+
* Triggers a populate operation with the current array, iteration count, and callback.
|
|
3298
|
+
* Used internally for rendering optimizations.
|
|
3299
|
+
*
|
|
3300
|
+
* @param {number} iteration - Iteration count for rendering
|
|
3301
|
+
* @param {Function} callback - Callback function for rendering items
|
|
3302
|
+
*/
|
|
3303
|
+
ObservableArray.prototype.populateAndRender = function(iteration, callback) {
|
|
3304
|
+
this.trigger({ action: 'populate', args: [this.$currentValue, iteration, callback] });
|
|
3305
|
+
};
|
|
3306
|
+
|
|
3307
|
+
|
|
3308
|
+
/**
|
|
3309
|
+
* Creates a filtered view of the array based on predicates.
|
|
3310
|
+
* The filtered array updates automatically when source data or predicates change.
|
|
3311
|
+
*
|
|
3312
|
+
* @param {Object} predicates - Object mapping property names to filter conditions or functions
|
|
3313
|
+
* @returns {ObservableArray} A new observable array containing filtered items
|
|
3314
|
+
* @example
|
|
3315
|
+
* const users = Observable.array([
|
|
3316
|
+
* { name: 'John', age: 25 },
|
|
3317
|
+
* { name: 'Jane', age: 30 }
|
|
3318
|
+
* ]);
|
|
3319
|
+
* const adults = users.where({ age: (val) => val >= 18 });
|
|
3320
|
+
*/
|
|
3321
|
+
ObservableArray.prototype.where = function(predicates) {
|
|
3322
|
+
const sourceArray = this;
|
|
3323
|
+
const observableDependencies = [sourceArray];
|
|
3324
|
+
const filterCallbacks = {};
|
|
3325
|
+
|
|
3326
|
+
for (const [key, rawPredicate] of Object.entries(predicates)) {
|
|
3327
|
+
const predicate = Validator.isObservable(rawPredicate) ? match(rawPredicate, false) : rawPredicate;
|
|
3328
|
+
if (predicate && typeof predicate === 'object' && 'callback' in predicate) {
|
|
3329
|
+
filterCallbacks[key] = predicate.callback;
|
|
3330
|
+
|
|
3331
|
+
if (predicate.dependencies) {
|
|
3332
|
+
const deps = Array.isArray(predicate.dependencies)
|
|
3333
|
+
? predicate.dependencies
|
|
3334
|
+
: [predicate.dependencies];
|
|
3335
|
+
observableDependencies.push.apply(observableDependencies, deps);
|
|
3336
|
+
}
|
|
3337
|
+
} else if(typeof predicate === 'function') {
|
|
3338
|
+
filterCallbacks[key] = predicate;
|
|
3339
|
+
} else {
|
|
3340
|
+
filterCallbacks[key] = (value) => value === predicate;
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3344
|
+
const viewArray = Observable.array();
|
|
3345
|
+
|
|
3346
|
+
const filters = Object.entries(filterCallbacks);
|
|
3347
|
+
const updateView = () => {
|
|
3348
|
+
const filtered = sourceArray.val().filter(item => {
|
|
3349
|
+
for (const [key, callback] of filters) {
|
|
3350
|
+
if(key === '_') {
|
|
3351
|
+
if (!callback(item)) return false;
|
|
3352
|
+
} else {
|
|
3353
|
+
if (!callback(item[key])) return false;
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
return true;
|
|
3357
|
+
});
|
|
3358
|
+
|
|
3359
|
+
viewArray.set(filtered);
|
|
3360
|
+
};
|
|
3361
|
+
|
|
3362
|
+
observableDependencies.forEach(dep => dep.subscribe(updateView));
|
|
3363
|
+
|
|
3364
|
+
updateView();
|
|
3365
|
+
|
|
3366
|
+
return viewArray;
|
|
3367
|
+
};
|
|
3368
|
+
|
|
3369
|
+
/**
|
|
3370
|
+
* Creates a filtered view where at least one of the specified fields matches the filter.
|
|
3371
|
+
*
|
|
3372
|
+
* @param {Array<string>} fields - Array of field names to check
|
|
3373
|
+
* @param {FilterResult} filter - Filter condition with callback and dependencies
|
|
3374
|
+
* @returns {ObservableArray} A new observable array containing filtered items
|
|
3375
|
+
* @example
|
|
3376
|
+
* const products = Observable.array([
|
|
3377
|
+
* { name: 'Apple', category: 'Fruit' },
|
|
3378
|
+
* { name: 'Carrot', category: 'Vegetable' }
|
|
3379
|
+
* ]);
|
|
3380
|
+
* const searchTerm = Observable('App');
|
|
3381
|
+
* const filtered = products.whereSome(['name', 'category'], match(searchTerm));
|
|
3382
|
+
*/
|
|
3383
|
+
ObservableArray.prototype.whereSome = function(fields, filter) {
|
|
3384
|
+
return this.where({
|
|
3385
|
+
_: {
|
|
3386
|
+
dependencies: filter.dependencies,
|
|
3387
|
+
callback: (item) => fields.some(field => filter.callback(item[field]))
|
|
3388
|
+
}
|
|
3389
|
+
});
|
|
3390
|
+
};
|
|
3391
|
+
|
|
3392
|
+
/**
|
|
3393
|
+
* Creates a filtered view where all specified fields match the filter.
|
|
3394
|
+
*
|
|
3395
|
+
* @param {Array<string>} fields - Array of field names to check
|
|
3396
|
+
* @param {FilterResult} filter - Filter condition with callback and dependencies
|
|
3397
|
+
* @returns {ObservableArray} A new observable array containing filtered items
|
|
3398
|
+
* @example
|
|
3399
|
+
* const items = Observable.array([
|
|
3400
|
+
* { status: 'active', verified: true },
|
|
3401
|
+
* { status: 'active', verified: false }
|
|
3402
|
+
* ]);
|
|
3403
|
+
* const activeFilter = equals('active');
|
|
3404
|
+
* const filtered = items.whereEvery(['status', 'verified'], activeFilter);
|
|
3405
|
+
*/
|
|
3406
|
+
ObservableArray.prototype.whereEvery = function(fields, filter) {
|
|
3407
|
+
return this.where({
|
|
3408
|
+
_: {
|
|
3409
|
+
dependencies: filter.dependencies,
|
|
3410
|
+
callback: (item) => fields.every(field => filter.callback(item[field]))
|
|
3411
|
+
}
|
|
3412
|
+
});
|
|
3413
|
+
};
|
|
3414
|
+
|
|
3415
|
+
ObservableArray.prototype.deepSubscribe = function(callback) {
|
|
3416
|
+
const updatedValue = nextTick(() => callback(this.val()));
|
|
3417
|
+
const $listeners = new WeakMap();
|
|
3418
|
+
|
|
3419
|
+
const bindItem = (item) => {
|
|
3420
|
+
if ($listeners.has(item)) {
|
|
3421
|
+
return;
|
|
3422
|
+
}
|
|
3423
|
+
if (item?.__$isObservableArray) {
|
|
3424
|
+
$listeners.set(item, item.deepSubscribe(updatedValue));
|
|
3425
|
+
return;
|
|
3426
|
+
}
|
|
3427
|
+
if (item?.__$isObservable) {
|
|
3428
|
+
item.subscribe(updatedValue);
|
|
3429
|
+
$listeners.set(item, () => item.unsubscribe(updatedValue));
|
|
3430
|
+
}
|
|
3431
|
+
};
|
|
3432
|
+
|
|
3433
|
+
const unbindItem = (item) => {
|
|
3434
|
+
const unsub = $listeners.get(item);
|
|
3435
|
+
if (unsub) {
|
|
3436
|
+
unsub();
|
|
3437
|
+
$listeners.delete(item);
|
|
3438
|
+
}
|
|
3439
|
+
};
|
|
3440
|
+
|
|
3441
|
+
this.$currentValue.forEach(bindItem);
|
|
3442
|
+
this.subscribe(updatedValue);
|
|
3443
|
+
|
|
3444
|
+
this.subscribe((items, _, operations) => {
|
|
3445
|
+
switch (operations?.action) {
|
|
3446
|
+
case 'push':
|
|
3447
|
+
case 'unshift':
|
|
3448
|
+
operations.args.forEach(bindItem);
|
|
3449
|
+
break;
|
|
3450
|
+
|
|
3451
|
+
case 'splice': {
|
|
3452
|
+
const [start, deleteCount, ...newItems] = operations.args;
|
|
3453
|
+
operations.result?.forEach(unbindItem);
|
|
3454
|
+
newItems.forEach(bindItem);
|
|
3455
|
+
break;
|
|
3456
|
+
}
|
|
3457
|
+
|
|
3458
|
+
case 'remove':
|
|
3459
|
+
unbindItem(operations.result);
|
|
3460
|
+
break;
|
|
3461
|
+
|
|
3462
|
+
case 'merge':
|
|
3463
|
+
operations.args.forEach(bindItem);
|
|
3464
|
+
break;
|
|
3465
|
+
|
|
3466
|
+
case 'clear':
|
|
3467
|
+
this.$currentValue.forEach(unbindItem);
|
|
3468
|
+
break;
|
|
3469
|
+
}
|
|
3470
|
+
});
|
|
3471
|
+
|
|
3472
|
+
return () => {
|
|
3473
|
+
this.$currentValue.forEach(unbindItem);
|
|
3474
|
+
};
|
|
3475
|
+
};
|
|
3476
|
+
|
|
3477
|
+
/**
|
|
3478
|
+
* Creates an observable array with reactive array methods.
|
|
3479
|
+
* All mutations trigger updates automatically.
|
|
3480
|
+
*
|
|
3481
|
+
* @param {Array} [target=[]] - Initial array value
|
|
3482
|
+
* @param {Object|null} [configs=null] - Configuration options
|
|
3483
|
+
* // @param {boolean} [configs.propagation=true] - Whether to propagate changes to parent observables
|
|
3484
|
+
* // @param {boolean} [configs.deep=false] - Whether to make nested objects observable
|
|
3485
|
+
* @param {boolean} [configs.reset=false] - Whether to store initial value for reset()
|
|
3486
|
+
* @returns {ObservableArray} An observable array with reactive methods
|
|
3487
|
+
* @example
|
|
3488
|
+
* const items = Observable.array([1, 2, 3]);
|
|
3489
|
+
* items.push(4); // Triggers update
|
|
3490
|
+
* items.subscribe((arr) => console.log(arr));
|
|
3491
|
+
*/
|
|
3492
|
+
Observable.array = function(target = [], configs = null) {
|
|
3493
|
+
return new ObservableArray(target, configs);
|
|
3494
|
+
};
|
|
3495
|
+
|
|
3496
|
+
/**
|
|
3652
3497
|
*
|
|
3653
|
-
* @
|
|
3654
|
-
* @
|
|
3655
|
-
* const items = Observable.array([]);
|
|
3656
|
-
* items.isEmpty(); // true
|
|
3498
|
+
* @param {Function} callback
|
|
3499
|
+
* @returns {Function}
|
|
3657
3500
|
*/
|
|
3658
|
-
|
|
3659
|
-
|
|
3501
|
+
Observable.batch = function(callback) {
|
|
3502
|
+
const $observer = Observable(0);
|
|
3503
|
+
const batch = function() {
|
|
3504
|
+
if(Validator.isAsyncFunction(callback)) {
|
|
3505
|
+
return (callback(...arguments)).then(() => {
|
|
3506
|
+
$observer.trigger();
|
|
3507
|
+
}).catch(error => { throw error; });
|
|
3508
|
+
}
|
|
3509
|
+
callback(...arguments);
|
|
3510
|
+
$observer.trigger();
|
|
3511
|
+
};
|
|
3512
|
+
batch.$observer = $observer;
|
|
3513
|
+
return batch;
|
|
3660
3514
|
};
|
|
3661
3515
|
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3516
|
+
const ObservableObject = function(target, configs) {
|
|
3517
|
+
ObservableItem.call(this, target);
|
|
3518
|
+
this.$observables = {};
|
|
3519
|
+
this.configs = configs;
|
|
3520
|
+
|
|
3521
|
+
this.$load(target);
|
|
3522
|
+
|
|
3523
|
+
for(const name in target) {
|
|
3524
|
+
if(!Object.hasOwn(this, name)) {
|
|
3525
|
+
Object.defineProperty(this, name, {
|
|
3526
|
+
get: () => this.$observables[name],
|
|
3527
|
+
set: (value) => this.$observables[name].set(value)
|
|
3528
|
+
});
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3671
3532
|
};
|
|
3672
3533
|
|
|
3534
|
+
ObservableObject.prototype = Object.create(ObservableItem.prototype);
|
|
3673
3535
|
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
* { name: 'John', age: 25 },
|
|
3683
|
-
* { name: 'Jane', age: 30 }
|
|
3684
|
-
* ]);
|
|
3685
|
-
* const adults = users.where({ age: (val) => val >= 18 });
|
|
3686
|
-
*/
|
|
3687
|
-
ObservableArray.prototype.where = function(predicates) {
|
|
3688
|
-
const sourceArray = this;
|
|
3689
|
-
const observableDependencies = [sourceArray];
|
|
3690
|
-
const filterCallbacks = {};
|
|
3536
|
+
Object.defineProperty(ObservableObject, '$value', {
|
|
3537
|
+
get() {
|
|
3538
|
+
return this.val();
|
|
3539
|
+
},
|
|
3540
|
+
set(value) {
|
|
3541
|
+
this.set(value);
|
|
3542
|
+
}
|
|
3543
|
+
});
|
|
3691
3544
|
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
if (predicate && typeof predicate === 'object' && 'callback' in predicate) {
|
|
3695
|
-
filterCallbacks[key] = predicate.callback;
|
|
3545
|
+
ObservableObject.prototype.__$isObservableObject = true;
|
|
3546
|
+
ObservableObject.prototype.__isProxy__ = true;
|
|
3696
3547
|
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3548
|
+
ObservableObject.prototype.$load = function(initialValue) {
|
|
3549
|
+
const configs = this.configs;
|
|
3550
|
+
for(const key in initialValue) {
|
|
3551
|
+
const itemValue = initialValue[key];
|
|
3552
|
+
if(Array.isArray(itemValue)) {
|
|
3553
|
+
if(configs?.deep !== false) {
|
|
3554
|
+
const mappedItemValue = itemValue.map(item => {
|
|
3555
|
+
if(Validator.isJson(item)) {
|
|
3556
|
+
return Observable.json(item, configs);
|
|
3557
|
+
}
|
|
3558
|
+
if(Validator.isArray(item)) {
|
|
3559
|
+
return Observable.array(item, configs);
|
|
3560
|
+
}
|
|
3561
|
+
return Observable(item, configs);
|
|
3562
|
+
});
|
|
3563
|
+
this.$observables[key] = Observable.array(mappedItemValue, configs);
|
|
3564
|
+
continue;
|
|
3702
3565
|
}
|
|
3703
|
-
|
|
3704
|
-
|
|
3566
|
+
this.$observables[key] = Observable.array(itemValue, configs);
|
|
3567
|
+
continue;
|
|
3568
|
+
}
|
|
3569
|
+
if(Validator.isObservable(itemValue) || Validator.isProxy(itemValue)) {
|
|
3570
|
+
this.$observables[key] = itemValue;
|
|
3571
|
+
continue;
|
|
3572
|
+
}
|
|
3573
|
+
this.$observables[key] = (typeof itemValue === 'object') ? Observable.object(itemValue, configs) : Observable(itemValue, configs);
|
|
3574
|
+
}
|
|
3575
|
+
};
|
|
3576
|
+
|
|
3577
|
+
ObservableObject.prototype.val = function() {
|
|
3578
|
+
const result = {};
|
|
3579
|
+
for(const key in this.$observables) {
|
|
3580
|
+
const dataItem = this.$observables[key];
|
|
3581
|
+
if(Validator.isObservable(dataItem)) {
|
|
3582
|
+
let value = dataItem.val();
|
|
3583
|
+
if(Array.isArray(value)) {
|
|
3584
|
+
value = value.map(item => {
|
|
3585
|
+
if(Validator.isObservable(item)) {
|
|
3586
|
+
return item.val();
|
|
3587
|
+
}
|
|
3588
|
+
if(Validator.isProxy(item)) {
|
|
3589
|
+
return item.$value;
|
|
3590
|
+
}
|
|
3591
|
+
return item;
|
|
3592
|
+
});
|
|
3593
|
+
}
|
|
3594
|
+
result[key] = value;
|
|
3595
|
+
} else if(Validator.isProxy(dataItem)) {
|
|
3596
|
+
result[key] = dataItem.$value;
|
|
3705
3597
|
} else {
|
|
3706
|
-
|
|
3598
|
+
result[key] = dataItem;
|
|
3707
3599
|
}
|
|
3708
3600
|
}
|
|
3601
|
+
return result;
|
|
3602
|
+
};
|
|
3603
|
+
ObservableObject.prototype.$val = ObservableObject.prototype.val;
|
|
3709
3604
|
|
|
3710
|
-
|
|
3605
|
+
ObservableObject.prototype.get = function(property) {
|
|
3606
|
+
const item = this.$observables[property];
|
|
3607
|
+
if(Validator.isObservable(item)) {
|
|
3608
|
+
return item.val();
|
|
3609
|
+
}
|
|
3610
|
+
if(Validator.isProxy(item)) {
|
|
3611
|
+
return item.$value;
|
|
3612
|
+
}
|
|
3613
|
+
return item;
|
|
3614
|
+
};
|
|
3615
|
+
ObservableObject.prototype.$get = ObservableObject.prototype.get;
|
|
3711
3616
|
|
|
3712
|
-
|
|
3713
|
-
const
|
|
3714
|
-
|
|
3715
|
-
for (const [key, callback] of filters) {
|
|
3716
|
-
if(key === '_') {
|
|
3717
|
-
if (!callback(item)) return false;
|
|
3718
|
-
} else {
|
|
3719
|
-
if (!callback(item[key])) return false;
|
|
3720
|
-
}
|
|
3721
|
-
}
|
|
3722
|
-
return true;
|
|
3723
|
-
});
|
|
3617
|
+
ObservableObject.prototype.set = function(newData) {
|
|
3618
|
+
const data = Validator.isProxy(newData) ? newData.$value : newData;
|
|
3619
|
+
const configs = this.configs;
|
|
3724
3620
|
|
|
3725
|
-
|
|
3726
|
-
|
|
3621
|
+
for(const key in data) {
|
|
3622
|
+
const targetItem = this.$observables[key];
|
|
3623
|
+
const newValueOrigin = newData[key];
|
|
3624
|
+
const newValue = data[key];
|
|
3727
3625
|
|
|
3728
|
-
|
|
3626
|
+
if(Validator.isObservable(targetItem)) {
|
|
3627
|
+
if(!Validator.isArray(newValue)) {
|
|
3628
|
+
targetItem.set(newValue);
|
|
3629
|
+
continue;
|
|
3630
|
+
}
|
|
3631
|
+
const firstElementFromOriginalValue = newValueOrigin.at(0);
|
|
3632
|
+
if(Validator.isObservable(firstElementFromOriginalValue) || Validator.isProxy(firstElementFromOriginalValue)) {
|
|
3633
|
+
const newValues = newValue.map(item => {
|
|
3634
|
+
if(Validator.isProxy(firstElementFromOriginalValue)) {
|
|
3635
|
+
return Observable.init(item, configs);
|
|
3636
|
+
}
|
|
3637
|
+
return Observable(item, configs);
|
|
3638
|
+
});
|
|
3639
|
+
targetItem.set(newValues);
|
|
3640
|
+
continue;
|
|
3641
|
+
}
|
|
3642
|
+
targetItem.set([...newValue]);
|
|
3643
|
+
continue;
|
|
3644
|
+
}
|
|
3645
|
+
if(Validator.isProxy(targetItem)) {
|
|
3646
|
+
targetItem.update(newValue);
|
|
3647
|
+
continue;
|
|
3648
|
+
}
|
|
3649
|
+
this[key] = newValue;
|
|
3650
|
+
}
|
|
3651
|
+
};
|
|
3652
|
+
ObservableObject.prototype.$set = ObservableObject.prototype.set;
|
|
3653
|
+
ObservableObject.prototype.$updateWith = ObservableObject.prototype.set;
|
|
3729
3654
|
|
|
3730
|
-
|
|
3655
|
+
ObservableObject.prototype.observables = function() {
|
|
3656
|
+
return Object.values(this.$observables);
|
|
3657
|
+
};
|
|
3658
|
+
ObservableObject.prototype.$observables = ObservableObject.prototype.observables;
|
|
3731
3659
|
|
|
3732
|
-
|
|
3660
|
+
ObservableObject.prototype.keys = function() {
|
|
3661
|
+
return Object.keys(this.$observables);
|
|
3662
|
+
};
|
|
3663
|
+
ObservableObject.prototype.$keys = ObservableObject.prototype.keys;
|
|
3664
|
+
ObservableObject.prototype.clone = function() {
|
|
3665
|
+
return Observable.init(this.val(), this.configs);
|
|
3666
|
+
};
|
|
3667
|
+
ObservableObject.prototype.$clone = ObservableObject.prototype.clone;
|
|
3668
|
+
ObservableObject.prototype.reset = function() {
|
|
3669
|
+
for(const key in this.$observables) {
|
|
3670
|
+
this.$observables[key].reset();
|
|
3671
|
+
}
|
|
3733
3672
|
};
|
|
3673
|
+
ObservableObject.prototype.originalSubscribe = ObservableObject.prototype.subscribe;
|
|
3674
|
+
ObservableObject.prototype.subscribe = function(callback) {
|
|
3675
|
+
const observables = this.observables();
|
|
3676
|
+
const updatedValue = nextTick(() => this.trigger());
|
|
3734
3677
|
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
* const products = Observable.array([
|
|
3743
|
-
* { name: 'Apple', category: 'Fruit' },
|
|
3744
|
-
* { name: 'Carrot', category: 'Vegetable' }
|
|
3745
|
-
* ]);
|
|
3746
|
-
* const searchTerm = Observable('App');
|
|
3747
|
-
* const filtered = products.whereSome(['name', 'category'], match(searchTerm));
|
|
3748
|
-
*/
|
|
3749
|
-
ObservableArray.prototype.whereSome = function(fields, filter) {
|
|
3750
|
-
return this.where({
|
|
3751
|
-
_: {
|
|
3752
|
-
dependencies: filter.dependencies,
|
|
3753
|
-
callback: (item) => fields.some(field => filter.callback(item[field]))
|
|
3678
|
+
this.originalSubscribe(callback);
|
|
3679
|
+
|
|
3680
|
+
for (let i = 0, length = observables.length; i < length; i++) {
|
|
3681
|
+
const observable = observables[i];
|
|
3682
|
+
if (observable.__$isObservableArray) {
|
|
3683
|
+
observable.deepSubscribe(updatedValue);
|
|
3684
|
+
continue
|
|
3754
3685
|
}
|
|
3755
|
-
|
|
3686
|
+
observable.subscribe(updatedValue);
|
|
3687
|
+
}
|
|
3688
|
+
};
|
|
3689
|
+
ObservableObject.prototype.configs = function() {
|
|
3690
|
+
return this.configs;
|
|
3691
|
+
};
|
|
3692
|
+
|
|
3693
|
+
ObservableObject.prototype.update = ObservableObject.prototype.set;
|
|
3694
|
+
|
|
3695
|
+
Observable.init = function(initialValue, configs = null) {
|
|
3696
|
+
return new ObservableObject(initialValue, configs)
|
|
3756
3697
|
};
|
|
3757
3698
|
|
|
3758
3699
|
/**
|
|
3759
|
-
* Creates a filtered view where all specified fields match the filter.
|
|
3760
3700
|
*
|
|
3761
|
-
* @param {
|
|
3762
|
-
* @
|
|
3763
|
-
* @returns {ObservableArray} A new observable array containing filtered items
|
|
3764
|
-
* @example
|
|
3765
|
-
* const items = Observable.array([
|
|
3766
|
-
* { status: 'active', verified: true },
|
|
3767
|
-
* { status: 'active', verified: false }
|
|
3768
|
-
* ]);
|
|
3769
|
-
* const activeFilter = equals('active');
|
|
3770
|
-
* const filtered = items.whereEvery(['status', 'verified'], activeFilter);
|
|
3701
|
+
* @param {any[]} data
|
|
3702
|
+
* @return Proxy[]
|
|
3771
3703
|
*/
|
|
3772
|
-
|
|
3773
|
-
return
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3704
|
+
Observable.arrayOfObject = function(data) {
|
|
3705
|
+
return data.map(item => Observable.object(item));
|
|
3706
|
+
};
|
|
3707
|
+
|
|
3708
|
+
/**
|
|
3709
|
+
* Get the value of an observable or an object of observables.
|
|
3710
|
+
* @param {ObservableItem|Object<ObservableItem>} data
|
|
3711
|
+
* @returns {{}|*|null}
|
|
3712
|
+
*/
|
|
3713
|
+
Observable.value = function(data) {
|
|
3714
|
+
if(Validator.isObservable(data)) {
|
|
3715
|
+
return data.val();
|
|
3716
|
+
}
|
|
3717
|
+
if(Validator.isProxy(data)) {
|
|
3718
|
+
return data.$value;
|
|
3719
|
+
}
|
|
3720
|
+
if(Validator.isArray(data)) {
|
|
3721
|
+
const result = [];
|
|
3722
|
+
for(let i = 0, length = data.length; i < length; i++) {
|
|
3723
|
+
const item = data[i];
|
|
3724
|
+
result.push(Observable.value(item));
|
|
3777
3725
|
}
|
|
3778
|
-
|
|
3726
|
+
return result;
|
|
3727
|
+
}
|
|
3728
|
+
return data;
|
|
3779
3729
|
};
|
|
3780
3730
|
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
const $listeners = new WeakMap();
|
|
3731
|
+
Observable.object = Observable.init;
|
|
3732
|
+
Observable.json = Observable.init;
|
|
3784
3733
|
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3734
|
+
/**
|
|
3735
|
+
* Creates a computed observable that automatically updates when its dependencies change.
|
|
3736
|
+
* The callback is re-executed whenever any dependency observable changes.
|
|
3737
|
+
*
|
|
3738
|
+
* @param {Function} callback - Function that returns the computed value
|
|
3739
|
+
* @param {Array<ObservableItem|ObservableChecker|ObservableProxy>|Function} [dependencies=[]] - Array of observables to watch, or batch function
|
|
3740
|
+
* @returns {ObservableItem} A new observable that updates automatically
|
|
3741
|
+
* @example
|
|
3742
|
+
* const firstName = Observable('John');
|
|
3743
|
+
* const lastName = Observable('Doe');
|
|
3744
|
+
* const fullName = Observable.computed(
|
|
3745
|
+
* () => `${firstName.val()} ${lastName.val()}`,
|
|
3746
|
+
* [firstName, lastName]
|
|
3747
|
+
* );
|
|
3748
|
+
*
|
|
3749
|
+
* // With batch function
|
|
3750
|
+
* const batch = Observable.batch(() => { ... });
|
|
3751
|
+
* const computed = Observable.computed(() => { ... }, batch);
|
|
3752
|
+
*/
|
|
3753
|
+
Observable.computed = function(callback, dependencies = []) {
|
|
3754
|
+
const initialValue = callback();
|
|
3755
|
+
const observable = new ObservableItem(initialValue);
|
|
3756
|
+
const updatedValue = nextTick(() => observable.set(callback()));
|
|
3798
3757
|
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
unsub();
|
|
3803
|
-
$listeners.delete(item);
|
|
3758
|
+
if(Validator.isFunction(dependencies)) {
|
|
3759
|
+
if(!Validator.isObservable(dependencies.$observer)) {
|
|
3760
|
+
throw new NativeDocumentError('Observable.computed : dependencies must be valid batch function');
|
|
3804
3761
|
}
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
this.subscribe(updatedValue);
|
|
3762
|
+
dependencies.$observer.subscribe(updatedValue);
|
|
3763
|
+
return observable;
|
|
3764
|
+
}
|
|
3809
3765
|
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3766
|
+
dependencies.forEach(dependency => {
|
|
3767
|
+
if(Validator.isProxy(dependency)) {
|
|
3768
|
+
dependency.$observables.forEach((observable) => {
|
|
3769
|
+
observable.subscribe(updatedValue);
|
|
3770
|
+
});
|
|
3771
|
+
return;
|
|
3772
|
+
}
|
|
3773
|
+
dependency.subscribe(updatedValue);
|
|
3774
|
+
});
|
|
3816
3775
|
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
operations.result?.forEach(unbindItem);
|
|
3820
|
-
newItems.forEach(bindItem);
|
|
3821
|
-
break;
|
|
3822
|
-
}
|
|
3776
|
+
return observable;
|
|
3777
|
+
};
|
|
3823
3778
|
|
|
3824
|
-
|
|
3825
|
-
unbindItem(operations.result);
|
|
3826
|
-
break;
|
|
3779
|
+
const StoreFactory = function() {
|
|
3827
3780
|
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
break;
|
|
3781
|
+
const $stores = new Map();
|
|
3782
|
+
const $followersCache = new Map();
|
|
3831
3783
|
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3784
|
+
/**
|
|
3785
|
+
* Internal helper — retrieves a store entry or throws if not found.
|
|
3786
|
+
*/
|
|
3787
|
+
const $getStoreOrThrow = (method, name) => {
|
|
3788
|
+
const item = $stores.get(name);
|
|
3789
|
+
if (!item) {
|
|
3790
|
+
DebugManager.error('Store', `Store.${method}('${name}') : store not found. Did you call Store.create('${name}') first?`);
|
|
3791
|
+
throw new NativeDocumentError(
|
|
3792
|
+
`Store.${method}('${name}') : store not found.`
|
|
3793
|
+
);
|
|
3835
3794
|
}
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
return () => {
|
|
3839
|
-
this.$currentValue.forEach(unbindItem);
|
|
3795
|
+
return item;
|
|
3840
3796
|
};
|
|
3841
|
-
};
|
|
3842
3797
|
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
*/
|
|
3858
|
-
Observable.array = function(target = [], configs = null) {
|
|
3859
|
-
return new ObservableArray(target, configs);
|
|
3860
|
-
};
|
|
3798
|
+
/**
|
|
3799
|
+
* Internal helper — blocks write operations on a read-only observer.
|
|
3800
|
+
*/
|
|
3801
|
+
const $applyReadOnly = (observer, name, context) => {
|
|
3802
|
+
const readOnlyError = (method) => () => {
|
|
3803
|
+
DebugManager.error('Store', `Store.${context}('${name}') is read-only. '${method}()' is not allowed.`);
|
|
3804
|
+
throw new NativeDocumentError(
|
|
3805
|
+
`Store.${context}('${name}') is read-only.`
|
|
3806
|
+
);
|
|
3807
|
+
};
|
|
3808
|
+
observer.set = readOnlyError('set');
|
|
3809
|
+
observer.toggle = readOnlyError('toggle');
|
|
3810
|
+
observer.reset = readOnlyError('reset');
|
|
3811
|
+
};
|
|
3861
3812
|
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
* @returns {Function}
|
|
3866
|
-
*/
|
|
3867
|
-
Observable.batch = function(callback) {
|
|
3868
|
-
const $observer = Observable(0);
|
|
3869
|
-
const batch = function() {
|
|
3870
|
-
if(Validator.isAsyncFunction(callback)) {
|
|
3871
|
-
return (callback(...arguments)).then(() => {
|
|
3872
|
-
$observer.trigger();
|
|
3873
|
-
}).catch(error => { throw error; });
|
|
3813
|
+
const $createObservable = (value, options = {}) => {
|
|
3814
|
+
if(Array.isArray(value)) {
|
|
3815
|
+
return Observable.array(value, options);
|
|
3874
3816
|
}
|
|
3875
|
-
|
|
3876
|
-
|
|
3817
|
+
if(typeof value === 'object') {
|
|
3818
|
+
return Observable.object(value, options);
|
|
3819
|
+
}
|
|
3820
|
+
return Observable(value, options);
|
|
3877
3821
|
};
|
|
3878
|
-
batch.$observer = $observer;
|
|
3879
|
-
return batch;
|
|
3880
|
-
};
|
|
3881
3822
|
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3823
|
+
const $api = {
|
|
3824
|
+
/**
|
|
3825
|
+
* Create a new state and return the observer.
|
|
3826
|
+
* Throws if a store with the same name already exists.
|
|
3827
|
+
*
|
|
3828
|
+
* @param {string} name
|
|
3829
|
+
* @param {*} value
|
|
3830
|
+
* @returns {ObservableItem}
|
|
3831
|
+
*/
|
|
3832
|
+
create(name, value) {
|
|
3833
|
+
if ($stores.has(name)) {
|
|
3834
|
+
DebugManager.warn('Store', `Store.create('${name}') : a store with this name already exists. Use Store.get('${name}') to retrieve it.`);
|
|
3835
|
+
throw new NativeDocumentError(
|
|
3836
|
+
`Store.create('${name}') : a store with this name already exists.`
|
|
3837
|
+
);
|
|
3838
|
+
}
|
|
3839
|
+
const observer = $createObservable(value);
|
|
3840
|
+
$stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: false });
|
|
3841
|
+
return observer;
|
|
3842
|
+
},
|
|
3843
|
+
|
|
3844
|
+
/**
|
|
3845
|
+
* Create a new resettable state and return the observer.
|
|
3846
|
+
* The store can be reset to its initial value via Store.reset(name).
|
|
3847
|
+
* Throws if a store with the same name already exists.
|
|
3848
|
+
*
|
|
3849
|
+
* @param {string} name
|
|
3850
|
+
* @param {*} value
|
|
3851
|
+
* @returns {ObservableItem}
|
|
3852
|
+
*/
|
|
3853
|
+
createResettable(name, value) {
|
|
3854
|
+
if ($stores.has(name)) {
|
|
3855
|
+
DebugManager.warn('Store', `Store.createResettable('${name}') : a store with this name already exists.`);
|
|
3856
|
+
throw new NativeDocumentError(
|
|
3857
|
+
`Store.createResettable('${name}') : a store with this name already exists.`
|
|
3858
|
+
);
|
|
3859
|
+
}
|
|
3860
|
+
const observer = $createObservable(value, { reset: true });
|
|
3861
|
+
$stores.set(name, { observer, subscribers: new Set(), resettable: true, composed: false });
|
|
3862
|
+
return observer;
|
|
3863
|
+
},
|
|
3886
3864
|
|
|
3887
|
-
|
|
3865
|
+
/**
|
|
3866
|
+
* Create a computed store derived from other stores.
|
|
3867
|
+
* The value is automatically recalculated when any dependency changes.
|
|
3868
|
+
* This store is read-only — Store.use() and Store.set() will throw.
|
|
3869
|
+
* Throws if a store with the same name already exists.
|
|
3870
|
+
*
|
|
3871
|
+
* @param {string} name
|
|
3872
|
+
* @param {() => *} computation - Function that returns the computed value
|
|
3873
|
+
* @param {string[]} dependencies - Names of the stores to watch
|
|
3874
|
+
* @returns {ObservableItem}
|
|
3875
|
+
*
|
|
3876
|
+
* @example
|
|
3877
|
+
* Store.create('products', [{ id: 1, price: 10 }]);
|
|
3878
|
+
* Store.create('cart', [{ productId: 1, quantity: 2 }]);
|
|
3879
|
+
*
|
|
3880
|
+
* Store.createComposed('total', () => {
|
|
3881
|
+
* const products = Store.get('products').val();
|
|
3882
|
+
* const cart = Store.get('cart').val();
|
|
3883
|
+
* return cart.reduce((sum, item) => {
|
|
3884
|
+
* const product = products.find(p => p.id === item.productId);
|
|
3885
|
+
* return sum + (product.price * item.quantity);
|
|
3886
|
+
* }, 0);
|
|
3887
|
+
* }, ['products', 'cart']);
|
|
3888
|
+
*/
|
|
3889
|
+
createComposed(name, computation, dependencies) {
|
|
3890
|
+
if ($stores.has(name)) {
|
|
3891
|
+
DebugManager.warn('Store', `Store.createComposed('${name}') : a store with this name already exists.`);
|
|
3892
|
+
throw new NativeDocumentError(
|
|
3893
|
+
`Store.createComposed('${name}') : a store with this name already exists.`
|
|
3894
|
+
);
|
|
3895
|
+
}
|
|
3896
|
+
if (typeof computation !== 'function') {
|
|
3897
|
+
throw new NativeDocumentError(
|
|
3898
|
+
`Store.createComposed('${name}') : computation must be a function.`
|
|
3899
|
+
);
|
|
3900
|
+
}
|
|
3901
|
+
if (!Array.isArray(dependencies) || dependencies.length === 0) {
|
|
3902
|
+
throw new NativeDocumentError(
|
|
3903
|
+
`Store.createComposed('${name}') : dependencies must be a non-empty array of store names.`
|
|
3904
|
+
);
|
|
3905
|
+
}
|
|
3888
3906
|
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3907
|
+
// Resolve dependency observers
|
|
3908
|
+
const depObservers = dependencies.map(depName => {
|
|
3909
|
+
if(typeof depName !== 'string') {
|
|
3910
|
+
return depName;
|
|
3911
|
+
}
|
|
3912
|
+
const depItem = $stores.get(depName);
|
|
3913
|
+
if (!depItem) {
|
|
3914
|
+
DebugManager.error('Store', `Store.createComposed('${name}') : dependency '${depName}' not found. Create it first.`);
|
|
3915
|
+
throw new NativeDocumentError(
|
|
3916
|
+
`Store.createComposed('${name}') : dependency store '${depName}' not found.`
|
|
3917
|
+
);
|
|
3918
|
+
}
|
|
3919
|
+
return depItem.observer;
|
|
3894
3920
|
});
|
|
3895
|
-
}
|
|
3896
|
-
}
|
|
3897
|
-
|
|
3898
|
-
};
|
|
3899
3921
|
|
|
3900
|
-
|
|
3922
|
+
// Create computed observable from dependency observers
|
|
3923
|
+
const observer = Observable.computed(computation, depObservers);
|
|
3901
3924
|
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
},
|
|
3906
|
-
set(value) {
|
|
3907
|
-
this.set(value);
|
|
3908
|
-
}
|
|
3909
|
-
});
|
|
3925
|
+
$stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: true });
|
|
3926
|
+
return observer;
|
|
3927
|
+
},
|
|
3910
3928
|
|
|
3911
|
-
|
|
3912
|
-
|
|
3929
|
+
/**
|
|
3930
|
+
* Returns true if a store with the given name exists.
|
|
3931
|
+
*
|
|
3932
|
+
* @param {string} name
|
|
3933
|
+
* @returns {boolean}
|
|
3934
|
+
*/
|
|
3935
|
+
has(name) {
|
|
3936
|
+
return $stores.has(name);
|
|
3937
|
+
},
|
|
3913
3938
|
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
return Observable(item, configs);
|
|
3928
|
-
});
|
|
3929
|
-
this.$observables[key] = Observable.array(mappedItemValue, configs);
|
|
3930
|
-
continue;
|
|
3939
|
+
/**
|
|
3940
|
+
* Resets a resettable store to its initial value and notifies all subscribers.
|
|
3941
|
+
* Throws if the store was not created with createResettable().
|
|
3942
|
+
*
|
|
3943
|
+
* @param {string} name
|
|
3944
|
+
*/
|
|
3945
|
+
reset(name) {
|
|
3946
|
+
const item = $getStoreOrThrow('reset', name);
|
|
3947
|
+
if (item.composed) {
|
|
3948
|
+
DebugManager.error('Store', `Store.reset('${name}') : composed stores cannot be reset. Their value is derived from dependencies.`);
|
|
3949
|
+
throw new NativeDocumentError(
|
|
3950
|
+
`Store.reset('${name}') : composed stores cannot be reset.`
|
|
3951
|
+
);
|
|
3931
3952
|
}
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
}
|
|
3941
|
-
};
|
|
3953
|
+
if (!item.resettable) {
|
|
3954
|
+
DebugManager.error('Store', `Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`);
|
|
3955
|
+
throw new NativeDocumentError(
|
|
3956
|
+
`Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`
|
|
3957
|
+
);
|
|
3958
|
+
}
|
|
3959
|
+
item.observer.reset();
|
|
3960
|
+
},
|
|
3942
3961
|
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3962
|
+
/**
|
|
3963
|
+
* Returns a two-way synchronized follower of the store.
|
|
3964
|
+
* Writing to the follower propagates the value back to the store and all its subscribers.
|
|
3965
|
+
* Throws if called on a composed store — use Store.follow() instead.
|
|
3966
|
+
* Call follower.destroy() or follower.dispose() to unsubscribe.
|
|
3967
|
+
*
|
|
3968
|
+
* @param {string} name
|
|
3969
|
+
* @returns {ObservableItem}
|
|
3970
|
+
*/
|
|
3971
|
+
use(name) {
|
|
3972
|
+
const item = $getStoreOrThrow('use', name);
|
|
3973
|
+
|
|
3974
|
+
if (item.composed) {
|
|
3975
|
+
DebugManager.error('Store', `Store.use('${name}') : composed stores are read-only. Use Store.follow('${name}') instead.`);
|
|
3976
|
+
throw new NativeDocumentError(
|
|
3977
|
+
`Store.use('${name}') : composed stores are read-only. Use Store.follow('${name}') instead.`
|
|
3978
|
+
);
|
|
3959
3979
|
}
|
|
3960
|
-
result[key] = value;
|
|
3961
|
-
} else if(Validator.isProxy(dataItem)) {
|
|
3962
|
-
result[key] = dataItem.$value;
|
|
3963
|
-
} else {
|
|
3964
|
-
result[key] = dataItem;
|
|
3965
|
-
}
|
|
3966
|
-
}
|
|
3967
|
-
return result;
|
|
3968
|
-
};
|
|
3969
|
-
ObservableObject.prototype.$val = ObservableObject.prototype.val;
|
|
3970
3980
|
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
if(Validator.isObservable(item)) {
|
|
3974
|
-
return item.val();
|
|
3975
|
-
}
|
|
3976
|
-
if(Validator.isProxy(item)) {
|
|
3977
|
-
return item.$value;
|
|
3978
|
-
}
|
|
3979
|
-
return item;
|
|
3980
|
-
};
|
|
3981
|
-
ObservableObject.prototype.$get = ObservableObject.prototype.get;
|
|
3981
|
+
const { observer: originalObserver, subscribers } = item;
|
|
3982
|
+
const observerFollower = $createObservable(originalObserver.val());
|
|
3982
3983
|
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
const configs = this.configs;
|
|
3984
|
+
const onStoreChange = value => observerFollower.set(value);
|
|
3985
|
+
const onFollowerChange = value => originalObserver.set(value);
|
|
3986
3986
|
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
const newValueOrigin = newData[key];
|
|
3990
|
-
const newValue = data[key];
|
|
3987
|
+
originalObserver.subscribe(onStoreChange);
|
|
3988
|
+
observerFollower.subscribe(onFollowerChange);
|
|
3991
3989
|
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
const newValues = newValue.map(item => {
|
|
4000
|
-
if(Validator.isProxy(firstElementFromOriginalValue)) {
|
|
4001
|
-
return Observable.init(item, configs);
|
|
4002
|
-
}
|
|
4003
|
-
return Observable(item, configs);
|
|
4004
|
-
});
|
|
4005
|
-
targetItem.set(newValues);
|
|
4006
|
-
continue;
|
|
4007
|
-
}
|
|
4008
|
-
targetItem.set([...newValue]);
|
|
4009
|
-
continue;
|
|
4010
|
-
}
|
|
4011
|
-
if(Validator.isProxy(targetItem)) {
|
|
4012
|
-
targetItem.update(newValue);
|
|
4013
|
-
continue;
|
|
4014
|
-
}
|
|
4015
|
-
this[key] = newValue;
|
|
4016
|
-
}
|
|
4017
|
-
};
|
|
4018
|
-
ObservableObject.prototype.$set = ObservableObject.prototype.set;
|
|
4019
|
-
ObservableObject.prototype.$updateWith = ObservableObject.prototype.set;
|
|
3990
|
+
observerFollower.destroy = () => {
|
|
3991
|
+
originalObserver.unsubscribe(onStoreChange);
|
|
3992
|
+
observerFollower.unsubscribe(onFollowerChange);
|
|
3993
|
+
subscribers.delete(observerFollower);
|
|
3994
|
+
observerFollower.cleanup();
|
|
3995
|
+
};
|
|
3996
|
+
observerFollower.dispose = observerFollower.destroy;
|
|
4020
3997
|
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
3998
|
+
subscribers.add(observerFollower);
|
|
3999
|
+
return observerFollower;
|
|
4000
|
+
},
|
|
4001
|
+
|
|
4002
|
+
/**
|
|
4003
|
+
* Returns a read-only follower of the store.
|
|
4004
|
+
* The follower reflects store changes but cannot write back to the store.
|
|
4005
|
+
* Any attempt to call .set(), .toggle() or .reset() will throw.
|
|
4006
|
+
* Call follower.destroy() or follower.dispose() to unsubscribe.
|
|
4007
|
+
*
|
|
4008
|
+
* @param {string} name
|
|
4009
|
+
* @returns {ObservableItem}
|
|
4010
|
+
*/
|
|
4011
|
+
follow(name) {
|
|
4012
|
+
const { observer: originalObserver, subscribers } = $getStoreOrThrow('follow', name);
|
|
4013
|
+
const observerFollower = $createObservable(originalObserver.val());
|
|
4025
4014
|
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
};
|
|
4029
|
-
ObservableObject.prototype.$keys = ObservableObject.prototype.keys;
|
|
4030
|
-
ObservableObject.prototype.clone = function() {
|
|
4031
|
-
return Observable.init(this.val(), this.configs);
|
|
4032
|
-
};
|
|
4033
|
-
ObservableObject.prototype.$clone = ObservableObject.prototype.clone;
|
|
4034
|
-
ObservableObject.prototype.reset = function() {
|
|
4035
|
-
for(const key in this.$observables) {
|
|
4036
|
-
this.$observables[key].reset();
|
|
4037
|
-
}
|
|
4038
|
-
};
|
|
4039
|
-
ObservableObject.prototype.originalSubscribe = ObservableObject.prototype.subscribe;
|
|
4040
|
-
ObservableObject.prototype.subscribe = function(callback) {
|
|
4041
|
-
const observables = this.observables();
|
|
4042
|
-
const updatedValue = nextTick(() => this.trigger());
|
|
4015
|
+
const onStoreChange = value => observerFollower.set(value);
|
|
4016
|
+
originalObserver.subscribe(onStoreChange);
|
|
4043
4017
|
|
|
4044
|
-
|
|
4018
|
+
$applyReadOnly(observerFollower, name, 'follow');
|
|
4045
4019
|
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
observable.subscribe(updatedValue);
|
|
4053
|
-
}
|
|
4054
|
-
};
|
|
4055
|
-
ObservableObject.prototype.configs = function() {
|
|
4056
|
-
return this.configs;
|
|
4057
|
-
};
|
|
4020
|
+
observerFollower.destroy = () => {
|
|
4021
|
+
originalObserver.unsubscribe(onStoreChange);
|
|
4022
|
+
subscribers.delete(observerFollower);
|
|
4023
|
+
observerFollower.cleanup();
|
|
4024
|
+
};
|
|
4025
|
+
observerFollower.dispose = observerFollower.destroy;
|
|
4058
4026
|
|
|
4059
|
-
|
|
4027
|
+
subscribers.add(observerFollower);
|
|
4028
|
+
return observerFollower;
|
|
4029
|
+
},
|
|
4060
4030
|
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4031
|
+
/**
|
|
4032
|
+
* Returns the raw store observer directly (no follower, no cleanup contract).
|
|
4033
|
+
* Use this for direct read access when you don't need to unsubscribe.
|
|
4034
|
+
* WARNING : mutations on this observer impact all subscribers immediately.
|
|
4035
|
+
*
|
|
4036
|
+
* @param {string} name
|
|
4037
|
+
* @returns {ObservableItem|null}
|
|
4038
|
+
*/
|
|
4039
|
+
get(name) {
|
|
4040
|
+
const item = $stores.get(name);
|
|
4041
|
+
if (!item) {
|
|
4042
|
+
DebugManager.warn('Store', `Store.get('${name}') : store not found.`);
|
|
4043
|
+
return null;
|
|
4044
|
+
}
|
|
4045
|
+
return item.observer;
|
|
4046
|
+
},
|
|
4064
4047
|
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
};
|
|
4048
|
+
/**
|
|
4049
|
+
* @param {string} name
|
|
4050
|
+
* @returns {{ observer: ObservableItem, subscribers: Set } | null}
|
|
4051
|
+
*/
|
|
4052
|
+
getWithSubscribers(name) {
|
|
4053
|
+
return $stores.get(name) ?? null;
|
|
4054
|
+
},
|
|
4073
4055
|
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4056
|
+
/**
|
|
4057
|
+
* Destroys a store : cleans up the observer, destroys all followers, and removes the entry.
|
|
4058
|
+
*
|
|
4059
|
+
* @param {string} name
|
|
4060
|
+
*/
|
|
4061
|
+
delete(name) {
|
|
4062
|
+
const item = $stores.get(name);
|
|
4063
|
+
if (!item) {
|
|
4064
|
+
DebugManager.warn('Store', `Store.delete('${name}') : store not found, nothing to delete.`);
|
|
4065
|
+
return;
|
|
4066
|
+
}
|
|
4067
|
+
item.subscribers.forEach(follower => follower.destroy());
|
|
4068
|
+
item.subscribers.clear();
|
|
4069
|
+
item.observer.cleanup();
|
|
4070
|
+
$stores.delete(name);
|
|
4071
|
+
},
|
|
4072
|
+
/**
|
|
4073
|
+
* Creates an isolated store group with its own state namespace.
|
|
4074
|
+
* Each group is a fully independent StoreFactory instance —
|
|
4075
|
+
* no key conflicts, no shared state with the parent store.
|
|
4076
|
+
*
|
|
4077
|
+
* @param {string | ((group: ReturnType<typeof StoreFactory>) => void)} name - Group name for debugging, or setup callback if no name is provided
|
|
4078
|
+
* @param {((group: ReturnType<typeof StoreFactory>) => void)} [callback] - Setup function receiving the isolated store instance
|
|
4079
|
+
* @returns {ReturnType<typeof StoreFactory>}
|
|
4080
|
+
*
|
|
4081
|
+
* @example
|
|
4082
|
+
* // With name (recommended)
|
|
4083
|
+
* const EventStore = Store.group('events', (group) => {
|
|
4084
|
+
* group.create('catalog', []);
|
|
4085
|
+
* group.create('filters', { category: null, date: null });
|
|
4086
|
+
* group.createResettable('selected', null);
|
|
4087
|
+
* group.createComposed('filtered', () => {
|
|
4088
|
+
* const catalog = EventStore.get('catalog').val();
|
|
4089
|
+
* const filters = EventStore.get('filters').val();
|
|
4090
|
+
* return catalog.filter(event => {
|
|
4091
|
+
* if (filters.category && event.category !== filters.category) return false;
|
|
4092
|
+
* return true;
|
|
4093
|
+
* });
|
|
4094
|
+
* }, ['catalog', 'filters']);
|
|
4095
|
+
* });
|
|
4096
|
+
*
|
|
4097
|
+
* // Without name
|
|
4098
|
+
* const CartStore = Store.group((group) => {
|
|
4099
|
+
* group.create('items', []);
|
|
4100
|
+
* });
|
|
4101
|
+
*
|
|
4102
|
+
* // Usage
|
|
4103
|
+
* EventStore.use('catalog'); // two-way follower
|
|
4104
|
+
* EventStore.follow('filtered'); // read-only follower
|
|
4105
|
+
* EventStore.get('filters'); // raw observable
|
|
4106
|
+
*
|
|
4107
|
+
* // Cross-group composed
|
|
4108
|
+
* const OrderStore = Store.group('orders', (group) => {
|
|
4109
|
+
* group.createComposed('summary', () => {
|
|
4110
|
+
* const items = CartStore.get('items').val();
|
|
4111
|
+
* const events = EventStore.get('catalog').val();
|
|
4112
|
+
* return { items, events };
|
|
4113
|
+
* }, [CartStore.get('items'), EventStore.get('catalog')]);
|
|
4114
|
+
* });
|
|
4115
|
+
*/
|
|
4116
|
+
group(name, callback) {
|
|
4117
|
+
if (typeof name === 'function') {
|
|
4118
|
+
callback = name;
|
|
4119
|
+
name = 'anonymous';
|
|
4120
|
+
}
|
|
4121
|
+
const store = StoreFactory();
|
|
4122
|
+
callback && callback(store);
|
|
4123
|
+
return store;
|
|
4124
|
+
},
|
|
4125
|
+
createPersistent(name, value, localstorage_key) {
|
|
4126
|
+
localstorage_key = localstorage_key || name;
|
|
4127
|
+
const observer = this.create(name, $getFromStorage(localstorage_key, value));
|
|
4128
|
+
const saver = $saveToStorage(value);
|
|
4096
4129
|
|
|
4097
|
-
|
|
4098
|
-
|
|
4130
|
+
observer.subscribe((val) => saver(localstorage_key, val));
|
|
4131
|
+
return observer;
|
|
4132
|
+
},
|
|
4133
|
+
createPersistentResettable(name, value, localstorage_key) {
|
|
4134
|
+
localstorage_key = localstorage_key || name;
|
|
4135
|
+
const observer = this.createResettable(name, $getFromStorage(localstorage_key, value));
|
|
4136
|
+
const saver = $saveToStorage(value);
|
|
4137
|
+
observer.subscribe((val) => saver(localstorage_key, val));
|
|
4099
4138
|
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
* @param {Array<ObservableItem|ObservableChecker|ObservableProxy>|Function} [dependencies=[]] - Array of observables to watch, or batch function
|
|
4106
|
-
* @returns {ObservableItem} A new observable that updates automatically
|
|
4107
|
-
* @example
|
|
4108
|
-
* const firstName = Observable('John');
|
|
4109
|
-
* const lastName = Observable('Doe');
|
|
4110
|
-
* const fullName = Observable.computed(
|
|
4111
|
-
* () => `${firstName.val()} ${lastName.val()}`,
|
|
4112
|
-
* [firstName, lastName]
|
|
4113
|
-
* );
|
|
4114
|
-
*
|
|
4115
|
-
* // With batch function
|
|
4116
|
-
* const batch = Observable.batch(() => { ... });
|
|
4117
|
-
* const computed = Observable.computed(() => { ... }, batch);
|
|
4118
|
-
*/
|
|
4119
|
-
Observable.computed = function(callback, dependencies = []) {
|
|
4120
|
-
const initialValue = callback();
|
|
4121
|
-
const observable = new ObservableItem(initialValue);
|
|
4122
|
-
const updatedValue = nextTick(() => observable.set(callback()));
|
|
4139
|
+
const originalReset = observer.reset.bind(observer);
|
|
4140
|
+
observer.reset = () => {
|
|
4141
|
+
LocalStorage.remove(localstorage_key);
|
|
4142
|
+
originalReset();
|
|
4143
|
+
};
|
|
4123
4144
|
|
|
4124
|
-
|
|
4125
|
-
if(!Validator.isObservable(dependencies.$observer)) {
|
|
4126
|
-
throw new NativeDocumentError('Observable.computed : dependencies must be valid batch function');
|
|
4145
|
+
return observer;
|
|
4127
4146
|
}
|
|
4128
|
-
|
|
4129
|
-
return observable;
|
|
4130
|
-
}
|
|
4147
|
+
};
|
|
4131
4148
|
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4149
|
+
|
|
4150
|
+
return new Proxy($api, {
|
|
4151
|
+
get(target, prop) {
|
|
4152
|
+
if (typeof prop === 'symbol' || prop.startsWith('$') || prop in target) {
|
|
4153
|
+
return target[prop];
|
|
4154
|
+
}
|
|
4155
|
+
if (target.has(prop)) {
|
|
4156
|
+
if ($followersCache.has(prop)) {
|
|
4157
|
+
return $followersCache.get(prop);
|
|
4158
|
+
}
|
|
4159
|
+
const follower = target.follow(prop);
|
|
4160
|
+
$followersCache.set(prop, follower);
|
|
4161
|
+
return follower;
|
|
4162
|
+
}
|
|
4163
|
+
return undefined;
|
|
4164
|
+
},
|
|
4165
|
+
set(target, prop, value) {
|
|
4166
|
+
DebugManager.error('Store', `Forbidden: You cannot overwrite the store key '${String(prop)}'. Use .use('${String(prop)}').set(value) instead.`);
|
|
4167
|
+
throw new NativeDocumentError(`Store structure is immutable. Use .set() on the observable.`);
|
|
4168
|
+
},
|
|
4169
|
+
deleteProperty(target, prop) {
|
|
4170
|
+
throw new NativeDocumentError(`Store keys cannot be deleted.`);
|
|
4138
4171
|
}
|
|
4139
|
-
dependency.subscribe(updatedValue);
|
|
4140
4172
|
});
|
|
4141
|
-
|
|
4142
|
-
return observable;
|
|
4143
4173
|
};
|
|
4144
4174
|
|
|
4175
|
+
const Store = StoreFactory();
|
|
4176
|
+
|
|
4177
|
+
Store.create('locale', navigator.language.split('-')[0] || 'en');
|
|
4178
|
+
|
|
4145
4179
|
/**
|
|
4146
4180
|
* Creates a `<button>` element.
|
|
4147
4181
|
* @type {function(ButtonAttributes=, NdChild|NdChild[]=): HTMLButtonElement}
|