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