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