native-document 1.0.117 → 1.0.118
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/native-document.components.min.js +1136 -1095
- package/dist/native-document.dev.js +1125 -1086
- package/dist/native-document.dev.js.map +1 -1
- package/dist/native-document.min.js +1 -1
- package/package.json +1 -1
- package/src/core/data/ObservableItem.js +1 -2
- package/src/core/wrappers/AttributesWrapper.js +0 -1
- package/src/core/wrappers/template-cloner/NodeCloner.js +55 -13
- package/src/core/wrappers/template-cloner/TemplateCloner.js +2 -4
|
@@ -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,688 +1148,197 @@ 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.error('Store', `Store.${method}('${name}') : store not found. Did you call Store.create('${name}') first?`);
|
|
1072
|
-
throw new NativeDocumentError(
|
|
1073
|
-
`Store.${method}('${name}') : store not found.`
|
|
1074
|
-
);
|
|
1075
|
-
}
|
|
1076
|
-
return item;
|
|
1077
|
-
};
|
|
1166
|
+
this.$firstListener = null;
|
|
1167
|
+
this.$listeners = null;
|
|
1168
|
+
this.$watchers = null;
|
|
1078
1169
|
|
|
1079
|
-
|
|
1080
|
-
* Internal helper — blocks write operations on a read-only observer.
|
|
1081
|
-
*/
|
|
1082
|
-
const $applyReadOnly = (observer, name, context) => {
|
|
1083
|
-
const readOnlyError = (method) => () => {
|
|
1084
|
-
DebugManager.error('Store', `Store.${context}('${name}') is read-only. '${method}()' is not allowed.`);
|
|
1085
|
-
throw new NativeDocumentError(
|
|
1086
|
-
`Store.${context}('${name}') is read-only.`
|
|
1087
|
-
);
|
|
1088
|
-
};
|
|
1089
|
-
observer.set = readOnlyError('set');
|
|
1090
|
-
observer.toggle = readOnlyError('toggle');
|
|
1091
|
-
observer.reset = readOnlyError('reset');
|
|
1092
|
-
};
|
|
1170
|
+
this.$memoryId = null;
|
|
1093
1171
|
|
|
1094
|
-
|
|
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
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
* Create a new state and return the observer.
|
|
1107
|
-
* Throws if a store with the same name already exists.
|
|
1108
|
-
*
|
|
1109
|
-
* @param {string} name
|
|
1110
|
-
* @param {*} value
|
|
1111
|
-
* @returns {ObservableItem}
|
|
1112
|
-
*/
|
|
1113
|
-
create(name, value) {
|
|
1114
|
-
if ($stores.has(name)) {
|
|
1115
|
-
DebugManager.warn('Store', `Store.create('${name}') : a store with this name already exists. Use Store.get('${name}') to retrieve it.`);
|
|
1116
|
-
throw new NativeDocumentError(
|
|
1117
|
-
`Store.create('${name}') : a store with this name already exists.`
|
|
1118
|
-
);
|
|
1119
|
-
}
|
|
1120
|
-
const observer = $createObservable(value);
|
|
1121
|
-
$stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: false });
|
|
1122
|
-
return observer;
|
|
1123
|
-
},
|
|
1177
|
+
}
|
|
1178
|
+
{
|
|
1179
|
+
PluginsManager.emit('CreateObservable', this);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1124
1182
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
createResettable(name, value) {
|
|
1135
|
-
if ($stores.has(name)) {
|
|
1136
|
-
DebugManager.warn('Store', `Store.createResettable('${name}') : a store with this name already exists.`);
|
|
1137
|
-
throw new NativeDocumentError(
|
|
1138
|
-
`Store.createResettable('${name}') : a store with this name already exists.`
|
|
1139
|
-
);
|
|
1140
|
-
}
|
|
1141
|
-
const observer = $createObservable(value, { reset: true });
|
|
1142
|
-
$stores.set(name, { observer, subscribers: new Set(), resettable: true, composed: false });
|
|
1143
|
-
return observer;
|
|
1144
|
-
},
|
|
1183
|
+
Object.defineProperty(ObservableItem.prototype, '$value', {
|
|
1184
|
+
get() {
|
|
1185
|
+
return this.$currentValue;
|
|
1186
|
+
},
|
|
1187
|
+
set(value) {
|
|
1188
|
+
this.set(value);
|
|
1189
|
+
},
|
|
1190
|
+
configurable: true,
|
|
1191
|
+
});
|
|
1145
1192
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
* The value is automatically recalculated when any dependency changes.
|
|
1149
|
-
* This store is read-only — Store.use() and Store.set() will throw.
|
|
1150
|
-
* Throws if a store with the same name already exists.
|
|
1151
|
-
*
|
|
1152
|
-
* @param {string} name
|
|
1153
|
-
* @param {() => *} computation - Function that returns the computed value
|
|
1154
|
-
* @param {string[]} dependencies - Names of the stores to watch
|
|
1155
|
-
* @returns {ObservableItem}
|
|
1156
|
-
*
|
|
1157
|
-
* @example
|
|
1158
|
-
* Store.create('products', [{ id: 1, price: 10 }]);
|
|
1159
|
-
* Store.create('cart', [{ productId: 1, quantity: 2 }]);
|
|
1160
|
-
*
|
|
1161
|
-
* Store.createComposed('total', () => {
|
|
1162
|
-
* const products = Store.get('products').val();
|
|
1163
|
-
* const cart = Store.get('cart').val();
|
|
1164
|
-
* return cart.reduce((sum, item) => {
|
|
1165
|
-
* const product = products.find(p => p.id === item.productId);
|
|
1166
|
-
* return sum + (product.price * item.quantity);
|
|
1167
|
-
* }, 0);
|
|
1168
|
-
* }, ['products', 'cart']);
|
|
1169
|
-
*/
|
|
1170
|
-
createComposed(name, computation, dependencies) {
|
|
1171
|
-
if ($stores.has(name)) {
|
|
1172
|
-
DebugManager.warn('Store', `Store.createComposed('${name}') : a store with this name already exists.`);
|
|
1173
|
-
throw new NativeDocumentError(
|
|
1174
|
-
`Store.createComposed('${name}') : a store with this name already exists.`
|
|
1175
|
-
);
|
|
1176
|
-
}
|
|
1177
|
-
if (typeof computation !== 'function') {
|
|
1178
|
-
throw new NativeDocumentError(
|
|
1179
|
-
`Store.createComposed('${name}') : computation must be a function.`
|
|
1180
|
-
);
|
|
1181
|
-
}
|
|
1182
|
-
if (!Array.isArray(dependencies) || dependencies.length === 0) {
|
|
1183
|
-
throw new NativeDocumentError(
|
|
1184
|
-
`Store.createComposed('${name}') : dependencies must be a non-empty array of store names.`
|
|
1185
|
-
);
|
|
1186
|
-
}
|
|
1193
|
+
ObservableItem.prototype.__$isObservable = true;
|
|
1194
|
+
const noneTrigger = function() {};
|
|
1187
1195
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
// Create computed observable from dependency observers
|
|
1204
|
-
const observer = Observable.computed(computation, depObservers);
|
|
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
|
+
};
|
|
1205
1211
|
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1212
|
+
ObservableItem.prototype.triggerFirstListener = function(operations) {
|
|
1213
|
+
this.$firstListener(this.$currentValue, this.$previousValue, operations);
|
|
1214
|
+
};
|
|
1209
1215
|
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
* @returns {boolean}
|
|
1215
|
-
*/
|
|
1216
|
-
has(name) {
|
|
1217
|
-
return $stores.has(name);
|
|
1218
|
-
},
|
|
1216
|
+
ObservableItem.prototype.triggerListeners = function(operations) {
|
|
1217
|
+
const $listeners = this.$listeners;
|
|
1218
|
+
const $previousValue = this.$previousValue;
|
|
1219
|
+
const $currentValue = this.$currentValue;
|
|
1219
1220
|
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
* @param {string} name
|
|
1225
|
-
*/
|
|
1226
|
-
reset(name) {
|
|
1227
|
-
const item = $getStoreOrThrow('reset', name);
|
|
1228
|
-
if (item.composed) {
|
|
1229
|
-
DebugManager.error('Store', `Store.reset('${name}') : composed stores cannot be reset. Their value is derived from dependencies.`);
|
|
1230
|
-
throw new NativeDocumentError(
|
|
1231
|
-
`Store.reset('${name}') : composed stores cannot be reset.`
|
|
1232
|
-
);
|
|
1233
|
-
}
|
|
1234
|
-
if (!item.resettable) {
|
|
1235
|
-
DebugManager.error('Store', `Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`);
|
|
1236
|
-
throw new NativeDocumentError(
|
|
1237
|
-
`Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`
|
|
1238
|
-
);
|
|
1239
|
-
}
|
|
1240
|
-
item.observer.reset();
|
|
1241
|
-
},
|
|
1221
|
+
for(let i = 0, length = $listeners.length; i < length; i++) {
|
|
1222
|
+
$listeners[i]($currentValue, $previousValue, operations);
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1242
1225
|
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
* Call follower.destroy() or follower.dispose() to unsubscribe.
|
|
1248
|
-
*
|
|
1249
|
-
* @param {string} name
|
|
1250
|
-
* @returns {ObservableItem}
|
|
1251
|
-
*/
|
|
1252
|
-
use(name) {
|
|
1253
|
-
const item = $getStoreOrThrow('use', name);
|
|
1226
|
+
ObservableItem.prototype.triggerWatchers = function(operations) {
|
|
1227
|
+
const $watchers = this.$watchers;
|
|
1228
|
+
const $previousValue = this.$previousValue;
|
|
1229
|
+
const $currentValue = this.$currentValue;
|
|
1254
1230
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
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
|
+
};
|
|
1261
1240
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1241
|
+
ObservableItem.prototype.triggerAll = function(operations) {
|
|
1242
|
+
this.triggerWatchers(operations);
|
|
1243
|
+
this.triggerListeners(operations);
|
|
1244
|
+
};
|
|
1264
1245
|
|
|
1265
|
-
|
|
1266
|
-
|
|
1246
|
+
ObservableItem.prototype.triggerWatchersAndFirstListener = function(operations) {
|
|
1247
|
+
this.triggerWatchers(operations);
|
|
1248
|
+
this.triggerFirstListener(operations);
|
|
1249
|
+
};
|
|
1267
1250
|
|
|
1268
|
-
|
|
1269
|
-
|
|
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;
|
|
1270
1274
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
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
|
+
};
|
|
1278
1291
|
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
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);
|
|
1282
1298
|
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
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());
|
|
1299
|
+
if (result !== undefined) {
|
|
1300
|
+
newValue = result;
|
|
1301
|
+
}
|
|
1295
1302
|
|
|
1296
|
-
|
|
1297
|
-
|
|
1303
|
+
this.$updateWithNewValue(newValue);
|
|
1304
|
+
};
|
|
1298
1305
|
|
|
1299
|
-
|
|
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
|
+
};
|
|
1300
1313
|
|
|
1301
|
-
|
|
1302
|
-
originalObserver.unsubscribe(onStoreChange);
|
|
1303
|
-
subscribers.delete(observerFollower);
|
|
1304
|
-
observerFollower.cleanup();
|
|
1305
|
-
};
|
|
1306
|
-
observerFollower.dispose = observerFollower.destroy;
|
|
1314
|
+
ObservableItem.prototype.set = ObservableItem.prototype.$basicSet;
|
|
1307
1315
|
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1316
|
+
ObservableItem.prototype.val = function() {
|
|
1317
|
+
return this.$currentValue;
|
|
1318
|
+
};
|
|
1311
1319
|
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
*/
|
|
1320
|
-
get(name) {
|
|
1321
|
-
const item = $stores.get(name);
|
|
1322
|
-
if (!item) {
|
|
1323
|
-
DebugManager.warn('Store', `Store.get('${name}') : store not found.`);
|
|
1324
|
-
return null;
|
|
1325
|
-
}
|
|
1326
|
-
return item.observer;
|
|
1327
|
-
},
|
|
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
|
+
};
|
|
1328
1327
|
|
|
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.warn('Store', `Store.delete('${name}') : store not found, nothing to delete.`);
|
|
1346
|
-
return;
|
|
1347
|
-
}
|
|
1348
|
-
item.subscribers.forEach(follower => follower.destroy());
|
|
1349
|
-
item.subscribers.clear();
|
|
1350
|
-
item.observer.cleanup();
|
|
1351
|
-
$stores.delete(name);
|
|
1352
|
-
},
|
|
1353
|
-
/**
|
|
1354
|
-
* Creates an isolated store group with its own state namespace.
|
|
1355
|
-
* Each group is a fully independent StoreFactory instance —
|
|
1356
|
-
* no key conflicts, no shared state with the parent store.
|
|
1357
|
-
*
|
|
1358
|
-
* @param {string | ((group: ReturnType<typeof StoreFactory>) => void)} name - Group name for debugging, or setup callback if no name is provided
|
|
1359
|
-
* @param {((group: ReturnType<typeof StoreFactory>) => void)} [callback] - Setup function receiving the isolated store instance
|
|
1360
|
-
* @returns {ReturnType<typeof StoreFactory>}
|
|
1361
|
-
*
|
|
1362
|
-
* @example
|
|
1363
|
-
* // With name (recommended)
|
|
1364
|
-
* const EventStore = Store.group('events', (group) => {
|
|
1365
|
-
* group.create('catalog', []);
|
|
1366
|
-
* group.create('filters', { category: null, date: null });
|
|
1367
|
-
* group.createResettable('selected', null);
|
|
1368
|
-
* group.createComposed('filtered', () => {
|
|
1369
|
-
* const catalog = EventStore.get('catalog').val();
|
|
1370
|
-
* const filters = EventStore.get('filters').val();
|
|
1371
|
-
* return catalog.filter(event => {
|
|
1372
|
-
* if (filters.category && event.category !== filters.category) return false;
|
|
1373
|
-
* return true;
|
|
1374
|
-
* });
|
|
1375
|
-
* }, ['catalog', 'filters']);
|
|
1376
|
-
* });
|
|
1377
|
-
*
|
|
1378
|
-
* // Without name
|
|
1379
|
-
* const CartStore = Store.group((group) => {
|
|
1380
|
-
* group.create('items', []);
|
|
1381
|
-
* });
|
|
1382
|
-
*
|
|
1383
|
-
* // Usage
|
|
1384
|
-
* EventStore.use('catalog'); // two-way follower
|
|
1385
|
-
* EventStore.follow('filtered'); // read-only follower
|
|
1386
|
-
* EventStore.get('filters'); // raw observable
|
|
1387
|
-
*
|
|
1388
|
-
* // Cross-group composed
|
|
1389
|
-
* const OrderStore = Store.group('orders', (group) => {
|
|
1390
|
-
* group.createComposed('summary', () => {
|
|
1391
|
-
* const items = CartStore.get('items').val();
|
|
1392
|
-
* const events = EventStore.get('catalog').val();
|
|
1393
|
-
* return { items, events };
|
|
1394
|
-
* }, [CartStore.get('items'), EventStore.get('catalog')]);
|
|
1395
|
-
* });
|
|
1396
|
-
*/
|
|
1397
|
-
group(name, callback) {
|
|
1398
|
-
if (typeof name === 'function') {
|
|
1399
|
-
callback = name;
|
|
1400
|
-
name = 'anonymous';
|
|
1401
|
-
}
|
|
1402
|
-
const store = StoreFactory();
|
|
1403
|
-
callback && callback(store);
|
|
1404
|
-
return store;
|
|
1405
|
-
},
|
|
1406
|
-
createPersistent(name, value, localstorage_key) {
|
|
1407
|
-
localstorage_key = localstorage_key || name;
|
|
1408
|
-
const observer = this.create(name, $getFromStorage(localstorage_key, value));
|
|
1409
|
-
const saver = $saveToStorage(value);
|
|
1410
|
-
|
|
1411
|
-
observer.subscribe((val) => saver(localstorage_key, val));
|
|
1412
|
-
return observer;
|
|
1413
|
-
},
|
|
1414
|
-
createPersistentResettable(name, value, localstorage_key) {
|
|
1415
|
-
localstorage_key = localstorage_key || name;
|
|
1416
|
-
const observer = this.createResettable(name, $getFromStorage(localstorage_key, value));
|
|
1417
|
-
const saver = $saveToStorage(value);
|
|
1418
|
-
observer.subscribe((val) => saver(localstorage_key, val));
|
|
1419
|
-
|
|
1420
|
-
const originalReset = observer.reset.bind(observer);
|
|
1421
|
-
observer.reset = () => {
|
|
1422
|
-
LocalStorage.remove(localstorage_key);
|
|
1423
|
-
originalReset();
|
|
1424
|
-
};
|
|
1425
|
-
|
|
1426
|
-
return observer;
|
|
1427
|
-
}
|
|
1428
|
-
};
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
return new Proxy($api, {
|
|
1432
|
-
get(target, prop) {
|
|
1433
|
-
if (typeof prop === 'symbol' || prop.startsWith('$') || prop in target) {
|
|
1434
|
-
return target[prop];
|
|
1435
|
-
}
|
|
1436
|
-
if (target.has(prop)) {
|
|
1437
|
-
if ($followersCache.has(prop)) {
|
|
1438
|
-
return $followersCache.get(prop);
|
|
1439
|
-
}
|
|
1440
|
-
const follower = target.follow(prop);
|
|
1441
|
-
$followersCache.set(prop, follower);
|
|
1442
|
-
return follower;
|
|
1443
|
-
}
|
|
1444
|
-
return undefined;
|
|
1445
|
-
},
|
|
1446
|
-
set(target, prop, value) {
|
|
1447
|
-
DebugManager.error('Store', `Forbidden: You cannot overwrite the store key '${String(prop)}'. Use .use('${String(prop)}').set(value) instead.`);
|
|
1448
|
-
throw new NativeDocumentError(`Store structure is immutable. Use .set() on the observable.`);
|
|
1449
|
-
},
|
|
1450
|
-
deleteProperty(target, prop) {
|
|
1451
|
-
throw new NativeDocumentError(`Store keys cannot be deleted.`);
|
|
1452
|
-
}
|
|
1453
|
-
});
|
|
1454
|
-
};
|
|
1455
|
-
|
|
1456
|
-
const Store = StoreFactory();
|
|
1457
|
-
|
|
1458
|
-
Store.create('locale', navigator.language.split('-')[0] || 'en');
|
|
1459
|
-
|
|
1460
|
-
const $parseDateParts = (value, locale) => {
|
|
1461
|
-
const d = new Date(value);
|
|
1462
|
-
return {
|
|
1463
|
-
d,
|
|
1464
|
-
parts: new Intl.DateTimeFormat(locale, {
|
|
1465
|
-
year: 'numeric',
|
|
1466
|
-
month: 'long',
|
|
1467
|
-
day: '2-digit',
|
|
1468
|
-
hour: '2-digit',
|
|
1469
|
-
minute: '2-digit',
|
|
1470
|
-
second: '2-digit',
|
|
1471
|
-
}).formatToParts(d).reduce((acc, { type, value }) => {
|
|
1472
|
-
acc[type] = value;
|
|
1473
|
-
return acc;
|
|
1474
|
-
}, {})
|
|
1475
|
-
};
|
|
1476
|
-
};
|
|
1477
|
-
|
|
1478
|
-
const $applyDatePattern = (pattern, d, parts) => {
|
|
1479
|
-
const pad = n => String(n).padStart(2, '0');
|
|
1480
|
-
return pattern
|
|
1481
|
-
.replace('YYYY', parts.year)
|
|
1482
|
-
.replace('YY', parts.year.slice(-2))
|
|
1483
|
-
.replace('MMMM', parts.month)
|
|
1484
|
-
.replace('MMM', parts.month.slice(0, 3))
|
|
1485
|
-
.replace('MM', pad(d.getMonth() + 1))
|
|
1486
|
-
.replace('DD', pad(d.getDate()))
|
|
1487
|
-
.replace('D', d.getDate())
|
|
1488
|
-
.replace('HH', parts.hour)
|
|
1489
|
-
.replace('mm', parts.minute)
|
|
1490
|
-
.replace('ss', parts.second);
|
|
1491
|
-
};
|
|
1492
|
-
|
|
1493
|
-
const Formatters = {
|
|
1494
|
-
currency: (value, locale, { currency = 'XOF', notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
|
|
1495
|
-
new Intl.NumberFormat(locale, {
|
|
1496
|
-
style: 'currency',
|
|
1497
|
-
currency,
|
|
1498
|
-
notation,
|
|
1499
|
-
minimumFractionDigits,
|
|
1500
|
-
maximumFractionDigits
|
|
1501
|
-
}).format(value),
|
|
1502
|
-
|
|
1503
|
-
number: (value, locale, { notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
|
|
1504
|
-
new Intl.NumberFormat(locale, {
|
|
1505
|
-
notation,
|
|
1506
|
-
minimumFractionDigits,
|
|
1507
|
-
maximumFractionDigits
|
|
1508
|
-
}).format(value),
|
|
1509
|
-
|
|
1510
|
-
percent: (value, locale, { decimals = 1 } = {}) =>
|
|
1511
|
-
new Intl.NumberFormat(locale, {
|
|
1512
|
-
style: 'percent',
|
|
1513
|
-
maximumFractionDigits: decimals
|
|
1514
|
-
}).format(value),
|
|
1515
|
-
|
|
1516
|
-
date: (value, locale, { format, dateStyle = 'long' } = {}) => {
|
|
1517
|
-
if (format) {
|
|
1518
|
-
const { d, parts } = $parseDateParts(value, locale);
|
|
1519
|
-
return $applyDatePattern(format, d, parts);
|
|
1520
|
-
}
|
|
1521
|
-
return new Intl.DateTimeFormat(locale, { dateStyle }).format(new Date(value));
|
|
1522
|
-
},
|
|
1523
|
-
|
|
1524
|
-
time: (value, locale, { format, hour = '2-digit', minute = '2-digit', second } = {}) => {
|
|
1525
|
-
if (format) {
|
|
1526
|
-
const { d, parts } = $parseDateParts(value, locale);
|
|
1527
|
-
return $applyDatePattern(format, d, parts);
|
|
1528
|
-
}
|
|
1529
|
-
return new Intl.DateTimeFormat(locale, { hour, minute, second }).format(new Date(value));
|
|
1530
|
-
},
|
|
1531
|
-
|
|
1532
|
-
datetime: (value, locale, { format, dateStyle = 'long', hour = '2-digit', minute = '2-digit', second } = {}) => {
|
|
1533
|
-
if (format) {
|
|
1534
|
-
const { d, parts } = $parseDateParts(value, locale);
|
|
1535
|
-
return $applyDatePattern(format, d, parts);
|
|
1536
|
-
}
|
|
1537
|
-
return new Intl.DateTimeFormat(locale, { dateStyle, hour, minute, second }).format(new Date(value));
|
|
1538
|
-
},
|
|
1539
|
-
|
|
1540
|
-
relative: (value, locale, { unit = 'day', numeric = 'auto' } = {}) => {
|
|
1541
|
-
const diff = Math.round((value - Date.now()) / (1000 * 60 * 60 * 24));
|
|
1542
|
-
return new Intl.RelativeTimeFormat(locale, { numeric }).format(diff, unit);
|
|
1543
|
-
},
|
|
1544
|
-
|
|
1545
|
-
plural: (value, locale, { singular, plural } = {}) => {
|
|
1546
|
-
const rule = new Intl.PluralRules(locale).select(value);
|
|
1547
|
-
return `${value} ${rule === 'one' ? singular : plural}`;
|
|
1548
|
-
},
|
|
1549
|
-
};
|
|
1550
|
-
|
|
1551
|
-
/**
|
|
1552
|
-
*
|
|
1553
|
-
* @param {*} value
|
|
1554
|
-
* @param {{ propagation: boolean, reset: boolean} | null} configs
|
|
1555
|
-
* @class ObservableItem
|
|
1556
|
-
*/
|
|
1557
|
-
function ObservableItem(value, configs = null) {
|
|
1558
|
-
value = Validator.isObservable(value) ? value.val() : value;
|
|
1559
|
-
|
|
1560
|
-
this.$previousValue = null;
|
|
1561
|
-
this.$currentValue = value;
|
|
1562
|
-
{
|
|
1563
|
-
this.$isCleanedUp = false;
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
this.$firstListener = null;
|
|
1567
|
-
this.$listeners = null;
|
|
1568
|
-
this.$watchers = null;
|
|
1569
|
-
|
|
1570
|
-
this.$memoryId = null;
|
|
1571
|
-
|
|
1572
|
-
if(configs) {
|
|
1573
|
-
this.configs = configs;
|
|
1574
|
-
if(configs.reset) {
|
|
1575
|
-
this.$initialValue = Validator.isObject(value) ? deepClone(value) : value;
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1578
|
-
{
|
|
1579
|
-
PluginsManager.emit('CreateObservable', this);
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
Object.defineProperty(ObservableItem.prototype, '$value', {
|
|
1584
|
-
get() {
|
|
1585
|
-
return this.$currentValue;
|
|
1586
|
-
},
|
|
1587
|
-
set(value) {
|
|
1588
|
-
this.set(value);
|
|
1589
|
-
},
|
|
1590
|
-
configurable: true,
|
|
1591
|
-
});
|
|
1592
|
-
|
|
1593
|
-
ObservableItem.prototype.__$isObservable = true;
|
|
1594
|
-
const noneTrigger = function() {};
|
|
1595
|
-
|
|
1596
|
-
/**
|
|
1597
|
-
* Intercepts and transforms values before they are set on the observable.
|
|
1598
|
-
* The interceptor can modify the value or return undefined to use the original value.
|
|
1599
|
-
*
|
|
1600
|
-
* @param {(value) => any} callback - Interceptor function that receives (newValue, currentValue) and returns the transformed value or undefined
|
|
1601
|
-
* @returns {ObservableItem} The observable instance for chaining
|
|
1602
|
-
* @example
|
|
1603
|
-
* const count = Observable(0);
|
|
1604
|
-
* count.intercept((newVal, oldVal) => Math.max(0, newVal)); // Prevent negative values
|
|
1605
|
-
*/
|
|
1606
|
-
ObservableItem.prototype.intercept = function(callback) {
|
|
1607
|
-
this.$interceptor = callback;
|
|
1608
|
-
this.set = this.$setWithInterceptor;
|
|
1609
|
-
return this;
|
|
1610
|
-
};
|
|
1611
|
-
|
|
1612
|
-
ObservableItem.prototype.triggerFirstListener = function(operations) {
|
|
1613
|
-
this.$firstListener(this.$currentValue, this.$previousValue, operations);
|
|
1614
|
-
};
|
|
1615
|
-
|
|
1616
|
-
ObservableItem.prototype.triggerListeners = function(operations) {
|
|
1617
|
-
const $listeners = this.$listeners;
|
|
1618
|
-
const $previousValue = this.$previousValue;
|
|
1619
|
-
const $currentValue = this.$currentValue;
|
|
1620
|
-
|
|
1621
|
-
for(let i = 0, length = $listeners.length; i < length; i++) {
|
|
1622
|
-
$listeners[i]($currentValue, $previousValue, operations);
|
|
1623
|
-
}
|
|
1624
|
-
};
|
|
1625
|
-
|
|
1626
|
-
ObservableItem.prototype.triggerWatchers = function(operations) {
|
|
1627
|
-
const $watchers = this.$watchers;
|
|
1628
|
-
const $previousValue = this.$previousValue;
|
|
1629
|
-
const $currentValue = this.$currentValue;
|
|
1630
|
-
|
|
1631
|
-
const $currentValueCallbacks = $watchers.get($currentValue);
|
|
1632
|
-
const $previousValueCallbacks = $watchers.get($previousValue);
|
|
1633
|
-
if($currentValueCallbacks) {
|
|
1634
|
-
$currentValueCallbacks(true, $previousValue, operations);
|
|
1635
|
-
}
|
|
1636
|
-
if($previousValueCallbacks) {
|
|
1637
|
-
$previousValueCallbacks(false, $currentValue, operations);
|
|
1638
|
-
}
|
|
1639
|
-
};
|
|
1640
|
-
|
|
1641
|
-
ObservableItem.prototype.triggerAll = function(operations) {
|
|
1642
|
-
this.triggerWatchers(operations);
|
|
1643
|
-
this.triggerListeners(operations);
|
|
1644
|
-
};
|
|
1645
|
-
|
|
1646
|
-
ObservableItem.prototype.triggerWatchersAndFirstListener = function(operations) {
|
|
1647
|
-
this.triggerWatchers(operations);
|
|
1648
|
-
this.triggerFirstListener(operations);
|
|
1649
|
-
};
|
|
1650
|
-
|
|
1651
|
-
ObservableItem.prototype.assocTrigger = function() {
|
|
1652
|
-
this.$firstListener = null;
|
|
1653
|
-
if(this.$watchers?.size && this.$listeners?.length) {
|
|
1654
|
-
this.trigger = (this.$listeners.length === 1) ? this.triggerWatchersAndFirstListener : this.triggerAll;
|
|
1655
|
-
return;
|
|
1656
|
-
}
|
|
1657
|
-
if(this.$listeners?.length) {
|
|
1658
|
-
if(this.$listeners.length === 1) {
|
|
1659
|
-
this.$firstListener = this.$listeners[0];
|
|
1660
|
-
this.trigger = this.triggerFirstListener;
|
|
1661
|
-
}
|
|
1662
|
-
else {
|
|
1663
|
-
this.trigger = this.triggerListeners;
|
|
1664
|
-
}
|
|
1665
|
-
return;
|
|
1666
|
-
}
|
|
1667
|
-
if(this.$watchers?.size) {
|
|
1668
|
-
this.trigger = this.triggerWatchers;
|
|
1669
|
-
return;
|
|
1670
|
-
}
|
|
1671
|
-
this.trigger = noneTrigger;
|
|
1672
|
-
};
|
|
1673
|
-
ObservableItem.prototype.trigger = noneTrigger;
|
|
1674
|
-
|
|
1675
|
-
ObservableItem.prototype.$updateWithNewValue = function(newValue) {
|
|
1676
|
-
newValue = newValue?.__$isObservable ? newValue.val() : newValue;
|
|
1677
|
-
if(this.$currentValue === newValue) {
|
|
1678
|
-
return;
|
|
1679
|
-
}
|
|
1680
|
-
this.$previousValue = this.$currentValue;
|
|
1681
|
-
this.$currentValue = newValue;
|
|
1682
|
-
{
|
|
1683
|
-
PluginsManager.emit('ObservableBeforeChange', this);
|
|
1684
|
-
}
|
|
1685
|
-
this.trigger();
|
|
1686
|
-
this.$previousValue = null;
|
|
1687
|
-
{
|
|
1688
|
-
PluginsManager.emit('ObservableAfterChange', this);
|
|
1689
|
-
}
|
|
1690
|
-
};
|
|
1691
|
-
|
|
1692
|
-
/**
|
|
1693
|
-
* @param {*} data
|
|
1694
|
-
*/
|
|
1695
|
-
ObservableItem.prototype.$setWithInterceptor = function(data) {
|
|
1696
|
-
let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
|
|
1697
|
-
const result = this.$interceptor(newValue, this.$currentValue);
|
|
1698
|
-
|
|
1699
|
-
if (result !== undefined) {
|
|
1700
|
-
newValue = result;
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
this.$updateWithNewValue(newValue);
|
|
1704
|
-
};
|
|
1705
|
-
|
|
1706
|
-
/**
|
|
1707
|
-
* @param {*} data
|
|
1708
|
-
*/
|
|
1709
|
-
ObservableItem.prototype.$basicSet = function(data) {
|
|
1710
|
-
let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
|
|
1711
|
-
this.$updateWithNewValue(newValue);
|
|
1712
|
-
};
|
|
1713
|
-
|
|
1714
|
-
ObservableItem.prototype.set = ObservableItem.prototype.$basicSet;
|
|
1715
|
-
|
|
1716
|
-
ObservableItem.prototype.val = function() {
|
|
1717
|
-
return this.$currentValue;
|
|
1718
|
-
};
|
|
1719
|
-
|
|
1720
|
-
ObservableItem.prototype.disconnectAll = function() {
|
|
1721
|
-
this.$previousValue = null;
|
|
1722
|
-
this.$currentValue = null;
|
|
1723
|
-
this.$listeners = null;
|
|
1724
|
-
this.$watchers = null;
|
|
1725
|
-
this.trigger = noneTrigger;
|
|
1726
|
-
};
|
|
1727
|
-
|
|
1728
|
-
/**
|
|
1729
|
-
* Registers a cleanup callback that will be executed when the observable is cleaned up.
|
|
1730
|
-
* Useful for disposing resources, removing event listeners, or other cleanup tasks.
|
|
1731
|
-
*
|
|
1732
|
-
* @param {Function} callback - Cleanup function to execute on observable disposal
|
|
1733
|
-
* @example
|
|
1734
|
-
* const obs = Observable(0);
|
|
1735
|
-
* obs.onCleanup(() => console.log('Cleaned up!'));
|
|
1736
|
-
* obs.cleanup(); // Logs: "Cleaned up!"
|
|
1737
|
-
*/
|
|
1738
|
-
ObservableItem.prototype.onCleanup = function(callback) {
|
|
1739
|
-
this.$cleanupListeners = this.$cleanupListeners ?? [];
|
|
1740
|
-
this.$cleanupListeners.push(callback);
|
|
1741
|
-
};
|
|
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
|
+
};
|
|
1742
1342
|
|
|
1743
1343
|
ObservableItem.prototype.cleanup = function() {
|
|
1744
1344
|
if (this.$cleanupListeners) {
|
|
@@ -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
|
/**
|
|
@@ -3267,29 +2866,71 @@ 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;
|
|
@@ -3379,8 +3020,7 @@ var NativeDocument = (function (exports) {
|
|
|
3379
3020
|
$node.dynamicCloneNode = (data) => {
|
|
3380
3021
|
const clonedNode = $node.nodeCloner.cloneNode(data);
|
|
3381
3022
|
for(let i = 0; i < childNodesLength; i++) {
|
|
3382
|
-
|
|
3383
|
-
clonedNode.appendChild(child);
|
|
3023
|
+
clonedNode.appendChild(childNodes[i].dynamicCloneNode(data));
|
|
3384
3024
|
}
|
|
3385
3025
|
return clonedNode;
|
|
3386
3026
|
};
|
|
@@ -3388,8 +3028,7 @@ var NativeDocument = (function (exports) {
|
|
|
3388
3028
|
$node.dynamicCloneNode = (data) => {
|
|
3389
3029
|
const clonedNode = $node.cloneNode();
|
|
3390
3030
|
for(let i = 0; i < childNodesLength; i++) {
|
|
3391
|
-
|
|
3392
|
-
clonedNode.appendChild(child);
|
|
3031
|
+
clonedNode.appendChild(childNodes[i].dynamicCloneNode(data));
|
|
3393
3032
|
}
|
|
3394
3033
|
return clonedNode;
|
|
3395
3034
|
};
|
|
@@ -4221,491 +3860,891 @@ var NativeDocument = (function (exports) {
|
|
|
4221
3860
|
};
|
|
4222
3861
|
|
|
4223
3862
|
/**
|
|
4224
|
-
* Triggers a populate operation with the current array, iteration count, and callback.
|
|
4225
|
-
* Used internally for rendering optimizations.
|
|
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
|
|
4053
|
+
* @example
|
|
4054
|
+
* const items = Observable.array([1, 2, 3]);
|
|
4055
|
+
* items.push(4); // Triggers update
|
|
4056
|
+
* items.subscribe((arr) => console.log(arr));
|
|
4057
|
+
*/
|
|
4058
|
+
Observable.array = function(target = [], configs = null) {
|
|
4059
|
+
return new ObservableArray(target, configs);
|
|
4060
|
+
};
|
|
4061
|
+
|
|
4062
|
+
/**
|
|
4226
4063
|
*
|
|
4227
|
-
* @param {
|
|
4228
|
-
* @
|
|
4064
|
+
* @param {Function} callback
|
|
4065
|
+
* @returns {Function}
|
|
4229
4066
|
*/
|
|
4230
|
-
|
|
4231
|
-
|
|
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;
|
|
4232
4080
|
};
|
|
4233
4081
|
|
|
4082
|
+
const ObservableObject = function(target, configs) {
|
|
4083
|
+
ObservableItem.call(this, target);
|
|
4084
|
+
this.$observables = {};
|
|
4085
|
+
this.configs = configs;
|
|
4234
4086
|
|
|
4235
|
-
|
|
4236
|
-
* Creates a filtered view of the array based on predicates.
|
|
4237
|
-
* The filtered array updates automatically when source data or predicates change.
|
|
4238
|
-
*
|
|
4239
|
-
* @param {Object} predicates - Object mapping property names to filter conditions or functions
|
|
4240
|
-
* @returns {ObservableArray} A new observable array containing filtered items
|
|
4241
|
-
* @example
|
|
4242
|
-
* const users = Observable.array([
|
|
4243
|
-
* { name: 'John', age: 25 },
|
|
4244
|
-
* { name: 'Jane', age: 30 }
|
|
4245
|
-
* ]);
|
|
4246
|
-
* const adults = users.where({ age: (val) => val >= 18 });
|
|
4247
|
-
*/
|
|
4248
|
-
ObservableArray.prototype.where = function(predicates) {
|
|
4249
|
-
const sourceArray = this;
|
|
4250
|
-
const observableDependencies = [sourceArray];
|
|
4251
|
-
const filterCallbacks = {};
|
|
4087
|
+
this.$load(target);
|
|
4252
4088
|
|
|
4253
|
-
for
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
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
|
+
}
|
|
4257
4097
|
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
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;
|
|
4263
4131
|
}
|
|
4264
|
-
|
|
4265
|
-
|
|
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;
|
|
4266
4163
|
} else {
|
|
4267
|
-
|
|
4164
|
+
result[key] = dataItem;
|
|
4268
4165
|
}
|
|
4269
4166
|
}
|
|
4167
|
+
return result;
|
|
4168
|
+
};
|
|
4169
|
+
ObservableObject.prototype.$val = ObservableObject.prototype.val;
|
|
4270
4170
|
|
|
4271
|
-
|
|
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;
|
|
4272
4182
|
|
|
4273
|
-
|
|
4274
|
-
const
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
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;
|
|
4282
4196
|
}
|
|
4283
|
-
|
|
4284
|
-
|
|
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;
|
|
4285
4220
|
|
|
4286
|
-
|
|
4287
|
-
|
|
4221
|
+
ObservableObject.prototype.observables = function() {
|
|
4222
|
+
return Object.values(this.$observables);
|
|
4223
|
+
};
|
|
4224
|
+
ObservableObject.prototype.$observables = ObservableObject.prototype.observables;
|
|
4288
4225
|
|
|
4289
|
-
|
|
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());
|
|
4290
4243
|
|
|
4291
|
-
|
|
4244
|
+
this.originalSubscribe(callback);
|
|
4292
4245
|
|
|
4293
|
-
|
|
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)
|
|
4294
4263
|
};
|
|
4295
4264
|
|
|
4296
4265
|
/**
|
|
4297
|
-
*
|
|
4298
|
-
*
|
|
4299
|
-
* @
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
* const products = Observable.array([
|
|
4304
|
-
* { name: 'Apple', category: 'Fruit' },
|
|
4305
|
-
* { name: 'Carrot', category: 'Vegetable' }
|
|
4306
|
-
* ]);
|
|
4307
|
-
* const searchTerm = Observable('App');
|
|
4308
|
-
* const filtered = products.whereSome(['name', 'category'], match(searchTerm));
|
|
4309
|
-
*/
|
|
4310
|
-
ObservableArray.prototype.whereSome = function(fields, filter) {
|
|
4311
|
-
return this.where({
|
|
4312
|
-
_: {
|
|
4313
|
-
dependencies: filter.dependencies,
|
|
4314
|
-
callback: (item) => fields.some(field => filter.callback(item[field]))
|
|
4315
|
-
}
|
|
4316
|
-
});
|
|
4266
|
+
*
|
|
4267
|
+
* @param {any[]} data
|
|
4268
|
+
* @return Proxy[]
|
|
4269
|
+
*/
|
|
4270
|
+
Observable.arrayOfObject = function(data) {
|
|
4271
|
+
return data.map(item => Observable.object(item));
|
|
4317
4272
|
};
|
|
4318
4273
|
|
|
4319
4274
|
/**
|
|
4320
|
-
*
|
|
4321
|
-
*
|
|
4322
|
-
* @
|
|
4323
|
-
* @param {FilterResult} filter - Filter condition with callback and dependencies
|
|
4324
|
-
* @returns {ObservableArray} A new observable array containing filtered items
|
|
4325
|
-
* @example
|
|
4326
|
-
* const items = Observable.array([
|
|
4327
|
-
* { status: 'active', verified: true },
|
|
4328
|
-
* { status: 'active', verified: false }
|
|
4329
|
-
* ]);
|
|
4330
|
-
* const activeFilter = equals('active');
|
|
4331
|
-
* const filtered = items.whereEvery(['status', 'verified'], activeFilter);
|
|
4275
|
+
* Get the value of an observable or an object of observables.
|
|
4276
|
+
* @param {ObservableItem|Object<ObservableItem>} data
|
|
4277
|
+
* @returns {{}|*|null}
|
|
4332
4278
|
*/
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
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));
|
|
4338
4291
|
}
|
|
4339
|
-
|
|
4292
|
+
return result;
|
|
4293
|
+
}
|
|
4294
|
+
return data;
|
|
4340
4295
|
};
|
|
4341
4296
|
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
const $listeners = new WeakMap();
|
|
4297
|
+
Observable.object = Observable.init;
|
|
4298
|
+
Observable.json = Observable.init;
|
|
4345
4299
|
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4300
|
+
/**
|
|
4301
|
+
* Creates a computed observable that automatically updates when its dependencies change.
|
|
4302
|
+
* The callback is re-executed whenever any dependency observable changes.
|
|
4303
|
+
*
|
|
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
|
|
4307
|
+
* @example
|
|
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
|
+
}
|
|
4359
4326
|
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
unsub();
|
|
4364
|
-
$listeners.delete(item);
|
|
4327
|
+
if(Validator.isFunction(dependencies)) {
|
|
4328
|
+
if(!Validator.isObservable(dependencies.$observer)) {
|
|
4329
|
+
throw new NativeDocumentError('Observable.computed : dependencies must be valid batch function');
|
|
4365
4330
|
}
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
this.subscribe(updatedValue);
|
|
4331
|
+
dependencies.$observer.subscribe(updatedValue);
|
|
4332
|
+
return observable;
|
|
4333
|
+
}
|
|
4370
4334
|
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4335
|
+
dependencies.forEach(dependency => {
|
|
4336
|
+
if(Validator.isProxy(dependency)) {
|
|
4337
|
+
dependency.$observables.forEach((observable) => {
|
|
4338
|
+
observable.subscribe(updatedValue);
|
|
4339
|
+
});
|
|
4340
|
+
return;
|
|
4341
|
+
}
|
|
4342
|
+
dependency.subscribe(updatedValue);
|
|
4343
|
+
});
|
|
4377
4344
|
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
operations.result?.forEach(unbindItem);
|
|
4381
|
-
newItems.forEach(bindItem);
|
|
4382
|
-
break;
|
|
4383
|
-
}
|
|
4345
|
+
return observable;
|
|
4346
|
+
};
|
|
4384
4347
|
|
|
4385
|
-
|
|
4386
|
-
unbindItem(operations.result);
|
|
4387
|
-
break;
|
|
4348
|
+
const StoreFactory = function() {
|
|
4388
4349
|
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
break;
|
|
4350
|
+
const $stores = new Map();
|
|
4351
|
+
const $followersCache = new Map();
|
|
4392
4352
|
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
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
|
+
);
|
|
4396
4363
|
}
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
return () => {
|
|
4400
|
-
this.$currentValue.forEach(unbindItem);
|
|
4364
|
+
return item;
|
|
4401
4365
|
};
|
|
4402
|
-
};
|
|
4403
4366
|
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
*/
|
|
4419
|
-
Observable.array = function(target = [], configs = null) {
|
|
4420
|
-
return new ObservableArray(target, configs);
|
|
4421
|
-
};
|
|
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
|
+
};
|
|
4422
4381
|
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
* @returns {Function}
|
|
4427
|
-
*/
|
|
4428
|
-
Observable.batch = function(callback) {
|
|
4429
|
-
const $observer = Observable(0);
|
|
4430
|
-
const batch = function() {
|
|
4431
|
-
if(Validator.isAsyncFunction(callback)) {
|
|
4432
|
-
return (callback(...arguments)).then(() => {
|
|
4433
|
-
$observer.trigger();
|
|
4434
|
-
}).catch(error => { throw error; });
|
|
4382
|
+
const $createObservable = (value, options = {}) => {
|
|
4383
|
+
if(Array.isArray(value)) {
|
|
4384
|
+
return Observable.array(value, options);
|
|
4435
4385
|
}
|
|
4436
|
-
|
|
4437
|
-
|
|
4386
|
+
if(typeof value === 'object') {
|
|
4387
|
+
return Observable.object(value, options);
|
|
4388
|
+
}
|
|
4389
|
+
return Observable(value, options);
|
|
4438
4390
|
};
|
|
4439
|
-
batch.$observer = $observer;
|
|
4440
|
-
return batch;
|
|
4441
|
-
};
|
|
4442
4391
|
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
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
|
+
},
|
|
4447
4412
|
|
|
4448
|
-
|
|
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
|
+
},
|
|
4433
|
+
|
|
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
|
+
}
|
|
4449
4475
|
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
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;
|
|
4455
4489
|
});
|
|
4456
|
-
}
|
|
4457
|
-
}
|
|
4458
|
-
|
|
4459
|
-
};
|
|
4460
4490
|
|
|
4461
|
-
|
|
4491
|
+
// Create computed observable from dependency observers
|
|
4492
|
+
const observer = Observable.computed(computation, depObservers);
|
|
4462
4493
|
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
},
|
|
4467
|
-
set(value) {
|
|
4468
|
-
this.set(value);
|
|
4469
|
-
}
|
|
4470
|
-
});
|
|
4494
|
+
$stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: true });
|
|
4495
|
+
return observer;
|
|
4496
|
+
},
|
|
4471
4497
|
|
|
4472
|
-
|
|
4473
|
-
|
|
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
|
+
},
|
|
4474
4507
|
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
return Observable(item, configs);
|
|
4489
|
-
});
|
|
4490
|
-
this.$observables[key] = Observable.array(mappedItemValue, configs);
|
|
4491
|
-
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
|
+
);
|
|
4492
4521
|
}
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
}
|
|
4502
|
-
};
|
|
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
|
+
},
|
|
4503
4530
|
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
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
|
+
);
|
|
4520
4548
|
}
|
|
4521
|
-
result[key] = value;
|
|
4522
|
-
} else if(Validator.isProxy(dataItem)) {
|
|
4523
|
-
result[key] = dataItem.$value;
|
|
4524
|
-
} else {
|
|
4525
|
-
result[key] = dataItem;
|
|
4526
|
-
}
|
|
4527
|
-
}
|
|
4528
|
-
return result;
|
|
4529
|
-
};
|
|
4530
|
-
ObservableObject.prototype.$val = ObservableObject.prototype.val;
|
|
4531
4549
|
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
if(Validator.isObservable(item)) {
|
|
4535
|
-
return item.val();
|
|
4536
|
-
}
|
|
4537
|
-
if(Validator.isProxy(item)) {
|
|
4538
|
-
return item.$value;
|
|
4539
|
-
}
|
|
4540
|
-
return item;
|
|
4541
|
-
};
|
|
4542
|
-
ObservableObject.prototype.$get = ObservableObject.prototype.get;
|
|
4550
|
+
const { observer: originalObserver, subscribers } = item;
|
|
4551
|
+
const observerFollower = $createObservable(originalObserver.val());
|
|
4543
4552
|
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
const configs = this.configs;
|
|
4553
|
+
const onStoreChange = value => observerFollower.set(value);
|
|
4554
|
+
const onFollowerChange = value => originalObserver.set(value);
|
|
4547
4555
|
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
const newValueOrigin = newData[key];
|
|
4551
|
-
const newValue = data[key];
|
|
4556
|
+
originalObserver.subscribe(onStoreChange);
|
|
4557
|
+
observerFollower.subscribe(onFollowerChange);
|
|
4552
4558
|
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
const newValues = newValue.map(item => {
|
|
4561
|
-
if(Validator.isProxy(firstElementFromOriginalValue)) {
|
|
4562
|
-
return Observable.init(item, configs);
|
|
4563
|
-
}
|
|
4564
|
-
return Observable(item, configs);
|
|
4565
|
-
});
|
|
4566
|
-
targetItem.set(newValues);
|
|
4567
|
-
continue;
|
|
4568
|
-
}
|
|
4569
|
-
targetItem.set([...newValue]);
|
|
4570
|
-
continue;
|
|
4571
|
-
}
|
|
4572
|
-
if(Validator.isProxy(targetItem)) {
|
|
4573
|
-
targetItem.update(newValue);
|
|
4574
|
-
continue;
|
|
4575
|
-
}
|
|
4576
|
-
this[key] = newValue;
|
|
4577
|
-
}
|
|
4578
|
-
};
|
|
4579
|
-
ObservableObject.prototype.$set = ObservableObject.prototype.set;
|
|
4580
|
-
ObservableObject.prototype.$updateWith = ObservableObject.prototype.set;
|
|
4559
|
+
observerFollower.destroy = () => {
|
|
4560
|
+
originalObserver.unsubscribe(onStoreChange);
|
|
4561
|
+
observerFollower.unsubscribe(onFollowerChange);
|
|
4562
|
+
subscribers.delete(observerFollower);
|
|
4563
|
+
observerFollower.cleanup();
|
|
4564
|
+
};
|
|
4565
|
+
observerFollower.dispose = observerFollower.destroy;
|
|
4581
4566
|
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
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());
|
|
4586
4583
|
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
};
|
|
4590
|
-
ObservableObject.prototype.$keys = ObservableObject.prototype.keys;
|
|
4591
|
-
ObservableObject.prototype.clone = function() {
|
|
4592
|
-
return Observable.init(this.val(), this.configs);
|
|
4593
|
-
};
|
|
4594
|
-
ObservableObject.prototype.$clone = ObservableObject.prototype.clone;
|
|
4595
|
-
ObservableObject.prototype.reset = function() {
|
|
4596
|
-
for(const key in this.$observables) {
|
|
4597
|
-
this.$observables[key].reset();
|
|
4598
|
-
}
|
|
4599
|
-
};
|
|
4600
|
-
ObservableObject.prototype.originalSubscribe = ObservableObject.prototype.subscribe;
|
|
4601
|
-
ObservableObject.prototype.subscribe = function(callback) {
|
|
4602
|
-
const observables = this.observables();
|
|
4603
|
-
const updatedValue = nextTick(() => this.trigger());
|
|
4584
|
+
const onStoreChange = value => observerFollower.set(value);
|
|
4585
|
+
originalObserver.subscribe(onStoreChange);
|
|
4604
4586
|
|
|
4605
|
-
|
|
4587
|
+
$applyReadOnly(observerFollower, name, 'follow');
|
|
4606
4588
|
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
observable.subscribe(updatedValue);
|
|
4614
|
-
}
|
|
4615
|
-
};
|
|
4616
|
-
ObservableObject.prototype.configs = function() {
|
|
4617
|
-
return this.configs;
|
|
4618
|
-
};
|
|
4589
|
+
observerFollower.destroy = () => {
|
|
4590
|
+
originalObserver.unsubscribe(onStoreChange);
|
|
4591
|
+
subscribers.delete(observerFollower);
|
|
4592
|
+
observerFollower.cleanup();
|
|
4593
|
+
};
|
|
4594
|
+
observerFollower.dispose = observerFollower.destroy;
|
|
4619
4595
|
|
|
4620
|
-
|
|
4596
|
+
subscribers.add(observerFollower);
|
|
4597
|
+
return observerFollower;
|
|
4598
|
+
},
|
|
4621
4599
|
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
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
|
+
},
|
|
4625
4616
|
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
};
|
|
4617
|
+
/**
|
|
4618
|
+
* @param {string} name
|
|
4619
|
+
* @returns {{ observer: ObservableItem, subscribers: Set } | null}
|
|
4620
|
+
*/
|
|
4621
|
+
getWithSubscribers(name) {
|
|
4622
|
+
return $stores.get(name) ?? null;
|
|
4623
|
+
},
|
|
4634
4624
|
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
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);
|
|
4657
4698
|
|
|
4658
|
-
|
|
4659
|
-
|
|
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));
|
|
4660
4707
|
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
* @param {Array<ObservableItem|ObservableChecker|ObservableProxy>|Function} [dependencies=[]] - Array of observables to watch, or batch function
|
|
4667
|
-
* @returns {ObservableItem} A new observable that updates automatically
|
|
4668
|
-
* @example
|
|
4669
|
-
* const firstName = Observable('John');
|
|
4670
|
-
* const lastName = Observable('Doe');
|
|
4671
|
-
* const fullName = Observable.computed(
|
|
4672
|
-
* () => `${firstName.val()} ${lastName.val()}`,
|
|
4673
|
-
* [firstName, lastName]
|
|
4674
|
-
* );
|
|
4675
|
-
*
|
|
4676
|
-
* // With batch function
|
|
4677
|
-
* const batch = Observable.batch(() => { ... });
|
|
4678
|
-
* const computed = Observable.computed(() => { ... }, batch);
|
|
4679
|
-
*/
|
|
4680
|
-
Observable.computed = function(callback, dependencies = []) {
|
|
4681
|
-
const initialValue = callback();
|
|
4682
|
-
const observable = new ObservableItem(initialValue);
|
|
4683
|
-
const updatedValue = nextTick(() => observable.set(callback()));
|
|
4684
|
-
{
|
|
4685
|
-
PluginsManager.emit('CreateObservableComputed', observable, dependencies);
|
|
4686
|
-
}
|
|
4708
|
+
const originalReset = observer.reset.bind(observer);
|
|
4709
|
+
observer.reset = () => {
|
|
4710
|
+
LocalStorage.remove(localstorage_key);
|
|
4711
|
+
originalReset();
|
|
4712
|
+
};
|
|
4687
4713
|
|
|
4688
|
-
|
|
4689
|
-
if(!Validator.isObservable(dependencies.$observer)) {
|
|
4690
|
-
throw new NativeDocumentError('Observable.computed : dependencies must be valid batch function');
|
|
4714
|
+
return observer;
|
|
4691
4715
|
}
|
|
4692
|
-
|
|
4693
|
-
return observable;
|
|
4694
|
-
}
|
|
4716
|
+
};
|
|
4695
4717
|
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
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.`);
|
|
4702
4740
|
}
|
|
4703
|
-
dependency.subscribe(updatedValue);
|
|
4704
4741
|
});
|
|
4705
|
-
|
|
4706
|
-
return observable;
|
|
4707
4742
|
};
|
|
4708
4743
|
|
|
4744
|
+
const Store = StoreFactory();
|
|
4745
|
+
|
|
4746
|
+
Store.create('locale', navigator.language.split('-')[0] || 'en');
|
|
4747
|
+
|
|
4709
4748
|
/**
|
|
4710
4749
|
* Renders a list of items from an observable array or object, automatically updating when data changes.
|
|
4711
4750
|
* Efficiently manages DOM updates by tracking items with keys.
|