rasti 3.0.1 → 4.0.0-alpha.0
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/README.md +42 -11
- package/dist/rasti.js +1767 -622
- package/dist/rasti.min.js +1 -1
- package/es/Component.js +728 -467
- package/es/Emitter.js +182 -28
- package/es/Model.js +237 -51
- package/es/View.js +73 -31
- package/es/core/Element.js +55 -0
- package/es/core/EventsManager.js +41 -0
- package/es/core/Interpolation.js +70 -0
- package/es/core/InterpolationWrapper.js +14 -0
- package/es/core/Partial.js +12 -0
- package/es/core/PathManager.js +88 -0
- package/es/core/SafeHTML.js +17 -0
- package/es/index.js +13 -0
- package/es/utils/deepFlat.js +4 -2
- package/es/utils/findComment.js +42 -0
- package/es/utils/getAttributesDiff.js +33 -0
- package/es/utils/getAttributesHTML.js +25 -0
- package/es/utils/getResult.js +4 -2
- package/es/utils/parseHTML.js +14 -0
- package/es/utils/syncNode.js +109 -0
- package/es/utils/validateListener.js +14 -0
- package/lib/Component.cjs +729 -468
- package/lib/Emitter.cjs +182 -28
- package/lib/Model.cjs +237 -51
- package/lib/View.cjs +73 -31
- package/lib/core/Element.cjs +57 -0
- package/lib/core/EventsManager.cjs +43 -0
- package/lib/core/Interpolation.cjs +72 -0
- package/lib/core/InterpolationWrapper.cjs +16 -0
- package/lib/core/Partial.cjs +14 -0
- package/lib/core/PathManager.cjs +90 -0
- package/lib/core/SafeHTML.cjs +19 -0
- package/lib/index.cjs +13 -0
- package/lib/utils/deepFlat.cjs +4 -2
- package/lib/utils/findComment.cjs +44 -0
- package/lib/utils/getAttributesDiff.cjs +35 -0
- package/lib/utils/getAttributesHTML.cjs +27 -0
- package/lib/utils/getResult.cjs +4 -2
- package/lib/utils/parseHTML.cjs +16 -0
- package/lib/utils/syncNode.cjs +111 -0
- package/lib/utils/validateListener.cjs +16 -0
- package/package.json +11 -8
- package/src/Component.js +725 -466
- package/src/Emitter.js +182 -28
- package/src/Model.js +236 -51
- package/src/View.js +73 -31
- package/src/core/Element.js +55 -0
- package/src/core/EventsManager.js +41 -0
- package/src/core/Interpolation.js +70 -0
- package/src/core/InterpolationWrapper.js +14 -0
- package/src/core/Partial.js +12 -0
- package/src/core/PathManager.js +88 -0
- package/src/core/SafeHTML.js +17 -0
- package/src/index.js +4 -5
- package/src/utils/deepFlat.js +4 -2
- package/src/utils/findComment.js +40 -0
- package/src/utils/getAttributesDiff.js +31 -0
- package/src/utils/getAttributesHTML.js +23 -0
- package/src/utils/getResult.js +6 -2
- package/src/utils/parseHTML.js +12 -0
- package/src/utils/syncNode.js +107 -0
- package/src/utils/validateListener.js +12 -0
package/lib/View.cjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var Emitter = require('./Emitter.cjs');
|
|
4
4
|
var utils_getResult = require('./utils/getResult.cjs');
|
|
5
|
+
var utils_validateListener = require('./utils/validateListener.cjs');
|
|
5
6
|
|
|
6
7
|
/*
|
|
7
8
|
* These option keys will be extended on the view instance.
|
|
@@ -23,15 +24,15 @@ const viewOptions = ['el', 'tag', 'attributes', 'events', 'model', 'template', '
|
|
|
23
24
|
* @module
|
|
24
25
|
* @extends Emitter
|
|
25
26
|
* @param {object} options Object containing options. The following keys will be merged into the view instance: `el`, `tag`, `attributes`, `events`, `model`, `template`, `onDestroy`.
|
|
26
|
-
* @property {node|
|
|
27
|
-
* @property {string|
|
|
28
|
-
* @property {object|
|
|
29
|
-
* @property {object|
|
|
27
|
+
* @property {node|Function} el Every view has a root DOM element stored at `this.el`. If not present, it will be created. If `this.el` is a function, it will be called to get the element at `this.ensureElement`, bound to the view instance. See {@link module_view__ensureelement View.ensureElement}.
|
|
28
|
+
* @property {string|Function} tag If `this.el` is not present, an element will be created using `this.tag` and `this.attributes`. Default is `div`. If it is a function, it will be called to get the tag, bound to the view instance. See {@link module_view__ensureelement View.ensureElement}.
|
|
29
|
+
* @property {object|Function} attributes If `this.el` is not present, an element will be created using `this.tag` and `this.attributes`. If it is a function, it will be called to get the attributes object, bound to the view instance. See {@link module_view__ensureelement View.ensureElement}.
|
|
30
|
+
* @property {object|Function} events Object in the format `{'event selector' : 'listener'}`. It will be used to bind delegated event listeners to the root element. If it is a function, it will be called to get the events object, bound to the view instance. See {@link module_view_delegateevents View.delegateEvents}.
|
|
30
31
|
* @property {object} model A model or any object containing data and business logic.
|
|
31
|
-
* @property {
|
|
32
|
+
* @property {Function} template A function that returns a string with the view's inner HTML. See {@link module_view__render View.render}.
|
|
32
33
|
* @property {number} uid Unique identifier for the view instance. This can be used to generate unique IDs for elements within the view. It is automatically generated and should not be set manually.
|
|
33
34
|
* @example
|
|
34
|
-
* import { View } from 'rasti';
|
|
35
|
+
* import { View, Model } from 'rasti';
|
|
35
36
|
*
|
|
36
37
|
* class Timer extends View {
|
|
37
38
|
* constructor(options) {
|
|
@@ -56,9 +57,6 @@ class View extends Emitter {
|
|
|
56
57
|
super();
|
|
57
58
|
// Call preinitialize.
|
|
58
59
|
this.preinitialize.apply(this, arguments);
|
|
59
|
-
// Generate unique id.
|
|
60
|
-
// Useful to generate element ids.
|
|
61
|
-
this.uid = `rasti-${++View.uid}`;
|
|
62
60
|
// Store delegated event listeners,
|
|
63
61
|
// so they can be unbound later.
|
|
64
62
|
this.delegatedEventListeners = [];
|
|
@@ -67,17 +65,23 @@ class View extends Emitter {
|
|
|
67
65
|
this.children = [];
|
|
68
66
|
// Mutable array to store handlers to be called on destroy.
|
|
69
67
|
this.destroyQueue = [];
|
|
68
|
+
this.viewOptions = [];
|
|
70
69
|
// Extend "this" with options.
|
|
71
70
|
viewOptions.forEach(key => {
|
|
72
|
-
if (key in options)
|
|
71
|
+
if (key in options) {
|
|
72
|
+
this[key] = options[key];
|
|
73
|
+
this.viewOptions.push(key);
|
|
74
|
+
}
|
|
73
75
|
});
|
|
76
|
+
// Ensure that the view has a unique id at `this.uid`.
|
|
77
|
+
this.ensureUid();
|
|
74
78
|
// Ensure that the view has a root element at `this.el`.
|
|
75
79
|
this.ensureElement();
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
/**
|
|
79
83
|
* If you define a preinitialize method, it will be invoked when the view is first created, before any instantiation logic is run.
|
|
80
|
-
* @param {object}
|
|
84
|
+
* @param {object} options The view options.
|
|
81
85
|
*/
|
|
82
86
|
preinitialize() {}
|
|
83
87
|
|
|
@@ -112,6 +116,8 @@ class View extends Emitter {
|
|
|
112
116
|
this.destroyChildren();
|
|
113
117
|
// Undelegate `this.el` event listeners
|
|
114
118
|
this.undelegateEvents();
|
|
119
|
+
// Stop listening to events.
|
|
120
|
+
this.stopListening();
|
|
115
121
|
// Unbind `this` events.
|
|
116
122
|
this.off();
|
|
117
123
|
// Call destroy queue.
|
|
@@ -119,6 +125,8 @@ class View extends Emitter {
|
|
|
119
125
|
this.destroyQueue = [];
|
|
120
126
|
// Call onDestroy lifecycle method
|
|
121
127
|
this.onDestroy.apply(this, arguments);
|
|
128
|
+
// Set destroyed flag.
|
|
129
|
+
this.destroyed = true;
|
|
122
130
|
// Return `this` for chaining.
|
|
123
131
|
return this;
|
|
124
132
|
}
|
|
@@ -150,6 +158,13 @@ class View extends Emitter {
|
|
|
150
158
|
this.children = [];
|
|
151
159
|
}
|
|
152
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Ensure that the view has a unique id at `this.uid`.
|
|
163
|
+
*/
|
|
164
|
+
ensureUid() {
|
|
165
|
+
if (!this.uid) this.uid = `r-${++View.uid}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
153
168
|
/**
|
|
154
169
|
* Ensure that the view has a root element at `this.el`.
|
|
155
170
|
* You shouldn't call this method directly. It's called from the constructor.
|
|
@@ -216,26 +231,43 @@ class View extends Emitter {
|
|
|
216
231
|
* All attached listeners are bound to the view, ensuring that `this` refers to the view object when the listeners are invoked.
|
|
217
232
|
* When `delegateEvents` is called again, possibly with a different events object, all previous listeners are removed and delegated afresh.
|
|
218
233
|
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
234
|
+
* **Listener signature:** `(event, view, matched)`
|
|
235
|
+
* - `event`: The native DOM event object.
|
|
236
|
+
* - `view`: The current view instance (`this`).
|
|
237
|
+
* - `matched`: The element that satisfies the selector. If no selector is provided, it will be the view's root element (`this.el`).
|
|
238
|
+
*
|
|
239
|
+
* If more than one ancestor between `event.target` and the view's root element matches the selector, the listener will be
|
|
240
|
+
* invoked **once for each matched element** (from inner to outer).
|
|
241
|
+
*
|
|
221
242
|
* @param {object} [events] Object in the format `{'event selector' : 'listener'}`. Used to bind delegated event listeners to the root element.
|
|
222
243
|
* @return {Rasti.View} Returns `this` for chaining.
|
|
223
244
|
* @example
|
|
224
|
-
* // Using
|
|
245
|
+
* // Using prototype (recommended for static events)
|
|
225
246
|
* class Modal extends View {
|
|
247
|
+
* onClickOk(event, view, matched) {
|
|
248
|
+
* // matched === the button.ok element that was clicked
|
|
249
|
+
* this.close();
|
|
250
|
+
* }
|
|
251
|
+
*
|
|
252
|
+
* onClickCancel() {
|
|
253
|
+
* this.destroy();
|
|
254
|
+
* }
|
|
255
|
+
* }
|
|
256
|
+
* Modal.prototype.events = {
|
|
257
|
+
* 'click button.ok': 'onClickOk',
|
|
258
|
+
* 'click button.cancel': 'onClickCancel',
|
|
259
|
+
* 'submit form': 'onSubmit'
|
|
260
|
+
* };
|
|
261
|
+
*
|
|
262
|
+
* // Using a function for dynamic events
|
|
263
|
+
* class DynamicView extends View {
|
|
226
264
|
* events() {
|
|
227
265
|
* return {
|
|
228
|
-
*
|
|
229
|
-
* 'click
|
|
266
|
+
* [`click .${this.model.buttonClass}`]: 'onButtonClick',
|
|
267
|
+
* 'click': 'onRootClick'
|
|
230
268
|
* };
|
|
231
269
|
* }
|
|
232
270
|
* }
|
|
233
|
-
*
|
|
234
|
-
* // Using an object.
|
|
235
|
-
* Modal.prototype.events = {
|
|
236
|
-
* 'click button.ok' : 'onClickOkButton',
|
|
237
|
-
* 'click button.cancel' : function() {}
|
|
238
|
-
* };
|
|
239
271
|
*/
|
|
240
272
|
delegateEvents(events) {
|
|
241
273
|
if (!events) events = utils_getResult(this.events, this);
|
|
@@ -252,13 +284,10 @@ class View extends Emitter {
|
|
|
252
284
|
const selector = keyParts.join(' ');
|
|
253
285
|
|
|
254
286
|
let listener = events[key];
|
|
255
|
-
// Listener may be a string representing a method name on the view,
|
|
256
|
-
|
|
257
|
-
listener
|
|
258
|
-
|
|
259
|
-
this[listener] :
|
|
260
|
-
listener
|
|
261
|
-
).bind(this);
|
|
287
|
+
// Listener may be a string representing a method name on the view, or a function.
|
|
288
|
+
if (typeof listener === 'string') listener = this[listener];
|
|
289
|
+
// Validate listener is a function.
|
|
290
|
+
utils_validateListener(listener);
|
|
262
291
|
|
|
263
292
|
if (!eventTypes[type]) eventTypes[type] = [];
|
|
264
293
|
|
|
@@ -270,7 +299,20 @@ class View extends Emitter {
|
|
|
270
299
|
const typeListener = (event) => {
|
|
271
300
|
// Iterate and run every individual listener if the selector matches.
|
|
272
301
|
eventTypes[type].forEach(({ selector, listener }) => {
|
|
273
|
-
|
|
302
|
+
// No selector provided: invoke listener once with root element.
|
|
303
|
+
if (!selector) {
|
|
304
|
+
listener.call(this, event, this, this.el);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
let node = event.target;
|
|
309
|
+
// Traverse ancestors until reaching the view root (`this.el`).
|
|
310
|
+
while (node && node !== this.el) {
|
|
311
|
+
if (node.matches && node.matches(selector)) {
|
|
312
|
+
listener.call(this, event, this, node);
|
|
313
|
+
}
|
|
314
|
+
node = node.parentElement;
|
|
315
|
+
}
|
|
274
316
|
});
|
|
275
317
|
};
|
|
276
318
|
|
|
@@ -324,7 +366,7 @@ class View extends Emitter {
|
|
|
324
366
|
* Override this method to provide a custom escape function.
|
|
325
367
|
* This method is inherited by {@link #module_component Component} and used to escape template interpolations.
|
|
326
368
|
* @static
|
|
327
|
-
* @param {string}
|
|
369
|
+
* @param {string} value String to escape.
|
|
328
370
|
* @return {string} Escaped string.
|
|
329
371
|
*/
|
|
330
372
|
static sanitize(value) {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var utils_getAttributesDiff = require('../utils/getAttributesDiff.cjs');
|
|
4
|
+
|
|
5
|
+
const SYNC_PROPS = ['value', 'checked', 'selected'];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Element reference for managing DOM element attributes.
|
|
9
|
+
* @param {Object} options The options object.
|
|
10
|
+
* @param {Function} options.getSelector Function that returns the CSS selector for the element.
|
|
11
|
+
* @param {Function} options.getAttributes Function that returns the attributes object for the element.
|
|
12
|
+
* @private
|
|
13
|
+
*/
|
|
14
|
+
class Element {
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.getSelector = options.getSelector;
|
|
17
|
+
this.getAttributes = options.getAttributes;
|
|
18
|
+
this.previousAttributes = {};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Attach the element reference to a DOM element.
|
|
23
|
+
* @param {Node} parent The parent node to search in.
|
|
24
|
+
*/
|
|
25
|
+
hydrate(parent) {
|
|
26
|
+
this.ref = parent.querySelector(this.getSelector());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Update the element's attributes based on the difference with previous attributes.
|
|
31
|
+
*/
|
|
32
|
+
update() {
|
|
33
|
+
// Attributes diff.
|
|
34
|
+
const attributes = this.getAttributes();
|
|
35
|
+
const { remove, add } = utils_getAttributesDiff(attributes, this.previousAttributes);
|
|
36
|
+
// Store previous attributes.
|
|
37
|
+
this.previousAttributes = attributes;
|
|
38
|
+
// Remove attributes first so later `setAttribute` overrides if needed.
|
|
39
|
+
remove.forEach(attr => {
|
|
40
|
+
this.ref.removeAttribute(attr);
|
|
41
|
+
if (SYNC_PROPS.includes(attr) && attr in this.ref) {
|
|
42
|
+
// Reset property to default.
|
|
43
|
+
this.ref[attr] = attr === 'value' ? '' : false;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
// Add / update attributes.
|
|
47
|
+
Object.keys(add).forEach(attr => {
|
|
48
|
+
const value = add[attr];
|
|
49
|
+
this.ref.setAttribute(attr, value);
|
|
50
|
+
if (SYNC_PROPS.includes(attr) && attr in this.ref) {
|
|
51
|
+
this.ref[attr] = attr === 'value' ? value : value !== false && value !== 'false';
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = Element;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Manager for delegated events.
|
|
5
|
+
* @private
|
|
6
|
+
*/
|
|
7
|
+
class EventsManager {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.listeners = [];
|
|
10
|
+
this.types = new Set();
|
|
11
|
+
this.previousSize = 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Add a listener to the events manager.
|
|
16
|
+
* @param {Function} listener The listener to add.
|
|
17
|
+
* @param {string} type The type of event.
|
|
18
|
+
* @return {number} The index of the listener.
|
|
19
|
+
*/
|
|
20
|
+
addListener(listener, type) {
|
|
21
|
+
this.types.add(type);
|
|
22
|
+
this.listeners.push(listener);
|
|
23
|
+
return this.listeners.length - 1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Reset the events manager.
|
|
28
|
+
*/
|
|
29
|
+
reset() {
|
|
30
|
+
this.listeners = [];
|
|
31
|
+
this.previousSize = this.types.size;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if there are pending types.
|
|
36
|
+
* @return {boolean} True if there are pending types, false otherwise.
|
|
37
|
+
*/
|
|
38
|
+
hasPendingTypes() {
|
|
39
|
+
return this.types.size > this.previousSize;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = EventsManager;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var utils_syncNode = require('../utils/syncNode.cjs');
|
|
4
|
+
var utils_findComment = require('../utils/findComment.cjs');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Interpolation reference for managing dynamic content between comment markers.
|
|
8
|
+
* Handles the lifecycle of content that can change between renders, including
|
|
9
|
+
* component recycling and DOM synchronization.
|
|
10
|
+
* @param {Object} options The options object.
|
|
11
|
+
* @param {Function} options.getStart Function that returns the start comment marker text.
|
|
12
|
+
* @param {Function} options.getEnd Function that returns the end comment marker text.
|
|
13
|
+
* @param {any} options.expression The expression to be evaluated for the interpolation.
|
|
14
|
+
* @param {Function} options.isComponent Function that checks if an element is a component root element.
|
|
15
|
+
* @param {Function} options.isElement Function that checks if an element has the Rasti data attribute.
|
|
16
|
+
* @private
|
|
17
|
+
*/
|
|
18
|
+
class Interpolation {
|
|
19
|
+
constructor(options) {
|
|
20
|
+
this.getStart = options.getStart;
|
|
21
|
+
this.getEnd = options.getEnd;
|
|
22
|
+
this.expression = options.expression;
|
|
23
|
+
this.isComponent = options.isComponent;
|
|
24
|
+
this.isElement = options.isElement;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Attach the interpolation reference to comment markers in the DOM.
|
|
29
|
+
* Searches for start and end comment markers, skipping component subtrees.
|
|
30
|
+
* @param {Node} parent The parent node to search in.
|
|
31
|
+
*/
|
|
32
|
+
hydrate(parent) {
|
|
33
|
+
const startComment = utils_findComment(parent, this.getStart(), this.isComponent);
|
|
34
|
+
const endComment = utils_findComment(parent, this.getEnd(), this.isComponent, startComment);
|
|
35
|
+
|
|
36
|
+
this.ref = [
|
|
37
|
+
startComment,
|
|
38
|
+
endComment
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Update the interpolation content with a new fragment.
|
|
44
|
+
* Optimizes updates by syncing single non-component elements or replacing content entirely.
|
|
45
|
+
* @param {DocumentFragment} fragment The new content fragment to insert.
|
|
46
|
+
*/
|
|
47
|
+
update(fragment) {
|
|
48
|
+
const [startComment, endComment] = this.ref;
|
|
49
|
+
|
|
50
|
+
const currentFirstElement = startComment.nextSibling;
|
|
51
|
+
const currentSingleChildElement = currentFirstElement.nextSibling === endComment;
|
|
52
|
+
const currentEmpty = currentFirstElement == endComment;
|
|
53
|
+
const fragmentChildren = fragment.children;
|
|
54
|
+
|
|
55
|
+
if (currentSingleChildElement && fragmentChildren.length === 1 && !this.isElement(currentFirstElement)) {
|
|
56
|
+
// There is a single child element that is not a component's root element. Sync node attributes and content.
|
|
57
|
+
utils_syncNode(currentFirstElement, fragmentChildren[0]);
|
|
58
|
+
} else if (currentEmpty) {
|
|
59
|
+
// Interpolation is empty. Insert the fragment.
|
|
60
|
+
endComment.parentNode.insertBefore(fragment, endComment);
|
|
61
|
+
} else {
|
|
62
|
+
// Interpolation is not empty. Replace the interpolation content.
|
|
63
|
+
const range = document.createRange();
|
|
64
|
+
range.setStartAfter(startComment);
|
|
65
|
+
range.setEndBefore(endComment);
|
|
66
|
+
range.deleteContents();
|
|
67
|
+
range.insertNode(fragment);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = Interpolation;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wrapper for interpolation results with markers.
|
|
5
|
+
* @param {number} interpolationUid The interpolation UID for markers.
|
|
6
|
+
* @param {any} result The interpolation result.
|
|
7
|
+
* @private
|
|
8
|
+
*/
|
|
9
|
+
class InterpolationWrapper {
|
|
10
|
+
constructor(interpolationUid, result) {
|
|
11
|
+
this.interpolationUid = interpolationUid;
|
|
12
|
+
this.result = result;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = InterpolationWrapper;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wrapper class for partial templates that preserves structure.
|
|
5
|
+
* @param {Array} items The items in the partial.
|
|
6
|
+
* @private
|
|
7
|
+
*/
|
|
8
|
+
class Partial {
|
|
9
|
+
constructor(items) {
|
|
10
|
+
this.items = items;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = Partial;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Manager for component position tracking and recycling.
|
|
5
|
+
* @private
|
|
6
|
+
*/
|
|
7
|
+
class PathManager {
|
|
8
|
+
constructor() {}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Reset before render.
|
|
12
|
+
*/
|
|
13
|
+
reset() {
|
|
14
|
+
this.paused = 0;
|
|
15
|
+
this.previous = this.tracked || new Map();
|
|
16
|
+
this.tracked = new Map();
|
|
17
|
+
this.positionStack = [0];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Push position to stack.
|
|
22
|
+
*/
|
|
23
|
+
push() {
|
|
24
|
+
this.positionStack.push(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Pop position from stack.
|
|
29
|
+
*/
|
|
30
|
+
pop() {
|
|
31
|
+
this.positionStack.pop();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Increment position.
|
|
36
|
+
*/
|
|
37
|
+
increment() {
|
|
38
|
+
this.positionStack[this.positionStack.length - 1]++;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Pause tracking.
|
|
43
|
+
*/
|
|
44
|
+
pause() {
|
|
45
|
+
this.paused++;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resume tracking.
|
|
50
|
+
*/
|
|
51
|
+
resume() {
|
|
52
|
+
this.paused--;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get current path as array.
|
|
57
|
+
* @return {string} Current position path.
|
|
58
|
+
*/
|
|
59
|
+
getPath() {
|
|
60
|
+
return this.positionStack.join('-');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Track component at current path.
|
|
65
|
+
* @param {Component} component The component to track.
|
|
66
|
+
* @return {Component} The component.
|
|
67
|
+
*/
|
|
68
|
+
track(component) {
|
|
69
|
+
if (this.paused === 0) {
|
|
70
|
+
this.tracked.set(
|
|
71
|
+
this.getPath(),
|
|
72
|
+
component
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return component;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Find recyclable component by path and type.
|
|
81
|
+
* @param {Function} constructor The component constructor.
|
|
82
|
+
* @return {Component|null} The recyclable component or null.
|
|
83
|
+
*/
|
|
84
|
+
findRecyclable(constructor) {
|
|
85
|
+
const prev = this.previous.get(this.getPath());
|
|
86
|
+
return prev && prev.constructor === constructor && !prev.key ? prev : null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = PathManager;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wrapper class for HTML strings marked as safe.
|
|
5
|
+
* @param {string} value The HTML string to be marked as safe.
|
|
6
|
+
* @property {string} value The HTML string.
|
|
7
|
+
* @private
|
|
8
|
+
*/
|
|
9
|
+
class SafeHTML {
|
|
10
|
+
constructor(value) {
|
|
11
|
+
this.value = value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
toString() {
|
|
15
|
+
return this.value;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = SafeHTML;
|
package/lib/index.cjs
CHANGED
|
@@ -4,8 +4,21 @@ var Emitter = require('./Emitter.cjs');
|
|
|
4
4
|
var Model = require('./Model.cjs');
|
|
5
5
|
var View = require('./View.cjs');
|
|
6
6
|
var Component = require('./Component.cjs');
|
|
7
|
+
require('./utils/validateListener.cjs');
|
|
7
8
|
require('./utils/getResult.cjs');
|
|
9
|
+
require('./core/SafeHTML.cjs');
|
|
10
|
+
require('./core/Partial.cjs');
|
|
11
|
+
require('./core/InterpolationWrapper.cjs');
|
|
12
|
+
require('./core/EventsManager.cjs');
|
|
13
|
+
require('./core/PathManager.cjs');
|
|
14
|
+
require('./core/Element.cjs');
|
|
15
|
+
require('./utils/getAttributesDiff.cjs');
|
|
16
|
+
require('./core/Interpolation.cjs');
|
|
17
|
+
require('./utils/syncNode.cjs');
|
|
18
|
+
require('./utils/findComment.cjs');
|
|
8
19
|
require('./utils/deepFlat.cjs');
|
|
20
|
+
require('./utils/parseHTML.cjs');
|
|
21
|
+
require('./utils/getAttributesHTML.cjs');
|
|
9
22
|
|
|
10
23
|
|
|
11
24
|
|
package/lib/utils/deepFlat.cjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
* Flatten an array recursively
|
|
3
|
+
/**
|
|
4
|
+
* Flatten an array recursively.
|
|
5
5
|
* @param {Array} arr Array to flat recursively
|
|
6
6
|
* @return {Array} Flat array
|
|
7
|
+
* @module
|
|
8
|
+
* @private
|
|
7
9
|
*/
|
|
8
10
|
const deepFlat = (arr) => arr.reduce((acc, val) => {
|
|
9
11
|
if (Array.isArray(val)) acc.push(...deepFlat(val));
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Finds the first comment node whose text matches exactly the given `text`.
|
|
5
|
+
* Uses manual DOM traversal. Skips entire subtrees if `shouldSkip(element)` returns true.
|
|
6
|
+
* Starts searching from `startNode` if provided, otherwise from `root.firstChild`.
|
|
7
|
+
* @param {Node} root - Root node or fragment that limits the search scope.
|
|
8
|
+
* @param {string} text - Exact comment text to match.
|
|
9
|
+
* @param {Function} [shouldSkip] - Function that receives an element and returns true if its subtree should be skipped.
|
|
10
|
+
* @param {Node} [startNode] - Node to start searching from (defaults to root.firstChild).
|
|
11
|
+
* @return {Comment|null} The first matching comment node, or null if not found.
|
|
12
|
+
* @module
|
|
13
|
+
* @private
|
|
14
|
+
*/
|
|
15
|
+
function findComment(
|
|
16
|
+
root,
|
|
17
|
+
text,
|
|
18
|
+
shouldSkip = () => false,
|
|
19
|
+
startNode
|
|
20
|
+
) {
|
|
21
|
+
let node = startNode || root.firstChild;
|
|
22
|
+
|
|
23
|
+
while (node) {
|
|
24
|
+
// Check if current node is a comment with matching text.
|
|
25
|
+
if (node.nodeType === Node.COMMENT_NODE && node.data.trim() === text) {
|
|
26
|
+
return node;
|
|
27
|
+
}
|
|
28
|
+
// Descend into children if allowed and present.
|
|
29
|
+
if (node.nodeType === Node.ELEMENT_NODE && !shouldSkip(node) && node.firstChild) {
|
|
30
|
+
node = node.firstChild;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
// Move to next sibling, or climb up until a sibling is found.
|
|
34
|
+
while (node && !node.nextSibling) {
|
|
35
|
+
node = node.parentNode;
|
|
36
|
+
if (!node || node === root) return null;
|
|
37
|
+
}
|
|
38
|
+
if (node) node = node.nextSibling;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = findComment;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Get difference between current and previous attributes.
|
|
5
|
+
* @param {Object} attributes Current attributes object.
|
|
6
|
+
* @param {Object} previous Previous attributes object.
|
|
7
|
+
* @return {Object} Object with add and remove properties.
|
|
8
|
+
* @module
|
|
9
|
+
* @private
|
|
10
|
+
*/
|
|
11
|
+
function getAttributesDiff(attributes, previous = {}) {
|
|
12
|
+
const add = {};
|
|
13
|
+
const remove = [];
|
|
14
|
+
// Find attributes to add/update.
|
|
15
|
+
Object.keys(attributes).forEach(key => {
|
|
16
|
+
let value = attributes[key];
|
|
17
|
+
|
|
18
|
+
if (value === true) {
|
|
19
|
+
add[key] = '';
|
|
20
|
+
} else if (value !== false) {
|
|
21
|
+
if (value === null || typeof value === 'undefined') value = '';
|
|
22
|
+
add[key] = value;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
// Find attributes to remove.
|
|
26
|
+
Object.keys(previous).forEach(key => {
|
|
27
|
+
if (!(key in attributes) || ((previous[key] !== attributes[key]) && attributes[key] === false)) {
|
|
28
|
+
remove.push(key);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return { add, remove };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = getAttributesDiff;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate HTML string from attributes object.
|
|
5
|
+
* @param {Object} attributes Object containing attribute names and values.
|
|
6
|
+
* @return {string} HTML string of attributes.
|
|
7
|
+
* @module
|
|
8
|
+
* @private
|
|
9
|
+
*/
|
|
10
|
+
function getAttributesHTML(attributes) {
|
|
11
|
+
const html = [];
|
|
12
|
+
|
|
13
|
+
Object.keys(attributes).forEach(key => {
|
|
14
|
+
let value = attributes[key];
|
|
15
|
+
|
|
16
|
+
if (value === true) {
|
|
17
|
+
html.push(key);
|
|
18
|
+
} else if (value !== false) {
|
|
19
|
+
if (value === null || typeof value === 'undefined') value = '';
|
|
20
|
+
html.push(`${key}="${value}"`);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return html.join(' ');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = getAttributesHTML;
|