native-document 1.0.117 → 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 +1136 -1095
- package/dist/native-document.dev.js +1125 -1086
- 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/template-cloner/NodeCloner.js +55 -13
- package/src/core/wrappers/template-cloner/TemplateCloner.js +2 -4
|
@@ -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,29 +2897,71 @@ 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;
|
|
@@ -3641,500 +3282,900 @@ var NativeComponents = (function (exports) {
|
|
|
3641
3282
|
};
|
|
3642
3283
|
|
|
3643
3284
|
/**
|
|
3644
|
-
* 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
|
+
/**
|
|
3645
3497
|
*
|
|
3646
|
-
* @
|
|
3647
|
-
* @
|
|
3648
|
-
* const items = Observable.array([]);
|
|
3649
|
-
* items.isEmpty(); // true
|
|
3498
|
+
* @param {Function} callback
|
|
3499
|
+
* @returns {Function}
|
|
3650
3500
|
*/
|
|
3651
|
-
|
|
3652
|
-
|
|
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;
|
|
3653
3514
|
};
|
|
3654
3515
|
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
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
|
+
|
|
3664
3532
|
};
|
|
3665
3533
|
|
|
3534
|
+
ObservableObject.prototype = Object.create(ObservableItem.prototype);
|
|
3666
3535
|
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
* { name: 'John', age: 25 },
|
|
3676
|
-
* { name: 'Jane', age: 30 }
|
|
3677
|
-
* ]);
|
|
3678
|
-
* const adults = users.where({ age: (val) => val >= 18 });
|
|
3679
|
-
*/
|
|
3680
|
-
ObservableArray.prototype.where = function(predicates) {
|
|
3681
|
-
const sourceArray = this;
|
|
3682
|
-
const observableDependencies = [sourceArray];
|
|
3683
|
-
const filterCallbacks = {};
|
|
3536
|
+
Object.defineProperty(ObservableObject, '$value', {
|
|
3537
|
+
get() {
|
|
3538
|
+
return this.val();
|
|
3539
|
+
},
|
|
3540
|
+
set(value) {
|
|
3541
|
+
this.set(value);
|
|
3542
|
+
}
|
|
3543
|
+
});
|
|
3684
3544
|
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
if (predicate && typeof predicate === 'object' && 'callback' in predicate) {
|
|
3688
|
-
filterCallbacks[key] = predicate.callback;
|
|
3545
|
+
ObservableObject.prototype.__$isObservableObject = true;
|
|
3546
|
+
ObservableObject.prototype.__isProxy__ = true;
|
|
3689
3547
|
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
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;
|
|
3695
3565
|
}
|
|
3696
|
-
|
|
3697
|
-
|
|
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;
|
|
3698
3597
|
} else {
|
|
3699
|
-
|
|
3598
|
+
result[key] = dataItem;
|
|
3700
3599
|
}
|
|
3701
3600
|
}
|
|
3601
|
+
return result;
|
|
3602
|
+
};
|
|
3603
|
+
ObservableObject.prototype.$val = ObservableObject.prototype.val;
|
|
3702
3604
|
|
|
3703
|
-
|
|
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;
|
|
3704
3616
|
|
|
3705
|
-
|
|
3706
|
-
const
|
|
3707
|
-
|
|
3708
|
-
for (const [key, callback] of filters) {
|
|
3709
|
-
if(key === '_') {
|
|
3710
|
-
if (!callback(item)) return false;
|
|
3711
|
-
} else {
|
|
3712
|
-
if (!callback(item[key])) return false;
|
|
3713
|
-
}
|
|
3714
|
-
}
|
|
3715
|
-
return true;
|
|
3716
|
-
});
|
|
3617
|
+
ObservableObject.prototype.set = function(newData) {
|
|
3618
|
+
const data = Validator.isProxy(newData) ? newData.$value : newData;
|
|
3619
|
+
const configs = this.configs;
|
|
3717
3620
|
|
|
3718
|
-
|
|
3719
|
-
|
|
3621
|
+
for(const key in data) {
|
|
3622
|
+
const targetItem = this.$observables[key];
|
|
3623
|
+
const newValueOrigin = newData[key];
|
|
3624
|
+
const newValue = data[key];
|
|
3720
3625
|
|
|
3721
|
-
|
|
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;
|
|
3722
3654
|
|
|
3723
|
-
|
|
3655
|
+
ObservableObject.prototype.observables = function() {
|
|
3656
|
+
return Object.values(this.$observables);
|
|
3657
|
+
};
|
|
3658
|
+
ObservableObject.prototype.$observables = ObservableObject.prototype.observables;
|
|
3724
3659
|
|
|
3725
|
-
|
|
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
|
+
}
|
|
3726
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());
|
|
3727
3677
|
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
* const products = Observable.array([
|
|
3736
|
-
* { name: 'Apple', category: 'Fruit' },
|
|
3737
|
-
* { name: 'Carrot', category: 'Vegetable' }
|
|
3738
|
-
* ]);
|
|
3739
|
-
* const searchTerm = Observable('App');
|
|
3740
|
-
* const filtered = products.whereSome(['name', 'category'], match(searchTerm));
|
|
3741
|
-
*/
|
|
3742
|
-
ObservableArray.prototype.whereSome = function(fields, filter) {
|
|
3743
|
-
return this.where({
|
|
3744
|
-
_: {
|
|
3745
|
-
dependencies: filter.dependencies,
|
|
3746
|
-
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
|
|
3747
3685
|
}
|
|
3748
|
-
|
|
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)
|
|
3749
3697
|
};
|
|
3750
3698
|
|
|
3751
3699
|
/**
|
|
3752
|
-
* Creates a filtered view where all specified fields match the filter.
|
|
3753
3700
|
*
|
|
3754
|
-
* @param {
|
|
3755
|
-
* @
|
|
3756
|
-
* @returns {ObservableArray} A new observable array containing filtered items
|
|
3757
|
-
* @example
|
|
3758
|
-
* const items = Observable.array([
|
|
3759
|
-
* { status: 'active', verified: true },
|
|
3760
|
-
* { status: 'active', verified: false }
|
|
3761
|
-
* ]);
|
|
3762
|
-
* const activeFilter = equals('active');
|
|
3763
|
-
* const filtered = items.whereEvery(['status', 'verified'], activeFilter);
|
|
3701
|
+
* @param {any[]} data
|
|
3702
|
+
* @return Proxy[]
|
|
3764
3703
|
*/
|
|
3765
|
-
|
|
3766
|
-
return
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
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));
|
|
3770
3725
|
}
|
|
3771
|
-
|
|
3726
|
+
return result;
|
|
3727
|
+
}
|
|
3728
|
+
return data;
|
|
3772
3729
|
};
|
|
3773
3730
|
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
const $listeners = new WeakMap();
|
|
3731
|
+
Observable.object = Observable.init;
|
|
3732
|
+
Observable.json = Observable.init;
|
|
3777
3733
|
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
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()));
|
|
3791
3757
|
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
unsub();
|
|
3796
|
-
$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');
|
|
3797
3761
|
}
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
this.subscribe(updatedValue);
|
|
3762
|
+
dependencies.$observer.subscribe(updatedValue);
|
|
3763
|
+
return observable;
|
|
3764
|
+
}
|
|
3802
3765
|
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
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
|
+
});
|
|
3809
3775
|
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
operations.result?.forEach(unbindItem);
|
|
3813
|
-
newItems.forEach(bindItem);
|
|
3814
|
-
break;
|
|
3815
|
-
}
|
|
3776
|
+
return observable;
|
|
3777
|
+
};
|
|
3816
3778
|
|
|
3817
|
-
|
|
3818
|
-
unbindItem(operations.result);
|
|
3819
|
-
break;
|
|
3779
|
+
const StoreFactory = function() {
|
|
3820
3780
|
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
break;
|
|
3781
|
+
const $stores = new Map();
|
|
3782
|
+
const $followersCache = new Map();
|
|
3824
3783
|
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
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
|
+
);
|
|
3828
3794
|
}
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
return () => {
|
|
3832
|
-
this.$currentValue.forEach(unbindItem);
|
|
3795
|
+
return item;
|
|
3833
3796
|
};
|
|
3834
|
-
};
|
|
3835
3797
|
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
*/
|
|
3851
|
-
Observable.array = function(target = [], configs = null) {
|
|
3852
|
-
return new ObservableArray(target, configs);
|
|
3853
|
-
};
|
|
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
|
+
};
|
|
3854
3812
|
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
* @returns {Function}
|
|
3859
|
-
*/
|
|
3860
|
-
Observable.batch = function(callback) {
|
|
3861
|
-
const $observer = Observable(0);
|
|
3862
|
-
const batch = function() {
|
|
3863
|
-
if(Validator.isAsyncFunction(callback)) {
|
|
3864
|
-
return (callback(...arguments)).then(() => {
|
|
3865
|
-
$observer.trigger();
|
|
3866
|
-
}).catch(error => { throw error; });
|
|
3813
|
+
const $createObservable = (value, options = {}) => {
|
|
3814
|
+
if(Array.isArray(value)) {
|
|
3815
|
+
return Observable.array(value, options);
|
|
3867
3816
|
}
|
|
3868
|
-
|
|
3869
|
-
|
|
3817
|
+
if(typeof value === 'object') {
|
|
3818
|
+
return Observable.object(value, options);
|
|
3819
|
+
}
|
|
3820
|
+
return Observable(value, options);
|
|
3870
3821
|
};
|
|
3871
|
-
batch.$observer = $observer;
|
|
3872
|
-
return batch;
|
|
3873
|
-
};
|
|
3874
3822
|
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
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
|
+
},
|
|
3879
3864
|
|
|
3880
|
-
|
|
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
|
+
}
|
|
3881
3906
|
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
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;
|
|
3887
3920
|
});
|
|
3888
|
-
}
|
|
3889
|
-
}
|
|
3890
|
-
|
|
3891
|
-
};
|
|
3892
3921
|
|
|
3893
|
-
|
|
3922
|
+
// Create computed observable from dependency observers
|
|
3923
|
+
const observer = Observable.computed(computation, depObservers);
|
|
3894
3924
|
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
},
|
|
3899
|
-
set(value) {
|
|
3900
|
-
this.set(value);
|
|
3901
|
-
}
|
|
3902
|
-
});
|
|
3925
|
+
$stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: true });
|
|
3926
|
+
return observer;
|
|
3927
|
+
},
|
|
3903
3928
|
|
|
3904
|
-
|
|
3905
|
-
|
|
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
|
+
},
|
|
3906
3938
|
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
return Observable(item, configs);
|
|
3921
|
-
});
|
|
3922
|
-
this.$observables[key] = Observable.array(mappedItemValue, configs);
|
|
3923
|
-
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
|
+
);
|
|
3924
3952
|
}
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
}
|
|
3934
|
-
};
|
|
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
|
+
},
|
|
3935
3961
|
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
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
|
+
);
|
|
3952
3979
|
}
|
|
3953
|
-
result[key] = value;
|
|
3954
|
-
} else if(Validator.isProxy(dataItem)) {
|
|
3955
|
-
result[key] = dataItem.$value;
|
|
3956
|
-
} else {
|
|
3957
|
-
result[key] = dataItem;
|
|
3958
|
-
}
|
|
3959
|
-
}
|
|
3960
|
-
return result;
|
|
3961
|
-
};
|
|
3962
|
-
ObservableObject.prototype.$val = ObservableObject.prototype.val;
|
|
3963
3980
|
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
if(Validator.isObservable(item)) {
|
|
3967
|
-
return item.val();
|
|
3968
|
-
}
|
|
3969
|
-
if(Validator.isProxy(item)) {
|
|
3970
|
-
return item.$value;
|
|
3971
|
-
}
|
|
3972
|
-
return item;
|
|
3973
|
-
};
|
|
3974
|
-
ObservableObject.prototype.$get = ObservableObject.prototype.get;
|
|
3981
|
+
const { observer: originalObserver, subscribers } = item;
|
|
3982
|
+
const observerFollower = $createObservable(originalObserver.val());
|
|
3975
3983
|
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
const configs = this.configs;
|
|
3984
|
+
const onStoreChange = value => observerFollower.set(value);
|
|
3985
|
+
const onFollowerChange = value => originalObserver.set(value);
|
|
3979
3986
|
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
const newValueOrigin = newData[key];
|
|
3983
|
-
const newValue = data[key];
|
|
3987
|
+
originalObserver.subscribe(onStoreChange);
|
|
3988
|
+
observerFollower.subscribe(onFollowerChange);
|
|
3984
3989
|
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
const newValues = newValue.map(item => {
|
|
3993
|
-
if(Validator.isProxy(firstElementFromOriginalValue)) {
|
|
3994
|
-
return Observable.init(item, configs);
|
|
3995
|
-
}
|
|
3996
|
-
return Observable(item, configs);
|
|
3997
|
-
});
|
|
3998
|
-
targetItem.set(newValues);
|
|
3999
|
-
continue;
|
|
4000
|
-
}
|
|
4001
|
-
targetItem.set([...newValue]);
|
|
4002
|
-
continue;
|
|
4003
|
-
}
|
|
4004
|
-
if(Validator.isProxy(targetItem)) {
|
|
4005
|
-
targetItem.update(newValue);
|
|
4006
|
-
continue;
|
|
4007
|
-
}
|
|
4008
|
-
this[key] = newValue;
|
|
4009
|
-
}
|
|
4010
|
-
};
|
|
4011
|
-
ObservableObject.prototype.$set = ObservableObject.prototype.set;
|
|
4012
|
-
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;
|
|
4013
3997
|
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
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());
|
|
4018
4014
|
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
};
|
|
4022
|
-
ObservableObject.prototype.$keys = ObservableObject.prototype.keys;
|
|
4023
|
-
ObservableObject.prototype.clone = function() {
|
|
4024
|
-
return Observable.init(this.val(), this.configs);
|
|
4025
|
-
};
|
|
4026
|
-
ObservableObject.prototype.$clone = ObservableObject.prototype.clone;
|
|
4027
|
-
ObservableObject.prototype.reset = function() {
|
|
4028
|
-
for(const key in this.$observables) {
|
|
4029
|
-
this.$observables[key].reset();
|
|
4030
|
-
}
|
|
4031
|
-
};
|
|
4032
|
-
ObservableObject.prototype.originalSubscribe = ObservableObject.prototype.subscribe;
|
|
4033
|
-
ObservableObject.prototype.subscribe = function(callback) {
|
|
4034
|
-
const observables = this.observables();
|
|
4035
|
-
const updatedValue = nextTick(() => this.trigger());
|
|
4015
|
+
const onStoreChange = value => observerFollower.set(value);
|
|
4016
|
+
originalObserver.subscribe(onStoreChange);
|
|
4036
4017
|
|
|
4037
|
-
|
|
4018
|
+
$applyReadOnly(observerFollower, name, 'follow');
|
|
4038
4019
|
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
observable.subscribe(updatedValue);
|
|
4046
|
-
}
|
|
4047
|
-
};
|
|
4048
|
-
ObservableObject.prototype.configs = function() {
|
|
4049
|
-
return this.configs;
|
|
4050
|
-
};
|
|
4020
|
+
observerFollower.destroy = () => {
|
|
4021
|
+
originalObserver.unsubscribe(onStoreChange);
|
|
4022
|
+
subscribers.delete(observerFollower);
|
|
4023
|
+
observerFollower.cleanup();
|
|
4024
|
+
};
|
|
4025
|
+
observerFollower.dispose = observerFollower.destroy;
|
|
4051
4026
|
|
|
4052
|
-
|
|
4027
|
+
subscribers.add(observerFollower);
|
|
4028
|
+
return observerFollower;
|
|
4029
|
+
},
|
|
4053
4030
|
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
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
|
+
},
|
|
4057
4047
|
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
};
|
|
4048
|
+
/**
|
|
4049
|
+
* @param {string} name
|
|
4050
|
+
* @returns {{ observer: ObservableItem, subscribers: Set } | null}
|
|
4051
|
+
*/
|
|
4052
|
+
getWithSubscribers(name) {
|
|
4053
|
+
return $stores.get(name) ?? null;
|
|
4054
|
+
},
|
|
4066
4055
|
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
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);
|
|
4089
4129
|
|
|
4090
|
-
|
|
4091
|
-
|
|
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));
|
|
4092
4138
|
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
* @param {Array<ObservableItem|ObservableChecker|ObservableProxy>|Function} [dependencies=[]] - Array of observables to watch, or batch function
|
|
4099
|
-
* @returns {ObservableItem} A new observable that updates automatically
|
|
4100
|
-
* @example
|
|
4101
|
-
* const firstName = Observable('John');
|
|
4102
|
-
* const lastName = Observable('Doe');
|
|
4103
|
-
* const fullName = Observable.computed(
|
|
4104
|
-
* () => `${firstName.val()} ${lastName.val()}`,
|
|
4105
|
-
* [firstName, lastName]
|
|
4106
|
-
* );
|
|
4107
|
-
*
|
|
4108
|
-
* // With batch function
|
|
4109
|
-
* const batch = Observable.batch(() => { ... });
|
|
4110
|
-
* const computed = Observable.computed(() => { ... }, batch);
|
|
4111
|
-
*/
|
|
4112
|
-
Observable.computed = function(callback, dependencies = []) {
|
|
4113
|
-
const initialValue = callback();
|
|
4114
|
-
const observable = new ObservableItem(initialValue);
|
|
4115
|
-
const updatedValue = nextTick(() => observable.set(callback()));
|
|
4139
|
+
const originalReset = observer.reset.bind(observer);
|
|
4140
|
+
observer.reset = () => {
|
|
4141
|
+
LocalStorage.remove(localstorage_key);
|
|
4142
|
+
originalReset();
|
|
4143
|
+
};
|
|
4116
4144
|
|
|
4117
|
-
|
|
4118
|
-
if(!Validator.isObservable(dependencies.$observer)) {
|
|
4119
|
-
throw new NativeDocumentError('Observable.computed : dependencies must be valid batch function');
|
|
4145
|
+
return observer;
|
|
4120
4146
|
}
|
|
4121
|
-
|
|
4122
|
-
return observable;
|
|
4123
|
-
}
|
|
4147
|
+
};
|
|
4124
4148
|
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
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.`);
|
|
4131
4171
|
}
|
|
4132
|
-
dependency.subscribe(updatedValue);
|
|
4133
4172
|
});
|
|
4134
|
-
|
|
4135
|
-
return observable;
|
|
4136
4173
|
};
|
|
4137
4174
|
|
|
4175
|
+
const Store = StoreFactory();
|
|
4176
|
+
|
|
4177
|
+
Store.create('locale', navigator.language.split('-')[0] || 'en');
|
|
4178
|
+
|
|
4138
4179
|
/**
|
|
4139
4180
|
* Creates a `<button>` element.
|
|
4140
4181
|
* @type {function(ButtonAttributes=, NdChild|NdChild[]=): HTMLButtonElement}
|