native-document 1.0.17-1.1 → 1.0.17-1.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "native-document",
3
- "version": "1.0.171.1",
3
+ "version": "1.0.171.10",
4
4
  "description": "A reactive JavaScript framework that preserves native DOM simplicity without sacrificing modern features",
5
5
  "author": "AfroCodeur <https://github.com/afrocodeur>",
6
6
  "license": "MIT",
@@ -23,7 +23,12 @@ const MemoryManager = (function() {
23
23
  $observables.delete(id);
24
24
  },
25
25
  getObservableById(id) {
26
- return $observables.get(id)?.deref();
26
+ const observable = $observables.get(id);
27
+ if(!observable) {
28
+ $observables.delete(id);
29
+ return null;
30
+ }
31
+ return observable.deref();
27
32
  },
28
33
  cleanup() {
29
34
  for (const [_, weakObservableRef] of $observables) {
@@ -124,6 +124,9 @@ ObservableItem.prototype.triggerListeners = function(operations) {
124
124
  * @param {{ action?: string, args?: any[], result?: any }} [operations] - Mutation metadata
125
125
  */
126
126
  ObservableItem.prototype.triggerWatchers = function(operations) {
127
+ if(!this.$watchers) {
128
+ return;
129
+ }
127
130
  const $watchers = this.$watchers;
128
131
  const $previousValue = this.$previousValue;
129
132
  const $currentValue = this.$currentValue;
@@ -160,6 +163,7 @@ ObservableItem.prototype.triggerWatchersAndFirstListener = function(operations)
160
163
  this.triggerFirstListener(operations);
161
164
  };
162
165
 
166
+
163
167
  /**
164
168
  * Selects and assigns the optimal trigger strategy based on the current
165
169
  * combination of listeners and watchers (internal).
@@ -167,31 +171,40 @@ ObservableItem.prototype.triggerWatchersAndFirstListener = function(operations)
167
171
  *
168
172
  * @internal
169
173
  */
170
- ObservableItem.prototype.assocTrigger = function() {
174
+ ObservableItem.prototype.$runAssocTrigger = function() {
171
175
  this.$firstListener = null;
172
176
  if(this.$watchers?.size && this.$listeners?.length) {
173
- this.$firstListener = this.$listeners[0];
174
- this.trigger = this.$firstListener.length === 0 ? this.$firstListener : this.triggerFirstListener;
175
- this.trigger = (this.$listeners.length === 1) ? this.triggerWatchersAndFirstListener : this.triggerAll;
177
+ this.$trigger = (this.$listeners.length === 1) ? this.triggerWatchersAndFirstListener : this.triggerAll;
176
178
  return;
177
179
  }
178
180
  if(this.$listeners?.length) {
179
181
  if(this.$listeners.length === 1) {
180
182
  this.$firstListener = this.$listeners[0];
181
- this.trigger = this.$firstListener.length === 0 ? this.$firstListener : this.triggerFirstListener;
183
+ this.$trigger = this.$firstListener.length === 0 ? this.$firstListener : this.triggerFirstListener;
182
184
  }
183
185
  else {
184
- this.trigger = this.triggerListeners;
186
+ this.$trigger = this.triggerListeners;
185
187
  }
186
188
  return;
187
189
  }
188
190
  if(this.$watchers?.size) {
189
- this.trigger = this.triggerWatchers;
191
+ this.$trigger = this.triggerWatchers;
190
192
  return;
191
193
  }
192
- this.trigger = noneTrigger;
194
+ this.$trigger = noneTrigger;
193
195
  };
194
- ObservableItem.prototype.trigger = noneTrigger;
196
+ ObservableItem.prototype.assocTrigger = function() {
197
+ this.$trigger = null;
198
+ };
199
+ Object.defineProperty(ObservableItem.prototype, 'trigger', {
200
+ value: function (operations) {
201
+ if(!this.$trigger) {
202
+ this.$runAssocTrigger();
203
+ }
204
+ return this.$trigger(operations);
205
+ },
206
+ configurable: false,
207
+ });
195
208
 
196
209
 
197
210
  const $setOperation = { action: 'set' };
@@ -543,7 +556,12 @@ ObservableItem.prototype.reset = function() {
543
556
  * @returns {string} String representation of the current value
544
557
  */
545
558
  ObservableItem.prototype.toString = function() {
546
- return String(this.$currentValue);
559
+ if(this.$id) {
560
+ return this.$id.toString();
561
+ }
562
+ const id = MemoryManager.register(this);
563
+ this.$id = id;
564
+ return '{{obs:'+id+'}}';
547
565
  };
548
566
 
549
567
  /**
@@ -2,6 +2,7 @@ import Validator from '../../utils/validator';
2
2
  import {ElementCreator} from '../../wrappers/ElementCreator';
3
3
  import AnchorWithSentinel from './anchor-with-sentinel';
4
4
  import oneChildAnchorOverwriting from './one-child-anchor-overwriting';
5
+ import DebugManager from '../../utils/debug-manager';
5
6
 
6
7
  /**
7
8
  * Creates an anchor fragment — a managed DocumentFragment delimited by comment sentinels.
@@ -34,7 +35,7 @@ export default function Anchor(name, isUniqueChild = false) {
34
35
  ? () => true: (parent) => (parent.firstChild === anchorStart && parent.lastChild === anchorEnd);
35
36
 
36
37
  const insertBefore = (parent, child, target) => {
37
- const childElement = Validator.isElement(child) ? child : ElementCreator.getChild(child);
38
+ const childElement = child.__$isNativeNode ? child : ElementCreator.getChild(child);
38
39
  insertBeforeRaw(parent, childElement, target);
39
40
  };
40
41
 
@@ -84,7 +85,7 @@ export default function Anchor(name, isUniqueChild = false) {
84
85
  anchorFragment.appendRaw = anchorFragment.appendChildRaw;
85
86
 
86
87
  anchorFragment.insertAtStart = function(child) {
87
- child = Validator.isElement(child) ? child : ElementCreator.getChild(child);
88
+ child = child.__$isNativeNode? child : ElementCreator.getChild(child);
88
89
  anchorFragment.insertAtStartRaw(child);
89
90
  };
90
91
 
@@ -141,7 +142,7 @@ export default function Anchor(name, isUniqueChild = false) {
141
142
  anchorFragment.delete = anchorFragment.removeWithAnchors;
142
143
 
143
144
  anchorFragment.replaceContent = function(child) {
144
- const childElement = Validator.isElement(child) ? child : ElementCreator.getChild(child);
145
+ const childElement = child.__$isNativeNode ? child : ElementCreator.getChild(child);
145
146
  anchorFragment.replaceContentRaw(childElement);
146
147
  };
147
148
 
@@ -10,7 +10,7 @@ export default function oneChildAnchorOverwriting(anchor, parent) {
10
10
  anchor.getParent = () => parent;
11
11
 
12
12
  anchor.appendChild = (child) => {
13
- child = Validator.isElement(child) ? child : ElementCreator.getChild(child);
13
+ child = child.__$isNativeNode ? child : ElementCreator.getChild(child);
14
14
  parent.appendChild(child);
15
15
  };
16
16
 
@@ -19,7 +19,7 @@ export default function oneChildAnchorOverwriting(anchor, parent) {
19
19
  anchor.appendRaw = anchor.appendChildRaw;
20
20
 
21
21
  anchor.insertAtStart = (child) => {
22
- child = Validator.isElement(child) ? child : ElementCreator.getChild(child);
22
+ child = child.__$isNativeNode ? child : ElementCreator.getChild(child);
23
23
  parent.firstChild ? parent.insertBefore(child, parent.firstChild) : parent.appendChild(child);
24
24
  };
25
25
  anchor.insertAtStartRaw = (child) => {
@@ -33,7 +33,7 @@ export default function oneChildAnchorOverwriting(anchor, parent) {
33
33
  };
34
34
 
35
35
  anchor.replaceContent = function(content) {
36
- const child = Validator.isElement(content) ? content : ElementCreator.getChild(content);
36
+ const child = (content && content.__$isNativeNode) ? content : ElementCreator.getChild(content);
37
37
  parent.replaceChildren(child);
38
38
  };
39
39
 
@@ -43,7 +43,7 @@ export default function oneChildAnchorOverwriting(anchor, parent) {
43
43
  anchor.setContent = anchor.replaceContent;
44
44
 
45
45
  anchor.insertBefore = (child, anchor) => {
46
- child = Validator.isElement(child) ? child : ElementCreator.getChild(child);
46
+ child = child.__$isNativeNode ? child : ElementCreator.getChild(child);
47
47
  parent.insertBefore(child, anchor);
48
48
  };
49
49
  anchor.insertBeforeRaw = (child, anchor) => {
@@ -24,6 +24,25 @@ export const Label = HtmlElementWrapper('label');
24
24
  */
25
25
  export const P = HtmlElementWrapper('p');
26
26
 
27
+
28
+ /**
29
+ * Parses a string containing Observable placeholders and returns an array
30
+ * of strings and Observable instances ready to be used as NativeDocument children.
31
+ *
32
+ * Observable placeholders are generated automatically when an ObservableItem
33
+ * is interpolated in a template string via its toString() method.
34
+ *
35
+ * @param {string} string - Template string with optional Observable placeholders
36
+ * @returns {Array<string|ObservableItem>} Mixed array of static strings and resolved Observables
37
+ * @example
38
+ * const name = Observable('John');
39
+ * const count = Observable(5);
40
+ *
41
+ * Text(`Hello ${name}, count: ${count}`)
42
+ * // → ['Hello ', ObservableItem(John), ', count: ', ObservableItem(5)]
43
+ */
44
+ export const Text = (string) => string.toNdChildren();
45
+
27
46
  /**
28
47
  * Alias for {@link P}.
29
48
  * @type {function(GlobalAttributes=, NdChild|NdChild[]=): HTMLParagraphElement}
@@ -0,0 +1,59 @@
1
+
2
+
3
+ export default function ForEachArrayCache(isIndexesRequired) {
4
+ this.$nodes = new Map();
5
+ this.$indexes = isIndexesRequired ? new Map() : null;
6
+
7
+ this.has = this.$nodes.has.bind(this.$nodes);
8
+
9
+ this.keys = function() {
10
+ return Array.from(this.$nodes.keys());
11
+ };
12
+
13
+ if(isIndexesRequired) {
14
+ this.delete = function(item) {
15
+ this.$nodes.get(item)?.nd.destroy();
16
+ this.$nodes.delete(item);
17
+ this.$indexes.delete(item);
18
+ };
19
+
20
+ this.set = function(item, child, index) {
21
+ this.$nodes.set(item, child);
22
+ this.$indexes.set(item, index);
23
+ };
24
+
25
+ this.clear = function() {
26
+ for(const [_, node] in this.$nodes.entries()) {
27
+ node.nd.destroy();
28
+ }
29
+ this.$nodes.clear();
30
+ this.$indexes.clear();
31
+ };
32
+
33
+ this.get = function(item) {
34
+ return {
35
+ child: this.$nodes.get(item),
36
+ indexObserver: this.$indexes.get(item),
37
+ };
38
+ };
39
+ } else {
40
+
41
+ this.delete = (item) => {
42
+ this.$nodes.get(item)?.nd.destroy();
43
+ this.$nodes.delete(item);
44
+ };
45
+ this.set = this.$nodes.set.bind(this.$nodes);
46
+ this.clear = function() {
47
+ for(const [_, node] in this.$nodes.entries()) {
48
+ node.nd.destroy();
49
+ }
50
+ this.$nodes.clear();
51
+ };
52
+
53
+ this.get = function(item) {
54
+ return { child: this.$nodes.get(item), indexObserver: null };
55
+ };
56
+ }
57
+
58
+ }
59
+
@@ -1,6 +1,7 @@
1
1
  import Anchor from '../anchor/anchor';
2
2
  import { ElementCreator } from '../../wrappers/ElementCreator';
3
3
  import NativeDocumentError from '../../errors/NativeDocumentError';
4
+ import ForEachArrayCache from './for-each-array-cache';
4
5
 
5
6
 
6
7
  const CREATE_AND_CACHE_ACTIONS = new Set(['clear', 'push', 'unshift', 'replace']);
@@ -30,9 +31,9 @@ export function ForEachArray(data, callback, configs = {}) {
30
31
  const element = Anchor('ForEach Array', configs.isParentUniqueChild);
31
32
  const blockEnd = element.endElement();
32
33
 
33
- const cache = new Map();
34
34
  let lastNumberOfItems = 0;
35
35
  const isIndexRequired = callback.length >= 2;
36
+ const cache = new ForEachArrayCache(isIndexRequired);
36
37
 
37
38
  const clear = (items) => {
38
39
  element.removeChildren();
@@ -64,7 +65,7 @@ export function ForEachArray(data, callback, configs = {}) {
64
65
  throw new NativeDocumentError('ForEachArray child can\'t be null or undefined!');
65
66
  }
66
67
  }
67
- cache.set(item, { child, indexObserver: null });
68
+ cache.set(item, child);
68
69
  return child;
69
70
  };
70
71
 
@@ -76,7 +77,7 @@ export function ForEachArray(data, callback, configs = {}) {
76
77
  throw new NativeDocumentError('ForEachArray child can\'t be null or undefined!');
77
78
  }
78
79
  }
79
- cache.set(item, { child, indexObserver });
80
+ cache.set(item, child, indexObserver);
80
81
  return child;
81
82
  };
82
83
  if(!data.__$Observable) {
@@ -87,7 +88,7 @@ export function ForEachArray(data, callback, configs = {}) {
87
88
  throw new NativeDocumentError('ForEachArray child can\'t be null or undefined!');
88
89
  }
89
90
  }
90
- cache.set(item, { child, indexObserver: null });
91
+ cache.set(item, child);
91
92
  return child;
92
93
  };
93
94
  }
@@ -150,14 +151,14 @@ export function ForEachArray(data, callback, configs = {}) {
150
151
  if(Array.isArray(data)) {
151
152
  set = () => {
152
153
  clear(data);
153
- element.appendChildRaw(Actions.toFragment(data));
154
+ Actions.appendToElement(data);
154
155
  };
155
156
  }
156
157
  else {
157
158
  set = () => {
158
159
  const items = data.val();
159
160
  clear(items);
160
- element.appendChildRaw(Actions.toFragment(items));
161
+ Actions.appendToElement(items);
161
162
  };
162
163
  }
163
164
 
@@ -166,45 +167,36 @@ export function ForEachArray(data, callback, configs = {}) {
166
167
  const fragment = document.createDocumentFragment();
167
168
  for(let i = 0, length = items.length; i < length; i++) {
168
169
  fragment.appendChild(buildItem(items[i], lastNumberOfItems));
169
- lastNumberOfItems++;
170
170
  }
171
171
  return fragment;
172
172
  },
173
+ appendToElement: (items) => {
174
+ for(let i = 0, length = items.length; i < length; i++) {
175
+ element.appendChild(buildItem(items[i], i));
176
+ }
177
+ },
173
178
  add: (items) => {
174
- element.appendChildRaw(Actions.toFragment(items));
179
+ Actions.appendToElement(items);
175
180
  },
176
181
  replace: (items) => {
177
182
  clear(items);
178
- element.appendChildRaw(Actions.toFragment(items));
183
+ Actions.appendToElement(items);
179
184
  },
180
185
  set,
181
186
  reOrder: (items) => {
182
187
  let child = null;
183
- const fragment = document.createDocumentFragment();
184
188
  for(const item of items) {
185
189
  child = getItemChild(item);
186
190
  if(child) {
187
- fragment.appendChild(child);
191
+ element.appendChild(child);
188
192
  }
189
193
  }
190
194
  child = null;
191
- element.appendChildRaw(fragment);
192
195
  },
193
196
  removeOne: (element, index) => {
194
197
  removeCacheItem(element, true);
195
198
  },
196
199
  clear,
197
- populate: ([target, iteration, callback]) => {
198
- const fragment = document.createDocumentFragment();
199
- for (let i = 0; i < iteration; i++) {
200
- const data = callback(i);
201
- target.push(data);
202
- fragment.append(buildItem(data, i));
203
- lastNumberOfItems++;
204
- }
205
- element.appendChildRaw(fragment);
206
- fragment.replaceChildren();
207
- },
208
200
  unshift: (values) => {
209
201
  element.insertAtStartRaw(Actions.toFragment(values));
210
202
  },
@@ -1,35 +1,8 @@
1
1
  import Validator from '../utils/validator';
2
- import NativeDocumentError from '../errors/NativeDocumentError';
3
2
  import {BOOL_ATTRIBUTES_NAME, BOOLEAN_ATTRIBUTES} from './constants.js';
4
3
  import {Observable} from '../data/Observable';
5
4
 
6
5
 
7
- const addNdDestroy = (element, observable, callback) => {
8
- if(element.ndDestroy) {
9
- element.ndAddDestroy(observable, callback);
10
- return;
11
- }
12
- let destroyCallbacks = [];
13
-
14
- element.ndAddDestroy = (observable, callback) => {
15
- destroyCallbacks.push(observable);
16
- destroyCallbacks.push(callback);
17
- };
18
- element.ndDestroy = () => {
19
-
20
- for(let i = 0, len = destroyCallbacks.length; i < len; i+= 2) {
21
- destroyCallbacks[i].unsubscribe(destroyCallbacks[i+1]);
22
- }
23
-
24
- destroyCallbacks.length = 0;
25
- destroyCallbacks = null;
26
- element.ndAddDestroy = null;
27
- element.ndDestroy = null;
28
- element = null;
29
- };
30
-
31
- element.ndAddDestroy(observable, callback);
32
- };
33
6
 
34
7
  /**
35
8
  * Applies a reactive class map to an HTMLElement.
@@ -42,33 +15,28 @@ const addNdDestroy = (element, observable, callback) => {
42
15
  export const bindClassAttribute = (element, data) => {
43
16
  for(const className in data) {
44
17
  const value = data[className];
45
-
46
18
  if (value.__$isObservableChecker) {
47
19
  let lastClass = value.val();
48
20
  if (typeof lastClass === 'string') {
49
21
  element.classes.toggle(lastClass, true);
50
- const callback = (currentValue) => {
22
+ value.subscribe((currentValue) => {
51
23
  element.classes.remove(lastClass);
52
24
  element.classes.toggle(currentValue, true);
53
25
  lastClass = currentValue;
54
- };
55
- value.subscribe(callback);
56
- addNdDestroy(element, value, callback);
57
- continue;
26
+ });
27
+ return;
58
28
  }
59
29
  }
60
30
 
61
31
  if (value.__$Observable) {
62
32
  element.classes.toggle(className, value.val());
63
- const callback = (shouldAdd) => element.classes.toggle(className, shouldAdd);
64
- value.subscribe(callback);
65
- addNdDestroy(element, value, callback);
66
- continue;
33
+ value.subscribe((shouldAdd) => element.classes.toggle(className, shouldAdd));
34
+ return;
67
35
  }
68
36
 
69
37
  if (value.$hydrate) {
70
38
  value.$hydrate(element, className);
71
- continue;
39
+ return;
72
40
  }
73
41
 
74
42
  element.classes.toggle(className, value);
@@ -92,26 +60,22 @@ export const bindStyleAttribute = (element, data) => {
92
60
  if(value.__$Observable) {
93
61
  if(isCustomProperty) {
94
62
  element.style.setProperty(styleName, value.val());
95
- const callback = (newValue) => {
63
+ value.subscribe((newValue) => {
96
64
  if(newValue === false) {
97
65
  element.style.removeProperty(styleName);
98
66
  return;
99
67
  }
100
68
  element.style.setProperty(styleName, newValue);
101
- };
102
- value.subscribe(callback);
103
- addNdDestroy(element, value, callback);
69
+ });
104
70
  } else {
105
71
  element.style[styleName] = value.val();
106
- const callback = (newValue) => {
72
+ value.subscribe((newValue) => {
107
73
  if(newValue === false) {
108
74
  element.style.removeProperty(styleName);
109
75
  return;
110
76
  }
111
77
  element.style[styleName] = newValue;
112
- };
113
- value.subscribe(callback);
114
- addNdDestroy(element, value, callback);
78
+ });
115
79
  }
116
80
  continue;
117
81
  }
@@ -135,7 +99,7 @@ export const bindBooleanAttribute = (element, attributeName, value) => {
135
99
  const isObservable = value.__$Observable;
136
100
  const defaultValue = isObservable? value.val() : value;
137
101
 
138
- const attributeRealName = BOOL_ATTRIBUTES_NAME[attributeName];
102
+ const attributeRealName = BOOL_ATTRIBUTES_NAME[attributeName] || attributeName;
139
103
 
140
104
  if(typeof defaultValue === 'boolean') {
141
105
  element[attributeRealName] = defaultValue;
@@ -151,14 +115,10 @@ export const bindBooleanAttribute = (element, attributeName, value) => {
151
115
  else {
152
116
  element.addEventListener('input', () => value.set(element.value));
153
117
  }
154
- const callback = (newValue) => element[attributeRealName] = newValue;
155
- value.subscribe(callback);
156
- addNdDestroy(element, value, callback);
118
+ value.subscribe((newValue) => element[attributeRealName] = newValue);
157
119
  return;
158
120
  }
159
- const callback = (newValue) => element[attributeRealName] = (newValue === element.value);
160
- value.subscribe(callback);
161
- addNdDestroy(element, value, callback);
121
+ value.subscribe((newValue) => element[attributeRealName] = (newValue === element.value));
162
122
  }
163
123
  };
164
124
 
@@ -173,15 +133,11 @@ export const bindAttributeWithObservable = (element, attributeName, value) => {
173
133
  if(attributeName === 'value') {
174
134
  element.value = value.val();
175
135
  element.addEventListener('input', () => value.set(element.value));
176
- const callback = (newValue) => element.value = newValue;
177
- value.subscribe(callback);
178
- addNdDestroy(element, value, callback);
136
+ value.subscribe((newValue) => element.value = newValue);
179
137
  return;
180
138
  }
181
139
  element.setAttribute(attributeName, value.val());
182
- const callback = (newValue) => element.setAttribute(attributeName, newValue);
183
- value.subscribe(callback);
184
- addNdDestroy(element, value, callback);
140
+ value.subscribe((newValue) => element.setAttribute(attributeName, newValue));
185
141
  };
186
142
 
187
143
  /**
@@ -189,29 +145,29 @@ export const bindAttributeWithObservable = (element, attributeName, value) => {
189
145
  * @param {HTMLElement} element
190
146
  * @param {Object} attributes
191
147
  */
192
- const AttributesWrapper = (element, attributes) => {
148
+ const AttributesWrapper = (element, attributes = {}) => {
193
149
 
194
150
  if(process.env.NODE_ENV === 'development') {
195
151
  Validator.validateAttributes(attributes);
196
152
  }
197
153
 
198
- for(const originalAttributeName in attributes) {
199
- const value = attributes[originalAttributeName];
154
+ for(const attributeName in attributes) {
155
+ const value = attributes[attributeName];
200
156
  if(value == null) {
201
157
  continue;
202
158
  }
203
159
  const type = typeof value;
204
160
  if(type === 'string' || type === 'number') {
205
- element.setAttribute(originalAttributeName, value);
161
+ element.setAttribute(attributeName, value);
206
162
  continue;
207
163
  }
208
- const attributeName = originalAttributeName.toLowerCase();
164
+ // const attributeName = originalAttributeName.toLowerCase();
209
165
  if(value.__$Observable) {
210
166
  if(BOOLEAN_ATTRIBUTES.has(attributeName)) {
211
167
  bindBooleanAttribute(element, attributeName, value);
212
168
  continue;
213
169
  }
214
- bindAttributeWithObservable(element, originalAttributeName, value);
170
+ bindAttributeWithObservable(element, attributeName, value);
215
171
  continue;
216
172
  }
217
173
  if(type === 'object') {
@@ -1,7 +1,5 @@
1
- import Validator from '../utils/validator';
2
1
  import AttributesWrapper, { bindClassAttribute, bindStyleAttribute } from './AttributesWrapper';
3
2
  import PluginsManager from '../utils/plugins-manager';
4
- import {call} from '@babel/traverse/lib/path/context';
5
3
 
6
4
  let $textNodeCache = null;
7
5
 
@@ -9,7 +7,7 @@ export const ElementCreator = {
9
7
  createTextNode() {
10
8
  if(!$textNodeCache) {
11
9
  $textNodeCache = document.createTextNode('');
12
- ElementCreator.createTextNode = () => $textNodeCache.cloneNode();
10
+ ElementCreator.createTextNode = $textNodeCache.cloneNode.bind($textNodeCache);
13
11
  }
14
12
  return $textNodeCache.cloneNode();
15
13
  },
@@ -21,9 +19,7 @@ export const ElementCreator = {
21
19
  */
22
20
  createObservableNode: (parent, observable) => {
23
21
  const text = ElementCreator.createObservableNodeWithoutParent(observable);
24
- if(parent) {
25
- parent.appendChild(text);
26
- }
22
+ parent && parent.appendChild(text);
27
23
  return text;
28
24
  },
29
25
  /**
@@ -33,19 +29,8 @@ export const ElementCreator = {
33
29
  */
34
30
  createObservableNodeWithoutParent: (observable) => {
35
31
  const text = ElementCreator.createTextNode();
36
- const callback = value => text.nodeValue = value;
37
- observable.subscribe(callback);
32
+ observable.subscribe(value => text.nodeValue = value);
38
33
  text.nodeValue = observable.val();
39
- let unsubscribe = () => {
40
- observable.unsubscribe(callback);
41
- observable = null;
42
- };
43
- text.ndDestroy = () => {
44
- text.nodeValue = null;
45
- unsubscribe();
46
- unsubscribe = null;
47
- text.ndDestroy = null;
48
- };
49
34
  return text;
50
35
  },
51
36
  /**
@@ -72,6 +57,11 @@ export const ElementCreator = {
72
57
  parent && parent.appendChild(text);
73
58
  return text;
74
59
  },
60
+ createStaticTextNodeWithoutParent: (value) => {
61
+ const text = ElementCreator.createTextNode();
62
+ text.nodeValue = value;
63
+ return text;
64
+ },
75
65
  /**
76
66
  *
77
67
  * @param {string} name
@@ -95,27 +85,37 @@ export const ElementCreator = {
95
85
  * @param {HTMLElement|DocumentFragment} parent
96
86
  */
97
87
  processChildren: (children, parent) => {
98
- if(children === null) return;
99
88
  if(process.env.NODE_ENV === 'development') {
100
89
  PluginsManager.emit('BeforeProcessChildren', parent);
101
90
  }
102
- const child = ElementCreator.getChild(children);
103
- if(child) {
104
- parent.appendChild(child);
91
+ if(Array.isArray(children)) {
92
+ for(let i = 0, length = children.length; i < length; i++) {
93
+ const child = ElementCreator.getChild(children[i]);
94
+ if(child === null) {
95
+ continue;
96
+ }
97
+ parent.appendChild(child);
98
+ }
99
+ return;
105
100
  }
101
+ const child = ElementCreator.getChild(children);
102
+ parent.appendChild(child);
106
103
  if(process.env.NODE_ENV === 'development') {
107
104
  PluginsManager.emit('AfterProcessChildren', parent);
108
105
  }
109
106
  },
110
107
  async safeRemove(element) {
111
108
  await element.remove();
112
-
113
109
  },
114
110
  getChild: (child) => {
115
- if (child == null) return null;
111
+ if (child == null) {
112
+ return null;
113
+ }
116
114
 
117
115
  child = child.toNdElement();
118
- if (child instanceof Node) return child;
116
+ if (child && (child instanceof Node)) {
117
+ return child;
118
+ }
119
119
 
120
120
  while (child != null && !(child instanceof Node)) {
121
121
  child = child.toNdElement?.();
@@ -36,10 +36,11 @@ export const createTextNode = (value) => {
36
36
  * @returns {HTMLElement} The configured element
37
37
  */
38
38
  export const createVoidHtmlElement = (element, attributes) => {
39
- ElementCreator.processAttributes(element, attributes);
39
+ attributes && ElementCreator.processAttributes(element, attributes);
40
40
  return element;
41
41
  };
42
42
 
43
+
43
44
  const OBJECT_PROTOTYPE = Object.prototype;
44
45
  /**
45
46
  * Applies attributes and children to an existing HTMLElement.
@@ -53,13 +54,17 @@ const OBJECT_PROTOTYPE = Object.prototype;
53
54
  */
54
55
  export const createHtmlElement = (element, _attributes, _children = null) => {
55
56
  let attributes = _attributes, children = _children;
56
- if((!_attributes || !_children) && (typeof _attributes !== 'object' || Array.isArray(_attributes) || _attributes === null || Object.getPrototypeOf(_attributes) !== OBJECT_PROTOTYPE || _attributes.$hydrate)) { // IF it's not a JSON
57
+ if(_attributes?.__$isValidNdChild) { // If attributes is a ValidChild
57
58
  attributes = _children;
58
59
  children = _attributes;
59
60
  }
60
61
 
61
- ElementCreator.processAttributes(element, attributes);
62
- ElementCreator.processChildren(children, element);
62
+ if(attributes) {
63
+ ElementCreator.processAttributes(element, attributes);
64
+ }
65
+ if(children) {
66
+ ElementCreator.processChildren(children, element);
67
+ }
63
68
  return element;
64
69
  };
65
70
 
@@ -195,27 +195,24 @@ NDElement.prototype.destroy = function() {
195
195
  observer.disconnect();
196
196
  }
197
197
 
198
- const children = this.$element?.querySelectorAll('[data--nd-before-unmount]') || [];
198
+ const treewalk = document.createTreeWalker(this.$element, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT);
199
+ const nodes = [];
200
+ while(treewalk.nextNode()) {
201
+ nodes.push(treewalk.currentNode);
202
+ }
199
203
 
200
- for(let i = 0, length = children.length; i < length; i++) {
201
- const child = children[i];
204
+ for(let i = 0, length = nodes.length; i < length; i++) {
205
+ const child = nodes[i];
202
206
  child.remove();
203
- child.__$controller?.abort();
204
- child.__$controller = null;
207
+ const childObserver = $lifeCycleObservers.get(child);
208
+ if(childObserver) {
209
+ childObserver.disconnect();
210
+ }
205
211
  $lifeCycleObservers.delete(child);
206
212
  }
207
213
 
208
- const treeWalker = document.createTreeWalker(this.$element, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT);
209
- console.log(treeWalker);
210
- while(treeWalker.nextNode()) {
211
- const node = treeWalker.currentNode;
212
- node.ndDestroy?.();
213
- }
214
-
215
- this.$element.ndDestroy?.();
216
- this.$element.__$controller?.abort();
217
- this.$element.__$controller = null;
218
214
  $lifeCycleObservers.delete(this.$element);
215
+ this.$element.remove();
219
216
  this.$element = null;
220
217
  };
221
218
 
@@ -437,7 +434,6 @@ NDElement.prototype.style = function(style) {
437
434
  return this;
438
435
  };
439
436
 
440
-
441
437
  /**
442
438
  * Extends the NDElement prototype with new methods available to all NDElement instances.
443
439
  * Use this to add global methods to all NDElements.
@@ -637,7 +633,7 @@ NDElement.prototype.contentEditable = function($obs, {format = 'html'} = {}) {
637
633
 
638
634
  this.$element.addEventListener('input', () => {
639
635
  $obs?.set(getValue());
640
- }, { signal: this.$getSignal() });
636
+ });
641
637
 
642
638
  return this;
643
639
  };
@@ -20,65 +20,42 @@ Object.defineProperty(NDElement.prototype, 'nd', {
20
20
  });
21
21
 
22
22
 
23
-
24
23
  // ----------------------------------------------------------------
25
24
  // Events helpers
26
25
  // ----------------------------------------------------------------
27
26
  EVENTS.forEach(eventSourceName => {
28
27
  const eventName = eventSourceName.toLowerCase();
29
- NDElement.prototype['on'+eventSourceName] = function(callback = null, options = {}) {
30
- this.$element.addEventListener(eventName, callback, {
31
- signal: this.$getSignal(),
32
- ...options,
33
- });
28
+ const inlineHandler = 'on'+eventName;
29
+ NDElement.prototype['on'+eventSourceName] = function(callback = null, options = null) {
30
+ if(!this.$element[inlineHandler] && !options) {
31
+ this.$element[inlineHandler] = callback;
32
+ return this;
33
+ }
34
+ this.$element.addEventListener(eventName, callback, options);
34
35
  return this;
35
36
  };
36
37
  });
37
38
 
38
39
  EVENTS_WITH_STOP.forEach(eventSourceName => {
39
40
  const eventName = eventSourceName.toLowerCase();
40
- NDElement.prototype['onStop'+eventSourceName] = function(callback = null, options = {}) {
41
- _stop(this.$element, eventName, callback, {
42
- signal: this.$getSignal(),
43
- ...options,
44
- });
41
+ NDElement.prototype['onStop'+eventSourceName] = function(callback = null, options = null) {
42
+ _stop(this.$element, eventName, callback, options);
45
43
  return this;
46
44
  };
47
- NDElement.prototype['onPreventStop'+eventSourceName] = function(callback = null, options = {}) {
48
- _preventStop(this.$element, eventName, callback, {
49
- signal: this.$getSignal(),
50
- ...options,
51
- });
45
+ NDElement.prototype['onPreventStop'+eventSourceName] = function(callback = null, options = null) {
46
+ _preventStop(this.$element, eventName, callback, options);
52
47
  return this;
53
48
  };
54
49
  });
55
50
 
56
51
  EVENTS_WITH_PREVENT.forEach(eventSourceName => {
57
52
  const eventName = eventSourceName.toLowerCase();
58
- NDElement.prototype['onPrevent'+eventSourceName] = function(callback = null, options = {}) {
59
- _prevent(this.$element, eventName, callback, {
60
- signal: this.$getSignal(),
61
- ...options,
62
- });
53
+ NDElement.prototype['onPrevent'+eventSourceName] = function(callback = null, options = null) {
54
+ _prevent(this.$element, eventName, callback, options);
63
55
  return this;
64
56
  };
65
57
  });
66
58
 
67
- /**
68
- * Retrieves or creates an AbortController signal tied to this element's lifecycle.
69
- * The signal is automatically aborted when the element is removed via .nd.remove().
70
- * Used internally by all event listeners (onClick, onInput, etc.) to auto-cleanup on unmount.
71
- *
72
- * @internal
73
- * @returns {AbortSignal} The signal for this element's AbortController
74
- */
75
- NDElement.prototype.$getSignal = function() {
76
- if(!this.$element.__$controller) {
77
- this.$element.__$controller = new AbortController();
78
- }
79
- return this.$element.__$controller.signal;
80
- };
81
-
82
59
 
83
60
  /**
84
61
  * Adds a native event listener to the underlying HTMLElement.
@@ -91,7 +68,6 @@ NDElement.prototype.$getSignal = function() {
91
68
  */
92
69
  NDElement.prototype.on = function(name, callback, options) {
93
70
  this.$element.addEventListener(name.toLowerCase(), callback, {
94
- signal: this.$getSignal(),
95
71
  ...options,
96
72
  });
97
73
  return this;
@@ -119,7 +95,6 @@ NDElement.prototype.off = function(name, callback) {
119
95
  */
120
96
  NDElement.prototype.once = function(name, callback) {
121
97
  this.$element.addEventListener(name.toLowerCase(), callback, {
122
- signal: this.$getSignal(),
123
98
  once: true,
124
99
  });
125
100
  return this;
@@ -151,8 +126,13 @@ const _prevent = function(element, eventName, callback, options) {
151
126
  event.preventDefault();
152
127
  callback && callback.call(element, event);
153
128
  };
129
+
130
+ const inlineHandler = 'on' + eventName;
131
+ if(!element[inlineHandler] && !options) {
132
+ element[inlineHandler] = handler;
133
+ return;
134
+ }
154
135
  element.addEventListener(eventName, handler, options);
155
- return this;
156
136
  };
157
137
 
158
138
  const _stop = function(element, eventName, callback, options) {
@@ -160,6 +140,13 @@ const _stop = function(element, eventName, callback, options) {
160
140
  event.stopPropagation();
161
141
  callback && callback.call(element, event);
162
142
  };
143
+
144
+ const inlineHandler = 'on' + eventName;
145
+ if(!element[inlineHandler] && !options) {
146
+ element[inlineHandler] = handler;
147
+ return;
148
+ }
149
+
163
150
  element.addEventListener(eventName, handler, options);
164
151
  return this;
165
152
  };
@@ -170,6 +157,11 @@ const _preventStop = function(element, eventName, callback, options) {
170
157
  event.preventDefault();
171
158
  callback && callback.call(element, event);
172
159
  };
160
+ const inlineHandler = 'on' + eventName;
161
+ if(!element[inlineHandler] && !options) {
162
+ element[inlineHandler] = handler;
163
+ return;
164
+ }
173
165
  element.addEventListener(eventName, handler, options);
174
166
  return this;
175
167
  };
@@ -3,19 +3,19 @@ export const BOOLEAN_ATTRIBUTES = new Set([
3
3
  'checked',
4
4
  'selected',
5
5
  'disabled',
6
- 'readonly',
6
+ 'readonly', 'readOnly',
7
7
  'required',
8
- 'autofocus',
8
+ 'autofocus', 'autoFocus',
9
9
  'multiple',
10
- 'autocomplete',
10
+ 'autocomplete', 'autoComplete',
11
11
  'hidden',
12
- 'contenteditable',
13
- 'spellcheck',
12
+ 'contenteditable', 'contentEditable',
13
+ 'spellcheck', 'spellCheck',
14
14
  'translate',
15
15
  'draggable',
16
16
  'async',
17
17
  'defer',
18
- 'autoplay',
18
+ 'autoplay', 'autoPlay',
19
19
  'controls',
20
20
  'loop',
21
21
  'muted',
@@ -23,13 +23,13 @@ export const BOOLEAN_ATTRIBUTES = new Set([
23
23
  'reversed',
24
24
  'open',
25
25
  'default',
26
- 'formnovalidate',
27
- 'novalidate',
26
+ 'formnovalidate', 'formNoValidate',
27
+ 'novalidate', 'noValidate',
28
28
  'scoped',
29
- 'itemscope',
30
- 'allowfullscreen',
31
- 'allowpaymentrequest',
32
- 'playsinline',
29
+ 'itemscope', 'itemScope',
30
+ 'allowfullscreen', 'allowFullscreen',
31
+ 'allowpaymentrequest', 'allowPaymentRequest',
32
+ 'playsinline', 'playsInline',
33
33
  ]);
34
34
 
35
35
  export const BOOL_ATTRIBUTES_NAME = {
@@ -4,6 +4,43 @@ import TemplateBinding from '../TemplateBinding';
4
4
  import { ElementCreator } from '../ElementCreator';
5
5
  import PluginsManager from '../../utils/plugins-manager';
6
6
  import ObservableChecker from '../../data/ObservableChecker';
7
+ import MemoryManager from '../../data/MemoryManager';
8
+
9
+
10
+
11
+ const OBS_PATTERN = /({{obs:\d+}})/;
12
+ const OBS_ID_PATTERN = /\d+/;
13
+
14
+
15
+ /**
16
+ * Parses a string containing Observable placeholders and returns an array
17
+ * of strings and Observable instances.
18
+ *
19
+ * Placeholders are generated automatically when an ObservableItem is interpolated
20
+ * in a template string via its toString() method.
21
+ *
22
+ * @returns {Array<string|ObservableItem>} Mixed array of static strings and resolved Observables
23
+ * @example
24
+ * const name = Observable('John');
25
+ * const count = Observable(5);
26
+ *
27
+ * `Hello ${name}, count: ${count}`.toNdChildren()
28
+ * // → ['Hello ', ObservableItem(John), ', count: ', ObservableItem(5)]
29
+ */
30
+ Object.defineProperty(String.prototype, 'toNdChildren', {
31
+ value: function() {
32
+ return this.split(OBS_PATTERN).filter(Boolean).map((item) => {
33
+ if(OBS_PATTERN.test(item)) {
34
+ const id = item.match(OBS_ID_PATTERN)[0];
35
+ return MemoryManager.getObservableById(Number(id));
36
+ }
37
+ return item;
38
+ });
39
+ },
40
+ writable: true,
41
+ configurable: true,
42
+ enumerable: false,
43
+ });
7
44
 
8
45
  /**
9
46
  * Registers a non-enumerable toNdElement method on a prototype.
@@ -20,9 +57,62 @@ const defineToNdElement = (target, fn) => {
20
57
  });
21
58
  };
22
59
 
60
+ /**
61
+ * Marks a prototype as a valid NativeDocument child node.
62
+ *
63
+ * @param {object} target - The prototype to mark
64
+ */
65
+ const defineValidNdChild = (target) => {
66
+ Object.defineProperty(target, '__$isValidNdChild', {
67
+ value: true,
68
+ writable: true,
69
+ configurable: true,
70
+ enumerable: false,
71
+ });
72
+ };
73
+
74
+ /**
75
+ * Marks a prototype as a native DOM node.
76
+ * Faster alternative to instanceof Node for hot paths.
77
+ *
78
+ * @param {object} target - The prototype to mark
79
+ */
80
+ const defineNativeNode = (target) => {
81
+ Object.defineProperty(target, '__$isNativeNode', {
82
+ value: true,
83
+ writable: true,
84
+ configurable: true,
85
+ enumerable: false,
86
+ });
87
+ };
88
+
23
89
  NDElement.$getChild = ElementCreator.getChild;
24
90
 
25
- // -- String -------------------------------------------------------------------
91
+ // -- __$isNativeNode ----------------------------------------------------------
92
+
93
+ defineNativeNode(Element.prototype);
94
+ defineNativeNode(Text.prototype);
95
+ defineNativeNode(Comment.prototype);
96
+ defineNativeNode(Document.prototype);
97
+ defineNativeNode(DocumentFragment.prototype);
98
+
99
+ // -- __$isValidNdChild --------------------------------------------------------
100
+
101
+ defineValidNdChild(String.prototype);
102
+ defineValidNdChild(Number.prototype);
103
+ defineValidNdChild(Boolean.prototype);
104
+ defineValidNdChild(Array.prototype);
105
+ defineValidNdChild(Element.prototype);
106
+ defineValidNdChild(Text.prototype);
107
+ defineValidNdChild(Comment.prototype);
108
+ defineValidNdChild(DocumentFragment.prototype);
109
+ defineValidNdChild(NDElement.prototype);
110
+ defineValidNdChild(ObservableItem.prototype);
111
+ defineValidNdChild(ObservableChecker.prototype);
112
+ defineValidNdChild(TemplateBinding.prototype);
113
+ defineValidNdChild(Function.prototype);
114
+
115
+ // -- toNdElement --------------------------------------------------------------
26
116
 
27
117
  /**
28
118
  * Converts a string to a static text node.
@@ -30,22 +120,18 @@ NDElement.$getChild = ElementCreator.getChild;
30
120
  * @returns {Text}
31
121
  */
32
122
  defineToNdElement(String.prototype, function () {
33
- return ElementCreator.createStaticTextNode(null, this);
123
+ return ElementCreator.createStaticTextNodeWithoutParent(this);
34
124
  });
35
125
 
36
- // -- Number -------------------------------------------------------------------
37
-
38
126
  /**
39
127
  * Converts a number to a static text node.
40
128
  *
41
129
  * @returns {Text}
42
130
  */
43
131
  defineToNdElement(Number.prototype, function () {
44
- return ElementCreator.createStaticTextNode(null, this.toString());
132
+ return ElementCreator.createStaticTextNodeWithoutParent(this.toString());
45
133
  });
46
134
 
47
- // -- DOM nodes (identity) -----------------------------------------------------
48
-
49
135
  /**
50
136
  * Returns the DOM node itself (identity).
51
137
  *
@@ -57,8 +143,6 @@ defineToNdElement(Comment.prototype, function () { return this; });
57
143
  defineToNdElement(Document.prototype, function () { return this; });
58
144
  defineToNdElement(DocumentFragment.prototype, function () { return this; });
59
145
 
60
- // -- ObservableItem -----------------------------------------------------------
61
-
62
146
  /**
63
147
  * Converts an ObservableItem to a reactive text node that updates when the value changes.
64
148
  *
@@ -70,8 +154,6 @@ defineToNdElement(ObservableItem.prototype, function () {
70
154
 
71
155
  ObservableChecker.prototype.toNdElement = ObservableItem.prototype.toNdElement;
72
156
 
73
- // -- NDElement ----------------------------------------------------------------
74
-
75
157
  /**
76
158
  * Returns the underlying HTMLElement of this NDElement.
77
159
  *
@@ -81,8 +163,6 @@ defineToNdElement(NDElement.prototype, function () {
81
163
  return this.$element;
82
164
  });
83
165
 
84
- // -- Array --------------------------------------------------------------------
85
-
86
166
  /**
87
167
  * Converts the array to a DocumentFragment containing all child elements.
88
168
  *
@@ -98,8 +178,6 @@ defineToNdElement(Array.prototype, function () {
98
178
  return fragment;
99
179
  });
100
180
 
101
- // -- Function -----------------------------------------------------------------
102
-
103
181
  /**
104
182
  * Calls the function and converts its return value to a DOM node.
105
183
  *
@@ -113,8 +191,6 @@ defineToNdElement(Function.prototype, function () {
113
191
  return ElementCreator.getChild(child());
114
192
  });
115
193
 
116
- // -- TemplateBinding ----------------------------------------------------------
117
-
118
194
  /**
119
195
  * Converts the TemplateBinding to a hydratable DOM node.
120
196
  *
@@ -36,8 +36,8 @@ const buildHeader = ($desc, instance, headerId, panelId) => {
36
36
  ];
37
37
 
38
38
  const header = Div({
39
- class: 'accordion-header',
40
- id: headerId,
39
+ class: 'accordion-header',
40
+ id: headerId,
41
41
  // [a11y] aria-expanded reactive, aria-controls points to panel
42
42
  'aria-expanded': $desc.expanded.transform((v) => String(v)),
43
43
  'aria-controls': panelId,
@@ -64,12 +64,12 @@ const buildIndicator = ($desc) => {
64
64
  }, '▾');
65
65
  };
66
66
 
67
- const buildContent = ($desc, instance) => {
67
+ const buildContent = ($desc, instance, headerId, panelId) => {
68
68
  // [a11y] role=region + aria-labelledby on panel
69
69
  const panelProps = {
70
- class: 'accordion-content',
71
- id: panelId,
72
- role: 'region',
70
+ class: 'accordion-content',
71
+ id: panelId,
72
+ role: 'region',
73
73
  'aria-labelledby': headerId,
74
74
  };
75
75
 
@@ -2,6 +2,9 @@
2
2
  import { ObservableItem } from './observable';
3
3
  import { BindingHydrator } from './template-cloner';
4
4
  import { NDElement } from './nd-element';
5
+ import {AnchorAttributes,
6
+ AudioAttributes, CanvasAttributes, DetailsAttributes, DialogAttributes, GlobalAttributes, OlAttributes,
7
+ SourceAttributes, SvgAttributes, TrackAttributes, VideoAttributes, ThAttributes, ModAttributes, TdAttributes, TimeAttributes } from './globals';
5
8
 
6
9
  // - Re-export all attribute types and utilities from globals
7
10
  export type {
@@ -75,6 +78,8 @@ export type NdHTMLElement<T extends HTMLElement = HTMLElement> = T & { nd: NDEle
75
78
  // Text & structural elements
76
79
  // ---------------------------------------------------------
77
80
 
81
+
82
+ export declare function Text(string: string): string | Array<string | ObservableItem>;
78
83
  export declare const Div: ElementFunction<GlobalAttributes, HTMLDivElement>;
79
84
  export declare const Span: ElementFunction<GlobalAttributes, HTMLSpanElement>;
80
85
  export declare const P: ElementFunction<GlobalAttributes, HTMLParagraphElement>;