solarite 0.3.2 → 0.5.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/dist/Solarite-debug.js +2149 -2090
- package/dist/Solarite.js +1895 -1821
- package/dist/Solarite.min.js +2 -2
- package/package.json +12 -2
- package/readme.md +2 -2
- package/src/Globals.js +18 -24
- package/src/HtmlParser.js +1 -1
- package/src/MultiValueMap.js +3 -3
- package/src/NodeGroup.js +238 -483
- package/src/Path.js +212 -0
- package/src/PathToAttribValue.js +259 -0
- package/src/PathToAttribs.js +77 -0
- package/src/PathToComment.js +8 -0
- package/src/PathToComponent.js +169 -0
- package/src/PathToEvent.js +57 -0
- package/src/PathToNodes.js +566 -0
- package/src/RootNodeGroup.js +8 -0
- package/src/Shell.js +92 -83
- package/src/Solarite.d.ts +73 -29
- package/src/Solarite.js +133 -21
- package/src/Template.js +173 -42
- package/src/Util.js +74 -52
- package/src/assert.js +5 -5
- package/src/getArg.js +26 -24
- package/src/h.js +83 -128
- package/src/hash.js +11 -9
- package/src/toEl.js +83 -0
- package/src/udomdiff.js +0 -57
- package/src/watch.js +78 -81
- package/src/ExprPath.js +0 -1109
- package/src/createSolarite.js +0 -154
package/dist/Solarite.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
/*@__NO_SIDE_EFFECTS__*/
|
|
2
|
+
function assert(val) {
|
|
3
|
+
|
|
4
|
+
}
|
|
2
5
|
|
|
3
6
|
let lastObjectId = 1>>>0; // Is a 32-bit int faster to increment than JavaScript's Number, which is a 64-bit float?
|
|
4
7
|
let objectIds = new WeakMap();
|
|
@@ -11,8 +14,8 @@ function getObjectId(obj) {
|
|
|
11
14
|
// return obj.toString(); // This fails to detect when a function's bound variables changes.
|
|
12
15
|
|
|
13
16
|
let result = objectIds.get(obj);
|
|
14
|
-
if (
|
|
15
|
-
result = '
|
|
17
|
+
if (result===undefined) { // convert to string, store in result, then add 1 to lastObjectId.
|
|
18
|
+
result = '~@' + (lastObjectId++); // We use a unique, 2-byte prefix to ensure it doesn't collide w/ strings not from getObjectId()
|
|
16
19
|
objectIds.set(obj, result);
|
|
17
20
|
}
|
|
18
21
|
return result;
|
|
@@ -22,7 +25,8 @@ function getObjectId(obj) {
|
|
|
22
25
|
* Control how JSON.stringify() handles Nodes and Functions.
|
|
23
26
|
* Normally, we'd pass a replacer() function argument to JSON.stringify() to handle Nodes and Functions.
|
|
24
27
|
* But that makes JSON.stringify() take twice as long to run.
|
|
25
|
-
* Adding a toJSON method globally on these object prototypes doesn't incur that performance penalty.
|
|
28
|
+
* Adding a toJSON method globally on these object prototypes doesn't incur that performance penalty.
|
|
29
|
+
* TODO: This needs to be benchmarked again after the json rewrite in Chrome 138. */
|
|
26
30
|
let isHashing = true;
|
|
27
31
|
function toJSON() {
|
|
28
32
|
return isHashing ? getObjectId(this) : this
|
|
@@ -50,26 +54,27 @@ function getObjectHash(obj) {
|
|
|
50
54
|
// Sometimes these get unassigned by Chrome and Brave 119, as well as Firefox, seemingly randomly!
|
|
51
55
|
// The same tests sometimes pass, sometimes fail, even after browser and OS restarts.
|
|
52
56
|
// So we check the assignments on every run of getObjectHash()
|
|
57
|
+
// TODO: Cache references to Node.prototype and Function.prototype:
|
|
53
58
|
if (Node.prototype.toJSON !== toJSON) {
|
|
54
59
|
Node.prototype.toJSON = toJSON;
|
|
55
60
|
if (Function.prototype.toJSON !== toJSON) // Will it only unmap one but not the other?
|
|
56
61
|
Function.prototype.toJSON = toJSON;
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
let result;
|
|
60
64
|
isHashing = true;
|
|
61
65
|
try {
|
|
62
|
-
|
|
66
|
+
return JSON.stringify(obj);
|
|
63
67
|
}
|
|
64
68
|
catch(e) {
|
|
65
|
-
|
|
69
|
+
return getObjectHashCircular(obj);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
isHashing = false;
|
|
66
73
|
}
|
|
67
|
-
isHashing = false;
|
|
68
|
-
return result;
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
/**
|
|
72
|
-
* Slower hashing method that supports.
|
|
77
|
+
* Slower hashing method that supports circular references.
|
|
73
78
|
* @param obj
|
|
74
79
|
* @returns {string} */
|
|
75
80
|
function getObjectHashCircular(obj) {
|
|
@@ -95,23 +100,27 @@ var Globals;
|
|
|
95
100
|
function reset() {
|
|
96
101
|
Globals = {
|
|
97
102
|
|
|
98
|
-
/**
|
|
99
|
-
* Used by NodeGroup.applyComponentExprs() */
|
|
100
|
-
componentArgsHash: new WeakMap(),
|
|
101
|
-
|
|
102
103
|
/**
|
|
103
104
|
* Store which instances of Solarite have already been added to the DOM.
|
|
104
105
|
* @type {WeakSet<HTMLElement>} */
|
|
105
106
|
connected: new WeakSet(),
|
|
106
107
|
|
|
107
108
|
/**
|
|
108
|
-
*
|
|
109
|
-
* watch() then adds the
|
|
110
|
-
* @type {
|
|
111
|
-
|
|
109
|
+
* Path.applyExactNodes() sets this property when an expression is being accessed.
|
|
110
|
+
* watch() then adds the Path to the list of Paths that should be re-rendered when the value changes.
|
|
111
|
+
* @type {Path}*/
|
|
112
|
+
currentPath: null,
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Set by NodeGroup.instantiateComponent()
|
|
116
|
+
* Used by RootNodeGroup.getSlotChildren(). */
|
|
117
|
+
currentSlotChildren: null,
|
|
112
118
|
|
|
113
119
|
div: document.createElement("div"),
|
|
114
120
|
|
|
121
|
+
/** @type {HTMLDocument} The global document. */
|
|
122
|
+
doc: document,
|
|
123
|
+
|
|
115
124
|
/**
|
|
116
125
|
* @type {Record<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
|
|
117
126
|
elementClasses: {},
|
|
@@ -120,33 +129,25 @@ function reset() {
|
|
|
120
129
|
htmlProps: {},
|
|
121
130
|
|
|
122
131
|
/**
|
|
123
|
-
* Used by
|
|
132
|
+
* Used by Path.applyEventAttrib()
|
|
124
133
|
* @type {WeakMap<Node, Record<eventName:string, [original:function, bound:function, args:*[]]>>} */
|
|
125
134
|
nodeEvents: new WeakMap(),
|
|
126
135
|
|
|
127
136
|
/**
|
|
128
137
|
* Get the RootNodeGroup for an element.
|
|
129
138
|
* @type {WeakMap<HTMLElement, RootNodeGroup>} */
|
|
130
|
-
|
|
139
|
+
rootNodeGroups: new WeakMap(),
|
|
131
140
|
|
|
132
141
|
/**
|
|
133
|
-
* Used by
|
|
142
|
+
* Used by h() path 9. */
|
|
134
143
|
objToEl: new WeakMap(),
|
|
135
144
|
|
|
136
|
-
//pendingChildren: [],
|
|
137
|
-
|
|
138
|
-
|
|
139
145
|
/**
|
|
140
|
-
* Elements that have been rendered to by
|
|
146
|
+
* Elements that have been rendered to by h() at least once.
|
|
141
147
|
* This is used by the Solarite class to know when to call onFirstConnect()
|
|
142
148
|
* @type {WeakSet<HTMLElement>} */
|
|
143
149
|
rendered: new WeakSet(),
|
|
144
150
|
|
|
145
|
-
/**
|
|
146
|
-
* Elements that are currently rendering via the r() function.
|
|
147
|
-
* @type {WeakSet<HTMLElement>} */
|
|
148
|
-
rendering: new WeakSet(),
|
|
149
|
-
|
|
150
151
|
/**
|
|
151
152
|
* Map from array of Html strings to a Shell created from them.
|
|
152
153
|
* @type {WeakMap<string[], Shell>} */
|
|
@@ -155,13 +156,11 @@ function reset() {
|
|
|
155
156
|
/**
|
|
156
157
|
* A map of individual untagged strings to their Templates.
|
|
157
158
|
* This way we don't keep creating new Templates for the same string when re-rendering.
|
|
158
|
-
* This is used by
|
|
159
|
+
* This is used by Path.applyExactNodes()
|
|
159
160
|
* @type {Record<string, Template>} */
|
|
160
161
|
//stringTemplates: {},
|
|
161
162
|
|
|
162
|
-
reset
|
|
163
|
-
|
|
164
|
-
count: 0
|
|
163
|
+
reset
|
|
165
164
|
};
|
|
166
165
|
}
|
|
167
166
|
reset();
|
|
@@ -230,12 +229,27 @@ let Util = {
|
|
|
230
229
|
return true; // the same.
|
|
231
230
|
},
|
|
232
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Convert HTMLElement attributes to an object.
|
|
234
|
+
* Converts dash (kebob-case) attribute names to camelCase.
|
|
235
|
+
* See also Solarite.getAttribs()
|
|
236
|
+
* @param el {HTMLElement}
|
|
237
|
+
* @param ignore {?string} Optionally ignore this attribute.
|
|
238
|
+
* @return {Object} */
|
|
239
|
+
attribsToObject(el, ignore=null) {
|
|
240
|
+
let result = {};
|
|
241
|
+
for (let attrib of el.attributes)
|
|
242
|
+
if (attrib.name !== ignore)
|
|
243
|
+
result[Util.dashesToCamel(attrib.name)] = attrib.value;
|
|
244
|
+
return result;
|
|
245
|
+
},
|
|
246
|
+
|
|
233
247
|
bindId(root, el) {
|
|
234
248
|
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
235
249
|
if (id) { // If something hasn't removed the id.
|
|
236
250
|
|
|
237
251
|
// Don't allow overwriting existing class properties if they already have a non-Node value.
|
|
238
|
-
if (root[id] && !(root[id]
|
|
252
|
+
if (root[id] && !(root[id]?.nodeType))
|
|
239
253
|
throw new Error(`${root.constructor.name}.${id} already has a value. ` +
|
|
240
254
|
`Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
|
|
241
255
|
|
|
@@ -244,26 +258,50 @@ let Util = {
|
|
|
244
258
|
},
|
|
245
259
|
|
|
246
260
|
/**
|
|
261
|
+
* If the style tab has a global attribute:
|
|
262
|
+
* 1. Put it in the document head as <style data-style="tag-name">...</style>
|
|
263
|
+
* 2. Replace the :host {...} CSS selector as tag-name {...}.
|
|
264
|
+
* Otherwise keep it where it is and:
|
|
265
|
+
* 1. Add data-style="1" attribute to the root element.
|
|
266
|
+
* 2. Replace the :host {...} selector in the style as tag-name[data-style='1'] {...}
|
|
247
267
|
* @param style {HTMLStyleElement}
|
|
248
268
|
* @param root {HTMLElement} */
|
|
249
269
|
bindStyles(style, root) {
|
|
250
|
-
let styleId = root.getAttribute('data-style');
|
|
251
|
-
if (!styleId) {
|
|
252
|
-
// Keep track of one style id for each class.
|
|
253
|
-
// TODO: Put this outside the class in a map, so it doesn't conflict with static properties.
|
|
254
|
-
if (!root.constructor.styleId)
|
|
255
|
-
root.constructor.styleId = 1;
|
|
256
|
-
styleId = root.constructor.styleId++;
|
|
257
270
|
|
|
258
|
-
|
|
271
|
+
let tagName = root.tagName.toLowerCase();
|
|
272
|
+
let styleId, attribSelector;
|
|
273
|
+
|
|
274
|
+
if (style.hasAttribute('global') || style.hasAttribute('data-global')) {
|
|
275
|
+
styleId = tagName;
|
|
276
|
+
attribSelector = '';
|
|
277
|
+
let doc = Globals$1.doc || root.ownerDocument || document;
|
|
278
|
+
if (!doc.head.querySelector(`style[data-style="${styleId}"]`)) {
|
|
279
|
+
doc.head.append(style);
|
|
280
|
+
style.setAttribute('data-style', styleId);
|
|
281
|
+
}
|
|
282
|
+
else // TODO: Make sure the style has no expressions.
|
|
283
|
+
style.remove(); // already in the head.
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
let styleId = root.getAttribute('data-style');
|
|
287
|
+
if (!styleId) {
|
|
288
|
+
// Keep track of one style id for each class.
|
|
289
|
+
// TODO: Put this outside the class in a map, so it doesn't conflict with static properties.
|
|
290
|
+
if (!root.constructor.styleId)
|
|
291
|
+
root.constructor.styleId = 1;
|
|
292
|
+
styleId = root.constructor.styleId++;
|
|
293
|
+
|
|
294
|
+
root.setAttribute('data-style', styleId);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
attribSelector = `[data-style="${styleId}"]`;
|
|
259
298
|
}
|
|
260
299
|
|
|
261
300
|
// Replace ":host" with "tagName[data-style=...]" in the css.
|
|
262
|
-
let tagName = root.tagName.toLowerCase();
|
|
263
301
|
for (let child of style.childNodes) {
|
|
264
302
|
if (child.nodeType === 3) {
|
|
265
303
|
let oldText = child.textContent;
|
|
266
|
-
let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, `${tagName}
|
|
304
|
+
let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, `${tagName}${attribSelector}`);
|
|
267
305
|
if (oldText !== newText)
|
|
268
306
|
child.textContent = newText;
|
|
269
307
|
}
|
|
@@ -310,48 +348,18 @@ let Util = {
|
|
|
310
348
|
return str.replace(/-([a-z])/g, g => g[1].toUpperCase());
|
|
311
349
|
},
|
|
312
350
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
* - Otherwise it yields the value as is.
|
|
322
|
-
*
|
|
323
|
-
* This function does not create a new array for the flattened values. Instead,
|
|
324
|
-
* it lazily yields each item as it is encountered. This can be more memory-efficient
|
|
325
|
-
* for large or deeply nested structures.
|
|
326
|
-
*
|
|
327
|
-
* @param {any} value - The value to flatten. Can be an array, object, function, or primitive.
|
|
328
|
-
* @yields {any} - The next item in the flattened structure.
|
|
329
|
-
*
|
|
330
|
-
* @example
|
|
331
|
-
* const complexArray = [
|
|
332
|
-
* 1,
|
|
333
|
-
* [2, () => 3, [4, () => [5, 6]], { a: 'object' }],
|
|
334
|
-
* () => () => 7,
|
|
335
|
-
* () => [() => 8, 9],
|
|
336
|
-
* ]; *
|
|
337
|
-
* for (const item of flatten(complexArray))
|
|
338
|
-
* console.log(item); // Outputs: 1, 2, 3, 4, 5, 6, { a: 'object' }, 7, 8, 9
|
|
339
|
-
*/
|
|
340
|
-
// *flatten(value) {
|
|
341
|
-
// if (Array.isArray(value)) {
|
|
342
|
-
// for (const item of value) {
|
|
343
|
-
// yield* Util.flatten(item); // Recursively flatten arrays
|
|
344
|
-
// }
|
|
345
|
-
// } else if (typeof value === 'function') {
|
|
346
|
-
// const result = value();
|
|
347
|
-
// yield* Util.flatten(result); // Recursively flatten the result of a function
|
|
348
|
-
// } else
|
|
349
|
-
// yield value; // Yield primitive values as is
|
|
350
|
-
// },
|
|
351
|
+
defineClass(Class, tagName) {
|
|
352
|
+
if (!customElements[getName](Class)) { // If not previously defined.
|
|
353
|
+
tagName = tagName || Util.camelToDashes(Class.name);
|
|
354
|
+
if (!tagName.includes('-')) // Browsers require that web components always have a dash in the name.
|
|
355
|
+
tagName += '-element';
|
|
356
|
+
customElements[define](tagName, Class);
|
|
357
|
+
}
|
|
358
|
+
},
|
|
351
359
|
|
|
352
360
|
/**
|
|
353
361
|
* Get the value of an input as the most appropriate JavaScript type.
|
|
354
|
-
* @param node {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|
|
|
362
|
+
* @param node {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|HTMLElement}
|
|
355
363
|
* @return {string|string[]|number|[]|File[]|Date|boolean} */
|
|
356
364
|
getInputValue(node) {
|
|
357
365
|
// .type is a built-in DOM property
|
|
@@ -365,6 +373,8 @@ let Util = {
|
|
|
365
373
|
return node.valueAsDate; // Date Object
|
|
366
374
|
if (node.type === 'select-multiple') // <select multiple>
|
|
367
375
|
return [...node.selectedOptions].map(option => option.value); // Array of Strings
|
|
376
|
+
if (node.hasAttribute('contenteditable'))
|
|
377
|
+
return node.innerHTML;
|
|
368
378
|
|
|
369
379
|
return node.value; // String
|
|
370
380
|
},
|
|
@@ -435,7 +445,7 @@ let Util = {
|
|
|
435
445
|
|
|
436
446
|
/**
|
|
437
447
|
* Use an array as the value of a map, appending to it when we add.
|
|
438
|
-
* Used by watch.js.
|
|
448
|
+
* Used only by watch.js.
|
|
439
449
|
* @param map {Map|WeakMap|Object}
|
|
440
450
|
* @param key
|
|
441
451
|
* @param value */
|
|
@@ -449,6 +459,11 @@ let Util = {
|
|
|
449
459
|
result.push(value);
|
|
450
460
|
},
|
|
451
461
|
|
|
462
|
+
saveOrphans(nodes) {
|
|
463
|
+
let fragment = Globals$1.doc.createDocumentFragment();
|
|
464
|
+
fragment.append(...nodes);
|
|
465
|
+
},
|
|
466
|
+
|
|
452
467
|
/**
|
|
453
468
|
* Remove nodes from the beginning and end that are not:
|
|
454
469
|
* 1. Elements.
|
|
@@ -477,168 +492,699 @@ let Util = {
|
|
|
477
492
|
|
|
478
493
|
|
|
479
494
|
|
|
495
|
+
// Trick to prevent minifier from renaming these methods.
|
|
496
|
+
let define = 'define';
|
|
497
|
+
let getName = 'getName';
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
|
|
480
501
|
// For debugging only
|
|
481
502
|
|
|
482
503
|
|
|
483
|
-
|
|
504
|
+
/**
|
|
505
|
+
* Path to where an expression should be evaluated within a Shell or NodeGroup. */
|
|
506
|
+
class Path {
|
|
484
507
|
|
|
485
|
-
|
|
486
|
-
data = {};
|
|
508
|
+
// Used for attributes:
|
|
487
509
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
}
|
|
496
|
-
set.add(value);
|
|
497
|
-
}
|
|
510
|
+
/**
|
|
511
|
+
* @type {Node} Node that occurs before this Path's first Node.
|
|
512
|
+
* This is necessary because udomdiff() can steal nodes from another Path.
|
|
513
|
+
* If we had a pointer to our own startNode then that node could be moved somewhere else w/o us knowing it.
|
|
514
|
+
* Used only for type='content'
|
|
515
|
+
* Will be null if Path has no Nodes. */
|
|
516
|
+
nodeBefore;
|
|
498
517
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
518
|
+
/**
|
|
519
|
+
* If type is AttribType.Multiple or AttribType.Value, points to the node having the attribute.
|
|
520
|
+
* If type is 'content', points to a node that never changes that this NodeGroup should always insert its nodes before.
|
|
521
|
+
* An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.
|
|
522
|
+
* @type {Node|HTMLElement} */
|
|
523
|
+
nodeMarker;
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
// These are set after an expression is assigned:
|
|
527
|
+
|
|
528
|
+
/** @type {NodeGroup} */
|
|
529
|
+
parentNg;
|
|
530
|
+
|
|
531
|
+
/** @type {NodeGroup[]} */
|
|
532
|
+
nodeGroups = [];
|
|
533
|
+
|
|
534
|
+
// Caches to make things faster
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* @private
|
|
538
|
+
* @type {Node[]} Cached result of getNodes() */
|
|
539
|
+
nodesCache;
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* @type {int} Index of nodeBefore among its parentNode's children. */
|
|
543
|
+
nodeBeforeIndex;
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* @type {int[]} Path to the node marker, in reverse for performance reasons. */
|
|
547
|
+
nodeMarkerPath;
|
|
548
|
+
|
|
549
|
+
/** @type {?function} A function called by renderWatched() to update the value of this expression. */
|
|
550
|
+
watchFunction
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* @param nodeBefore {Node}
|
|
555
|
+
* @param nodeMarker {?Node}*/
|
|
556
|
+
constructor(nodeBefore, nodeMarker) {
|
|
557
|
+
this.nodeBefore = nodeBefore;
|
|
558
|
+
this.nodeMarker = nodeMarker;
|
|
559
|
+
|
|
503
560
|
}
|
|
504
561
|
|
|
505
562
|
/**
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
|
|
510
|
-
|
|
563
|
+
* Apply expressions to a path.
|
|
564
|
+
* This is called by NodeGroup.applyExprs() when it's time to put the expression values into the DOM.
|
|
565
|
+
*
|
|
566
|
+
* @param exprs {Expr[]}
|
|
567
|
+
* Suppose we have the following tagged template:
|
|
568
|
+
* `<div title=${expr1} class="big ${expr2} muted ${expr3}">
|
|
569
|
+
* ${expr4}
|
|
570
|
+
* <my-component></my-component>
|
|
571
|
+
* <my-component user=${expr5} roles="${expr6},${expr7}"></my-component>
|
|
572
|
+
* </div>`
|
|
573
|
+
* The exprs arrays will look like this, with each being passed to a path.
|
|
574
|
+
* [expr1] // title attribute value.
|
|
575
|
+
* [expr2, expr3] // class attribute values.
|
|
576
|
+
* [expr4] // children of div.
|
|
577
|
+
* [] // arguments to first my-component constructor.
|
|
578
|
+
* [[expr5], [expr6, expr7]] // arguments to second my-component constructor.
|
|
579
|
+
* [expr5] // user attribute value.
|
|
580
|
+
* [expr6, expr7] // role attribute value.
|
|
581
|
+
* @param freeNodeGroups {boolean} Used only by watch. */
|
|
582
|
+
apply(exprs, freeNodeGroups=true) {}
|
|
583
|
+
|
|
584
|
+
getExpressionCount() { return 1 }
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Resolve nodeMarkerPath to new root.
|
|
589
|
+
* TODO: Make clone() use this.*/
|
|
590
|
+
getNewNodeMarker(newRoot, pathOffset) {
|
|
591
|
+
let root = newRoot;
|
|
592
|
+
let path = this.nodeMarkerPath;
|
|
593
|
+
let pathLength = path.length - pathOffset;
|
|
594
|
+
for (let i=pathLength-1; i>0; i--) { // Resolve the path.
|
|
595
|
+
|
|
596
|
+
root = root.childNodes[path[i]];
|
|
597
|
+
}
|
|
598
|
+
let childNodes = root.childNodes;
|
|
599
|
+
|
|
600
|
+
return pathLength
|
|
601
|
+
? childNodes[path[0]]
|
|
602
|
+
: newRoot;
|
|
511
603
|
}
|
|
512
604
|
|
|
605
|
+
|
|
513
606
|
/**
|
|
514
|
-
*
|
|
515
|
-
* @param
|
|
516
|
-
* @
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
let data = this.data;
|
|
520
|
-
let result;
|
|
521
|
-
let set = data[key];
|
|
522
|
-
if (!set)
|
|
523
|
-
return undefined;
|
|
607
|
+
* @param newRoot {HTMLElement}
|
|
608
|
+
* @param pathOffset {int}
|
|
609
|
+
* @return {Path} */
|
|
610
|
+
clone(newRoot, pathOffset=0) {
|
|
611
|
+
|
|
524
612
|
|
|
525
|
-
//
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
613
|
+
// Resolve node paths.
|
|
614
|
+
let nodeMarker, nodeBefore;
|
|
615
|
+
let root = newRoot;
|
|
616
|
+
let path = this.nodeMarkerPath;
|
|
617
|
+
let pathLength = path.length - pathOffset;
|
|
618
|
+
for (let i=pathLength-1; i>0; i--) { // Resolve the path.
|
|
619
|
+
|
|
620
|
+
root = root.childNodes[path[i]];
|
|
529
621
|
}
|
|
622
|
+
let childNodes = root.childNodes;
|
|
530
623
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
624
|
+
nodeMarker = pathLength
|
|
625
|
+
? childNodes[path[0]]
|
|
626
|
+
: newRoot;
|
|
627
|
+
if (this.nodeBefore) {
|
|
628
|
+
|
|
629
|
+
nodeBefore = childNodes[this.nodeBeforeIndex];
|
|
536
630
|
|
|
537
|
-
|
|
538
|
-
delete data[key];
|
|
631
|
+
}
|
|
539
632
|
|
|
540
|
-
|
|
541
|
-
}
|
|
633
|
+
let result = new this.constructor(nodeBefore, nodeMarker, this.attrName, this.attrValue);
|
|
542
634
|
|
|
543
|
-
|
|
544
|
-
* Remove one value from a key, and return it.
|
|
545
|
-
* @param key {string}
|
|
546
|
-
* @returns {*|undefined} The deleted item. */
|
|
547
|
-
deleteAny(key) {
|
|
548
|
-
let data = this.data;
|
|
549
|
-
let result;
|
|
550
|
-
let set = data[key];
|
|
551
|
-
if (!set) // slower than pre-check.
|
|
552
|
-
return undefined;
|
|
635
|
+
result.isComponentAttrib = this.isComponentAttrib;
|
|
553
636
|
|
|
554
|
-
|
|
555
|
-
|
|
637
|
+
// TODO: Put this in PathToAttribValue.clone().
|
|
638
|
+
result.isHtmlProperty = this.isHtmlProperty;
|
|
556
639
|
|
|
557
|
-
|
|
558
|
-
delete data[key];
|
|
640
|
+
|
|
559
641
|
|
|
560
642
|
return result;
|
|
561
643
|
}
|
|
562
644
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
if (!set)
|
|
568
|
-
return undefined;
|
|
569
|
-
|
|
570
|
-
set.delete(val);
|
|
571
|
-
result = val;
|
|
572
|
-
|
|
573
|
-
if (set.size === 0)
|
|
574
|
-
delete data[key];
|
|
645
|
+
// Only used for watch.js
|
|
646
|
+
getNodes() {
|
|
647
|
+
return [this.nodeMarker];
|
|
648
|
+
}
|
|
575
649
|
|
|
650
|
+
/** @return {int[]} Returns indices in reverse order, because doing it that way is faster. */
|
|
651
|
+
static get(node) {
|
|
652
|
+
let result = [];
|
|
653
|
+
while(true) {
|
|
654
|
+
let parent = node.parentNode;
|
|
655
|
+
if (!parent)
|
|
656
|
+
break;
|
|
657
|
+
result.push(Array.prototype.indexOf.call(node.parentNode.childNodes, node));
|
|
658
|
+
node = parent;
|
|
659
|
+
}
|
|
576
660
|
return result;
|
|
577
661
|
}
|
|
578
662
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
663
|
+
/**
|
|
664
|
+
* Note that the path is backward, with the outermost element at the end.
|
|
665
|
+
* @param root {HTMLElement|Document|DocumentFragment|ParentNode}
|
|
666
|
+
* @param path {int[]}
|
|
667
|
+
* @returns {Node|HTMLElement|HTMLStyleElement} */
|
|
668
|
+
static resolve(root, path) {
|
|
669
|
+
for (let i=path.length-1; i>=0; i--)
|
|
670
|
+
root = root.childNodes[path[i]];
|
|
671
|
+
return root;
|
|
586
672
|
}
|
|
673
|
+
|
|
674
|
+
|
|
587
675
|
}
|
|
588
676
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
600
|
-
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
601
|
-
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
602
|
-
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
|
603
|
-
* OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
604
|
-
* PERFORMANCE OF THIS SOFTWARE.
|
|
605
|
-
*/
|
|
677
|
+
class HtmlParser {
|
|
678
|
+
constructor() {
|
|
679
|
+
this.defaultState = {
|
|
680
|
+
context: HtmlParser.Text, // possible values: 'TEXT', 'TAG', 'ATTRIBUTE'
|
|
681
|
+
quote: null, // possible values: null, '"', "'"
|
|
682
|
+
buffer: '',
|
|
683
|
+
lastChar: null
|
|
684
|
+
};
|
|
685
|
+
this.state = {...this.defaultState};
|
|
686
|
+
}
|
|
606
687
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
* @param {(entry: Node, action: number) => Node} get
|
|
612
|
-
* The callback invoked per each entry related DOM operation.
|
|
613
|
-
* @param {Node} [before] The optional node used as anchor to insert before.
|
|
614
|
-
* @returns {Node[]} The same list of future children.
|
|
615
|
-
*/
|
|
616
|
-
const udomdiff = (parentNode, a, b, before) => {
|
|
617
|
-
|
|
688
|
+
reset() {
|
|
689
|
+
this.state = {...this.defaultState};
|
|
690
|
+
return this.state.context;
|
|
691
|
+
}
|
|
618
692
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
693
|
+
/**
|
|
694
|
+
* Parse the next chunk of html, starting with the same context we left off with from the previous chunk.
|
|
695
|
+
* @param html {string}
|
|
696
|
+
* @param onContextChange {?function(html:string, index:int, prevContext:string, nextContext:string)}
|
|
697
|
+
* Called every time the context changes, and again at the last context.
|
|
698
|
+
* @return {('Attribute','Text','Tag')} The context at the end of html. */
|
|
699
|
+
parse(html, onContextChange=null) {
|
|
700
|
+
if (html === null)
|
|
701
|
+
return this.reset();
|
|
702
|
+
|
|
703
|
+
for (let i = 0; i < html.length; i++) {
|
|
704
|
+
const char = html[i];
|
|
705
|
+
switch (this.state.context) {
|
|
706
|
+
case HtmlParser.Text:
|
|
707
|
+
if (char === '<' && html[i + 1].match(/[/a-z!]/i)) { // Start of a tag or comment.
|
|
708
|
+
onContextChange?.(html, i, this.state.context, HtmlParser.Tag);
|
|
709
|
+
this.state.context = HtmlParser.Tag;
|
|
710
|
+
this.state.buffer = '';
|
|
711
|
+
}
|
|
712
|
+
break;
|
|
713
|
+
case HtmlParser.Tag:
|
|
714
|
+
if (char === '>') {
|
|
715
|
+
onContextChange?.(html, i+1, this.state.context, HtmlParser.Text);
|
|
716
|
+
this.state.context = HtmlParser.Text;
|
|
717
|
+
this.state.quote = null;
|
|
718
|
+
this.state.buffer = '';
|
|
719
|
+
}
|
|
720
|
+
else if (char === ' ' && !this.state.buffer) {
|
|
721
|
+
// No attribute name is present. Skipping the space.
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
else if (char === ' ' || char === '/' || char === '?') {
|
|
725
|
+
this.state.buffer = ''; // Reset the buffer when a delimiter or potential self-closing sign is found.
|
|
726
|
+
}
|
|
727
|
+
else if (char === '"' || char === "'" || char === '=') {
|
|
728
|
+
onContextChange?.(html, i, this.state.context, HtmlParser.Attribute);
|
|
729
|
+
this.state.context = HtmlParser.Attribute;
|
|
730
|
+
this.state.quote = char === '=' ? null : char;
|
|
731
|
+
this.state.buffer = '';
|
|
732
|
+
}
|
|
733
|
+
else
|
|
734
|
+
this.state.buffer += char;
|
|
735
|
+
break;
|
|
736
|
+
case HtmlParser.Attribute:
|
|
737
|
+
// Start an attribute quote.
|
|
738
|
+
if (!this.state.quote && !this.state.buffer.length && (char === '"' || char === "'")) {
|
|
739
|
+
this.state.quote = char;
|
|
740
|
+
}
|
|
741
|
+
else if (char === this.state.quote || (!this.state.quote && this.state.buffer.length)) {
|
|
742
|
+
onContextChange?.(html, i, this.state.context, HtmlParser.Tag);
|
|
743
|
+
this.state.context = HtmlParser.Tag;
|
|
744
|
+
this.state.quote = null;
|
|
745
|
+
this.state.buffer = '';
|
|
746
|
+
}
|
|
747
|
+
else if (!this.state.quote && char === '>') {
|
|
748
|
+
onContextChange?.(html, i+1, this.state.context, HtmlParser.Text);
|
|
749
|
+
this.state.context = HtmlParser.Text;
|
|
750
|
+
this.state.quote = null;
|
|
751
|
+
this.state.buffer = '';
|
|
752
|
+
}
|
|
753
|
+
else if (char !== ' ')
|
|
754
|
+
this.state.buffer += char;
|
|
755
|
+
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
onContextChange?.(html, html.length, this.state.context, null);
|
|
760
|
+
return this.state.context;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
HtmlParser.Attribute = 'Attribute';
|
|
765
|
+
HtmlParser.Text = 'Text';
|
|
766
|
+
HtmlParser.Tag = 'Tag';
|
|
767
|
+
|
|
768
|
+
class PathToAttribValue extends Path {
|
|
769
|
+
|
|
770
|
+
/** @type {?string} Used only if type=AttribType.Value. */
|
|
771
|
+
attrName;
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* @type {?string[]} Used only if type=AttribType.Value. If null, use one expr to set the whole attribute value. */
|
|
775
|
+
attrValue;
|
|
776
|
+
|
|
777
|
+
isHtmlProperty;
|
|
778
|
+
|
|
779
|
+
constructor(nodeBefore, nodeMarker, attrName=null, attrValue=null) {
|
|
780
|
+
super(null, nodeMarker);
|
|
781
|
+
this.attrName = attrName;
|
|
782
|
+
this.attrValue = attrValue;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Set the value of an attribute. This can be for any attribute, not just attributes named "value".
|
|
787
|
+
* @param exprs {Expr[]} */
|
|
788
|
+
apply(exprs) {
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
let node = this.nodeMarker;
|
|
792
|
+
let expr = exprs[0];
|
|
793
|
+
|
|
794
|
+
let multiple = this.attrValue;
|
|
795
|
+
|
|
796
|
+
// Two-way binding between attributes
|
|
797
|
+
// Passing a path to the value attribute.
|
|
798
|
+
// Copies the attribute to the property when the input event fires.
|
|
799
|
+
// value=${[this, 'value]'}
|
|
800
|
+
// checked=${[this, 'isAgree']}
|
|
801
|
+
// This same logic is in NodeGroup.instantiateComponent() for components.
|
|
802
|
+
if (!multiple && Util.isPath(expr)) {
|
|
803
|
+
|
|
804
|
+
// Don't bind events to component placeholders.
|
|
805
|
+
// PathToComponent will do the binding later when it instantiates the component.
|
|
806
|
+
if (this.isComponentAttrib && node.tagName.endsWith('-SOLARITE-PLACEHOLDER'))
|
|
807
|
+
return;
|
|
808
|
+
|
|
809
|
+
/** @type {[Object, string[]]} */
|
|
810
|
+
let [obj, path] = [expr[0], expr.slice(1)];
|
|
811
|
+
|
|
812
|
+
if (!obj)
|
|
813
|
+
throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${[${expr.map(item => item ? `'${item}'` : item+'').join(', ')}]}>.`);
|
|
814
|
+
|
|
815
|
+
let value = delve(obj, path);
|
|
816
|
+
|
|
817
|
+
// Special case to allow setting select-multiple value from an array
|
|
818
|
+
if (this.attrName === 'value' && node.type === 'select-multiple' && Array.isArray(value)) {
|
|
819
|
+
// Set the .selected property on the options having a value within value.
|
|
820
|
+
let strValues = value.map(v => v + '');
|
|
821
|
+
for (let option of node.options)
|
|
822
|
+
option.selected = strValues.includes(option.value);
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
// TODO: should we remove isFalsy, since these are always props?
|
|
826
|
+
const strValue = Util.isFalsy(value) ? '' : value;
|
|
827
|
+
|
|
828
|
+
// Special case for contenteditable
|
|
829
|
+
if (this.attrName === 'value' && node.hasAttribute('contenteditable')) {
|
|
830
|
+
const existingValue = node.innerHTML;
|
|
831
|
+
if (strValue !== existingValue)
|
|
832
|
+
node.innerHTML = strValue;
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
835
|
+
|
|
836
|
+
// If we don't have this condition, when we call render(), the browser will scroll to the currently
|
|
837
|
+
// selected item in a <select> and mess up manually scrolling to a different value.
|
|
838
|
+
if (strValue !== node[this.attrName])
|
|
839
|
+
node[this.attrName] = strValue;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// TODO: We need to remove any old listeners, like in bindEventAttribute.
|
|
844
|
+
// Does bindEvent() now handle that?
|
|
845
|
+
let func = () => {
|
|
846
|
+
let value = (this.attrName === 'value')
|
|
847
|
+
? Util.getInputValue(node)
|
|
848
|
+
: node[this.attrName];
|
|
849
|
+
delve(obj, path, value);
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
// We use capture so we update the values before other events added by the user.
|
|
853
|
+
// TODO: Bind to scroll events also?
|
|
854
|
+
// What about resize events and width/height?
|
|
855
|
+
this.bindEvent(node, this.parentNg.getRootNode(), this.attrName, 'input', func, [], true);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Regular attribute
|
|
859
|
+
else {
|
|
860
|
+
// Cache this on Path.isHtmlProperty when Shell creates the props.
|
|
861
|
+
// Have Path.clone() copy .isHtmlProperty?
|
|
862
|
+
let isProp = this.isHtmlProperty;
|
|
863
|
+
|
|
864
|
+
// Values to toggle an attribute
|
|
865
|
+
if (!multiple) {
|
|
866
|
+
Globals$1.currentPath = this; // Used by watch()
|
|
867
|
+
if (typeof expr === 'function') {
|
|
868
|
+
if (this.isComponentAttrib)
|
|
869
|
+
return;
|
|
870
|
+
|
|
871
|
+
this.watchFunction = expr; // The function that gets the expression, used for renderWatched()
|
|
872
|
+
expr = expr();
|
|
873
|
+
}
|
|
874
|
+
else
|
|
875
|
+
expr = Util.makePrimitive(expr);
|
|
876
|
+
Globals$1.currentPath = null;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
if (!multiple && (expr === undefined || expr === false || expr === null)) { // Util.isFalsy() inlined.
|
|
881
|
+
if (isProp)
|
|
882
|
+
node[this.attrName] = false;
|
|
883
|
+
node.removeAttribute(this.attrName);
|
|
884
|
+
}
|
|
885
|
+
else if (!multiple && expr === true) {
|
|
886
|
+
if (isProp)
|
|
887
|
+
node[this.attrName] = true;
|
|
888
|
+
node.setAttribute(this.attrName, '');
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// A non-toggled attribute
|
|
892
|
+
else {
|
|
893
|
+
|
|
894
|
+
// If it's a series of expressions among strings, join them together.
|
|
895
|
+
let joinedValue = multiple // avoid function call if there are no strings
|
|
896
|
+
? this.getValue(exprs)
|
|
897
|
+
: expr; // If the attribute is one expression with no strings
|
|
898
|
+
|
|
899
|
+
// Only update attributes if the value has changed.
|
|
900
|
+
// This is needed for setting input.value, .checked, option.selected, etc.
|
|
901
|
+
let oldVal = isProp
|
|
902
|
+
? node[this.attrName]
|
|
903
|
+
: node.getAttribute(this.attrName);
|
|
904
|
+
if (oldVal !== joinedValue) {
|
|
905
|
+
|
|
906
|
+
// <textarea value=${expr}></textarea>
|
|
907
|
+
// Without this branch we have no way to set the value of a textarea,
|
|
908
|
+
// since we also prohibit expressions that are a child of textarea.
|
|
909
|
+
if (isProp)
|
|
910
|
+
node[this.attrName] = joinedValue;
|
|
911
|
+
|
|
912
|
+
// Allow one-way binding to contenteditable value attribute.
|
|
913
|
+
// Contenteditables normally don't have a value attribute and have their content set via innerHTML.
|
|
914
|
+
// Solarite doesn't allow contenteditables to have expressions as their children.
|
|
915
|
+
else if (this.attrName === 'value' && node.hasAttribute('contenteditable')) {
|
|
916
|
+
node.innerHTML = joinedValue;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// TODO: Putting an 'else' here would be more performant
|
|
920
|
+
node.setAttribute(this.attrName, joinedValue);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
getExpressionCount() { return this.attrValue ? this.attrValue.length-1 : 1 }
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* @param exprs {Expr|Expr[]} // TODO: Why is this sometimes not an array?
|
|
931
|
+
* @return {string} The joined values of the expressions, or the first expression if there are no strings. */
|
|
932
|
+
getValue(exprs) {
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
//if (!Array.isArray(exprs))
|
|
936
|
+
// return exprs;
|
|
937
|
+
|
|
938
|
+
if (!this.attrValue) {// If it's not multiple paths inside a single attribute, return first (and only) expression.
|
|
939
|
+
|
|
940
|
+
return exprs[0];
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
let result = [];
|
|
944
|
+
let values = this.attrValue;
|
|
945
|
+
for (let i = 0; i < values.length; i++) {
|
|
946
|
+
result.push(values[i]);
|
|
947
|
+
if (i < values.length - 1) {
|
|
948
|
+
Globals$1.currentPath = this; // Used by watch()
|
|
949
|
+
let val = Util.makePrimitive(exprs[i]);
|
|
950
|
+
Globals$1.currentPath = null;
|
|
951
|
+
if (!Util.isFalsy(val))
|
|
952
|
+
result.push(val);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return result.join('')
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Call function when eventName is triggerd on node.
|
|
960
|
+
* @param node {HTMLElement}
|
|
961
|
+
* @param root {HTMLElement}
|
|
962
|
+
* @param key {string}
|
|
963
|
+
* @param eventName {string}
|
|
964
|
+
* @param func {function}
|
|
965
|
+
* @param args {array}
|
|
966
|
+
* @param capture {boolean} */
|
|
967
|
+
bindEvent(node, root, key, eventName, func, args, capture=false) {
|
|
968
|
+
let nodeEvents = Globals$1.nodeEvents.get(node);
|
|
969
|
+
if (!nodeEvents) {
|
|
970
|
+
nodeEvents = {[key]: new Array(3)};
|
|
971
|
+
Globals$1.nodeEvents.set(node, nodeEvents);
|
|
972
|
+
}
|
|
973
|
+
let nodeEvent = nodeEvents[key];
|
|
974
|
+
if (!nodeEvent)
|
|
975
|
+
nodeEvents[key] = nodeEvent = new Array(3);
|
|
976
|
+
|
|
977
|
+
if (typeof func !== 'function')
|
|
978
|
+
throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${${func}}> because it's not a function.`);
|
|
979
|
+
|
|
980
|
+
// If function has changed, remove and rebind the event.
|
|
981
|
+
if (nodeEvent[0] !== func) {
|
|
982
|
+
|
|
983
|
+
// TODO: We should be removing event listeners when calling getNodeGroup(),
|
|
984
|
+
// when we get the node from the list of nodeGroupsAttached/nodeGroupsDetached,
|
|
985
|
+
// instead of only when we rebind an event.
|
|
986
|
+
let [existing, existingBound, _] = nodeEvent;
|
|
987
|
+
if (existing)
|
|
988
|
+
node.removeEventListener(eventName, existingBound, capture);
|
|
989
|
+
|
|
990
|
+
let originalFunc = func;
|
|
991
|
+
|
|
992
|
+
// BoundFunc sets the "this" variable to be the current Solarite component.
|
|
993
|
+
let boundFunc = (event) => {
|
|
994
|
+
let args = nodeEvent[2];
|
|
995
|
+
return originalFunc.call(root, ...args, event, node);
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
// Save both the original and bound functions.
|
|
999
|
+
// Original so we can compare it against a newly assigned function.
|
|
1000
|
+
// Bound so we can use it with removeEventListner().
|
|
1001
|
+
nodeEvent[0] = originalFunc;
|
|
1002
|
+
nodeEvent[1] = boundFunc;
|
|
1003
|
+
|
|
1004
|
+
node.addEventListener(eventName, boundFunc, capture);
|
|
1005
|
+
|
|
1006
|
+
// TODO: classic event attribs?
|
|
1007
|
+
//el[attr.name] = e => // e.g. el.onclick = ... // put "event", "el", and "this" in scope for the event code.
|
|
1008
|
+
// (new Function('event', 'el', attr.value)).bind(this.manager.rootEl)(e, el)
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// Otherwise just update the args to the function.
|
|
1012
|
+
nodeEvents[key][2] = args;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// TODO: Merge this into PathToAttribValue?
|
|
1017
|
+
class PathToEvent extends PathToAttribValue {
|
|
1018
|
+
|
|
1019
|
+
constructor(nodeBefore, nodeMarker, attrName=null, attrValue=null) {
|
|
1020
|
+
super(null, nodeMarker, attrName, attrValue);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Handle attributes for event binding, such as:
|
|
1025
|
+
* onclick=${(e, el) => this.doSomething(el, 'meow')}
|
|
1026
|
+
* oninput=${[this.doSomething, 'meow']}
|
|
1027
|
+
* onclick=${[this, 'doSomething', 'meow']}
|
|
1028
|
+
*
|
|
1029
|
+
* @param exprs {Expr[]} Only the first is used.*/
|
|
1030
|
+
apply(exprs) {
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
// Don't bind events to component placeholders.
|
|
1034
|
+
// PathToComponent will do the binding later when it instantiates the component.
|
|
1035
|
+
if (this.isComponentAttrib && this.nodeMarker.tagName.endsWith('-SOLARITE-PLACEHOLDER'))
|
|
1036
|
+
return;
|
|
1037
|
+
|
|
1038
|
+
let expr = exprs[0];
|
|
1039
|
+
let root = this.parentNg.rootNg.root;
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
let node = this.nodeMarker;
|
|
1044
|
+
|
|
1045
|
+
let eventName = this.attrName.slice(2); // remove "on-" prefix.
|
|
1046
|
+
let func;
|
|
1047
|
+
let args = [];
|
|
1048
|
+
|
|
1049
|
+
// Convert array to function.
|
|
1050
|
+
// oninput=${[this.doSomething, 'meow']}
|
|
1051
|
+
if (Array.isArray(expr) && typeof expr[0] === 'function') {
|
|
1052
|
+
func = expr[0];
|
|
1053
|
+
args = expr.slice(1);
|
|
1054
|
+
}
|
|
1055
|
+
else if (typeof expr === 'function')
|
|
1056
|
+
func = expr;
|
|
1057
|
+
else
|
|
1058
|
+
throw new Error(`Invalid event binding: <${node.tagName.toLowerCase()} ${this.attrName}=\${${JSON.stringify(expr)}}>`);
|
|
1059
|
+
|
|
1060
|
+
this.bindEvent(node, root, eventName, eventName, func, args);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
class PathToAttribs extends Path {
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* @type {Set<string>} Used for type=AttribType.Multiple to remember the attributes that were added. */
|
|
1071
|
+
attrNames;
|
|
1072
|
+
|
|
1073
|
+
constructor(nodeBefore, nodeMarker) {
|
|
1074
|
+
super(null, null);
|
|
1075
|
+
this.nodeMarker = nodeMarker;
|
|
1076
|
+
this.attrNames = new Set();
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* @param exprs {Expr[][]} Only the first is used.
|
|
1081
|
+
* @param freeNodeGroups {boolean} Used only for watch. */
|
|
1082
|
+
apply(exprs, freeNodeGroups) {
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
let expr = exprs[0];
|
|
1086
|
+
let node = this.nodeMarker;
|
|
1087
|
+
|
|
1088
|
+
if (Array.isArray(expr))
|
|
1089
|
+
expr = expr.flat().join(' '); // flat and join so we can accept arrays of arrays of strings.
|
|
1090
|
+
|
|
1091
|
+
// Add new attributes
|
|
1092
|
+
let oldNames = this.attrNames;
|
|
1093
|
+
this.attrNames = new Set();
|
|
1094
|
+
if (expr) {
|
|
1095
|
+
if (typeof expr === 'function') {
|
|
1096
|
+
Globals$1.currentPath = this; // Used by watch()
|
|
1097
|
+
this.watchFunction = expr; // used by renderWatched()
|
|
1098
|
+
expr = expr();
|
|
1099
|
+
Globals$1.currentPath = null;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// Attribute as name: value object.
|
|
1103
|
+
if (typeof expr === 'object') {
|
|
1104
|
+
for (let name in expr) {
|
|
1105
|
+
let value = expr[name];
|
|
1106
|
+
if (value === undefined || value === false || value === null)
|
|
1107
|
+
continue;
|
|
1108
|
+
node.setAttribute(name, value);
|
|
1109
|
+
this.attrNames.add(name);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Attributes as string
|
|
1114
|
+
else {
|
|
1115
|
+
let attrs = (expr + '') // Split string into multiple attributes.
|
|
1116
|
+
.split(/([\w-]+\s*=\s*(?:"[^"]*"|'[^']*'|\S+))/g)
|
|
1117
|
+
.map(text => text.trim())
|
|
1118
|
+
.filter(text => text.length);
|
|
1119
|
+
|
|
1120
|
+
for (let attr of attrs) {
|
|
1121
|
+
let [name, value] = attr.split(/\s*=\s*/); // split on first equals.
|
|
1122
|
+
value = (value || '').replace(/^(['"])(.*)\1$/, '$2'); // trim value quotes if they match.
|
|
1123
|
+
node.setAttribute(name, value);
|
|
1124
|
+
this.attrNames.add(name);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Remove old attributes.
|
|
1130
|
+
for (let oldName of oldNames)
|
|
1131
|
+
if (!this.attrNames.has(oldName))
|
|
1132
|
+
node.removeAttribute(oldName);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
getExpressionCount() { return 1 }
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* ISC License
|
|
1141
|
+
*
|
|
1142
|
+
* Copyright (c) 2020, Andrea Giammarchi, @WebReflection
|
|
1143
|
+
*
|
|
1144
|
+
* Permission to use, copy, modify, and/or distribute this software for any
|
|
1145
|
+
* purpose with or without fee is hereby granted, provided that the above
|
|
1146
|
+
* copyright notice and this permission notice appear in all copies.
|
|
1147
|
+
*
|
|
1148
|
+
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
1149
|
+
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
1150
|
+
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
1151
|
+
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
1152
|
+
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
|
1153
|
+
* OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
1154
|
+
* PERFORMANCE OF THIS SOFTWARE.
|
|
1155
|
+
*/
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* @param {Node} parentNode The container where children live
|
|
1159
|
+
* @param {Node[]} a The list of current/live children
|
|
1160
|
+
* @param {Node[]} b The list of future children
|
|
1161
|
+
* @param {(entry: Node, action: number) => Node} get
|
|
1162
|
+
* The callback invoked per each entry related DOM operation.
|
|
1163
|
+
* @param {Node} [before] The optional node used as anchor to insert before.
|
|
1164
|
+
* @returns {Node[]} The same list of future children.
|
|
1165
|
+
*/
|
|
1166
|
+
const udomdiff = (parentNode, a, b, before) => {
|
|
1167
|
+
const bLength = b.length;
|
|
1168
|
+
let aEnd = a.length;
|
|
1169
|
+
let bEnd = bLength;
|
|
1170
|
+
let aStart = 0;
|
|
1171
|
+
let bStart = 0;
|
|
1172
|
+
let map = null;
|
|
1173
|
+
while (aStart < aEnd || bStart < bEnd) {
|
|
1174
|
+
// append head, tail, or nodes in between: fast path
|
|
1175
|
+
if (aEnd === aStart) {
|
|
1176
|
+
// we could be in a situation where the rest of nodes that
|
|
1177
|
+
// need to be added are not at the end, and in such case
|
|
1178
|
+
// the node to `insertBefore`, if the index is more than 0
|
|
1179
|
+
// must be retrieved, otherwise it's gonna be the first item.
|
|
1180
|
+
const node = bEnd < bLength
|
|
1181
|
+
? (bStart
|
|
1182
|
+
? (b[bStart - 1].nextSibling)
|
|
635
1183
|
: b[bEnd - bStart])
|
|
636
1184
|
: before;
|
|
637
1185
|
while (bStart < bEnd) {
|
|
638
1186
|
let bNode = b[bStart++];
|
|
639
1187
|
parentNode.insertBefore(bNode, node);
|
|
640
|
-
|
|
641
|
-
|
|
642
1188
|
}
|
|
643
1189
|
}
|
|
644
1190
|
// remove head or tail: fast path
|
|
@@ -648,8 +1194,6 @@ const udomdiff = (parentNode, a, b, before) => {
|
|
|
648
1194
|
let aNode = a[aStart];
|
|
649
1195
|
if (!map || !map.has(aNode)) {
|
|
650
1196
|
parentNode.removeChild(aNode);
|
|
651
|
-
|
|
652
|
-
|
|
653
1197
|
}
|
|
654
1198
|
aStart++;
|
|
655
1199
|
}
|
|
@@ -686,13 +1230,10 @@ const udomdiff = (parentNode, a, b, before) => {
|
|
|
686
1230
|
a2,
|
|
687
1231
|
b2.nextSibling
|
|
688
1232
|
);
|
|
689
|
-
|
|
690
1233
|
|
|
691
1234
|
let bNode = b[--bEnd];
|
|
692
1235
|
parentNode.insertBefore(bNode, node);
|
|
693
1236
|
|
|
694
|
-
|
|
695
|
-
|
|
696
1237
|
// mark the future index as identical (yeah, it's dirty, but cheap 👍)
|
|
697
1238
|
// The main reason to do this, is that when a[aEnd] will be reached,
|
|
698
1239
|
// the loop will likely be on the fast path, as identical to b[bEnd].
|
|
@@ -740,8 +1281,6 @@ const udomdiff = (parentNode, a, b, before) => {
|
|
|
740
1281
|
while (bStart < index) {
|
|
741
1282
|
let bNode = b[bStart++];
|
|
742
1283
|
parentNode.insertBefore(bNode, node);
|
|
743
|
-
|
|
744
|
-
|
|
745
1284
|
}
|
|
746
1285
|
}
|
|
747
1286
|
// if the effort wasn't good enough, fallback to a replace,
|
|
@@ -754,8 +1293,6 @@ const udomdiff = (parentNode, a, b, before) => {
|
|
|
754
1293
|
bNode,
|
|
755
1294
|
aNode
|
|
756
1295
|
);
|
|
757
|
-
|
|
758
|
-
|
|
759
1296
|
}
|
|
760
1297
|
}
|
|
761
1298
|
// otherwise move the source forward, 'cause there's nothing to do
|
|
@@ -768,153 +1305,165 @@ const udomdiff = (parentNode, a, b, before) => {
|
|
|
768
1305
|
else {
|
|
769
1306
|
let aNode = a[aStart++];
|
|
770
1307
|
parentNode.removeChild(aNode);
|
|
771
|
-
|
|
772
|
-
|
|
773
1308
|
}
|
|
774
1309
|
}
|
|
775
1310
|
}
|
|
776
|
-
return b;
|
|
777
|
-
};
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
1311
|
+
return b;
|
|
1312
|
+
};
|
|
1313
|
+
|
|
1314
|
+
class MultiValueMap {
|
|
1315
|
+
|
|
1316
|
+
/** @type {Record<string, Set>} */
|
|
1317
|
+
data = {};
|
|
1318
|
+
|
|
1319
|
+
// Set a new value for a key
|
|
1320
|
+
add(key, value) {
|
|
1321
|
+
let data = this.data;
|
|
1322
|
+
let set = data[key];
|
|
1323
|
+
if (!set) {
|
|
1324
|
+
set = new Set();
|
|
1325
|
+
data[key] = set;
|
|
1326
|
+
}
|
|
1327
|
+
set.add(value);
|
|
1328
|
+
}
|
|
781
1329
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1330
|
+
isEmpty() {
|
|
1331
|
+
for (let key in this.data)
|
|
1332
|
+
return true;
|
|
1333
|
+
return false;
|
|
1334
|
+
}
|
|
787
1335
|
|
|
788
|
-
|
|
1336
|
+
/**
|
|
1337
|
+
* Get all values for a key.
|
|
1338
|
+
* @param key {string}
|
|
1339
|
+
* @returns {Set|*[]} */
|
|
1340
|
+
getAll(key) {
|
|
1341
|
+
return this.data[key] || [];
|
|
1342
|
+
}
|
|
789
1343
|
|
|
790
1344
|
/**
|
|
791
|
-
*
|
|
792
|
-
|
|
1345
|
+
* Remove one value from a key, and return it.
|
|
1346
|
+
* @param key {string}
|
|
1347
|
+
* @param val If specified, make sure we delete this specific value, if a key exists more than once.
|
|
1348
|
+
* @returns {*|undefined} The deleted item. */
|
|
1349
|
+
delete(key, val=undefined) {
|
|
1350
|
+
let data = this.data;
|
|
1351
|
+
let result;
|
|
1352
|
+
let set = data[key];
|
|
1353
|
+
if (!set)
|
|
1354
|
+
return undefined;
|
|
793
1355
|
|
|
794
|
-
|
|
1356
|
+
// Delete any value.
|
|
1357
|
+
if (val === undefined) {
|
|
1358
|
+
[result] = set; // Get the first value from the set.
|
|
1359
|
+
set.delete(result);
|
|
1360
|
+
}
|
|
795
1361
|
|
|
796
|
-
|
|
797
|
-
|
|
1362
|
+
// Delete a specific value.
|
|
1363
|
+
else {
|
|
1364
|
+
set.delete(val);
|
|
1365
|
+
result = val;
|
|
1366
|
+
}
|
|
798
1367
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
attrValue;
|
|
1368
|
+
if (set.size === 0)
|
|
1369
|
+
delete data[key];
|
|
802
1370
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
attrNames;
|
|
1371
|
+
return result;
|
|
1372
|
+
}
|
|
806
1373
|
|
|
807
1374
|
/**
|
|
808
|
-
*
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1375
|
+
* Remove any one value from a key, and return it.
|
|
1376
|
+
* @param key {string}
|
|
1377
|
+
* @returns {*|undefined} The deleted item. */
|
|
1378
|
+
deleteAny(key) {
|
|
1379
|
+
let data = this.data;
|
|
1380
|
+
let result;
|
|
1381
|
+
let set = data[key];
|
|
1382
|
+
if (!set) // slower than pre-check.
|
|
1383
|
+
return undefined;
|
|
814
1384
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
* If type is 'content', points to a node that never changes that this NodeGroup should always insert its nodes before.
|
|
818
|
-
* An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.
|
|
819
|
-
* @type {Node|HTMLElement} */
|
|
820
|
-
nodeMarker;
|
|
1385
|
+
[result] = set; // Get the first value from the set.
|
|
1386
|
+
set.delete(result);
|
|
821
1387
|
|
|
1388
|
+
if (set.size === 0)
|
|
1389
|
+
delete data[key];
|
|
822
1390
|
|
|
823
|
-
|
|
1391
|
+
return result;
|
|
1392
|
+
}
|
|
824
1393
|
|
|
825
|
-
|
|
826
|
-
|
|
1394
|
+
deleteSpecific(key, val) {
|
|
1395
|
+
let data = this.data;
|
|
1396
|
+
let result;
|
|
1397
|
+
let set = data[key];
|
|
1398
|
+
if (!set)
|
|
1399
|
+
return undefined;
|
|
827
1400
|
|
|
828
|
-
|
|
829
|
-
|
|
1401
|
+
set.delete(val);
|
|
1402
|
+
result = val;
|
|
830
1403
|
|
|
1404
|
+
if (set.size === 0)
|
|
1405
|
+
delete data[key];
|
|
831
1406
|
|
|
832
|
-
|
|
1407
|
+
return result;
|
|
1408
|
+
}
|
|
833
1409
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1410
|
+
hasValue(val) {
|
|
1411
|
+
let data = this.data;
|
|
1412
|
+
let names = [];
|
|
1413
|
+
for (let name in data)
|
|
1414
|
+
if (data[name].has(val)) // TODO: iterate twice to pre-size array?
|
|
1415
|
+
names.push(name);
|
|
1416
|
+
return names;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
class PathToNodes extends Path {
|
|
838
1421
|
|
|
839
|
-
/**
|
|
840
|
-
* @type {int} Index of nodeBefore among its parentNode's children. */
|
|
841
|
-
nodeBeforeIndex;
|
|
842
1422
|
|
|
843
1423
|
/**
|
|
844
|
-
* @type {
|
|
845
|
-
|
|
1424
|
+
* @type {?function} The most recent callback passed to a .map() function in this Path. This is only used for watch.js
|
|
1425
|
+
* TODO: What if one Path has two .map() calls? Maybe we just won't support that. */
|
|
1426
|
+
mapCallback;
|
|
846
1427
|
|
|
847
1428
|
|
|
848
|
-
/** @type {?function} A function called by renderWatched() to update the value of this expression. */
|
|
849
|
-
watchFunction
|
|
850
1429
|
|
|
851
1430
|
/**
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1431
|
+
* TODO: Rename this to nodeGroupsInUse, nodeGroupsAvialableAttached and nodeGroupsAvailableDetached?
|
|
1432
|
+
* Nodes that have been used during the current render().
|
|
1433
|
+
* Used with getNodeGroup() and freeNodeGroups().
|
|
1434
|
+
* TODO: Use an array of WeakRef so the gc can collect them?
|
|
1435
|
+
* TODO: Put items back in nodeGroupsInUse after applyExpr() is called, not before.
|
|
1436
|
+
* @type {NodeGroup[]} */
|
|
1437
|
+
nodeGroupsRendered = [];
|
|
857
1438
|
|
|
858
1439
|
/**
|
|
859
|
-
*
|
|
860
|
-
*
|
|
861
|
-
*
|
|
862
|
-
* @
|
|
863
|
-
|
|
864
|
-
constructor(nodeBefore, nodeMarker, type=ExprPathType.Content, attrName=null, attrValue=null) {
|
|
865
|
-
|
|
866
|
-
// If path is a node.
|
|
867
|
-
this.nodeBefore = nodeBefore;
|
|
868
|
-
this.nodeMarker = nodeMarker;
|
|
869
|
-
this.type = type;
|
|
870
|
-
this.attrName = attrName;
|
|
871
|
-
this.attrValue = attrValue;
|
|
872
|
-
if (type === ExprPathType.AttribMultiple)
|
|
873
|
-
this.attrNames = new Set();
|
|
874
|
-
}
|
|
1440
|
+
* Nodes that were added to the web component during the last render(), but are available to be used again.
|
|
1441
|
+
* Used with getNodeGroup() and freeNodeGroups().
|
|
1442
|
+
* Each NodeGroup is here twice, once under an exact key, and once under the close key.
|
|
1443
|
+
* @type {MultiValueMap<key:string, value:NodeGroup>} */
|
|
1444
|
+
nodeGroupsAttachedAvailable = new MultiValueMap();
|
|
875
1445
|
|
|
876
1446
|
/**
|
|
877
|
-
*
|
|
878
|
-
*
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
* We should modify path.applyValueAttrib so it stores the procssed parts and then only calls
|
|
884
|
-
* setAttribute() once all the pieces are in place.
|
|
885
|
-
*
|
|
886
|
-
* @param exprs {Expr[]}
|
|
887
|
-
* @param freeNodeGroups {boolean} */
|
|
888
|
-
apply(exprs, freeNodeGroups=true) {
|
|
889
|
-
switch (this.type) {
|
|
890
|
-
case 1: // PathType.Content:
|
|
891
|
-
this.applyNodes(exprs[0], freeNodeGroups);
|
|
892
|
-
break;
|
|
893
|
-
case 2: // PathType.Multiple:
|
|
894
|
-
this.applyMultipleAttribs(this.nodeMarker, exprs[0]);
|
|
895
|
-
break;
|
|
896
|
-
case 5: // PathType.Comment:
|
|
897
|
-
// Expressions inside Html comments. Deliberately empty because we won't waste time updating them.
|
|
898
|
-
break;
|
|
899
|
-
case 6: // PathType.Event:
|
|
900
|
-
this.applyEventAttrib(this.nodeMarker, exprs[0], this.parentNg.rootNg.root);
|
|
901
|
-
break;
|
|
902
|
-
default: // TODO: Is this still used? Lots of tests fail without it.
|
|
903
|
-
// One attribute value may have multiple expressions. Here we apply them all at once.
|
|
904
|
-
this.applyValueAttrib(this.nodeMarker, exprs);
|
|
905
|
-
break;
|
|
906
|
-
}
|
|
1447
|
+
* Nodes that were not added to the web component during the last render(), and available to be used again.
|
|
1448
|
+
* @type {MultiValueMap} */
|
|
1449
|
+
nodeGroupsDetachedAvailable = new MultiValueMap();
|
|
1450
|
+
|
|
1451
|
+
constructor(nodeBefore, nodeMarker) {
|
|
1452
|
+
super(nodeBefore, nodeMarker);
|
|
907
1453
|
}
|
|
908
1454
|
|
|
909
1455
|
/**
|
|
910
1456
|
* Insert/replace the nodes created by a single expression.
|
|
911
1457
|
* Called by applyExprs()
|
|
912
|
-
* This function is recursive
|
|
913
|
-
* @param
|
|
1458
|
+
* This function is recursive. It calls functions that call applyNodes().
|
|
1459
|
+
* @param exprs {Expr[]} Only the first is used.
|
|
914
1460
|
* @param freeNodeGroups {boolean}
|
|
915
1461
|
* @return {Node[]} New Nodes created. */
|
|
916
|
-
|
|
1462
|
+
apply(exprs, freeNodeGroups=true) {
|
|
1463
|
+
|
|
1464
|
+
|
|
917
1465
|
let path = this;
|
|
1466
|
+
let expr = exprs[0];
|
|
918
1467
|
|
|
919
1468
|
// This can be done at the beginning or the end of this function.
|
|
920
1469
|
// If at the end, we may get rendering done faster.
|
|
@@ -944,7 +1493,6 @@ class ExprPath {
|
|
|
944
1493
|
if (secondPass.length) {
|
|
945
1494
|
for (let [nodesIndex, ngIndex] of secondPass) {
|
|
946
1495
|
let ng = path.getNodeGroup(newNodes[nodesIndex], false);
|
|
947
|
-
|
|
948
1496
|
let ngNodes = ng.getNodes();
|
|
949
1497
|
|
|
950
1498
|
|
|
@@ -965,11 +1513,8 @@ class ExprPath {
|
|
|
965
1513
|
|
|
966
1514
|
|
|
967
1515
|
|
|
968
|
-
|
|
969
|
-
|
|
970
1516
|
let oldNodes = path.getNodes();
|
|
971
1517
|
|
|
972
|
-
|
|
973
1518
|
// This pre-check makes it a few percent faster?
|
|
974
1519
|
let same = Util.arraySame(oldNodes, newNodes);
|
|
975
1520
|
if (!same) {
|
|
@@ -981,195 +1526,71 @@ class ExprPath {
|
|
|
981
1526
|
|
|
982
1527
|
// Fast clear method
|
|
983
1528
|
let isNowEmpty = oldNodes.length && !newNodes.length;
|
|
984
|
-
if (!isNowEmpty || !path.fastClear())
|
|
985
|
-
|
|
986
|
-
// Rearrange nodes.
|
|
987
|
-
udomdiff(path.nodeMarker.parentNode, oldNodes, newNodes, path.nodeMarker);
|
|
988
|
-
|
|
989
|
-
// TODO: Put this in a remove() function of NodeGroup.
|
|
990
|
-
// Then only run it on the old nodeGroups that were actually removed.
|
|
991
|
-
//Util.saveOrphans(oldNodeGroups, oldNodes);
|
|
992
|
-
|
|
993
|
-
for (let ng of oldNodeGroups)
|
|
994
|
-
if (!ng.startNode.parentNode)
|
|
995
|
-
ng.removeAndSaveOrphans();
|
|
996
|
-
|
|
997
|
-
// Instantiate components created within ${...} expressions.
|
|
998
|
-
// Embedded style tags are handled elsewhere, but where?
|
|
999
|
-
for (let el of newNodes) {
|
|
1000
|
-
if (el instanceof HTMLElement) {
|
|
1001
|
-
if (el.hasAttribute('solarite-placeholder'))
|
|
1002
|
-
this.parentNg.instantiateComponent(el);
|
|
1003
|
-
for (let child of el.querySelectorAll('[solarite-placeholder]'))
|
|
1004
|
-
this.parentNg.instantiateComponent(child);
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
/**
|
|
1014
|
-
* Used by watch() for inserting/removing/replacing individual loop items.
|
|
1015
|
-
* @param op {ArraySpliceOp} */
|
|
1016
|
-
applyArrayOp(op) {
|
|
1017
|
-
|
|
1018
|
-
// Replace NodeGroups
|
|
1019
|
-
let replaceCount = Math.min(op.deleteCount, op.items.length);
|
|
1020
|
-
let deleteCount = op.deleteCount - replaceCount;
|
|
1021
|
-
for (let i=0; i<replaceCount; i++) {
|
|
1022
|
-
let oldNg = this.nodeGroups[op.index + i]; // TODO: One expr can create multiple nodegroups.
|
|
1023
|
-
|
|
1024
|
-
// Try to find an exact match
|
|
1025
|
-
let func = this.mapCallback || this.watchFunction;
|
|
1026
|
-
let expr = func(op.items[i]);
|
|
1027
|
-
|
|
1028
|
-
// If the result of func isn't a template, conver it to one or more templates.
|
|
1029
|
-
this.exprToTemplates(expr, template => { // TODO: An expr can create multiple NodeGroups. I need a way to group them.
|
|
1030
|
-
|
|
1031
|
-
let ng = this.getNodeGroup(template, true); // Removes from nodeGroupsAttached and adds to nodeGroupsRendered()
|
|
1032
|
-
if (ng && ng === oldNg) ; else {
|
|
1033
|
-
|
|
1034
|
-
// Find a close match or create a new node group
|
|
1035
|
-
if (!ng)
|
|
1036
|
-
ng = this.getNodeGroup(template, false); // adds back to nodeGroupsRendered()
|
|
1037
|
-
this.nodeGroups[op.index + i] = ng; // TODO: Remove old one to nodeGroupsDetached?
|
|
1038
|
-
|
|
1039
|
-
// Splice in the new nodes.
|
|
1040
|
-
let insertBefore = oldNg.startNode;
|
|
1041
|
-
for (let node of ng.getNodes())
|
|
1042
|
-
insertBefore.parentNode.insertBefore(node, insertBefore);
|
|
1043
|
-
|
|
1044
|
-
// Remove the old nodes.
|
|
1045
|
-
if (ng !== oldNg)
|
|
1046
|
-
oldNg.removeAndSaveOrphans();
|
|
1047
|
-
}
|
|
1048
|
-
});
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
// Delete extra at the end.
|
|
1052
|
-
if (deleteCount > 0) {
|
|
1053
|
-
for (let i=0; i<deleteCount; i++) {
|
|
1054
|
-
let oldNg = this.nodeGroups[op.index + replaceCount + i];
|
|
1055
|
-
oldNg.removeAndSaveOrphans();
|
|
1056
|
-
}
|
|
1057
|
-
this.nodeGroups.splice(op.index + replaceCount, deleteCount);
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
// Add extra at the end.
|
|
1061
|
-
else {
|
|
1062
|
-
let newItems = op.items.slice(replaceCount);
|
|
1063
|
-
|
|
1064
|
-
let insertBefore = this.nodeGroups[op.index + replaceCount]?.startNode || this.nodeMarker;
|
|
1065
|
-
for (let i = 0; i < newItems.length; i++) { // We use nodeMarker if the subequent (or all) nodeGroups have been removed.
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
// Try to find exact match
|
|
1069
|
-
let template = this.mapCallback(newItems[i]);
|
|
1070
|
-
let ng = this.getNodeGroup(template, true); // Removes from nodeGroupsAttached and adds to nodeGroupsRendered()
|
|
1071
|
-
if (!ng) // Find a close match or create a new node group
|
|
1072
|
-
ng = this.getNodeGroup(template, false); // adds back to nodeGroupsRendered()
|
|
1073
|
-
|
|
1074
|
-
this.nodeGroups.push(ng);
|
|
1075
|
-
|
|
1076
|
-
// Splice in the new nodes.
|
|
1077
|
-
for (let node of ng.getNodes())
|
|
1078
|
-
insertBefore.parentNode.insertBefore(node, insertBefore);
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
// TODO: update or invalidate the nodes cache?
|
|
1085
|
-
this.nodesCache = null;
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
/**
|
|
1089
|
-
* Recursively traverse expr.
|
|
1090
|
-
* If a value is a function, evaluate it.
|
|
1091
|
-
* If a value is an array, recurse on each item.
|
|
1092
|
-
* If it's a primitive, convert it to a Template.
|
|
1093
|
-
* Otherwise pass the item (which is now either a Template or a Node) to callback.
|
|
1094
|
-
* @param expr
|
|
1095
|
-
* @param callback {function(Node|Template)}
|
|
1096
|
-
*
|
|
1097
|
-
* TODO: have applyExactNodes() use this function. */
|
|
1098
|
-
exprToTemplates(expr, callback) {
|
|
1099
|
-
if (Array.isArray(expr))
|
|
1100
|
-
for (let subExpr of expr)
|
|
1101
|
-
this.exprToTemplates(subExpr, callback);
|
|
1529
|
+
if (!isNowEmpty || !path.fastClear()) {
|
|
1102
1530
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
// So maybe this is ok.
|
|
1107
|
-
Globals$1.currentExprPath = this; // Used by watch()
|
|
1531
|
+
// Rearrange nodes.
|
|
1532
|
+
udomdiff(path.nodeMarker.parentNode, oldNodes, newNodes, path.nodeMarker);
|
|
1533
|
+
}
|
|
1108
1534
|
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1535
|
+
// TODO: Put this in a remove() function of NodeGroup.
|
|
1536
|
+
// Then only run it on the old nodeGroups that were actually removed.
|
|
1537
|
+
//Util.saveOrphans(oldNodeGroups, oldNodes);
|
|
1112
1538
|
|
|
1113
|
-
|
|
1539
|
+
for (let ng of oldNodeGroups)
|
|
1540
|
+
if (!ng.startNode.parentNode)
|
|
1541
|
+
Util.saveOrphans(ng.getNodes());
|
|
1114
1542
|
}
|
|
1115
1543
|
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
// Convert expression to a string.
|
|
1119
|
-
if (expr === undefined || expr === false || expr === null) // Util.isFalsy() inlined
|
|
1120
|
-
expr = '';
|
|
1121
|
-
else if (typeof expr !== 'string')
|
|
1122
|
-
expr += '';
|
|
1544
|
+
|
|
1545
|
+
}
|
|
1123
1546
|
|
|
1124
|
-
// Get the same Template for the same string each time.
|
|
1125
|
-
// let template = Globals.stringTemplates[expr];
|
|
1126
|
-
// if (!template) {
|
|
1127
|
-
let template = new Template([expr], []);
|
|
1128
|
-
// Globals.stringTemplates[expr] = template;
|
|
1129
|
-
//}
|
|
1130
1547
|
|
|
1131
|
-
// Recurse.
|
|
1132
|
-
this.exprToTemplates(template, callback);
|
|
1133
|
-
}
|
|
1134
|
-
else
|
|
1135
|
-
callback(expr);
|
|
1136
|
-
}
|
|
1137
1548
|
|
|
1138
1549
|
|
|
1139
1550
|
/**
|
|
1140
1551
|
* Try to apply Nodes that are an exact match, by finding existing nodes from the last render
|
|
1141
1552
|
* that have the same value as created by the expr.
|
|
1142
|
-
* This is called from
|
|
1553
|
+
* This is called from Path.applyNodes().
|
|
1143
1554
|
*
|
|
1144
1555
|
* @param expr {Template|Node|Array|function|*}
|
|
1145
1556
|
* @param newNodes {(Node|Template)[]} An inout parameter; we add the nodes here as we go.
|
|
1146
|
-
* @param secondPass {[int, int][]} Locations within newNodes for
|
|
1557
|
+
* @param secondPass {[int, int][]} Locations within newNodes for Path.applyNodes() to evaluate later,
|
|
1147
1558
|
* when it tries to find partial matches. */
|
|
1148
1559
|
applyExactNodes(expr, newNodes, secondPass) {
|
|
1149
1560
|
|
|
1150
1561
|
if (expr instanceof Template) {
|
|
1151
1562
|
let ng = this.getNodeGroup(expr, true);
|
|
1563
|
+
|
|
1152
1564
|
if (ng) {
|
|
1565
|
+
let newestNodes = ng.getNodes();
|
|
1566
|
+
newNodes.push(...newestNodes);
|
|
1567
|
+
|
|
1568
|
+
// New!
|
|
1569
|
+
// Call render() on web components even though none of their arguments have changed:
|
|
1570
|
+
// Do we want it to work this way? Yes, because even if this component hasn't changed,
|
|
1571
|
+
// perhaps something in a sub-component has.
|
|
1572
|
+
ng.applyExprs(expr.exprs, false, false);
|
|
1153
1573
|
|
|
1154
|
-
// TODO: Track ranges of changed nodes and only pass those to udomdiff?
|
|
1155
|
-
// But will that break the swap benchmark?
|
|
1156
|
-
newNodes.push(...ng.getNodes());
|
|
1157
1574
|
this.nodeGroups.push(ng);
|
|
1575
|
+
return ng;
|
|
1158
1576
|
}
|
|
1159
1577
|
|
|
1160
|
-
// If expression, mark it to be evaluated later in
|
|
1578
|
+
// If expression, mark it to be evaluated later in Path.apply() to find partial match.
|
|
1161
1579
|
else {
|
|
1162
1580
|
secondPass.push([newNodes.length, this.nodeGroups.length]);
|
|
1163
1581
|
newNodes.push(expr);
|
|
1164
1582
|
this.nodeGroups.push(null); // placeholder
|
|
1165
1583
|
}
|
|
1166
1584
|
}
|
|
1585
|
+
else if (expr instanceof NodeList) {
|
|
1586
|
+
newNodes.push(...expr);
|
|
1587
|
+
}
|
|
1167
1588
|
|
|
1168
1589
|
// Node(s) created by an expression.
|
|
1169
|
-
else if (expr
|
|
1590
|
+
else if (expr?.nodeType) {
|
|
1170
1591
|
|
|
1171
1592
|
// DocumentFragment created by an expression.
|
|
1172
|
-
if (expr
|
|
1593
|
+
if (expr?.nodeType === 11) // DocumentFragment
|
|
1173
1594
|
newNodes.push(...expr.childNodes);
|
|
1174
1595
|
else
|
|
1175
1596
|
newNodes.push(expr);
|
|
@@ -1184,322 +1605,88 @@ class ExprPath {
|
|
|
1184
1605
|
});
|
|
1185
1606
|
}
|
|
1186
1607
|
|
|
1187
|
-
applyMultipleAttribs(node, expr) {
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
if (Array.isArray(expr))
|
|
1191
|
-
expr = expr.flat().join(' '); // flat and join so we can accept arrays of arrays of strings.
|
|
1192
|
-
|
|
1193
|
-
// Add new attributes
|
|
1194
|
-
let oldNames = this.attrNames;
|
|
1195
|
-
this.attrNames = new Set();
|
|
1196
|
-
if (expr) {
|
|
1197
|
-
if (typeof expr === 'function') {
|
|
1198
|
-
Globals$1.currentExprPath = this; // Used by watch()
|
|
1199
|
-
this.watchFunction = expr; // used by renderWatched()
|
|
1200
|
-
expr = expr();
|
|
1201
|
-
Globals$1.currentExprPath = null;
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
// Attribute as name: value object.
|
|
1205
|
-
if (typeof expr === 'object') {
|
|
1206
|
-
for (let name in expr) {
|
|
1207
|
-
let value = expr[name];
|
|
1208
|
-
if (value === undefined || value === false || value === null)
|
|
1209
|
-
continue;
|
|
1210
|
-
node.setAttribute(name, value);
|
|
1211
|
-
this.attrNames.add(name);
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
// Attributes as string
|
|
1216
|
-
else {
|
|
1217
|
-
let attrs = (expr + '') // Split string into multiple attributes.
|
|
1218
|
-
.split(/([\w-]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s]+))/g)
|
|
1219
|
-
.map(text => text.trim())
|
|
1220
|
-
.filter(text => text.length);
|
|
1221
|
-
|
|
1222
|
-
for (let attr of attrs) {
|
|
1223
|
-
let [name, value] = attr.split(/\s*=\s*/); // split on first equals.
|
|
1224
|
-
value = (value || '').replace(/^(['"])(.*)\1$/, '$2'); // trim value quotes if they match.
|
|
1225
|
-
node.setAttribute(name, value);
|
|
1226
|
-
this.attrNames.add(name);
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
// Remove old attributes.
|
|
1232
|
-
for (let oldName of oldNames)
|
|
1233
|
-
if (!this.attrNames.has(oldName))
|
|
1234
|
-
node.removeAttribute(oldName);
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
/**
|
|
1238
|
-
* Handle attributes for event binding, such as:
|
|
1239
|
-
* onclick=${(e, el) => this.doSomething(el, 'meow')}
|
|
1240
|
-
* oninput=${[this.doSomething, 'meow']}
|
|
1241
|
-
* onclick=${[this, 'doSomething', 'meow']}
|
|
1242
|
-
*
|
|
1243
|
-
* @param node
|
|
1244
|
-
* @param expr
|
|
1245
|
-
* @param root */
|
|
1246
|
-
applyEventAttrib(node, expr, root) {
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
let eventName = this.attrName.slice(2); // remove "on-" prefix.
|
|
1250
|
-
let func;
|
|
1251
|
-
let args = [];
|
|
1252
|
-
|
|
1253
|
-
// Convert array to function.
|
|
1254
|
-
// oninput=${[this.doSomething, 'meow']}
|
|
1255
|
-
if (Array.isArray(expr) && typeof expr[0] === 'function') {
|
|
1256
|
-
func = expr[0];
|
|
1257
|
-
args = expr.slice(1);
|
|
1258
|
-
}
|
|
1259
|
-
else if (typeof expr === 'function')
|
|
1260
|
-
func = expr;
|
|
1261
|
-
else
|
|
1262
|
-
throw new Error(`Invalid event binding: <${node.tagName.toLowerCase()} ${this.attrName}=\${${JSON.stringify(expr)}}>`);
|
|
1263
|
-
|
|
1264
|
-
this.bindEvent(node, root, eventName, eventName, func, args);
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
/**
|
|
1269
|
-
* Call function when eventName is triggerd on node.
|
|
1270
|
-
* @param node {HTMLElement}
|
|
1271
|
-
* @param root {HTMLElement}
|
|
1272
|
-
* @param key {string}
|
|
1273
|
-
* @param eventName {string}
|
|
1274
|
-
* @param func {function}
|
|
1275
|
-
* @param args {array}
|
|
1276
|
-
* @param capture {boolean} */
|
|
1277
|
-
bindEvent(node, root, key, eventName, func, args, capture=false) {
|
|
1278
|
-
let nodeEvents = Globals$1.nodeEvents.get(node);
|
|
1279
|
-
if (!nodeEvents) {
|
|
1280
|
-
nodeEvents = {[key]: new Array(3)};
|
|
1281
|
-
Globals$1.nodeEvents.set(node, nodeEvents);
|
|
1282
|
-
}
|
|
1283
|
-
let nodeEvent = nodeEvents[key];
|
|
1284
|
-
if (!nodeEvent)
|
|
1285
|
-
nodeEvents[key] = nodeEvent = new Array(3);
|
|
1286
|
-
|
|
1287
|
-
if (typeof func !== 'function')
|
|
1288
|
-
throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${${func}}> because it's not a function.`);
|
|
1289
|
-
|
|
1290
|
-
// If function has changed, remove and rebind the event.
|
|
1291
|
-
if (nodeEvent[0] !== func) {
|
|
1292
|
-
|
|
1293
|
-
// TODO: We should be removing event listeners when calling getNodeGroup(),
|
|
1294
|
-
// when we get the node from the list of nodeGroupsAttached/nodeGroupsDetached,
|
|
1295
|
-
// instead of only when we rebind an event.
|
|
1296
|
-
let [existing, existingBound, _] = nodeEvent;
|
|
1297
|
-
if (existing)
|
|
1298
|
-
node.removeEventListener(eventName, existingBound, capture);
|
|
1299
|
-
|
|
1300
|
-
let originalFunc = func;
|
|
1301
|
-
|
|
1302
|
-
// BoundFunc sets the "this" variable to be the current Solarite component.
|
|
1303
|
-
let boundFunc = (event) => {
|
|
1304
|
-
let args = nodeEvent[2];
|
|
1305
|
-
return originalFunc.call(root, ...args, event, node);
|
|
1306
|
-
};
|
|
1307
|
-
|
|
1308
|
-
// Save both the original and bound functions.
|
|
1309
|
-
// Original so we can compare it against a newly assigned function.
|
|
1310
|
-
// Bound so we can use it with removeEventListner().
|
|
1311
|
-
nodeEvent[0] = originalFunc;
|
|
1312
|
-
nodeEvent[1] = boundFunc;
|
|
1313
|
-
|
|
1314
|
-
node.addEventListener(eventName, boundFunc, capture);
|
|
1315
|
-
|
|
1316
|
-
// TODO: classic event attribs?
|
|
1317
|
-
//el[attr.name] = e => // e.g. el.onclick = ... // put "event", "el", and "this" in scope for the event code.
|
|
1318
|
-
// (new Function('event', 'el', attr.value)).bind(this.manager.rootEl)(e, el)
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
// Otherwise just update the args to the function.
|
|
1322
|
-
nodeEvents[key][2] = args;
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
1608
|
/**
|
|
1326
|
-
*
|
|
1327
|
-
* @param
|
|
1328
|
-
|
|
1329
|
-
// TODO: node is always this.nodeMarker?
|
|
1330
|
-
applyValueAttrib(node, exprs) {
|
|
1331
|
-
let expr = exprs[0];
|
|
1332
|
-
|
|
1333
|
-
// Two-way binding between attributes
|
|
1334
|
-
// Passing a path to the value attribute.
|
|
1335
|
-
// Copies the attribute to the property when the input event fires.
|
|
1336
|
-
// value=${[this, 'value]'}
|
|
1337
|
-
// checked=${[this, 'isAgree']}
|
|
1338
|
-
// This same logic is in NodeGroup.instantiateComponent() for components.
|
|
1339
|
-
if (Util.isPath(expr)) {
|
|
1340
|
-
let [obj, path] = [expr[0], expr.slice(1)];
|
|
1341
|
-
|
|
1342
|
-
if (!obj)
|
|
1343
|
-
throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${[${expr.map(item => item ? `'${item}'` : item+'').join(', ')}]}>.`);
|
|
1344
|
-
|
|
1345
|
-
let value = delve(obj, path);
|
|
1346
|
-
|
|
1347
|
-
// Special case to allow setting select-multiple value from an array
|
|
1348
|
-
if (this.attrName === 'value' && node.type === 'select-multiple' && Array.isArray(value)) {
|
|
1349
|
-
// Set the .selected property on the options having a value within value.
|
|
1350
|
-
let strValues = value.map(v => v + '');
|
|
1351
|
-
for (let option of node.options)
|
|
1352
|
-
option.selected = strValues.includes(option.value);
|
|
1353
|
-
}
|
|
1354
|
-
else {
|
|
1355
|
-
// TODO: should we remove isFalsy, since these are always props?
|
|
1356
|
-
let strValue = Util.isFalsy(value) ? '' : value;
|
|
1357
|
-
|
|
1358
|
-
// If we don't have this condition, when we call render(), the browser will scroll to the currently
|
|
1359
|
-
// selected item in a <select> and mess up manually scrolling to a different value.
|
|
1360
|
-
if (strValue !== node[this.attrName])
|
|
1361
|
-
node[this.attrName] = strValue;
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
// TODO: We need to remove any old listeners, like in bindEventAttribute.
|
|
1365
|
-
// Does bindEvent() now handle that?
|
|
1366
|
-
let func = () => {
|
|
1367
|
-
let value = (this.attrName === 'value')
|
|
1368
|
-
? Util.getInputValue(node)
|
|
1369
|
-
: node[this.attrName];
|
|
1370
|
-
delve(obj, path, value);
|
|
1371
|
-
};
|
|
1372
|
-
|
|
1373
|
-
// We use capture so we update the values before other events added by the user.
|
|
1374
|
-
// TODO: Bind to scroll events also?
|
|
1375
|
-
// What about resize events and width/height?
|
|
1376
|
-
this.bindEvent(node, path[0], this.attrName, 'input', func, [], true);
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
// Regular attribute
|
|
1380
|
-
else {
|
|
1381
|
-
// TODO: Cache this on ExprPath.isProp when Shell creates the props. Have ExprPath.clone() copy .isProp
|
|
1382
|
-
// Or make it a new PathType.
|
|
1383
|
-
//if (this.attrName === 'disabled')
|
|
1384
|
-
// debugger;
|
|
1385
|
-
|
|
1386
|
-
// hasOwnProperty() checks only the object, not the parents
|
|
1387
|
-
// this.attrName in node checks the node and the parents.
|
|
1388
|
-
// This version checks the html element it extends from, to see if has a setter set:
|
|
1389
|
-
// Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), this.attrName)?.set
|
|
1390
|
-
//let isProp = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), this.attrName)?.set;
|
|
1391
|
-
let isProp = this.isHtmlProperty;
|
|
1392
|
-
if (isProp === undefined)
|
|
1393
|
-
isProp = this.isHtmlProperty = Util.isHtmlProp(node, this.attrName);
|
|
1609
|
+
* Used by watch() for inserting/removing/replacing individual loop items.
|
|
1610
|
+
* @param op {ArraySpliceOp} */
|
|
1611
|
+
applyWatchArrayOp(op) {
|
|
1394
1612
|
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
if (this.type === 4) { // Don't evaluate functions before passing them to components
|
|
1401
|
-
return
|
|
1402
|
-
}
|
|
1403
|
-
this.watchFunction = expr; // The function that gets the expression, used for renderWatched()
|
|
1404
|
-
expr = expr();
|
|
1405
|
-
}
|
|
1406
|
-
else
|
|
1407
|
-
expr = Util.makePrimitive(expr);
|
|
1408
|
-
Globals$1.currentExprPath = null;
|
|
1409
|
-
}
|
|
1410
|
-
if (!multiple && (expr === undefined || expr === false || expr === null)) { // Util.isFalsy() inlined.
|
|
1411
|
-
if (isProp)
|
|
1412
|
-
node[this.attrName] = false;
|
|
1413
|
-
node.removeAttribute(this.attrName);
|
|
1414
|
-
}
|
|
1415
|
-
else if (!multiple && expr === true) {
|
|
1416
|
-
if (isProp)
|
|
1417
|
-
node[this.attrName] = true;
|
|
1418
|
-
node.setAttribute(this.attrName, '');
|
|
1419
|
-
}
|
|
1613
|
+
// Replace NodeGroups
|
|
1614
|
+
let replaceCount = Math.min(op.deleteCount, op.items.length);
|
|
1615
|
+
let deleteCount = op.deleteCount - replaceCount;
|
|
1616
|
+
for (let i=0; i<replaceCount; i++) {
|
|
1617
|
+
let oldNg = this.nodeGroups[op.index + i]; // TODO: One expr can create multiple nodegroups.
|
|
1420
1618
|
|
|
1421
|
-
//
|
|
1422
|
-
|
|
1619
|
+
// Try to find an exact match
|
|
1620
|
+
let func = this.mapCallback || this.watchFunction;
|
|
1621
|
+
let expr = func(op.items[i]);
|
|
1423
1622
|
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
if (multiple) {
|
|
1427
|
-
let value = [];
|
|
1428
|
-
for (let i = 0; i < this.attrValue.length; i++) {
|
|
1429
|
-
value.push(this.attrValue[i]);
|
|
1430
|
-
if (i < this.attrValue.length - 1) {
|
|
1431
|
-
Globals$1.currentExprPath = this; // Used by watch()
|
|
1432
|
-
let val = Util.makePrimitive(exprs[i]);
|
|
1433
|
-
Globals$1.currentExprPath = null;
|
|
1434
|
-
if (!Util.isFalsy(val))
|
|
1435
|
-
value.push(val);
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
joinedValue = value.join('');
|
|
1439
|
-
}
|
|
1623
|
+
// If the result of func isn't a template, conver it to one or more templates.
|
|
1624
|
+
this.exprToTemplates(expr, template => { // TODO: An expr can create multiple NodeGroups. I need a way to group them.
|
|
1440
1625
|
|
|
1441
|
-
|
|
1442
|
-
else
|
|
1443
|
-
joinedValue = expr;
|
|
1626
|
+
let ng = this.getNodeGroup(template, true); // Removes from nodeGroupsAttached and adds to nodeGroupsRendered()
|
|
1627
|
+
if (ng && ng === oldNg) ; else {
|
|
1444
1628
|
|
|
1445
|
-
|
|
1446
|
-
|
|
1629
|
+
// Find a close match or create a new node group
|
|
1630
|
+
if (!ng)
|
|
1631
|
+
ng = this.getNodeGroup(template, false); // adds back to nodeGroupsRendered()
|
|
1632
|
+
this.nodeGroups[op.index + i] = ng; // TODO: Remove old one to nodeGroupsDetached?
|
|
1447
1633
|
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1634
|
+
// Splice in the new nodes.
|
|
1635
|
+
let insertBefore = oldNg.startNode;
|
|
1636
|
+
for (let node of ng.getNodes())
|
|
1637
|
+
insertBefore.parentNode.insertBefore(node, insertBefore);
|
|
1452
1638
|
|
|
1453
|
-
//
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
if (isProp)
|
|
1457
|
-
node[this.attrName] = joinedValue;
|
|
1458
|
-
// TODO: Putting an 'else' here would be more performant
|
|
1459
|
-
node.setAttribute(this.attrName, joinedValue);
|
|
1639
|
+
// Remove the old nodes.
|
|
1640
|
+
if (ng !== oldNg)
|
|
1641
|
+
Util.saveOrphans(oldNg.getNodes());
|
|
1460
1642
|
}
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// Delete extra at the end.
|
|
1647
|
+
if (deleteCount > 0) {
|
|
1648
|
+
for (let i=0; i<deleteCount; i++) {
|
|
1649
|
+
let oldNg = this.nodeGroups[op.index + replaceCount + i];
|
|
1650
|
+
Util.saveOrphans(oldNg.getNodes());
|
|
1461
1651
|
}
|
|
1652
|
+
this.nodeGroups.splice(op.index + replaceCount, deleteCount);
|
|
1462
1653
|
}
|
|
1463
|
-
}
|
|
1464
1654
|
|
|
1655
|
+
// Add extra at the end.
|
|
1656
|
+
else {
|
|
1657
|
+
let newItems = op.items.slice(replaceCount);
|
|
1465
1658
|
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
* @param newRoot {HTMLElement}
|
|
1469
|
-
* @param pathOffset {int}
|
|
1470
|
-
* @return {ExprPath} */
|
|
1471
|
-
clone(newRoot, pathOffset=0) {
|
|
1472
|
-
|
|
1659
|
+
let insertBefore = this.nodeGroups[op.index + replaceCount]?.startNode || this.nodeMarker;
|
|
1660
|
+
for (let i = 0; i < newItems.length; i++) { // We use nodeMarker if the subequent (or all) nodeGroups have been removed.
|
|
1473
1661
|
|
|
1474
|
-
// Resolve node paths.
|
|
1475
|
-
let nodeMarker, nodeBefore;
|
|
1476
|
-
let root = newRoot;
|
|
1477
|
-
let path = pathOffset ? this.nodeMarkerPath.slice(0, -pathOffset) : this.nodeMarkerPath;
|
|
1478
|
-
let length = path.length-1;
|
|
1479
|
-
for (let i=length; i>0; i--) // Resolve the path.
|
|
1480
|
-
root = root.childNodes[path[i]];
|
|
1481
|
-
let childNodes = root.childNodes;
|
|
1482
1662
|
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1663
|
+
// Try to find exact match
|
|
1664
|
+
let template = this.mapCallback(newItems[i]);
|
|
1665
|
+
let ng = this.getNodeGroup(template, true); // Removes from nodeGroupsAttached and adds to nodeGroupsRendered()
|
|
1666
|
+
if (!ng) // Find a close match or create a new node group
|
|
1667
|
+
ng = this.getNodeGroup(template, false); // adds back to nodeGroupsRendered()
|
|
1668
|
+
|
|
1669
|
+
this.nodeGroups.push(ng);
|
|
1486
1670
|
|
|
1487
|
-
|
|
1671
|
+
// Splice in the new nodes.
|
|
1672
|
+
for (let node of ng.getNodes())
|
|
1673
|
+
insertBefore.parentNode.insertBefore(node, insertBefore);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1488
1676
|
|
|
1489
1677
|
|
|
1490
1678
|
|
|
1491
|
-
|
|
1679
|
+
// TODO: update or invalidate the nodes cache?
|
|
1680
|
+
this.nodesCache = null;
|
|
1492
1681
|
}
|
|
1493
1682
|
|
|
1494
1683
|
/**
|
|
1495
|
-
* Clear the nodeCache of this
|
|
1496
|
-
* share the same DOM parent node.
|
|
1497
|
-
*
|
|
1498
|
-
* TODO: Is recursive clearing ever necessary? */
|
|
1684
|
+
* Clear the nodeCache of this Path, as well as all parent and child Paths that
|
|
1685
|
+
* share the same DOM parent node. */
|
|
1499
1686
|
clearNodesCache() {
|
|
1500
1687
|
let path = this;
|
|
1501
1688
|
|
|
1502
|
-
// Clear cache parent
|
|
1689
|
+
// Clear cache parent Paths that have the same parentNode
|
|
1503
1690
|
let parentNode = this.nodeMarker.parentNode;
|
|
1504
1691
|
while (path && path.nodeMarker.parentNode === parentNode) {
|
|
1505
1692
|
path.nodesCache = null;
|
|
@@ -1508,14 +1695,10 @@ class ExprPath {
|
|
|
1508
1695
|
// If stuck in an infinite loop here, the problem is likely due to Template hash colisions.
|
|
1509
1696
|
// Which cause one path to be the descendant of itself, creating a cycle.
|
|
1510
1697
|
}
|
|
1511
|
-
|
|
1512
|
-
// Commented out on Sep 30, 2024 b/c it was making the benchmark never finish when adding 10k rows.
|
|
1513
|
-
//clearChildNodeCache(this);
|
|
1514
1698
|
}
|
|
1515
1699
|
|
|
1516
|
-
|
|
1517
1700
|
/**
|
|
1518
|
-
* Attempt to remove all of this
|
|
1701
|
+
* Attempt to remove all of this Path's nodes from the DOM, if it can be done using a special fast method.
|
|
1519
1702
|
* @returns {boolean} Returns false if Nodes weren't removed, and they should instead be removed manually. */
|
|
1520
1703
|
fastClear() {
|
|
1521
1704
|
let parent = this.nodeBefore.parentNode;
|
|
@@ -1534,8 +1717,8 @@ class ExprPath {
|
|
|
1534
1717
|
// parent.replaceWith(replacement)
|
|
1535
1718
|
// }
|
|
1536
1719
|
// else {
|
|
1537
|
-
|
|
1538
|
-
|
|
1720
|
+
parent.innerHTML = ''; // Faster than calling .removeChild() a thousand times.
|
|
1721
|
+
parent.append(this.nodeBefore, this.nodeMarker);
|
|
1539
1722
|
//}
|
|
1540
1723
|
return true;
|
|
1541
1724
|
}
|
|
@@ -1543,46 +1726,54 @@ class ExprPath {
|
|
|
1543
1726
|
}
|
|
1544
1727
|
|
|
1545
1728
|
/**
|
|
1546
|
-
*
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1729
|
+
* Recursively traverse expr.
|
|
1730
|
+
* If a value is a function, evaluate it.
|
|
1731
|
+
* If a value is an array, recurse on each item.
|
|
1732
|
+
* If it's a primitive, convert it to a Template.
|
|
1733
|
+
* Otherwise pass the item (which is now either a Template or a Node) to callback.
|
|
1734
|
+
* TODO: This could be static if not for the watch code, which doesn't work anyway.
|
|
1735
|
+
* @param expr
|
|
1736
|
+
* @param callback {function(Node|Template)}*/
|
|
1737
|
+
exprToTemplates(expr, callback) {
|
|
1738
|
+
if (Array.isArray(expr)) // TODO: use typeof obj[Symbol.iterator] === 'function' so we can also iterate over objects and NodeList?
|
|
1739
|
+
for (let subExpr of expr)
|
|
1740
|
+
this.exprToTemplates(subExpr, callback);
|
|
1554
1741
|
|
|
1555
|
-
if (
|
|
1556
|
-
|
|
1557
|
-
|
|
1742
|
+
else if (typeof expr === 'function') {
|
|
1743
|
+
// TODO: One Path can have multiple expr functions.
|
|
1744
|
+
// But if using it as a watch, it should only have one at the top level.
|
|
1745
|
+
// So maybe this is ok.
|
|
1746
|
+
Globals$1.currentPath = this; // Used by watch()
|
|
1558
1747
|
|
|
1748
|
+
this.watchFunction = expr; // TODO: Only do this if it's a top level function.
|
|
1749
|
+
expr = expr(); // As expr accesses watched variables, watch() uses Globals.currentPath to mark where those watched variables are being used.
|
|
1750
|
+
Globals$1.currentPath = null;
|
|
1559
1751
|
|
|
1560
|
-
|
|
1752
|
+
this.exprToTemplates(expr, callback);
|
|
1753
|
+
}
|
|
1561
1754
|
|
|
1562
|
-
//
|
|
1563
|
-
|
|
1564
|
-
|
|
1755
|
+
// String/Number/Date/Boolean
|
|
1756
|
+
else if (!(expr instanceof Template) && !(expr?.nodeType)){
|
|
1757
|
+
// Convert expression to a string.
|
|
1758
|
+
if (expr === undefined || expr === false || expr === null) // Util.isFalsy() inlined
|
|
1759
|
+
expr = '';
|
|
1760
|
+
else if (typeof expr !== 'string')
|
|
1761
|
+
expr += '';
|
|
1565
1762
|
|
|
1566
|
-
|
|
1763
|
+
// Get the same Template for the same string each time.
|
|
1764
|
+
// let template = Globals.stringTemplates[expr];
|
|
1765
|
+
// if (!template) {
|
|
1567
1766
|
|
|
1568
|
-
|
|
1569
|
-
|
|
1767
|
+
let template = new Template([expr], []);
|
|
1768
|
+
template.isText = true;
|
|
1769
|
+
// Globals.stringTemplates[expr] = template;
|
|
1770
|
+
//}
|
|
1570
1771
|
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
let nodeMarker = this.nodeMarker;
|
|
1574
|
-
while (current && current !== nodeMarker) {
|
|
1575
|
-
result.push(current);
|
|
1576
|
-
current = current.nextSibling;
|
|
1772
|
+
// Recurse.
|
|
1773
|
+
this.exprToTemplates(template, callback);
|
|
1577
1774
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
return result;
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
/** @return {HTMLElement|ParentNode} */
|
|
1584
|
-
getParentNode() {
|
|
1585
|
-
return this.nodeMarker.parentNode
|
|
1775
|
+
else
|
|
1776
|
+
callback(expr);
|
|
1586
1777
|
}
|
|
1587
1778
|
|
|
1588
1779
|
/**
|
|
@@ -1598,7 +1789,6 @@ class ExprPath {
|
|
|
1598
1789
|
* or createa new NodeGroup from the template.
|
|
1599
1790
|
* @return {NodeGroup} */
|
|
1600
1791
|
getNodeGroup(template, exact=true) {
|
|
1601
|
-
|
|
1602
1792
|
let result;
|
|
1603
1793
|
let collection = this.nodeGroupsAttachedAvailable;
|
|
1604
1794
|
|
|
@@ -1610,11 +1800,13 @@ class ExprPath {
|
|
|
1610
1800
|
result = collection.deleteAny(template.getExactKey());
|
|
1611
1801
|
}
|
|
1612
1802
|
|
|
1613
|
-
if (result) // also delete the matching close key.
|
|
1803
|
+
if (result) {// also delete the matching close key.
|
|
1614
1804
|
collection.deleteSpecific(template.getCloseKey(), result);
|
|
1615
|
-
|
|
1616
|
-
|
|
1805
|
+
|
|
1806
|
+
//result.applyExprs(template.exprs);
|
|
1617
1807
|
}
|
|
1808
|
+
else
|
|
1809
|
+
return null;
|
|
1618
1810
|
}
|
|
1619
1811
|
|
|
1620
1812
|
// Find a close match.
|
|
@@ -1634,47 +1826,23 @@ class ExprPath {
|
|
|
1634
1826
|
|
|
1635
1827
|
// Update this close match with the new expression values.
|
|
1636
1828
|
result.applyExprs(template.exprs);
|
|
1637
|
-
result.exactKey = template.getExactKey();
|
|
1829
|
+
result.exactKey = template.getExactKey();
|
|
1638
1830
|
}
|
|
1639
1831
|
}
|
|
1640
1832
|
|
|
1641
|
-
if (!result)
|
|
1833
|
+
if (!result) {
|
|
1642
1834
|
result = new NodeGroup(template, this);
|
|
1835
|
+
result.applyExprs(template.exprs);
|
|
1836
|
+
result.exactKey = template.getExactKey();
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1643
1839
|
|
|
1644
|
-
// old:
|
|
1645
1840
|
this.nodeGroupsRendered.push(result);
|
|
1646
1841
|
|
|
1647
1842
|
|
|
1648
1843
|
return result;
|
|
1649
1844
|
}
|
|
1650
1845
|
|
|
1651
|
-
isComponent() {
|
|
1652
|
-
// Events won't have type===Component.
|
|
1653
|
-
// TODO: Have a special flag for components instead of it being on the type?
|
|
1654
|
-
return this.type === ExprPathType.ComponentAttribValue || (this.attrName && this.nodeMarker.tagName && this.nodeMarker.tagName.includes('-'));
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
/**
|
|
1658
|
-
* TODO: Rename this to nodeGroupsInUse, nodeGroupsAvialableAttached and nodeGroupsAvailableDetached?
|
|
1659
|
-
* Nodes that have been used during the current render().
|
|
1660
|
-
* Used with getNodeGroup() and freeNodeGroups().
|
|
1661
|
-
* TODO: Use an array of WeakRef so the gc can collect them?
|
|
1662
|
-
* TODO: Put items back in nodeGroupsInUse after applyExpr() is called, not before.
|
|
1663
|
-
* @type {NodeGroup[]} */
|
|
1664
|
-
nodeGroupsRendered = [];
|
|
1665
|
-
|
|
1666
|
-
/**
|
|
1667
|
-
* Nodes that were added to the web component during the last render(), but are available to be used again.
|
|
1668
|
-
* Used with getNodeGroup() and freeNodeGroups().
|
|
1669
|
-
* Each NodeGroup is here twice, once under an exact key, and once under the close key.
|
|
1670
|
-
* @type {MultiValueMap<key:string, value:NodeGroup>} */
|
|
1671
|
-
nodeGroupsAttachedAvailable = new MultiValueMap();
|
|
1672
|
-
|
|
1673
|
-
/**
|
|
1674
|
-
* Nodes that were not added to the web component during the last render(), and available to be used again.
|
|
1675
|
-
* @type {MultiValueMap} */
|
|
1676
|
-
nodeGroupsDetachedAvailable = new MultiValueMap();
|
|
1677
|
-
|
|
1678
1846
|
|
|
1679
1847
|
/**
|
|
1680
1848
|
* Move everything from this.nodeGroupsRendered to this.nodeGroupsAttached and nodeGroupsDetached.
|
|
@@ -1704,145 +1872,174 @@ class ExprPath {
|
|
|
1704
1872
|
this.nodeGroupsRendered = [];
|
|
1705
1873
|
}
|
|
1706
1874
|
|
|
1707
|
-
|
|
1708
|
-
}
|
|
1709
|
-
|
|
1710
|
-
/** @enum {int} */
|
|
1711
|
-
const ExprPathType = {
|
|
1712
|
-
/** Child of a node */
|
|
1713
|
-
Content: 1,
|
|
1714
|
-
|
|
1715
|
-
/** One or more whole attributes */
|
|
1716
|
-
AttribMultiple: 2,
|
|
1717
|
-
|
|
1718
|
-
/** Value of an attribute. */
|
|
1719
|
-
AttribValue: 3,
|
|
1720
|
-
|
|
1721
|
-
/** Value of an attribute being passed to a component. */
|
|
1722
|
-
ComponentAttribValue: 4,
|
|
1723
|
-
|
|
1724
|
-
/** Expressions inside Html comments. */
|
|
1725
|
-
Comment: 5,
|
|
1726
|
-
|
|
1727
|
-
/** Value of an attribute. */
|
|
1728
|
-
Event: 6,
|
|
1729
|
-
};
|
|
1730
|
-
|
|
1731
1875
|
|
|
1732
|
-
/** @return {int[]} Returns indices in reverse order, because doing it that way is faster. */
|
|
1733
|
-
function getNodePath(node) {
|
|
1734
|
-
let result = [];
|
|
1735
|
-
while(true) {
|
|
1736
|
-
let parent = node.parentNode;
|
|
1737
|
-
if (!parent)
|
|
1738
|
-
break;
|
|
1739
|
-
result.push(Array.prototype.indexOf.call(node.parentNode.childNodes, node));
|
|
1740
|
-
node = parent;
|
|
1741
|
-
}
|
|
1742
|
-
return result;
|
|
1743
|
-
}
|
|
1744
1876
|
|
|
1745
|
-
/**
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
* @returns {Node|HTMLElement|HTMLStyleElement} */
|
|
1750
|
-
function resolveNodePath(root, path) {
|
|
1751
|
-
for (let i=path.length-1; i>=0; i--)
|
|
1752
|
-
root = root.childNodes[path[i]];
|
|
1753
|
-
return root;
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
|
-
class HtmlParser {
|
|
1757
|
-
constructor() {
|
|
1758
|
-
this.defaultState = {
|
|
1759
|
-
context: HtmlParser.Text, // possible values: 'TEXT', 'TAG', 'ATTRIBUTE'
|
|
1760
|
-
quote: null, // possible values: null, '"', "'"
|
|
1761
|
-
buffer: '',
|
|
1762
|
-
lastChar: null
|
|
1763
|
-
};
|
|
1764
|
-
this.state = {...this.defaultState};
|
|
1765
|
-
}
|
|
1877
|
+
/**
|
|
1878
|
+
* If not for watch.js, this could be moved to PathToNodes.js
|
|
1879
|
+
* @return {(Node|HTMLElement)[]} */
|
|
1880
|
+
getNodes() {
|
|
1766
1881
|
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1882
|
+
// Why doesn't this work?
|
|
1883
|
+
// let result2 = [];
|
|
1884
|
+
// for (let ng of this.nodeGroups)
|
|
1885
|
+
// result2.push(...ng.getNodes())
|
|
1886
|
+
// return result2;
|
|
1771
1887
|
|
|
1772
|
-
|
|
1773
|
-
* Parse the next chunk of html, starting with the same context we left off with from the previous chunk.
|
|
1774
|
-
* @param html {string}
|
|
1775
|
-
* @param onContextChange {?function(html:string, index:int, oldContext:string, newContext:string)}
|
|
1776
|
-
* Called every time the context changes, and again at the last context.
|
|
1777
|
-
* @return {('Attribute','Text','Tag')} The context at the end of html. */
|
|
1778
|
-
parse(html, onContextChange=null) {
|
|
1779
|
-
if (html === null)
|
|
1780
|
-
return this.reset();
|
|
1888
|
+
let result;
|
|
1781
1889
|
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
this.state.context = HtmlParser.Tag;
|
|
1789
|
-
this.state.buffer = '';
|
|
1790
|
-
}
|
|
1791
|
-
break;
|
|
1792
|
-
case HtmlParser.Tag:
|
|
1793
|
-
if (char === '>') {
|
|
1794
|
-
onContextChange?.(html, i+1, this.state.context, HtmlParser.Text);
|
|
1795
|
-
this.state.context = HtmlParser.Text;
|
|
1796
|
-
this.state.quote = null;
|
|
1797
|
-
this.state.buffer = '';
|
|
1798
|
-
}
|
|
1799
|
-
else if (char === ' ' && !this.state.buffer) {
|
|
1800
|
-
// No attribute name is present. Skipping the space.
|
|
1801
|
-
continue;
|
|
1802
|
-
}
|
|
1803
|
-
else if (char === ' ' || char === '/' || char === '?') {
|
|
1804
|
-
this.state.buffer = ''; // Reset the buffer when a delimiter or potential self-closing sign is found.
|
|
1805
|
-
}
|
|
1806
|
-
else if (char === '"' || char === "'" || char === '=') {
|
|
1807
|
-
onContextChange?.(html, i, this.state.context, HtmlParser.Attribute);
|
|
1808
|
-
this.state.context = HtmlParser.Attribute;
|
|
1809
|
-
this.state.quote = char === '=' ? null : char;
|
|
1810
|
-
this.state.buffer = '';
|
|
1811
|
-
}
|
|
1812
|
-
else
|
|
1813
|
-
this.state.buffer += char;
|
|
1814
|
-
break;
|
|
1815
|
-
case HtmlParser.Attribute:
|
|
1816
|
-
// Start an attribute quote.
|
|
1817
|
-
if (!this.state.quote && !this.state.buffer.length && (char === '"' || char === "'")) {
|
|
1818
|
-
this.state.quote = char;
|
|
1819
|
-
}
|
|
1820
|
-
else if (char === this.state.quote || (!this.state.quote && this.state.buffer.length)) {
|
|
1821
|
-
onContextChange?.(html, i, this.state.context, HtmlParser.Tag);
|
|
1822
|
-
this.state.context = HtmlParser.Tag;
|
|
1823
|
-
this.state.quote = null;
|
|
1824
|
-
this.state.buffer = '';
|
|
1825
|
-
}
|
|
1826
|
-
else if (!this.state.quote && char === '>') {
|
|
1827
|
-
onContextChange?.(html, i+1, this.state.context, HtmlParser.Text);
|
|
1828
|
-
this.state.context = HtmlParser.Text;
|
|
1829
|
-
this.state.quote = null;
|
|
1830
|
-
this.state.buffer = '';
|
|
1831
|
-
}
|
|
1832
|
-
else if (char !== ' ')
|
|
1833
|
-
this.state.buffer += char;
|
|
1890
|
+
// This shaves about 5ms off the partialUpdate benchmark.
|
|
1891
|
+
result = this.nodesCache;
|
|
1892
|
+
if (result) {
|
|
1893
|
+
|
|
1894
|
+
return result
|
|
1895
|
+
}
|
|
1834
1896
|
|
|
1835
|
-
|
|
1897
|
+
result = [];
|
|
1898
|
+
let current = this.nodeBefore.nextSibling;
|
|
1899
|
+
let nodeMarker = this.nodeMarker;
|
|
1900
|
+
while (current && current !== nodeMarker) {
|
|
1901
|
+
result.push(current);
|
|
1902
|
+
current = current.nextSibling;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
this.nodesCache = result;
|
|
1906
|
+
return result;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
class PathToComponent extends Path {
|
|
1913
|
+
|
|
1914
|
+
/** @type {PathToAttribValue[]} Paths to dynamics attributes that will be set on the component.*/
|
|
1915
|
+
attribPaths;
|
|
1916
|
+
|
|
1917
|
+
constructor(nodeBefore, nodeMarker) {
|
|
1918
|
+
super(null, nodeMarker);
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
/**
|
|
1922
|
+
* Call render() on the component pointed to by this Path.
|
|
1923
|
+
* And instantiate it (from a -solarite-placeholder element) if it hasn't been done yet.
|
|
1924
|
+
* @param exprs {Expr[][]} Expressions to evaluate for each attribute to pass to the constructor.
|
|
1925
|
+
* This is different than other Path.apply() functions which only receive Expr[] and not Expr[][].
|
|
1926
|
+
* Because here we're receiving an array of arrays of expressions, one for each dynamic attribute.
|
|
1927
|
+
* @param freeNodeGroups {boolean} Used only by watch.js.
|
|
1928
|
+
* @param changed {boolean} True if the exprs have changed since the last time render() was called.*/
|
|
1929
|
+
apply(exprs, freeNodeGroups=true, changed=true) {
|
|
1930
|
+
|
|
1931
|
+
|
|
1932
|
+
|
|
1933
|
+
|
|
1934
|
+
let el = this.nodeMarker;
|
|
1935
|
+
|
|
1936
|
+
// 1. Attributes
|
|
1937
|
+
let attribs = Util.attribsToObject(el, '_is');
|
|
1938
|
+
for (let i=0, attribPath; attribPath = this.attribPaths[i]; i++) {
|
|
1939
|
+
let name = Util.dashesToCamel(attribPath.attrName);
|
|
1940
|
+
attribs[name] = attribPath.getValue(exprs[i]);
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
// 2. Instantiate component on first time.
|
|
1944
|
+
let isAttrib = el.getAttribute('_is');
|
|
1945
|
+
if (el.tagName.endsWith('-SOLARITE-PLACEHOLDER') || isAttrib) {
|
|
1946
|
+
|
|
1947
|
+
|
|
1948
|
+
// 2a. Instantiate component
|
|
1949
|
+
let tagName = (isAttrib || el.tagName.slice(0, -21)).toLowerCase(); // Remove -SOLARITE-PLACEHOLDER
|
|
1950
|
+
let Constructor = customElements.get(tagName);
|
|
1951
|
+
if (!Constructor)
|
|
1952
|
+
throw new Error(`Must call customElements.define('${tagName}', Class) before using it.`);
|
|
1953
|
+
|
|
1954
|
+
Globals$1.currentSlotChildren = [...el.childNodes]; // TODO: Does this need to be a stack?
|
|
1955
|
+
let newEl = new Constructor(attribs);
|
|
1956
|
+
|
|
1957
|
+
// 2b. Copy attributes over.
|
|
1958
|
+
if (isAttrib) {
|
|
1959
|
+
newEl.setAttribute('is', isAttrib);
|
|
1960
|
+
// el.removeAttribute('_is');
|
|
1836
1961
|
}
|
|
1962
|
+
for (let attrib of el.attributes)
|
|
1963
|
+
if (attrib.name !== '_is')
|
|
1964
|
+
newEl.setAttribute(attrib.name, attrib.value);
|
|
1965
|
+
|
|
1966
|
+
// Set dynamic attributes if they are primitive types.
|
|
1967
|
+
for (let name in attribs) {
|
|
1968
|
+
let val = attribs[name];
|
|
1969
|
+
let valType = typeof val;
|
|
1970
|
+
if (valType === 'boolean') {
|
|
1971
|
+
if (val !== false && val !== undefined && val !== null) // Util.isFalsy() inlined
|
|
1972
|
+
newEl.setAttribute(name, '');
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
// If type is a non-boolean primitive, set the attribute value.
|
|
1976
|
+
else if (valType==='string' || valType === 'number' || valType==='bigint')
|
|
1977
|
+
newEl.setAttribute(name, val);
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
|
|
1981
|
+
// 2c. If an id pointed at the placeholder, update it to point to the new element.
|
|
1982
|
+
let id = newEl.getAttribute('data-id') || newEl.getAttribute('id');
|
|
1983
|
+
if (id)
|
|
1984
|
+
delve(this.parentNg.getRootNode(), id.split(/\./g), newEl);
|
|
1985
|
+
|
|
1986
|
+
// 2d. Update paths to use replaced element.
|
|
1987
|
+
let ng = this.parentNg;
|
|
1988
|
+
this.nodeMarker = newEl;
|
|
1989
|
+
for (let path of ng.paths) {
|
|
1990
|
+
if (path.nodeMarker === el)
|
|
1991
|
+
path.nodeMarker = newEl;
|
|
1992
|
+
if (path.nodeBefore === el)
|
|
1993
|
+
path.nodeBefore = newEl;
|
|
1994
|
+
}
|
|
1995
|
+
if (ng.startNode === el)
|
|
1996
|
+
ng.startNode = newEl;
|
|
1997
|
+
if (ng.endNode === el)
|
|
1998
|
+
ng.endNode = newEl;
|
|
1999
|
+
|
|
2000
|
+
// 2f. Call render() if it wasn't called by the constructor.
|
|
2001
|
+
// This must happen before we add it to the DOM which can trigger connectedCallback() -> renderFirstTime()
|
|
2002
|
+
// Because that path renders it without the attribute expressions.
|
|
2003
|
+
if (typeof newEl.render === 'function' && !Globals$1.rendered.has(newEl))
|
|
2004
|
+
newEl.render(attribs, changed);
|
|
2005
|
+
|
|
2006
|
+
// 2g. Update attribute paths to use the new element and re-apply them.
|
|
2007
|
+
for (let i=0, attribPath; attribPath = this.attribPaths[i]; i++) {
|
|
2008
|
+
attribPath.parentNg = this.parentNg;
|
|
2009
|
+
attribPath.nodeMarker = newEl;
|
|
2010
|
+
attribPath.apply(exprs[i]);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
// 2e. Swap it to the DOM.
|
|
2014
|
+
el.replaceWith(newEl);
|
|
1837
2015
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
2016
|
+
|
|
2017
|
+
// 2f. Render
|
|
2018
|
+
else if (typeof el.render === 'function')
|
|
2019
|
+
el.render(attribs, changed);
|
|
2020
|
+
|
|
2021
|
+
Globals$1.currentSlotChildren = null;
|
|
1840
2022
|
}
|
|
1841
|
-
}
|
|
1842
2023
|
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
2024
|
+
/**
|
|
2025
|
+
* @param newRoot {HTMLElement}
|
|
2026
|
+
* @param pathOffset {int}
|
|
2027
|
+
* @return {Path} */
|
|
2028
|
+
clone(newRoot, pathOffset=0) {
|
|
2029
|
+
|
|
2030
|
+
let nodeMarker = this.getNewNodeMarker(newRoot, pathOffset);
|
|
2031
|
+
let result = new PathToComponent(null, nodeMarker);
|
|
2032
|
+
result.attribPaths = this.attribPaths.map(path => path.clone(newRoot, pathOffset));
|
|
2033
|
+
|
|
2034
|
+
|
|
2035
|
+
|
|
2036
|
+
return result;
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
getExpressionCount() { return 0 }
|
|
2040
|
+
|
|
2041
|
+
|
|
2042
|
+
}
|
|
1846
2043
|
|
|
1847
2044
|
/**
|
|
1848
2045
|
* A Shell is created from a tagged template expression instantiated as Nodes,
|
|
@@ -1857,10 +2054,10 @@ class Shell {
|
|
|
1857
2054
|
* @type {DocumentFragment|Text} DOM parent of the shell's nodes. */
|
|
1858
2055
|
fragment;
|
|
1859
2056
|
|
|
1860
|
-
/** @type {
|
|
2057
|
+
/** @type {Path[]} Paths to where expressions should go. */
|
|
1861
2058
|
paths = [];
|
|
1862
2059
|
|
|
1863
|
-
// Elements with events.
|
|
2060
|
+
// Elements with events. Is there a reason to use this? We already mark event Exprs in Shell.js.
|
|
1864
2061
|
// events = [];
|
|
1865
2062
|
|
|
1866
2063
|
/** @type {int[][]} Array of paths */
|
|
@@ -1872,14 +2069,6 @@ class Shell {
|
|
|
1872
2069
|
/** @type {int[][]} Array of paths */
|
|
1873
2070
|
styles = [];
|
|
1874
2071
|
|
|
1875
|
-
/** @type {int[][]} Array of paths. Used by activateEmbeds() to quickly find components. */
|
|
1876
|
-
staticComponents = [];
|
|
1877
|
-
|
|
1878
|
-
/** @type {{path:int[], attribs:Record<string, string>}[]} */
|
|
1879
|
-
//componentAttribs = [];
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
2072
|
/**
|
|
1884
2073
|
* Create the nodes but without filling in the expressions.
|
|
1885
2074
|
* This is useful because the expression-less nodes created by a template can be cached.
|
|
@@ -1890,43 +2079,52 @@ class Shell {
|
|
|
1890
2079
|
|
|
1891
2080
|
|
|
1892
2081
|
|
|
2082
|
+
// If no html tags or entities, just create a text node.
|
|
1893
2083
|
if (html.length === 1 && !html[0].match(/[<&]/)) {
|
|
1894
|
-
this.fragment =
|
|
2084
|
+
this.fragment = Globals$1.doc.createTextNode(html[0]);
|
|
1895
2085
|
return;
|
|
1896
2086
|
}
|
|
1897
2087
|
|
|
1898
2088
|
|
|
1899
2089
|
// 1. Add placeholders
|
|
1900
|
-
let
|
|
2090
|
+
let htmlWithPlaceholders = Shell.addPlaceholders(html);
|
|
1901
2091
|
|
|
1902
|
-
let template =
|
|
1903
|
-
if (
|
|
1904
|
-
template.innerHTML =
|
|
2092
|
+
let template = Globals$1.doc.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
|
|
2093
|
+
if (htmlWithPlaceholders)
|
|
2094
|
+
template.innerHTML = htmlWithPlaceholders;
|
|
1905
2095
|
else // Create one text node, so shell isn't empty and NodeGroups created from it have something to point the startNode and endNode at.
|
|
1906
|
-
template.content.append(
|
|
2096
|
+
template.content.append(Globals$1.doc.createTextNode(''));
|
|
1907
2097
|
this.fragment = template.content;
|
|
1908
2098
|
|
|
1909
2099
|
// 2. Find placeholders
|
|
1910
2100
|
let node;
|
|
1911
2101
|
let toRemove = [];
|
|
1912
2102
|
let placeholdersUsed = 0;
|
|
1913
|
-
const walker =
|
|
2103
|
+
const walker = Globals$1.doc.createTreeWalker(this.fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
|
|
1914
2104
|
while (node = walker.nextNode()) {
|
|
1915
2105
|
|
|
1916
|
-
// Remove previous after each iteration, so paths will still be calculated correctly.
|
|
2106
|
+
// Remove previous elements after each iteration, so paths will still be calculated correctly.
|
|
1917
2107
|
toRemove.map(el => el.remove());
|
|
1918
2108
|
toRemove = [];
|
|
1919
2109
|
|
|
1920
2110
|
// Replace attributes
|
|
1921
2111
|
if (node.nodeType === 1) {
|
|
1922
|
-
|
|
2112
|
+
const hasIs = node.hasAttribute('is');
|
|
2113
|
+
const isComponent = (hasIs || node.tagName.includes('-'));
|
|
2114
|
+
const componentAttribPaths = [];
|
|
2115
|
+
|
|
2116
|
+
for (let attr of [...node.attributes]) { // Copy the attributes array b/c we remove attributes with placeholders as we go.
|
|
1923
2117
|
|
|
1924
2118
|
// Whole attribute
|
|
1925
2119
|
let matches = attr.name.match(/^[\ue000-\uf8ff]$/);
|
|
1926
2120
|
if (matches) {
|
|
1927
|
-
|
|
2121
|
+
let path = new PathToAttribs(null, node);
|
|
2122
|
+
this.paths.push(path);
|
|
2123
|
+
if (isComponent)
|
|
2124
|
+
componentAttribPaths.push(path);
|
|
2125
|
+
|
|
1928
2126
|
placeholdersUsed ++;
|
|
1929
|
-
node.removeAttribute(matches[0]);
|
|
2127
|
+
node.removeAttribute(matches[0]); // TODO: Is this necessary?
|
|
1930
2128
|
}
|
|
1931
2129
|
|
|
1932
2130
|
// Just the attribute value.
|
|
@@ -1934,22 +2132,46 @@ class Shell {
|
|
|
1934
2132
|
let parts = attr.value.split(/[\ue000-\uf8ff]/g);
|
|
1935
2133
|
if (parts.length > 1) {
|
|
1936
2134
|
let nonEmptyParts = (parts.length === 2 && !parts[0].length && !parts[1].length) ? null : parts;
|
|
1937
|
-
let type = Util.isEvent(attr.name) ? ExprPathType.Event : ExprPathType.AttribValue;
|
|
1938
2135
|
|
|
1939
|
-
|
|
2136
|
+
let path = Util.isEvent(attr.name)
|
|
2137
|
+
? new PathToEvent(null, node, attr.name, nonEmptyParts)
|
|
2138
|
+
: new PathToAttribValue(null, node, attr.name, nonEmptyParts);
|
|
2139
|
+
path.isHtmlProperty = Util.isHtmlProp(node, attr.name);
|
|
2140
|
+
this.paths.push(path);
|
|
2141
|
+
if (isComponent) {
|
|
2142
|
+
path.isComponentAttrib = true;
|
|
2143
|
+
componentAttribPaths.push(path);
|
|
2144
|
+
}
|
|
2145
|
+
|
|
1940
2146
|
placeholdersUsed += parts.length - 1;
|
|
1941
2147
|
node.setAttribute(attr.name, parts.join(''));
|
|
1942
2148
|
}
|
|
1943
2149
|
}
|
|
1944
2150
|
}
|
|
2151
|
+
|
|
2152
|
+
// Web components
|
|
2153
|
+
if (isComponent) {
|
|
2154
|
+
let path = new PathToComponent(null, node);
|
|
2155
|
+
path.attribPaths = componentAttribPaths;
|
|
2156
|
+
this.paths.splice(this.paths.length - componentAttribPaths.length, 0, path); // Insert before its componentAttribPaths
|
|
2157
|
+
|
|
2158
|
+
if (hasIs) {
|
|
2159
|
+
node.setAttribute('_is', node.getAttribute('is'));
|
|
2160
|
+
node.removeAttribute('is');
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
1945
2163
|
}
|
|
2164
|
+
|
|
1946
2165
|
// Replace comment placeholders
|
|
1947
2166
|
else if (node.nodeType === 8 && node.nodeValue === '!✨!') {
|
|
1948
2167
|
|
|
2168
|
+
if (node?.parentNode?.closest && node?.parentNode?.closest('[contenteditable]'))
|
|
2169
|
+
throw new Error(`Contenteditable can't have expressions inside them. Use <div contenteditable value="\${...}"> instead.`);
|
|
2170
|
+
|
|
1949
2171
|
// Get or create nodeBefore.
|
|
1950
2172
|
let nodeBefore = node.previousSibling; // Can be the same as another Path's nodeMarker.
|
|
1951
2173
|
if (!nodeBefore) {
|
|
1952
|
-
nodeBefore =
|
|
2174
|
+
nodeBefore = Globals$1.doc.createComment('Path:'+this.paths.length);
|
|
1953
2175
|
node.parentNode.insertBefore(nodeBefore, node);
|
|
1954
2176
|
}
|
|
1955
2177
|
|
|
@@ -1965,49 +2187,48 @@ class Shell {
|
|
|
1965
2187
|
// Re-use existing comment placeholder.
|
|
1966
2188
|
else {
|
|
1967
2189
|
nodeMarker = node;
|
|
1968
|
-
nodeMarker.textContent = '
|
|
2190
|
+
nodeMarker.textContent = 'PathEnd:'+ this.paths.length;
|
|
1969
2191
|
}
|
|
1970
2192
|
|
|
1971
2193
|
|
|
1972
|
-
let path = new
|
|
2194
|
+
let path = new PathToNodes(nodeBefore, nodeMarker);
|
|
1973
2195
|
this.paths.push(path);
|
|
1974
2196
|
placeholdersUsed ++;
|
|
1975
2197
|
}
|
|
1976
2198
|
|
|
2199
|
+
// Comments become text nodes when inside textareas.
|
|
1977
2200
|
else if (node.nodeType === 3 && node.parentNode?.tagName === 'TEXTAREA' && node.textContent.includes('<!--!✨!-->'))
|
|
1978
2201
|
throw new Error(`Textarea can't have expressions inside them. Use <textarea value="\${...}"> instead.`);
|
|
1979
|
-
|
|
1980
2202
|
|
|
1981
2203
|
|
|
1982
2204
|
// Sometimes users will comment out a block of html code that has expressions.
|
|
1983
2205
|
// Here we look for expressions in comments.
|
|
1984
2206
|
// We don't actually update them dynamically, but we still add paths for them.
|
|
1985
2207
|
// That way the expression count still matches.
|
|
1986
|
-
else if (node.nodeType === Node.COMMENT_NODE
|
|
2208
|
+
else if (node.nodeType === 8) { // Node.COMMENT_NODE
|
|
1987
2209
|
let parts = node.textContent.split(/[\ue000-\uf8ff]/g);
|
|
1988
2210
|
for (let i=0; i<parts.length-1; i++) {
|
|
1989
|
-
let path = new
|
|
1990
|
-
path.type = ExprPathType.Comment;
|
|
2211
|
+
let path = new Path(node.previousSibling, node);
|
|
1991
2212
|
this.paths.push(path);
|
|
1992
2213
|
placeholdersUsed ++;
|
|
1993
2214
|
}
|
|
1994
2215
|
}
|
|
1995
2216
|
|
|
1996
2217
|
// Replace comment placeholders inside script and style tags, which have become text nodes.
|
|
1997
|
-
else if (node.nodeType ===
|
|
2218
|
+
else if (node.nodeType === 3 && ['SCRIPT', 'STYLE'].includes(node.parentNode?.nodeName)) { // Node.TEXT_NODE
|
|
1998
2219
|
let parts = node.textContent.split(commentPlaceholder);
|
|
1999
2220
|
if (parts.length > 1) {
|
|
2000
2221
|
|
|
2001
2222
|
let placeholders = [];
|
|
2002
2223
|
for (let i = 0; i<parts.length; i++) {
|
|
2003
|
-
let current =
|
|
2224
|
+
let current = Globals$1.doc.createTextNode(parts[i]);
|
|
2004
2225
|
node.parentNode.insertBefore(current, node);
|
|
2005
2226
|
if (i > 0)
|
|
2006
2227
|
placeholders.push(current);
|
|
2007
2228
|
}
|
|
2008
2229
|
|
|
2009
2230
|
for (let i=0, node; node=placeholders[i]; i++) {
|
|
2010
|
-
let path = new
|
|
2231
|
+
let path = new PathToNodes(node.previousSibling, node);
|
|
2011
2232
|
this.paths.push(path);
|
|
2012
2233
|
placeholdersUsed ++;
|
|
2013
2234
|
|
|
@@ -2026,51 +2247,31 @@ class Shell {
|
|
|
2026
2247
|
if (placeholdersUsed !== html.length-1)
|
|
2027
2248
|
throw new Error(`Could not parse expressions in template. Check for duplicate attributes or malformed html: ${html.join('${...}')}`);
|
|
2028
2249
|
|
|
2029
|
-
// Handle solarite-placeholder's.
|
|
2030
|
-
|
|
2031
|
-
// 3. Rename "is" attributes so the Web Components don't instantiate until we have the values of their PathExpr arguments.
|
|
2032
|
-
// that happens in NodeGroup.applyComponentExprs()
|
|
2033
|
-
for (let el of this.fragment.querySelectorAll('[is]'))
|
|
2034
|
-
el.setAttribute('_is', el.getAttribute('is'));
|
|
2035
|
-
|
|
2036
2250
|
for (let path of this.paths) {
|
|
2037
2251
|
if (path.nodeBefore)
|
|
2038
2252
|
path.nodeBeforeIndex = Array.prototype.indexOf.call(path.nodeBefore.parentNode.childNodes, path.nodeBefore);
|
|
2039
|
-
path.nodeMarkerPath = getNodePath(path.nodeMarker);
|
|
2040
2253
|
|
|
2041
|
-
//
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
}
|
|
2254
|
+
// Must be calculated after we remove the toRemove nodes:
|
|
2255
|
+
path.nodeMarkerPath = Path.get(path.nodeMarker);
|
|
2256
|
+
|
|
2257
|
+
|
|
2046
2258
|
}
|
|
2047
2259
|
|
|
2048
2260
|
this.findEmbeds();
|
|
2049
2261
|
|
|
2262
|
+
|
|
2050
2263
|
|
|
2051
2264
|
}
|
|
2052
2265
|
|
|
2053
2266
|
/**
|
|
2054
2267
|
* 1. Add a Unicode placeholder char for where expressions go within attributes.
|
|
2055
2268
|
* 2. Add a comment placeholder for where expressions are children of other nodes.
|
|
2056
|
-
* 3. Append -solarite-placeholder to the tag names of custom components so that we can
|
|
2269
|
+
* 3. Append -solarite-placeholder to the tag names of custom components so that we can instantiate them later
|
|
2270
|
+
* when we can manually call their constructors with the proper attribute and children arguments from evaluated expressions.
|
|
2057
2271
|
* @param htmlChunks {string[]}
|
|
2058
|
-
* @returns {string} */
|
|
2272
|
+
* @returns {string} Html with the placeholders in place. */
|
|
2059
2273
|
static addPlaceholders(htmlChunks) {
|
|
2060
|
-
let
|
|
2061
|
-
|
|
2062
|
-
function addToken(token, context) {
|
|
2063
|
-
|
|
2064
|
-
if (context === HtmlParser.Tag) {
|
|
2065
|
-
// Find Solarite Components tags and append -solarite-placeholder to their tag names
|
|
2066
|
-
// and give them a solarite-placeholder attribute so we can easily find them later.
|
|
2067
|
-
// This way we can gather their constructor arguments and their children before we call their constructor.
|
|
2068
|
-
// Later, NodeGroup.instantiateComponent() will replace them with the real components.
|
|
2069
|
-
// Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
|
|
2070
|
-
token = token.replace(/^<\/?[a-z][a-z0-9]*-[a-z0-9-]+/i, match => match + '-solarite-placeholder solarite-placeholder');
|
|
2071
|
-
}
|
|
2072
|
-
tokens.push(token);
|
|
2073
|
-
}
|
|
2274
|
+
let result = [];
|
|
2074
2275
|
|
|
2075
2276
|
let htmlParser = new HtmlParser(); // Reset the context.
|
|
2076
2277
|
for (let i = 0; i < htmlChunks.length; i++) {
|
|
@@ -2078,10 +2279,20 @@ class Shell {
|
|
|
2078
2279
|
|
|
2079
2280
|
// Append -solarite-placholder to web component tags, so we can pass args to them when they're instantiated.
|
|
2080
2281
|
let lastIndex = 0;
|
|
2081
|
-
let context = htmlParser.parse(lastHtml, (html, index,
|
|
2282
|
+
let context = htmlParser.parse(lastHtml, (html, index, prevContext/*, nextContext*/) => { // This function is called every time the html context changes.
|
|
2082
2283
|
if (lastIndex !== index) {
|
|
2083
2284
|
let token = html.slice(lastIndex, index);
|
|
2084
|
-
|
|
2285
|
+
|
|
2286
|
+
if (prevContext === HtmlParser.Tag) {
|
|
2287
|
+
// Find Web Component tags and append -solarite-placeholder to their tag names
|
|
2288
|
+
// This way we can gather their constructor arguments and their children before we call their constructor.
|
|
2289
|
+
// Later, PathToComponent.apply() will replace them with the real components.
|
|
2290
|
+
// Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
|
|
2291
|
+
const isWebComponentTagName = /^<\/?[a-z][a-z0-9]*-[a-z0-9-]+/i; // a dash in the middle
|
|
2292
|
+
token = token.replace(isWebComponentTagName, match => match + '-SOLARITE-PLACEHOLDER'); // caps to match other instances of this string, for better compression.
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
result.push(token);
|
|
2085
2296
|
}
|
|
2086
2297
|
lastIndex = index;
|
|
2087
2298
|
});
|
|
@@ -2089,13 +2300,13 @@ class Shell {
|
|
|
2089
2300
|
// Insert placeholders
|
|
2090
2301
|
if (i < htmlChunks.length - 1) {
|
|
2091
2302
|
if (context === HtmlParser.Text)
|
|
2092
|
-
|
|
2303
|
+
result.push(commentPlaceholder); // Comment Placeholder. because we can't put text in between <tr> tags for example.
|
|
2093
2304
|
else
|
|
2094
|
-
|
|
2305
|
+
result.push(String.fromCharCode(attribPlaceholder + i));
|
|
2095
2306
|
}
|
|
2096
2307
|
}
|
|
2097
2308
|
|
|
2098
|
-
return
|
|
2309
|
+
return result.join('');
|
|
2099
2310
|
}
|
|
2100
2311
|
|
|
2101
2312
|
/**
|
|
@@ -2107,10 +2318,10 @@ class Shell {
|
|
|
2107
2318
|
* this.ids
|
|
2108
2319
|
* this.staticComponents */
|
|
2109
2320
|
findEmbeds() {
|
|
2110
|
-
this.scripts = Array.prototype.map.call(this.fragment.querySelectorAll('scripts'), el =>
|
|
2321
|
+
this.scripts = Array.prototype.map.call(this.fragment.querySelectorAll('scripts'), el => Path.get(el));
|
|
2111
2322
|
|
|
2112
|
-
// TODO: only find styles that have
|
|
2113
|
-
this.styles = Array.prototype.map.call(this.fragment.querySelectorAll('style'), el =>
|
|
2323
|
+
// TODO: only find styles that have Paths in them?
|
|
2324
|
+
this.styles = Array.prototype.map.call(this.fragment.querySelectorAll('style'), el => Path.get(el));
|
|
2114
2325
|
|
|
2115
2326
|
let idEls = this.fragment.querySelectorAll('[id],[data-id]');
|
|
2116
2327
|
|
|
@@ -2121,17 +2332,7 @@ class Shell {
|
|
|
2121
2332
|
throw new Error(`<${el.tagName.toLowerCase()} id="${id}"> can't override existing HTMLElement id property.`)
|
|
2122
2333
|
}
|
|
2123
2334
|
|
|
2124
|
-
this.ids = Array.prototype.map.call(idEls, el =>
|
|
2125
|
-
|
|
2126
|
-
for (let el of this.fragment.querySelectorAll('*')) {
|
|
2127
|
-
if (el.tagName.includes('-') || el.hasAttribute('_is'))
|
|
2128
|
-
|
|
2129
|
-
// Dynamic components are components that have attributes with expression values.
|
|
2130
|
-
// They are created from applyExprs()
|
|
2131
|
-
// But static components are created in a separate path inside the NodeGroup constructor.
|
|
2132
|
-
if (!this.paths.find(path => path.nodeMarker === el))
|
|
2133
|
-
this.staticComponents.push(getNodePath(el));
|
|
2134
|
-
}
|
|
2335
|
+
this.ids = Array.prototype.map.call(idEls, el => Path.get(el));
|
|
2135
2336
|
}
|
|
2136
2337
|
|
|
2137
2338
|
/**
|
|
@@ -2166,17 +2367,14 @@ const attribPlaceholder = 0xe000; // https://en.wikipedia.org/wiki/Private_Use_A
|
|
|
2166
2367
|
*
|
|
2167
2368
|
* The range is determined by startNode and nodeMarker.
|
|
2168
2369
|
* startNode - never null. An empty text node is created before the first path if none exists.
|
|
2169
|
-
* nodeMarker - null if this Nodegroup is at the end of its parents' nodes
|
|
2170
|
-
*
|
|
2171
|
-
*
|
|
2172
|
-
* */
|
|
2370
|
+
* nodeMarker - null if this Nodegroup is at the end of its parents' nodes.*/
|
|
2173
2371
|
class NodeGroup {
|
|
2174
2372
|
|
|
2175
2373
|
/**
|
|
2176
2374
|
* @Type {RootNodeGroup} */
|
|
2177
2375
|
rootNg;
|
|
2178
2376
|
|
|
2179
|
-
/** @type {
|
|
2377
|
+
/** @type {Path} */
|
|
2180
2378
|
parentPath;
|
|
2181
2379
|
|
|
2182
2380
|
/** @type {Node|HTMLElement} First node of NodeGroup. Should never be null. */
|
|
@@ -2184,10 +2382,10 @@ class NodeGroup {
|
|
|
2184
2382
|
|
|
2185
2383
|
/** @type {Node|HTMLElement} A node that never changes that this NodeGroup should always insert its nodes before.
|
|
2186
2384
|
* An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.
|
|
2187
|
-
* TODO: But sometimes startNode and endNode point to the same node. Document
|
|
2385
|
+
* TODO: But sometimes startNode and endNode point to the same node. Document this inconsistency. */
|
|
2188
2386
|
endNode;
|
|
2189
2387
|
|
|
2190
|
-
/** @type {
|
|
2388
|
+
/** @type {Path[]} */
|
|
2191
2389
|
paths = [];
|
|
2192
2390
|
|
|
2193
2391
|
/** @type {string} Key that matches the template and the expressions. */
|
|
@@ -2207,299 +2405,212 @@ class NodeGroup {
|
|
|
2207
2405
|
* @type {?Map<HTMLStyleElement, string>} */
|
|
2208
2406
|
styles;
|
|
2209
2407
|
|
|
2408
|
+
/** @type {Template} */
|
|
2409
|
+
template;
|
|
2210
2410
|
|
|
2211
2411
|
/**
|
|
2212
|
-
*
|
|
2213
|
-
*
|
|
2214
|
-
* @
|
|
2215
|
-
|
|
2216
|
-
if (!(this instanceof RootNodeGroup)) {
|
|
2217
|
-
|
|
2218
|
-
let [fragment, shell] = this.init(template, parentPath);
|
|
2219
|
-
|
|
2220
|
-
if (fragment && template.exprs.length) {
|
|
2221
|
-
this.updatePaths(fragment, shell.paths);
|
|
2222
|
-
|
|
2223
|
-
// Static web components can sometimes have children created via expressions.
|
|
2224
|
-
// But calling applyExprs() will mess up the shell's path to them.
|
|
2225
|
-
// So we find them first, then call activateStaticComponents() after their children have been created.
|
|
2226
|
-
let staticComponents = this.findStaticComponents(fragment, shell);
|
|
2227
|
-
|
|
2228
|
-
this.activateEmbeds(fragment, shell);
|
|
2229
|
-
|
|
2230
|
-
// Apply exprs
|
|
2231
|
-
this.applyExprs(template.exprs);
|
|
2412
|
+
* Root node at the top of the hierarchy.
|
|
2413
|
+
* Should be moved to RootNodeGroup
|
|
2414
|
+
* @type {HTMLElement} */
|
|
2415
|
+
root;
|
|
2232
2416
|
|
|
2233
|
-
this.instantiateStaticComponents(staticComponents);
|
|
2234
|
-
}
|
|
2235
|
-
else if (shell)
|
|
2236
|
-
this.activateEmbeds(fragment, shell);
|
|
2237
|
-
}
|
|
2238
|
-
}
|
|
2239
2417
|
|
|
2240
2418
|
/**
|
|
2241
|
-
*
|
|
2242
|
-
*
|
|
2419
|
+
* Create an "instantiated" NodeGroup from a Template and add it to an element.
|
|
2420
|
+
* Don't call applyExprs() yet to apply expressions or instantiate components yet.
|
|
2243
2421
|
* @param template {Template} Create it from the html strings and expressions in this template.
|
|
2244
|
-
* @param parentPath {?
|
|
2245
|
-
* @param
|
|
2246
|
-
* @param
|
|
2247
|
-
|
|
2248
|
-
init(template, parentPath=null, exactKey=null, closeKey=null) {
|
|
2249
|
-
this.exactKey = exactKey || template.getExactKey();
|
|
2250
|
-
this.closeKey = closeKey || template.getCloseKey();
|
|
2251
|
-
|
|
2252
|
-
this.parentPath = parentPath;
|
|
2422
|
+
* @param parentPath {?Path}
|
|
2423
|
+
* @param el {?HTMLElement} Optional, pre-existing htmlElement that will be the root.
|
|
2424
|
+
* @param options {?object} Only used for RootNodeGroup */
|
|
2425
|
+
constructor(template, parentPath=null, el=null, options=null) {
|
|
2253
2426
|
this.rootNg = parentPath?.parentNg?.rootNg || this;
|
|
2427
|
+
this.parentPath = parentPath;
|
|
2254
2428
|
|
|
2255
2429
|
|
|
2256
|
-
|
|
2257
|
-
/** @type {Template} */
|
|
2258
2430
|
this.template = template;
|
|
2259
|
-
|
|
2260
|
-
// new! Is this needed?
|
|
2261
|
-
template.nodeGroup = this;
|
|
2262
|
-
|
|
2263
|
-
// Get a cached version of the parsed and instantiated html, and ExprPaths.
|
|
2431
|
+
this.closeKey = template.getCloseKey();
|
|
2264
2432
|
|
|
2265
2433
|
// If it's just a text node, skip a bunch of unnecessary steps.
|
|
2266
|
-
if (
|
|
2267
|
-
|
|
2268
|
-
let textNode = document.createTextNode(template.html[0]);
|
|
2269
|
-
|
|
2270
|
-
this.startNode = this.endNode = textNode;
|
|
2271
|
-
return [];
|
|
2272
|
-
}
|
|
2273
|
-
else {
|
|
2274
|
-
let shell = Shell.get(template.html);
|
|
2275
|
-
let fragment = shell.fragment.cloneNode(true);
|
|
2276
|
-
|
|
2277
|
-
if (fragment instanceof DocumentFragment) {
|
|
2278
|
-
let childNodes = fragment.childNodes;
|
|
2279
|
-
this.startNode = childNodes[0];
|
|
2280
|
-
this.endNode = childNodes[childNodes.length - 1];
|
|
2281
|
-
}
|
|
2282
|
-
else {
|
|
2283
|
-
this.startNode = this.endNode = fragment;
|
|
2284
|
-
}
|
|
2285
|
-
return [fragment, shell];
|
|
2434
|
+
if (template.isText) {
|
|
2435
|
+
this.startNode = this.endNode = Globals$1.doc.createTextNode(template.html[0]);
|
|
2286
2436
|
}
|
|
2287
|
-
}
|
|
2288
|
-
|
|
2289
|
-
/**
|
|
2290
|
-
* Use the paths to insert the given expressions.
|
|
2291
|
-
* Dispatches expression handling to other functions depending on the path type.
|
|
2292
|
-
* @param exprs {(*|*[]|function|Template)[]}
|
|
2293
|
-
* @param paths {?ExprPath[]} Optional. Only used for testing. Normally uses this.paths. */
|
|
2294
|
-
applyExprs(exprs, paths=null) {
|
|
2295
|
-
paths = paths || this.paths;
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
2437
|
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
// before its instantiated and its value attribute is set via an expression.
|
|
2438
|
+
else {
|
|
2439
|
+
// Get a cached version of the parsed and instantiated html, and Paths:
|
|
2440
|
+
const shell = Shell.get(template.html);
|
|
2441
|
+
const shellFragment = shell.fragment.cloneNode(true);
|
|
2304
2442
|
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
let nextPath = paths[i + 1];
|
|
2443
|
+
if (shellFragment.nodeType === 11) { // DocumentFragment
|
|
2444
|
+
this.startNode = shellFragment.firstChild;
|
|
2445
|
+
this.endNode = shellFragment.lastChild;
|
|
2446
|
+
} else
|
|
2447
|
+
this.startNode = this.endNode = shellFragment;
|
|
2311
2448
|
|
|
2312
|
-
// Get the expressions associated with this path.
|
|
2313
|
-
if (path.attrValue?.length > 2) {
|
|
2314
|
-
let startIndex = (exprIndex - (path.attrValue.length - 1)) + 1;
|
|
2315
|
-
pathExprs[i] = exprs.slice(startIndex, exprIndex + 1); // probably doesn't allocate if the JS vm implements copy on write.
|
|
2316
|
-
exprIndex -= pathExprs[i].length;
|
|
2317
|
-
} else {
|
|
2318
|
-
pathExprs[i] = [exprs[exprIndex]];
|
|
2319
|
-
exprIndex--;
|
|
2320
|
-
}
|
|
2321
2449
|
|
|
2322
|
-
//
|
|
2323
|
-
|
|
2324
|
-
// But the dynamicAttribsAdjacet test already passes.
|
|
2450
|
+
// Special setup for RootNodeGroup
|
|
2451
|
+
if (this instanceof RootNodeGroup) {
|
|
2325
2452
|
|
|
2326
|
-
// If expr is an attribute in a component:
|
|
2327
|
-
// 1. Instantiate it if it hasn't already been, sending all expr's to its constructor.
|
|
2328
|
-
// 2. Otherwise send them to its render function.
|
|
2329
|
-
// Components with no expressions as attributes are instead activated in activateEmbeds().
|
|
2330
|
-
if (path.nodeMarker !== this.rootNg.root && path.isComponent()) {
|
|
2331
2453
|
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2454
|
+
let startingPathDepth = 0;
|
|
2455
|
+
this.options = options;
|
|
2456
|
+
if (shellFragment instanceof Text) {
|
|
2457
|
+
if (!el)
|
|
2458
|
+
throw new Error('Cannot create a standalone text node');
|
|
2335
2459
|
|
|
2336
|
-
|
|
2460
|
+
this.root = el;
|
|
2461
|
+
if (shellFragment.nodeValue.length)
|
|
2462
|
+
this.root.append(shellFragment);
|
|
2463
|
+
}
|
|
2337
2464
|
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2465
|
+
else {
|
|
2466
|
+
if (el) {
|
|
2467
|
+
this.root = el;
|
|
2468
|
+
|
|
2469
|
+
// Save slot
|
|
2470
|
+
// 1. Globals.currentSlotChildren is set if this is called via PathToComponent.applyComponent() calls render()
|
|
2471
|
+
// 2. el.childNodes is set if render() is called manually for the first time.
|
|
2472
|
+
let slotChildren;
|
|
2473
|
+
if (Globals$1.currentSlotChildren || el.childNodes.length) {
|
|
2474
|
+
slotChildren = Globals$1.doc.createDocumentFragment();
|
|
2475
|
+
slotChildren.append(...(Globals$1.currentSlotChildren || el.childNodes));
|
|
2476
|
+
}
|
|
2343
2477
|
|
|
2344
|
-
|
|
2478
|
+
// If el should replace the root node of the fragment.
|
|
2479
|
+
if (isReplaceEl(shellFragment, this.root.tagName)) {
|
|
2480
|
+
this.root.append(...shellFragment.children[0].childNodes);
|
|
2345
2481
|
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
}
|
|
2482
|
+
// Copy attributes
|
|
2483
|
+
for (let attrib of shellFragment.children[0].attributes)
|
|
2484
|
+
if (!this.root.hasAttribute(attrib.name))
|
|
2485
|
+
this.root.setAttribute(attrib.name, attrib.value);
|
|
2351
2486
|
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2487
|
+
// Go one level deeper into all of shell's paths.
|
|
2488
|
+
startingPathDepth = 1;
|
|
2489
|
+
}
|
|
2355
2490
|
|
|
2491
|
+
else {
|
|
2492
|
+
let isEmpty = shellFragment.childNodes.length === 1 && shellFragment.childNodes[0].nodeType === 3 && shellFragment.childNodes[0].textContent === '';
|
|
2493
|
+
if (!isEmpty)
|
|
2494
|
+
this.root.append(...shellFragment.childNodes);
|
|
2495
|
+
}
|
|
2356
2496
|
|
|
2357
|
-
} // end for(path of this.paths)
|
|
2358
2497
|
|
|
2498
|
+
// Setup slot children (deprecated)
|
|
2499
|
+
if (slotChildren) {
|
|
2500
|
+
// Named slots
|
|
2501
|
+
for (let slot of el.querySelectorAll('slot[name]')) {
|
|
2502
|
+
let name = slot.getAttribute('name');
|
|
2503
|
+
if (name) {
|
|
2504
|
+
let slotChildren2 = slotChildren.querySelectorAll(`[slot='${name}']`);
|
|
2505
|
+
slot.append(...slotChildren2);
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
// Unnamed slots
|
|
2509
|
+
let unamedSlot = el.querySelector('slot:not([name])');
|
|
2510
|
+
if (unamedSlot)
|
|
2511
|
+
unamedSlot.append(slotChildren);
|
|
2512
|
+
// No slots
|
|
2513
|
+
else
|
|
2514
|
+
el.append(slotChildren);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2359
2517
|
|
|
2360
|
-
|
|
2361
|
-
|
|
2518
|
+
// Instantiate as a standalone element.
|
|
2519
|
+
else {
|
|
2520
|
+
let onlyChild = getSingleEl(shellFragment);
|
|
2521
|
+
this.root = onlyChild || shellFragment; // We return the whole fragment when calling h() with a collection of nodes.
|
|
2522
|
+
if (onlyChild)
|
|
2523
|
+
startingPathDepth = 1;
|
|
2524
|
+
}
|
|
2362
2525
|
|
|
2526
|
+
// Exclude the path to ourself. Otherwise we get infinite recursion.
|
|
2527
|
+
// let paths = [...shell.paths];
|
|
2528
|
+
// if (paths[0] instanceof PathToComponent)
|
|
2529
|
+
// paths.shift();
|
|
2363
2530
|
|
|
2531
|
+
this.setPathsFromFragment(this.root, shell.paths, startingPathDepth);
|
|
2532
|
+
this.activateEmbeds(this.root, shell, startingPathDepth);
|
|
2533
|
+
}
|
|
2534
|
+
this.startNode = this.endNode = this.root;
|
|
2364
2535
|
|
|
2365
|
-
|
|
2366
|
-
|
|
2536
|
+
Globals$1.rootNodeGroups.set(this.root, this);
|
|
2537
|
+
} // end if RootNodeGroup
|
|
2367
2538
|
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2539
|
+
else if (shell) {
|
|
2540
|
+
if (shell.paths.length) {
|
|
2541
|
+
this.setPathsFromFragment(shellFragment, shell.paths);
|
|
2542
|
+
}
|
|
2371
2543
|
|
|
2544
|
+
this.activateEmbeds(shellFragment, shell);
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2372
2547
|
|
|
2373
2548
|
|
|
2374
2549
|
}
|
|
2375
2550
|
|
|
2376
|
-
/**
|
|
2377
|
-
* Create a nested Component or call render with the new props.
|
|
2378
|
-
* @param el {Solarite:HTMLElement}
|
|
2379
|
-
* @param props {Object} */
|
|
2380
|
-
applyComponentExprs(el, props) {
|
|
2381
|
-
|
|
2382
|
-
// TODO: Does a hash of this already exist somewhere?
|
|
2383
|
-
// Perhaps if Components were treated as child NodeGroups, which would need to be the child of an ExprPath,
|
|
2384
|
-
// then we could re-use the hash and logic from NodeManager?
|
|
2385
|
-
let newHash = getObjectHash(props);
|
|
2386
|
-
|
|
2387
|
-
let isPreHtmlElement = el.hasAttribute('solarite-placeholder');
|
|
2388
|
-
let isPreIsElement = el.hasAttribute('_is');
|
|
2389
2551
|
|
|
2552
|
+
/**
|
|
2553
|
+
* Use the paths to insert the given expressions.
|
|
2554
|
+
* Dispatches expression handling to other functions depending on the path type.
|
|
2555
|
+
* @param exprs {(*|*[]|function|Template)[]}
|
|
2556
|
+
* @param changed {boolean} If true, the expr's have changed since the last time thsi function was called.
|
|
2557
|
+
* @param includeNonComponents {boolean}
|
|
2558
|
+
* We still need to call PathToComponent.apply() even if changed=false so the user can handle the rendering. */
|
|
2559
|
+
applyExprs(exprs, changed=true, includeNonComponents=true) {
|
|
2390
2560
|
|
|
2391
|
-
|
|
2392
|
-
if (isPreHtmlElement || isPreIsElement)
|
|
2393
|
-
el = this.instantiateComponent(el, isPreHtmlElement, props);
|
|
2561
|
+
|
|
2394
2562
|
|
|
2395
|
-
|
|
2396
|
-
// We do this even if the arguments haven't changed, so we can let the child component
|
|
2397
|
-
// compare the arguments and then decide for itself whether it wants to re-render.
|
|
2398
|
-
else if (el.render) {
|
|
2399
|
-
//let oldHash = Globals.componentArgsHash.get(el);
|
|
2400
|
-
//if (oldHash !== newHash) { // Only if not changed.
|
|
2401
|
-
let args = {};
|
|
2402
|
-
for (let name in props || {})
|
|
2403
|
-
args[Util.dashesToCamel(name)] = props[name];
|
|
2404
|
-
el.render(args); // Pass new values of props to render so it can decide how it wants to respond.
|
|
2405
|
-
//}
|
|
2406
|
-
}
|
|
2563
|
+
let paths = this.paths;
|
|
2407
2564
|
|
|
2408
|
-
|
|
2409
|
-
|
|
2565
|
+
// Things to consider:
|
|
2566
|
+
// 1. Paths consume a varying number of expressions.
|
|
2567
|
+
// An PathToAttribs may use multipe expressions. E.g. <div class="${1} ${2}">
|
|
2568
|
+
// While an PathToComponent uses zero.
|
|
2569
|
+
// 2. An PathToComponent references other Paths that set its attribute values.
|
|
2570
|
+
// 3. We apply them in reverse order so that a <select> box has its children created from an expression
|
|
2571
|
+
// before its instantiated and its value attribute is set via an expression.
|
|
2572
|
+
let exprIndex = exprs.length; // Update exprs at paths.
|
|
2573
|
+
let pathExprs = new Array(paths.length); // Store all the expressions that map to a single path. Only paths to attribute values can have more than one.
|
|
2574
|
+
for (let i = paths.length - 1, path; path = paths[i]; i--) {
|
|
2575
|
+
if (i===0 && path instanceof PathToComponent && path.nodeMarker === this.getRootNode())
|
|
2576
|
+
continue;
|
|
2410
2577
|
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
instantiateComponent(el, isPreHtmlElement=undefined, props=undefined) {
|
|
2422
|
-
if (isPreHtmlElement === undefined)
|
|
2423
|
-
isPreHtmlElement = !el.hasAttribute('_is');
|
|
2424
|
-
|
|
2425
|
-
let tagName = (isPreHtmlElement
|
|
2426
|
-
? el.tagName.slice(0, -21) // Remove -SOLARITE-PLACEHOLDER
|
|
2427
|
-
: el.getAttribute('is')).toLowerCase();
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
// Throw if custom element isn't defined.
|
|
2431
|
-
let Constructor = customElements.get(tagName);
|
|
2432
|
-
if (!Constructor)
|
|
2433
|
-
throw new Error(`The custom tag name ${tagName} is not registered.`)
|
|
2434
|
-
|
|
2435
|
-
let attribs = {};
|
|
2436
|
-
for (let name in props || {})
|
|
2437
|
-
attribs[Util.dashesToCamel(name)] = props[name];
|
|
2438
|
-
|
|
2439
|
-
// Pass other attribs to constructor, since otherwise they're not yet set on the element,
|
|
2440
|
-
// and the constructor would otherwise have no way to see them.
|
|
2441
|
-
if (el.attributes.length) {
|
|
2442
|
-
for (let attrib of el.attributes) {
|
|
2443
|
-
let attribName = Util.dashesToCamel(attrib.name);
|
|
2444
|
-
if (!attribs.hasOwnProperty(attribName) && attribName !== 'solarite-placeholder')
|
|
2445
|
-
attribs[attribName] = attrib.value;
|
|
2578
|
+
// Get the expressions associated with this path.
|
|
2579
|
+
let exprCount = path.getExpressionCount();
|
|
2580
|
+
pathExprs[i] = exprs.slice(exprIndex-exprCount, exprIndex); // slice() probably doesn't allocate if the JS vm implements copy on write.
|
|
2581
|
+
exprIndex -= exprCount;
|
|
2582
|
+
|
|
2583
|
+
// Component expressions don't have a corresponding user-provided expression.
|
|
2584
|
+
// They use expressions from the paths that provide their attributes.
|
|
2585
|
+
if (path instanceof PathToComponent) {
|
|
2586
|
+
let attribExprs = pathExprs.slice(i+1, i+1 + path.attribPaths.length); // +1 b/c we move forward from the component path.
|
|
2587
|
+
path.apply(attribExprs, true, changed);
|
|
2446
2588
|
}
|
|
2589
|
+
else if (includeNonComponents)
|
|
2590
|
+
path.apply(pathExprs[i]);
|
|
2447
2591
|
}
|
|
2448
2592
|
|
|
2449
|
-
//
|
|
2450
|
-
//
|
|
2451
|
-
|
|
2452
|
-
let newEl = new Constructor(attribs, children);
|
|
2593
|
+
// If there's leftover expressions, there's probably an issue with the Shell that created this NodeGroup,
|
|
2594
|
+
// and the number of paths not matching.
|
|
2595
|
+
|
|
2453
2596
|
|
|
2454
|
-
if (!isPreHtmlElement)
|
|
2455
|
-
newEl.setAttribute('is', el.getAttribute('is').toLowerCase());
|
|
2456
2597
|
|
|
2457
|
-
|
|
2458
|
-
el.replaceWith(newEl);
|
|
2598
|
+
if (includeNonComponents) {
|
|
2459
2599
|
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
if (id)
|
|
2463
|
-
delve(this.getRootNode(), id.split(/\./g), newEl);
|
|
2600
|
+
// TODO: Only do this if we have Paths within styles?
|
|
2601
|
+
this.updateStyles();
|
|
2464
2602
|
|
|
2603
|
+
// Invalidate the nodes cache because we just changed it.
|
|
2604
|
+
this.nodesCache = null;
|
|
2465
2605
|
|
|
2466
|
-
// Update paths to use replaced element.
|
|
2467
|
-
for (let path of this.paths) {
|
|
2468
|
-
if (path.nodeMarker === el)
|
|
2469
|
-
path.nodeMarker = newEl;
|
|
2470
|
-
if (path.nodeBefore === el)
|
|
2471
|
-
path.nodeBefore = newEl;
|
|
2472
2606
|
}
|
|
2473
|
-
if (this.startNode === el)
|
|
2474
|
-
this.startNode = newEl;
|
|
2475
|
-
if (this.endNode === el)
|
|
2476
|
-
this.endNode = newEl;
|
|
2477
|
-
|
|
2478
2607
|
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
if (newEl.renderFirstTime)
|
|
2482
|
-
newEl.renderFirstTime();
|
|
2483
|
-
|
|
2484
|
-
// Copy attributes over.
|
|
2485
|
-
for (let attrib of el.attributes)
|
|
2486
|
-
if (attrib.name !== '_is' && attrib.name !== 'solarite-placeholder')
|
|
2487
|
-
newEl.setAttribute(attrib.name, attrib.value);
|
|
2488
|
-
|
|
2489
|
-
// Set dynamic attributes if they are primitive types.
|
|
2490
|
-
for (let name in props) {
|
|
2491
|
-
let val = props[name];
|
|
2492
|
-
if (typeof val === 'boolean') {
|
|
2493
|
-
if (val !== false && val !== undefined && val !== null)
|
|
2494
|
-
newEl.setAttribute(name, '');
|
|
2495
|
-
}
|
|
2608
|
+
|
|
2609
|
+
}
|
|
2496
2610
|
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
newEl.setAttribute(name, val);
|
|
2500
|
-
}
|
|
2611
|
+
// TODO: Give it a better name.
|
|
2612
|
+
applyExprs2(exprs) {
|
|
2501
2613
|
|
|
2502
|
-
return newEl;
|
|
2503
2614
|
}
|
|
2504
2615
|
|
|
2505
2616
|
/**
|
|
@@ -2525,10 +2636,6 @@ class NodeGroup {
|
|
|
2525
2636
|
return result;
|
|
2526
2637
|
}
|
|
2527
2638
|
|
|
2528
|
-
getParentNode() {
|
|
2529
|
-
return this.startNode?.parentNode
|
|
2530
|
-
}
|
|
2531
|
-
|
|
2532
2639
|
/**
|
|
2533
2640
|
* Get the root element of the NodeGroup's RootNodeGroup.
|
|
2534
2641
|
* @returns {HTMLElement|DocumentFragment} */
|
|
@@ -2543,21 +2650,15 @@ class NodeGroup {
|
|
|
2543
2650
|
}
|
|
2544
2651
|
|
|
2545
2652
|
/**
|
|
2546
|
-
*
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
}
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
updatePaths(fragment, paths, offset) {
|
|
2556
|
-
// Update paths to point to the fragment.
|
|
2557
|
-
let pathLength = paths.length;
|
|
2653
|
+
* Copy paths in fragment to this.paths.
|
|
2654
|
+
* @param fragment {DocumentFragment|HTMLElement}
|
|
2655
|
+
* @param paths
|
|
2656
|
+
* @param startingPathDepth {int} */
|
|
2657
|
+
setPathsFromFragment(fragment, paths, startingPathDepth=0) {
|
|
2658
|
+
let pathLength = paths.length; // For faster iteration
|
|
2558
2659
|
this.paths.length = pathLength;
|
|
2559
2660
|
for (let i=0; i<pathLength; i++) {
|
|
2560
|
-
let path = paths[i].clone(fragment,
|
|
2661
|
+
let path = paths[i].clone(fragment, startingPathDepth);
|
|
2561
2662
|
path.parentNg = this;
|
|
2562
2663
|
this.paths[i] = path;
|
|
2563
2664
|
}
|
|
@@ -2572,33 +2673,6 @@ class NodeGroup {
|
|
|
2572
2673
|
}
|
|
2573
2674
|
}
|
|
2574
2675
|
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
findStaticComponents(root, shell, pathOffset=0) {
|
|
2578
|
-
let result = [];
|
|
2579
|
-
|
|
2580
|
-
// static components. These are WebComponents that do not have any constructor arguments that are expressions.
|
|
2581
|
-
// Those are instead created by applyExpr() which calls applyComponentExprs() which calls instantiateComponent().
|
|
2582
|
-
// Maybe someday these two paths will be merged?
|
|
2583
|
-
// Must happen before ids because instantiateComponent will replace the element.
|
|
2584
|
-
for (let path of shell.staticComponents) {
|
|
2585
|
-
if (pathOffset)
|
|
2586
|
-
path = path.slice(0, -pathOffset);
|
|
2587
|
-
let el = resolveNodePath(root, path);
|
|
2588
|
-
|
|
2589
|
-
// Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
|
|
2590
|
-
// Recreating it is necessary so we can pass the constructor args to it.
|
|
2591
|
-
if (root !== el/* && !isReplaceEl(root, el)*/) // TODO: is isReplaceEl necessary?
|
|
2592
|
-
result.push(el);
|
|
2593
|
-
}
|
|
2594
|
-
return result;
|
|
2595
|
-
}
|
|
2596
|
-
|
|
2597
|
-
instantiateStaticComponents(staticComponents) {
|
|
2598
|
-
for (let el of staticComponents)
|
|
2599
|
-
this.instantiateComponent(el);
|
|
2600
|
-
}
|
|
2601
|
-
|
|
2602
2676
|
/**
|
|
2603
2677
|
* @param root {HTMLElement|DocumentFragment}
|
|
2604
2678
|
* @param shell {Shell}
|
|
@@ -2614,10 +2688,10 @@ class NodeGroup {
|
|
|
2614
2688
|
for (let path of shell.ids) {
|
|
2615
2689
|
if (pathOffset)
|
|
2616
2690
|
path = path.slice(0, -pathOffset);
|
|
2617
|
-
let el =
|
|
2691
|
+
let el = Path.resolve(root, path);
|
|
2618
2692
|
Util.bindId(rootEl, el);
|
|
2619
2693
|
}
|
|
2620
|
-
|
|
2694
|
+
}
|
|
2621
2695
|
|
|
2622
2696
|
// styles
|
|
2623
2697
|
if (options?.styles !== false) {
|
|
@@ -2628,7 +2702,7 @@ class NodeGroup {
|
|
|
2628
2702
|
path = path.slice(0, -pathOffset);
|
|
2629
2703
|
|
|
2630
2704
|
/** @type {HTMLStyleElement} */
|
|
2631
|
-
let style =
|
|
2705
|
+
let style = Path.resolve(root, path);
|
|
2632
2706
|
if (rootEl.nodeType === 1) {
|
|
2633
2707
|
Util.bindStyles(style, rootEl);
|
|
2634
2708
|
this.styles.set(style, style.textContent);
|
|
@@ -2640,140 +2714,19 @@ class NodeGroup {
|
|
|
2640
2714
|
if (options?.scripts !== false) {
|
|
2641
2715
|
for (let path of shell.scripts) {
|
|
2642
2716
|
if (pathOffset)
|
|
2643
|
-
path = path.slice(0, -pathOffset);
|
|
2644
|
-
let script =
|
|
2645
|
-
eval(script.textContent);
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2648
|
-
}
|
|
2649
|
-
}
|
|
2650
|
-
}
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
class RootNodeGroup extends NodeGroup {
|
|
2654
|
-
|
|
2655
|
-
/**
|
|
2656
|
-
* Root node at the top of the hierarchy.
|
|
2657
|
-
* @type {HTMLElement} */
|
|
2658
|
-
root;
|
|
2659
|
-
|
|
2660
|
-
/**
|
|
2661
|
-
* When we call renerWatched() we re-render these expressions, then clear this to a new Map()
|
|
2662
|
-
* @type {Map<ExprPath, ValueOp|WholeArrayOp|ArraySpliceOp[]>} */
|
|
2663
|
-
exprsToRender = new Map();
|
|
2664
|
-
|
|
2665
|
-
/**
|
|
2666
|
-
*
|
|
2667
|
-
* @param template
|
|
2668
|
-
* @param el
|
|
2669
|
-
* @param options {?object}
|
|
2670
|
-
*/
|
|
2671
|
-
constructor(template, el, options) {
|
|
2672
|
-
super(template);
|
|
2673
|
-
|
|
2674
|
-
this.options = options;
|
|
2675
|
-
|
|
2676
|
-
this.rootNg = this;
|
|
2677
|
-
let [fragment, shell] = this.init(template);
|
|
2678
|
-
|
|
2679
|
-
if (fragment instanceof Text) {
|
|
2680
|
-
|
|
2681
|
-
if (el) {
|
|
2682
|
-
this.startNode = el;
|
|
2683
|
-
this.endNode = el;
|
|
2684
|
-
if (fragment.nodeValue.length)
|
|
2685
|
-
el.append(fragment);
|
|
2686
|
-
this.root = el;
|
|
2687
|
-
}
|
|
2688
|
-
Globals$1.nodeGroups.set(this.root, this);
|
|
2689
|
-
}
|
|
2690
|
-
else {
|
|
2691
|
-
|
|
2692
|
-
// If adding NodeGroup to an element.
|
|
2693
|
-
let offset = 0;
|
|
2694
|
-
let root = fragment; // TODO: Rename so it's not confused with this.root.
|
|
2695
|
-
if (el) {
|
|
2696
|
-
Globals$1.nodeGroups.set(el, this);
|
|
2697
|
-
|
|
2698
|
-
// Save slot children
|
|
2699
|
-
let slotChildren;
|
|
2700
|
-
if (el.childNodes.length) {
|
|
2701
|
-
slotChildren = document.createDocumentFragment();
|
|
2702
|
-
slotChildren.append(...el.childNodes);
|
|
2703
|
-
}
|
|
2704
|
-
|
|
2705
|
-
this.root = el;
|
|
2706
|
-
|
|
2707
|
-
// If el should replace the root node of the fragment.
|
|
2708
|
-
if (isReplaceEl(fragment, el)) {
|
|
2709
|
-
el.append(...fragment.children[0].childNodes);
|
|
2710
|
-
|
|
2711
|
-
// Copy attributes
|
|
2712
|
-
for (let attrib of fragment.children[0].attributes)
|
|
2713
|
-
if (!el.hasAttribute(attrib.name) && attrib.name !== 'solarite-placeholder')
|
|
2714
|
-
el.setAttribute(attrib.name, attrib.value);
|
|
2715
|
-
|
|
2716
|
-
// Go one level deeper into all of shell's paths.
|
|
2717
|
-
offset = 1;
|
|
2718
|
-
} else {
|
|
2719
|
-
let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
|
|
2720
|
-
if (!isEmpty)
|
|
2721
|
-
el.append(...fragment.childNodes);
|
|
2722
|
-
}
|
|
2723
|
-
|
|
2724
|
-
// Setup children
|
|
2725
|
-
if (slotChildren) {
|
|
2726
|
-
|
|
2727
|
-
// Named slots
|
|
2728
|
-
for (let slot of el.querySelectorAll('slot[name]')) {
|
|
2729
|
-
let name = slot.getAttribute('name');
|
|
2730
|
-
if (name) {
|
|
2731
|
-
let slotChildren2 = slotChildren.querySelectorAll(`[slot='${name}']`);
|
|
2732
|
-
slot.append(...slotChildren2);
|
|
2733
|
-
}
|
|
2734
|
-
}
|
|
2735
|
-
|
|
2736
|
-
// Unnamed slots
|
|
2737
|
-
let unamedSlot = el.querySelector('slot:not([name])');
|
|
2738
|
-
if (unamedSlot)
|
|
2739
|
-
unamedSlot.append(slotChildren);
|
|
2740
|
-
|
|
2741
|
-
// No slots
|
|
2742
|
-
else
|
|
2743
|
-
el.append(slotChildren);
|
|
2744
|
-
}
|
|
2745
|
-
|
|
2746
|
-
root = el;
|
|
2747
|
-
|
|
2748
|
-
this.startNode = el;
|
|
2749
|
-
this.endNode = el;
|
|
2750
|
-
} else {
|
|
2751
|
-
let singleEl = getSingleEl(fragment);
|
|
2752
|
-
this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
|
|
2753
|
-
|
|
2754
|
-
Globals$1.nodeGroups.set(this.root, this);
|
|
2755
|
-
if (singleEl) {
|
|
2756
|
-
root = singleEl;
|
|
2757
|
-
offset = 1;
|
|
2717
|
+
path = path.slice(0, -pathOffset);
|
|
2718
|
+
let script = Path.resolve(root, path);
|
|
2719
|
+
eval(script.textContent);
|
|
2758
2720
|
}
|
|
2759
2721
|
}
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2760
2724
|
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
// Static web components can sometimes have children created via expressions.
|
|
2764
|
-
// But calling applyExprs() will mess up the shell's path to them.
|
|
2765
|
-
// So we find them first, then call activateStaticComponents() after their children have been created.
|
|
2766
|
-
let staticComponents = this.findStaticComponents(root, shell, offset);
|
|
2725
|
+
|
|
2726
|
+
}
|
|
2767
2727
|
|
|
2768
|
-
this.activateEmbeds(root, shell, offset);
|
|
2769
2728
|
|
|
2770
|
-
// Apply exprs
|
|
2771
|
-
this.applyExprs(template.exprs);
|
|
2772
2729
|
|
|
2773
|
-
this.instantiateStaticComponents(staticComponents);
|
|
2774
|
-
}
|
|
2775
|
-
}
|
|
2776
|
-
}
|
|
2777
2730
|
|
|
2778
2731
|
function getSingleEl(fragment) {
|
|
2779
2732
|
let nonempty = [];
|
|
@@ -2790,12 +2743,19 @@ function getSingleEl(fragment) {
|
|
|
2790
2743
|
/**
|
|
2791
2744
|
* Does the fragment have one child that's an element matching the tagname of el?
|
|
2792
2745
|
* @param fragment {DocumentFragment}
|
|
2793
|
-
* @param
|
|
2746
|
+
* @param tagName {string}
|
|
2794
2747
|
* @returns {boolean} */
|
|
2795
|
-
function isReplaceEl(fragment,
|
|
2748
|
+
function isReplaceEl(fragment, tagName) {
|
|
2796
2749
|
return fragment.children.length===1
|
|
2797
|
-
&&
|
|
2798
|
-
&& fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') ===
|
|
2750
|
+
&& tagName.includes('-')
|
|
2751
|
+
&& fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') === tagName;
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
class RootNodeGroup extends NodeGroup {
|
|
2755
|
+
|
|
2756
|
+
// Used only by watch.js
|
|
2757
|
+
exprsToRender;
|
|
2758
|
+
|
|
2799
2759
|
}
|
|
2800
2760
|
|
|
2801
2761
|
/**
|
|
@@ -2804,24 +2764,24 @@ function isReplaceEl(fragment, el) {
|
|
|
2804
2764
|
* Although the reference to the html strings is shared among templates. */
|
|
2805
2765
|
class Template {
|
|
2806
2766
|
|
|
2807
|
-
/** @type {
|
|
2808
|
-
exprs = []
|
|
2767
|
+
/** @type {Expr[]} Evaulated expressions. */
|
|
2768
|
+
'exprs' = []
|
|
2809
2769
|
|
|
2810
2770
|
/** @type {string[]} */
|
|
2811
|
-
html = [];
|
|
2771
|
+
'html' = [];
|
|
2812
2772
|
|
|
2813
2773
|
/** @type {Array} Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
|
|
2814
2774
|
hashedFields;
|
|
2815
2775
|
|
|
2816
|
-
|
|
2817
|
-
nodeGroup;
|
|
2776
|
+
isText;
|
|
2818
2777
|
|
|
2819
2778
|
/**
|
|
2820
2779
|
*
|
|
2821
2780
|
* @param htmlStrings {string[]}
|
|
2822
2781
|
* @param exprs {*[]} */
|
|
2823
|
-
constructor(htmlStrings, exprs) {
|
|
2782
|
+
constructor(htmlStrings=[''], exprs=[]) {
|
|
2824
2783
|
this.html = htmlStrings;
|
|
2784
|
+
|
|
2825
2785
|
this.exprs = exprs;
|
|
2826
2786
|
|
|
2827
2787
|
//this.trace = new Error().stack.split(/\n/g)
|
|
@@ -2836,51 +2796,50 @@ class Template {
|
|
|
2836
2796
|
* Called by JSON.serialize when it encounters a Template.
|
|
2837
2797
|
* This prevents the hashed version from being too large. */
|
|
2838
2798
|
toJSON() {
|
|
2839
|
-
if (
|
|
2799
|
+
if (this.hashedFields===undefined)
|
|
2840
2800
|
this.hashedFields = [getObjectId(this.html), this.exprs];
|
|
2841
2801
|
|
|
2842
2802
|
return this.hashedFields
|
|
2843
2803
|
}
|
|
2844
2804
|
|
|
2845
2805
|
/**
|
|
2846
|
-
* Render the main
|
|
2847
|
-
* @param el {HTMLElement}
|
|
2806
|
+
* Render the main (root) template.
|
|
2807
|
+
* @param el {?HTMLElement} Null if we're rendering to a standalone element.
|
|
2848
2808
|
* @param options {RenderOptions}
|
|
2849
2809
|
* @return {?DocumentFragment|HTMLElement} */
|
|
2850
|
-
render(el=null, options={}) {
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
el
|
|
2860
|
-
Globals$1.nodeGroups.set(el, ng); // Why was this commented out?
|
|
2861
|
-
firstTime = true;
|
|
2810
|
+
'render'(el=null, options={}) {
|
|
2811
|
+
|
|
2812
|
+
|
|
2813
|
+
|
|
2814
|
+
let ng = el && Globals$1.rootNodeGroups.get(el);
|
|
2815
|
+
if (!ng) {
|
|
2816
|
+
ng = new RootNodeGroup(this, null, el, options);
|
|
2817
|
+
if (!el) // null if it's a standalone elment.
|
|
2818
|
+
el = ng.getRootNode();
|
|
2819
|
+
Globals$1.rootNodeGroups.set(el, ng); // All tests still pass if this is commented out!
|
|
2862
2820
|
}
|
|
2863
|
-
else {
|
|
2864
|
-
ng = Globals$1.nodeGroups.get(el);
|
|
2865
|
-
if (!ng) {
|
|
2866
|
-
ng = new RootNodeGroup(this, el, options);
|
|
2867
|
-
Globals$1.nodeGroups.set(el, ng); // Why was this commented out?
|
|
2868
|
-
firstTime = true;
|
|
2869
|
-
}
|
|
2870
2821
|
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2822
|
+
// Make sure the expresion count matches match the Path "hole" count.
|
|
2823
|
+
// This can happen if we try manually rendering one template to a NodeGroup that was created expecting a different template.
|
|
2824
|
+
// These don't always have the same length, for example if one attribute has multiple expressions.
|
|
2825
|
+
// if (ng.paths.length === 0 && this.exprs.length || ng.paths.length > this.exprs.length)
|
|
2826
|
+
// throw new Error(
|
|
2827
|
+
// `Solarite Error: Parent HTMLElement ${ng.template.html.join('${...}')} and ${ng.paths.length} \${value} ` +
|
|
2828
|
+
// `placeholders can't accomodate a Template with ${this.exprs.length} values.`);
|
|
2875
2829
|
|
|
2876
2830
|
// Creating the root nodegroup also renders it.
|
|
2877
2831
|
// If we didn't just create it, we need to render it.
|
|
2878
|
-
if (!
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2832
|
+
if (this.html?.length === 1 && !this.html[0]) // An empty string.
|
|
2833
|
+
el.innerHTML = ''; // Fast path for empty component.
|
|
2834
|
+
else {
|
|
2835
|
+
|
|
2836
|
+
let oldKey = ng.exactKey;
|
|
2837
|
+
let newKey = this.getExactKey();
|
|
2838
|
+
ng.applyExprs(this.exprs, oldKey !== newKey);
|
|
2839
|
+
ng.exactKey = newKey;
|
|
2840
|
+
|
|
2841
|
+
//if (firstTime)
|
|
2842
|
+
// ng.instantiateStaticComponents(ng.staticComponents);
|
|
2884
2843
|
}
|
|
2885
2844
|
|
|
2886
2845
|
ng.exprsToRender = new Map();
|
|
@@ -2888,7 +2847,7 @@ class Template {
|
|
|
2888
2847
|
}
|
|
2889
2848
|
|
|
2890
2849
|
getExactKey() {
|
|
2891
|
-
if (
|
|
2850
|
+
if (this.exactKey===undefined) {
|
|
2892
2851
|
if (this.exprs.length)
|
|
2893
2852
|
this.exactKey = getObjectHash(this);// calls this.toJSON().
|
|
2894
2853
|
else // Don't hash plain html.
|
|
@@ -2899,7 +2858,7 @@ class Template {
|
|
|
2899
2858
|
|
|
2900
2859
|
getCloseKey() {
|
|
2901
2860
|
//console.log(this.exprs.length)
|
|
2902
|
-
if (
|
|
2861
|
+
if (this.closeKey===undefined) {
|
|
2903
2862
|
if (this.exprs.length)
|
|
2904
2863
|
this.closeKey = /*'@' + */this.toJSON()[0];
|
|
2905
2864
|
else
|
|
@@ -2910,169 +2869,203 @@ class Template {
|
|
|
2910
2869
|
|
|
2911
2870
|
return this.closeKey;
|
|
2912
2871
|
}
|
|
2913
|
-
}
|
|
2914
2872
|
|
|
2873
|
+
/**
|
|
2874
|
+
* @param tag {string}
|
|
2875
|
+
* @param props {?Record<string, any>}
|
|
2876
|
+
* @param children
|
|
2877
|
+
* @returns {Template} */
|
|
2878
|
+
static fromJsx(tag, props, children) {
|
|
2915
2879
|
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
* @property {boolean=} styles - Replace :host in style tags to scope them locally.
|
|
2919
|
-
* @property {boolean=} scripts - Execute script tags.
|
|
2920
|
-
* @property {boolean=} ids - Create references to elements with id or data-id attributes.
|
|
2921
|
-
* @property {?boolean} render - Deprecated.
|
|
2922
|
-
* Used only when options are given to a class super constructor inheriting from Solarite.
|
|
2923
|
-
* True to call render() immediately in super constructor.
|
|
2924
|
-
* False to automatically call render() at all.
|
|
2925
|
-
* Undefined (default) to call render() when added to the DOM, unless already rendered.
|
|
2926
|
-
*/
|
|
2927
|
-
|
|
2928
|
-
/**
|
|
2929
|
-
* Convert strings to HTMLNodes.
|
|
2930
|
-
* Using h`...` as a tag will always create a Template.
|
|
2931
|
-
* Using h() as a function() will always create a DOM element.
|
|
2932
|
-
*
|
|
2933
|
-
* Features beyond what standard js tagged template strings do:
|
|
2934
|
-
* 1. r`` sub-expressions
|
|
2935
|
-
* 2. functions, nodes, and arrays of nodes as sub-expressions.
|
|
2936
|
-
* 3. html-escape all expressions by default, unless wrapped in r()
|
|
2937
|
-
* 4. event binding
|
|
2938
|
-
* 5. TODO: list more
|
|
2939
|
-
*
|
|
2940
|
-
* Currently supported:
|
|
2941
|
-
* 1. h(el, options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
|
|
2942
|
-
* 2. h(el, template, ?options) // Render the Template created by #1 to element.
|
|
2943
|
-
*
|
|
2944
|
-
* 3. h`<b>Hello</b> ${'World'}!` // Create Template that can later be used to create nodes.
|
|
2945
|
-
*
|
|
2946
|
-
* 4. h('Hello'); // Create single text node.
|
|
2947
|
-
* 5. h('<b>Hello</b>'); // Create single HTMLElement
|
|
2948
|
-
* 6. h('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
|
|
2949
|
-
* 7. h()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which
|
|
2950
|
-
* // includes properly handling nested components and r`` sub-expressions.
|
|
2951
|
-
* 8. h(template) // Render Template created by #1.
|
|
2952
|
-
*
|
|
2953
|
-
* 9. h({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
|
|
2954
|
-
* 10. h(string, object, ...) // JSX TODO
|
|
2955
|
-
* @param htmlStrings {?HTMLElement|string|string[]|function():Template|{render:function()}}
|
|
2956
|
-
* @param exprs {*[]|string|Template|Object}
|
|
2957
|
-
* @return {Node|HTMLElement|Template} */
|
|
2958
|
-
function h(htmlStrings=undefined, ...exprs) {
|
|
2880
|
+
// HTML void elements that must not have closing tags
|
|
2881
|
+
const isVoid = selfClosingTags.has(tag.toLowerCase());
|
|
2959
2882
|
|
|
2960
|
-
|
|
2961
|
-
|
|
2883
|
+
// Build htmlStrings/exprs so Shell can place placeholders in attribute values and child content.
|
|
2884
|
+
let htmlStrings = [];
|
|
2885
|
+
let templateExprs = [];
|
|
2962
2886
|
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
let parent = htmlStrings, template = exprs[0];
|
|
2887
|
+
// Opening tag
|
|
2888
|
+
let open = `<${tag}`;
|
|
2966
2889
|
|
|
2967
|
-
//
|
|
2968
|
-
if (
|
|
2969
|
-
|
|
2970
|
-
|
|
2890
|
+
// Attributes
|
|
2891
|
+
if (props && typeof props === 'object') {
|
|
2892
|
+
for (let name in props) {
|
|
2893
|
+
let value = props[name];
|
|
2971
2894
|
|
|
2972
|
-
|
|
2895
|
+
// id and data-id are static in templates — never expressions
|
|
2896
|
+
if (name === 'id' || name === 'data-id') {
|
|
2897
|
+
// Write directly into the opening string with quotes
|
|
2898
|
+
open += ` ${name}="${value}"`;
|
|
2899
|
+
continue;
|
|
2900
|
+
}
|
|
2973
2901
|
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2902
|
+
// Dynamic attribute value: functions are unquoted (e.g., onclick=${fn}), others quoted
|
|
2903
|
+
if (typeof value === 'function') {
|
|
2904
|
+
open += ` ${name}=`;
|
|
2905
|
+
htmlStrings.push(open);
|
|
2906
|
+
templateExprs.push(value);
|
|
2907
|
+
// reset so subsequent attributes start fresh (e.g., ' title=')
|
|
2908
|
+
open = ``;
|
|
2909
|
+
}
|
|
2910
|
+
else {
|
|
2911
|
+
open += ` ${name}=`;
|
|
2912
|
+
htmlStrings.push(open);
|
|
2913
|
+
templateExprs.push(value);
|
|
2914
|
+
// reset so subsequent attributes start fresh (e.g., ' title=')
|
|
2915
|
+
open = ``;
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2981
2918
|
}
|
|
2982
2919
|
|
|
2983
|
-
//
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
//
|
|
2991
|
-
|
|
2992
|
-
|
|
2920
|
+
// Finalize opening tag precisely to match tagged template splitting
|
|
2921
|
+
if (!isVoid) {
|
|
2922
|
+
const pushedAny = htmlStrings.length > 0;
|
|
2923
|
+
// If nothing pushed yet (no dynamic attrs), push the entire open + '>'
|
|
2924
|
+
if (!pushedAny)
|
|
2925
|
+
htmlStrings.push(open + '>');
|
|
2926
|
+
else {
|
|
2927
|
+
// If we were in a quoted attr (open === '"'), then the string after expr is '">' ;
|
|
2928
|
+
// Otherwise (function-valued attr), the string after expr is just '>'
|
|
2929
|
+
htmlStrings.push(open === '"' ? '">' : '>');
|
|
2993
2930
|
}
|
|
2931
|
+
|
|
2932
|
+
for (let child of children)
|
|
2933
|
+
addChild(child, htmlStrings, templateExprs);
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
// Closing tag (not for void tags)
|
|
2937
|
+
if (!isVoid) {
|
|
2938
|
+
// If we never emitted the '>' for the open tag (no children were added),
|
|
2939
|
+
// then it was appended above before children. Now just add the closing tag to the last html segment.
|
|
2940
|
+
let lastIdx = htmlStrings.length - 1;
|
|
2941
|
+
htmlStrings[lastIdx] += `</${tag}>`;
|
|
2942
|
+
}
|
|
2943
|
+
else {
|
|
2944
|
+
// Void element: ensure we emitted a trailing '>' segment
|
|
2945
|
+
const pushedAny = htmlStrings.length > 0;
|
|
2946
|
+
if (!pushedAny)
|
|
2947
|
+
htmlStrings.push(open + '>');
|
|
2948
|
+
else
|
|
2949
|
+
htmlStrings.push('>');
|
|
2994
2950
|
}
|
|
2995
|
-
}
|
|
2996
2951
|
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
2952
|
+
// Ensure invariant
|
|
2953
|
+
//assert(htmlStrings.length === templateExprs.length + 1);
|
|
2954
|
+
//console.log([htmlStrings, templateExprs])
|
|
2955
|
+
return new Template(htmlStrings, templateExprs);
|
|
3000
2956
|
}
|
|
2957
|
+
}
|
|
2958
|
+
|
|
3001
2959
|
|
|
3002
|
-
|
|
3003
|
-
// 10. JSX
|
|
3004
|
-
if (typeof exprs[0] === 'object') {
|
|
3005
|
-
exprs[0] || {};
|
|
3006
|
-
exprs.slice(1);
|
|
2960
|
+
const selfClosingTags = new Set(['area','base','br','col','embed','hr','img','input','link','meta','param','source','track','wbr']);
|
|
3007
2961
|
|
|
3008
|
-
let templateHtmlStrings = [];
|
|
3009
|
-
let templateExprs = [];
|
|
3010
2962
|
|
|
3011
|
-
|
|
3012
|
-
|
|
2963
|
+
/**
|
|
2964
|
+
* Add child Templates that were already created via h() and Template.fromJsx()
|
|
2965
|
+
* @param template {Template}
|
|
2966
|
+
* @param html {string[]}
|
|
2967
|
+
* @param exprs {any[]} */
|
|
2968
|
+
const addChild = (template, html, exprs) => {
|
|
2969
|
+
|
|
2970
|
+
if (Array.isArray(template)) {
|
|
2971
|
+
for (let c of template)
|
|
2972
|
+
addChild(c, html, exprs);
|
|
2973
|
+
}
|
|
2974
|
+
else {
|
|
2975
|
+
let flatten = false;
|
|
2976
|
+
if (template instanceof Template) {
|
|
2977
|
+
// Heuristic to match tagged-template splitting:
|
|
2978
|
+
// - Flatten if the child has expressions (so JSX can inline attribute/value placeholders like tagged literals would).
|
|
2979
|
+
// - Also flatten void elements (e.g., <img>) so they inline like literals.
|
|
2980
|
+
// - Otherwise, keep as a dynamic child placeholder to match cases where the tagged template used an expression child.
|
|
2981
|
+
const childHasExprs = template.exprs.length > 0;
|
|
2982
|
+
if (childHasExprs)
|
|
2983
|
+
flatten = true;
|
|
2984
|
+
else {
|
|
2985
|
+
const m = (template.html[0] || '').match(/^<([a-zA-Z][\w:-]*)/);
|
|
2986
|
+
const childTag = m ? m[1].toLowerCase() : '';
|
|
2987
|
+
flatten = selfClosingTags.has(childTag);
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
3013
2990
|
|
|
3014
|
-
|
|
3015
|
-
|
|
2991
|
+
if (flatten) {
|
|
2992
|
+
// Flatten/interleave into current segment to match tagged template splitting
|
|
2993
|
+
html[html.length - 1] += template.html[0];
|
|
2994
|
+
for (let i = 0; i < template.exprs.length; i++) {
|
|
2995
|
+
exprs.push(template.exprs[i]);
|
|
2996
|
+
html.push(template.html[i + 1] ?? '');
|
|
2997
|
+
}
|
|
2998
|
+
} else {
|
|
2999
|
+
// Keep as dynamic child
|
|
3000
|
+
exprs.push(template);
|
|
3001
|
+
html.push('');
|
|
3016
3002
|
}
|
|
3003
|
+
}
|
|
3004
|
+
};
|
|
3017
3005
|
|
|
3018
3006
|
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3007
|
+
/**
|
|
3008
|
+
* @typedef {Object} RenderOptions
|
|
3009
|
+
* @property {boolean=} styles - Replace :host in style tags to scope them locally.
|
|
3010
|
+
* @property {boolean=} scripts - Execute script tags.
|
|
3011
|
+
* @property {boolean=} ids - Create references to elements with id or data-id attributes.
|
|
3012
|
+
* @property {?boolean} render - Deprecated.
|
|
3013
|
+
* Used only when options are given to a class super constructor inheriting from Solarite.
|
|
3014
|
+
* True to call render() immediately in super constructor.
|
|
3015
|
+
* False to automatically call render() at all.
|
|
3016
|
+
* Undefined (default) to call render() when added to the DOM, unless already rendered.
|
|
3017
|
+
*/
|
|
3018
|
+
|
|
3019
|
+
/**
|
|
3020
|
+
* Convert a template, string, or object into a DOM Node or Element
|
|
3021
|
+
*
|
|
3022
|
+
* 1. toEl('Hello'); // Create single text node.
|
|
3023
|
+
* 2. toEl('<b>Hello</b>'); // Create single HTMLElement
|
|
3024
|
+
* 3. toEl('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
|
|
3025
|
+
* 4. toEl(template) // Render Template created by h`<html>` or h();
|
|
3026
|
+
* 5. toEl({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
|
|
3027
|
+
* @param arg {string|Template|{render:()=>void}}
|
|
3028
|
+
* @returns {Node|DocumentFragment|HTMLElement} */
|
|
3029
|
+
function toEl(arg) {
|
|
3030
|
+
|
|
3031
|
+
if (typeof arg === 'string') {
|
|
3032
|
+
let html = arg;
|
|
3033
|
+
|
|
3034
|
+
// If it's an element with whitespace before or after it, trim both ends.
|
|
3035
|
+
if (html.match(/^\s^<\S+/) || html.match(/\S+>\s+$/))
|
|
3036
|
+
html = html.trim();
|
|
3023
3037
|
|
|
3024
3038
|
// We create a new one each time because otherwise
|
|
3025
3039
|
// the returned fragment will have its content replaced by a subsequent call.
|
|
3026
|
-
let templateEl =
|
|
3027
|
-
templateEl.innerHTML =
|
|
3040
|
+
let templateEl = Globals$1.doc.createElement('template');
|
|
3041
|
+
templateEl.innerHTML = html;
|
|
3028
3042
|
|
|
3029
|
-
//
|
|
3043
|
+
// 1+2. Return Node if there's one child.
|
|
3030
3044
|
let relevantNodes = Util.trimEmptyNodes(templateEl.content.childNodes);
|
|
3031
3045
|
if (relevantNodes.length === 1)
|
|
3032
3046
|
return relevantNodes[0];
|
|
3033
3047
|
|
|
3034
|
-
//
|
|
3048
|
+
// 3. Otherwise return DocumentFragment.
|
|
3035
3049
|
return templateEl.content;
|
|
3036
3050
|
}
|
|
3037
3051
|
|
|
3038
|
-
//
|
|
3039
|
-
|
|
3040
|
-
return (
|
|
3041
|
-
//Globals.rendered.add(parent)
|
|
3042
|
-
let template = h(htmlStrings, ...exprs);
|
|
3043
|
-
return template.render();
|
|
3044
|
-
}
|
|
3045
|
-
}
|
|
3046
|
-
|
|
3047
|
-
// 8.
|
|
3048
|
-
else if (htmlStrings instanceof Template) {
|
|
3049
|
-
return htmlStrings.render();
|
|
3052
|
+
// 4.
|
|
3053
|
+
if (arg instanceof Template) {
|
|
3054
|
+
return arg.render();
|
|
3050
3055
|
}
|
|
3051
3056
|
|
|
3052
|
-
|
|
3053
|
-
// 9. Create dynamic element with render() function.
|
|
3057
|
+
// 5. Create dynamic element from an object with a render() function.
|
|
3054
3058
|
// TODO: This path doesn't handle embeds like data-id="..."
|
|
3055
|
-
else if (typeof
|
|
3056
|
-
let obj =
|
|
3059
|
+
else if (arg && typeof arg === 'object') {
|
|
3060
|
+
let obj = arg;
|
|
3057
3061
|
|
|
3058
|
-
if (obj.constructor.name !== 'Object')
|
|
3062
|
+
if (obj.constructor.name !== 'Object')
|
|
3059
3063
|
throw new Error(`Solarate Web Component class ${obj.constructor?.name} must extend HTMLElement.`);
|
|
3060
3064
|
|
|
3061
|
-
|
|
3062
|
-
// Special rebound render path, called by normal path.
|
|
3063
|
-
// Intercepts the main r`...` function call inside render().
|
|
3064
|
-
if (Globals$1.objToEl.has(obj)) {
|
|
3065
|
-
return function(...args) {
|
|
3066
|
-
let template = h(...args);
|
|
3067
|
-
let el = template.render();
|
|
3068
|
-
Globals$1.objToEl.set(obj, el);
|
|
3069
|
-
}.bind(obj);
|
|
3070
|
-
}
|
|
3071
|
-
|
|
3072
3065
|
// Normal path
|
|
3073
|
-
|
|
3066
|
+
if (!Globals$1.objToEl.has(obj)) {
|
|
3074
3067
|
Globals$1.objToEl.set(obj, null);
|
|
3075
|
-
obj[renderF](); // Calls the Special rebound render path above, when the render function calls
|
|
3068
|
+
obj[renderF](); // Calls the Special rebound render path above, when the render function calls h(this)
|
|
3076
3069
|
let el = Globals$1.objToEl.get(obj);
|
|
3077
3070
|
Globals$1.objToEl.delete(obj);
|
|
3078
3071
|
|
|
@@ -3080,7 +3073,7 @@ function h(htmlStrings=undefined, ...exprs) {
|
|
|
3080
3073
|
if (typeof obj[name] === 'function')
|
|
3081
3074
|
el[name] = obj[name].bind(el); // Make the "this" of functions be el.
|
|
3082
3075
|
// TODO: But this doesn't work for passing an object with functions as a constructor arg via an attribute:
|
|
3083
|
-
|
|
3076
|
+
// <my-element arg=${{myFunc() { return this }}}
|
|
3084
3077
|
else
|
|
3085
3078
|
el[name] = obj[name];
|
|
3086
3079
|
|
|
@@ -3096,18 +3089,152 @@ function h(htmlStrings=undefined, ...exprs) {
|
|
|
3096
3089
|
}
|
|
3097
3090
|
}
|
|
3098
3091
|
|
|
3099
|
-
|
|
3100
|
-
throw new Error('Unsupported arguments.')
|
|
3092
|
+
throw new Error('toEl() does not support argument of type: ' + (arg ? typeof arg : arg));
|
|
3101
3093
|
}
|
|
3102
3094
|
|
|
3095
|
+
|
|
3103
3096
|
// Trick to prevent minifier from renaming this function.
|
|
3104
3097
|
let renderF = 'render';
|
|
3105
3098
|
|
|
3106
3099
|
/**
|
|
3100
|
+
* Convert strings to HTMLNodes.
|
|
3101
|
+
* Using h`...` as a tag will always create a Template.
|
|
3102
|
+
* Using h() as a function() will always create a DOM element.
|
|
3103
|
+
*
|
|
3104
|
+
* Features beyond what standard js tagged template strings do:
|
|
3105
|
+
* 1. h`` sub-expressions
|
|
3106
|
+
* 2. functions, nodes, and arrays of nodes as sub-expressions.
|
|
3107
|
+
* 3. html-escape all expressions by default, unless wrapped in h()
|
|
3108
|
+
* 4. event binding
|
|
3109
|
+
* 5. TODO: list more
|
|
3110
|
+
*
|
|
3111
|
+
* General rule:
|
|
3112
|
+
* If h() is a function with null or an HTMLElement as its first argument create a Node.
|
|
3113
|
+
* Otherwise create a template
|
|
3114
|
+
*
|
|
3115
|
+
* Currently supported:
|
|
3116
|
+
*
|
|
3117
|
+
* Create Tempataes
|
|
3118
|
+
* 1. h`<b>Hello</b> ${'World'}!` // Create Template that can later be used to create nodes.
|
|
3119
|
+
* 2. h('<b>Hello</b><u>Goodbye</u>'); // Create Template from string, that can later be used to create nodes.
|
|
3120
|
+
*
|
|
3121
|
+
* Add children to an element.
|
|
3122
|
+
* 3. h(el, h`<b>${'Hi'}</b>`, ?options)
|
|
3123
|
+
* 4. h(el, ?options)`<b>${'Hi'}</b>` // typical path used in render(). Create template and render its nodes to el.
|
|
3124
|
+
*
|
|
3125
|
+
* Create top-level element
|
|
3126
|
+
* 5. h()`Hello<b>${'World'}!</b>`
|
|
3127
|
+
*
|
|
3128
|
+
* 6. h(string, object, ...) // Used for JSX
|
|
3129
|
+
* @param htmlStrings {?HTMLElement|string|string[]|function():Template|{render:function()}}
|
|
3130
|
+
* @param exprs {*[]|string|Template|Object}
|
|
3131
|
+
* @return {Node|HTMLElement|Template|Function} */
|
|
3132
|
+
function h(htmlStrings=undefined, ...exprs) {
|
|
3133
|
+
|
|
3134
|
+
// 1. Tagged template: h`<div>...</div>`
|
|
3135
|
+
if (Array.isArray(arguments[0])) {
|
|
3136
|
+
return new Template(arguments[0], exprs);
|
|
3137
|
+
}
|
|
3138
|
+
|
|
3139
|
+
// 2. String to template, or JSX factory form h(tag, props, ...children)
|
|
3140
|
+
else if (typeof arguments[0] === 'string' || arguments[0] instanceof String) {
|
|
3141
|
+
let tagOrHtml = arguments[0];
|
|
3142
|
+
|
|
3143
|
+
// 2a. JSX: h("tag", {props}, ...children)
|
|
3144
|
+
if (exprs.length && (typeof exprs[0] === 'object' || exprs[0] === null)) {
|
|
3145
|
+
let tag = tagOrHtml + '';
|
|
3146
|
+
let props = exprs[0] || {};
|
|
3147
|
+
let children = exprs.slice(1);
|
|
3148
|
+
|
|
3149
|
+
return Template.fromJsx(tag, props, children);
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
// 2b. Plain html string => template: h('<div>...</div>')
|
|
3153
|
+
else {
|
|
3154
|
+
let html = tagOrHtml;
|
|
3155
|
+
// If it starts with whitespace and then a tag, trim it.
|
|
3156
|
+
if (html.match(/^\s^</))
|
|
3157
|
+
html = html.trim();
|
|
3158
|
+
return new Template([html], []);
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
else if (arguments[0] instanceof HTMLElement || arguments[0] instanceof DocumentFragment) {
|
|
3163
|
+
|
|
3164
|
+
// 3. Render template to element: h(el, template)
|
|
3165
|
+
if (arguments[1] instanceof Template) {
|
|
3166
|
+
|
|
3167
|
+
/** @type Template */
|
|
3168
|
+
let template = arguments[1];
|
|
3169
|
+
let parent = arguments[0];
|
|
3170
|
+
let options = arguments[2];
|
|
3171
|
+
template.render(parent, options);
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
// 4. Render tagged template to element: h(el)`<div>...</div>`
|
|
3175
|
+
else {
|
|
3176
|
+
let parent = arguments[0], options = arguments[1];
|
|
3177
|
+
|
|
3178
|
+
// Remove shadowroot if present. TODO: This could mess up paths?
|
|
3179
|
+
if (parent.shadowRoot)
|
|
3180
|
+
parent.innerHTML = '';
|
|
3181
|
+
|
|
3182
|
+
// Return a tagged template function that applies the tagged template to parent.
|
|
3183
|
+
let renderTemplate = (htmlStrings, ...exprs) => {
|
|
3184
|
+
Globals$1.rendered.add(parent);
|
|
3185
|
+
let template = new Template(htmlStrings, exprs);
|
|
3186
|
+
return template.render(parent, options);
|
|
3187
|
+
};
|
|
3188
|
+
return renderTemplate;
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
// 5. Create a static element: h()`<div></div>`
|
|
3193
|
+
else if (!arguments.length) {
|
|
3194
|
+
return (htmlStrings, ...exprs) => {
|
|
3195
|
+
let template = h(htmlStrings, ...exprs);
|
|
3196
|
+
return toEl(template);
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
// 6. Help toEl() with objects: h(this)`<div>...</div>` inside an object's render()
|
|
3201
|
+
// Intercepts the main h(this)`...` function call inside render().
|
|
3202
|
+
// TODO: This path doesn't handle embeds like data-id="..."
|
|
3203
|
+
else if (typeof arguments[0] === 'object' && Globals$1.objToEl.has(arguments[0])) {
|
|
3204
|
+
let obj = arguments[0];
|
|
3205
|
+
|
|
3206
|
+
if (obj.constructor.name !== 'Object')
|
|
3207
|
+
throw new Error(`Solarate Web Component class ${obj.constructor?.name} must extend HTMLElement.`);
|
|
3208
|
+
|
|
3209
|
+
// Jsx with h(this, <jsx>)
|
|
3210
|
+
if (arguments[1] instanceof Template) {
|
|
3211
|
+
let template = arguments[1];
|
|
3212
|
+
let el = template.render();
|
|
3213
|
+
Globals$1.objToEl.set(obj, el);
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
// h(this)`<div>...</div>`
|
|
3217
|
+
else
|
|
3218
|
+
return function(...args) {
|
|
3219
|
+
let template = h(...args);
|
|
3220
|
+
let el = template.render();
|
|
3221
|
+
Globals$1.objToEl.set(obj, el);
|
|
3222
|
+
}.bind(obj);
|
|
3223
|
+
}
|
|
3224
|
+
// TODO: Handle other primitive types?
|
|
3225
|
+
else if (Util.isFalsy(arguments[0]))
|
|
3226
|
+
return new Template();
|
|
3227
|
+
|
|
3228
|
+
else
|
|
3229
|
+
throw new Error('h() does not support argument of type: ' + (arguments[0] ? typeof arguments[0] : arguments[0]))
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
/**
|
|
3233
|
+
* @deprecated Inherit from Solarite and pass arribs to super() instead.
|
|
3107
3234
|
* There are three ways to create an instance of a Solarite Component:
|
|
3108
|
-
* 1. new ComponentName();
|
|
3109
|
-
* 2. this
|
|
3110
|
-
* 3. <body><component-name></component-name></body>
|
|
3235
|
+
* 1. new ComponentName(3); // direct class instantiation
|
|
3236
|
+
* 2. h(this)`<div><component-name user-id=${3}></component-name></div>; // as a child of another Component.
|
|
3237
|
+
* 3. <body><component-name user-id="3"></component-name></body> // in the Document html.
|
|
3111
3238
|
*
|
|
3112
3239
|
* When created via #3, Solarite has no way to pass attributes as arguments to the constructor. So to make
|
|
3113
3240
|
* sure we get the correct value via all three paths, we write our constructors according to the following
|
|
@@ -3115,40 +3242,38 @@ let renderF = 'render';
|
|
|
3115
3242
|
* Browsers make all html attribute names lowercase.
|
|
3116
3243
|
*
|
|
3117
3244
|
* @example
|
|
3118
|
-
* constructor({name,
|
|
3245
|
+
* constructor({name, userId=1}={}) {
|
|
3119
3246
|
* super();
|
|
3120
3247
|
*
|
|
3121
3248
|
* // Get value from "name" attriute if persent, otherwise from name constructor arg.
|
|
3122
3249
|
* this.name = getArg(this, 'name', name);
|
|
3123
3250
|
*
|
|
3124
3251
|
* // Optionally convert the value to an integer.
|
|
3125
|
-
* this.userId = getArg(this, '
|
|
3252
|
+
* this.userId = getArg(this, 'user-id', userId, ArgType.Int);
|
|
3126
3253
|
* }
|
|
3127
3254
|
*
|
|
3128
3255
|
* @param el {HTMLElement}
|
|
3129
3256
|
* @param attributeName {string} Attribute name. Not case-sensitive.
|
|
3130
|
-
* @param defaultValue {*} Default value to use if attribute doesn't exist.
|
|
3257
|
+
* @param defaultValue {*} Default value to use if attribute doesn't exist. Typically the argument from the constructor.
|
|
3131
3258
|
* @param type {ArgType|function|Class|*[]}
|
|
3132
3259
|
* If an array, use the value if it's in the array, otherwise return undefined.
|
|
3133
3260
|
* If it's a function, pass the value to the function and return the result.
|
|
3134
|
-
* @
|
|
3135
|
-
|
|
3136
|
-
* @return {*} Undefined if attribute isn't set. */
|
|
3137
|
-
function getArg(el, attributeName, defaultValue=undefined, type=ArgType.String, fallback=undefined) {
|
|
3261
|
+
* @return {*} Undefined if attribute isn't set and there's no defaultValue, or if the value couldn't be parsed as the type. */
|
|
3262
|
+
function getArg(el, attributeName, defaultValue=undefined, type=ArgType.String) {
|
|
3138
3263
|
let val = defaultValue;
|
|
3139
3264
|
let attrVal = el.getAttribute(attributeName) || el.getAttribute(Util.camelToDashes(attributeName));
|
|
3140
3265
|
if (attrVal !== null) // If attribute doesn't exist.
|
|
3141
3266
|
val = attrVal;
|
|
3142
|
-
|
|
3267
|
+
|
|
3143
3268
|
if (Array.isArray(type))
|
|
3144
|
-
return type.includes(val) ? val :
|
|
3145
|
-
|
|
3269
|
+
return type.includes(val) ? val : undefined;
|
|
3270
|
+
|
|
3146
3271
|
if (typeof type === 'function') {
|
|
3147
3272
|
return type.constructor
|
|
3148
3273
|
? new type(val) // arg type is custom Class
|
|
3149
3274
|
: type(val); // arg type is custom function
|
|
3150
3275
|
}
|
|
3151
|
-
|
|
3276
|
+
|
|
3152
3277
|
// If bool, it's true as long as it exists and its value isn't falsey.
|
|
3153
3278
|
if (type===ArgType.Bool) {
|
|
3154
3279
|
let lAttrVal = typeof val === 'string' ? val.toLowerCase() : val;
|
|
@@ -3156,20 +3281,17 @@ function getArg(el, attributeName, defaultValue=undefined, type=ArgType.String,
|
|
|
3156
3281
|
return false;
|
|
3157
3282
|
if (['true', true].includes(lAttrVal) || parseFloat(lAttrVal) !== 0)
|
|
3158
3283
|
return true;
|
|
3159
|
-
return
|
|
3284
|
+
return undefined;
|
|
3160
3285
|
}
|
|
3161
|
-
|
|
3286
|
+
|
|
3162
3287
|
// Attribute doesn't exist
|
|
3163
|
-
let result;
|
|
3164
3288
|
switch (type) {
|
|
3165
3289
|
case ArgType.Int:
|
|
3166
|
-
|
|
3167
|
-
return isNaN(result) ? fallback : result;
|
|
3290
|
+
return parseInt(val);
|
|
3168
3291
|
case ArgType.Float:
|
|
3169
|
-
|
|
3170
|
-
return isNaN(result) ? fallback : result;
|
|
3292
|
+
return parseFloat(val);
|
|
3171
3293
|
case ArgType.String:
|
|
3172
|
-
return [undefined, null, false].includes(val) ? '' : val+'';
|
|
3294
|
+
return [undefined, null, false].includes(val) ? '' : (val+'');
|
|
3173
3295
|
case ArgType.Json:
|
|
3174
3296
|
case ArgType.Eval:
|
|
3175
3297
|
if (typeof val === 'string' && val.length)
|
|
@@ -3191,6 +3313,7 @@ function getArg(el, attributeName, defaultValue=undefined, type=ArgType.String,
|
|
|
3191
3313
|
|
|
3192
3314
|
|
|
3193
3315
|
/**
|
|
3316
|
+
* @deprecated for Solarite.getAttribs()
|
|
3194
3317
|
* Experimental. Set multiple arguments/attributes all at once.
|
|
3195
3318
|
* @param el {HTMLElement}
|
|
3196
3319
|
* @param args {Record<string, any>}
|
|
@@ -3199,6 +3322,10 @@ function getArg(el, attributeName, defaultValue=undefined, type=ArgType.String,
|
|
|
3199
3322
|
* @example
|
|
3200
3323
|
* constructor({user, path}={}) {
|
|
3201
3324
|
* setArgs(this, arguments[0], {user: User, path: ArgType.String});
|
|
3325
|
+
*
|
|
3326
|
+
* // Equivalent to:
|
|
3327
|
+
* this.user = getArg(this, user, 'user', User); // or new User(user);
|
|
3328
|
+
* this.path = getArg(this, path, 'path', ArgType.String);
|
|
3202
3329
|
* }
|
|
3203
3330
|
*/
|
|
3204
3331
|
function setArgs(el, args, types) {
|
|
@@ -3208,15 +3335,16 @@ function setArgs(el, args, types) {
|
|
|
3208
3335
|
|
|
3209
3336
|
|
|
3210
3337
|
/**
|
|
3338
|
+
* @deprecated
|
|
3211
3339
|
* @enum */
|
|
3212
3340
|
var ArgType = {
|
|
3213
|
-
|
|
3341
|
+
|
|
3214
3342
|
/**
|
|
3215
3343
|
* false, 0, null, undefined, '0', and 'false' (case-insensitive) become false.
|
|
3216
3344
|
* Anything else, including empty string becomes true.
|
|
3217
3345
|
* Empty string is true because attributes with no value should be evaulated as true. */
|
|
3218
3346
|
Bool: 'Bool',
|
|
3219
|
-
|
|
3347
|
+
|
|
3220
3348
|
Int: 'Int',
|
|
3221
3349
|
Float: 'Float',
|
|
3222
3350
|
String: 'String',
|
|
@@ -3235,174 +3363,120 @@ var ArgType = {
|
|
|
3235
3363
|
Eval: 'Eval'
|
|
3236
3364
|
};
|
|
3237
3365
|
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3366
|
+
/*
|
|
3367
|
+
┏┓ ┓ •
|
|
3368
|
+
┗┓┏┓┃┏┓┏┓┓╋▗▖
|
|
3369
|
+
┗┛┗┛┗┗┻╹ ╹╹┗
|
|
3370
|
+
JavasCript UI library
|
|
3371
|
+
@license MIT
|
|
3372
|
+
@copyright Vorticode LLC
|
|
3373
|
+
https://vorticode.github.io/solarite/ */
|
|
3374
|
+
function t(html) {
|
|
3375
|
+
return new Template([html], []);
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
/**
|
|
3379
|
+
* Intercept the construct call to auto-define the class before the constructor is called. */
|
|
3380
|
+
let HTMLElementAutoDefine = new Proxy(HTMLElement, {
|
|
3381
|
+
construct(Parent, args, Class) {
|
|
3243
3382
|
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
options = {extends: extendsTag};
|
|
3383
|
+
// 1. Call customElements.define() automatically.
|
|
3384
|
+
Util.defineClass(Class);
|
|
3247
3385
|
|
|
3248
|
-
|
|
3386
|
+
// 2. This line is equivalent the to super() call to HTMLElement:
|
|
3387
|
+
return Reflect.construct(Parent, args, Class);
|
|
3249
3388
|
}
|
|
3250
|
-
}
|
|
3389
|
+
});
|
|
3251
3390
|
|
|
3252
3391
|
/**
|
|
3253
|
-
*
|
|
3254
|
-
*
|
|
3392
|
+
* Solarite provides more features if your web component extends Solarite instead of HTMLElement.
|
|
3393
|
+
*
|
|
3394
|
+
* Reasons to inherit from Solarite instead of HTMLElement.
|
|
3255
3395
|
* 1. customElements.define() is called automatically when you create the first instance.
|
|
3256
3396
|
* 2. Calls render() when added to the DOM, if it hasn't been called already.
|
|
3257
|
-
* 3.
|
|
3258
|
-
*
|
|
3259
|
-
*
|
|
3260
|
-
* Can't figure out how to have these work standalone though, and still be synchronous.
|
|
3261
|
-
* 6. Can we extend from other element types like TR?
|
|
3262
|
-
* 7. Shows default text if render() function isn't defined.
|
|
3397
|
+
* 3. Populates the attribs argument to the constructor when instantiated from regular html outside a template string.
|
|
3398
|
+
* It parses JSON from DOM attribute values surrouned with '${...}'
|
|
3399
|
+
* 4. Shows an error if render() isn't defined.
|
|
3263
3400
|
*
|
|
3264
3401
|
* Advantages to inheriting from HTMLElement
|
|
3265
|
-
* 1.
|
|
3266
|
-
* 2.
|
|
3267
|
-
* 3.
|
|
3268
|
-
*
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
* Intercept the construct call to auto-define the class before the constructor is called.
|
|
3286
|
-
* @type {HTMLElement} */
|
|
3287
|
-
let HTMLElementAutoDefine = new Proxy(BaseClass, {
|
|
3288
|
-
construct(Parent, args, Class) {
|
|
3289
|
-
defineClass(Class, null, extendsTag);
|
|
3290
|
-
|
|
3291
|
-
// This is a good place to manipulate any args before they're sent to the constructor.
|
|
3292
|
-
// Such as loading them from attributes, if I could find a way to do so.
|
|
3293
|
-
|
|
3294
|
-
// This line is equivalent the to super() call.
|
|
3295
|
-
return Reflect.construct(Parent, args, Class);
|
|
3296
|
-
}
|
|
3297
|
-
});
|
|
3298
|
-
|
|
3299
|
-
return class Solarite extends HTMLElementAutoDefine {
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
/**
|
|
3303
|
-
* TODO: Make these standalone functions.
|
|
3304
|
-
* Callbacks.
|
|
3305
|
-
* Use onConnect.push(() => ...); to add new callbacks. */
|
|
3306
|
-
onConnect;
|
|
3307
|
-
|
|
3308
|
-
onFirstConnect;
|
|
3309
|
-
onDisconnect;
|
|
3310
|
-
|
|
3311
|
-
/**
|
|
3312
|
-
* @param options {RenderOptions} */
|
|
3313
|
-
constructor(options={}) {
|
|
3314
|
-
super();
|
|
3315
|
-
|
|
3316
|
-
// TODO: Is options.render ever used?
|
|
3317
|
-
if (options.render===true)
|
|
3318
|
-
this.render();
|
|
3319
|
-
|
|
3320
|
-
else if (options.render===false)
|
|
3321
|
-
Globals$1.rendered.add(this); // Don't render on connectedCallback()
|
|
3322
|
-
|
|
3323
|
-
// Add slot children before constructor code executes.
|
|
3324
|
-
// This breaks the styleStaticNested test.
|
|
3325
|
-
// PendingChildren is setup in NodeGroup.instantiateComponent()
|
|
3326
|
-
// TODO: Match named slots.
|
|
3327
|
-
//let ch = Globals.pendingChildren.pop();
|
|
3328
|
-
//if (ch) // TODO: how could there be a slot before render is called?
|
|
3329
|
-
// (this.querySelector('slot') || this).append(...ch);
|
|
3330
|
-
|
|
3331
|
-
/** @deprecated
|
|
3332
|
-
Object.defineProperty(this, 'html', {
|
|
3333
|
-
set(html) {
|
|
3334
|
-
Globals.rendered.add(this);
|
|
3335
|
-
if (typeof html === 'string') {
|
|
3336
|
-
console.warn("Assigning to this.html without the r template prefix.")
|
|
3337
|
-
this.innerHTML = html;
|
|
3338
|
-
}
|
|
3339
|
-
else
|
|
3340
|
-
this.modifications = r(this, html, options);
|
|
3402
|
+
* 1. We can inherit from things like HTMLTableRowElement directly.
|
|
3403
|
+
* 2. There's less magic, since everyone is familiar with defining custom elements.
|
|
3404
|
+
* 3. No confusion about how the class name becomes a tag name.
|
|
3405
|
+
* @extends {HTMLElement} */
|
|
3406
|
+
class Solarite extends HTMLElementAutoDefine {
|
|
3407
|
+
|
|
3408
|
+
/**
|
|
3409
|
+
* @param attribs {?Record<string, any>} */
|
|
3410
|
+
constructor(attribs=null) {
|
|
3411
|
+
super();
|
|
3412
|
+
|
|
3413
|
+
if (attribs) {
|
|
3414
|
+
if (typeof attribs !== 'object')
|
|
3415
|
+
throw new Error('First argument to custom element constructor must be an object.');
|
|
3416
|
+
|
|
3417
|
+
// 1. Populate attribs if it's an empty object.
|
|
3418
|
+
if (attribs && !Object.keys(attribs).length) {
|
|
3419
|
+
let attribs2 = Solarite.getAttribs(this);
|
|
3420
|
+
for (let name in attribs2) {
|
|
3421
|
+
attribs[name] = attribs2[name];
|
|
3341
3422
|
}
|
|
3342
|
-
}
|
|
3423
|
+
}
|
|
3343
3424
|
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
this
|
|
3351
|
-
|
|
3425
|
+
// 2. Populate fields from attribs.
|
|
3426
|
+
// This does nothing because the fields are overwritten by the child class after this super() constructor executes.
|
|
3427
|
+
//for (let name in attribs || {}) {
|
|
3428
|
+
// if (name in this) {
|
|
3429
|
+
// const descriptor = Object.getOwnPropertyDescriptor(this, name);
|
|
3430
|
+
// if (!descriptor || descriptor.writable || descriptor.set)
|
|
3431
|
+
// this[name] = attribs[name];
|
|
3432
|
+
// }
|
|
3433
|
+
//}
|
|
3352
3434
|
}
|
|
3353
3435
|
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
this.renderFirstTime();
|
|
3365
|
-
if (!Globals$1.connected.has(this)) {
|
|
3366
|
-
Globals$1.connected.add(this);
|
|
3367
|
-
if (this.onFirstConnect)
|
|
3368
|
-
this.onFirstConnect();
|
|
3369
|
-
}
|
|
3370
|
-
if (this.onConnect)
|
|
3371
|
-
this.onConnect();
|
|
3372
|
-
}
|
|
3373
|
-
|
|
3374
|
-
disconnectedCallback() {
|
|
3375
|
-
if (this.onDisconnect)
|
|
3376
|
-
this.onDisconnect();
|
|
3377
|
-
}
|
|
3436
|
+
// 3. Wrap render function so it always provides the attribs argument.
|
|
3437
|
+
// Disabled because this gives us strings for attribute values when we call render manually.
|
|
3438
|
+
// Instead of values given from ${...} expressions.
|
|
3439
|
+
// let originalRender = this.render;
|
|
3440
|
+
// this.render = (attribs, changed=true) => {
|
|
3441
|
+
// if (!attribs) // If we have to look up the attribs, we don't know if they changed or not.
|
|
3442
|
+
// attribs = Solarite.getAttribs(this);
|
|
3443
|
+
// originalRender.call(this, attribs, changed);
|
|
3444
|
+
// }
|
|
3445
|
+
}
|
|
3378
3446
|
|
|
3447
|
+
'render'() {
|
|
3448
|
+
throw new Error('render() is not defined for ' + this.constructor.name);
|
|
3449
|
+
}
|
|
3379
3450
|
|
|
3380
|
-
|
|
3381
|
-
|
|
3451
|
+
/**
|
|
3452
|
+
* Call render() only if it hasn't already been called. */
|
|
3453
|
+
'renderFirstTime'() {
|
|
3454
|
+
if (!Globals$1.rendered.has(this)) {
|
|
3455
|
+
let attribs = Solarite.getAttribs(this);
|
|
3456
|
+
this.render(attribs); // calls Globals.rendered.add(this); inside the call to h()'...'.
|
|
3382
3457
|
}
|
|
3383
3458
|
}
|
|
3384
|
-
}
|
|
3385
3459
|
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
* Solarite JavasCript UI library.
|
|
3392
|
-
* MIT License
|
|
3393
|
-
* https://vorticode.github.io/solarite/
|
|
3394
|
-
*/
|
|
3460
|
+
/**
|
|
3461
|
+
* Called automatically by the browser. */
|
|
3462
|
+
'connectedCallback'() { // quoted so terser doesn't remove it.
|
|
3463
|
+
this.renderFirstTime();
|
|
3464
|
+
}
|
|
3395
3465
|
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
* @type {Node|Class<HTMLElement>|function(tagName:string):Node|Class<HTMLElement>} */
|
|
3399
|
-
const Solarite = new Proxy(createSolarite(), {
|
|
3400
|
-
apply(self, _, args) {
|
|
3401
|
-
return createSolarite(...args)
|
|
3466
|
+
static 'define'(tagName=null) {
|
|
3467
|
+
Util.defineClass(this, tagName);
|
|
3402
3468
|
}
|
|
3403
|
-
});
|
|
3404
3469
|
|
|
3405
|
-
|
|
3470
|
+
static 'getAttribs'(el) {
|
|
3471
|
+
let result = Util.attribsToObject(el);
|
|
3472
|
+
for (let name in result) {
|
|
3473
|
+
let val = result[name];
|
|
3474
|
+
if (val.startsWith('${') && val.endsWith('}'))
|
|
3475
|
+
result[name] = JSON.parse(val.slice(2, -1));
|
|
3476
|
+
}
|
|
3477
|
+
return result;
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3406
3480
|
|
|
3407
3481
|
export default h;
|
|
3408
|
-
export { ArgType, Globals$1 as Globals, Solarite, Util as SolariteUtil, Template, delve, getArg, h, h as r, setArgs };
|
|
3482
|
+
export { ArgType, Globals$1 as Globals, HtmlParser, NodeGroup, Shell, Solarite, Util as SolariteUtil, Template, delve, getArg, h, h as r, setArgs, t, toEl };
|