solarite 0.1.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 (63) hide show
  1. package/build/build.bat +3 -0
  2. package/build/build.js +139 -0
  3. package/build/lib/rollup.min.js +11 -0
  4. package/build/lib/source-map.min.js +1 -0
  5. package/build/lib/terser.min.js +1 -0
  6. package/dist/Solarite-debug.js +4143 -0
  7. package/dist/Solarite.js +3740 -0
  8. package/dist/Solarite.min.js +4 -0
  9. package/docs/index.md +423 -0
  10. package/docs/js/Playground.js +184 -0
  11. package/docs/js/codemirror/codemirror6.js +32036 -0
  12. package/docs/js/codemirror/themeSolarIce.js +312 -0
  13. package/docs/js/documentation.js +32 -0
  14. package/docs/js/ui/CodeEditor.js +840 -0
  15. package/docs/js/ui/DarkToggle.js +52 -0
  16. package/docs/js/ui/FlexResizer.js +142 -0
  17. package/docs/js/util/Draggable2.js +151 -0
  18. package/docs/js/util/Errors.js +9 -0
  19. package/docs/js/util/Html.js +147 -0
  20. package/docs/js/util/Icons.js +623 -0
  21. package/docs/js/util/Input.js +253 -0
  22. package/docs/js/util/Util.js +88 -0
  23. package/docs/js/util/delve.js +43 -0
  24. package/docs/media/FiraCode400.woff2 +0 -0
  25. package/docs/media/cabin-latin-700.woff2 +0 -0
  26. package/docs/media/documentation.css +93 -0
  27. package/docs/media/eternium.css +1123 -0
  28. package/docs/media/solarite-machine.webp +0 -0
  29. package/index.html +325 -0
  30. package/package.json +33 -0
  31. package/readme.md +3 -0
  32. package/src/solarite/ExprPath.js +554 -0
  33. package/src/solarite/MultiValueMap.js +65 -0
  34. package/src/solarite/NodeGroup.js +706 -0
  35. package/src/solarite/NodeGroupManager.js +582 -0
  36. package/src/solarite/Shell.js +307 -0
  37. package/src/solarite/Solarite.js +19 -0
  38. package/src/solarite/Template.js +85 -0
  39. package/src/solarite/Util.js +264 -0
  40. package/src/solarite/createSolarite.js +267 -0
  41. package/src/solarite/getArg.js +99 -0
  42. package/src/solarite/hash.js +101 -0
  43. package/src/solarite/r.js +143 -0
  44. package/src/solarite/udomdiff.js +233 -0
  45. package/src/solarite/watch.js +302 -0
  46. package/src/solarite/watch2.js +439 -0
  47. package/src/unused/FastLookupArray.js +54 -0
  48. package/src/unused/Hashes.js +339 -0
  49. package/src/unused/InUse.test.js +92 -0
  50. package/src/unused/InUseMap.js +98 -0
  51. package/src/unused/LinkedList.js +117 -0
  52. package/src/unused/LinkedList.test.js +115 -0
  53. package/src/unused/Perf.js +47 -0
  54. package/src/unused/Template.js +108 -0
  55. package/src/util/Errors.js +9 -0
  56. package/src/util/Util.js +88 -0
  57. package/src/util/delve.js +43 -0
  58. package/tests/Benchmark.test.js +319 -0
  59. package/tests/NodeGroup.test.js +115 -0
  60. package/tests/Shell.test.js +75 -0
  61. package/tests/Solarite.test.js +2896 -0
  62. package/tests/Testimony.js +602 -0
  63. package/tests/index.html +75 -0
@@ -0,0 +1,706 @@
1
+ import {assert} from "../util/Errors.js";
2
+ import ExprPath, {PathType, resolveNodePath} from "./ExprPath.js";
3
+ import {getObjectHash} from "./hash.js";
4
+ import Shell from "./Shell.js";
5
+ import Template from "./Template.js";
6
+ 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";
10
+
11
+ /** @typedef {boolean|string|number|function|Object|Array|Date|Node} Expr */
12
+
13
+ /**
14
+ * A group of Nodes instantiated from a Shell, with Expr's filled in.
15
+ *
16
+ * The range is determined by startNode and nodeMarker.
17
+ * startNode - never null. An empty text node is created before the first path if none exists.
18
+ * nodeMarker - null if this Nodegroup is at the end of its parents' nodes.
19
+ *
20
+ *
21
+ * */
22
+ export default class NodeGroup {
23
+
24
+ /** @Type {NodeGroupManager} */
25
+ manager;
26
+
27
+ /** @type {ExprPath} */
28
+ parentPath;
29
+
30
+ /** @type {Node} First node of NodeGroup. Should never be null. */
31
+ startNode;
32
+
33
+ /** @type {Node} A node that never changes that this NodeGroup should always insert its nodes before.
34
+ * An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.*/
35
+ endNode;
36
+
37
+ /** @type {ExprPath[]} */
38
+ paths = [];
39
+
40
+ /** @type {string} */
41
+ exactKey;
42
+
43
+ /** @type {string} */
44
+ closeKey;
45
+
46
+ /** @type {boolean} Used by NodeGroupManager. */
47
+ inUse;
48
+
49
+
50
+ /**
51
+ * @internal
52
+ * @type {Node[]} Cached result of getNodes() used only for improving performance.*/
53
+ nodesCache;
54
+
55
+ /**
56
+ * @type {?Map<HTMLStyleElement, string>} */
57
+ styles;
58
+
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
+ currentComponentProps = {};
67
+
68
+
69
+ /**
70
+ * Create an "instantiated" NodeGroup from a Template and add it to an element.
71
+ * @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);
87
+
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]
109
+
110
+
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);
116
+ }
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
+ }
140
+
141
+ activateEmbeds(root, shell) {
142
+
143
+ // static components
144
+ // Must happen before ids.
145
+ for (let path of shell.staticComponents) {
146
+ let el = resolveNodePath(root, path)
147
+
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
+ }
152
+
153
+ if (this.manager.rootEl) {
154
+
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
+ }
162
+
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
+ }
172
+
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
+ }
183
+
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
+ }
191
+ }
192
+
193
+ /**
194
+ * Use the paths to insert the given expressions.
195
+ * Dispatches expression handling to other functions depending on the path type.
196
+ * @param exprs {(*|*[]|function|Template)[]} */
197
+ applyExprs(exprs) {
198
+
199
+ /*#IFDEV*/this.verify();/*#ENDIF*/
200
+ // Update exprs at paths.
201
+ let exprIndex = exprs.length-1, expr, lastNode;
202
+
203
+ // We apply them in reverse order so that a <select> box has its options created from an expression
204
+ // before its value attribute is set via an expression.
205
+ for (let path of this.paths.toReversed()) {
206
+ expr = exprs[exprIndex];
207
+
208
+ // 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
+
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 {
238
+
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
+ }
244
+
245
+ // Regular attribute value.
246
+ else
247
+ exprIndex = path.applyValueAttrib(node2, exprs, exprIndex);
248
+ }
249
+
250
+ lastNode = path.nodeMarker;
251
+ }
252
+
253
+ exprIndex--;
254
+ } // end for(path of this.paths)
255
+
256
+
257
+ // Check again after we iterate through all paths to apply to a component.
258
+ if (lastNode && lastNode !== this.pseudoRoot && Object.keys(this.currentComponentProps).length) {
259
+ this.applyComponentExprs(lastNode, this.currentComponentProps);
260
+ this.currentComponentProps = {};
261
+ }
262
+
263
+ this.updateStyles();
264
+
265
+ // Invalidate the nodes cache because we just changed it.
266
+ this.nodesCache = null;
267
+
268
+ // If there's leftover expressions, there's probably an issue with the Shell that created this NodeGroup,
269
+ // and the number of paths not matching.
270
+ /*#IFDEV*/assert(exprIndex === -1);/*#ENDIF*/
271
+
272
+
273
+ /*#IFDEV*/this.verify();/*#ENDIF*/
274
+ }
275
+
276
+ /**
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.
474
+ * @param el {Solarite:HTMLElement}
475
+ * @param props {Object} */
476
+ applyComponentExprs(el, props) {
477
+
478
+ // TODO: Does a hash of this already exist somewhere?
479
+ // Perhaps if Components were treated as child NodeGroups, which would need to be the child of an ExprPath,
480
+ // then we could re-use the hash and logic from NodeManager?
481
+ let newHash = getObjectHash(props);
482
+
483
+ let isPreHtmlElement = el.tagName.endsWith('-REDCOMPONENT-PLACEHOLDER');
484
+ let isPreIsElement = el.hasAttribute('_is')
485
+
486
+
487
+ // Instantiate a placeholder.
488
+ if (isPreHtmlElement || isPreIsElement)
489
+ el = this.createNewComponent(el, isPreHtmlElement, props);
490
+
491
+ // Update params of placeholder.
492
+ else if (el.render) {
493
+ let oldHash = componentHash.get(el);
494
+ if (oldHash !== newHash)
495
+ el.render(props); // Pass new values of props to render so it can decide how it wants to respond.
496
+ }
497
+
498
+ componentHash.set(el, newHash);
499
+ }
500
+
501
+ /**
502
+ * We swap the placeholder element for the real element so we can pass its dynamic attributes
503
+ * to its constructor.
504
+ *
505
+ * The logic of this function is complex and could use cleaning up.
506
+ *
507
+ * @param el
508
+ * @param isPreHtmlElement
509
+ * @param props {Object} Attributes with dynamic values.
510
+ * @return {HTMLElement} */
511
+ createNewComponent(el, isPreHtmlElement=undefined, props=undefined) {
512
+ if (isPreHtmlElement === undefined)
513
+ isPreHtmlElement = !el.hasAttribute('_is');
514
+
515
+ let tagName = (isPreHtmlElement
516
+ ? el.tagName.endsWith('-REDCOMPONENT-PLACEHOLDER')
517
+ ? el.tagName.slice(0, -25)
518
+ : el.tagName
519
+ : el.getAttribute('is')).toLowerCase();
520
+
521
+ let dynamicProps = {...(props || {})}
522
+
523
+ // Pass other attribs to constructor, since otherwise they're not yet set on the element,
524
+ // and the constructor would otherwise have no way to see them.
525
+ if (el.attributes.length) {
526
+ if (!props)
527
+ props = {};
528
+ for (let attrib of el.attributes)
529
+ if (!props.hasOwnProperty(attrib.name))
530
+ props[attrib.name] = attrib.value;
531
+ }
532
+
533
+ // Create CustomElement and
534
+ let Constructor = customElements.get(tagName);
535
+ if (!Constructor)
536
+ throw new Error(`The custom tag name ${tagName} is not registered.`)
537
+
538
+ // We pass the childNodes to the constructor so it can know about them,
539
+ // instead of only afterward when they're appended to the slot below.
540
+ // This is useful for a custom selectbox, for example.
541
+ // NodeGroupManager.pendingChildren stores the childen so the super construtor call to Solarite's constructor
542
+ // can add them as children before the rest of the constructor code executes.
543
+ let ch = [... el.childNodes];
544
+ NodeGroupManager.pendingChildren.push(ch); // pop() is called in Solarite constructor.
545
+ let newEl = new Constructor(props, ch);
546
+
547
+ if (!isPreHtmlElement)
548
+ newEl.setAttribute('is', el.getAttribute('is').toLowerCase())
549
+ el.replaceWith(newEl);
550
+
551
+ // Set children / slot children
552
+ // TODO: Match named slots.
553
+ // TODO: This only appends to slot if render() is called in the constructor.
554
+ //let slot = newEl.querySelector('slot') || newEl;
555
+ //slot.append(...el.childNodes);
556
+
557
+ // Copy over event attributes.
558
+ for (let propName in props) {
559
+ let val = props[propName];
560
+ if (propName.startsWith('on') && typeof val === 'function')
561
+ newEl.addEventListener(propName.slice(2), e => val(e, newEl));
562
+ }
563
+
564
+ // If an id pointed at the placeholder, update it to point to the new element.
565
+ let id = el.getAttribute('data-id') || el.getAttribute('id');
566
+ if (id)
567
+ this.manager.rootEl[id] = newEl;
568
+
569
+
570
+ // Update paths to use replaced element.
571
+ for (let path of this.paths) {
572
+ if (path.nodeMarker === el)
573
+ path.nodeMarker = newEl;
574
+ if (path.nodeBefore === el)
575
+ path.nodeBefore = newEl;
576
+ }
577
+ if (this.startNode === el)
578
+ this.startNode = newEl;
579
+ if (this.endNode === el)
580
+ this.endNode = newEl;
581
+
582
+
583
+ // applyComponentExprs() is called because we're rendering.
584
+ // So we want to render the sub-component also.
585
+ if (newEl.renderFirstTime)
586
+ newEl.renderFirstTime();
587
+
588
+ // Copy attributes over.
589
+ for (let attrib of el.attributes)
590
+ if (attrib.name !== '_is')
591
+ newEl.setAttribute(attrib.name, attrib.value);
592
+
593
+ // Set dynamic attributes if they are primitive types.
594
+ for (let name in dynamicProps) {
595
+ let val = dynamicProps[name];
596
+ if (typeof val === 'boolean') {
597
+ if (val !== false && val !== undefined && val !== null)
598
+ newEl.setAttribute(name, '');
599
+ }
600
+
601
+ // If type isn't an object or array, set the attribute.
602
+ else if (['number', 'bigint', 'string'].includes(typeof val))
603
+ newEl.setAttribute(name, val);
604
+ }
605
+
606
+ return newEl;
607
+ }
608
+
609
+ /**
610
+ * Get all the nodes inclusive between startNode and endNode.
611
+ * TODO: when not using nodesCache, could this use less memory with yield?
612
+ * But we'd need to save the reference to the next Node in case it's removed.
613
+ * @return {(Node|HTMLElement)[]} */
614
+ getNodes() {
615
+ // applyExprs() invalidates this cache.
616
+ let result = this.nodesCache;
617
+ if (result) // This does speed up the partialUpdate benchmark by 10-15%.
618
+ return result;
619
+
620
+ result = [];
621
+ let current = this.startNode
622
+ let afterLast = this.endNode?.nextSibling
623
+ while (current && current !== afterLast) {
624
+ result.push(current)
625
+ current = current.nextSibling
626
+ }
627
+
628
+ this.nodesCache = result;
629
+ return result;
630
+ }
631
+
632
+ getParentNode() {
633
+ return this.startNode?.parentNode
634
+ }
635
+
636
+
637
+ //#IFDEV
638
+ /**
639
+ * @deprecated
640
+ * An interleaved array of sets of nodes and top-level ExprPaths
641
+ * @type {(Node|HTMLElement|ExprPath)[]} */
642
+ get nodes() { throw new Error('')};
643
+
644
+ get debug() {
645
+ return [
646
+ `parentNode: ${this.parentNode?.tagName?.toLowerCase()}`,
647
+ 'nodes:',
648
+ ...setIndent(this.getNodes().map(item => {
649
+ if (item instanceof Node) {
650
+
651
+ let tree = nodeToArrayTree(item, nextNode => {
652
+
653
+ let path = this.paths.find(path=>path.type === PathType.Content && path.getNodes().includes(nextNode));
654
+ if (path)
655
+ return [`Path[${path.parentIndex}].nodes:`]
656
+
657
+ return [];
658
+ })
659
+
660
+ // TODO: How to indend nodes belonging to a path vs those that just occur after the path?
661
+ return flattenAndIndent(tree)
662
+ }
663
+ else if (item instanceof ExprPath)
664
+ return setIndent(item.debug, 1)
665
+ }).flat(), 1)
666
+ ]
667
+ }
668
+
669
+ get debugNodes() { return this.getNodes() }
670
+
671
+
672
+ get debugNodesHtml() { return this.getNodes().map(n => n.outerHTML || n.textContent) }
673
+
674
+ verify() {
675
+ if (!window.verify)
676
+ return;
677
+
678
+ assert(this.startNode)
679
+ assert(this.endNode)
680
+ //assert(this.startNode !== this.endNode) // This can be true.
681
+ assert(this.startNode.parentNode === this.endNode.parentNode)
682
+
683
+ // Only if connected:
684
+ assert(!this.startNode.parentNode || this.startNode === this.endNode || this.startNode.compareDocumentPosition(this.endNode) === Node.DOCUMENT_POSITION_FOLLOWING)
685
+
686
+ // if (this.parentPath)
687
+ // assert(this.parentPath.nodeGroups.includes(this));
688
+
689
+ for (let path of this.paths) {
690
+ assert(path.parentNg === this)
691
+
692
+ // Fails for detached NodeGroups.
693
+ // NodeGroups get detached when their nodes are removed by udomdiff()
694
+ let parentNode = this.getParentNode();
695
+ if (parentNode)
696
+ assert(this.getParentNode().contains(path.getParentNode()))
697
+ path.verify();
698
+ // TODO: Make sure path nodes are all within our own node range.
699
+ }
700
+ return true;
701
+ }
702
+ //#ENDIF
703
+ }
704
+
705
+
706
+ let componentHash = new WeakMap()