solarite 0.1.1 → 0.2.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.
Files changed (45) hide show
  1. package/benchmarks/naive/Solarite.min.js +4 -0
  2. package/benchmarks/naive/index.html +14 -0
  3. package/benchmarks/naive/main.js +339 -0
  4. package/benchmarks/naive/package-lock.json +13 -0
  5. package/benchmarks/naive/package.json +23 -0
  6. package/benchmarks/readme.md +48 -0
  7. package/build/build.bat +2 -2
  8. package/build/build.js +1 -1
  9. package/dist/Solarite-debug.js +1344 -1532
  10. package/dist/Solarite.js +1298 -1334
  11. package/dist/Solarite.min.js +3 -3
  12. package/dist/udomdiff-license.txt +18 -0
  13. package/docs/index.md +440 -130
  14. package/docs/js/Playground.js +1 -1
  15. package/docs/js/codemirror/codemirror6.js +3683 -3206
  16. package/docs/js/codemirror/themeSolarIce.js +1 -1
  17. package/docs/js/documentation.js +1 -1
  18. package/docs/js/ui/CodeEditor.js +183 -45
  19. package/docs/js/ui/FlexResizer.js +14 -3
  20. package/docs/js/util/Errors.js +11 -0
  21. package/docs/media/documentation.css +6 -3
  22. package/index.html +449 -30
  23. package/package.json +1 -1
  24. package/readme.md +1 -1
  25. package/src/solarite/ExprPath.js +349 -151
  26. package/src/solarite/Globals.js +43 -1
  27. package/src/solarite/NodeGroup.js +275 -293
  28. package/src/solarite/Shell.js +26 -28
  29. package/src/solarite/Solarite.js +12 -0
  30. package/src/solarite/Template.js +55 -128
  31. package/src/solarite/Util.js +131 -7
  32. package/src/solarite/createSolarite.js +14 -19
  33. package/src/solarite/getArg.js +1 -1
  34. package/src/solarite/hash.js +18 -35
  35. package/src/solarite/r.js +127 -127
  36. package/src/solarite/watch3.js +18 -11
  37. package/src/{solarite → unused}/NodeGroupManager.js +81 -109
  38. package/src/{solarite → unused}/watch.js +1 -1
  39. package/src/{solarite → unused}/watch2.js +1 -2
  40. package/src/{solarite → util}/MultiValueMap.js +18 -11
  41. package/src/util/WeakArray.js +33 -0
  42. package/tests/Solarite.test.js +862 -167
  43. package/tests/index.html +4 -2
  44. package/tests/run.bat +1 -1
  45. package/deno.lock +0 -176
@@ -3,11 +3,13 @@ import ExprPath, {PathType, resolveNodePath} from "./ExprPath.js";
3
3
  import {getObjectHash} from "./hash.js";
4
4
  import Shell from "./Shell.js";
5
5
  import udomdiff from "./udomdiff.js";
6
- import Util, {div, findArrayDiff, flattenAndIndent, isEvent, nodeToArrayTree, setIndent} from "./Util.js";
7
- import NodeGroupManager from "./NodeGroupManager.js";
6
+ import Util, {arraySame, flattenAndIndent, isEvent, nodeToArrayTree, setIndent} from "./Util.js";
7
+ //import NodeGroupManager from "./NodeGroupManager.js";
8
+ import delve from "../util/delve.js";
9
+ import Globals from "./Globals.js";
8
10
 
9
11
 
10
- /** @typedef {boolean|string|number|function|Object|Array|Date|Node} Expr */
12
+ /** @typedef {boolean|string|number|function|Object|Array|Date|Node|Template} Expr */
11
13
 
12
14
  /**
13
15
  * A group of Nodes instantiated from a Shell, with Expr's filled in.
@@ -20,16 +22,17 @@ import NodeGroupManager from "./NodeGroupManager.js";
20
22
  * */
21
23
  export default class NodeGroup {
22
24
 
23
- /** @Type {NodeGroupManager} */
24
- manager;
25
+ /**
26
+ * @Type {RootNodeGroup} */
27
+ rootNg;
25
28
 
26
29
  /** @type {ExprPath} */
27
30
  parentPath;
28
31
 
29
- /** @type {Node} First node of NodeGroup. Should never be null. */
32
+ /** @type {Node|HTMLElement} First node of NodeGroup. Should never be null. */
30
33
  startNode;
31
34
 
32
- /** @type {Node} A node that never changes that this NodeGroup should always insert its nodes before.
35
+ /** @type {Node|HTMLElement} A node that never changes that this NodeGroup should always insert its nodes before.
33
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
37
  endNode;
35
38
 
@@ -39,13 +42,9 @@ export default class NodeGroup {
39
42
  /** @type {string} Key that matches the template and the expressions. */
40
43
  exactKey;
41
44
 
42
- /** @type {string} Key that only matches the template. */
45
+ /** @type {string} Key that only matches the template. */
43
46
  closeKey;
44
47
 
45
- /** @type {boolean} Used by NodeGroupManager. */
46
- inUse;
47
-
48
-
49
48
  /**
50
49
  * @internal
51
50
  * @type {Node[]} Cached result of getNodes() used only for improving performance.*/
@@ -55,137 +54,58 @@ export default class NodeGroup {
55
54
  * @type {?Map<HTMLStyleElement, string>} */
56
55
  styles;
57
56
 
58
- /**
59
- * If rendering a Template with replaceMode=true, pseudoRoot points to the element where the attributes are rendered.
60
- * But pseudoRoot is outside of this.getNodes().
61
- * NodeGroupManager.render() copies the attributes from pseudoRoot to the actual web component root element.
62
- * @type {?HTMLElement} */
63
- pseudoRoot;
64
-
65
57
  currentComponentProps = {};
66
58
 
67
59
 
68
60
  /**
69
61
  * Create an "instantiated" NodeGroup from a Template and add it to an element.
70
62
  * @param template {Template} Create it from the html strings and expressions in this template.
71
- * @param manager {?NodeGroupManager}
72
- * @returns {NodeGroup} */
73
- constructor(template, manager=null) {
63
+ * @param parentPath {?ExprPath} */
64
+ constructor(template, parentPath=null) {
65
+ if (!(this instanceof RootNodeGroup)) {
66
+ let [fragment, shell] = this.init(template, parentPath);
74
67
 
75
- /** @type {Template} */
76
- this.template = template;
68
+ this.updatePaths(fragment, shell.paths);
77
69
 
78
- /** @type {NodeGroupManager} */
79
- this.manager = manager;
80
-
81
- // new!
82
- template.nodeGroup = this;
83
-
84
- // Get a cached version of the parsed and instantiated html, and ExprPaths.
85
- let shell = Shell.get(template.html);
70
+ this.activateEmbeds(fragment, shell);
86
71
 
87
- let fragment = shell.fragment.cloneNode(true);
88
-
89
- // Figure out value of replaceMode option if it isn't set,
90
- // Assume replaceMode if there's only one child element and its tagname matches the root el.
91
- let replaceMode = typeof template.replaceMode === 'boolean'
92
- ? template.replaceMode
93
- : fragment.children.length===1 &&
94
- fragment.firstElementChild?.tagName.replace(/-SOLARITE-PLACEHOLDER$/, '')
95
- === manager?.rootEl?.tagName
96
- if (replaceMode) {
97
- this.pseudoRoot = fragment.firstElementChild;
98
- // if (!manager.rootEl)
99
- // manager.rootEl = this.pseudoRoot;
100
- /*#IFDEV*/assert(this.pseudoRoot)/*#ENDIF*/
101
- }
102
-
103
- let childNodes = replaceMode
104
- ? fragment.firstElementChild.childNodes
105
- : fragment.childNodes;
106
-
107
-
108
- this.startNode = childNodes[0]
109
- this.endNode = childNodes[childNodes.length - 1]
110
-
111
-
112
- // Update paths
113
- for (let oldPath of shell.paths) {
114
- let path = oldPath.clone(fragment)
115
- path.parentNg = this;
116
- this.paths.push(path);
72
+ // Apply exprs
73
+ this.applyExprs(template.exprs);
117
74
  }
118
-
119
-
120
- // Update web component placeholders.
121
- // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
122
- // Is this list needed at all?
123
- //for (let component of shell.components)
124
- // this.components.push(resolveNodePath(this.startNode.parentNode, getNodePath(component)))
125
-
126
- /*#IFDEV*/this.verify();/*#ENDIF*/
127
-
128
- this.activateEmbeds(fragment, shell);
129
-
130
- /*#IFDEV*/
131
- assert(this.paths.length <= template.exprs.length);
132
- if (template.exprs.length)
133
- assert(this.paths.length);
134
- /*#ENDIF*/
135
-
136
- // Apply exprs
137
- this.applyExprs(template.exprs);
138
-
139
- /*#IFDEV*/this.verify();/*#ENDIF*/
140
75
  }
141
76
 
142
- activateEmbeds(root, shell) {
143
-
144
- // static components
145
- // Must happen before ids.
146
- for (let path of shell.staticComponents) {
147
- let el = resolveNodePath(root, path)
77
+ /**
78
+ * Common init shared by RootNodeGroup and NodeGroup constructors.
79
+ * But in a separate function because they need to do this at a different step.
80
+ * @param template {Template} Create it from the html strings and expressions in this template.
81
+ * @param parentPath {?ExprPath}
82
+ * @param exactKey {?string} Optional, if already calculated.
83
+ * @param closeKey {?string}
84
+ * @returns {[DocumentFragment, Shell]} */
85
+ init(template, parentPath=null, exactKey=null, closeKey=null) {
86
+ this.exactKey = exactKey || template.getExactKey();
87
+ this.closeKey = closeKey || template.getCloseKey();
148
88
 
149
- // Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
150
- if (el.tagName !== this.pseudoRoot?.tagName)
151
- this.createNewComponent(el)
152
- }
89
+ this.parentPath = parentPath;
90
+ this.rootNg = parentPath?.parentNg?.rootNg || this;
153
91
 
154
- if (this.manager?.rootEl) {
92
+ /*#IFDEV*/assert(this.rootNg);/*#ENDIF*/
155
93
 
156
- // ids
157
- if (this.manager.options.ids !== false)
158
- for (let path of shell.ids) {
159
- let el = resolveNodePath(root, path);
160
- let id = el.getAttribute('data-id') || el.getAttribute('id');
94
+ /** @type {Template} */
95
+ this.template = template;
161
96
 
162
- // Don't allow overwriting existing class properties if they already have a non-Node value.
163
- if (this.manager.rootEl[id] && !(this.manager.rootEl[id] instanceof Node))
164
- throw new Error(`${this.manager.rootEl.constructor.name}.${id} already has a value. `+
165
- `Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
97
+ // new! Is this needed?
98
+ template.nodeGroup = this;
166
99
 
167
- this.manager.rootEl[id] = el;
168
- }
100
+ // 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);
169
103
 
170
- // styles
171
- if (this.manager.options.styles !== false) {
172
- if (shell.styles.length)
173
- this.styles = new Map();
174
- for (let path of shell.styles) {
175
- let style = resolveNodePath(root, path);
176
- Util.bindStyles(style, this.manager.rootEl);
177
- this.styles.set(style, style.textContent);
178
- }
104
+ let childNodes = fragment.childNodes;
105
+ this.startNode = childNodes[0];
106
+ this.endNode = childNodes[childNodes.length - 1];
179
107
 
180
- }
181
- // scripts
182
- if (this.manager.options.scripts !== false) {
183
- for (let path of shell.scripts) {
184
- let script = resolveNodePath(root, path);
185
- eval(script.textContent)
186
- }
187
- }
188
- }
108
+ return [fragment, shell];
189
109
  }
190
110
 
191
111
  /**
@@ -195,8 +115,9 @@ export default class NodeGroup {
195
115
  * @param paths {?ExprPath[]} Optional. */
196
116
  applyExprs(exprs, paths=null) {
197
117
  paths = paths || this.paths;
198
-
118
+
199
119
  /*#IFDEV*/this.verify();/*#ENDIF*/
120
+
200
121
  // Update exprs at paths.
201
122
  let exprIndex = exprs.length-1, expr, lastNode;
202
123
 
@@ -206,56 +127,24 @@ export default class NodeGroup {
206
127
  expr = exprs[exprIndex];
207
128
 
208
129
  // Nodes
209
- if (path.type === PathType.Content) {
210
- this.applyNodeExpr(path, expr);
211
- /*#IFDEV*/path.verify()/*#ENDIF*/
212
- }
213
-
214
- // Attributes
215
- else {
216
- let node = path.nodeMarker;
217
- let el = (this.manager?.rootEl && node === this.pseudoRoot) ? this.manager.rootEl : node;
218
- /*#IFDEV*/assert(node);/*#ENDIF*/
219
-
220
- // This is necessary both here and below.
221
- if (lastNode && lastNode !== this.pseudoRoot && lastNode !== node && Object.keys(this.currentComponentProps).length) {
222
- this.applyComponentExprs(lastNode, this.currentComponentProps);
223
- this.currentComponentProps = {};
224
- }
225
-
226
- if (path.type === PathType.Multiple)
227
- path.applyMultipleAttribs(el, expr)
228
130
 
229
- // Capture attribute expressions to later send to the constructor of a web component.
230
- // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
231
- else if (path.nodeMarker !== this.pseudoRoot && path.type === PathType.Component)
232
- this.currentComponentProps[path.attrName] = expr;
233
-
234
- else if (path.type === PathType.Comment) {
235
- // Expressions inside Html comments. Deliberately empty because we won't waste time updating them.
236
- }
237
- else {
131
+ // This is necessary both here and below.
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
+ }
238
136
 
239
- // Event attribute value
240
- if (path.attrValue===null && (typeof expr === 'function' || Array.isArray(expr)) && isEvent(path.attrName)) {
241
- let root = this.manager?.rootEl || this.startNode.parentNode; // latter is used when constructing a whole element.
242
- path.applyEventAttrib(el, expr, root);
243
- }
137
+ exprIndex = path.apply(expr, exprs, exprIndex, this.currentComponentProps);
244
138
 
245
- // Regular attribute value.
246
- else // One node value may have multiple expressions. Here we apply them all at once.
247
- exprIndex = path.applyValueAttrib(el, exprs, exprIndex);
248
- }
139
+ lastNode = path.nodeMarker;
249
140
 
250
- lastNode = path.nodeMarker;
251
- }
252
141
 
253
142
  exprIndex--;
254
143
  } // end for(path of this.paths)
255
144
 
256
145
 
257
146
  // Check again after we iterate through all paths to apply to a component.
258
- if (lastNode && lastNode !== this.pseudoRoot && Object.keys(this.currentComponentProps).length) {
147
+ if (lastNode && lastNode !== this.rootNg.root && Object.keys(this.currentComponentProps).length) {
259
148
  this.applyComponentExprs(lastNode, this.currentComponentProps);
260
149
  this.currentComponentProps = {};
261
150
  }
@@ -273,123 +162,8 @@ export default class NodeGroup {
273
162
  /*#IFDEV*/this.verify();/*#ENDIF*/
274
163
  }
275
164
 
276
- applyExpr(path, expr) {
277
- // TODO: Use this if I can figure out how to adapt applyValueAttrib() to it.
278
- }
279
-
280
- /**
281
- * Insert/replace the nodes created by a single expression.
282
- * Called by applyExprs()
283
- * This function is recursive, as the functions it calls also call it.
284
- * TODO: Move this to ExprPath?
285
- * @param path {ExprPath}
286
- * @param expr {Expr}
287
- * @return {Node[]} New Nodes created. */
288
- applyNodeExpr(path, expr) {
289
- /*#IFDEV*/path.verify();/*#ENDIF*/
290
-
291
- /** @type {(Node|NodeGroup|Expr)[]} */
292
- let newNodes = [];
293
- let oldNodeGroups = path.nodeGroups;
294
- /*#IFDEV*/assert(!oldNodeGroups.includes(null))/*#ENDIF*/
295
- let secondPass = []; // indices
296
-
297
- // First Pass
298
- //for (let ng of path.nodeGroups) // TODO: Is this necessary?
299
- // ng.parentPath = null;
300
- path.nodeGroups = [];
301
- path.apply(expr, newNodes, secondPass);
302
- this.existingTextNodes = null;
303
-
304
- // TODO: Create an array of old vs Nodes and NodeGroups together.
305
- // If they're all the same, skip the next steps.
306
- // Or calculate it in the loop above as we go? Have a path.lastNodeGroups property?
307
-
308
- // Second pass to find close-match NodeGroups.
309
- let flatten = false;
310
- if (secondPass.length) {
311
- for (let [nodesIndex, ngIndex] of secondPass) {
312
- let ng = this.manager.getNodeGroup(newNodes[nodesIndex], false);
313
-
314
- ng.parentPath = path;
315
- let ngNodes = ng.getNodes();
316
-
317
- /*#IFDEV*/assert(!(newNodes[nodesIndex] instanceof NodeGroup))/*#ENDIF*/
318
-
319
- if (ngNodes.length === 1)
320
- newNodes[nodesIndex] = ngNodes[0];
321
-
322
- else {
323
- newNodes[nodesIndex] = ngNodes;
324
- flatten = true;
325
- }
326
- path.nodeGroups[ngIndex] = ng;
327
- }
328
-
329
- if (flatten)
330
- newNodes = newNodes.flat(); // TODO: Only if second pass happens?
331
- }
332
-
333
- /*#IFDEV*/assert(!path.nodeGroups.includes(null))/*#ENDIF*/
334
-
335
-
336
-
337
- let oldNodes = path.getNodes();
338
- path.nodesCache = newNodes; // Replaces value set by path.getNodes()
339
-
340
-
341
- // This pre-check makes it a few percent faster?
342
- let diff = findArrayDiff(oldNodes, newNodes);
343
- if (diff !== false) {
344
-
345
- if (this.parentPath)
346
- this.parentPath.clearNodesCache();
347
-
348
- // Fast clear method
349
- let isNowEmpty = oldNodes.length && !newNodes.length;
350
- if (!isNowEmpty || !path.fastClear(oldNodes, newNodes))
351
-
352
- // Rearrange nodes.
353
- udomdiff(path.parentNode, oldNodes, newNodes, path.nodeMarker)
354
-
355
- this.saveOrphans(oldNodeGroups, oldNodes);
356
- }
357
- /*#IFDEV*/path.verify();/*#ENDIF*/
358
- }
359
-
360
- /**
361
- * Find NodeGroups that had their nodes removed and add those nodes to a Fragment so
362
- * they're not lost forever and the NodeGroup's internal structure is still consistent.
363
- * Called from NodeGroup.applyNodeExpr().
364
- * @param oldNodeGroups {NodeGroup[]}
365
- * @param oldNodes {Node[]} */
366
- saveOrphans(oldNodeGroups, oldNodes) {
367
- let oldNgMap = new Map();
368
- for (let ng of oldNodeGroups) {
369
- oldNgMap.set(ng.startNode, ng)
370
-
371
- // TODO: Is this necessary?
372
- // if (ng.parentPath)
373
- // ng.parentPath.clearNodesCache();
374
- }
375
-
376
- for (let i=0, node; node = oldNodes[i]; i++) {
377
- let ng;
378
- if (!node.parentNode && (ng = oldNgMap.get(node))) {
379
- let fragment = document.createDocumentFragment();
380
- let endNode = ng.endNode;
381
- while (node !== endNode) {
382
- fragment.append(node)
383
- i++;
384
- node = oldNodes[i];
385
- }
386
- fragment.append(endNode);
387
- }
388
- }
389
- }
390
-
391
165
  /**
392
- * Create a nested RedComponent or call render with the new props.
166
+ * Create a nested Component or call render with the new props.
393
167
  * @param el {Solarite:HTMLElement}
394
168
  * @param props {Object} */
395
169
  applyComponentExprs(el, props) {
@@ -407,14 +181,14 @@ export default class NodeGroup {
407
181
  if (isPreHtmlElement || isPreIsElement)
408
182
  el = this.createNewComponent(el, isPreHtmlElement, props);
409
183
 
410
- // Update params of placeholder.
184
+ // Call render() with the same params that would've been passed to the constructor.
411
185
  else if (el.render) {
412
- let oldHash = componentHash.get(el);
186
+ let oldHash = Globals.componentHash.get(el);
413
187
  if (oldHash !== newHash)
414
188
  el.render(props); // Pass new values of props to render so it can decide how it wants to respond.
415
189
  }
416
190
 
417
- componentHash.set(el, newHash);
191
+ Globals.componentHash.set(el, newHash);
418
192
  }
419
193
 
420
194
  /**
@@ -457,33 +231,43 @@ export default class NodeGroup {
457
231
  // We pass the childNodes to the constructor so it can know about them,
458
232
  // instead of only afterward when they're appended to the slot below.
459
233
  // This is useful for a custom selectbox, for example.
460
- // NodeGroupManager.pendingChildren stores the childen so the super construtor call to Solarite's constructor
234
+ // Globals.pendingChildren stores the childen so the super construtor call to Solarite's constructor
461
235
  // can add them as children before the rest of the constructor code executes.
462
236
  let ch = [... el.childNodes];
463
- NodeGroupManager.pendingChildren.push(ch); // pop() is called in Solarite constructor.
237
+ Globals.pendingChildren.push(ch); // pop() is called in Solarite constructor.
464
238
  let newEl = new Constructor(props, ch);
465
239
 
466
240
  if (!isPreHtmlElement)
467
241
  newEl.setAttribute('is', el.getAttribute('is').toLowerCase())
468
242
  el.replaceWith(newEl);
469
-
243
+
470
244
  // Set children / slot children
471
245
  // TODO: Match named slots.
472
246
  // TODO: This only appends to slot if render() is called in the constructor.
473
247
  //let slot = newEl.querySelector('slot') || newEl;
474
248
  //slot.append(...el.childNodes);
475
-
249
+
476
250
  // Copy over event attributes.
477
251
  for (let propName in props) {
478
252
  let val = props[propName];
479
253
  if (propName.startsWith('on') && typeof val === 'function')
480
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
+ }
481
265
  }
482
266
 
483
267
  // If an id pointed at the placeholder, update it to point to the new element.
484
268
  let id = el.getAttribute('data-id') || el.getAttribute('id');
485
269
  if (id)
486
- this.manager.rootEl[id] = newEl;
270
+ delve(this.getRootNode(), id.split(/\./g), newEl);
487
271
 
488
272
 
489
273
  // Update paths to use replaced element.
@@ -552,18 +336,39 @@ export default class NodeGroup {
552
336
  return this.startNode?.parentNode
553
337
  }
554
338
 
339
+ /**
340
+ * Get the root element of the NodeGroup's RootNodeGroup.
341
+ * @returns {HTMLElement|DocumentFragment} */
342
+ getRootNode() {
343
+ return this.rootNg.root;
344
+ }
345
+
346
+ /**
347
+ * @returns {RootNodeGroup} */
348
+ getRootNodeGroup() {
349
+ return this.rootNg;
350
+ }
351
+
555
352
 
353
+ updatePaths(fragment, paths, offset) {
354
+ // Update paths to point to the fragment.
355
+ this.paths.length = paths.length;
356
+ for (let i=0; i<paths.length; i++) {
357
+ let path = paths[i].clone(fragment, offset)
358
+ path.parentNg = this;
359
+ this.paths[i] = path;
360
+ }
361
+ }
556
362
 
557
363
  updateStyles() {
558
364
  if (this.styles)
559
365
  for (let [style, oldText] of this.styles) {
560
366
  let newText = style.textContent;
561
367
  if (oldText !== newText)
562
- Util.bindStyles(style, this.manager.rootEl);
368
+ Util.bindStyles(style, this.getRootNodeGroup().root);
563
369
  }
564
370
  }
565
371
 
566
-
567
372
  //#IFDEV
568
373
  /**
569
374
  * @deprecated
@@ -582,7 +387,7 @@ export default class NodeGroup {
582
387
 
583
388
  let path = this.paths.find(path=>path.type === PathType.Content && path.getNodes().includes(nextNode));
584
389
  if (path)
585
- return [`Path[${path.parentIndex}].nodes:`]
390
+ return [`Path.nodes:`]
586
391
 
587
392
  return [];
588
393
  })
@@ -630,7 +435,184 @@ export default class NodeGroup {
630
435
  return true;
631
436
  }
632
437
  //#ENDIF
438
+
439
+
440
+ /**
441
+ * @param root {HTMLElement}
442
+ * @param shell {Shell}
443
+ * @param pathOffset {int} */
444
+ activateEmbeds(root, shell, pathOffset=0) {
445
+
446
+ // static components. These are WebComponents not created by an expression.
447
+ // Must happen before ids.
448
+ for (let path of shell.staticComponents) {
449
+ if (pathOffset)
450
+ path = path.slice(0, -pathOffset);
451
+ let el = resolveNodePath(root, path);
452
+
453
+ // Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
454
+ if (root !== el/* && !isReplaceEl(root, el)*/) // TODO: is isReplaceEl necessary?
455
+ this.createNewComponent(el)
456
+ }
457
+
458
+ let rootEl = this.rootNg.root;
459
+ if (rootEl) {
460
+
461
+ // ids
462
+ if (this.options?.ids !== false)
463
+ for (let path of shell.ids) {
464
+ if (pathOffset)
465
+ path = path.slice(0, -pathOffset);
466
+ let el = resolveNodePath(root, path);
467
+ let id = el.getAttribute('data-id') || el.getAttribute('id');
468
+ if (id) { // If something hasn't removed the id.
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
+ }
477
+ }
478
+
479
+ // styles
480
+ if (this.options?.styles !== false) {
481
+ if (shell.styles.length)
482
+ this.styles = new Map();
483
+ for (let path of shell.styles) {
484
+ if (pathOffset)
485
+ path = path.slice(0, -pathOffset);
486
+ let style = resolveNodePath(root, path);
487
+ Util.bindStyles(style, rootEl);
488
+ this.styles.set(style, style.textContent);
489
+ }
490
+
491
+ }
492
+ // scripts
493
+ if (this.options?.scripts !== false) {
494
+ for (let path of shell.scripts) {
495
+ if (pathOffset)
496
+ path = path.slice(0, -pathOffset);
497
+ let script = resolveNodePath(root, path);
498
+ eval(script.textContent)
499
+ }
500
+ }
501
+ }
502
+ }
633
503
  }
634
504
 
635
505
 
636
- let componentHash = new WeakMap()
506
+ export class RootNodeGroup extends NodeGroup {
507
+
508
+ /**
509
+ * Root node at the top of the hierarchy.
510
+ * @type {HTMLElement} */
511
+ root;
512
+
513
+ /**
514
+ *
515
+ * @param template
516
+ * @param el
517
+ * @param options {?object}
518
+ */
519
+ constructor(template, el, options) {
520
+ super(template);
521
+
522
+ this.options = options;
523
+
524
+ this.rootNg = this;
525
+ let [fragment, shell] = this.init(template);
526
+
527
+ // If adding NodeGroup to an element.
528
+ let offset = 0;
529
+ let root = fragment; // TODO: Rename so it's not confused with this.root.
530
+ if (el) {
531
+
532
+ // Save slot children
533
+ let slotFragment;
534
+ if (el.childNodes.length) {
535
+ slotFragment = document.createDocumentFragment();
536
+ slotFragment.append(...el.childNodes);
537
+ }
538
+
539
+ this.root = el;
540
+
541
+ // If el should replace the root node of the fragment.
542
+ if (isReplaceEl(fragment, el)) {
543
+ el.append(...fragment.children[0].childNodes);
544
+
545
+ // Copy attributes
546
+ for (let attrib of fragment.children[0].attributes)
547
+ if (!el.hasAttribute(attrib.name))
548
+ el.setAttribute(attrib.name, attrib.value);
549
+
550
+ // Go one level deeper into all of shell's paths.
551
+ offset = 1;
552
+ }
553
+ else {
554
+ let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
555
+ if (!isEmpty)
556
+ el.append(...fragment.childNodes);
557
+ }
558
+
559
+ // Setup slots
560
+ if (slotFragment) {
561
+ for (let slot of el.querySelectorAll('slot[name]')) {
562
+ let name = slot.getAttribute('name')
563
+ if (name) {
564
+ let slotChildren = slotFragment.querySelectorAll(`[slot='${name}']`);
565
+ slot.append(...slotChildren);
566
+ }
567
+ }
568
+ let unamedSlot = el.querySelector('slot:not([name])')
569
+ if (unamedSlot)
570
+ unamedSlot.append(slotFragment);
571
+ else
572
+ el.append(slotFragment);
573
+ }
574
+
575
+ root = el;
576
+ this.startNode = el;
577
+ this.endNode = el;
578
+ }
579
+ else {
580
+ let singleEl = getSingleEl(fragment);
581
+ this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
582
+ if (singleEl) {
583
+ root = singleEl;
584
+ offset = 1;
585
+ }
586
+ }
587
+
588
+ this.updatePaths(root, shell.paths, offset);
589
+
590
+ this.activateEmbeds(root, shell, offset);
591
+
592
+ // Apply exprs
593
+ this.applyExprs(template.exprs);
594
+ }
595
+ }
596
+
597
+ function getSingleEl(fragment) {
598
+ let nonempty = [];
599
+ for (let n of fragment.childNodes) {
600
+ if (n.nodeType === 1 || n.nodeType === 3 && n.textContent.trim().length) {
601
+ if (nonempty.length)
602
+ return null;
603
+ nonempty.push(n);
604
+ }
605
+ }
606
+ return nonempty[0];
607
+ }
608
+
609
+ /**
610
+ * Does the fragment have one child that's an element matching the tagname of el?
611
+ * @param fragment {DocumentFragment}
612
+ * @param el {HTMLElement}
613
+ * @returns {boolean} */
614
+ function isReplaceEl(fragment, el) {
615
+ return el.tagName.includes('-')
616
+ && fragment.children.length===1
617
+ && fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') === el.tagName;
618
+ }