solarite 0.1.0 → 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 (48) 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 +3 -2
  8. package/build/build.js +1 -1
  9. package/dist/Solarite-debug.js +1941 -2791
  10. package/dist/Solarite.js +1697 -2511
  11. package/dist/Solarite.min.js +3 -3
  12. package/dist/udomdiff-license.txt +18 -0
  13. package/docs/index.md +695 -235
  14. package/docs/js/Playground.js +1 -1
  15. package/docs/js/codemirror/codemirror6.js +3683 -3206
  16. package/docs/js/codemirror/themeSolarIce.js +2 -2
  17. package/docs/js/documentation.js +2 -2
  18. package/docs/js/ui/CodeEditor.js +183 -45
  19. package/docs/js/ui/FlexResizer.js +15 -5
  20. package/docs/js/util/Errors.js +11 -0
  21. package/docs/media/documentation.css +6 -3
  22. package/index.html +462 -26
  23. package/package.json +1 -1
  24. package/readme.md +11 -1
  25. package/src/solarite/ExprPath.js +422 -135
  26. package/src/solarite/Globals.js +53 -0
  27. package/src/solarite/NodeGroup.js +300 -388
  28. package/src/solarite/Shell.js +31 -33
  29. package/src/solarite/Solarite.js +17 -2
  30. package/src/solarite/Template.js +75 -25
  31. package/src/solarite/Util.js +131 -7
  32. package/src/solarite/createSolarite.js +32 -25
  33. package/src/solarite/getArg.js +12 -12
  34. package/src/solarite/hash.js +18 -35
  35. package/src/solarite/r.js +128 -118
  36. package/src/solarite/watch3.js +98 -0
  37. package/src/{solarite → unused}/NodeGroupManager.js +86 -224
  38. package/src/unused/onConnect.js +79 -0
  39. package/src/{solarite → unused}/watch.js +2 -2
  40. package/src/{solarite → unused}/watch2.js +4 -4
  41. package/src/{solarite → util}/MultiValueMap.js +23 -12
  42. package/src/util/WeakArray.js +33 -0
  43. package/tests/Solarite.test.js +1108 -166
  44. package/tests/Testimony.js +274 -67
  45. package/tests/index.html +6 -35
  46. package/tests/run.bat +2 -0
  47. package/tests/NodeGroup.test.js +0 -115
  48. package/tests/Shell.test.js +0 -75
@@ -2,13 +2,14 @@ import {assert} from "../util/Errors.js";
2
2
  import ExprPath, {PathType, resolveNodePath} from "./ExprPath.js";
3
3
  import {getObjectHash} from "./hash.js";
4
4
  import Shell from "./Shell.js";
5
- import Template from "./Template.js";
6
5
  import udomdiff from "./udomdiff.js";
7
- import {watchFunction} from './watch2.js';
8
- import Util, {div, findArrayDiff, flattenAndIndent, isEvent, nodeToArrayTree, setIndent} from "./Util.js";
9
- 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";
10
10
 
11
- /** @typedef {boolean|string|number|function|Object|Array|Date|Node} Expr */
11
+
12
+ /** @typedef {boolean|string|number|function|Object|Array|Date|Node|Template} Expr */
12
13
 
13
14
  /**
14
15
  * A group of Nodes instantiated from a Shell, with Expr's filled in.
@@ -21,32 +22,29 @@ import NodeGroupManager from "./NodeGroupManager.js";
21
22
  * */
22
23
  export default class NodeGroup {
23
24
 
24
- /** @Type {NodeGroupManager} */
25
- manager;
25
+ /**
26
+ * @Type {RootNodeGroup} */
27
+ rootNg;
26
28
 
27
29
  /** @type {ExprPath} */
28
30
  parentPath;
29
31
 
30
- /** @type {Node} First node of NodeGroup. Should never be null. */
32
+ /** @type {Node|HTMLElement} First node of NodeGroup. Should never be null. */
31
33
  startNode;
32
34
 
33
- /** @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.
34
36
  * An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.*/
35
37
  endNode;
36
38
 
37
39
  /** @type {ExprPath[]} */
38
40
  paths = [];
39
41
 
40
- /** @type {string} */
42
+ /** @type {string} Key that matches the template and the expressions. */
41
43
  exactKey;
42
44
 
43
- /** @type {string} */
45
+ /** @type {string} Key that only matches the template. */
44
46
  closeKey;
45
47
 
46
- /** @type {boolean} Used by NodeGroupManager. */
47
- inUse;
48
-
49
-
50
48
  /**
51
49
  * @internal
52
50
  * @type {Node[]} Cached result of getNodes() used only for improving performance.*/
@@ -56,206 +54,97 @@ export default class NodeGroup {
56
54
  * @type {?Map<HTMLStyleElement, string>} */
57
55
  styles;
58
56
 
59
- /**
60
- * If rendering a Template with replaceMode=true, pseudoRoot points to the element where the attributes are rendered.
61
- * But pseudoRoot is outside of this.getNodes().
62
- * NodeGroupManager.render() copies the attributes from pseudoRoot to the actual web component root element.
63
- * @type {?HTMLElement} */
64
- pseudoRoot;
65
-
66
57
  currentComponentProps = {};
67
58
 
68
59
 
69
60
  /**
70
61
  * Create an "instantiated" NodeGroup from a Template and add it to an element.
71
62
  * @param template {Template} Create it from the html strings and expressions in this template.
72
- * @param manager {?NodeGroupManager}
73
- * @returns {NodeGroup} */
74
- constructor(template, manager=null) {
75
-
76
- // Used for forEach()
77
- this.template = template;
78
- this.manager = manager;
79
-
80
- // new!
81
- template.nodeGroup = this;
82
-
83
- // Get a cached version of the parsed and instantiated html, and ExprPaths.
84
- let shell = Shell.get(template.html);
85
-
86
- let fragment = shell.fragment.cloneNode(true);
63
+ * @param parentPath {?ExprPath} */
64
+ constructor(template, parentPath=null) {
65
+ if (!(this instanceof RootNodeGroup)) {
66
+ let [fragment, shell] = this.init(template, parentPath);
87
67
 
88
- // Figure out value of replaceMode option if it isn't set,
89
- // Assume replaceMode if there's only one child element and its tagname matches the root el.
90
- let replaceMode = typeof template.replaceMode === 'boolean'
91
- ? template.replaceMode
92
- : fragment.children.length===1 &&
93
- fragment.firstElementChild?.tagName.replace(/-REDCOMPONENT-PLACEHOLDER$/, '')
94
- === manager?.rootEl?.tagName
95
- if (replaceMode) {
96
- this.pseudoRoot = fragment.firstElementChild;
97
- // if (!manager.rootEl)
98
- // manager.rootEl = this.pseudoRoot;
99
- /*#IFDEV*/assert(this.pseudoRoot)/*#ENDIF*/
100
- }
101
-
102
- let childNodes = replaceMode
103
- ? fragment.firstElementChild.childNodes
104
- : fragment.childNodes;
105
-
106
-
107
- this.startNode = childNodes[0]
108
- this.endNode = childNodes[childNodes.length - 1]
68
+ this.updatePaths(fragment, shell.paths);
109
69
 
70
+ this.activateEmbeds(fragment, shell);
110
71
 
111
- // Update paths
112
- for (let oldPath of shell.paths) {
113
- let path = oldPath.clone(fragment)
114
- path.parentNg = this;
115
- this.paths.push(path);
72
+ // Apply exprs
73
+ this.applyExprs(template.exprs);
116
74
  }
117
-
118
-
119
- // Update web component placeholders.
120
- // Ctrl+F "redcomponent-placeholder" in project to find all code that manages subcomponents.
121
- // Is this list needed at all?
122
- //for (let component of shell.components)
123
- // this.components.push(resolveNodePath(this.startNode.parentNode, getNodePath(component)))
124
-
125
- /*#IFDEV*/this.verify();/*#ENDIF*/
126
-
127
- this.activateEmbeds(fragment, shell);
128
-
129
- /*#IFDEV*/
130
- assert(this.paths.length <= template.exprs.length);
131
- if (template.exprs.length)
132
- assert(this.paths.length);
133
- /*#ENDIF*/
134
-
135
- // Apply exprs
136
- this.applyExprs(template.exprs);
137
-
138
- /*#IFDEV*/this.verify();/*#ENDIF*/
139
75
  }
140
76
 
141
- activateEmbeds(root, shell) {
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();
142
88
 
143
- // static components
144
- // Must happen before ids.
145
- for (let path of shell.staticComponents) {
146
- let el = resolveNodePath(root, path)
89
+ this.parentPath = parentPath;
90
+ this.rootNg = parentPath?.parentNg?.rootNg || this;
147
91
 
148
- // Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
149
- if (el.tagName !== this.pseudoRoot?.tagName)
150
- this.createNewComponent(el)
151
- }
92
+ /*#IFDEV*/assert(this.rootNg);/*#ENDIF*/
152
93
 
153
- if (this.manager.rootEl) {
94
+ /** @type {Template} */
95
+ this.template = template;
154
96
 
155
- // ids
156
- if (this.manager.options.ids !== false)
157
- for (let path of shell.ids) {
158
- let el = resolveNodePath(root, path);
159
- let id = el.getAttribute('data-id') || el.getAttribute('id')
160
- this.manager.rootEl[id] = el;
161
- }
97
+ // new! Is this needed?
98
+ template.nodeGroup = this;
162
99
 
163
- // styles
164
- if (this.manager.options.styles !== false) {
165
- if (shell.styles.length)
166
- this.styles = new Map();
167
- for (let path of shell.styles) {
168
- let style = resolveNodePath(root, path);
169
- Util.bindStyles(style, this.manager.rootEl);
170
- this.styles.set(style, style.textContent);
171
- }
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);
172
103
 
173
- }
174
- // scripts
175
- if (this.manager.options.scripts !== false) {
176
- for (let path of shell.scripts) {
177
- let script = resolveNodePath(root, path);
178
- eval(script.textContent)
179
- }
180
- }
181
- }
182
- }
104
+ let childNodes = fragment.childNodes;
105
+ this.startNode = childNodes[0];
106
+ this.endNode = childNodes[childNodes.length - 1];
183
107
 
184
- updateStyles() {
185
- if (this.styles)
186
- for (let [style, oldText] of this.styles) {
187
- let newText = style.textContent;
188
- if (oldText !== newText)
189
- Util.bindStyles(style, this.manager.rootEl);
190
- }
108
+ return [fragment, shell];
191
109
  }
192
110
 
193
111
  /**
194
112
  * Use the paths to insert the given expressions.
195
113
  * Dispatches expression handling to other functions depending on the path type.
196
- * @param exprs {(*|*[]|function|Template)[]} */
197
- applyExprs(exprs) {
198
-
114
+ * @param exprs {(*|*[]|function|Template)[]}
115
+ * @param paths {?ExprPath[]} Optional. */
116
+ applyExprs(exprs, paths=null) {
117
+ paths = paths || this.paths;
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
 
203
124
  // We apply them in reverse order so that a <select> box has its options created from an expression
204
125
  // before its value attribute is set via an expression.
205
- for (let path of this.paths.toReversed()) {
126
+ for (let path of paths.toReversed()) {
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; // path.resolve(result);
217
- let node2 = (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(node2, expr)
228
130
 
229
- // Capture attribute expressions to later send to the constructor of a web component.
230
- // Ctrl+F "redcomponent-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(node2, expr, root);
243
- }
137
+ exprIndex = path.apply(expr, exprs, exprIndex, this.currentComponentProps);
244
138
 
245
- // Regular attribute value.
246
- else
247
- exprIndex = path.applyValueAttrib(node2, 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
  }
@@ -274,228 +163,32 @@ export default class NodeGroup {
274
163
  }
275
164
 
276
165
  /**
277
- * Insert/replace the nodes created by a single expression.
278
- * Called by applyExprs()
279
- * This function is recursive, as the functions it calls also call it.
280
- * TODO: Move this to ExprPath?
281
- * @param path {ExprPath}
282
- * @param expr {Expr}
283
- * @return {Node[]} New Nodes created. */
284
- applyNodeExpr(path, expr) {
285
- /*#IFDEV*/path.verify();/*#ENDIF*/
286
-
287
- /** @type {(Node|NodeGroup|Expr)[]} */
288
- let newNodes = [];
289
- let oldNodeGroups = path.nodeGroups;
290
- /*#IFDEV*/assert(!oldNodeGroups.includes(null))/*#ENDIF*/
291
- let secondPass = []; // indices
292
-
293
- // First Pass
294
- //for (let ng of path.nodeGroups) // TODO: Is this necessary?
295
- // ng.parentPath = null;
296
- path.nodeGroups = [];
297
- this.applyOneExpr(expr, path, newNodes, secondPass);
298
- this.existingTextNodes = null;
299
-
300
- // TODO: Create an array of old vs Nodes and NodeGroups together.
301
- // If they're all the same, skip the next steps.
302
- // Or calculate it in the loop above as we go? Have a path.lastNodeGroups property?
303
-
304
- // Second pass to find close-match NodeGroups.
305
- let flatten = false;
306
- if (secondPass.length) {
307
- for (let [nodesIndex, ngIndex] of secondPass) {
308
- let ng = this.manager.getNodeGroup(newNodes[nodesIndex], false);
309
-
310
- ng.parentPath = path;
311
- let ngNodes = ng.getNodes();
312
-
313
- /*#IFDEV*/assert(!(newNodes[nodesIndex] instanceof NodeGroup))/*#ENDIF*/
314
-
315
- if (ngNodes.length === 1)
316
- newNodes[nodesIndex] = ngNodes[0];
317
-
318
- else {
319
- newNodes[nodesIndex] = ngNodes;
320
- flatten = true;
321
- }
322
- path.nodeGroups[ngIndex] = ng;
323
- }
324
-
325
- if (flatten)
326
- newNodes = newNodes.flat(); // TODO: Only if second pass happens?
327
- }
328
-
329
- /*#IFDEV*/assert(!path.nodeGroups.includes(null))/*#ENDIF*/
330
-
331
-
332
-
333
- let oldNodes = path.getNodes();
334
- path.nodesCache = newNodes; // Replaces value set by path.getNodes()
335
-
336
-
337
- // This pre-check makes it a few percent faster?
338
- let diff = findArrayDiff(oldNodes, newNodes);
339
- if (diff !== false) {
340
-
341
- if (this.parentPath)
342
- this.parentPath.clearNodesCache();
343
-
344
- // Fast clear method
345
- let isNowEmpty = oldNodes.length && !newNodes.length;
346
- if (!isNowEmpty || !path.fastClear(oldNodes, newNodes))
347
-
348
- // Rearrange nodes.
349
- udomdiff(path.parentNode, oldNodes, newNodes, path.nodeMarker)
350
-
351
- this.saveOrphans(oldNodeGroups, oldNodes);
352
- }
353
- /*#IFDEV*/path.verify();/*#ENDIF*/
354
- }
355
-
356
- /**
357
- * Find NodeGroups that had their nodes removed and add those nodes to a Fragment so
358
- * they're not lost forever and the NodeGroup's internal structure is still consistent.
359
- * Called from NodeGroup.applyNodeExpr().
360
- * @param oldNodeGroups {NodeGroup[]}
361
- * @param oldNodes {Node[]} */
362
- saveOrphans(oldNodeGroups, oldNodes) {
363
- let oldNgMap = new Map();
364
- for (let ng of oldNodeGroups) {
365
- oldNgMap.set(ng.startNode, ng)
366
-
367
- // TODO: Is this necessary?
368
- // if (ng.parentPath)
369
- // ng.parentPath.clearNodesCache();
370
- }
371
-
372
- for (let i=0, node; node = oldNodes[i]; i++) {
373
- let ng;
374
- if (!node.parentNode && (ng = oldNgMap.get(node))) {
375
- let fragment = document.createDocumentFragment();
376
- let endNode = ng.endNode;
377
- while (node !== endNode) {
378
- fragment.append(node)
379
- i++;
380
- node = oldNodes[i];
381
- }
382
- fragment.append(endNode);
383
- }
384
- }
385
- }
386
-
387
- // TODO: Move to ExprPath?
388
- applyOneExpr(expr, path, newNodes, secondPass) {
389
-
390
- if (expr instanceof Template) {
391
- expr.parentPath = path;
392
- expr.nodegroup = this;
393
-
394
- //if (window.debug && expr.exprs[0] === 'Banana' && path.nodeGroups.length === 0)
395
- //if (window.debug && expr.exprs[0] === 'Banana')
396
- // debugger;
397
-
398
- let ng = this.manager.getNodeGroup(expr, true);
399
-
400
-
401
- if (ng) {
402
- //#IFDEV
403
- // Make sure the nodeCache of the ExprPath we took it from is sitll valid.
404
- if (ng.parentPath)
405
- ng.parentPath.verify();
406
- //#ENDIF
407
-
408
-
409
- // TODO: Track ranges of changed nodes and only pass those to udomdiff?
410
- // But will that break the swap benchmark?
411
- newNodes.push(...ng.getNodes());
412
- path.nodeGroups.push(ng);
413
- }
414
-
415
- // If expression, evaluate later to find partial match.
416
- else {
417
- secondPass.push([newNodes.length, path.nodeGroups.length])
418
- newNodes.push(expr)
419
- path.nodeGroups.push(null); // placeholder
420
- }
421
- }
422
-
423
- // Node created by an expression.
424
- else if (expr instanceof Node) {
425
-
426
- // DocumentFragment created by an expression.
427
- if (expr instanceof DocumentFragment)
428
- newNodes.push(...expr.childNodes);
429
- else
430
- newNodes.push(expr);
431
- }
432
-
433
- else if (Array.isArray(expr))
434
- for (let subExpr of expr)
435
- this.applyOneExpr(subExpr, path, newNodes, secondPass)
436
-
437
- else if (typeof expr === 'function') {
438
- expr = watchFunction(expr, this.manager)
439
-
440
- this.applyOneExpr(expr, path, newNodes, secondPass)
441
- }
442
-
443
- // Text
444
- else {
445
- // Convert falsy values (but not 0) to empty string.
446
- // Convert numbers to string so they compare the same.
447
- let text = (expr === undefined || expr === false || expr === null) ? '' : expr + '';
448
-
449
- // Fast path for updating the text of a single text node.
450
- let first = path.nodeBefore.nextSibling;
451
- if (first.nodeType === 3 && first.nextSibling === path.nodeMarker && !newNodes.includes(first)) {
452
- if (first.textContent !== text)
453
- first.textContent = text;
454
-
455
- newNodes.push(first);
456
- }
457
-
458
- else {
459
- // TODO: Optimize this into a Set or Map or something?
460
- if (!this.existingTextNodes)
461
- this.existingTextNodes = path.getNodes().filter(n => n.nodeType === 3);
462
-
463
- let idx = this.existingTextNodes.findIndex(n => n.textContent === text);
464
- if (idx !== -1)
465
- newNodes.push(...this.existingTextNodes.splice(idx, 1))
466
- else
467
- newNodes.push(path.parentNode.ownerDocument.createTextNode(text));
468
- }
469
- }
470
- }
471
-
472
- /**
473
- * Create a nested RedComponent or call render with the new props.
166
+ * Create a nested Component or call render with the new props.
474
167
  * @param el {Solarite:HTMLElement}
475
168
  * @param props {Object} */
476
169
  applyComponentExprs(el, props) {
477
-
170
+
478
171
  // TODO: Does a hash of this already exist somewhere?
479
172
  // Perhaps if Components were treated as child NodeGroups, which would need to be the child of an ExprPath,
480
173
  // then we could re-use the hash and logic from NodeManager?
481
174
  let newHash = getObjectHash(props);
482
-
483
- let isPreHtmlElement = el.tagName.endsWith('-REDCOMPONENT-PLACEHOLDER');
175
+
176
+ let isPreHtmlElement = el.tagName.endsWith('-SOLARITE-PLACEHOLDER');
484
177
  let isPreIsElement = el.hasAttribute('_is')
485
-
486
-
178
+
179
+
487
180
  // Instantiate a placeholder.
488
181
  if (isPreHtmlElement || isPreIsElement)
489
182
  el = this.createNewComponent(el, isPreHtmlElement, props);
490
-
491
- // Update params of placeholder.
183
+
184
+ // Call render() with the same params that would've been passed to the constructor.
492
185
  else if (el.render) {
493
- let oldHash = componentHash.get(el);
186
+ let oldHash = Globals.componentHash.get(el);
494
187
  if (oldHash !== newHash)
495
188
  el.render(props); // Pass new values of props to render so it can decide how it wants to respond.
496
189
  }
497
-
498
- componentHash.set(el, newHash);
190
+
191
+ Globals.componentHash.set(el, newHash);
499
192
  }
500
193
 
501
194
  /**
@@ -513,8 +206,8 @@ export default class NodeGroup {
513
206
  isPreHtmlElement = !el.hasAttribute('_is');
514
207
 
515
208
  let tagName = (isPreHtmlElement
516
- ? el.tagName.endsWith('-REDCOMPONENT-PLACEHOLDER')
517
- ? el.tagName.slice(0, -25)
209
+ ? el.tagName.endsWith('-SOLARITE-PLACEHOLDER')
210
+ ? el.tagName.slice(0, -21)
518
211
  : el.tagName
519
212
  : el.getAttribute('is')).toLowerCase();
520
213
 
@@ -538,33 +231,43 @@ export default class NodeGroup {
538
231
  // We pass the childNodes to the constructor so it can know about them,
539
232
  // instead of only afterward when they're appended to the slot below.
540
233
  // This is useful for a custom selectbox, for example.
541
- // 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
542
235
  // can add them as children before the rest of the constructor code executes.
543
236
  let ch = [... el.childNodes];
544
- NodeGroupManager.pendingChildren.push(ch); // pop() is called in Solarite constructor.
237
+ Globals.pendingChildren.push(ch); // pop() is called in Solarite constructor.
545
238
  let newEl = new Constructor(props, ch);
546
239
 
547
240
  if (!isPreHtmlElement)
548
241
  newEl.setAttribute('is', el.getAttribute('is').toLowerCase())
549
242
  el.replaceWith(newEl);
550
-
243
+
551
244
  // Set children / slot children
552
245
  // TODO: Match named slots.
553
246
  // TODO: This only appends to slot if render() is called in the constructor.
554
247
  //let slot = newEl.querySelector('slot') || newEl;
555
248
  //slot.append(...el.childNodes);
556
-
249
+
557
250
  // Copy over event attributes.
558
251
  for (let propName in props) {
559
252
  let val = props[propName];
560
253
  if (propName.startsWith('on') && typeof val === 'function')
561
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
+ }
562
265
  }
563
266
 
564
267
  // If an id pointed at the placeholder, update it to point to the new element.
565
268
  let id = el.getAttribute('data-id') || el.getAttribute('id');
566
269
  if (id)
567
- this.manager.rootEl[id] = newEl;
270
+ delve(this.getRootNode(), id.split(/\./g), newEl);
568
271
 
569
272
 
570
273
  // Update paths to use replaced element.
@@ -633,6 +336,38 @@ export default class NodeGroup {
633
336
  return this.startNode?.parentNode
634
337
  }
635
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
+
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
+ }
362
+
363
+ updateStyles() {
364
+ if (this.styles)
365
+ for (let [style, oldText] of this.styles) {
366
+ let newText = style.textContent;
367
+ if (oldText !== newText)
368
+ Util.bindStyles(style, this.getRootNodeGroup().root);
369
+ }
370
+ }
636
371
 
637
372
  //#IFDEV
638
373
  /**
@@ -652,7 +387,7 @@ export default class NodeGroup {
652
387
 
653
388
  let path = this.paths.find(path=>path.type === PathType.Content && path.getNodes().includes(nextNode));
654
389
  if (path)
655
- return [`Path[${path.parentIndex}].nodes:`]
390
+ return [`Path.nodes:`]
656
391
 
657
392
  return [];
658
393
  })
@@ -700,7 +435,184 @@ export default class NodeGroup {
700
435
  return true;
701
436
  }
702
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
+ }
703
503
  }
704
504
 
705
505
 
706
- 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
+ }