solarite 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Solarite-debug.js +571 -405
- package/dist/Solarite.js +567 -358
- package/dist/Solarite.min.js +11 -2
- package/package.json +2 -2
- package/readme.md +2 -2
- package/src/ExprPath.js +87 -79
- package/src/Globals.js +9 -5
- package/src/NodeGroup.js +79 -242
- package/src/RootNodeGroup.js +153 -0
- package/src/Shell.js +12 -9
- package/src/Solarite.d.ts +8 -4
- package/src/Solarite.js +10 -8
- package/src/Template.js +135 -3
- package/src/Util.js +52 -14
- package/src/createSolarite.js +2 -2
- package/src/h.js +81 -129
- package/src/toEl.js +84 -0
- package/src/udomdiff.js +0 -55
- package/src/watch.js +2 -2
package/dist/Solarite-debug.js
CHANGED
|
@@ -105,8 +105,9 @@ function reset() {
|
|
|
105
105
|
Globals = {
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
|
-
*
|
|
109
|
-
|
|
108
|
+
* Dynamic values that should be passed to a Component's constructor and render() function.
|
|
109
|
+
* @type {Map<HTMLElement, any[]>} */
|
|
110
|
+
componentArgs: new Map(),
|
|
110
111
|
|
|
111
112
|
/**
|
|
112
113
|
* Store which instances of Solarite have already been added to the DOM.
|
|
@@ -121,6 +122,9 @@ function reset() {
|
|
|
121
122
|
|
|
122
123
|
div: document.createElement("div"),
|
|
123
124
|
|
|
125
|
+
/** @type {HTMLDocument} */
|
|
126
|
+
doc: document,
|
|
127
|
+
|
|
124
128
|
/**
|
|
125
129
|
* @type {Record<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
|
|
126
130
|
elementClasses: {},
|
|
@@ -139,20 +143,20 @@ function reset() {
|
|
|
139
143
|
nodeGroups: new WeakMap(),
|
|
140
144
|
|
|
141
145
|
/**
|
|
142
|
-
* Used by
|
|
146
|
+
* Used by h() path 9. */
|
|
143
147
|
objToEl: new WeakMap(),
|
|
144
148
|
|
|
145
149
|
//pendingChildren: [],
|
|
146
150
|
|
|
147
151
|
|
|
148
152
|
/**
|
|
149
|
-
* Elements that have been rendered to by
|
|
153
|
+
* Elements that have been rendered to by h() at least once.
|
|
150
154
|
* This is used by the Solarite class to know when to call onFirstConnect()
|
|
151
155
|
* @type {WeakSet<HTMLElement>} */
|
|
152
156
|
rendered: new WeakSet(),
|
|
153
157
|
|
|
154
158
|
/**
|
|
155
|
-
* Elements that are currently rendering via the
|
|
159
|
+
* Elements that are currently rendering via the h() function.
|
|
156
160
|
* @type {WeakSet<HTMLElement>} */
|
|
157
161
|
rendering: new WeakSet(),
|
|
158
162
|
|
|
@@ -239,12 +243,25 @@ let Util = {
|
|
|
239
243
|
return true; // the same.
|
|
240
244
|
},
|
|
241
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Convert HTMLElement attributes to an object.
|
|
248
|
+
* @param el {HTMLElement}
|
|
249
|
+
* @param ignore {?string} Optionally ignore this attribute.
|
|
250
|
+
* @return {Object} */
|
|
251
|
+
attribsToObject(el, ignore=null) {
|
|
252
|
+
let result = {};
|
|
253
|
+
for (let attrib of el.attributes)
|
|
254
|
+
if (attrib.name !== ignore)
|
|
255
|
+
result[Util.dashesToCamel(attrib.name)] = attrib.value;
|
|
256
|
+
return result;
|
|
257
|
+
},
|
|
258
|
+
|
|
242
259
|
bindId(root, el) {
|
|
243
260
|
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
244
261
|
if (id) { // If something hasn't removed the id.
|
|
245
262
|
|
|
246
263
|
// Don't allow overwriting existing class properties if they already have a non-Node value.
|
|
247
|
-
if (root[id] && !(root[id]
|
|
264
|
+
if (root[id] && !(root[id]?.nodeType))
|
|
248
265
|
throw new Error(`${root.constructor.name}.${id} already has a value. ` +
|
|
249
266
|
`Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
|
|
250
267
|
|
|
@@ -253,26 +270,50 @@ let Util = {
|
|
|
253
270
|
},
|
|
254
271
|
|
|
255
272
|
/**
|
|
273
|
+
* If the style tab has a global attribute:
|
|
274
|
+
* 1. Put it in the document head as <style data-style="tag-name">...</style>
|
|
275
|
+
* 2. Replace the :host {...} CSS selector as tag-name {...}.
|
|
276
|
+
* Otherwise keep it where it is and:
|
|
277
|
+
* 1. Add data-style="1" attribute to the root element.
|
|
278
|
+
* 2. Replace the :host {...} selector in the style as tag-name[data-style='1'] {...}
|
|
256
279
|
* @param style {HTMLStyleElement}
|
|
257
280
|
* @param root {HTMLElement} */
|
|
258
281
|
bindStyles(style, root) {
|
|
259
|
-
let styleId = root.getAttribute('data-style');
|
|
260
|
-
if (!styleId) {
|
|
261
|
-
// Keep track of one style id for each class.
|
|
262
|
-
// TODO: Put this outside the class in a map, so it doesn't conflict with static properties.
|
|
263
|
-
if (!root.constructor.styleId)
|
|
264
|
-
root.constructor.styleId = 1;
|
|
265
|
-
styleId = root.constructor.styleId++;
|
|
266
282
|
|
|
267
|
-
|
|
283
|
+
let tagName = root.tagName.toLowerCase();
|
|
284
|
+
let styleId, attribSelector;
|
|
285
|
+
|
|
286
|
+
if (style.hasAttribute('global') || style.hasAttribute('data-global')) {
|
|
287
|
+
styleId = tagName;
|
|
288
|
+
attribSelector = '';
|
|
289
|
+
let doc = Globals$1.doc || root.ownerDocument || document;
|
|
290
|
+
if (!doc.head.querySelector(`style[data-style="${styleId}"]`)) {
|
|
291
|
+
doc.head.append(style);
|
|
292
|
+
style.setAttribute('data-style', styleId);
|
|
293
|
+
}
|
|
294
|
+
else // TODO: Make sure the style has no expressions.
|
|
295
|
+
style.remove(); // already in the head.
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
let styleId = root.getAttribute('data-style');
|
|
299
|
+
if (!styleId) {
|
|
300
|
+
// Keep track of one style id for each class.
|
|
301
|
+
// TODO: Put this outside the class in a map, so it doesn't conflict with static properties.
|
|
302
|
+
if (!root.constructor.styleId)
|
|
303
|
+
root.constructor.styleId = 1;
|
|
304
|
+
styleId = root.constructor.styleId++;
|
|
305
|
+
|
|
306
|
+
root.setAttribute('data-style', styleId);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
attribSelector = `[data-style="${styleId}"]`;
|
|
268
310
|
}
|
|
269
311
|
|
|
270
312
|
// Replace ":host" with "tagName[data-style=...]" in the css.
|
|
271
|
-
let tagName = root.tagName.toLowerCase();
|
|
272
313
|
for (let child of style.childNodes) {
|
|
273
314
|
if (child.nodeType === 3) {
|
|
274
315
|
let oldText = child.textContent;
|
|
275
|
-
let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, `${tagName}
|
|
316
|
+
let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, `${tagName}${attribSelector}`);
|
|
276
317
|
if (oldText !== newText)
|
|
277
318
|
child.textContent = newText;
|
|
278
319
|
}
|
|
@@ -319,7 +360,6 @@ let Util = {
|
|
|
319
360
|
return str.replace(/-([a-z])/g, g => g[1].toUpperCase());
|
|
320
361
|
},
|
|
321
362
|
|
|
322
|
-
|
|
323
363
|
/**
|
|
324
364
|
* A generator function that recursively traverses and flattens a value.
|
|
325
365
|
*
|
|
@@ -360,7 +400,7 @@ let Util = {
|
|
|
360
400
|
|
|
361
401
|
/**
|
|
362
402
|
* Get the value of an input as the most appropriate JavaScript type.
|
|
363
|
-
* @param node {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|
|
|
403
|
+
* @param node {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|HTMLElement}
|
|
364
404
|
* @return {string|string[]|number|[]|File[]|Date|boolean} */
|
|
365
405
|
getInputValue(node) {
|
|
366
406
|
// .type is a built-in DOM property
|
|
@@ -374,6 +414,8 @@ let Util = {
|
|
|
374
414
|
return node.valueAsDate; // Date Object
|
|
375
415
|
if (node.type === 'select-multiple') // <select multiple>
|
|
376
416
|
return [...node.selectedOptions].map(option => option.value); // Array of Strings
|
|
417
|
+
if (node.hasAttribute('contenteditable'))
|
|
418
|
+
return node.innerHTML;
|
|
377
419
|
|
|
378
420
|
return node.value; // String
|
|
379
421
|
},
|
|
@@ -682,11 +724,6 @@ class MultiValueMap {
|
|
|
682
724
|
* @returns {Node[]} The same list of future children.
|
|
683
725
|
*/
|
|
684
726
|
const udomdiff = (parentNode, a, b, before) => {
|
|
685
|
-
//#IFDEV
|
|
686
|
-
// if (parentNode instanceof ExprPath)
|
|
687
|
-
// parentNode.verify();
|
|
688
|
-
//#ENDIF
|
|
689
|
-
|
|
690
727
|
const bLength = b.length;
|
|
691
728
|
let aEnd = a.length;
|
|
692
729
|
let bEnd = bLength;
|
|
@@ -708,13 +745,6 @@ const udomdiff = (parentNode, a, b, before) => {
|
|
|
708
745
|
while (bStart < bEnd) {
|
|
709
746
|
let bNode = b[bStart++];
|
|
710
747
|
parentNode.insertBefore(bNode, node);
|
|
711
|
-
|
|
712
|
-
//#IFDEV
|
|
713
|
-
if (bNode instanceof NodeGroup)
|
|
714
|
-
bNode.verify();
|
|
715
|
-
// if (parentNode instanceof ExprPath)
|
|
716
|
-
// parentNode.verify();
|
|
717
|
-
//#ENDIF
|
|
718
748
|
}
|
|
719
749
|
}
|
|
720
750
|
// remove head or tail: fast path
|
|
@@ -724,13 +754,6 @@ const udomdiff = (parentNode, a, b, before) => {
|
|
|
724
754
|
let aNode = a[aStart];
|
|
725
755
|
if (!map || !map.has(aNode)) {
|
|
726
756
|
parentNode.removeChild(aNode);
|
|
727
|
-
|
|
728
|
-
//#IFDEV
|
|
729
|
-
if (aNode instanceof NodeGroup)
|
|
730
|
-
aNode.verify();
|
|
731
|
-
// if (parentNode instanceof ExprPath)
|
|
732
|
-
// parentNode.verify();
|
|
733
|
-
//#ENDIF
|
|
734
757
|
}
|
|
735
758
|
aStart++;
|
|
736
759
|
}
|
|
@@ -767,24 +790,10 @@ const udomdiff = (parentNode, a, b, before) => {
|
|
|
767
790
|
a2,
|
|
768
791
|
b2.nextSibling
|
|
769
792
|
);
|
|
770
|
-
//#IFDEV
|
|
771
|
-
if (a2 instanceof NodeGroup)
|
|
772
|
-
a2.verify();
|
|
773
|
-
// if (parentNode instanceof ExprPath)
|
|
774
|
-
// parentNode.verify();
|
|
775
|
-
//#ENDIF
|
|
776
793
|
|
|
777
794
|
let bNode = b[--bEnd];
|
|
778
795
|
parentNode.insertBefore(bNode, node);
|
|
779
796
|
|
|
780
|
-
//#IFDEV
|
|
781
|
-
if (bNode instanceof NodeGroup)
|
|
782
|
-
bNode.verify();
|
|
783
|
-
// if (parentNode instanceof ExprPath)
|
|
784
|
-
// parentNode.verify();
|
|
785
|
-
|
|
786
|
-
//#ENDIF
|
|
787
|
-
|
|
788
797
|
// mark the future index as identical (yeah, it's dirty, but cheap 👍)
|
|
789
798
|
// The main reason to do this, is that when a[aEnd] will be reached,
|
|
790
799
|
// the loop will likely be on the fast path, as identical to b[bEnd].
|
|
@@ -832,14 +841,6 @@ const udomdiff = (parentNode, a, b, before) => {
|
|
|
832
841
|
while (bStart < index) {
|
|
833
842
|
let bNode = b[bStart++];
|
|
834
843
|
parentNode.insertBefore(bNode, node);
|
|
835
|
-
|
|
836
|
-
//#IFDEV
|
|
837
|
-
if (bNode instanceof NodeGroup)
|
|
838
|
-
bNode.verify();
|
|
839
|
-
// if (parentNode instanceof ExprPath)
|
|
840
|
-
// parentNode.verify();
|
|
841
|
-
|
|
842
|
-
//#ENDIF
|
|
843
844
|
}
|
|
844
845
|
}
|
|
845
846
|
// if the effort wasn't good enough, fallback to a replace,
|
|
@@ -852,13 +853,6 @@ const udomdiff = (parentNode, a, b, before) => {
|
|
|
852
853
|
bNode,
|
|
853
854
|
aNode
|
|
854
855
|
);
|
|
855
|
-
|
|
856
|
-
//#IFDEV
|
|
857
|
-
if (aNode instanceof NodeGroup)
|
|
858
|
-
aNode.verify();
|
|
859
|
-
// if (parentNode instanceof ExprPath)
|
|
860
|
-
// parentNode.verify();
|
|
861
|
-
//#ENDIF
|
|
862
856
|
}
|
|
863
857
|
}
|
|
864
858
|
// otherwise move the source forward, 'cause there's nothing to do
|
|
@@ -871,13 +865,6 @@ const udomdiff = (parentNode, a, b, before) => {
|
|
|
871
865
|
else {
|
|
872
866
|
let aNode = a[aStart++];
|
|
873
867
|
parentNode.removeChild(aNode);
|
|
874
|
-
|
|
875
|
-
//#IFDEV
|
|
876
|
-
if (aNode instanceof NodeGroup)
|
|
877
|
-
aNode.verify();
|
|
878
|
-
// if (parentNode instanceof ExprPath)
|
|
879
|
-
// parentNode.verify();
|
|
880
|
-
//#ENDIF
|
|
881
868
|
}
|
|
882
869
|
}
|
|
883
870
|
}
|
|
@@ -1005,13 +992,13 @@ class ExprPath {
|
|
|
1005
992
|
case 2: // PathType.Multiple:
|
|
1006
993
|
this.applyMultipleAttribs(this.nodeMarker, exprs[0]);
|
|
1007
994
|
break;
|
|
1008
|
-
case
|
|
995
|
+
case 4: // PathType.Comment:
|
|
1009
996
|
// Expressions inside Html comments. Deliberately empty because we won't waste time updating them.
|
|
1010
997
|
break;
|
|
1011
|
-
case
|
|
998
|
+
case 5: // PathType.Event:
|
|
1012
999
|
this.applyEventAttrib(this.nodeMarker, exprs[0], this.parentNg.rootNg.root);
|
|
1013
1000
|
break;
|
|
1014
|
-
default: //
|
|
1001
|
+
default: // 3 PathType.Attribute
|
|
1015
1002
|
// One attribute value may have multiple expressions. Here we apply them all at once.
|
|
1016
1003
|
this.applyValueAttrib(this.nodeMarker, exprs);
|
|
1017
1004
|
break;
|
|
@@ -1021,7 +1008,7 @@ class ExprPath {
|
|
|
1021
1008
|
/**
|
|
1022
1009
|
* Insert/replace the nodes created by a single expression.
|
|
1023
1010
|
* Called by applyExprs()
|
|
1024
|
-
* This function is recursive
|
|
1011
|
+
* This function is recursive. It calls functions that call applyNodes().
|
|
1025
1012
|
* @param expr {Expr}
|
|
1026
1013
|
* @param freeNodeGroups {boolean}
|
|
1027
1014
|
* @return {Node[]} New Nodes created. */
|
|
@@ -1056,7 +1043,6 @@ class ExprPath {
|
|
|
1056
1043
|
if (secondPass.length) {
|
|
1057
1044
|
for (let [nodesIndex, ngIndex] of secondPass) {
|
|
1058
1045
|
let ng = path.getNodeGroup(newNodes[nodesIndex], false);
|
|
1059
|
-
|
|
1060
1046
|
let ngNodes = ng.getNodes();
|
|
1061
1047
|
|
|
1062
1048
|
/*#IFDEV*/assert(!(newNodes[nodesIndex] instanceof NodeGroup));/*#ENDIF*/
|
|
@@ -1077,11 +1063,8 @@ class ExprPath {
|
|
|
1077
1063
|
|
|
1078
1064
|
/*#IFDEV*/assert(!path.nodeGroups.includes(null));/*#ENDIF*/
|
|
1079
1065
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
1066
|
let oldNodes = path.getNodes();
|
|
1083
1067
|
|
|
1084
|
-
|
|
1085
1068
|
// This pre-check makes it a few percent faster?
|
|
1086
1069
|
let same = Util.arraySame(oldNodes, newNodes);
|
|
1087
1070
|
if (!same) {
|
|
@@ -1094,7 +1077,6 @@ class ExprPath {
|
|
|
1094
1077
|
// Fast clear method
|
|
1095
1078
|
let isNowEmpty = oldNodes.length && !newNodes.length;
|
|
1096
1079
|
if (!isNowEmpty || !path.fastClear())
|
|
1097
|
-
|
|
1098
1080
|
// Rearrange nodes.
|
|
1099
1081
|
udomdiff(path.nodeMarker.parentNode, oldNodes, newNodes, path.nodeMarker);
|
|
1100
1082
|
|
|
@@ -1107,18 +1089,17 @@ class ExprPath {
|
|
|
1107
1089
|
ng.removeAndSaveOrphans();
|
|
1108
1090
|
|
|
1109
1091
|
// Instantiate components created within ${...} expressions.
|
|
1110
|
-
//
|
|
1092
|
+
// Also see this.applyExactNodes() which handles calling render() on web components even if they are unchanged.
|
|
1111
1093
|
for (let el of newNodes) {
|
|
1112
|
-
if (el
|
|
1094
|
+
if (el?.nodeType === 1) { // HTMLElement
|
|
1113
1095
|
if (el.hasAttribute('solarite-placeholder'))
|
|
1114
|
-
this.parentNg.
|
|
1096
|
+
this.parentNg.handleComponent(el, null, true);
|
|
1115
1097
|
for (let child of el.querySelectorAll('[solarite-placeholder]'))
|
|
1116
|
-
this.parentNg.
|
|
1098
|
+
this.parentNg.handleComponent(child, null, true);
|
|
1117
1099
|
}
|
|
1118
1100
|
}
|
|
1119
1101
|
}
|
|
1120
1102
|
|
|
1121
|
-
|
|
1122
1103
|
/*#IFDEV*/path.verify();/*#ENDIF*/
|
|
1123
1104
|
}
|
|
1124
1105
|
|
|
@@ -1228,7 +1209,7 @@ class ExprPath {
|
|
|
1228
1209
|
}
|
|
1229
1210
|
|
|
1230
1211
|
// String/Number/Date/Boolean
|
|
1231
|
-
else if (!(expr instanceof Template) && !(expr
|
|
1212
|
+
else if (!(expr instanceof Template) && !(expr?.nodeType)){
|
|
1232
1213
|
// Convert expression to a string.
|
|
1233
1214
|
if (expr === undefined || expr === false || expr === null) // Util.isFalsy() inlined
|
|
1234
1215
|
expr = '';
|
|
@@ -1238,7 +1219,9 @@ class ExprPath {
|
|
|
1238
1219
|
// Get the same Template for the same string each time.
|
|
1239
1220
|
// let template = Globals.stringTemplates[expr];
|
|
1240
1221
|
// if (!template) {
|
|
1222
|
+
|
|
1241
1223
|
let template = new Template([expr], []);
|
|
1224
|
+
template.isText = true;
|
|
1242
1225
|
// Globals.stringTemplates[expr] = template;
|
|
1243
1226
|
//}
|
|
1244
1227
|
|
|
@@ -1263,12 +1246,44 @@ class ExprPath {
|
|
|
1263
1246
|
|
|
1264
1247
|
if (expr instanceof Template) {
|
|
1265
1248
|
let ng = this.getNodeGroup(expr, true);
|
|
1249
|
+
|
|
1266
1250
|
if (ng) {
|
|
1251
|
+
let newestNodes = ng.getNodes();
|
|
1252
|
+
newNodes.push(...newestNodes);
|
|
1253
|
+
|
|
1254
|
+
// New!
|
|
1255
|
+
// Re-apply all expressions if there's a web component, so we can pass them to its constructor.
|
|
1256
|
+
// NodeGroup.applyExprs() is used to call applyComponentExprs() on web components that have expression attributes.
|
|
1257
|
+
// For those that don't, we call applyComponentExprs() directly here.
|
|
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
|
+
}
|
|
1267
1279
|
|
|
1268
|
-
//
|
|
1269
|
-
|
|
1270
|
-
|
|
1280
|
+
// This calls render() on web components that have expressions as attributes.
|
|
1281
|
+
if (apply)
|
|
1282
|
+
ng.applyExprs(expr.exprs);
|
|
1283
|
+
|
|
1271
1284
|
this.nodeGroups.push(ng);
|
|
1285
|
+
|
|
1286
|
+
return ng;
|
|
1272
1287
|
}
|
|
1273
1288
|
|
|
1274
1289
|
// If expression, mark it to be evaluated later in ExprPath.apply() to find partial match.
|
|
@@ -1280,10 +1295,10 @@ class ExprPath {
|
|
|
1280
1295
|
}
|
|
1281
1296
|
|
|
1282
1297
|
// Node(s) created by an expression.
|
|
1283
|
-
else if (expr
|
|
1298
|
+
else if (expr?.nodeType) {
|
|
1284
1299
|
|
|
1285
1300
|
// DocumentFragment created by an expression.
|
|
1286
|
-
if (expr
|
|
1301
|
+
if (expr?.nodeType === 11) // DocumentFragment
|
|
1287
1302
|
newNodes.push(...expr.childNodes);
|
|
1288
1303
|
else
|
|
1289
1304
|
newNodes.push(expr);
|
|
@@ -1359,8 +1374,8 @@ class ExprPath {
|
|
|
1359
1374
|
* @param root */
|
|
1360
1375
|
applyEventAttrib(node, expr, root) {
|
|
1361
1376
|
/*#IFDEV*/
|
|
1362
|
-
assert(this.type === ExprPathType.Event
|
|
1363
|
-
assert(root
|
|
1377
|
+
assert(this.type === ExprPathType.Event);
|
|
1378
|
+
assert(root?.nodeType === 1);
|
|
1364
1379
|
/*#ENDIF*/
|
|
1365
1380
|
|
|
1366
1381
|
let eventName = this.attrName.slice(2); // remove "on-" prefix.
|
|
@@ -1440,7 +1455,7 @@ class ExprPath {
|
|
|
1440
1455
|
}
|
|
1441
1456
|
|
|
1442
1457
|
/**
|
|
1443
|
-
*
|
|
1458
|
+
* Set the value of an attribute. This can be for any attribute, not just attributes named "value".
|
|
1444
1459
|
* @param node
|
|
1445
1460
|
* @param exprs */
|
|
1446
1461
|
// TODO: node is always this.nodeMarker?
|
|
@@ -1470,12 +1485,21 @@ class ExprPath {
|
|
|
1470
1485
|
}
|
|
1471
1486
|
else {
|
|
1472
1487
|
// TODO: should we remove isFalsy, since these are always props?
|
|
1473
|
-
|
|
1488
|
+
const strValue = Util.isFalsy(value) ? '' : value;
|
|
1474
1489
|
|
|
1475
|
-
//
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
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
|
+
|
|
1498
|
+
// If we don't have this condition, when we call render(), the browser will scroll to the currently
|
|
1499
|
+
// selected item in a <select> and mess up manually scrolling to a different value.
|
|
1500
|
+
if (strValue !== node[this.attrName])
|
|
1501
|
+
node[this.attrName] = strValue;
|
|
1502
|
+
}
|
|
1479
1503
|
}
|
|
1480
1504
|
|
|
1481
1505
|
// TODO: We need to remove any old listeners, like in bindEventAttribute.
|
|
@@ -1495,16 +1519,8 @@ class ExprPath {
|
|
|
1495
1519
|
|
|
1496
1520
|
// Regular attribute
|
|
1497
1521
|
else {
|
|
1498
|
-
//
|
|
1499
|
-
//
|
|
1500
|
-
//if (this.attrName === 'disabled')
|
|
1501
|
-
// debugger;
|
|
1502
|
-
|
|
1503
|
-
// hasOwnProperty() checks only the object, not the parents
|
|
1504
|
-
// this.attrName in node checks the node and the parents.
|
|
1505
|
-
// This version checks the html element it extends from, to see if has a setter set:
|
|
1506
|
-
// Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), this.attrName)?.set
|
|
1507
|
-
//let isProp = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), this.attrName)?.set;
|
|
1522
|
+
// Cache this on ExprPath.isHtmlProperty when Shell creates the props.
|
|
1523
|
+
// Have ExprPath.clone() copy .isHtmlProperty?
|
|
1508
1524
|
let isProp = this.isHtmlProperty;
|
|
1509
1525
|
if (isProp === undefined)
|
|
1510
1526
|
isProp = this.isHtmlProperty = Util.isHtmlProp(node, this.attrName);
|
|
@@ -1514,7 +1530,7 @@ class ExprPath {
|
|
|
1514
1530
|
if (!multiple) {
|
|
1515
1531
|
Globals$1.currentExprPath = this; // Used by watch()
|
|
1516
1532
|
if (typeof expr === 'function') {
|
|
1517
|
-
if (this.
|
|
1533
|
+
if (this.isComponent) { // Don't evaluate functions before passing them to components
|
|
1518
1534
|
return
|
|
1519
1535
|
}
|
|
1520
1536
|
this.watchFunction = expr; // The function that gets the expression, used for renderWatched()
|
|
@@ -1572,6 +1588,13 @@ class ExprPath {
|
|
|
1572
1588
|
// since we also prohibit expressions that are a child of textarea.
|
|
1573
1589
|
if (isProp)
|
|
1574
1590
|
node[this.attrName] = joinedValue;
|
|
1591
|
+
|
|
1592
|
+
// Allow one-way binding to contenteditable value attribute.
|
|
1593
|
+
// Contenteditables normally don't have a value attribute and have their content set via innerHTML.
|
|
1594
|
+
// Solarite doesn't allow contenteditables to have expressions as their children.
|
|
1595
|
+
else if (node.hasAttribute('contenteditable'))
|
|
1596
|
+
node.innerHTML = joinedValue;
|
|
1597
|
+
|
|
1575
1598
|
// TODO: Putting an 'else' here would be more performant
|
|
1576
1599
|
node.setAttribute(this.attrName, joinedValue);
|
|
1577
1600
|
}
|
|
@@ -1602,11 +1625,9 @@ class ExprPath {
|
|
|
1602
1625
|
nodeBefore = childNodes[this.nodeBeforeIndex];
|
|
1603
1626
|
|
|
1604
1627
|
let result = new ExprPath(nodeBefore, nodeMarker, this.type, this.attrName, this.attrValue);
|
|
1628
|
+
result.isComponent = this.isComponent;
|
|
1605
1629
|
|
|
1606
1630
|
//#IFDEV
|
|
1607
|
-
result.nodeMarker.exprPath = result;
|
|
1608
|
-
if (result.nodeBefore)
|
|
1609
|
-
result.nodeBefore.prevExprPath = result;
|
|
1610
1631
|
result.verify();
|
|
1611
1632
|
//#ENDIF
|
|
1612
1633
|
|
|
@@ -1615,9 +1636,7 @@ class ExprPath {
|
|
|
1615
1636
|
|
|
1616
1637
|
/**
|
|
1617
1638
|
* Clear the nodeCache of this ExprPath, as well as all parent and child ExprPaths that
|
|
1618
|
-
* share the same DOM parent node.
|
|
1619
|
-
*
|
|
1620
|
-
* TODO: Is recursive clearing ever necessary? */
|
|
1639
|
+
* share the same DOM parent node. */
|
|
1621
1640
|
clearNodesCache() {
|
|
1622
1641
|
let path = this;
|
|
1623
1642
|
|
|
@@ -1630,9 +1649,6 @@ class ExprPath {
|
|
|
1630
1649
|
// If stuck in an infinite loop here, the problem is likely due to Template hash colisions.
|
|
1631
1650
|
// Which cause one path to be the descendant of itself, creating a cycle.
|
|
1632
1651
|
}
|
|
1633
|
-
|
|
1634
|
-
// Commented out on Sep 30, 2024 b/c it was making the benchmark never finish when adding 10k rows.
|
|
1635
|
-
//clearChildNodeCache(this);
|
|
1636
1652
|
}
|
|
1637
1653
|
|
|
1638
1654
|
|
|
@@ -1674,7 +1690,7 @@ class ExprPath {
|
|
|
1674
1690
|
// result2.push(...ng.getNodes())
|
|
1675
1691
|
// return result2;
|
|
1676
1692
|
|
|
1677
|
-
if (this.type === ExprPathType.AttribValue || this.type === ExprPathType.AttribMultiple
|
|
1693
|
+
if (this.type === ExprPathType.AttribValue || this.type === ExprPathType.AttribMultiple) {
|
|
1678
1694
|
return [this.nodeMarker];
|
|
1679
1695
|
}
|
|
1680
1696
|
|
|
@@ -1734,11 +1750,13 @@ class ExprPath {
|
|
|
1734
1750
|
result = collection.deleteAny(template.getExactKey());
|
|
1735
1751
|
}
|
|
1736
1752
|
|
|
1737
|
-
if (result) // also delete the matching close key.
|
|
1753
|
+
if (result) {// also delete the matching close key.
|
|
1738
1754
|
collection.deleteSpecific(template.getCloseKey(), result);
|
|
1739
|
-
|
|
1740
|
-
|
|
1755
|
+
|
|
1756
|
+
//result.applyExprs(template.exprs);
|
|
1741
1757
|
}
|
|
1758
|
+
else
|
|
1759
|
+
return null;
|
|
1742
1760
|
}
|
|
1743
1761
|
|
|
1744
1762
|
// Find a close match.
|
|
@@ -1772,12 +1790,6 @@ class ExprPath {
|
|
|
1772
1790
|
return result;
|
|
1773
1791
|
}
|
|
1774
1792
|
|
|
1775
|
-
isComponent() {
|
|
1776
|
-
// Events won't have type===Component.
|
|
1777
|
-
// TODO: Have a special flag for components instead of it being on the type?
|
|
1778
|
-
return this.type === ExprPathType.ComponentAttribValue || (this.attrName && this.nodeMarker.tagName && this.nodeMarker.tagName.includes('-'));
|
|
1779
|
-
}
|
|
1780
|
-
|
|
1781
1793
|
/**
|
|
1782
1794
|
* TODO: Rename this to nodeGroupsInUse, nodeGroupsAvialableAttached and nodeGroupsAvailableDetached?
|
|
1783
1795
|
* Nodes that have been used during the current render().
|
|
@@ -1835,7 +1847,7 @@ class ExprPath {
|
|
|
1835
1847
|
`parentNode: ${this.nodeBefore.parentNode?.tagName?.toLowerCase()}`,
|
|
1836
1848
|
'nodes:',
|
|
1837
1849
|
...setIndent(this.getNodes().map(item => {
|
|
1838
|
-
if (item
|
|
1850
|
+
if (item?.nodeType)
|
|
1839
1851
|
return item.outerHTML || item.textContent
|
|
1840
1852
|
else if (item instanceof NodeGroup)
|
|
1841
1853
|
return item.debug
|
|
@@ -1892,7 +1904,7 @@ class ExprPath {
|
|
|
1892
1904
|
/** @enum {int} */
|
|
1893
1905
|
const ExprPathType = {
|
|
1894
1906
|
/** Child of a node */
|
|
1895
|
-
Content: 1,
|
|
1907
|
+
Content: 1, // TODO: Rename to Nodes
|
|
1896
1908
|
|
|
1897
1909
|
/** One or more whole attributes */
|
|
1898
1910
|
AttribMultiple: 2,
|
|
@@ -1900,14 +1912,11 @@ const ExprPathType = {
|
|
|
1900
1912
|
/** Value of an attribute. */
|
|
1901
1913
|
AttribValue: 3,
|
|
1902
1914
|
|
|
1903
|
-
/** Value of an attribute being passed to a component. */
|
|
1904
|
-
ComponentAttribValue: 4,
|
|
1905
|
-
|
|
1906
1915
|
/** Expressions inside Html comments. */
|
|
1907
|
-
Comment:
|
|
1916
|
+
Comment: 4,
|
|
1908
1917
|
|
|
1909
1918
|
/** Value of an attribute. */
|
|
1910
|
-
Event:
|
|
1919
|
+
Event: 5,
|
|
1911
1920
|
};
|
|
1912
1921
|
|
|
1913
1922
|
|
|
@@ -2075,7 +2084,7 @@ class Shell {
|
|
|
2075
2084
|
//#ENDIF
|
|
2076
2085
|
|
|
2077
2086
|
if (html.length === 1 && !html[0].match(/[<&]/)) {
|
|
2078
|
-
this.fragment =
|
|
2087
|
+
this.fragment = Globals$1.doc.createTextNode(html[0]);
|
|
2079
2088
|
return;
|
|
2080
2089
|
}
|
|
2081
2090
|
|
|
@@ -2083,18 +2092,18 @@ class Shell {
|
|
|
2083
2092
|
// 1. Add placeholders
|
|
2084
2093
|
let joinedHtml = Shell.addPlaceholders(html);
|
|
2085
2094
|
|
|
2086
|
-
let template =
|
|
2095
|
+
let template = Globals$1.doc.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
|
|
2087
2096
|
if (joinedHtml)
|
|
2088
2097
|
template.innerHTML = joinedHtml;
|
|
2089
2098
|
else // Create one text node, so shell isn't empty and NodeGroups created from it have something to point the startNode and endNode at.
|
|
2090
|
-
template.content.append(
|
|
2099
|
+
template.content.append(Globals$1.doc.createTextNode(''));
|
|
2091
2100
|
this.fragment = template.content;
|
|
2092
2101
|
|
|
2093
2102
|
// 2. Find placeholders
|
|
2094
2103
|
let node;
|
|
2095
2104
|
let toRemove = [];
|
|
2096
2105
|
let placeholdersUsed = 0;
|
|
2097
|
-
const walker =
|
|
2106
|
+
const walker = Globals$1.doc.createTreeWalker(this.fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
|
|
2098
2107
|
while (node = walker.nextNode()) {
|
|
2099
2108
|
|
|
2100
2109
|
// Remove previous after each iteration, so paths will still be calculated correctly.
|
|
@@ -2130,10 +2139,13 @@ class Shell {
|
|
|
2130
2139
|
// Replace comment placeholders
|
|
2131
2140
|
else if (node.nodeType === 8 && node.nodeValue === '!✨!') {
|
|
2132
2141
|
|
|
2142
|
+
if (node?.parentNode?.closest && node?.parentNode?.closest('[contenteditable]'))
|
|
2143
|
+
throw new Error(`Contenteditable can't have expressions inside them. Use <div contenteditable value="\${...}"> instead.`);
|
|
2144
|
+
|
|
2133
2145
|
// Get or create nodeBefore.
|
|
2134
2146
|
let nodeBefore = node.previousSibling; // Can be the same as another Path's nodeMarker.
|
|
2135
2147
|
if (!nodeBefore) {
|
|
2136
|
-
nodeBefore =
|
|
2148
|
+
nodeBefore = Globals$1.doc.createComment('ExprPath:'+this.paths.length);
|
|
2137
2149
|
node.parentNode.insertBefore(nodeBefore, node);
|
|
2138
2150
|
}
|
|
2139
2151
|
/*#IFDEV*/assert(nodeBefore);/*#ENDIF*/
|
|
@@ -2158,9 +2170,9 @@ class Shell {
|
|
|
2158
2170
|
placeholdersUsed ++;
|
|
2159
2171
|
}
|
|
2160
2172
|
|
|
2173
|
+
// Comments become text nodes when inside textareas.
|
|
2161
2174
|
else if (node.nodeType === 3 && node.parentNode?.tagName === 'TEXTAREA' && node.textContent.includes('<!--!✨!-->'))
|
|
2162
2175
|
throw new Error(`Textarea can't have expressions inside them. Use <textarea value="\${...}"> instead.`);
|
|
2163
|
-
|
|
2164
2176
|
|
|
2165
2177
|
|
|
2166
2178
|
// Sometimes users will comment out a block of html code that has expressions.
|
|
@@ -2184,7 +2196,7 @@ class Shell {
|
|
|
2184
2196
|
|
|
2185
2197
|
let placeholders = [];
|
|
2186
2198
|
for (let i = 0; i<parts.length; i++) {
|
|
2187
|
-
let current =
|
|
2199
|
+
let current = Globals$1.doc.createTextNode(parts[i]);
|
|
2188
2200
|
node.parentNode.insertBefore(current, node);
|
|
2189
2201
|
if (i > 0)
|
|
2190
2202
|
placeholders.push(current);
|
|
@@ -2223,9 +2235,9 @@ class Shell {
|
|
|
2223
2235
|
path.nodeMarkerPath = getNodePath(path.nodeMarker);
|
|
2224
2236
|
|
|
2225
2237
|
// Cache so we don't have to calculate this later inside NodeGroup.applyExprs()
|
|
2226
|
-
if (path.type === ExprPathType.AttribValue && path.nodeMarker.nodeType === 1 &&
|
|
2238
|
+
if ((path.type === ExprPathType.AttribValue || path.type === ExprPathType.Event) && path.nodeMarker.nodeType === 1 &&
|
|
2227
2239
|
(path.nodeMarker.tagName.includes('-') || path.nodeMarker.hasAttribute('is'))) {
|
|
2228
|
-
path.
|
|
2240
|
+
path.isComponent = true;
|
|
2229
2241
|
}
|
|
2230
2242
|
}
|
|
2231
2243
|
|
|
@@ -2399,30 +2411,39 @@ class NodeGroup {
|
|
|
2399
2411
|
* @type {?Map<HTMLStyleElement, string>} */
|
|
2400
2412
|
styles;
|
|
2401
2413
|
|
|
2414
|
+
dynamicComponents = new Set();
|
|
2415
|
+
staticComponents = [];
|
|
2416
|
+
|
|
2417
|
+
/** @type {Template} */
|
|
2418
|
+
template;
|
|
2419
|
+
|
|
2402
2420
|
|
|
2403
2421
|
/**
|
|
2404
2422
|
* Create an "instantiated" NodeGroup from a Template and add it to an element.
|
|
2405
2423
|
* @param template {Template} Create it from the html strings and expressions in this template.
|
|
2406
2424
|
* @param parentPath {?ExprPath} */
|
|
2407
2425
|
constructor(template, parentPath=null) {
|
|
2426
|
+
this.rootNg = parentPath?.parentNg?.rootNg || this;
|
|
2427
|
+
this.parentPath = parentPath;
|
|
2428
|
+
|
|
2408
2429
|
if (!(this instanceof RootNodeGroup)) {
|
|
2409
2430
|
|
|
2410
|
-
let [fragment, shell] = this.
|
|
2431
|
+
let [fragment, shell] = this.populateFromTemplate(template);
|
|
2411
2432
|
|
|
2412
2433
|
if (fragment && template.exprs.length) {
|
|
2413
2434
|
this.updatePaths(fragment, shell.paths);
|
|
2414
2435
|
|
|
2415
2436
|
// Static web components can sometimes have children created via expressions.
|
|
2416
2437
|
// But calling applyExprs() will mess up the shell's path to them.
|
|
2417
|
-
// So we find them first, then call
|
|
2418
|
-
|
|
2438
|
+
// So we find them first, then call instantiateStaticComponents() after their children have been created.
|
|
2439
|
+
this.staticComponents = this.findStaticComponents(fragment, shell);
|
|
2419
2440
|
|
|
2420
2441
|
this.activateEmbeds(fragment, shell);
|
|
2421
2442
|
|
|
2422
2443
|
// Apply exprs
|
|
2423
2444
|
this.applyExprs(template.exprs);
|
|
2424
2445
|
|
|
2425
|
-
this.instantiateStaticComponents(staticComponents);
|
|
2446
|
+
this.instantiateStaticComponents(this.staticComponents);
|
|
2426
2447
|
}
|
|
2427
2448
|
else if (shell)
|
|
2428
2449
|
this.activateEmbeds(fragment, shell);
|
|
@@ -2433,47 +2454,33 @@ class NodeGroup {
|
|
|
2433
2454
|
* Common init shared by RootNodeGroup and NodeGroup constructors.
|
|
2434
2455
|
* But in a separate function because they need to do this at a different step.
|
|
2435
2456
|
* @param template {Template} Create it from the html strings and expressions in this template.
|
|
2436
|
-
* @
|
|
2437
|
-
|
|
2438
|
-
* @param closeKey {?string}
|
|
2439
|
-
* @returns {[DocumentFragment, Shell]} */
|
|
2440
|
-
init(template, parentPath=null, exactKey=null, closeKey=null) {
|
|
2441
|
-
this.exactKey = exactKey || template.getExactKey();
|
|
2442
|
-
this.closeKey = closeKey || template.getCloseKey();
|
|
2443
|
-
|
|
2444
|
-
this.parentPath = parentPath;
|
|
2445
|
-
this.rootNg = parentPath?.parentNg?.rootNg || this;
|
|
2446
|
-
|
|
2457
|
+
* @returns {[DocumentFragment, Shell]} The Shell created from the template,a nd the fragment cloned from the Shell.*/
|
|
2458
|
+
populateFromTemplate(template) {
|
|
2447
2459
|
/*#IFDEV*/assert(this.rootNg);/*#ENDIF*/
|
|
2448
|
-
|
|
2449
|
-
/** @type {Template} */
|
|
2450
2460
|
this.template = template;
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
template.nodeGroup = this;
|
|
2454
|
-
|
|
2455
|
-
// Get a cached version of the parsed and instantiated html, and ExprPaths.
|
|
2461
|
+
this.exactKey = template.getExactKey();
|
|
2462
|
+
this.closeKey = template.getCloseKey();
|
|
2456
2463
|
|
|
2457
2464
|
// If it's just a text node, skip a bunch of unnecessary steps.
|
|
2458
|
-
if (
|
|
2459
|
-
|
|
2460
|
-
let textNode = document.createTextNode(template.html[0]);
|
|
2461
|
-
|
|
2465
|
+
if (template.isText) {
|
|
2466
|
+
let textNode = Globals$1.doc.createTextNode(template.html[0]);
|
|
2462
2467
|
this.startNode = this.endNode = textNode;
|
|
2463
2468
|
return [];
|
|
2464
2469
|
}
|
|
2470
|
+
|
|
2471
|
+
// Get a cached version of the parsed and instantiated html, and ExprPaths:
|
|
2465
2472
|
else {
|
|
2466
2473
|
let shell = Shell.get(template.html);
|
|
2467
2474
|
let fragment = shell.fragment.cloneNode(true);
|
|
2468
2475
|
|
|
2469
|
-
if (fragment
|
|
2476
|
+
if (fragment?.nodeType === 11) { // DocumentFragment
|
|
2470
2477
|
let childNodes = fragment.childNodes;
|
|
2471
2478
|
this.startNode = childNodes[0];
|
|
2472
2479
|
this.endNode = childNodes[childNodes.length - 1];
|
|
2473
2480
|
}
|
|
2474
|
-
else
|
|
2481
|
+
else
|
|
2475
2482
|
this.startNode = this.endNode = fragment;
|
|
2476
|
-
|
|
2483
|
+
|
|
2477
2484
|
return [fragment, shell];
|
|
2478
2485
|
}
|
|
2479
2486
|
}
|
|
@@ -2512,6 +2519,7 @@ class NodeGroup {
|
|
|
2512
2519
|
exprIndex--;
|
|
2513
2520
|
}
|
|
2514
2521
|
|
|
2522
|
+
|
|
2515
2523
|
// TODO: Need to end and restart this block when going from one component to the next?
|
|
2516
2524
|
// Think of having two adjacent components.
|
|
2517
2525
|
// But the dynamicAttribsAdjacet test already passes.
|
|
@@ -2520,11 +2528,11 @@ class NodeGroup {
|
|
|
2520
2528
|
// 1. Instantiate it if it hasn't already been, sending all expr's to its constructor.
|
|
2521
2529
|
// 2. Otherwise send them to its render function.
|
|
2522
2530
|
// Components with no expressions as attributes are instead activated in activateEmbeds().
|
|
2523
|
-
if (path.nodeMarker !== this.rootNg.root && path.isComponent
|
|
2531
|
+
if (path.nodeMarker !== this.rootNg.root && path.isComponent) {
|
|
2524
2532
|
|
|
2525
|
-
if (!nextPath || !nextPath.isComponent
|
|
2533
|
+
if (!nextPath || !nextPath.isComponent || nextPath.nodeMarker !== path.nodeMarker)
|
|
2526
2534
|
lastComponentPathIndex = i;
|
|
2527
|
-
let isFirstComponentPath = !prevPath || !prevPath.isComponent
|
|
2535
|
+
let isFirstComponentPath = !prevPath || !prevPath.isComponent || prevPath.nodeMarker !== path.nodeMarker;
|
|
2528
2536
|
|
|
2529
2537
|
if (isFirstComponentPath) {
|
|
2530
2538
|
|
|
@@ -2534,7 +2542,7 @@ class NodeGroup {
|
|
|
2534
2542
|
componentProps[attrName] = pathExprs[j].length > 1 ? pathExprs[j].join('') : pathExprs[j][0];
|
|
2535
2543
|
}
|
|
2536
2544
|
|
|
2537
|
-
this.
|
|
2545
|
+
this.handleComponent(path.nodeMarker, componentProps, true);
|
|
2538
2546
|
|
|
2539
2547
|
// Set attributes on component.
|
|
2540
2548
|
for (let j=i; j<=lastComponentPathIndex; j++)
|
|
@@ -2553,7 +2561,10 @@ class NodeGroup {
|
|
|
2553
2561
|
// TODO: Only do this if we have ExprPaths within styles?
|
|
2554
2562
|
this.updateStyles();
|
|
2555
2563
|
|
|
2556
|
-
|
|
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.
|
|
2557
2568
|
|
|
2558
2569
|
// Invalidate the nodes cache because we just changed it.
|
|
2559
2570
|
this.nodesCache = null;
|
|
@@ -2569,50 +2580,39 @@ class NodeGroup {
|
|
|
2569
2580
|
}
|
|
2570
2581
|
|
|
2571
2582
|
/**
|
|
2572
|
-
*
|
|
2573
|
-
* @param el {
|
|
2574
|
-
* @param props {Object}
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
// Perhaps if Components were treated as child NodeGroups, which would need to be the child of an ExprPath,
|
|
2579
|
-
// then we could re-use the hash and logic from NodeManager?
|
|
2580
|
-
let newHash = getObjectHash(props);
|
|
2581
|
-
|
|
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) {
|
|
2582
2589
|
let isPreHtmlElement = el.hasAttribute('solarite-placeholder');
|
|
2583
2590
|
let isPreIsElement = el.hasAttribute('_is');
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
// Instantiate a placeholder.
|
|
2591
|
+
let attribs, children;
|
|
2587
2592
|
if (isPreHtmlElement || isPreIsElement)
|
|
2588
|
-
el = this.instantiateComponent(el, isPreHtmlElement, props);
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
// compare the arguments and then decide for itself whether it wants to re-render.
|
|
2593
|
-
else if (el.render) {
|
|
2594
|
-
//let oldHash = Globals.componentArgsHash.get(el);
|
|
2595
|
-
//if (oldHash !== newHash) { // Only if not changed.
|
|
2596
|
-
let args = {};
|
|
2593
|
+
[el, attribs, children] = this.instantiateComponent(el, isPreHtmlElement, props);
|
|
2594
|
+
if (doRender && el.render) {
|
|
2595
|
+
if (!attribs) {
|
|
2596
|
+
attribs = Util.attribsToObject(el);
|
|
2597
2597
|
for (let name in props || {})
|
|
2598
|
-
|
|
2599
|
-
el.
|
|
2600
|
-
|
|
2598
|
+
attribs[Util.dashesToCamel(name)] = props[name];
|
|
2599
|
+
children = el.childNodes;
|
|
2600
|
+
}
|
|
2601
|
+
el.render(attribs, children);
|
|
2601
2602
|
}
|
|
2602
|
-
|
|
2603
|
-
Globals$1.componentArgsHash.set(el, newHash);
|
|
2603
|
+
return el;
|
|
2604
2604
|
}
|
|
2605
|
-
|
|
2605
|
+
|
|
2606
2606
|
/**
|
|
2607
2607
|
* We swap the placeholder element for the real element so we can pass its dynamic attributes
|
|
2608
2608
|
* to its constructor.
|
|
2609
|
+
* This is only called by handleComponent()
|
|
2610
|
+
* This does not call render()
|
|
2609
2611
|
*
|
|
2610
|
-
*
|
|
2611
|
-
*
|
|
2612
|
-
* @param el
|
|
2612
|
+
* @param el {HTMLElement}
|
|
2613
2613
|
* @param isPreHtmlElement {?boolean} True if the element's tag name ends with -solarite-placeholder
|
|
2614
2614
|
* @param props {Object} Attributes with dynamic values.
|
|
2615
|
-
* @return {HTMLElement} */
|
|
2615
|
+
* @return {[HTMLElement, attribs:Object, children:Node[]]}} */
|
|
2616
2616
|
instantiateComponent(el, isPreHtmlElement=undefined, props=undefined) {
|
|
2617
2617
|
if (isPreHtmlElement === undefined)
|
|
2618
2618
|
isPreHtmlElement = !el.hasAttribute('_is');
|
|
@@ -2627,19 +2627,12 @@ class NodeGroup {
|
|
|
2627
2627
|
if (!Constructor)
|
|
2628
2628
|
throw new Error(`The custom tag name ${tagName} is not registered.`)
|
|
2629
2629
|
|
|
2630
|
-
|
|
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');
|
|
2631
2633
|
for (let name in props || {})
|
|
2632
2634
|
attribs[Util.dashesToCamel(name)] = props[name];
|
|
2633
2635
|
|
|
2634
|
-
// Pass other attribs to constructor, since otherwise they're not yet set on the element,
|
|
2635
|
-
// and the constructor would otherwise have no way to see them.
|
|
2636
|
-
if (el.attributes.length) {
|
|
2637
|
-
for (let attrib of el.attributes) {
|
|
2638
|
-
let attribName = Util.dashesToCamel(attrib.name);
|
|
2639
|
-
if (!attribs.hasOwnProperty(attribName) && attribName !== 'solarite-placeholder')
|
|
2640
|
-
attribs[attribName] = attrib.value;
|
|
2641
|
-
}
|
|
2642
|
-
}
|
|
2643
2636
|
|
|
2644
2637
|
// Create the web component.
|
|
2645
2638
|
// Get the children that aren't Solarite's comment placeholders.
|
|
@@ -2670,7 +2663,7 @@ class NodeGroup {
|
|
|
2670
2663
|
if (this.endNode === el)
|
|
2671
2664
|
this.endNode = newEl;
|
|
2672
2665
|
|
|
2673
|
-
|
|
2666
|
+
// This is used only if inheriting from the Solarite class.
|
|
2674
2667
|
// applyComponentExprs() is called because we're rendering.
|
|
2675
2668
|
// So we want to render the sub-component also.
|
|
2676
2669
|
if (newEl.renderFirstTime)
|
|
@@ -2694,7 +2687,7 @@ class NodeGroup {
|
|
|
2694
2687
|
newEl.setAttribute(name, val);
|
|
2695
2688
|
}
|
|
2696
2689
|
|
|
2697
|
-
return newEl;
|
|
2690
|
+
return [newEl, attribs, children];
|
|
2698
2691
|
}
|
|
2699
2692
|
|
|
2700
2693
|
/**
|
|
@@ -2741,18 +2734,21 @@ class NodeGroup {
|
|
|
2741
2734
|
* Requires the nodeCache to be present. */
|
|
2742
2735
|
removeAndSaveOrphans() {
|
|
2743
2736
|
/*#IFDEV*/assert(this.nodesCache);/*#ENDIF*/
|
|
2744
|
-
let fragment =
|
|
2737
|
+
let fragment = Globals$1.doc.createDocumentFragment();
|
|
2745
2738
|
for (let node of this.getNodes())
|
|
2746
2739
|
fragment.append(node);
|
|
2747
2740
|
}
|
|
2748
2741
|
|
|
2749
2742
|
|
|
2750
|
-
|
|
2751
|
-
|
|
2743
|
+
/**
|
|
2744
|
+
* @param fragment {DocumentFragment}
|
|
2745
|
+
* @param paths
|
|
2746
|
+
* @param startingPathDepth {int} */
|
|
2747
|
+
updatePaths(fragment, paths, startingPathDepth) {
|
|
2752
2748
|
let pathLength = paths.length;
|
|
2753
2749
|
this.paths.length = pathLength;
|
|
2754
2750
|
for (let i=0; i<pathLength; i++) {
|
|
2755
|
-
let path = paths[i].clone(fragment,
|
|
2751
|
+
let path = paths[i].clone(fragment, startingPathDepth);
|
|
2756
2752
|
path.parentNg = this;
|
|
2757
2753
|
this.paths[i] = path;
|
|
2758
2754
|
}
|
|
@@ -2779,7 +2775,7 @@ class NodeGroup {
|
|
|
2779
2775
|
`parentNode: ${this.parentNode?.tagName?.toLowerCase()}`,
|
|
2780
2776
|
'nodes:',
|
|
2781
2777
|
...setIndent(this.getNodes().map(item => {
|
|
2782
|
-
if (item
|
|
2778
|
+
if (item?.nodeType) {
|
|
2783
2779
|
|
|
2784
2780
|
let tree = nodeToArrayTree(item, nextNode => {
|
|
2785
2781
|
|
|
@@ -2834,7 +2830,7 @@ class NodeGroup {
|
|
|
2834
2830
|
}
|
|
2835
2831
|
//#ENDIF
|
|
2836
2832
|
|
|
2837
|
-
findStaticComponents(root, shell,
|
|
2833
|
+
findStaticComponents(root, shell, startingPathDepth=0) {
|
|
2838
2834
|
let result = [];
|
|
2839
2835
|
|
|
2840
2836
|
// static components. These are WebComponents that do not have any constructor arguments that are expressions.
|
|
@@ -2842,8 +2838,8 @@ class NodeGroup {
|
|
|
2842
2838
|
// Maybe someday these two paths will be merged?
|
|
2843
2839
|
// Must happen before ids because instantiateComponent will replace the element.
|
|
2844
2840
|
for (let path of shell.staticComponents) {
|
|
2845
|
-
if (
|
|
2846
|
-
path = path.slice(0, -
|
|
2841
|
+
if (startingPathDepth)
|
|
2842
|
+
path = path.slice(0, -startingPathDepth);
|
|
2847
2843
|
let el = resolveNodePath(root, path);
|
|
2848
2844
|
|
|
2849
2845
|
// Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
|
|
@@ -2855,8 +2851,9 @@ class NodeGroup {
|
|
|
2855
2851
|
}
|
|
2856
2852
|
|
|
2857
2853
|
instantiateStaticComponents(staticComponents) {
|
|
2858
|
-
|
|
2859
|
-
|
|
2854
|
+
// TODO: Why do we not call render() on the static component here? The tests pass either way.
|
|
2855
|
+
for (let i in staticComponents)
|
|
2856
|
+
staticComponents[i] = this.handleComponent(staticComponents[i], null, false);
|
|
2860
2857
|
}
|
|
2861
2858
|
|
|
2862
2859
|
/**
|
|
@@ -2877,7 +2874,7 @@ class NodeGroup {
|
|
|
2877
2874
|
let el = resolveNodePath(root, path);
|
|
2878
2875
|
Util.bindId(rootEl, el);
|
|
2879
2876
|
}
|
|
2880
|
-
|
|
2877
|
+
}
|
|
2881
2878
|
|
|
2882
2879
|
// styles
|
|
2883
2880
|
if (options?.styles !== false) {
|
|
@@ -2907,9 +2904,8 @@ class NodeGroup {
|
|
|
2907
2904
|
}
|
|
2908
2905
|
}
|
|
2909
2906
|
}
|
|
2910
|
-
}
|
|
2911
|
-
|
|
2912
|
-
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2913
2909
|
class RootNodeGroup extends NodeGroup {
|
|
2914
2910
|
|
|
2915
2911
|
/**
|
|
@@ -2923,18 +2919,18 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2923
2919
|
exprsToRender = new Map();
|
|
2924
2920
|
|
|
2925
2921
|
/**
|
|
2926
|
-
*
|
|
2927
|
-
* @param
|
|
2928
|
-
* @param
|
|
2929
|
-
* @param options {?object}
|
|
2930
|
-
*/
|
|
2922
|
+
* @param template {Template}
|
|
2923
|
+
* @param el {?HTMLElement} Optional, pre-existing htmlElement tat will be the root.
|
|
2924
|
+
* @param options {?object} */
|
|
2931
2925
|
constructor(template, el, options) {
|
|
2932
2926
|
super(template);
|
|
2933
2927
|
|
|
2934
2928
|
this.options = options;
|
|
2935
2929
|
|
|
2936
|
-
|
|
2937
|
-
|
|
2930
|
+
let [fragment, shell] = this.populateFromTemplate(template);
|
|
2931
|
+
|
|
2932
|
+
let startingPathDepth = 0;
|
|
2933
|
+
|
|
2938
2934
|
|
|
2939
2935
|
if (fragment instanceof Text) {
|
|
2940
2936
|
|
|
@@ -2945,25 +2941,25 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2945
2941
|
el.append(fragment);
|
|
2946
2942
|
this.root = el;
|
|
2947
2943
|
}
|
|
2944
|
+
else
|
|
2945
|
+
throw new Error('Cannot create a standalone text node');
|
|
2948
2946
|
Globals$1.nodeGroups.set(this.root, this);
|
|
2949
2947
|
}
|
|
2948
|
+
|
|
2949
|
+
|
|
2950
2950
|
else {
|
|
2951
2951
|
|
|
2952
2952
|
// If adding NodeGroup to an element.
|
|
2953
|
-
let offset = 0;
|
|
2954
|
-
let root = fragment; // TODO: Rename so it's not confused with this.root.
|
|
2955
2953
|
if (el) {
|
|
2956
|
-
|
|
2954
|
+
this.root = el;
|
|
2957
2955
|
|
|
2958
2956
|
// Save slot children
|
|
2959
2957
|
let slotChildren;
|
|
2960
2958
|
if (el.childNodes.length) {
|
|
2961
|
-
slotChildren =
|
|
2959
|
+
slotChildren = Globals$1.doc.createDocumentFragment();
|
|
2962
2960
|
slotChildren.append(...el.childNodes);
|
|
2963
2961
|
}
|
|
2964
2962
|
|
|
2965
|
-
this.root = el;
|
|
2966
|
-
|
|
2967
2963
|
// If el should replace the root node of the fragment.
|
|
2968
2964
|
if (isReplaceEl(fragment, el)) {
|
|
2969
2965
|
el.append(...fragment.children[0].childNodes);
|
|
@@ -2974,8 +2970,10 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2974
2970
|
el.setAttribute(attrib.name, attrib.value);
|
|
2975
2971
|
|
|
2976
2972
|
// Go one level deeper into all of shell's paths.
|
|
2977
|
-
|
|
2978
|
-
}
|
|
2973
|
+
startingPathDepth = 1;
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
else {
|
|
2979
2977
|
let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
|
|
2980
2978
|
if (!isEmpty)
|
|
2981
2979
|
el.append(...fragment.childNodes);
|
|
@@ -3003,35 +3001,35 @@ class RootNodeGroup extends NodeGroup {
|
|
|
3003
3001
|
el.append(slotChildren);
|
|
3004
3002
|
}
|
|
3005
3003
|
|
|
3006
|
-
root = el;
|
|
3007
|
-
|
|
3008
3004
|
this.startNode = el;
|
|
3009
3005
|
this.endNode = el;
|
|
3010
|
-
}
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
// Instantiate as a standalone element.
|
|
3009
|
+
else {
|
|
3011
3010
|
let singleEl = getSingleEl(fragment);
|
|
3012
|
-
this.root = singleEl || fragment; // We return the whole fragment when calling
|
|
3011
|
+
this.root = singleEl || fragment; // We return the whole fragment when calling h() with a collection of nodes.
|
|
3013
3012
|
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
root = singleEl;
|
|
3017
|
-
offset = 1;
|
|
3018
|
-
}
|
|
3013
|
+
if (singleEl)
|
|
3014
|
+
startingPathDepth = 1;
|
|
3019
3015
|
}
|
|
3020
|
-
|
|
3021
|
-
this.updatePaths(root, shell.paths,
|
|
3016
|
+
Globals$1.nodeGroups.set(this.root, this);
|
|
3017
|
+
this.updatePaths(this.root, shell.paths, startingPathDepth);
|
|
3022
3018
|
|
|
3023
3019
|
// Static web components can sometimes have children created via expressions.
|
|
3024
3020
|
// But calling applyExprs() will mess up the shell's path to them.
|
|
3025
3021
|
// So we find them first, then call activateStaticComponents() after their children have been created.
|
|
3026
|
-
|
|
3022
|
+
this.staticComponents = this.findStaticComponents(this.root, shell, startingPathDepth);
|
|
3027
3023
|
|
|
3028
|
-
this.activateEmbeds(root, shell,
|
|
3024
|
+
this.activateEmbeds(this.root, shell, startingPathDepth);
|
|
3029
3025
|
|
|
3030
3026
|
// Apply exprs
|
|
3031
3027
|
this.applyExprs(template.exprs);
|
|
3032
3028
|
|
|
3033
|
-
this.instantiateStaticComponents(staticComponents);
|
|
3029
|
+
this.instantiateStaticComponents(this.staticComponents);
|
|
3034
3030
|
}
|
|
3031
|
+
|
|
3032
|
+
|
|
3035
3033
|
}
|
|
3036
3034
|
}
|
|
3037
3035
|
|
|
@@ -3073,8 +3071,7 @@ class Template {
|
|
|
3073
3071
|
/** @type {Array} Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
|
|
3074
3072
|
hashedFields;
|
|
3075
3073
|
|
|
3076
|
-
|
|
3077
|
-
nodeGroup;
|
|
3074
|
+
isText;
|
|
3078
3075
|
|
|
3079
3076
|
/**
|
|
3080
3077
|
*
|
|
@@ -3179,169 +3176,203 @@ class Template {
|
|
|
3179
3176
|
|
|
3180
3177
|
return this.closeKey;
|
|
3181
3178
|
}
|
|
3182
|
-
}
|
|
3183
3179
|
|
|
3180
|
+
/**
|
|
3181
|
+
* @param tag {string}
|
|
3182
|
+
* @param props {?Record<string, any>}
|
|
3183
|
+
* @param children
|
|
3184
|
+
* @returns {Template} */
|
|
3185
|
+
static fromJsx(tag, props, children) {
|
|
3184
3186
|
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
* @property {boolean=} styles - Replace :host in style tags to scope them locally.
|
|
3188
|
-
* @property {boolean=} scripts - Execute script tags.
|
|
3189
|
-
* @property {boolean=} ids - Create references to elements with id or data-id attributes.
|
|
3190
|
-
* @property {?boolean} render - Deprecated.
|
|
3191
|
-
* Used only when options are given to a class super constructor inheriting from Solarite.
|
|
3192
|
-
* True to call render() immediately in super constructor.
|
|
3193
|
-
* False to automatically call render() at all.
|
|
3194
|
-
* Undefined (default) to call render() when added to the DOM, unless already rendered.
|
|
3195
|
-
*/
|
|
3196
|
-
|
|
3197
|
-
/**
|
|
3198
|
-
* Convert strings to HTMLNodes.
|
|
3199
|
-
* Using h`...` as a tag will always create a Template.
|
|
3200
|
-
* Using h() as a function() will always create a DOM element.
|
|
3201
|
-
*
|
|
3202
|
-
* Features beyond what standard js tagged template strings do:
|
|
3203
|
-
* 1. r`` sub-expressions
|
|
3204
|
-
* 2. functions, nodes, and arrays of nodes as sub-expressions.
|
|
3205
|
-
* 3. html-escape all expressions by default, unless wrapped in r()
|
|
3206
|
-
* 4. event binding
|
|
3207
|
-
* 5. TODO: list more
|
|
3208
|
-
*
|
|
3209
|
-
* Currently supported:
|
|
3210
|
-
* 1. h(el, options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
|
|
3211
|
-
* 2. h(el, template, ?options) // Render the Template created by #1 to element.
|
|
3212
|
-
*
|
|
3213
|
-
* 3. h`<b>Hello</b> ${'World'}!` // Create Template that can later be used to create nodes.
|
|
3214
|
-
*
|
|
3215
|
-
* 4. h('Hello'); // Create single text node.
|
|
3216
|
-
* 5. h('<b>Hello</b>'); // Create single HTMLElement
|
|
3217
|
-
* 6. h('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
|
|
3218
|
-
* 7. h()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which
|
|
3219
|
-
* // includes properly handling nested components and r`` sub-expressions.
|
|
3220
|
-
* 8. h(template) // Render Template created by #1.
|
|
3221
|
-
*
|
|
3222
|
-
* 9. h({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
|
|
3223
|
-
* 10. h(string, object, ...) // JSX TODO
|
|
3224
|
-
* @param htmlStrings {?HTMLElement|string|string[]|function():Template|{render:function()}}
|
|
3225
|
-
* @param exprs {*[]|string|Template|Object}
|
|
3226
|
-
* @return {Node|HTMLElement|Template} */
|
|
3227
|
-
function h(htmlStrings=undefined, ...exprs) {
|
|
3187
|
+
// HTML void elements that must not have closing tags
|
|
3188
|
+
const isVoid = selfClosingTags.has(tag.toLowerCase());
|
|
3228
3189
|
|
|
3229
|
-
|
|
3230
|
-
|
|
3190
|
+
// Build htmlStrings/exprs so Shell can place placeholders in attribute values and child content.
|
|
3191
|
+
let htmlStrings = [];
|
|
3192
|
+
let templateExprs = [];
|
|
3231
3193
|
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
let parent = htmlStrings, template = exprs[0];
|
|
3194
|
+
// Opening tag
|
|
3195
|
+
let open = `<${tag}`;
|
|
3235
3196
|
|
|
3236
|
-
//
|
|
3237
|
-
if (
|
|
3238
|
-
|
|
3239
|
-
|
|
3197
|
+
// Attributes
|
|
3198
|
+
if (props && typeof props === 'object') {
|
|
3199
|
+
for (let name in props) {
|
|
3200
|
+
let value = props[name];
|
|
3240
3201
|
|
|
3241
|
-
|
|
3202
|
+
// id and data-id are static in templates — never expressions
|
|
3203
|
+
if (name === 'id' || name === 'data-id') {
|
|
3204
|
+
// Write directly into the opening string with quotes
|
|
3205
|
+
open += ` ${name}="${value}"`;
|
|
3206
|
+
continue;
|
|
3207
|
+
}
|
|
3242
3208
|
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3209
|
+
// Dynamic attribute value: functions are unquoted (e.g., onclick=${fn}), others quoted
|
|
3210
|
+
if (typeof value === 'function') {
|
|
3211
|
+
open += ` ${name}=`;
|
|
3212
|
+
htmlStrings.push(open);
|
|
3213
|
+
templateExprs.push(value);
|
|
3214
|
+
// reset so subsequent attributes start fresh (e.g., ' title=')
|
|
3215
|
+
open = ``;
|
|
3216
|
+
}
|
|
3217
|
+
else {
|
|
3218
|
+
open += ` ${name}=`;
|
|
3219
|
+
htmlStrings.push(open);
|
|
3220
|
+
templateExprs.push(value);
|
|
3221
|
+
// reset so subsequent attributes start fresh (e.g., ' title=')
|
|
3222
|
+
open = ``;
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3250
3225
|
}
|
|
3251
3226
|
|
|
3252
|
-
//
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
//
|
|
3260
|
-
|
|
3261
|
-
|
|
3227
|
+
// Finalize opening tag precisely to match tagged template splitting
|
|
3228
|
+
if (!isVoid) {
|
|
3229
|
+
const pushedAny = htmlStrings.length > 0;
|
|
3230
|
+
// If nothing pushed yet (no dynamic attrs), push the entire open + '>'
|
|
3231
|
+
if (!pushedAny)
|
|
3232
|
+
htmlStrings.push(open + '>');
|
|
3233
|
+
else {
|
|
3234
|
+
// If we were in a quoted attr (open === '"'), then the string after expr is '">' ;
|
|
3235
|
+
// Otherwise (function-valued attr), the string after expr is just '>'
|
|
3236
|
+
htmlStrings.push(open === '"' ? '">' : '>');
|
|
3262
3237
|
}
|
|
3238
|
+
|
|
3239
|
+
for (let child of children)
|
|
3240
|
+
addChild(child, htmlStrings, templateExprs);
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
// Closing tag (not for void tags)
|
|
3244
|
+
if (!isVoid) {
|
|
3245
|
+
// If we never emitted the '>' for the open tag (no children were added),
|
|
3246
|
+
// then it was appended above before children. Now just add the closing tag to the last html segment.
|
|
3247
|
+
let lastIdx = htmlStrings.length - 1;
|
|
3248
|
+
htmlStrings[lastIdx] += `</${tag}>`;
|
|
3249
|
+
}
|
|
3250
|
+
else {
|
|
3251
|
+
// Void element: ensure we emitted a trailing '>' segment
|
|
3252
|
+
const pushedAny = htmlStrings.length > 0;
|
|
3253
|
+
if (!pushedAny)
|
|
3254
|
+
htmlStrings.push(open + '>');
|
|
3255
|
+
else
|
|
3256
|
+
htmlStrings.push('>');
|
|
3263
3257
|
}
|
|
3264
|
-
}
|
|
3265
3258
|
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3259
|
+
// Ensure invariant
|
|
3260
|
+
//assert(htmlStrings.length === templateExprs.length + 1);
|
|
3261
|
+
//console.log([htmlStrings, templateExprs])
|
|
3262
|
+
return new Template(htmlStrings, templateExprs);
|
|
3269
3263
|
}
|
|
3264
|
+
}
|
|
3270
3265
|
|
|
3271
|
-
else if (typeof htmlStrings === 'string' || htmlStrings instanceof String) {
|
|
3272
|
-
// 10. JSX
|
|
3273
|
-
if (typeof exprs[0] === 'object') {
|
|
3274
|
-
exprs[0] || {};
|
|
3275
|
-
exprs.slice(1);
|
|
3276
3266
|
|
|
3277
|
-
|
|
3278
|
-
let templateExprs = [];
|
|
3267
|
+
const selfClosingTags = new Set(['area','base','br','col','embed','hr','img','input','link','meta','param','source','track','wbr']);
|
|
3279
3268
|
|
|
3280
|
-
// TODO How to know which children are static html and which are expression placeholders?
|
|
3281
|
-
// Perhaps we have to treat every text child as a string?
|
|
3282
3269
|
|
|
3283
|
-
|
|
3284
|
-
|
|
3270
|
+
/**
|
|
3271
|
+
* Add child Templates that were already created via h() and Template.fromJsx()
|
|
3272
|
+
* @param template {Template}
|
|
3273
|
+
* @param html {string[]}
|
|
3274
|
+
* @param exprs {any[]} */
|
|
3275
|
+
const addChild = (template, html, exprs) => {
|
|
3276
|
+
|
|
3277
|
+
if (Array.isArray(template)) {
|
|
3278
|
+
for (let c of template)
|
|
3279
|
+
addChild(c, html, exprs);
|
|
3280
|
+
}
|
|
3281
|
+
else {
|
|
3282
|
+
let flatten = false;
|
|
3283
|
+
if (template instanceof Template) {
|
|
3284
|
+
// Heuristic to match tagged-template splitting:
|
|
3285
|
+
// - Flatten if the child has expressions (so JSX can inline attribute/value placeholders like tagged literals would).
|
|
3286
|
+
// - Also flatten void elements (e.g., <img>) so they inline like literals.
|
|
3287
|
+
// - Otherwise, keep as a dynamic child placeholder to match cases where the tagged template used an expression child.
|
|
3288
|
+
const childHasExprs = template.exprs.length > 0;
|
|
3289
|
+
if (childHasExprs)
|
|
3290
|
+
flatten = true;
|
|
3291
|
+
else {
|
|
3292
|
+
const m = (template.html[0] || '').match(/^<([a-zA-Z][\w:-]*)/);
|
|
3293
|
+
const childTag = m ? m[1].toLowerCase() : '';
|
|
3294
|
+
flatten = selfClosingTags.has(childTag);
|
|
3295
|
+
}
|
|
3285
3296
|
}
|
|
3286
3297
|
|
|
3298
|
+
if (flatten) {
|
|
3299
|
+
// Flatten/interleave into current segment to match tagged template splitting
|
|
3300
|
+
html[html.length - 1] += template.html[0];
|
|
3301
|
+
for (let i = 0; i < template.exprs.length; i++) {
|
|
3302
|
+
exprs.push(template.exprs[i]);
|
|
3303
|
+
html.push(template.html[i + 1] ?? '');
|
|
3304
|
+
}
|
|
3305
|
+
} else {
|
|
3306
|
+
// Keep as dynamic child
|
|
3307
|
+
exprs.push(template);
|
|
3308
|
+
html.push('');
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
};
|
|
3287
3312
|
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3313
|
+
|
|
3314
|
+
/**
|
|
3315
|
+
* @typedef {Object} RenderOptions
|
|
3316
|
+
* @property {boolean=} styles - Replace :host in style tags to scope them locally.
|
|
3317
|
+
* @property {boolean=} scripts - Execute script tags.
|
|
3318
|
+
* @property {boolean=} ids - Create references to elements with id or data-id attributes.
|
|
3319
|
+
* @property {?boolean} render - Deprecated.
|
|
3320
|
+
* Used only when options are given to a class super constructor inheriting from Solarite.
|
|
3321
|
+
* True to call render() immediately in super constructor.
|
|
3322
|
+
* False to automatically call render() at all.
|
|
3323
|
+
* Undefined (default) to call render() when added to the DOM, unless already rendered.
|
|
3324
|
+
*/
|
|
3325
|
+
|
|
3326
|
+
/**
|
|
3327
|
+
* Convert a template, string, or object into a DOM Node or Element
|
|
3328
|
+
*
|
|
3329
|
+
* 1. h('Hello'); // Create single text node.
|
|
3330
|
+
* 2. h('<b>Hello</b>'); // Create single HTMLElement
|
|
3331
|
+
* 3. h('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
|
|
3332
|
+
* 4. h(template) // Render Template created by h`<html>` or h();
|
|
3333
|
+
* 5. h({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
|
|
3334
|
+
* @param arg {string|Template|{render:()=>void}}
|
|
3335
|
+
* @returns {Node|DocumentFragment|HTMLElement} */
|
|
3336
|
+
function toEl(arg) {
|
|
3337
|
+
|
|
3338
|
+
if (typeof arg === 'string') {
|
|
3339
|
+
let html = arg;
|
|
3340
|
+
|
|
3341
|
+
// If it's an element with whitespace before or after it, trim both ends.
|
|
3342
|
+
if (html.match(/^\s^</) || html.match(/>\s+$/))
|
|
3343
|
+
html = html.trim();
|
|
3292
3344
|
|
|
3293
3345
|
// We create a new one each time because otherwise
|
|
3294
3346
|
// the returned fragment will have its content replaced by a subsequent call.
|
|
3295
|
-
let templateEl =
|
|
3296
|
-
templateEl.innerHTML =
|
|
3347
|
+
let templateEl = Globals$1.doc.createElement('template');
|
|
3348
|
+
templateEl.innerHTML = html;
|
|
3297
3349
|
|
|
3298
|
-
//
|
|
3350
|
+
// 1+2. Return Node if there's one child.
|
|
3299
3351
|
let relevantNodes = Util.trimEmptyNodes(templateEl.content.childNodes);
|
|
3300
3352
|
if (relevantNodes.length === 1)
|
|
3301
3353
|
return relevantNodes[0];
|
|
3302
3354
|
|
|
3303
|
-
//
|
|
3355
|
+
// 3. Otherwise return DocumentFragment.
|
|
3304
3356
|
return templateEl.content;
|
|
3305
3357
|
}
|
|
3306
3358
|
|
|
3307
|
-
//
|
|
3308
|
-
|
|
3309
|
-
return (
|
|
3310
|
-
//Globals.rendered.add(parent)
|
|
3311
|
-
let template = h(htmlStrings, ...exprs);
|
|
3312
|
-
return template.render();
|
|
3313
|
-
}
|
|
3314
|
-
}
|
|
3315
|
-
|
|
3316
|
-
// 8.
|
|
3317
|
-
else if (htmlStrings instanceof Template) {
|
|
3318
|
-
return htmlStrings.render();
|
|
3359
|
+
// 4.
|
|
3360
|
+
if (arg instanceof Template) {
|
|
3361
|
+
return arg.render();
|
|
3319
3362
|
}
|
|
3320
3363
|
|
|
3321
|
-
|
|
3322
|
-
// 9. Create dynamic element with render() function.
|
|
3364
|
+
// 5. Create dynamic element from an object with a render() function.
|
|
3323
3365
|
// TODO: This path doesn't handle embeds like data-id="..."
|
|
3324
|
-
else if (typeof
|
|
3325
|
-
let obj =
|
|
3366
|
+
else if (arg && typeof arg === 'object') {
|
|
3367
|
+
let obj = arg;
|
|
3326
3368
|
|
|
3327
|
-
if (obj.constructor.name !== 'Object')
|
|
3369
|
+
if (obj.constructor.name !== 'Object')
|
|
3328
3370
|
throw new Error(`Solarate Web Component class ${obj.constructor?.name} must extend HTMLElement.`);
|
|
3329
3371
|
|
|
3330
|
-
|
|
3331
|
-
// Special rebound render path, called by normal path.
|
|
3332
|
-
// Intercepts the main r`...` function call inside render().
|
|
3333
|
-
if (Globals$1.objToEl.has(obj)) {
|
|
3334
|
-
return function(...args) {
|
|
3335
|
-
let template = h(...args);
|
|
3336
|
-
let el = template.render();
|
|
3337
|
-
Globals$1.objToEl.set(obj, el);
|
|
3338
|
-
}.bind(obj);
|
|
3339
|
-
}
|
|
3340
|
-
|
|
3341
3372
|
// Normal path
|
|
3342
|
-
|
|
3373
|
+
if (!Globals$1.objToEl.has(obj)) {
|
|
3343
3374
|
Globals$1.objToEl.set(obj, null);
|
|
3344
|
-
obj[renderF](); // Calls the Special rebound render path above, when the render function calls
|
|
3375
|
+
obj[renderF](); // Calls the Special rebound render path above, when the render function calls h(this)
|
|
3345
3376
|
let el = Globals$1.objToEl.get(obj);
|
|
3346
3377
|
Globals$1.objToEl.delete(obj);
|
|
3347
3378
|
|
|
@@ -3349,7 +3380,7 @@ function h(htmlStrings=undefined, ...exprs) {
|
|
|
3349
3380
|
if (typeof obj[name] === 'function')
|
|
3350
3381
|
el[name] = obj[name].bind(el); // Make the "this" of functions be el.
|
|
3351
3382
|
// TODO: But this doesn't work for passing an object with functions as a constructor arg via an attribute:
|
|
3352
|
-
|
|
3383
|
+
// <my-element arg=${{myFunc() { return this }}}
|
|
3353
3384
|
else
|
|
3354
3385
|
el[name] = obj[name];
|
|
3355
3386
|
|
|
@@ -3365,13 +3396,145 @@ function h(htmlStrings=undefined, ...exprs) {
|
|
|
3365
3396
|
}
|
|
3366
3397
|
}
|
|
3367
3398
|
|
|
3368
|
-
|
|
3369
|
-
|
|
3399
|
+
throw new Error('toEl() does not support argument of type: ' + (arg ? typeof arg : arg));
|
|
3400
|
+
|
|
3370
3401
|
}
|
|
3371
3402
|
|
|
3403
|
+
|
|
3372
3404
|
// Trick to prevent minifier from renaming this function.
|
|
3373
3405
|
let renderF = 'render';
|
|
3374
3406
|
|
|
3407
|
+
/**
|
|
3408
|
+
* Convert strings to HTMLNodes.
|
|
3409
|
+
* Using h`...` as a tag will always create a Template.
|
|
3410
|
+
* Using h() as a function() will always create a DOM element.
|
|
3411
|
+
*
|
|
3412
|
+
* Features beyond what standard js tagged template strings do:
|
|
3413
|
+
* 1. r`` sub-expressions
|
|
3414
|
+
* 2. functions, nodes, and arrays of nodes as sub-expressions.
|
|
3415
|
+
* 3. html-escape all expressions by default, unless wrapped in h()
|
|
3416
|
+
* 4. event binding
|
|
3417
|
+
* 5. TODO: list more
|
|
3418
|
+
*
|
|
3419
|
+
* General rule:
|
|
3420
|
+
* If h() is a function with null or an HTMLElement as its first argument create a Node.
|
|
3421
|
+
* Otherwise create a template
|
|
3422
|
+
*
|
|
3423
|
+
* Currently supported:
|
|
3424
|
+
*
|
|
3425
|
+
* Create Tempataes
|
|
3426
|
+
* 1. h`<b>Hello</b> ${'World'}!` // Create Template that can later be used to create nodes.
|
|
3427
|
+
* 2. h('<b>Hello</b><u>Goodbye</u>'); // Create Template from string, that can later be used to create nodes.
|
|
3428
|
+
*
|
|
3429
|
+
* Add children to an element.
|
|
3430
|
+
* 3. h(el, h`<b>${'Hi'}</b>`, ?options)
|
|
3431
|
+
* 4. h(el, ?options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
|
|
3432
|
+
*
|
|
3433
|
+
* Create top-level element
|
|
3434
|
+
* 5. h()`Hello<b>${'World'}!</b>`
|
|
3435
|
+
*
|
|
3436
|
+
* 6. h(string, object, ...) // Used for JSX
|
|
3437
|
+
* @param htmlStrings {?HTMLElement|string|string[]|function():Template|{render:function()}}
|
|
3438
|
+
* @param exprs {*[]|string|Template|Object}
|
|
3439
|
+
* @return {Node|HTMLElement|Template} */
|
|
3440
|
+
function h(htmlStrings=undefined, ...exprs) {
|
|
3441
|
+
|
|
3442
|
+
// 1. Tagged template
|
|
3443
|
+
if (Array.isArray(arguments[0])) {
|
|
3444
|
+
return new Template(arguments[0], exprs);
|
|
3445
|
+
}
|
|
3446
|
+
|
|
3447
|
+
// 2. String to template, or JSX factory form h(tag, props, ...children)
|
|
3448
|
+
else if (typeof arguments[0] === 'string' || arguments[0] instanceof String) {
|
|
3449
|
+
let tagOrHtml = arguments[0];
|
|
3450
|
+
|
|
3451
|
+
// 2a. JSX: h("tag", {props}, ...children)
|
|
3452
|
+
if (exprs.length && (typeof exprs[0] === 'object' || exprs[0] === null)) {
|
|
3453
|
+
let tag = tagOrHtml + '';
|
|
3454
|
+
let props = exprs[0] || {};
|
|
3455
|
+
let children = exprs.slice(1);
|
|
3456
|
+
|
|
3457
|
+
return Template.fromJsx(tag, props, children);
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
// 2b. Plain html string => template
|
|
3461
|
+
else {
|
|
3462
|
+
let html = tagOrHtml;
|
|
3463
|
+
// If it starts with whitespace, trim both ends.
|
|
3464
|
+
// TODO: Also trim if it ends with whitespace?
|
|
3465
|
+
if (html.match(/^\s^</))
|
|
3466
|
+
html = html.trim();
|
|
3467
|
+
return new Template([html], []);
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
else if (arguments[0] instanceof HTMLElement || arguments[0] instanceof DocumentFragment) {
|
|
3472
|
+
|
|
3473
|
+
// 3. Render template to element.
|
|
3474
|
+
if (arguments[1] instanceof Template) {
|
|
3475
|
+
|
|
3476
|
+
/** @type Template */
|
|
3477
|
+
let template = arguments[1];
|
|
3478
|
+
let parent = arguments[0];
|
|
3479
|
+
let options = arguments[2]; // deprecated?
|
|
3480
|
+
template.render(parent, options);
|
|
3481
|
+
}
|
|
3482
|
+
|
|
3483
|
+
// 4. Render tagged template to element
|
|
3484
|
+
else {
|
|
3485
|
+
let parent = arguments[0], options = arguments[1];
|
|
3486
|
+
|
|
3487
|
+
// Remove shadowroot. TODO: This could mess up paths?
|
|
3488
|
+
if (parent.shadowRoot)
|
|
3489
|
+
parent.innerHTML = '';
|
|
3490
|
+
|
|
3491
|
+
// Return a tagged template function that applies the tagged template to parent.
|
|
3492
|
+
let taggedTemplate = (htmlStrings, ...exprs) => {
|
|
3493
|
+
Globals$1.rendered.add(parent);
|
|
3494
|
+
let template = new Template(htmlStrings, exprs);
|
|
3495
|
+
return template.render(parent, options);
|
|
3496
|
+
};
|
|
3497
|
+
return taggedTemplate;
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
// 5. Create a static element h()'<div></div>' (Deprecated?)
|
|
3502
|
+
else if (!arguments.length) {
|
|
3503
|
+
return (htmlStrings, ...exprs) => {
|
|
3504
|
+
let template = h(htmlStrings, ...exprs);
|
|
3505
|
+
return toEl(template); // Go to path 6.
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
// 6. Help toEl() with objects.
|
|
3510
|
+
// Special rebound render path, called by normal path.
|
|
3511
|
+
// Intercepts the main h(this)`...` function call inside render().
|
|
3512
|
+
// TODO: This path doesn't handle embeds like data-id="..."
|
|
3513
|
+
else if (typeof arguments[0] === 'object' && Globals$1.objToEl.has(arguments[0])) {
|
|
3514
|
+
let obj = arguments[0];
|
|
3515
|
+
|
|
3516
|
+
if (obj.constructor.name !== 'Object')
|
|
3517
|
+
throw new Error(`Solarate Web Component class ${obj.constructor?.name} must extend HTMLElement.`);
|
|
3518
|
+
|
|
3519
|
+
// Jsx with h(this, <jsx>)
|
|
3520
|
+
if (arguments[1] instanceof Template) {
|
|
3521
|
+
let template = arguments[1];
|
|
3522
|
+
let el = template.render();
|
|
3523
|
+
Globals$1.objToEl.set(obj, el);
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
// h(this)`<div>...</div>
|
|
3527
|
+
else
|
|
3528
|
+
return function(...args) {
|
|
3529
|
+
let template = h(...args);
|
|
3530
|
+
let el = template.render();
|
|
3531
|
+
Globals$1.objToEl.set(obj, el);
|
|
3532
|
+
}.bind(obj);
|
|
3533
|
+
}
|
|
3534
|
+
else
|
|
3535
|
+
throw new Error('h() does not support argument of type: ' + (arguments[0] ? typeof arguments[0] : arguments[0]))
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3375
3538
|
/**
|
|
3376
3539
|
* There are three ways to create an instance of a Solarite Component:
|
|
3377
3540
|
* 1. new ComponentName(); // direct class instantiation
|
|
@@ -3545,7 +3708,7 @@ function createSolarite(extendsTag=null) {
|
|
|
3545
3708
|
|
|
3546
3709
|
BaseClass = Globals$1.elementClasses[extendsTag];
|
|
3547
3710
|
if (!BaseClass) { // TODO: Use Cache
|
|
3548
|
-
BaseClass =
|
|
3711
|
+
BaseClass = Globals$1.doc.createElement(extendsTag).constructor;
|
|
3549
3712
|
Globals$1.elementClasses[extendsTag] = BaseClass;
|
|
3550
3713
|
}
|
|
3551
3714
|
}
|
|
@@ -3606,7 +3769,7 @@ function createSolarite(extendsTag=null) {
|
|
|
3606
3769
|
this.innerHTML = html;
|
|
3607
3770
|
}
|
|
3608
3771
|
else
|
|
3609
|
-
this.modifications =
|
|
3772
|
+
this.modifications = h(this, html, options);
|
|
3610
3773
|
}
|
|
3611
3774
|
})*/
|
|
3612
3775
|
|
|
@@ -3656,11 +3819,14 @@ function createSolarite(extendsTag=null) {
|
|
|
3656
3819
|
let define = 'define';
|
|
3657
3820
|
let getName = 'getName';
|
|
3658
3821
|
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3822
|
+
/*
|
|
3823
|
+
┏┓ ┓ •
|
|
3824
|
+
┗┓┏┓┃┏┓┏┓┓╋▗▖
|
|
3825
|
+
┗┛┗┛┗┗┻╹ ╹╹┗
|
|
3826
|
+
JavasCript UI library
|
|
3827
|
+
@license MIT
|
|
3828
|
+
@copyright Vorticode LLC
|
|
3829
|
+
https://vorticode.github.io/solarite/ */
|
|
3664
3830
|
|
|
3665
3831
|
/**
|
|
3666
3832
|
* TODO: The Proxy and the multiple base classes mess up 'instanceof Solarite'
|
|
@@ -3674,4 +3840,4 @@ const Solarite = new Proxy(createSolarite(), {
|
|
|
3674
3840
|
//export {default as watch, renderWatched} from './watch.js'; // unfinished
|
|
3675
3841
|
|
|
3676
3842
|
export default h;
|
|
3677
|
-
export { ArgType, Globals$1 as Globals, Solarite, Util as SolariteUtil, Template,
|
|
3843
|
+
export { ArgType, Globals$1 as Globals, Solarite, Util as SolariteUtil, Template, getArg, h, h as r, setArgs, toEl };
|