solarite 0.2.3 → 0.3.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 +1708 -1211
- package/dist/Solarite.js +1642 -1047
- package/dist/Solarite.min.js +2 -2
- package/package.json +1 -1
- package/readme.md +1 -3
- package/src/solarite/ExprPath.js +479 -196
- package/src/solarite/Globals.js +77 -51
- package/src/solarite/HtmlParser.js +91 -0
- package/src/solarite/NodeGroup.js +288 -195
- package/src/solarite/Shell.js +117 -91
- package/src/solarite/Solarite.js +7 -3
- package/src/solarite/Template.js +23 -18
- package/src/solarite/Util.js +117 -179
- package/src/solarite/createSolarite.js +16 -138
- package/src/solarite/getArg.js +31 -16
- package/src/solarite/{r.js → h.js} +56 -18
- package/src/solarite/hash.js +12 -9
- package/src/solarite/watch.js +546 -0
- package/src/util/Errors.js +1 -0
- package/src/util/MultiValueMap.js +73 -13
- package/src/util/Util.js +15 -2
- package/src/util/delve.js +5 -4
- package/src/solarite/watch3.js +0 -98
- package/src/util/WeakArray.js +0 -33
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import {assert} from "../util/Errors.js";
|
|
2
|
-
import ExprPath, {
|
|
2
|
+
import ExprPath, {ExprPathType, resolveNodePath} from "./ExprPath.js";
|
|
3
3
|
import {getObjectHash} from "./hash.js";
|
|
4
4
|
import Shell from "./Shell.js";
|
|
5
|
-
import
|
|
6
|
-
import Util, {arraySame, flattenAndIndent, isEvent, nodeToArrayTree, setIndent} from "./Util.js";
|
|
5
|
+
import Util, {flattenAndIndent, nodeToArrayTree, setIndent} from "./Util.js";
|
|
7
6
|
//import NodeGroupManager from "./NodeGroupManager.js";
|
|
8
7
|
import delve from "../util/delve.js";
|
|
9
8
|
import Globals from "./Globals.js";
|
|
10
9
|
|
|
11
|
-
|
|
12
10
|
/** @typedef {boolean|string|number|function|Object|Array|Date|Node|Template} Expr */
|
|
13
11
|
|
|
14
12
|
/**
|
|
@@ -33,7 +31,8 @@ export default class NodeGroup {
|
|
|
33
31
|
startNode;
|
|
34
32
|
|
|
35
33
|
/** @type {Node|HTMLElement} A node that never changes that this NodeGroup should always insert its nodes before.
|
|
36
|
-
* An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position
|
|
34
|
+
* An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.
|
|
35
|
+
* TODO: But sometimes startNode and endNode point to the same node. Document htis inconsistency. */
|
|
37
36
|
endNode;
|
|
38
37
|
|
|
39
38
|
/** @type {ExprPath[]} */
|
|
@@ -51,11 +50,11 @@ export default class NodeGroup {
|
|
|
51
50
|
nodesCache;
|
|
52
51
|
|
|
53
52
|
/**
|
|
53
|
+
* A map between <style> Elements and their text content.
|
|
54
|
+
* This lets NodeGroup.updateStyles() see when the style text has changed.
|
|
54
55
|
* @type {?Map<HTMLStyleElement, string>} */
|
|
55
56
|
styles;
|
|
56
57
|
|
|
57
|
-
currentComponentProps = {};
|
|
58
|
-
|
|
59
58
|
|
|
60
59
|
/**
|
|
61
60
|
* Create an "instantiated" NodeGroup from a Template and add it to an element.
|
|
@@ -63,14 +62,26 @@ export default class NodeGroup {
|
|
|
63
62
|
* @param parentPath {?ExprPath} */
|
|
64
63
|
constructor(template, parentPath=null) {
|
|
65
64
|
if (!(this instanceof RootNodeGroup)) {
|
|
65
|
+
|
|
66
66
|
let [fragment, shell] = this.init(template, parentPath);
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
if (fragment && template.exprs.length) {
|
|
69
|
+
this.updatePaths(fragment, shell.paths);
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
// Static web components can sometimes have children created via expressions.
|
|
72
|
+
// But calling applyExprs() will mess up the shell's path to them.
|
|
73
|
+
// So we find them first, then call activateStaticComponents() after their children have been created.
|
|
74
|
+
let staticComponents = this.findStaticComponents(fragment, shell);
|
|
71
75
|
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
this.activateEmbeds(fragment, shell);
|
|
77
|
+
|
|
78
|
+
// Apply exprs
|
|
79
|
+
this.applyExprs(template.exprs);
|
|
80
|
+
|
|
81
|
+
this.activateStaticComponents(staticComponents);
|
|
82
|
+
}
|
|
83
|
+
else if (shell)
|
|
84
|
+
this.activateEmbeds(fragment, shell);
|
|
74
85
|
}
|
|
75
86
|
}
|
|
76
87
|
|
|
@@ -98,57 +109,104 @@ export default class NodeGroup {
|
|
|
98
109
|
template.nodeGroup = this;
|
|
99
110
|
|
|
100
111
|
// Get a cached version of the parsed and instantiated html, and ExprPaths.
|
|
101
|
-
let shell = Shell.get(template.html);
|
|
102
|
-
let fragment = shell.fragment.cloneNode(true);
|
|
103
112
|
|
|
104
|
-
|
|
105
|
-
this.
|
|
106
|
-
|
|
113
|
+
// If it's just a text node, skip a bunch of unnecessary steps.
|
|
114
|
+
if (!(this instanceof RootNodeGroup) && !template.exprs.length && !template.html[0].includes('<')) {
|
|
115
|
+
//let doc = this.rootNg.startNode?.ownerDocument || document;
|
|
116
|
+
let textNode = document.createTextNode(template.html[0]);
|
|
107
117
|
|
|
108
|
-
|
|
118
|
+
this.startNode = this.endNode = textNode;
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
let shell = Shell.get(template.html);
|
|
123
|
+
let fragment = shell.fragment.cloneNode(true);
|
|
124
|
+
|
|
125
|
+
if (fragment instanceof DocumentFragment) {
|
|
126
|
+
let childNodes = fragment.childNodes;
|
|
127
|
+
this.startNode = childNodes[0];
|
|
128
|
+
this.endNode = childNodes[childNodes.length - 1];
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
this.startNode = this.endNode = fragment;
|
|
132
|
+
}
|
|
133
|
+
return [fragment, shell];
|
|
134
|
+
}
|
|
109
135
|
}
|
|
110
136
|
|
|
111
137
|
/**
|
|
112
138
|
* Use the paths to insert the given expressions.
|
|
113
139
|
* Dispatches expression handling to other functions depending on the path type.
|
|
114
140
|
* @param exprs {(*|*[]|function|Template)[]}
|
|
115
|
-
* @param paths {?ExprPath[]} Optional. */
|
|
141
|
+
* @param paths {?ExprPath[]} Optional. Only used for testing. Normally uses this.paths. */
|
|
116
142
|
applyExprs(exprs, paths=null) {
|
|
117
143
|
paths = paths || this.paths;
|
|
118
144
|
|
|
119
|
-
/*#IFDEV*/
|
|
145
|
+
/*#IFDEV*/
|
|
146
|
+
this.verify();/*#ENDIF*/
|
|
147
|
+
|
|
148
|
+
// Things to consider:
|
|
149
|
+
// 1. One path may use multipe expressions. E.g. <div class="${1} ${2}">
|
|
150
|
+
// 2. One component may need to use multiple attribute paths to be instantiated.
|
|
151
|
+
// 3. We apply them in reverse order so that a <select> box has its children created from an expression
|
|
152
|
+
// before its instantiated and its value attribute is set via an expression.
|
|
153
|
+
|
|
154
|
+
let exprIndex = exprs.length - 1; // Update exprs at paths.
|
|
155
|
+
let lastComponentPathIndex;
|
|
156
|
+
let pathExprs = new Array(paths.length); // Store all the expressions that map to a single path. Only paths to attribute values can have more than one.
|
|
157
|
+
for (let i = paths.length - 1, path; path = paths[i]; i--) {
|
|
158
|
+
let prevPath = paths[i - 1];
|
|
159
|
+
let nextPath = paths[i + 1];
|
|
160
|
+
|
|
161
|
+
// Get the expressions associated with this path.
|
|
162
|
+
if (path.attrValue?.length > 2) {
|
|
163
|
+
let startIndex = (exprIndex - (path.attrValue.length - 1)) + 1;
|
|
164
|
+
pathExprs[i] = exprs.slice(startIndex, exprIndex + 1); // probably doesn't allocate if the JS vm implements copy on write.
|
|
165
|
+
exprIndex -= pathExprs[i].length;
|
|
166
|
+
} else {
|
|
167
|
+
pathExprs[i] = [exprs[exprIndex]];
|
|
168
|
+
exprIndex--;
|
|
169
|
+
}
|
|
120
170
|
|
|
121
|
-
|
|
122
|
-
|
|
171
|
+
// TODO: Need to end and restart this block when going from one component to the next?
|
|
172
|
+
// Think of having two adjacent components.
|
|
173
|
+
// But the dynamicAttribsAdjacet test already passes.
|
|
123
174
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
175
|
+
// If a component:
|
|
176
|
+
// 1. Instantiate it if it hasn't already been, sending all expr's to its constructor.
|
|
177
|
+
// 2. Otherwise send them to its render function.
|
|
178
|
+
// Components with no expressions as attributes are instead activated in activateEmbeds().
|
|
179
|
+
if (path.nodeMarker !== this.rootNg.root && path.isComponent()) {
|
|
128
180
|
|
|
129
|
-
|
|
181
|
+
if (!nextPath || !nextPath.isComponent() || nextPath.nodeMarker !== path.nodeMarker)
|
|
182
|
+
lastComponentPathIndex = i;
|
|
183
|
+
let isFirstComponentPath = !prevPath || !prevPath.isComponent() || prevPath.nodeMarker !== path.nodeMarker;
|
|
130
184
|
|
|
131
|
-
|
|
132
|
-
if (lastNode && lastNode !== this.rootNg.root && lastNode !== path.nodeMarker && Object.keys(this.currentComponentProps).length) {
|
|
133
|
-
this.applyComponentExprs(lastNode, this.currentComponentProps);
|
|
134
|
-
this.currentComponentProps = {};
|
|
135
|
-
}
|
|
185
|
+
if (isFirstComponentPath) {
|
|
136
186
|
|
|
137
|
-
|
|
187
|
+
let componentProps = {}
|
|
188
|
+
for (let j=i; j<=lastComponentPathIndex; j++) {
|
|
189
|
+
let attrName = paths[j].attrName; // Util.dashesToCamel(paths[j].attrName);
|
|
190
|
+
componentProps[attrName] = pathExprs[j].length > 1 ? pathExprs[j].join('') : pathExprs[j][0];
|
|
191
|
+
}
|
|
138
192
|
|
|
139
|
-
|
|
193
|
+
this.applyComponentExprs(path.nodeMarker, componentProps);
|
|
140
194
|
|
|
195
|
+
// Set attributes on component.
|
|
196
|
+
for (let j=i; j<=lastComponentPathIndex; j++)
|
|
197
|
+
paths[j].apply(pathExprs[j]);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
141
200
|
|
|
142
|
-
|
|
143
|
-
|
|
201
|
+
// Else apply it normally
|
|
202
|
+
else
|
|
203
|
+
path.apply(pathExprs[i]);
|
|
144
204
|
|
|
145
205
|
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
this.applyComponentExprs(lastNode, this.currentComponentProps);
|
|
149
|
-
this.currentComponentProps = {};
|
|
150
|
-
}
|
|
206
|
+
} // end for(path of this.paths)
|
|
207
|
+
|
|
151
208
|
|
|
209
|
+
// TODO: Only do this if we have ExprPaths within styles?
|
|
152
210
|
this.updateStyles();
|
|
153
211
|
|
|
154
212
|
// Invalidate the nodes cache because we just changed it.
|
|
@@ -156,10 +214,12 @@ export default class NodeGroup {
|
|
|
156
214
|
|
|
157
215
|
// If there's leftover expressions, there's probably an issue with the Shell that created this NodeGroup,
|
|
158
216
|
// and the number of paths not matching.
|
|
159
|
-
/*#IFDEV*/
|
|
217
|
+
/*#IFDEV*/
|
|
218
|
+
assert(exprIndex === -1);/*#ENDIF*/
|
|
160
219
|
|
|
161
220
|
|
|
162
|
-
/*#IFDEV*/
|
|
221
|
+
/*#IFDEV*/
|
|
222
|
+
this.verify();/*#ENDIF*/
|
|
163
223
|
}
|
|
164
224
|
|
|
165
225
|
/**
|
|
@@ -183,14 +243,18 @@ export default class NodeGroup {
|
|
|
183
243
|
|
|
184
244
|
// Call render() with the same params that would've been passed to the constructor.
|
|
185
245
|
else if (el.render) {
|
|
186
|
-
let oldHash = Globals.
|
|
187
|
-
if (oldHash !== newHash)
|
|
188
|
-
|
|
246
|
+
let oldHash = Globals.componentArgsHash.get(el);
|
|
247
|
+
if (oldHash !== newHash) {
|
|
248
|
+
let args = {};
|
|
249
|
+
for (let name in props || {})
|
|
250
|
+
args[Util.dashesToCamel(name)] = props[name];
|
|
251
|
+
el.render(args); // Pass new values of props to render so it can decide how it wants to respond.
|
|
252
|
+
}
|
|
189
253
|
}
|
|
190
254
|
|
|
191
|
-
Globals.
|
|
255
|
+
Globals.componentArgsHash.set(el, newHash);
|
|
192
256
|
}
|
|
193
|
-
|
|
257
|
+
|
|
194
258
|
/**
|
|
195
259
|
* We swap the placeholder element for the real element so we can pass its dynamic attributes
|
|
196
260
|
* to its constructor.
|
|
@@ -204,72 +268,48 @@ export default class NodeGroup {
|
|
|
204
268
|
createNewComponent(el, isPreHtmlElement=undefined, props=undefined) {
|
|
205
269
|
if (isPreHtmlElement === undefined)
|
|
206
270
|
isPreHtmlElement = !el.hasAttribute('_is');
|
|
207
|
-
|
|
271
|
+
|
|
208
272
|
let tagName = (isPreHtmlElement
|
|
209
|
-
? el.tagName.
|
|
210
|
-
? el.tagName.slice(0, -21)
|
|
211
|
-
: el.tagName
|
|
273
|
+
? el.tagName.slice(0, -21) // Remove -SOLARITE-PLACEHOLDER
|
|
212
274
|
: el.getAttribute('is')).toLowerCase();
|
|
213
275
|
|
|
214
|
-
|
|
215
|
-
|
|
276
|
+
|
|
277
|
+
// Throw if custom element isn't defined.
|
|
278
|
+
let Constructor = customElements.get(tagName);
|
|
279
|
+
if (!Constructor)
|
|
280
|
+
throw new Error(`The custom tag name ${tagName} is not registered.`)
|
|
281
|
+
|
|
282
|
+
let args = {};
|
|
283
|
+
for (let name in props || {})
|
|
284
|
+
args[Util.dashesToCamel(name)] = props[name];
|
|
285
|
+
|
|
216
286
|
// Pass other attribs to constructor, since otherwise they're not yet set on the element,
|
|
217
287
|
// and the constructor would otherwise have no way to see them.
|
|
218
288
|
if (el.attributes.length) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
289
|
+
for (let attrib of el.attributes) {
|
|
290
|
+
let attribName = Util.dashesToCamel(attrib.name);
|
|
291
|
+
if (!args.hasOwnProperty(attribName))
|
|
292
|
+
args[attribName] = attrib.value;
|
|
293
|
+
}
|
|
224
294
|
}
|
|
225
|
-
|
|
226
|
-
// Create CustomElement and
|
|
227
|
-
let Constructor = customElements.get(tagName);
|
|
228
|
-
if (!Constructor)
|
|
229
|
-
throw new Error(`The custom tag name ${tagName} is not registered.`)
|
|
230
295
|
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
// can add them as children before the rest of the constructor code executes.
|
|
236
|
-
let ch = [... el.childNodes];
|
|
237
|
-
Globals.pendingChildren.push(ch); // pop() is called in Solarite constructor.
|
|
238
|
-
let newEl = new Constructor(props, ch);
|
|
296
|
+
// Create the web component.
|
|
297
|
+
// Get the children that aren't Solarite's comment placeholders.
|
|
298
|
+
let ch = [...el.childNodes].filter(node => node.nodeType !== Node.COMMENT_NODE || !node.nodeValue.startsWith('ExprPath'));
|
|
299
|
+
let newEl = new Constructor(args, ch);
|
|
239
300
|
|
|
240
301
|
if (!isPreHtmlElement)
|
|
241
|
-
newEl.setAttribute('is', el.getAttribute('is').toLowerCase())
|
|
302
|
+
newEl.setAttribute('is', el.getAttribute('is').toLowerCase());
|
|
303
|
+
|
|
304
|
+
// Replace the placeholder tag with the instantiated web component.
|
|
242
305
|
el.replaceWith(newEl);
|
|
243
306
|
|
|
244
|
-
// Set children / slot children
|
|
245
|
-
// TODO: Match named slots.
|
|
246
|
-
// TODO: This only appends to slot if render() is called in the constructor.
|
|
247
|
-
//let slot = newEl.querySelector('slot') || newEl;
|
|
248
|
-
//slot.append(...el.childNodes);
|
|
249
|
-
|
|
250
|
-
// Copy over event attributes.
|
|
251
|
-
for (let propName in props) {
|
|
252
|
-
let val = props[propName];
|
|
253
|
-
if (propName.startsWith('on') && typeof val === 'function')
|
|
254
|
-
newEl.addEventListener(propName.slice(2), e => val(e, newEl));
|
|
255
|
-
|
|
256
|
-
// Bind array based event attributes on value.
|
|
257
|
-
// This same logic is in ExprPath.applyValueAttrib() for non-components.
|
|
258
|
-
if ((propName === 'value' || propName === 'data-value') && Util.isPath(val)) {
|
|
259
|
-
let [obj, path] = [val[0], val.slice(1)];
|
|
260
|
-
newEl.value = delve(obj, path);
|
|
261
|
-
newEl.addEventListener('input', e => {
|
|
262
|
-
delve(obj, path, Util.getInputValue(newEl));
|
|
263
|
-
}, true); // We use capture so we update the values before other events added by the user.
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
307
|
// If an id pointed at the placeholder, update it to point to the new element.
|
|
268
308
|
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
269
309
|
if (id)
|
|
270
310
|
delve(this.getRootNode(), id.split(/\./g), newEl);
|
|
271
|
-
|
|
272
|
-
|
|
311
|
+
|
|
312
|
+
|
|
273
313
|
// Update paths to use replaced element.
|
|
274
314
|
for (let path of this.paths) {
|
|
275
315
|
if (path.nodeMarker === el)
|
|
@@ -281,31 +321,31 @@ export default class NodeGroup {
|
|
|
281
321
|
this.startNode = newEl;
|
|
282
322
|
if (this.endNode === el)
|
|
283
323
|
this.endNode = newEl;
|
|
284
|
-
|
|
285
|
-
|
|
324
|
+
|
|
325
|
+
|
|
286
326
|
// applyComponentExprs() is called because we're rendering.
|
|
287
327
|
// So we want to render the sub-component also.
|
|
288
328
|
if (newEl.renderFirstTime)
|
|
289
329
|
newEl.renderFirstTime();
|
|
290
|
-
|
|
330
|
+
|
|
291
331
|
// Copy attributes over.
|
|
292
332
|
for (let attrib of el.attributes)
|
|
293
333
|
if (attrib.name !== '_is')
|
|
294
334
|
newEl.setAttribute(attrib.name, attrib.value);
|
|
295
335
|
|
|
296
336
|
// Set dynamic attributes if they are primitive types.
|
|
297
|
-
for (let name in
|
|
298
|
-
let val =
|
|
337
|
+
for (let name in props) {
|
|
338
|
+
let val = props[name];
|
|
299
339
|
if (typeof val === 'boolean') {
|
|
300
340
|
if (val !== false && val !== undefined && val !== null)
|
|
301
341
|
newEl.setAttribute(name, '');
|
|
302
342
|
}
|
|
303
343
|
|
|
304
|
-
// If type
|
|
344
|
+
// If type is a non-boolean primitive, set the attribute value.
|
|
305
345
|
else if (['number', 'bigint', 'string'].includes(typeof val))
|
|
306
346
|
newEl.setAttribute(name, val);
|
|
307
347
|
}
|
|
308
|
-
|
|
348
|
+
|
|
309
349
|
return newEl;
|
|
310
350
|
}
|
|
311
351
|
|
|
@@ -349,11 +389,21 @@ export default class NodeGroup {
|
|
|
349
389
|
return this.rootNg;
|
|
350
390
|
}
|
|
351
391
|
|
|
392
|
+
/**
|
|
393
|
+
* Requires the nodeCache to be present. */
|
|
394
|
+
removeAndSaveOrphans() {
|
|
395
|
+
/*#IFDEV*/assert(this.nodesCache);/*#ENDIF*/
|
|
396
|
+
let fragment = document.createDocumentFragment();
|
|
397
|
+
for (let node of this.getNodes())
|
|
398
|
+
fragment.append(node);
|
|
399
|
+
}
|
|
400
|
+
|
|
352
401
|
|
|
353
402
|
updatePaths(fragment, paths, offset) {
|
|
354
403
|
// Update paths to point to the fragment.
|
|
355
|
-
|
|
356
|
-
|
|
404
|
+
let pathLength = paths.length;
|
|
405
|
+
this.paths.length = pathLength;
|
|
406
|
+
for (let i=0; i<pathLength; i++) {
|
|
357
407
|
let path = paths[i].clone(fragment, offset)
|
|
358
408
|
path.parentNg = this;
|
|
359
409
|
this.paths[i] = path;
|
|
@@ -375,23 +425,23 @@ export default class NodeGroup {
|
|
|
375
425
|
* An interleaved array of sets of nodes and top-level ExprPaths
|
|
376
426
|
* @type {(Node|HTMLElement|ExprPath)[]} */
|
|
377
427
|
get nodes() { throw new Error('')};
|
|
378
|
-
|
|
428
|
+
|
|
379
429
|
get debug() {
|
|
380
430
|
return [
|
|
381
431
|
`parentNode: ${this.parentNode?.tagName?.toLowerCase()}`,
|
|
382
432
|
'nodes:',
|
|
383
433
|
...setIndent(this.getNodes().map(item => {
|
|
384
434
|
if (item instanceof Node) {
|
|
385
|
-
|
|
435
|
+
|
|
386
436
|
let tree = nodeToArrayTree(item, nextNode => {
|
|
387
|
-
|
|
388
|
-
let path = this.paths.find(path=>path.type ===
|
|
437
|
+
|
|
438
|
+
let path = this.paths.find(path=>path.type === ExprPathType.Content && path.getNodes().includes(nextNode));
|
|
389
439
|
if (path)
|
|
390
440
|
return [`Path.nodes:`]
|
|
391
|
-
|
|
441
|
+
|
|
392
442
|
return [];
|
|
393
443
|
})
|
|
394
|
-
|
|
444
|
+
|
|
395
445
|
// TODO: How to indend nodes belonging to a path vs those that just occur after the path?
|
|
396
446
|
return flattenAndIndent(tree)
|
|
397
447
|
}
|
|
@@ -402,10 +452,10 @@ export default class NodeGroup {
|
|
|
402
452
|
}
|
|
403
453
|
|
|
404
454
|
get debugNodes() { return this.getNodes() }
|
|
405
|
-
|
|
406
|
-
|
|
455
|
+
|
|
456
|
+
|
|
407
457
|
get debugNodesHtml() { return this.getNodes().map(n => n.outerHTML || n.textContent) }
|
|
408
|
-
|
|
458
|
+
|
|
409
459
|
verify() {
|
|
410
460
|
if (!window.verify)
|
|
411
461
|
return;
|
|
@@ -420,7 +470,7 @@ export default class NodeGroup {
|
|
|
420
470
|
|
|
421
471
|
// if (this.parentPath)
|
|
422
472
|
// assert(this.parentPath.nodeGroups.includes(this));
|
|
423
|
-
|
|
473
|
+
|
|
424
474
|
for (let path of this.paths) {
|
|
425
475
|
assert(path.parentNg === this)
|
|
426
476
|
|
|
@@ -436,61 +486,70 @@ export default class NodeGroup {
|
|
|
436
486
|
}
|
|
437
487
|
//#ENDIF
|
|
438
488
|
|
|
489
|
+
findStaticComponents(root, shell, pathOffset=0) {
|
|
490
|
+
let result = [];
|
|
439
491
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
activateEmbeds(root, shell, pathOffset=0) {
|
|
445
|
-
|
|
446
|
-
// static components. These are WebComponents not created by an expression.
|
|
447
|
-
// Must happen before ids.
|
|
492
|
+
// static components. These are WebComponents that do not have any constructor arguments that are expressions.
|
|
493
|
+
// Those are instead created by applyExpr() which calls applyComponentExprs() which calls createNewcomponent().
|
|
494
|
+
// Maybe someday these two paths will be merged?
|
|
495
|
+
// Must happen before ids because createNewComponent will replace the element.
|
|
448
496
|
for (let path of shell.staticComponents) {
|
|
449
497
|
if (pathOffset)
|
|
450
498
|
path = path.slice(0, -pathOffset);
|
|
451
499
|
let el = resolveNodePath(root, path);
|
|
452
500
|
|
|
453
501
|
// Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
|
|
502
|
+
// Recreating it is necessary so we can pass the constructor args to it.
|
|
454
503
|
if (root !== el/* && !isReplaceEl(root, el)*/) // TODO: is isReplaceEl necessary?
|
|
455
|
-
|
|
504
|
+
result.push(el);
|
|
456
505
|
}
|
|
506
|
+
return result;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
activateStaticComponents(staticComponents) {
|
|
510
|
+
for (let el of staticComponents)
|
|
511
|
+
this.createNewComponent(el)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* @param root {HTMLElement}
|
|
516
|
+
* @param shell {Shell}
|
|
517
|
+
* @param pathOffset {int} */
|
|
518
|
+
activateEmbeds(root, shell, pathOffset=0) {
|
|
457
519
|
|
|
458
520
|
let rootEl = this.rootNg.root;
|
|
459
521
|
if (rootEl) {
|
|
522
|
+
let options = this.rootNg.options;
|
|
460
523
|
|
|
461
524
|
// ids
|
|
462
|
-
if (
|
|
525
|
+
if (options?.ids !== false) {
|
|
463
526
|
for (let path of shell.ids) {
|
|
464
527
|
if (pathOffset)
|
|
465
528
|
path = path.slice(0, -pathOffset);
|
|
466
529
|
let el = resolveNodePath(root, path);
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
// Don't allow overwriting existing class properties if they already have a non-Node value.
|
|
471
|
-
if (rootEl[id] && !(rootEl[id] instanceof Node))
|
|
472
|
-
throw new Error(`${rootEl.constructor.name}.${id} already has a value. ` +
|
|
473
|
-
`Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
|
|
474
|
-
|
|
475
|
-
delve(rootEl, id.split(/\./g), el);
|
|
476
|
-
}
|
|
530
|
+
Util.bindId(rootEl, el);
|
|
531
|
+
}
|
|
477
532
|
}
|
|
478
533
|
|
|
479
534
|
// styles
|
|
480
|
-
if (
|
|
535
|
+
if (options?.styles !== false) {
|
|
481
536
|
if (shell.styles.length)
|
|
482
537
|
this.styles = new Map();
|
|
483
538
|
for (let path of shell.styles) {
|
|
484
539
|
if (pathOffset)
|
|
485
540
|
path = path.slice(0, -pathOffset);
|
|
541
|
+
|
|
542
|
+
/** @type {HTMLStyleElement} */
|
|
486
543
|
let style = resolveNodePath(root, path);
|
|
487
|
-
|
|
488
|
-
|
|
544
|
+
if (rootEl.nodeType === 1) {
|
|
545
|
+
Util.bindStyles(style, rootEl);
|
|
546
|
+
this.styles.set(style, style.textContent);
|
|
547
|
+
}
|
|
489
548
|
}
|
|
490
549
|
|
|
491
550
|
}
|
|
492
551
|
// scripts
|
|
493
|
-
if (
|
|
552
|
+
if (options?.scripts !== false) {
|
|
494
553
|
for (let path of shell.scripts) {
|
|
495
554
|
if (pathOffset)
|
|
496
555
|
path = path.slice(0, -pathOffset);
|
|
@@ -510,6 +569,11 @@ export class RootNodeGroup extends NodeGroup {
|
|
|
510
569
|
* @type {HTMLElement} */
|
|
511
570
|
root;
|
|
512
571
|
|
|
572
|
+
/**
|
|
573
|
+
* When we call renerWatched() we re-render these expressions, then clear this to a new Map()
|
|
574
|
+
* @type {Map<ExprPath, ValueOp|WholeArrayOp|ArraySpliceOp[]>} */
|
|
575
|
+
exprsToRender = new Map();
|
|
576
|
+
|
|
513
577
|
/**
|
|
514
578
|
*
|
|
515
579
|
* @param template
|
|
@@ -524,73 +588,102 @@ export class RootNodeGroup extends NodeGroup {
|
|
|
524
588
|
this.rootNg = this;
|
|
525
589
|
let [fragment, shell] = this.init(template);
|
|
526
590
|
|
|
527
|
-
|
|
528
|
-
let offset = 0;
|
|
529
|
-
let root = fragment; // TODO: Rename so it's not confused with this.root.
|
|
530
|
-
if (el) {
|
|
591
|
+
if (fragment instanceof Text) {
|
|
531
592
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
593
|
+
if (el) {
|
|
594
|
+
this.startNode = el;
|
|
595
|
+
this.endNode = el;
|
|
596
|
+
if (fragment.nodeValue.length)
|
|
597
|
+
el.append(fragment);
|
|
598
|
+
this.root = el;
|
|
537
599
|
}
|
|
600
|
+
Globals.nodeGroups.set(this.root, this);
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
538
603
|
|
|
539
|
-
|
|
604
|
+
// If adding NodeGroup to an element.
|
|
605
|
+
let offset = 0;
|
|
606
|
+
let root = fragment; // TODO: Rename so it's not confused with this.root.
|
|
607
|
+
if (el) {
|
|
608
|
+
Globals.nodeGroups.set(el, this);
|
|
609
|
+
|
|
610
|
+
// Save slot children
|
|
611
|
+
let slotChildren;
|
|
612
|
+
if (el.childNodes.length) {
|
|
613
|
+
slotChildren = document.createDocumentFragment();
|
|
614
|
+
slotChildren.append(...el.childNodes);
|
|
615
|
+
}
|
|
540
616
|
|
|
541
|
-
|
|
542
|
-
if (isReplaceEl(fragment, el)) {
|
|
543
|
-
el.append(...fragment.children[0].childNodes);
|
|
617
|
+
this.root = el;
|
|
544
618
|
|
|
545
|
-
//
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
el.setAttribute(attrib.name, attrib.value);
|
|
619
|
+
// If el should replace the root node of the fragment.
|
|
620
|
+
if (isReplaceEl(fragment, el)) {
|
|
621
|
+
el.append(...fragment.children[0].childNodes);
|
|
549
622
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
623
|
+
// Copy attributes
|
|
624
|
+
for (let attrib of fragment.children[0].attributes)
|
|
625
|
+
if (!el.hasAttribute(attrib.name))
|
|
626
|
+
el.setAttribute(attrib.name, attrib.value);
|
|
627
|
+
|
|
628
|
+
// Go one level deeper into all of shell's paths.
|
|
629
|
+
offset = 1;
|
|
630
|
+
} else {
|
|
631
|
+
let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
|
|
632
|
+
if (!isEmpty)
|
|
633
|
+
el.append(...fragment.childNodes);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Setup children
|
|
637
|
+
if (slotChildren) {
|
|
558
638
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
639
|
+
// Named slots
|
|
640
|
+
for (let slot of el.querySelectorAll('slot[name]')) {
|
|
641
|
+
let name = slot.getAttribute('name')
|
|
642
|
+
if (name) {
|
|
643
|
+
let slotChildren2 = slotChildren.querySelectorAll(`[slot='${name}']`);
|
|
644
|
+
slot.append(...slotChildren2);
|
|
645
|
+
}
|
|
566
646
|
}
|
|
647
|
+
|
|
648
|
+
// Unnamed slots
|
|
649
|
+
let unamedSlot = el.querySelector('slot:not([name])')
|
|
650
|
+
if (unamedSlot)
|
|
651
|
+
unamedSlot.append(slotChildren);
|
|
652
|
+
|
|
653
|
+
// No slots
|
|
654
|
+
else
|
|
655
|
+
el.append(slotChildren);
|
|
567
656
|
}
|
|
568
|
-
let unamedSlot = el.querySelector('slot:not([name])')
|
|
569
|
-
if (unamedSlot)
|
|
570
|
-
unamedSlot.append(slotFragment);
|
|
571
|
-
else
|
|
572
|
-
el.append(slotFragment);
|
|
573
|
-
}
|
|
574
657
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
root
|
|
584
|
-
|
|
658
|
+
root = el;
|
|
659
|
+
|
|
660
|
+
this.startNode = el;
|
|
661
|
+
this.endNode = el;
|
|
662
|
+
} else {
|
|
663
|
+
let singleEl = getSingleEl(fragment);
|
|
664
|
+
this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
|
|
665
|
+
|
|
666
|
+
Globals.nodeGroups.set(this.root, this);
|
|
667
|
+
if (singleEl) {
|
|
668
|
+
root = singleEl;
|
|
669
|
+
offset = 1;
|
|
670
|
+
}
|
|
585
671
|
}
|
|
586
|
-
}
|
|
587
672
|
|
|
588
|
-
|
|
673
|
+
this.updatePaths(root, shell.paths, offset);
|
|
674
|
+
|
|
675
|
+
// Static web components can sometimes have children created via expressions.
|
|
676
|
+
// But calling applyExprs() will mess up the shell's path to them.
|
|
677
|
+
// So we find them first, then call activateStaticComponents() after their children have been created.
|
|
678
|
+
let staticComponents = this.findStaticComponents(root, shell, offset);
|
|
679
|
+
|
|
680
|
+
this.activateEmbeds(root, shell, offset);
|
|
589
681
|
|
|
590
|
-
|
|
682
|
+
// Apply exprs
|
|
683
|
+
this.applyExprs(template.exprs);
|
|
591
684
|
|
|
592
|
-
|
|
593
|
-
|
|
685
|
+
this.activateStaticComponents(staticComponents);
|
|
686
|
+
}
|
|
594
687
|
}
|
|
595
688
|
}
|
|
596
689
|
|
|
@@ -612,7 +705,7 @@ function getSingleEl(fragment) {
|
|
|
612
705
|
* @param el {HTMLElement}
|
|
613
706
|
* @returns {boolean} */
|
|
614
707
|
function isReplaceEl(fragment, el) {
|
|
615
|
-
return
|
|
616
|
-
&&
|
|
708
|
+
return fragment.children.length===1
|
|
709
|
+
&& el.tagName.includes('-')
|
|
617
710
|
&& fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') === el.tagName;
|
|
618
711
|
}
|