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