solarite 0.3.2 → 0.5.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/src/NodeGroup.js CHANGED
@@ -1,11 +1,11 @@
1
- import {assert} from "./assert.js";
2
- import ExprPath, {ExprPathType, resolveNodePath} from "./ExprPath.js";
3
- import {getObjectHash} from "./hash.js";
4
- import Shell from "./Shell.js";
1
+ import assert from "./assert.js";
5
2
  import Util, {flattenAndIndent, nodeToArrayTree, setIndent} from "./Util.js";
6
- //import NodeGroupManager from "./NodeGroupManager.js";
7
- import delve from "./delve.js";
8
- import Globals from "./Globals.js";
3
+ import Shell from "./Shell.js";
4
+ import RootNodeGroup from './RootNodeGroup.js';
5
+ import Path from "./Path.js";
6
+ import Globals from './Globals.js';
7
+ import PathToComponent from "./PathToComponent.js";
8
+ import PathToNodes from "./PathToNodes.js";
9
9
 
10
10
  /** @typedef {boolean|string|number|function|Object|Array|Date|Node|Template} Expr */
11
11
 
@@ -14,17 +14,14 @@ import Globals from "./Globals.js";
14
14
  *
15
15
  * The range is determined by startNode and nodeMarker.
16
16
  * startNode - never null. An empty text node is created before the first path if none exists.
17
- * nodeMarker - null if this Nodegroup is at the end of its parents' nodes.
18
- *
19
- *
20
- * */
17
+ * nodeMarker - null if this Nodegroup is at the end of its parents' nodes.*/
21
18
  export default class NodeGroup {
22
19
 
23
20
  /**
24
21
  * @Type {RootNodeGroup} */
25
22
  rootNg;
26
23
 
27
- /** @type {ExprPath} */
24
+ /** @type {Path} */
28
25
  parentPath;
29
26
 
30
27
  /** @type {Node|HTMLElement} First node of NodeGroup. Should never be null. */
@@ -32,10 +29,10 @@ export default class NodeGroup {
32
29
 
33
30
  /** @type {Node|HTMLElement} A node that never changes that this NodeGroup should always insert its nodes before.
34
31
  * 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. */
32
+ * TODO: But sometimes startNode and endNode point to the same node. Document this inconsistency. */
36
33
  endNode;
37
34
 
38
- /** @type {ExprPath[]} */
35
+ /** @type {Path[]} */
39
36
  paths = [];
40
37
 
41
38
  /** @type {string} Key that matches the template and the expressions. */
@@ -55,302 +52,220 @@ export default class NodeGroup {
55
52
  * @type {?Map<HTMLStyleElement, string>} */
56
53
  styles;
57
54
 
55
+ /** @type {Template} */
56
+ template;
58
57
 
59
58
  /**
60
- * Create an "instantiated" NodeGroup from a Template and add it to an element.
61
- * @param template {Template} Create it from the html strings and expressions in this template.
62
- * @param parentPath {?ExprPath} */
63
- constructor(template, parentPath=null) {
64
- if (!(this instanceof RootNodeGroup)) {
65
-
66
- let [fragment, shell] = this.init(template, parentPath);
67
-
68
- if (fragment && template.exprs.length) {
69
- this.updatePaths(fragment, shell.paths);
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);
75
-
76
- this.activateEmbeds(fragment, shell);
59
+ * Root node at the top of the hierarchy.
60
+ * Should be moved to RootNodeGroup
61
+ * @type {HTMLElement} */
62
+ root;
77
63
 
78
- // Apply exprs
79
- this.applyExprs(template.exprs);
80
-
81
- this.instantiateStaticComponents(staticComponents);
82
- }
83
- else if (shell)
84
- this.activateEmbeds(fragment, shell);
85
- }
86
- }
87
64
 
88
65
  /**
89
- * Common init shared by RootNodeGroup and NodeGroup constructors.
90
- * But in a separate function because they need to do this at a different step.
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.
91
68
  * @param template {Template} Create it from the html strings and expressions in this template.
92
- * @param parentPath {?ExprPath}
93
- * @param exactKey {?string} Optional, if already calculated.
94
- * @param closeKey {?string}
95
- * @returns {[DocumentFragment, Shell]} */
96
- init(template, parentPath=null, exactKey=null, closeKey=null) {
97
- this.exactKey = exactKey || template.getExactKey();
98
- this.closeKey = closeKey || template.getCloseKey();
99
-
100
- this.parentPath = parentPath;
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) {
101
73
  this.rootNg = parentPath?.parentNg?.rootNg || this;
74
+ this.parentPath = parentPath;
102
75
 
103
76
  /*#IFDEV*/assert(this.rootNg);/*#ENDIF*/
104
-
105
- /** @type {Template} */
106
77
  this.template = template;
107
-
108
- // new! Is this needed?
109
- template.nodeGroup = this;
110
-
111
- // Get a cached version of the parsed and instantiated html, and ExprPaths.
78
+ this.closeKey = template.getCloseKey();
112
79
 
113
80
  // 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]);
117
-
118
- this.startNode = this.endNode = textNode;
119
- return [];
81
+ if (template.isText) {
82
+ this.startNode = this.endNode = Globals.doc.createTextNode(template.html[0]);
120
83
  }
121
- else {
122
- let shell = Shell.get(template.html);
123
- let fragment = shell.fragment.cloneNode(true);
124
84
 
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
- }
135
- }
85
+ else {
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);
136
89
 
137
- /**
138
- * Use the paths to insert the given expressions.
139
- * Dispatches expression handling to other functions depending on the path type.
140
- * @param exprs {(*|*[]|function|Template)[]}
141
- * @param paths {?ExprPath[]} Optional. Only used for testing. Normally uses this.paths. */
142
- applyExprs(exprs, paths=null) {
143
- paths = paths || this.paths;
90
+ if (shellFragment.nodeType === 11) { // DocumentFragment
91
+ this.startNode = shellFragment.firstChild;
92
+ this.endNode = shellFragment.lastChild;
93
+ } else
94
+ this.startNode = this.endNode = shellFragment;
144
95
 
145
- /*#IFDEV*/
146
- this.verify();/*#ENDIF*/
147
96
 
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.
97
+ // Special setup for RootNodeGroup
98
+ if (this instanceof RootNodeGroup) {
153
99
 
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
100
 
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
- }
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');
170
106
 
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.
107
+ this.root = el;
108
+ if (shellFragment.nodeValue.length)
109
+ this.root.append(shellFragment);
110
+ }
174
111
 
175
- // If expr is an attribute in 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()) {
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
+ }
180
124
 
181
- if (!nextPath || !nextPath.isComponent() || nextPath.nodeMarker !== path.nodeMarker)
182
- lastComponentPathIndex = i;
183
- let isFirstComponentPath = !prevPath || !prevPath.isComponent() || prevPath.nodeMarker !== path.nodeMarker;
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);
184
128
 
185
- if (isFirstComponentPath) {
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);
186
133
 
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
- }
134
+ // Go one level deeper into all of shell's paths.
135
+ startingPathDepth = 1;
136
+ }
192
137
 
193
- this.applyComponentExprs(path.nodeMarker, componentProps);
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
+ }
194
143
 
195
- // Set attributes on component.
196
- for (let j=i; j<=lastComponentPathIndex; j++)
197
- paths[j].apply(pathExprs[j]);
198
- }
199
- }
200
144
 
201
- // Else apply it normally
202
- else
203
- path.apply(pathExprs[i]);
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
+ }
204
164
 
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
+ }
205
172
 
206
- } // end for(path of this.paths)
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();
207
177
 
178
+ this.setPathsFromFragment(this.root, shell.paths, startingPathDepth);
179
+ this.activateEmbeds(this.root, shell, startingPathDepth);
180
+ }
181
+ this.startNode = this.endNode = this.root;
208
182
 
209
- // TODO: Only do this if we have ExprPaths within styles?
210
- this.updateStyles();
183
+ Globals.rootNodeGroups.set(this.root, this);
184
+ } // end if RootNodeGroup
211
185
 
186
+ else if (shell) {
187
+ if (shell.paths.length) {
188
+ this.setPathsFromFragment(shellFragment, shell.paths);
189
+ }
212
190
 
191
+ this.activateEmbeds(shellFragment, shell);
192
+ }
193
+ }
213
194
 
214
- // Invalidate the nodes cache because we just changed it.
215
- this.nodesCache = null;
195
+ //#IFDEV
196
+ this.verify();
197
+ //#ENDIF
198
+ }
216
199
 
217
- // If there's leftover expressions, there's probably an issue with the Shell that created this NodeGroup,
218
- // and the number of paths not matching.
219
- /*#IFDEV*/
220
- assert(exprIndex === -1);/*#ENDIF*/
221
200
 
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) {
222
209
 
223
210
  /*#IFDEV*/
224
- this.verify();/*#ENDIF*/
225
- }
211
+ this.verify();
212
+ /*#ENDIF*/
226
213
 
227
- /**
228
- * Create a nested Component or call render with the new props.
229
- * @param el {Solarite:HTMLElement}
230
- * @param props {Object} */
231
- applyComponentExprs(el, props) {
232
-
233
- // TODO: Does a hash of this already exist somewhere?
234
- // Perhaps if Components were treated as child NodeGroups, which would need to be the child of an ExprPath,
235
- // then we could re-use the hash and logic from NodeManager?
236
- let newHash = getObjectHash(props);
237
-
238
- let isPreHtmlElement = el.hasAttribute('solarite-placeholder');
239
- let isPreIsElement = el.hasAttribute('_is')
240
-
241
-
242
- // Instantiate a placeholder.
243
- if (isPreHtmlElement || isPreIsElement)
244
- el = this.instantiateComponent(el, isPreHtmlElement, props);
245
-
246
- // Call render() with the same params that would've been passed to the constructor.
247
- // We do this even if the arguments haven't changed, so we can let the child component
248
- // compare the arguments and then decide for itself whether it wants to re-render.
249
- else if (el.render) {
250
- //let oldHash = Globals.componentArgsHash.get(el);
251
- //if (oldHash !== newHash) { // Only if not changed.
252
- let args = {};
253
- for (let name in props || {})
254
- args[Util.dashesToCamel(name)] = props[name];
255
- el.render(args); // Pass new values of props to render so it can decide how it wants to respond.
256
- //}
257
- }
214
+ let paths = this.paths;
258
215
 
259
- Globals.componentArgsHash.set(el, newHash);
260
- }
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;
261
228
 
262
- /**
263
- * We swap the placeholder element for the real element so we can pass its dynamic attributes
264
- * to its constructor.
265
- *
266
- * The logic of this function is complex and could use cleaning up.
267
- *
268
- * @param el
269
- * @param isPreHtmlElement {?boolean} True if the element's tag name ends with -solarite-placeholder
270
- * @param props {Object} Attributes with dynamic values.
271
- * @return {HTMLElement} */
272
- instantiateComponent(el, isPreHtmlElement=undefined, props=undefined) {
273
- if (isPreHtmlElement === undefined)
274
- isPreHtmlElement = !el.hasAttribute('_is');
275
-
276
- let tagName = (isPreHtmlElement
277
- ? el.tagName.slice(0, -21) // Remove -SOLARITE-PLACEHOLDER
278
- : el.getAttribute('is')).toLowerCase();
279
-
280
-
281
- // Throw if custom element isn't defined.
282
- let Constructor = customElements.get(tagName);
283
- if (!Constructor)
284
- throw new Error(`The custom tag name ${tagName} is not registered.`)
285
-
286
- let attribs = {};
287
- for (let name in props || {})
288
- attribs[Util.dashesToCamel(name)] = props[name];
289
-
290
- // Pass other attribs to constructor, since otherwise they're not yet set on the element,
291
- // and the constructor would otherwise have no way to see them.
292
- if (el.attributes.length) {
293
- for (let attrib of el.attributes) {
294
- let attribName = Util.dashesToCamel(attrib.name);
295
- if (!attribs.hasOwnProperty(attribName) && attribName !== 'solarite-placeholder')
296
- attribs[attribName] = attrib.value;
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);
297
239
  }
240
+ else if (includeNonComponents)
241
+ path.apply(pathExprs[i]);
298
242
  }
299
243
 
300
- // Create the web component.
301
- // Get the children that aren't Solarite's comment placeholders.
302
- let children = [...el.childNodes].filter(node => node.nodeType !== Node.COMMENT_NODE || !node.nodeValue.startsWith('ExprPath'));
303
- let newEl = new Constructor(attribs, children);
244
+ // If there's leftover expressions, there's probably an issue with the Shell that created this NodeGroup,
245
+ // and the number of paths not matching.
246
+ /*#IFDEV*/
247
+ assert(exprIndex === 0);
248
+ /*#ENDIF*/
304
249
 
305
- if (!isPreHtmlElement)
306
- newEl.setAttribute('is', el.getAttribute('is').toLowerCase());
307
250
 
308
- // Replace the placeholder tag with the instantiated web component.
309
- el.replaceWith(newEl);
251
+ if (includeNonComponents) {
310
252
 
311
- // If an id pointed at the placeholder, update it to point to the new element.
312
- let id = el.getAttribute('data-id') || el.getAttribute('id');
313
- if (id)
314
- delve(this.getRootNode(), id.split(/\./g), newEl);
253
+ // TODO: Only do this if we have Paths within styles?
254
+ this.updateStyles();
315
255
 
256
+ // Invalidate the nodes cache because we just changed it.
257
+ this.nodesCache = null;
316
258
 
317
- // Update paths to use replaced element.
318
- for (let path of this.paths) {
319
- if (path.nodeMarker === el)
320
- path.nodeMarker = newEl;
321
- if (path.nodeBefore === el)
322
- path.nodeBefore = newEl;
323
259
  }
324
- if (this.startNode === el)
325
- this.startNode = newEl;
326
- if (this.endNode === el)
327
- this.endNode = newEl;
328
-
329
-
330
- // applyComponentExprs() is called because we're rendering.
331
- // So we want to render the sub-component also.
332
- if (newEl.renderFirstTime)
333
- newEl.renderFirstTime();
334
-
335
- // Copy attributes over.
336
- for (let attrib of el.attributes)
337
- if (attrib.name !== '_is' && attrib.name !== 'solarite-placeholder')
338
- newEl.setAttribute(attrib.name, attrib.value);
339
-
340
- // Set dynamic attributes if they are primitive types.
341
- for (let name in props) {
342
- let val = props[name];
343
- if (typeof val === 'boolean') {
344
- if (val !== false && val !== undefined && val !== null)
345
- newEl.setAttribute(name, '');
346
- }
347
260
 
348
- // If type is a non-boolean primitive, set the attribute value.
349
- else if (['number', 'bigint', 'string'].includes(typeof val))
350
- newEl.setAttribute(name, val);
351
- }
261
+ /*#IFDEV*/
262
+ this.verify();
263
+ /*#ENDIF*/
264
+ }
265
+
266
+ // TODO: Give it a better name.
267
+ applyExprs2(exprs) {
352
268
 
353
- return newEl;
354
269
  }
355
270
 
356
271
  /**
@@ -376,10 +291,6 @@ export default class NodeGroup {
376
291
  return result;
377
292
  }
378
293
 
379
- getParentNode() {
380
- return this.startNode?.parentNode
381
- }
382
-
383
294
  /**
384
295
  * Get the root element of the NodeGroup's RootNodeGroup.
385
296
  * @returns {HTMLElement|DocumentFragment} */
@@ -394,21 +305,15 @@ export default class NodeGroup {
394
305
  }
395
306
 
396
307
  /**
397
- * Requires the nodeCache to be present. */
398
- removeAndSaveOrphans() {
399
- /*#IFDEV*/assert(this.nodesCache);/*#ENDIF*/
400
- let fragment = document.createDocumentFragment();
401
- for (let node of this.getNodes())
402
- fragment.append(node);
403
- }
404
-
405
-
406
- updatePaths(fragment, paths, offset) {
407
- // Update paths to point to the fragment.
408
- let pathLength = paths.length;
308
+ * Copy paths in fragment to this.paths.
309
+ * @param fragment {DocumentFragment|HTMLElement}
310
+ * @param paths
311
+ * @param startingPathDepth {int} */
312
+ setPathsFromFragment(fragment, paths, startingPathDepth=0) {
313
+ let pathLength = paths.length; // For faster iteration
409
314
  this.paths.length = pathLength;
410
315
  for (let i=0; i<pathLength; i++) {
411
- let path = paths[i].clone(fragment, offset)
316
+ let path = paths[i].clone(fragment, startingPathDepth)
412
317
  path.parentNg = this;
413
318
  this.paths[i] = path;
414
319
  }
@@ -423,98 +328,6 @@ export default class NodeGroup {
423
328
  }
424
329
  }
425
330
 
426
- //#IFDEV
427
- /**
428
- * @deprecated
429
- * An interleaved array of sets of nodes and top-level ExprPaths
430
- * @type {(Node|HTMLElement|ExprPath)[]} */
431
- get nodes() { throw new Error('')};
432
-
433
- get debug() {
434
- return [
435
- `parentNode: ${this.parentNode?.tagName?.toLowerCase()}`,
436
- 'nodes:',
437
- ...setIndent(this.getNodes().map(item => {
438
- if (item instanceof Node) {
439
-
440
- let tree = nodeToArrayTree(item, nextNode => {
441
-
442
- let path = this.paths.find(path=>path.type === ExprPathType.Content && path.getNodes().includes(nextNode));
443
- if (path)
444
- return [`Path.nodes:`]
445
-
446
- return [];
447
- })
448
-
449
- // TODO: How to indend nodes belonging to a path vs those that just occur after the path?
450
- return flattenAndIndent(tree)
451
- }
452
- else if (item instanceof ExprPath)
453
- return setIndent(item.debug, 1)
454
- }).flat(), 1)
455
- ]
456
- }
457
-
458
- get debugNodes() { return this.getNodes() }
459
-
460
-
461
- get debugNodesHtml() { return this.getNodes().map(n => n.outerHTML || n.textContent) }
462
-
463
- verify() {
464
- if (!window.verify)
465
- return;
466
-
467
- assert(this.startNode)
468
- assert(this.endNode)
469
- //assert(this.startNode !== this.endNode) // This can be true.
470
- assert(this.startNode.parentNode === this.endNode.parentNode)
471
-
472
- // Only if connected:
473
- assert(!this.startNode.parentNode || this.startNode === this.endNode || this.startNode.compareDocumentPosition(this.endNode) === Node.DOCUMENT_POSITION_FOLLOWING)
474
-
475
- // if (this.parentPath)
476
- // assert(this.parentPath.nodeGroups.includes(this));
477
-
478
- for (let path of this.paths) {
479
- assert(path.parentNg === this)
480
-
481
- // Fails for detached NodeGroups.
482
- // NodeGroups get detached when their nodes are removed by udomdiff()
483
- let parentNode = this.getParentNode();
484
- if (parentNode)
485
- assert(this.getParentNode().contains(path.getParentNode()))
486
- path.verify();
487
- // TODO: Make sure path nodes are all within our own node range.
488
- }
489
- return true;
490
- }
491
- //#ENDIF
492
-
493
- findStaticComponents(root, shell, pathOffset=0) {
494
- let result = [];
495
-
496
- // static components. These are WebComponents that do not have any constructor arguments that are expressions.
497
- // Those are instead created by applyExpr() which calls applyComponentExprs() which calls instantiateComponent().
498
- // Maybe someday these two paths will be merged?
499
- // Must happen before ids because instantiateComponent will replace the element.
500
- for (let path of shell.staticComponents) {
501
- if (pathOffset)
502
- path = path.slice(0, -pathOffset);
503
- let el = resolveNodePath(root, path);
504
-
505
- // Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
506
- // Recreating it is necessary so we can pass the constructor args to it.
507
- if (root !== el/* && !isReplaceEl(root, el)*/) // TODO: is isReplaceEl necessary?
508
- result.push(el);
509
- }
510
- return result;
511
- }
512
-
513
- instantiateStaticComponents(staticComponents) {
514
- for (let el of staticComponents)
515
- this.instantiateComponent(el)
516
- }
517
-
518
331
  /**
519
332
  * @param root {HTMLElement|DocumentFragment}
520
333
  * @param shell {Shell}
@@ -530,10 +343,10 @@ export default class NodeGroup {
530
343
  for (let path of shell.ids) {
531
344
  if (pathOffset)
532
345
  path = path.slice(0, -pathOffset);
533
- let el = resolveNodePath(root, path);
346
+ let el = Path.resolve(root, path);
534
347
  Util.bindId(rootEl, el);
535
348
  }
536
- }
349
+ }
537
350
 
538
351
  // styles
539
352
  if (options?.styles !== false) {
@@ -544,7 +357,7 @@ export default class NodeGroup {
544
357
  path = path.slice(0, -pathOffset);
545
358
 
546
359
  /** @type {HTMLStyleElement} */
547
- let style = resolveNodePath(root, path);
360
+ let style = Path.resolve(root, path);
548
361
  if (rootEl.nodeType === 1) {
549
362
  Util.bindStyles(style, rootEl);
550
363
  this.styles.set(style, style.textContent);
@@ -557,140 +370,82 @@ export default class NodeGroup {
557
370
  for (let path of shell.scripts) {
558
371
  if (pathOffset)
559
372
  path = path.slice(0, -pathOffset);
560
- let script = resolveNodePath(root, path);
373
+ let script = Path.resolve(root, path);
561
374
  eval(script.textContent)
562
375
  }
563
376
  }
564
377
  }
565
378
  }
566
- }
567
-
568
-
569
- export class RootNodeGroup extends NodeGroup {
570
-
571
- /**
572
- * Root node at the top of the hierarchy.
573
- * @type {HTMLElement} */
574
- root;
575
379
 
576
- /**
577
- * When we call renerWatched() we re-render these expressions, then clear this to a new Map()
578
- * @type {Map<ExprPath, ValueOp|WholeArrayOp|ArraySpliceOp[]>} */
579
- exprsToRender = new Map();
580
-
581
- /**
582
- *
583
- * @param template
584
- * @param el
585
- * @param options {?object}
586
- */
587
- constructor(template, el, options) {
588
- super(template);
589
-
590
- this.options = options;
591
-
592
- this.rootNg = this;
593
- let [fragment, shell] = this.init(template);
594
-
595
- if (fragment instanceof Text) {
596
-
597
- if (el) {
598
- this.startNode = el;
599
- this.endNode = el;
600
- if (fragment.nodeValue.length)
601
- el.append(fragment);
602
- this.root = el;
603
- }
604
- Globals.nodeGroups.set(this.root, this);
605
- }
606
- else {
380
+ //#IFDEV
381
+ getParentNode() {
382
+ return this.startNode?.parentNode
383
+ }
607
384
 
608
- // If adding NodeGroup to an element.
609
- let offset = 0;
610
- let root = fragment; // TODO: Rename so it's not confused with this.root.
611
- if (el) {
612
- Globals.nodeGroups.set(el, this);
613
-
614
- // Save slot children
615
- let slotChildren;
616
- if (el.childNodes.length) {
617
- slotChildren = document.createDocumentFragment();
618
- slotChildren.append(...el.childNodes);
619
- }
385
+ get debug() {
386
+ return [
387
+ `parentNode: ${this.parentNode?.tagName?.toLowerCase()}`,
388
+ 'nodes:',
389
+ ...setIndent(this.getNodes().map(item => {
390
+ if (item?.nodeType) {
620
391
 
621
- this.root = el;
392
+ let tree = nodeToArrayTree(item, nextNode => {
622
393
 
623
- // If el should replace the root node of the fragment.
624
- if (isReplaceEl(fragment, el)) {
625
- el.append(...fragment.children[0].childNodes);
394
+ let path = this.paths.find(path=>(path instanceof PathToNodes) && path.getNodes().includes(nextNode));
395
+ if (path)
396
+ return [`Path.nodes:`]
626
397
 
627
- // Copy attributes
628
- for (let attrib of fragment.children[0].attributes)
629
- if (!el.hasAttribute(attrib.name) && attrib.name !== 'solarite-placeholder')
630
- el.setAttribute(attrib.name, attrib.value);
398
+ return [];
399
+ })
631
400
 
632
- // Go one level deeper into all of shell's paths.
633
- offset = 1;
634
- } else {
635
- let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
636
- if (!isEmpty)
637
- el.append(...fragment.childNodes);
401
+ // TODO: How to indend nodes belonging to a path vs those that just occur after the path?
402
+ return flattenAndIndent(tree)
638
403
  }
404
+ else if (item instanceof Path)
405
+ return setIndent(item.debug, 1)
406
+ }).flat(), 1)
407
+ ]
408
+ }
639
409
 
640
- // Setup children
641
- if (slotChildren) {
642
-
643
- // Named slots
644
- for (let slot of el.querySelectorAll('slot[name]')) {
645
- let name = slot.getAttribute('name')
646
- if (name) {
647
- let slotChildren2 = slotChildren.querySelectorAll(`[slot='${name}']`);
648
- slot.append(...slotChildren2);
649
- }
650
- }
651
-
652
- // Unnamed slots
653
- let unamedSlot = el.querySelector('slot:not([name])')
654
- if (unamedSlot)
655
- unamedSlot.append(slotChildren);
656
-
657
- // No slots
658
- else
659
- el.append(slotChildren);
660
- }
410
+ get debugNodes() { return this.getNodes() }
661
411
 
662
- root = el;
663
412
 
664
- this.startNode = el;
665
- this.endNode = el;
666
- } else {
667
- let singleEl = getSingleEl(fragment);
668
- this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
413
+ get debugNodesHtml() { return this.getNodes().map(n => n.outerHTML || n.textContent) }
669
414
 
670
- Globals.nodeGroups.set(this.root, this);
671
- if (singleEl) {
672
- root = singleEl;
673
- offset = 1;
674
- }
675
- }
415
+ verify() {
416
+ if (!window.verify)
417
+ return;
676
418
 
677
- this.updatePaths(root, shell.paths, offset);
419
+ assert(this.startNode)
420
+ assert(this.endNode)
421
+ //assert(this.startNode !== this.endNode) // This can be true.
422
+ assert(this.startNode.parentNode === this.endNode.parentNode)
678
423
 
679
- // Static web components can sometimes have children created via expressions.
680
- // But calling applyExprs() will mess up the shell's path to them.
681
- // So we find them first, then call activateStaticComponents() after their children have been created.
682
- let staticComponents = this.findStaticComponents(root, shell, offset);
424
+ // Only if connected:
425
+ assert(!this.startNode.parentNode || this.startNode === this.endNode || this.startNode.compareDocumentPosition(this.endNode) === Node.DOCUMENT_POSITION_FOLLOWING)
683
426
 
684
- this.activateEmbeds(root, shell, offset);
427
+ // if (this.parentPath)
428
+ // assert(this.parentPath.nodeGroups.includes(this));
685
429
 
686
- // Apply exprs
687
- this.applyExprs(template.exprs);
430
+ for (let path of this.paths) {
431
+ assert(path.parentNg === this)
688
432
 
689
- this.instantiateStaticComponents(staticComponents);
433
+ // Fails for detached NodeGroups.
434
+ // NodeGroups get detached when their nodes are removed by udomdiff()
435
+ let parentNode = this.getParentNode();
436
+ if (parentNode)
437
+ assert(this.getParentNode().contains(path.getParentNode()))
438
+ path.verify();
439
+ // TODO: Make sure path nodes are all within our own node range.
690
440
  }
441
+ return true;
691
442
  }
443
+ //#ENDIF
692
444
  }
693
445
 
446
+
447
+
448
+
694
449
  function getSingleEl(fragment) {
695
450
  let nonempty = [];
696
451
  for (let n of fragment.childNodes) {
@@ -706,10 +461,10 @@ function getSingleEl(fragment) {
706
461
  /**
707
462
  * Does the fragment have one child that's an element matching the tagname of el?
708
463
  * @param fragment {DocumentFragment}
709
- * @param el {HTMLElement}
464
+ * @param tagName {string}
710
465
  * @returns {boolean} */
711
- function isReplaceEl(fragment, el) {
466
+ function isReplaceEl(fragment, tagName) {
712
467
  return fragment.children.length===1
713
- && el.tagName.includes('-') // TODO: Check for solarite-placeholder attribute instead?
714
- && fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') === el.tagName;
468
+ && tagName.includes('-')
469
+ && fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') === tagName;
715
470
  }