solarite 0.3.2 → 0.5.0

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