native-document 1.0.14 → 1.0.15

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.
Files changed (37) hide show
  1. package/dist/native-document.dev.js +1262 -839
  2. package/dist/native-document.min.js +1 -1
  3. package/docs/anchor.md +216 -53
  4. package/docs/conditional-rendering.md +25 -24
  5. package/docs/core-concepts.md +20 -19
  6. package/docs/elements.md +21 -20
  7. package/docs/getting-started.md +28 -27
  8. package/docs/lifecycle-events.md +2 -2
  9. package/docs/list-rendering.md +607 -0
  10. package/docs/memory-management.md +1 -1
  11. package/docs/observables.md +15 -14
  12. package/docs/routing.md +22 -22
  13. package/docs/state-management.md +8 -8
  14. package/docs/validation.md +0 -2
  15. package/index.js +6 -1
  16. package/package.json +1 -1
  17. package/readme.md +5 -4
  18. package/src/data/MemoryManager.js +8 -20
  19. package/src/data/Observable.js +2 -180
  20. package/src/data/ObservableChecker.js +25 -24
  21. package/src/data/ObservableItem.js +158 -79
  22. package/src/data/observable-helpers/array.js +74 -0
  23. package/src/data/observable-helpers/batch.js +22 -0
  24. package/src/data/observable-helpers/computed.js +28 -0
  25. package/src/data/observable-helpers/object.js +111 -0
  26. package/src/elements/anchor.js +54 -9
  27. package/src/elements/control/for-each-array.js +280 -0
  28. package/src/elements/control/for-each.js +87 -110
  29. package/src/elements/index.js +1 -0
  30. package/src/elements/list.js +4 -0
  31. package/src/utils/helpers.js +44 -21
  32. package/src/wrappers/AttributesWrapper.js +5 -18
  33. package/src/wrappers/DocumentObserver.js +58 -29
  34. package/src/wrappers/ElementCreator.js +114 -0
  35. package/src/wrappers/HtmlElementEventsWrapper.js +52 -65
  36. package/src/wrappers/HtmlElementWrapper.js +11 -167
  37. package/src/wrappers/NdPrototype.js +109 -0
@@ -1,84 +1,6 @@
1
1
  var NativeDocument = (function (exports) {
2
2
  'use strict';
3
3
 
4
- function eventWrapper(element, name, callback) {
5
- element.addEventListener(name, callback);
6
- return element;
7
- }
8
-
9
- /**
10
- *
11
- * @param {HTMLElement} element
12
- * @returns {HTMLElement}
13
- */
14
- function HtmlElementEventsWrapper(element) {
15
-
16
- if(!element.nd) {
17
- element.nd = {};
18
- }
19
-
20
- /**
21
- * @param {Object<string,Function>} events
22
- */
23
- element.nd.on = function(events) {
24
- for(const event in events) {
25
- const callback = events[event];
26
- eventWrapper(element, event, callback);
27
- }
28
- return element;
29
- };
30
- element.nd.on.prevent = function(events) {
31
- for(const event in events) {
32
- const callback = events[event];
33
- eventWrapper(element, event, (event) => {
34
- event.preventDefault();
35
- callback && callback(event);
36
- return element;
37
- });
38
- }
39
- return element;
40
- };
41
- const events = {
42
- click: (callback) => eventWrapper(element, 'click', callback),
43
- focus: (callback) => eventWrapper(element, 'focus', callback),
44
- blur: (callback) => eventWrapper(element, 'blur', callback),
45
- input: (callback) => eventWrapper(element, 'input', callback),
46
- change: (callback) => eventWrapper(element, 'change', callback),
47
- keyup: (callback) => eventWrapper(element, 'keyup', callback),
48
- keydown: (callback) => eventWrapper(element, 'keydown', callback),
49
- beforeInput: (callback) => eventWrapper(element, 'beforeinput', callback),
50
- mouseOver: (callback) => eventWrapper(element, 'mouseover', callback),
51
- mouseOut: (callback) => eventWrapper(element, 'mouseout', callback),
52
- mouseDown: (callback) => eventWrapper(element, 'mousedown', callback),
53
- mouseUp: (callback) => eventWrapper(element, 'mouseup', callback),
54
- mouseMove: (callback) => eventWrapper(element, 'mousemove', callback),
55
- hover: (mouseInCallback, mouseOutCallback) => {
56
- element.addEventListener('mouseover', mouseInCallback);
57
- element.addEventListener('mouseout', mouseOutCallback);
58
- },
59
- dropped: (callback) => eventWrapper(element, 'drop', callback),
60
- submit: (callback) => eventWrapper(element, 'submit', callback),
61
- dragEnd: (callback) => eventWrapper(element, 'dragend', callback),
62
- dragStart: (callback) => eventWrapper(element, 'dragstart', callback),
63
- drop: (callback) => eventWrapper(element, 'drop', callback),
64
- dragOver: (callback) => eventWrapper(element, 'dragover', callback),
65
- dragEnter: (callback) => eventWrapper(element, 'dragenter', callback),
66
- dragLeave: (callback) => eventWrapper(element, 'dragleave', callback),
67
- };
68
- for(let event in events) {
69
- element.nd.on[event] = events[event];
70
- element.nd.on.prevent[event] = function(callback) {
71
- eventWrapper(element, event.toLowerCase(), (event) => {
72
- event.preventDefault();
73
- callback && callback(event);
74
- });
75
- return element;
76
- };
77
- }
78
-
79
- return element;
80
- }
81
-
82
4
  const DebugManager = {
83
5
  enabled: false,
84
6
 
@@ -111,38 +33,25 @@ var NativeDocument = (function (exports) {
111
33
 
112
34
  const MemoryManager = (function() {
113
35
 
114
- let $nexObserverId = 0;
36
+ let $nextObserverId = 0;
115
37
  const $observables = new Map();
116
- let $registry = null;
117
- try {
118
- $registry = new FinalizationRegistry((heldValue) => {
119
- DebugManager.log('MemoryManager', '🧹 Auto-cleanup observable:', heldValue);
120
- heldValue.listeners.splice(0);
121
- });
122
- } catch (e) {
123
- DebugManager.warn('MemoryManager', 'FinalizationRegistry not supported, observables will not be cleaned automatically');
124
- }
125
38
 
126
39
  return {
127
40
  /**
128
41
  * Register an observable and return an id.
129
42
  *
130
43
  * @param {ObservableItem} observable
131
- * @param {Function[]} listeners
44
+ * @param {Function} getListeners
132
45
  * @returns {number}
133
46
  */
134
- register(observable, listeners) {
135
- const id = ++$nexObserverId;
136
- const heldValue = {
137
- id: id,
138
- listeners
139
- };
140
- if($registry) {
141
- $registry.register(observable, heldValue);
142
- }
47
+ register(observable) {
48
+ const id = ++$nextObserverId;
143
49
  $observables.set(id, new WeakRef(observable));
144
50
  return id;
145
51
  },
52
+ unregister(id) {
53
+ $observables.delete(id);
54
+ },
146
55
  getObservableById(id) {
147
56
  return $observables.get(id)?.deref();
148
57
  },
@@ -193,35 +102,36 @@ var NativeDocument = (function (exports) {
193
102
  function ObservableChecker($observable, $checker) {
194
103
  this.observable = $observable;
195
104
  this.checker = $checker;
196
- const $unSubscriptions = [];
105
+ this.unSubscriptions = [];
106
+ }
197
107
 
198
- this.subscribe = function(callback) {
199
- const unSubscribe = $observable.subscribe((value) => {
200
- callback && callback($checker(value));
201
- });
202
- $unSubscriptions.push(unSubscribe);
108
+ ObservableChecker.prototype.subscribe = function(callback) {
109
+ const unSubscribe = this.observable.subscribe((value) => {
110
+ callback && callback(this.checker(value));
111
+ });
112
+ this.unSubscriptions.push(unSubscribe);
113
+ return unSubscribe;
114
+ };
203
115
 
204
- return unSubscribe;
205
- };
116
+ ObservableChecker.prototype.check = function(callback) {
117
+ return this.observable.check(() => callback(this.val()));
118
+ };
206
119
 
207
- this.val = function() {
208
- return $checker && $checker($observable.val());
209
- };
210
- this.check = function(callback) {
211
- return $observable.check(() => callback(this.val()));
212
- };
120
+ ObservableChecker.prototype.val = function() {
121
+ return this.checker && this.checker(this.observable.val());
122
+ };
213
123
 
214
- this.set = function(value) {
215
- return $observable.set(value);
216
- };
217
- this.trigger = function() {
218
- return $observable.trigger();
219
- };
124
+ ObservableChecker.prototype.set = function(value) {
125
+ return this.observable.set(value);
126
+ };
220
127
 
221
- this.cleanup = function() {
222
- $unSubscriptions.forEach(unSubscription => unSubscription());
223
- };
224
- }
128
+ ObservableChecker.prototype.trigger = function() {
129
+ return this.observable.trigger();
130
+ };
131
+
132
+ ObservableChecker.prototype.cleanup = function() {
133
+ return this.observable.cleanup();
134
+ };
225
135
 
226
136
  /**
227
137
  *
@@ -236,104 +146,183 @@ var NativeDocument = (function (exports) {
236
146
  throw new NativeDocumentError('ObservableItem cannot be an Observable');
237
147
  }
238
148
 
239
- const $initialValue = (typeof value === 'object') ? JSON.parse(JSON.stringify(value)) : value;
149
+ this.$previousValue = value;
150
+ this.$currentValue = value;
151
+ this.$isCleanedUp = false;
240
152
 
241
- let $previousValue = value;
242
- let $currentValue = value;
243
- let $isCleanedUp = false;
153
+ this.$listeners = null;
154
+ this.$watchers = null;
244
155
 
245
- const $listeners = [];
156
+ this.$memoryId = MemoryManager.register(this);
157
+ }
246
158
 
247
- const $memoryId = MemoryManager.register(this, $listeners);
159
+ Object.defineProperty(ObservableItem.prototype, '$value', {
160
+ get() {
161
+ return this.$currentValue;
162
+ },
163
+ set(value) {
164
+ this.set(value);
165
+ },
166
+ configurable: true,
167
+ });
248
168
 
249
- this.trigger = () => {
250
- $listeners.forEach(listener => {
251
- try {
252
- listener($currentValue, $previousValue);
253
- } catch (error) {
254
- DebugManager.error('Listener Undefined', 'Error in observable listener:', error);
255
- this.unsubscribe(listener);
169
+ ObservableItem.prototype.triggerListeners = function(operations) {
170
+ const $listeners = this.$listeners;
171
+ const $previousValue = this.$previousValue;
172
+ const $currentValue = this.$currentValue;
173
+
174
+ operations = operations || {};
175
+ if($listeners?.length) {
176
+ for(let i = 0, length = $listeners.length; i < length; i++) {
177
+ $listeners[i]($currentValue, $previousValue, operations);
178
+ }
179
+ }
180
+ };
181
+
182
+ ObservableItem.prototype.triggerWatchers = function() {
183
+ if(!this.$watchers) {
184
+ return;
185
+ }
186
+
187
+ const $watchers = this.$watchers;
188
+ const $previousValue = this.$previousValue;
189
+ const $currentValue = this.$currentValue;
190
+
191
+ if($watchers.has($currentValue)) {
192
+ const watchValueList = $watchers.get($currentValue);
193
+ watchValueList.forEach(itemValue => {
194
+ if(itemValue.ifTrue.called) {
195
+ return;
196
+ }
197
+ itemValue.ifTrue.callback();
198
+ itemValue.else.called = false;
199
+ });
200
+ }
201
+ if($watchers.has($previousValue)) {
202
+ const watchValueList = $watchers.get($previousValue);
203
+ watchValueList.forEach(itemValue => {
204
+ if(itemValue.else.called) {
205
+ return;
256
206
  }
207
+ itemValue.else.callback();
208
+ itemValue.ifTrue.called = false;
257
209
  });
210
+ }
211
+ };
258
212
 
259
- };
213
+ ObservableItem.prototype.trigger = function(operations) {
214
+ this.triggerListeners(operations);
215
+ this.triggerWatchers();
216
+ };
260
217
 
261
- this.originalValue = () => $initialValue;
218
+ /**
219
+ * @param {*} data
220
+ */
221
+ ObservableItem.prototype.set = function(data) {
222
+ const newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
223
+ if(this.$currentValue === newValue) {
224
+ return;
225
+ }
226
+ this.$previousValue = this.$currentValue;
227
+ this.$currentValue = newValue;
228
+ this.trigger();
229
+ };
262
230
 
263
- /**
264
- * @param {*} data
265
- */
266
- this.set = (data) => {
267
- const newValue = (typeof data === 'function') ? data($currentValue) : data;
268
- if($currentValue === newValue) {
269
- return;
231
+ ObservableItem.prototype.val = function() {
232
+ return this.$currentValue;
233
+ };
234
+
235
+ ObservableItem.prototype.disconnectAll = function() {
236
+ this.$listeners?.splice(0);
237
+ this.$previousValue = null;
238
+ this.$currentValue = null;
239
+ if(this.$watchers) {
240
+ for (const [_, watchValueList] of this.$watchers) {
241
+ for (const itemValue of watchValueList) {
242
+ itemValue.ifTrue.callback = null;
243
+ itemValue.else.callback = null;
244
+ }
245
+ watchValueList.clear();
270
246
  }
271
- $previousValue = $currentValue;
272
- $currentValue = newValue;
273
- this.trigger();
274
- };
247
+ }
248
+ this.$watchers?.clear();
249
+ this.$listeners = null;
250
+ this.$watchers = null;
251
+ };
252
+ ObservableItem.prototype.cleanup = function() {
253
+ MemoryManager.unregister(this.$memoryId);
254
+ this.disconnectAll();
255
+ this.$isCleanedUp = true;
256
+ delete this.$value;
257
+ };
275
258
 
276
- this.val = () => $currentValue;
259
+ /**
260
+ *
261
+ * @param {Function} callback
262
+ * @returns {(function(): void)}
263
+ */
264
+ ObservableItem.prototype.subscribe = function(callback) {
265
+ this.$listeners = this.$listeners ?? [];
266
+ if (this.$isCleanedUp) {
267
+ DebugManager.warn('Observable subscription', '⚠️ Attempted to subscribe to a cleaned up observable.');
268
+ return () => {};
269
+ }
270
+ if (typeof callback !== 'function') {
271
+ throw new NativeDocumentError('Callback must be a function');
272
+ }
277
273
 
278
- this.cleanup = function() {
279
- $listeners.splice(0);
280
- $isCleanedUp = true;
281
- };
274
+ this.$listeners.push(callback);
275
+ return () => this.unsubscribe(callback);
276
+ };
282
277
 
283
- /**
284
- *
285
- * @param {Function} callback
286
- * @returns {(function(): void)}
287
- */
288
- this.subscribe = (callback) => {
289
- if ($isCleanedUp) {
290
- DebugManager.warn('Observable subscription', '⚠️ Attempted to subscribe to a cleaned up observable.');
291
- return () => {};
292
- }
293
- if (typeof callback !== 'function') {
294
- throw new NativeDocumentError('Callback must be a function');
295
- }
278
+ ObservableItem.prototype.on = function(value, callback, elseCallback) {
279
+ this.$watchers = this.$watchers ?? new Map();
296
280
 
297
- $listeners.push(callback);
298
- return () => this.unsubscribe(callback);
299
- };
281
+ let watchValueList = this.$watchers.get(value);
282
+ if(!watchValueList) {
283
+ watchValueList = new Set();
284
+ this.$watchers.set(value, watchValueList);
285
+ }
300
286
 
301
- /**
302
- * Unsubscribe from an observable.
303
- * @param {Function} callback
304
- */
305
- this.unsubscribe = (callback) => {
306
- const index = $listeners.indexOf(callback);
307
- if (index > -1) {
308
- $listeners.splice(index, 1);
309
- }
287
+ let itemValue = {
288
+ ifTrue: { callback, called: false },
289
+ else: { callback: elseCallback, called: false }
310
290
  };
311
-
312
- /**
313
- * Create an Observable checker instance
314
- * @param callback
315
- * @returns {ObservableChecker}
316
- */
317
- this.check = function(callback) {
318
- return new ObservableChecker(this, callback)
291
+ watchValueList.add(itemValue);
292
+ return () => {
293
+ watchValueList?.delete(itemValue);
294
+ if(watchValueList.size === 0) {
295
+ this.$watchers?.delete(value);
296
+ }
297
+ watchValueList = null;
298
+ itemValue = null;
319
299
  };
300
+ };
320
301
 
321
- const $object = this;
322
- Object.defineProperty($object, '$value', {
323
- get() {
324
- return $object.val();
325
- },
326
- set(value) {
327
- $object.set(value);
328
- return $object;
329
- }
330
- });
302
+ /**
303
+ * Unsubscribe from an observable.
304
+ * @param {Function} callback
305
+ */
306
+ ObservableItem.prototype.unsubscribe = function(callback) {
307
+ const index = this.$listeners.indexOf(callback);
308
+ if (index > -1) {
309
+ this.$listeners.splice(index, 1);
310
+ }
311
+ };
331
312
 
332
- this.toString = function() {
333
- return '{{#ObItem::(' +$memoryId+ ')}}';
334
- };
313
+ /**
314
+ * Create an Observable checker instance
315
+ * @param callback
316
+ * @returns {ObservableChecker}
317
+ */
318
+ ObservableItem.prototype.check = function(callback) {
319
+ return new ObservableChecker(this, callback)
320
+ };
321
+ ObservableItem.prototype.get = ObservableItem.prototype.check;
335
322
 
336
- }
323
+ ObservableItem.prototype.toString = function() {
324
+ return '{{#ObItem::(' +this.$memoryId+ ')}}';
325
+ };
337
326
 
338
327
  const Validator = {
339
328
  isObservable(value) {
@@ -450,40 +439,182 @@ var NativeDocument = (function (exports) {
450
439
  }
451
440
  };
452
441
 
442
+ const getChildAsNode = (child) => {
443
+ if(Validator.isFunction(child)) {
444
+ return getChildAsNode(child());
445
+ }
446
+ if(Validator.isElement(child)) {
447
+ return child;
448
+ }
449
+ return createTextNode(child)
450
+ };
451
+
452
+ function Anchor(name) {
453
+ const element = document.createDocumentFragment();
454
+
455
+ const anchorStart = document.createComment('Anchor Start : '+name);
456
+ const anchorEnd = document.createComment('/ Anchor End '+name);
457
+
458
+ element.appendChild(anchorStart);
459
+ element.appendChild(anchorEnd);
460
+
461
+ element.nativeInsertBefore = element.insertBefore;
462
+ element.nativeAppendChild = element.appendChild;
463
+
464
+ const insertBefore = function(parent, child, target) {
465
+ if(parent === element) {
466
+ parent.nativeInsertBefore(getChildAsNode(child), target);
467
+ return;
468
+ }
469
+ parent.insertBefore(getChildAsNode(child), target);
470
+ };
471
+
472
+ element.appendElement = function(child, before = null) {
473
+ if(anchorEnd.parentNode === element) {
474
+ anchorEnd.parentNode.nativeInsertBefore(child, before || anchorEnd);
475
+ return;
476
+ }
477
+ anchorEnd.parentNode?.insertBefore(child, before || anchorEnd);
478
+ };
479
+
480
+ element.appendChild = function(child, before = null) {
481
+ const parent = anchorEnd.parentNode;
482
+ if(!parent) {
483
+ DebugManager.error('Anchor', 'Anchor : parent not found', child);
484
+ return;
485
+ }
486
+ before = before ?? anchorEnd;
487
+ if(Validator.isArray(child)) {
488
+ const fragment = document.createDocumentFragment();
489
+ for(let i = 0, length = child.length; i < length; i++) {
490
+ fragment.appendChild(getChildAsNode(child[i]));
491
+ }
492
+ insertBefore(parent, fragment, before);
493
+ return element;
494
+ }
495
+ insertBefore(parent, child, before);
496
+ };
497
+
498
+ element.removeChildren = function() {
499
+ const parent = anchorEnd.parentNode;
500
+ if(parent === element) {
501
+ return;
502
+ }
503
+ if(parent.firstChild === anchorStart && parent.lastChild === anchorEnd) {
504
+ parent.replaceChildren(anchorStart, anchorEnd);
505
+ return;
506
+ }
507
+
508
+ let itemToRemove = anchorStart.nextSibling, tempItem;
509
+ const fragment = document.createDocumentFragment();
510
+ while(itemToRemove && itemToRemove !== anchorEnd) {
511
+ tempItem = itemToRemove.nextSibling;
512
+ fragment.append(itemToRemove);
513
+ itemToRemove = tempItem;
514
+ }
515
+ fragment.replaceChildren();
516
+ };
517
+ element.remove = function() {
518
+ const parent = anchorEnd.parentNode;
519
+ if(parent === element) {
520
+ return;
521
+ }
522
+ let itemToRemove = anchorStart.nextSibling, tempItem;
523
+ while(itemToRemove !== anchorEnd) {
524
+ tempItem = itemToRemove.nextSibling;
525
+ element.nativeAppendChild(itemToRemove);
526
+ itemToRemove = tempItem;
527
+ }
528
+ };
529
+
530
+ element.removeWithAnchors = function() {
531
+ element.removeChildren();
532
+ anchorStart.remove();
533
+ anchorEnd.remove();
534
+ };
535
+
536
+ element.replaceContent = function(child) {
537
+ const parent = anchorEnd.parentNode;
538
+ if(!parent) {
539
+ return;
540
+ }
541
+ if(parent.firstChild === anchorStart && parent.lastChild === anchorEnd) {
542
+ parent.replaceChildren(anchorStart, child, anchorEnd);
543
+ return;
544
+ }
545
+ element.removeChildren();
546
+ parent.insertBefore(child, anchorEnd);
547
+ };
548
+
549
+ element.insertBefore = function(child, anchor = null) {
550
+ element.appendChild(child, anchor);
551
+ };
552
+
553
+ element.clear = function() {
554
+ element.remove();
555
+ };
556
+
557
+ element.endElement = function() {
558
+ return anchorEnd;
559
+ };
560
+ element.startElement = function() {
561
+ return anchorStart;
562
+ };
563
+
564
+ return element;
565
+ }
566
+
453
567
  const BOOLEAN_ATTRIBUTES = ['checked', 'selected', 'disabled', 'readonly', 'required', 'autofocus', 'multiple', 'autocomplete', 'hidden', 'contenteditable', 'spellcheck', 'translate', 'draggable', 'async', 'defer', 'autoplay', 'controls', 'loop', 'muted', 'download', 'reversed', 'open', 'default', 'formnovalidate', 'novalidate', 'scoped', 'itemscope', 'allowfullscreen', 'allowpaymentrequest', 'playsinline'];
454
568
 
569
+ const invoke = function(fn, args, context) {
570
+ if(context) {
571
+ fn.apply(context, args);
572
+ } else {
573
+ fn(...args);
574
+ }
575
+ };
455
576
  /**
456
577
  *
457
578
  * @param {Function} fn
458
579
  * @param {number} delay
459
- * @param {{leading?:Boolean, trailing?:Boolean, debounce?:Boolean}}options
580
+ * @param {{leading?:Boolean, trailing?:Boolean, debounce?:Boolean, check: Function}}options
460
581
  * @returns {(function(...[*]): void)|*}
461
582
  */
462
- const throttle = function(fn, delay, options = {}) {
583
+ const debounce = function(fn, delay, options = {}) {
463
584
  let timer = null;
464
- let lastExecTime = 0;
465
- const { leading = true, trailing = true, debounce = false } = options;
585
+ let lastArgs = null;
466
586
 
467
587
  return function(...args) {
468
- const now = Date.now();
469
- if (debounce) {
470
- // debounce mode: reset the timer for each call
471
- clearTimeout(timer);
472
- timer = setTimeout(() => fn.apply(this, args), delay);
473
- return;
474
- }
475
- if (leading && now - lastExecTime >= delay) {
476
- fn.apply(this, args);
477
- lastExecTime = now;
478
- }
479
- if (trailing && !timer) {
480
- timer = setTimeout(() => {
481
- fn.apply(this, args);
482
- lastExecTime = Date.now();
483
- timer = null;
484
- }, delay - (now - lastExecTime));
588
+ const context = options.context === true ? this : null;
589
+ if(options.check) {
590
+ options.check(...args);
485
591
  }
592
+ lastArgs = args;
593
+
594
+ // debounce mode: reset the timer for each call
595
+ clearTimeout(timer);
596
+ timer = setTimeout(() => invoke(fn, lastArgs, context), delay);
597
+ }
598
+ };
599
+
600
+
601
+ /**
602
+ *
603
+ * @param {*} item
604
+ * @param {string|null} defaultKey
605
+ * @param {?Function} key
606
+ * @returns {*}
607
+ */
608
+ const getKey = (item, defaultKey, key) => {
609
+ if(Validator.isFunction(key)) return key(item, defaultKey);
610
+ if(Validator.isObservable(item)) {
611
+ const val = item.val();
612
+ return (val && key) ? val[key] : defaultKey;
613
+ }
614
+ if(!Validator.isObject(item)) {
615
+ return item;
486
616
  }
617
+ return item[key] ?? defaultKey;
487
618
  };
488
619
 
489
620
  const trim = function(str, char) {
@@ -500,45 +631,6 @@ var NativeDocument = (function (exports) {
500
631
  return new ObservableItem(value);
501
632
  }
502
633
 
503
- /**
504
- *
505
- * @param {Function} callback
506
- * @param {Array|Function} dependencies
507
- * @returns {ObservableItem}
508
- */
509
- Observable.computed = function(callback, dependencies = []) {
510
- const initialValue = callback();
511
- const observable = new ObservableItem(initialValue);
512
- const updatedValue = () => observable.set(callback());
513
-
514
- if(Validator.isFunction(dependencies)) {
515
- if(!Validator.isObservable(dependencies.$observer)) {
516
- throw new NativeDocumentError('Observable.computed : dependencies must be valid batch function');
517
- }
518
- dependencies.$observer.subscribe(updatedValue);
519
- return observable;
520
- }
521
-
522
- dependencies.forEach(dependency => dependency.subscribe(updatedValue));
523
-
524
- return observable;
525
- };
526
-
527
- Observable.batch = function(callback) {
528
- const $observer = Observable(0);
529
- const batch = function() {
530
- if(Validator.isAsyncFunction(callback)) {
531
- return (callback(...arguments)).then(() => {
532
- $observer.trigger();
533
- }).catch(error => { throw error; });
534
- }
535
- callback(...arguments);
536
- $observer.trigger();
537
- };
538
- batch.$observer = $observer;
539
- return batch;
540
- };
541
-
542
634
  /**
543
635
  *
544
636
  * @param id
@@ -552,7 +644,6 @@ var NativeDocument = (function (exports) {
552
644
  return item;
553
645
  };
554
646
 
555
-
556
647
  /**
557
648
  *
558
649
  * @param {ObservableItem} observable
@@ -561,144 +652,6 @@ var NativeDocument = (function (exports) {
561
652
  observable.cleanup();
562
653
  };
563
654
 
564
- /**
565
- * Get the value of an observable or an object of observables.
566
- * @param {ObservableItem|Object<ObservableItem>} object
567
- * @returns {{}|*|null}
568
- */
569
- Observable.value = function(data) {
570
- if(Validator.isObservable(data)) {
571
- return data.val();
572
- }
573
- if(Validator.isProxy(data)) {
574
- return data.$val();
575
- }
576
- if(Validator.isArray(data)) {
577
- const result = [];
578
- data.forEach(item => {
579
- result.push(Observable.value(item));
580
- });
581
- return result;
582
- }
583
- return data;
584
- };
585
-
586
- /**
587
- *
588
- * @param {Object} value
589
- * @returns {Proxy}
590
- */
591
- Observable.init = function(value) {
592
- const data = {};
593
- for(const key in value) {
594
- const itemValue = value[key];
595
- if(Validator.isJson(itemValue)) {
596
- data[key] = Observable.init(itemValue);
597
- continue;
598
- }
599
- else if(Validator.isArray(itemValue)) {
600
- data[key] = Observable.array(itemValue);
601
- continue;
602
- }
603
- data[key] = Observable(itemValue);
604
- }
605
-
606
- const $val = function() {
607
- const result = {};
608
- for(const key in data) {
609
- const dataItem = data[key];
610
- if(Validator.isObservable(dataItem)) {
611
- result[key] = dataItem.val();
612
- } else if(Validator.isProxy(dataItem)) {
613
- result[key] = dataItem.$val();
614
- } else {
615
- result[key] = dataItem;
616
- }
617
- }
618
- return result;
619
- };
620
- const $clone = function() {
621
-
622
- };
623
-
624
- return new Proxy(data, {
625
- get(target, property) {
626
- if(property === '__isProxy__') {
627
- return true;
628
- }
629
- if(property === '$val') {
630
- return $val;
631
- }
632
- if(property === '$clone') {
633
- return $clone;
634
- }
635
- if(target[property] !== undefined) {
636
- return target[property];
637
- }
638
- return undefined;
639
- },
640
- set(target, prop, newValue) {
641
- if(target[prop] !== undefined) {
642
- target[prop].set(newValue);
643
- }
644
- }
645
- })
646
- };
647
-
648
- Observable.object = Observable.init;
649
- Observable.json = Observable.init;
650
- Observable.update = function($target, data) {
651
- for(const key in data) {
652
- const targetItem = $target[key];
653
- const newValue = data[key];
654
-
655
- if(Validator.isObservable(targetItem)) {
656
- if(Validator.isArray(newValue)) {
657
- Observable.update(targetItem, newValue);
658
- continue;
659
- }
660
- targetItem.set(newValue);
661
- continue;
662
- }
663
- if(Validator.isProxy(targetItem)) {
664
- Observable.update(targetItem, newValue);
665
- continue;
666
- }
667
- $target[key] = newValue;
668
- }
669
- };
670
- /**
671
- *
672
- * @param {Array} target
673
- * @returns {ObservableItem}
674
- */
675
- Observable.array = function(target) {
676
- if(!Array.isArray(target)) {
677
- throw new NativeDocumentError('Observable.array : target must be an array');
678
- }
679
- const observer = Observable(target);
680
-
681
- const methods = ['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice'];
682
-
683
- methods.forEach((method) => {
684
- observer[method] = function(...values) {
685
- const target = observer.val();
686
- const result = target[method].apply(target, arguments);
687
- observer.trigger();
688
- return result;
689
- };
690
- });
691
-
692
- const overrideMethods = ['map', 'filter', 'reduce', 'some', 'every', 'find', 'findIndex'];
693
- overrideMethods.forEach((method) => {
694
- observer[method] = function(callback) {
695
- return observer.val()[method](callback);
696
- };
697
- });
698
-
699
- return observer;
700
- };
701
-
702
655
  /**
703
656
  * Enable auto cleanup of observables.
704
657
  * @param {Boolean} enable
@@ -717,20 +670,6 @@ var NativeDocument = (function (exports) {
717
670
  setInterval(() => MemoryManager.cleanObservables(threshold), interval);
718
671
  };
719
672
 
720
- /**
721
- *
722
- * @param {HTMLElement} element
723
- * @param {string} className
724
- * @param {string} value
725
- */
726
- const toggleClassItem = function(element, className, value) {
727
- if(value) {
728
- element.classList.add(className);
729
- } else {
730
- element.classList.remove(className);
731
- }
732
- };
733
-
734
673
  /**
735
674
  *
736
675
  * @param {HTMLElement} element
@@ -740,11 +679,11 @@ var NativeDocument = (function (exports) {
740
679
  for(let className in data) {
741
680
  const value = data[className];
742
681
  if(Validator.isObservable(value)) {
743
- toggleClassItem(element, className, value.val());
744
- value.subscribe(newValue => toggleClassItem(element, className, newValue));
682
+ element.classList.toggle(className, value.val());
683
+ value.subscribe(newValue => element.classList.toggle(className, newValue));
745
684
  continue;
746
685
  }
747
- toggleClassItem(element, className, value);
686
+ element.classList.toggle(className, value);
748
687
  }
749
688
  }
750
689
 
@@ -816,8 +755,8 @@ var NativeDocument = (function (exports) {
816
755
  }
817
756
  element.setAttribute(attributeName, newValue);
818
757
  };
819
- value.subscribe(applyValue);
820
758
  applyValue(value.val());
759
+ value.subscribe(applyValue);
821
760
 
822
761
  if(attributeName === 'value') {
823
762
  element.addEventListener('input', () => value.set(element.value));
@@ -866,228 +805,305 @@ var NativeDocument = (function (exports) {
866
805
  continue;
867
806
  }
868
807
  element.setAttribute(attributeName, value);
808
+
869
809
  }
870
810
  return element;
871
811
  }
872
812
 
873
- const DocumentObserver = {
874
- elements: new Map(),
875
- observer: null,
876
- checkMutation: throttle(function() {
877
- for(const [element, data] of DocumentObserver.elements.entries()) {
878
- const isCurrentlyInDom = document.body.contains(element);
879
- if(isCurrentlyInDom && !data.inDom) {
880
- data.inDom = true;
881
- data.mounted.forEach(callback => callback(element));
882
- } else if(!isCurrentlyInDom && data.inDom) {
883
- data.inDom = false;
884
- data.unmounted.forEach(callback => callback(element));
813
+ const $nodeCache = new Map();
814
+ let $textNodeCache = null;
815
+
816
+ const ElementCreator = {
817
+ createTextNode() {
818
+ if(!$textNodeCache) {
819
+ $textNodeCache = document.createTextNode('');
820
+ }
821
+ return $textNodeCache.cloneNode();
822
+ },
823
+ /**
824
+ *
825
+ * @param {HTMLElement|DocumentFragment} parent
826
+ * @param {ObservableItem} observable
827
+ * @returns {Text}
828
+ */
829
+ createObservableNode(parent, observable) {
830
+ const text = ElementCreator.createTextNode();
831
+ observable.subscribe(value => text.nodeValue = String(value));
832
+ text.nodeValue = observable.val();
833
+ parent && parent.appendChild(text);
834
+ return text;
835
+ },
836
+
837
+ /**
838
+ *
839
+ * @param {HTMLElement|DocumentFragment} parent
840
+ * @param {*} value
841
+ * @returns {Text}
842
+ */
843
+ createStaticTextNode(parent, value) {
844
+ let text = ElementCreator.createTextNode();
845
+ text.nodeValue = String(value);
846
+ parent && parent.appendChild(text);
847
+ return text;
848
+ },
849
+ /**
850
+ *
851
+ * @param {string} name
852
+ * @returns {HTMLElement|DocumentFragment}
853
+ */
854
+ createElement(name) {
855
+ if(name) {
856
+ if($nodeCache.has(name)) {
857
+ return $nodeCache.get(name).cloneNode();
858
+ }
859
+ const node = document.createElement(name);
860
+ $nodeCache.set(name, node);
861
+ return node.cloneNode();
862
+ }
863
+ return new Anchor('Fragment');
864
+ },
865
+ /**
866
+ *
867
+ * @param {*} children
868
+ * @param {HTMLElement|DocumentFragment} parent
869
+ */
870
+ processChildren(children, parent) {
871
+ if(children === null) return;
872
+ const childrenArray = Array.isArray(children) ? children : [children];
873
+
874
+ for(let i = 0, length = childrenArray.length; i < length; i++) {
875
+ let child = childrenArray[i];
876
+ if (child === null) continue;
877
+ if(Validator.isString(child) && Validator.isFunction(child.resolveObservableTemplate)) {
878
+ child = child.resolveObservableTemplate();
879
+ }
880
+ if(Validator.isFunction(child)) {
881
+ this.processChildren(child(), parent);
882
+ continue;
883
+ }
884
+ if(Validator.isArray(child)) {
885
+ this.processChildren(child, parent);
886
+ continue;
887
+ }
888
+ if (Validator.isElement(child)) {
889
+ parent.appendChild(child);
890
+ continue;
891
+ }
892
+ if (Validator.isObservable(child)) {
893
+ ElementCreator.createObservableNode(parent, child);
894
+ continue;
895
+ }
896
+ if (child) {
897
+ ElementCreator.createStaticTextNode(parent, child);
885
898
  }
886
899
  }
887
- }, 10, { debounce: true }),
900
+ },
888
901
  /**
889
902
  *
890
903
  * @param {HTMLElement} element
891
- * @returns {{watch: (function(): Map<any, any>), disconnect: (function(): boolean), mounted: (function(*): Set<any>), unmounted: (function(*): Set<any>)}}
904
+ * @param {Object} attributes
892
905
  */
893
- watch: function(element) {
894
- let data = {};
895
- if(DocumentObserver.elements.has(element)) {
896
- data = DocumentObserver.elements.get(element);
897
- } else {
898
- const inDom = document.body.contains(element);
899
- data = {
900
- inDom,
901
- mounted: new Set(),
902
- unmounted: new Set(),
903
- };
904
- DocumentObserver.elements.set(element, data);
906
+ processAttributes(element, attributes) {
907
+ if(Validator.isFragment(element)) return;
908
+ if (attributes) {
909
+ AttributesWrapper(element, attributes);
905
910
  }
906
-
907
- return {
908
- watch: () => DocumentObserver.elements.set(element, data),
909
- disconnect: () => DocumentObserver.elements.delete(element),
910
- mounted: (callback) => data.mounted.add(callback),
911
- unmounted: (callback) => data.unmounted.add(callback)
912
- };
911
+ },
912
+ /**
913
+ *
914
+ * @param {HTMLElement} element
915
+ * @param {Object} attributes
916
+ * @param {?Function} customWrapper
917
+ * @returns {HTMLElement|DocumentFragment}
918
+ */
919
+ setup(element, attributes, customWrapper) {
920
+ return element;
913
921
  }
914
922
  };
915
923
 
916
- DocumentObserver.observer = new MutationObserver(DocumentObserver.checkMutation);
917
- DocumentObserver.observer.observe(document.body, {
918
- childList: true,
919
- subtree: true,
920
- });
921
-
922
- const getChildAsNode = (child) => {
923
- if(Validator.isFunction(child)) {
924
- return getChildAsNode(child());
925
- }
926
- if(Validator.isElement(child)) {
927
- return child;
928
- }
929
- return createTextNode(child)
930
- };
931
-
932
- function Anchor(name) {
933
- const element = document.createDocumentFragment();
934
-
935
- const anchorStart = document.createComment('Anchor Start : '+name);
936
- const anchorEnd = document.createComment('/ Anchor End '+name);
937
-
938
- element.appendChild(anchorStart);
939
- element.appendChild(anchorEnd);
940
-
941
- element.nativeInsertBefore = element.insertBefore;
942
- element.nativeAppendChild = element.appendChild;
943
-
944
- const insertBefore = function(parent, child, target) {
945
- if(parent === element) {
946
- parent.nativeInsertBefore(getChildAsNode(child), target);
947
- return;
948
- }
949
- parent.insertBefore(getChildAsNode(child), target);
950
- };
951
-
952
- element.appendChild = function(child, before = null) {
953
- const parent = anchorEnd.parentNode;
954
- if(!parent) {
955
- DebugManager.error('Anchor', 'Anchor : parent not found', child);
956
- return;
957
- }
958
- before = before ?? anchorEnd;
959
- if(Validator.isArray(child)) {
960
- child.forEach((element) => {
961
- insertBefore(parent, element, before);
962
- });
963
- return element;
964
- }
965
- insertBefore(parent, child, before);
966
- };
924
+ const DocumentObserver = {
925
+ mounted: new WeakMap(),
926
+ mountedSupposedSize: 0,
927
+ unmounted: new WeakMap(),
928
+ unmountedSupposedSize: 0,
929
+ observer: null,
930
+ checkMutation: debounce(function(mutationsList) {
931
+ for(const mutation of mutationsList) {
932
+ if(DocumentObserver.mountedSupposedSize > 0 ) {
933
+ for(const node of mutation.addedNodes) {
934
+ const data = DocumentObserver.mounted.get(node);
935
+ if(!data) {
936
+ continue;
937
+ }
938
+ data.inDom = true;
939
+ data.mounted && data.mounted(node);
940
+ }
941
+ }
967
942
 
968
- element.remove = function(trueRemove) {
969
- if(anchorEnd.parentNode === element) {
970
- return;
971
- }
972
- let itemToRemove = anchorStart.nextSibling, tempItem;
973
- while(itemToRemove !== anchorEnd) {
974
- tempItem = itemToRemove.nextSibling;
975
- trueRemove ? itemToRemove.remove() : element.nativeAppendChild(itemToRemove);
976
- itemToRemove = tempItem;
977
- }
978
- if(trueRemove) {
979
- anchorEnd.remove();
980
- anchorStart.remove();
943
+ if(DocumentObserver.unmountedSupposedSize > 0 ) {
944
+ for(const node of mutation.removedNodes) {
945
+ const data = DocumentObserver.unmounted.get(node);
946
+ if(!data) {
947
+ continue;
948
+ }
949
+
950
+ data.inDom = false;
951
+ if(data.unmounted && data.unmounted(node) === true) {
952
+ data.disconnect();
953
+ node.nd?.remove();
954
+ }
955
+ }
956
+ }
981
957
  }
982
- };
983
-
984
- element.insertBefore = function(child, anchor = null) {
985
- element.appendChild(child, anchor);
986
- };
987
-
988
- element.clear = function() {
989
- element.remove();
990
- };
991
-
992
- element.endElement = function() {
993
- return anchorEnd;
994
- };
995
- element.startElement = function() {
996
- return anchorStart;
997
- };
998
-
999
- return element;
1000
- }
1001
-
1002
- const pluginsManager = (function() {
1003
-
1004
- const $plugins = [];
1005
-
1006
- return {
1007
- list : () => $plugins,
1008
- add : (plugin) => $plugins.push(plugin)
1009
- };
1010
- }());
1011
-
1012
- /**
1013
- *
1014
- * @param {HTMLElement|DocumentFragment} parent
1015
- * @param {ObservableItem} observable
1016
- * @returns {Text}
1017
- */
1018
- const createObservableNode = function(parent, observable) {
1019
- const text = document.createTextNode('');
1020
- observable.subscribe(value => text.textContent = String(value));
1021
- text.textContent = observable.val();
1022
- parent && parent.appendChild(text);
1023
- return text;
1024
- };
958
+ }, 16),
959
+ /**
960
+ *
961
+ * @param {HTMLElement} element
962
+ * @param {boolean} inDom
963
+ * @returns {{watch: (function(): Map<any, any>), disconnect: (function(): boolean), mounted: (function(*): Set<any>), unmounted: (function(*): Set<any>)}}
964
+ */
965
+ watch: function(element, inDom = false) {
966
+ let data = {
967
+ inDom,
968
+ mounted: null,
969
+ unmounted: null,
970
+ disconnect: () => {
971
+ DocumentObserver.mounted.delete(element);
972
+ DocumentObserver.unmounted.delete(element);
973
+ DocumentObserver.mountedSupposedSize--;
974
+ DocumentObserver.unmountedSupposedSize--;
975
+ data = null;
976
+ }
977
+ };
1025
978
 
1026
- /**
1027
- *
1028
- * @param {HTMLElement|DocumentFragment} parent
1029
- * @param {*} value
1030
- * @returns {Text}
1031
- */
1032
- const createStaticTextNode = function(parent, value) {
1033
- const text = document.createTextNode('');
1034
- text.textContent = String(value);
1035
- parent && parent.appendChild(text);
1036
- return text;
979
+ return {
980
+ disconnect: data.disconnect,
981
+ mounted: (callback) => {
982
+ data.mounted = callback;
983
+ DocumentObserver.mounted.set(element, data);
984
+ DocumentObserver.mountedSupposedSize++;
985
+ },
986
+ unmounted: (callback) => {
987
+ data.unmounted = callback;
988
+ DocumentObserver.unmounted.set(element, data);
989
+ DocumentObserver.unmountedSupposedSize++;
990
+ }
991
+ };
992
+ }
1037
993
  };
1038
994
 
1039
- /**
1040
- *
1041
- * @param {HTMLElement} element
1042
- */
1043
- const addUtilsMethods = function(element) {
1044
- element.nd.wrap = (callback) => {
1045
- if(!Validator.isFunction(callback)) {
1046
- throw new NativeDocumentError('Callback must be a function');
1047
- }
1048
- callback && callback(element);
1049
- return element;
1050
- };
1051
- element.nd.ref = (target, name) => {
1052
- target[name] = element;
1053
- return element;
1054
- };
1055
-
1056
- let $observer = null;
1057
-
1058
- element.nd.appendChild = function(child) {
1059
- if(Validator.isArray(child)) {
1060
- ElementCreator.processChildren(child, element);
1061
- return;
1062
- }
1063
- if(Validator.isFunction(child)) {
1064
- child = child();
1065
- ElementCreator.processChildren(child(), element);
1066
- }
1067
- if(Validator.isElement(child)) {
1068
- ElementCreator.processChildren(child, element);
1069
- }
1070
- };
1071
-
1072
- element.nd.lifecycle = function(states) {
1073
- $observer = $observer || DocumentObserver.watch(element);
995
+ DocumentObserver.observer = new MutationObserver(DocumentObserver.checkMutation);
996
+ DocumentObserver.observer.observe(document.body, {
997
+ childList: true,
998
+ subtree: true,
999
+ });
1074
1000
 
1075
- states.mounted && $observer.mounted(states.mounted);
1076
- states.unmounted && $observer.unmounted(states.unmounted);
1077
- return element;
1078
- };
1001
+ Object.defineProperty(HTMLElement.prototype, 'nd', {
1002
+ get() {
1003
+ if(this.$ndProx) {
1004
+ return this.$ndProx;
1005
+ }
1006
+ let element = this;
1007
+ let lifecycle = null;
1008
+
1009
+ this.$ndProx = new Proxy({}, {
1010
+ get(target, property) {
1011
+ if(/^on[A-Z]/.test(property)) {
1012
+ const event = property.replace(/^on/, '').toLowerCase();
1013
+ const shouldPrevent = event.toLowerCase().startsWith('prevent');
1014
+ let eventName = event.replace(/^prevent/i, '');
1015
+ const shouldStop = event.toLowerCase().startsWith('stop');
1016
+ eventName = eventName.replace(/^stop/i, '');
1017
+
1018
+ return function(callback) {
1019
+ if(shouldPrevent && !shouldStop) {
1020
+ element.addEventListener(eventName, function(event) {
1021
+ event.preventDefault();
1022
+ callback(event);
1023
+ });
1024
+ return element;
1025
+ }
1026
+ if(!shouldPrevent && shouldStop) {
1027
+ element.addEventListener(eventName, function(event) {
1028
+ event.stopPropagation();
1029
+ callback(event);
1030
+ });
1031
+ return element;
1032
+ }
1033
+ if(shouldPrevent && shouldStop) {
1034
+ element.addEventListener(eventName, function(event) {
1035
+ event.preventDefault();
1036
+ event.stopPropagation();
1037
+ callback(event);
1038
+ });
1039
+ return element;
1040
+ }
1041
+ element.addEventListener(eventName, callback);
1042
+ return element;
1043
+ };
1044
+ }
1045
+ if(property === 'ref') {
1046
+ if(ref) {
1047
+ return ref;
1048
+ }
1049
+ return function(target, name) {
1050
+ target[name] = element;
1051
+ return element;
1052
+ };
1053
+ }
1054
+ if(property === 'unmountChildren') {
1055
+ return () => {
1056
+ for(let i = 0, length = element.children.length; i < length; i++) {
1057
+ let elementchildren = element.children[i];
1058
+ if(!elementchildren.$ndProx) {
1059
+ elementchildren.nd?.remove();
1060
+ }
1061
+ elementchildren = null;
1062
+ }
1063
+ };
1064
+ }
1065
+ if(property === 'remove') {
1066
+ return function() {
1067
+ element.nd.unmountChildren();
1068
+ lifecycle = null;
1069
+ element.$ndProx = null;
1070
+ delete element.nd?.on?.prevent;
1071
+ delete element.nd?.on;
1072
+ delete element.nd;
1073
+ };
1074
+ }
1075
+ if(property === 'hasLifecycle') {
1076
+ return lifecycle !== null;
1077
+ }
1078
+ if(property === 'lifecycle') {
1079
+ if(lifecycle) {
1080
+ return lifecycle;
1081
+ }
1082
+ let $observer = null;
1083
+ lifecycle = function(states) {
1084
+ $observer = $observer || DocumentObserver.watch(element);
1085
+
1086
+ states.mounted && $observer.mounted(states.mounted);
1087
+ states.unmounted && $observer.unmounted(states.unmounted);
1088
+ return element;
1089
+ };
1090
+ return lifecycle;
1091
+ }
1092
+ if(property === 'mounted' || property === 'unmounted') {
1093
+ return function(callback) {
1094
+ element.nd.lifecycle({ [property]: callback});
1095
+ return element;
1096
+ };
1097
+ }
1098
+ },
1099
+ set(target, p, newValue, receiver) {
1079
1100
 
1080
- element.nd.mounted = (callback) => {
1081
- $observer = $observer || DocumentObserver.watch(element);
1082
- $observer.mounted(callback);
1083
- return element;
1084
- };
1085
- element.nd.unmounted = (callback) => {
1086
- $observer = $observer || DocumentObserver.watch(element);
1087
- $observer.unmounted(callback);
1088
- return element;
1089
- };
1090
- };
1101
+ },
1102
+ configurable: true
1103
+ });
1104
+ return this.$ndProx;
1105
+ }
1106
+ });
1091
1107
 
1092
1108
  /**
1093
1109
  *
@@ -1096,84 +1112,10 @@ var NativeDocument = (function (exports) {
1096
1112
  */
1097
1113
  const createTextNode = function(value) {
1098
1114
  return (Validator.isObservable(value))
1099
- ? createObservableNode(null, value)
1100
- : createStaticTextNode(null, value);
1115
+ ? ElementCreator.createObservableNode(null, value)
1116
+ : ElementCreator.createStaticTextNode(null, value);
1101
1117
  };
1102
1118
 
1103
- const ElementCreator = {
1104
- /**
1105
- *
1106
- * @param {string} name
1107
- * @returns {HTMLElement|DocumentFragment}
1108
- */
1109
- createElement(name) {
1110
- return name ? document.createElement(name) : new Anchor('Fragment');
1111
- },
1112
- /**
1113
- *
1114
- * @param {*} children
1115
- * @param {HTMLElement|DocumentFragment} parent
1116
- */
1117
- processChildren(children, parent) {
1118
- if(children === null) return;
1119
- const childrenArray = Array.isArray(children) ? children : [children];
1120
- childrenArray.forEach(child => {
1121
- if (child === null) return;
1122
- if(Validator.isString(child) && Validator.isFunction(child.resolveObservableTemplate)) {
1123
- child = child.resolveObservableTemplate();
1124
- }
1125
- if(Validator.isFunction(child)) {
1126
- this.processChildren(child(), parent);
1127
- return;
1128
- }
1129
- if(Validator.isArray(child)) {
1130
- this.processChildren(child, parent);
1131
- return;
1132
- }
1133
- if (Validator.isElement(child)) {
1134
- parent.appendChild(child);
1135
- return;
1136
- }
1137
- if (Validator.isObservable(child)) {
1138
- createObservableNode(parent, child);
1139
- return;
1140
- }
1141
- if (child) {
1142
- createStaticTextNode(parent, child);
1143
- }
1144
- });
1145
- },
1146
- /**
1147
- *
1148
- * @param {HTMLElement} element
1149
- * @param {Object} attributes
1150
- */
1151
- processAttributes(element, attributes) {
1152
- if(Validator.isFragment(element)) return;
1153
- if (attributes) {
1154
- AttributesWrapper(element, attributes);
1155
- }
1156
- },
1157
- /**
1158
- *
1159
- * @param {HTMLElement} element
1160
- * @param {Object} attributes
1161
- * @param {?Function} customWrapper
1162
- * @returns {HTMLElement|DocumentFragment}
1163
- */
1164
- setup(element, attributes, customWrapper) {
1165
- element.nd = {};
1166
- HtmlElementEventsWrapper(element);
1167
- const item = (typeof customWrapper === 'function') ? customWrapper(element) : element;
1168
- addUtilsMethods(item);
1169
-
1170
- pluginsManager.list().forEach(plugin => {
1171
- plugin?.element?.setup && plugin.element.setup(item, attributes);
1172
- });
1173
-
1174
- return item;
1175
- }
1176
- };
1177
1119
 
1178
1120
  /**
1179
1121
  *
@@ -1182,7 +1124,7 @@ var NativeDocument = (function (exports) {
1182
1124
  * @returns {Function}
1183
1125
  */
1184
1126
  function HtmlElementWrapper(name, customWrapper) {
1185
- const $tagName = name.toLowerCase().trim();
1127
+ const $tagName = name.toLowerCase();
1186
1128
 
1187
1129
  const builder = function(attributes, children = null) {
1188
1130
  try {
@@ -1192,11 +1134,12 @@ var NativeDocument = (function (exports) {
1192
1134
  attributes = tempChildren;
1193
1135
  }
1194
1136
  const element = ElementCreator.createElement($tagName);
1137
+ const finalElement = (typeof customWrapper === 'function') ? customWrapper(element) : element;
1195
1138
 
1196
- ElementCreator.processAttributes(element, attributes);
1197
- ElementCreator.processChildren(children, element);
1139
+ ElementCreator.processAttributes(finalElement, attributes);
1140
+ ElementCreator.processChildren(children, finalElement);
1198
1141
 
1199
- return ElementCreator.setup(element, attributes, customWrapper);
1142
+ return ElementCreator.setup(finalElement, attributes, customWrapper);
1200
1143
  } catch (error) {
1201
1144
  DebugManager.error('ElementCreation', `Error creating ${$tagName}`, error);
1202
1145
  }
@@ -1304,51 +1247,274 @@ var NativeDocument = (function (exports) {
1304
1247
  if(!Validator.isArray(argSchema)) {
1305
1248
  throw new NativeDocumentError('withValidation : argSchema must be an array');
1306
1249
  }
1307
- return function(...args) {
1308
- validateArgs(args, argSchema, fn.name || fnName);
1309
- return fn.apply(this, args);
1310
- };
1250
+ return function(...args) {
1251
+ validateArgs(args, argSchema, fn.name || fnName);
1252
+ return fn.apply(this, args);
1253
+ };
1254
+ };
1255
+
1256
+ Function.prototype.args = function(...args) {
1257
+ return withValidation(this, args);
1258
+ };
1259
+
1260
+ Function.prototype.errorBoundary = function(callback) {
1261
+ return (...args) => {
1262
+ try {
1263
+ return this.apply(this, args);
1264
+ } catch(e) {
1265
+ return callback(e);
1266
+ }
1267
+ };
1268
+ };
1269
+
1270
+ String.prototype.use = function(args) {
1271
+ const value = this;
1272
+
1273
+ return Observable.computed(() => {
1274
+ return value.replace(/\$\{(.*?)}/g, (match, key) => {
1275
+ const data = args[key];
1276
+ if(Validator.isObservable(data)) {
1277
+ return data.val();
1278
+ }
1279
+ return data;
1280
+ });
1281
+ }, Object.values(args));
1282
+ };
1283
+
1284
+ String.prototype.resolveObservableTemplate = function() {
1285
+ if(!Validator.containsObservableReference(this)) {
1286
+ return this;
1287
+ }
1288
+ return this.split(/(\{\{#ObItem::\([0-9]+\)\}\})/g).filter(Boolean).map((value) => {
1289
+ if(!Validator.containsObservableReference(value)) {
1290
+ return value;
1291
+ }
1292
+ const [_, id] = value.match(/\{\{#ObItem::\(([0-9]+)\)\}\}/);
1293
+ return Observable.getById(id);
1294
+ });
1295
+ };
1296
+
1297
+ const methods = ['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice'];
1298
+
1299
+ /**
1300
+ *
1301
+ * @param {Array} target
1302
+ * @returns {ObservableItem}
1303
+ */
1304
+ Observable.array = function(target) {
1305
+ if(!Array.isArray(target)) {
1306
+ throw new NativeDocumentError('Observable.array : target must be an array');
1307
+ }
1308
+ const observer = Observable(target);
1309
+
1310
+ methods.forEach((method) => {
1311
+ observer[method] = function(...values) {
1312
+ const result = observer.val()[method](...values);
1313
+ observer.trigger({ action: method, args: values, result });
1314
+ return result;
1315
+ };
1316
+ });
1317
+
1318
+ observer.clear = function() {
1319
+ observer.$value.length = 0;
1320
+ observer.trigger({ action: 'clear' });
1321
+ return true;
1322
+ };
1323
+
1324
+ observer.remove = function(index) {
1325
+ const deleted = observer.$value.splice(index, 1);
1326
+ if(deleted.length === 0) {
1327
+ return [];
1328
+ }
1329
+ observer.trigger({ action: 'remove', args: [index], result: deleted[0] });
1330
+ return deleted;
1331
+ };
1332
+
1333
+ observer.swap = function(indexA, indexB) {
1334
+ const value = observer.$value;
1335
+ const length = value.length;
1336
+ if(length < indexA || length < indexB) {
1337
+ return false;
1338
+ }
1339
+ if(indexB < indexA) {
1340
+ const temp = indexA;
1341
+ indexA = indexB;
1342
+ indexB = temp;
1343
+ }
1344
+ const elementA = value[indexA];
1345
+ const elementB = value[indexB];
1346
+
1347
+ value[indexA] = elementB;
1348
+ value[indexB] = elementA;
1349
+ observer.trigger({ action: 'swap', args: [indexA, indexB], result: [elementA, elementB] });
1350
+ return true;
1351
+ };
1352
+
1353
+ observer.length = function() {
1354
+ return observer.$value.length;
1355
+ };
1356
+
1357
+ const overrideMethods = ['map', 'filter', 'reduce', 'some', 'every', 'find', 'findIndex', 'concat'];
1358
+ overrideMethods.forEach((method) => {
1359
+ observer[method] = function(...args) {
1360
+ return observer.val()[method](...args);
1361
+ };
1362
+ });
1363
+
1364
+ return observer;
1365
+ };
1366
+
1367
+ /**
1368
+ *
1369
+ * @param {Function} callback
1370
+ * @returns {Function}
1371
+ */
1372
+ Observable.batch = function(callback) {
1373
+ const $observer = Observable(0);
1374
+ const batch = function() {
1375
+ if(Validator.isAsyncFunction(callback)) {
1376
+ return (callback(...arguments)).then(() => {
1377
+ $observer.trigger();
1378
+ }).catch(error => { throw error; });
1379
+ }
1380
+ callback(...arguments);
1381
+ $observer.trigger();
1382
+ };
1383
+ batch.$observer = $observer;
1384
+ return batch;
1385
+ };
1386
+
1387
+ /**
1388
+ *
1389
+ * @param {Object} value
1390
+ * @returns {Proxy}
1391
+ */
1392
+ Observable.init = function(value) {
1393
+ const data = {};
1394
+ for(const key in value) {
1395
+ const itemValue = value[key];
1396
+ if(Validator.isJson(itemValue)) {
1397
+ data[key] = Observable.init(itemValue);
1398
+ continue;
1399
+ }
1400
+ else if(Validator.isArray(itemValue)) {
1401
+ data[key] = Observable.array(itemValue);
1402
+ continue;
1403
+ }
1404
+ data[key] = Observable(itemValue);
1405
+ }
1406
+
1407
+ const $val = function() {
1408
+ const result = {};
1409
+ for(const key in data) {
1410
+ const dataItem = data[key];
1411
+ if(Validator.isObservable(dataItem)) {
1412
+ result[key] = dataItem.val();
1413
+ } else if(Validator.isProxy(dataItem)) {
1414
+ result[key] = dataItem.$value;
1415
+ } else {
1416
+ result[key] = dataItem;
1417
+ }
1418
+ }
1419
+ return result;
1420
+ };
1421
+ const $clone = function() {
1422
+
1423
+ };
1424
+
1425
+ return new Proxy(data, {
1426
+ get(target, property) {
1427
+ if(property === '__isProxy__') {
1428
+ return true;
1429
+ }
1430
+ if(property === '$value') {
1431
+ return $val();
1432
+ }
1433
+ if(property === '$clone') {
1434
+ return $clone;
1435
+ }
1436
+ if(target[property] !== undefined) {
1437
+ return target[property];
1438
+ }
1439
+ return undefined;
1440
+ },
1441
+ set(target, prop, newValue) {
1442
+ if(target[prop] !== undefined) {
1443
+ target[prop].set(newValue);
1444
+ }
1445
+ }
1446
+ })
1447
+ };
1448
+
1449
+ /**
1450
+ * Get the value of an observable or an object of observables.
1451
+ * @param {ObservableItem|Object<ObservableItem>} data
1452
+ * @returns {{}|*|null}
1453
+ */
1454
+ Observable.value = function(data) {
1455
+ if(Validator.isObservable(data)) {
1456
+ return data.val();
1457
+ }
1458
+ if(Validator.isProxy(data)) {
1459
+ return data.$value;
1460
+ }
1461
+ if(Validator.isArray(data)) {
1462
+ const result = [];
1463
+ data.forEach(item => {
1464
+ result.push(Observable.value(item));
1465
+ });
1466
+ return result;
1467
+ }
1468
+ return data;
1311
1469
  };
1312
1470
 
1313
- Function.prototype.args = function(...args) {
1314
- return withValidation(this, args);
1315
- };
1316
1471
 
1317
- Function.prototype.errorBoundary = function(callback) {
1318
- return (...args) => {
1319
- try {
1320
- return this.apply(this, args);
1321
- } catch(e) {
1322
- return callback(e);
1472
+ Observable.update = function($target, data) {
1473
+ for(const key in data) {
1474
+ const targetItem = $target[key];
1475
+ const newValue = data[key];
1476
+
1477
+ if(Validator.isObservable(targetItem)) {
1478
+ if(Validator.isArray(newValue)) {
1479
+ Observable.update(targetItem, newValue);
1480
+ continue;
1481
+ }
1482
+ targetItem.set(newValue);
1483
+ continue;
1323
1484
  }
1324
- };
1485
+ if(Validator.isProxy(targetItem)) {
1486
+ Observable.update(targetItem, newValue);
1487
+ continue;
1488
+ }
1489
+ $target[key] = newValue;
1490
+ }
1325
1491
  };
1326
1492
 
1327
- String.prototype.use = function(args) {
1328
- const value = this;
1493
+ Observable.object = Observable.init;
1494
+ Observable.json = Observable.init;
1329
1495
 
1330
- return Observable.computed(() => {
1331
- return value.replace(/\$\{(.*?)}/g, (match, key) => {
1332
- const data = args[key];
1333
- if(Validator.isObservable(data)) {
1334
- return data.val();
1335
- }
1336
- return data;
1337
- });
1338
- }, Object.values(args));
1339
- };
1496
+ /**
1497
+ *
1498
+ * @param {Function} callback
1499
+ * @param {Array|Function} dependencies
1500
+ * @returns {ObservableItem}
1501
+ */
1502
+ Observable.computed = function(callback, dependencies = []) {
1503
+ const initialValue = callback();
1504
+ const observable = new ObservableItem(initialValue);
1505
+ const updatedValue = () => observable.set(callback());
1340
1506
 
1341
- String.prototype.resolveObservableTemplate = function() {
1342
- if(!Validator.containsObservableReference(this)) {
1343
- return this;
1344
- }
1345
- return this.split(/(\{\{#ObItem::\([0-9]+\)\}\})/g).filter(Boolean).map((value) => {
1346
- if(!Validator.containsObservableReference(value)) {
1347
- return value;
1507
+ if(Validator.isFunction(dependencies)) {
1508
+ if(!Validator.isObservable(dependencies.$observer)) {
1509
+ throw new NativeDocumentError('Observable.computed : dependencies must be valid batch function');
1348
1510
  }
1349
- const [_, id] = value.match(/\{\{#ObItem::\(([0-9]+)\)\}\}/);
1350
- return Observable.getById(id);
1351
- });
1511
+ dependencies.$observer.subscribe(updatedValue);
1512
+ return observable;
1513
+ }
1514
+
1515
+ dependencies.forEach(dependency => dependency.subscribe(updatedValue));
1516
+
1517
+ return observable;
1352
1518
  };
1353
1519
 
1354
1520
  const Store = (function() {
@@ -1424,45 +1590,6 @@ var NativeDocument = (function (exports) {
1424
1590
  };
1425
1591
  }());
1426
1592
 
1427
- /**
1428
- *
1429
- * @param {*} item
1430
- * @param {string|null} defaultKey
1431
- * @param {?Function} key
1432
- * @returns {*}
1433
- */
1434
- const getKey = (item, defaultKey, key) => {
1435
- if(Validator.isFunction(key)) return key(item, defaultKey);
1436
- if(Validator.isObservable(item)) {
1437
- const val = item.val();
1438
- return (val && key) ? val[key] : defaultKey;
1439
- }
1440
- return item[key] ?? defaultKey;
1441
- };
1442
-
1443
- /**
1444
- *
1445
- * @param {Map} cache
1446
- * @param {Set} keyIds
1447
- */
1448
- const cleanBlockByCache = (cache, keyIds) => {
1449
- const toRemove = [];
1450
- for(const [key, cacheItem] of cache.entries()) {
1451
- if(keyIds.has(key)) {
1452
- continue;
1453
- }
1454
- toRemove.push({ key, cacheItem });
1455
- }
1456
- if(toRemove.length === 0) {
1457
- return;
1458
- }
1459
- toRemove.forEach(({ key, cacheItem }) => {
1460
- cacheItem.child.remove();
1461
- cacheItem.indexObserver.cleanup();
1462
- cache.delete(key);
1463
- });
1464
- };
1465
-
1466
1593
  /**
1467
1594
  *
1468
1595
  * @param {Array|Object|ObservableItem} data
@@ -1473,120 +1600,408 @@ var NativeDocument = (function (exports) {
1473
1600
  function ForEach(data, callback, key) {
1474
1601
  const element = new Anchor('ForEach');
1475
1602
  const blockEnd = element.endElement();
1476
- const blockStart = element.startElement();
1603
+ element.startElement();
1477
1604
 
1478
1605
  let cache = new Map();
1606
+ let lastKeyOrder = null;
1479
1607
  const keyIds = new Set();
1480
1608
 
1609
+ const clear = () => {
1610
+ element.removeChildren();
1611
+ cleanCache();
1612
+ };
1613
+
1614
+ const cleanCache = (parent) => {
1615
+ for(const [keyId, cacheItem] of cache.entries()) {
1616
+ if(keyIds.has(keyId)) {
1617
+ continue;
1618
+ }
1619
+ const child = cacheItem.child?.deref();
1620
+ if(parent && child) {
1621
+ parent.removeChild(child);
1622
+ }
1623
+ cacheItem.indexObserver?.cleanup();
1624
+ cacheItem.child = null;
1625
+ cacheItem.indexObserver = null;
1626
+ cache.delete(cacheItem.keyId);
1627
+ lastKeyOrder && lastKeyOrder.delete(cacheItem.keyId);
1628
+ }
1629
+ };
1630
+
1481
1631
  const handleContentItem = (item, indexKey) => {
1482
1632
  const keyId = getKey(item, indexKey, key);
1483
1633
 
1484
1634
  if(cache.has(keyId)) {
1485
1635
  const cacheItem = cache.get(keyId);
1486
- cacheItem.indexObserver.set(indexKey);
1636
+ cacheItem.indexObserver?.set(indexKey);
1487
1637
  cacheItem.isNew = false;
1638
+ if(cacheItem.child?.deref()) {
1639
+ return keyId;
1640
+ }
1641
+ cache.delete(keyId);
1488
1642
  }
1489
- else {
1490
1643
 
1491
- try {
1492
- const indexObserver = Observable(indexKey);
1493
- let child = callback(item, indexObserver);
1494
- if(Validator.isStringOrObservable(child)) {
1495
- child = createTextNode(child);
1496
- }
1497
- cache.set(keyId, { isNew: true, child, indexObserver});
1498
- } catch (e) {
1499
- DebugManager.error('ForEach', `Error creating element for key ${keyId}` , e);
1500
- throw e;
1644
+ try {
1645
+ const indexObserver = callback.length >= 2 ? Observable(indexKey) : null;
1646
+ let child = callback(item, indexObserver);
1647
+ if(Validator.isStringOrObservable(child)) {
1648
+ child = createTextNode(child);
1501
1649
  }
1650
+ cache.set(keyId, { keyId, isNew: true, child: new WeakRef(child), indexObserver});
1651
+ } catch (e) {
1652
+ DebugManager.error('ForEach', `Error creating element for key ${keyId}` , e);
1653
+ throw e;
1502
1654
  }
1503
1655
  return keyId;
1504
1656
  };
1505
1657
 
1506
- const batchDOMUpdates = () => {
1658
+ const batchDOMUpdates = (parent) => {
1659
+ const fragment = document.createDocumentFragment();
1660
+ for(const itemKey of keyIds) {
1661
+ const cacheItem = cache.get(itemKey);
1662
+ if(!cacheItem) {
1663
+ continue;
1664
+ }
1665
+ const child = cacheItem.child?.deref();
1666
+ child && fragment.appendChild(child);
1667
+ }
1668
+ parent.insertBefore(fragment, blockEnd);
1669
+ };
1670
+
1671
+ const diffingDOMUpdates = (parent) => {
1672
+ let fragment = document.createDocumentFragment();
1673
+ const newKeys = Array.from(keyIds);
1674
+ Array.from(lastKeyOrder);
1675
+
1676
+ for(const index in newKeys) {
1677
+ const itemKey = newKeys[index];
1678
+ const cacheItem = cache.get(itemKey);
1679
+ if(!cacheItem) {
1680
+ continue;
1681
+ }
1682
+ const child = cacheItem.child.deref();
1683
+ if(!child) {
1684
+ continue;
1685
+ }
1686
+ fragment.appendChild(child);
1687
+ }
1688
+ element.replaceContent(fragment);
1689
+ };
1690
+
1691
+ const buildContent = () => {
1507
1692
  const parent = blockEnd.parentNode;
1508
1693
  if(!parent) {
1509
1694
  return;
1510
1695
  }
1511
1696
 
1512
- let previousElementSibling = blockStart;
1513
- const elementsToInsert = [];
1514
- const elementsToMove = [];
1515
- let fragment = null;
1516
-
1517
- let saveFragment = (beforeTarget) => {
1518
- if(fragment) {
1519
- elementsToInsert.push({ child: fragment, before: beforeTarget });
1520
- fragment = null;
1697
+ const items = (Validator.isObservable(data)) ? data.val() : data;
1698
+ keyIds.clear();
1699
+ if(Array.isArray(items)) {
1700
+ for(let i = 0, length = items.length; i < length; i++) {
1701
+ const keyId= handleContentItem(items[i], i);
1702
+ keyIds.add(keyId);
1521
1703
  }
1522
- };
1704
+ } else {
1705
+ for(const indexKey in items) {
1706
+ const keyId = handleContentItem(items[indexKey], indexKey);
1707
+ keyIds.add(keyId);
1708
+ }
1709
+ }
1523
1710
 
1524
- const keyIdsArray = Array.from(keyIds);
1525
- for(let i = 0; i < keyIdsArray.length; i++) {
1526
- const itemKey = keyIdsArray[i];
1527
- const cacheItem = cache.get(itemKey);
1711
+ if(keyIds.size === 0) {
1712
+ clear();
1713
+ lastKeyOrder?.clear();
1714
+ return;
1715
+ }
1716
+
1717
+ cleanCache(parent);
1718
+ if(!lastKeyOrder || lastKeyOrder.size === 0) {
1719
+ batchDOMUpdates(parent);
1720
+ } else {
1721
+ diffingDOMUpdates();
1722
+ }
1723
+ lastKeyOrder?.clear();
1724
+ lastKeyOrder = new Set([...keyIds]);
1725
+ };
1726
+
1727
+ buildContent();
1728
+ if(Validator.isObservable(data)) {
1729
+ data.subscribe(buildContent);
1730
+ }
1731
+ return element;
1732
+ }
1733
+
1734
+ function ForEachArray(data, callback, key, configs = {}) {
1735
+ const element = new Anchor('ForEach Array');
1736
+ const blockEnd = element.endElement();
1737
+ const blockStart = element.startElement();
1738
+
1739
+ let cache = new Map();
1740
+ let nodeCacheByElement = new WeakMap();
1741
+ let lastNumberOfItems = 0;
1742
+
1743
+ const keysCache = new WeakMap();
1744
+
1745
+ const clear = () => {
1746
+ element.removeChildren();
1747
+ cleanCache();
1748
+ lastNumberOfItems = 0;
1749
+ };
1750
+ const getItemKey = (item, indexKey) => {
1751
+ if(keysCache.has(item)) {
1752
+ return keysCache.get(item);
1753
+ }
1754
+ return getKey(item, indexKey, key);
1755
+ };
1756
+
1757
+ const updateIndexObservers = (items, startFrom = 0) => {
1758
+ if(callback.length < 2) {
1759
+ return;
1760
+ }
1761
+ let index = startFrom;
1762
+ for(let i = startFrom, length = items?.length; i < length; i++) {
1763
+ const cacheItem = cache.get(getItemKey(items[i], i));
1528
1764
  if(!cacheItem) {
1529
1765
  continue;
1530
1766
  }
1767
+ cacheItem.indexObserver?.deref()?.set(index);
1768
+ index++;
1769
+ }
1770
+ };
1531
1771
 
1532
- if(previousElementSibling && previousElementSibling.nextSibling === cacheItem.child) {
1533
- previousElementSibling = cacheItem.child;
1534
- saveFragment(cacheItem.child);
1535
- continue;
1772
+ const removeCacheItem = (cacheItem, removeChild = true) => {
1773
+ if(!cacheItem) {
1774
+ return;
1775
+ }
1776
+ const child = cacheItem.child?.deref();
1777
+ cacheItem.indexObserver?.deref()?.cleanup();
1778
+ cacheItem.child = null;
1779
+ cacheItem.indexObserver = null;
1780
+ nodeCacheByElement.delete(cacheItem.item);
1781
+ keysCache.delete(cacheItem.item);
1782
+ cacheItem.item = null;
1783
+ if(removeChild) {
1784
+ child?.remove();
1785
+ cache.delete(cacheItem.keyId);
1786
+ }
1787
+ };
1788
+
1789
+ const removeCacheItemByKey = (keyId, removeChild = true) => {
1790
+ removeCacheItem(cache.get(keyId), removeChild);
1791
+ };
1792
+
1793
+ const cleanCache = () => {
1794
+ for (const [keyId, cacheItem] of cache.entries()) {
1795
+ removeCacheItem(cacheItem, false);
1796
+ }
1797
+ cache.clear();
1798
+ };
1799
+
1800
+ const buildItem = (item, indexKey) => {
1801
+ const keyId = getItemKey(item, indexKey);
1802
+
1803
+ if(cache.has(keyId)) {
1804
+ const cacheItem = cache.get(keyId);
1805
+ cacheItem.indexObserver?.deref()?.set(indexKey);
1806
+ cacheItem.isNew = false;
1807
+ const child = cacheItem.child?.deref();
1808
+ if(child) {
1809
+ return child;
1536
1810
  }
1537
- if(cacheItem.isNew) {
1538
- fragment = fragment || document.createDocumentFragment();
1539
- fragment.append(cacheItem.child);
1540
- cacheItem.isNew = false;
1541
- continue;
1811
+ cache.delete(keyId);
1812
+ }
1813
+
1814
+ try {
1815
+ const indexObserver = callback.length >= 2 ? Observable(indexKey) : null;
1816
+ let child = callback(item, indexObserver);
1817
+ if(Validator.isStringOrObservable(child)) {
1818
+ child = createTextNode(child);
1542
1819
  }
1543
- saveFragment(cacheItem.child);
1544
- const nextChild = cache.get(keyIdsArray[i + 1])?.child;
1545
- if(nextChild) {
1546
- if(cacheItem.child.nextSibling !== nextChild) {
1547
- elementsToMove.push({ child: cacheItem.child, before: nextChild });
1548
- }
1820
+ cache.set(keyId, {
1821
+ keyId,
1822
+ isNew: true,
1823
+ item,
1824
+ child: new WeakRef(child),
1825
+ indexObserver: (indexObserver ? new WeakRef(indexObserver) : null)
1826
+ });
1827
+ keysCache.set(item, keyId);
1828
+ if(Validator.isObject(item)) {
1829
+ nodeCacheByElement.set(item, child);
1549
1830
  }
1831
+ return child;
1832
+ } catch (e) {
1833
+ DebugManager.error('ForEach', `Error creating element for key ${keyId}` , e);
1834
+ throw e;
1835
+ }
1836
+ };
1837
+ const getChildByKey = function(keyId, fragment) {
1838
+ const cacheItem = cache.get(keyId);
1839
+ if(!cacheItem) {
1840
+ return null;
1841
+ }
1842
+ const child = cacheItem.child?.deref();
1843
+ if(!child) {
1844
+ removeCacheItem(cacheItem, false);
1845
+ return null;
1846
+ }
1847
+ return child;
1848
+ };
1849
+
1850
+ const removeByKey = function(keyId, fragment) {
1851
+ const cacheItem = cache.get(keyId);
1852
+ if(!cacheItem) {
1853
+ return null;
1854
+ }
1855
+ const child = cacheItem.child?.deref();
1856
+ if(!child) {
1857
+ return null;
1858
+ }
1550
1859
 
1551
- previousElementSibling = cacheItem.child;
1860
+ if(fragment) {
1861
+ fragment.appendChild(child);
1862
+ return;
1552
1863
  }
1553
- saveFragment(blockEnd);
1864
+ child.remove();
1865
+ };
1554
1866
 
1555
- elementsToInsert.forEach(({ child, before }) => {
1556
- if(before) {
1557
- parent.insertBefore(child, before);
1867
+ const Actions = {
1868
+ toFragment(items, startIndexFrom = 0){
1869
+ const fragment = document.createDocumentFragment();
1870
+ for(let i = 0, length = items.length; i < length; i++) {
1871
+ fragment.append(buildItem(items[i], lastNumberOfItems));
1872
+ lastNumberOfItems++;
1873
+ }
1874
+ return fragment;
1875
+ },
1876
+ add(items, delay = 0) {
1877
+ setTimeout(() => {
1878
+ element.appendElement(Actions.toFragment(items));
1879
+ }, delay);
1880
+ },
1881
+ replace(items) {
1882
+ clear();
1883
+ Actions.add(items);
1884
+ },
1885
+ reOrder(items) {
1886
+ let child = null;
1887
+ const fragment = document.createDocumentFragment();
1888
+ for(const item of items) {
1889
+ child = nodeCacheByElement.get(item);
1890
+ if(child) {
1891
+ fragment.appendChild(child);
1892
+ }
1893
+ }
1894
+ child = null;
1895
+ element.appendElement(fragment, blockEnd);
1896
+ },
1897
+ removeOne(element, index) {
1898
+ let child = nodeCacheByElement.get(element);
1899
+ if(child) {
1900
+ child.remove();
1901
+ nodeCacheByElement.delete(element);
1902
+ removeCacheItemByKey(getItemKey(element, index));
1903
+ }
1904
+ child = null;
1905
+ },
1906
+ clear,
1907
+ push(items) {
1908
+ let delay = 0;
1909
+ if(configs.pushDelay) {
1910
+ delay = configs.pushDelay(items) ?? 0;
1558
1911
  } else {
1559
- element.appendChild(child);
1912
+ delay = (items.length >= 1000) ? 10 : 0;
1560
1913
  }
1561
- });
1914
+ Actions.add(items, delay);
1915
+ },
1916
+ unshift(values){
1917
+ element.insertBefore(Actions.toFragment(values), blockStart.nextSibling);
1918
+ },
1919
+ splice(args, deleted) {
1920
+ const [start, deleteCount, ...values] = args;
1921
+ let elementBeforeFirst = null;
1922
+ const garbageFragment = document.createDocumentFragment();
1923
+
1924
+ if(deleted.length > 0) {
1925
+ let firstKey = getItemKey(deleted[0], start);
1926
+ if(deleted.length === 1) {
1927
+ removeByKey(firstKey, garbageFragment);
1928
+ } else if(deleted.length > 1) {
1929
+ const firstChildRemoved = getChildByKey(firstKey);
1930
+ elementBeforeFirst = firstChildRemoved?.previousSibling;
1931
+
1932
+ for(let i = 0; i < deleted.length; i++) {
1933
+ const keyId = getItemKey(deleted[i], start + i);
1934
+ removeByKey(keyId, garbageFragment);
1935
+ }
1936
+ }
1937
+ } else {
1938
+ elementBeforeFirst = blockEnd;
1939
+ }
1940
+ garbageFragment.replaceChildren();
1562
1941
 
1563
- elementsToMove.forEach(({ child, before }) => {
1564
- parent.insertBefore(child, before);
1565
- });
1566
- saveFragment = null;
1942
+ if(values && values.length && elementBeforeFirst) {
1943
+ element.insertBefore(Actions.toFragment(values), elementBeforeFirst.nextSibling);
1944
+ }
1567
1945
 
1568
- };
1946
+ },
1947
+ reverse(_, reversed) {
1948
+ Actions.reOrder(reversed);
1949
+ },
1950
+ sort(_, sorted) {
1951
+ Actions.reOrder(sorted);
1952
+ },
1953
+ remove(_, deleted) {
1954
+ Actions.removeOne(deleted);
1955
+ },
1956
+ pop(_, deleted) {
1957
+ Actions.removeOne(deleted);
1958
+ },
1959
+ shift(_, deleted) {
1960
+ Actions.removeOne(deleted);
1961
+ },
1962
+ swap(args, elements) {
1963
+ const parent = blockEnd.parentNode;
1569
1964
 
1570
- const buildContent = () => {
1571
- const items = (Validator.isObservable(data)) ? data.val() : data;
1572
- keyIds.clear();
1573
- if(Array.isArray(items)) {
1574
- items.forEach((item, index) => keyIds.add(handleContentItem(item, index)));
1575
- } else {
1576
- for(const indexKey in items) {
1577
- keyIds.add(handleContentItem(items[indexKey], indexKey));
1965
+ let childA = nodeCacheByElement.get(elements[0]);
1966
+ let childB = nodeCacheByElement.get(elements[1]);
1967
+ if(!childA || !childB) {
1968
+ return;
1578
1969
  }
1970
+
1971
+ const childBNext = childB.nextSibling;
1972
+ parent.insertBefore(childB, childA);
1973
+ parent.insertBefore(childA, childBNext);
1974
+ childA = null;
1975
+ childB = null;
1579
1976
  }
1977
+ };
1580
1978
 
1581
- cleanBlockByCache(cache, keyIds);
1979
+ const buildContent = (items, _, operations) => {
1980
+ if(operations.action === 'clear' || !items.length) {
1981
+ if(lastNumberOfItems === 0) {
1982
+ return;
1983
+ }
1984
+ clear();
1985
+ }
1582
1986
 
1583
- batchDOMUpdates();
1987
+ if(!operations?.action) {
1988
+ if(lastNumberOfItems === 0) {
1989
+ Actions.add(items);
1990
+ return;
1991
+ }
1992
+ Actions.replace(items);
1993
+ }
1994
+ else if(Actions[operations.action]) {
1995
+ Actions[operations.action](operations.args, operations.result);
1996
+ }
1997
+ updateIndexObservers(items, 0);
1584
1998
  };
1585
1999
 
1586
- buildContent();
2000
+ buildContent(data.val(), null, {action: null});
1587
2001
  if(Validator.isObservable(data)) {
1588
2002
  data.subscribe(buildContent);
1589
2003
  }
2004
+
1590
2005
  return element;
1591
2006
  }
1592
2007
 
@@ -1921,6 +2336,10 @@ var NativeDocument = (function (exports) {
1921
2336
  const UnorderedList = HtmlElementWrapper('ul');
1922
2337
  const ListItem = HtmlElementWrapper('li');
1923
2338
 
2339
+ const Li = ListItem;
2340
+ const Ol = OrderedList;
2341
+ const Ul = UnorderedList;
2342
+
1924
2343
  const Audio = HtmlElementWrapper('audio');
1925
2344
  const Video = HtmlElementWrapper('video');
1926
2345
  const Source = HtmlElementWrapper('source');
@@ -1989,6 +2408,7 @@ var NativeDocument = (function (exports) {
1989
2408
  FileInput: FileInput,
1990
2409
  Footer: Footer,
1991
2410
  ForEach: ForEach,
2411
+ ForEachArray: ForEachArray,
1992
2412
  Form: Form,
1993
2413
  Fragment: Fragment,
1994
2414
  H1: H1,
@@ -2009,6 +2429,7 @@ var NativeDocument = (function (exports) {
2009
2429
  Label: Label,
2010
2430
  LazyImg: LazyImg,
2011
2431
  Legend: Legend,
2432
+ Li: Li,
2012
2433
  Link: Link$1,
2013
2434
  ListItem: ListItem,
2014
2435
  Main: Main,
@@ -2020,6 +2441,7 @@ var NativeDocument = (function (exports) {
2020
2441
  NativeDocumentFragment: Anchor,
2021
2442
  Nav: Nav,
2022
2443
  NumberInput: NumberInput,
2444
+ Ol: Ol,
2023
2445
  Option: Option,
2024
2446
  OrderedList: OrderedList,
2025
2447
  Output: Output,
@@ -2065,6 +2487,7 @@ var NativeDocument = (function (exports) {
2065
2487
  TimeInput: TimeInput,
2066
2488
  Tr: Tr,
2067
2489
  Track: Track,
2490
+ Ul: Ul,
2068
2491
  UnorderedList: UnorderedList,
2069
2492
  UrlInput: UrlInput,
2070
2493
  Var: Var,