solarite 0.3.1 → 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 +597 -420
- package/dist/Solarite.js +593 -373
- package/dist/Solarite.min.js +11 -2
- package/package.json +2 -2
- package/readme.md +2 -2
- package/src/ExprPath.js +110 -91
- package/src/Globals.js +9 -5
- package/src/NodeGroup.js +82 -245
- 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
|
|
|
@@ -994,22 +1016,18 @@ class ExprPath {
|
|
|
994
1016
|
if (!ng.startNode.parentNode)
|
|
995
1017
|
ng.removeAndSaveOrphans();
|
|
996
1018
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
1019
|
// Instantiate components created within ${...} expressions.
|
|
1001
|
-
//
|
|
1020
|
+
// Also see this.applyExactNodes() which handles calling render() on web components even if they are unchanged.
|
|
1002
1021
|
for (let el of newNodes) {
|
|
1003
|
-
if (el
|
|
1022
|
+
if (el?.nodeType === 1) { // HTMLElement
|
|
1004
1023
|
if (el.hasAttribute('solarite-placeholder'))
|
|
1005
|
-
this.parentNg.
|
|
1024
|
+
this.parentNg.handleComponent(el, null, true);
|
|
1006
1025
|
for (let child of el.querySelectorAll('[solarite-placeholder]'))
|
|
1007
|
-
this.parentNg.
|
|
1026
|
+
this.parentNg.handleComponent(child, null, true);
|
|
1008
1027
|
}
|
|
1009
1028
|
}
|
|
1010
1029
|
}
|
|
1011
1030
|
|
|
1012
|
-
|
|
1013
1031
|
|
|
1014
1032
|
}
|
|
1015
1033
|
|
|
@@ -1117,7 +1135,7 @@ class ExprPath {
|
|
|
1117
1135
|
}
|
|
1118
1136
|
|
|
1119
1137
|
// String/Number/Date/Boolean
|
|
1120
|
-
else if (!(expr instanceof Template) && !(expr
|
|
1138
|
+
else if (!(expr instanceof Template) && !(expr?.nodeType)){
|
|
1121
1139
|
// Convert expression to a string.
|
|
1122
1140
|
if (expr === undefined || expr === false || expr === null) // Util.isFalsy() inlined
|
|
1123
1141
|
expr = '';
|
|
@@ -1127,7 +1145,9 @@ class ExprPath {
|
|
|
1127
1145
|
// Get the same Template for the same string each time.
|
|
1128
1146
|
// let template = Globals.stringTemplates[expr];
|
|
1129
1147
|
// if (!template) {
|
|
1148
|
+
|
|
1130
1149
|
let template = new Template([expr], []);
|
|
1150
|
+
template.isText = true;
|
|
1131
1151
|
// Globals.stringTemplates[expr] = template;
|
|
1132
1152
|
//}
|
|
1133
1153
|
|
|
@@ -1152,12 +1172,44 @@ class ExprPath {
|
|
|
1152
1172
|
|
|
1153
1173
|
if (expr instanceof Template) {
|
|
1154
1174
|
let ng = this.getNodeGroup(expr, true);
|
|
1175
|
+
|
|
1155
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
|
+
}
|
|
1156
1205
|
|
|
1157
|
-
//
|
|
1158
|
-
|
|
1159
|
-
|
|
1206
|
+
// This calls render() on web components that have expressions as attributes.
|
|
1207
|
+
if (apply)
|
|
1208
|
+
ng.applyExprs(expr.exprs);
|
|
1209
|
+
|
|
1160
1210
|
this.nodeGroups.push(ng);
|
|
1211
|
+
|
|
1212
|
+
return ng;
|
|
1161
1213
|
}
|
|
1162
1214
|
|
|
1163
1215
|
// If expression, mark it to be evaluated later in ExprPath.apply() to find partial match.
|
|
@@ -1169,10 +1221,10 @@ class ExprPath {
|
|
|
1169
1221
|
}
|
|
1170
1222
|
|
|
1171
1223
|
// Node(s) created by an expression.
|
|
1172
|
-
else if (expr
|
|
1224
|
+
else if (expr?.nodeType) {
|
|
1173
1225
|
|
|
1174
1226
|
// DocumentFragment created by an expression.
|
|
1175
|
-
if (expr
|
|
1227
|
+
if (expr?.nodeType === 11) // DocumentFragment
|
|
1176
1228
|
newNodes.push(...expr.childNodes);
|
|
1177
1229
|
else
|
|
1178
1230
|
newNodes.push(expr);
|
|
@@ -1204,16 +1256,30 @@ class ExprPath {
|
|
|
1204
1256
|
Globals$1.currentExprPath = null;
|
|
1205
1257
|
}
|
|
1206
1258
|
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1259
|
+
// Attribute as name: value object.
|
|
1260
|
+
if (typeof expr === 'object') {
|
|
1261
|
+
for (let name in expr) {
|
|
1262
|
+
let value = expr[name];
|
|
1263
|
+
if (value === undefined || value === false || value === null)
|
|
1264
|
+
continue;
|
|
1265
|
+
node.setAttribute(name, value);
|
|
1266
|
+
this.attrNames.add(name);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1211
1269
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1270
|
+
// Attributes as string
|
|
1271
|
+
else {
|
|
1272
|
+
let attrs = (expr + '') // Split string into multiple attributes.
|
|
1273
|
+
.split(/([\w-]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s]+))/g)
|
|
1274
|
+
.map(text => text.trim())
|
|
1275
|
+
.filter(text => text.length);
|
|
1276
|
+
|
|
1277
|
+
for (let attr of attrs) {
|
|
1278
|
+
let [name, value] = attr.split(/\s*=\s*/); // split on first equals.
|
|
1279
|
+
value = (value || '').replace(/^(['"])(.*)\1$/, '$2'); // trim value quotes if they match.
|
|
1280
|
+
node.setAttribute(name, value);
|
|
1281
|
+
this.attrNames.add(name);
|
|
1282
|
+
}
|
|
1217
1283
|
}
|
|
1218
1284
|
}
|
|
1219
1285
|
|
|
@@ -1312,7 +1378,7 @@ class ExprPath {
|
|
|
1312
1378
|
}
|
|
1313
1379
|
|
|
1314
1380
|
/**
|
|
1315
|
-
*
|
|
1381
|
+
* Set the value of an attribute. This can be for any attribute, not just attributes named "value".
|
|
1316
1382
|
* @param node
|
|
1317
1383
|
* @param exprs */
|
|
1318
1384
|
// TODO: node is always this.nodeMarker?
|
|
@@ -1342,12 +1408,21 @@ class ExprPath {
|
|
|
1342
1408
|
}
|
|
1343
1409
|
else {
|
|
1344
1410
|
// TODO: should we remove isFalsy, since these are always props?
|
|
1345
|
-
|
|
1411
|
+
const strValue = Util.isFalsy(value) ? '' : value;
|
|
1346
1412
|
|
|
1347
|
-
//
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1413
|
+
// Special case for contenteditable
|
|
1414
|
+
if (this.attrName === 'value' && node.hasAttribute('contenteditable')) {
|
|
1415
|
+
const existingValue = node.innerHTML;
|
|
1416
|
+
if (strValue !== existingValue)
|
|
1417
|
+
node.innerHTML = strValue;
|
|
1418
|
+
}
|
|
1419
|
+
else {
|
|
1420
|
+
|
|
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
|
+
}
|
|
1351
1426
|
}
|
|
1352
1427
|
|
|
1353
1428
|
// TODO: We need to remove any old listeners, like in bindEventAttribute.
|
|
@@ -1367,16 +1442,8 @@ class ExprPath {
|
|
|
1367
1442
|
|
|
1368
1443
|
// Regular attribute
|
|
1369
1444
|
else {
|
|
1370
|
-
//
|
|
1371
|
-
//
|
|
1372
|
-
//if (this.attrName === 'disabled')
|
|
1373
|
-
// debugger;
|
|
1374
|
-
|
|
1375
|
-
// hasOwnProperty() checks only the object, not the parents
|
|
1376
|
-
// this.attrName in node checks the node and the parents.
|
|
1377
|
-
// This version checks the html element it extends from, to see if has a setter set:
|
|
1378
|
-
// Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), this.attrName)?.set
|
|
1379
|
-
//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?
|
|
1380
1447
|
let isProp = this.isHtmlProperty;
|
|
1381
1448
|
if (isProp === undefined)
|
|
1382
1449
|
isProp = this.isHtmlProperty = Util.isHtmlProp(node, this.attrName);
|
|
@@ -1386,7 +1453,7 @@ class ExprPath {
|
|
|
1386
1453
|
if (!multiple) {
|
|
1387
1454
|
Globals$1.currentExprPath = this; // Used by watch()
|
|
1388
1455
|
if (typeof expr === 'function') {
|
|
1389
|
-
if (this.
|
|
1456
|
+
if (this.isComponent) { // Don't evaluate functions before passing them to components
|
|
1390
1457
|
return
|
|
1391
1458
|
}
|
|
1392
1459
|
this.watchFunction = expr; // The function that gets the expression, used for renderWatched()
|
|
@@ -1444,6 +1511,13 @@ class ExprPath {
|
|
|
1444
1511
|
// since we also prohibit expressions that are a child of textarea.
|
|
1445
1512
|
if (isProp)
|
|
1446
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
|
+
|
|
1447
1521
|
// TODO: Putting an 'else' here would be more performant
|
|
1448
1522
|
node.setAttribute(this.attrName, joinedValue);
|
|
1449
1523
|
}
|
|
@@ -1474,6 +1548,7 @@ class ExprPath {
|
|
|
1474
1548
|
nodeBefore = childNodes[this.nodeBeforeIndex];
|
|
1475
1549
|
|
|
1476
1550
|
let result = new ExprPath(nodeBefore, nodeMarker, this.type, this.attrName, this.attrValue);
|
|
1551
|
+
result.isComponent = this.isComponent;
|
|
1477
1552
|
|
|
1478
1553
|
|
|
1479
1554
|
|
|
@@ -1482,9 +1557,7 @@ class ExprPath {
|
|
|
1482
1557
|
|
|
1483
1558
|
/**
|
|
1484
1559
|
* Clear the nodeCache of this ExprPath, as well as all parent and child ExprPaths that
|
|
1485
|
-
* share the same DOM parent node.
|
|
1486
|
-
*
|
|
1487
|
-
* TODO: Is recursive clearing ever necessary? */
|
|
1560
|
+
* share the same DOM parent node. */
|
|
1488
1561
|
clearNodesCache() {
|
|
1489
1562
|
let path = this;
|
|
1490
1563
|
|
|
@@ -1497,9 +1570,6 @@ class ExprPath {
|
|
|
1497
1570
|
// If stuck in an infinite loop here, the problem is likely due to Template hash colisions.
|
|
1498
1571
|
// Which cause one path to be the descendant of itself, creating a cycle.
|
|
1499
1572
|
}
|
|
1500
|
-
|
|
1501
|
-
// Commented out on Sep 30, 2024 b/c it was making the benchmark never finish when adding 10k rows.
|
|
1502
|
-
//clearChildNodeCache(this);
|
|
1503
1573
|
}
|
|
1504
1574
|
|
|
1505
1575
|
|
|
@@ -1541,7 +1611,7 @@ class ExprPath {
|
|
|
1541
1611
|
// result2.push(...ng.getNodes())
|
|
1542
1612
|
// return result2;
|
|
1543
1613
|
|
|
1544
|
-
if (this.type === ExprPathType.AttribValue || this.type === ExprPathType.AttribMultiple
|
|
1614
|
+
if (this.type === ExprPathType.AttribValue || this.type === ExprPathType.AttribMultiple) {
|
|
1545
1615
|
return [this.nodeMarker];
|
|
1546
1616
|
}
|
|
1547
1617
|
|
|
@@ -1599,11 +1669,13 @@ class ExprPath {
|
|
|
1599
1669
|
result = collection.deleteAny(template.getExactKey());
|
|
1600
1670
|
}
|
|
1601
1671
|
|
|
1602
|
-
if (result) // also delete the matching close key.
|
|
1672
|
+
if (result) {// also delete the matching close key.
|
|
1603
1673
|
collection.deleteSpecific(template.getCloseKey(), result);
|
|
1604
|
-
|
|
1605
|
-
|
|
1674
|
+
|
|
1675
|
+
//result.applyExprs(template.exprs);
|
|
1606
1676
|
}
|
|
1677
|
+
else
|
|
1678
|
+
return null;
|
|
1607
1679
|
}
|
|
1608
1680
|
|
|
1609
1681
|
// Find a close match.
|
|
@@ -1637,12 +1709,6 @@ class ExprPath {
|
|
|
1637
1709
|
return result;
|
|
1638
1710
|
}
|
|
1639
1711
|
|
|
1640
|
-
isComponent() {
|
|
1641
|
-
// Events won't have type===Component.
|
|
1642
|
-
// TODO: Have a special flag for components instead of it being on the type?
|
|
1643
|
-
return this.type === ExprPathType.ComponentAttribValue || (this.attrName && this.nodeMarker.tagName && this.nodeMarker.tagName.includes('-'));
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
1712
|
/**
|
|
1647
1713
|
* TODO: Rename this to nodeGroupsInUse, nodeGroupsAvialableAttached and nodeGroupsAvailableDetached?
|
|
1648
1714
|
* Nodes that have been used during the current render().
|
|
@@ -1699,7 +1765,7 @@ class ExprPath {
|
|
|
1699
1765
|
/** @enum {int} */
|
|
1700
1766
|
const ExprPathType = {
|
|
1701
1767
|
/** Child of a node */
|
|
1702
|
-
Content: 1,
|
|
1768
|
+
Content: 1, // TODO: Rename to Nodes
|
|
1703
1769
|
|
|
1704
1770
|
/** One or more whole attributes */
|
|
1705
1771
|
AttribMultiple: 2,
|
|
@@ -1707,14 +1773,11 @@ const ExprPathType = {
|
|
|
1707
1773
|
/** Value of an attribute. */
|
|
1708
1774
|
AttribValue: 3,
|
|
1709
1775
|
|
|
1710
|
-
/** Value of an attribute being passed to a component. */
|
|
1711
|
-
ComponentAttribValue: 4,
|
|
1712
|
-
|
|
1713
1776
|
/** Expressions inside Html comments. */
|
|
1714
|
-
Comment:
|
|
1777
|
+
Comment: 4,
|
|
1715
1778
|
|
|
1716
1779
|
/** Value of an attribute. */
|
|
1717
|
-
Event:
|
|
1780
|
+
Event: 5,
|
|
1718
1781
|
};
|
|
1719
1782
|
|
|
1720
1783
|
|
|
@@ -1880,7 +1943,7 @@ class Shell {
|
|
|
1880
1943
|
|
|
1881
1944
|
|
|
1882
1945
|
if (html.length === 1 && !html[0].match(/[<&]/)) {
|
|
1883
|
-
this.fragment =
|
|
1946
|
+
this.fragment = Globals$1.doc.createTextNode(html[0]);
|
|
1884
1947
|
return;
|
|
1885
1948
|
}
|
|
1886
1949
|
|
|
@@ -1888,18 +1951,18 @@ class Shell {
|
|
|
1888
1951
|
// 1. Add placeholders
|
|
1889
1952
|
let joinedHtml = Shell.addPlaceholders(html);
|
|
1890
1953
|
|
|
1891
|
-
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.
|
|
1892
1955
|
if (joinedHtml)
|
|
1893
1956
|
template.innerHTML = joinedHtml;
|
|
1894
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.
|
|
1895
|
-
template.content.append(
|
|
1958
|
+
template.content.append(Globals$1.doc.createTextNode(''));
|
|
1896
1959
|
this.fragment = template.content;
|
|
1897
1960
|
|
|
1898
1961
|
// 2. Find placeholders
|
|
1899
1962
|
let node;
|
|
1900
1963
|
let toRemove = [];
|
|
1901
1964
|
let placeholdersUsed = 0;
|
|
1902
|
-
const walker =
|
|
1965
|
+
const walker = Globals$1.doc.createTreeWalker(this.fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
|
|
1903
1966
|
while (node = walker.nextNode()) {
|
|
1904
1967
|
|
|
1905
1968
|
// Remove previous after each iteration, so paths will still be calculated correctly.
|
|
@@ -1935,10 +1998,13 @@ class Shell {
|
|
|
1935
1998
|
// Replace comment placeholders
|
|
1936
1999
|
else if (node.nodeType === 8 && node.nodeValue === '!✨!') {
|
|
1937
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
|
+
|
|
1938
2004
|
// Get or create nodeBefore.
|
|
1939
2005
|
let nodeBefore = node.previousSibling; // Can be the same as another Path's nodeMarker.
|
|
1940
2006
|
if (!nodeBefore) {
|
|
1941
|
-
nodeBefore =
|
|
2007
|
+
nodeBefore = Globals$1.doc.createComment('ExprPath:'+this.paths.length);
|
|
1942
2008
|
node.parentNode.insertBefore(nodeBefore, node);
|
|
1943
2009
|
}
|
|
1944
2010
|
|
|
@@ -1963,9 +2029,9 @@ class Shell {
|
|
|
1963
2029
|
placeholdersUsed ++;
|
|
1964
2030
|
}
|
|
1965
2031
|
|
|
2032
|
+
// Comments become text nodes when inside textareas.
|
|
1966
2033
|
else if (node.nodeType === 3 && node.parentNode?.tagName === 'TEXTAREA' && node.textContent.includes('<!--!✨!-->'))
|
|
1967
2034
|
throw new Error(`Textarea can't have expressions inside them. Use <textarea value="\${...}"> instead.`);
|
|
1968
|
-
|
|
1969
2035
|
|
|
1970
2036
|
|
|
1971
2037
|
// Sometimes users will comment out a block of html code that has expressions.
|
|
@@ -1989,7 +2055,7 @@ class Shell {
|
|
|
1989
2055
|
|
|
1990
2056
|
let placeholders = [];
|
|
1991
2057
|
for (let i = 0; i<parts.length; i++) {
|
|
1992
|
-
let current =
|
|
2058
|
+
let current = Globals$1.doc.createTextNode(parts[i]);
|
|
1993
2059
|
node.parentNode.insertBefore(current, node);
|
|
1994
2060
|
if (i > 0)
|
|
1995
2061
|
placeholders.push(current);
|
|
@@ -2028,9 +2094,9 @@ class Shell {
|
|
|
2028
2094
|
path.nodeMarkerPath = getNodePath(path.nodeMarker);
|
|
2029
2095
|
|
|
2030
2096
|
// Cache so we don't have to calculate this later inside NodeGroup.applyExprs()
|
|
2031
|
-
if (path.type === ExprPathType.AttribValue && path.nodeMarker.nodeType === 1 &&
|
|
2097
|
+
if ((path.type === ExprPathType.AttribValue || path.type === ExprPathType.Event) && path.nodeMarker.nodeType === 1 &&
|
|
2032
2098
|
(path.nodeMarker.tagName.includes('-') || path.nodeMarker.hasAttribute('is'))) {
|
|
2033
|
-
path.
|
|
2099
|
+
path.isComponent = true;
|
|
2034
2100
|
}
|
|
2035
2101
|
}
|
|
2036
2102
|
|
|
@@ -2196,30 +2262,39 @@ class NodeGroup {
|
|
|
2196
2262
|
* @type {?Map<HTMLStyleElement, string>} */
|
|
2197
2263
|
styles;
|
|
2198
2264
|
|
|
2265
|
+
dynamicComponents = new Set();
|
|
2266
|
+
staticComponents = [];
|
|
2267
|
+
|
|
2268
|
+
/** @type {Template} */
|
|
2269
|
+
template;
|
|
2270
|
+
|
|
2199
2271
|
|
|
2200
2272
|
/**
|
|
2201
2273
|
* Create an "instantiated" NodeGroup from a Template and add it to an element.
|
|
2202
2274
|
* @param template {Template} Create it from the html strings and expressions in this template.
|
|
2203
2275
|
* @param parentPath {?ExprPath} */
|
|
2204
2276
|
constructor(template, parentPath=null) {
|
|
2277
|
+
this.rootNg = parentPath?.parentNg?.rootNg || this;
|
|
2278
|
+
this.parentPath = parentPath;
|
|
2279
|
+
|
|
2205
2280
|
if (!(this instanceof RootNodeGroup)) {
|
|
2206
2281
|
|
|
2207
|
-
let [fragment, shell] = this.
|
|
2282
|
+
let [fragment, shell] = this.populateFromTemplate(template);
|
|
2208
2283
|
|
|
2209
2284
|
if (fragment && template.exprs.length) {
|
|
2210
2285
|
this.updatePaths(fragment, shell.paths);
|
|
2211
2286
|
|
|
2212
2287
|
// Static web components can sometimes have children created via expressions.
|
|
2213
2288
|
// But calling applyExprs() will mess up the shell's path to them.
|
|
2214
|
-
// So we find them first, then call
|
|
2215
|
-
|
|
2289
|
+
// So we find them first, then call instantiateStaticComponents() after their children have been created.
|
|
2290
|
+
this.staticComponents = this.findStaticComponents(fragment, shell);
|
|
2216
2291
|
|
|
2217
2292
|
this.activateEmbeds(fragment, shell);
|
|
2218
2293
|
|
|
2219
2294
|
// Apply exprs
|
|
2220
2295
|
this.applyExprs(template.exprs);
|
|
2221
2296
|
|
|
2222
|
-
this.instantiateStaticComponents(staticComponents);
|
|
2297
|
+
this.instantiateStaticComponents(this.staticComponents);
|
|
2223
2298
|
}
|
|
2224
2299
|
else if (shell)
|
|
2225
2300
|
this.activateEmbeds(fragment, shell);
|
|
@@ -2230,47 +2305,33 @@ class NodeGroup {
|
|
|
2230
2305
|
* Common init shared by RootNodeGroup and NodeGroup constructors.
|
|
2231
2306
|
* But in a separate function because they need to do this at a different step.
|
|
2232
2307
|
* @param template {Template} Create it from the html strings and expressions in this template.
|
|
2233
|
-
* @
|
|
2234
|
-
|
|
2235
|
-
* @param closeKey {?string}
|
|
2236
|
-
* @returns {[DocumentFragment, Shell]} */
|
|
2237
|
-
init(template, parentPath=null, exactKey=null, closeKey=null) {
|
|
2238
|
-
this.exactKey = exactKey || template.getExactKey();
|
|
2239
|
-
this.closeKey = closeKey || template.getCloseKey();
|
|
2240
|
-
|
|
2241
|
-
this.parentPath = parentPath;
|
|
2242
|
-
this.rootNg = parentPath?.parentNg?.rootNg || this;
|
|
2243
|
-
|
|
2308
|
+
* @returns {[DocumentFragment, Shell]} The Shell created from the template,a nd the fragment cloned from the Shell.*/
|
|
2309
|
+
populateFromTemplate(template) {
|
|
2244
2310
|
|
|
2245
|
-
|
|
2246
|
-
/** @type {Template} */
|
|
2247
2311
|
this.template = template;
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
template.nodeGroup = this;
|
|
2251
|
-
|
|
2252
|
-
// Get a cached version of the parsed and instantiated html, and ExprPaths.
|
|
2312
|
+
this.exactKey = template.getExactKey();
|
|
2313
|
+
this.closeKey = template.getCloseKey();
|
|
2253
2314
|
|
|
2254
2315
|
// If it's just a text node, skip a bunch of unnecessary steps.
|
|
2255
|
-
if (
|
|
2256
|
-
|
|
2257
|
-
let textNode = document.createTextNode(template.html[0]);
|
|
2258
|
-
|
|
2316
|
+
if (template.isText) {
|
|
2317
|
+
let textNode = Globals$1.doc.createTextNode(template.html[0]);
|
|
2259
2318
|
this.startNode = this.endNode = textNode;
|
|
2260
2319
|
return [];
|
|
2261
2320
|
}
|
|
2321
|
+
|
|
2322
|
+
// Get a cached version of the parsed and instantiated html, and ExprPaths:
|
|
2262
2323
|
else {
|
|
2263
2324
|
let shell = Shell.get(template.html);
|
|
2264
2325
|
let fragment = shell.fragment.cloneNode(true);
|
|
2265
2326
|
|
|
2266
|
-
if (fragment
|
|
2327
|
+
if (fragment?.nodeType === 11) { // DocumentFragment
|
|
2267
2328
|
let childNodes = fragment.childNodes;
|
|
2268
2329
|
this.startNode = childNodes[0];
|
|
2269
2330
|
this.endNode = childNodes[childNodes.length - 1];
|
|
2270
2331
|
}
|
|
2271
|
-
else
|
|
2332
|
+
else
|
|
2272
2333
|
this.startNode = this.endNode = fragment;
|
|
2273
|
-
|
|
2334
|
+
|
|
2274
2335
|
return [fragment, shell];
|
|
2275
2336
|
}
|
|
2276
2337
|
}
|
|
@@ -2308,6 +2369,7 @@ class NodeGroup {
|
|
|
2308
2369
|
exprIndex--;
|
|
2309
2370
|
}
|
|
2310
2371
|
|
|
2372
|
+
|
|
2311
2373
|
// TODO: Need to end and restart this block when going from one component to the next?
|
|
2312
2374
|
// Think of having two adjacent components.
|
|
2313
2375
|
// But the dynamicAttribsAdjacet test already passes.
|
|
@@ -2316,11 +2378,11 @@ class NodeGroup {
|
|
|
2316
2378
|
// 1. Instantiate it if it hasn't already been, sending all expr's to its constructor.
|
|
2317
2379
|
// 2. Otherwise send them to its render function.
|
|
2318
2380
|
// Components with no expressions as attributes are instead activated in activateEmbeds().
|
|
2319
|
-
if (path.nodeMarker !== this.rootNg.root && path.isComponent
|
|
2381
|
+
if (path.nodeMarker !== this.rootNg.root && path.isComponent) {
|
|
2320
2382
|
|
|
2321
|
-
if (!nextPath || !nextPath.isComponent
|
|
2383
|
+
if (!nextPath || !nextPath.isComponent || nextPath.nodeMarker !== path.nodeMarker)
|
|
2322
2384
|
lastComponentPathIndex = i;
|
|
2323
|
-
let isFirstComponentPath = !prevPath || !prevPath.isComponent
|
|
2385
|
+
let isFirstComponentPath = !prevPath || !prevPath.isComponent || prevPath.nodeMarker !== path.nodeMarker;
|
|
2324
2386
|
|
|
2325
2387
|
if (isFirstComponentPath) {
|
|
2326
2388
|
|
|
@@ -2330,7 +2392,7 @@ class NodeGroup {
|
|
|
2330
2392
|
componentProps[attrName] = pathExprs[j].length > 1 ? pathExprs[j].join('') : pathExprs[j][0];
|
|
2331
2393
|
}
|
|
2332
2394
|
|
|
2333
|
-
this.
|
|
2395
|
+
this.handleComponent(path.nodeMarker, componentProps, true);
|
|
2334
2396
|
|
|
2335
2397
|
// Set attributes on component.
|
|
2336
2398
|
for (let j=i; j<=lastComponentPathIndex; j++)
|
|
@@ -2349,7 +2411,10 @@ class NodeGroup {
|
|
|
2349
2411
|
// TODO: Only do this if we have ExprPaths within styles?
|
|
2350
2412
|
this.updateStyles();
|
|
2351
2413
|
|
|
2352
|
-
|
|
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.
|
|
2353
2418
|
|
|
2354
2419
|
// Invalidate the nodes cache because we just changed it.
|
|
2355
2420
|
this.nodesCache = null;
|
|
@@ -2363,50 +2428,39 @@ class NodeGroup {
|
|
|
2363
2428
|
}
|
|
2364
2429
|
|
|
2365
2430
|
/**
|
|
2366
|
-
*
|
|
2367
|
-
* @param el {
|
|
2368
|
-
* @param props {Object}
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
// Perhaps if Components were treated as child NodeGroups, which would need to be the child of an ExprPath,
|
|
2373
|
-
// then we could re-use the hash and logic from NodeManager?
|
|
2374
|
-
let newHash = getObjectHash(props);
|
|
2375
|
-
|
|
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) {
|
|
2376
2437
|
let isPreHtmlElement = el.hasAttribute('solarite-placeholder');
|
|
2377
2438
|
let isPreIsElement = el.hasAttribute('_is');
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
// Instantiate a placeholder.
|
|
2439
|
+
let attribs, children;
|
|
2381
2440
|
if (isPreHtmlElement || isPreIsElement)
|
|
2382
|
-
el = this.instantiateComponent(el, isPreHtmlElement, props);
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
// compare the arguments and then decide for itself whether it wants to re-render.
|
|
2387
|
-
else if (el.render) {
|
|
2388
|
-
//let oldHash = Globals.componentArgsHash.get(el);
|
|
2389
|
-
//if (oldHash !== newHash) { // Only if not changed.
|
|
2390
|
-
let args = {};
|
|
2441
|
+
[el, attribs, children] = this.instantiateComponent(el, isPreHtmlElement, props);
|
|
2442
|
+
if (doRender && el.render) {
|
|
2443
|
+
if (!attribs) {
|
|
2444
|
+
attribs = Util.attribsToObject(el);
|
|
2391
2445
|
for (let name in props || {})
|
|
2392
|
-
|
|
2393
|
-
el.
|
|
2394
|
-
|
|
2446
|
+
attribs[Util.dashesToCamel(name)] = props[name];
|
|
2447
|
+
children = el.childNodes;
|
|
2448
|
+
}
|
|
2449
|
+
el.render(attribs, children);
|
|
2395
2450
|
}
|
|
2396
|
-
|
|
2397
|
-
Globals$1.componentArgsHash.set(el, newHash);
|
|
2451
|
+
return el;
|
|
2398
2452
|
}
|
|
2399
|
-
|
|
2453
|
+
|
|
2400
2454
|
/**
|
|
2401
2455
|
* We swap the placeholder element for the real element so we can pass its dynamic attributes
|
|
2402
2456
|
* to its constructor.
|
|
2457
|
+
* This is only called by handleComponent()
|
|
2458
|
+
* This does not call render()
|
|
2403
2459
|
*
|
|
2404
|
-
*
|
|
2405
|
-
*
|
|
2406
|
-
* @param el
|
|
2460
|
+
* @param el {HTMLElement}
|
|
2407
2461
|
* @param isPreHtmlElement {?boolean} True if the element's tag name ends with -solarite-placeholder
|
|
2408
2462
|
* @param props {Object} Attributes with dynamic values.
|
|
2409
|
-
* @return {HTMLElement} */
|
|
2463
|
+
* @return {[HTMLElement, attribs:Object, children:Node[]]}} */
|
|
2410
2464
|
instantiateComponent(el, isPreHtmlElement=undefined, props=undefined) {
|
|
2411
2465
|
if (isPreHtmlElement === undefined)
|
|
2412
2466
|
isPreHtmlElement = !el.hasAttribute('_is');
|
|
@@ -2421,24 +2475,17 @@ class NodeGroup {
|
|
|
2421
2475
|
if (!Constructor)
|
|
2422
2476
|
throw new Error(`The custom tag name ${tagName} is not registered.`)
|
|
2423
2477
|
|
|
2424
|
-
let args = {};
|
|
2425
|
-
for (let name in props || {})
|
|
2426
|
-
args[Util.dashesToCamel(name)] = props[name];
|
|
2427
|
-
|
|
2428
2478
|
// Pass other attribs to constructor, since otherwise they're not yet set on the element,
|
|
2429
2479
|
// and the constructor would otherwise have no way to see them.
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
args[attribName] = attrib.value;
|
|
2435
|
-
}
|
|
2436
|
-
}
|
|
2480
|
+
let attribs = Util.attribsToObject(el, 'solarite-placeholder');
|
|
2481
|
+
for (let name in props || {})
|
|
2482
|
+
attribs[Util.dashesToCamel(name)] = props[name];
|
|
2483
|
+
|
|
2437
2484
|
|
|
2438
2485
|
// Create the web component.
|
|
2439
2486
|
// Get the children that aren't Solarite's comment placeholders.
|
|
2440
|
-
let
|
|
2441
|
-
let newEl = new Constructor(
|
|
2487
|
+
let children = [...el.childNodes].filter(node => node.nodeType !== Node.COMMENT_NODE || !node.nodeValue.startsWith('ExprPath'));
|
|
2488
|
+
let newEl = new Constructor(attribs, children);
|
|
2442
2489
|
|
|
2443
2490
|
if (!isPreHtmlElement)
|
|
2444
2491
|
newEl.setAttribute('is', el.getAttribute('is').toLowerCase());
|
|
@@ -2464,7 +2511,7 @@ class NodeGroup {
|
|
|
2464
2511
|
if (this.endNode === el)
|
|
2465
2512
|
this.endNode = newEl;
|
|
2466
2513
|
|
|
2467
|
-
|
|
2514
|
+
// This is used only if inheriting from the Solarite class.
|
|
2468
2515
|
// applyComponentExprs() is called because we're rendering.
|
|
2469
2516
|
// So we want to render the sub-component also.
|
|
2470
2517
|
if (newEl.renderFirstTime)
|
|
@@ -2488,7 +2535,7 @@ class NodeGroup {
|
|
|
2488
2535
|
newEl.setAttribute(name, val);
|
|
2489
2536
|
}
|
|
2490
2537
|
|
|
2491
|
-
return newEl;
|
|
2538
|
+
return [newEl, attribs, children];
|
|
2492
2539
|
}
|
|
2493
2540
|
|
|
2494
2541
|
/**
|
|
@@ -2535,18 +2582,21 @@ class NodeGroup {
|
|
|
2535
2582
|
* Requires the nodeCache to be present. */
|
|
2536
2583
|
removeAndSaveOrphans() {
|
|
2537
2584
|
|
|
2538
|
-
let fragment =
|
|
2585
|
+
let fragment = Globals$1.doc.createDocumentFragment();
|
|
2539
2586
|
for (let node of this.getNodes())
|
|
2540
2587
|
fragment.append(node);
|
|
2541
2588
|
}
|
|
2542
2589
|
|
|
2543
2590
|
|
|
2544
|
-
|
|
2545
|
-
|
|
2591
|
+
/**
|
|
2592
|
+
* @param fragment {DocumentFragment}
|
|
2593
|
+
* @param paths
|
|
2594
|
+
* @param startingPathDepth {int} */
|
|
2595
|
+
updatePaths(fragment, paths, startingPathDepth) {
|
|
2546
2596
|
let pathLength = paths.length;
|
|
2547
2597
|
this.paths.length = pathLength;
|
|
2548
2598
|
for (let i=0; i<pathLength; i++) {
|
|
2549
|
-
let path = paths[i].clone(fragment,
|
|
2599
|
+
let path = paths[i].clone(fragment, startingPathDepth);
|
|
2550
2600
|
path.parentNg = this;
|
|
2551
2601
|
this.paths[i] = path;
|
|
2552
2602
|
}
|
|
@@ -2563,7 +2613,7 @@ class NodeGroup {
|
|
|
2563
2613
|
|
|
2564
2614
|
|
|
2565
2615
|
|
|
2566
|
-
findStaticComponents(root, shell,
|
|
2616
|
+
findStaticComponents(root, shell, startingPathDepth=0) {
|
|
2567
2617
|
let result = [];
|
|
2568
2618
|
|
|
2569
2619
|
// static components. These are WebComponents that do not have any constructor arguments that are expressions.
|
|
@@ -2571,8 +2621,8 @@ class NodeGroup {
|
|
|
2571
2621
|
// Maybe someday these two paths will be merged?
|
|
2572
2622
|
// Must happen before ids because instantiateComponent will replace the element.
|
|
2573
2623
|
for (let path of shell.staticComponents) {
|
|
2574
|
-
if (
|
|
2575
|
-
path = path.slice(0, -
|
|
2624
|
+
if (startingPathDepth)
|
|
2625
|
+
path = path.slice(0, -startingPathDepth);
|
|
2576
2626
|
let el = resolveNodePath(root, path);
|
|
2577
2627
|
|
|
2578
2628
|
// Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
|
|
@@ -2584,8 +2634,9 @@ class NodeGroup {
|
|
|
2584
2634
|
}
|
|
2585
2635
|
|
|
2586
2636
|
instantiateStaticComponents(staticComponents) {
|
|
2587
|
-
|
|
2588
|
-
|
|
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);
|
|
2589
2640
|
}
|
|
2590
2641
|
|
|
2591
2642
|
/**
|
|
@@ -2606,7 +2657,7 @@ class NodeGroup {
|
|
|
2606
2657
|
let el = resolveNodePath(root, path);
|
|
2607
2658
|
Util.bindId(rootEl, el);
|
|
2608
2659
|
}
|
|
2609
|
-
|
|
2660
|
+
}
|
|
2610
2661
|
|
|
2611
2662
|
// styles
|
|
2612
2663
|
if (options?.styles !== false) {
|
|
@@ -2636,9 +2687,8 @@ class NodeGroup {
|
|
|
2636
2687
|
}
|
|
2637
2688
|
}
|
|
2638
2689
|
}
|
|
2639
|
-
}
|
|
2640
|
-
|
|
2641
|
-
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2642
2692
|
class RootNodeGroup extends NodeGroup {
|
|
2643
2693
|
|
|
2644
2694
|
/**
|
|
@@ -2652,18 +2702,18 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2652
2702
|
exprsToRender = new Map();
|
|
2653
2703
|
|
|
2654
2704
|
/**
|
|
2655
|
-
*
|
|
2656
|
-
* @param
|
|
2657
|
-
* @param
|
|
2658
|
-
* @param options {?object}
|
|
2659
|
-
*/
|
|
2705
|
+
* @param template {Template}
|
|
2706
|
+
* @param el {?HTMLElement} Optional, pre-existing htmlElement tat will be the root.
|
|
2707
|
+
* @param options {?object} */
|
|
2660
2708
|
constructor(template, el, options) {
|
|
2661
2709
|
super(template);
|
|
2662
2710
|
|
|
2663
2711
|
this.options = options;
|
|
2664
2712
|
|
|
2665
|
-
|
|
2666
|
-
|
|
2713
|
+
let [fragment, shell] = this.populateFromTemplate(template);
|
|
2714
|
+
|
|
2715
|
+
let startingPathDepth = 0;
|
|
2716
|
+
|
|
2667
2717
|
|
|
2668
2718
|
if (fragment instanceof Text) {
|
|
2669
2719
|
|
|
@@ -2674,25 +2724,25 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2674
2724
|
el.append(fragment);
|
|
2675
2725
|
this.root = el;
|
|
2676
2726
|
}
|
|
2727
|
+
else
|
|
2728
|
+
throw new Error('Cannot create a standalone text node');
|
|
2677
2729
|
Globals$1.nodeGroups.set(this.root, this);
|
|
2678
2730
|
}
|
|
2731
|
+
|
|
2732
|
+
|
|
2679
2733
|
else {
|
|
2680
2734
|
|
|
2681
2735
|
// If adding NodeGroup to an element.
|
|
2682
|
-
let offset = 0;
|
|
2683
|
-
let root = fragment; // TODO: Rename so it's not confused with this.root.
|
|
2684
2736
|
if (el) {
|
|
2685
|
-
|
|
2737
|
+
this.root = el;
|
|
2686
2738
|
|
|
2687
2739
|
// Save slot children
|
|
2688
2740
|
let slotChildren;
|
|
2689
2741
|
if (el.childNodes.length) {
|
|
2690
|
-
slotChildren =
|
|
2742
|
+
slotChildren = Globals$1.doc.createDocumentFragment();
|
|
2691
2743
|
slotChildren.append(...el.childNodes);
|
|
2692
2744
|
}
|
|
2693
2745
|
|
|
2694
|
-
this.root = el;
|
|
2695
|
-
|
|
2696
2746
|
// If el should replace the root node of the fragment.
|
|
2697
2747
|
if (isReplaceEl(fragment, el)) {
|
|
2698
2748
|
el.append(...fragment.children[0].childNodes);
|
|
@@ -2703,8 +2753,10 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2703
2753
|
el.setAttribute(attrib.name, attrib.value);
|
|
2704
2754
|
|
|
2705
2755
|
// Go one level deeper into all of shell's paths.
|
|
2706
|
-
|
|
2707
|
-
}
|
|
2756
|
+
startingPathDepth = 1;
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
else {
|
|
2708
2760
|
let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
|
|
2709
2761
|
if (!isEmpty)
|
|
2710
2762
|
el.append(...fragment.childNodes);
|
|
@@ -2732,35 +2784,35 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2732
2784
|
el.append(slotChildren);
|
|
2733
2785
|
}
|
|
2734
2786
|
|
|
2735
|
-
root = el;
|
|
2736
|
-
|
|
2737
2787
|
this.startNode = el;
|
|
2738
2788
|
this.endNode = el;
|
|
2739
|
-
}
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
// Instantiate as a standalone element.
|
|
2792
|
+
else {
|
|
2740
2793
|
let singleEl = getSingleEl(fragment);
|
|
2741
|
-
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.
|
|
2742
2795
|
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
root = singleEl;
|
|
2746
|
-
offset = 1;
|
|
2747
|
-
}
|
|
2796
|
+
if (singleEl)
|
|
2797
|
+
startingPathDepth = 1;
|
|
2748
2798
|
}
|
|
2749
|
-
|
|
2750
|
-
this.updatePaths(root, shell.paths,
|
|
2799
|
+
Globals$1.nodeGroups.set(this.root, this);
|
|
2800
|
+
this.updatePaths(this.root, shell.paths, startingPathDepth);
|
|
2751
2801
|
|
|
2752
2802
|
// Static web components can sometimes have children created via expressions.
|
|
2753
2803
|
// But calling applyExprs() will mess up the shell's path to them.
|
|
2754
2804
|
// So we find them first, then call activateStaticComponents() after their children have been created.
|
|
2755
|
-
|
|
2805
|
+
this.staticComponents = this.findStaticComponents(this.root, shell, startingPathDepth);
|
|
2756
2806
|
|
|
2757
|
-
this.activateEmbeds(root, shell,
|
|
2807
|
+
this.activateEmbeds(this.root, shell, startingPathDepth);
|
|
2758
2808
|
|
|
2759
2809
|
// Apply exprs
|
|
2760
2810
|
this.applyExprs(template.exprs);
|
|
2761
2811
|
|
|
2762
|
-
this.instantiateStaticComponents(staticComponents);
|
|
2812
|
+
this.instantiateStaticComponents(this.staticComponents);
|
|
2763
2813
|
}
|
|
2814
|
+
|
|
2815
|
+
|
|
2764
2816
|
}
|
|
2765
2817
|
}
|
|
2766
2818
|
|
|
@@ -2802,8 +2854,7 @@ class Template {
|
|
|
2802
2854
|
/** @type {Array} Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
|
|
2803
2855
|
hashedFields;
|
|
2804
2856
|
|
|
2805
|
-
|
|
2806
|
-
nodeGroup;
|
|
2857
|
+
isText;
|
|
2807
2858
|
|
|
2808
2859
|
/**
|
|
2809
2860
|
*
|
|
@@ -2899,169 +2950,203 @@ class Template {
|
|
|
2899
2950
|
|
|
2900
2951
|
return this.closeKey;
|
|
2901
2952
|
}
|
|
2902
|
-
}
|
|
2903
2953
|
|
|
2954
|
+
/**
|
|
2955
|
+
* @param tag {string}
|
|
2956
|
+
* @param props {?Record<string, any>}
|
|
2957
|
+
* @param children
|
|
2958
|
+
* @returns {Template} */
|
|
2959
|
+
static fromJsx(tag, props, children) {
|
|
2904
2960
|
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
* @property {boolean=} styles - Replace :host in style tags to scope them locally.
|
|
2908
|
-
* @property {boolean=} scripts - Execute script tags.
|
|
2909
|
-
* @property {boolean=} ids - Create references to elements with id or data-id attributes.
|
|
2910
|
-
* @property {?boolean} render - Deprecated.
|
|
2911
|
-
* Used only when options are given to a class super constructor inheriting from Solarite.
|
|
2912
|
-
* True to call render() immediately in super constructor.
|
|
2913
|
-
* False to automatically call render() at all.
|
|
2914
|
-
* Undefined (default) to call render() when added to the DOM, unless already rendered.
|
|
2915
|
-
*/
|
|
2916
|
-
|
|
2917
|
-
/**
|
|
2918
|
-
* Convert strings to HTMLNodes.
|
|
2919
|
-
* Using h`...` as a tag will always create a Template.
|
|
2920
|
-
* Using h() as a function() will always create a DOM element.
|
|
2921
|
-
*
|
|
2922
|
-
* Features beyond what standard js tagged template strings do:
|
|
2923
|
-
* 1. r`` sub-expressions
|
|
2924
|
-
* 2. functions, nodes, and arrays of nodes as sub-expressions.
|
|
2925
|
-
* 3. html-escape all expressions by default, unless wrapped in r()
|
|
2926
|
-
* 4. event binding
|
|
2927
|
-
* 5. TODO: list more
|
|
2928
|
-
*
|
|
2929
|
-
* Currently supported:
|
|
2930
|
-
* 1. h(el, options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
|
|
2931
|
-
* 2. h(el, template, ?options) // Render the Template created by #1 to element.
|
|
2932
|
-
*
|
|
2933
|
-
* 3. h`<b>Hello</b> ${'World'}!` // Create Template that can later be used to create nodes.
|
|
2934
|
-
*
|
|
2935
|
-
* 4. h('Hello'); // Create single text node.
|
|
2936
|
-
* 5. h('<b>Hello</b>'); // Create single HTMLElement
|
|
2937
|
-
* 6. h('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
|
|
2938
|
-
* 7. h()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which
|
|
2939
|
-
* // includes properly handling nested components and r`` sub-expressions.
|
|
2940
|
-
* 8. h(template) // Render Template created by #1.
|
|
2941
|
-
*
|
|
2942
|
-
* 9. h({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
|
|
2943
|
-
* 10. h(string, object, ...) // JSX TODO
|
|
2944
|
-
* @param htmlStrings {?HTMLElement|string|string[]|function():Template|{render:function()}}
|
|
2945
|
-
* @param exprs {*[]|string|Template|Object}
|
|
2946
|
-
* @return {Node|HTMLElement|Template} */
|
|
2947
|
-
function h(htmlStrings=undefined, ...exprs) {
|
|
2961
|
+
// HTML void elements that must not have closing tags
|
|
2962
|
+
const isVoid = selfClosingTags.has(tag.toLowerCase());
|
|
2948
2963
|
|
|
2949
|
-
|
|
2950
|
-
|
|
2964
|
+
// Build htmlStrings/exprs so Shell can place placeholders in attribute values and child content.
|
|
2965
|
+
let htmlStrings = [];
|
|
2966
|
+
let templateExprs = [];
|
|
2951
2967
|
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
let parent = htmlStrings, template = exprs[0];
|
|
2968
|
+
// Opening tag
|
|
2969
|
+
let open = `<${tag}`;
|
|
2955
2970
|
|
|
2956
|
-
//
|
|
2957
|
-
if (
|
|
2958
|
-
|
|
2959
|
-
|
|
2971
|
+
// Attributes
|
|
2972
|
+
if (props && typeof props === 'object') {
|
|
2973
|
+
for (let name in props) {
|
|
2974
|
+
let value = props[name];
|
|
2960
2975
|
|
|
2961
|
-
|
|
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
|
+
}
|
|
2962
2982
|
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
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
|
+
}
|
|
2970
2999
|
}
|
|
2971
3000
|
|
|
2972
|
-
//
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
//
|
|
2980
|
-
|
|
2981
|
-
|
|
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 === '"' ? '">' : '>');
|
|
2982
3011
|
}
|
|
3012
|
+
|
|
3013
|
+
for (let child of children)
|
|
3014
|
+
addChild(child, htmlStrings, templateExprs);
|
|
2983
3015
|
}
|
|
2984
|
-
}
|
|
2985
3016
|
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
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);
|
|
2989
3037
|
}
|
|
3038
|
+
}
|
|
2990
3039
|
|
|
2991
|
-
else if (typeof htmlStrings === 'string' || htmlStrings instanceof String) {
|
|
2992
|
-
// 10. JSX
|
|
2993
|
-
if (typeof exprs[0] === 'object') {
|
|
2994
|
-
exprs[0] || {};
|
|
2995
|
-
exprs.slice(1);
|
|
2996
3040
|
|
|
2997
|
-
|
|
2998
|
-
let templateExprs = [];
|
|
3041
|
+
const selfClosingTags = new Set(['area','base','br','col','embed','hr','img','input','link','meta','param','source','track','wbr']);
|
|
2999
3042
|
|
|
3000
|
-
// TODO How to know which children are static html and which are expression placeholders?
|
|
3001
|
-
// Perhaps we have to treat every text child as a string?
|
|
3002
3043
|
|
|
3003
|
-
|
|
3004
|
-
|
|
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
|
+
}
|
|
3005
3070
|
}
|
|
3006
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
|
+
};
|
|
3007
3086
|
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
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();
|
|
3012
3118
|
|
|
3013
3119
|
// We create a new one each time because otherwise
|
|
3014
3120
|
// the returned fragment will have its content replaced by a subsequent call.
|
|
3015
|
-
let templateEl =
|
|
3016
|
-
templateEl.innerHTML =
|
|
3121
|
+
let templateEl = Globals$1.doc.createElement('template');
|
|
3122
|
+
templateEl.innerHTML = html;
|
|
3017
3123
|
|
|
3018
|
-
//
|
|
3124
|
+
// 1+2. Return Node if there's one child.
|
|
3019
3125
|
let relevantNodes = Util.trimEmptyNodes(templateEl.content.childNodes);
|
|
3020
3126
|
if (relevantNodes.length === 1)
|
|
3021
3127
|
return relevantNodes[0];
|
|
3022
3128
|
|
|
3023
|
-
//
|
|
3129
|
+
// 3. Otherwise return DocumentFragment.
|
|
3024
3130
|
return templateEl.content;
|
|
3025
3131
|
}
|
|
3026
3132
|
|
|
3027
|
-
//
|
|
3028
|
-
|
|
3029
|
-
return (
|
|
3030
|
-
//Globals.rendered.add(parent)
|
|
3031
|
-
let template = h(htmlStrings, ...exprs);
|
|
3032
|
-
return template.render();
|
|
3033
|
-
}
|
|
3034
|
-
}
|
|
3035
|
-
|
|
3036
|
-
// 8.
|
|
3037
|
-
else if (htmlStrings instanceof Template) {
|
|
3038
|
-
return htmlStrings.render();
|
|
3133
|
+
// 4.
|
|
3134
|
+
if (arg instanceof Template) {
|
|
3135
|
+
return arg.render();
|
|
3039
3136
|
}
|
|
3040
3137
|
|
|
3041
|
-
|
|
3042
|
-
// 9. Create dynamic element with render() function.
|
|
3138
|
+
// 5. Create dynamic element from an object with a render() function.
|
|
3043
3139
|
// TODO: This path doesn't handle embeds like data-id="..."
|
|
3044
|
-
else if (typeof
|
|
3045
|
-
let obj =
|
|
3140
|
+
else if (arg && typeof arg === 'object') {
|
|
3141
|
+
let obj = arg;
|
|
3046
3142
|
|
|
3047
|
-
if (obj.constructor.name !== 'Object')
|
|
3143
|
+
if (obj.constructor.name !== 'Object')
|
|
3048
3144
|
throw new Error(`Solarate Web Component class ${obj.constructor?.name} must extend HTMLElement.`);
|
|
3049
3145
|
|
|
3050
|
-
|
|
3051
|
-
// Special rebound render path, called by normal path.
|
|
3052
|
-
// Intercepts the main r`...` function call inside render().
|
|
3053
|
-
if (Globals$1.objToEl.has(obj)) {
|
|
3054
|
-
return function(...args) {
|
|
3055
|
-
let template = h(...args);
|
|
3056
|
-
let el = template.render();
|
|
3057
|
-
Globals$1.objToEl.set(obj, el);
|
|
3058
|
-
}.bind(obj);
|
|
3059
|
-
}
|
|
3060
|
-
|
|
3061
3146
|
// Normal path
|
|
3062
|
-
|
|
3147
|
+
if (!Globals$1.objToEl.has(obj)) {
|
|
3063
3148
|
Globals$1.objToEl.set(obj, null);
|
|
3064
|
-
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)
|
|
3065
3150
|
let el = Globals$1.objToEl.get(obj);
|
|
3066
3151
|
Globals$1.objToEl.delete(obj);
|
|
3067
3152
|
|
|
@@ -3069,7 +3154,7 @@ function h(htmlStrings=undefined, ...exprs) {
|
|
|
3069
3154
|
if (typeof obj[name] === 'function')
|
|
3070
3155
|
el[name] = obj[name].bind(el); // Make the "this" of functions be el.
|
|
3071
3156
|
// TODO: But this doesn't work for passing an object with functions as a constructor arg via an attribute:
|
|
3072
|
-
|
|
3157
|
+
// <my-element arg=${{myFunc() { return this }}}
|
|
3073
3158
|
else
|
|
3074
3159
|
el[name] = obj[name];
|
|
3075
3160
|
|
|
@@ -3085,13 +3170,145 @@ function h(htmlStrings=undefined, ...exprs) {
|
|
|
3085
3170
|
}
|
|
3086
3171
|
}
|
|
3087
3172
|
|
|
3088
|
-
|
|
3089
|
-
|
|
3173
|
+
throw new Error('toEl() does not support argument of type: ' + (arg ? typeof arg : arg));
|
|
3174
|
+
|
|
3090
3175
|
}
|
|
3091
3176
|
|
|
3177
|
+
|
|
3092
3178
|
// Trick to prevent minifier from renaming this function.
|
|
3093
3179
|
let renderF = 'render';
|
|
3094
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
|
+
|
|
3095
3312
|
/**
|
|
3096
3313
|
* There are three ways to create an instance of a Solarite Component:
|
|
3097
3314
|
* 1. new ComponentName(); // direct class instantiation
|
|
@@ -3265,7 +3482,7 @@ function createSolarite(extendsTag=null) {
|
|
|
3265
3482
|
|
|
3266
3483
|
BaseClass = Globals$1.elementClasses[extendsTag];
|
|
3267
3484
|
if (!BaseClass) { // TODO: Use Cache
|
|
3268
|
-
BaseClass =
|
|
3485
|
+
BaseClass = Globals$1.doc.createElement(extendsTag).constructor;
|
|
3269
3486
|
Globals$1.elementClasses[extendsTag] = BaseClass;
|
|
3270
3487
|
}
|
|
3271
3488
|
}
|
|
@@ -3326,7 +3543,7 @@ function createSolarite(extendsTag=null) {
|
|
|
3326
3543
|
this.innerHTML = html;
|
|
3327
3544
|
}
|
|
3328
3545
|
else
|
|
3329
|
-
this.modifications =
|
|
3546
|
+
this.modifications = h(this, html, options);
|
|
3330
3547
|
}
|
|
3331
3548
|
})*/
|
|
3332
3549
|
|
|
@@ -3376,11 +3593,14 @@ function createSolarite(extendsTag=null) {
|
|
|
3376
3593
|
let define = 'define';
|
|
3377
3594
|
let getName = 'getName';
|
|
3378
3595
|
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3596
|
+
/*
|
|
3597
|
+
┏┓ ┓ •
|
|
3598
|
+
┗┓┏┓┃┏┓┏┓┓╋▗▖
|
|
3599
|
+
┗┛┗┛┗┗┻╹ ╹╹┗
|
|
3600
|
+
JavasCript UI library
|
|
3601
|
+
@license MIT
|
|
3602
|
+
@copyright Vorticode LLC
|
|
3603
|
+
https://vorticode.github.io/solarite/ */
|
|
3384
3604
|
|
|
3385
3605
|
/**
|
|
3386
3606
|
* TODO: The Proxy and the multiple base classes mess up 'instanceof Solarite'
|
|
@@ -3394,4 +3614,4 @@ const Solarite = new Proxy(createSolarite(), {
|
|
|
3394
3614
|
//export {default as watch, renderWatched} from './watch.js'; // unfinished
|
|
3395
3615
|
|
|
3396
3616
|
export default h;
|
|
3397
|
-
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 };
|