native-document 1.0.17-1.9 → 1.0.17-2.2
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/CHANGELOG.md +26 -1
- package/package.json +1 -1
- package/src/core/data/ObservableItem.js +5 -5
- package/src/core/wrappers/ElementCreator.js +2 -2
- package/src/core/wrappers/HtmlElementWrapper.js +10 -11
- package/src/core/wrappers/NdPrototype.js +37 -4
- package/src/core/wrappers/template-cloner/NodeCloner.js +10 -6
- package/src/core/wrappers/template-cloner/TemplateCloner.js +7 -4
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## [1.0.172] - 2026-06-15
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `Text(string)` — shorthand for `string.toNdChildren()`, parses Observable placeholders in template strings
|
|
19
|
+
- `String.prototype.toNdChildren()` — splits a string containing `{{obs:N}}` placeholders into a mixed array of strings and resolved Observables
|
|
20
|
+
- `ObservableItem.prototype.toString()` — auto-registers the Observable in MemoryManager and returns `{{obs:id}}`, enabling reactive interpolation via standard template literals
|
|
21
|
+
- `NDElement.prototype.className(classes)` — adds static classes, bypasses AttributesWrapper
|
|
22
|
+
- `NDElement.prototype.class(className, value)` — adds a reactive class binding, bypasses AttributesWrapper
|
|
23
|
+
- `__$isNativeNode` flag on DOM prototypes (`Element`, `Text`, `Comment`, `Document`, `DocumentFragment`) — faster alternative to `instanceof Node`
|
|
24
|
+
- `__$isValidNdChild` flag on all valid NdChild prototypes — single property check replaces multiple type guards
|
|
25
|
+
- `defineToNdElement`, `defineValidNdChild`, `defineNativeNode` — non-enumerable prototype helpers in `nd-element-extensions`
|
|
26
|
+
- camelCase variants added to `BOOLEAN_ATTRIBUTES` — removes `toLowerCase()` call on each attribute in the hot path
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- `ElementCreator.processChildren` and `getChild` — refactored for better performance
|
|
31
|
+
- `nd-element-extensions` — all prototype extensions converted to non-enumerable `defineProperty`
|
|
32
|
+
- `AttributesWrapper` — `options` defaults to `null` instead of `{}`, fixes truthy check issue
|
|
33
|
+
- NDElement events — inline handler (`on*`) used when no existing handler and no options; falls back to `addEventListener` for subsequent handlers or when options are provided
|
|
34
|
+
- `_prevent`, `_stop`, `_preventStop` — updated with same inline handler strategy
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
14
38
|
## [1.0.170] - 2026-06-10
|
|
15
39
|
|
|
16
40
|
### Added
|
|
@@ -216,7 +240,8 @@ Entries use the following categories:
|
|
|
216
240
|
|
|
217
241
|
---
|
|
218
242
|
|
|
219
|
-
[Unreleased]: https://github.com/afrocodeur/native-document/compare/v1.0.
|
|
243
|
+
[Unreleased]: https://github.com/afrocodeur/native-document/compare/v1.0.172...HEAD
|
|
244
|
+
[1.0.172]: https://github.com/afrocodeur/native-document/compare/v1.0.170...v1.0.172
|
|
220
245
|
[1.0.170]: https://github.com/afrocodeur/native-document/compare/v1.0.169...v1.0.170
|
|
221
246
|
[1.0.169]: https://github.com/afrocodeur/native-document/compare/v1.0.167...v1.0.169
|
|
222
247
|
[1.0.167]: https://github.com/afrocodeur/native-document/compare/v1.0.166...v1.0.167
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "native-document",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.172.2",
|
|
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",
|
|
@@ -18,10 +18,10 @@ import {$getFromStorage, $saveToStorage} from '../utils/localstorage';
|
|
|
18
18
|
* @param {boolean} [configs.propagation] - Controls whether changes propagate to parent observables
|
|
19
19
|
* @example
|
|
20
20
|
* const count = new ObservableItem(0);
|
|
21
|
-
* const name
|
|
21
|
+
* const name = new ObservableItem('John', { reset: true });
|
|
22
22
|
*/
|
|
23
23
|
export default function ObservableItem(value, configs = null) {
|
|
24
|
-
value =
|
|
24
|
+
value = value?.__$Observable ? value.val() : value;
|
|
25
25
|
|
|
26
26
|
this.$previousValue = null;
|
|
27
27
|
this.$currentValue = value;
|
|
@@ -33,9 +33,9 @@ export default function ObservableItem(value, configs = null) {
|
|
|
33
33
|
this.$listeners = null;
|
|
34
34
|
this.$watchers = null;
|
|
35
35
|
|
|
36
|
-
this.$
|
|
36
|
+
this.$id = null;
|
|
37
37
|
|
|
38
|
-
if(configs) {
|
|
38
|
+
if(configs !== null) {
|
|
39
39
|
this.configs = configs;
|
|
40
40
|
if(configs.reset) {
|
|
41
41
|
this.$initialValue = Validator.isObject(value) ? deepClone(value) : value;
|
|
@@ -308,7 +308,7 @@ ObservableItem.prototype.cleanup = function() {
|
|
|
308
308
|
}
|
|
309
309
|
this.$cleanupListeners = null;
|
|
310
310
|
}
|
|
311
|
-
MemoryManager.unregister(this.$
|
|
311
|
+
MemoryManager.unregister(this.$id);
|
|
312
312
|
this.disconnectAll();
|
|
313
313
|
if(process.env.NODE_ENV === 'development') {
|
|
314
314
|
this.$isCleanedUp = true;
|
|
@@ -113,11 +113,11 @@ export const ElementCreator = {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
child = child.toNdElement();
|
|
116
|
-
if (child && child
|
|
116
|
+
if (child && (child instanceof Node)) {
|
|
117
117
|
return child;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
while (child != null && !child
|
|
120
|
+
while (child != null && !(child instanceof Node)) {
|
|
121
121
|
child = child.toNdElement?.();
|
|
122
122
|
}
|
|
123
123
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import Anchor from '../elements/anchor/anchor';
|
|
2
2
|
import {ElementCreator} from './ElementCreator';
|
|
3
|
-
import {normalizeComponentArgs} from '../utils/args-types';
|
|
4
3
|
import './NdPrototype';
|
|
5
4
|
|
|
6
5
|
import '../../core/utils/prototypes.js';
|
|
@@ -19,7 +18,7 @@ import '../wrappers/prototypes/attributes-extensions';
|
|
|
19
18
|
* @returns {Text} Text node, reactive if value is an ObservableItem
|
|
20
19
|
*/
|
|
21
20
|
export const createTextNode = (value) => {
|
|
22
|
-
if(value) {
|
|
21
|
+
if(value != null) {
|
|
23
22
|
return value.toNdElement();
|
|
24
23
|
}
|
|
25
24
|
return ElementCreator.createTextNode();
|
|
@@ -58,11 +57,12 @@ export const createHtmlElement = (element, _attributes, _children = null) => {
|
|
|
58
57
|
attributes = _children;
|
|
59
58
|
children = _attributes;
|
|
60
59
|
}
|
|
60
|
+
element = element.cloneNode();
|
|
61
61
|
|
|
62
|
-
if(attributes) {
|
|
62
|
+
if(attributes != null) {
|
|
63
63
|
ElementCreator.processAttributes(element, attributes);
|
|
64
64
|
}
|
|
65
|
-
if(children) {
|
|
65
|
+
if(children != null) {
|
|
66
66
|
ElementCreator.processChildren(children, element);
|
|
67
67
|
}
|
|
68
68
|
return element;
|
|
@@ -75,6 +75,7 @@ export const createHtmlElement = (element, _attributes, _children = null) => {
|
|
|
75
75
|
*
|
|
76
76
|
* @param {string} name - HTML tag name (e.g. 'div', 'button', 'input'). Pass empty string to create a Fragment.
|
|
77
77
|
* @param {((element: HTMLElement) => HTMLElement)|null} [customWrapper=null] - Optional function to augment the element before returning
|
|
78
|
+
* @param {boolean} [isVoid=false] - If true, the element is a void element (no children allowed, e.g. input, img)
|
|
78
79
|
* @returns {(attr?: Object, children?: ValidChild) => HTMLElement} Element factory function
|
|
79
80
|
* @example
|
|
80
81
|
* const Div = HtmlElementWrapper('div');
|
|
@@ -89,14 +90,12 @@ export const createHtmlElement = (element, _attributes, _children = null) => {
|
|
|
89
90
|
export default function HtmlElementWrapper(name, customWrapper = null, isVoid = false) {
|
|
90
91
|
const elementCreator = isVoid ? createVoidHtmlElement : createHtmlElement;
|
|
91
92
|
if(name) {
|
|
92
|
-
if(customWrapper) {
|
|
93
|
+
if(customWrapper == null) {
|
|
93
94
|
let node = null;
|
|
94
95
|
let createElement = (attr, children) => {
|
|
95
96
|
node = document.createElement(name);
|
|
96
|
-
createElement = (
|
|
97
|
-
|
|
98
|
-
};
|
|
99
|
-
return elementCreator(customWrapper(node.cloneNode()), attr, children);;
|
|
97
|
+
createElement = elementCreator.bind(null, node);
|
|
98
|
+
return elementCreator(node.cloneNode(), attr, children);
|
|
100
99
|
};
|
|
101
100
|
|
|
102
101
|
return (attr, children) => createElement(attr, children);
|
|
@@ -106,9 +105,9 @@ export default function HtmlElementWrapper(name, customWrapper = null, isVoid =
|
|
|
106
105
|
let createElement = (attr, children) => {
|
|
107
106
|
node = document.createElement(name);
|
|
108
107
|
createElement = (attr, children) => {
|
|
109
|
-
return elementCreator(node.cloneNode(), attr, children);
|
|
108
|
+
return elementCreator(customWrapper(node.cloneNode()), attr, children);
|
|
110
109
|
};
|
|
111
|
-
return elementCreator(node.cloneNode(), attr, children)
|
|
110
|
+
return elementCreator(customWrapper(node.cloneNode()), attr, children);;
|
|
112
111
|
};
|
|
113
112
|
|
|
114
113
|
return (attr, children) => createElement(attr, children);
|
|
@@ -26,7 +26,8 @@ Object.defineProperty(NDElement.prototype, 'nd', {
|
|
|
26
26
|
EVENTS.forEach(eventSourceName => {
|
|
27
27
|
const eventName = eventSourceName.toLowerCase();
|
|
28
28
|
const inlineHandler = 'on'+eventName;
|
|
29
|
-
|
|
29
|
+
const fnName = 'on'+eventSourceName;
|
|
30
|
+
NDElement.prototype[fnName] = function(callback = null, options = null) {
|
|
30
31
|
if(!this.$element[inlineHandler] && !options) {
|
|
31
32
|
this.$element[inlineHandler] = callback;
|
|
32
33
|
return this;
|
|
@@ -34,26 +35,58 @@ EVENTS.forEach(eventSourceName => {
|
|
|
34
35
|
this.$element.addEventListener(eventName, callback, options);
|
|
35
36
|
return this;
|
|
36
37
|
};
|
|
38
|
+
if(!HTMLElement.prototype[fnName]) {
|
|
39
|
+
HTMLElement.prototype[fnName] = function(callback, options) {
|
|
40
|
+
if(!this[inlineHandler] && !options) {
|
|
41
|
+
this[inlineHandler] = callback;
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
this.addEventListener(eventName, callback, options);
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
37
48
|
});
|
|
38
49
|
|
|
39
50
|
EVENTS_WITH_STOP.forEach(eventSourceName => {
|
|
40
51
|
const eventName = eventSourceName.toLowerCase();
|
|
41
|
-
|
|
52
|
+
const stopFnName = 'onStop'+eventSourceName;
|
|
53
|
+
const preventStopFnName = 'onPreventStop'+eventSourceName;
|
|
54
|
+
|
|
55
|
+
NDElement.prototype[stopFnName] = function(callback = null, options = null) {
|
|
42
56
|
_stop(this.$element, eventName, callback, options);
|
|
43
57
|
return this;
|
|
44
58
|
};
|
|
45
|
-
NDElement.prototype[
|
|
59
|
+
NDElement.prototype[preventStopFnName] = function(callback = null, options = null) {
|
|
46
60
|
_preventStop(this.$element, eventName, callback, options);
|
|
47
61
|
return this;
|
|
48
62
|
};
|
|
63
|
+
if(HTMLElement.prototype[stopFnName]) {
|
|
64
|
+
HTMLElement.prototype[stopFnName] = function(callback = null, options = null) {
|
|
65
|
+
_stop(this, eventName, callback, options);
|
|
66
|
+
return this;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if(!HTMLElement.prototype[preventStopFnName]) {
|
|
70
|
+
HTMLElement.prototype[preventStopFnName] = function(callback = null, options = null) {
|
|
71
|
+
_preventStop(this, eventName, callback, options);
|
|
72
|
+
return this;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
49
75
|
});
|
|
50
76
|
|
|
51
77
|
EVENTS_WITH_PREVENT.forEach(eventSourceName => {
|
|
52
78
|
const eventName = eventSourceName.toLowerCase();
|
|
53
|
-
|
|
79
|
+
const preventFnName = 'onPrevent'+eventSourceName;
|
|
80
|
+
NDElement.prototype[preventFnName] = function(callback = null, options = null) {
|
|
54
81
|
_prevent(this.$element, eventName, callback, options);
|
|
55
82
|
return this;
|
|
56
83
|
};
|
|
84
|
+
if(HTMLElement.prototype[preventFnName]) {
|
|
85
|
+
HTMLElement.prototype[preventFnName] = function(callback = null, options = null) {
|
|
86
|
+
_prevent(this, eventName, callback, options);
|
|
87
|
+
return this;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
57
90
|
});
|
|
58
91
|
|
|
59
92
|
|
|
@@ -17,8 +17,12 @@ export default function NodeCloner($element) {
|
|
|
17
17
|
this.$styles = null;
|
|
18
18
|
this.$attrs = null;
|
|
19
19
|
this.$ndMethods = null;
|
|
20
|
+
this.$content = null;
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
NodeCloner.prototype.shouldBeHydrate = function() {
|
|
24
|
+
return this.$attrs !== null || this.$classes !== null || this.$styles !== null || this.$ndMethods !== null || this.$content !== null;
|
|
25
|
+
};
|
|
22
26
|
|
|
23
27
|
/**
|
|
24
28
|
* Attaches a template binding to the element by hydrating it with the specified method.
|
|
@@ -171,16 +175,16 @@ NodeCloner.prototype.attach = function(methodName, callback) {
|
|
|
171
175
|
* Registers a reactive text content binding for the element.
|
|
172
176
|
*
|
|
173
177
|
* @internal
|
|
174
|
-
* @param {Function}
|
|
178
|
+
* @param {Function} valueOrProperty - Function receiving data and returning the text content
|
|
175
179
|
* @returns {NodeCloner} this
|
|
176
180
|
*/
|
|
177
|
-
NodeCloner.prototype.text = function(
|
|
178
|
-
this.$content =
|
|
179
|
-
if(typeof
|
|
180
|
-
this.cloneNode = (data) => createTextNode(
|
|
181
|
+
NodeCloner.prototype.text = function(valueOrProperty) {
|
|
182
|
+
this.$content = valueOrProperty;
|
|
183
|
+
if(typeof valueOrProperty === 'function') {
|
|
184
|
+
this.cloneNode = (data) => createTextNode(valueOrProperty.apply(null, data));
|
|
181
185
|
return this;
|
|
182
186
|
}
|
|
183
|
-
this.cloneNode = (data) => createTextNode(data[0][
|
|
187
|
+
this.cloneNode = (data) => createTextNode(data[0][valueOrProperty]);
|
|
184
188
|
return this;
|
|
185
189
|
};
|
|
186
190
|
|
|
@@ -24,11 +24,11 @@ export function TemplateCloner($fn) {
|
|
|
24
24
|
|
|
25
25
|
const assignClonerToNode = ($node) => {
|
|
26
26
|
const childNodes = $node.childNodes;
|
|
27
|
-
let containDynamicNode =
|
|
27
|
+
let containDynamicNode = $node.nodeCloner?.shouldBeHydrate();
|
|
28
28
|
const childNodesLength = childNodes.length;
|
|
29
29
|
for(let i = 0; i < childNodesLength; i++) {
|
|
30
30
|
const child = childNodes[i];
|
|
31
|
-
if(child.nodeCloner) {
|
|
31
|
+
if(child.nodeCloner && child.nodeCloner.shouldBeHydrate()) {
|
|
32
32
|
containDynamicNode = true;
|
|
33
33
|
}
|
|
34
34
|
const localContainDynamicNode = assignClonerToNode(child);
|
|
@@ -179,9 +179,12 @@ export function useCache(fn) {
|
|
|
179
179
|
return node;
|
|
180
180
|
};
|
|
181
181
|
|
|
182
|
+
if(fn.length === 0) {
|
|
183
|
+
return () => wrapper();
|
|
184
|
+
}
|
|
182
185
|
if(fn.length < 2) {
|
|
183
|
-
return (
|
|
184
|
-
return wrapper(
|
|
186
|
+
return (arg) => {
|
|
187
|
+
return wrapper([arg]);
|
|
185
188
|
};
|
|
186
189
|
}
|
|
187
190
|
return (_, __, ...args) => {
|