native-document 1.0.16-8.1 → 1.0.16-8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "native-document",
3
- "version": "1.0.168.01",
3
+ "version": "1.0.168.02",
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",
@@ -14,28 +14,7 @@ import {Observable} from '../data/Observable';
14
14
  export const bindClassAttribute = (element, data) => {
15
15
  for(const className in data) {
16
16
  const value = data[className];
17
- if(value.__$Observable) {
18
- if(value.__$isObservableChecker) {
19
- let lastClass = value.val();
20
- if(typeof lastClass === 'string') {
21
- element.classes.toggle(lastClass, true);
22
- value.subscribe((currentValue) => {
23
- element.classes.remove(lastClass);
24
- element.classes.toggle(currentValue, true);
25
- lastClass = currentValue;
26
- });
27
- continue;
28
- }
29
- }
30
- element.classes.toggle(className, value.val());
31
- value.subscribe((shouldAdd) => element.classes.toggle(className, shouldAdd));
32
- continue;
33
- }
34
- if(value.$hydrate) {
35
- value.$hydrate(element, className);
36
- continue;
37
- }
38
- element.classes.toggle(className, value);
17
+ data[className].handleClassValue(element, className);
39
18
  }
40
19
  };
41
20
 
@@ -49,39 +28,11 @@ export const bindClassAttribute = (element, data) => {
49
28
  * @param {Record<string, string|ObservableItem<string>>} data - Style map
50
29
  */
51
30
  export const bindStyleAttribute = (element, data) => {
52
- for(const styleName in data) {
53
- const value = data[styleName];
54
- const isCustomProperty = styleName.startsWith('--');
55
-
56
- if(value.__$Observable) {
57
- if(isCustomProperty) {
58
- element.style.setProperty(styleName, value.val());
59
- value.subscribe((newValue) => {
60
- if(newValue === false) {
61
- element.style.removeProperty(styleName);
62
- return;
63
- }
64
- element.style.setProperty(styleName, newValue);
65
- });
66
- } else {
67
- element.style[styleName] = value.val();
68
- value.subscribe((newValue) => {
69
- if(newValue === false) {
70
- element.style.removeProperty(styleName);
71
- return;
72
- }
73
- element.style[styleName] = newValue;
74
- });
75
- }
76
- continue;
77
- }
78
-
79
- if(isCustomProperty) {
80
- element.style.setProperty(styleName, value);
81
- continue;
82
- }
83
-
84
- element.style[styleName] = value;
31
+ const keys = Object.keys(data);
32
+ const length = keys.length;
33
+ for(let i = 0; i < length; i++) {
34
+ const styleName = keys[i];
35
+ data[styleName].handleStyleProperty(element, styleName);
85
36
  }
86
37
  };
87
38
 
@@ -92,30 +43,8 @@ export const bindStyleAttribute = (element, data) => {
92
43
  * @param {boolean|number|Observable} value
93
44
  */
94
45
  export const bindBooleanAttribute = (element, attributeName, value) => {
95
- const isObservable = value.__$isObservable;
96
- const defaultValue = isObservable? value.val() : value;
97
-
98
- const attributeRealName = BOOL_ATTRIBUTES_NAME[attributeName];
99
-
100
- if(Validator.isBoolean(defaultValue)) {
101
- element[attributeRealName] = defaultValue;
102
- }
103
- else {
104
- element[attributeRealName] = defaultValue === element.value;
105
- }
106
- if(isObservable) {
107
- if(attributeName === 'checked') {
108
- if(typeof defaultValue === 'boolean') {
109
- element.addEventListener('input', () => value.set(element[attributeRealName]));
110
- }
111
- else {
112
- element.addEventListener('input', () => value.set(element.value));
113
- }
114
- value.subscribe((newValue) => element[attributeRealName] = newValue);
115
- return;
116
- }
117
- value.subscribe((newValue) => element[attributeRealName] = (newValue === element.value));
118
- }
46
+ const attributeRealName = BOOL_ATTRIBUTES_NAME[attributeName] || attributeName;
47
+ value.handleNdBooleanAttribute(element, attributeRealName);
119
48
  };
120
49
 
121
50
 
@@ -126,15 +55,14 @@ export const bindBooleanAttribute = (element, attributeName, value) => {
126
55
  * @param {Observable} value
127
56
  */
128
57
  export const bindAttributeWithObservable = (element, attributeName, value) => {
129
- const applyValue = attributeName === 'value' ? (newValue) => element.value = newValue : (newValue) => element.setAttribute(attributeName, newValue);
130
- value.subscribe(applyValue);
131
-
132
- if(attributeName === 'value') {
133
- element.value = value.val();
134
- element.addEventListener('input', () => value.set(element.value));
58
+ if(attributeName !== 'value') {
59
+ value.subscribe((newValue) => element.setAttribute(attributeName, newValue));
60
+ element.setAttribute(attributeName, value.val());
135
61
  return;
136
62
  }
137
- element.setAttribute(attributeName, value.val());
63
+ value.subscribe((newValue) => element.value = newValue);
64
+ element.value = value.val();
65
+ element.addEventListener('input', () => value.set(element.value));
138
66
  };
139
67
 
140
68
  /**
@@ -148,32 +76,21 @@ const AttributesWrapper = (element, attributes) => {
148
76
  Validator.validateAttributes(attributes);
149
77
  }
150
78
 
151
- for(const originalAttributeName in attributes) {
152
- const attributeName = originalAttributeName.toLowerCase();
79
+ if(!attributes) {
80
+ return;
81
+ }
82
+
83
+ const keys = Object.keys(attributes);
84
+ const length = keys.length;
85
+
86
+ for(let i = 0; i < length; i++) {
87
+ const originalAttributeName = keys[i];
153
88
  const value = attributes[originalAttributeName];
154
89
  if(value == null) {
155
90
  continue;
156
91
  }
157
- if(value.handleNdAttribute) {
158
- value.handleNdAttribute(element, attributeName, value);
159
- continue;
160
- }
161
- if(typeof value === 'object') {
162
- if(attributeName === 'class') {
163
- bindClassAttribute(element, value);
164
- continue;
165
- }
166
- if(attributeName === 'style') {
167
- bindStyleAttribute(element, value);
168
- continue;
169
- }
170
- }
171
- if(BOOLEAN_ATTRIBUTES.has(attributeName)) {
172
- bindBooleanAttribute(element, attributeName, value);
173
- continue;
174
- }
92
+ value.handleNdAttribute(element, originalAttributeName.toLowerCase());
175
93
 
176
- element.setAttribute(attributeName, value);
177
94
  }
178
95
  return element;
179
96
  };
@@ -89,15 +89,25 @@ export const ElementCreator = {
89
89
 
90
90
  },
91
91
  getChild: (child) => {
92
- return child.toNdElement();
92
+ if(child == null) {
93
+ return null;
94
+ }
95
+ do {
96
+ child = child.toNdElement();
97
+ if(Validator.isElement(child)) {
98
+ return child;
99
+ }
100
+ } while (child.toNdElement);
93
101
  },
94
102
  /**
95
103
  *
96
104
  * @param {HTMLElement} element
97
105
  * @param {Object} attributes
98
106
  */
99
- processAttributes: (element, attributes = {}) => {
100
- AttributesWrapper(element, attributes);
107
+ processAttributes: (element, attributes) => {
108
+ if (attributes) {
109
+ AttributesWrapper(element, attributes);
110
+ }
101
111
  },
102
112
  /**
103
113
  *
@@ -1,24 +1,179 @@
1
1
  import {
2
- bindAttributeWithObservable,
3
- bindBooleanAttribute,
2
+ bindAttributeWithObservable, bindClassAttribute, bindStyleAttribute,
4
3
  } from '../AttributesWrapper';
5
4
  import ObservableItem from '../../data/ObservableItem';
6
5
  import TemplateBinding from '../TemplateBinding';
7
- import {BOOLEAN_ATTRIBUTES} from '../constants';
6
+ import { BOOLEAN_ATTRIBUTES } from '../constants';
7
+ import { Observable } from '../../data/Observable';
8
8
  import ObservableChecker from '../../data/ObservableChecker';
9
9
 
10
+ /**
11
+ * Extends a prototype with a non-enumerable, configurable, writable property.
12
+ * Prevents prototype pollution issues (e.g. for...in loops) while keeping
13
+ * the property overridable and deletable.
14
+ *
15
+ * @param {Function} Constructor - The constructor whose prototype to extend
16
+ * @param {string} name - Property name
17
+ * @param {Function} fn - Implementation
18
+ */
19
+ const extendsPrototype = (Constructor, name, fn) => {
20
+ Object.defineProperty(Constructor.prototype, name, {
21
+ value: fn,
22
+ enumerable: false,
23
+ configurable: true,
24
+ writable: true,
25
+ });
26
+ }
10
27
 
11
- ObservableItem.prototype.handleNdAttribute = function(element, attributeName) {
12
- if(BOOLEAN_ATTRIBUTES.has(attributeName)) {
13
- bindBooleanAttribute(element, attributeName, this);
28
+ // -- handleNdAttribute --------------------------------------------------------
29
+
30
+ extendsPrototype(Object, 'handleNdAttribute', function(element, attributeName) {
31
+ if (attributeName === 'class') {
32
+ bindClassAttribute(element, this);
33
+ return;
34
+ }
35
+ if (attributeName === 'style') {
36
+ bindStyleAttribute(element, this);
14
37
  return;
15
38
  }
39
+ element.setAttribute(attributeName, JSON.stringify(this));
40
+ });
16
41
 
42
+ extendsPrototype(ObservableItem, 'handleNdAttribute', function(element, attributeName) {
43
+ if (BOOLEAN_ATTRIBUTES.has(attributeName)) {
44
+ this.handleNdBooleanAttribute(element, attributeName);
45
+ return;
46
+ }
17
47
  bindAttributeWithObservable(element, attributeName, this);
18
- };
48
+ });
19
49
 
20
- ObservableChecker.prototype.handleNdAttribute = ObservableItem.prototype.handleNdAttribute;
21
-
22
- TemplateBinding.prototype.handleNdAttribute = function(element, attributeName) {
50
+ extendsPrototype(TemplateBinding, 'handleNdAttribute', function(element, attributeName) {
23
51
  this.$hydrate(element, attributeName);
24
- };
52
+ });
53
+
54
+ extendsPrototype(Number, 'handleNdAttribute', function(element, attributeName) {
55
+ element.setAttribute(attributeName, this.toString());
56
+ });
57
+
58
+ extendsPrototype(String, 'handleNdAttribute', function(element, attributeName) {
59
+ element.setAttribute(attributeName, this.toString());
60
+ });
61
+
62
+ extendsPrototype(Array, 'handleNdAttribute', function(element, attributeName) {
63
+ element.setAttribute(attributeName, this.toString());
64
+ });
65
+
66
+ extendsPrototype(Boolean, 'handleNdAttribute', function(element, attributeName) {
67
+ element[attributeName] = this;
68
+ });
69
+
70
+ // -- handleNdBooleanAttribute -------------------------------------------------
71
+
72
+ extendsPrototype(Boolean, 'handleNdBooleanAttribute', function(element, attributeName) {
73
+ element[attributeName] = this;
74
+ });
75
+
76
+ extendsPrototype(String, 'handleNdBooleanAttribute', function(element, attributeName) {
77
+ element[attributeName] = this === element.value;
78
+ });
79
+
80
+ extendsPrototype(ObservableItem, 'handleNdBooleanAttribute', function(element, attributeName) {
81
+ const defaultValue = this.val();
82
+ defaultValue.handleNdBooleanAttribute(element, attributeName);
83
+
84
+ if (attributeName === 'checked') {
85
+ if (typeof defaultValue === 'boolean') {
86
+ element.addEventListener('input', () => this.set(element[attributeName]));
87
+ } else {
88
+ element.addEventListener('input', () => this.set(element.value));
89
+ }
90
+ this.subscribe((newValue) => element[attributeName] = newValue);
91
+ return;
92
+ }
93
+
94
+ this.subscribe((newValue) => element[attributeName] = (newValue === element.value));
95
+ });
96
+
97
+ // -- handleStyleProperty ------------------------------------------------------
98
+
99
+ extendsPrototype(String, 'handleStyleProperty', function(element, styleName) {
100
+ const isCustomProperty = styleName.startsWith('--');
101
+ if (isCustomProperty) {
102
+ element.style.setProperty(styleName, this);
103
+ return;
104
+ }
105
+ element.style[styleName] = this;
106
+ });
107
+
108
+ extendsPrototype(Number, 'handleStyleProperty', String.prototype.handleStyleProperty);
109
+
110
+ extendsPrototype(ObservableItem, 'handleStyleProperty', function(element, styleName) {
111
+ const isCustomProperty = styleName.startsWith('--');
112
+
113
+ if (isCustomProperty) {
114
+ element.style.setProperty(styleName, this.val());
115
+ this.subscribe((newValue) => {
116
+ if (newValue === false) {
117
+ element.style.removeProperty(styleName);
118
+ return;
119
+ }
120
+ element.style.setProperty(styleName, newValue);
121
+ });
122
+ return;
123
+ }
124
+
125
+ element.style[styleName] = this.val();
126
+ this.subscribe((newValue) => {
127
+ if (newValue === false) {
128
+ element.style.removeProperty(styleName);
129
+ return;
130
+ }
131
+ element.style[styleName] = newValue;
132
+ });
133
+ });
134
+
135
+ extendsPrototype(Array, 'handleStyleProperty', function(element, styleName) {
136
+ const dependencies = this.filter((item) => item.__$Observable);
137
+ if (dependencies.length) {
138
+ const style = Observable.computed(
139
+ () => this.map((item) => item.valueOf()).join(', '),
140
+ dependencies,
141
+ );
142
+ style.handleStyleProperty(element, styleName);
143
+ return;
144
+ }
145
+ element.style[styleName] = this.join(', ');
146
+ });
147
+
148
+ // -- handleClassValue ------------------------------------------------------
149
+ extendsPrototype(ObservableItem, 'handleClassValue', function(element, className) {
150
+ element.classes.toggle(className, this.val());
151
+ this.subscribe((shouldAdd) => element.classes.toggle(className, shouldAdd));
152
+ });
153
+
154
+ extendsPrototype(ObservableChecker, 'handleClassValue', function(element, className) {
155
+ let lastClass = this.val();
156
+ if(typeof lastClass === 'string') {
157
+ element.classes.toggle(lastClass, true);
158
+ this.subscribe((currentValue) => {
159
+ element.classes.remove(lastClass);
160
+ element.classes.toggle(currentValue, true);
161
+ lastClass = currentValue;
162
+ });
163
+ return;
164
+ }
165
+ element.classes.toggle(className, this.val());
166
+ this.subscribe((shouldAdd) => element.classes.toggle(className, shouldAdd));
167
+ });
168
+
169
+ extendsPrototype(TemplateBinding, 'handleClassValue', function(element, className) {
170
+ this.$hydrate(element, className);
171
+ });
172
+
173
+ extendsPrototype(String, 'handleClassValue', function(element, className) {
174
+ element.classes.toggle(className, this);
175
+ });
176
+
177
+ extendsPrototype(Boolean, 'handleClassValue', function(element, className) {
178
+ element.classes.toggle(className);
179
+ });
@@ -1,149 +1,157 @@
1
1
  import ObservableItem from '../../data/ObservableItem';
2
- import {NDElement} from '../NDElement';
2
+ import { NDElement } from '../NDElement';
3
3
  import TemplateBinding from '../TemplateBinding';
4
- import {ElementCreator} from '../ElementCreator';
4
+ import { ElementCreator } from '../ElementCreator';
5
5
  import PluginsManager from '../../utils/plugins-manager';
6
6
  import ObservableChecker from '../../data/ObservableChecker';
7
- import Validator from '../../utils/validator';
8
7
 
8
+ /**
9
+ * Extends a prototype with a non-enumerable, configurable, writable property.
10
+ *
11
+ * @param {Function} Constructor - The constructor whose prototype to extend
12
+ * @param {string} name - Property name
13
+ * @param {Function} fn - Implementation
14
+ */
15
+ function extendsPrototype(Constructor, name, fn) {
16
+ Object.defineProperty(Constructor.prototype, name, {
17
+ value: fn,
18
+ enumerable: false,
19
+ configurable: true,
20
+ writable: true,
21
+ });
22
+ }
23
+
24
+ // -- Static assignment (not a prototype method) -------------------------------
9
25
 
10
26
  NDElement.$getChild = ElementCreator.getChild;
11
27
 
12
- Object.prototype.toNdElement = function () {
13
- return ElementCreator.createStaticTextNode(null, this.toString());
14
- };
28
+ // -- toNdElement --------------------------------------------------------------
15
29
 
16
30
  /**
17
- * Converts a string to a reactive text node.
18
- *
19
- * @returns {Text} Static text node containing the string value
31
+ * Converts a string to a static text node.
32
+ * @returns {Text}
20
33
  */
21
- String.prototype.toNdElement = function () {
34
+ extendsPrototype(Object, 'toNdElement', function() {
35
+ return ElementCreator.createStaticTextNode(null, JSON.stringify(this));
36
+ });
37
+
38
+ /**
39
+ * Converts a string to a static text node.
40
+ * @returns {Text}
41
+ */
42
+ extendsPrototype(String, 'toNdElement', function() {
22
43
  return ElementCreator.createStaticTextNode(null, this);
23
- };
44
+ });
24
45
 
25
46
  /**
26
47
  * Converts a number to a static text node.
27
- *
28
- * @returns {Text} Static text node containing the number as a string
48
+ * @returns {Text}
29
49
  */
30
- Number.prototype.toNdElement = function () {
50
+ extendsPrototype(Number, 'toNdElement', function() {
31
51
  return ElementCreator.createStaticTextNode(null, this.toString());
32
- };
52
+ });
33
53
 
34
54
  /**
35
55
  * Returns the element itself (identity for DOM compatibility).
36
- *
37
- * @returns {Element} this
56
+ * @returns {Element}
38
57
  */
39
- Element.prototype.toNdElement = function () {
58
+ extendsPrototype(Element, 'toNdElement', function() {
40
59
  return this;
41
- };
60
+ });
42
61
 
43
62
  /**
44
63
  * Returns the text node itself (identity for DOM compatibility).
45
- *
46
- * @returns {Text} this
64
+ * @returns {Text}
47
65
  */
48
- Text.prototype.toNdElement = function () {
66
+ extendsPrototype(Text, 'toNdElement', function() {
49
67
  return this;
50
- };
68
+ });
51
69
 
52
70
  /**
53
71
  * Returns the comment node itself (identity for DOM compatibility).
54
- *
55
- * @returns {Comment} this
72
+ * @returns {Comment}
56
73
  */
57
- Comment.prototype.toNdElement = function () {
74
+ extendsPrototype(Comment, 'toNdElement', function() {
58
75
  return this;
59
- };
76
+ });
60
77
 
61
78
  /**
62
79
  * Returns the document itself (identity for DOM compatibility).
63
- *
64
- * @returns {Document} this
80
+ * @returns {Document}
65
81
  */
66
- Document.prototype.toNdElement = function () {
82
+ extendsPrototype(Document, 'toNdElement', function() {
67
83
  return this;
68
- };
84
+ });
69
85
 
70
86
  /**
71
87
  * Returns the document fragment itself (identity for DOM compatibility).
72
- *
73
- * @returns {DocumentFragment} this
88
+ * @returns {DocumentFragment}
74
89
  */
75
- DocumentFragment.prototype.toNdElement = function () {
90
+ extendsPrototype(DocumentFragment, 'toNdElement', function() {
76
91
  return this;
77
- };
92
+ });
78
93
 
79
94
  /**
80
95
  * Converts the ObservableItem to a reactive text node that updates automatically when the value changes.
81
- *
82
- * @returns {Text} Reactive text node bound to this observable
96
+ * @returns {Text}
83
97
  */
84
- ObservableItem.prototype.toNdElement = function () {
98
+ extendsPrototype(ObservableItem, 'toNdElement', function() {
85
99
  return ElementCreator.createObservableNode(null, this);
86
- };
100
+ });
87
101
 
88
- ObservableChecker.prototype.toNdElement = ObservableItem.prototype.toNdElement;
102
+ /**
103
+ * ObservableChecker shares the same reactive text node behaviour as ObservableItem.
104
+ * @returns {Text}
105
+ */
106
+ extendsPrototype(ObservableChecker, 'toNdElement', ObservableItem.prototype.toNdElement);
89
107
 
90
108
  /**
91
109
  * Converts the NDElement to its underlying HTMLElement (or ghost DOM fragment if ghostDom was used).
92
- *
93
- * @returns {HTMLElement|DocumentFragment} The underlying DOM node
110
+ * @returns {HTMLElement|DocumentFragment}
94
111
  */
95
- NDElement.prototype.toNdElement = function () {
112
+ extendsPrototype(NDElement, 'toNdElement', function() {
96
113
  const element = this.$element ?? this.$build?.() ?? this.build?.() ?? null;
97
- if(this.$attachements) {
98
- if(!this.$attachements.contains(this.$element)) {
114
+ if (this.$attachements) {
115
+ if (!this.$attachements.contains(this.$element)) {
99
116
  this.$attachements.append(this.$element);
100
117
  }
101
118
  return this.$attachements;
102
119
  }
103
120
  return element;
104
- };
121
+ });
105
122
 
106
123
  /**
107
- * Converts the array to a DocumentFragment containing all elements.
124
+ * Converts the array to a DocumentFragment containing all child elements.
108
125
  * Each item is processed through ElementCreator.getChild().
109
- *
110
- * @returns {DocumentFragment} Fragment containing all array children
126
+ * @returns {DocumentFragment}
111
127
  */
112
- Array.prototype.toNdElement = function () {
128
+ extendsPrototype(Array, 'toNdElement', function() {
113
129
  const fragment = document.createDocumentFragment();
114
- for(let i = 0, length = this.length; i < length; i++) {
130
+ for (let i = 0, length = this.length; i < length; i++) {
115
131
  const child = ElementCreator.getChild(this[i]);
116
- if(child === null) {
117
- continue;
118
- }
132
+ if (child === null) continue;
119
133
  fragment.appendChild(child);
120
134
  }
121
135
  return fragment;
122
- };
136
+ });
123
137
 
124
138
  /**
125
139
  * Calls the function and converts its return value to a DOM node.
126
140
  * Used internally by ElementCreator to process function-based children.
127
- *
128
- * @returns {Node} The DOM node returned by the function
141
+ * @returns {Node}
129
142
  */
130
- Function.prototype.toNdElement = function () {
131
- const callback = this;
132
- if(process.env.NODE_ENV === 'development') {
133
- PluginsManager.emit('BeforeProcessComponent', callback);
134
- }
135
- const child = callback();
136
- if(Validator.isElement(child)) {
137
- return child;
143
+ extendsPrototype(Function, 'toNdElement', function() {
144
+ const child = this;
145
+ if (process.env.NODE_ENV === 'development') {
146
+ PluginsManager.emit('BeforeProcessComponent', child);
138
147
  }
139
- return child.toNdElement();
140
- };
148
+ return ElementCreator.getChild(child());
149
+ });
141
150
 
142
151
  /**
143
152
  * Converts the TemplateBinding to a hydratable DOM node for use in TemplateCloner.
144
- *
145
- * @returns {Node} Hydratable node
153
+ * @returns {Node}
146
154
  */
147
- TemplateBinding.prototype.toNdElement = function () {
155
+ extendsPrototype(TemplateBinding, 'toNdElement', function() {
148
156
  return ElementCreator.createHydratableNode(null, this);
149
- };
157
+ });