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.js
CHANGED
|
@@ -96,8 +96,9 @@ function reset() {
|
|
|
96
96
|
Globals = {
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
|
-
*
|
|
100
|
-
|
|
99
|
+
* Dynamic values that should be passed to a Component's constructor and render() function.
|
|
100
|
+
* @type {Map<HTMLElement, any[]>} */
|
|
101
|
+
componentArgs: new Map(),
|
|
101
102
|
|
|
102
103
|
/**
|
|
103
104
|
* Store which instances of Solarite have already been added to the DOM.
|
|
@@ -112,6 +113,9 @@ function reset() {
|
|
|
112
113
|
|
|
113
114
|
div: document.createElement("div"),
|
|
114
115
|
|
|
116
|
+
/** @type {HTMLDocument} */
|
|
117
|
+
doc: document,
|
|
118
|
+
|
|
115
119
|
/**
|
|
116
120
|
* @type {Record<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
|
|
117
121
|
elementClasses: {},
|
|
@@ -130,20 +134,20 @@ function reset() {
|
|
|
130
134
|
nodeGroups: new WeakMap(),
|
|
131
135
|
|
|
132
136
|
/**
|
|
133
|
-
* Used by
|
|
137
|
+
* Used by h() path 9. */
|
|
134
138
|
objToEl: new WeakMap(),
|
|
135
139
|
|
|
136
140
|
//pendingChildren: [],
|
|
137
141
|
|
|
138
142
|
|
|
139
143
|
/**
|
|
140
|
-
* Elements that have been rendered to by
|
|
144
|
+
* Elements that have been rendered to by h() at least once.
|
|
141
145
|
* This is used by the Solarite class to know when to call onFirstConnect()
|
|
142
146
|
* @type {WeakSet<HTMLElement>} */
|
|
143
147
|
rendered: new WeakSet(),
|
|
144
148
|
|
|
145
149
|
/**
|
|
146
|
-
* Elements that are currently rendering via the
|
|
150
|
+
* Elements that are currently rendering via the h() function.
|
|
147
151
|
* @type {WeakSet<HTMLElement>} */
|
|
148
152
|
rendering: new WeakSet(),
|
|
149
153
|
|
|
@@ -230,12 +234,25 @@ let Util = {
|
|
|
230
234
|
return true; // the same.
|
|
231
235
|
},
|
|
232
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Convert HTMLElement attributes to an object.
|
|
239
|
+
* @param el {HTMLElement}
|
|
240
|
+
* @param ignore {?string} Optionally ignore this attribute.
|
|
241
|
+
* @return {Object} */
|
|
242
|
+
attribsToObject(el, ignore=null) {
|
|
243
|
+
let result = {};
|
|
244
|
+
for (let attrib of el.attributes)
|
|
245
|
+
if (attrib.name !== ignore)
|
|
246
|
+
result[Util.dashesToCamel(attrib.name)] = attrib.value;
|
|
247
|
+
return result;
|
|
248
|
+
},
|
|
249
|
+
|
|
233
250
|
bindId(root, el) {
|
|
234
251
|
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
235
252
|
if (id) { // If something hasn't removed the id.
|
|
236
253
|
|
|
237
254
|
// Don't allow overwriting existing class properties if they already have a non-Node value.
|
|
238
|
-
if (root[id] && !(root[id]
|
|
255
|
+
if (root[id] && !(root[id]?.nodeType))
|
|
239
256
|
throw new Error(`${root.constructor.name}.${id} already has a value. ` +
|
|
240
257
|
`Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
|
|
241
258
|
|
|
@@ -244,26 +261,50 @@ let Util = {
|
|
|
244
261
|
},
|
|
245
262
|
|
|
246
263
|
/**
|
|
264
|
+
* If the style tab has a global attribute:
|
|
265
|
+
* 1. Put it in the document head as <style data-style="tag-name">...</style>
|
|
266
|
+
* 2. Replace the :host {...} CSS selector as tag-name {...}.
|
|
267
|
+
* Otherwise keep it where it is and:
|
|
268
|
+
* 1. Add data-style="1" attribute to the root element.
|
|
269
|
+
* 2. Replace the :host {...} selector in the style as tag-name[data-style='1'] {...}
|
|
247
270
|
* @param style {HTMLStyleElement}
|
|
248
271
|
* @param root {HTMLElement} */
|
|
249
272
|
bindStyles(style, root) {
|
|
250
|
-
let styleId = root.getAttribute('data-style');
|
|
251
|
-
if (!styleId) {
|
|
252
|
-
// Keep track of one style id for each class.
|
|
253
|
-
// TODO: Put this outside the class in a map, so it doesn't conflict with static properties.
|
|
254
|
-
if (!root.constructor.styleId)
|
|
255
|
-
root.constructor.styleId = 1;
|
|
256
|
-
styleId = root.constructor.styleId++;
|
|
257
273
|
|
|
258
|
-
|
|
274
|
+
let tagName = root.tagName.toLowerCase();
|
|
275
|
+
let styleId, attribSelector;
|
|
276
|
+
|
|
277
|
+
if (style.hasAttribute('global') || style.hasAttribute('data-global')) {
|
|
278
|
+
styleId = tagName;
|
|
279
|
+
attribSelector = '';
|
|
280
|
+
let doc = Globals$1.doc || root.ownerDocument || document;
|
|
281
|
+
if (!doc.head.querySelector(`style[data-style="${styleId}"]`)) {
|
|
282
|
+
doc.head.append(style);
|
|
283
|
+
style.setAttribute('data-style', styleId);
|
|
284
|
+
}
|
|
285
|
+
else // TODO: Make sure the style has no expressions.
|
|
286
|
+
style.remove(); // already in the head.
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
let styleId = root.getAttribute('data-style');
|
|
290
|
+
if (!styleId) {
|
|
291
|
+
// Keep track of one style id for each class.
|
|
292
|
+
// TODO: Put this outside the class in a map, so it doesn't conflict with static properties.
|
|
293
|
+
if (!root.constructor.styleId)
|
|
294
|
+
root.constructor.styleId = 1;
|
|
295
|
+
styleId = root.constructor.styleId++;
|
|
296
|
+
|
|
297
|
+
root.setAttribute('data-style', styleId);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
attribSelector = `[data-style="${styleId}"]`;
|
|
259
301
|
}
|
|
260
302
|
|
|
261
303
|
// Replace ":host" with "tagName[data-style=...]" in the css.
|
|
262
|
-
let tagName = root.tagName.toLowerCase();
|
|
263
304
|
for (let child of style.childNodes) {
|
|
264
305
|
if (child.nodeType === 3) {
|
|
265
306
|
let oldText = child.textContent;
|
|
266
|
-
let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, `${tagName}
|
|
307
|
+
let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, `${tagName}${attribSelector}`);
|
|
267
308
|
if (oldText !== newText)
|
|
268
309
|
child.textContent = newText;
|
|
269
310
|
}
|
|
@@ -310,7 +351,6 @@ let Util = {
|
|
|
310
351
|
return str.replace(/-([a-z])/g, g => g[1].toUpperCase());
|
|
311
352
|
},
|
|
312
353
|
|
|
313
|
-
|
|
314
354
|
/**
|
|
315
355
|
* A generator function that recursively traverses and flattens a value.
|
|
316
356
|
*
|
|
@@ -351,7 +391,7 @@ let Util = {
|
|
|
351
391
|
|
|
352
392
|
/**
|
|
353
393
|
* Get the value of an input as the most appropriate JavaScript type.
|
|
354
|
-
* @param node {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|
|
|
394
|
+
* @param node {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|HTMLElement}
|
|
355
395
|
* @return {string|string[]|number|[]|File[]|Date|boolean} */
|
|
356
396
|
getInputValue(node) {
|
|
357
397
|
// .type is a built-in DOM property
|
|
@@ -365,6 +405,8 @@ let Util = {
|
|
|
365
405
|
return node.valueAsDate; // Date Object
|
|
366
406
|
if (node.type === 'select-multiple') // <select multiple>
|
|
367
407
|
return [...node.selectedOptions].map(option => option.value); // Array of Strings
|
|
408
|
+
if (node.hasAttribute('contenteditable'))
|
|
409
|
+
return node.innerHTML;
|
|
368
410
|
|
|
369
411
|
return node.value; // String
|
|
370
412
|
},
|
|
@@ -614,8 +656,6 @@ class MultiValueMap {
|
|
|
614
656
|
* @returns {Node[]} The same list of future children.
|
|
615
657
|
*/
|
|
616
658
|
const udomdiff = (parentNode, a, b, before) => {
|
|
617
|
-
|
|
618
|
-
|
|
619
659
|
const bLength = b.length;
|
|
620
660
|
let aEnd = a.length;
|
|
621
661
|
let bEnd = bLength;
|
|
@@ -637,8 +677,6 @@ const udomdiff = (parentNode, a, b, before) => {
|
|
|
637
677
|
while (bStart < bEnd) {
|
|
638
678
|
let bNode = b[bStart++];
|
|
639
679
|
parentNode.insertBefore(bNode, node);
|
|
640
|
-
|
|
641
|
-
|
|
642
680
|
}
|
|
643
681
|
}
|
|
644
682
|
// remove head or tail: fast path
|
|
@@ -648,8 +686,6 @@ const udomdiff = (parentNode, a, b, before) => {
|
|
|
648
686
|
let aNode = a[aStart];
|
|
649
687
|
if (!map || !map.has(aNode)) {
|
|
650
688
|
parentNode.removeChild(aNode);
|
|
651
|
-
|
|
652
|
-
|
|
653
689
|
}
|
|
654
690
|
aStart++;
|
|
655
691
|
}
|
|
@@ -686,13 +722,10 @@ const udomdiff = (parentNode, a, b, before) => {
|
|
|
686
722
|
a2,
|
|
687
723
|
b2.nextSibling
|
|
688
724
|
);
|
|
689
|
-
|
|
690
725
|
|
|
691
726
|
let bNode = b[--bEnd];
|
|
692
727
|
parentNode.insertBefore(bNode, node);
|
|
693
728
|
|
|
694
|
-
|
|
695
|
-
|
|
696
729
|
// mark the future index as identical (yeah, it's dirty, but cheap 👍)
|
|
697
730
|
// The main reason to do this, is that when a[aEnd] will be reached,
|
|
698
731
|
// the loop will likely be on the fast path, as identical to b[bEnd].
|
|
@@ -740,8 +773,6 @@ const udomdiff = (parentNode, a, b, before) => {
|
|
|
740
773
|
while (bStart < index) {
|
|
741
774
|
let bNode = b[bStart++];
|
|
742
775
|
parentNode.insertBefore(bNode, node);
|
|
743
|
-
|
|
744
|
-
|
|
745
776
|
}
|
|
746
777
|
}
|
|
747
778
|
// if the effort wasn't good enough, fallback to a replace,
|
|
@@ -754,8 +785,6 @@ const udomdiff = (parentNode, a, b, before) => {
|
|
|
754
785
|
bNode,
|
|
755
786
|
aNode
|
|
756
787
|
);
|
|
757
|
-
|
|
758
|
-
|
|
759
788
|
}
|
|
760
789
|
}
|
|
761
790
|
// otherwise move the source forward, 'cause there's nothing to do
|
|
@@ -768,8 +797,6 @@ const udomdiff = (parentNode, a, b, before) => {
|
|
|
768
797
|
else {
|
|
769
798
|
let aNode = a[aStart++];
|
|
770
799
|
parentNode.removeChild(aNode);
|
|
771
|
-
|
|
772
|
-
|
|
773
800
|
}
|
|
774
801
|
}
|
|
775
802
|
}
|
|
@@ -893,13 +920,13 @@ class ExprPath {
|
|
|
893
920
|
case 2: // PathType.Multiple:
|
|
894
921
|
this.applyMultipleAttribs(this.nodeMarker, exprs[0]);
|
|
895
922
|
break;
|
|
896
|
-
case
|
|
923
|
+
case 4: // PathType.Comment:
|
|
897
924
|
// Expressions inside Html comments. Deliberately empty because we won't waste time updating them.
|
|
898
925
|
break;
|
|
899
|
-
case
|
|
926
|
+
case 5: // PathType.Event:
|
|
900
927
|
this.applyEventAttrib(this.nodeMarker, exprs[0], this.parentNg.rootNg.root);
|
|
901
928
|
break;
|
|
902
|
-
default: //
|
|
929
|
+
default: // 3 PathType.Attribute
|
|
903
930
|
// One attribute value may have multiple expressions. Here we apply them all at once.
|
|
904
931
|
this.applyValueAttrib(this.nodeMarker, exprs);
|
|
905
932
|
break;
|
|
@@ -909,7 +936,7 @@ class ExprPath {
|
|
|
909
936
|
/**
|
|
910
937
|
* Insert/replace the nodes created by a single expression.
|
|
911
938
|
* Called by applyExprs()
|
|
912
|
-
* This function is recursive
|
|
939
|
+
* This function is recursive. It calls functions that call applyNodes().
|
|
913
940
|
* @param expr {Expr}
|
|
914
941
|
* @param freeNodeGroups {boolean}
|
|
915
942
|
* @return {Node[]} New Nodes created. */
|
|
@@ -944,7 +971,6 @@ class ExprPath {
|
|
|
944
971
|
if (secondPass.length) {
|
|
945
972
|
for (let [nodesIndex, ngIndex] of secondPass) {
|
|
946
973
|
let ng = path.getNodeGroup(newNodes[nodesIndex], false);
|
|
947
|
-
|
|
948
974
|
let ngNodes = ng.getNodes();
|
|
949
975
|
|
|
950
976
|
|
|
@@ -965,11 +991,8 @@ class ExprPath {
|
|
|
965
991
|
|
|
966
992
|
|
|
967
993
|
|
|
968
|
-
|
|
969
|
-
|
|
970
994
|
let oldNodes = path.getNodes();
|
|
971
995
|
|
|
972
|
-
|
|
973
996
|
// This pre-check makes it a few percent faster?
|
|
974
997
|
let same = Util.arraySame(oldNodes, newNodes);
|
|
975
998
|
if (!same) {
|
|
@@ -982,7 +1005,6 @@ class ExprPath {
|
|
|
982
1005
|
// Fast clear method
|
|
983
1006
|
let isNowEmpty = oldNodes.length && !newNodes.length;
|
|
984
1007
|
if (!isNowEmpty || !path.fastClear())
|
|
985
|
-
|
|
986
1008
|
// Rearrange nodes.
|
|
987
1009
|
udomdiff(path.nodeMarker.parentNode, oldNodes, newNodes, path.nodeMarker);
|
|
988
1010
|
|
|
@@ -995,18 +1017,17 @@ class ExprPath {
|
|
|
995
1017
|
ng.removeAndSaveOrphans();
|
|
996
1018
|
|
|
997
1019
|
// Instantiate components created within ${...} expressions.
|
|
998
|
-
//
|
|
1020
|
+
// Also see this.applyExactNodes() which handles calling render() on web components even if they are unchanged.
|
|
999
1021
|
for (let el of newNodes) {
|
|
1000
|
-
if (el
|
|
1022
|
+
if (el?.nodeType === 1) { // HTMLElement
|
|
1001
1023
|
if (el.hasAttribute('solarite-placeholder'))
|
|
1002
|
-
this.parentNg.
|
|
1024
|
+
this.parentNg.handleComponent(el, null, true);
|
|
1003
1025
|
for (let child of el.querySelectorAll('[solarite-placeholder]'))
|
|
1004
|
-
this.parentNg.
|
|
1026
|
+
this.parentNg.handleComponent(child, null, true);
|
|
1005
1027
|
}
|
|
1006
1028
|
}
|
|
1007
1029
|
}
|
|
1008
1030
|
|
|
1009
|
-
|
|
1010
1031
|
|
|
1011
1032
|
}
|
|
1012
1033
|
|
|
@@ -1114,7 +1135,7 @@ class ExprPath {
|
|
|
1114
1135
|
}
|
|
1115
1136
|
|
|
1116
1137
|
// String/Number/Date/Boolean
|
|
1117
|
-
else if (!(expr instanceof Template) && !(expr
|
|
1138
|
+
else if (!(expr instanceof Template) && !(expr?.nodeType)){
|
|
1118
1139
|
// Convert expression to a string.
|
|
1119
1140
|
if (expr === undefined || expr === false || expr === null) // Util.isFalsy() inlined
|
|
1120
1141
|
expr = '';
|
|
@@ -1124,7 +1145,9 @@ class ExprPath {
|
|
|
1124
1145
|
// Get the same Template for the same string each time.
|
|
1125
1146
|
// let template = Globals.stringTemplates[expr];
|
|
1126
1147
|
// if (!template) {
|
|
1148
|
+
|
|
1127
1149
|
let template = new Template([expr], []);
|
|
1150
|
+
template.isText = true;
|
|
1128
1151
|
// Globals.stringTemplates[expr] = template;
|
|
1129
1152
|
//}
|
|
1130
1153
|
|
|
@@ -1149,12 +1172,44 @@ class ExprPath {
|
|
|
1149
1172
|
|
|
1150
1173
|
if (expr instanceof Template) {
|
|
1151
1174
|
let ng = this.getNodeGroup(expr, true);
|
|
1175
|
+
|
|
1152
1176
|
if (ng) {
|
|
1177
|
+
let newestNodes = ng.getNodes();
|
|
1178
|
+
newNodes.push(...newestNodes);
|
|
1179
|
+
|
|
1180
|
+
// New!
|
|
1181
|
+
// Re-apply all expressions if there's a web component, so we can pass them to its constructor.
|
|
1182
|
+
// NodeGroup.applyExprs() is used to call applyComponentExprs() on web components that have expression attributes.
|
|
1183
|
+
// For those that don't, we call applyComponentExprs() directly here.
|
|
1184
|
+
// Also see similar code at the end of this.applyNodes() which handles web components being instantiated the first time.
|
|
1185
|
+
let apply = false;
|
|
1186
|
+
for (let el of newestNodes) {
|
|
1187
|
+
if (el?.nodeType === 1) { // HTMLElement
|
|
1188
|
+
|
|
1189
|
+
if (el.tagName.includes('-')) {
|
|
1190
|
+
if (!expr.exprs.find(expr => expr?.nodeMarker === el))
|
|
1191
|
+
this.parentNg.handleComponent(el, null, true);
|
|
1192
|
+
else // Commenting out this "else" causes render() to be called too often, but other UI code fails if it's present.
|
|
1193
|
+
apply = true;
|
|
1194
|
+
}
|
|
1195
|
+
for (let child of el.querySelectorAll('*')) {
|
|
1196
|
+
if (child.tagName.includes('-')) {
|
|
1197
|
+
if (!expr.exprs.find(expr => expr?.nodeMarker === child))
|
|
1198
|
+
this.parentNg.handleComponent(child, null, true);
|
|
1199
|
+
else
|
|
1200
|
+
apply = true;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1153
1205
|
|
|
1154
|
-
//
|
|
1155
|
-
|
|
1156
|
-
|
|
1206
|
+
// This calls render() on web components that have expressions as attributes.
|
|
1207
|
+
if (apply)
|
|
1208
|
+
ng.applyExprs(expr.exprs);
|
|
1209
|
+
|
|
1157
1210
|
this.nodeGroups.push(ng);
|
|
1211
|
+
|
|
1212
|
+
return ng;
|
|
1158
1213
|
}
|
|
1159
1214
|
|
|
1160
1215
|
// If expression, mark it to be evaluated later in ExprPath.apply() to find partial match.
|
|
@@ -1166,10 +1221,10 @@ class ExprPath {
|
|
|
1166
1221
|
}
|
|
1167
1222
|
|
|
1168
1223
|
// Node(s) created by an expression.
|
|
1169
|
-
else if (expr
|
|
1224
|
+
else if (expr?.nodeType) {
|
|
1170
1225
|
|
|
1171
1226
|
// DocumentFragment created by an expression.
|
|
1172
|
-
if (expr
|
|
1227
|
+
if (expr?.nodeType === 11) // DocumentFragment
|
|
1173
1228
|
newNodes.push(...expr.childNodes);
|
|
1174
1229
|
else
|
|
1175
1230
|
newNodes.push(expr);
|
|
@@ -1323,7 +1378,7 @@ class ExprPath {
|
|
|
1323
1378
|
}
|
|
1324
1379
|
|
|
1325
1380
|
/**
|
|
1326
|
-
*
|
|
1381
|
+
* Set the value of an attribute. This can be for any attribute, not just attributes named "value".
|
|
1327
1382
|
* @param node
|
|
1328
1383
|
* @param exprs */
|
|
1329
1384
|
// TODO: node is always this.nodeMarker?
|
|
@@ -1353,12 +1408,21 @@ class ExprPath {
|
|
|
1353
1408
|
}
|
|
1354
1409
|
else {
|
|
1355
1410
|
// TODO: should we remove isFalsy, since these are always props?
|
|
1356
|
-
|
|
1411
|
+
const strValue = Util.isFalsy(value) ? '' : value;
|
|
1412
|
+
|
|
1413
|
+
// Special case for contenteditable
|
|
1414
|
+
if (this.attrName === 'value' && node.hasAttribute('contenteditable')) {
|
|
1415
|
+
const existingValue = node.innerHTML;
|
|
1416
|
+
if (strValue !== existingValue)
|
|
1417
|
+
node.innerHTML = strValue;
|
|
1418
|
+
}
|
|
1419
|
+
else {
|
|
1357
1420
|
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1421
|
+
// If we don't have this condition, when we call render(), the browser will scroll to the currently
|
|
1422
|
+
// selected item in a <select> and mess up manually scrolling to a different value.
|
|
1423
|
+
if (strValue !== node[this.attrName])
|
|
1424
|
+
node[this.attrName] = strValue;
|
|
1425
|
+
}
|
|
1362
1426
|
}
|
|
1363
1427
|
|
|
1364
1428
|
// TODO: We need to remove any old listeners, like in bindEventAttribute.
|
|
@@ -1378,16 +1442,8 @@ class ExprPath {
|
|
|
1378
1442
|
|
|
1379
1443
|
// Regular attribute
|
|
1380
1444
|
else {
|
|
1381
|
-
//
|
|
1382
|
-
//
|
|
1383
|
-
//if (this.attrName === 'disabled')
|
|
1384
|
-
// debugger;
|
|
1385
|
-
|
|
1386
|
-
// hasOwnProperty() checks only the object, not the parents
|
|
1387
|
-
// this.attrName in node checks the node and the parents.
|
|
1388
|
-
// This version checks the html element it extends from, to see if has a setter set:
|
|
1389
|
-
// Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), this.attrName)?.set
|
|
1390
|
-
//let isProp = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), this.attrName)?.set;
|
|
1445
|
+
// Cache this on ExprPath.isHtmlProperty when Shell creates the props.
|
|
1446
|
+
// Have ExprPath.clone() copy .isHtmlProperty?
|
|
1391
1447
|
let isProp = this.isHtmlProperty;
|
|
1392
1448
|
if (isProp === undefined)
|
|
1393
1449
|
isProp = this.isHtmlProperty = Util.isHtmlProp(node, this.attrName);
|
|
@@ -1397,7 +1453,7 @@ class ExprPath {
|
|
|
1397
1453
|
if (!multiple) {
|
|
1398
1454
|
Globals$1.currentExprPath = this; // Used by watch()
|
|
1399
1455
|
if (typeof expr === 'function') {
|
|
1400
|
-
if (this.
|
|
1456
|
+
if (this.isComponent) { // Don't evaluate functions before passing them to components
|
|
1401
1457
|
return
|
|
1402
1458
|
}
|
|
1403
1459
|
this.watchFunction = expr; // The function that gets the expression, used for renderWatched()
|
|
@@ -1455,6 +1511,13 @@ class ExprPath {
|
|
|
1455
1511
|
// since we also prohibit expressions that are a child of textarea.
|
|
1456
1512
|
if (isProp)
|
|
1457
1513
|
node[this.attrName] = joinedValue;
|
|
1514
|
+
|
|
1515
|
+
// Allow one-way binding to contenteditable value attribute.
|
|
1516
|
+
// Contenteditables normally don't have a value attribute and have their content set via innerHTML.
|
|
1517
|
+
// Solarite doesn't allow contenteditables to have expressions as their children.
|
|
1518
|
+
else if (node.hasAttribute('contenteditable'))
|
|
1519
|
+
node.innerHTML = joinedValue;
|
|
1520
|
+
|
|
1458
1521
|
// TODO: Putting an 'else' here would be more performant
|
|
1459
1522
|
node.setAttribute(this.attrName, joinedValue);
|
|
1460
1523
|
}
|
|
@@ -1485,6 +1548,7 @@ class ExprPath {
|
|
|
1485
1548
|
nodeBefore = childNodes[this.nodeBeforeIndex];
|
|
1486
1549
|
|
|
1487
1550
|
let result = new ExprPath(nodeBefore, nodeMarker, this.type, this.attrName, this.attrValue);
|
|
1551
|
+
result.isComponent = this.isComponent;
|
|
1488
1552
|
|
|
1489
1553
|
|
|
1490
1554
|
|
|
@@ -1493,9 +1557,7 @@ class ExprPath {
|
|
|
1493
1557
|
|
|
1494
1558
|
/**
|
|
1495
1559
|
* Clear the nodeCache of this ExprPath, as well as all parent and child ExprPaths that
|
|
1496
|
-
* share the same DOM parent node.
|
|
1497
|
-
*
|
|
1498
|
-
* TODO: Is recursive clearing ever necessary? */
|
|
1560
|
+
* share the same DOM parent node. */
|
|
1499
1561
|
clearNodesCache() {
|
|
1500
1562
|
let path = this;
|
|
1501
1563
|
|
|
@@ -1508,9 +1570,6 @@ class ExprPath {
|
|
|
1508
1570
|
// If stuck in an infinite loop here, the problem is likely due to Template hash colisions.
|
|
1509
1571
|
// Which cause one path to be the descendant of itself, creating a cycle.
|
|
1510
1572
|
}
|
|
1511
|
-
|
|
1512
|
-
// Commented out on Sep 30, 2024 b/c it was making the benchmark never finish when adding 10k rows.
|
|
1513
|
-
//clearChildNodeCache(this);
|
|
1514
1573
|
}
|
|
1515
1574
|
|
|
1516
1575
|
|
|
@@ -1552,7 +1611,7 @@ class ExprPath {
|
|
|
1552
1611
|
// result2.push(...ng.getNodes())
|
|
1553
1612
|
// return result2;
|
|
1554
1613
|
|
|
1555
|
-
if (this.type === ExprPathType.AttribValue || this.type === ExprPathType.AttribMultiple
|
|
1614
|
+
if (this.type === ExprPathType.AttribValue || this.type === ExprPathType.AttribMultiple) {
|
|
1556
1615
|
return [this.nodeMarker];
|
|
1557
1616
|
}
|
|
1558
1617
|
|
|
@@ -1610,11 +1669,13 @@ class ExprPath {
|
|
|
1610
1669
|
result = collection.deleteAny(template.getExactKey());
|
|
1611
1670
|
}
|
|
1612
1671
|
|
|
1613
|
-
if (result) // also delete the matching close key.
|
|
1672
|
+
if (result) {// also delete the matching close key.
|
|
1614
1673
|
collection.deleteSpecific(template.getCloseKey(), result);
|
|
1615
|
-
|
|
1616
|
-
|
|
1674
|
+
|
|
1675
|
+
//result.applyExprs(template.exprs);
|
|
1617
1676
|
}
|
|
1677
|
+
else
|
|
1678
|
+
return null;
|
|
1618
1679
|
}
|
|
1619
1680
|
|
|
1620
1681
|
// Find a close match.
|
|
@@ -1648,12 +1709,6 @@ class ExprPath {
|
|
|
1648
1709
|
return result;
|
|
1649
1710
|
}
|
|
1650
1711
|
|
|
1651
|
-
isComponent() {
|
|
1652
|
-
// Events won't have type===Component.
|
|
1653
|
-
// TODO: Have a special flag for components instead of it being on the type?
|
|
1654
|
-
return this.type === ExprPathType.ComponentAttribValue || (this.attrName && this.nodeMarker.tagName && this.nodeMarker.tagName.includes('-'));
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
1712
|
/**
|
|
1658
1713
|
* TODO: Rename this to nodeGroupsInUse, nodeGroupsAvialableAttached and nodeGroupsAvailableDetached?
|
|
1659
1714
|
* Nodes that have been used during the current render().
|
|
@@ -1710,7 +1765,7 @@ class ExprPath {
|
|
|
1710
1765
|
/** @enum {int} */
|
|
1711
1766
|
const ExprPathType = {
|
|
1712
1767
|
/** Child of a node */
|
|
1713
|
-
Content: 1,
|
|
1768
|
+
Content: 1, // TODO: Rename to Nodes
|
|
1714
1769
|
|
|
1715
1770
|
/** One or more whole attributes */
|
|
1716
1771
|
AttribMultiple: 2,
|
|
@@ -1718,14 +1773,11 @@ const ExprPathType = {
|
|
|
1718
1773
|
/** Value of an attribute. */
|
|
1719
1774
|
AttribValue: 3,
|
|
1720
1775
|
|
|
1721
|
-
/** Value of an attribute being passed to a component. */
|
|
1722
|
-
ComponentAttribValue: 4,
|
|
1723
|
-
|
|
1724
1776
|
/** Expressions inside Html comments. */
|
|
1725
|
-
Comment:
|
|
1777
|
+
Comment: 4,
|
|
1726
1778
|
|
|
1727
1779
|
/** Value of an attribute. */
|
|
1728
|
-
Event:
|
|
1780
|
+
Event: 5,
|
|
1729
1781
|
};
|
|
1730
1782
|
|
|
1731
1783
|
|
|
@@ -1891,7 +1943,7 @@ class Shell {
|
|
|
1891
1943
|
|
|
1892
1944
|
|
|
1893
1945
|
if (html.length === 1 && !html[0].match(/[<&]/)) {
|
|
1894
|
-
this.fragment =
|
|
1946
|
+
this.fragment = Globals$1.doc.createTextNode(html[0]);
|
|
1895
1947
|
return;
|
|
1896
1948
|
}
|
|
1897
1949
|
|
|
@@ -1899,18 +1951,18 @@ class Shell {
|
|
|
1899
1951
|
// 1. Add placeholders
|
|
1900
1952
|
let joinedHtml = Shell.addPlaceholders(html);
|
|
1901
1953
|
|
|
1902
|
-
let template =
|
|
1954
|
+
let template = Globals$1.doc.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
|
|
1903
1955
|
if (joinedHtml)
|
|
1904
1956
|
template.innerHTML = joinedHtml;
|
|
1905
1957
|
else // Create one text node, so shell isn't empty and NodeGroups created from it have something to point the startNode and endNode at.
|
|
1906
|
-
template.content.append(
|
|
1958
|
+
template.content.append(Globals$1.doc.createTextNode(''));
|
|
1907
1959
|
this.fragment = template.content;
|
|
1908
1960
|
|
|
1909
1961
|
// 2. Find placeholders
|
|
1910
1962
|
let node;
|
|
1911
1963
|
let toRemove = [];
|
|
1912
1964
|
let placeholdersUsed = 0;
|
|
1913
|
-
const walker =
|
|
1965
|
+
const walker = Globals$1.doc.createTreeWalker(this.fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
|
|
1914
1966
|
while (node = walker.nextNode()) {
|
|
1915
1967
|
|
|
1916
1968
|
// Remove previous after each iteration, so paths will still be calculated correctly.
|
|
@@ -1946,10 +1998,13 @@ class Shell {
|
|
|
1946
1998
|
// Replace comment placeholders
|
|
1947
1999
|
else if (node.nodeType === 8 && node.nodeValue === '!✨!') {
|
|
1948
2000
|
|
|
2001
|
+
if (node?.parentNode?.closest && node?.parentNode?.closest('[contenteditable]'))
|
|
2002
|
+
throw new Error(`Contenteditable can't have expressions inside them. Use <div contenteditable value="\${...}"> instead.`);
|
|
2003
|
+
|
|
1949
2004
|
// Get or create nodeBefore.
|
|
1950
2005
|
let nodeBefore = node.previousSibling; // Can be the same as another Path's nodeMarker.
|
|
1951
2006
|
if (!nodeBefore) {
|
|
1952
|
-
nodeBefore =
|
|
2007
|
+
nodeBefore = Globals$1.doc.createComment('ExprPath:'+this.paths.length);
|
|
1953
2008
|
node.parentNode.insertBefore(nodeBefore, node);
|
|
1954
2009
|
}
|
|
1955
2010
|
|
|
@@ -1974,9 +2029,9 @@ class Shell {
|
|
|
1974
2029
|
placeholdersUsed ++;
|
|
1975
2030
|
}
|
|
1976
2031
|
|
|
2032
|
+
// Comments become text nodes when inside textareas.
|
|
1977
2033
|
else if (node.nodeType === 3 && node.parentNode?.tagName === 'TEXTAREA' && node.textContent.includes('<!--!✨!-->'))
|
|
1978
2034
|
throw new Error(`Textarea can't have expressions inside them. Use <textarea value="\${...}"> instead.`);
|
|
1979
|
-
|
|
1980
2035
|
|
|
1981
2036
|
|
|
1982
2037
|
// Sometimes users will comment out a block of html code that has expressions.
|
|
@@ -2000,7 +2055,7 @@ class Shell {
|
|
|
2000
2055
|
|
|
2001
2056
|
let placeholders = [];
|
|
2002
2057
|
for (let i = 0; i<parts.length; i++) {
|
|
2003
|
-
let current =
|
|
2058
|
+
let current = Globals$1.doc.createTextNode(parts[i]);
|
|
2004
2059
|
node.parentNode.insertBefore(current, node);
|
|
2005
2060
|
if (i > 0)
|
|
2006
2061
|
placeholders.push(current);
|
|
@@ -2039,9 +2094,9 @@ class Shell {
|
|
|
2039
2094
|
path.nodeMarkerPath = getNodePath(path.nodeMarker);
|
|
2040
2095
|
|
|
2041
2096
|
// Cache so we don't have to calculate this later inside NodeGroup.applyExprs()
|
|
2042
|
-
if (path.type === ExprPathType.AttribValue && path.nodeMarker.nodeType === 1 &&
|
|
2097
|
+
if ((path.type === ExprPathType.AttribValue || path.type === ExprPathType.Event) && path.nodeMarker.nodeType === 1 &&
|
|
2043
2098
|
(path.nodeMarker.tagName.includes('-') || path.nodeMarker.hasAttribute('is'))) {
|
|
2044
|
-
path.
|
|
2099
|
+
path.isComponent = true;
|
|
2045
2100
|
}
|
|
2046
2101
|
}
|
|
2047
2102
|
|
|
@@ -2207,30 +2262,39 @@ class NodeGroup {
|
|
|
2207
2262
|
* @type {?Map<HTMLStyleElement, string>} */
|
|
2208
2263
|
styles;
|
|
2209
2264
|
|
|
2265
|
+
dynamicComponents = new Set();
|
|
2266
|
+
staticComponents = [];
|
|
2267
|
+
|
|
2268
|
+
/** @type {Template} */
|
|
2269
|
+
template;
|
|
2270
|
+
|
|
2210
2271
|
|
|
2211
2272
|
/**
|
|
2212
2273
|
* Create an "instantiated" NodeGroup from a Template and add it to an element.
|
|
2213
2274
|
* @param template {Template} Create it from the html strings and expressions in this template.
|
|
2214
2275
|
* @param parentPath {?ExprPath} */
|
|
2215
2276
|
constructor(template, parentPath=null) {
|
|
2277
|
+
this.rootNg = parentPath?.parentNg?.rootNg || this;
|
|
2278
|
+
this.parentPath = parentPath;
|
|
2279
|
+
|
|
2216
2280
|
if (!(this instanceof RootNodeGroup)) {
|
|
2217
2281
|
|
|
2218
|
-
let [fragment, shell] = this.
|
|
2282
|
+
let [fragment, shell] = this.populateFromTemplate(template);
|
|
2219
2283
|
|
|
2220
2284
|
if (fragment && template.exprs.length) {
|
|
2221
2285
|
this.updatePaths(fragment, shell.paths);
|
|
2222
2286
|
|
|
2223
2287
|
// Static web components can sometimes have children created via expressions.
|
|
2224
2288
|
// But calling applyExprs() will mess up the shell's path to them.
|
|
2225
|
-
// So we find them first, then call
|
|
2226
|
-
|
|
2289
|
+
// So we find them first, then call instantiateStaticComponents() after their children have been created.
|
|
2290
|
+
this.staticComponents = this.findStaticComponents(fragment, shell);
|
|
2227
2291
|
|
|
2228
2292
|
this.activateEmbeds(fragment, shell);
|
|
2229
2293
|
|
|
2230
2294
|
// Apply exprs
|
|
2231
2295
|
this.applyExprs(template.exprs);
|
|
2232
2296
|
|
|
2233
|
-
this.instantiateStaticComponents(staticComponents);
|
|
2297
|
+
this.instantiateStaticComponents(this.staticComponents);
|
|
2234
2298
|
}
|
|
2235
2299
|
else if (shell)
|
|
2236
2300
|
this.activateEmbeds(fragment, shell);
|
|
@@ -2241,47 +2305,33 @@ class NodeGroup {
|
|
|
2241
2305
|
* Common init shared by RootNodeGroup and NodeGroup constructors.
|
|
2242
2306
|
* But in a separate function because they need to do this at a different step.
|
|
2243
2307
|
* @param template {Template} Create it from the html strings and expressions in this template.
|
|
2244
|
-
* @
|
|
2245
|
-
|
|
2246
|
-
* @param closeKey {?string}
|
|
2247
|
-
* @returns {[DocumentFragment, Shell]} */
|
|
2248
|
-
init(template, parentPath=null, exactKey=null, closeKey=null) {
|
|
2249
|
-
this.exactKey = exactKey || template.getExactKey();
|
|
2250
|
-
this.closeKey = closeKey || template.getCloseKey();
|
|
2251
|
-
|
|
2252
|
-
this.parentPath = parentPath;
|
|
2253
|
-
this.rootNg = parentPath?.parentNg?.rootNg || this;
|
|
2254
|
-
|
|
2308
|
+
* @returns {[DocumentFragment, Shell]} The Shell created from the template,a nd the fragment cloned from the Shell.*/
|
|
2309
|
+
populateFromTemplate(template) {
|
|
2255
2310
|
|
|
2256
|
-
|
|
2257
|
-
/** @type {Template} */
|
|
2258
2311
|
this.template = template;
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
template.nodeGroup = this;
|
|
2262
|
-
|
|
2263
|
-
// Get a cached version of the parsed and instantiated html, and ExprPaths.
|
|
2312
|
+
this.exactKey = template.getExactKey();
|
|
2313
|
+
this.closeKey = template.getCloseKey();
|
|
2264
2314
|
|
|
2265
2315
|
// If it's just a text node, skip a bunch of unnecessary steps.
|
|
2266
|
-
if (
|
|
2267
|
-
|
|
2268
|
-
let textNode = document.createTextNode(template.html[0]);
|
|
2269
|
-
|
|
2316
|
+
if (template.isText) {
|
|
2317
|
+
let textNode = Globals$1.doc.createTextNode(template.html[0]);
|
|
2270
2318
|
this.startNode = this.endNode = textNode;
|
|
2271
2319
|
return [];
|
|
2272
2320
|
}
|
|
2321
|
+
|
|
2322
|
+
// Get a cached version of the parsed and instantiated html, and ExprPaths:
|
|
2273
2323
|
else {
|
|
2274
2324
|
let shell = Shell.get(template.html);
|
|
2275
2325
|
let fragment = shell.fragment.cloneNode(true);
|
|
2276
2326
|
|
|
2277
|
-
if (fragment
|
|
2327
|
+
if (fragment?.nodeType === 11) { // DocumentFragment
|
|
2278
2328
|
let childNodes = fragment.childNodes;
|
|
2279
2329
|
this.startNode = childNodes[0];
|
|
2280
2330
|
this.endNode = childNodes[childNodes.length - 1];
|
|
2281
2331
|
}
|
|
2282
|
-
else
|
|
2332
|
+
else
|
|
2283
2333
|
this.startNode = this.endNode = fragment;
|
|
2284
|
-
|
|
2334
|
+
|
|
2285
2335
|
return [fragment, shell];
|
|
2286
2336
|
}
|
|
2287
2337
|
}
|
|
@@ -2319,6 +2369,7 @@ class NodeGroup {
|
|
|
2319
2369
|
exprIndex--;
|
|
2320
2370
|
}
|
|
2321
2371
|
|
|
2372
|
+
|
|
2322
2373
|
// TODO: Need to end and restart this block when going from one component to the next?
|
|
2323
2374
|
// Think of having two adjacent components.
|
|
2324
2375
|
// But the dynamicAttribsAdjacet test already passes.
|
|
@@ -2327,11 +2378,11 @@ class NodeGroup {
|
|
|
2327
2378
|
// 1. Instantiate it if it hasn't already been, sending all expr's to its constructor.
|
|
2328
2379
|
// 2. Otherwise send them to its render function.
|
|
2329
2380
|
// Components with no expressions as attributes are instead activated in activateEmbeds().
|
|
2330
|
-
if (path.nodeMarker !== this.rootNg.root && path.isComponent
|
|
2381
|
+
if (path.nodeMarker !== this.rootNg.root && path.isComponent) {
|
|
2331
2382
|
|
|
2332
|
-
if (!nextPath || !nextPath.isComponent
|
|
2383
|
+
if (!nextPath || !nextPath.isComponent || nextPath.nodeMarker !== path.nodeMarker)
|
|
2333
2384
|
lastComponentPathIndex = i;
|
|
2334
|
-
let isFirstComponentPath = !prevPath || !prevPath.isComponent
|
|
2385
|
+
let isFirstComponentPath = !prevPath || !prevPath.isComponent || prevPath.nodeMarker !== path.nodeMarker;
|
|
2335
2386
|
|
|
2336
2387
|
if (isFirstComponentPath) {
|
|
2337
2388
|
|
|
@@ -2341,7 +2392,7 @@ class NodeGroup {
|
|
|
2341
2392
|
componentProps[attrName] = pathExprs[j].length > 1 ? pathExprs[j].join('') : pathExprs[j][0];
|
|
2342
2393
|
}
|
|
2343
2394
|
|
|
2344
|
-
this.
|
|
2395
|
+
this.handleComponent(path.nodeMarker, componentProps, true);
|
|
2345
2396
|
|
|
2346
2397
|
// Set attributes on component.
|
|
2347
2398
|
for (let j=i; j<=lastComponentPathIndex; j++)
|
|
@@ -2360,7 +2411,10 @@ class NodeGroup {
|
|
|
2360
2411
|
// TODO: Only do this if we have ExprPaths within styles?
|
|
2361
2412
|
this.updateStyles();
|
|
2362
2413
|
|
|
2363
|
-
|
|
2414
|
+
// Call render() on static web components. This makes the component.staticAttribs() test work.
|
|
2415
|
+
for (let el of this.staticComponents)
|
|
2416
|
+
if (el.render)
|
|
2417
|
+
el.render(Util.attribsToObject(el)); // It has no expressions.
|
|
2364
2418
|
|
|
2365
2419
|
// Invalidate the nodes cache because we just changed it.
|
|
2366
2420
|
this.nodesCache = null;
|
|
@@ -2374,50 +2428,39 @@ class NodeGroup {
|
|
|
2374
2428
|
}
|
|
2375
2429
|
|
|
2376
2430
|
/**
|
|
2377
|
-
*
|
|
2378
|
-
* @param el {
|
|
2379
|
-
* @param props {Object}
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
// Perhaps if Components were treated as child NodeGroups, which would need to be the child of an ExprPath,
|
|
2384
|
-
// then we could re-use the hash and logic from NodeManager?
|
|
2385
|
-
let newHash = getObjectHash(props);
|
|
2386
|
-
|
|
2431
|
+
* Unified path to ensure a child component is instantiated (if placeholder) and optionally rendered.
|
|
2432
|
+
* @param el {HTMLElement}
|
|
2433
|
+
* @param props {?Object}
|
|
2434
|
+
* @param doRender {boolean}
|
|
2435
|
+
* @return {HTMLElement} The (possibly replaced) element. */
|
|
2436
|
+
handleComponent(el, props=null, doRender=true) {
|
|
2387
2437
|
let isPreHtmlElement = el.hasAttribute('solarite-placeholder');
|
|
2388
2438
|
let isPreIsElement = el.hasAttribute('_is');
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
// Instantiate a placeholder.
|
|
2439
|
+
let attribs, children;
|
|
2392
2440
|
if (isPreHtmlElement || isPreIsElement)
|
|
2393
|
-
el = this.instantiateComponent(el, isPreHtmlElement, props);
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
// compare the arguments and then decide for itself whether it wants to re-render.
|
|
2398
|
-
else if (el.render) {
|
|
2399
|
-
//let oldHash = Globals.componentArgsHash.get(el);
|
|
2400
|
-
//if (oldHash !== newHash) { // Only if not changed.
|
|
2401
|
-
let args = {};
|
|
2441
|
+
[el, attribs, children] = this.instantiateComponent(el, isPreHtmlElement, props);
|
|
2442
|
+
if (doRender && el.render) {
|
|
2443
|
+
if (!attribs) {
|
|
2444
|
+
attribs = Util.attribsToObject(el);
|
|
2402
2445
|
for (let name in props || {})
|
|
2403
|
-
|
|
2404
|
-
el.
|
|
2405
|
-
|
|
2446
|
+
attribs[Util.dashesToCamel(name)] = props[name];
|
|
2447
|
+
children = el.childNodes;
|
|
2448
|
+
}
|
|
2449
|
+
el.render(attribs, children);
|
|
2406
2450
|
}
|
|
2407
|
-
|
|
2408
|
-
Globals$1.componentArgsHash.set(el, newHash);
|
|
2451
|
+
return el;
|
|
2409
2452
|
}
|
|
2410
|
-
|
|
2453
|
+
|
|
2411
2454
|
/**
|
|
2412
2455
|
* We swap the placeholder element for the real element so we can pass its dynamic attributes
|
|
2413
2456
|
* to its constructor.
|
|
2457
|
+
* This is only called by handleComponent()
|
|
2458
|
+
* This does not call render()
|
|
2414
2459
|
*
|
|
2415
|
-
*
|
|
2416
|
-
*
|
|
2417
|
-
* @param el
|
|
2460
|
+
* @param el {HTMLElement}
|
|
2418
2461
|
* @param isPreHtmlElement {?boolean} True if the element's tag name ends with -solarite-placeholder
|
|
2419
2462
|
* @param props {Object} Attributes with dynamic values.
|
|
2420
|
-
* @return {HTMLElement} */
|
|
2463
|
+
* @return {[HTMLElement, attribs:Object, children:Node[]]}} */
|
|
2421
2464
|
instantiateComponent(el, isPreHtmlElement=undefined, props=undefined) {
|
|
2422
2465
|
if (isPreHtmlElement === undefined)
|
|
2423
2466
|
isPreHtmlElement = !el.hasAttribute('_is');
|
|
@@ -2432,19 +2475,12 @@ class NodeGroup {
|
|
|
2432
2475
|
if (!Constructor)
|
|
2433
2476
|
throw new Error(`The custom tag name ${tagName} is not registered.`)
|
|
2434
2477
|
|
|
2435
|
-
|
|
2478
|
+
// Pass other attribs to constructor, since otherwise they're not yet set on the element,
|
|
2479
|
+
// and the constructor would otherwise have no way to see them.
|
|
2480
|
+
let attribs = Util.attribsToObject(el, 'solarite-placeholder');
|
|
2436
2481
|
for (let name in props || {})
|
|
2437
2482
|
attribs[Util.dashesToCamel(name)] = props[name];
|
|
2438
2483
|
|
|
2439
|
-
// Pass other attribs to constructor, since otherwise they're not yet set on the element,
|
|
2440
|
-
// and the constructor would otherwise have no way to see them.
|
|
2441
|
-
if (el.attributes.length) {
|
|
2442
|
-
for (let attrib of el.attributes) {
|
|
2443
|
-
let attribName = Util.dashesToCamel(attrib.name);
|
|
2444
|
-
if (!attribs.hasOwnProperty(attribName) && attribName !== 'solarite-placeholder')
|
|
2445
|
-
attribs[attribName] = attrib.value;
|
|
2446
|
-
}
|
|
2447
|
-
}
|
|
2448
2484
|
|
|
2449
2485
|
// Create the web component.
|
|
2450
2486
|
// Get the children that aren't Solarite's comment placeholders.
|
|
@@ -2475,7 +2511,7 @@ class NodeGroup {
|
|
|
2475
2511
|
if (this.endNode === el)
|
|
2476
2512
|
this.endNode = newEl;
|
|
2477
2513
|
|
|
2478
|
-
|
|
2514
|
+
// This is used only if inheriting from the Solarite class.
|
|
2479
2515
|
// applyComponentExprs() is called because we're rendering.
|
|
2480
2516
|
// So we want to render the sub-component also.
|
|
2481
2517
|
if (newEl.renderFirstTime)
|
|
@@ -2499,7 +2535,7 @@ class NodeGroup {
|
|
|
2499
2535
|
newEl.setAttribute(name, val);
|
|
2500
2536
|
}
|
|
2501
2537
|
|
|
2502
|
-
return newEl;
|
|
2538
|
+
return [newEl, attribs, children];
|
|
2503
2539
|
}
|
|
2504
2540
|
|
|
2505
2541
|
/**
|
|
@@ -2546,18 +2582,21 @@ class NodeGroup {
|
|
|
2546
2582
|
* Requires the nodeCache to be present. */
|
|
2547
2583
|
removeAndSaveOrphans() {
|
|
2548
2584
|
|
|
2549
|
-
let fragment =
|
|
2585
|
+
let fragment = Globals$1.doc.createDocumentFragment();
|
|
2550
2586
|
for (let node of this.getNodes())
|
|
2551
2587
|
fragment.append(node);
|
|
2552
2588
|
}
|
|
2553
2589
|
|
|
2554
2590
|
|
|
2555
|
-
|
|
2556
|
-
|
|
2591
|
+
/**
|
|
2592
|
+
* @param fragment {DocumentFragment}
|
|
2593
|
+
* @param paths
|
|
2594
|
+
* @param startingPathDepth {int} */
|
|
2595
|
+
updatePaths(fragment, paths, startingPathDepth) {
|
|
2557
2596
|
let pathLength = paths.length;
|
|
2558
2597
|
this.paths.length = pathLength;
|
|
2559
2598
|
for (let i=0; i<pathLength; i++) {
|
|
2560
|
-
let path = paths[i].clone(fragment,
|
|
2599
|
+
let path = paths[i].clone(fragment, startingPathDepth);
|
|
2561
2600
|
path.parentNg = this;
|
|
2562
2601
|
this.paths[i] = path;
|
|
2563
2602
|
}
|
|
@@ -2574,7 +2613,7 @@ class NodeGroup {
|
|
|
2574
2613
|
|
|
2575
2614
|
|
|
2576
2615
|
|
|
2577
|
-
findStaticComponents(root, shell,
|
|
2616
|
+
findStaticComponents(root, shell, startingPathDepth=0) {
|
|
2578
2617
|
let result = [];
|
|
2579
2618
|
|
|
2580
2619
|
// static components. These are WebComponents that do not have any constructor arguments that are expressions.
|
|
@@ -2582,8 +2621,8 @@ class NodeGroup {
|
|
|
2582
2621
|
// Maybe someday these two paths will be merged?
|
|
2583
2622
|
// Must happen before ids because instantiateComponent will replace the element.
|
|
2584
2623
|
for (let path of shell.staticComponents) {
|
|
2585
|
-
if (
|
|
2586
|
-
path = path.slice(0, -
|
|
2624
|
+
if (startingPathDepth)
|
|
2625
|
+
path = path.slice(0, -startingPathDepth);
|
|
2587
2626
|
let el = resolveNodePath(root, path);
|
|
2588
2627
|
|
|
2589
2628
|
// Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
|
|
@@ -2595,8 +2634,9 @@ class NodeGroup {
|
|
|
2595
2634
|
}
|
|
2596
2635
|
|
|
2597
2636
|
instantiateStaticComponents(staticComponents) {
|
|
2598
|
-
|
|
2599
|
-
|
|
2637
|
+
// TODO: Why do we not call render() on the static component here? The tests pass either way.
|
|
2638
|
+
for (let i in staticComponents)
|
|
2639
|
+
staticComponents[i] = this.handleComponent(staticComponents[i], null, false);
|
|
2600
2640
|
}
|
|
2601
2641
|
|
|
2602
2642
|
/**
|
|
@@ -2617,7 +2657,7 @@ class NodeGroup {
|
|
|
2617
2657
|
let el = resolveNodePath(root, path);
|
|
2618
2658
|
Util.bindId(rootEl, el);
|
|
2619
2659
|
}
|
|
2620
|
-
|
|
2660
|
+
}
|
|
2621
2661
|
|
|
2622
2662
|
// styles
|
|
2623
2663
|
if (options?.styles !== false) {
|
|
@@ -2647,9 +2687,8 @@ class NodeGroup {
|
|
|
2647
2687
|
}
|
|
2648
2688
|
}
|
|
2649
2689
|
}
|
|
2650
|
-
}
|
|
2651
|
-
|
|
2652
|
-
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2653
2692
|
class RootNodeGroup extends NodeGroup {
|
|
2654
2693
|
|
|
2655
2694
|
/**
|
|
@@ -2663,18 +2702,18 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2663
2702
|
exprsToRender = new Map();
|
|
2664
2703
|
|
|
2665
2704
|
/**
|
|
2666
|
-
*
|
|
2667
|
-
* @param
|
|
2668
|
-
* @param
|
|
2669
|
-
* @param options {?object}
|
|
2670
|
-
*/
|
|
2705
|
+
* @param template {Template}
|
|
2706
|
+
* @param el {?HTMLElement} Optional, pre-existing htmlElement tat will be the root.
|
|
2707
|
+
* @param options {?object} */
|
|
2671
2708
|
constructor(template, el, options) {
|
|
2672
2709
|
super(template);
|
|
2673
2710
|
|
|
2674
2711
|
this.options = options;
|
|
2675
2712
|
|
|
2676
|
-
|
|
2677
|
-
|
|
2713
|
+
let [fragment, shell] = this.populateFromTemplate(template);
|
|
2714
|
+
|
|
2715
|
+
let startingPathDepth = 0;
|
|
2716
|
+
|
|
2678
2717
|
|
|
2679
2718
|
if (fragment instanceof Text) {
|
|
2680
2719
|
|
|
@@ -2685,25 +2724,25 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2685
2724
|
el.append(fragment);
|
|
2686
2725
|
this.root = el;
|
|
2687
2726
|
}
|
|
2727
|
+
else
|
|
2728
|
+
throw new Error('Cannot create a standalone text node');
|
|
2688
2729
|
Globals$1.nodeGroups.set(this.root, this);
|
|
2689
2730
|
}
|
|
2731
|
+
|
|
2732
|
+
|
|
2690
2733
|
else {
|
|
2691
2734
|
|
|
2692
2735
|
// If adding NodeGroup to an element.
|
|
2693
|
-
let offset = 0;
|
|
2694
|
-
let root = fragment; // TODO: Rename so it's not confused with this.root.
|
|
2695
2736
|
if (el) {
|
|
2696
|
-
|
|
2737
|
+
this.root = el;
|
|
2697
2738
|
|
|
2698
2739
|
// Save slot children
|
|
2699
2740
|
let slotChildren;
|
|
2700
2741
|
if (el.childNodes.length) {
|
|
2701
|
-
slotChildren =
|
|
2742
|
+
slotChildren = Globals$1.doc.createDocumentFragment();
|
|
2702
2743
|
slotChildren.append(...el.childNodes);
|
|
2703
2744
|
}
|
|
2704
2745
|
|
|
2705
|
-
this.root = el;
|
|
2706
|
-
|
|
2707
2746
|
// If el should replace the root node of the fragment.
|
|
2708
2747
|
if (isReplaceEl(fragment, el)) {
|
|
2709
2748
|
el.append(...fragment.children[0].childNodes);
|
|
@@ -2714,8 +2753,10 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2714
2753
|
el.setAttribute(attrib.name, attrib.value);
|
|
2715
2754
|
|
|
2716
2755
|
// Go one level deeper into all of shell's paths.
|
|
2717
|
-
|
|
2718
|
-
}
|
|
2756
|
+
startingPathDepth = 1;
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
else {
|
|
2719
2760
|
let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
|
|
2720
2761
|
if (!isEmpty)
|
|
2721
2762
|
el.append(...fragment.childNodes);
|
|
@@ -2743,35 +2784,35 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2743
2784
|
el.append(slotChildren);
|
|
2744
2785
|
}
|
|
2745
2786
|
|
|
2746
|
-
root = el;
|
|
2747
|
-
|
|
2748
2787
|
this.startNode = el;
|
|
2749
2788
|
this.endNode = el;
|
|
2750
|
-
}
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
// Instantiate as a standalone element.
|
|
2792
|
+
else {
|
|
2751
2793
|
let singleEl = getSingleEl(fragment);
|
|
2752
|
-
this.root = singleEl || fragment; // We return the whole fragment when calling
|
|
2794
|
+
this.root = singleEl || fragment; // We return the whole fragment when calling h() with a collection of nodes.
|
|
2753
2795
|
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
root = singleEl;
|
|
2757
|
-
offset = 1;
|
|
2758
|
-
}
|
|
2796
|
+
if (singleEl)
|
|
2797
|
+
startingPathDepth = 1;
|
|
2759
2798
|
}
|
|
2760
|
-
|
|
2761
|
-
this.updatePaths(root, shell.paths,
|
|
2799
|
+
Globals$1.nodeGroups.set(this.root, this);
|
|
2800
|
+
this.updatePaths(this.root, shell.paths, startingPathDepth);
|
|
2762
2801
|
|
|
2763
2802
|
// Static web components can sometimes have children created via expressions.
|
|
2764
2803
|
// But calling applyExprs() will mess up the shell's path to them.
|
|
2765
2804
|
// So we find them first, then call activateStaticComponents() after their children have been created.
|
|
2766
|
-
|
|
2805
|
+
this.staticComponents = this.findStaticComponents(this.root, shell, startingPathDepth);
|
|
2767
2806
|
|
|
2768
|
-
this.activateEmbeds(root, shell,
|
|
2807
|
+
this.activateEmbeds(this.root, shell, startingPathDepth);
|
|
2769
2808
|
|
|
2770
2809
|
// Apply exprs
|
|
2771
2810
|
this.applyExprs(template.exprs);
|
|
2772
2811
|
|
|
2773
|
-
this.instantiateStaticComponents(staticComponents);
|
|
2812
|
+
this.instantiateStaticComponents(this.staticComponents);
|
|
2774
2813
|
}
|
|
2814
|
+
|
|
2815
|
+
|
|
2775
2816
|
}
|
|
2776
2817
|
}
|
|
2777
2818
|
|
|
@@ -2813,8 +2854,7 @@ class Template {
|
|
|
2813
2854
|
/** @type {Array} Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
|
|
2814
2855
|
hashedFields;
|
|
2815
2856
|
|
|
2816
|
-
|
|
2817
|
-
nodeGroup;
|
|
2857
|
+
isText;
|
|
2818
2858
|
|
|
2819
2859
|
/**
|
|
2820
2860
|
*
|
|
@@ -2910,169 +2950,203 @@ class Template {
|
|
|
2910
2950
|
|
|
2911
2951
|
return this.closeKey;
|
|
2912
2952
|
}
|
|
2913
|
-
}
|
|
2914
2953
|
|
|
2954
|
+
/**
|
|
2955
|
+
* @param tag {string}
|
|
2956
|
+
* @param props {?Record<string, any>}
|
|
2957
|
+
* @param children
|
|
2958
|
+
* @returns {Template} */
|
|
2959
|
+
static fromJsx(tag, props, children) {
|
|
2915
2960
|
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
* @property {boolean=} styles - Replace :host in style tags to scope them locally.
|
|
2919
|
-
* @property {boolean=} scripts - Execute script tags.
|
|
2920
|
-
* @property {boolean=} ids - Create references to elements with id or data-id attributes.
|
|
2921
|
-
* @property {?boolean} render - Deprecated.
|
|
2922
|
-
* Used only when options are given to a class super constructor inheriting from Solarite.
|
|
2923
|
-
* True to call render() immediately in super constructor.
|
|
2924
|
-
* False to automatically call render() at all.
|
|
2925
|
-
* Undefined (default) to call render() when added to the DOM, unless already rendered.
|
|
2926
|
-
*/
|
|
2927
|
-
|
|
2928
|
-
/**
|
|
2929
|
-
* Convert strings to HTMLNodes.
|
|
2930
|
-
* Using h`...` as a tag will always create a Template.
|
|
2931
|
-
* Using h() as a function() will always create a DOM element.
|
|
2932
|
-
*
|
|
2933
|
-
* Features beyond what standard js tagged template strings do:
|
|
2934
|
-
* 1. r`` sub-expressions
|
|
2935
|
-
* 2. functions, nodes, and arrays of nodes as sub-expressions.
|
|
2936
|
-
* 3. html-escape all expressions by default, unless wrapped in r()
|
|
2937
|
-
* 4. event binding
|
|
2938
|
-
* 5. TODO: list more
|
|
2939
|
-
*
|
|
2940
|
-
* Currently supported:
|
|
2941
|
-
* 1. h(el, options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
|
|
2942
|
-
* 2. h(el, template, ?options) // Render the Template created by #1 to element.
|
|
2943
|
-
*
|
|
2944
|
-
* 3. h`<b>Hello</b> ${'World'}!` // Create Template that can later be used to create nodes.
|
|
2945
|
-
*
|
|
2946
|
-
* 4. h('Hello'); // Create single text node.
|
|
2947
|
-
* 5. h('<b>Hello</b>'); // Create single HTMLElement
|
|
2948
|
-
* 6. h('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
|
|
2949
|
-
* 7. h()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which
|
|
2950
|
-
* // includes properly handling nested components and r`` sub-expressions.
|
|
2951
|
-
* 8. h(template) // Render Template created by #1.
|
|
2952
|
-
*
|
|
2953
|
-
* 9. h({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
|
|
2954
|
-
* 10. h(string, object, ...) // JSX TODO
|
|
2955
|
-
* @param htmlStrings {?HTMLElement|string|string[]|function():Template|{render:function()}}
|
|
2956
|
-
* @param exprs {*[]|string|Template|Object}
|
|
2957
|
-
* @return {Node|HTMLElement|Template} */
|
|
2958
|
-
function h(htmlStrings=undefined, ...exprs) {
|
|
2961
|
+
// HTML void elements that must not have closing tags
|
|
2962
|
+
const isVoid = selfClosingTags.has(tag.toLowerCase());
|
|
2959
2963
|
|
|
2960
|
-
|
|
2961
|
-
|
|
2964
|
+
// Build htmlStrings/exprs so Shell can place placeholders in attribute values and child content.
|
|
2965
|
+
let htmlStrings = [];
|
|
2966
|
+
let templateExprs = [];
|
|
2962
2967
|
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
let parent = htmlStrings, template = exprs[0];
|
|
2968
|
+
// Opening tag
|
|
2969
|
+
let open = `<${tag}`;
|
|
2966
2970
|
|
|
2967
|
-
//
|
|
2968
|
-
if (
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
+
// Attributes
|
|
2972
|
+
if (props && typeof props === 'object') {
|
|
2973
|
+
for (let name in props) {
|
|
2974
|
+
let value = props[name];
|
|
2971
2975
|
|
|
2972
|
-
|
|
2976
|
+
// id and data-id are static in templates — never expressions
|
|
2977
|
+
if (name === 'id' || name === 'data-id') {
|
|
2978
|
+
// Write directly into the opening string with quotes
|
|
2979
|
+
open += ` ${name}="${value}"`;
|
|
2980
|
+
continue;
|
|
2981
|
+
}
|
|
2973
2982
|
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2983
|
+
// Dynamic attribute value: functions are unquoted (e.g., onclick=${fn}), others quoted
|
|
2984
|
+
if (typeof value === 'function') {
|
|
2985
|
+
open += ` ${name}=`;
|
|
2986
|
+
htmlStrings.push(open);
|
|
2987
|
+
templateExprs.push(value);
|
|
2988
|
+
// reset so subsequent attributes start fresh (e.g., ' title=')
|
|
2989
|
+
open = ``;
|
|
2990
|
+
}
|
|
2991
|
+
else {
|
|
2992
|
+
open += ` ${name}=`;
|
|
2993
|
+
htmlStrings.push(open);
|
|
2994
|
+
templateExprs.push(value);
|
|
2995
|
+
// reset so subsequent attributes start fresh (e.g., ' title=')
|
|
2996
|
+
open = ``;
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2981
2999
|
}
|
|
2982
3000
|
|
|
2983
|
-
//
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
//
|
|
2991
|
-
|
|
2992
|
-
|
|
3001
|
+
// Finalize opening tag precisely to match tagged template splitting
|
|
3002
|
+
if (!isVoid) {
|
|
3003
|
+
const pushedAny = htmlStrings.length > 0;
|
|
3004
|
+
// If nothing pushed yet (no dynamic attrs), push the entire open + '>'
|
|
3005
|
+
if (!pushedAny)
|
|
3006
|
+
htmlStrings.push(open + '>');
|
|
3007
|
+
else {
|
|
3008
|
+
// If we were in a quoted attr (open === '"'), then the string after expr is '">' ;
|
|
3009
|
+
// Otherwise (function-valued attr), the string after expr is just '>'
|
|
3010
|
+
htmlStrings.push(open === '"' ? '">' : '>');
|
|
2993
3011
|
}
|
|
3012
|
+
|
|
3013
|
+
for (let child of children)
|
|
3014
|
+
addChild(child, htmlStrings, templateExprs);
|
|
2994
3015
|
}
|
|
2995
|
-
}
|
|
2996
3016
|
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3017
|
+
// Closing tag (not for void tags)
|
|
3018
|
+
if (!isVoid) {
|
|
3019
|
+
// If we never emitted the '>' for the open tag (no children were added),
|
|
3020
|
+
// then it was appended above before children. Now just add the closing tag to the last html segment.
|
|
3021
|
+
let lastIdx = htmlStrings.length - 1;
|
|
3022
|
+
htmlStrings[lastIdx] += `</${tag}>`;
|
|
3023
|
+
}
|
|
3024
|
+
else {
|
|
3025
|
+
// Void element: ensure we emitted a trailing '>' segment
|
|
3026
|
+
const pushedAny = htmlStrings.length > 0;
|
|
3027
|
+
if (!pushedAny)
|
|
3028
|
+
htmlStrings.push(open + '>');
|
|
3029
|
+
else
|
|
3030
|
+
htmlStrings.push('>');
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
// Ensure invariant
|
|
3034
|
+
//assert(htmlStrings.length === templateExprs.length + 1);
|
|
3035
|
+
//console.log([htmlStrings, templateExprs])
|
|
3036
|
+
return new Template(htmlStrings, templateExprs);
|
|
3000
3037
|
}
|
|
3038
|
+
}
|
|
3001
3039
|
|
|
3002
|
-
else if (typeof htmlStrings === 'string' || htmlStrings instanceof String) {
|
|
3003
|
-
// 10. JSX
|
|
3004
|
-
if (typeof exprs[0] === 'object') {
|
|
3005
|
-
exprs[0] || {};
|
|
3006
|
-
exprs.slice(1);
|
|
3007
3040
|
|
|
3008
|
-
|
|
3009
|
-
let templateExprs = [];
|
|
3041
|
+
const selfClosingTags = new Set(['area','base','br','col','embed','hr','img','input','link','meta','param','source','track','wbr']);
|
|
3010
3042
|
|
|
3011
|
-
// TODO How to know which children are static html and which are expression placeholders?
|
|
3012
|
-
// Perhaps we have to treat every text child as a string?
|
|
3013
3043
|
|
|
3014
|
-
|
|
3015
|
-
|
|
3044
|
+
/**
|
|
3045
|
+
* Add child Templates that were already created via h() and Template.fromJsx()
|
|
3046
|
+
* @param template {Template}
|
|
3047
|
+
* @param html {string[]}
|
|
3048
|
+
* @param exprs {any[]} */
|
|
3049
|
+
const addChild = (template, html, exprs) => {
|
|
3050
|
+
|
|
3051
|
+
if (Array.isArray(template)) {
|
|
3052
|
+
for (let c of template)
|
|
3053
|
+
addChild(c, html, exprs);
|
|
3054
|
+
}
|
|
3055
|
+
else {
|
|
3056
|
+
let flatten = false;
|
|
3057
|
+
if (template instanceof Template) {
|
|
3058
|
+
// Heuristic to match tagged-template splitting:
|
|
3059
|
+
// - Flatten if the child has expressions (so JSX can inline attribute/value placeholders like tagged literals would).
|
|
3060
|
+
// - Also flatten void elements (e.g., <img>) so they inline like literals.
|
|
3061
|
+
// - Otherwise, keep as a dynamic child placeholder to match cases where the tagged template used an expression child.
|
|
3062
|
+
const childHasExprs = template.exprs.length > 0;
|
|
3063
|
+
if (childHasExprs)
|
|
3064
|
+
flatten = true;
|
|
3065
|
+
else {
|
|
3066
|
+
const m = (template.html[0] || '').match(/^<([a-zA-Z][\w:-]*)/);
|
|
3067
|
+
const childTag = m ? m[1].toLowerCase() : '';
|
|
3068
|
+
flatten = selfClosingTags.has(childTag);
|
|
3069
|
+
}
|
|
3016
3070
|
}
|
|
3017
3071
|
|
|
3072
|
+
if (flatten) {
|
|
3073
|
+
// Flatten/interleave into current segment to match tagged template splitting
|
|
3074
|
+
html[html.length - 1] += template.html[0];
|
|
3075
|
+
for (let i = 0; i < template.exprs.length; i++) {
|
|
3076
|
+
exprs.push(template.exprs[i]);
|
|
3077
|
+
html.push(template.html[i + 1] ?? '');
|
|
3078
|
+
}
|
|
3079
|
+
} else {
|
|
3080
|
+
// Keep as dynamic child
|
|
3081
|
+
exprs.push(template);
|
|
3082
|
+
html.push('');
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
};
|
|
3018
3086
|
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3087
|
+
|
|
3088
|
+
/**
|
|
3089
|
+
* @typedef {Object} RenderOptions
|
|
3090
|
+
* @property {boolean=} styles - Replace :host in style tags to scope them locally.
|
|
3091
|
+
* @property {boolean=} scripts - Execute script tags.
|
|
3092
|
+
* @property {boolean=} ids - Create references to elements with id or data-id attributes.
|
|
3093
|
+
* @property {?boolean} render - Deprecated.
|
|
3094
|
+
* Used only when options are given to a class super constructor inheriting from Solarite.
|
|
3095
|
+
* True to call render() immediately in super constructor.
|
|
3096
|
+
* False to automatically call render() at all.
|
|
3097
|
+
* Undefined (default) to call render() when added to the DOM, unless already rendered.
|
|
3098
|
+
*/
|
|
3099
|
+
|
|
3100
|
+
/**
|
|
3101
|
+
* Convert a template, string, or object into a DOM Node or Element
|
|
3102
|
+
*
|
|
3103
|
+
* 1. h('Hello'); // Create single text node.
|
|
3104
|
+
* 2. h('<b>Hello</b>'); // Create single HTMLElement
|
|
3105
|
+
* 3. h('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
|
|
3106
|
+
* 4. h(template) // Render Template created by h`<html>` or h();
|
|
3107
|
+
* 5. h({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
|
|
3108
|
+
* @param arg {string|Template|{render:()=>void}}
|
|
3109
|
+
* @returns {Node|DocumentFragment|HTMLElement} */
|
|
3110
|
+
function toEl(arg) {
|
|
3111
|
+
|
|
3112
|
+
if (typeof arg === 'string') {
|
|
3113
|
+
let html = arg;
|
|
3114
|
+
|
|
3115
|
+
// If it's an element with whitespace before or after it, trim both ends.
|
|
3116
|
+
if (html.match(/^\s^</) || html.match(/>\s+$/))
|
|
3117
|
+
html = html.trim();
|
|
3023
3118
|
|
|
3024
3119
|
// We create a new one each time because otherwise
|
|
3025
3120
|
// the returned fragment will have its content replaced by a subsequent call.
|
|
3026
|
-
let templateEl =
|
|
3027
|
-
templateEl.innerHTML =
|
|
3121
|
+
let templateEl = Globals$1.doc.createElement('template');
|
|
3122
|
+
templateEl.innerHTML = html;
|
|
3028
3123
|
|
|
3029
|
-
//
|
|
3124
|
+
// 1+2. Return Node if there's one child.
|
|
3030
3125
|
let relevantNodes = Util.trimEmptyNodes(templateEl.content.childNodes);
|
|
3031
3126
|
if (relevantNodes.length === 1)
|
|
3032
3127
|
return relevantNodes[0];
|
|
3033
3128
|
|
|
3034
|
-
//
|
|
3129
|
+
// 3. Otherwise return DocumentFragment.
|
|
3035
3130
|
return templateEl.content;
|
|
3036
3131
|
}
|
|
3037
3132
|
|
|
3038
|
-
//
|
|
3039
|
-
|
|
3040
|
-
return (
|
|
3041
|
-
//Globals.rendered.add(parent)
|
|
3042
|
-
let template = h(htmlStrings, ...exprs);
|
|
3043
|
-
return template.render();
|
|
3044
|
-
}
|
|
3045
|
-
}
|
|
3046
|
-
|
|
3047
|
-
// 8.
|
|
3048
|
-
else if (htmlStrings instanceof Template) {
|
|
3049
|
-
return htmlStrings.render();
|
|
3133
|
+
// 4.
|
|
3134
|
+
if (arg instanceof Template) {
|
|
3135
|
+
return arg.render();
|
|
3050
3136
|
}
|
|
3051
3137
|
|
|
3052
|
-
|
|
3053
|
-
// 9. Create dynamic element with render() function.
|
|
3138
|
+
// 5. Create dynamic element from an object with a render() function.
|
|
3054
3139
|
// TODO: This path doesn't handle embeds like data-id="..."
|
|
3055
|
-
else if (typeof
|
|
3056
|
-
let obj =
|
|
3140
|
+
else if (arg && typeof arg === 'object') {
|
|
3141
|
+
let obj = arg;
|
|
3057
3142
|
|
|
3058
|
-
if (obj.constructor.name !== 'Object')
|
|
3143
|
+
if (obj.constructor.name !== 'Object')
|
|
3059
3144
|
throw new Error(`Solarate Web Component class ${obj.constructor?.name} must extend HTMLElement.`);
|
|
3060
3145
|
|
|
3061
|
-
|
|
3062
|
-
// Special rebound render path, called by normal path.
|
|
3063
|
-
// Intercepts the main r`...` function call inside render().
|
|
3064
|
-
if (Globals$1.objToEl.has(obj)) {
|
|
3065
|
-
return function(...args) {
|
|
3066
|
-
let template = h(...args);
|
|
3067
|
-
let el = template.render();
|
|
3068
|
-
Globals$1.objToEl.set(obj, el);
|
|
3069
|
-
}.bind(obj);
|
|
3070
|
-
}
|
|
3071
|
-
|
|
3072
3146
|
// Normal path
|
|
3073
|
-
|
|
3147
|
+
if (!Globals$1.objToEl.has(obj)) {
|
|
3074
3148
|
Globals$1.objToEl.set(obj, null);
|
|
3075
|
-
obj[renderF](); // Calls the Special rebound render path above, when the render function calls
|
|
3149
|
+
obj[renderF](); // Calls the Special rebound render path above, when the render function calls h(this)
|
|
3076
3150
|
let el = Globals$1.objToEl.get(obj);
|
|
3077
3151
|
Globals$1.objToEl.delete(obj);
|
|
3078
3152
|
|
|
@@ -3080,7 +3154,7 @@ function h(htmlStrings=undefined, ...exprs) {
|
|
|
3080
3154
|
if (typeof obj[name] === 'function')
|
|
3081
3155
|
el[name] = obj[name].bind(el); // Make the "this" of functions be el.
|
|
3082
3156
|
// TODO: But this doesn't work for passing an object with functions as a constructor arg via an attribute:
|
|
3083
|
-
|
|
3157
|
+
// <my-element arg=${{myFunc() { return this }}}
|
|
3084
3158
|
else
|
|
3085
3159
|
el[name] = obj[name];
|
|
3086
3160
|
|
|
@@ -3096,13 +3170,145 @@ function h(htmlStrings=undefined, ...exprs) {
|
|
|
3096
3170
|
}
|
|
3097
3171
|
}
|
|
3098
3172
|
|
|
3099
|
-
|
|
3100
|
-
|
|
3173
|
+
throw new Error('toEl() does not support argument of type: ' + (arg ? typeof arg : arg));
|
|
3174
|
+
|
|
3101
3175
|
}
|
|
3102
3176
|
|
|
3177
|
+
|
|
3103
3178
|
// Trick to prevent minifier from renaming this function.
|
|
3104
3179
|
let renderF = 'render';
|
|
3105
3180
|
|
|
3181
|
+
/**
|
|
3182
|
+
* Convert strings to HTMLNodes.
|
|
3183
|
+
* Using h`...` as a tag will always create a Template.
|
|
3184
|
+
* Using h() as a function() will always create a DOM element.
|
|
3185
|
+
*
|
|
3186
|
+
* Features beyond what standard js tagged template strings do:
|
|
3187
|
+
* 1. r`` sub-expressions
|
|
3188
|
+
* 2. functions, nodes, and arrays of nodes as sub-expressions.
|
|
3189
|
+
* 3. html-escape all expressions by default, unless wrapped in h()
|
|
3190
|
+
* 4. event binding
|
|
3191
|
+
* 5. TODO: list more
|
|
3192
|
+
*
|
|
3193
|
+
* General rule:
|
|
3194
|
+
* If h() is a function with null or an HTMLElement as its first argument create a Node.
|
|
3195
|
+
* Otherwise create a template
|
|
3196
|
+
*
|
|
3197
|
+
* Currently supported:
|
|
3198
|
+
*
|
|
3199
|
+
* Create Tempataes
|
|
3200
|
+
* 1. h`<b>Hello</b> ${'World'}!` // Create Template that can later be used to create nodes.
|
|
3201
|
+
* 2. h('<b>Hello</b><u>Goodbye</u>'); // Create Template from string, that can later be used to create nodes.
|
|
3202
|
+
*
|
|
3203
|
+
* Add children to an element.
|
|
3204
|
+
* 3. h(el, h`<b>${'Hi'}</b>`, ?options)
|
|
3205
|
+
* 4. h(el, ?options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
|
|
3206
|
+
*
|
|
3207
|
+
* Create top-level element
|
|
3208
|
+
* 5. h()`Hello<b>${'World'}!</b>`
|
|
3209
|
+
*
|
|
3210
|
+
* 6. h(string, object, ...) // Used for JSX
|
|
3211
|
+
* @param htmlStrings {?HTMLElement|string|string[]|function():Template|{render:function()}}
|
|
3212
|
+
* @param exprs {*[]|string|Template|Object}
|
|
3213
|
+
* @return {Node|HTMLElement|Template} */
|
|
3214
|
+
function h(htmlStrings=undefined, ...exprs) {
|
|
3215
|
+
|
|
3216
|
+
// 1. Tagged template
|
|
3217
|
+
if (Array.isArray(arguments[0])) {
|
|
3218
|
+
return new Template(arguments[0], exprs);
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
// 2. String to template, or JSX factory form h(tag, props, ...children)
|
|
3222
|
+
else if (typeof arguments[0] === 'string' || arguments[0] instanceof String) {
|
|
3223
|
+
let tagOrHtml = arguments[0];
|
|
3224
|
+
|
|
3225
|
+
// 2a. JSX: h("tag", {props}, ...children)
|
|
3226
|
+
if (exprs.length && (typeof exprs[0] === 'object' || exprs[0] === null)) {
|
|
3227
|
+
let tag = tagOrHtml + '';
|
|
3228
|
+
let props = exprs[0] || {};
|
|
3229
|
+
let children = exprs.slice(1);
|
|
3230
|
+
|
|
3231
|
+
return Template.fromJsx(tag, props, children);
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
// 2b. Plain html string => template
|
|
3235
|
+
else {
|
|
3236
|
+
let html = tagOrHtml;
|
|
3237
|
+
// If it starts with whitespace, trim both ends.
|
|
3238
|
+
// TODO: Also trim if it ends with whitespace?
|
|
3239
|
+
if (html.match(/^\s^</))
|
|
3240
|
+
html = html.trim();
|
|
3241
|
+
return new Template([html], []);
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
else if (arguments[0] instanceof HTMLElement || arguments[0] instanceof DocumentFragment) {
|
|
3246
|
+
|
|
3247
|
+
// 3. Render template to element.
|
|
3248
|
+
if (arguments[1] instanceof Template) {
|
|
3249
|
+
|
|
3250
|
+
/** @type Template */
|
|
3251
|
+
let template = arguments[1];
|
|
3252
|
+
let parent = arguments[0];
|
|
3253
|
+
let options = arguments[2]; // deprecated?
|
|
3254
|
+
template.render(parent, options);
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
// 4. Render tagged template to element
|
|
3258
|
+
else {
|
|
3259
|
+
let parent = arguments[0], options = arguments[1];
|
|
3260
|
+
|
|
3261
|
+
// Remove shadowroot. TODO: This could mess up paths?
|
|
3262
|
+
if (parent.shadowRoot)
|
|
3263
|
+
parent.innerHTML = '';
|
|
3264
|
+
|
|
3265
|
+
// Return a tagged template function that applies the tagged template to parent.
|
|
3266
|
+
let taggedTemplate = (htmlStrings, ...exprs) => {
|
|
3267
|
+
Globals$1.rendered.add(parent);
|
|
3268
|
+
let template = new Template(htmlStrings, exprs);
|
|
3269
|
+
return template.render(parent, options);
|
|
3270
|
+
};
|
|
3271
|
+
return taggedTemplate;
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
// 5. Create a static element h()'<div></div>' (Deprecated?)
|
|
3276
|
+
else if (!arguments.length) {
|
|
3277
|
+
return (htmlStrings, ...exprs) => {
|
|
3278
|
+
let template = h(htmlStrings, ...exprs);
|
|
3279
|
+
return toEl(template); // Go to path 6.
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
|
|
3283
|
+
// 6. Help toEl() with objects.
|
|
3284
|
+
// Special rebound render path, called by normal path.
|
|
3285
|
+
// Intercepts the main h(this)`...` function call inside render().
|
|
3286
|
+
// TODO: This path doesn't handle embeds like data-id="..."
|
|
3287
|
+
else if (typeof arguments[0] === 'object' && Globals$1.objToEl.has(arguments[0])) {
|
|
3288
|
+
let obj = arguments[0];
|
|
3289
|
+
|
|
3290
|
+
if (obj.constructor.name !== 'Object')
|
|
3291
|
+
throw new Error(`Solarate Web Component class ${obj.constructor?.name} must extend HTMLElement.`);
|
|
3292
|
+
|
|
3293
|
+
// Jsx with h(this, <jsx>)
|
|
3294
|
+
if (arguments[1] instanceof Template) {
|
|
3295
|
+
let template = arguments[1];
|
|
3296
|
+
let el = template.render();
|
|
3297
|
+
Globals$1.objToEl.set(obj, el);
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
// h(this)`<div>...</div>
|
|
3301
|
+
else
|
|
3302
|
+
return function(...args) {
|
|
3303
|
+
let template = h(...args);
|
|
3304
|
+
let el = template.render();
|
|
3305
|
+
Globals$1.objToEl.set(obj, el);
|
|
3306
|
+
}.bind(obj);
|
|
3307
|
+
}
|
|
3308
|
+
else
|
|
3309
|
+
throw new Error('h() does not support argument of type: ' + (arguments[0] ? typeof arguments[0] : arguments[0]))
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3106
3312
|
/**
|
|
3107
3313
|
* There are three ways to create an instance of a Solarite Component:
|
|
3108
3314
|
* 1. new ComponentName(); // direct class instantiation
|
|
@@ -3276,7 +3482,7 @@ function createSolarite(extendsTag=null) {
|
|
|
3276
3482
|
|
|
3277
3483
|
BaseClass = Globals$1.elementClasses[extendsTag];
|
|
3278
3484
|
if (!BaseClass) { // TODO: Use Cache
|
|
3279
|
-
BaseClass =
|
|
3485
|
+
BaseClass = Globals$1.doc.createElement(extendsTag).constructor;
|
|
3280
3486
|
Globals$1.elementClasses[extendsTag] = BaseClass;
|
|
3281
3487
|
}
|
|
3282
3488
|
}
|
|
@@ -3337,7 +3543,7 @@ function createSolarite(extendsTag=null) {
|
|
|
3337
3543
|
this.innerHTML = html;
|
|
3338
3544
|
}
|
|
3339
3545
|
else
|
|
3340
|
-
this.modifications =
|
|
3546
|
+
this.modifications = h(this, html, options);
|
|
3341
3547
|
}
|
|
3342
3548
|
})*/
|
|
3343
3549
|
|
|
@@ -3387,11 +3593,14 @@ function createSolarite(extendsTag=null) {
|
|
|
3387
3593
|
let define = 'define';
|
|
3388
3594
|
let getName = 'getName';
|
|
3389
3595
|
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3596
|
+
/*
|
|
3597
|
+
┏┓ ┓ •
|
|
3598
|
+
┗┓┏┓┃┏┓┏┓┓╋▗▖
|
|
3599
|
+
┗┛┗┛┗┗┻╹ ╹╹┗
|
|
3600
|
+
JavasCript UI library
|
|
3601
|
+
@license MIT
|
|
3602
|
+
@copyright Vorticode LLC
|
|
3603
|
+
https://vorticode.github.io/solarite/ */
|
|
3395
3604
|
|
|
3396
3605
|
/**
|
|
3397
3606
|
* TODO: The Proxy and the multiple base classes mess up 'instanceof Solarite'
|
|
@@ -3405,4 +3614,4 @@ const Solarite = new Proxy(createSolarite(), {
|
|
|
3405
3614
|
//export {default as watch, renderWatched} from './watch.js'; // unfinished
|
|
3406
3615
|
|
|
3407
3616
|
export default h;
|
|
3408
|
-
export { ArgType, Globals$1 as Globals, Solarite, Util as SolariteUtil, Template,
|
|
3617
|
+
export { ArgType, Globals$1 as Globals, Solarite, Util as SolariteUtil, Template, getArg, h, h as r, setArgs, toEl };
|