solarite 0.4.0 → 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/dist/Solarite.js CHANGED
@@ -1,4 +1,7 @@
1
-
1
+ /*@__NO_SIDE_EFFECTS__*/
2
+ function assert(val) {
3
+
4
+ }
2
5
 
3
6
  let lastObjectId = 1>>>0; // Is a 32-bit int faster to increment than JavaScript's Number, which is a 64-bit float?
4
7
  let objectIds = new WeakMap();
@@ -11,8 +14,8 @@ function getObjectId(obj) {
11
14
  // return obj.toString(); // This fails to detect when a function's bound variables changes.
12
15
 
13
16
  let result = objectIds.get(obj);
14
- if (!result) { // convert to string, store in result, then add 1 to lastObjectId.
15
- result = '~\f' + (lastObjectId++); // We use a unique, 2-byte prefix to ensure it doesn't collide w/ strings not from getObjectId()
17
+ if (result===undefined) { // convert to string, store in result, then add 1 to lastObjectId.
18
+ result = '~@' + (lastObjectId++); // We use a unique, 2-byte prefix to ensure it doesn't collide w/ strings not from getObjectId()
16
19
  objectIds.set(obj, result);
17
20
  }
18
21
  return result;
@@ -22,7 +25,8 @@ function getObjectId(obj) {
22
25
  * Control how JSON.stringify() handles Nodes and Functions.
23
26
  * Normally, we'd pass a replacer() function argument to JSON.stringify() to handle Nodes and Functions.
24
27
  * But that makes JSON.stringify() take twice as long to run.
25
- * Adding a toJSON method globally on these object prototypes doesn't incur that performance penalty. */
28
+ * Adding a toJSON method globally on these object prototypes doesn't incur that performance penalty.
29
+ * TODO: This needs to be benchmarked again after the json rewrite in Chrome 138. */
26
30
  let isHashing = true;
27
31
  function toJSON() {
28
32
  return isHashing ? getObjectId(this) : this
@@ -50,26 +54,27 @@ function getObjectHash(obj) {
50
54
  // Sometimes these get unassigned by Chrome and Brave 119, as well as Firefox, seemingly randomly!
51
55
  // The same tests sometimes pass, sometimes fail, even after browser and OS restarts.
52
56
  // So we check the assignments on every run of getObjectHash()
57
+ // TODO: Cache references to Node.prototype and Function.prototype:
53
58
  if (Node.prototype.toJSON !== toJSON) {
54
59
  Node.prototype.toJSON = toJSON;
55
60
  if (Function.prototype.toJSON !== toJSON) // Will it only unmap one but not the other?
56
61
  Function.prototype.toJSON = toJSON;
57
62
  }
58
63
 
59
- let result;
60
64
  isHashing = true;
61
65
  try {
62
- result = JSON.stringify(obj);
66
+ return JSON.stringify(obj);
63
67
  }
64
68
  catch(e) {
65
- result = getObjectHashCircular(obj);
69
+ return getObjectHashCircular(obj);
70
+ }
71
+ finally {
72
+ isHashing = false;
66
73
  }
67
- isHashing = false;
68
- return result;
69
74
  }
70
75
 
71
76
  /**
72
- * Slower hashing method that supports.
77
+ * Slower hashing method that supports circular references.
73
78
  * @param obj
74
79
  * @returns {string} */
75
80
  function getObjectHashCircular(obj) {
@@ -95,25 +100,25 @@ var Globals;
95
100
  function reset() {
96
101
  Globals = {
97
102
 
98
- /**
99
- * Dynamic values that should be passed to a Component's constructor and render() function.
100
- * @type {Map<HTMLElement, any[]>} */
101
- componentArgs: new Map(),
102
-
103
103
  /**
104
104
  * Store which instances of Solarite have already been added to the DOM.
105
105
  * @type {WeakSet<HTMLElement>} */
106
106
  connected: new WeakSet(),
107
107
 
108
108
  /**
109
- * ExprPath.applyExactNodes() sets this property when an expression is being accessed.
110
- * watch() then adds the ExprPath to the list of ExprPaths that should be re-rendered when the value changes.
111
- * @type {ExprPath}*/
112
- currentExprPath: null,
109
+ * Path.applyExactNodes() sets this property when an expression is being accessed.
110
+ * watch() then adds the Path to the list of Paths that should be re-rendered when the value changes.
111
+ * @type {Path}*/
112
+ currentPath: null,
113
+
114
+ /**
115
+ * Set by NodeGroup.instantiateComponent()
116
+ * Used by RootNodeGroup.getSlotChildren(). */
117
+ currentSlotChildren: null,
113
118
 
114
119
  div: document.createElement("div"),
115
120
 
116
- /** @type {HTMLDocument} */
121
+ /** @type {HTMLDocument} The global document. */
117
122
  doc: document,
118
123
 
119
124
  /**
@@ -124,33 +129,25 @@ function reset() {
124
129
  htmlProps: {},
125
130
 
126
131
  /**
127
- * Used by ExprPath.applyEventAttrib()
132
+ * Used by Path.applyEventAttrib()
128
133
  * @type {WeakMap<Node, Record<eventName:string, [original:function, bound:function, args:*[]]>>} */
129
134
  nodeEvents: new WeakMap(),
130
135
 
131
136
  /**
132
137
  * Get the RootNodeGroup for an element.
133
138
  * @type {WeakMap<HTMLElement, RootNodeGroup>} */
134
- nodeGroups: new WeakMap(),
139
+ rootNodeGroups: new WeakMap(),
135
140
 
136
141
  /**
137
142
  * Used by h() path 9. */
138
143
  objToEl: new WeakMap(),
139
144
 
140
- //pendingChildren: [],
141
-
142
-
143
145
  /**
144
146
  * Elements that have been rendered to by h() at least once.
145
147
  * This is used by the Solarite class to know when to call onFirstConnect()
146
148
  * @type {WeakSet<HTMLElement>} */
147
149
  rendered: new WeakSet(),
148
150
 
149
- /**
150
- * Elements that are currently rendering via the h() function.
151
- * @type {WeakSet<HTMLElement>} */
152
- rendering: new WeakSet(),
153
-
154
151
  /**
155
152
  * Map from array of Html strings to a Shell created from them.
156
153
  * @type {WeakMap<string[], Shell>} */
@@ -159,13 +156,11 @@ function reset() {
159
156
  /**
160
157
  * A map of individual untagged strings to their Templates.
161
158
  * This way we don't keep creating new Templates for the same string when re-rendering.
162
- * This is used by ExprPath.applyExactNodes()
159
+ * This is used by Path.applyExactNodes()
163
160
  * @type {Record<string, Template>} */
164
161
  //stringTemplates: {},
165
162
 
166
- reset,
167
-
168
- count: 0
163
+ reset
169
164
  };
170
165
  }
171
166
  reset();
@@ -236,6 +231,8 @@ let Util = {
236
231
 
237
232
  /**
238
233
  * Convert HTMLElement attributes to an object.
234
+ * Converts dash (kebob-case) attribute names to camelCase.
235
+ * See also Solarite.getAttribs()
239
236
  * @param el {HTMLElement}
240
237
  * @param ignore {?string} Optionally ignore this attribute.
241
238
  * @return {Object} */
@@ -351,43 +348,14 @@ let Util = {
351
348
  return str.replace(/-([a-z])/g, g => g[1].toUpperCase());
352
349
  },
353
350
 
354
- /**
355
- * A generator function that recursively traverses and flattens a value.
356
- *
357
- * - If the input is an array, it recursively traverses and flattens the array.
358
- * - If the input is a function, it calls the function, replaces the function
359
- * with its result, and flattens the result if necessary. It will recursively
360
- * call functions that return other functions.
361
- * - Otherwise it yields the value as is.
362
- *
363
- * This function does not create a new array for the flattened values. Instead,
364
- * it lazily yields each item as it is encountered. This can be more memory-efficient
365
- * for large or deeply nested structures.
366
- *
367
- * @param {any} value - The value to flatten. Can be an array, object, function, or primitive.
368
- * @yields {any} - The next item in the flattened structure.
369
- *
370
- * @example
371
- * const complexArray = [
372
- * 1,
373
- * [2, () => 3, [4, () => [5, 6]], { a: 'object' }],
374
- * () => () => 7,
375
- * () => [() => 8, 9],
376
- * ]; *
377
- * for (const item of flatten(complexArray))
378
- * console.log(item); // Outputs: 1, 2, 3, 4, 5, 6, { a: 'object' }, 7, 8, 9
379
- */
380
- // *flatten(value) {
381
- // if (Array.isArray(value)) {
382
- // for (const item of value) {
383
- // yield* Util.flatten(item); // Recursively flatten arrays
384
- // }
385
- // } else if (typeof value === 'function') {
386
- // const result = value();
387
- // yield* Util.flatten(result); // Recursively flatten the result of a function
388
- // } else
389
- // yield value; // Yield primitive values as is
390
- // },
351
+ defineClass(Class, tagName) {
352
+ if (!customElements[getName](Class)) { // If not previously defined.
353
+ tagName = tagName || Util.camelToDashes(Class.name);
354
+ if (!tagName.includes('-')) // Browsers require that web components always have a dash in the name.
355
+ tagName += '-element';
356
+ customElements[define](tagName, Class);
357
+ }
358
+ },
391
359
 
392
360
  /**
393
361
  * Get the value of an input as the most appropriate JavaScript type.
@@ -477,7 +445,7 @@ let Util = {
477
445
 
478
446
  /**
479
447
  * Use an array as the value of a map, appending to it when we add.
480
- * Used by watch.js.
448
+ * Used only by watch.js.
481
449
  * @param map {Map|WeakMap|Object}
482
450
  * @param key
483
451
  * @param value */
@@ -491,6 +459,11 @@ let Util = {
491
459
  result.push(value);
492
460
  },
493
461
 
462
+ saveOrphans(nodes) {
463
+ let fragment = Globals$1.doc.createDocumentFragment();
464
+ fragment.append(...nodes);
465
+ },
466
+
494
467
  /**
495
468
  * Remove nodes from the beginning and end that are not:
496
469
  * 1. Elements.
@@ -519,306 +492,280 @@ let Util = {
519
492
 
520
493
 
521
494
 
495
+ // Trick to prevent minifier from renaming these methods.
496
+ let define = 'define';
497
+ let getName = 'getName';
498
+
499
+
500
+
522
501
  // For debugging only
523
502
 
524
503
 
525
- class MultiValueMap {
504
+ /**
505
+ * Path to where an expression should be evaluated within a Shell or NodeGroup. */
506
+ class Path {
526
507
 
527
- /** @type {Record<string, Set>} */
528
- data = {};
508
+ // Used for attributes:
529
509
 
530
- // Set a new value for a key
531
- add(key, value) {
532
- let data = this.data;
533
- let set = data[key];
534
- if (!set) {
535
- set = new Set();
536
- data[key] = set;
537
- }
538
- set.add(value);
539
- }
510
+ /**
511
+ * @type {Node} Node that occurs before this Path's first Node.
512
+ * This is necessary because udomdiff() can steal nodes from another Path.
513
+ * If we had a pointer to our own startNode then that node could be moved somewhere else w/o us knowing it.
514
+ * Used only for type='content'
515
+ * Will be null if Path has no Nodes. */
516
+ nodeBefore;
540
517
 
541
- isEmpty() {
542
- for (let key in this.data)
543
- return true;
544
- return false;
518
+ /**
519
+ * If type is AttribType.Multiple or AttribType.Value, points to the node having the attribute.
520
+ * If type is 'content', points to a node that never changes that this NodeGroup should always insert its nodes before.
521
+ * An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.
522
+ * @type {Node|HTMLElement} */
523
+ nodeMarker;
524
+
525
+
526
+ // These are set after an expression is assigned:
527
+
528
+ /** @type {NodeGroup} */
529
+ parentNg;
530
+
531
+ /** @type {NodeGroup[]} */
532
+ nodeGroups = [];
533
+
534
+ // Caches to make things faster
535
+
536
+ /**
537
+ * @private
538
+ * @type {Node[]} Cached result of getNodes() */
539
+ nodesCache;
540
+
541
+ /**
542
+ * @type {int} Index of nodeBefore among its parentNode's children. */
543
+ nodeBeforeIndex;
544
+
545
+ /**
546
+ * @type {int[]} Path to the node marker, in reverse for performance reasons. */
547
+ nodeMarkerPath;
548
+
549
+ /** @type {?function} A function called by renderWatched() to update the value of this expression. */
550
+ watchFunction
551
+
552
+
553
+ /**
554
+ * @param nodeBefore {Node}
555
+ * @param nodeMarker {?Node}*/
556
+ constructor(nodeBefore, nodeMarker) {
557
+ this.nodeBefore = nodeBefore;
558
+ this.nodeMarker = nodeMarker;
559
+
545
560
  }
546
561
 
547
562
  /**
548
- * Get all values for a key.
549
- * @param key {string}
550
- * @returns {Set|*[]} */
551
- getAll(key) {
552
- return this.data[key] || [];
563
+ * Apply expressions to a path.
564
+ * This is called by NodeGroup.applyExprs() when it's time to put the expression values into the DOM.
565
+ *
566
+ * @param exprs {Expr[]}
567
+ * Suppose we have the following tagged template:
568
+ * `<div title=${expr1} class="big ${expr2} muted ${expr3}">
569
+ * ${expr4}
570
+ * <my-component></my-component>
571
+ * <my-component user=${expr5} roles="${expr6},${expr7}"></my-component>
572
+ * </div>`
573
+ * The exprs arrays will look like this, with each being passed to a path.
574
+ * [expr1] // title attribute value.
575
+ * [expr2, expr3] // class attribute values.
576
+ * [expr4] // children of div.
577
+ * [] // arguments to first my-component constructor.
578
+ * [[expr5], [expr6, expr7]] // arguments to second my-component constructor.
579
+ * [expr5] // user attribute value.
580
+ * [expr6, expr7] // role attribute value.
581
+ * @param freeNodeGroups {boolean} Used only by watch. */
582
+ apply(exprs, freeNodeGroups=true) {}
583
+
584
+ getExpressionCount() { return 1 }
585
+
586
+
587
+ /**
588
+ * Resolve nodeMarkerPath to new root.
589
+ * TODO: Make clone() use this.*/
590
+ getNewNodeMarker(newRoot, pathOffset) {
591
+ let root = newRoot;
592
+ let path = this.nodeMarkerPath;
593
+ let pathLength = path.length - pathOffset;
594
+ for (let i=pathLength-1; i>0; i--) { // Resolve the path.
595
+
596
+ root = root.childNodes[path[i]];
597
+ }
598
+ let childNodes = root.childNodes;
599
+
600
+ return pathLength
601
+ ? childNodes[path[0]]
602
+ : newRoot;
553
603
  }
554
604
 
605
+
555
606
  /**
556
- * Remove one value from a key, and return it.
557
- * @param key {string}
558
- * @param val If specified, make sure we delete this specific value, if a key exists more than once.
559
- * @returns {*|undefined} The deleted item. */
560
- delete(key, val=undefined) {
561
- let data = this.data;
562
- let result;
563
- let set = data[key];
564
- if (!set)
565
- return undefined;
607
+ * @param newRoot {HTMLElement}
608
+ * @param pathOffset {int}
609
+ * @return {Path} */
610
+ clone(newRoot, pathOffset=0) {
611
+
566
612
 
567
- // Delete any value.
568
- if (val === undefined) {
569
- [result] = set; // Does the same as above and seems to be about the same speed.
570
- set.delete(result);
613
+ // Resolve node paths.
614
+ let nodeMarker, nodeBefore;
615
+ let root = newRoot;
616
+ let path = this.nodeMarkerPath;
617
+ let pathLength = path.length - pathOffset;
618
+ for (let i=pathLength-1; i>0; i--) { // Resolve the path.
619
+
620
+ root = root.childNodes[path[i]];
571
621
  }
622
+ let childNodes = root.childNodes;
572
623
 
573
- // Delete a specific value.
574
- else {
575
- set.delete(val);
576
- result = val;
577
- }
624
+ nodeMarker = pathLength
625
+ ? childNodes[path[0]]
626
+ : newRoot;
627
+ if (this.nodeBefore) {
628
+
629
+ nodeBefore = childNodes[this.nodeBeforeIndex];
578
630
 
579
- if (set.size === 0)
580
- delete data[key];
631
+ }
581
632
 
582
- return result;
583
- }
633
+ let result = new this.constructor(nodeBefore, nodeMarker, this.attrName, this.attrValue);
584
634
 
585
- /**
586
- * Remove one value from a key, and return it.
587
- * @param key {string}
588
- * @returns {*|undefined} The deleted item. */
589
- deleteAny(key) {
590
- let data = this.data;
591
- let result;
592
- let set = data[key];
593
- if (!set) // slower than pre-check.
594
- return undefined;
635
+ result.isComponentAttrib = this.isComponentAttrib;
595
636
 
596
- [result] = set; // Does the same as above and seems to be about the same speed.
597
- set.delete(result);
637
+ // TODO: Put this in PathToAttribValue.clone().
638
+ result.isHtmlProperty = this.isHtmlProperty;
598
639
 
599
- if (set.size === 0)
600
- delete data[key];
640
+
601
641
 
602
642
  return result;
603
643
  }
604
644
 
605
- deleteSpecific(key, val) {
606
- let data = this.data;
607
- let result;
608
- let set = data[key];
609
- if (!set)
610
- return undefined;
611
-
612
- set.delete(val);
613
- result = val;
614
-
615
- if (set.size === 0)
616
- delete data[key];
645
+ // Only used for watch.js
646
+ getNodes() {
647
+ return [this.nodeMarker];
648
+ }
617
649
 
650
+ /** @return {int[]} Returns indices in reverse order, because doing it that way is faster. */
651
+ static get(node) {
652
+ let result = [];
653
+ while(true) {
654
+ let parent = node.parentNode;
655
+ if (!parent)
656
+ break;
657
+ result.push(Array.prototype.indexOf.call(node.parentNode.childNodes, node));
658
+ node = parent;
659
+ }
618
660
  return result;
619
661
  }
620
662
 
621
- hasValue(val) {
622
- let data = this.data;
623
- let names = [];
624
- for (let name in data)
625
- if (data[name].has(val)) // TODO: iterate twice to pre-size array?
626
- names.push(name);
627
- return names;
663
+ /**
664
+ * Note that the path is backward, with the outermost element at the end.
665
+ * @param root {HTMLElement|Document|DocumentFragment|ParentNode}
666
+ * @param path {int[]}
667
+ * @returns {Node|HTMLElement|HTMLStyleElement} */
668
+ static resolve(root, path) {
669
+ for (let i=path.length-1; i>=0; i--)
670
+ root = root.childNodes[path[i]];
671
+ return root;
628
672
  }
673
+
674
+
629
675
  }
630
676
 
631
- /**
632
- * ISC License
633
- *
634
- * Copyright (c) 2020, Andrea Giammarchi, @WebReflection
635
- *
636
- * Permission to use, copy, modify, and/or distribute this software for any
637
- * purpose with or without fee is hereby granted, provided that the above
638
- * copyright notice and this permission notice appear in all copies.
639
- *
640
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
641
- * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
642
- * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
643
- * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
644
- * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
645
- * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
646
- * PERFORMANCE OF THIS SOFTWARE.
647
- */
677
+ class HtmlParser {
678
+ constructor() {
679
+ this.defaultState = {
680
+ context: HtmlParser.Text, // possible values: 'TEXT', 'TAG', 'ATTRIBUTE'
681
+ quote: null, // possible values: null, '"', "'"
682
+ buffer: '',
683
+ lastChar: null
684
+ };
685
+ this.state = {...this.defaultState};
686
+ }
648
687
 
649
- /**
650
- * @param {Node} parentNode The container where children live
651
- * @param {Node[]} a The list of current/live children
652
- * @param {Node[]} b The list of future children
653
- * @param {(entry: Node, action: number) => Node} get
654
- * The callback invoked per each entry related DOM operation.
655
- * @param {Node} [before] The optional node used as anchor to insert before.
656
- * @returns {Node[]} The same list of future children.
657
- */
658
- const udomdiff = (parentNode, a, b, before) => {
659
- const bLength = b.length;
660
- let aEnd = a.length;
661
- let bEnd = bLength;
662
- let aStart = 0;
663
- let bStart = 0;
664
- let map = null;
665
- while (aStart < aEnd || bStart < bEnd) {
666
- // append head, tail, or nodes in between: fast path
667
- if (aEnd === aStart) {
668
- // we could be in a situation where the rest of nodes that
669
- // need to be added are not at the end, and in such case
670
- // the node to `insertBefore`, if the index is more than 0
671
- // must be retrieved, otherwise it's gonna be the first item.
672
- const node = bEnd < bLength
673
- ? (bStart
674
- ? (b[bStart - 1].nextSibling)
675
- : b[bEnd - bStart])
676
- : before;
677
- while (bStart < bEnd) {
678
- let bNode = b[bStart++];
679
- parentNode.insertBefore(bNode, node);
680
- }
681
- }
682
- // remove head or tail: fast path
683
- else if (bEnd === bStart) {
684
- while (aStart < aEnd) {
685
- // remove the node only if it's unknown or not live
686
- let aNode = a[aStart];
687
- if (!map || !map.has(aNode)) {
688
- parentNode.removeChild(aNode);
689
- }
690
- aStart++;
691
- }
692
- }
693
- // same node: fast path
694
- else if (a[aStart] === b[bStart]) {
695
- aStart++;
696
- bStart++;
697
- }
698
- // same tail: fast path
699
- else if (a[aEnd - 1] === b[bEnd - 1]) {
700
- aEnd--;
701
- bEnd--;
702
- }
703
- // The once here single last swap "fast path" has been removed in v1.1.0
704
- // https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85
705
- // reverse swap: also fast path
706
- else if (
707
- a[aStart] === b[bEnd - 1] &&
708
- b[bStart] === a[aEnd - 1]
709
- ) {
710
- // this is a "shrink" operation that could happen in these cases:
711
- // [1, 2, 3, 4, 5]
712
- // [1, 4, 3, 2, 5]
713
- // or asymmetric too
714
- // [1, 2, 3, 4, 5]
715
- // [1, 2, 3, 5, 6, 4]
716
- const node = a[--aEnd].nextSibling;
717
-
718
-
719
- let a2 = b[bStart++];
720
- let b2 = a[aStart++];
721
- parentNode.insertBefore(
722
- a2,
723
- b2.nextSibling
724
- );
688
+ reset() {
689
+ this.state = {...this.defaultState};
690
+ return this.state.context;
691
+ }
725
692
 
726
- let bNode = b[--bEnd];
727
- parentNode.insertBefore(bNode, node);
693
+ /**
694
+ * Parse the next chunk of html, starting with the same context we left off with from the previous chunk.
695
+ * @param html {string}
696
+ * @param onContextChange {?function(html:string, index:int, prevContext:string, nextContext:string)}
697
+ * Called every time the context changes, and again at the last context.
698
+ * @return {('Attribute','Text','Tag')} The context at the end of html. */
699
+ parse(html, onContextChange=null) {
700
+ if (html === null)
701
+ return this.reset();
728
702
 
729
- // mark the future index as identical (yeah, it's dirty, but cheap 👍)
730
- // The main reason to do this, is that when a[aEnd] will be reached,
731
- // the loop will likely be on the fast path, as identical to b[bEnd].
732
- // In the best case scenario, the next loop will skip the tail,
733
- // but in the worst one, this node will be considered as already
734
- // processed, bailing out pretty quickly from the map index check
735
- a[aEnd] = b[bEnd];
736
- }
737
- // map based fallback, "slow" path
738
- else {
739
- // the map requires an O(bEnd - bStart) operation once
740
- // to store all future nodes indexes for later purposes.
741
- // In the worst case scenario, this is a full O(N) cost,
742
- // and such scenario happens at least when all nodes are different,
743
- // but also if both first and last items of the lists are different
744
- if (!map) {
745
- map = new Map;
746
- let i = bStart;
747
- while (i < bEnd)
748
- map.set(b[i], i++);
749
- }
750
- // if it's a future node, hence it needs some handling
751
- if (map.has(a[aStart])) {
752
- // grab the index of such node, 'cause it might have been processed
753
- const index = map.get(a[aStart]);
754
- // if it's not already processed, look on demand for the next LCS
755
- if (bStart < index && index < bEnd) {
756
- let i = aStart;
757
- // counts the amount of nodes that are the same in the future
758
- let sequence = 1;
759
- while (++i < aEnd && i < bEnd && map.get(a[i]) === (index + sequence))
760
- sequence++;
761
- // effort decision here: if the sequence is longer than replaces
762
- // needed to reach such sequence, which would brings again this loop
763
- // to the fast path, prepend the difference before a sequence,
764
- // and move only the future list index forward, so that aStart
765
- // and bStart will be aligned again, hence on the fast path.
766
- // An example considering aStart and bStart are both 0:
767
- // a: [1, 2, 3, 4]
768
- // b: [7, 1, 2, 3, 6]
769
- // this would place 7 before 1 and, from that time on, 1, 2, and 3
770
- // will be processed at zero cost
771
- if (sequence > (index - bStart)) {
772
- const node = a[aStart];
773
- while (bStart < index) {
774
- let bNode = b[bStart++];
775
- parentNode.insertBefore(bNode, node);
776
- }
703
+ for (let i = 0; i < html.length; i++) {
704
+ const char = html[i];
705
+ switch (this.state.context) {
706
+ case HtmlParser.Text:
707
+ if (char === '<' && html[i + 1].match(/[/a-z!]/i)) { // Start of a tag or comment.
708
+ onContextChange?.(html, i, this.state.context, HtmlParser.Tag);
709
+ this.state.context = HtmlParser.Tag;
710
+ this.state.buffer = '';
777
711
  }
778
- // if the effort wasn't good enough, fallback to a replace,
779
- // moving both source and target indexes forward, hoping that some
780
- // similar node will be found later on, to go back to the fast path
781
- else {
782
- let aNode = a[aStart++];
783
- let bNode = b[bStart++];
784
- parentNode.replaceChild(
785
- bNode,
786
- aNode
787
- );
712
+ break;
713
+ case HtmlParser.Tag:
714
+ if (char === '>') {
715
+ onContextChange?.(html, i+1, this.state.context, HtmlParser.Text);
716
+ this.state.context = HtmlParser.Text;
717
+ this.state.quote = null;
718
+ this.state.buffer = '';
788
719
  }
789
- }
790
- // otherwise move the source forward, 'cause there's nothing to do
791
- else
792
- aStart++;
793
- }
794
- // this node has no meaning in the future list, so it's more than safe
795
- // to remove it, and check the next live node out instead, meaning
796
- // that only the live list index should be forwarded
797
- else {
798
- let aNode = a[aStart++];
799
- parentNode.removeChild(aNode);
720
+ else if (char === ' ' && !this.state.buffer) {
721
+ // No attribute name is present. Skipping the space.
722
+ continue;
723
+ }
724
+ else if (char === ' ' || char === '/' || char === '?') {
725
+ this.state.buffer = ''; // Reset the buffer when a delimiter or potential self-closing sign is found.
726
+ }
727
+ else if (char === '"' || char === "'" || char === '=') {
728
+ onContextChange?.(html, i, this.state.context, HtmlParser.Attribute);
729
+ this.state.context = HtmlParser.Attribute;
730
+ this.state.quote = char === '=' ? null : char;
731
+ this.state.buffer = '';
732
+ }
733
+ else
734
+ this.state.buffer += char;
735
+ break;
736
+ case HtmlParser.Attribute:
737
+ // Start an attribute quote.
738
+ if (!this.state.quote && !this.state.buffer.length && (char === '"' || char === "'")) {
739
+ this.state.quote = char;
740
+ }
741
+ else if (char === this.state.quote || (!this.state.quote && this.state.buffer.length)) {
742
+ onContextChange?.(html, i, this.state.context, HtmlParser.Tag);
743
+ this.state.context = HtmlParser.Tag;
744
+ this.state.quote = null;
745
+ this.state.buffer = '';
746
+ }
747
+ else if (!this.state.quote && char === '>') {
748
+ onContextChange?.(html, i+1, this.state.context, HtmlParser.Text);
749
+ this.state.context = HtmlParser.Text;
750
+ this.state.quote = null;
751
+ this.state.buffer = '';
752
+ }
753
+ else if (char !== ' ')
754
+ this.state.buffer += char;
755
+
756
+ break;
800
757
  }
801
758
  }
759
+ onContextChange?.(html, html.length, this.state.context, null);
760
+ return this.state.context;
802
761
  }
803
- return b;
804
- };
805
-
806
- //import {ArraySpliceOp} from "./watch.js";
807
-
808
-
809
- /**
810
- * Path to where an expression should be evaluated within a Shell or NodeGroup.
811
- * Path is only valid until the expressions before it are evaluated.
812
- * TODO: Make this based on parent and node instead of path? */
813
- class ExprPath {
814
-
815
-
816
-
817
- /**
818
- * @type {ExprPathType} */
819
- type;
762
+ }
820
763
 
821
- // Used for attributes:
764
+ HtmlParser.Attribute = 'Attribute';
765
+ HtmlParser.Text = 'Text';
766
+ HtmlParser.Tag = 'Tag';
767
+
768
+ class PathToAttribValue extends Path {
822
769
 
823
770
  /** @type {?string} Used only if type=AttribType.Value. */
824
771
  attrName;
@@ -827,741 +774,919 @@ class ExprPath {
827
774
  * @type {?string[]} Used only if type=AttribType.Value. If null, use one expr to set the whole attribute value. */
828
775
  attrValue;
829
776
 
830
- /**
831
- * @type {Set<string>} Used for type=AttribType.Multiple to remember the attributes that were added. */
832
- attrNames;
777
+ isHtmlProperty;
833
778
 
834
- /**
835
- * @type {Node} Node that occurs before this ExprPath's first Node.
836
- * This is necessary because udomdiff() can steal nodes from another ExprPath.
837
- * If we had a pointer to our own startNode then that node could be moved somewhere else w/o us knowing it.
838
- * Used only for type='content'
839
- * Will be null if ExprPath has no Nodes. */
840
- nodeBefore;
779
+ constructor(nodeBefore, nodeMarker, attrName=null, attrValue=null) {
780
+ super(null, nodeMarker);
781
+ this.attrName = attrName;
782
+ this.attrValue = attrValue;
783
+ }
841
784
 
842
785
  /**
843
- * If type is AttribType.Multiple or AttribType.Value, points to the node having the attribute.
844
- * If type is 'content', points to a node that never changes that this NodeGroup should always insert its nodes before.
845
- * An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.
846
- * @type {Node|HTMLElement} */
847
- nodeMarker;
848
-
849
-
850
- // These are set after an expression is assigned:
786
+ * Set the value of an attribute. This can be for any attribute, not just attributes named "value".
787
+ * @param exprs {Expr[]} */
788
+ apply(exprs) {
789
+
851
790
 
852
- /** @type {NodeGroup} */
853
- parentNg;
791
+ let node = this.nodeMarker;
792
+ let expr = exprs[0];
854
793
 
855
- /** @type {NodeGroup[]} */
856
- nodeGroups = [];
794
+ let multiple = this.attrValue;
857
795
 
796
+ // Two-way binding between attributes
797
+ // Passing a path to the value attribute.
798
+ // Copies the attribute to the property when the input event fires.
799
+ // value=${[this, 'value]'}
800
+ // checked=${[this, 'isAgree']}
801
+ // This same logic is in NodeGroup.instantiateComponent() for components.
802
+ if (!multiple && Util.isPath(expr)) {
858
803
 
859
- // Caches to make things faster
804
+ // Don't bind events to component placeholders.
805
+ // PathToComponent will do the binding later when it instantiates the component.
806
+ if (this.isComponentAttrib && node.tagName.endsWith('-SOLARITE-PLACEHOLDER'))
807
+ return;
860
808
 
861
- /**
862
- * @private
863
- * @type {Node[]} Cached result of getNodes() */
864
- nodesCache;
809
+ /** @type {[Object, string[]]} */
810
+ let [obj, path] = [expr[0], expr.slice(1)];
865
811
 
866
- /**
867
- * @type {int} Index of nodeBefore among its parentNode's children. */
868
- nodeBeforeIndex;
812
+ if (!obj)
813
+ throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${[${expr.map(item => item ? `'${item}'` : item+'').join(', ')}]}>.`);
869
814
 
870
- /**
871
- * @type {int[]} Path to the node marker, in reverse for performance reasons. */
872
- nodeMarkerPath;
815
+ let value = delve(obj, path);
873
816
 
817
+ // Special case to allow setting select-multiple value from an array
818
+ if (this.attrName === 'value' && node.type === 'select-multiple' && Array.isArray(value)) {
819
+ // Set the .selected property on the options having a value within value.
820
+ let strValues = value.map(v => v + '');
821
+ for (let option of node.options)
822
+ option.selected = strValues.includes(option.value);
823
+ }
824
+ else {
825
+ // TODO: should we remove isFalsy, since these are always props?
826
+ const strValue = Util.isFalsy(value) ? '' : value;
874
827
 
875
- /** @type {?function} A function called by renderWatched() to update the value of this expression. */
876
- watchFunction
828
+ // Special case for contenteditable
829
+ if (this.attrName === 'value' && node.hasAttribute('contenteditable')) {
830
+ const existingValue = node.innerHTML;
831
+ if (strValue !== existingValue)
832
+ node.innerHTML = strValue;
833
+ }
834
+ else {
877
835
 
878
- /**
879
- * @type {?function} The most recent callback passed to a .map() function in this ExprPath.
880
- * TODO: What if one ExprPath has two .map() calls? Maybe we just won't support that. */
881
- mapCallback
836
+ // If we don't have this condition, when we call render(), the browser will scroll to the currently
837
+ // selected item in a <select> and mess up manually scrolling to a different value.
838
+ if (strValue !== node[this.attrName])
839
+ node[this.attrName] = strValue;
840
+ }
841
+ }
882
842
 
883
- isHtmlProperty = undefined;
884
-
885
- /**
886
- * @param nodeBefore {Node}
887
- * @param nodeMarker {?Node}
888
- * @param type {ExprPathType}
889
- * @param attrName {?string}
890
- * @param attrValue {string[]} */
891
- constructor(nodeBefore, nodeMarker, type=ExprPathType.Content, attrName=null, attrValue=null) {
892
-
893
- // If path is a node.
894
- this.nodeBefore = nodeBefore;
895
- this.nodeMarker = nodeMarker;
896
- this.type = type;
897
- this.attrName = attrName;
898
- this.attrValue = attrValue;
899
- if (type === ExprPathType.AttribMultiple)
900
- this.attrNames = new Set();
901
- }
843
+ // TODO: We need to remove any old listeners, like in bindEventAttribute.
844
+ // Does bindEvent() now handle that?
845
+ let func = () => {
846
+ let value = (this.attrName === 'value')
847
+ ? Util.getInputValue(node)
848
+ : node[this.attrName];
849
+ delve(obj, path, value);
850
+ };
902
851
 
903
- /**
904
- * Apply any type of expression.
905
- * This calls other apply functions.
906
- *
907
- * One very messy part of this function is that it may apply multiple expressions if they're all part
908
- * of the same attribute value.
909
- *
910
- * We should modify path.applyValueAttrib so it stores the procssed parts and then only calls
911
- * setAttribute() once all the pieces are in place.
912
- *
913
- * @param exprs {Expr[]}
914
- * @param freeNodeGroups {boolean} */
915
- apply(exprs, freeNodeGroups=true) {
916
- switch (this.type) {
917
- case 1: // PathType.Content:
918
- this.applyNodes(exprs[0], freeNodeGroups);
919
- break;
920
- case 2: // PathType.Multiple:
921
- this.applyMultipleAttribs(this.nodeMarker, exprs[0]);
922
- break;
923
- case 4: // PathType.Comment:
924
- // Expressions inside Html comments. Deliberately empty because we won't waste time updating them.
925
- break;
926
- case 5: // PathType.Event:
927
- this.applyEventAttrib(this.nodeMarker, exprs[0], this.parentNg.rootNg.root);
928
- break;
929
- default: // 3 PathType.Attribute
930
- // One attribute value may have multiple expressions. Here we apply them all at once.
931
- this.applyValueAttrib(this.nodeMarker, exprs);
932
- break;
852
+ // We use capture so we update the values before other events added by the user.
853
+ // TODO: Bind to scroll events also?
854
+ // What about resize events and width/height?
855
+ this.bindEvent(node, this.parentNg.getRootNode(), this.attrName, 'input', func, [], true);
933
856
  }
934
- }
935
857
 
936
- /**
937
- * Insert/replace the nodes created by a single expression.
938
- * Called by applyExprs()
939
- * This function is recursive. It calls functions that call applyNodes().
940
- * @param expr {Expr}
941
- * @param freeNodeGroups {boolean}
942
- * @return {Node[]} New Nodes created. */
943
- applyNodes(expr, freeNodeGroups=true) {
944
- let path = this;
858
+ // Regular attribute
859
+ else {
860
+ // Cache this on Path.isHtmlProperty when Shell creates the props.
861
+ // Have Path.clone() copy .isHtmlProperty?
862
+ let isProp = this.isHtmlProperty;
945
863
 
946
- // This can be done at the beginning or the end of this function.
947
- // If at the end, we may get rendering done faster.
948
- // But when at the beginning, it leaves all the nodes in-use so we can do a renderWatched().
949
- if (freeNodeGroups)
950
- path.freeNodeGroups();
864
+ // Values to toggle an attribute
865
+ if (!multiple) {
866
+ Globals$1.currentPath = this; // Used by watch()
867
+ if (typeof expr === 'function') {
868
+ if (this.isComponentAttrib)
869
+ return;
951
870
 
952
-
871
+ this.watchFunction = expr; // The function that gets the expression, used for renderWatched()
872
+ expr = expr();
873
+ }
874
+ else
875
+ expr = Util.makePrimitive(expr);
876
+ Globals$1.currentPath = null;
877
+ }
953
878
 
954
- /** @type {(Node|NodeGroup|Expr)[]} */
955
- let newNodes = [];
956
- let oldNodeGroups = path.nodeGroups;
957
-
958
- let secondPass = []; // indices
959
879
 
960
- path.nodeGroups = []; // Reset before applyExactNodes and the code below rebuilds it.
961
- path.applyExactNodes(expr, newNodes, secondPass);
880
+ if (!multiple && (expr === undefined || expr === false || expr === null)) { // Util.isFalsy() inlined.
881
+ if (isProp)
882
+ node[this.attrName] = false;
883
+ node.removeAttribute(this.attrName);
884
+ }
885
+ else if (!multiple && expr === true) {
886
+ if (isProp)
887
+ node[this.attrName] = true;
888
+ node.setAttribute(this.attrName, '');
889
+ }
962
890
 
963
- //this.existingTextNodes = null;
891
+ // A non-toggled attribute
892
+ else {
964
893
 
965
- // TODO: Create an array of old vs Nodes and NodeGroups together.
966
- // If they're all the same, skip the next steps.
967
- // Or calculate it in the loop above as we go? Have a path.lastNodeGroups property?
894
+ // If it's a series of expressions among strings, join them together.
895
+ let joinedValue = multiple // avoid function call if there are no strings
896
+ ? this.getValue(exprs)
897
+ : expr; // If the attribute is one expression with no strings
968
898
 
969
- // Second pass to find close-match NodeGroups.
970
- let flatten = false;
971
- if (secondPass.length) {
972
- for (let [nodesIndex, ngIndex] of secondPass) {
973
- let ng = path.getNodeGroup(newNodes[nodesIndex], false);
974
- let ngNodes = ng.getNodes();
899
+ // Only update attributes if the value has changed.
900
+ // This is needed for setting input.value, .checked, option.selected, etc.
901
+ let oldVal = isProp
902
+ ? node[this.attrName]
903
+ : node.getAttribute(this.attrName);
904
+ if (oldVal !== joinedValue) {
975
905
 
976
-
906
+ // <textarea value=${expr}></textarea>
907
+ // Without this branch we have no way to set the value of a textarea,
908
+ // since we also prohibit expressions that are a child of textarea.
909
+ if (isProp)
910
+ node[this.attrName] = joinedValue;
977
911
 
978
- if (ngNodes.length === 1) // flatten manually so we can skip flattening below.
979
- newNodes[nodesIndex] = ngNodes[0];
912
+ // Allow one-way binding to contenteditable value attribute.
913
+ // Contenteditables normally don't have a value attribute and have their content set via innerHTML.
914
+ // Solarite doesn't allow contenteditables to have expressions as their children.
915
+ else if (this.attrName === 'value' && node.hasAttribute('contenteditable')) {
916
+ node.innerHTML = joinedValue;
917
+ }
980
918
 
981
- else {
982
- newNodes[nodesIndex] = ngNodes;
983
- flatten = true;
919
+ // TODO: Putting an 'else' here would be more performant
920
+ node.setAttribute(this.attrName, joinedValue);
984
921
  }
985
- path.nodeGroups[ngIndex] = ng;
986
922
  }
987
-
988
- if (flatten)
989
- newNodes = newNodes.flat(); // Only if second pass happens.
990
923
  }
924
+ }
991
925
 
992
-
993
-
994
- let oldNodes = path.getNodes();
995
-
996
- // This pre-check makes it a few percent faster?
997
- let same = Util.arraySame(oldNodes, newNodes);
998
- if (!same) {
999
926
 
1000
- path.nodesCache = newNodes; // Replaces value set by path.getNodes()
927
+ getExpressionCount() { return this.attrValue ? this.attrValue.length-1 : 1 }
1001
928
 
1002
- if (this.parentNg.parentPath)
1003
- this.parentNg.parentPath.clearNodesCache();
929
+ /**
930
+ * @param exprs {Expr|Expr[]} // TODO: Why is this sometimes not an array?
931
+ * @return {string} The joined values of the expressions, or the first expression if there are no strings. */
932
+ getValue(exprs) {
1004
933
 
1005
- // Fast clear method
1006
- let isNowEmpty = oldNodes.length && !newNodes.length;
1007
- if (!isNowEmpty || !path.fastClear())
1008
- // Rearrange nodes.
1009
- udomdiff(path.nodeMarker.parentNode, oldNodes, newNodes, path.nodeMarker);
934
+
935
+ //if (!Array.isArray(exprs))
936
+ // return exprs;
1010
937
 
1011
- // TODO: Put this in a remove() function of NodeGroup.
1012
- // Then only run it on the old nodeGroups that were actually removed.
1013
- //Util.saveOrphans(oldNodeGroups, oldNodes);
938
+ if (!this.attrValue) {// If it's not multiple paths inside a single attribute, return first (and only) expression.
939
+
940
+ return exprs[0];
941
+ }
1014
942
 
1015
- for (let ng of oldNodeGroups)
1016
- if (!ng.startNode.parentNode)
1017
- ng.removeAndSaveOrphans();
1018
-
1019
- // Instantiate components created within ${...} expressions.
1020
- // Also see this.applyExactNodes() which handles calling render() on web components even if they are unchanged.
1021
- for (let el of newNodes) {
1022
- if (el?.nodeType === 1) { // HTMLElement
1023
- if (el.hasAttribute('solarite-placeholder'))
1024
- this.parentNg.handleComponent(el, null, true);
1025
- for (let child of el.querySelectorAll('[solarite-placeholder]'))
1026
- this.parentNg.handleComponent(child, null, true);
1027
- }
943
+ let result = [];
944
+ let values = this.attrValue;
945
+ for (let i = 0; i < values.length; i++) {
946
+ result.push(values[i]);
947
+ if (i < values.length - 1) {
948
+ Globals$1.currentPath = this; // Used by watch()
949
+ let val = Util.makePrimitive(exprs[i]);
950
+ Globals$1.currentPath = null;
951
+ if (!Util.isFalsy(val))
952
+ result.push(val);
1028
953
  }
1029
954
  }
1030
-
1031
-
955
+ return result.join('')
1032
956
  }
1033
957
 
1034
958
  /**
1035
- * Used by watch() for inserting/removing/replacing individual loop items.
1036
- * @param op {ArraySpliceOp} */
1037
- applyArrayOp(op) {
959
+ * Call function when eventName is triggerd on node.
960
+ * @param node {HTMLElement}
961
+ * @param root {HTMLElement}
962
+ * @param key {string}
963
+ * @param eventName {string}
964
+ * @param func {function}
965
+ * @param args {array}
966
+ * @param capture {boolean} */
967
+ bindEvent(node, root, key, eventName, func, args, capture=false) {
968
+ let nodeEvents = Globals$1.nodeEvents.get(node);
969
+ if (!nodeEvents) {
970
+ nodeEvents = {[key]: new Array(3)};
971
+ Globals$1.nodeEvents.set(node, nodeEvents);
972
+ }
973
+ let nodeEvent = nodeEvents[key];
974
+ if (!nodeEvent)
975
+ nodeEvents[key] = nodeEvent = new Array(3);
1038
976
 
1039
- // Replace NodeGroups
1040
- let replaceCount = Math.min(op.deleteCount, op.items.length);
1041
- let deleteCount = op.deleteCount - replaceCount;
1042
- for (let i=0; i<replaceCount; i++) {
1043
- let oldNg = this.nodeGroups[op.index + i]; // TODO: One expr can create multiple nodegroups.
977
+ if (typeof func !== 'function')
978
+ throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${${func}}> because it's not a function.`);
1044
979
 
1045
- // Try to find an exact match
1046
- let func = this.mapCallback || this.watchFunction;
1047
- let expr = func(op.items[i]);
980
+ // If function has changed, remove and rebind the event.
981
+ if (nodeEvent[0] !== func) {
1048
982
 
1049
- // If the result of func isn't a template, conver it to one or more templates.
1050
- this.exprToTemplates(expr, template => { // TODO: An expr can create multiple NodeGroups. I need a way to group them.
983
+ // TODO: We should be removing event listeners when calling getNodeGroup(),
984
+ // when we get the node from the list of nodeGroupsAttached/nodeGroupsDetached,
985
+ // instead of only when we rebind an event.
986
+ let [existing, existingBound, _] = nodeEvent;
987
+ if (existing)
988
+ node.removeEventListener(eventName, existingBound, capture);
1051
989
 
1052
- let ng = this.getNodeGroup(template, true); // Removes from nodeGroupsAttached and adds to nodeGroupsRendered()
1053
- if (ng && ng === oldNg) ; else {
990
+ let originalFunc = func;
1054
991
 
1055
- // Find a close match or create a new node group
1056
- if (!ng)
1057
- ng = this.getNodeGroup(template, false); // adds back to nodeGroupsRendered()
1058
- this.nodeGroups[op.index + i] = ng; // TODO: Remove old one to nodeGroupsDetached?
992
+ // BoundFunc sets the "this" variable to be the current Solarite component.
993
+ let boundFunc = (event) => {
994
+ let args = nodeEvent[2];
995
+ return originalFunc.call(root, ...args, event, node);
996
+ };
1059
997
 
1060
- // Splice in the new nodes.
1061
- let insertBefore = oldNg.startNode;
1062
- for (let node of ng.getNodes())
1063
- insertBefore.parentNode.insertBefore(node, insertBefore);
998
+ // Save both the original and bound functions.
999
+ // Original so we can compare it against a newly assigned function.
1000
+ // Bound so we can use it with removeEventListner().
1001
+ nodeEvent[0] = originalFunc;
1002
+ nodeEvent[1] = boundFunc;
1064
1003
 
1065
- // Remove the old nodes.
1066
- if (ng !== oldNg)
1067
- oldNg.removeAndSaveOrphans();
1068
- }
1069
- });
1070
- }
1004
+ node.addEventListener(eventName, boundFunc, capture);
1071
1005
 
1072
- // Delete extra at the end.
1073
- if (deleteCount > 0) {
1074
- for (let i=0; i<deleteCount; i++) {
1075
- let oldNg = this.nodeGroups[op.index + replaceCount + i];
1076
- oldNg.removeAndSaveOrphans();
1077
- }
1078
- this.nodeGroups.splice(op.index + replaceCount, deleteCount);
1006
+ // TODO: classic event attribs?
1007
+ //el[attr.name] = e => // e.g. el.onclick = ... // put "event", "el", and "this" in scope for the event code.
1008
+ // (new Function('event', 'el', attr.value)).bind(this.manager.rootEl)(e, el)
1079
1009
  }
1080
1010
 
1081
- // Add extra at the end.
1082
- else {
1083
- let newItems = op.items.slice(replaceCount);
1084
-
1085
- let insertBefore = this.nodeGroups[op.index + replaceCount]?.startNode || this.nodeMarker;
1086
- for (let i = 0; i < newItems.length; i++) { // We use nodeMarker if the subequent (or all) nodeGroups have been removed.
1011
+ // Otherwise just update the args to the function.
1012
+ nodeEvents[key][2] = args;
1013
+ }
1014
+ }
1015
+
1016
+ // TODO: Merge this into PathToAttribValue?
1017
+ class PathToEvent extends PathToAttribValue {
1087
1018
 
1019
+ constructor(nodeBefore, nodeMarker, attrName=null, attrValue=null) {
1020
+ super(null, nodeMarker, attrName, attrValue);
1021
+ }
1088
1022
 
1089
- // Try to find exact match
1090
- let template = this.mapCallback(newItems[i]);
1091
- let ng = this.getNodeGroup(template, true); // Removes from nodeGroupsAttached and adds to nodeGroupsRendered()
1092
- if (!ng) // Find a close match or create a new node group
1093
- ng = this.getNodeGroup(template, false); // adds back to nodeGroupsRendered()
1023
+ /**
1024
+ * Handle attributes for event binding, such as:
1025
+ * onclick=${(e, el) => this.doSomething(el, 'meow')}
1026
+ * oninput=${[this.doSomething, 'meow']}
1027
+ * onclick=${[this, 'doSomething', 'meow']}
1028
+ *
1029
+ * @param exprs {Expr[]} Only the first is used.*/
1030
+ apply(exprs) {
1031
+
1094
1032
 
1095
- this.nodeGroups.push(ng);
1033
+ // Don't bind events to component placeholders.
1034
+ // PathToComponent will do the binding later when it instantiates the component.
1035
+ if (this.isComponentAttrib && this.nodeMarker.tagName.endsWith('-SOLARITE-PLACEHOLDER'))
1036
+ return;
1096
1037
 
1097
- // Splice in the new nodes.
1098
- for (let node of ng.getNodes())
1099
- insertBefore.parentNode.insertBefore(node, insertBefore);
1100
- }
1101
- }
1038
+ let expr = exprs[0];
1039
+ let root = this.parentNg.rootNg.root;
1102
1040
 
1103
1041
 
1104
1042
 
1105
- // TODO: update or invalidate the nodes cache?
1106
- this.nodesCache = null;
1107
- }
1043
+ let node = this.nodeMarker;
1044
+
1045
+ let eventName = this.attrName.slice(2); // remove "on-" prefix.
1046
+ let func;
1047
+ let args = [];
1048
+
1049
+ // Convert array to function.
1050
+ // oninput=${[this.doSomething, 'meow']}
1051
+ if (Array.isArray(expr) && typeof expr[0] === 'function') {
1052
+ func = expr[0];
1053
+ args = expr.slice(1);
1054
+ }
1055
+ else if (typeof expr === 'function')
1056
+ func = expr;
1057
+ else
1058
+ throw new Error(`Invalid event binding: <${node.tagName.toLowerCase()} ${this.attrName}=\${${JSON.stringify(expr)}}>`);
1059
+
1060
+ this.bindEvent(node, root, eventName, eventName, func, args);
1061
+ }
1062
+
1063
+
1064
+
1065
+ }
1066
+
1067
+ class PathToAttribs extends Path {
1068
+
1069
+ /**
1070
+ * @type {Set<string>} Used for type=AttribType.Multiple to remember the attributes that were added. */
1071
+ attrNames;
1072
+
1073
+ constructor(nodeBefore, nodeMarker) {
1074
+ super(null, null);
1075
+ this.nodeMarker = nodeMarker;
1076
+ this.attrNames = new Set();
1077
+ }
1078
+
1079
+ /**
1080
+ * @param exprs {Expr[][]} Only the first is used.
1081
+ * @param freeNodeGroups {boolean} Used only for watch. */
1082
+ apply(exprs, freeNodeGroups) {
1083
+
1084
+
1085
+ let expr = exprs[0];
1086
+ let node = this.nodeMarker;
1108
1087
 
1109
- /**
1110
- * Recursively traverse expr.
1111
- * If a value is a function, evaluate it.
1112
- * If a value is an array, recurse on each item.
1113
- * If it's a primitive, convert it to a Template.
1114
- * Otherwise pass the item (which is now either a Template or a Node) to callback.
1115
- * @param expr
1116
- * @param callback {function(Node|Template)}
1117
- *
1118
- * TODO: have applyExactNodes() use this function. */
1119
- exprToTemplates(expr, callback) {
1120
1088
  if (Array.isArray(expr))
1121
- for (let subExpr of expr)
1122
- this.exprToTemplates(subExpr, callback);
1089
+ expr = expr.flat().join(' '); // flat and join so we can accept arrays of arrays of strings.
1123
1090
 
1124
- else if (typeof expr === 'function') {
1125
- // TODO: One ExprPath can have multiple expr functions.
1126
- // But if using it as a watch, it should only have one at the top level.
1127
- // So maybe this is ok.
1128
- Globals$1.currentExprPath = this; // Used by watch()
1091
+ // Add new attributes
1092
+ let oldNames = this.attrNames;
1093
+ this.attrNames = new Set();
1094
+ if (expr) {
1095
+ if (typeof expr === 'function') {
1096
+ Globals$1.currentPath = this; // Used by watch()
1097
+ this.watchFunction = expr; // used by renderWatched()
1098
+ expr = expr();
1099
+ Globals$1.currentPath = null;
1100
+ }
1129
1101
 
1130
- this.watchFunction = expr; // TODO: Only do this if it's a top level function.
1131
- expr = expr(); // As expr accesses watched variables, watch() uses Globals.currentExprPath to mark where those watched variables are being used.
1132
- Globals$1.currentExprPath = null;
1102
+ // Attribute as name: value object.
1103
+ if (typeof expr === 'object') {
1104
+ for (let name in expr) {
1105
+ let value = expr[name];
1106
+ if (value === undefined || value === false || value === null)
1107
+ continue;
1108
+ node.setAttribute(name, value);
1109
+ this.attrNames.add(name);
1110
+ }
1111
+ }
1112
+
1113
+ // Attributes as string
1114
+ else {
1115
+ let attrs = (expr + '') // Split string into multiple attributes.
1116
+ .split(/([\w-]+\s*=\s*(?:"[^"]*"|'[^']*'|\S+))/g)
1117
+ .map(text => text.trim())
1118
+ .filter(text => text.length);
1119
+
1120
+ for (let attr of attrs) {
1121
+ let [name, value] = attr.split(/\s*=\s*/); // split on first equals.
1122
+ value = (value || '').replace(/^(['"])(.*)\1$/, '$2'); // trim value quotes if they match.
1123
+ node.setAttribute(name, value);
1124
+ this.attrNames.add(name);
1125
+ }
1126
+ }
1127
+ }
1128
+
1129
+ // Remove old attributes.
1130
+ for (let oldName of oldNames)
1131
+ if (!this.attrNames.has(oldName))
1132
+ node.removeAttribute(oldName);
1133
+ }
1134
+
1135
+
1136
+ getExpressionCount() { return 1 }
1137
+ }
1138
+
1139
+ /**
1140
+ * ISC License
1141
+ *
1142
+ * Copyright (c) 2020, Andrea Giammarchi, @WebReflection
1143
+ *
1144
+ * Permission to use, copy, modify, and/or distribute this software for any
1145
+ * purpose with or without fee is hereby granted, provided that the above
1146
+ * copyright notice and this permission notice appear in all copies.
1147
+ *
1148
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
1149
+ * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
1150
+ * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
1151
+ * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
1152
+ * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
1153
+ * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
1154
+ * PERFORMANCE OF THIS SOFTWARE.
1155
+ */
1156
+
1157
+ /**
1158
+ * @param {Node} parentNode The container where children live
1159
+ * @param {Node[]} a The list of current/live children
1160
+ * @param {Node[]} b The list of future children
1161
+ * @param {(entry: Node, action: number) => Node} get
1162
+ * The callback invoked per each entry related DOM operation.
1163
+ * @param {Node} [before] The optional node used as anchor to insert before.
1164
+ * @returns {Node[]} The same list of future children.
1165
+ */
1166
+ const udomdiff = (parentNode, a, b, before) => {
1167
+ const bLength = b.length;
1168
+ let aEnd = a.length;
1169
+ let bEnd = bLength;
1170
+ let aStart = 0;
1171
+ let bStart = 0;
1172
+ let map = null;
1173
+ while (aStart < aEnd || bStart < bEnd) {
1174
+ // append head, tail, or nodes in between: fast path
1175
+ if (aEnd === aStart) {
1176
+ // we could be in a situation where the rest of nodes that
1177
+ // need to be added are not at the end, and in such case
1178
+ // the node to `insertBefore`, if the index is more than 0
1179
+ // must be retrieved, otherwise it's gonna be the first item.
1180
+ const node = bEnd < bLength
1181
+ ? (bStart
1182
+ ? (b[bStart - 1].nextSibling)
1183
+ : b[bEnd - bStart])
1184
+ : before;
1185
+ while (bStart < bEnd) {
1186
+ let bNode = b[bStart++];
1187
+ parentNode.insertBefore(bNode, node);
1188
+ }
1189
+ }
1190
+ // remove head or tail: fast path
1191
+ else if (bEnd === bStart) {
1192
+ while (aStart < aEnd) {
1193
+ // remove the node only if it's unknown or not live
1194
+ let aNode = a[aStart];
1195
+ if (!map || !map.has(aNode)) {
1196
+ parentNode.removeChild(aNode);
1197
+ }
1198
+ aStart++;
1199
+ }
1200
+ }
1201
+ // same node: fast path
1202
+ else if (a[aStart] === b[bStart]) {
1203
+ aStart++;
1204
+ bStart++;
1205
+ }
1206
+ // same tail: fast path
1207
+ else if (a[aEnd - 1] === b[bEnd - 1]) {
1208
+ aEnd--;
1209
+ bEnd--;
1210
+ }
1211
+ // The once here single last swap "fast path" has been removed in v1.1.0
1212
+ // https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85
1213
+ // reverse swap: also fast path
1214
+ else if (
1215
+ a[aStart] === b[bEnd - 1] &&
1216
+ b[bStart] === a[aEnd - 1]
1217
+ ) {
1218
+ // this is a "shrink" operation that could happen in these cases:
1219
+ // [1, 2, 3, 4, 5]
1220
+ // [1, 4, 3, 2, 5]
1221
+ // or asymmetric too
1222
+ // [1, 2, 3, 4, 5]
1223
+ // [1, 2, 3, 5, 6, 4]
1224
+ const node = a[--aEnd].nextSibling;
1225
+
1226
+
1227
+ let a2 = b[bStart++];
1228
+ let b2 = a[aStart++];
1229
+ parentNode.insertBefore(
1230
+ a2,
1231
+ b2.nextSibling
1232
+ );
1233
+
1234
+ let bNode = b[--bEnd];
1235
+ parentNode.insertBefore(bNode, node);
1236
+
1237
+ // mark the future index as identical (yeah, it's dirty, but cheap 👍)
1238
+ // The main reason to do this, is that when a[aEnd] will be reached,
1239
+ // the loop will likely be on the fast path, as identical to b[bEnd].
1240
+ // In the best case scenario, the next loop will skip the tail,
1241
+ // but in the worst one, this node will be considered as already
1242
+ // processed, bailing out pretty quickly from the map index check
1243
+ a[aEnd] = b[bEnd];
1244
+ }
1245
+ // map based fallback, "slow" path
1246
+ else {
1247
+ // the map requires an O(bEnd - bStart) operation once
1248
+ // to store all future nodes indexes for later purposes.
1249
+ // In the worst case scenario, this is a full O(N) cost,
1250
+ // and such scenario happens at least when all nodes are different,
1251
+ // but also if both first and last items of the lists are different
1252
+ if (!map) {
1253
+ map = new Map;
1254
+ let i = bStart;
1255
+ while (i < bEnd)
1256
+ map.set(b[i], i++);
1257
+ }
1258
+ // if it's a future node, hence it needs some handling
1259
+ if (map.has(a[aStart])) {
1260
+ // grab the index of such node, 'cause it might have been processed
1261
+ const index = map.get(a[aStart]);
1262
+ // if it's not already processed, look on demand for the next LCS
1263
+ if (bStart < index && index < bEnd) {
1264
+ let i = aStart;
1265
+ // counts the amount of nodes that are the same in the future
1266
+ let sequence = 1;
1267
+ while (++i < aEnd && i < bEnd && map.get(a[i]) === (index + sequence))
1268
+ sequence++;
1269
+ // effort decision here: if the sequence is longer than replaces
1270
+ // needed to reach such sequence, which would brings again this loop
1271
+ // to the fast path, prepend the difference before a sequence,
1272
+ // and move only the future list index forward, so that aStart
1273
+ // and bStart will be aligned again, hence on the fast path.
1274
+ // An example considering aStart and bStart are both 0:
1275
+ // a: [1, 2, 3, 4]
1276
+ // b: [7, 1, 2, 3, 6]
1277
+ // this would place 7 before 1 and, from that time on, 1, 2, and 3
1278
+ // will be processed at zero cost
1279
+ if (sequence > (index - bStart)) {
1280
+ const node = a[aStart];
1281
+ while (bStart < index) {
1282
+ let bNode = b[bStart++];
1283
+ parentNode.insertBefore(bNode, node);
1284
+ }
1285
+ }
1286
+ // if the effort wasn't good enough, fallback to a replace,
1287
+ // moving both source and target indexes forward, hoping that some
1288
+ // similar node will be found later on, to go back to the fast path
1289
+ else {
1290
+ let aNode = a[aStart++];
1291
+ let bNode = b[bStart++];
1292
+ parentNode.replaceChild(
1293
+ bNode,
1294
+ aNode
1295
+ );
1296
+ }
1297
+ }
1298
+ // otherwise move the source forward, 'cause there's nothing to do
1299
+ else
1300
+ aStart++;
1301
+ }
1302
+ // this node has no meaning in the future list, so it's more than safe
1303
+ // to remove it, and check the next live node out instead, meaning
1304
+ // that only the live list index should be forwarded
1305
+ else {
1306
+ let aNode = a[aStart++];
1307
+ parentNode.removeChild(aNode);
1308
+ }
1309
+ }
1310
+ }
1311
+ return b;
1312
+ };
1313
+
1314
+ class MultiValueMap {
1315
+
1316
+ /** @type {Record<string, Set>} */
1317
+ data = {};
1318
+
1319
+ // Set a new value for a key
1320
+ add(key, value) {
1321
+ let data = this.data;
1322
+ let set = data[key];
1323
+ if (!set) {
1324
+ set = new Set();
1325
+ data[key] = set;
1326
+ }
1327
+ set.add(value);
1328
+ }
1329
+
1330
+ isEmpty() {
1331
+ for (let key in this.data)
1332
+ return true;
1333
+ return false;
1334
+ }
1335
+
1336
+ /**
1337
+ * Get all values for a key.
1338
+ * @param key {string}
1339
+ * @returns {Set|*[]} */
1340
+ getAll(key) {
1341
+ return this.data[key] || [];
1342
+ }
1343
+
1344
+ /**
1345
+ * Remove one value from a key, and return it.
1346
+ * @param key {string}
1347
+ * @param val If specified, make sure we delete this specific value, if a key exists more than once.
1348
+ * @returns {*|undefined} The deleted item. */
1349
+ delete(key, val=undefined) {
1350
+ let data = this.data;
1351
+ let result;
1352
+ let set = data[key];
1353
+ if (!set)
1354
+ return undefined;
1355
+
1356
+ // Delete any value.
1357
+ if (val === undefined) {
1358
+ [result] = set; // Get the first value from the set.
1359
+ set.delete(result);
1360
+ }
1361
+
1362
+ // Delete a specific value.
1363
+ else {
1364
+ set.delete(val);
1365
+ result = val;
1366
+ }
1367
+
1368
+ if (set.size === 0)
1369
+ delete data[key];
1133
1370
 
1134
- this.exprToTemplates(expr, callback);
1135
- }
1371
+ return result;
1372
+ }
1136
1373
 
1137
- // String/Number/Date/Boolean
1138
- else if (!(expr instanceof Template) && !(expr?.nodeType)){
1139
- // Convert expression to a string.
1140
- if (expr === undefined || expr === false || expr === null) // Util.isFalsy() inlined
1141
- expr = '';
1142
- else if (typeof expr !== 'string')
1143
- expr += '';
1374
+ /**
1375
+ * Remove any one value from a key, and return it.
1376
+ * @param key {string}
1377
+ * @returns {*|undefined} The deleted item. */
1378
+ deleteAny(key) {
1379
+ let data = this.data;
1380
+ let result;
1381
+ let set = data[key];
1382
+ if (!set) // slower than pre-check.
1383
+ return undefined;
1144
1384
 
1145
- // Get the same Template for the same string each time.
1146
- // let template = Globals.stringTemplates[expr];
1147
- // if (!template) {
1385
+ [result] = set; // Get the first value from the set.
1386
+ set.delete(result);
1148
1387
 
1149
- let template = new Template([expr], []);
1150
- template.isText = true;
1151
- // Globals.stringTemplates[expr] = template;
1152
- //}
1388
+ if (set.size === 0)
1389
+ delete data[key];
1153
1390
 
1154
- // Recurse.
1155
- this.exprToTemplates(template, callback);
1156
- }
1157
- else
1158
- callback(expr);
1391
+ return result;
1159
1392
  }
1160
1393
 
1394
+ deleteSpecific(key, val) {
1395
+ let data = this.data;
1396
+ let result;
1397
+ let set = data[key];
1398
+ if (!set)
1399
+ return undefined;
1161
1400
 
1162
- /**
1163
- * Try to apply Nodes that are an exact match, by finding existing nodes from the last render
1164
- * that have the same value as created by the expr.
1165
- * This is called from ExprPath.applyNodes().
1166
- *
1167
- * @param expr {Template|Node|Array|function|*}
1168
- * @param newNodes {(Node|Template)[]} An inout parameter; we add the nodes here as we go.
1169
- * @param secondPass {[int, int][]} Locations within newNodes for ExprPath.applyNodes() to evaluate later,
1170
- * when it tries to find partial matches. */
1171
- applyExactNodes(expr, newNodes, secondPass) {
1401
+ set.delete(val);
1402
+ result = val;
1172
1403
 
1173
- if (expr instanceof Template) {
1174
- let ng = this.getNodeGroup(expr, true);
1404
+ if (set.size === 0)
1405
+ delete data[key];
1175
1406
 
1176
- if (ng) {
1177
- let newestNodes = ng.getNodes();
1178
- newNodes.push(...newestNodes);
1407
+ return result;
1408
+ }
1179
1409
 
1180
- // New!
1181
- // Re-apply all expressions if there's a web component, so we can pass them to its constructor.
1182
- // NodeGroup.applyExprs() is used to call applyComponentExprs() on web components that have expression attributes.
1183
- // For those that don't, we call applyComponentExprs() directly here.
1184
- // Also see similar code at the end of this.applyNodes() which handles web components being instantiated the first time.
1185
- let apply = false;
1186
- for (let el of newestNodes) {
1187
- if (el?.nodeType === 1) { // HTMLElement
1188
-
1189
- if (el.tagName.includes('-')) {
1190
- if (!expr.exprs.find(expr => expr?.nodeMarker === el))
1191
- this.parentNg.handleComponent(el, null, true);
1192
- else // Commenting out this "else" causes render() to be called too often, but other UI code fails if it's present.
1193
- apply = true;
1194
- }
1195
- for (let child of el.querySelectorAll('*')) {
1196
- if (child.tagName.includes('-')) {
1197
- if (!expr.exprs.find(expr => expr?.nodeMarker === child))
1198
- this.parentNg.handleComponent(child, null, true);
1199
- else
1200
- apply = true;
1201
- }
1202
- }
1203
- }
1204
- }
1410
+ hasValue(val) {
1411
+ let data = this.data;
1412
+ let names = [];
1413
+ for (let name in data)
1414
+ if (data[name].has(val)) // TODO: iterate twice to pre-size array?
1415
+ names.push(name);
1416
+ return names;
1417
+ }
1418
+ }
1419
+
1420
+ class PathToNodes extends Path {
1205
1421
 
1206
- // This calls render() on web components that have expressions as attributes.
1207
- if (apply)
1208
- ng.applyExprs(expr.exprs);
1209
-
1210
- this.nodeGroups.push(ng);
1211
1422
 
1212
- return ng;
1213
- }
1423
+ /**
1424
+ * @type {?function} The most recent callback passed to a .map() function in this Path. This is only used for watch.js
1425
+ * TODO: What if one Path has two .map() calls? Maybe we just won't support that. */
1426
+ mapCallback;
1214
1427
 
1215
- // If expression, mark it to be evaluated later in ExprPath.apply() to find partial match.
1216
- else {
1217
- secondPass.push([newNodes.length, this.nodeGroups.length]);
1218
- newNodes.push(expr);
1219
- this.nodeGroups.push(null); // placeholder
1220
- }
1221
- }
1222
1428
 
1223
- // Node(s) created by an expression.
1224
- else if (expr?.nodeType) {
1225
1429
 
1226
- // DocumentFragment created by an expression.
1227
- if (expr?.nodeType === 11) // DocumentFragment
1228
- newNodes.push(...expr.childNodes);
1229
- else
1230
- newNodes.push(expr);
1231
- }
1430
+ /**
1431
+ * TODO: Rename this to nodeGroupsInUse, nodeGroupsAvialableAttached and nodeGroupsAvailableDetached?
1432
+ * Nodes that have been used during the current render().
1433
+ * Used with getNodeGroup() and freeNodeGroups().
1434
+ * TODO: Use an array of WeakRef so the gc can collect them?
1435
+ * TODO: Put items back in nodeGroupsInUse after applyExpr() is called, not before.
1436
+ * @type {NodeGroup[]} */
1437
+ nodeGroupsRendered = [];
1232
1438
 
1233
- // Arrays and functions.
1234
- // I tried iterating over the result of a generator function to avoid this recursion and simplify the code,
1235
- // but that consistently made the js-framework-benchmarks a few percentage points slower.
1236
- else
1237
- this.exprToTemplates(expr, template => {
1238
- this.applyExactNodes(template, newNodes, secondPass);
1239
- });
1439
+ /**
1440
+ * Nodes that were added to the web component during the last render(), but are available to be used again.
1441
+ * Used with getNodeGroup() and freeNodeGroups().
1442
+ * Each NodeGroup is here twice, once under an exact key, and once under the close key.
1443
+ * @type {MultiValueMap<key:string, value:NodeGroup>} */
1444
+ nodeGroupsAttachedAvailable = new MultiValueMap();
1445
+
1446
+ /**
1447
+ * Nodes that were not added to the web component during the last render(), and available to be used again.
1448
+ * @type {MultiValueMap} */
1449
+ nodeGroupsDetachedAvailable = new MultiValueMap();
1450
+
1451
+ constructor(nodeBefore, nodeMarker) {
1452
+ super(nodeBefore, nodeMarker);
1240
1453
  }
1241
1454
 
1242
- applyMultipleAttribs(node, expr) {
1455
+ /**
1456
+ * Insert/replace the nodes created by a single expression.
1457
+ * Called by applyExprs()
1458
+ * This function is recursive. It calls functions that call applyNodes().
1459
+ * @param exprs {Expr[]} Only the first is used.
1460
+ * @param freeNodeGroups {boolean}
1461
+ * @return {Node[]} New Nodes created. */
1462
+ apply(exprs, freeNodeGroups=true) {
1243
1463
 
1244
1464
 
1245
- if (Array.isArray(expr))
1246
- expr = expr.flat().join(' '); // flat and join so we can accept arrays of arrays of strings.
1465
+ let path = this;
1466
+ let expr = exprs[0];
1247
1467
 
1248
- // Add new attributes
1249
- let oldNames = this.attrNames;
1250
- this.attrNames = new Set();
1251
- if (expr) {
1252
- if (typeof expr === 'function') {
1253
- Globals$1.currentExprPath = this; // Used by watch()
1254
- this.watchFunction = expr; // used by renderWatched()
1255
- expr = expr();
1256
- Globals$1.currentExprPath = null;
1257
- }
1468
+ // This can be done at the beginning or the end of this function.
1469
+ // If at the end, we may get rendering done faster.
1470
+ // But when at the beginning, it leaves all the nodes in-use so we can do a renderWatched().
1471
+ if (freeNodeGroups)
1472
+ path.freeNodeGroups();
1258
1473
 
1259
- // Attribute as name: value object.
1260
- if (typeof expr === 'object') {
1261
- for (let name in expr) {
1262
- let value = expr[name];
1263
- if (value === undefined || value === false || value === null)
1264
- continue;
1265
- node.setAttribute(name, value);
1266
- this.attrNames.add(name);
1267
- }
1268
- }
1474
+
1269
1475
 
1270
- // Attributes as string
1271
- else {
1272
- let attrs = (expr + '') // Split string into multiple attributes.
1273
- .split(/([\w-]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s]+))/g)
1274
- .map(text => text.trim())
1275
- .filter(text => text.length);
1476
+ /** @type {(Node|NodeGroup|Expr)[]} */
1477
+ let newNodes = [];
1478
+ let oldNodeGroups = path.nodeGroups;
1479
+
1480
+ let secondPass = []; // indices
1276
1481
 
1277
- for (let attr of attrs) {
1278
- let [name, value] = attr.split(/\s*=\s*/); // split on first equals.
1279
- value = (value || '').replace(/^(['"])(.*)\1$/, '$2'); // trim value quotes if they match.
1280
- node.setAttribute(name, value);
1281
- this.attrNames.add(name);
1282
- }
1283
- }
1284
- }
1482
+ path.nodeGroups = []; // Reset before applyExactNodes and the code below rebuilds it.
1483
+ path.applyExactNodes(expr, newNodes, secondPass);
1285
1484
 
1286
- // Remove old attributes.
1287
- for (let oldName of oldNames)
1288
- if (!this.attrNames.has(oldName))
1289
- node.removeAttribute(oldName);
1290
- }
1485
+ //this.existingTextNodes = null;
1291
1486
 
1292
- /**
1293
- * Handle attributes for event binding, such as:
1294
- * onclick=${(e, el) => this.doSomething(el, 'meow')}
1295
- * oninput=${[this.doSomething, 'meow']}
1296
- * onclick=${[this, 'doSomething', 'meow']}
1297
- *
1298
- * @param node
1299
- * @param expr
1300
- * @param root */
1301
- applyEventAttrib(node, expr, root) {
1302
-
1487
+ // TODO: Create an array of old vs Nodes and NodeGroups together.
1488
+ // If they're all the same, skip the next steps.
1489
+ // Or calculate it in the loop above as we go? Have a path.lastNodeGroups property?
1303
1490
 
1304
- let eventName = this.attrName.slice(2); // remove "on-" prefix.
1305
- let func;
1306
- let args = [];
1491
+ // Second pass to find close-match NodeGroups.
1492
+ let flatten = false;
1493
+ if (secondPass.length) {
1494
+ for (let [nodesIndex, ngIndex] of secondPass) {
1495
+ let ng = path.getNodeGroup(newNodes[nodesIndex], false);
1496
+ let ngNodes = ng.getNodes();
1307
1497
 
1308
- // Convert array to function.
1309
- // oninput=${[this.doSomething, 'meow']}
1310
- if (Array.isArray(expr) && typeof expr[0] === 'function') {
1311
- func = expr[0];
1312
- args = expr.slice(1);
1313
- }
1314
- else if (typeof expr === 'function')
1315
- func = expr;
1316
- else
1317
- throw new Error(`Invalid event binding: <${node.tagName.toLowerCase()} ${this.attrName}=\${${JSON.stringify(expr)}}>`);
1498
+
1318
1499
 
1319
- this.bindEvent(node, root, eventName, eventName, func, args);
1320
- }
1500
+ if (ngNodes.length === 1) // flatten manually so we can skip flattening below.
1501
+ newNodes[nodesIndex] = ngNodes[0];
1321
1502
 
1503
+ else {
1504
+ newNodes[nodesIndex] = ngNodes;
1505
+ flatten = true;
1506
+ }
1507
+ path.nodeGroups[ngIndex] = ng;
1508
+ }
1322
1509
 
1323
- /**
1324
- * Call function when eventName is triggerd on node.
1325
- * @param node {HTMLElement}
1326
- * @param root {HTMLElement}
1327
- * @param key {string}
1328
- * @param eventName {string}
1329
- * @param func {function}
1330
- * @param args {array}
1331
- * @param capture {boolean} */
1332
- bindEvent(node, root, key, eventName, func, args, capture=false) {
1333
- let nodeEvents = Globals$1.nodeEvents.get(node);
1334
- if (!nodeEvents) {
1335
- nodeEvents = {[key]: new Array(3)};
1336
- Globals$1.nodeEvents.set(node, nodeEvents);
1510
+ if (flatten)
1511
+ newNodes = newNodes.flat(); // Only if second pass happens.
1337
1512
  }
1338
- let nodeEvent = nodeEvents[key];
1339
- if (!nodeEvent)
1340
- nodeEvents[key] = nodeEvent = new Array(3);
1341
1513
 
1342
- if (typeof func !== 'function')
1343
- throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${${func}}> because it's not a function.`);
1514
+
1344
1515
 
1345
- // If function has changed, remove and rebind the event.
1346
- if (nodeEvent[0] !== func) {
1516
+ let oldNodes = path.getNodes();
1347
1517
 
1348
- // TODO: We should be removing event listeners when calling getNodeGroup(),
1349
- // when we get the node from the list of nodeGroupsAttached/nodeGroupsDetached,
1350
- // instead of only when we rebind an event.
1351
- let [existing, existingBound, _] = nodeEvent;
1352
- if (existing)
1353
- node.removeEventListener(eventName, existingBound, capture);
1518
+ // This pre-check makes it a few percent faster?
1519
+ let same = Util.arraySame(oldNodes, newNodes);
1520
+ if (!same) {
1354
1521
 
1355
- let originalFunc = func;
1522
+ path.nodesCache = newNodes; // Replaces value set by path.getNodes()
1356
1523
 
1357
- // BoundFunc sets the "this" variable to be the current Solarite component.
1358
- let boundFunc = (event) => {
1359
- let args = nodeEvent[2];
1360
- return originalFunc.call(root, ...args, event, node);
1361
- };
1524
+ if (this.parentNg.parentPath)
1525
+ this.parentNg.parentPath.clearNodesCache();
1362
1526
 
1363
- // Save both the original and bound functions.
1364
- // Original so we can compare it against a newly assigned function.
1365
- // Bound so we can use it with removeEventListner().
1366
- nodeEvent[0] = originalFunc;
1367
- nodeEvent[1] = boundFunc;
1527
+ // Fast clear method
1528
+ let isNowEmpty = oldNodes.length && !newNodes.length;
1529
+ if (!isNowEmpty || !path.fastClear()) {
1368
1530
 
1369
- node.addEventListener(eventName, boundFunc, capture);
1531
+ // Rearrange nodes.
1532
+ udomdiff(path.nodeMarker.parentNode, oldNodes, newNodes, path.nodeMarker);
1533
+ }
1370
1534
 
1371
- // TODO: classic event attribs?
1372
- //el[attr.name] = e => // e.g. el.onclick = ... // put "event", "el", and "this" in scope for the event code.
1373
- // (new Function('event', 'el', attr.value)).bind(this.manager.rootEl)(e, el)
1535
+ // TODO: Put this in a remove() function of NodeGroup.
1536
+ // Then only run it on the old nodeGroups that were actually removed.
1537
+ //Util.saveOrphans(oldNodeGroups, oldNodes);
1538
+
1539
+ for (let ng of oldNodeGroups)
1540
+ if (!ng.startNode.parentNode)
1541
+ Util.saveOrphans(ng.getNodes());
1374
1542
  }
1375
1543
 
1376
- // Otherwise just update the args to the function.
1377
- nodeEvents[key][2] = args;
1544
+
1378
1545
  }
1379
1546
 
1547
+
1548
+
1549
+
1380
1550
  /**
1381
- * Set the value of an attribute. This can be for any attribute, not just attributes named "value".
1382
- * @param node
1383
- * @param exprs */
1384
- // TODO: node is always this.nodeMarker?
1385
- applyValueAttrib(node, exprs) {
1386
- let expr = exprs[0];
1551
+ * Try to apply Nodes that are an exact match, by finding existing nodes from the last render
1552
+ * that have the same value as created by the expr.
1553
+ * This is called from Path.applyNodes().
1554
+ *
1555
+ * @param expr {Template|Node|Array|function|*}
1556
+ * @param newNodes {(Node|Template)[]} An inout parameter; we add the nodes here as we go.
1557
+ * @param secondPass {[int, int][]} Locations within newNodes for Path.applyNodes() to evaluate later,
1558
+ * when it tries to find partial matches. */
1559
+ applyExactNodes(expr, newNodes, secondPass) {
1387
1560
 
1388
- // Two-way binding between attributes
1389
- // Passing a path to the value attribute.
1390
- // Copies the attribute to the property when the input event fires.
1391
- // value=${[this, 'value]'}
1392
- // checked=${[this, 'isAgree']}
1393
- // This same logic is in NodeGroup.instantiateComponent() for components.
1394
- if (Util.isPath(expr)) {
1395
- let [obj, path] = [expr[0], expr.slice(1)];
1561
+ if (expr instanceof Template) {
1562
+ let ng = this.getNodeGroup(expr, true);
1396
1563
 
1397
- if (!obj)
1398
- throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${[${expr.map(item => item ? `'${item}'` : item+'').join(', ')}]}>.`);
1564
+ if (ng) {
1565
+ let newestNodes = ng.getNodes();
1566
+ newNodes.push(...newestNodes);
1399
1567
 
1400
- let value = delve(obj, path);
1568
+ // New!
1569
+ // Call render() on web components even though none of their arguments have changed:
1570
+ // Do we want it to work this way? Yes, because even if this component hasn't changed,
1571
+ // perhaps something in a sub-component has.
1572
+ ng.applyExprs(expr.exprs, false, false);
1401
1573
 
1402
- // Special case to allow setting select-multiple value from an array
1403
- if (this.attrName === 'value' && node.type === 'select-multiple' && Array.isArray(value)) {
1404
- // Set the .selected property on the options having a value within value.
1405
- let strValues = value.map(v => v + '');
1406
- for (let option of node.options)
1407
- option.selected = strValues.includes(option.value);
1574
+ this.nodeGroups.push(ng);
1575
+ return ng;
1408
1576
  }
1409
- else {
1410
- // TODO: should we remove isFalsy, since these are always props?
1411
- const strValue = Util.isFalsy(value) ? '' : value;
1412
-
1413
- // Special case for contenteditable
1414
- if (this.attrName === 'value' && node.hasAttribute('contenteditable')) {
1415
- const existingValue = node.innerHTML;
1416
- if (strValue !== existingValue)
1417
- node.innerHTML = strValue;
1418
- }
1419
- else {
1420
1577
 
1421
- // If we don't have this condition, when we call render(), the browser will scroll to the currently
1422
- // selected item in a <select> and mess up manually scrolling to a different value.
1423
- if (strValue !== node[this.attrName])
1424
- node[this.attrName] = strValue;
1425
- }
1578
+ // If expression, mark it to be evaluated later in Path.apply() to find partial match.
1579
+ else {
1580
+ secondPass.push([newNodes.length, this.nodeGroups.length]);
1581
+ newNodes.push(expr);
1582
+ this.nodeGroups.push(null); // placeholder
1426
1583
  }
1584
+ }
1585
+ else if (expr instanceof NodeList) {
1586
+ newNodes.push(...expr);
1587
+ }
1427
1588
 
1428
- // TODO: We need to remove any old listeners, like in bindEventAttribute.
1429
- // Does bindEvent() now handle that?
1430
- let func = () => {
1431
- let value = (this.attrName === 'value')
1432
- ? Util.getInputValue(node)
1433
- : node[this.attrName];
1434
- delve(obj, path, value);
1435
- };
1589
+ // Node(s) created by an expression.
1590
+ else if (expr?.nodeType) {
1436
1591
 
1437
- // We use capture so we update the values before other events added by the user.
1438
- // TODO: Bind to scroll events also?
1439
- // What about resize events and width/height?
1440
- this.bindEvent(node, path[0], this.attrName, 'input', func, [], true);
1592
+ // DocumentFragment created by an expression.
1593
+ if (expr?.nodeType === 11) // DocumentFragment
1594
+ newNodes.push(...expr.childNodes);
1595
+ else
1596
+ newNodes.push(expr);
1441
1597
  }
1442
1598
 
1443
- // Regular attribute
1444
- else {
1445
- // Cache this on ExprPath.isHtmlProperty when Shell creates the props.
1446
- // Have ExprPath.clone() copy .isHtmlProperty?
1447
- let isProp = this.isHtmlProperty;
1448
- if (isProp === undefined)
1449
- isProp = this.isHtmlProperty = Util.isHtmlProp(node, this.attrName);
1450
-
1451
- // Values to toggle an attribute
1452
- let multiple = this.attrValue;
1453
- if (!multiple) {
1454
- Globals$1.currentExprPath = this; // Used by watch()
1455
- if (typeof expr === 'function') {
1456
- if (this.isComponent) { // Don't evaluate functions before passing them to components
1457
- return
1458
- }
1459
- this.watchFunction = expr; // The function that gets the expression, used for renderWatched()
1460
- expr = expr();
1461
- }
1462
- else
1463
- expr = Util.makePrimitive(expr);
1464
- Globals$1.currentExprPath = null;
1465
- }
1466
- if (!multiple && (expr === undefined || expr === false || expr === null)) { // Util.isFalsy() inlined.
1467
- if (isProp)
1468
- node[this.attrName] = false;
1469
- node.removeAttribute(this.attrName);
1470
- }
1471
- else if (!multiple && expr === true) {
1472
- if (isProp)
1473
- node[this.attrName] = true;
1474
- node.setAttribute(this.attrName, '');
1475
- }
1599
+ // Arrays and functions.
1600
+ // I tried iterating over the result of a generator function to avoid this recursion and simplify the code,
1601
+ // but that consistently made the js-framework-benchmarks a few percentage points slower.
1602
+ else
1603
+ this.exprToTemplates(expr, template => {
1604
+ this.applyExactNodes(template, newNodes, secondPass);
1605
+ });
1606
+ }
1476
1607
 
1477
- // A non-toggled attribute
1478
- else {
1608
+ /**
1609
+ * Used by watch() for inserting/removing/replacing individual loop items.
1610
+ * @param op {ArraySpliceOp} */
1611
+ applyWatchArrayOp(op) {
1479
1612
 
1480
- // If it's a series of expressions among strings, join them together.
1481
- let joinedValue;
1482
- if (multiple) {
1483
- let value = [];
1484
- for (let i = 0; i < this.attrValue.length; i++) {
1485
- value.push(this.attrValue[i]);
1486
- if (i < this.attrValue.length - 1) {
1487
- Globals$1.currentExprPath = this; // Used by watch()
1488
- let val = Util.makePrimitive(exprs[i]);
1489
- Globals$1.currentExprPath = null;
1490
- if (!Util.isFalsy(val))
1491
- value.push(val);
1492
- }
1493
- }
1494
- joinedValue = value.join('');
1495
- }
1613
+ // Replace NodeGroups
1614
+ let replaceCount = Math.min(op.deleteCount, op.items.length);
1615
+ let deleteCount = op.deleteCount - replaceCount;
1616
+ for (let i=0; i<replaceCount; i++) {
1617
+ let oldNg = this.nodeGroups[op.index + i]; // TODO: One expr can create multiple nodegroups.
1496
1618
 
1497
- // If the attribute is one expression with no strings:
1498
- else
1499
- joinedValue = expr;
1619
+ // Try to find an exact match
1620
+ let func = this.mapCallback || this.watchFunction;
1621
+ let expr = func(op.items[i]);
1500
1622
 
1501
- // Only update attributes if the value has changed.
1502
- // This is needed for setting input.value, .checked, option.selected, etc.
1623
+ // If the result of func isn't a template, conver it to one or more templates.
1624
+ this.exprToTemplates(expr, template => { // TODO: An expr can create multiple NodeGroups. I need a way to group them.
1503
1625
 
1504
- let oldVal = isProp
1505
- ? node[this.attrName]
1506
- : node.getAttribute(this.attrName);
1507
- if (oldVal !== joinedValue) {
1626
+ let ng = this.getNodeGroup(template, true); // Removes from nodeGroupsAttached and adds to nodeGroupsRendered()
1627
+ if (ng && ng === oldNg) ; else {
1508
1628
 
1509
- // <textarea value=${expr}></textarea>
1510
- // Without this branch we have no way to set the value of a textarea,
1511
- // since we also prohibit expressions that are a child of textarea.
1512
- if (isProp)
1513
- node[this.attrName] = joinedValue;
1629
+ // Find a close match or create a new node group
1630
+ if (!ng)
1631
+ ng = this.getNodeGroup(template, false); // adds back to nodeGroupsRendered()
1632
+ this.nodeGroups[op.index + i] = ng; // TODO: Remove old one to nodeGroupsDetached?
1514
1633
 
1515
- // Allow one-way binding to contenteditable value attribute.
1516
- // Contenteditables normally don't have a value attribute and have their content set via innerHTML.
1517
- // Solarite doesn't allow contenteditables to have expressions as their children.
1518
- else if (node.hasAttribute('contenteditable'))
1519
- node.innerHTML = joinedValue;
1634
+ // Splice in the new nodes.
1635
+ let insertBefore = oldNg.startNode;
1636
+ for (let node of ng.getNodes())
1637
+ insertBefore.parentNode.insertBefore(node, insertBefore);
1520
1638
 
1521
- // TODO: Putting an 'else' here would be more performant
1522
- node.setAttribute(this.attrName, joinedValue);
1639
+ // Remove the old nodes.
1640
+ if (ng !== oldNg)
1641
+ Util.saveOrphans(oldNg.getNodes());
1523
1642
  }
1643
+ });
1644
+ }
1645
+
1646
+ // Delete extra at the end.
1647
+ if (deleteCount > 0) {
1648
+ for (let i=0; i<deleteCount; i++) {
1649
+ let oldNg = this.nodeGroups[op.index + replaceCount + i];
1650
+ Util.saveOrphans(oldNg.getNodes());
1524
1651
  }
1652
+ this.nodeGroups.splice(op.index + replaceCount, deleteCount);
1525
1653
  }
1526
- }
1527
1654
 
1655
+ // Add extra at the end.
1656
+ else {
1657
+ let newItems = op.items.slice(replaceCount);
1528
1658
 
1529
- /**
1530
- *
1531
- * @param newRoot {HTMLElement}
1532
- * @param pathOffset {int}
1533
- * @return {ExprPath} */
1534
- clone(newRoot, pathOffset=0) {
1535
-
1659
+ let insertBefore = this.nodeGroups[op.index + replaceCount]?.startNode || this.nodeMarker;
1660
+ for (let i = 0; i < newItems.length; i++) { // We use nodeMarker if the subequent (or all) nodeGroups have been removed.
1536
1661
 
1537
- // Resolve node paths.
1538
- let nodeMarker, nodeBefore;
1539
- let root = newRoot;
1540
- let path = pathOffset ? this.nodeMarkerPath.slice(0, -pathOffset) : this.nodeMarkerPath;
1541
- let length = path.length-1;
1542
- for (let i=length; i>0; i--) // Resolve the path.
1543
- root = root.childNodes[path[i]];
1544
- let childNodes = root.childNodes;
1545
1662
 
1546
- nodeMarker = path.length ? childNodes[path[0]] : newRoot;
1547
- if (this.nodeBefore)
1548
- nodeBefore = childNodes[this.nodeBeforeIndex];
1663
+ // Try to find exact match
1664
+ let template = this.mapCallback(newItems[i]);
1665
+ let ng = this.getNodeGroup(template, true); // Removes from nodeGroupsAttached and adds to nodeGroupsRendered()
1666
+ if (!ng) // Find a close match or create a new node group
1667
+ ng = this.getNodeGroup(template, false); // adds back to nodeGroupsRendered()
1668
+
1669
+ this.nodeGroups.push(ng);
1549
1670
 
1550
- let result = new ExprPath(nodeBefore, nodeMarker, this.type, this.attrName, this.attrValue);
1551
- result.isComponent = this.isComponent;
1671
+ // Splice in the new nodes.
1672
+ for (let node of ng.getNodes())
1673
+ insertBefore.parentNode.insertBefore(node, insertBefore);
1674
+ }
1675
+ }
1552
1676
 
1553
1677
 
1554
1678
 
1555
- return result;
1679
+ // TODO: update or invalidate the nodes cache?
1680
+ this.nodesCache = null;
1556
1681
  }
1557
1682
 
1558
1683
  /**
1559
- * Clear the nodeCache of this ExprPath, as well as all parent and child ExprPaths that
1684
+ * Clear the nodeCache of this Path, as well as all parent and child Paths that
1560
1685
  * share the same DOM parent node. */
1561
1686
  clearNodesCache() {
1562
1687
  let path = this;
1563
1688
 
1564
- // Clear cache parent ExprPaths that have the same parentNode
1689
+ // Clear cache parent Paths that have the same parentNode
1565
1690
  let parentNode = this.nodeMarker.parentNode;
1566
1691
  while (path && path.nodeMarker.parentNode === parentNode) {
1567
1692
  path.nodesCache = null;
@@ -1572,9 +1697,8 @@ class ExprPath {
1572
1697
  }
1573
1698
  }
1574
1699
 
1575
-
1576
1700
  /**
1577
- * Attempt to remove all of this ExprPath's nodes from the DOM, if it can be done using a special fast method.
1701
+ * Attempt to remove all of this Path's nodes from the DOM, if it can be done using a special fast method.
1578
1702
  * @returns {boolean} Returns false if Nodes weren't removed, and they should instead be removed manually. */
1579
1703
  fastClear() {
1580
1704
  let parent = this.nodeBefore.parentNode;
@@ -1593,8 +1717,8 @@ class ExprPath {
1593
1717
  // parent.replaceWith(replacement)
1594
1718
  // }
1595
1719
  // else {
1596
- parent.innerHTML = ''; // Faster than calling .removeChild() a thousand times.
1597
- parent.append(this.nodeBefore, this.nodeMarker);
1720
+ parent.innerHTML = ''; // Faster than calling .removeChild() a thousand times.
1721
+ parent.append(this.nodeBefore, this.nodeMarker);
1598
1722
  //}
1599
1723
  return true;
1600
1724
  }
@@ -1602,46 +1726,54 @@ class ExprPath {
1602
1726
  }
1603
1727
 
1604
1728
  /**
1605
- * @return {(Node|HTMLElement)[]} */
1606
- getNodes() {
1607
-
1608
- // Why doesn't this work?
1609
- // let result2 = [];
1610
- // for (let ng of this.nodeGroups)
1611
- // result2.push(...ng.getNodes())
1612
- // return result2;
1613
-
1614
- if (this.type === ExprPathType.AttribValue || this.type === ExprPathType.AttribMultiple) {
1615
- return [this.nodeMarker];
1616
- }
1617
-
1618
-
1619
- let result;
1729
+ * Recursively traverse expr.
1730
+ * If a value is a function, evaluate it.
1731
+ * If a value is an array, recurse on each item.
1732
+ * If it's a primitive, convert it to a Template.
1733
+ * Otherwise pass the item (which is now either a Template or a Node) to callback.
1734
+ * TODO: This could be static if not for the watch code, which doesn't work anyway.
1735
+ * @param expr
1736
+ * @param callback {function(Node|Template)}*/
1737
+ exprToTemplates(expr, callback) {
1738
+ if (Array.isArray(expr)) // TODO: use typeof obj[Symbol.iterator] === 'function' so we can also iterate over objects and NodeList?
1739
+ for (let subExpr of expr)
1740
+ this.exprToTemplates(subExpr, callback);
1620
1741
 
1621
- // This shaves about 5ms off the partialUpdate benchmark.
1622
- result = this.nodesCache;
1623
- if (result) {
1742
+ else if (typeof expr === 'function') {
1743
+ // TODO: One Path can have multiple expr functions.
1744
+ // But if using it as a watch, it should only have one at the top level.
1745
+ // So maybe this is ok.
1746
+ Globals$1.currentPath = this; // Used by watch()
1624
1747
 
1625
-
1748
+ this.watchFunction = expr; // TODO: Only do this if it's a top level function.
1749
+ expr = expr(); // As expr accesses watched variables, watch() uses Globals.currentPath to mark where those watched variables are being used.
1750
+ Globals$1.currentPath = null;
1626
1751
 
1627
- return result
1752
+ this.exprToTemplates(expr, callback);
1628
1753
  }
1629
1754
 
1630
- result = [];
1631
- let current = this.nodeBefore.nextSibling;
1632
- let nodeMarker = this.nodeMarker;
1633
- while (current && current !== nodeMarker) {
1634
- result.push(current);
1635
- current = current.nextSibling;
1636
- }
1755
+ // String/Number/Date/Boolean
1756
+ else if (!(expr instanceof Template) && !(expr?.nodeType)){
1757
+ // Convert expression to a string.
1758
+ if (expr === undefined || expr === false || expr === null) // Util.isFalsy() inlined
1759
+ expr = '';
1760
+ else if (typeof expr !== 'string')
1761
+ expr += '';
1637
1762
 
1638
- this.nodesCache = result;
1639
- return result;
1640
- }
1763
+ // Get the same Template for the same string each time.
1764
+ // let template = Globals.stringTemplates[expr];
1765
+ // if (!template) {
1766
+
1767
+ let template = new Template([expr], []);
1768
+ template.isText = true;
1769
+ // Globals.stringTemplates[expr] = template;
1770
+ //}
1641
1771
 
1642
- /** @return {HTMLElement|ParentNode} */
1643
- getParentNode() {
1644
- return this.nodeMarker.parentNode
1772
+ // Recurse.
1773
+ this.exprToTemplates(template, callback);
1774
+ }
1775
+ else
1776
+ callback(expr);
1645
1777
  }
1646
1778
 
1647
1779
  /**
@@ -1657,7 +1789,6 @@ class ExprPath {
1657
1789
  * or createa new NodeGroup from the template.
1658
1790
  * @return {NodeGroup} */
1659
1791
  getNodeGroup(template, exact=true) {
1660
-
1661
1792
  let result;
1662
1793
  let collection = this.nodeGroupsAttachedAvailable;
1663
1794
 
@@ -1695,41 +1826,23 @@ class ExprPath {
1695
1826
 
1696
1827
  // Update this close match with the new expression values.
1697
1828
  result.applyExprs(template.exprs);
1698
- result.exactKey = template.getExactKey(); // TODO: Should this be set elsewhere?
1829
+ result.exactKey = template.getExactKey();
1699
1830
  }
1700
1831
  }
1701
1832
 
1702
- if (!result)
1833
+ if (!result) {
1703
1834
  result = new NodeGroup(template, this);
1835
+ result.applyExprs(template.exprs);
1836
+ result.exactKey = template.getExactKey();
1837
+ }
1838
+
1704
1839
 
1705
- // old:
1706
1840
  this.nodeGroupsRendered.push(result);
1707
1841
 
1708
1842
 
1709
1843
  return result;
1710
1844
  }
1711
1845
 
1712
- /**
1713
- * TODO: Rename this to nodeGroupsInUse, nodeGroupsAvialableAttached and nodeGroupsAvailableDetached?
1714
- * Nodes that have been used during the current render().
1715
- * Used with getNodeGroup() and freeNodeGroups().
1716
- * TODO: Use an array of WeakRef so the gc can collect them?
1717
- * TODO: Put items back in nodeGroupsInUse after applyExpr() is called, not before.
1718
- * @type {NodeGroup[]} */
1719
- nodeGroupsRendered = [];
1720
-
1721
- /**
1722
- * Nodes that were added to the web component during the last render(), but are available to be used again.
1723
- * Used with getNodeGroup() and freeNodeGroups().
1724
- * Each NodeGroup is here twice, once under an exact key, and once under the close key.
1725
- * @type {MultiValueMap<key:string, value:NodeGroup>} */
1726
- nodeGroupsAttachedAvailable = new MultiValueMap();
1727
-
1728
- /**
1729
- * Nodes that were not added to the web component during the last render(), and available to be used again.
1730
- * @type {MultiValueMap} */
1731
- nodeGroupsDetachedAvailable = new MultiValueMap();
1732
-
1733
1846
 
1734
1847
  /**
1735
1848
  * Move everything from this.nodeGroupsRendered to this.nodeGroupsAttached and nodeGroupsDetached.
@@ -1759,142 +1872,174 @@ class ExprPath {
1759
1872
  this.nodeGroupsRendered = [];
1760
1873
  }
1761
1874
 
1762
-
1763
- }
1764
1875
 
1765
- /** @enum {int} */
1766
- const ExprPathType = {
1767
- /** Child of a node */
1768
- Content: 1, // TODO: Rename to Nodes
1769
1876
 
1770
- /** One or more whole attributes */
1771
- AttribMultiple: 2,
1877
+ /**
1878
+ * If not for watch.js, this could be moved to PathToNodes.js
1879
+ * @return {(Node|HTMLElement)[]} */
1880
+ getNodes() {
1772
1881
 
1773
- /** Value of an attribute. */
1774
- AttribValue: 3,
1882
+ // Why doesn't this work?
1883
+ // let result2 = [];
1884
+ // for (let ng of this.nodeGroups)
1885
+ // result2.push(...ng.getNodes())
1886
+ // return result2;
1775
1887
 
1776
- /** Expressions inside Html comments. */
1777
- Comment: 4,
1888
+ let result;
1778
1889
 
1779
- /** Value of an attribute. */
1780
- Event: 5,
1781
- };
1890
+ // This shaves about 5ms off the partialUpdate benchmark.
1891
+ result = this.nodesCache;
1892
+ if (result) {
1893
+
1894
+ return result
1895
+ }
1782
1896
 
1897
+ result = [];
1898
+ let current = this.nodeBefore.nextSibling;
1899
+ let nodeMarker = this.nodeMarker;
1900
+ while (current && current !== nodeMarker) {
1901
+ result.push(current);
1902
+ current = current.nextSibling;
1903
+ }
1783
1904
 
1784
- /** @return {int[]} Returns indices in reverse order, because doing it that way is faster. */
1785
- function getNodePath(node) {
1786
- let result = [];
1787
- while(true) {
1788
- let parent = node.parentNode;
1789
- if (!parent)
1790
- break;
1791
- result.push(Array.prototype.indexOf.call(node.parentNode.childNodes, node));
1792
- node = parent;
1905
+ this.nodesCache = result;
1906
+ return result;
1793
1907
  }
1794
- return result;
1795
- }
1796
1908
 
1797
- /**
1798
- * Note that the path is backward, with the outermost element at the end.
1799
- * @param root {HTMLElement|Document|DocumentFragment|ParentNode}
1800
- * @param path {int[]}
1801
- * @returns {Node|HTMLElement|HTMLStyleElement} */
1802
- function resolveNodePath(root, path) {
1803
- for (let i=path.length-1; i>=0; i--)
1804
- root = root.childNodes[path[i]];
1805
- return root;
1909
+
1806
1910
  }
1807
1911
 
1808
- class HtmlParser {
1809
- constructor() {
1810
- this.defaultState = {
1811
- context: HtmlParser.Text, // possible values: 'TEXT', 'TAG', 'ATTRIBUTE'
1812
- quote: null, // possible values: null, '"', "'"
1813
- buffer: '',
1814
- lastChar: null
1815
- };
1816
- this.state = {...this.defaultState};
1817
- }
1912
+ class PathToComponent extends Path {
1818
1913
 
1819
- reset() {
1820
- this.state = {...this.defaultState};
1821
- return this.state.context;
1914
+ /** @type {PathToAttribValue[]} Paths to dynamics attributes that will be set on the component.*/
1915
+ attribPaths;
1916
+
1917
+ constructor(nodeBefore, nodeMarker) {
1918
+ super(null, nodeMarker);
1822
1919
  }
1823
1920
 
1824
1921
  /**
1825
- * Parse the next chunk of html, starting with the same context we left off with from the previous chunk.
1826
- * @param html {string}
1827
- * @param onContextChange {?function(html:string, index:int, oldContext:string, newContext:string)}
1828
- * Called every time the context changes, and again at the last context.
1829
- * @return {('Attribute','Text','Tag')} The context at the end of html. */
1830
- parse(html, onContextChange=null) {
1831
- if (html === null)
1832
- return this.reset();
1922
+ * Call render() on the component pointed to by this Path.
1923
+ * And instantiate it (from a -solarite-placeholder element) if it hasn't been done yet.
1924
+ * @param exprs {Expr[][]} Expressions to evaluate for each attribute to pass to the constructor.
1925
+ * This is different than other Path.apply() functions which only receive Expr[] and not Expr[][].
1926
+ * Because here we're receiving an array of arrays of expressions, one for each dynamic attribute.
1927
+ * @param freeNodeGroups {boolean} Used only by watch.js.
1928
+ * @param changed {boolean} True if the exprs have changed since the last time render() was called.*/
1929
+ apply(exprs, freeNodeGroups=true, changed=true) {
1930
+
1833
1931
 
1834
- for (let i = 0; i < html.length; i++) {
1835
- const char = html[i];
1836
- switch (this.state.context) {
1837
- case HtmlParser.Text:
1838
- if (char === '<' && html[i + 1].match(/[/a-z!]/i)) { // Start of a tag or comment.
1839
- onContextChange?.(html, i, this.state.context, HtmlParser.Tag);
1840
- this.state.context = HtmlParser.Tag;
1841
- this.state.buffer = '';
1842
- }
1843
- break;
1844
- case HtmlParser.Tag:
1845
- if (char === '>') {
1846
- onContextChange?.(html, i+1, this.state.context, HtmlParser.Text);
1847
- this.state.context = HtmlParser.Text;
1848
- this.state.quote = null;
1849
- this.state.buffer = '';
1850
- }
1851
- else if (char === ' ' && !this.state.buffer) {
1852
- // No attribute name is present. Skipping the space.
1853
- continue;
1854
- }
1855
- else if (char === ' ' || char === '/' || char === '?') {
1856
- this.state.buffer = ''; // Reset the buffer when a delimiter or potential self-closing sign is found.
1857
- }
1858
- else if (char === '"' || char === "'" || char === '=') {
1859
- onContextChange?.(html, i, this.state.context, HtmlParser.Attribute);
1860
- this.state.context = HtmlParser.Attribute;
1861
- this.state.quote = char === '=' ? null : char;
1862
- this.state.buffer = '';
1863
- }
1864
- else
1865
- this.state.buffer += char;
1866
- break;
1867
- case HtmlParser.Attribute:
1868
- // Start an attribute quote.
1869
- if (!this.state.quote && !this.state.buffer.length && (char === '"' || char === "'")) {
1870
- this.state.quote = char;
1871
- }
1872
- else if (char === this.state.quote || (!this.state.quote && this.state.buffer.length)) {
1873
- onContextChange?.(html, i, this.state.context, HtmlParser.Tag);
1874
- this.state.context = HtmlParser.Tag;
1875
- this.state.quote = null;
1876
- this.state.buffer = '';
1877
- }
1878
- else if (!this.state.quote && char === '>') {
1879
- onContextChange?.(html, i+1, this.state.context, HtmlParser.Text);
1880
- this.state.context = HtmlParser.Text;
1881
- this.state.quote = null;
1882
- this.state.buffer = '';
1883
- }
1884
- else if (char !== ' ')
1885
- this.state.buffer += char;
1932
+
1886
1933
 
1887
- break;
1934
+ let el = this.nodeMarker;
1935
+
1936
+ // 1. Attributes
1937
+ let attribs = Util.attribsToObject(el, '_is');
1938
+ for (let i=0, attribPath; attribPath = this.attribPaths[i]; i++) {
1939
+ let name = Util.dashesToCamel(attribPath.attrName);
1940
+ attribs[name] = attribPath.getValue(exprs[i]);
1941
+ }
1942
+
1943
+ // 2. Instantiate component on first time.
1944
+ let isAttrib = el.getAttribute('_is');
1945
+ if (el.tagName.endsWith('-SOLARITE-PLACEHOLDER') || isAttrib) {
1946
+
1947
+
1948
+ // 2a. Instantiate component
1949
+ let tagName = (isAttrib || el.tagName.slice(0, -21)).toLowerCase(); // Remove -SOLARITE-PLACEHOLDER
1950
+ let Constructor = customElements.get(tagName);
1951
+ if (!Constructor)
1952
+ throw new Error(`Must call customElements.define('${tagName}', Class) before using it.`);
1953
+
1954
+ Globals$1.currentSlotChildren = [...el.childNodes]; // TODO: Does this need to be a stack?
1955
+ let newEl = new Constructor(attribs);
1956
+
1957
+ // 2b. Copy attributes over.
1958
+ if (isAttrib) {
1959
+ newEl.setAttribute('is', isAttrib);
1960
+ // el.removeAttribute('_is');
1961
+ }
1962
+ for (let attrib of el.attributes)
1963
+ if (attrib.name !== '_is')
1964
+ newEl.setAttribute(attrib.name, attrib.value);
1965
+
1966
+ // Set dynamic attributes if they are primitive types.
1967
+ for (let name in attribs) {
1968
+ let val = attribs[name];
1969
+ let valType = typeof val;
1970
+ if (valType === 'boolean') {
1971
+ if (val !== false && val !== undefined && val !== null) // Util.isFalsy() inlined
1972
+ newEl.setAttribute(name, '');
1973
+ }
1974
+
1975
+ // If type is a non-boolean primitive, set the attribute value.
1976
+ else if (valType==='string' || valType === 'number' || valType==='bigint')
1977
+ newEl.setAttribute(name, val);
1888
1978
  }
1979
+
1980
+
1981
+ // 2c. If an id pointed at the placeholder, update it to point to the new element.
1982
+ let id = newEl.getAttribute('data-id') || newEl.getAttribute('id');
1983
+ if (id)
1984
+ delve(this.parentNg.getRootNode(), id.split(/\./g), newEl);
1985
+
1986
+ // 2d. Update paths to use replaced element.
1987
+ let ng = this.parentNg;
1988
+ this.nodeMarker = newEl;
1989
+ for (let path of ng.paths) {
1990
+ if (path.nodeMarker === el)
1991
+ path.nodeMarker = newEl;
1992
+ if (path.nodeBefore === el)
1993
+ path.nodeBefore = newEl;
1994
+ }
1995
+ if (ng.startNode === el)
1996
+ ng.startNode = newEl;
1997
+ if (ng.endNode === el)
1998
+ ng.endNode = newEl;
1999
+
2000
+ // 2f. Call render() if it wasn't called by the constructor.
2001
+ // This must happen before we add it to the DOM which can trigger connectedCallback() -> renderFirstTime()
2002
+ // Because that path renders it without the attribute expressions.
2003
+ if (typeof newEl.render === 'function' && !Globals$1.rendered.has(newEl))
2004
+ newEl.render(attribs, changed);
2005
+
2006
+ // 2g. Update attribute paths to use the new element and re-apply them.
2007
+ for (let i=0, attribPath; attribPath = this.attribPaths[i]; i++) {
2008
+ attribPath.parentNg = this.parentNg;
2009
+ attribPath.nodeMarker = newEl;
2010
+ attribPath.apply(exprs[i]);
2011
+ }
2012
+
2013
+ // 2e. Swap it to the DOM.
2014
+ el.replaceWith(newEl);
1889
2015
  }
1890
- onContextChange?.(html, html.length, this.state.context, null);
1891
- return this.state.context;
2016
+
2017
+ // 2f. Render
2018
+ else if (typeof el.render === 'function')
2019
+ el.render(attribs, changed);
2020
+
2021
+ Globals$1.currentSlotChildren = null;
1892
2022
  }
1893
- }
1894
2023
 
1895
- HtmlParser.Attribute = 'Attribute';
1896
- HtmlParser.Text = 'Text';
1897
- HtmlParser.Tag = 'Tag';
2024
+ /**
2025
+ * @param newRoot {HTMLElement}
2026
+ * @param pathOffset {int}
2027
+ * @return {Path} */
2028
+ clone(newRoot, pathOffset=0) {
2029
+
2030
+ let nodeMarker = this.getNewNodeMarker(newRoot, pathOffset);
2031
+ let result = new PathToComponent(null, nodeMarker);
2032
+ result.attribPaths = this.attribPaths.map(path => path.clone(newRoot, pathOffset));
2033
+
2034
+
2035
+
2036
+ return result;
2037
+ }
2038
+
2039
+ getExpressionCount() { return 0 }
2040
+
2041
+
2042
+ }
1898
2043
 
1899
2044
  /**
1900
2045
  * A Shell is created from a tagged template expression instantiated as Nodes,
@@ -1909,10 +2054,10 @@ class Shell {
1909
2054
  * @type {DocumentFragment|Text} DOM parent of the shell's nodes. */
1910
2055
  fragment;
1911
2056
 
1912
- /** @type {ExprPath[]} Paths to where expressions should go. */
2057
+ /** @type {Path[]} Paths to where expressions should go. */
1913
2058
  paths = [];
1914
2059
 
1915
- // Elements with events. Not yet used.
2060
+ // Elements with events. Is there a reason to use this? We already mark event Exprs in Shell.js.
1916
2061
  // events = [];
1917
2062
 
1918
2063
  /** @type {int[][]} Array of paths */
@@ -1924,14 +2069,6 @@ class Shell {
1924
2069
  /** @type {int[][]} Array of paths */
1925
2070
  styles = [];
1926
2071
 
1927
- /** @type {int[][]} Array of paths. Used by activateEmbeds() to quickly find components. */
1928
- staticComponents = [];
1929
-
1930
- /** @type {{path:int[], attribs:Record<string, string>}[]} */
1931
- //componentAttribs = [];
1932
-
1933
-
1934
-
1935
2072
  /**
1936
2073
  * Create the nodes but without filling in the expressions.
1937
2074
  * This is useful because the expression-less nodes created by a template can be cached.
@@ -1942,6 +2079,7 @@ class Shell {
1942
2079
 
1943
2080
 
1944
2081
 
2082
+ // If no html tags or entities, just create a text node.
1945
2083
  if (html.length === 1 && !html[0].match(/[<&]/)) {
1946
2084
  this.fragment = Globals$1.doc.createTextNode(html[0]);
1947
2085
  return;
@@ -1949,11 +2087,11 @@ class Shell {
1949
2087
 
1950
2088
 
1951
2089
  // 1. Add placeholders
1952
- let joinedHtml = Shell.addPlaceholders(html);
2090
+ let htmlWithPlaceholders = Shell.addPlaceholders(html);
1953
2091
 
1954
2092
  let template = Globals$1.doc.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
1955
- if (joinedHtml)
1956
- template.innerHTML = joinedHtml;
2093
+ if (htmlWithPlaceholders)
2094
+ template.innerHTML = htmlWithPlaceholders;
1957
2095
  else // Create one text node, so shell isn't empty and NodeGroups created from it have something to point the startNode and endNode at.
1958
2096
  template.content.append(Globals$1.doc.createTextNode(''));
1959
2097
  this.fragment = template.content;
@@ -1965,20 +2103,28 @@ class Shell {
1965
2103
  const walker = Globals$1.doc.createTreeWalker(this.fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
1966
2104
  while (node = walker.nextNode()) {
1967
2105
 
1968
- // Remove previous after each iteration, so paths will still be calculated correctly.
2106
+ // Remove previous elements after each iteration, so paths will still be calculated correctly.
1969
2107
  toRemove.map(el => el.remove());
1970
2108
  toRemove = [];
1971
2109
 
1972
2110
  // Replace attributes
1973
2111
  if (node.nodeType === 1) {
1974
- for (let attr of [...node.attributes]) { // Copy the attributes array b/c we remove attributes as we go.
2112
+ const hasIs = node.hasAttribute('is');
2113
+ const isComponent = (hasIs || node.tagName.includes('-'));
2114
+ const componentAttribPaths = [];
2115
+
2116
+ for (let attr of [...node.attributes]) { // Copy the attributes array b/c we remove attributes with placeholders as we go.
1975
2117
 
1976
2118
  // Whole attribute
1977
2119
  let matches = attr.name.match(/^[\ue000-\uf8ff]$/);
1978
2120
  if (matches) {
1979
- this.paths.push(new ExprPath(null, node, ExprPathType.AttribMultiple));
2121
+ let path = new PathToAttribs(null, node);
2122
+ this.paths.push(path);
2123
+ if (isComponent)
2124
+ componentAttribPaths.push(path);
2125
+
1980
2126
  placeholdersUsed ++;
1981
- node.removeAttribute(matches[0]);
2127
+ node.removeAttribute(matches[0]); // TODO: Is this necessary?
1982
2128
  }
1983
2129
 
1984
2130
  // Just the attribute value.
@@ -1986,15 +2132,36 @@ class Shell {
1986
2132
  let parts = attr.value.split(/[\ue000-\uf8ff]/g);
1987
2133
  if (parts.length > 1) {
1988
2134
  let nonEmptyParts = (parts.length === 2 && !parts[0].length && !parts[1].length) ? null : parts;
1989
- let type = Util.isEvent(attr.name) ? ExprPathType.Event : ExprPathType.AttribValue;
1990
2135
 
1991
- this.paths.push(new ExprPath(null, node, type, attr.name, nonEmptyParts));
2136
+ let path = Util.isEvent(attr.name)
2137
+ ? new PathToEvent(null, node, attr.name, nonEmptyParts)
2138
+ : new PathToAttribValue(null, node, attr.name, nonEmptyParts);
2139
+ path.isHtmlProperty = Util.isHtmlProp(node, attr.name);
2140
+ this.paths.push(path);
2141
+ if (isComponent) {
2142
+ path.isComponentAttrib = true;
2143
+ componentAttribPaths.push(path);
2144
+ }
2145
+
1992
2146
  placeholdersUsed += parts.length - 1;
1993
2147
  node.setAttribute(attr.name, parts.join(''));
1994
2148
  }
1995
2149
  }
1996
2150
  }
2151
+
2152
+ // Web components
2153
+ if (isComponent) {
2154
+ let path = new PathToComponent(null, node);
2155
+ path.attribPaths = componentAttribPaths;
2156
+ this.paths.splice(this.paths.length - componentAttribPaths.length, 0, path); // Insert before its componentAttribPaths
2157
+
2158
+ if (hasIs) {
2159
+ node.setAttribute('_is', node.getAttribute('is'));
2160
+ node.removeAttribute('is');
2161
+ }
2162
+ }
1997
2163
  }
2164
+
1998
2165
  // Replace comment placeholders
1999
2166
  else if (node.nodeType === 8 && node.nodeValue === '!✨!') {
2000
2167
 
@@ -2004,7 +2171,7 @@ class Shell {
2004
2171
  // Get or create nodeBefore.
2005
2172
  let nodeBefore = node.previousSibling; // Can be the same as another Path's nodeMarker.
2006
2173
  if (!nodeBefore) {
2007
- nodeBefore = Globals$1.doc.createComment('ExprPath:'+this.paths.length);
2174
+ nodeBefore = Globals$1.doc.createComment('Path:'+this.paths.length);
2008
2175
  node.parentNode.insertBefore(nodeBefore, node);
2009
2176
  }
2010
2177
 
@@ -2020,11 +2187,11 @@ class Shell {
2020
2187
  // Re-use existing comment placeholder.
2021
2188
  else {
2022
2189
  nodeMarker = node;
2023
- nodeMarker.textContent = 'ExprPathEnd:'+ this.paths.length;
2190
+ nodeMarker.textContent = 'PathEnd:'+ this.paths.length;
2024
2191
  }
2025
2192
 
2026
2193
 
2027
- let path = new ExprPath(nodeBefore, nodeMarker, ExprPathType.Content);
2194
+ let path = new PathToNodes(nodeBefore, nodeMarker);
2028
2195
  this.paths.push(path);
2029
2196
  placeholdersUsed ++;
2030
2197
  }
@@ -2038,18 +2205,17 @@ class Shell {
2038
2205
  // Here we look for expressions in comments.
2039
2206
  // We don't actually update them dynamically, but we still add paths for them.
2040
2207
  // That way the expression count still matches.
2041
- else if (node.nodeType === Node.COMMENT_NODE) {
2208
+ else if (node.nodeType === 8) { // Node.COMMENT_NODE
2042
2209
  let parts = node.textContent.split(/[\ue000-\uf8ff]/g);
2043
2210
  for (let i=0; i<parts.length-1; i++) {
2044
- let path = new ExprPath(node.previousSibling, node);
2045
- path.type = ExprPathType.Comment;
2211
+ let path = new Path(node.previousSibling, node);
2046
2212
  this.paths.push(path);
2047
2213
  placeholdersUsed ++;
2048
2214
  }
2049
2215
  }
2050
2216
 
2051
2217
  // Replace comment placeholders inside script and style tags, which have become text nodes.
2052
- else if (node.nodeType === Node.TEXT_NODE && ['SCRIPT', 'STYLE'].includes(node.parentNode?.nodeName)) {
2218
+ else if (node.nodeType === 3 && ['SCRIPT', 'STYLE'].includes(node.parentNode?.nodeName)) { // Node.TEXT_NODE
2053
2219
  let parts = node.textContent.split(commentPlaceholder);
2054
2220
  if (parts.length > 1) {
2055
2221
 
@@ -2062,7 +2228,7 @@ class Shell {
2062
2228
  }
2063
2229
 
2064
2230
  for (let i=0, node; node=placeholders[i]; i++) {
2065
- let path = new ExprPath(node.previousSibling, node, ExprPathType.Content);
2231
+ let path = new PathToNodes(node.previousSibling, node);
2066
2232
  this.paths.push(path);
2067
2233
  placeholdersUsed ++;
2068
2234
 
@@ -2081,51 +2247,31 @@ class Shell {
2081
2247
  if (placeholdersUsed !== html.length-1)
2082
2248
  throw new Error(`Could not parse expressions in template. Check for duplicate attributes or malformed html: ${html.join('${...}')}`);
2083
2249
 
2084
- // Handle solarite-placeholder's.
2085
-
2086
- // 3. Rename "is" attributes so the Web Components don't instantiate until we have the values of their PathExpr arguments.
2087
- // that happens in NodeGroup.applyComponentExprs()
2088
- for (let el of this.fragment.querySelectorAll('[is]'))
2089
- el.setAttribute('_is', el.getAttribute('is'));
2090
-
2091
2250
  for (let path of this.paths) {
2092
2251
  if (path.nodeBefore)
2093
2252
  path.nodeBeforeIndex = Array.prototype.indexOf.call(path.nodeBefore.parentNode.childNodes, path.nodeBefore);
2094
- path.nodeMarkerPath = getNodePath(path.nodeMarker);
2095
2253
 
2096
- // Cache so we don't have to calculate this later inside NodeGroup.applyExprs()
2097
- if ((path.type === ExprPathType.AttribValue || path.type === ExprPathType.Event) && path.nodeMarker.nodeType === 1 &&
2098
- (path.nodeMarker.tagName.includes('-') || path.nodeMarker.hasAttribute('is'))) {
2099
- path.isComponent = true;
2100
- }
2254
+ // Must be calculated after we remove the toRemove nodes:
2255
+ path.nodeMarkerPath = Path.get(path.nodeMarker);
2256
+
2257
+
2101
2258
  }
2102
2259
 
2103
2260
  this.findEmbeds();
2104
2261
 
2262
+
2105
2263
 
2106
2264
  }
2107
2265
 
2108
2266
  /**
2109
2267
  * 1. Add a Unicode placeholder char for where expressions go within attributes.
2110
2268
  * 2. Add a comment placeholder for where expressions are children of other nodes.
2111
- * 3. Append -solarite-placeholder to the tag names of custom components so that we can wait to instantiate them later.
2269
+ * 3. Append -solarite-placeholder to the tag names of custom components so that we can instantiate them later
2270
+ * when we can manually call their constructors with the proper attribute and children arguments from evaluated expressions.
2112
2271
  * @param htmlChunks {string[]}
2113
- * @returns {string} */
2272
+ * @returns {string} Html with the placeholders in place. */
2114
2273
  static addPlaceholders(htmlChunks) {
2115
- let tokens = [];
2116
-
2117
- function addToken(token, context) {
2118
-
2119
- if (context === HtmlParser.Tag) {
2120
- // Find Solarite Components tags and append -solarite-placeholder to their tag names
2121
- // and give them a solarite-placeholder attribute so we can easily find them later.
2122
- // This way we can gather their constructor arguments and their children before we call their constructor.
2123
- // Later, NodeGroup.instantiateComponent() will replace them with the real components.
2124
- // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
2125
- token = token.replace(/^<\/?[a-z][a-z0-9]*-[a-z0-9-]+/i, match => match + '-solarite-placeholder solarite-placeholder');
2126
- }
2127
- tokens.push(token);
2128
- }
2274
+ let result = [];
2129
2275
 
2130
2276
  let htmlParser = new HtmlParser(); // Reset the context.
2131
2277
  for (let i = 0; i < htmlChunks.length; i++) {
@@ -2133,10 +2279,20 @@ class Shell {
2133
2279
 
2134
2280
  // Append -solarite-placholder to web component tags, so we can pass args to them when they're instantiated.
2135
2281
  let lastIndex = 0;
2136
- let context = htmlParser.parse(lastHtml, (html, index, oldContext, newContext) => {
2282
+ let context = htmlParser.parse(lastHtml, (html, index, prevContext/*, nextContext*/) => { // This function is called every time the html context changes.
2137
2283
  if (lastIndex !== index) {
2138
2284
  let token = html.slice(lastIndex, index);
2139
- addToken(token, oldContext);
2285
+
2286
+ if (prevContext === HtmlParser.Tag) {
2287
+ // Find Web Component tags and append -solarite-placeholder to their tag names
2288
+ // This way we can gather their constructor arguments and their children before we call their constructor.
2289
+ // Later, PathToComponent.apply() will replace them with the real components.
2290
+ // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
2291
+ const isWebComponentTagName = /^<\/?[a-z][a-z0-9]*-[a-z0-9-]+/i; // a dash in the middle
2292
+ token = token.replace(isWebComponentTagName, match => match + '-SOLARITE-PLACEHOLDER'); // caps to match other instances of this string, for better compression.
2293
+ }
2294
+
2295
+ result.push(token);
2140
2296
  }
2141
2297
  lastIndex = index;
2142
2298
  });
@@ -2144,13 +2300,13 @@ class Shell {
2144
2300
  // Insert placeholders
2145
2301
  if (i < htmlChunks.length - 1) {
2146
2302
  if (context === HtmlParser.Text)
2147
- tokens.push(commentPlaceholder); // Comment Placeholder. because we can't put text in between <tr> tags for example.
2303
+ result.push(commentPlaceholder); // Comment Placeholder. because we can't put text in between <tr> tags for example.
2148
2304
  else
2149
- tokens.push(String.fromCharCode(attribPlaceholder + i));
2305
+ result.push(String.fromCharCode(attribPlaceholder + i));
2150
2306
  }
2151
2307
  }
2152
2308
 
2153
- return tokens.join('');
2309
+ return result.join('');
2154
2310
  }
2155
2311
 
2156
2312
  /**
@@ -2162,10 +2318,10 @@ class Shell {
2162
2318
  * this.ids
2163
2319
  * this.staticComponents */
2164
2320
  findEmbeds() {
2165
- this.scripts = Array.prototype.map.call(this.fragment.querySelectorAll('scripts'), el => getNodePath(el));
2321
+ this.scripts = Array.prototype.map.call(this.fragment.querySelectorAll('scripts'), el => Path.get(el));
2166
2322
 
2167
- // TODO: only find styles that have ExprPaths in them?
2168
- this.styles = Array.prototype.map.call(this.fragment.querySelectorAll('style'), el => getNodePath(el));
2323
+ // TODO: only find styles that have Paths in them?
2324
+ this.styles = Array.prototype.map.call(this.fragment.querySelectorAll('style'), el => Path.get(el));
2169
2325
 
2170
2326
  let idEls = this.fragment.querySelectorAll('[id],[data-id]');
2171
2327
 
@@ -2176,17 +2332,7 @@ class Shell {
2176
2332
  throw new Error(`<${el.tagName.toLowerCase()} id="${id}"> can't override existing HTMLElement id property.`)
2177
2333
  }
2178
2334
 
2179
- this.ids = Array.prototype.map.call(idEls, el => getNodePath(el));
2180
-
2181
- for (let el of this.fragment.querySelectorAll('*')) {
2182
- if (el.tagName.includes('-') || el.hasAttribute('_is'))
2183
-
2184
- // Dynamic components are components that have attributes with expression values.
2185
- // They are created from applyExprs()
2186
- // But static components are created in a separate path inside the NodeGroup constructor.
2187
- if (!this.paths.find(path => path.nodeMarker === el))
2188
- this.staticComponents.push(getNodePath(el));
2189
- }
2335
+ this.ids = Array.prototype.map.call(idEls, el => Path.get(el));
2190
2336
  }
2191
2337
 
2192
2338
  /**
@@ -2221,17 +2367,14 @@ const attribPlaceholder = 0xe000; // https://en.wikipedia.org/wiki/Private_Use_A
2221
2367
  *
2222
2368
  * The range is determined by startNode and nodeMarker.
2223
2369
  * startNode - never null. An empty text node is created before the first path if none exists.
2224
- * nodeMarker - null if this Nodegroup is at the end of its parents' nodes.
2225
- *
2226
- *
2227
- * */
2370
+ * nodeMarker - null if this Nodegroup is at the end of its parents' nodes.*/
2228
2371
  class NodeGroup {
2229
2372
 
2230
2373
  /**
2231
2374
  * @Type {RootNodeGroup} */
2232
2375
  rootNg;
2233
2376
 
2234
- /** @type {ExprPath} */
2377
+ /** @type {Path} */
2235
2378
  parentPath;
2236
2379
 
2237
2380
  /** @type {Node|HTMLElement} First node of NodeGroup. Should never be null. */
@@ -2239,10 +2382,10 @@ class NodeGroup {
2239
2382
 
2240
2383
  /** @type {Node|HTMLElement} A node that never changes that this NodeGroup should always insert its nodes before.
2241
2384
  * An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.
2242
- * TODO: But sometimes startNode and endNode point to the same node. Document htis inconsistency. */
2385
+ * TODO: But sometimes startNode and endNode point to the same node. Document this inconsistency. */
2243
2386
  endNode;
2244
2387
 
2245
- /** @type {ExprPath[]} */
2388
+ /** @type {Path[]} */
2246
2389
  paths = [];
2247
2390
 
2248
2391
  /** @type {string} Key that matches the template and the expressions. */
@@ -2262,280 +2405,212 @@ class NodeGroup {
2262
2405
  * @type {?Map<HTMLStyleElement, string>} */
2263
2406
  styles;
2264
2407
 
2265
- dynamicComponents = new Set();
2266
- staticComponents = [];
2267
-
2268
2408
  /** @type {Template} */
2269
2409
  template;
2270
2410
 
2411
+ /**
2412
+ * Root node at the top of the hierarchy.
2413
+ * Should be moved to RootNodeGroup
2414
+ * @type {HTMLElement} */
2415
+ root;
2416
+
2271
2417
 
2272
2418
  /**
2273
2419
  * Create an "instantiated" NodeGroup from a Template and add it to an element.
2420
+ * Don't call applyExprs() yet to apply expressions or instantiate components yet.
2274
2421
  * @param template {Template} Create it from the html strings and expressions in this template.
2275
- * @param parentPath {?ExprPath} */
2276
- constructor(template, parentPath=null) {
2422
+ * @param parentPath {?Path}
2423
+ * @param el {?HTMLElement} Optional, pre-existing htmlElement that will be the root.
2424
+ * @param options {?object} Only used for RootNodeGroup */
2425
+ constructor(template, parentPath=null, el=null, options=null) {
2277
2426
  this.rootNg = parentPath?.parentNg?.rootNg || this;
2278
2427
  this.parentPath = parentPath;
2279
-
2280
- if (!(this instanceof RootNodeGroup)) {
2281
-
2282
- let [fragment, shell] = this.populateFromTemplate(template);
2283
-
2284
- if (fragment && template.exprs.length) {
2285
- this.updatePaths(fragment, shell.paths);
2286
-
2287
- // Static web components can sometimes have children created via expressions.
2288
- // But calling applyExprs() will mess up the shell's path to them.
2289
- // So we find them first, then call instantiateStaticComponents() after their children have been created.
2290
- this.staticComponents = this.findStaticComponents(fragment, shell);
2291
-
2292
- this.activateEmbeds(fragment, shell);
2293
-
2294
- // Apply exprs
2295
- this.applyExprs(template.exprs);
2296
-
2297
- this.instantiateStaticComponents(this.staticComponents);
2298
- }
2299
- else if (shell)
2300
- this.activateEmbeds(fragment, shell);
2301
- }
2302
- }
2303
-
2304
- /**
2305
- * Common init shared by RootNodeGroup and NodeGroup constructors.
2306
- * But in a separate function because they need to do this at a different step.
2307
- * @param template {Template} Create it from the html strings and expressions in this template.
2308
- * @returns {[DocumentFragment, Shell]} The Shell created from the template,a nd the fragment cloned from the Shell.*/
2309
- populateFromTemplate(template) {
2428
+
2310
2429
 
2311
2430
  this.template = template;
2312
- this.exactKey = template.getExactKey();
2313
2431
  this.closeKey = template.getCloseKey();
2314
2432
 
2315
2433
  // If it's just a text node, skip a bunch of unnecessary steps.
2316
2434
  if (template.isText) {
2317
- let textNode = Globals$1.doc.createTextNode(template.html[0]);
2318
- this.startNode = this.endNode = textNode;
2319
- return [];
2435
+ this.startNode = this.endNode = Globals$1.doc.createTextNode(template.html[0]);
2320
2436
  }
2321
2437
 
2322
- // Get a cached version of the parsed and instantiated html, and ExprPaths:
2323
2438
  else {
2324
- let shell = Shell.get(template.html);
2325
- let fragment = shell.fragment.cloneNode(true);
2439
+ // Get a cached version of the parsed and instantiated html, and Paths:
2440
+ const shell = Shell.get(template.html);
2441
+ const shellFragment = shell.fragment.cloneNode(true);
2326
2442
 
2327
- if (fragment?.nodeType === 11) { // DocumentFragment
2328
- let childNodes = fragment.childNodes;
2329
- this.startNode = childNodes[0];
2330
- this.endNode = childNodes[childNodes.length - 1];
2331
- }
2332
- else
2333
- this.startNode = this.endNode = fragment;
2443
+ if (shellFragment.nodeType === 11) { // DocumentFragment
2444
+ this.startNode = shellFragment.firstChild;
2445
+ this.endNode = shellFragment.lastChild;
2446
+ } else
2447
+ this.startNode = this.endNode = shellFragment;
2334
2448
 
2335
- return [fragment, shell];
2336
- }
2337
- }
2338
2449
 
2339
- /**
2340
- * Use the paths to insert the given expressions.
2341
- * Dispatches expression handling to other functions depending on the path type.
2342
- * @param exprs {(*|*[]|function|Template)[]}
2343
- * @param paths {?ExprPath[]} Optional. Only used for testing. Normally uses this.paths. */
2344
- applyExprs(exprs, paths=null) {
2345
- paths = paths || this.paths;
2450
+ // Special setup for RootNodeGroup
2451
+ if (this instanceof RootNodeGroup) {
2346
2452
 
2347
-
2348
2453
 
2349
- // Things to consider:
2350
- // 1. One path may use multipe expressions. E.g. <div class="${1} ${2}">
2351
- // 2. One component may need to use multiple attribute paths to be instantiated.
2352
- // 3. We apply them in reverse order so that a <select> box has its children created from an expression
2353
- // before its instantiated and its value attribute is set via an expression.
2454
+ let startingPathDepth = 0;
2455
+ this.options = options;
2456
+ if (shellFragment instanceof Text) {
2457
+ if (!el)
2458
+ throw new Error('Cannot create a standalone text node');
2354
2459
 
2355
- let exprIndex = exprs.length - 1; // Update exprs at paths.
2356
- let lastComponentPathIndex;
2357
- 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.
2358
- for (let i = paths.length - 1, path; path = paths[i]; i--) {
2359
- let prevPath = paths[i - 1];
2360
- let nextPath = paths[i + 1];
2460
+ this.root = el;
2461
+ if (shellFragment.nodeValue.length)
2462
+ this.root.append(shellFragment);
2463
+ }
2361
2464
 
2362
- // Get the expressions associated with this path.
2363
- if (path.attrValue?.length > 2) {
2364
- let startIndex = (exprIndex - (path.attrValue.length - 1)) + 1;
2365
- pathExprs[i] = exprs.slice(startIndex, exprIndex + 1); // probably doesn't allocate if the JS vm implements copy on write.
2366
- exprIndex -= pathExprs[i].length;
2367
- } else {
2368
- pathExprs[i] = [exprs[exprIndex]];
2369
- exprIndex--;
2370
- }
2465
+ else {
2466
+ if (el) {
2467
+ this.root = el;
2468
+
2469
+ // Save slot
2470
+ // 1. Globals.currentSlotChildren is set if this is called via PathToComponent.applyComponent() calls render()
2471
+ // 2. el.childNodes is set if render() is called manually for the first time.
2472
+ let slotChildren;
2473
+ if (Globals$1.currentSlotChildren || el.childNodes.length) {
2474
+ slotChildren = Globals$1.doc.createDocumentFragment();
2475
+ slotChildren.append(...(Globals$1.currentSlotChildren || el.childNodes));
2476
+ }
2477
+
2478
+ // If el should replace the root node of the fragment.
2479
+ if (isReplaceEl(shellFragment, this.root.tagName)) {
2480
+ this.root.append(...shellFragment.children[0].childNodes);
2371
2481
 
2482
+ // Copy attributes
2483
+ for (let attrib of shellFragment.children[0].attributes)
2484
+ if (!this.root.hasAttribute(attrib.name))
2485
+ this.root.setAttribute(attrib.name, attrib.value);
2372
2486
 
2373
- // TODO: Need to end and restart this block when going from one component to the next?
2374
- // Think of having two adjacent components.
2375
- // But the dynamicAttribsAdjacet test already passes.
2487
+ // Go one level deeper into all of shell's paths.
2488
+ startingPathDepth = 1;
2489
+ }
2376
2490
 
2377
- // If expr is an attribute in a component:
2378
- // 1. Instantiate it if it hasn't already been, sending all expr's to its constructor.
2379
- // 2. Otherwise send them to its render function.
2380
- // Components with no expressions as attributes are instead activated in activateEmbeds().
2381
- if (path.nodeMarker !== this.rootNg.root && path.isComponent) {
2491
+ else {
2492
+ let isEmpty = shellFragment.childNodes.length === 1 && shellFragment.childNodes[0].nodeType === 3 && shellFragment.childNodes[0].textContent === '';
2493
+ if (!isEmpty)
2494
+ this.root.append(...shellFragment.childNodes);
2495
+ }
2382
2496
 
2383
- if (!nextPath || !nextPath.isComponent || nextPath.nodeMarker !== path.nodeMarker)
2384
- lastComponentPathIndex = i;
2385
- let isFirstComponentPath = !prevPath || !prevPath.isComponent || prevPath.nodeMarker !== path.nodeMarker;
2386
2497
 
2387
- if (isFirstComponentPath) {
2498
+ // Setup slot children (deprecated)
2499
+ if (slotChildren) {
2500
+ // Named slots
2501
+ for (let slot of el.querySelectorAll('slot[name]')) {
2502
+ let name = slot.getAttribute('name');
2503
+ if (name) {
2504
+ let slotChildren2 = slotChildren.querySelectorAll(`[slot='${name}']`);
2505
+ slot.append(...slotChildren2);
2506
+ }
2507
+ }
2508
+ // Unnamed slots
2509
+ let unamedSlot = el.querySelector('slot:not([name])');
2510
+ if (unamedSlot)
2511
+ unamedSlot.append(slotChildren);
2512
+ // No slots
2513
+ else
2514
+ el.append(slotChildren);
2515
+ }
2516
+ }
2388
2517
 
2389
- let componentProps = {};
2390
- for (let j=i; j<=lastComponentPathIndex; j++) {
2391
- let attrName = paths[j].attrName; // Util.dashesToCamel(paths[j].attrName);
2392
- componentProps[attrName] = pathExprs[j].length > 1 ? pathExprs[j].join('') : pathExprs[j][0];
2518
+ // Instantiate as a standalone element.
2519
+ else {
2520
+ let onlyChild = getSingleEl(shellFragment);
2521
+ this.root = onlyChild || shellFragment; // We return the whole fragment when calling h() with a collection of nodes.
2522
+ if (onlyChild)
2523
+ startingPathDepth = 1;
2393
2524
  }
2394
2525
 
2395
- this.handleComponent(path.nodeMarker, componentProps, true);
2526
+ // Exclude the path to ourself. Otherwise we get infinite recursion.
2527
+ // let paths = [...shell.paths];
2528
+ // if (paths[0] instanceof PathToComponent)
2529
+ // paths.shift();
2396
2530
 
2397
- // Set attributes on component.
2398
- for (let j=i; j<=lastComponentPathIndex; j++)
2399
- paths[j].apply(pathExprs[j]);
2531
+ this.setPathsFromFragment(this.root, shell.paths, startingPathDepth);
2532
+ this.activateEmbeds(this.root, shell, startingPathDepth);
2400
2533
  }
2401
- }
2402
-
2403
- // Else apply it normally
2404
- else
2405
- path.apply(pathExprs[i]);
2534
+ this.startNode = this.endNode = this.root;
2406
2535
 
2536
+ Globals$1.rootNodeGroups.set(this.root, this);
2537
+ } // end if RootNodeGroup
2407
2538
 
2408
- } // end for(path of this.paths)
2539
+ else if (shell) {
2540
+ if (shell.paths.length) {
2541
+ this.setPathsFromFragment(shellFragment, shell.paths);
2542
+ }
2409
2543
 
2544
+ this.activateEmbeds(shellFragment, shell);
2545
+ }
2546
+ }
2410
2547
 
2411
- // TODO: Only do this if we have ExprPaths within styles?
2412
- this.updateStyles();
2548
+
2549
+ }
2413
2550
 
2414
- // Call render() on static web components. This makes the component.staticAttribs() test work.
2415
- for (let el of this.staticComponents)
2416
- if (el.render)
2417
- el.render(Util.attribsToObject(el)); // It has no expressions.
2418
2551
 
2419
- // Invalidate the nodes cache because we just changed it.
2420
- this.nodesCache = null;
2552
+ /**
2553
+ * Use the paths to insert the given expressions.
2554
+ * Dispatches expression handling to other functions depending on the path type.
2555
+ * @param exprs {(*|*[]|function|Template)[]}
2556
+ * @param changed {boolean} If true, the expr's have changed since the last time thsi function was called.
2557
+ * @param includeNonComponents {boolean}
2558
+ * We still need to call PathToComponent.apply() even if changed=false so the user can handle the rendering. */
2559
+ applyExprs(exprs, changed=true, includeNonComponents=true) {
2421
2560
 
2422
- // If there's leftover expressions, there's probably an issue with the Shell that created this NodeGroup,
2423
- // and the number of paths not matching.
2424
2561
 
2425
2562
 
2563
+ let paths = this.paths;
2426
2564
 
2427
-
2428
- }
2565
+ // Things to consider:
2566
+ // 1. Paths consume a varying number of expressions.
2567
+ // An PathToAttribs may use multipe expressions. E.g. <div class="${1} ${2}">
2568
+ // While an PathToComponent uses zero.
2569
+ // 2. An PathToComponent references other Paths that set its attribute values.
2570
+ // 3. We apply them in reverse order so that a <select> box has its children created from an expression
2571
+ // before its instantiated and its value attribute is set via an expression.
2572
+ let exprIndex = exprs.length; // Update exprs at paths.
2573
+ 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.
2574
+ for (let i = paths.length - 1, path; path = paths[i]; i--) {
2575
+ if (i===0 && path instanceof PathToComponent && path.nodeMarker === this.getRootNode())
2576
+ continue;
2429
2577
 
2430
- /**
2431
- * Unified path to ensure a child component is instantiated (if placeholder) and optionally rendered.
2432
- * @param el {HTMLElement}
2433
- * @param props {?Object}
2434
- * @param doRender {boolean}
2435
- * @return {HTMLElement} The (possibly replaced) element. */
2436
- handleComponent(el, props=null, doRender=true) {
2437
- let isPreHtmlElement = el.hasAttribute('solarite-placeholder');
2438
- let isPreIsElement = el.hasAttribute('_is');
2439
- let attribs, children;
2440
- if (isPreHtmlElement || isPreIsElement)
2441
- [el, attribs, children] = this.instantiateComponent(el, isPreHtmlElement, props);
2442
- if (doRender && el.render) {
2443
- if (!attribs) {
2444
- attribs = Util.attribsToObject(el);
2445
- for (let name in props || {})
2446
- attribs[Util.dashesToCamel(name)] = props[name];
2447
- children = el.childNodes;
2578
+ // Get the expressions associated with this path.
2579
+ let exprCount = path.getExpressionCount();
2580
+ pathExprs[i] = exprs.slice(exprIndex-exprCount, exprIndex); // slice() probably doesn't allocate if the JS vm implements copy on write.
2581
+ exprIndex -= exprCount;
2582
+
2583
+ // Component expressions don't have a corresponding user-provided expression.
2584
+ // They use expressions from the paths that provide their attributes.
2585
+ if (path instanceof PathToComponent) {
2586
+ let attribExprs = pathExprs.slice(i+1, i+1 + path.attribPaths.length); // +1 b/c we move forward from the component path.
2587
+ path.apply(attribExprs, true, changed);
2448
2588
  }
2449
- el.render(attribs, children);
2589
+ else if (includeNonComponents)
2590
+ path.apply(pathExprs[i]);
2450
2591
  }
2451
- return el;
2452
- }
2453
-
2454
- /**
2455
- * We swap the placeholder element for the real element so we can pass its dynamic attributes
2456
- * to its constructor.
2457
- * This is only called by handleComponent()
2458
- * This does not call render()
2459
- *
2460
- * @param el {HTMLElement}
2461
- * @param isPreHtmlElement {?boolean} True if the element's tag name ends with -solarite-placeholder
2462
- * @param props {Object} Attributes with dynamic values.
2463
- * @return {[HTMLElement, attribs:Object, children:Node[]]}} */
2464
- instantiateComponent(el, isPreHtmlElement=undefined, props=undefined) {
2465
- if (isPreHtmlElement === undefined)
2466
- isPreHtmlElement = !el.hasAttribute('_is');
2467
-
2468
- let tagName = (isPreHtmlElement
2469
- ? el.tagName.slice(0, -21) // Remove -SOLARITE-PLACEHOLDER
2470
- : el.getAttribute('is')).toLowerCase();
2471
-
2472
-
2473
- // Throw if custom element isn't defined.
2474
- let Constructor = customElements.get(tagName);
2475
- if (!Constructor)
2476
- throw new Error(`The custom tag name ${tagName} is not registered.`)
2477
-
2478
- // Pass other attribs to constructor, since otherwise they're not yet set on the element,
2479
- // and the constructor would otherwise have no way to see them.
2480
- let attribs = Util.attribsToObject(el, 'solarite-placeholder');
2481
- for (let name in props || {})
2482
- attribs[Util.dashesToCamel(name)] = props[name];
2483
2592
 
2593
+ // If there's leftover expressions, there's probably an issue with the Shell that created this NodeGroup,
2594
+ // and the number of paths not matching.
2595
+
2484
2596
 
2485
- // Create the web component.
2486
- // Get the children that aren't Solarite's comment placeholders.
2487
- let children = [...el.childNodes].filter(node => node.nodeType !== Node.COMMENT_NODE || !node.nodeValue.startsWith('ExprPath'));
2488
- let newEl = new Constructor(attribs, children);
2489
2597
 
2490
- if (!isPreHtmlElement)
2491
- newEl.setAttribute('is', el.getAttribute('is').toLowerCase());
2598
+ if (includeNonComponents) {
2492
2599
 
2493
- // Replace the placeholder tag with the instantiated web component.
2494
- el.replaceWith(newEl);
2600
+ // TODO: Only do this if we have Paths within styles?
2601
+ this.updateStyles();
2495
2602
 
2496
- // If an id pointed at the placeholder, update it to point to the new element.
2497
- let id = el.getAttribute('data-id') || el.getAttribute('id');
2498
- if (id)
2499
- delve(this.getRootNode(), id.split(/\./g), newEl);
2603
+ // Invalidate the nodes cache because we just changed it.
2604
+ this.nodesCache = null;
2500
2605
 
2606
+ }
2501
2607
 
2502
- // Update paths to use replaced element.
2503
- for (let path of this.paths) {
2504
- if (path.nodeMarker === el)
2505
- path.nodeMarker = newEl;
2506
- if (path.nodeBefore === el)
2507
- path.nodeBefore = newEl;
2508
- }
2509
- if (this.startNode === el)
2510
- this.startNode = newEl;
2511
- if (this.endNode === el)
2512
- this.endNode = newEl;
2513
-
2514
- // This is used only if inheriting from the Solarite class.
2515
- // applyComponentExprs() is called because we're rendering.
2516
- // So we want to render the sub-component also.
2517
- if (newEl.renderFirstTime)
2518
- newEl.renderFirstTime();
2519
-
2520
- // Copy attributes over.
2521
- for (let attrib of el.attributes)
2522
- if (attrib.name !== '_is' && attrib.name !== 'solarite-placeholder')
2523
- newEl.setAttribute(attrib.name, attrib.value);
2524
-
2525
- // Set dynamic attributes if they are primitive types.
2526
- for (let name in props) {
2527
- let val = props[name];
2528
- if (typeof val === 'boolean') {
2529
- if (val !== false && val !== undefined && val !== null)
2530
- newEl.setAttribute(name, '');
2531
- }
2608
+
2609
+ }
2532
2610
 
2533
- // If type is a non-boolean primitive, set the attribute value.
2534
- else if (['number', 'bigint', 'string'].includes(typeof val))
2535
- newEl.setAttribute(name, val);
2536
- }
2611
+ // TODO: Give it a better name.
2612
+ applyExprs2(exprs) {
2537
2613
 
2538
- return [newEl, attribs, children];
2539
2614
  }
2540
2615
 
2541
2616
  /**
@@ -2561,10 +2636,6 @@ class NodeGroup {
2561
2636
  return result;
2562
2637
  }
2563
2638
 
2564
- getParentNode() {
2565
- return this.startNode?.parentNode
2566
- }
2567
-
2568
2639
  /**
2569
2640
  * Get the root element of the NodeGroup's RootNodeGroup.
2570
2641
  * @returns {HTMLElement|DocumentFragment} */
@@ -2579,21 +2650,12 @@ class NodeGroup {
2579
2650
  }
2580
2651
 
2581
2652
  /**
2582
- * Requires the nodeCache to be present. */
2583
- removeAndSaveOrphans() {
2584
-
2585
- let fragment = Globals$1.doc.createDocumentFragment();
2586
- for (let node of this.getNodes())
2587
- fragment.append(node);
2588
- }
2589
-
2590
-
2591
- /**
2592
- * @param fragment {DocumentFragment}
2653
+ * Copy paths in fragment to this.paths.
2654
+ * @param fragment {DocumentFragment|HTMLElement}
2593
2655
  * @param paths
2594
2656
  * @param startingPathDepth {int} */
2595
- updatePaths(fragment, paths, startingPathDepth) {
2596
- let pathLength = paths.length;
2657
+ setPathsFromFragment(fragment, paths, startingPathDepth=0) {
2658
+ let pathLength = paths.length; // For faster iteration
2597
2659
  this.paths.length = pathLength;
2598
2660
  for (let i=0; i<pathLength; i++) {
2599
2661
  let path = paths[i].clone(fragment, startingPathDepth);
@@ -2611,34 +2673,6 @@ class NodeGroup {
2611
2673
  }
2612
2674
  }
2613
2675
 
2614
-
2615
-
2616
- findStaticComponents(root, shell, startingPathDepth=0) {
2617
- let result = [];
2618
-
2619
- // static components. These are WebComponents that do not have any constructor arguments that are expressions.
2620
- // Those are instead created by applyExpr() which calls applyComponentExprs() which calls instantiateComponent().
2621
- // Maybe someday these two paths will be merged?
2622
- // Must happen before ids because instantiateComponent will replace the element.
2623
- for (let path of shell.staticComponents) {
2624
- if (startingPathDepth)
2625
- path = path.slice(0, -startingPathDepth);
2626
- let el = resolveNodePath(root, path);
2627
-
2628
- // Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
2629
- // Recreating it is necessary so we can pass the constructor args to it.
2630
- if (root !== el/* && !isReplaceEl(root, el)*/) // TODO: is isReplaceEl necessary?
2631
- result.push(el);
2632
- }
2633
- return result;
2634
- }
2635
-
2636
- instantiateStaticComponents(staticComponents) {
2637
- // TODO: Why do we not call render() on the static component here? The tests pass either way.
2638
- for (let i in staticComponents)
2639
- staticComponents[i] = this.handleComponent(staticComponents[i], null, false);
2640
- }
2641
-
2642
2676
  /**
2643
2677
  * @param root {HTMLElement|DocumentFragment}
2644
2678
  * @param shell {Shell}
@@ -2654,7 +2688,7 @@ class NodeGroup {
2654
2688
  for (let path of shell.ids) {
2655
2689
  if (pathOffset)
2656
2690
  path = path.slice(0, -pathOffset);
2657
- let el = resolveNodePath(root, path);
2691
+ let el = Path.resolve(root, path);
2658
2692
  Util.bindId(rootEl, el);
2659
2693
  }
2660
2694
  }
@@ -2668,7 +2702,7 @@ class NodeGroup {
2668
2702
  path = path.slice(0, -pathOffset);
2669
2703
 
2670
2704
  /** @type {HTMLStyleElement} */
2671
- let style = resolveNodePath(root, path);
2705
+ let style = Path.resolve(root, path);
2672
2706
  if (rootEl.nodeType === 1) {
2673
2707
  Util.bindStyles(style, rootEl);
2674
2708
  this.styles.set(style, style.textContent);
@@ -2681,140 +2715,18 @@ class NodeGroup {
2681
2715
  for (let path of shell.scripts) {
2682
2716
  if (pathOffset)
2683
2717
  path = path.slice(0, -pathOffset);
2684
- let script = resolveNodePath(root, path);
2718
+ let script = Path.resolve(root, path);
2685
2719
  eval(script.textContent);
2686
2720
  }
2687
2721
  }
2688
2722
  }
2689
2723
  }
2690
- }
2691
-
2692
- class RootNodeGroup extends NodeGroup {
2693
-
2694
- /**
2695
- * Root node at the top of the hierarchy.
2696
- * @type {HTMLElement} */
2697
- root;
2698
-
2699
- /**
2700
- * When we call renerWatched() we re-render these expressions, then clear this to a new Map()
2701
- * @type {Map<ExprPath, ValueOp|WholeArrayOp|ArraySpliceOp[]>} */
2702
- exprsToRender = new Map();
2703
-
2704
- /**
2705
- * @param template {Template}
2706
- * @param el {?HTMLElement} Optional, pre-existing htmlElement tat will be the root.
2707
- * @param options {?object} */
2708
- constructor(template, el, options) {
2709
- super(template);
2710
-
2711
- this.options = options;
2712
-
2713
- let [fragment, shell] = this.populateFromTemplate(template);
2714
-
2715
- let startingPathDepth = 0;
2716
-
2717
-
2718
- if (fragment instanceof Text) {
2719
-
2720
- if (el) {
2721
- this.startNode = el;
2722
- this.endNode = el;
2723
- if (fragment.nodeValue.length)
2724
- el.append(fragment);
2725
- this.root = el;
2726
- }
2727
- else
2728
- throw new Error('Cannot create a standalone text node');
2729
- Globals$1.nodeGroups.set(this.root, this);
2730
- }
2731
-
2732
-
2733
- else {
2734
-
2735
- // If adding NodeGroup to an element.
2736
- if (el) {
2737
- this.root = el;
2738
-
2739
- // Save slot children
2740
- let slotChildren;
2741
- if (el.childNodes.length) {
2742
- slotChildren = Globals$1.doc.createDocumentFragment();
2743
- slotChildren.append(...el.childNodes);
2744
- }
2745
-
2746
- // If el should replace the root node of the fragment.
2747
- if (isReplaceEl(fragment, el)) {
2748
- el.append(...fragment.children[0].childNodes);
2749
-
2750
- // Copy attributes
2751
- for (let attrib of fragment.children[0].attributes)
2752
- if (!el.hasAttribute(attrib.name) && attrib.name !== 'solarite-placeholder')
2753
- el.setAttribute(attrib.name, attrib.value);
2754
-
2755
- // Go one level deeper into all of shell's paths.
2756
- startingPathDepth = 1;
2757
- }
2758
-
2759
- else {
2760
- let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
2761
- if (!isEmpty)
2762
- el.append(...fragment.childNodes);
2763
- }
2764
-
2765
- // Setup children
2766
- if (slotChildren) {
2767
-
2768
- // Named slots
2769
- for (let slot of el.querySelectorAll('slot[name]')) {
2770
- let name = slot.getAttribute('name');
2771
- if (name) {
2772
- let slotChildren2 = slotChildren.querySelectorAll(`[slot='${name}']`);
2773
- slot.append(...slotChildren2);
2774
- }
2775
- }
2776
-
2777
- // Unnamed slots
2778
- let unamedSlot = el.querySelector('slot:not([name])');
2779
- if (unamedSlot)
2780
- unamedSlot.append(slotChildren);
2781
-
2782
- // No slots
2783
- else
2784
- el.append(slotChildren);
2785
- }
2786
-
2787
- this.startNode = el;
2788
- this.endNode = el;
2789
- }
2790
-
2791
- // Instantiate as a standalone element.
2792
- else {
2793
- let singleEl = getSingleEl(fragment);
2794
- this.root = singleEl || fragment; // We return the whole fragment when calling h() with a collection of nodes.
2795
-
2796
- if (singleEl)
2797
- startingPathDepth = 1;
2798
- }
2799
- Globals$1.nodeGroups.set(this.root, this);
2800
- this.updatePaths(this.root, shell.paths, startingPathDepth);
2801
2724
 
2802
- // Static web components can sometimes have children created via expressions.
2803
- // But calling applyExprs() will mess up the shell's path to them.
2804
- // So we find them first, then call activateStaticComponents() after their children have been created.
2805
- this.staticComponents = this.findStaticComponents(this.root, shell, startingPathDepth);
2806
-
2807
- this.activateEmbeds(this.root, shell, startingPathDepth);
2808
-
2809
- // Apply exprs
2810
- this.applyExprs(template.exprs);
2725
+
2726
+ }
2811
2727
 
2812
- this.instantiateStaticComponents(this.staticComponents);
2813
- }
2814
2728
 
2815
2729
 
2816
- }
2817
- }
2818
2730
 
2819
2731
  function getSingleEl(fragment) {
2820
2732
  let nonempty = [];
@@ -2831,12 +2743,19 @@ function getSingleEl(fragment) {
2831
2743
  /**
2832
2744
  * Does the fragment have one child that's an element matching the tagname of el?
2833
2745
  * @param fragment {DocumentFragment}
2834
- * @param el {HTMLElement}
2746
+ * @param tagName {string}
2835
2747
  * @returns {boolean} */
2836
- function isReplaceEl(fragment, el) {
2748
+ function isReplaceEl(fragment, tagName) {
2837
2749
  return fragment.children.length===1
2838
- && el.tagName.includes('-') // TODO: Check for solarite-placeholder attribute instead?
2839
- && fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') === el.tagName;
2750
+ && tagName.includes('-')
2751
+ && fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') === tagName;
2752
+ }
2753
+
2754
+ class RootNodeGroup extends NodeGroup {
2755
+
2756
+ // Used only by watch.js
2757
+ exprsToRender;
2758
+
2840
2759
  }
2841
2760
 
2842
2761
  /**
@@ -2845,11 +2764,11 @@ function isReplaceEl(fragment, el) {
2845
2764
  * Although the reference to the html strings is shared among templates. */
2846
2765
  class Template {
2847
2766
 
2848
- /** @type {(Template|string|function)|(Template|string|function)[]} Evaulated expressions. */
2849
- exprs = []
2767
+ /** @type {Expr[]} Evaulated expressions. */
2768
+ 'exprs' = []
2850
2769
 
2851
2770
  /** @type {string[]} */
2852
- html = [];
2771
+ 'html' = [];
2853
2772
 
2854
2773
  /** @type {Array} Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
2855
2774
  hashedFields;
@@ -2860,8 +2779,9 @@ class Template {
2860
2779
  *
2861
2780
  * @param htmlStrings {string[]}
2862
2781
  * @param exprs {*[]} */
2863
- constructor(htmlStrings, exprs) {
2782
+ constructor(htmlStrings=[''], exprs=[]) {
2864
2783
  this.html = htmlStrings;
2784
+
2865
2785
  this.exprs = exprs;
2866
2786
 
2867
2787
  //this.trace = new Error().stack.split(/\n/g)
@@ -2876,51 +2796,50 @@ class Template {
2876
2796
  * Called by JSON.serialize when it encounters a Template.
2877
2797
  * This prevents the hashed version from being too large. */
2878
2798
  toJSON() {
2879
- if (!this.hashedFields)
2799
+ if (this.hashedFields===undefined)
2880
2800
  this.hashedFields = [getObjectId(this.html), this.exprs];
2881
2801
 
2882
2802
  return this.hashedFields
2883
2803
  }
2884
2804
 
2885
2805
  /**
2886
- * Render the main template, which may indirectly call renderTemplate() to create children.
2887
- * @param el {HTMLElement}
2806
+ * Render the main (root) template.
2807
+ * @param el {?HTMLElement} Null if we're rendering to a standalone element.
2888
2808
  * @param options {RenderOptions}
2889
2809
  * @return {?DocumentFragment|HTMLElement} */
2890
- render(el=null, options={}) {
2891
- let ng;
2892
- let standalone = !el;
2893
- let firstTime = false;
2894
-
2895
- // Rendering a standalone element.
2896
- // TODO: figure out when to not use RootNodeGroup
2897
- if (standalone) {
2898
- ng = new RootNodeGroup(this, null, options);
2899
- el = ng.getRootNode();
2900
- Globals$1.nodeGroups.set(el, ng); // Why was this commented out?
2901
- firstTime = true;
2810
+ 'render'(el=null, options={}) {
2811
+
2812
+
2813
+
2814
+ let ng = el && Globals$1.rootNodeGroups.get(el);
2815
+ if (!ng) {
2816
+ ng = new RootNodeGroup(this, null, el, options);
2817
+ if (!el) // null if it's a standalone elment.
2818
+ el = ng.getRootNode();
2819
+ Globals$1.rootNodeGroups.set(el, ng); // All tests still pass if this is commented out!
2902
2820
  }
2903
- else {
2904
- ng = Globals$1.nodeGroups.get(el);
2905
- if (!ng) {
2906
- ng = new RootNodeGroup(this, el, options);
2907
- Globals$1.nodeGroups.set(el, ng); // Why was this commented out?
2908
- firstTime = true;
2909
- }
2910
2821
 
2911
- // This can happen if we try manually rendering one template to a NodeGroup that was created expecting a different template.
2912
- // These don't always have the same length, for example if one attribute has multiple expressions.
2913
- if (ng.paths.length === 0 && this.exprs.length || ng.paths.length > this.exprs.length)
2914
- throw new Error(`Solarite Error: Parent HTMLElement ${ng.template.html.join('${...}')} and ${ng.paths.length} \${value} placeholders can't accomodate a Template with ${this.exprs.length} values.`); }
2822
+ // Make sure the expresion count matches match the Path "hole" count.
2823
+ // This can happen if we try manually rendering one template to a NodeGroup that was created expecting a different template.
2824
+ // These don't always have the same length, for example if one attribute has multiple expressions.
2825
+ // if (ng.paths.length === 0 && this.exprs.length || ng.paths.length > this.exprs.length)
2826
+ // throw new Error(
2827
+ // `Solarite Error: Parent HTMLElement ${ng.template.html.join('${...}')} and ${ng.paths.length} \${value} ` +
2828
+ // `placeholders can't accomodate a Template with ${this.exprs.length} values.`);
2915
2829
 
2916
2830
  // Creating the root nodegroup also renders it.
2917
2831
  // If we didn't just create it, we need to render it.
2918
- if (!firstTime) {
2919
- if (this.html?.length === 1 && !this.html[0])
2920
- el.innerHTML = ''; // Fast path for empty component.
2921
- else {
2922
- ng.applyExprs(this.exprs);
2923
- }
2832
+ if (this.html?.length === 1 && !this.html[0]) // An empty string.
2833
+ el.innerHTML = ''; // Fast path for empty component.
2834
+ else {
2835
+
2836
+ let oldKey = ng.exactKey;
2837
+ let newKey = this.getExactKey();
2838
+ ng.applyExprs(this.exprs, oldKey !== newKey);
2839
+ ng.exactKey = newKey;
2840
+
2841
+ //if (firstTime)
2842
+ // ng.instantiateStaticComponents(ng.staticComponents);
2924
2843
  }
2925
2844
 
2926
2845
  ng.exprsToRender = new Map();
@@ -2928,7 +2847,7 @@ class Template {
2928
2847
  }
2929
2848
 
2930
2849
  getExactKey() {
2931
- if (!this.exactKey) {
2850
+ if (this.exactKey===undefined) {
2932
2851
  if (this.exprs.length)
2933
2852
  this.exactKey = getObjectHash(this);// calls this.toJSON().
2934
2853
  else // Don't hash plain html.
@@ -2939,7 +2858,7 @@ class Template {
2939
2858
 
2940
2859
  getCloseKey() {
2941
2860
  //console.log(this.exprs.length)
2942
- if (!this.closeKey) {
2861
+ if (this.closeKey===undefined) {
2943
2862
  if (this.exprs.length)
2944
2863
  this.closeKey = /*'@' + */this.toJSON()[0];
2945
2864
  else
@@ -3100,11 +3019,11 @@ const addChild = (template, html, exprs) => {
3100
3019
  /**
3101
3020
  * Convert a template, string, or object into a DOM Node or Element
3102
3021
  *
3103
- * 1. h('Hello'); // Create single text node.
3104
- * 2. h('<b>Hello</b>'); // Create single HTMLElement
3105
- * 3. h('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
3106
- * 4. h(template) // Render Template created by h`<html>` or h();
3107
- * 5. h({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
3022
+ * 1. toEl('Hello'); // Create single text node.
3023
+ * 2. toEl('<b>Hello</b>'); // Create single HTMLElement
3024
+ * 3. toEl('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
3025
+ * 4. toEl(template) // Render Template created by h`<html>` or h();
3026
+ * 5. toEl({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
3108
3027
  * @param arg {string|Template|{render:()=>void}}
3109
3028
  * @returns {Node|DocumentFragment|HTMLElement} */
3110
3029
  function toEl(arg) {
@@ -3113,7 +3032,7 @@ function toEl(arg) {
3113
3032
  let html = arg;
3114
3033
 
3115
3034
  // If it's an element with whitespace before or after it, trim both ends.
3116
- if (html.match(/^\s^</) || html.match(/>\s+$/))
3035
+ if (html.match(/^\s^<\S+/) || html.match(/\S+>\s+$/))
3117
3036
  html = html.trim();
3118
3037
 
3119
3038
  // We create a new one each time because otherwise
@@ -3171,7 +3090,6 @@ function toEl(arg) {
3171
3090
  }
3172
3091
 
3173
3092
  throw new Error('toEl() does not support argument of type: ' + (arg ? typeof arg : arg));
3174
-
3175
3093
  }
3176
3094
 
3177
3095
 
@@ -3184,7 +3102,7 @@ let renderF = 'render';
3184
3102
  * Using h() as a function() will always create a DOM element.
3185
3103
  *
3186
3104
  * Features beyond what standard js tagged template strings do:
3187
- * 1. r`` sub-expressions
3105
+ * 1. h`` sub-expressions
3188
3106
  * 2. functions, nodes, and arrays of nodes as sub-expressions.
3189
3107
  * 3. html-escape all expressions by default, unless wrapped in h()
3190
3108
  * 4. event binding
@@ -3202,7 +3120,7 @@ let renderF = 'render';
3202
3120
  *
3203
3121
  * Add children to an element.
3204
3122
  * 3. h(el, h`<b>${'Hi'}</b>`, ?options)
3205
- * 4. h(el, ?options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
3123
+ * 4. h(el, ?options)`<b>${'Hi'}</b>` // typical path used in render(). Create template and render its nodes to el.
3206
3124
  *
3207
3125
  * Create top-level element
3208
3126
  * 5. h()`Hello<b>${'World'}!</b>`
@@ -3210,10 +3128,10 @@ let renderF = 'render';
3210
3128
  * 6. h(string, object, ...) // Used for JSX
3211
3129
  * @param htmlStrings {?HTMLElement|string|string[]|function():Template|{render:function()}}
3212
3130
  * @param exprs {*[]|string|Template|Object}
3213
- * @return {Node|HTMLElement|Template} */
3131
+ * @return {Node|HTMLElement|Template|Function} */
3214
3132
  function h(htmlStrings=undefined, ...exprs) {
3215
3133
 
3216
- // 1. Tagged template
3134
+ // 1. Tagged template: h`<div>...</div>`
3217
3135
  if (Array.isArray(arguments[0])) {
3218
3136
  return new Template(arguments[0], exprs);
3219
3137
  }
@@ -3231,11 +3149,10 @@ function h(htmlStrings=undefined, ...exprs) {
3231
3149
  return Template.fromJsx(tag, props, children);
3232
3150
  }
3233
3151
 
3234
- // 2b. Plain html string => template
3152
+ // 2b. Plain html string => template: h('<div>...</div>')
3235
3153
  else {
3236
3154
  let html = tagOrHtml;
3237
- // If it starts with whitespace, trim both ends.
3238
- // TODO: Also trim if it ends with whitespace?
3155
+ // If it starts with whitespace and then a tag, trim it.
3239
3156
  if (html.match(/^\s^</))
3240
3157
  html = html.trim();
3241
3158
  return new Template([html], []);
@@ -3244,44 +3161,43 @@ function h(htmlStrings=undefined, ...exprs) {
3244
3161
 
3245
3162
  else if (arguments[0] instanceof HTMLElement || arguments[0] instanceof DocumentFragment) {
3246
3163
 
3247
- // 3. Render template to element.
3164
+ // 3. Render template to element: h(el, template)
3248
3165
  if (arguments[1] instanceof Template) {
3249
3166
 
3250
3167
  /** @type Template */
3251
3168
  let template = arguments[1];
3252
3169
  let parent = arguments[0];
3253
- let options = arguments[2]; // deprecated?
3170
+ let options = arguments[2];
3254
3171
  template.render(parent, options);
3255
3172
  }
3256
3173
 
3257
- // 4. Render tagged template to element
3174
+ // 4. Render tagged template to element: h(el)`<div>...</div>`
3258
3175
  else {
3259
3176
  let parent = arguments[0], options = arguments[1];
3260
3177
 
3261
- // Remove shadowroot. TODO: This could mess up paths?
3178
+ // Remove shadowroot if present. TODO: This could mess up paths?
3262
3179
  if (parent.shadowRoot)
3263
3180
  parent.innerHTML = '';
3264
3181
 
3265
3182
  // Return a tagged template function that applies the tagged template to parent.
3266
- let taggedTemplate = (htmlStrings, ...exprs) => {
3183
+ let renderTemplate = (htmlStrings, ...exprs) => {
3267
3184
  Globals$1.rendered.add(parent);
3268
3185
  let template = new Template(htmlStrings, exprs);
3269
3186
  return template.render(parent, options);
3270
3187
  };
3271
- return taggedTemplate;
3188
+ return renderTemplate;
3272
3189
  }
3273
3190
  }
3274
3191
 
3275
- // 5. Create a static element h()'<div></div>' (Deprecated?)
3192
+ // 5. Create a static element: h()`<div></div>`
3276
3193
  else if (!arguments.length) {
3277
3194
  return (htmlStrings, ...exprs) => {
3278
- let template = h(htmlStrings, ...exprs);
3279
- return toEl(template); // Go to path 6.
3280
- }
3195
+ let template = h(htmlStrings, ...exprs);
3196
+ return toEl(template);
3197
+ }
3281
3198
  }
3282
3199
 
3283
- // 6. Help toEl() with objects.
3284
- // Special rebound render path, called by normal path.
3200
+ // 6. Help toEl() with objects: h(this)`<div>...</div>` inside an object's render()
3285
3201
  // Intercepts the main h(this)`...` function call inside render().
3286
3202
  // TODO: This path doesn't handle embeds like data-id="..."
3287
3203
  else if (typeof arguments[0] === 'object' && Globals$1.objToEl.has(arguments[0])) {
@@ -3297,7 +3213,7 @@ function h(htmlStrings=undefined, ...exprs) {
3297
3213
  Globals$1.objToEl.set(obj, el);
3298
3214
  }
3299
3215
 
3300
- // h(this)`<div>...</div>
3216
+ // h(this)`<div>...</div>`
3301
3217
  else
3302
3218
  return function(...args) {
3303
3219
  let template = h(...args);
@@ -3305,15 +3221,20 @@ function h(htmlStrings=undefined, ...exprs) {
3305
3221
  Globals$1.objToEl.set(obj, el);
3306
3222
  }.bind(obj);
3307
3223
  }
3224
+ // TODO: Handle other primitive types?
3225
+ else if (Util.isFalsy(arguments[0]))
3226
+ return new Template();
3227
+
3308
3228
  else
3309
3229
  throw new Error('h() does not support argument of type: ' + (arguments[0] ? typeof arguments[0] : arguments[0]))
3310
3230
  }
3311
3231
 
3312
3232
  /**
3233
+ * @deprecated Inherit from Solarite and pass arribs to super() instead.
3313
3234
  * There are three ways to create an instance of a Solarite Component:
3314
- * 1. new ComponentName(); // direct class instantiation
3315
- * 2. this.html = r`<div><component-name></component-name></div>; // as a child of another RedComponent.
3316
- * 3. <body><component-name></component-name></body> // in the Document html.
3235
+ * 1. new ComponentName(3); // direct class instantiation
3236
+ * 2. h(this)`<div><component-name user-id=${3}></component-name></div>; // as a child of another Component.
3237
+ * 3. <body><component-name user-id="3"></component-name></body> // in the Document html.
3317
3238
  *
3318
3239
  * When created via #3, Solarite has no way to pass attributes as arguments to the constructor. So to make
3319
3240
  * sure we get the correct value via all three paths, we write our constructors according to the following
@@ -3321,40 +3242,38 @@ function h(htmlStrings=undefined, ...exprs) {
3321
3242
  * Browsers make all html attribute names lowercase.
3322
3243
  *
3323
3244
  * @example
3324
- * constructor({name, userid=1}={}) {
3245
+ * constructor({name, userId=1}={}) {
3325
3246
  * super();
3326
3247
  *
3327
3248
  * // Get value from "name" attriute if persent, otherwise from name constructor arg.
3328
3249
  * this.name = getArg(this, 'name', name);
3329
3250
  *
3330
3251
  * // Optionally convert the value to an integer.
3331
- * this.userId = getArg(this, 'userid', userid, ArgType.Int);
3252
+ * this.userId = getArg(this, 'user-id', userId, ArgType.Int);
3332
3253
  * }
3333
3254
  *
3334
3255
  * @param el {HTMLElement}
3335
3256
  * @param attributeName {string} Attribute name. Not case-sensitive.
3336
- * @param defaultValue {*} Default value to use if attribute doesn't exist.
3257
+ * @param defaultValue {*} Default value to use if attribute doesn't exist. Typically the argument from the constructor.
3337
3258
  * @param type {ArgType|function|Class|*[]}
3338
3259
  * If an array, use the value if it's in the array, otherwise return undefined.
3339
3260
  * If it's a function, pass the value to the function and return the result.
3340
- * @param fallback {*} If the defaultValue is undefiend and type can't be parsed as the given type, use this value.
3341
- * TODO: Should this be merged with the defaultValue argument?
3342
- * @return {*} Undefined if attribute isn't set. */
3343
- function getArg(el, attributeName, defaultValue=undefined, type=ArgType.String, fallback=undefined) {
3261
+ * @return {*} Undefined if attribute isn't set and there's no defaultValue, or if the value couldn't be parsed as the type. */
3262
+ function getArg(el, attributeName, defaultValue=undefined, type=ArgType.String) {
3344
3263
  let val = defaultValue;
3345
3264
  let attrVal = el.getAttribute(attributeName) || el.getAttribute(Util.camelToDashes(attributeName));
3346
3265
  if (attrVal !== null) // If attribute doesn't exist.
3347
3266
  val = attrVal;
3348
-
3267
+
3349
3268
  if (Array.isArray(type))
3350
- return type.includes(val) ? val : fallback;
3351
-
3269
+ return type.includes(val) ? val : undefined;
3270
+
3352
3271
  if (typeof type === 'function') {
3353
3272
  return type.constructor
3354
3273
  ? new type(val) // arg type is custom Class
3355
3274
  : type(val); // arg type is custom function
3356
3275
  }
3357
-
3276
+
3358
3277
  // If bool, it's true as long as it exists and its value isn't falsey.
3359
3278
  if (type===ArgType.Bool) {
3360
3279
  let lAttrVal = typeof val === 'string' ? val.toLowerCase() : val;
@@ -3362,20 +3281,17 @@ function getArg(el, attributeName, defaultValue=undefined, type=ArgType.String,
3362
3281
  return false;
3363
3282
  if (['true', true].includes(lAttrVal) || parseFloat(lAttrVal) !== 0)
3364
3283
  return true;
3365
- return fallback;
3284
+ return undefined;
3366
3285
  }
3367
-
3286
+
3368
3287
  // Attribute doesn't exist
3369
- let result;
3370
3288
  switch (type) {
3371
3289
  case ArgType.Int:
3372
- result = parseInt(val);
3373
- return isNaN(result) ? fallback : result;
3290
+ return parseInt(val);
3374
3291
  case ArgType.Float:
3375
- result = parseFloat(val);
3376
- return isNaN(result) ? fallback : result;
3292
+ return parseFloat(val);
3377
3293
  case ArgType.String:
3378
- return [undefined, null, false].includes(val) ? '' : val+'';
3294
+ return [undefined, null, false].includes(val) ? '' : (val+'');
3379
3295
  case ArgType.Json:
3380
3296
  case ArgType.Eval:
3381
3297
  if (typeof val === 'string' && val.length)
@@ -3397,6 +3313,7 @@ function getArg(el, attributeName, defaultValue=undefined, type=ArgType.String,
3397
3313
 
3398
3314
 
3399
3315
  /**
3316
+ * @deprecated for Solarite.getAttribs()
3400
3317
  * Experimental. Set multiple arguments/attributes all at once.
3401
3318
  * @param el {HTMLElement}
3402
3319
  * @param args {Record<string, any>}
@@ -3405,6 +3322,10 @@ function getArg(el, attributeName, defaultValue=undefined, type=ArgType.String,
3405
3322
  * @example
3406
3323
  * constructor({user, path}={}) {
3407
3324
  * setArgs(this, arguments[0], {user: User, path: ArgType.String});
3325
+ *
3326
+ * // Equivalent to:
3327
+ * this.user = getArg(this, user, 'user', User); // or new User(user);
3328
+ * this.path = getArg(this, path, 'path', ArgType.String);
3408
3329
  * }
3409
3330
  */
3410
3331
  function setArgs(el, args, types) {
@@ -3414,15 +3335,16 @@ function setArgs(el, args, types) {
3414
3335
 
3415
3336
 
3416
3337
  /**
3338
+ * @deprecated
3417
3339
  * @enum */
3418
3340
  var ArgType = {
3419
-
3341
+
3420
3342
  /**
3421
3343
  * false, 0, null, undefined, '0', and 'false' (case-insensitive) become false.
3422
3344
  * Anything else, including empty string becomes true.
3423
3345
  * Empty string is true because attributes with no value should be evaulated as true. */
3424
3346
  Bool: 'Bool',
3425
-
3347
+
3426
3348
  Int: 'Int',
3427
3349
  Float: 'Float',
3428
3350
  String: 'String',
@@ -3441,177 +3363,120 @@ var ArgType = {
3441
3363
  Eval: 'Eval'
3442
3364
  };
3443
3365
 
3444
- function defineClass(Class, tagName, extendsTag) {
3445
- if (!customElements[getName](Class)) { // If not previously defined.
3446
- tagName = tagName || Util.camelToDashes(Class.name);
3447
- if (!tagName.includes('-'))
3448
- tagName += '-element';
3366
+ /*
3367
+ ┏┓ ┓ •
3368
+ ┗┓┏┓┃┏┓┏┓┓╋▗▖
3369
+ ┗┛┗┛┗┗┻╹ ╹╹┗
3370
+ JavasCript UI library
3371
+ @license MIT
3372
+ @copyright Vorticode LLC
3373
+ https://vorticode.github.io/solarite/ */
3374
+ function t(html) {
3375
+ return new Template([html], []);
3376
+ }
3449
3377
 
3450
- let options = null;
3451
- if (extendsTag)
3452
- options = {extends: extendsTag};
3378
+ /**
3379
+ * Intercept the construct call to auto-define the class before the constructor is called. */
3380
+ let HTMLElementAutoDefine = new Proxy(HTMLElement, {
3381
+ construct(Parent, args, Class) {
3382
+
3383
+ // 1. Call customElements.define() automatically.
3384
+ Util.defineClass(Class);
3453
3385
 
3454
- customElements[define](tagName, Class, options);
3386
+ // 2. This line is equivalent the to super() call to HTMLElement:
3387
+ return Reflect.construct(Parent, args, Class);
3455
3388
  }
3456
- }
3389
+ });
3457
3390
 
3458
3391
  /**
3459
- * Create a version of the Solarite class that extends from the given tag name.
3460
- * Reasons to inherit from this instead of HTMLElement. None of these are all that useful.
3392
+ * Solarite provides more features if your web component extends Solarite instead of HTMLElement.
3393
+ *
3394
+ * Reasons to inherit from Solarite instead of HTMLElement.
3461
3395
  * 1. customElements.define() is called automatically when you create the first instance.
3462
3396
  * 2. Calls render() when added to the DOM, if it hasn't been called already.
3463
- * 3. Child elements are added before constructor is called. But they're also passed to the constructor. (deprecated?)
3464
- * 4. We can use this.html = r`...` to set html. (deprecated)
3465
- * 5. We have the onConnect, onFirstConnect, and onDisconnect methods.
3466
- * Can't figure out how to have these work standalone though, and still be synchronous.
3467
- * 6. Can we extend from other element types like TR?
3468
- * 7. Shows default text if render() function isn't defined.
3397
+ * 3. Populates the attribs argument to the constructor when instantiated from regular html outside a template string.
3398
+ * It parses JSON from DOM attribute values surrouned with '${...}'
3399
+ * 4. Shows an error if render() isn't defined.
3469
3400
  *
3470
3401
  * Advantages to inheriting from HTMLElement
3471
- * 1. Minimization won't break when it renames the Class and we call customElements.define() on the wrong name.
3472
- * 2. We can inherit from things like HTMLTableRowElement directly.
3473
- * 3. There's less magic, since everyone is familiar with defining custom elements.
3474
- *
3475
- * @param extendsTag {?string}
3476
- * @return {Class} */
3477
- function createSolarite(extendsTag=null) {
3478
-
3479
- let BaseClass = HTMLElement;
3480
- if (extendsTag && !extendsTag.includes('-')) {
3481
- extendsTag = extendsTag.toLowerCase();
3482
-
3483
- BaseClass = Globals$1.elementClasses[extendsTag];
3484
- if (!BaseClass) { // TODO: Use Cache
3485
- BaseClass = Globals$1.doc.createElement(extendsTag).constructor;
3486
- Globals$1.elementClasses[extendsTag] = BaseClass;
3487
- }
3488
- }
3489
-
3490
- /**
3491
- * Intercept the construct call to auto-define the class before the constructor is called.
3492
- * @type {HTMLElement} */
3493
- let HTMLElementAutoDefine = new Proxy(BaseClass, {
3494
- construct(Parent, args, Class) {
3495
- defineClass(Class, null, extendsTag);
3496
-
3497
- // This is a good place to manipulate any args before they're sent to the constructor.
3498
- // Such as loading them from attributes, if I could find a way to do so.
3499
-
3500
- // This line is equivalent the to super() call.
3501
- return Reflect.construct(Parent, args, Class);
3502
- }
3503
- });
3504
-
3505
- return class Solarite extends HTMLElementAutoDefine {
3506
-
3507
-
3508
- /**
3509
- * TODO: Make these standalone functions.
3510
- * Callbacks.
3511
- * Use onConnect.push(() => ...); to add new callbacks. */
3512
- onConnect;
3513
-
3514
- onFirstConnect;
3515
- onDisconnect;
3516
-
3517
- /**
3518
- * @param options {RenderOptions} */
3519
- constructor(options={}) {
3520
- super();
3521
-
3522
- // TODO: Is options.render ever used?
3523
- if (options.render===true)
3524
- this.render();
3525
-
3526
- else if (options.render===false)
3527
- Globals$1.rendered.add(this); // Don't render on connectedCallback()
3528
-
3529
- // Add slot children before constructor code executes.
3530
- // This breaks the styleStaticNested test.
3531
- // PendingChildren is setup in NodeGroup.instantiateComponent()
3532
- // TODO: Match named slots.
3533
- //let ch = Globals.pendingChildren.pop();
3534
- //if (ch) // TODO: how could there be a slot before render is called?
3535
- // (this.querySelector('slot') || this).append(...ch);
3536
-
3537
- /** @deprecated
3538
- Object.defineProperty(this, 'html', {
3539
- set(html) {
3540
- Globals.rendered.add(this);
3541
- if (typeof html === 'string') {
3542
- console.warn("Assigning to this.html without the r template prefix.")
3543
- this.innerHTML = html;
3544
- }
3545
- else
3546
- this.modifications = h(this, html, options);
3402
+ * 1. We can inherit from things like HTMLTableRowElement directly.
3403
+ * 2. There's less magic, since everyone is familiar with defining custom elements.
3404
+ * 3. No confusion about how the class name becomes a tag name.
3405
+ * @extends {HTMLElement} */
3406
+ class Solarite extends HTMLElementAutoDefine {
3407
+
3408
+ /**
3409
+ * @param attribs {?Record<string, any>} */
3410
+ constructor(attribs=null) {
3411
+ super();
3412
+
3413
+ if (attribs) {
3414
+ if (typeof attribs !== 'object')
3415
+ throw new Error('First argument to custom element constructor must be an object.');
3416
+
3417
+ // 1. Populate attribs if it's an empty object.
3418
+ if (attribs && !Object.keys(attribs).length) {
3419
+ let attribs2 = Solarite.getAttribs(this);
3420
+ for (let name in attribs2) {
3421
+ attribs[name] = attribs2[name];
3547
3422
  }
3548
- })*/
3423
+ }
3549
3424
 
3550
- /*
3551
- let pthis = new Proxy(this, {
3552
- get(obj, prop) {
3553
- return Reflect.get(obj, prop)
3554
- }
3555
- });
3556
- this.render = this.render.bind(pthis);
3557
- */
3425
+ // 2. Populate fields from attribs.
3426
+ // This does nothing because the fields are overwritten by the child class after this super() constructor executes.
3427
+ //for (let name in attribs || {}) {
3428
+ // if (name in this) {
3429
+ // const descriptor = Object.getOwnPropertyDescriptor(this, name);
3430
+ // if (!descriptor || descriptor.writable || descriptor.set)
3431
+ // this[name] = attribs[name];
3432
+ // }
3433
+ //}
3558
3434
  }
3559
3435
 
3560
- /**
3561
- * Call render() only if it hasn't already been called. */
3562
- renderFirstTime() {
3563
- if (!Globals$1.rendered.has(this) && this.render)
3564
- this.render();
3565
- }
3566
-
3567
- /**
3568
- * Called automatically by the browser. */
3569
- connectedCallback() {
3570
- this.renderFirstTime();
3571
- if (!Globals$1.connected.has(this)) {
3572
- Globals$1.connected.add(this);
3573
- if (this.onFirstConnect)
3574
- this.onFirstConnect();
3575
- }
3576
- if (this.onConnect)
3577
- this.onConnect();
3578
- }
3579
-
3580
- disconnectedCallback() {
3581
- if (this.onDisconnect)
3582
- this.onDisconnect();
3583
- }
3436
+ // 3. Wrap render function so it always provides the attribs argument.
3437
+ // Disabled because this gives us strings for attribute values when we call render manually.
3438
+ // Instead of values given from ${...} expressions.
3439
+ // let originalRender = this.render;
3440
+ // this.render = (attribs, changed=true) => {
3441
+ // if (!attribs) // If we have to look up the attribs, we don't know if they changed or not.
3442
+ // attribs = Solarite.getAttribs(this);
3443
+ // originalRender.call(this, attribs, changed);
3444
+ // }
3445
+ }
3584
3446
 
3447
+ 'render'() {
3448
+ throw new Error('render() is not defined for ' + this.constructor.name);
3449
+ }
3585
3450
 
3586
- static define(tagName=null) {
3587
- defineClass(this, tagName, extendsTag);
3451
+ /**
3452
+ * Call render() only if it hasn't already been called. */
3453
+ 'renderFirstTime'() {
3454
+ if (!Globals$1.rendered.has(this)) {
3455
+ let attribs = Solarite.getAttribs(this);
3456
+ this.render(attribs); // calls Globals.rendered.add(this); inside the call to h()'...'.
3588
3457
  }
3589
3458
  }
3590
- }
3591
3459
 
3592
- // Trick to prevent minifier from renaming this method.
3593
- let define = 'define';
3594
- let getName = 'getName';
3595
-
3596
- /*
3597
- ┏┓ ┓ •
3598
- ┗┓┏┓┃┏┓┏┓┓╋▗▖
3599
- ┗┛┗┛┗┗┻╹ ╹╹┗
3600
- JavasCript UI library
3601
- @license MIT
3602
- @copyright Vorticode LLC
3603
- https://vorticode.github.io/solarite/ */
3460
+ /**
3461
+ * Called automatically by the browser. */
3462
+ 'connectedCallback'() { // quoted so terser doesn't remove it.
3463
+ this.renderFirstTime();
3464
+ }
3604
3465
 
3605
- /**
3606
- * TODO: The Proxy and the multiple base classes mess up 'instanceof Solarite'
3607
- * @type {Node|Class<HTMLElement>|function(tagName:string):Node|Class<HTMLElement>} */
3608
- const Solarite = new Proxy(createSolarite(), {
3609
- apply(self, _, args) {
3610
- return createSolarite(...args)
3466
+ static 'define'(tagName=null) {
3467
+ Util.defineClass(this, tagName);
3611
3468
  }
3612
- });
3613
3469
 
3614
- //export {default as watch, renderWatched} from './watch.js'; // unfinished
3470
+ static 'getAttribs'(el) {
3471
+ let result = Util.attribsToObject(el);
3472
+ for (let name in result) {
3473
+ let val = result[name];
3474
+ if (val.startsWith('${') && val.endsWith('}'))
3475
+ result[name] = JSON.parse(val.slice(2, -1));
3476
+ }
3477
+ return result;
3478
+ }
3479
+ }
3615
3480
 
3616
3481
  export default h;
3617
- export { ArgType, Globals$1 as Globals, Solarite, Util as SolariteUtil, Template, getArg, h, h as r, setArgs, toEl };
3482
+ export { ArgType, Globals$1 as Globals, HtmlParser, NodeGroup, Shell, Solarite, Util as SolariteUtil, Template, delve, getArg, h, h as r, setArgs, t, toEl };