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