native-document 1.0.95 → 1.0.99
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/{src/devtools/hrm → devtools}/ComponentRegistry.js +2 -2
- package/devtools/index.js +8 -0
- package/{src/devtools/plugin.js → devtools/plugin/dev-tools-plugin.js} +2 -2
- package/{src/devtools/hrm/nd-vite-hot-reload.js → devtools/transformers/nd-vite-devtools.js} +16 -6
- package/devtools/transformers/src/transformComponentForHrm.js +74 -0
- package/devtools/transformers/src/transformJsFile.js +9 -0
- package/devtools/transformers/src/utils.js +79 -0
- package/{src/devtools/hrm → devtools/transformers/templates}/hrm.orbservable.hook.template.js +8 -0
- package/devtools/widget/Widget.js +48 -0
- package/devtools/widget/widget.css +81 -0
- package/devtools/widget.js +23 -0
- package/dist/native-document.components.min.js +1953 -1245
- package/dist/native-document.dev.js +2022 -1375
- package/dist/native-document.dev.js.map +1 -1
- package/dist/native-document.devtools.min.js +1 -1
- package/dist/native-document.min.js +1 -1
- package/docs/cache.md +1 -1
- package/docs/core-concepts.md +1 -1
- package/docs/native-document-element.md +51 -15
- package/docs/observables.md +333 -315
- package/docs/state-management.md +198 -193
- package/package.json +1 -1
- package/readme.md +1 -1
- package/rollup.config.js +1 -1
- package/src/core/data/ObservableArray.js +67 -0
- package/src/core/data/ObservableChecker.js +2 -0
- package/src/core/data/ObservableItem.js +97 -0
- package/src/core/data/ObservableObject.js +183 -0
- package/src/core/data/Store.js +364 -34
- package/src/core/data/observable-helpers/object.js +2 -166
- package/src/core/utils/formatters.js +91 -0
- package/src/core/utils/localstorage.js +57 -0
- package/src/core/utils/validator.js +0 -2
- package/src/fetch/NativeFetch.js +5 -2
- package/types/observable.d.ts +73 -15
- package/types/plugins-manager.d.ts +1 -1
- package/types/store.d.ts +33 -6
- package/hrm.js +0 -7
- package/src/devtools/app/App.js +0 -66
- package/src/devtools/app/app.css +0 -0
- package/src/devtools/hrm/transformComponent.js +0 -129
- package/src/devtools/index.js +0 -18
- package/src/devtools/widget/DevToolsWidget.js +0 -26
- /package/{src/devtools/hrm → devtools/transformers/templates}/hrm.hook.template.js +0 -0
|
@@ -37,59 +37,6 @@ var NativeDocument = (function (exports) {
|
|
|
37
37
|
}
|
|
38
38
|
var DebugManager = DebugManager$1;
|
|
39
39
|
|
|
40
|
-
const MemoryManager = (function() {
|
|
41
|
-
|
|
42
|
-
let $nextObserverId = 0;
|
|
43
|
-
const $observables = new Map();
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
/**
|
|
47
|
-
* Register an observable and return an id.
|
|
48
|
-
*
|
|
49
|
-
* @param {ObservableItem} observable
|
|
50
|
-
* @param {Function} getListeners
|
|
51
|
-
* @returns {number}
|
|
52
|
-
*/
|
|
53
|
-
register(observable) {
|
|
54
|
-
const id = ++$nextObserverId;
|
|
55
|
-
$observables.set(id, new WeakRef(observable));
|
|
56
|
-
return id;
|
|
57
|
-
},
|
|
58
|
-
unregister(id) {
|
|
59
|
-
$observables.delete(id);
|
|
60
|
-
},
|
|
61
|
-
getObservableById(id) {
|
|
62
|
-
return $observables.get(id)?.deref();
|
|
63
|
-
},
|
|
64
|
-
cleanup() {
|
|
65
|
-
for (const [_, weakObservableRef] of $observables) {
|
|
66
|
-
const observable = weakObservableRef.deref();
|
|
67
|
-
if (observable) {
|
|
68
|
-
observable.cleanup();
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
$observables.clear();
|
|
72
|
-
},
|
|
73
|
-
/**
|
|
74
|
-
* Clean observables that are not referenced anymore.
|
|
75
|
-
* @param {number} threshold
|
|
76
|
-
*/
|
|
77
|
-
cleanObservables(threshold) {
|
|
78
|
-
if($observables.size < threshold) return;
|
|
79
|
-
let cleanedCount = 0;
|
|
80
|
-
for (const [id, weakObservableRef] of $observables) {
|
|
81
|
-
if (!weakObservableRef.deref()) {
|
|
82
|
-
$observables.delete(id);
|
|
83
|
-
cleanedCount++;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
if (cleanedCount > 0) {
|
|
87
|
-
DebugManager.log('Memory Auto Clean', `🧹 Cleaned ${cleanedCount} orphaned observables`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
}());
|
|
92
|
-
|
|
93
40
|
class NativeDocumentError extends Error {
|
|
94
41
|
constructor(message, context = {}) {
|
|
95
42
|
super(message);
|
|
@@ -191,6 +138,181 @@ var NativeDocument = (function (exports) {
|
|
|
191
138
|
return this.observable.cleanup();
|
|
192
139
|
};
|
|
193
140
|
|
|
141
|
+
const DocumentObserver = {
|
|
142
|
+
mounted: new WeakMap(),
|
|
143
|
+
beforeUnmount: new WeakMap(),
|
|
144
|
+
mountedSupposedSize: 0,
|
|
145
|
+
unmounted: new WeakMap(),
|
|
146
|
+
unmountedSupposedSize: 0,
|
|
147
|
+
observer: null,
|
|
148
|
+
|
|
149
|
+
executeMountedCallback(node) {
|
|
150
|
+
const data = DocumentObserver.mounted.get(node);
|
|
151
|
+
if(!data) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
data.inDom = true;
|
|
155
|
+
if(!data.mounted) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if(Array.isArray(data.mounted)) {
|
|
159
|
+
for(const cb of data.mounted) {
|
|
160
|
+
cb(node);
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
data.mounted(node);
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
executeUnmountedCallback(node) {
|
|
168
|
+
const data = DocumentObserver.unmounted.get(node);
|
|
169
|
+
if(!data) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
data.inDom = false;
|
|
173
|
+
if(!data.unmounted) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let shouldRemove = false;
|
|
178
|
+
if(Array.isArray(data.unmounted)) {
|
|
179
|
+
for(const cb of data.unmounted) {
|
|
180
|
+
if(cb(node) === true) {
|
|
181
|
+
shouldRemove = true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
shouldRemove = data.unmounted(node) === true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if(shouldRemove) {
|
|
189
|
+
data.disconnect();
|
|
190
|
+
node.nd?.remove();
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
checkMutation: function(mutationsList) {
|
|
195
|
+
for(const mutation of mutationsList) {
|
|
196
|
+
if(DocumentObserver.mountedSupposedSize > 0) {
|
|
197
|
+
for(const node of mutation.addedNodes) {
|
|
198
|
+
DocumentObserver.executeMountedCallback(node);
|
|
199
|
+
if(!node.querySelectorAll) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const children = node.querySelectorAll('[data--nd-mounted]');
|
|
203
|
+
for(const child of children) {
|
|
204
|
+
DocumentObserver.executeMountedCallback(child);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (DocumentObserver.unmountedSupposedSize > 0) {
|
|
210
|
+
for (const node of mutation.removedNodes) {
|
|
211
|
+
DocumentObserver.executeUnmountedCallback(node);
|
|
212
|
+
if(!node.querySelectorAll) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const children = node.querySelectorAll('[data--nd-unmounted]');
|
|
216
|
+
for(const child of children) {
|
|
217
|
+
DocumentObserver.executeUnmountedCallback(child);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @param {HTMLElement} element
|
|
226
|
+
* @param {boolean} inDom
|
|
227
|
+
* @returns {{ disconnect: Function, mounted: Function, unmounted: Function, off: Function }}
|
|
228
|
+
*/
|
|
229
|
+
watch: function(element, inDom = false) {
|
|
230
|
+
let mountedRegistered = false;
|
|
231
|
+
let unmountedRegistered = false;
|
|
232
|
+
|
|
233
|
+
let data = {
|
|
234
|
+
inDom,
|
|
235
|
+
mounted: null,
|
|
236
|
+
unmounted: null,
|
|
237
|
+
disconnect: () => {
|
|
238
|
+
if (mountedRegistered) {
|
|
239
|
+
DocumentObserver.mounted.delete(element);
|
|
240
|
+
DocumentObserver.mountedSupposedSize--;
|
|
241
|
+
}
|
|
242
|
+
if (unmountedRegistered) {
|
|
243
|
+
DocumentObserver.unmounted.delete(element);
|
|
244
|
+
DocumentObserver.unmountedSupposedSize--;
|
|
245
|
+
}
|
|
246
|
+
data = null;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const addListener = (type, callback) => {
|
|
251
|
+
if (!data[type]) {
|
|
252
|
+
data[type] = callback;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (!Array.isArray(data[type])) {
|
|
256
|
+
data[type] = [data[type], callback];
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
data[type].push(callback);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const removeListener = (type, callback) => {
|
|
263
|
+
if(!data?.[type]) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if(Array.isArray(data[type])) {
|
|
267
|
+
const index = data[type].indexOf(callback);
|
|
268
|
+
if(index > -1) {
|
|
269
|
+
data[type].splice(index, 1);
|
|
270
|
+
}
|
|
271
|
+
if(data[type].length === 1) {
|
|
272
|
+
data[type] = data[type][0];
|
|
273
|
+
}
|
|
274
|
+
if(data[type].length === 0) {
|
|
275
|
+
data[type] = null;
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
data[type] = null;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
disconnect: () => data?.disconnect(),
|
|
284
|
+
|
|
285
|
+
mounted: (callback) => {
|
|
286
|
+
addListener('mounted', callback);
|
|
287
|
+
DocumentObserver.mounted.set(element, data);
|
|
288
|
+
if (!mountedRegistered) {
|
|
289
|
+
DocumentObserver.mountedSupposedSize++;
|
|
290
|
+
mountedRegistered = true;
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
unmounted: (callback) => {
|
|
295
|
+
addListener('unmounted', callback);
|
|
296
|
+
DocumentObserver.unmounted.set(element, data);
|
|
297
|
+
if (!unmountedRegistered) {
|
|
298
|
+
DocumentObserver.unmountedSupposedSize++;
|
|
299
|
+
unmountedRegistered = true;
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
off: (type, callback) => {
|
|
304
|
+
removeListener(type, callback);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
DocumentObserver.observer = new MutationObserver(DocumentObserver.checkMutation);
|
|
311
|
+
DocumentObserver.observer.observe(document.body, {
|
|
312
|
+
childList: true,
|
|
313
|
+
subtree: true,
|
|
314
|
+
});
|
|
315
|
+
|
|
194
316
|
let PluginsManager$1 = null;
|
|
195
317
|
|
|
196
318
|
{
|
|
@@ -271,1385 +393,1898 @@ var NativeDocument = (function (exports) {
|
|
|
271
393
|
|
|
272
394
|
var PluginsManager = PluginsManager$1;
|
|
273
395
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
396
|
+
function NDElement(element) {
|
|
397
|
+
this.$element = element;
|
|
398
|
+
this.$observer = null;
|
|
399
|
+
{
|
|
400
|
+
PluginsManager.emit('NDElementCreated', element, this);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
NDElement.prototype.__$isNDElement = true;
|
|
405
|
+
|
|
406
|
+
NDElement.prototype.valueOf = function() {
|
|
407
|
+
return this.$element;
|
|
284
408
|
};
|
|
285
409
|
|
|
286
|
-
|
|
410
|
+
NDElement.prototype.ref = function(target, name) {
|
|
411
|
+
target[name] = this.$element;
|
|
412
|
+
return this;
|
|
413
|
+
};
|
|
287
414
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
* @param {Function} callback - Function called with boolean indicating if values match
|
|
292
|
-
* @returns {Function} Unsubscribe function
|
|
293
|
-
* @example
|
|
294
|
-
* const status = Observable('idle');
|
|
295
|
-
* const isLoading = status.when('loading');
|
|
296
|
-
* isLoading.subscribe(active => console.log('Loading:', active));
|
|
297
|
-
*/
|
|
298
|
-
ObservableWhen.prototype.subscribe = function(callback) {
|
|
299
|
-
return this.$observer.on(this.$target, callback);
|
|
415
|
+
NDElement.prototype.refSelf = function(target, name) {
|
|
416
|
+
target[name] = this;
|
|
417
|
+
return this;
|
|
300
418
|
};
|
|
301
419
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
420
|
+
NDElement.prototype.unmountChildren = function() {
|
|
421
|
+
let element = this.$element;
|
|
422
|
+
for(let i = 0, length = element.children.length; i < length; i++) {
|
|
423
|
+
let elementChildren = element.children[i];
|
|
424
|
+
if(!elementChildren.$ndProx) {
|
|
425
|
+
elementChildren.nd?.remove();
|
|
426
|
+
}
|
|
427
|
+
elementChildren = null;
|
|
428
|
+
}
|
|
429
|
+
element = null;
|
|
430
|
+
return this;
|
|
309
431
|
};
|
|
310
432
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
433
|
+
NDElement.prototype.remove = function() {
|
|
434
|
+
let element = this.$element;
|
|
435
|
+
element.nd.unmountChildren();
|
|
436
|
+
element.$ndProx = null;
|
|
437
|
+
delete element.nd?.on?.prevent;
|
|
438
|
+
delete element.nd?.on;
|
|
439
|
+
delete element.nd;
|
|
440
|
+
element = null;
|
|
441
|
+
return this;
|
|
442
|
+
};
|
|
318
443
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
* Alias for val().
|
|
322
|
-
*
|
|
323
|
-
* @returns {boolean} True if observable value matches target value
|
|
324
|
-
*/
|
|
325
|
-
ObservableWhen.prototype.isActive = ObservableWhen.prototype.val;
|
|
444
|
+
NDElement.prototype.lifecycle = function(states) {
|
|
445
|
+
this.$observer = this.$observer || DocumentObserver.watch(this.$element);
|
|
326
446
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
447
|
+
if(states.mounted) {
|
|
448
|
+
this.$element.setAttribute('data--nd-mounted', '1');
|
|
449
|
+
this.$observer.mounted(states.mounted);
|
|
450
|
+
}
|
|
451
|
+
if(states.unmounted) {
|
|
452
|
+
this.$element.setAttribute('data--nd-unmounted', '1');
|
|
453
|
+
this.$observer.unmounted(states.unmounted);
|
|
454
|
+
}
|
|
455
|
+
return this;
|
|
456
|
+
};
|
|
332
457
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
pending = false;
|
|
336
|
-
});
|
|
337
|
-
};
|
|
458
|
+
NDElement.prototype.mounted = function(callback) {
|
|
459
|
+
return this.lifecycle({ mounted: callback });
|
|
338
460
|
};
|
|
339
461
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
* @param {string|null} defaultKey
|
|
344
|
-
* @param {?Function} key
|
|
345
|
-
* @returns {*}
|
|
346
|
-
*/
|
|
347
|
-
const getKey = (item, defaultKey, key) => {
|
|
348
|
-
if (Validator.isString(key)) {
|
|
349
|
-
const val = Validator.isObservable(item) ? item.val() : item;
|
|
350
|
-
const result = val?.[key];
|
|
351
|
-
return Validator.isObservable(result) ? result.val() : (result ?? defaultKey);
|
|
352
|
-
}
|
|
462
|
+
NDElement.prototype.unmounted = function(callback) {
|
|
463
|
+
return this.lifecycle({ unmounted: callback });
|
|
464
|
+
};
|
|
353
465
|
|
|
354
|
-
|
|
355
|
-
|
|
466
|
+
NDElement.prototype.beforeUnmount = function(id, callback) {
|
|
467
|
+
const el = this.$element;
|
|
468
|
+
|
|
469
|
+
if(!DocumentObserver.beforeUnmount.has(el)) {
|
|
470
|
+
DocumentObserver.beforeUnmount.set(el, new Map());
|
|
471
|
+
const originalRemove = el.remove.bind(el);
|
|
472
|
+
|
|
473
|
+
let $isUnmounting = false;
|
|
474
|
+
|
|
475
|
+
el.remove = async () => {
|
|
476
|
+
if($isUnmounting) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
$isUnmounting = true;
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
const callbacks = DocumentObserver.beforeUnmount.get(el);
|
|
483
|
+
for (const cb of callbacks.values()) {
|
|
484
|
+
await cb.call(this, el);
|
|
485
|
+
}
|
|
486
|
+
} finally {
|
|
487
|
+
originalRemove();
|
|
488
|
+
$isUnmounting = false;
|
|
489
|
+
}
|
|
490
|
+
};
|
|
356
491
|
}
|
|
357
492
|
|
|
358
|
-
|
|
359
|
-
return
|
|
493
|
+
DocumentObserver.beforeUnmount.get(el).set(id, callback);
|
|
494
|
+
return this;
|
|
360
495
|
};
|
|
361
496
|
|
|
362
|
-
|
|
363
|
-
return
|
|
497
|
+
NDElement.prototype.htmlElement = function() {
|
|
498
|
+
return this.$element;
|
|
364
499
|
};
|
|
365
500
|
|
|
366
|
-
|
|
367
|
-
try {
|
|
368
|
-
if(window.structuredClone !== undefined) {
|
|
369
|
-
return window.structuredClone(value);
|
|
370
|
-
}
|
|
371
|
-
} catch (e){}
|
|
372
|
-
|
|
373
|
-
if (value === null || typeof value !== 'object') {
|
|
374
|
-
return value;
|
|
375
|
-
}
|
|
501
|
+
NDElement.prototype.node = NDElement.prototype.htmlElement;
|
|
376
502
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
503
|
+
NDElement.prototype.shadow = function(mode, style = null) {
|
|
504
|
+
const $element = this.$element;
|
|
505
|
+
const children = Array.from($element.childNodes);
|
|
506
|
+
const shadowRoot = $element.attachShadow({ mode });
|
|
507
|
+
if(style) {
|
|
508
|
+
const styleNode = document.createElement("style");
|
|
509
|
+
styleNode.textContent = style;
|
|
510
|
+
shadowRoot.appendChild(styleNode);
|
|
380
511
|
}
|
|
512
|
+
$element.append = shadowRoot.append.bind(shadowRoot);
|
|
513
|
+
$element.appendChild = shadowRoot.appendChild.bind(shadowRoot);
|
|
514
|
+
shadowRoot.append(...children);
|
|
381
515
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
return value.map(item => deepClone(item));
|
|
385
|
-
}
|
|
516
|
+
return this;
|
|
517
|
+
};
|
|
386
518
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
return value;
|
|
391
|
-
}
|
|
519
|
+
NDElement.prototype.openShadow = function(style = null) {
|
|
520
|
+
return this.shadow('open', style);
|
|
521
|
+
};
|
|
392
522
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
for (const key in value) {
|
|
396
|
-
if (Object.hasOwn(value, key)) {
|
|
397
|
-
cloned[key] = deepClone(value[key]);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
return cloned;
|
|
523
|
+
NDElement.prototype.closedShadow = function(style = null) {
|
|
524
|
+
return this.shadow('closed', style);
|
|
401
525
|
};
|
|
402
526
|
|
|
403
527
|
/**
|
|
528
|
+
* Attaches a template binding to the element by hydrating it with the specified method.
|
|
404
529
|
*
|
|
405
|
-
* @param {
|
|
406
|
-
* @param {
|
|
407
|
-
* @
|
|
530
|
+
* @param {string} methodName - Name of the hydration method to call
|
|
531
|
+
* @param {BindingHydrator} bindingHydrator - Template binding with $hydrate method
|
|
532
|
+
* @returns {HTMLElement} The underlying HTML element
|
|
533
|
+
* @example
|
|
534
|
+
* const onClick = $binder.attach((event, data) => console.log(data));
|
|
535
|
+
* element.nd.attach('onClick', onClick);
|
|
408
536
|
*/
|
|
409
|
-
function
|
|
410
|
-
|
|
537
|
+
NDElement.prototype.attach = function(methodName, bindingHydrator) {
|
|
538
|
+
bindingHydrator.$hydrate(this.$element, methodName);
|
|
539
|
+
return this.$element;
|
|
540
|
+
};
|
|
411
541
|
|
|
412
|
-
|
|
413
|
-
|
|
542
|
+
/**
|
|
543
|
+
* Extends the current NDElement instance with custom methods.
|
|
544
|
+
* Methods are bound to the instance and available for chaining.
|
|
545
|
+
*
|
|
546
|
+
* @param {Object} methods - Object containing method definitions
|
|
547
|
+
* @returns {this} The NDElement instance with added methods for chaining
|
|
548
|
+
* @example
|
|
549
|
+
* element.nd.with({
|
|
550
|
+
* highlight() {
|
|
551
|
+
* this.$element.style.background = 'yellow';
|
|
552
|
+
* return this;
|
|
553
|
+
* }
|
|
554
|
+
* }).highlight().onClick(() => console.log('Clicked'));
|
|
555
|
+
*/
|
|
556
|
+
NDElement.prototype.with = function(methods) {
|
|
557
|
+
if (!methods || typeof methods !== 'object') {
|
|
558
|
+
throw new NativeDocumentError('extend() requires an object of methods');
|
|
559
|
+
}
|
|
414
560
|
{
|
|
415
|
-
this.$
|
|
561
|
+
if (!this.$localExtensions) {
|
|
562
|
+
this.$localExtensions = new Map();
|
|
563
|
+
}
|
|
416
564
|
}
|
|
417
565
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
this.$watchers = null;
|
|
421
|
-
|
|
422
|
-
this.$memoryId = null;
|
|
566
|
+
for (const name in methods) {
|
|
567
|
+
const method = methods[name];
|
|
423
568
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
569
|
+
if (typeof method !== 'function') {
|
|
570
|
+
console.warn(`⚠️ extends(): "${name}" is not a function, skipping`);
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
{
|
|
574
|
+
if (this[name] && !this.$localExtensions.has(name)) {
|
|
575
|
+
DebugManager.warn('NDElement.extend', `Method "${name}" already exists and will be overwritten`);
|
|
576
|
+
}
|
|
577
|
+
this.$localExtensions.set(name, method);
|
|
428
578
|
}
|
|
429
|
-
}
|
|
430
|
-
{
|
|
431
|
-
PluginsManager.emit('CreateObservable', this);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
579
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
return this.$currentValue;
|
|
438
|
-
},
|
|
439
|
-
set(value) {
|
|
440
|
-
this.set(value);
|
|
441
|
-
},
|
|
442
|
-
configurable: true,
|
|
443
|
-
});
|
|
580
|
+
this[name] = method.bind(this);
|
|
581
|
+
}
|
|
444
582
|
|
|
445
|
-
|
|
446
|
-
|
|
583
|
+
return this;
|
|
584
|
+
};
|
|
447
585
|
|
|
448
586
|
/**
|
|
449
|
-
*
|
|
450
|
-
*
|
|
587
|
+
* Extends the NDElement prototype with new methods available to all NDElement instances.
|
|
588
|
+
* Use this to add global methods to all NDElements.
|
|
451
589
|
*
|
|
452
|
-
* @param {
|
|
453
|
-
* @returns {
|
|
590
|
+
* @param {Object} methods - Object containing method definitions to add to prototype
|
|
591
|
+
* @returns {typeof NDElement} The NDElement constructor
|
|
592
|
+
* @throws {NativeDocumentError} If methods is not an object or contains non-function values
|
|
454
593
|
* @example
|
|
455
|
-
*
|
|
456
|
-
*
|
|
594
|
+
* NDElement.extend({
|
|
595
|
+
* fadeIn() {
|
|
596
|
+
* this.$element.style.opacity = '1';
|
|
597
|
+
* return this;
|
|
598
|
+
* }
|
|
599
|
+
* });
|
|
600
|
+
* // Now all NDElements have .fadeIn() method
|
|
601
|
+
* Div().nd.fadeIn();
|
|
457
602
|
*/
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
};
|
|
463
|
-
|
|
464
|
-
ObservableItem.prototype.triggerFirstListener = function(operations) {
|
|
465
|
-
this.$firstListener(this.$currentValue, this.$previousValue, operations);
|
|
466
|
-
};
|
|
467
|
-
|
|
468
|
-
ObservableItem.prototype.triggerListeners = function(operations) {
|
|
469
|
-
const $listeners = this.$listeners;
|
|
470
|
-
const $previousValue = this.$previousValue;
|
|
471
|
-
const $currentValue = this.$currentValue;
|
|
603
|
+
NDElement.extend = function(methods) {
|
|
604
|
+
if (!methods || typeof methods !== 'object') {
|
|
605
|
+
throw new NativeDocumentError('NDElement.extend() requires an object of methods');
|
|
606
|
+
}
|
|
472
607
|
|
|
473
|
-
|
|
474
|
-
|
|
608
|
+
if (Array.isArray(methods)) {
|
|
609
|
+
throw new NativeDocumentError('NDElement.extend() requires an object, not an array');
|
|
475
610
|
}
|
|
476
|
-
};
|
|
477
611
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
612
|
+
const protectedMethods = new Set([
|
|
613
|
+
'constructor', 'valueOf', '$element', '$observer',
|
|
614
|
+
'ref', 'remove', 'cleanup', 'with', 'extend', 'attach',
|
|
615
|
+
'lifecycle', 'mounted', 'unmounted', 'unmountChildren'
|
|
616
|
+
]);
|
|
482
617
|
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
}
|
|
488
|
-
if($previousValueCallbacks) {
|
|
489
|
-
$previousValueCallbacks(false, $currentValue, operations);
|
|
490
|
-
}
|
|
491
|
-
};
|
|
618
|
+
for (const name in methods) {
|
|
619
|
+
if (!Object.hasOwn(methods, name)) {
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
492
622
|
|
|
493
|
-
|
|
494
|
-
this.triggerWatchers(operations);
|
|
495
|
-
this.triggerListeners(operations);
|
|
496
|
-
};
|
|
623
|
+
const method = methods[name];
|
|
497
624
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
625
|
+
if (typeof method !== 'function') {
|
|
626
|
+
DebugManager.warn('NDElement.extend', `"${name}" is not a function, skipping`);
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
502
629
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
this.trigger = (this.$listeners.length === 1) ? this.triggerWatchersAndFirstListener : this.triggerAll;
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
if(this.$listeners?.length) {
|
|
510
|
-
if(this.$listeners.length === 1) {
|
|
511
|
-
this.$firstListener = this.$listeners[0];
|
|
512
|
-
this.trigger = this.triggerFirstListener;
|
|
630
|
+
if (protectedMethods.has(name)) {
|
|
631
|
+
DebugManager.error('NDElement.extend', `Cannot override protected method "${name}"`);
|
|
632
|
+
throw new NativeDocumentError(`Cannot override protected method "${name}"`);
|
|
513
633
|
}
|
|
514
|
-
|
|
515
|
-
|
|
634
|
+
|
|
635
|
+
if (NDElement.prototype[name]) {
|
|
636
|
+
DebugManager.warn('NDElement.extend', `Overwriting existing prototype method "${name}"`);
|
|
516
637
|
}
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
519
|
-
if(this.$watchers?.size) {
|
|
520
|
-
this.trigger = this.triggerWatchers;
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
523
|
-
this.trigger = noneTrigger;
|
|
524
|
-
};
|
|
525
|
-
ObservableItem.prototype.trigger = noneTrigger;
|
|
526
638
|
|
|
527
|
-
|
|
528
|
-
newValue = newValue?.__$isObservable ? newValue.val() : newValue;
|
|
529
|
-
if(this.$currentValue === newValue) {
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
this.$previousValue = this.$currentValue;
|
|
533
|
-
this.$currentValue = newValue;
|
|
534
|
-
{
|
|
535
|
-
PluginsManager.emit('ObservableBeforeChange', this);
|
|
639
|
+
NDElement.prototype[name] = method;
|
|
536
640
|
}
|
|
537
|
-
this.trigger();
|
|
538
|
-
this.$previousValue = null;
|
|
539
641
|
{
|
|
540
|
-
PluginsManager.emit('
|
|
541
|
-
}
|
|
542
|
-
};
|
|
543
|
-
|
|
544
|
-
/**
|
|
545
|
-
* @param {*} data
|
|
546
|
-
*/
|
|
547
|
-
ObservableItem.prototype.$setWithInterceptor = function(data) {
|
|
548
|
-
let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
|
|
549
|
-
const result = this.$interceptor(newValue, this.$currentValue);
|
|
550
|
-
|
|
551
|
-
if (result !== undefined) {
|
|
552
|
-
newValue = result;
|
|
642
|
+
PluginsManager.emit('NDElementExtended', methods);
|
|
553
643
|
}
|
|
554
644
|
|
|
555
|
-
|
|
645
|
+
return NDElement;
|
|
556
646
|
};
|
|
557
647
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
this.$updateWithNewValue(newValue);
|
|
648
|
+
const COMMON_NODE_TYPES = {
|
|
649
|
+
ELEMENT: 1,
|
|
650
|
+
TEXT: 3,
|
|
651
|
+
COMMENT: 8,
|
|
652
|
+
DOCUMENT_FRAGMENT: 11
|
|
564
653
|
};
|
|
565
654
|
|
|
566
|
-
|
|
655
|
+
const Validator = {
|
|
656
|
+
isObservable(value) {
|
|
657
|
+
return value?.__$isObservable;
|
|
658
|
+
},
|
|
659
|
+
isTemplateBinding(value) {
|
|
660
|
+
return value?.__$isTemplateBinding;
|
|
661
|
+
},
|
|
662
|
+
isObservableWhenResult(value) {
|
|
663
|
+
return value && (value.__$isObservableWhen || (typeof value === 'object' && '$target' in value && '$observer' in value));
|
|
664
|
+
},
|
|
665
|
+
isArrayObservable(value) {
|
|
666
|
+
return value?.__$isObservableArray;
|
|
667
|
+
},
|
|
668
|
+
isProxy(value) {
|
|
669
|
+
return value?.__isProxy__
|
|
670
|
+
},
|
|
671
|
+
isObservableOrProxy(value) {
|
|
672
|
+
return Validator.isObservable(value) || Validator.isProxy(value);
|
|
673
|
+
},
|
|
674
|
+
isAnchor(value) {
|
|
675
|
+
return value?.__Anchor__
|
|
676
|
+
},
|
|
677
|
+
isObservableChecker(value) {
|
|
678
|
+
return value?.__$isObservableChecker || value instanceof ObservableChecker;
|
|
679
|
+
},
|
|
680
|
+
isArray(value) {
|
|
681
|
+
return Array.isArray(value);
|
|
682
|
+
},
|
|
683
|
+
isString(value) {
|
|
684
|
+
return typeof value === 'string';
|
|
685
|
+
},
|
|
686
|
+
isNumber(value) {
|
|
687
|
+
return typeof value === 'number';
|
|
688
|
+
},
|
|
689
|
+
isBoolean(value) {
|
|
690
|
+
return typeof value === 'boolean';
|
|
691
|
+
},
|
|
692
|
+
isFunction(value) {
|
|
693
|
+
return typeof value === 'function';
|
|
694
|
+
},
|
|
695
|
+
isAsyncFunction(value) {
|
|
696
|
+
return typeof value === 'function' && value.constructor.name === 'AsyncFunction';
|
|
697
|
+
},
|
|
698
|
+
isObject(value) {
|
|
699
|
+
return typeof value === 'object' && value !== null;
|
|
700
|
+
},
|
|
701
|
+
isJson(value) {
|
|
702
|
+
return !(typeof value !== 'object' || value === null || Array.isArray(value) || value.constructor.name !== 'Object')
|
|
703
|
+
},
|
|
704
|
+
isElement(value) {
|
|
705
|
+
return value && (
|
|
706
|
+
value.nodeType === COMMON_NODE_TYPES.ELEMENT ||
|
|
707
|
+
value.nodeType === COMMON_NODE_TYPES.TEXT ||
|
|
708
|
+
value.nodeType === COMMON_NODE_TYPES.DOCUMENT_FRAGMENT ||
|
|
709
|
+
value.nodeType === COMMON_NODE_TYPES.COMMENT
|
|
710
|
+
);
|
|
711
|
+
},
|
|
712
|
+
isFragment(value) {
|
|
713
|
+
return value?.nodeType === COMMON_NODE_TYPES.DOCUMENT_FRAGMENT;
|
|
714
|
+
},
|
|
715
|
+
isStringOrObservable(value) {
|
|
716
|
+
return this.isString(value) || this.isObservable(value);
|
|
717
|
+
},
|
|
718
|
+
isValidChild(child) {
|
|
719
|
+
return child === null ||
|
|
720
|
+
this.isElement(child) ||
|
|
721
|
+
this.isObservable(child) ||
|
|
722
|
+
this.isNDElement(child) ||
|
|
723
|
+
['string', 'number', 'boolean'].includes(typeof child);
|
|
724
|
+
},
|
|
725
|
+
isNDElement(child) {
|
|
726
|
+
return child?.__$isNDElement || child instanceof NDElement;
|
|
727
|
+
},
|
|
728
|
+
isValidChildren(children) {
|
|
729
|
+
if (!Array.isArray(children)) {
|
|
730
|
+
children = [children];
|
|
731
|
+
}
|
|
567
732
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
733
|
+
const invalid = children.filter(child => !this.isValidChild(child));
|
|
734
|
+
return invalid.length === 0;
|
|
735
|
+
},
|
|
736
|
+
validateChildren(children) {
|
|
737
|
+
if (!Array.isArray(children)) {
|
|
738
|
+
children = [children];
|
|
739
|
+
}
|
|
571
740
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
this.$currentValue = null;
|
|
576
|
-
if(this.$watchers) {
|
|
577
|
-
for (const [_, watchValueList] of this.$watchers) {
|
|
578
|
-
if(Validator.isArray(watchValueList)) {
|
|
579
|
-
watchValueList.splice(0);
|
|
580
|
-
}
|
|
741
|
+
const invalid = children.filter(child => !this.isValidChild(child));
|
|
742
|
+
if (invalid.length > 0) {
|
|
743
|
+
throw new NativeDocumentError(`Invalid children detected: ${invalid.map(i => typeof i).join(', ')}`);
|
|
581
744
|
}
|
|
582
|
-
}
|
|
583
|
-
this.$watchers?.clear();
|
|
584
|
-
this.$listeners = null;
|
|
585
|
-
this.$watchers = null;
|
|
586
|
-
this.trigger = noneTrigger;
|
|
587
|
-
};
|
|
588
745
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
ObservableItem.prototype.onCleanup = function(callback) {
|
|
600
|
-
this.$cleanupListeners = this.$cleanupListeners ?? [];
|
|
601
|
-
this.$cleanupListeners.push(callback);
|
|
602
|
-
};
|
|
603
|
-
|
|
604
|
-
ObservableItem.prototype.cleanup = function() {
|
|
605
|
-
if (this.$cleanupListeners) {
|
|
606
|
-
for (let i = 0; i < this.$cleanupListeners.length; i++) {
|
|
607
|
-
this.$cleanupListeners[i]();
|
|
746
|
+
return children;
|
|
747
|
+
},
|
|
748
|
+
/**
|
|
749
|
+
* Check if the data contains observables.
|
|
750
|
+
* @param {Array|Object} data
|
|
751
|
+
* @returns {boolean}
|
|
752
|
+
*/
|
|
753
|
+
containsObservables(data) {
|
|
754
|
+
if(!data) {
|
|
755
|
+
return false;
|
|
608
756
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
*
|
|
621
|
-
* @param {Function} callback
|
|
622
|
-
* @returns {(function(): void)}
|
|
623
|
-
*/
|
|
624
|
-
ObservableItem.prototype.subscribe = function(callback) {
|
|
625
|
-
{
|
|
626
|
-
if (this.$isCleanedUp) {
|
|
627
|
-
DebugManager.warn('Observable subscription', '⚠️ Attempted to subscribe to a cleaned up observable.');
|
|
628
|
-
return;
|
|
757
|
+
return Validator.isObject(data)
|
|
758
|
+
&& Object.values(data).some(value => Validator.isObservable(value));
|
|
759
|
+
},
|
|
760
|
+
/**
|
|
761
|
+
* Check if the data contains an observable reference.
|
|
762
|
+
* @param {string} data
|
|
763
|
+
* @returns {boolean}
|
|
764
|
+
*/
|
|
765
|
+
containsObservableReference(data) {
|
|
766
|
+
if(!data || typeof data !== 'string') {
|
|
767
|
+
return false;
|
|
629
768
|
}
|
|
769
|
+
return /\{\{#ObItem::\([0-9]+\)\}\}/.test(data);
|
|
770
|
+
},
|
|
771
|
+
validateAttributes(attributes) {},
|
|
772
|
+
|
|
773
|
+
validateEventCallback(callback) {
|
|
630
774
|
if (typeof callback !== 'function') {
|
|
631
|
-
throw new NativeDocumentError('
|
|
775
|
+
throw new NativeDocumentError('Event callback must be a function');
|
|
632
776
|
}
|
|
633
777
|
}
|
|
634
|
-
this.$listeners = this.$listeners ?? [];
|
|
635
|
-
|
|
636
|
-
this.$listeners.push(callback);
|
|
637
|
-
this.assocTrigger();
|
|
638
|
-
{
|
|
639
|
-
PluginsManager.emit('ObservableSubscribe', this);
|
|
640
|
-
}
|
|
641
778
|
};
|
|
779
|
+
{
|
|
780
|
+
Validator.validateAttributes = function(attributes) {
|
|
781
|
+
if (!attributes || typeof attributes !== 'object') {
|
|
782
|
+
return attributes;
|
|
783
|
+
}
|
|
642
784
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
* Creates a watcher that only triggers when the observable changes to the specified value.
|
|
646
|
-
*
|
|
647
|
-
* @param {*} value - The value to watch for
|
|
648
|
-
* @param {(value) => void|ObservableItem} callback - Callback function or observable to set when value matches
|
|
649
|
-
* @example
|
|
650
|
-
* const status = Observable('idle');
|
|
651
|
-
* status.on('loading', () => console.log('Started loading'));
|
|
652
|
-
* status.on('error', isError); // Set another observable
|
|
653
|
-
*/
|
|
654
|
-
ObservableItem.prototype.on = function(value, callback) {
|
|
655
|
-
this.$watchers = this.$watchers ?? new Map();
|
|
785
|
+
const reserved = [];
|
|
786
|
+
const foundReserved = Object.keys(attributes).filter(key => reserved.includes(key));
|
|
656
787
|
|
|
657
|
-
|
|
788
|
+
if (foundReserved.length > 0) {
|
|
789
|
+
DebugManager.warn('Validator', `Reserved attributes found: ${foundReserved.join(', ')}`);
|
|
790
|
+
}
|
|
658
791
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
792
|
+
return attributes;
|
|
793
|
+
};
|
|
794
|
+
}
|
|
662
795
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
} else if(!Validator.isArray(watchValueList.list)) {
|
|
667
|
-
watchValueList = [watchValueList, callback];
|
|
668
|
-
callback = (value) => {
|
|
669
|
-
for(let i = 0, length = watchValueList.length; i < length; i++) {
|
|
670
|
-
watchValueList[i](value);
|
|
671
|
-
}
|
|
672
|
-
};
|
|
673
|
-
callback.list = watchValueList;
|
|
674
|
-
this.$watchers.set(value, callback);
|
|
675
|
-
} else {
|
|
676
|
-
watchValueList.list.push(callback);
|
|
677
|
-
}
|
|
796
|
+
function Anchor(name, isUniqueChild = false) {
|
|
797
|
+
const anchorFragment = document.createDocumentFragment();
|
|
798
|
+
anchorFragment.__Anchor__ = true;
|
|
678
799
|
|
|
679
|
-
|
|
680
|
-
|
|
800
|
+
const anchorStart = document.createComment('Anchor Start : '+name);
|
|
801
|
+
const anchorEnd = document.createComment('/ Anchor End '+name);
|
|
681
802
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
*
|
|
685
|
-
* @param {*} value - The value to stop watching
|
|
686
|
-
* @param {Function} [callback] - Specific callback to remove. If omitted, removes all watchers for this value
|
|
687
|
-
* @example
|
|
688
|
-
* const status = Observable('idle');
|
|
689
|
-
* const handler = () => console.log('Loading');
|
|
690
|
-
* status.on('loading', handler);
|
|
691
|
-
* status.off('loading', handler); // Remove specific handler
|
|
692
|
-
* status.off('loading'); // Remove all handlers for 'loading'
|
|
693
|
-
*/
|
|
694
|
-
ObservableItem.prototype.off = function(value, callback) {
|
|
695
|
-
if(!this.$watchers) return;
|
|
803
|
+
anchorFragment.appendChild(anchorStart);
|
|
804
|
+
anchorFragment.appendChild(anchorEnd);
|
|
696
805
|
|
|
697
|
-
|
|
698
|
-
|
|
806
|
+
anchorFragment.nativeInsertBefore = anchorFragment.insertBefore;
|
|
807
|
+
anchorFragment.nativeAppendChild = anchorFragment.appendChild;
|
|
808
|
+
anchorFragment.nativeAppend = anchorFragment.append;
|
|
699
809
|
|
|
700
|
-
|
|
701
|
-
this.$watchers?.delete(value);
|
|
702
|
-
this.assocTrigger();
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
const index = watchValueList.indexOf(callback);
|
|
706
|
-
watchValueList?.splice(index, 1);
|
|
707
|
-
if(watchValueList.length === 1) {
|
|
708
|
-
this.$watchers.set(value, watchValueList[0]);
|
|
709
|
-
}
|
|
710
|
-
else if(watchValueList.length === 0) {
|
|
711
|
-
this.$watchers?.delete(value);
|
|
712
|
-
}
|
|
713
|
-
this.assocTrigger();
|
|
714
|
-
};
|
|
810
|
+
const isParentUniqueChild = (parent) => (isUniqueChild || (parent.firstChild === anchorStart && parent.lastChild === anchorEnd));
|
|
715
811
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
812
|
+
const insertBefore = function(parent, child, target) {
|
|
813
|
+
const childElement = Validator.isElement(child) ? child : ElementCreator.getChild(child);
|
|
814
|
+
if(parent === anchorFragment) {
|
|
815
|
+
parent.nativeInsertBefore(childElement, target);
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
if(isParentUniqueChild(parent) && target === anchorEnd) {
|
|
819
|
+
parent.append(childElement, target);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
parent.insertBefore(childElement, target);
|
|
823
|
+
};
|
|
728
824
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
825
|
+
anchorFragment.appendElement = function(child, before = null) {
|
|
826
|
+
const parentNode = anchorStart.parentNode;
|
|
827
|
+
const targetBefore = before || anchorEnd;
|
|
828
|
+
if(parentNode === anchorFragment) {
|
|
829
|
+
parentNode.nativeInsertBefore(child, targetBefore);
|
|
830
|
+
return;
|
|
733
831
|
}
|
|
832
|
+
parentNode?.insertBefore(child, targetBefore);
|
|
734
833
|
};
|
|
735
|
-
this.subscribe(handler);
|
|
736
|
-
};
|
|
737
834
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
}
|
|
748
|
-
this.assocTrigger();
|
|
749
|
-
{
|
|
750
|
-
PluginsManager.emit('ObservableUnsubscribe', this);
|
|
751
|
-
}
|
|
752
|
-
};
|
|
835
|
+
anchorFragment.appendChild = function(child, before = null) {
|
|
836
|
+
const parent = anchorEnd.parentNode;
|
|
837
|
+
if(!parent) {
|
|
838
|
+
DebugManager.error('Anchor', 'Anchor : parent not found', child);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
before = before ?? anchorEnd;
|
|
842
|
+
insertBefore(parent, child, before);
|
|
843
|
+
};
|
|
753
844
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
* @returns {ObservableChecker}
|
|
758
|
-
*/
|
|
759
|
-
ObservableItem.prototype.check = function(callback) {
|
|
760
|
-
return new ObservableChecker(this, callback)
|
|
761
|
-
};
|
|
845
|
+
anchorFragment.append = function(...args ) {
|
|
846
|
+
return anchorFragment.appendChild(args);
|
|
847
|
+
};
|
|
762
848
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
* user.get('age'); // 25 (unwrapped from observable)
|
|
773
|
-
*/
|
|
774
|
-
ObservableItem.prototype.get = function(key) {
|
|
775
|
-
const item = this.$currentValue[key];
|
|
776
|
-
return Validator.isObservable(item) ? item.val() : item;
|
|
777
|
-
};
|
|
849
|
+
anchorFragment.removeChildren = async function() {
|
|
850
|
+
const parent = anchorEnd.parentNode;
|
|
851
|
+
if(parent === anchorFragment) {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
// if(isParentUniqueChild(parent)) {
|
|
855
|
+
// parent.replaceChildren(anchorStart, anchorEnd);
|
|
856
|
+
// return;
|
|
857
|
+
// }
|
|
778
858
|
|
|
859
|
+
let itemToRemove = anchorStart.nextSibling, tempItem;
|
|
860
|
+
const removes = [];
|
|
861
|
+
while(itemToRemove && itemToRemove !== anchorEnd) {
|
|
862
|
+
tempItem = itemToRemove.nextSibling;
|
|
863
|
+
removes.push(itemToRemove.remove());
|
|
864
|
+
itemToRemove = tempItem;
|
|
865
|
+
}
|
|
866
|
+
await Promise.all(removes);
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
anchorFragment.remove = async function() {
|
|
870
|
+
const parent = anchorEnd.parentNode;
|
|
871
|
+
if(parent === anchorFragment) {
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
let itemToRemove = anchorStart.nextSibling, tempItem;
|
|
875
|
+
const allItemToRemove = [];
|
|
876
|
+
const removes = [];
|
|
877
|
+
while(itemToRemove && itemToRemove !== anchorEnd) {
|
|
878
|
+
tempItem = itemToRemove.nextSibling;
|
|
879
|
+
allItemToRemove.push(itemToRemove);
|
|
880
|
+
removes.push(itemToRemove.remove());
|
|
881
|
+
itemToRemove = tempItem;
|
|
882
|
+
}
|
|
883
|
+
await Promise.all(removes);
|
|
884
|
+
anchorFragment.nativeAppend(...allItemToRemove);
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
anchorFragment.removeWithAnchors = async function() {
|
|
888
|
+
await anchorFragment.removeChildren();
|
|
889
|
+
anchorStart.remove();
|
|
890
|
+
anchorEnd.remove();
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
anchorFragment.replaceContent = async function(child) {
|
|
894
|
+
const childElement = Validator.isElement(child) ? child : ElementCreator.getChild(child);
|
|
895
|
+
const parent = anchorEnd.parentNode;
|
|
896
|
+
if(!parent) {
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
// if(isParentUniqueChild(parent)) {
|
|
900
|
+
// parent.replaceChildren(anchorStart, childElement, anchorEnd);
|
|
901
|
+
// return;
|
|
902
|
+
// }
|
|
903
|
+
await anchorFragment.removeChildren();
|
|
904
|
+
parent.insertBefore(childElement, anchorEnd);
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
anchorFragment.setContent = anchorFragment.replaceContent;
|
|
908
|
+
|
|
909
|
+
anchorFragment.insertBefore = function(child, anchor = null) {
|
|
910
|
+
anchorFragment.appendChild(child, anchor);
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
anchorFragment.endElement = function() {
|
|
915
|
+
return anchorEnd;
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
anchorFragment.startElement = function() {
|
|
919
|
+
return anchorStart;
|
|
920
|
+
};
|
|
921
|
+
anchorFragment.restore = function() {
|
|
922
|
+
anchorFragment.appendChild(anchorFragment);
|
|
923
|
+
};
|
|
924
|
+
anchorFragment.clear = anchorFragment.remove;
|
|
925
|
+
anchorFragment.detach = anchorFragment.remove;
|
|
926
|
+
|
|
927
|
+
anchorFragment.getByIndex = function(index) {
|
|
928
|
+
let currentNode = anchorStart;
|
|
929
|
+
for(let i = 0; i <= index; i++) {
|
|
930
|
+
if(!currentNode.nextSibling) {
|
|
931
|
+
return null;
|
|
932
|
+
}
|
|
933
|
+
currentNode = currentNode.nextSibling;
|
|
934
|
+
}
|
|
935
|
+
return currentNode !== anchorStart ? currentNode : null;
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
return anchorFragment;
|
|
939
|
+
}
|
|
779
940
|
/**
|
|
780
|
-
* Creates an ObservableWhen that represents whether the observable equals a specific value.
|
|
781
|
-
* Returns an object that can be subscribed to and will emit true/false.
|
|
782
941
|
*
|
|
942
|
+
* @param {HTMLElement|DocumentFragment|Text|String|Array} children
|
|
943
|
+
* @param {{ parent?: HTMLElement, name?: String}} configs
|
|
944
|
+
* @returns {DocumentFragment}
|
|
945
|
+
*/
|
|
946
|
+
function createPortal(children, { parent, name = 'unnamed' } = {}) {
|
|
947
|
+
const anchor = Anchor('Portal '+name);
|
|
948
|
+
anchor.appendChild(ElementCreator.getChild(children));
|
|
949
|
+
|
|
950
|
+
(parent || document.body).appendChild(anchor);
|
|
951
|
+
return anchor;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
DocumentFragment.prototype.setAttribute = () => {};
|
|
955
|
+
|
|
956
|
+
const BOOLEAN_ATTRIBUTES = new Set([
|
|
957
|
+
'checked',
|
|
958
|
+
'selected',
|
|
959
|
+
'disabled',
|
|
960
|
+
'readonly',
|
|
961
|
+
'required',
|
|
962
|
+
'autofocus',
|
|
963
|
+
'multiple',
|
|
964
|
+
'autocomplete',
|
|
965
|
+
'hidden',
|
|
966
|
+
'contenteditable',
|
|
967
|
+
'spellcheck',
|
|
968
|
+
'translate',
|
|
969
|
+
'draggable',
|
|
970
|
+
'async',
|
|
971
|
+
'defer',
|
|
972
|
+
'autoplay',
|
|
973
|
+
'controls',
|
|
974
|
+
'loop',
|
|
975
|
+
'muted',
|
|
976
|
+
'download',
|
|
977
|
+
'reversed',
|
|
978
|
+
'open',
|
|
979
|
+
'default',
|
|
980
|
+
'formnovalidate',
|
|
981
|
+
'novalidate',
|
|
982
|
+
'scoped',
|
|
983
|
+
'itemscope',
|
|
984
|
+
'allowfullscreen',
|
|
985
|
+
'allowpaymentrequest',
|
|
986
|
+
'playsinline'
|
|
987
|
+
]);
|
|
988
|
+
|
|
989
|
+
const MemoryManager = (function() {
|
|
990
|
+
|
|
991
|
+
let $nextObserverId = 0;
|
|
992
|
+
const $observables = new Map();
|
|
993
|
+
|
|
994
|
+
return {
|
|
995
|
+
/**
|
|
996
|
+
* Register an observable and return an id.
|
|
997
|
+
*
|
|
998
|
+
* @param {ObservableItem} observable
|
|
999
|
+
* @param {Function} getListeners
|
|
1000
|
+
* @returns {number}
|
|
1001
|
+
*/
|
|
1002
|
+
register(observable) {
|
|
1003
|
+
const id = ++$nextObserverId;
|
|
1004
|
+
$observables.set(id, new WeakRef(observable));
|
|
1005
|
+
return id;
|
|
1006
|
+
},
|
|
1007
|
+
unregister(id) {
|
|
1008
|
+
$observables.delete(id);
|
|
1009
|
+
},
|
|
1010
|
+
getObservableById(id) {
|
|
1011
|
+
return $observables.get(id)?.deref();
|
|
1012
|
+
},
|
|
1013
|
+
cleanup() {
|
|
1014
|
+
for (const [_, weakObservableRef] of $observables) {
|
|
1015
|
+
const observable = weakObservableRef.deref();
|
|
1016
|
+
if (observable) {
|
|
1017
|
+
observable.cleanup();
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
$observables.clear();
|
|
1021
|
+
},
|
|
1022
|
+
/**
|
|
1023
|
+
* Clean observables that are not referenced anymore.
|
|
1024
|
+
* @param {number} threshold
|
|
1025
|
+
*/
|
|
1026
|
+
cleanObservables(threshold) {
|
|
1027
|
+
if($observables.size < threshold) return;
|
|
1028
|
+
let cleanedCount = 0;
|
|
1029
|
+
for (const [id, weakObservableRef] of $observables) {
|
|
1030
|
+
if (!weakObservableRef.deref()) {
|
|
1031
|
+
$observables.delete(id);
|
|
1032
|
+
cleanedCount++;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
if (cleanedCount > 0) {
|
|
1036
|
+
DebugManager.log('Memory Auto Clean', `🧹 Cleaned ${cleanedCount} orphaned observables`);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
}());
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* Creates an ObservableWhen that tracks whether an observable equals a specific value.
|
|
1044
|
+
*
|
|
1045
|
+
* @param {ObservableItem} observer - The observable to watch
|
|
783
1046
|
* @param {*} value - The value to compare against
|
|
784
|
-
* @
|
|
785
|
-
* @example
|
|
786
|
-
* const status = Observable('idle');
|
|
787
|
-
* const isLoading = status.when('loading');
|
|
788
|
-
* isLoading.subscribe(active => console.log('Loading:', active));
|
|
789
|
-
* status.set('loading'); // Logs: "Loading: true"
|
|
1047
|
+
* @class ObservableWhen
|
|
790
1048
|
*/
|
|
791
|
-
|
|
792
|
-
|
|
1049
|
+
const ObservableWhen = function(observer, value) {
|
|
1050
|
+
this.$target = value;
|
|
1051
|
+
this.$observer = observer;
|
|
793
1052
|
};
|
|
794
1053
|
|
|
1054
|
+
ObservableWhen.prototype.__$isObservableWhen = true;
|
|
1055
|
+
|
|
795
1056
|
/**
|
|
796
|
-
*
|
|
1057
|
+
* Subscribes to changes in the match status (true when observable equals target value).
|
|
797
1058
|
*
|
|
798
|
-
* @param {
|
|
799
|
-
* @returns {
|
|
1059
|
+
* @param {Function} callback - Function called with boolean indicating if values match
|
|
1060
|
+
* @returns {Function} Unsubscribe function
|
|
800
1061
|
* @example
|
|
801
|
-
* const
|
|
802
|
-
* const
|
|
803
|
-
*
|
|
804
|
-
* a.equals(b); // true
|
|
805
|
-
* a.equals(10); // false
|
|
1062
|
+
* const status = Observable('idle');
|
|
1063
|
+
* const isLoading = status.when('loading');
|
|
1064
|
+
* isLoading.subscribe(active => console.log('Loading:', active));
|
|
806
1065
|
*/
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
return this.$currentValue === other.$currentValue;
|
|
810
|
-
}
|
|
811
|
-
return this.$currentValue === other;
|
|
1066
|
+
ObservableWhen.prototype.subscribe = function(callback) {
|
|
1067
|
+
return this.$observer.on(this.$target, callback);
|
|
812
1068
|
};
|
|
813
1069
|
|
|
814
1070
|
/**
|
|
815
|
-
*
|
|
1071
|
+
* Returns true if the observable's current value equals the target value.
|
|
816
1072
|
*
|
|
817
|
-
* @returns {boolean}
|
|
818
|
-
* @example
|
|
819
|
-
* const count = Observable(0);
|
|
820
|
-
* count.toBool(); // false
|
|
821
|
-
* count.set(5);
|
|
822
|
-
* count.toBool(); // true
|
|
1073
|
+
* @returns {boolean} True if observable value matches target value
|
|
823
1074
|
*/
|
|
824
|
-
|
|
825
|
-
return
|
|
1075
|
+
ObservableWhen.prototype.val = function() {
|
|
1076
|
+
return this.$observer.$currentValue === this.$target;
|
|
826
1077
|
};
|
|
827
1078
|
|
|
828
1079
|
/**
|
|
829
|
-
*
|
|
1080
|
+
* Returns true if the observable's current value equals the target value.
|
|
1081
|
+
* Alias for val().
|
|
830
1082
|
*
|
|
831
|
-
* @
|
|
832
|
-
* const isOpen = Observable(false);
|
|
833
|
-
* isOpen.toggle(); // Now true
|
|
834
|
-
* isOpen.toggle(); // Now false
|
|
1083
|
+
* @returns {boolean} True if observable value matches target value
|
|
835
1084
|
*/
|
|
836
|
-
|
|
837
|
-
this.set(!this.$currentValue);
|
|
838
|
-
};
|
|
1085
|
+
ObservableWhen.prototype.isMatch = ObservableWhen.prototype.val;
|
|
839
1086
|
|
|
840
1087
|
/**
|
|
841
|
-
*
|
|
842
|
-
*
|
|
1088
|
+
* Returns true if the observable's current value equals the target value.
|
|
1089
|
+
* Alias for val().
|
|
843
1090
|
*
|
|
844
|
-
* @
|
|
845
|
-
* const count = Observable(0, { reset: true });
|
|
846
|
-
* count.set(10);
|
|
847
|
-
* count.reset(); // Back to 0
|
|
1091
|
+
* @returns {boolean} True if observable value matches target value
|
|
848
1092
|
*/
|
|
849
|
-
|
|
850
|
-
if(!this.configs?.reset) {
|
|
851
|
-
return;
|
|
852
|
-
}
|
|
853
|
-
const resetValue = (Validator.isObject(this.$initialValue))
|
|
854
|
-
? deepClone(this.$initialValue, (observable) => {
|
|
855
|
-
observable.reset();
|
|
856
|
-
})
|
|
857
|
-
: this.$initialValue;
|
|
858
|
-
this.set(resetValue);
|
|
859
|
-
};
|
|
1093
|
+
ObservableWhen.prototype.isActive = ObservableWhen.prototype.val;
|
|
860
1094
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1095
|
+
const nextTick = function(fn) {
|
|
1096
|
+
let pending = false;
|
|
1097
|
+
return function(...args) {
|
|
1098
|
+
if (pending) return;
|
|
1099
|
+
pending = true;
|
|
1100
|
+
|
|
1101
|
+
Promise.resolve().then(() => {
|
|
1102
|
+
fn.apply(this, args);
|
|
1103
|
+
pending = false;
|
|
1104
|
+
});
|
|
1105
|
+
};
|
|
868
1106
|
};
|
|
869
1107
|
|
|
870
1108
|
/**
|
|
871
|
-
* Returns the primitive value of the observable (its current value).
|
|
872
|
-
* Called automatically in type coercion contexts.
|
|
873
1109
|
*
|
|
874
|
-
* @
|
|
1110
|
+
* @param {*} item
|
|
1111
|
+
* @param {string|null} defaultKey
|
|
1112
|
+
* @param {?Function} key
|
|
1113
|
+
* @returns {*}
|
|
875
1114
|
*/
|
|
876
|
-
|
|
877
|
-
|
|
1115
|
+
const getKey = (item, defaultKey, key) => {
|
|
1116
|
+
if (Validator.isString(key)) {
|
|
1117
|
+
const val = Validator.isObservable(item) ? item.val() : item;
|
|
1118
|
+
const result = val?.[key];
|
|
1119
|
+
return Validator.isObservable(result) ? result.val() : (result ?? defaultKey);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
if (Validator.isFunction(key)) {
|
|
1123
|
+
return key(item, defaultKey);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
const val = Validator.isObservable(item) ? item.val() : item;
|
|
1127
|
+
return val ?? defaultKey;
|
|
878
1128
|
};
|
|
879
1129
|
|
|
880
|
-
const
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
mountedSupposedSize: 0,
|
|
884
|
-
unmounted: new WeakMap(),
|
|
885
|
-
unmountedSupposedSize: 0,
|
|
886
|
-
observer: null,
|
|
1130
|
+
const trim = function(str, char) {
|
|
1131
|
+
return str.replace(new RegExp(`^[${char}]+|[${char}]+$`, 'g'), '');
|
|
1132
|
+
};
|
|
887
1133
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
if(
|
|
891
|
-
return;
|
|
892
|
-
}
|
|
893
|
-
data.inDom = true;
|
|
894
|
-
if(!data.mounted) {
|
|
895
|
-
return;
|
|
896
|
-
}
|
|
897
|
-
if(Array.isArray(data.mounted)) {
|
|
898
|
-
for(const cb of data.mounted) {
|
|
899
|
-
cb(node);
|
|
900
|
-
}
|
|
901
|
-
return;
|
|
1134
|
+
const deepClone = (value, onObservableFound) => {
|
|
1135
|
+
try {
|
|
1136
|
+
if(window.structuredClone !== undefined) {
|
|
1137
|
+
return window.structuredClone(value);
|
|
902
1138
|
}
|
|
903
|
-
|
|
904
|
-
},
|
|
1139
|
+
} catch (e){}
|
|
905
1140
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
return;
|
|
910
|
-
}
|
|
911
|
-
data.inDom = false;
|
|
912
|
-
if(!data.unmounted) {
|
|
913
|
-
return;
|
|
914
|
-
}
|
|
1141
|
+
if (value === null || typeof value !== 'object') {
|
|
1142
|
+
return value;
|
|
1143
|
+
}
|
|
915
1144
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1145
|
+
// Dates
|
|
1146
|
+
if (value instanceof Date) {
|
|
1147
|
+
return new Date(value.getTime());
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// Arrays
|
|
1151
|
+
if (Array.isArray(value)) {
|
|
1152
|
+
return value.map(item => deepClone(item));
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// Observables - keep the référence
|
|
1156
|
+
if (Validator.isObservable(value)) {
|
|
1157
|
+
onObservableFound && onObservableFound(value);
|
|
1158
|
+
return value;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// Objects
|
|
1162
|
+
const cloned = {};
|
|
1163
|
+
for (const key in value) {
|
|
1164
|
+
if (Object.hasOwn(value, key)) {
|
|
1165
|
+
cloned[key] = deepClone(value[key]);
|
|
925
1166
|
}
|
|
1167
|
+
}
|
|
1168
|
+
return cloned;
|
|
1169
|
+
};
|
|
926
1170
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1171
|
+
const LocalStorage = {
|
|
1172
|
+
getJson(key) {
|
|
1173
|
+
let value = localStorage.getItem(key);
|
|
1174
|
+
try {
|
|
1175
|
+
return JSON.parse(value);
|
|
1176
|
+
} catch (e) {
|
|
1177
|
+
throw new NativeDocumentError('invalid_json:'+key);
|
|
930
1178
|
}
|
|
931
1179
|
},
|
|
1180
|
+
getNumber(key) {
|
|
1181
|
+
return Number(this.get(key));
|
|
1182
|
+
},
|
|
1183
|
+
getBool(key) {
|
|
1184
|
+
const value = this.get(key);
|
|
1185
|
+
return value === 'true' || value === '1';
|
|
1186
|
+
},
|
|
1187
|
+
setJson(key, value) {
|
|
1188
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
1189
|
+
},
|
|
1190
|
+
setBool(key, value) {
|
|
1191
|
+
localStorage.setItem(key, value ? 'true' : 'false');
|
|
1192
|
+
},
|
|
1193
|
+
get(key, defaultValue = null) {
|
|
1194
|
+
return localStorage.getItem(key) || defaultValue;
|
|
1195
|
+
},
|
|
1196
|
+
set(key, value) {
|
|
1197
|
+
return localStorage.setItem(key, value);
|
|
1198
|
+
},
|
|
1199
|
+
remove(key) {
|
|
1200
|
+
localStorage.removeItem(key);
|
|
1201
|
+
},
|
|
1202
|
+
has(key) {
|
|
1203
|
+
return localStorage.getItem(key) != null;
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
932
1206
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
}
|
|
1207
|
+
const $getFromStorage = (key, value) => {
|
|
1208
|
+
if(!LocalStorage.has(key)) {
|
|
1209
|
+
return value;
|
|
1210
|
+
}
|
|
1211
|
+
switch (typeof value) {
|
|
1212
|
+
case 'object': return LocalStorage.getJson(key) ?? value;
|
|
1213
|
+
case 'boolean': return LocalStorage.getBool(key) ?? value;
|
|
1214
|
+
case 'number': return LocalStorage.getNumber(key) ?? value;
|
|
1215
|
+
default: return LocalStorage.get(key, value) ?? value;
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
947
1218
|
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
}
|
|
961
|
-
},
|
|
1219
|
+
const $saveToStorage = (value) => {
|
|
1220
|
+
switch (typeof value) {
|
|
1221
|
+
case 'object': return LocalStorage.setJson;
|
|
1222
|
+
case 'boolean': return LocalStorage.setBool;
|
|
1223
|
+
default: return LocalStorage.set;
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
|
|
1227
|
+
const StoreFactory = function() {
|
|
1228
|
+
|
|
1229
|
+
const $stores = new Map();
|
|
1230
|
+
const $followersCache = new Map();
|
|
962
1231
|
|
|
963
1232
|
/**
|
|
964
|
-
*
|
|
965
|
-
* @param {boolean} inDom
|
|
966
|
-
* @returns {{ disconnect: Function, mounted: Function, unmounted: Function, off: Function }}
|
|
1233
|
+
* Internal helper — retrieves a store entry or throws if not found.
|
|
967
1234
|
*/
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1235
|
+
const $getStoreOrThrow = (method, name) => {
|
|
1236
|
+
const item = $stores.get(name);
|
|
1237
|
+
if (!item) {
|
|
1238
|
+
DebugManager.error('Store', `Store.${method}('${name}') : store not found. Did you call Store.create('${name}') first?`);
|
|
1239
|
+
throw new NativeDocumentError(
|
|
1240
|
+
`Store.${method}('${name}') : store not found.`
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
return item;
|
|
1244
|
+
};
|
|
971
1245
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
if (unmountedRegistered) {
|
|
982
|
-
DocumentObserver.unmounted.delete(element);
|
|
983
|
-
DocumentObserver.unmountedSupposedSize--;
|
|
984
|
-
}
|
|
985
|
-
data = null;
|
|
986
|
-
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Internal helper — blocks write operations on a read-only observer.
|
|
1248
|
+
*/
|
|
1249
|
+
const $applyReadOnly = (observer, name, context) => {
|
|
1250
|
+
const readOnlyError = (method) => () => {
|
|
1251
|
+
DebugManager.error('Store', `Store.${context}('${name}') is read-only. '${method}()' is not allowed.`);
|
|
1252
|
+
throw new NativeDocumentError(
|
|
1253
|
+
`Store.${context}('${name}') is read-only.`
|
|
1254
|
+
);
|
|
987
1255
|
};
|
|
1256
|
+
observer.set = readOnlyError('set');
|
|
1257
|
+
observer.toggle = readOnlyError('toggle');
|
|
1258
|
+
observer.reset = readOnlyError('reset');
|
|
1259
|
+
};
|
|
988
1260
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1261
|
+
const $createObservable = (value, options = {}) => {
|
|
1262
|
+
if(Array.isArray(value)) {
|
|
1263
|
+
return Observable.array(value, options);
|
|
1264
|
+
}
|
|
1265
|
+
if(typeof value === 'object') {
|
|
1266
|
+
return Observable.object(value, options);
|
|
1267
|
+
}
|
|
1268
|
+
return Observable(value, options);
|
|
1269
|
+
};
|
|
1270
|
+
|
|
1271
|
+
const $api = {
|
|
1272
|
+
/**
|
|
1273
|
+
* Create a new state and return the observer.
|
|
1274
|
+
* Throws if a store with the same name already exists.
|
|
1275
|
+
*
|
|
1276
|
+
* @param {string} name
|
|
1277
|
+
* @param {*} value
|
|
1278
|
+
* @returns {ObservableItem}
|
|
1279
|
+
*/
|
|
1280
|
+
create(name, value) {
|
|
1281
|
+
if ($stores.has(name)) {
|
|
1282
|
+
DebugManager.warn('Store', `Store.create('${name}') : a store with this name already exists. Use Store.get('${name}') to retrieve it.`);
|
|
1283
|
+
throw new NativeDocumentError(
|
|
1284
|
+
`Store.create('${name}') : a store with this name already exists.`
|
|
1285
|
+
);
|
|
993
1286
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1287
|
+
const observer = $createObservable(value);
|
|
1288
|
+
$stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: false });
|
|
1289
|
+
return observer;
|
|
1290
|
+
},
|
|
1291
|
+
|
|
1292
|
+
/**
|
|
1293
|
+
* Create a new resettable state and return the observer.
|
|
1294
|
+
* The store can be reset to its initial value via Store.reset(name).
|
|
1295
|
+
* Throws if a store with the same name already exists.
|
|
1296
|
+
*
|
|
1297
|
+
* @param {string} name
|
|
1298
|
+
* @param {*} value
|
|
1299
|
+
* @returns {ObservableItem}
|
|
1300
|
+
*/
|
|
1301
|
+
createResettable(name, value) {
|
|
1302
|
+
if ($stores.has(name)) {
|
|
1303
|
+
DebugManager.warn('Store', `Store.createResettable('${name}') : a store with this name already exists.`);
|
|
1304
|
+
throw new NativeDocumentError(
|
|
1305
|
+
`Store.createResettable('${name}') : a store with this name already exists.`
|
|
1306
|
+
);
|
|
997
1307
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1308
|
+
const observer = $createObservable(value, { reset: true });
|
|
1309
|
+
$stores.set(name, { observer, subscribers: new Set(), resettable: true, composed: false });
|
|
1310
|
+
return observer;
|
|
1311
|
+
},
|
|
1000
1312
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1313
|
+
/**
|
|
1314
|
+
* Create a computed store derived from other stores.
|
|
1315
|
+
* The value is automatically recalculated when any dependency changes.
|
|
1316
|
+
* This store is read-only — Store.use() and Store.set() will throw.
|
|
1317
|
+
* Throws if a store with the same name already exists.
|
|
1318
|
+
*
|
|
1319
|
+
* @param {string} name
|
|
1320
|
+
* @param {() => *} computation - Function that returns the computed value
|
|
1321
|
+
* @param {string[]} dependencies - Names of the stores to watch
|
|
1322
|
+
* @returns {ObservableItem}
|
|
1323
|
+
*
|
|
1324
|
+
* @example
|
|
1325
|
+
* Store.create('products', [{ id: 1, price: 10 }]);
|
|
1326
|
+
* Store.create('cart', [{ productId: 1, quantity: 2 }]);
|
|
1327
|
+
*
|
|
1328
|
+
* Store.createComposed('total', () => {
|
|
1329
|
+
* const products = Store.get('products').val();
|
|
1330
|
+
* const cart = Store.get('cart').val();
|
|
1331
|
+
* return cart.reduce((sum, item) => {
|
|
1332
|
+
* const product = products.find(p => p.id === item.productId);
|
|
1333
|
+
* return sum + (product.price * item.quantity);
|
|
1334
|
+
* }, 0);
|
|
1335
|
+
* }, ['products', 'cart']);
|
|
1336
|
+
*/
|
|
1337
|
+
createComposed(name, computation, dependencies) {
|
|
1338
|
+
if ($stores.has(name)) {
|
|
1339
|
+
DebugManager.warn('Store', `Store.createComposed('${name}') : a store with this name already exists.`);
|
|
1340
|
+
throw new NativeDocumentError(
|
|
1341
|
+
`Store.createComposed('${name}') : a store with this name already exists.`
|
|
1342
|
+
);
|
|
1004
1343
|
}
|
|
1005
|
-
if(
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1344
|
+
if (typeof computation !== 'function') {
|
|
1345
|
+
throw new NativeDocumentError(
|
|
1346
|
+
`Store.createComposed('${name}') : computation must be a function.`
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
if (!Array.isArray(dependencies) || dependencies.length === 0) {
|
|
1350
|
+
throw new NativeDocumentError(
|
|
1351
|
+
`Store.createComposed('${name}') : dependencies must be a non-empty array of store names.`
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// Resolve dependency observers
|
|
1356
|
+
const depObservers = dependencies.map(depName => {
|
|
1357
|
+
if(typeof depName !== 'string') {
|
|
1358
|
+
return depName;
|
|
1012
1359
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1360
|
+
const depItem = $stores.get(depName);
|
|
1361
|
+
if (!depItem) {
|
|
1362
|
+
DebugManager.error('Store', `Store.createComposed('${name}') : dependency '${depName}' not found. Create it first.`);
|
|
1363
|
+
throw new NativeDocumentError(
|
|
1364
|
+
`Store.createComposed('${name}') : dependency store '${depName}' not found.`
|
|
1365
|
+
);
|
|
1015
1366
|
}
|
|
1016
|
-
return;
|
|
1017
|
-
}
|
|
1018
|
-
data[type] = null;
|
|
1019
|
-
};
|
|
1367
|
+
return depItem.observer;
|
|
1368
|
+
});
|
|
1020
1369
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1370
|
+
// Create computed observable from dependency observers
|
|
1371
|
+
const observer = Observable.computed(computation, depObservers);
|
|
1023
1372
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
if (!mountedRegistered) {
|
|
1028
|
-
DocumentObserver.mountedSupposedSize++;
|
|
1029
|
-
mountedRegistered = true;
|
|
1030
|
-
}
|
|
1031
|
-
},
|
|
1373
|
+
$stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: true });
|
|
1374
|
+
return observer;
|
|
1375
|
+
},
|
|
1032
1376
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1377
|
+
/**
|
|
1378
|
+
* Returns true if a store with the given name exists.
|
|
1379
|
+
*
|
|
1380
|
+
* @param {string} name
|
|
1381
|
+
* @returns {boolean}
|
|
1382
|
+
*/
|
|
1383
|
+
has(name) {
|
|
1384
|
+
return $stores.has(name);
|
|
1385
|
+
},
|
|
1041
1386
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1387
|
+
/**
|
|
1388
|
+
* Resets a resettable store to its initial value and notifies all subscribers.
|
|
1389
|
+
* Throws if the store was not created with createResettable().
|
|
1390
|
+
*
|
|
1391
|
+
* @param {string} name
|
|
1392
|
+
*/
|
|
1393
|
+
reset(name) {
|
|
1394
|
+
const item = $getStoreOrThrow('reset', name);
|
|
1395
|
+
if (item.composed) {
|
|
1396
|
+
DebugManager.error('Store', `Store.reset('${name}') : composed stores cannot be reset. Their value is derived from dependencies.`);
|
|
1397
|
+
throw new NativeDocumentError(
|
|
1398
|
+
`Store.reset('${name}') : composed stores cannot be reset.`
|
|
1399
|
+
);
|
|
1044
1400
|
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
});
|
|
1401
|
+
if (!item.resettable) {
|
|
1402
|
+
DebugManager.error('Store', `Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`);
|
|
1403
|
+
throw new NativeDocumentError(
|
|
1404
|
+
`Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
item.observer.reset();
|
|
1408
|
+
},
|
|
1054
1409
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1410
|
+
/**
|
|
1411
|
+
* Returns a two-way synchronized follower of the store.
|
|
1412
|
+
* Writing to the follower propagates the value back to the store and all its subscribers.
|
|
1413
|
+
* Throws if called on a composed store — use Store.follow() instead.
|
|
1414
|
+
* Call follower.destroy() or follower.dispose() to unsubscribe.
|
|
1415
|
+
*
|
|
1416
|
+
* @param {string} name
|
|
1417
|
+
* @returns {ObservableItem}
|
|
1418
|
+
*/
|
|
1419
|
+
use(name) {
|
|
1420
|
+
const item = $getStoreOrThrow('use', name);
|
|
1062
1421
|
|
|
1063
|
-
|
|
1422
|
+
if (item.composed) {
|
|
1423
|
+
DebugManager.error('Store', `Store.use('${name}') : composed stores are read-only. Use Store.follow('${name}') instead.`);
|
|
1424
|
+
throw new NativeDocumentError(
|
|
1425
|
+
`Store.use('${name}') : composed stores are read-only. Use Store.follow('${name}') instead.`
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1064
1428
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
};
|
|
1429
|
+
const { observer: originalObserver, subscribers } = item;
|
|
1430
|
+
const observerFollower = $createObservable(originalObserver.val());
|
|
1068
1431
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
return this;
|
|
1072
|
-
};
|
|
1432
|
+
const onStoreChange = value => observerFollower.set(value);
|
|
1433
|
+
const onFollowerChange = value => originalObserver.set(value);
|
|
1073
1434
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
return this;
|
|
1077
|
-
};
|
|
1435
|
+
originalObserver.subscribe(onStoreChange);
|
|
1436
|
+
observerFollower.subscribe(onFollowerChange);
|
|
1078
1437
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
elementChildren = null;
|
|
1087
|
-
}
|
|
1088
|
-
element = null;
|
|
1089
|
-
return this;
|
|
1090
|
-
};
|
|
1438
|
+
observerFollower.destroy = () => {
|
|
1439
|
+
originalObserver.unsubscribe(onStoreChange);
|
|
1440
|
+
observerFollower.unsubscribe(onFollowerChange);
|
|
1441
|
+
subscribers.delete(observerFollower);
|
|
1442
|
+
observerFollower.cleanup();
|
|
1443
|
+
};
|
|
1444
|
+
observerFollower.dispose = observerFollower.destroy;
|
|
1091
1445
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
element.$ndProx = null;
|
|
1096
|
-
delete element.nd?.on?.prevent;
|
|
1097
|
-
delete element.nd?.on;
|
|
1098
|
-
delete element.nd;
|
|
1099
|
-
element = null;
|
|
1100
|
-
return this;
|
|
1101
|
-
};
|
|
1446
|
+
subscribers.add(observerFollower);
|
|
1447
|
+
return observerFollower;
|
|
1448
|
+
},
|
|
1102
1449
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1450
|
+
/**
|
|
1451
|
+
* Returns a read-only follower of the store.
|
|
1452
|
+
* The follower reflects store changes but cannot write back to the store.
|
|
1453
|
+
* Any attempt to call .set(), .toggle() or .reset() will throw.
|
|
1454
|
+
* Call follower.destroy() or follower.dispose() to unsubscribe.
|
|
1455
|
+
*
|
|
1456
|
+
* @param {string} name
|
|
1457
|
+
* @returns {ObservableItem}
|
|
1458
|
+
*/
|
|
1459
|
+
follow(name) {
|
|
1460
|
+
const { observer: originalObserver, subscribers } = $getStoreOrThrow('follow', name);
|
|
1461
|
+
const observerFollower = $createObservable(originalObserver.val());
|
|
1105
1462
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
this.$observer.mounted(states.mounted);
|
|
1109
|
-
}
|
|
1110
|
-
if(states.unmounted) {
|
|
1111
|
-
this.$element.setAttribute('data--nd-unmounted', '1');
|
|
1112
|
-
this.$observer.unmounted(states.unmounted);
|
|
1113
|
-
}
|
|
1114
|
-
return this;
|
|
1115
|
-
};
|
|
1463
|
+
const onStoreChange = value => observerFollower.set(value);
|
|
1464
|
+
originalObserver.subscribe(onStoreChange);
|
|
1116
1465
|
|
|
1117
|
-
|
|
1118
|
-
return this.lifecycle({ mounted: callback });
|
|
1119
|
-
};
|
|
1466
|
+
$applyReadOnly(observerFollower, name, 'follow');
|
|
1120
1467
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1468
|
+
observerFollower.destroy = () => {
|
|
1469
|
+
originalObserver.unsubscribe(onStoreChange);
|
|
1470
|
+
subscribers.delete(observerFollower);
|
|
1471
|
+
observerFollower.cleanup();
|
|
1472
|
+
};
|
|
1473
|
+
observerFollower.dispose = observerFollower.destroy;
|
|
1124
1474
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1475
|
+
subscribers.add(observerFollower);
|
|
1476
|
+
return observerFollower;
|
|
1477
|
+
},
|
|
1127
1478
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1479
|
+
/**
|
|
1480
|
+
* Returns the raw store observer directly (no follower, no cleanup contract).
|
|
1481
|
+
* Use this for direct read access when you don't need to unsubscribe.
|
|
1482
|
+
* WARNING : mutations on this observer impact all subscribers immediately.
|
|
1483
|
+
*
|
|
1484
|
+
* @param {string} name
|
|
1485
|
+
* @returns {ObservableItem|null}
|
|
1486
|
+
*/
|
|
1487
|
+
get(name) {
|
|
1488
|
+
const item = $stores.get(name);
|
|
1489
|
+
if (!item) {
|
|
1490
|
+
DebugManager.warn('Store', `Store.get('${name}') : store not found.`);
|
|
1491
|
+
return null;
|
|
1492
|
+
}
|
|
1493
|
+
return item.observer;
|
|
1494
|
+
},
|
|
1131
1495
|
|
|
1132
|
-
|
|
1496
|
+
/**
|
|
1497
|
+
* @param {string} name
|
|
1498
|
+
* @returns {{ observer: ObservableItem, subscribers: Set } | null}
|
|
1499
|
+
*/
|
|
1500
|
+
getWithSubscribers(name) {
|
|
1501
|
+
return $stores.get(name) ?? null;
|
|
1502
|
+
},
|
|
1133
1503
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1504
|
+
/**
|
|
1505
|
+
* Destroys a store : cleans up the observer, destroys all followers, and removes the entry.
|
|
1506
|
+
*
|
|
1507
|
+
* @param {string} name
|
|
1508
|
+
*/
|
|
1509
|
+
delete(name) {
|
|
1510
|
+
const item = $stores.get(name);
|
|
1511
|
+
if (!item) {
|
|
1512
|
+
DebugManager.warn('Store', `Store.delete('${name}') : store not found, nothing to delete.`);
|
|
1136
1513
|
return;
|
|
1137
1514
|
}
|
|
1138
|
-
|
|
1515
|
+
item.subscribers.forEach(follower => follower.destroy());
|
|
1516
|
+
item.subscribers.clear();
|
|
1517
|
+
item.observer.cleanup();
|
|
1518
|
+
$stores.delete(name);
|
|
1519
|
+
},
|
|
1520
|
+
/**
|
|
1521
|
+
* Creates an isolated store group with its own state namespace.
|
|
1522
|
+
* Each group is a fully independent StoreFactory instance —
|
|
1523
|
+
* no key conflicts, no shared state with the parent store.
|
|
1524
|
+
*
|
|
1525
|
+
* @param {string | ((group: ReturnType<typeof StoreFactory>) => void)} name - Group name for debugging, or setup callback if no name is provided
|
|
1526
|
+
* @param {((group: ReturnType<typeof StoreFactory>) => void)} [callback] - Setup function receiving the isolated store instance
|
|
1527
|
+
* @returns {ReturnType<typeof StoreFactory>}
|
|
1528
|
+
*
|
|
1529
|
+
* @example
|
|
1530
|
+
* // With name (recommended)
|
|
1531
|
+
* const EventStore = Store.group('events', (group) => {
|
|
1532
|
+
* group.create('catalog', []);
|
|
1533
|
+
* group.create('filters', { category: null, date: null });
|
|
1534
|
+
* group.createResettable('selected', null);
|
|
1535
|
+
* group.createComposed('filtered', () => {
|
|
1536
|
+
* const catalog = EventStore.get('catalog').val();
|
|
1537
|
+
* const filters = EventStore.get('filters').val();
|
|
1538
|
+
* return catalog.filter(event => {
|
|
1539
|
+
* if (filters.category && event.category !== filters.category) return false;
|
|
1540
|
+
* return true;
|
|
1541
|
+
* });
|
|
1542
|
+
* }, ['catalog', 'filters']);
|
|
1543
|
+
* });
|
|
1544
|
+
*
|
|
1545
|
+
* // Without name
|
|
1546
|
+
* const CartStore = Store.group((group) => {
|
|
1547
|
+
* group.create('items', []);
|
|
1548
|
+
* });
|
|
1549
|
+
*
|
|
1550
|
+
* // Usage
|
|
1551
|
+
* EventStore.use('catalog'); // two-way follower
|
|
1552
|
+
* EventStore.follow('filtered'); // read-only follower
|
|
1553
|
+
* EventStore.get('filters'); // raw observable
|
|
1554
|
+
*
|
|
1555
|
+
* // Cross-group composed
|
|
1556
|
+
* const OrderStore = Store.group('orders', (group) => {
|
|
1557
|
+
* group.createComposed('summary', () => {
|
|
1558
|
+
* const items = CartStore.get('items').val();
|
|
1559
|
+
* const events = EventStore.get('catalog').val();
|
|
1560
|
+
* return { items, events };
|
|
1561
|
+
* }, [CartStore.get('items'), EventStore.get('catalog')]);
|
|
1562
|
+
* });
|
|
1563
|
+
*/
|
|
1564
|
+
group(name, callback) {
|
|
1565
|
+
if (typeof name === 'function') {
|
|
1566
|
+
callback = name;
|
|
1567
|
+
name = 'anonymous';
|
|
1568
|
+
}
|
|
1569
|
+
const store = StoreFactory();
|
|
1570
|
+
callback && callback(store);
|
|
1571
|
+
return store;
|
|
1572
|
+
},
|
|
1573
|
+
createPersistent(name, value, localstorage_key) {
|
|
1574
|
+
localstorage_key = localstorage_key || name;
|
|
1575
|
+
const observer = this.create(name, $getFromStorage(localstorage_key, value));
|
|
1576
|
+
const saver = $saveToStorage(value);
|
|
1139
1577
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1578
|
+
observer.subscribe((val) => saver(localstorage_key, val));
|
|
1579
|
+
return observer;
|
|
1580
|
+
},
|
|
1581
|
+
createPersistentResettable(name, value, localstorage_key) {
|
|
1582
|
+
localstorage_key = localstorage_key || name;
|
|
1583
|
+
const observer = this.createResettable(name, $getFromStorage(localstorage_key, value));
|
|
1584
|
+
const saver = $saveToStorage(value);
|
|
1585
|
+
observer.subscribe((val) => saver(localstorage_key, val));
|
|
1586
|
+
|
|
1587
|
+
const originalReset = observer.reset.bind(observer);
|
|
1588
|
+
observer.reset = () => {
|
|
1589
|
+
LocalStorage.remove(localstorage_key);
|
|
1590
|
+
originalReset();
|
|
1591
|
+
};
|
|
1592
|
+
|
|
1593
|
+
return observer;
|
|
1594
|
+
}
|
|
1595
|
+
};
|
|
1596
|
+
|
|
1597
|
+
|
|
1598
|
+
return new Proxy($api, {
|
|
1599
|
+
get(target, prop) {
|
|
1600
|
+
if (typeof prop === 'symbol' || prop.startsWith('$') || prop in target) {
|
|
1601
|
+
return target[prop];
|
|
1602
|
+
}
|
|
1603
|
+
if (target.has(prop)) {
|
|
1604
|
+
if ($followersCache.has(prop)) {
|
|
1605
|
+
return $followersCache.get(prop);
|
|
1144
1606
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1607
|
+
const follower = target.follow(prop);
|
|
1608
|
+
$followersCache.set(prop, follower);
|
|
1609
|
+
return follower;
|
|
1148
1610
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1611
|
+
return undefined;
|
|
1612
|
+
},
|
|
1613
|
+
set(target, prop, value) {
|
|
1614
|
+
DebugManager.error('Store', `Forbidden: You cannot overwrite the store key '${String(prop)}'. Use .use('${String(prop)}').set(value) instead.`);
|
|
1615
|
+
throw new NativeDocumentError(`Store structure is immutable. Use .set() on the observable.`);
|
|
1616
|
+
},
|
|
1617
|
+
deleteProperty(target, prop) {
|
|
1618
|
+
throw new NativeDocumentError(`Store keys cannot be deleted.`);
|
|
1619
|
+
}
|
|
1620
|
+
});
|
|
1154
1621
|
};
|
|
1155
1622
|
|
|
1156
|
-
|
|
1157
|
-
return this.$element;
|
|
1158
|
-
};
|
|
1623
|
+
const Store = StoreFactory();
|
|
1159
1624
|
|
|
1160
|
-
|
|
1625
|
+
Store.create('locale', 'fr');
|
|
1161
1626
|
|
|
1162
|
-
|
|
1163
|
-
const
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1627
|
+
const $parseDateParts = (value, locale) => {
|
|
1628
|
+
const d = new Date(value);
|
|
1629
|
+
return {
|
|
1630
|
+
d,
|
|
1631
|
+
parts: new Intl.DateTimeFormat(locale, {
|
|
1632
|
+
year: 'numeric',
|
|
1633
|
+
month: 'long',
|
|
1634
|
+
day: '2-digit',
|
|
1635
|
+
hour: '2-digit',
|
|
1636
|
+
minute: '2-digit',
|
|
1637
|
+
second: '2-digit',
|
|
1638
|
+
}).formatToParts(d).reduce((acc, { type, value }) => {
|
|
1639
|
+
acc[type] = value;
|
|
1640
|
+
return acc;
|
|
1641
|
+
}, {})
|
|
1642
|
+
};
|
|
1643
|
+
};
|
|
1644
|
+
|
|
1645
|
+
const $applyDatePattern = (pattern, d, parts) => {
|
|
1646
|
+
const pad = n => String(n).padStart(2, '0');
|
|
1647
|
+
return pattern
|
|
1648
|
+
.replace('YYYY', parts.year)
|
|
1649
|
+
.replace('YY', parts.year.slice(-2))
|
|
1650
|
+
.replace('MMMM', parts.month)
|
|
1651
|
+
.replace('MMM', parts.month.slice(0, 3))
|
|
1652
|
+
.replace('MM', pad(d.getMonth() + 1))
|
|
1653
|
+
.replace('DD', pad(d.getDate()))
|
|
1654
|
+
.replace('D', d.getDate())
|
|
1655
|
+
.replace('HH', parts.hour)
|
|
1656
|
+
.replace('mm', parts.minute)
|
|
1657
|
+
.replace('ss', parts.second);
|
|
1658
|
+
};
|
|
1659
|
+
|
|
1660
|
+
const Formatters = {
|
|
1661
|
+
|
|
1662
|
+
currency: (value, locale, { currency = 'XOF', notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
|
|
1663
|
+
new Intl.NumberFormat(locale, {
|
|
1664
|
+
style: 'currency',
|
|
1665
|
+
currency,
|
|
1666
|
+
notation,
|
|
1667
|
+
minimumFractionDigits,
|
|
1668
|
+
maximumFractionDigits
|
|
1669
|
+
}).format(value),
|
|
1670
|
+
|
|
1671
|
+
number: (value, locale, { notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
|
|
1672
|
+
new Intl.NumberFormat(locale, {
|
|
1673
|
+
notation,
|
|
1674
|
+
minimumFractionDigits,
|
|
1675
|
+
maximumFractionDigits
|
|
1676
|
+
}).format(value),
|
|
1677
|
+
|
|
1678
|
+
percent: (value, locale, { decimals = 1 } = {}) =>
|
|
1679
|
+
new Intl.NumberFormat(locale, {
|
|
1680
|
+
style: 'percent',
|
|
1681
|
+
maximumFractionDigits: decimals
|
|
1682
|
+
}).format(value),
|
|
1683
|
+
|
|
1684
|
+
date: (value, locale, { format, dateStyle = 'long' } = {}) => {
|
|
1685
|
+
if (format) {
|
|
1686
|
+
const { d, parts } = $parseDateParts(value, locale);
|
|
1687
|
+
return $applyDatePattern(format, d, parts);
|
|
1688
|
+
}
|
|
1689
|
+
return new Intl.DateTimeFormat(locale, { dateStyle }).format(new Date(value));
|
|
1690
|
+
},
|
|
1174
1691
|
|
|
1175
|
-
|
|
1176
|
-
|
|
1692
|
+
time: (value, locale, { format, hour = '2-digit', minute = '2-digit', second } = {}) => {
|
|
1693
|
+
if (format) {
|
|
1694
|
+
const { d, parts } = $parseDateParts(value, locale);
|
|
1695
|
+
return $applyDatePattern(format, d, parts);
|
|
1696
|
+
}
|
|
1697
|
+
return new Intl.DateTimeFormat(locale, { hour, minute, second }).format(new Date(value));
|
|
1698
|
+
},
|
|
1177
1699
|
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1700
|
+
datetime: (value, locale, { format, dateStyle = 'long', hour = '2-digit', minute = '2-digit', second } = {}) => {
|
|
1701
|
+
if (format) {
|
|
1702
|
+
const { d, parts } = $parseDateParts(value, locale);
|
|
1703
|
+
return $applyDatePattern(format, d, parts);
|
|
1704
|
+
}
|
|
1705
|
+
return new Intl.DateTimeFormat(locale, { dateStyle, hour, minute, second }).format(new Date(value));
|
|
1706
|
+
},
|
|
1181
1707
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1708
|
+
relative: (value, locale, { unit = 'day', numeric = 'auto' } = {}) => {
|
|
1709
|
+
const diff = Math.round((value - Date.now()) / (1000 * 60 * 60 * 24));
|
|
1710
|
+
return new Intl.RelativeTimeFormat(locale, { numeric }).format(diff, unit);
|
|
1711
|
+
},
|
|
1712
|
+
|
|
1713
|
+
plural: (value, locale, { singular, plural } = {}) => {
|
|
1714
|
+
const rule = new Intl.PluralRules(locale).select(value);
|
|
1715
|
+
return `${value} ${rule === 'one' ? singular : plural}`;
|
|
1716
|
+
},
|
|
1184
1717
|
};
|
|
1185
1718
|
|
|
1186
1719
|
/**
|
|
1187
|
-
* Attaches a template binding to the element by hydrating it with the specified method.
|
|
1188
1720
|
*
|
|
1189
|
-
* @param {
|
|
1190
|
-
* @param {
|
|
1191
|
-
* @
|
|
1192
|
-
* @example
|
|
1193
|
-
* const onClick = $binder.attach((event, data) => console.log(data));
|
|
1194
|
-
* element.nd.attach('onClick', onClick);
|
|
1721
|
+
* @param {*} value
|
|
1722
|
+
* @param {{ propagation: boolean, reset: boolean} | null} configs
|
|
1723
|
+
* @class ObservableItem
|
|
1195
1724
|
*/
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
return this.$element;
|
|
1199
|
-
};
|
|
1725
|
+
function ObservableItem(value, configs = null) {
|
|
1726
|
+
value = Validator.isObservable(value) ? value.val() : value;
|
|
1200
1727
|
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
* Methods are bound to the instance and available for chaining.
|
|
1204
|
-
*
|
|
1205
|
-
* @param {Object} methods - Object containing method definitions
|
|
1206
|
-
* @returns {this} The NDElement instance with added methods for chaining
|
|
1207
|
-
* @example
|
|
1208
|
-
* element.nd.with({
|
|
1209
|
-
* highlight() {
|
|
1210
|
-
* this.$element.style.background = 'yellow';
|
|
1211
|
-
* return this;
|
|
1212
|
-
* }
|
|
1213
|
-
* }).highlight().onClick(() => console.log('Clicked'));
|
|
1214
|
-
*/
|
|
1215
|
-
NDElement.prototype.with = function(methods) {
|
|
1216
|
-
if (!methods || typeof methods !== 'object') {
|
|
1217
|
-
throw new NativeDocumentError('extend() requires an object of methods');
|
|
1218
|
-
}
|
|
1728
|
+
this.$previousValue = null;
|
|
1729
|
+
this.$currentValue = value;
|
|
1219
1730
|
{
|
|
1220
|
-
|
|
1221
|
-
this.$localExtensions = new Map();
|
|
1222
|
-
}
|
|
1731
|
+
this.$isCleanedUp = false;
|
|
1223
1732
|
}
|
|
1224
1733
|
|
|
1225
|
-
|
|
1226
|
-
|
|
1734
|
+
this.$firstListener = null;
|
|
1735
|
+
this.$listeners = null;
|
|
1736
|
+
this.$watchers = null;
|
|
1227
1737
|
|
|
1228
|
-
|
|
1229
|
-
console.warn(`⚠️ extends(): "${name}" is not a function, skipping`);
|
|
1230
|
-
continue;
|
|
1231
|
-
}
|
|
1232
|
-
{
|
|
1233
|
-
if (this[name] && !this.$localExtensions.has(name)) {
|
|
1234
|
-
DebugManager.warn('NDElement.extend', `Method "${name}" already exists and will be overwritten`);
|
|
1235
|
-
}
|
|
1236
|
-
this.$localExtensions.set(name, method);
|
|
1237
|
-
}
|
|
1738
|
+
this.$memoryId = null;
|
|
1238
1739
|
|
|
1239
|
-
|
|
1740
|
+
if(configs) {
|
|
1741
|
+
this.configs = configs;
|
|
1742
|
+
if(configs.reset) {
|
|
1743
|
+
this.$initialValue = Validator.isObject(value) ? deepClone(value) : value;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
{
|
|
1747
|
+
PluginsManager.emit('CreateObservable', this);
|
|
1240
1748
|
}
|
|
1749
|
+
}
|
|
1241
1750
|
|
|
1242
|
-
|
|
1243
|
-
|
|
1751
|
+
Object.defineProperty(ObservableItem.prototype, '$value', {
|
|
1752
|
+
get() {
|
|
1753
|
+
return this.$currentValue;
|
|
1754
|
+
},
|
|
1755
|
+
set(value) {
|
|
1756
|
+
this.set(value);
|
|
1757
|
+
},
|
|
1758
|
+
configurable: true,
|
|
1759
|
+
});
|
|
1760
|
+
|
|
1761
|
+
ObservableItem.prototype.__$isObservable = true;
|
|
1762
|
+
const noneTrigger = function() {};
|
|
1244
1763
|
|
|
1245
1764
|
/**
|
|
1246
|
-
*
|
|
1247
|
-
*
|
|
1765
|
+
* Intercepts and transforms values before they are set on the observable.
|
|
1766
|
+
* The interceptor can modify the value or return undefined to use the original value.
|
|
1248
1767
|
*
|
|
1249
|
-
* @param {
|
|
1250
|
-
* @returns {
|
|
1251
|
-
* @throws {NativeDocumentError} If methods is not an object or contains non-function values
|
|
1768
|
+
* @param {(value) => any} callback - Interceptor function that receives (newValue, currentValue) and returns the transformed value or undefined
|
|
1769
|
+
* @returns {ObservableItem} The observable instance for chaining
|
|
1252
1770
|
* @example
|
|
1253
|
-
*
|
|
1254
|
-
*
|
|
1255
|
-
* this.$element.style.opacity = '1';
|
|
1256
|
-
* return this;
|
|
1257
|
-
* }
|
|
1258
|
-
* });
|
|
1259
|
-
* // Now all NDElements have .fadeIn() method
|
|
1260
|
-
* Div().nd.fadeIn();
|
|
1771
|
+
* const count = Observable(0);
|
|
1772
|
+
* count.intercept((newVal, oldVal) => Math.max(0, newVal)); // Prevent negative values
|
|
1261
1773
|
*/
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1774
|
+
ObservableItem.prototype.intercept = function(callback) {
|
|
1775
|
+
this.$interceptor = callback;
|
|
1776
|
+
this.set = this.$setWithInterceptor;
|
|
1777
|
+
return this;
|
|
1778
|
+
};
|
|
1266
1779
|
|
|
1267
|
-
|
|
1268
|
-
|
|
1780
|
+
ObservableItem.prototype.triggerFirstListener = function(operations) {
|
|
1781
|
+
this.$firstListener(this.$currentValue, this.$previousValue, operations);
|
|
1782
|
+
};
|
|
1783
|
+
|
|
1784
|
+
ObservableItem.prototype.triggerListeners = function(operations) {
|
|
1785
|
+
const $listeners = this.$listeners;
|
|
1786
|
+
const $previousValue = this.$previousValue;
|
|
1787
|
+
const $currentValue = this.$currentValue;
|
|
1788
|
+
|
|
1789
|
+
for(let i = 0, length = $listeners.length; i < length; i++) {
|
|
1790
|
+
$listeners[i]($currentValue, $previousValue, operations);
|
|
1269
1791
|
}
|
|
1792
|
+
};
|
|
1270
1793
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
]);
|
|
1794
|
+
ObservableItem.prototype.triggerWatchers = function(operations) {
|
|
1795
|
+
const $watchers = this.$watchers;
|
|
1796
|
+
const $previousValue = this.$previousValue;
|
|
1797
|
+
const $currentValue = this.$currentValue;
|
|
1276
1798
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1799
|
+
const $currentValueCallbacks = $watchers.get($currentValue);
|
|
1800
|
+
const $previousValueCallbacks = $watchers.get($previousValue);
|
|
1801
|
+
if($currentValueCallbacks) {
|
|
1802
|
+
$currentValueCallbacks(true, $previousValue, operations);
|
|
1803
|
+
}
|
|
1804
|
+
if($previousValueCallbacks) {
|
|
1805
|
+
$previousValueCallbacks(false, $currentValue, operations);
|
|
1806
|
+
}
|
|
1807
|
+
};
|
|
1281
1808
|
|
|
1282
|
-
|
|
1809
|
+
ObservableItem.prototype.triggerAll = function(operations) {
|
|
1810
|
+
this.triggerWatchers(operations);
|
|
1811
|
+
this.triggerListeners(operations);
|
|
1812
|
+
};
|
|
1283
1813
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1814
|
+
ObservableItem.prototype.triggerWatchersAndFirstListener = function(operations) {
|
|
1815
|
+
this.triggerWatchers(operations);
|
|
1816
|
+
this.triggerFirstListener(operations);
|
|
1817
|
+
};
|
|
1288
1818
|
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1819
|
+
ObservableItem.prototype.assocTrigger = function() {
|
|
1820
|
+
this.$firstListener = null;
|
|
1821
|
+
if(this.$watchers?.size && this.$listeners?.length) {
|
|
1822
|
+
this.trigger = (this.$listeners.length === 1) ? this.triggerWatchersAndFirstListener : this.triggerAll;
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
if(this.$listeners?.length) {
|
|
1826
|
+
if(this.$listeners.length === 1) {
|
|
1827
|
+
this.$firstListener = this.$listeners[0];
|
|
1828
|
+
this.trigger = this.triggerFirstListener;
|
|
1292
1829
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
DebugManager.warn('NDElement.extend', `Overwriting existing prototype method "${name}"`);
|
|
1830
|
+
else {
|
|
1831
|
+
this.trigger = this.triggerListeners;
|
|
1296
1832
|
}
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
if(this.$watchers?.size) {
|
|
1836
|
+
this.trigger = this.triggerWatchers;
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
this.trigger = noneTrigger;
|
|
1840
|
+
};
|
|
1841
|
+
ObservableItem.prototype.trigger = noneTrigger;
|
|
1297
1842
|
|
|
1298
|
-
|
|
1843
|
+
ObservableItem.prototype.$updateWithNewValue = function(newValue) {
|
|
1844
|
+
newValue = newValue?.__$isObservable ? newValue.val() : newValue;
|
|
1845
|
+
if(this.$currentValue === newValue) {
|
|
1846
|
+
return;
|
|
1299
1847
|
}
|
|
1848
|
+
this.$previousValue = this.$currentValue;
|
|
1849
|
+
this.$currentValue = newValue;
|
|
1300
1850
|
{
|
|
1301
|
-
PluginsManager.emit('
|
|
1851
|
+
PluginsManager.emit('ObservableBeforeChange', this);
|
|
1852
|
+
}
|
|
1853
|
+
this.trigger();
|
|
1854
|
+
this.$previousValue = null;
|
|
1855
|
+
{
|
|
1856
|
+
PluginsManager.emit('ObservableAfterChange', this);
|
|
1302
1857
|
}
|
|
1858
|
+
};
|
|
1303
1859
|
|
|
1304
|
-
|
|
1860
|
+
/**
|
|
1861
|
+
* @param {*} data
|
|
1862
|
+
*/
|
|
1863
|
+
ObservableItem.prototype.$setWithInterceptor = function(data) {
|
|
1864
|
+
let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
|
|
1865
|
+
const result = this.$interceptor(newValue, this.$currentValue);
|
|
1866
|
+
|
|
1867
|
+
if (result !== undefined) {
|
|
1868
|
+
newValue = result;
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
this.$updateWithNewValue(newValue);
|
|
1305
1872
|
};
|
|
1306
1873
|
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1874
|
+
/**
|
|
1875
|
+
* @param {*} data
|
|
1876
|
+
*/
|
|
1877
|
+
ObservableItem.prototype.$basicSet = function(data) {
|
|
1878
|
+
let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
|
|
1879
|
+
this.$updateWithNewValue(newValue);
|
|
1880
|
+
};
|
|
1310
1881
|
|
|
1311
|
-
|
|
1882
|
+
ObservableItem.prototype.set = ObservableItem.prototype.$basicSet;
|
|
1312
1883
|
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
TEXT: 3,
|
|
1316
|
-
COMMENT: 8,
|
|
1317
|
-
DOCUMENT_FRAGMENT: 11
|
|
1884
|
+
ObservableItem.prototype.val = function() {
|
|
1885
|
+
return this.$currentValue;
|
|
1318
1886
|
};
|
|
1319
1887
|
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
},
|
|
1330
|
-
isArrayObservable(value) {
|
|
1331
|
-
return value?.__$isObservableArray;
|
|
1332
|
-
},
|
|
1333
|
-
isProxy(value) {
|
|
1334
|
-
return value?.__isProxy__
|
|
1335
|
-
},
|
|
1336
|
-
isObservableOrProxy(value) {
|
|
1337
|
-
return Validator.isObservable(value) || Validator.isProxy(value);
|
|
1338
|
-
},
|
|
1339
|
-
isAnchor(value) {
|
|
1340
|
-
return value?.__Anchor__
|
|
1341
|
-
},
|
|
1342
|
-
isObservableChecker(value) {
|
|
1343
|
-
return value?.__$isObservableChecker || value instanceof ObservableChecker;
|
|
1344
|
-
},
|
|
1345
|
-
isArray(value) {
|
|
1346
|
-
return Array.isArray(value);
|
|
1347
|
-
},
|
|
1348
|
-
isString(value) {
|
|
1349
|
-
return typeof value === 'string';
|
|
1350
|
-
},
|
|
1351
|
-
isNumber(value) {
|
|
1352
|
-
return typeof value === 'number';
|
|
1353
|
-
},
|
|
1354
|
-
isBoolean(value) {
|
|
1355
|
-
return typeof value === 'boolean';
|
|
1356
|
-
},
|
|
1357
|
-
isFunction(value) {
|
|
1358
|
-
return typeof value === 'function';
|
|
1359
|
-
},
|
|
1360
|
-
isAsyncFunction(value) {
|
|
1361
|
-
return typeof value === 'function' && value.constructor.name === 'AsyncFunction';
|
|
1362
|
-
},
|
|
1363
|
-
isObject(value) {
|
|
1364
|
-
return typeof value === 'object' && value !== null;
|
|
1365
|
-
},
|
|
1366
|
-
isJson(value) {
|
|
1367
|
-
return !(typeof value !== 'object' || value === null || Array.isArray(value) || value.constructor.name !== 'Object')
|
|
1368
|
-
},
|
|
1369
|
-
isElement(value) {
|
|
1370
|
-
return value && (
|
|
1371
|
-
value.nodeType === COMMON_NODE_TYPES.ELEMENT ||
|
|
1372
|
-
value.nodeType === COMMON_NODE_TYPES.TEXT ||
|
|
1373
|
-
value.nodeType === COMMON_NODE_TYPES.DOCUMENT_FRAGMENT ||
|
|
1374
|
-
value.nodeType === COMMON_NODE_TYPES.COMMENT
|
|
1375
|
-
);
|
|
1376
|
-
},
|
|
1377
|
-
isFragment(value) {
|
|
1378
|
-
return value?.nodeType === COMMON_NODE_TYPES.DOCUMENT_FRAGMENT;
|
|
1379
|
-
},
|
|
1380
|
-
isStringOrObservable(value) {
|
|
1381
|
-
return this.isString(value) || this.isObservable(value);
|
|
1382
|
-
},
|
|
1383
|
-
isValidChild(child) {
|
|
1384
|
-
return child === null ||
|
|
1385
|
-
this.isElement(child) ||
|
|
1386
|
-
this.isObservable(child) ||
|
|
1387
|
-
this.isNDElement(child) ||
|
|
1388
|
-
['string', 'number', 'boolean'].includes(typeof child);
|
|
1389
|
-
},
|
|
1390
|
-
isNDElement(child) {
|
|
1391
|
-
return child?.__$isNDElement || child instanceof NDElement;
|
|
1392
|
-
},
|
|
1393
|
-
isValidChildren(children) {
|
|
1394
|
-
if (!Array.isArray(children)) {
|
|
1395
|
-
children = [children];
|
|
1888
|
+
ObservableItem.prototype.disconnectAll = function() {
|
|
1889
|
+
this.$listeners?.splice(0);
|
|
1890
|
+
this.$previousValue = null;
|
|
1891
|
+
this.$currentValue = null;
|
|
1892
|
+
if(this.$watchers) {
|
|
1893
|
+
for (const [_, watchValueList] of this.$watchers) {
|
|
1894
|
+
if(Validator.isArray(watchValueList)) {
|
|
1895
|
+
watchValueList.splice(0);
|
|
1896
|
+
}
|
|
1396
1897
|
}
|
|
1898
|
+
}
|
|
1899
|
+
this.$watchers?.clear();
|
|
1900
|
+
this.$listeners = null;
|
|
1901
|
+
this.$watchers = null;
|
|
1902
|
+
this.trigger = noneTrigger;
|
|
1903
|
+
};
|
|
1397
1904
|
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1905
|
+
/**
|
|
1906
|
+
* Registers a cleanup callback that will be executed when the observable is cleaned up.
|
|
1907
|
+
* Useful for disposing resources, removing event listeners, or other cleanup tasks.
|
|
1908
|
+
*
|
|
1909
|
+
* @param {Function} callback - Cleanup function to execute on observable disposal
|
|
1910
|
+
* @example
|
|
1911
|
+
* const obs = Observable(0);
|
|
1912
|
+
* obs.onCleanup(() => console.log('Cleaned up!'));
|
|
1913
|
+
* obs.cleanup(); // Logs: "Cleaned up!"
|
|
1914
|
+
*/
|
|
1915
|
+
ObservableItem.prototype.onCleanup = function(callback) {
|
|
1916
|
+
this.$cleanupListeners = this.$cleanupListeners ?? [];
|
|
1917
|
+
this.$cleanupListeners.push(callback);
|
|
1918
|
+
};
|
|
1405
1919
|
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1920
|
+
ObservableItem.prototype.cleanup = function() {
|
|
1921
|
+
if (this.$cleanupListeners) {
|
|
1922
|
+
for (let i = 0; i < this.$cleanupListeners.length; i++) {
|
|
1923
|
+
this.$cleanupListeners[i]();
|
|
1409
1924
|
}
|
|
1925
|
+
this.$cleanupListeners = null;
|
|
1926
|
+
}
|
|
1927
|
+
MemoryManager.unregister(this.$memoryId);
|
|
1928
|
+
this.disconnectAll();
|
|
1929
|
+
{
|
|
1930
|
+
this.$isCleanedUp = true;
|
|
1931
|
+
}
|
|
1932
|
+
delete this.$value;
|
|
1933
|
+
};
|
|
1410
1934
|
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
return
|
|
1421
|
-
}
|
|
1422
|
-
return Validator.isObject(data)
|
|
1423
|
-
&& Object.values(data).some(value => Validator.isObservable(value));
|
|
1424
|
-
},
|
|
1425
|
-
/**
|
|
1426
|
-
* Check if the data contains an observable reference.
|
|
1427
|
-
* @param {string} data
|
|
1428
|
-
* @returns {boolean}
|
|
1429
|
-
*/
|
|
1430
|
-
containsObservableReference(data) {
|
|
1431
|
-
if(!data || typeof data !== 'string') {
|
|
1432
|
-
return false;
|
|
1935
|
+
/**
|
|
1936
|
+
*
|
|
1937
|
+
* @param {Function} callback
|
|
1938
|
+
* @returns {(function(): void)}
|
|
1939
|
+
*/
|
|
1940
|
+
ObservableItem.prototype.subscribe = function(callback) {
|
|
1941
|
+
{
|
|
1942
|
+
if (this.$isCleanedUp) {
|
|
1943
|
+
DebugManager.warn('Observable subscription', '⚠️ Attempted to subscribe to a cleaned up observable.');
|
|
1944
|
+
return;
|
|
1433
1945
|
}
|
|
1434
|
-
return /\{\{#ObItem::\([0-9]+\)\}\}/.test(data);
|
|
1435
|
-
},
|
|
1436
|
-
validateAttributes(attributes) {},
|
|
1437
|
-
|
|
1438
|
-
validateEventCallback(callback) {
|
|
1439
1946
|
if (typeof callback !== 'function') {
|
|
1440
|
-
throw new NativeDocumentError('
|
|
1947
|
+
throw new NativeDocumentError('Callback must be a function');
|
|
1441
1948
|
}
|
|
1442
1949
|
}
|
|
1443
|
-
|
|
1444
|
-
{
|
|
1445
|
-
Validator.validateAttributes = function(attributes) {
|
|
1446
|
-
if (!attributes || typeof attributes !== 'object') {
|
|
1447
|
-
return attributes;
|
|
1448
|
-
}
|
|
1950
|
+
this.$listeners = this.$listeners ?? [];
|
|
1449
1951
|
|
|
1450
|
-
|
|
1451
|
-
|
|
1952
|
+
this.$listeners.push(callback);
|
|
1953
|
+
this.assocTrigger();
|
|
1954
|
+
{
|
|
1955
|
+
PluginsManager.emit('ObservableSubscribe', this);
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1452
1958
|
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1959
|
+
/**
|
|
1960
|
+
* Watches for a specific value and executes callback when the observable equals that value.
|
|
1961
|
+
* Creates a watcher that only triggers when the observable changes to the specified value.
|
|
1962
|
+
*
|
|
1963
|
+
* @param {*} value - The value to watch for
|
|
1964
|
+
* @param {(value) => void|ObservableItem} callback - Callback function or observable to set when value matches
|
|
1965
|
+
* @example
|
|
1966
|
+
* const status = Observable('idle');
|
|
1967
|
+
* status.on('loading', () => console.log('Started loading'));
|
|
1968
|
+
* status.on('error', isError); // Set another observable
|
|
1969
|
+
*/
|
|
1970
|
+
ObservableItem.prototype.on = function(value, callback) {
|
|
1971
|
+
this.$watchers = this.$watchers ?? new Map();
|
|
1456
1972
|
|
|
1457
|
-
|
|
1458
|
-
};
|
|
1459
|
-
}
|
|
1973
|
+
let watchValueList = this.$watchers.get(value);
|
|
1460
1974
|
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1975
|
+
if(callback.__$isObservable) {
|
|
1976
|
+
callback = callback.set.bind(callback);
|
|
1977
|
+
}
|
|
1464
1978
|
|
|
1465
|
-
|
|
1466
|
-
|
|
1979
|
+
if(!watchValueList) {
|
|
1980
|
+
watchValueList = callback;
|
|
1981
|
+
this.$watchers.set(value, callback);
|
|
1982
|
+
} else if(!Validator.isArray(watchValueList.list)) {
|
|
1983
|
+
watchValueList = [watchValueList, callback];
|
|
1984
|
+
callback = (value) => {
|
|
1985
|
+
for(let i = 0, length = watchValueList.length; i < length; i++) {
|
|
1986
|
+
watchValueList[i](value);
|
|
1987
|
+
}
|
|
1988
|
+
};
|
|
1989
|
+
callback.list = watchValueList;
|
|
1990
|
+
this.$watchers.set(value, callback);
|
|
1991
|
+
} else {
|
|
1992
|
+
watchValueList.list.push(callback);
|
|
1993
|
+
}
|
|
1467
1994
|
|
|
1468
|
-
|
|
1469
|
-
|
|
1995
|
+
this.assocTrigger();
|
|
1996
|
+
};
|
|
1470
1997
|
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1998
|
+
/**
|
|
1999
|
+
* Removes a watcher for a specific value. If no callback is provided, removes all watchers for that value.
|
|
2000
|
+
*
|
|
2001
|
+
* @param {*} value - The value to stop watching
|
|
2002
|
+
* @param {Function} [callback] - Specific callback to remove. If omitted, removes all watchers for this value
|
|
2003
|
+
* @example
|
|
2004
|
+
* const status = Observable('idle');
|
|
2005
|
+
* const handler = () => console.log('Loading');
|
|
2006
|
+
* status.on('loading', handler);
|
|
2007
|
+
* status.off('loading', handler); // Remove specific handler
|
|
2008
|
+
* status.off('loading'); // Remove all handlers for 'loading'
|
|
2009
|
+
*/
|
|
2010
|
+
ObservableItem.prototype.off = function(value, callback) {
|
|
2011
|
+
if(!this.$watchers) return;
|
|
1474
2012
|
|
|
1475
|
-
const
|
|
2013
|
+
const watchValueList = this.$watchers.get(value);
|
|
2014
|
+
if(!watchValueList) return;
|
|
1476
2015
|
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
2016
|
+
if(!callback || !Array.isArray(watchValueList.list)) {
|
|
2017
|
+
this.$watchers?.delete(value);
|
|
2018
|
+
this.assocTrigger();
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
const index = watchValueList.indexOf(callback);
|
|
2022
|
+
watchValueList?.splice(index, 1);
|
|
2023
|
+
if(watchValueList.length === 1) {
|
|
2024
|
+
this.$watchers.set(value, watchValueList[0]);
|
|
2025
|
+
}
|
|
2026
|
+
else if(watchValueList.length === 0) {
|
|
2027
|
+
this.$watchers?.delete(value);
|
|
2028
|
+
}
|
|
2029
|
+
this.assocTrigger();
|
|
2030
|
+
};
|
|
1489
2031
|
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
2032
|
+
/**
|
|
2033
|
+
* Subscribes to the observable but automatically unsubscribes after the first time the predicate matches.
|
|
2034
|
+
*
|
|
2035
|
+
* @param {(value) => Boolean|any} predicate - Value to match or function that returns true when condition is met
|
|
2036
|
+
* @param {(value) => void} callback - Callback to execute when predicate matches, receives the matched value
|
|
2037
|
+
* @example
|
|
2038
|
+
* const status = Observable('loading');
|
|
2039
|
+
* status.once('ready', (val) => console.log('Ready!'));
|
|
2040
|
+
* status.once(val => val === 'error', (val) => console.log('Error occurred'));
|
|
2041
|
+
*/
|
|
2042
|
+
ObservableItem.prototype.once = function(predicate, callback) {
|
|
2043
|
+
const fn = typeof predicate === 'function' ? predicate : (v) => v === predicate;
|
|
1499
2044
|
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
return;
|
|
2045
|
+
const handler = (val) => {
|
|
2046
|
+
if (fn(val)) {
|
|
2047
|
+
this.unsubscribe(handler);
|
|
2048
|
+
callback(val);
|
|
1505
2049
|
}
|
|
1506
|
-
before = before ?? anchorEnd;
|
|
1507
|
-
insertBefore(parent, child, before);
|
|
1508
2050
|
};
|
|
2051
|
+
this.subscribe(handler);
|
|
2052
|
+
};
|
|
1509
2053
|
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
2054
|
+
/**
|
|
2055
|
+
* Unsubscribe from an observable.
|
|
2056
|
+
* @param {Function} callback
|
|
2057
|
+
*/
|
|
2058
|
+
ObservableItem.prototype.unsubscribe = function(callback) {
|
|
2059
|
+
if(!this.$listeners) return;
|
|
2060
|
+
const index = this.$listeners.indexOf(callback);
|
|
2061
|
+
if (index > -1) {
|
|
2062
|
+
this.$listeners.splice(index, 1);
|
|
2063
|
+
}
|
|
2064
|
+
this.assocTrigger();
|
|
2065
|
+
{
|
|
2066
|
+
PluginsManager.emit('ObservableUnsubscribe', this);
|
|
2067
|
+
}
|
|
2068
|
+
};
|
|
1513
2069
|
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
// }
|
|
2070
|
+
/**
|
|
2071
|
+
* Create an Observable checker instance
|
|
2072
|
+
* @param callback
|
|
2073
|
+
* @returns {ObservableChecker}
|
|
2074
|
+
*/
|
|
2075
|
+
ObservableItem.prototype.check = function(callback) {
|
|
2076
|
+
return new ObservableChecker(this, callback)
|
|
2077
|
+
};
|
|
1523
2078
|
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
removes.push(itemToRemove.remove());
|
|
1529
|
-
itemToRemove = tempItem;
|
|
1530
|
-
}
|
|
1531
|
-
await Promise.all(removes);
|
|
1532
|
-
};
|
|
2079
|
+
ObservableItem.prototype.transform = ObservableItem.prototype.check;
|
|
2080
|
+
ObservableItem.prototype.pluck = ObservableItem.prototype.check;
|
|
2081
|
+
ObservableItem.prototype.is = ObservableItem.prototype.check;
|
|
2082
|
+
ObservableItem.prototype.select = ObservableItem.prototype.check;
|
|
1533
2083
|
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
anchorFragment.nativeAppend(...allItemToRemove);
|
|
1550
|
-
};
|
|
2084
|
+
/**
|
|
2085
|
+
* Gets a property value from the observable's current value.
|
|
2086
|
+
* If the property is an observable, returns its value.
|
|
2087
|
+
*
|
|
2088
|
+
* @param {string|number} key - Property key to retrieve
|
|
2089
|
+
* @returns {*} The value of the property, unwrapped if it's an observable
|
|
2090
|
+
* @example
|
|
2091
|
+
* const user = Observable({ name: 'John', age: Observable(25) });
|
|
2092
|
+
* user.get('name'); // 'John'
|
|
2093
|
+
* user.get('age'); // 25 (unwrapped from observable)
|
|
2094
|
+
*/
|
|
2095
|
+
ObservableItem.prototype.get = function(key) {
|
|
2096
|
+
const item = this.$currentValue[key];
|
|
2097
|
+
return Validator.isObservable(item) ? item.val() : item;
|
|
2098
|
+
};
|
|
1551
2099
|
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
2100
|
+
/**
|
|
2101
|
+
* Creates an ObservableWhen that represents whether the observable equals a specific value.
|
|
2102
|
+
* Returns an object that can be subscribed to and will emit true/false.
|
|
2103
|
+
*
|
|
2104
|
+
* @param {*} value - The value to compare against
|
|
2105
|
+
* @returns {ObservableWhen} An ObservableWhen instance that tracks when the observable equals the value
|
|
2106
|
+
* @example
|
|
2107
|
+
* const status = Observable('idle');
|
|
2108
|
+
* const isLoading = status.when('loading');
|
|
2109
|
+
* isLoading.subscribe(active => console.log('Loading:', active));
|
|
2110
|
+
* status.set('loading'); // Logs: "Loading: true"
|
|
2111
|
+
*/
|
|
2112
|
+
ObservableItem.prototype.when = function(value) {
|
|
2113
|
+
return new ObservableWhen(this, value);
|
|
2114
|
+
};
|
|
1557
2115
|
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
2116
|
+
/**
|
|
2117
|
+
* Compares the observable's current value with another value or observable.
|
|
2118
|
+
*
|
|
2119
|
+
* @param {*|ObservableItem} other - Value or observable to compare against
|
|
2120
|
+
* @returns {boolean} True if values are equal
|
|
2121
|
+
* @example
|
|
2122
|
+
* const a = Observable(5);
|
|
2123
|
+
* const b = Observable(5);
|
|
2124
|
+
* a.equals(5); // true
|
|
2125
|
+
* a.equals(b); // true
|
|
2126
|
+
* a.equals(10); // false
|
|
2127
|
+
*/
|
|
2128
|
+
ObservableItem.prototype.equals = function(other) {
|
|
2129
|
+
if(Validator.isObservable(other)) {
|
|
2130
|
+
return this.$currentValue === other.$currentValue;
|
|
2131
|
+
}
|
|
2132
|
+
return this.$currentValue === other;
|
|
2133
|
+
};
|
|
1571
2134
|
|
|
1572
|
-
|
|
2135
|
+
/**
|
|
2136
|
+
* Converts the observable's current value to a boolean.
|
|
2137
|
+
*
|
|
2138
|
+
* @returns {boolean} The boolean representation of the current value
|
|
2139
|
+
* @example
|
|
2140
|
+
* const count = Observable(0);
|
|
2141
|
+
* count.toBool(); // false
|
|
2142
|
+
* count.set(5);
|
|
2143
|
+
* count.toBool(); // true
|
|
2144
|
+
*/
|
|
2145
|
+
ObservableItem.prototype.toBool = function() {
|
|
2146
|
+
return !!this.$currentValue;
|
|
2147
|
+
};
|
|
1573
2148
|
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
2149
|
+
/**
|
|
2150
|
+
* Toggles the boolean value of the observable (false becomes true, true becomes false).
|
|
2151
|
+
*
|
|
2152
|
+
* @example
|
|
2153
|
+
* const isOpen = Observable(false);
|
|
2154
|
+
* isOpen.toggle(); // Now true
|
|
2155
|
+
* isOpen.toggle(); // Now false
|
|
2156
|
+
*/
|
|
2157
|
+
ObservableItem.prototype.toggle = function() {
|
|
2158
|
+
this.set(!this.$currentValue);
|
|
2159
|
+
};
|
|
1577
2160
|
|
|
2161
|
+
/**
|
|
2162
|
+
* Resets the observable to its initial value.
|
|
2163
|
+
* Only works if the observable was created with { reset: true } config.
|
|
2164
|
+
*
|
|
2165
|
+
* @example
|
|
2166
|
+
* const count = Observable(0, { reset: true });
|
|
2167
|
+
* count.set(10);
|
|
2168
|
+
* count.reset(); // Back to 0
|
|
2169
|
+
*/
|
|
2170
|
+
ObservableItem.prototype.reset = function() {
|
|
2171
|
+
if(!this.configs?.reset) {
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
const resetValue = (Validator.isObject(this.$initialValue))
|
|
2175
|
+
? deepClone(this.$initialValue, (observable) => {
|
|
2176
|
+
observable.reset();
|
|
2177
|
+
})
|
|
2178
|
+
: this.$initialValue;
|
|
2179
|
+
this.set(resetValue);
|
|
2180
|
+
};
|
|
1578
2181
|
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
2182
|
+
/**
|
|
2183
|
+
* Returns a string representation of the observable's current value.
|
|
2184
|
+
*
|
|
2185
|
+
* @returns {string} String representation of the current value
|
|
2186
|
+
*/
|
|
2187
|
+
ObservableItem.prototype.toString = function() {
|
|
2188
|
+
return String(this.$currentValue);
|
|
2189
|
+
};
|
|
1582
2190
|
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
2191
|
+
/**
|
|
2192
|
+
* Returns the primitive value of the observable (its current value).
|
|
2193
|
+
* Called automatically in type coercion contexts.
|
|
2194
|
+
*
|
|
2195
|
+
* @returns {*} The current value of the observable
|
|
2196
|
+
*/
|
|
2197
|
+
ObservableItem.prototype.valueOf = function() {
|
|
2198
|
+
return this.$currentValue;
|
|
2199
|
+
};
|
|
1591
2200
|
|
|
1592
|
-
anchorFragment.getByIndex = function(index) {
|
|
1593
|
-
let currentNode = anchorStart;
|
|
1594
|
-
for(let i = 0; i <= index; i++) {
|
|
1595
|
-
if(!currentNode.nextSibling) {
|
|
1596
|
-
return null;
|
|
1597
|
-
}
|
|
1598
|
-
currentNode = currentNode.nextSibling;
|
|
1599
|
-
}
|
|
1600
|
-
return currentNode !== anchorStart ? currentNode : null;
|
|
1601
|
-
};
|
|
1602
2201
|
|
|
1603
|
-
return anchorFragment;
|
|
1604
|
-
}
|
|
1605
2202
|
/**
|
|
2203
|
+
* Creates a derived observable that formats the current value using Intl.
|
|
2204
|
+
* Automatically reacts to both value changes and locale changes (Store.__nd.locale).
|
|
2205
|
+
*
|
|
2206
|
+
* @param {string | Function} type - Format type or custom formatter function
|
|
2207
|
+
* @param {Object} [options={}] - Options passed to the formatter
|
|
2208
|
+
* @returns {ObservableItem<string>}
|
|
2209
|
+
*
|
|
2210
|
+
* @example
|
|
2211
|
+
* // Currency
|
|
2212
|
+
* price.format('currency') // "15 000 FCFA"
|
|
2213
|
+
* price.format('currency', { currency: 'EUR' }) // "15 000,00 €"
|
|
2214
|
+
* price.format('currency', { notation: 'compact' }) // "15 K FCFA"
|
|
2215
|
+
*
|
|
2216
|
+
* // Number
|
|
2217
|
+
* count.format('number') // "15 000"
|
|
2218
|
+
*
|
|
2219
|
+
* // Percent
|
|
2220
|
+
* rate.format('percent') // "15,0 %"
|
|
2221
|
+
* rate.format('percent', { decimals: 2 }) // "15,00 %"
|
|
2222
|
+
*
|
|
2223
|
+
* // Date
|
|
2224
|
+
* date.format('date') // "3 mars 2026"
|
|
2225
|
+
* date.format('date', { dateStyle: 'full' }) // "mardi 3 mars 2026"
|
|
2226
|
+
* date.format('date', { format: 'DD/MM/YYYY' }) // "03/03/2026"
|
|
2227
|
+
* date.format('date', { format: 'DD MMM YYYY' }) // "03 mar 2026"
|
|
2228
|
+
* date.format('date', { format: 'DD MMMM YYYY' }) // "03 mars 2026"
|
|
2229
|
+
*
|
|
2230
|
+
* // Time
|
|
2231
|
+
* date.format('time') // "20:30"
|
|
2232
|
+
* date.format('time', { second: '2-digit' }) // "20:30:00"
|
|
2233
|
+
* date.format('time', { format: 'HH:mm:ss' }) // "20:30:00"
|
|
2234
|
+
*
|
|
2235
|
+
* // Datetime
|
|
2236
|
+
* date.format('datetime') // "3 mars 2026, 20:30"
|
|
2237
|
+
* date.format('datetime', { dateStyle: 'full' }) // "mardi 3 mars 2026, 20:30"
|
|
2238
|
+
* date.format('datetime', { format: 'DD/MM/YYYY HH:mm' }) // "03/03/2026 20:30"
|
|
2239
|
+
*
|
|
2240
|
+
* // Relative
|
|
2241
|
+
* date.format('relative') // "dans 11 jours"
|
|
2242
|
+
* date.format('relative', { unit: 'month' }) // "dans 1 mois"
|
|
1606
2243
|
*
|
|
1607
|
-
*
|
|
1608
|
-
*
|
|
1609
|
-
*
|
|
2244
|
+
* // Plural
|
|
2245
|
+
* count.format('plural', { singular: 'billet', plural: 'billets' }) // "3 billets"
|
|
2246
|
+
*
|
|
2247
|
+
* // Custom formatter
|
|
2248
|
+
* price.format(value => `${value.toLocaleString()} FCFA`)
|
|
2249
|
+
*
|
|
2250
|
+
* // Reacts to locale changes automatically
|
|
2251
|
+
* Store.setLocale('en-US');
|
|
1610
2252
|
*/
|
|
1611
|
-
function
|
|
1612
|
-
const
|
|
1613
|
-
anchor.appendChild(ElementCreator.getChild(children));
|
|
2253
|
+
ObservableItem.prototype.format = function(type, options = {}) {
|
|
2254
|
+
const self = this;
|
|
1614
2255
|
|
|
1615
|
-
(
|
|
1616
|
-
|
|
1617
|
-
|
|
2256
|
+
if (typeof type === 'function') {
|
|
2257
|
+
return new ObservableChecker(self, type);
|
|
2258
|
+
}
|
|
1618
2259
|
|
|
1619
|
-
|
|
2260
|
+
{
|
|
2261
|
+
if (!Formatters[type]) {
|
|
2262
|
+
throw new NativeDocumentError(
|
|
2263
|
+
`Observable.format : unknown type '${type}'. Available : ${Object.keys(Formatters).join(', ')}.`
|
|
2264
|
+
);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
1620
2267
|
|
|
1621
|
-
|
|
1622
|
-
'
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
'download',
|
|
1642
|
-
'reversed',
|
|
1643
|
-
'open',
|
|
1644
|
-
'default',
|
|
1645
|
-
'formnovalidate',
|
|
1646
|
-
'novalidate',
|
|
1647
|
-
'scoped',
|
|
1648
|
-
'itemscope',
|
|
1649
|
-
'allowfullscreen',
|
|
1650
|
-
'allowpaymentrequest',
|
|
1651
|
-
'playsinline'
|
|
1652
|
-
]);
|
|
2268
|
+
const formatter = Formatters[type];
|
|
2269
|
+
const localeObservable = Store.follow('locale');
|
|
2270
|
+
|
|
2271
|
+
return Observable.computed(() => formatter(self.val(), localeObservable.val(), options),
|
|
2272
|
+
[self, localeObservable]
|
|
2273
|
+
);
|
|
2274
|
+
};
|
|
2275
|
+
|
|
2276
|
+
ObservableItem.prototype.persist = function(key, options = {}) {
|
|
2277
|
+
let value = $getFromStorage(key, this.$currentValue);
|
|
2278
|
+
if(options.get) {
|
|
2279
|
+
value = options.get(value);
|
|
2280
|
+
}
|
|
2281
|
+
this.set(value);
|
|
2282
|
+
const saver = $saveToStorage(this.$currentValue);
|
|
2283
|
+
this.subscribe((newValue) => {
|
|
2284
|
+
saver(key, options.set ? options.set(newValue) : newValue);
|
|
2285
|
+
});
|
|
2286
|
+
return this;
|
|
2287
|
+
};
|
|
1653
2288
|
|
|
1654
2289
|
/**
|
|
1655
2290
|
*
|
|
@@ -1853,6 +2488,12 @@ var NativeDocument = (function (exports) {
|
|
|
1853
2488
|
return element;
|
|
1854
2489
|
}
|
|
1855
2490
|
|
|
2491
|
+
function TemplateBinding(hydrate) {
|
|
2492
|
+
this.$hydrate = hydrate;
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
TemplateBinding.prototype.__$isTemplateBinding = true;
|
|
2496
|
+
|
|
1856
2497
|
String.prototype.toNdElement = function () {
|
|
1857
2498
|
const formattedChild = this.resolveObservableTemplate ? this.resolveObservableTemplate() : this;
|
|
1858
2499
|
if(Validator.isString(formattedChild)) {
|
|
@@ -3640,6 +4281,68 @@ var NativeDocument = (function (exports) {
|
|
|
3640
4281
|
});
|
|
3641
4282
|
};
|
|
3642
4283
|
|
|
4284
|
+
ObservableArray.prototype.deepSubscribe = function(callback) {
|
|
4285
|
+
const updatedValue = nextTick(() => callback(this.val()));
|
|
4286
|
+
const $listeners = new WeakMap();
|
|
4287
|
+
|
|
4288
|
+
const bindItem = (item) => {
|
|
4289
|
+
if ($listeners.has(item)) {
|
|
4290
|
+
return;
|
|
4291
|
+
}
|
|
4292
|
+
if (item?.__$isObservableArray) {
|
|
4293
|
+
$listeners.set(item, item.deepSubscribe(updatedValue));
|
|
4294
|
+
return;
|
|
4295
|
+
}
|
|
4296
|
+
if (item?.__$isObservable) {
|
|
4297
|
+
item.subscribe(updatedValue);
|
|
4298
|
+
$listeners.set(item, () => item.unsubscribe(updatedValue));
|
|
4299
|
+
}
|
|
4300
|
+
};
|
|
4301
|
+
|
|
4302
|
+
const unbindItem = (item) => {
|
|
4303
|
+
const unsub = $listeners.get(item);
|
|
4304
|
+
if (unsub) {
|
|
4305
|
+
unsub();
|
|
4306
|
+
$listeners.delete(item);
|
|
4307
|
+
}
|
|
4308
|
+
};
|
|
4309
|
+
|
|
4310
|
+
this.$currentValue.forEach(bindItem);
|
|
4311
|
+
this.subscribe(updatedValue);
|
|
4312
|
+
|
|
4313
|
+
this.subscribe((items, _, operations) => {
|
|
4314
|
+
switch (operations?.action) {
|
|
4315
|
+
case 'push':
|
|
4316
|
+
case 'unshift':
|
|
4317
|
+
operations.args.forEach(bindItem);
|
|
4318
|
+
break;
|
|
4319
|
+
|
|
4320
|
+
case 'splice': {
|
|
4321
|
+
const [start, deleteCount, ...newItems] = operations.args;
|
|
4322
|
+
operations.result?.forEach(unbindItem);
|
|
4323
|
+
newItems.forEach(bindItem);
|
|
4324
|
+
break;
|
|
4325
|
+
}
|
|
4326
|
+
|
|
4327
|
+
case 'remove':
|
|
4328
|
+
unbindItem(operations.result);
|
|
4329
|
+
break;
|
|
4330
|
+
|
|
4331
|
+
case 'merge':
|
|
4332
|
+
operations.args.forEach(bindItem);
|
|
4333
|
+
break;
|
|
4334
|
+
|
|
4335
|
+
case 'clear':
|
|
4336
|
+
this.$currentValue.forEach(unbindItem);
|
|
4337
|
+
break;
|
|
4338
|
+
}
|
|
4339
|
+
});
|
|
4340
|
+
|
|
4341
|
+
return () => {
|
|
4342
|
+
this.$currentValue.forEach(unbindItem);
|
|
4343
|
+
};
|
|
4344
|
+
};
|
|
4345
|
+
|
|
3643
4346
|
/**
|
|
3644
4347
|
* Creates an observable array with reactive array methods.
|
|
3645
4348
|
* All mutations trigger updates automatically.
|
|
@@ -3679,10 +4382,71 @@ var NativeDocument = (function (exports) {
|
|
|
3679
4382
|
return batch;
|
|
3680
4383
|
};
|
|
3681
4384
|
|
|
3682
|
-
const
|
|
4385
|
+
const ObservableObject = function(target, configs) {
|
|
4386
|
+
ObservableItem.call(this, target);
|
|
4387
|
+
this.$observables = {};
|
|
4388
|
+
this.configs = configs;
|
|
4389
|
+
|
|
4390
|
+
this.$load(target);
|
|
4391
|
+
|
|
4392
|
+
for(const name in target) {
|
|
4393
|
+
if(!Object.hasOwn(this, name)) {
|
|
4394
|
+
Object.defineProperty(this, name, {
|
|
4395
|
+
get: () => this.$observables[name],
|
|
4396
|
+
set: (value) => this.$observables[name].set(value)
|
|
4397
|
+
});
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
|
|
4401
|
+
};
|
|
4402
|
+
|
|
4403
|
+
ObservableObject.prototype = Object.create(ObservableItem.prototype);
|
|
4404
|
+
|
|
4405
|
+
Object.defineProperty(ObservableObject, '$value', {
|
|
4406
|
+
get() {
|
|
4407
|
+
return this.val();
|
|
4408
|
+
},
|
|
4409
|
+
set(value) {
|
|
4410
|
+
this.set(value);
|
|
4411
|
+
}
|
|
4412
|
+
});
|
|
4413
|
+
|
|
4414
|
+
ObservableObject.prototype.__$isObservableObject = true;
|
|
4415
|
+
ObservableObject.prototype.__isProxy__ = true;
|
|
4416
|
+
|
|
4417
|
+
ObservableObject.prototype.$load = function(initialValue) {
|
|
4418
|
+
const configs = this.configs;
|
|
4419
|
+
for(const key in initialValue) {
|
|
4420
|
+
const itemValue = initialValue[key];
|
|
4421
|
+
if(Array.isArray(itemValue)) {
|
|
4422
|
+
if(configs?.deep !== false) {
|
|
4423
|
+
const mappedItemValue = itemValue.map(item => {
|
|
4424
|
+
if(Validator.isJson(item)) {
|
|
4425
|
+
return Observable.json(item, configs);
|
|
4426
|
+
}
|
|
4427
|
+
if(Validator.isArray(item)) {
|
|
4428
|
+
return Observable.array(item, configs);
|
|
4429
|
+
}
|
|
4430
|
+
return Observable(item, configs);
|
|
4431
|
+
});
|
|
4432
|
+
this.$observables[key] = Observable.array(mappedItemValue, configs);
|
|
4433
|
+
continue;
|
|
4434
|
+
}
|
|
4435
|
+
this.$observables[key] = Observable.array(itemValue, configs);
|
|
4436
|
+
continue;
|
|
4437
|
+
}
|
|
4438
|
+
if(Validator.isObservable(itemValue) || Validator.isProxy(itemValue)) {
|
|
4439
|
+
this.$observables[key] = itemValue;
|
|
4440
|
+
continue;
|
|
4441
|
+
}
|
|
4442
|
+
this.$observables[key] = (typeof itemValue === 'object') ? Observable.object(itemValue, configs) : Observable(itemValue, configs);
|
|
4443
|
+
}
|
|
4444
|
+
};
|
|
4445
|
+
|
|
4446
|
+
ObservableObject.prototype.val = function() {
|
|
3683
4447
|
const result = {};
|
|
3684
|
-
for(const key in
|
|
3685
|
-
const dataItem =
|
|
4448
|
+
for(const key in this.$observables) {
|
|
4449
|
+
const dataItem = this.$observables[key];
|
|
3686
4450
|
if(Validator.isObservable(dataItem)) {
|
|
3687
4451
|
let value = dataItem.val();
|
|
3688
4452
|
if(Array.isArray(value)) {
|
|
@@ -3705,9 +4469,10 @@ var NativeDocument = (function (exports) {
|
|
|
3705
4469
|
}
|
|
3706
4470
|
return result;
|
|
3707
4471
|
};
|
|
4472
|
+
ObservableObject.prototype.$val = ObservableObject.prototype.val;
|
|
3708
4473
|
|
|
3709
|
-
|
|
3710
|
-
const item =
|
|
4474
|
+
ObservableObject.prototype.get = function(property) {
|
|
4475
|
+
const item = this.$observables[property];
|
|
3711
4476
|
if(Validator.isObservable(item)) {
|
|
3712
4477
|
return item.val();
|
|
3713
4478
|
}
|
|
@@ -3716,100 +4481,88 @@ var NativeDocument = (function (exports) {
|
|
|
3716
4481
|
}
|
|
3717
4482
|
return item;
|
|
3718
4483
|
};
|
|
4484
|
+
ObservableObject.prototype.$get = ObservableObject.prototype.get;
|
|
3719
4485
|
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
* user.name = 'Jane X'
|
|
3740
|
-
* user.age.subscribe(val => console.log('Age:', val));
|
|
3741
|
-
*/
|
|
3742
|
-
Observable.init = function(initialValue, configs = null) {
|
|
3743
|
-
const data = {};
|
|
3744
|
-
for(const key in initialValue) {
|
|
3745
|
-
const itemValue = initialValue[key];
|
|
3746
|
-
if(Array.isArray(itemValue)) {
|
|
3747
|
-
if(configs?.deep !== false) {
|
|
3748
|
-
const mappedItemValue = itemValue.map(item => {
|
|
3749
|
-
if(Validator.isJson(item)) {
|
|
3750
|
-
return Observable.json(item, configs);
|
|
3751
|
-
}
|
|
3752
|
-
if(Validator.isArray(item)) {
|
|
3753
|
-
return Observable.array(item, configs);
|
|
4486
|
+
ObservableObject.prototype.set = function(newData) {
|
|
4487
|
+
const data = Validator.isProxy(newData) ? newData.$value : newData;
|
|
4488
|
+
const configs = this.configs;
|
|
4489
|
+
|
|
4490
|
+
for(const key in data) {
|
|
4491
|
+
const targetItem = this.$observables[key];
|
|
4492
|
+
const newValueOrigin = newData[key];
|
|
4493
|
+
const newValue = data[key];
|
|
4494
|
+
|
|
4495
|
+
if(Validator.isObservable(targetItem)) {
|
|
4496
|
+
if(!Validator.isArray(newValue)) {
|
|
4497
|
+
targetItem.set(newValue);
|
|
4498
|
+
continue;
|
|
4499
|
+
}
|
|
4500
|
+
const firstElementFromOriginalValue = newValueOrigin.at(0);
|
|
4501
|
+
if(Validator.isObservable(firstElementFromOriginalValue) || Validator.isProxy(firstElementFromOriginalValue)) {
|
|
4502
|
+
const newValues = newValue.map(item => {
|
|
4503
|
+
if(Validator.isProxy(firstElementFromOriginalValue)) {
|
|
4504
|
+
return Observable.init(item, configs);
|
|
3754
4505
|
}
|
|
3755
4506
|
return Observable(item, configs);
|
|
3756
4507
|
});
|
|
3757
|
-
|
|
4508
|
+
targetItem.set(newValues);
|
|
3758
4509
|
continue;
|
|
3759
4510
|
}
|
|
3760
|
-
|
|
4511
|
+
targetItem.set([...newValue]);
|
|
3761
4512
|
continue;
|
|
3762
4513
|
}
|
|
3763
|
-
if(Validator.
|
|
3764
|
-
|
|
4514
|
+
if(Validator.isProxy(targetItem)) {
|
|
4515
|
+
targetItem.update(newValue);
|
|
3765
4516
|
continue;
|
|
3766
4517
|
}
|
|
3767
|
-
|
|
4518
|
+
this[key] = newValue;
|
|
3768
4519
|
}
|
|
4520
|
+
};
|
|
4521
|
+
ObservableObject.prototype.$set = ObservableObject.prototype.set;
|
|
4522
|
+
ObservableObject.prototype.$updateWith = ObservableObject.prototype.set;
|
|
3769
4523
|
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
}
|
|
3775
|
-
};
|
|
3776
|
-
|
|
3777
|
-
const $val = () => ObservableObjectValue(data);
|
|
3778
|
-
|
|
3779
|
-
const $clone = () => Observable.init($val(), configs);
|
|
4524
|
+
ObservableObject.prototype.observables = function() {
|
|
4525
|
+
return Object.values(this.$observables);
|
|
4526
|
+
};
|
|
4527
|
+
ObservableObject.prototype.$observables = ObservableObject.prototype.observables;
|
|
3780
4528
|
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
4529
|
+
ObservableObject.prototype.keys = function() {
|
|
4530
|
+
return Object.keys(this.$observables);
|
|
4531
|
+
};
|
|
4532
|
+
ObservableObject.prototype.$keys = ObservableObject.prototype.keys;
|
|
4533
|
+
ObservableObject.prototype.clone = function() {
|
|
4534
|
+
return Observable.init(this.val(), this.configs);
|
|
4535
|
+
};
|
|
4536
|
+
ObservableObject.prototype.$clone = ObservableObject.prototype.clone;
|
|
4537
|
+
ObservableObject.prototype.reset = function() {
|
|
4538
|
+
for(const key in this.$observables) {
|
|
4539
|
+
this.$observables[key].reset();
|
|
4540
|
+
}
|
|
4541
|
+
};
|
|
4542
|
+
ObservableObject.prototype.originalSubscribe = ObservableObject.prototype.subscribe;
|
|
4543
|
+
ObservableObject.prototype.subscribe = function(callback) {
|
|
4544
|
+
const observables = this.observables();
|
|
4545
|
+
const updatedValue = nextTick(() => this.trigger());
|
|
3784
4546
|
|
|
3785
|
-
|
|
4547
|
+
this.originalSubscribe(callback);
|
|
3786
4548
|
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
if(property === 'val' || property === '$val') { return $val; }
|
|
3793
|
-
if(property === 'set' || property === '$set' || property === '$updateWith') { return $updateWith; }
|
|
3794
|
-
if(property === 'observables' || property === '$observables') { return Object.values(target); }
|
|
3795
|
-
if(property === 'keys'|| property === '$keys') { return Object.keys(initialValue); }
|
|
3796
|
-
if(property === 'clone' || property === '$clone') { return $clone; }
|
|
3797
|
-
if(property === 'reset') { return $reset; }
|
|
3798
|
-
if(property === 'configs') { return configs; }
|
|
3799
|
-
return target[property];
|
|
3800
|
-
},
|
|
3801
|
-
set(target, prop, newValue) {
|
|
3802
|
-
if(target[prop] !== undefined) {
|
|
3803
|
-
Validator.isObservable(newValue)
|
|
3804
|
-
? target[prop].set(newValue.val())
|
|
3805
|
-
: target[prop].set(newValue);
|
|
3806
|
-
return true;
|
|
3807
|
-
}
|
|
3808
|
-
return true;
|
|
4549
|
+
for (let i = 0, length = observables.length; i < length; i++) {
|
|
4550
|
+
const observable = observables[i];
|
|
4551
|
+
if (observable.__$isObservableArray) {
|
|
4552
|
+
observable.deepSubscribe(updatedValue);
|
|
4553
|
+
continue
|
|
3809
4554
|
}
|
|
3810
|
-
|
|
4555
|
+
observable.subscribe(updatedValue);
|
|
4556
|
+
}
|
|
4557
|
+
};
|
|
4558
|
+
ObservableObject.prototype.configs = function() {
|
|
4559
|
+
return this.configs;
|
|
4560
|
+
};
|
|
4561
|
+
|
|
4562
|
+
ObservableObject.prototype.update = ObservableObject.prototype.set;
|
|
3811
4563
|
|
|
3812
|
-
|
|
4564
|
+
Observable.init = function(initialValue, configs = null) {
|
|
4565
|
+
return new ObservableObject(initialValue, configs)
|
|
3813
4566
|
};
|
|
3814
4567
|
|
|
3815
4568
|
/**
|
|
@@ -3844,43 +4597,6 @@ var NativeDocument = (function (exports) {
|
|
|
3844
4597
|
return data;
|
|
3845
4598
|
};
|
|
3846
4599
|
|
|
3847
|
-
|
|
3848
|
-
Observable.update = function($target, newData) {
|
|
3849
|
-
const data = Validator.isProxy(newData) ? newData.$value : newData;
|
|
3850
|
-
const configs = $target.configs;
|
|
3851
|
-
|
|
3852
|
-
for(const key in data) {
|
|
3853
|
-
const targetItem = $target[key];
|
|
3854
|
-
const newValueOrigin = newData[key];
|
|
3855
|
-
const newValue = data[key];
|
|
3856
|
-
|
|
3857
|
-
if(Validator.isObservable(targetItem)) {
|
|
3858
|
-
if(Validator.isArray(newValue)) {
|
|
3859
|
-
const firstElementFromOriginalValue = newValueOrigin.at(0);
|
|
3860
|
-
if(Validator.isObservable(firstElementFromOriginalValue) || Validator.isProxy(firstElementFromOriginalValue)) {
|
|
3861
|
-
const newValues = newValue.map(item => {
|
|
3862
|
-
if(Validator.isProxy(firstElementFromOriginalValue)) {
|
|
3863
|
-
return Observable.init(item, configs);
|
|
3864
|
-
}
|
|
3865
|
-
return Observable(item, configs);
|
|
3866
|
-
});
|
|
3867
|
-
targetItem.set(newValues);
|
|
3868
|
-
continue;
|
|
3869
|
-
}
|
|
3870
|
-
targetItem.set([...newValue]);
|
|
3871
|
-
continue;
|
|
3872
|
-
}
|
|
3873
|
-
targetItem.set(newValue);
|
|
3874
|
-
continue;
|
|
3875
|
-
}
|
|
3876
|
-
if(Validator.isProxy(targetItem)) {
|
|
3877
|
-
Observable.update(targetItem, newValue);
|
|
3878
|
-
continue;
|
|
3879
|
-
}
|
|
3880
|
-
$target[key] = newValue;
|
|
3881
|
-
}
|
|
3882
|
-
};
|
|
3883
|
-
|
|
3884
4600
|
Observable.object = Observable.init;
|
|
3885
4601
|
Observable.json = Observable.init;
|
|
3886
4602
|
|
|
@@ -3932,79 +4648,6 @@ var NativeDocument = (function (exports) {
|
|
|
3932
4648
|
return observable;
|
|
3933
4649
|
};
|
|
3934
4650
|
|
|
3935
|
-
const Store = (function() {
|
|
3936
|
-
|
|
3937
|
-
const $stores = new Map();
|
|
3938
|
-
|
|
3939
|
-
return {
|
|
3940
|
-
/**
|
|
3941
|
-
* Create a new state follower and return it.
|
|
3942
|
-
* @param {string} name
|
|
3943
|
-
* @returns {ObservableItem}
|
|
3944
|
-
*/
|
|
3945
|
-
use(name) {
|
|
3946
|
-
const {observer: originalObserver, subscribers } = $stores.get(name);
|
|
3947
|
-
const observerFollower = Observable(originalObserver.val());
|
|
3948
|
-
const unSubscriber = originalObserver.subscribe(value => observerFollower.set(value));
|
|
3949
|
-
const updaterUnsubscriber = observerFollower.subscribe(value => originalObserver.set(value));
|
|
3950
|
-
observerFollower.destroy = () => {
|
|
3951
|
-
unSubscriber();
|
|
3952
|
-
updaterUnsubscriber();
|
|
3953
|
-
observerFollower.cleanup();
|
|
3954
|
-
};
|
|
3955
|
-
subscribers.add(observerFollower);
|
|
3956
|
-
|
|
3957
|
-
return observerFollower;
|
|
3958
|
-
},
|
|
3959
|
-
/**
|
|
3960
|
-
* @param {string} name
|
|
3961
|
-
* @returns {ObservableItem}
|
|
3962
|
-
*/
|
|
3963
|
-
follow(name) {
|
|
3964
|
-
return this.use(name);
|
|
3965
|
-
},
|
|
3966
|
-
/**
|
|
3967
|
-
* Create a new state and return the observer.
|
|
3968
|
-
* @param {string} name
|
|
3969
|
-
* @param {*} value
|
|
3970
|
-
* @returns {ObservableItem}
|
|
3971
|
-
*/
|
|
3972
|
-
create(name, value) {
|
|
3973
|
-
const observer = Observable(value);
|
|
3974
|
-
$stores.set(name, { observer, subscribers: new Set()});
|
|
3975
|
-
return observer;
|
|
3976
|
-
},
|
|
3977
|
-
/**
|
|
3978
|
-
* Get the observer for a state.
|
|
3979
|
-
* @param {string} name
|
|
3980
|
-
* @returns {null|ObservableItem}
|
|
3981
|
-
*/
|
|
3982
|
-
get(name) {
|
|
3983
|
-
const item = $stores.get(name);
|
|
3984
|
-
return item ? item.observer : null;
|
|
3985
|
-
},
|
|
3986
|
-
/**
|
|
3987
|
-
*
|
|
3988
|
-
* @param {string} name
|
|
3989
|
-
* @returns {{observer: ObservableItem, subscribers: Set}}
|
|
3990
|
-
*/
|
|
3991
|
-
getWithSubscribers(name) {
|
|
3992
|
-
return $stores.get(name);
|
|
3993
|
-
},
|
|
3994
|
-
/**
|
|
3995
|
-
* Delete a state.
|
|
3996
|
-
* @param {string} name
|
|
3997
|
-
*/
|
|
3998
|
-
delete(name) {
|
|
3999
|
-
const item = $stores.get(name);
|
|
4000
|
-
if(!item) return;
|
|
4001
|
-
item.observer.cleanup();
|
|
4002
|
-
item.subscribers.forEach(follower => follower.destroy());
|
|
4003
|
-
item.observer.clear();
|
|
4004
|
-
}
|
|
4005
|
-
};
|
|
4006
|
-
}());
|
|
4007
|
-
|
|
4008
4651
|
/**
|
|
4009
4652
|
* Renders a list of items from an observable array or object, automatically updating when data changes.
|
|
4010
4653
|
* Efficiently manages DOM updates by tracking items with keys.
|
|
@@ -6430,11 +7073,14 @@ var NativeDocument = (function (exports) {
|
|
|
6430
7073
|
configs.body = params;
|
|
6431
7074
|
}
|
|
6432
7075
|
else {
|
|
6433
|
-
configs.headers['Content-Type'] = 'application/json';
|
|
6434
7076
|
if(method !== 'GET') {
|
|
7077
|
+
configs.headers['Content-Type'] = 'application/json';
|
|
6435
7078
|
configs.body = JSON.stringify(params);
|
|
6436
7079
|
} else {
|
|
6437
|
-
|
|
7080
|
+
const queryString = new URLSearchParams(params).toString();
|
|
7081
|
+
if (queryString) {
|
|
7082
|
+
endpoint = endpoint + (endpoint.includes('?') ? '&' : '?') + queryString;
|
|
7083
|
+
}
|
|
6438
7084
|
}
|
|
6439
7085
|
}
|
|
6440
7086
|
}
|
|
@@ -6505,6 +7151,7 @@ var NativeDocument = (function (exports) {
|
|
|
6505
7151
|
exports.PluginsManager = PluginsManager;
|
|
6506
7152
|
exports.SingletonView = SingletonView;
|
|
6507
7153
|
exports.Store = Store;
|
|
7154
|
+
exports.StoreFactory = StoreFactory;
|
|
6508
7155
|
exports.TemplateCloner = TemplateCloner;
|
|
6509
7156
|
exports.Validator = Validator;
|
|
6510
7157
|
exports.autoMemoize = autoMemoize;
|