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