rasti 3.0.0 → 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 +45 -14
- package/dist/rasti.js +1854 -679
- package/dist/rasti.min.js +1 -1
- package/es/Component.js +770 -479
- package/es/Emitter.js +182 -28
- package/es/Model.js +237 -51
- package/es/View.js +95 -53
- 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 +771 -480
- package/lib/Emitter.cjs +182 -28
- package/lib/Model.cjs +237 -51
- package/lib/View.cjs +95 -53
- 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 +767 -478
- package/src/Emitter.js +182 -28
- package/src/Model.js +236 -51
- package/src/View.js +95 -53
- 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,19 +2,12 @@
|
|
|
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.
|
|
8
9
|
*/
|
|
9
|
-
const viewOptions =
|
|
10
|
-
el : true,
|
|
11
|
-
tag : true,
|
|
12
|
-
attributes : true,
|
|
13
|
-
events : true,
|
|
14
|
-
model : true,
|
|
15
|
-
template : true,
|
|
16
|
-
onDestroy : true
|
|
17
|
-
};
|
|
10
|
+
const viewOptions = ['el', 'tag', 'attributes', 'events', 'model', 'template', 'onDestroy'];
|
|
18
11
|
|
|
19
12
|
/**
|
|
20
13
|
* - Listens for changes and renders the UI.
|
|
@@ -31,14 +24,15 @@ const viewOptions = {
|
|
|
31
24
|
* @module
|
|
32
25
|
* @extends Emitter
|
|
33
26
|
* @param {object} options Object containing options. The following keys will be merged into the view instance: `el`, `tag`, `attributes`, `events`, `model`, `template`, `onDestroy`.
|
|
34
|
-
* @property {node|
|
|
35
|
-
* @property {string|
|
|
36
|
-
* @property {object|
|
|
37
|
-
* @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}.
|
|
38
31
|
* @property {object} model A model or any object containing data and business logic.
|
|
39
|
-
* @property {
|
|
32
|
+
* @property {Function} template A function that returns a string with the view's inner HTML. See {@link module_view__render View.render}.
|
|
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.
|
|
40
34
|
* @example
|
|
41
|
-
* import { View } from 'rasti';
|
|
35
|
+
* import { View, Model } from 'rasti';
|
|
42
36
|
*
|
|
43
37
|
* class Timer extends View {
|
|
44
38
|
* constructor(options) {
|
|
@@ -63,9 +57,6 @@ class View extends Emitter {
|
|
|
63
57
|
super();
|
|
64
58
|
// Call preinitialize.
|
|
65
59
|
this.preinitialize.apply(this, arguments);
|
|
66
|
-
// Generate unique id.
|
|
67
|
-
// Useful to generate element ids.
|
|
68
|
-
this.uid = `uid${++View.uid}`;
|
|
69
60
|
// Store delegated event listeners,
|
|
70
61
|
// so they can be unbound later.
|
|
71
62
|
this.delegatedEventListeners = [];
|
|
@@ -74,17 +65,23 @@ class View extends Emitter {
|
|
|
74
65
|
this.children = [];
|
|
75
66
|
// Mutable array to store handlers to be called on destroy.
|
|
76
67
|
this.destroyQueue = [];
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
68
|
+
this.viewOptions = [];
|
|
69
|
+
// Extend "this" with options.
|
|
70
|
+
viewOptions.forEach(key => {
|
|
71
|
+
if (key in options) {
|
|
72
|
+
this[key] = options[key];
|
|
73
|
+
this.viewOptions.push(key);
|
|
74
|
+
}
|
|
80
75
|
});
|
|
76
|
+
// Ensure that the view has a unique id at `this.uid`.
|
|
77
|
+
this.ensureUid();
|
|
81
78
|
// Ensure that the view has a root element at `this.el`.
|
|
82
79
|
this.ensureElement();
|
|
83
80
|
}
|
|
84
81
|
|
|
85
82
|
/**
|
|
86
83
|
* If you define a preinitialize method, it will be invoked when the view is first created, before any instantiation logic is run.
|
|
87
|
-
* @param {object}
|
|
84
|
+
* @param {object} options The view options.
|
|
88
85
|
*/
|
|
89
86
|
preinitialize() {}
|
|
90
87
|
|
|
@@ -119,6 +116,8 @@ class View extends Emitter {
|
|
|
119
116
|
this.destroyChildren();
|
|
120
117
|
// Undelegate `this.el` event listeners
|
|
121
118
|
this.undelegateEvents();
|
|
119
|
+
// Stop listening to events.
|
|
120
|
+
this.stopListening();
|
|
122
121
|
// Unbind `this` events.
|
|
123
122
|
this.off();
|
|
124
123
|
// Call destroy queue.
|
|
@@ -126,6 +125,8 @@ class View extends Emitter {
|
|
|
126
125
|
this.destroyQueue = [];
|
|
127
126
|
// Call onDestroy lifecycle method
|
|
128
127
|
this.onDestroy.apply(this, arguments);
|
|
128
|
+
// Set destroyed flag.
|
|
129
|
+
this.destroyed = true;
|
|
129
130
|
// Return `this` for chaining.
|
|
130
131
|
return this;
|
|
131
132
|
}
|
|
@@ -157,6 +158,13 @@ class View extends Emitter {
|
|
|
157
158
|
this.children = [];
|
|
158
159
|
}
|
|
159
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
|
+
|
|
160
168
|
/**
|
|
161
169
|
* Ensure that the view has a root element at `this.el`.
|
|
162
170
|
* You shouldn't call this method directly. It's called from the constructor.
|
|
@@ -223,26 +231,43 @@ class View extends Emitter {
|
|
|
223
231
|
* All attached listeners are bound to the view, ensuring that `this` refers to the view object when the listeners are invoked.
|
|
224
232
|
* When `delegateEvents` is called again, possibly with a different events object, all previous listeners are removed and delegated afresh.
|
|
225
233
|
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
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
|
+
*
|
|
228
242
|
* @param {object} [events] Object in the format `{'event selector' : 'listener'}`. Used to bind delegated event listeners to the root element.
|
|
229
243
|
* @return {Rasti.View} Returns `this` for chaining.
|
|
230
244
|
* @example
|
|
231
|
-
* // Using
|
|
245
|
+
* // Using prototype (recommended for static events)
|
|
232
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 {
|
|
233
264
|
* events() {
|
|
234
265
|
* return {
|
|
235
|
-
*
|
|
236
|
-
* 'click
|
|
266
|
+
* [`click .${this.model.buttonClass}`]: 'onButtonClick',
|
|
267
|
+
* 'click': 'onRootClick'
|
|
237
268
|
* };
|
|
238
269
|
* }
|
|
239
270
|
* }
|
|
240
|
-
*
|
|
241
|
-
* // Using an object.
|
|
242
|
-
* Modal.prototype.events = {
|
|
243
|
-
* 'click button.ok' : 'onClickOkButton',
|
|
244
|
-
* 'click button.cancel' : function() {}
|
|
245
|
-
* };
|
|
246
271
|
*/
|
|
247
272
|
delegateEvents(events) {
|
|
248
273
|
if (!events) events = utils_getResult(this.events, this);
|
|
@@ -259,13 +284,10 @@ class View extends Emitter {
|
|
|
259
284
|
const selector = keyParts.join(' ');
|
|
260
285
|
|
|
261
286
|
let listener = events[key];
|
|
262
|
-
// Listener may be a string representing a method name on the view,
|
|
263
|
-
|
|
264
|
-
listener
|
|
265
|
-
|
|
266
|
-
this[listener] :
|
|
267
|
-
listener
|
|
268
|
-
).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);
|
|
269
291
|
|
|
270
292
|
if (!eventTypes[type]) eventTypes[type] = [];
|
|
271
293
|
|
|
@@ -277,7 +299,20 @@ class View extends Emitter {
|
|
|
277
299
|
const typeListener = (event) => {
|
|
278
300
|
// Iterate and run every individual listener if the selector matches.
|
|
279
301
|
eventTypes[type].forEach(({ selector, listener }) => {
|
|
280
|
-
|
|
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
|
+
}
|
|
281
316
|
});
|
|
282
317
|
};
|
|
283
318
|
|
|
@@ -305,17 +340,16 @@ class View extends Emitter {
|
|
|
305
340
|
}
|
|
306
341
|
|
|
307
342
|
/**
|
|
308
|
-
* Renders the view.
|
|
343
|
+
* Renders the view.
|
|
309
344
|
* This method should be overridden with custom logic.
|
|
310
345
|
* The only convention is to manipulate the DOM within the scope of `this.el`,
|
|
311
|
-
* and to return `this` for chaining.
|
|
312
|
-
* If you add any child views, you should call `this.destroyChildren` before re-rendering.
|
|
313
|
-
* The default implementation
|
|
314
|
-
* of calling `this.template`, passing `this.model` as
|
|
315
|
-
* <br><br> ⚠ **Security Notice:** The default implementation utilizes `innerHTML
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
* static method to escape HTML entities in a string.
|
|
346
|
+
* and to return `this` for chaining.
|
|
347
|
+
* If you add any child views, you should call `this.destroyChildren` before re-rendering.
|
|
348
|
+
* The default implementation updates `this.el`'s innerHTML with the result
|
|
349
|
+
* of calling `this.template`, passing `this.model` as the argument.
|
|
350
|
+
* <br><br> ⚠ **Security Notice:** The default implementation utilizes `innerHTML`, which may introduce Cross-Site Scripting (XSS) risks.
|
|
351
|
+
* Ensure that any user-generated content is properly sanitized before inserting it into the DOM.
|
|
352
|
+
* You can use the {@link #module_view_sanitize View.sanitize} static method to escape HTML entities in a string.
|
|
319
353
|
* For best practices on secure data handling, refer to the
|
|
320
354
|
* [OWASP's XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html).<br><br>
|
|
321
355
|
* @return {Rasti.View} Returns `this` for chaining.
|
|
@@ -332,7 +366,7 @@ class View extends Emitter {
|
|
|
332
366
|
* Override this method to provide a custom escape function.
|
|
333
367
|
* This method is inherited by {@link #module_component Component} and used to escape template interpolations.
|
|
334
368
|
* @static
|
|
335
|
-
* @param {string}
|
|
369
|
+
* @param {string} value String to escape.
|
|
336
370
|
* @return {string} Escaped string.
|
|
337
371
|
*/
|
|
338
372
|
static sanitize(value) {
|
|
@@ -346,8 +380,16 @@ class View extends Emitter {
|
|
|
346
380
|
}
|
|
347
381
|
}
|
|
348
382
|
|
|
349
|
-
|
|
350
|
-
*
|
|
383
|
+
/**
|
|
384
|
+
* Counter for generating unique IDs for view instances.
|
|
385
|
+
* This is primarily used to assign unique identifiers to each view instance (`this.uid`), which can be helpful for tasks like
|
|
386
|
+
* generating element IDs.
|
|
387
|
+
* {@link #module_component Component}s use `this.uid` to generate data attributes for their elements, to be looked up on hydration.
|
|
388
|
+
* For server-side rendering, this counter should be reset to `0` on every request to ensure that the generated
|
|
389
|
+
* unique IDs match those on the client, enabling seamless hydration of components.
|
|
390
|
+
* @static
|
|
391
|
+
* @type {number}
|
|
392
|
+
* @default 0
|
|
351
393
|
*/
|
|
352
394
|
View.uid = 0;
|
|
353
395
|
|
|
@@ -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;
|