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