solarite 0.4.0 → 0.5.1

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/src/NodeGroup.js CHANGED
@@ -1,10 +1,11 @@
1
- import {assert} from "./assert.js";
1
+ import assert from "./assert.js";
2
2
  import Util, {flattenAndIndent, nodeToArrayTree, setIndent} from "./Util.js";
3
- import delve from "./delve.js";
4
3
  import Shell from "./Shell.js";
5
4
  import RootNodeGroup from './RootNodeGroup.js';
6
- import ExprPath, {ExprPathType, resolveNodePath} from "./ExprPath.js";
5
+ import Path from "./Path.js";
7
6
  import Globals from './Globals.js';
7
+ import PathToComponent from "./PathToComponent.js";
8
+ import PathToNodes from "./PathToNodes.js";
8
9
 
9
10
  /** @typedef {boolean|string|number|function|Object|Array|Date|Node|Template} Expr */
10
11
 
@@ -13,17 +14,14 @@ import Globals from './Globals.js';
13
14
  *
14
15
  * The range is determined by startNode and nodeMarker.
15
16
  * startNode - never null. An empty text node is created before the first path if none exists.
16
- * nodeMarker - null if this Nodegroup is at the end of its parents' nodes.
17
- *
18
- *
19
- * */
17
+ * nodeMarker - null if this Nodegroup is at the end of its parents' nodes.*/
20
18
  export default class NodeGroup {
21
19
 
22
20
  /**
23
21
  * @Type {RootNodeGroup} */
24
22
  rootNg;
25
23
 
26
- /** @type {ExprPath} */
24
+ /** @type {Path} */
27
25
  parentPath;
28
26
 
29
27
  /** @type {Node|HTMLElement} First node of NodeGroup. Should never be null. */
@@ -31,10 +29,10 @@ export default class NodeGroup {
31
29
 
32
30
  /** @type {Node|HTMLElement} A node that never changes that this NodeGroup should always insert its nodes before.
33
31
  * An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.
34
- * TODO: But sometimes startNode and endNode point to the same node. Document htis inconsistency. */
32
+ * TODO: But sometimes startNode and endNode point to the same node. Document this inconsistency. */
35
33
  endNode;
36
34
 
37
- /** @type {ExprPath[]} */
35
+ /** @type {Path[]} */
38
36
  paths = [];
39
37
 
40
38
  /** @type {string} Key that matches the template and the expressions. */
@@ -54,283 +52,220 @@ export default class NodeGroup {
54
52
  * @type {?Map<HTMLStyleElement, string>} */
55
53
  styles;
56
54
 
57
- dynamicComponents = new Set();
58
- staticComponents = [];
59
-
60
55
  /** @type {Template} */
61
56
  template;
62
57
 
58
+ /**
59
+ * Root node at the top of the hierarchy.
60
+ * Should be moved to RootNodeGroup
61
+ * @type {HTMLElement} */
62
+ root;
63
+
63
64
 
64
65
  /**
65
66
  * Create an "instantiated" NodeGroup from a Template and add it to an element.
67
+ * Don't call applyExprs() yet to apply expressions or instantiate components yet.
66
68
  * @param template {Template} Create it from the html strings and expressions in this template.
67
- * @param parentPath {?ExprPath} */
68
- constructor(template, parentPath=null) {
69
+ * @param parentPath {?Path}
70
+ * @param el {?HTMLElement} Optional, pre-existing htmlElement that will be the root.
71
+ * @param options {?object} Only used for RootNodeGroup */
72
+ constructor(template, parentPath=null, el=null, options=null) {
69
73
  this.rootNg = parentPath?.parentNg?.rootNg || this;
70
74
  this.parentPath = parentPath;
71
75
 
72
- if (!(this instanceof RootNodeGroup)) {
73
-
74
- let [fragment, shell] = this.populateFromTemplate(template);
75
-
76
- if (fragment && template.exprs.length) {
77
- this.updatePaths(fragment, shell.paths);
78
-
79
- // Static web components can sometimes have children created via expressions.
80
- // But calling applyExprs() will mess up the shell's path to them.
81
- // So we find them first, then call instantiateStaticComponents() after their children have been created.
82
- this.staticComponents = this.findStaticComponents(fragment, shell);
83
-
84
- this.activateEmbeds(fragment, shell);
85
-
86
- // Apply exprs
87
- this.applyExprs(template.exprs);
88
-
89
- this.instantiateStaticComponents(this.staticComponents);
90
- }
91
- else if (shell)
92
- this.activateEmbeds(fragment, shell);
93
- }
94
- }
95
-
96
- /**
97
- * Common init shared by RootNodeGroup and NodeGroup constructors.
98
- * But in a separate function because they need to do this at a different step.
99
- * @param template {Template} Create it from the html strings and expressions in this template.
100
- * @returns {[DocumentFragment, Shell]} The Shell created from the template,a nd the fragment cloned from the Shell.*/
101
- populateFromTemplate(template) {
102
76
  /*#IFDEV*/assert(this.rootNg);/*#ENDIF*/
103
77
  this.template = template;
104
- this.exactKey = template.getExactKey();
105
78
  this.closeKey = template.getCloseKey();
106
79
 
107
80
  // If it's just a text node, skip a bunch of unnecessary steps.
108
81
  if (template.isText) {
109
- let textNode = Globals.doc.createTextNode(template.html[0]);
110
- this.startNode = this.endNode = textNode;
111
- return [];
82
+ this.startNode = this.endNode = Globals.doc.createTextNode(template.html[0]);
112
83
  }
113
84
 
114
- // Get a cached version of the parsed and instantiated html, and ExprPaths:
115
85
  else {
116
- let shell = Shell.get(template.html);
117
- let fragment = shell.fragment.cloneNode(true);
86
+ // Get a cached version of the parsed and instantiated html, and Paths:
87
+ const shell = Shell.get(template.html);
88
+ const shellFragment = shell.fragment.cloneNode(true);
118
89
 
119
- if (fragment?.nodeType === 11) { // DocumentFragment
120
- let childNodes = fragment.childNodes;
121
- this.startNode = childNodes[0];
122
- this.endNode = childNodes[childNodes.length - 1];
123
- }
124
- else
125
- this.startNode = this.endNode = fragment;
90
+ if (shellFragment.nodeType === 11) { // DocumentFragment
91
+ this.startNode = shellFragment.firstChild;
92
+ this.endNode = shellFragment.lastChild;
93
+ } else
94
+ this.startNode = this.endNode = shellFragment;
126
95
 
127
- return [fragment, shell];
128
- }
129
- }
130
96
 
131
- /**
132
- * Use the paths to insert the given expressions.
133
- * Dispatches expression handling to other functions depending on the path type.
134
- * @param exprs {(*|*[]|function|Template)[]}
135
- * @param paths {?ExprPath[]} Optional. Only used for testing. Normally uses this.paths. */
136
- applyExprs(exprs, paths=null) {
137
- paths = paths || this.paths;
138
-
139
- /*#IFDEV*/
140
- this.verify();/*#ENDIF*/
97
+ // Special setup for RootNodeGroup
98
+ if (this instanceof RootNodeGroup) {
141
99
 
142
- // Things to consider:
143
- // 1. One path may use multipe expressions. E.g. <div class="${1} ${2}">
144
- // 2. One component may need to use multiple attribute paths to be instantiated.
145
- // 3. We apply them in reverse order so that a <select> box has its children created from an expression
146
- // before its instantiated and its value attribute is set via an expression.
147
-
148
- let exprIndex = exprs.length - 1; // Update exprs at paths.
149
- let lastComponentPathIndex;
150
- 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.
151
- for (let i = paths.length - 1, path; path = paths[i]; i--) {
152
- let prevPath = paths[i - 1];
153
- let nextPath = paths[i + 1];
154
-
155
- // Get the expressions associated with this path.
156
- if (path.attrValue?.length > 2) {
157
- let startIndex = (exprIndex - (path.attrValue.length - 1)) + 1;
158
- pathExprs[i] = exprs.slice(startIndex, exprIndex + 1); // probably doesn't allocate if the JS vm implements copy on write.
159
- exprIndex -= pathExprs[i].length;
160
- } else {
161
- pathExprs[i] = [exprs[exprIndex]];
162
- exprIndex--;
163
- }
164
100
 
101
+ let startingPathDepth = 0;
102
+ this.options = options;
103
+ if (shellFragment instanceof Text) {
104
+ if (!el)
105
+ throw new Error('Cannot create a standalone text node');
165
106
 
166
- // TODO: Need to end and restart this block when going from one component to the next?
167
- // Think of having two adjacent components.
168
- // But the dynamicAttribsAdjacet test already passes.
107
+ this.root = el;
108
+ if (shellFragment.nodeValue.length)
109
+ this.root.append(shellFragment);
110
+ }
169
111
 
170
- // If expr is an attribute in a component:
171
- // 1. Instantiate it if it hasn't already been, sending all expr's to its constructor.
172
- // 2. Otherwise send them to its render function.
173
- // Components with no expressions as attributes are instead activated in activateEmbeds().
174
- if (path.nodeMarker !== this.rootNg.root && path.isComponent) {
112
+ else {
113
+ if (el) {
114
+ this.root = el;
115
+
116
+ // Save slot
117
+ // 1. Globals.currentSlotChildren is set if this is called via PathToComponent.applyComponent() calls render()
118
+ // 2. el.childNodes is set if render() is called manually for the first time.
119
+ let slotChildren;
120
+ if (Globals.currentSlotChildren || el.childNodes.length) {
121
+ slotChildren = Globals.doc.createDocumentFragment();
122
+ slotChildren.append(...(Globals.currentSlotChildren || el.childNodes));
123
+ }
124
+
125
+ // If el should replace the root node of the fragment.
126
+ if (isReplaceEl(shellFragment, this.root.tagName)) {
127
+ this.root.append(...shellFragment.children[0].childNodes);
128
+
129
+ // Copy attributes
130
+ for (let attrib of shellFragment.children[0].attributes)
131
+ if (!this.root.hasAttribute(attrib.name))
132
+ this.root.setAttribute(attrib.name, attrib.value);
133
+
134
+ // Go one level deeper into all of shell's paths.
135
+ startingPathDepth = 1;
136
+ }
137
+
138
+ else {
139
+ let isEmpty = shellFragment.childNodes.length === 1 && shellFragment.childNodes[0].nodeType === 3 && shellFragment.childNodes[0].textContent === '';
140
+ if (!isEmpty)
141
+ this.root.append(...shellFragment.childNodes);
142
+ }
143
+
144
+
145
+ // Setup slot children (deprecated)
146
+ if (slotChildren) {
147
+ // Named slots
148
+ for (let slot of el.querySelectorAll('slot[name]')) {
149
+ let name = slot.getAttribute('name')
150
+ if (name) {
151
+ let slotChildren2 = slotChildren.querySelectorAll(`[slot='${name}']`);
152
+ slot.append(...slotChildren2);
153
+ }
154
+ }
155
+ // Unnamed slots
156
+ let unamedSlot = el.querySelector('slot:not([name])')
157
+ if (unamedSlot)
158
+ unamedSlot.append(slotChildren);
159
+ // No slots
160
+ else
161
+ el.append(slotChildren);
162
+ }
163
+ }
175
164
 
176
- if (!nextPath || !nextPath.isComponent || nextPath.nodeMarker !== path.nodeMarker)
177
- lastComponentPathIndex = i;
178
- let isFirstComponentPath = !prevPath || !prevPath.isComponent || prevPath.nodeMarker !== path.nodeMarker;
165
+ // Instantiate as a standalone element.
166
+ else {
167
+ let onlyChild = getSingleEl(shellFragment);
168
+ this.root = onlyChild || shellFragment; // We return the whole fragment when calling h() with a collection of nodes.
169
+ if (onlyChild)
170
+ startingPathDepth = 1;
171
+ }
179
172
 
180
- if (isFirstComponentPath) {
173
+ // Exclude the path to ourself. Otherwise we get infinite recursion.
174
+ // let paths = [...shell.paths];
175
+ // if (paths[0] instanceof PathToComponent)
176
+ // paths.shift();
181
177
 
182
- let componentProps = {}
183
- for (let j=i; j<=lastComponentPathIndex; j++) {
184
- let attrName = paths[j].attrName; // Util.dashesToCamel(paths[j].attrName);
185
- componentProps[attrName] = pathExprs[j].length > 1 ? pathExprs[j].join('') : pathExprs[j][0];
186
- }
178
+ this.setPathsFromFragment(this.root, shell.paths, startingPathDepth);
179
+ this.activateEmbeds(this.root, shell, startingPathDepth);
180
+ }
181
+ this.startNode = this.endNode = this.root;
187
182
 
188
- this.handleComponent(path.nodeMarker, componentProps, true);
183
+ Globals.rootNodeGroups.set(this.root, this);
184
+ } // end if RootNodeGroup
189
185
 
190
- // Set attributes on component.
191
- for (let j=i; j<=lastComponentPathIndex; j++)
192
- paths[j].apply(pathExprs[j]);
186
+ else if (shell) {
187
+ if (shell.paths.length) {
188
+ this.setPathsFromFragment(shellFragment, shell.paths);
193
189
  }
190
+
191
+ this.activateEmbeds(shellFragment, shell);
194
192
  }
193
+ }
195
194
 
196
- // Else apply it normally
197
- else
198
- path.apply(pathExprs[i]);
195
+ //#IFDEV
196
+ this.verify();
197
+ //#ENDIF
198
+ }
199
199
 
200
200
 
201
- } // end for(path of this.paths)
201
+ /**
202
+ * Use the paths to insert the given expressions.
203
+ * Dispatches expression handling to other functions depending on the path type.
204
+ * @param exprs {(*|*[]|function|Template)[]}
205
+ * @param changed {boolean} If true, the expr's have changed since the last time thsi function was called.
206
+ * @param includeNonComponents {boolean}
207
+ * We still need to call PathToComponent.apply() even if changed=false so the user can handle the rendering. */
208
+ applyExprs(exprs, changed=true, includeNonComponents=true) {
202
209
 
210
+ /*#IFDEV*/
211
+ this.verify();
212
+ /*#ENDIF*/
203
213
 
204
- // TODO: Only do this if we have ExprPaths within styles?
205
- this.updateStyles();
214
+ let paths = this.paths;
206
215
 
207
- // Call render() on static web components. This makes the component.staticAttribs() test work.
208
- for (let el of this.staticComponents)
209
- if (el.render)
210
- el.render(Util.attribsToObject(el)); // It has no expressions.
216
+ // Things to consider:
217
+ // 1. Paths consume a varying number of expressions.
218
+ // An PathToAttribs may use multipe expressions. E.g. <div class="${1} ${2}">
219
+ // While an PathToComponent uses zero.
220
+ // 2. An PathToComponent references other Paths that set its attribute values.
221
+ // 3. We apply them in reverse order so that a <select> box has its children created from an expression
222
+ // before its instantiated and its value attribute is set via an expression.
223
+ let exprIndex = exprs.length; // Update exprs at paths.
224
+ 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.
225
+ for (let i = paths.length - 1, path; path = paths[i]; i--) {
226
+ if (i===0 && path instanceof PathToComponent && path.nodeMarker === this.getRootNode())
227
+ continue;
211
228
 
212
- // Invalidate the nodes cache because we just changed it.
213
- this.nodesCache = null;
229
+ // Get the expressions associated with this path.
230
+ let exprCount = path.getExpressionCount();
231
+ pathExprs[i] = exprs.slice(exprIndex-exprCount, exprIndex); // slice() probably doesn't allocate if the JS vm implements copy on write.
232
+ exprIndex -= exprCount;
233
+
234
+ // Component expressions don't have a corresponding user-provided expression.
235
+ // They use expressions from the paths that provide their attributes.
236
+ if (path instanceof PathToComponent) {
237
+ let attribExprs = pathExprs.slice(i+1, i+1 + path.attribPaths.length); // +1 b/c we move forward from the component path.
238
+ path.apply(attribExprs, true, changed);
239
+ }
240
+ else if (includeNonComponents)
241
+ path.apply(pathExprs[i]);
242
+ }
214
243
 
215
244
  // If there's leftover expressions, there's probably an issue with the Shell that created this NodeGroup,
216
245
  // and the number of paths not matching.
217
246
  /*#IFDEV*/
218
- assert(exprIndex === -1);/*#ENDIF*/
247
+ assert(exprIndex === 0);
248
+ /*#ENDIF*/
219
249
 
220
250
 
221
- /*#IFDEV*/
222
- this.verify();/*#ENDIF*/
223
- }
251
+ if (includeNonComponents) {
252
+
253
+ // TODO: Only do this if we have Paths within styles?
254
+ this.updateStyles();
255
+
256
+ // Invalidate the nodes cache because we just changed it.
257
+ this.nodesCache = null;
224
258
 
225
- /**
226
- * Unified path to ensure a child component is instantiated (if placeholder) and optionally rendered.
227
- * @param el {HTMLElement}
228
- * @param props {?Object}
229
- * @param doRender {boolean}
230
- * @return {HTMLElement} The (possibly replaced) element. */
231
- handleComponent(el, props=null, doRender=true) {
232
- let isPreHtmlElement = el.hasAttribute('solarite-placeholder');
233
- let isPreIsElement = el.hasAttribute('_is');
234
- let attribs, children;
235
- if (isPreHtmlElement || isPreIsElement)
236
- [el, attribs, children] = this.instantiateComponent(el, isPreHtmlElement, props);
237
- if (doRender && el.render) {
238
- if (!attribs) {
239
- attribs = Util.attribsToObject(el);
240
- for (let name in props || {})
241
- attribs[Util.dashesToCamel(name)] = props[name];
242
- children = el.childNodes;
243
- }
244
- el.render(attribs, children);
245
259
  }
246
- return el;
260
+
261
+ /*#IFDEV*/
262
+ this.verify();
263
+ /*#ENDIF*/
247
264
  }
248
-
249
- /**
250
- * We swap the placeholder element for the real element so we can pass its dynamic attributes
251
- * to its constructor.
252
- * This is only called by handleComponent()
253
- * This does not call render()
254
- *
255
- * @param el {HTMLElement}
256
- * @param isPreHtmlElement {?boolean} True if the element's tag name ends with -solarite-placeholder
257
- * @param props {Object} Attributes with dynamic values.
258
- * @return {[HTMLElement, attribs:Object, children:Node[]]}} */
259
- instantiateComponent(el, isPreHtmlElement=undefined, props=undefined) {
260
- if (isPreHtmlElement === undefined)
261
- isPreHtmlElement = !el.hasAttribute('_is');
262
-
263
- let tagName = (isPreHtmlElement
264
- ? el.tagName.slice(0, -21) // Remove -SOLARITE-PLACEHOLDER
265
- : el.getAttribute('is')).toLowerCase();
266
-
267
-
268
- // Throw if custom element isn't defined.
269
- let Constructor = customElements.get(tagName);
270
- if (!Constructor)
271
- throw new Error(`The custom tag name ${tagName} is not registered.`)
272
-
273
- // Pass other attribs to constructor, since otherwise they're not yet set on the element,
274
- // and the constructor would otherwise have no way to see them.
275
- let attribs = Util.attribsToObject(el, 'solarite-placeholder');
276
- for (let name in props || {})
277
- attribs[Util.dashesToCamel(name)] = props[name];
278
-
279
-
280
- // Create the web component.
281
- // Get the children that aren't Solarite's comment placeholders.
282
- let children = [...el.childNodes].filter(node => node.nodeType !== Node.COMMENT_NODE || !node.nodeValue.startsWith('ExprPath'));
283
- let newEl = new Constructor(attribs, children);
284
-
285
- if (!isPreHtmlElement)
286
- newEl.setAttribute('is', el.getAttribute('is').toLowerCase());
287
-
288
- // Replace the placeholder tag with the instantiated web component.
289
- el.replaceWith(newEl);
290
-
291
- // If an id pointed at the placeholder, update it to point to the new element.
292
- let id = el.getAttribute('data-id') || el.getAttribute('id');
293
- if (id)
294
- delve(this.getRootNode(), id.split(/\./g), newEl);
295
-
296
-
297
- // Update paths to use replaced element.
298
- for (let path of this.paths) {
299
- if (path.nodeMarker === el)
300
- path.nodeMarker = newEl;
301
- if (path.nodeBefore === el)
302
- path.nodeBefore = newEl;
303
- }
304
- if (this.startNode === el)
305
- this.startNode = newEl;
306
- if (this.endNode === el)
307
- this.endNode = newEl;
308
-
309
- // This is used only if inheriting from the Solarite class.
310
- // applyComponentExprs() is called because we're rendering.
311
- // So we want to render the sub-component also.
312
- if (newEl.renderFirstTime)
313
- newEl.renderFirstTime();
314
-
315
- // Copy attributes over.
316
- for (let attrib of el.attributes)
317
- if (attrib.name !== '_is' && attrib.name !== 'solarite-placeholder')
318
- newEl.setAttribute(attrib.name, attrib.value);
319
-
320
- // Set dynamic attributes if they are primitive types.
321
- for (let name in props) {
322
- let val = props[name];
323
- if (typeof val === 'boolean') {
324
- if (val !== false && val !== undefined && val !== null)
325
- newEl.setAttribute(name, '');
326
- }
327
265
 
328
- // If type is a non-boolean primitive, set the attribute value.
329
- else if (['number', 'bigint', 'string'].includes(typeof val))
330
- newEl.setAttribute(name, val);
331
- }
266
+ // TODO: Give it a better name.
267
+ applyExprs2(exprs) {
332
268
 
333
- return [newEl, attribs, children];
334
269
  }
335
270
 
336
271
  /**
@@ -356,10 +291,6 @@ export default class NodeGroup {
356
291
  return result;
357
292
  }
358
293
 
359
- getParentNode() {
360
- return this.startNode?.parentNode
361
- }
362
-
363
294
  /**
364
295
  * Get the root element of the NodeGroup's RootNodeGroup.
365
296
  * @returns {HTMLElement|DocumentFragment} */
@@ -374,21 +305,12 @@ export default class NodeGroup {
374
305
  }
375
306
 
376
307
  /**
377
- * Requires the nodeCache to be present. */
378
- removeAndSaveOrphans() {
379
- /*#IFDEV*/assert(this.nodesCache);/*#ENDIF*/
380
- let fragment = Globals.doc.createDocumentFragment();
381
- for (let node of this.getNodes())
382
- fragment.append(node);
383
- }
384
-
385
-
386
- /**
387
- * @param fragment {DocumentFragment}
308
+ * Copy paths in fragment to this.paths.
309
+ * @param fragment {DocumentFragment|HTMLElement}
388
310
  * @param paths
389
311
  * @param startingPathDepth {int} */
390
- updatePaths(fragment, paths, startingPathDepth) {
391
- let pathLength = paths.length;
312
+ setPathsFromFragment(fragment, paths, startingPathDepth=0) {
313
+ let pathLength = paths.length; // For faster iteration
392
314
  this.paths.length = pathLength;
393
315
  for (let i=0; i<pathLength; i++) {
394
316
  let path = paths[i].clone(fragment, startingPathDepth)
@@ -406,12 +328,59 @@ export default class NodeGroup {
406
328
  }
407
329
  }
408
330
 
409
- //#IFDEV
410
331
  /**
411
- * @deprecated
412
- * An interleaved array of sets of nodes and top-level ExprPaths
413
- * @type {(Node|HTMLElement|ExprPath)[]} */
414
- get nodes() { throw new Error('')};
332
+ * @param root {HTMLElement|DocumentFragment}
333
+ * @param shell {Shell}
334
+ * @param pathOffset {int} */
335
+ activateEmbeds(root, shell, pathOffset=0) {
336
+
337
+ let rootEl = this.rootNg.root;
338
+ if (rootEl) {
339
+ let options = this.rootNg.options;
340
+
341
+ // ids
342
+ if (options?.ids !== false) {
343
+ for (let path of shell.ids) {
344
+ if (pathOffset)
345
+ path = path.slice(0, -pathOffset);
346
+ let el = Path.resolve(root, path);
347
+ Util.bindId(rootEl, el);
348
+ }
349
+ }
350
+
351
+ // styles
352
+ if (options?.styles !== false) {
353
+ if (shell.styles.length)
354
+ this.styles = new Map();
355
+ for (let path of shell.styles) {
356
+ if (pathOffset)
357
+ path = path.slice(0, -pathOffset);
358
+
359
+ /** @type {HTMLStyleElement} */
360
+ let style = Path.resolve(root, path);
361
+ if (rootEl.nodeType === 1) {
362
+ Util.bindStyles(style, rootEl);
363
+ this.styles.set(style, style.textContent);
364
+ }
365
+ }
366
+
367
+ }
368
+ // scripts
369
+ if (options?.scripts !== false) {
370
+ for (let path of shell.scripts) {
371
+ if (pathOffset)
372
+ path = path.slice(0, -pathOffset);
373
+ let script = Path.resolve(root, path);
374
+ eval(script.textContent)
375
+ }
376
+ }
377
+ }
378
+ }
379
+
380
+ //#IFDEV
381
+ getParentNode() {
382
+ return this.startNode?.parentNode
383
+ }
415
384
 
416
385
  get debug() {
417
386
  return [
@@ -422,7 +391,7 @@ export default class NodeGroup {
422
391
 
423
392
  let tree = nodeToArrayTree(item, nextNode => {
424
393
 
425
- let path = this.paths.find(path=>path.type === ExprPathType.Content && path.getNodes().includes(nextNode));
394
+ let path = this.paths.find(path=>(path instanceof PathToNodes) && path.getNodes().includes(nextNode));
426
395
  if (path)
427
396
  return [`Path.nodes:`]
428
397
 
@@ -432,7 +401,7 @@ export default class NodeGroup {
432
401
  // TODO: How to indend nodes belonging to a path vs those that just occur after the path?
433
402
  return flattenAndIndent(tree)
434
403
  }
435
- else if (item instanceof ExprPath)
404
+ else if (item instanceof Path)
436
405
  return setIndent(item.debug, 1)
437
406
  }).flat(), 1)
438
407
  ]
@@ -472,81 +441,30 @@ export default class NodeGroup {
472
441
  return true;
473
442
  }
474
443
  //#ENDIF
444
+ }
475
445
 
476
- findStaticComponents(root, shell, startingPathDepth=0) {
477
- let result = [];
478
-
479
- // static components. These are WebComponents that do not have any constructor arguments that are expressions.
480
- // Those are instead created by applyExpr() which calls applyComponentExprs() which calls instantiateComponent().
481
- // Maybe someday these two paths will be merged?
482
- // Must happen before ids because instantiateComponent will replace the element.
483
- for (let path of shell.staticComponents) {
484
- if (startingPathDepth)
485
- path = path.slice(0, -startingPathDepth);
486
- let el = resolveNodePath(root, path);
487
-
488
- // Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
489
- // Recreating it is necessary so we can pass the constructor args to it.
490
- if (root !== el/* && !isReplaceEl(root, el)*/) // TODO: is isReplaceEl necessary?
491
- result.push(el);
492
- }
493
- return result;
494
- }
495
-
496
- instantiateStaticComponents(staticComponents) {
497
- // TODO: Why do we not call render() on the static component here? The tests pass either way.
498
- for (let i in staticComponents)
499
- staticComponents[i] = this.handleComponent(staticComponents[i], null, false);
500
- }
501
-
502
- /**
503
- * @param root {HTMLElement|DocumentFragment}
504
- * @param shell {Shell}
505
- * @param pathOffset {int} */
506
- activateEmbeds(root, shell, pathOffset=0) {
507
-
508
- let rootEl = this.rootNg.root;
509
- if (rootEl) {
510
- let options = this.rootNg.options;
511
-
512
- // ids
513
- if (options?.ids !== false) {
514
- for (let path of shell.ids) {
515
- if (pathOffset)
516
- path = path.slice(0, -pathOffset);
517
- let el = resolveNodePath(root, path);
518
- Util.bindId(rootEl, el);
519
- }
520
- }
521
446
 
522
- // styles
523
- if (options?.styles !== false) {
524
- if (shell.styles.length)
525
- this.styles = new Map();
526
- for (let path of shell.styles) {
527
- if (pathOffset)
528
- path = path.slice(0, -pathOffset);
529
447
 
530
- /** @type {HTMLStyleElement} */
531
- let style = resolveNodePath(root, path);
532
- if (rootEl.nodeType === 1) {
533
- Util.bindStyles(style, rootEl);
534
- this.styles.set(style, style.textContent);
535
- }
536
- }
537
448
 
538
- }
539
- // scripts
540
- if (options?.scripts !== false) {
541
- for (let path of shell.scripts) {
542
- if (pathOffset)
543
- path = path.slice(0, -pathOffset);
544
- let script = resolveNodePath(root, path);
545
- eval(script.textContent)
546
- }
547
- }
449
+ function getSingleEl(fragment) {
450
+ let nonempty = [];
451
+ for (let n of fragment.childNodes) {
452
+ if (n.nodeType === 1 || n.nodeType === 3 && n.textContent.trim().length) {
453
+ if (nonempty.length)
454
+ return null;
455
+ nonempty.push(n);
548
456
  }
549
457
  }
458
+ return nonempty[0];
550
459
  }
551
460
 
552
-
461
+ /**
462
+ * Does the fragment have one child that's an element matching the tagname of el?
463
+ * @param fragment {DocumentFragment}
464
+ * @param tagName {string}
465
+ * @returns {boolean} */
466
+ function isReplaceEl(fragment, tagName) {
467
+ return fragment.children.length===1
468
+ && tagName.includes('-')
469
+ && fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') === tagName;
470
+ }