solarite 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/benchmarks/naive/Solarite.min.js +4 -0
  2. package/benchmarks/naive/index.html +14 -0
  3. package/benchmarks/naive/main.js +339 -0
  4. package/benchmarks/naive/package-lock.json +13 -0
  5. package/benchmarks/naive/package.json +23 -0
  6. package/benchmarks/readme.md +48 -0
  7. package/build/build.bat +2 -2
  8. package/build/build.js +1 -1
  9. package/dist/Solarite-debug.js +1344 -1532
  10. package/dist/Solarite.js +1298 -1334
  11. package/dist/Solarite.min.js +3 -3
  12. package/dist/udomdiff-license.txt +18 -0
  13. package/docs/index.md +440 -130
  14. package/docs/js/Playground.js +1 -1
  15. package/docs/js/codemirror/codemirror6.js +3683 -3206
  16. package/docs/js/codemirror/themeSolarIce.js +1 -1
  17. package/docs/js/documentation.js +1 -1
  18. package/docs/js/ui/CodeEditor.js +183 -45
  19. package/docs/js/ui/FlexResizer.js +14 -3
  20. package/docs/js/util/Errors.js +11 -0
  21. package/docs/media/documentation.css +6 -3
  22. package/index.html +449 -30
  23. package/package.json +1 -1
  24. package/readme.md +1 -1
  25. package/src/solarite/ExprPath.js +349 -151
  26. package/src/solarite/Globals.js +43 -1
  27. package/src/solarite/NodeGroup.js +275 -293
  28. package/src/solarite/Shell.js +26 -28
  29. package/src/solarite/Solarite.js +12 -0
  30. package/src/solarite/Template.js +55 -128
  31. package/src/solarite/Util.js +131 -7
  32. package/src/solarite/createSolarite.js +14 -19
  33. package/src/solarite/getArg.js +1 -1
  34. package/src/solarite/hash.js +18 -35
  35. package/src/solarite/r.js +127 -127
  36. package/src/solarite/watch3.js +18 -11
  37. package/src/{solarite → unused}/NodeGroupManager.js +81 -109
  38. package/src/{solarite → unused}/watch.js +1 -1
  39. package/src/{solarite → unused}/watch2.js +1 -2
  40. package/src/{solarite → util}/MultiValueMap.js +18 -11
  41. package/src/util/WeakArray.js +33 -0
  42. package/tests/Solarite.test.js +862 -167
  43. package/tests/index.html +4 -2
  44. package/tests/run.bat +1 -1
  45. package/deno.lock +0 -176
package/dist/Solarite.js CHANGED
@@ -128,7 +128,7 @@ let delveDontCreate = {};
128
128
  /**
129
129
  * There are three ways to create an instance of a Solarite Component:
130
130
  * 1. new ComponentName(); // direct class instantiation
131
- * 2. this.html = r`<div><component-name></component-name></div>; // as a child of another RedComponent.
131
+ * 2. this.html = r`<div><component-name></component-name></div>; // as a child of another Component.
132
132
  * 3. <body><component-name></component-name></body> // in the Document html.
133
133
  *
134
134
  * When created via #3, Solarite has no way to pass attributes as arguments to the constructor. So to make
@@ -226,22 +226,14 @@ let objectIds = new WeakMap();
226
226
 
227
227
  /**
228
228
  * @param obj {Object|string|Node}
229
- * @param prefix
230
229
  * @returns {string} */
231
- function getObjectId(obj, prefix=null) {
232
-
233
-
234
-
235
-
236
-
237
- prefix = prefix || '~\f';
238
-
230
+ function getObjectId(obj) {
239
231
  // if (typeof obj === 'function')
240
- // return obj.toString();
232
+ // return obj.toString(); // This fails to detect when a function's bound variables changes.
241
233
 
242
234
  let result = objectIds.get(obj);
243
235
  if (!result) { // convert to string, store in result, then add 1 to lastObjectId.
244
- result = prefix+(lastObjectId++); // We use a unique prefix to ensure it doesn't collide w/ strings not from getObjectId()
236
+ result = (lastObjectId++); // We use a unique prefix to ensure it doesn't collide w/ strings not from getObjectId()
245
237
  objectIds.set(obj, result);
246
238
  }
247
239
  return result;
@@ -254,12 +246,20 @@ function getObjectId(obj, prefix=null) {
254
246
  * Adding a toJSON method globally on these object prototypes doesn't incur that performance penalty. */
255
247
  let isHashing = true;
256
248
  function toJSON() {
257
- //return (isHashing && !Array.isArray(this)) ? getObjectId(this) : this
258
249
  return isHashing ? getObjectId(this) : this
259
250
  }
251
+
252
+
260
253
  // Node.prototype.toJSON = toJSON;
261
254
  // Function.prototype.toJSON = toJSON;
262
-
255
+ // Sometimes these get unassigned by Chrome and Brave 119, as well as Firefox, seemingly randomly!
256
+ // The same tests sometimes pass, sometimes fail, even after browser and OS restarts.
257
+ // So we check the assignments on every run of getObjectHash()
258
+ if (Node.prototype.toJSON !== toJSON) {
259
+ Node.prototype.toJSON = toJSON;
260
+ if (Function.prototype.toJSON !== toJSON) // Will it only unmap one but not the other?
261
+ Function.prototype.toJSON = toJSON;
262
+ }
263
263
 
264
264
  /**
265
265
  * Get a string that uniquely maps to the values of the given object.
@@ -268,25 +268,18 @@ function toJSON() {
268
268
  *
269
269
  * Relies on the Node and Function prototypes being overridden above.
270
270
  *
271
+ * Note that passing an integer may collide with the number we get from hashing an object.
272
+ * But we don't handle that case because we need max performance and Solarite never passes integers to this function.
273
+ *
271
274
  * @param obj {*}
272
275
  * @returns {string} */
273
276
  function getObjectHash(obj) {
274
-
275
- // Sometimes these get unassigned by Chrome and Brave 119, as well as Firefox, seemingly randomly!
276
- // The same tests sometimes pass, sometimes fail, even after browser and OS restarts.
277
- // So we check the assignments on every run of getObjectHash()
278
- if (Node.prototype.toJSON !== toJSON) {
279
- Node.prototype.toJSON = toJSON;
280
- if (Function.prototype.toJSON !== toJSON) // Will it only unmap one but not the other?
281
- Function.prototype.toJSON = toJSON;
282
- }
283
-
284
277
  let result;
285
278
  isHashing = true;
286
279
  try {
287
280
  result = JSON.stringify(obj);
288
281
  }
289
- catch(e){
282
+ catch(e) {
290
283
  result = getObjectHashCircular(obj);
291
284
  }
292
285
  isHashing = false;
@@ -294,7 +287,7 @@ function getObjectHash(obj) {
294
287
  }
295
288
 
296
289
  /**
297
- * Having this separate might help the optimzer for getObjectHash() ?
290
+ * Slower hashing method that supports.
298
291
  * @param obj
299
292
  * @returns {string} */
300
293
  function getObjectHashCircular(obj) {
@@ -313,77 +306,59 @@ function getObjectHashCircular(obj) {
313
306
  });
314
307
  }
315
308
 
316
- class MultiValueMap {
309
+
310
+
311
+ var Globals = {
317
312
 
318
- /** @type {Object<string, Set>} */
319
- data = {};
313
+ /**
314
+ * Used by NodeGroup.applyComponentExprs() */
315
+ componentHash: new WeakMap(),
320
316
 
321
- // Set a new value for a key
322
- add(key, value) {
323
- let data = this.data;
324
- let set = data[key];
325
- if (!set) {
326
- set = new Set();
327
- data[key] = set;
328
- }
329
- set.add(value);
330
- }
317
+ /**
318
+ * Store which instances of Solarite have already been added to the DOM.
319
+ * @type {WeakSet<HTMLElement>} */
320
+ connected: new WeakSet(),
331
321
 
332
- // Get all values for a key
333
- getAll(key) {
334
- return this.data[key] || [];
335
- }
322
+ /**
323
+ * Elements that have been rendered to by r() at least once.
324
+ * This is used by the Solarite class to know when to call onFirstConnect()
325
+ * @type {WeakSet<HTMLElement>} */
326
+ rendered: new WeakSet(),
336
327
 
337
328
  /**
338
- * Remove one value from a key, and return it.
339
- * @param key {string}
340
- * @param val If specified, make sure we delete this specific value, if a key exists more than once.
341
- * @returns {*} */
342
- delete(key, val=undefined) {
343
- // if (key === '["Html2",[[["Html3",["F1","A"]],["Html3",["F1","B"]]]]]')
344
- // debugger;
345
-
346
- let data = this.data;
347
- // The partialUpdate benchmark shows having this check first makes the function slightly faster.
348
- // if (!data.hasOwnProperty(key))
349
- // return undefined;
329
+ * Used by watch3 to see which expressions are being accessed. */
330
+ currentExprPath: [],
350
331
 
351
- // Delete a specific value.
352
- let result;
353
- let set = data[key];
354
- if (!set) // slower than pre-check.
355
- return undefined;
332
+ /**
333
+ * @type {Object<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
334
+ elementClasses: {},
356
335
 
357
- if (val !== undefined) {
358
- set.delete(val);
359
- result = val;
360
- }
336
+ /**
337
+ * Used by ExprPath.applyEventAttrib()
338
+ * @type {WeakMap<Node, Object<eventName:string, [original:function, bound:function, args:*[]]>>} */
339
+ nodeEvents: new WeakMap(),
361
340
 
362
- // Delete any value.
363
- else {
364
- result = set.values().next().value;
365
- // [result] = set; // Does the same as above. is about the same speed?
366
- set.delete(result);
367
- }
341
+ /**
342
+ * Get the RootNodeGroup for an element.
343
+ * @type {WeakMap<HTMLElement, RootNodeGroup>} */
344
+ nodeGroups: new WeakMap(),
368
345
 
369
- // TODO: Will this make it slower?
370
- if (set.size === 0)
371
- delete data[key];
372
-
373
- return result;
374
- }
346
+ /**
347
+ * Used by r() path 9. */
348
+ objToEl: new WeakMap(),
375
349
 
376
- hasValue(val) {
377
- let data = this.data;
378
- let names = [];
379
- for (let name in data)
380
- if (data[name].has(val)) // TODO: iterate twice to pre-size array?
381
- names.push(name);
382
- return names;
383
- }
384
- }
385
-
386
-
350
+ pendingChildren: [],
351
+
352
+ /**
353
+ * Elements that are currently rendering via the r() function.
354
+ * @type {WeakSet<HTMLElement>} */
355
+ rendering: new WeakSet(),
356
+
357
+ /**
358
+ * Map from array of Html strings to a Shell created from them.
359
+ * @type {WeakMap<string[], Shell>} */
360
+ shells: new WeakMap()
361
+ };
387
362
 
388
363
  let Util = {
389
364
 
@@ -408,9 +383,133 @@ let Util = {
408
383
  child.textContent = newText;
409
384
  }
410
385
  }
411
- }
386
+ },
412
387
 
388
+ /**
389
+ * A generator function that recursively traverses and flattens a value.
390
+ *
391
+ * - If the input is an array, it recursively traverses and flattens the array.
392
+ * - If the input is a function, it calls the function, replaces the function
393
+ * with its result, and flattens the result if necessary. It will recursively
394
+ * call functions that return other functions.
395
+ * - Otherwise it yields the value as is.
396
+ *
397
+ * This function does not create a new array for the flattened values. Instead,
398
+ * it lazily yields each item as it is encountered. This can be more memory-efficient
399
+ * for large or deeply nested structures.
400
+ *
401
+ * @param {any} value - The value to flatten. Can be an array, object, function, or primitive.
402
+ * @yields {any} - The next item in the flattened structure.
403
+ *
404
+ * @example
405
+ * const complexArray = [
406
+ * 1,
407
+ * [2, () => 3, [4, () => [5, 6]], { a: 'object' }],
408
+ * () => () => 7,
409
+ * () => [() => 8, 9],
410
+ * ]; *
411
+ * for (const item of flatten(complexArray))
412
+ * console.log(item); // Outputs: 1, 2, 3, 4, 5, 6, { a: 'object' }, 7, 8, 9
413
+ */
414
+ *flatten(value) {
415
+ if (Array.isArray(value)) {
416
+ for (const item of value) {
417
+ yield* Util.flatten(item); // Recursively flatten arrays
418
+ }
419
+ } else if (typeof value === 'function') {
420
+ const result = value();
421
+ yield* Util.flatten(result); // Recursively flatten the result of a function
422
+ } else
423
+ yield value; // Yield primitive values as is
424
+ },
425
+
426
+ /**
427
+ * Get the value of an input as the most appropriate JavaScript type.
428
+ * @param node {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|HTMLDivElement}
429
+ * @return {string|string[]|number|[]|File[]|Date|boolean} */
430
+ getInputValue(node) {
431
+ if (node.type === 'checkbox' || node.type === 'radio')
432
+ return node.checked; // Boolean
433
+ if (node.type === 'file')
434
+ return [...node.files]; // FileList
435
+ if (node.type === 'number' || node.type === 'range')
436
+ return node.valueAsNumber; // Number
437
+ if (node.type === 'date' || node.type === 'time' || node.type === 'datetime-local')
438
+ return node.valueAsDate; // Date Object
439
+ if (node.type === 'select-multiple') // <select multiple>
440
+ return [...node.selectedOptions].map(option => option.value); // Array of Strings
441
+
442
+ return node.value; // String
443
+ },
444
+
445
+ /**
446
+ * Is it an array and a path that can be evaluated by delve() ?
447
+ * @param arr {Array|*}
448
+ * @returns {boolean} */
449
+ isPath(arr) {
450
+ return Array.isArray(arr) && typeof arr[0] === 'object' && !arr.slice(1).find(p => typeof p !== 'string' && typeof p !== 'number');
451
+ },
452
+
453
+ /**
454
+ * Find NodeGroups that had their nodes removed and add those nodes to a Fragment so
455
+ * they're not lost forever and the NodeGroup's internal structure is still consistent.
456
+ * This saves all of a NodeGroup's nodes in order, so that nextChildNode still works.
457
+ * This is necessary because a NodeGroup normally only stores the first and last node.
458
+ * Called from ExprPath.apply().
459
+ * @param oldNodeGroups {NodeGroup[]}
460
+ * @param oldNodes {Node[]} */
461
+ saveOrphans(oldNodeGroups, oldNodes) {
462
+ let oldNgMap = new Map();
463
+ for (let ng of oldNodeGroups) {
464
+ oldNgMap.set(ng.startNode, ng);
465
+
466
+ // TODO: Is this necessary?
467
+ // if (ng.parentPath)
468
+ // ng.parentPath.clearNodesCache();
469
+ }
470
+
471
+ for (let i=0, node; node = oldNodes[i]; i++) {
472
+ let ng;
473
+ if (!node.parentNode && (ng = oldNgMap.get(node))) {
474
+ //ng.nodesCache = [];
475
+ let fragment = document.createDocumentFragment();
476
+ let endNode = ng.endNode;
477
+ while (node !== endNode) {
478
+ fragment.append(node);
479
+ //ng.nodesCache.push(node);
480
+ i++;
481
+ node = oldNodes[i];
482
+ }
483
+ fragment.append(endNode);
484
+ //ng.nodesCache.push(endNode);
485
+ }
486
+ }
487
+ },
488
+
489
+ /**
490
+ * Remove nodes from the beginning and end that are not:
491
+ * 1. Elements.
492
+ * 2. Non-whitespace text nodes.
493
+ * @param nodes {Node[]|NodeList}
494
+ * @returns {Node[]} */
495
+ trimEmptyNodes(nodes) {
496
+ const shouldTrimNode = node =>
497
+ node.nodeType !== Node.ELEMENT_NODE &&
498
+ (node.nodeType !== Node.TEXT_NODE || node.textContent.trim() === '');
499
+
500
+ // Convert nodeList to an array for easier manipulation
501
+ const result = [...nodes];
502
+
503
+ // Trim from the start
504
+ while (result.length > 0 && shouldTrimNode(result[0]))
505
+ result.shift();
506
+
507
+ // Trim from the end
508
+ while (result.length > 0 && shouldTrimNode(result[result.length - 1]))
509
+ result.pop();
413
510
 
511
+ return result;
512
+ }
414
513
  };
415
514
 
416
515
 
@@ -455,15 +554,15 @@ function camelToDashes(str) {
455
554
  * Returns false if they're the same. Or the first index where they differ.
456
555
  * @param a
457
556
  * @param b
458
- * @returns {int|false} */
459
- function findArrayDiff(a, b) {
460
- if (a.length !== b.length)
461
- return -1;
557
+ * @returns {boolean} */
558
+ function arraySame(a, b) {
462
559
  let aLength = a.length;
560
+ if (aLength !== b.length)
561
+ return false;
463
562
  for (let i=0; i<aLength; i++)
464
563
  if (a[i] !== b[i])
465
- return i;
466
- return false; // the same.
564
+ return false;
565
+ return true; // the same.
467
566
  }
468
567
 
469
568
 
@@ -546,193 +645,275 @@ let state = {...defaultState};
546
645
  // For debugging only
547
646
 
548
647
 
549
- /**
550
- * The html strings and evaluated expressions from an html tagged template.
551
- * A unique Template is created for each item in a loop.
552
- * Although the reference to the html strings is shared among templates. */
553
- class Template {
648
+ class MultiValueMap {
554
649
 
555
- /** @type {(Template|string|function)|(Template|string|function)[]} Evaulated expressions. */
556
- exprs = []
650
+ /** @type {Object<string, Set>} */
651
+ data = {};
557
652
 
558
- /** @type {string[]} */
559
- html = [];
653
+ // Set a new value for a key
654
+ add(key, value) {
655
+ let data = this.data;
656
+ let set = data[key];
657
+ if (!set) {
658
+ set = new Set();
659
+ data[key] = set;
660
+ }
661
+ set.add(value);
662
+ }
560
663
 
561
- /**
562
- * If true, use this template to replace an existing element, instead of appending children to it.
563
- * @type {?boolean} */
564
- replaceMode;
664
+ isEmpty() {
665
+ for (let key in this.data)
666
+ return true;
667
+ return false;
668
+ }
565
669
 
566
- /** Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
567
- hashedFields;
670
+ // Get all values for a key
671
+ getAll(key) {
672
+ return this.data[key] || [];
673
+ }
568
674
 
569
675
  /**
570
- * @deprecated
571
- * @type {ExprPath} Used with forEach() from watch.js
572
- * Set in ExprPath.apply() */
573
- parentPath;
574
-
575
- /** @type {NodeGroup} */
576
- nodeGroup;
577
-
578
- /**
579
- * @type {string[][]} */
580
- paths = [];
676
+ * Remove one value from a key, and return it.
677
+ * @param key {string}
678
+ * @param val If specified, make sure we delete this specific value, if a key exists more than once.
679
+ * @returns {*} */
680
+ delete(key, val=undefined) {
681
+ // if (key === '["Html2",[[["Html3",["F1","A"]],["Html3",["F1","B"]]]]]')
682
+ // debugger;
581
683
 
582
- /**
583
- *
584
- * @param htmlStrings {string[]}
585
- * @param exprs {*[]} */
586
- constructor(htmlStrings, exprs) {
587
- this.html = htmlStrings;
588
- this.exprs = exprs;
589
-
590
- //this.trace = new Error().stack.split(/\n/g)
684
+ let data = this.data;
591
685
 
592
- // Multiple templates can share the same htmlStrings array.
593
- //this.hashedFields = [getObjectId(htmlStrings), exprs]
686
+ // if (!data.hasOwnProperty(key))
687
+ // return undefined;
594
688
 
595
-
689
+ // Delete a specific value.
690
+ let result;
691
+ let set = data[key];
692
+ if (!set) // slower than pre-check.
693
+ return undefined;
694
+
695
+ // Delete any value.
696
+ if (val === undefined) {
697
+ //result = set.values().next().value; // get first item from set.
698
+ [result] = set; // Does the same as above and seems to be about the same speed.
699
+ set.delete(result);
700
+ }
701
+
702
+ // Delete a specific value.
703
+ else {
704
+ set.delete(val);
705
+ result = val;
706
+ }
707
+
708
+ // TODO: Will this make it slower?
709
+ if (set.size === 0)
710
+ delete data[key];
711
+
712
+ return result;
596
713
  }
597
714
 
598
- /**
599
- * Called by JSON.serialize when it encounters a Template.
600
- * This prevents the hashed version from being too large. */
601
- toJSON() {
602
- if (!this.hashedFields)
603
- this.hashedFields = [getObjectId(this.html, 'Html'), this.exprs];
604
-
605
- return this.hashedFields
715
+ hasValue(val) {
716
+ let data = this.data;
717
+ let names = [];
718
+ for (let name in data)
719
+ if (data[name].has(val)) // TODO: iterate twice to pre-size array?
720
+ names.push(name);
721
+ return names;
606
722
  }
723
+ }
724
+
725
+ /**
726
+ * ISC License
727
+ *
728
+ * Copyright (c) 2020, Andrea Giammarchi, @WebReflection
729
+ *
730
+ * Permission to use, copy, modify, and/or distribute this software for any
731
+ * purpose with or without fee is hereby granted, provided that the above
732
+ * copyright notice and this permission notice appear in all copies.
733
+ *
734
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
735
+ * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
736
+ * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
737
+ * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
738
+ * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
739
+ * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
740
+ * PERFORMANCE OF THIS SOFTWARE.
741
+ */
607
742
 
608
- /**
609
- * Render the main template, which may indirectly call renderTemplate() to create children.
610
- * @param el {HTMLElement}
611
- * @param options {RenderOptions}
612
- * @return {?DocumentFragment|HTMLElement} */
613
- render(el=null, options={}) {
743
+ /**
744
+ * @param {Node} parentNode The container where children live
745
+ * @param {Node[]} a The list of current/live children
746
+ * @param {Node[]} b The list of future children
747
+ * @param {(entry: Node, action: number) => Node} get
748
+ * The callback invoked per each entry related DOM operation.
749
+ * @param {Node} [before] The optional node used as anchor to insert before.
750
+ * @returns {Node[]} The same list of future children.
751
+ */
752
+ const udomdiff = (parentNode, a, b, before) => {
753
+
614
754
 
615
- let ng;
616
- if (!el) {
617
- ng = new NodeGroup(this);
618
- el = ng.getParentNode();
755
+ const bLength = b.length;
756
+ let aEnd = a.length;
757
+ let bEnd = bLength;
758
+ let aStart = 0;
759
+ let bStart = 0;
760
+ let map = null;
761
+ while (aStart < aEnd || bStart < bEnd) {
762
+ // append head, tail, or nodes in between: fast path
763
+ if (aEnd === aStart) {
764
+ // we could be in a situation where the rest of nodes that
765
+ // need to be added are not at the end, and in such case
766
+ // the node to `insertBefore`, if the index is more than 0
767
+ // must be retrieved, otherwise it's gonna be the first item.
768
+ const node = bEnd < bLength
769
+ ? (bStart
770
+ ? (b[bStart - 1].nextSibling)
771
+ : b[bEnd - bStart])
772
+ : before;
773
+ while (bStart < bEnd) {
774
+ let bNode = b[bStart++];
775
+ parentNode.insertBefore(bNode, node);
776
+
777
+
778
+ }
779
+ }
780
+ // remove head or tail: fast path
781
+ else if (bEnd === bStart) {
782
+ while (aStart < aEnd) {
783
+ // remove the node only if it's unknown or not live
784
+ let aNode = a[aStart];
785
+ if (!map || !map.has(aNode)) {
786
+ parentNode.removeChild(aNode);
787
+
788
+
789
+ }
790
+ aStart++;
791
+ }
792
+ }
793
+ // same node: fast path
794
+ else if (a[aStart] === b[bStart]) {
795
+ aStart++;
796
+ bStart++;
619
797
  }
798
+ // same tail: fast path
799
+ else if (a[aEnd - 1] === b[bEnd - 1]) {
800
+ aEnd--;
801
+ bEnd--;
802
+ }
803
+ // The once here single last swap "fast path" has been removed in v1.1.0
804
+ // https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85
805
+ // reverse swap: also fast path
806
+ else if (
807
+ a[aStart] === b[bEnd - 1] &&
808
+ b[bStart] === a[aEnd - 1]
809
+ ) {
810
+ // this is a "shrink" operation that could happen in these cases:
811
+ // [1, 2, 3, 4, 5]
812
+ // [1, 4, 3, 2, 5]
813
+ // or asymmetric too
814
+ // [1, 2, 3, 4, 5]
815
+ // [1, 2, 3, 5, 6, 4]
816
+ const node = a[--aEnd].nextSibling;
620
817
 
621
- let ngm = NodeGroupManager.get(el);
622
- if (ng)
623
- ng.manager = ngm;
624
818
 
625
-
819
+ let a2 = b[bStart++];
820
+ let b2 = a[aStart++];
821
+ parentNode.insertBefore(
822
+ a2,
823
+ b2.nextSibling
824
+ );
825
+
626
826
 
627
- ngm.options = options;
628
- ngm.clearSubscribers = false; // Used for deprecated watch() path?
629
- ngm.mutationWatcherEnabled = false;
827
+ let bNode = b[--bEnd];
828
+ parentNode.insertBefore(bNode, node);
630
829
 
631
- // Fast path for empty component.
632
- if (this.html?.length === 1 && !this.html[0]) {
633
- el.innerHTML = '';
830
+
831
+
832
+ // mark the future index as identical (yeah, it's dirty, but cheap 👍)
833
+ // The main reason to do this, is that when a[aEnd] will be reached,
834
+ // the loop will likely be on the fast path, as identical to b[bEnd].
835
+ // In the best case scenario, the next loop will skip the tail,
836
+ // but in the worst one, this node will be considered as already
837
+ // processed, bailing out pretty quickly from the map index check
838
+ a[aEnd] = b[bEnd];
634
839
  }
840
+ // map based fallback, "slow" path
635
841
  else {
636
-
637
- // Find or create a NodeGroup for the template.
638
- // This updates all nodes from the template.
639
- let close;
640
- let exact = ngm.getNodeGroup(this, true);
641
- if (!exact) {
642
- close = ngm.getNodeGroup(this, false);
842
+ // the map requires an O(bEnd - bStart) operation once
843
+ // to store all future nodes indexes for later purposes.
844
+ // In the worst case scenario, this is a full O(N) cost,
845
+ // and such scenario happens at least when all nodes are different,
846
+ // but also if both first and last items of the lists are different
847
+ if (!map) {
848
+ map = new Map;
849
+ let i = bStart;
850
+ while (i < bEnd)
851
+ map.set(b[i], i++);
643
852
  }
853
+ // if it's a future node, hence it needs some handling
854
+ if (map.has(a[aStart])) {
855
+ // grab the index of such node, 'cause it might have been processed
856
+ const index = map.get(a[aStart]);
857
+ // if it's not already processed, look on demand for the next LCS
858
+ if (bStart < index && index < bEnd) {
859
+ let i = aStart;
860
+ // counts the amount of nodes that are the same in the future
861
+ let sequence = 1;
862
+ while (++i < aEnd && i < bEnd && map.get(a[i]) === (index + sequence))
863
+ sequence++;
864
+ // effort decision here: if the sequence is longer than replaces
865
+ // needed to reach such sequence, which would brings again this loop
866
+ // to the fast path, prepend the difference before a sequence,
867
+ // and move only the future list index forward, so that aStart
868
+ // and bStart will be aligned again, hence on the fast path.
869
+ // An example considering aStart and bStart are both 0:
870
+ // a: [1, 2, 3, 4]
871
+ // b: [7, 1, 2, 3, 6]
872
+ // this would place 7 before 1 and, from that time on, 1, 2, and 3
873
+ // will be processed at zero cost
874
+ if (sequence > (index - bStart)) {
875
+ const node = a[aStart];
876
+ while (bStart < index) {
877
+ let bNode = b[bStart++];
878
+ parentNode.insertBefore(bNode, node);
644
879
 
645
- let firstTime = !ngm.rootNg;
646
- ngm.rootNg = exact || close;
647
-
648
- // Reparent NodeGroup
649
- // TODO: Move this to NodeGroup?
650
- let parent = ngm.rootNg.getParentNode();
651
-
652
-
653
- // If this is the first time rendering this element.
654
- if (firstTime) {
655
-
656
- // Save slot children
657
- let fragment;
658
- if (el.childNodes.length) {
659
- fragment = document.createDocumentFragment();
660
- fragment.append(...el.childNodes);
661
- }
880
+
881
+ }
882
+ }
883
+ // if the effort wasn't good enough, fallback to a replace,
884
+ // moving both source and target indexes forward, hoping that some
885
+ // similar node will be found later on, to go back to the fast path
886
+ else {
887
+ let aNode = a[aStart++];
888
+ let bNode = b[bStart++];
889
+ parentNode.replaceChild(
890
+ bNode,
891
+ aNode
892
+ );
662
893
 
663
- // Add rendered elements.
664
- if (parent instanceof DocumentFragment)
665
- el.append(parent);
666
- else if (parent)
667
- el.append(...parent.childNodes);
668
-
669
- // Apply slot children
670
- if (fragment) {
671
- for (let slot of el.querySelectorAll('slot[name]')) {
672
- let name = slot.getAttribute('name');
673
- if (name)
674
- slot.append(...fragment.querySelectorAll(`[slot='${name}']`));
894
+
675
895
  }
676
- let unamedSlot = el.querySelector('slot:not([name])');
677
- if (unamedSlot)
678
- unamedSlot.append(fragment);
679
896
  }
897
+ // otherwise move the source forward, 'cause there's nothing to do
898
+ else
899
+ aStart++;
680
900
  }
901
+ // this node has no meaning in the future list, so it's more than safe
902
+ // to remove it, and check the next live node out instead, meaning
903
+ // that only the live list index should be forwarded
904
+ else {
905
+ let aNode = a[aStart++];
906
+ parentNode.removeChild(aNode);
681
907
 
682
- ngm.rootEl = el;
683
-
684
- // this.rootNg was rendered as childrenOnly=true
685
- // Apply attributes from a root element to the real root element.
686
- let ng = ngm.rootNg;
687
- if (ng.pseudoRoot && ng.pseudoRoot !== el) {
688
908
 
689
-
690
- // Remove old attributes
691
- // for (let attrib of this.rootEl.attributes)
692
- // if (attrib.name !== 'is' && attrib.name !== 'data-style' && !ng.pseudoRoot.hasAttribute(attrib.name))
693
- // this.rootEl.removeAttribute(attrib.name)
694
-
695
- // Add/set new attributes
696
- if (firstTime)
697
- for (let attrib of ng.pseudoRoot.attributes)
698
- if (!el.hasAttribute(attrib.name))
699
- el.setAttribute(attrib.name, attrib.value);
700
-
701
- // ng.startNode = ng.endNode = this.rootEl;
702
- // ng.nodesCache = [ng.startNode]
703
- // for (let path of ng.paths) {
704
- // if (path.nodeMarker === ng.rootEl)
705
- // path.nodeMarker = this.rootEl;
706
- // path.nodesCache = null;
707
- //
708
- // }
709
- //
710
- // ng.rootEl = this.rootEl;
711
909
  }
712
-
713
-
714
- ngm.reset(); // Mark all NodeGroups as available, for next render.
715
-
716
-
717
- window.ngm = ngm;
718
910
  }
719
-
720
- ngm.mutationWatcherEnabled = true;
721
- return el;
722
-
723
- }
724
-
725
-
726
- getCloseKey() {
727
- // Use the joined html when debugging?
728
- //return '@'+this.html.join('|')
729
-
730
- return '@'+this.hashedFields[0];
731
911
  }
732
- }
912
+ return b;
913
+ };
733
914
 
734
915
  /**
735
- * Path to where an expression should be evaluated within a Shell.
916
+ * Path to where an expression should be evaluated within a Shell or NodeGroup.
736
917
  * Path is only valid until the expressions before it are evaluated.
737
918
  * TODO: Make this based on parent and node instead of path? */
738
919
  class ExprPath {
@@ -771,14 +952,9 @@ class ExprPath {
771
952
  * @type {Node|HTMLElement} */
772
953
  nodeMarker;
773
954
 
774
- /** @deprecated */
775
- get parentNode() {
776
- return this.nodeMarker.parentNode;
777
- }
778
955
 
779
956
  // These are set after an expression is assigned:
780
957
 
781
-
782
958
  /** @type {NodeGroup} */
783
959
  parentNg;
784
960
 
@@ -786,8 +962,6 @@ class ExprPath {
786
962
  nodeGroups = [];
787
963
 
788
964
 
789
-
790
-
791
965
  // Caches to make things faster
792
966
 
793
967
  /**
@@ -795,26 +969,23 @@ class ExprPath {
795
969
  * @type {Node[]} Cached result of getNodes() */
796
970
  nodesCache;
797
971
 
798
- // What are these?
972
+ /**
973
+ * @type {int} Index of nodeBefore among its parentNode's children. */
799
974
  nodeBeforeIndex;
800
- nodeMarkerPath;
801
975
 
802
- // TODO: Keep this cached?
803
- expr;
976
+ /**
977
+ * @type {int[]} Path to the node marker, in reverse for performance reasons. */
978
+ nodeMarkerPath;
804
979
 
805
- // for debugging
806
-
807
980
 
808
981
  /**
809
982
  * @param nodeBefore {Node}
810
983
  * @param nodeMarker {?Node}
811
- * @param type {string}
984
+ * @param type {PathType}
812
985
  * @param attrName {?string}
813
986
  * @param attrValue {string[]} */
814
987
  constructor(nodeBefore, nodeMarker, type=PathType.Content, attrName=null, attrValue=null) {
815
988
 
816
-
817
-
818
989
  // If path is a node.
819
990
  this.nodeBefore = nodeBefore;
820
991
  this.nodeMarker = nodeMarker;
@@ -826,21 +997,142 @@ class ExprPath {
826
997
  }
827
998
 
828
999
  /**
1000
+ * Apply any type of expression.
1001
+ * This calls other apply functions.
829
1002
  *
830
- * @param expr {Template|Node|Array|function|*}
831
- * @param newNodes {(Node|Template)[]}
832
- * @param secondPass {Array} Locations within newNodes to evaluate later. */
833
- apply(expr, newNodes, secondPass) {
1003
+ * One very messy part of this function is that it may apply multiple expressions if they're all part
1004
+ * of the same attribute value.
1005
+ *
1006
+ * We should modify path.applyValueAttrib so it stores the procssed parts and then only calls
1007
+ * setAttribute() once all the pieces are in place.
1008
+ *
1009
+ * @param expr {Expr}
1010
+ * @param exprs {Expr[]}
1011
+ * @param exprIndex {int}
1012
+ * @param componentExprs {object}
1013
+ * @returns {int} */
1014
+ apply(expr, exprs=null, exprIndex=0, componentExprs={}) {
1015
+ switch (this.type) {
1016
+ case 1: // PathType.Content:
1017
+ this.applyNodes(expr);
1018
+ break;
1019
+ case 2: // PathType.Multiple:
1020
+ this.applyMultipleAttribs(this.nodeMarker, expr);
1021
+ break;
1022
+ case 5: // PathType.Comment:
1023
+ // Expressions inside Html comments. Deliberately empty because we won't waste time updating them.
1024
+ break;
1025
+ case 6: // PathType.Event:
1026
+ this.applyEventAttrib(this.nodeMarker, expr, this.parentNg.rootNg.root);
1027
+ break;
1028
+ default:
1029
+ if (this.type === 4 /*PathType.Component*/ && this.nodeMarker !== this.parentNg.rootNg.root)
1030
+ componentExprs[this.attrName] = expr;
1031
+ else {
1032
+ // One attribute value may have multiple expressions. Here we apply them all at once.
1033
+ exprIndex = this.applyValueAttrib(this.nodeMarker, exprs || [expr], exprIndex);
1034
+ }
1035
+ break;
1036
+ }
834
1037
 
835
- if (expr instanceof Template) {
836
- expr.nodegroup = this.parentNg; // All tests pass w/o this.
1038
+ return exprIndex;
1039
+ }
837
1040
 
838
- let ng = this.parentNg.manager.getNodeGroup(expr, true);
1041
+ /**
1042
+ * Insert/replace the nodes created by a single expression.
1043
+ * Called by applyExprs()
1044
+ * This function is recursive, as the functions it calls also call it.
1045
+ * @param expr {Expr}
1046
+ * @return {Node[]} New Nodes created. */
1047
+ applyNodes(expr) {
1048
+ let path = this;
839
1049
 
1050
+
1051
+
1052
+ /** @type {(Node|NodeGroup|Expr)[]} */
1053
+ let newNodes = [];
1054
+ let oldNodeGroups = path.nodeGroups;
1055
+
1056
+ let secondPass = []; // indices
1057
+
1058
+ path.nodeGroups = []; // Reset before applyExact and the code below rebuilds it.
1059
+ path.applyExact(expr, newNodes, secondPass);
1060
+
1061
+ this.existingTextNodes = null;
1062
+
1063
+ // TODO: Create an array of old vs Nodes and NodeGroups together.
1064
+ // If they're all the same, skip the next steps.
1065
+ // Or calculate it in the loop above as we go? Have a path.lastNodeGroups property?
1066
+
1067
+ // Second pass to find close-match NodeGroups.
1068
+ let flatten = false;
1069
+ if (secondPass.length) {
1070
+ for (let [nodesIndex, ngIndex] of secondPass) {
1071
+ let ng = path.getNodeGroup(newNodes[nodesIndex], false);
1072
+
1073
+ let ngNodes = ng.getNodes();
840
1074
 
841
- if (ng) {
842
1075
 
843
1076
 
1077
+ if (ngNodes.length === 1) // flatten manually so we can skip flattening below.
1078
+ newNodes[nodesIndex] = ngNodes[0];
1079
+
1080
+ else {
1081
+ newNodes[nodesIndex] = ngNodes;
1082
+ flatten = true;
1083
+ }
1084
+ path.nodeGroups[ngIndex] = ng;
1085
+ }
1086
+
1087
+ if (flatten)
1088
+ newNodes = newNodes.flat(); // Only if second pass happens.
1089
+ }
1090
+
1091
+
1092
+
1093
+
1094
+
1095
+ let oldNodes = path.getNodes();
1096
+
1097
+
1098
+ // This pre-check makes it a few percent faster?
1099
+ let same = arraySame(oldNodes, newNodes);
1100
+ if (!same) {
1101
+
1102
+ path.nodesCache = newNodes; // Replaces value set by path.getNodes()
1103
+
1104
+ if (this.parentNg.parentPath)
1105
+ this.parentNg.parentPath.clearNodesCache();
1106
+
1107
+ // Fast clear method
1108
+ let isNowEmpty = oldNodes.length && !newNodes.length;
1109
+ if (!isNowEmpty || !path.fastClear())
1110
+
1111
+ // Rearrange nodes.
1112
+ udomdiff(path.nodeMarker.parentNode, oldNodes, newNodes, path.nodeMarker);
1113
+
1114
+ Util.saveOrphans(oldNodeGroups, oldNodes);
1115
+ }
1116
+
1117
+ // Must happen after second pass.
1118
+ path.freeNodeGroups();
1119
+
1120
+
1121
+ }
1122
+
1123
+
1124
+
1125
+ /**
1126
+ * Apply Nodes that are an exact match.
1127
+ * @param expr {Template|Node|Array|function|*}
1128
+ * @param newNodes {(Node|Template)[]}
1129
+ * @param secondPass {Array} Locations within newNodes to evaluate later. */
1130
+ applyExact(expr, newNodes, secondPass) {
1131
+
1132
+ if (expr instanceof Template) {
1133
+
1134
+ let ng = this.getNodeGroup(expr, true);
1135
+ if (ng) {
844
1136
 
845
1137
  // TODO: Track ranges of changed nodes and only pass those to udomdiff?
846
1138
  // But will that break the swap benchmark?
@@ -848,7 +1140,7 @@ class ExprPath {
848
1140
  this.nodeGroups.push(ng);
849
1141
  }
850
1142
 
851
- // If expression, evaluate later to find partial match.
1143
+ // If expression, mark it to be evaluated later in ExprPath.apply() to find partial match.
852
1144
  else {
853
1145
  secondPass.push([newNodes.length, this.nodeGroups.length]);
854
1146
  newNodes.push(expr);
@@ -866,21 +1158,26 @@ class ExprPath {
866
1158
  newNodes.push(expr);
867
1159
  }
868
1160
 
1161
+ // Arrays and functions.
1162
+ // I tried iterating over the result of a generator function to avoid this recursion and simplify the code,
1163
+ // but that consistently made the js-framework-benchmarks a few percentage points slower.
869
1164
  else if (Array.isArray(expr))
870
1165
  for (let subExpr of expr)
871
- this.apply(subExpr, newNodes, secondPass);
1166
+ this.applyExact(subExpr, newNodes, secondPass);
872
1167
 
873
1168
  else if (typeof expr === 'function') {
1169
+ Globals.currentExprPath = [this, expr]; // Used by watch3()
874
1170
  let result = expr();
1171
+ Globals.currentExprPath = null;
875
1172
 
876
- this.apply(result, newNodes, secondPass);
1173
+ this.applyExact(result, newNodes, secondPass);
877
1174
  }
878
1175
 
879
1176
  // Text
880
1177
  else {
881
1178
  // Convert falsy values (but not 0) to empty string.
882
1179
  // Convert numbers to string so they compare the same.
883
- let text = (expr === undefined || expr === false || expr === null) ? '' : expr + '';
1180
+ let text = (expr === undefined || expr === false || expr === null) ? '' : (expr + '');
884
1181
 
885
1182
  // Fast path for updating the text of a single text node.
886
1183
  let first = this.nodeBefore.nextSibling;
@@ -900,7 +1197,7 @@ class ExprPath {
900
1197
  if (idx !== -1)
901
1198
  newNodes.push(...this.existingTextNodes.splice(idx, 1));
902
1199
  else
903
- newNodes.push(this.parentNode.ownerDocument.createTextNode(text));
1200
+ newNodes.push(this.nodeMarker.ownerDocument.createTextNode(text));
904
1201
  }
905
1202
  }
906
1203
  }
@@ -937,7 +1234,7 @@ class ExprPath {
937
1234
  /**
938
1235
  * Handle attributes for event binding, such as:
939
1236
  * onclick=${(e, el) => this.doSomething(el, 'meow')}
940
- * onclick=${[this.doSomething, 'meow']}
1237
+ * oninput=${[this.doSomething, 'meow']}
941
1238
  * onclick=${[this, 'doSomething', 'meow']}
942
1239
  *
943
1240
  * @param node
@@ -946,77 +1243,91 @@ class ExprPath {
946
1243
  applyEventAttrib(node, expr, root) {
947
1244
 
948
1245
 
949
- let eventName = this.attrName.slice(2);
1246
+ let eventName = this.attrName.slice(2); // remove "on-" prefix.
950
1247
  let func;
951
1248
 
952
1249
  // Convert array to function.
953
- // TODO: This doesn't work for [this, 'doSomething', 'meow']
954
1250
  let args = [];
955
1251
  if (Array.isArray(expr)) {
956
- for (let i=0; i<expr.length; i++)
957
- if (typeof expr[i] === 'function') {
958
- func = expr[i];
959
- args = expr.slice(i+1);
960
- break;
961
- }
962
1252
 
1253
+ // oninput=${[this.doSomething, 'meow']}
1254
+ if (typeof expr[0] === 'function') {
1255
+ func = expr[0];
1256
+ args = expr.slice(1);
1257
+ }
1258
+
1259
+ // Undocumented.
963
1260
  // oninput=${[this, 'value']}
964
- if (!func) {
1261
+ else {
965
1262
  func = setValue;
966
1263
  args = [expr[0], expr.slice(1), node];
967
1264
  node.value = delve(expr[0], expr.slice(1));
1265
+ // root.render(); // TODO: This causes infinite recursion.
968
1266
  }
969
1267
  }
970
1268
  else
971
1269
  func = expr;
972
1270
 
973
- let eventKey = getObjectId(node) + eventName;
974
- let [existing, existingBound] = nodeEvents[eventKey] || [];
975
- nodeEventArgs[eventKey] = args; // TODO: Put this in nodeEvents[]
1271
+ let nodeEvents = Globals.nodeEvents.get(node);
1272
+ if (!nodeEvents) {
1273
+ nodeEvents = {[eventName]: new Array(3)};
1274
+ Globals.nodeEvents.set(node, nodeEvents);
1275
+ }
1276
+ let nodeEvent = nodeEvents[eventName];
1277
+
976
1278
 
977
1279
 
978
- if (existing !== func) {
1280
+ // If function has changed, remove and rebind the event.
1281
+ if (nodeEvent[0] !== func) {
1282
+ let [existing, existingBound, _] = nodeEvent;
979
1283
  if (existing)
980
1284
  node.removeEventListener(eventName, existingBound);
981
1285
 
982
1286
  let originalFunc = func;
983
1287
 
984
1288
  // BoundFunc sets the "this" variable to be the current Solarite component.
985
- let boundFunc = event => originalFunc.call(root, ...args, event, node);
1289
+ let boundFunc = (event) => {
1290
+ let args = nodeEvent[2];
1291
+ return originalFunc.call(root, ...args, event, node);
1292
+ };
986
1293
 
987
1294
  // Save both the original and bound functions.
988
1295
  // Original so we can compare it against a newly assigned function.
989
1296
  // Bound so we can use it with removeEventListner().
990
- nodeEvents[eventKey] = [originalFunc, boundFunc];
1297
+ nodeEvent[0] = originalFunc;
1298
+ nodeEvent[1] = boundFunc;
991
1299
 
992
1300
  node.addEventListener(eventName, boundFunc);
993
1301
 
994
- // TODO: classic event attribs:
1302
+ // TODO: classic event attribs?
995
1303
  //el[attr.name] = e => // e.g. el.onclick = ...
996
1304
  // (new Function('event', 'el', attr.value)).bind(this.manager.rootEl)(e, el) // put "event", "el", and "this" in scope for the event code.
997
1305
  }
1306
+
1307
+ // Otherwise just update the args to the function.
1308
+ nodeEvents[eventName][2] = args;
998
1309
  }
999
1310
 
1000
1311
  applyValueAttrib(node, exprs, exprIndex) {
1001
1312
  let expr = exprs[exprIndex];
1002
-
1003
- // Array for form element data binding.
1004
- // TODO: This never worked, and was moved to applyEventAttrib.
1005
- // let isArrayValue = Array.isArray(expr);
1006
- // if (isArrayValue && expr.length >= 2 && !expr.slice(1).find(v => !['string', 'number'].includes(typeof v))) {
1007
- // node.value = delve(expr[0], expr.slice(1));
1008
- // node.addEventListener('input', e => {
1009
- // delve(expr[0], expr.slice(1), node.value) // TODO: support other properties like checked
1010
- // });
1011
- // }
1012
1313
 
1013
1314
  // Values to toggle an attribute
1014
1315
  if (!this.attrValue && (expr === false || expr === null || expr === undefined))
1015
1316
  node.removeAttribute(this.attrName);
1016
-
1317
+
1017
1318
  else if (!this.attrValue && expr === true)
1018
1319
  node.setAttribute(this.attrName, '');
1019
1320
 
1321
+ // Passing a path to the value attribute.
1322
+ // This same logic is in NodeGroup.createNewComponent() for components.
1323
+ else if ((this.attrName === 'value' || this.attrName === 'data-value') && Util.isPath(expr)) {
1324
+ let [obj, path] = [expr[0], expr.slice(1)];
1325
+ node.value = delve(obj, path);
1326
+ node.addEventListener('input', () => {
1327
+ delve(obj, path, Util.getInputValue(node));
1328
+ }, true); // We use capture so we update the values before other events added by the user.
1329
+ }
1330
+
1020
1331
  // Regular attribute
1021
1332
  else {
1022
1333
  let value = [];
@@ -1032,21 +1343,25 @@ class ExprPath {
1032
1343
  exprIndex--;
1033
1344
  }
1034
1345
  }
1035
-
1036
1346
  exprIndex ++;
1037
1347
  }
1038
1348
  else
1039
1349
  value.unshift(expr);
1040
1350
 
1041
1351
  let joinedValue = value.join('');
1042
- node.setAttribute(this.attrName, joinedValue);
1352
+
1353
+ // Only update attributes if the value has changed.
1354
+ // The .value property is special. If it changes we don't update the attribute.
1355
+ let oldVal = this.attrName === 'value' ? node.value : node.getAttribute(this.attrName);
1356
+ if (oldVal !== joinedValue) {
1357
+ node.setAttribute(this.attrName, joinedValue);
1358
+ }
1043
1359
 
1044
1360
  // This is needed for setting input.value, .checked, option.selected, etc.
1045
1361
  // But in some cases setting the attribute is enough. such as div.setAttribute('title') updates div.title.
1046
1362
  // TODO: How to tell which is which?
1047
1363
  if (this.attrName in node)
1048
1364
  node[this.attrName] = joinedValue;
1049
-
1050
1365
  }
1051
1366
 
1052
1367
  return exprIndex;
@@ -1056,20 +1371,22 @@ class ExprPath {
1056
1371
  /**
1057
1372
  *
1058
1373
  * @param newRoot {HTMLElement}
1374
+ * @param pathOffset {int}
1059
1375
  * @return {ExprPath} */
1060
- clone(newRoot) {
1376
+ clone(newRoot, pathOffset=0) {
1061
1377
 
1062
1378
 
1063
- // Resolve node paths.
1379
+ // Resolve node paths.
1064
1380
  let nodeMarker, nodeBefore;
1065
- let root = newRoot;
1066
- let path = this.nodeMarkerPath;
1067
- for (let i=path.length-1; i>0; i--)
1068
- root = root.childNodes[path[i]];
1381
+ let root = newRoot;
1382
+ let path = pathOffset ? this.nodeMarkerPath.slice(0, -pathOffset) : this.nodeMarkerPath;
1383
+ for (let i=path.length-1; i>0; i--) // Resolve the path.
1384
+ root = root.childNodes[path[i]];
1069
1385
  let childNodes = root.childNodes;
1070
- nodeMarker = childNodes[path[0]];
1071
- if (this.nodeBefore)
1072
- nodeBefore = childNodes[this.nodeBeforeIndex];
1386
+
1387
+ nodeMarker = path.length ? childNodes[path[0]] : newRoot;
1388
+ if (this.nodeBefore)
1389
+ nodeBefore = childNodes[this.nodeBeforeIndex];
1073
1390
 
1074
1391
  let result = new ExprPath(nodeBefore, nodeMarker, this.type, this.attrName, this.attrValue);
1075
1392
 
@@ -1077,7 +1394,7 @@ class ExprPath {
1077
1394
 
1078
1395
  return result;
1079
1396
  }
1080
-
1397
+
1081
1398
  /**
1082
1399
  * Clear the nodeCache of this ExprPath, as well as all parent and child ExprPaths that
1083
1400
  * share the same DOM parent node.
@@ -1085,32 +1402,19 @@ class ExprPath {
1085
1402
  * TODO: Is recursive clearing ever necessary? */
1086
1403
  clearNodesCache() {
1087
1404
  let path = this;
1088
-
1405
+
1089
1406
  // Clear cache parent ExprPaths that have the same parentNode
1090
- let parentNode = this.parentNode;
1091
- while (path && path.parentNode === parentNode) {
1407
+ let parentNode = this.nodeMarker.parentNode;
1408
+ while (path && path.nodeMarker.parentNode === parentNode) {
1092
1409
  path.nodesCache = null;
1093
1410
  path = path.parentNg?.parentPath;
1094
-
1411
+
1095
1412
  // If stuck in an infinite loop here, the problem is likely due to Template hash colisions.
1096
1413
  // Which cause one path to be the descendant of itself, creating a cycle.
1097
1414
  }
1098
-
1099
- function clearChildNodeCache(path) {
1100
-
1101
- // Clear cache of child ExprPaths that have the same parentNode
1102
- for (let ng of path.nodeGroups) {
1103
- if (ng) // Can be null from apply()'s push(null) call.
1104
- for (let path2 of ng.paths) {
1105
- if (path2.type === PathType.Content && path2.parentNode === parentNode) {
1106
- path2.nodesCache = null;
1107
- clearChildNodeCache(path2);
1108
- }
1109
- }
1110
- }
1111
- }
1112
-
1113
- clearChildNodeCache(this);
1415
+
1416
+ // Commented out on Sep 30, 2024 b/c it was making the benchmark never finish when adding 10k rows.
1417
+ //clearChildNodeCache(this);
1114
1418
  }
1115
1419
 
1116
1420
 
@@ -1123,44 +1427,46 @@ class ExprPath {
1123
1427
 
1124
1428
  // If parent is the only child of the grandparent, replace the whole parent.
1125
1429
  // And if it has no siblings, it's not created by a NodeGroup/path.
1126
- let grandparent = parent.parentNode;
1127
- if (grandparent && parent === grandparent.firstChild && parent === grandparent.lastChild && !parent.hasAttribute('id')) {
1128
- let replacement = document.createElement(parent.tagName);
1129
- replacement.append(this.nodeBefore, this.nodeMarker);
1130
- for (let attrib of parent.attributes)
1131
- replacement.setAttribute(attrib.name, attrib.value);
1132
- parent.replaceWith(replacement);
1133
- }
1134
- else {
1430
+ // Commented out because this will break any references.
1431
+ // And because I don't see much performance difference.
1432
+ // let grandparent = parent.parentNode
1433
+ // if (grandparent && parent === grandparent.firstChild && parent === grandparent.lastChild && !parent.hasAttribute('id')) {
1434
+ // let replacement = document.createElement(parent.tagName)
1435
+ // replacement.append(this.nodeBefore, this.nodeMarker)
1436
+ // for (let attrib of parent.attributes)
1437
+ // replacement.setAttribute(attrib.name, attrib.value)
1438
+ // parent.replaceWith(replacement)
1439
+ // }
1440
+ // else {
1135
1441
  parent.innerHTML = ''; // Faster than calling .removeChild() a thousand times.
1136
1442
  parent.append(this.nodeBefore, this.nodeMarker);
1137
- }
1443
+ //}
1138
1444
  return true;
1139
1445
  }
1140
1446
  return false;
1141
1447
  }
1142
-
1448
+
1143
1449
  /**
1144
1450
  * @return {(Node|HTMLElement)[]} */
1145
1451
  getNodes() {
1146
-
1452
+
1147
1453
  // Why doesn't this work?
1148
1454
  // let result2 = [];
1149
1455
  // for (let ng of this.nodeGroups)
1150
1456
  // result2.push(...ng.getNodes())
1151
1457
  // return result2;
1152
-
1153
-
1458
+
1459
+
1154
1460
  let result;
1155
1461
 
1156
1462
  // This shaves about 5ms off the partialUpdate benchmark.
1157
- /*result = this.nodesCache;
1463
+ result = this.nodesCache;
1158
1464
  if (result) {
1465
+
1159
1466
 
1160
-
1161
-
1467
+
1162
1468
  return result
1163
- }*/
1469
+ }
1164
1470
 
1165
1471
  result = [];
1166
1472
  let current = this.nodeBefore.nextSibling;
@@ -1170,20 +1476,107 @@ class ExprPath {
1170
1476
  current = current.nextSibling;
1171
1477
  }
1172
1478
 
1173
- this.nodesCache = result;
1479
+ this.nodesCache = result;
1480
+ return result;
1481
+ }
1482
+
1483
+ getParentNode() { // Same as this.parentNode
1484
+ return this.nodeMarker.parentNode
1485
+ }
1486
+
1487
+ /**
1488
+ * Get an unused NodeGroup that matches the template's html and expressions (exact=true)
1489
+ * or at least the html (exact=false).
1490
+ * Remove it from nodeGroupsFree if it exists, or create it if not.
1491
+ * Then add it to nodeGroupsInUse.
1492
+ *
1493
+ * @param template {Template}
1494
+ * @param exact {boolean}
1495
+ * If true, return an exact match, or null.
1496
+ * If false, either find a match for the template's html and then apply the template's expressions,
1497
+ * or createa new NodeGroup from the template.
1498
+ * @return {NodeGroup} */
1499
+ getNodeGroup(template, exact=true) {
1500
+ //if (exact && this.nodeGroupsFree.isEmpty())
1501
+ // return null;
1502
+
1503
+ let result;
1504
+
1505
+ if (exact) {
1506
+ result = this.nodeGroupsFree.delete(template.getExactKey());
1507
+ if (result) // also delete the matching close key.
1508
+ this.nodeGroupsFree.delete(template.getCloseKey(), result);
1509
+ else
1510
+ return null;
1511
+ }
1512
+
1513
+ // Find a close match.
1514
+ // This is a match that has matching html, but different expressions applied.
1515
+ // We can then apply the expressions to make it an exact match.
1516
+ else {
1517
+ result = this.nodeGroupsFree.delete(template.getCloseKey());
1518
+ if (result) {
1519
+
1520
+ this.nodeGroupsFree.delete(result.exactKey, result);
1521
+
1522
+ // Update this close match with the new expression values.
1523
+ result.applyExprs(template.exprs);
1524
+ result.exactKey = template.getExactKey(); // TODO: Should this be set elsewhere?
1525
+ }
1526
+ }
1527
+
1528
+ if (!result)
1529
+ result = new NodeGroup(template, this);
1530
+
1531
+ // old:
1532
+ this.nodeGroupsInUse.push(result);
1533
+
1534
+ // new:
1535
+ // let ngiu = this.nodeGroupsInUse;
1536
+ // ngiu.add(result.exactKey, result);
1537
+ // ngiu.add(result.closeKey, result);
1538
+
1539
+
1174
1540
  return result;
1175
1541
  }
1176
1542
 
1177
- getParentNode() { // Same as this.parentNode
1178
- return this.nodeMarker.parentNode
1179
- }
1180
-
1181
- removeNodeGroup(ng) {
1182
- let idx = this.nodeGroups.indexOf(ng);
1183
-
1184
- this.nodeGroups.splice(idx);
1185
- ng.parentPath = null;
1186
- this.clearNodesCache();
1543
+
1544
+ /**
1545
+ * Used with getNodeGroup() and freeNodeGroups().
1546
+ * TODO: Use an array of WeakRef so the gc can collect them?
1547
+ * TODO: Put items back in nodeGroupsInUse after applyExpr() is called, not before.
1548
+ * @type {NodeGroup[]} */
1549
+ nodeGroupsInUse = [];
1550
+
1551
+ /** @type {MultiValueMap<key:string, value:NodeGroup>} */
1552
+ //nodeGroupsInUse = new MultiValueMap();
1553
+
1554
+ /**
1555
+ * Used with getNodeGroup() and freeNodeGroups().
1556
+ * Each NodeGroup is here twice, once under an exact key, and once under the close key.
1557
+ * @type {MultiValueMap<key:string, value:NodeGroup>} */
1558
+ nodeGroupsFree = new MultiValueMap();
1559
+
1560
+
1561
+ /**
1562
+ * Move everything from this.nodeGroupsInUse to this.nodeGroupsFree.
1563
+ * TODO: this could run as needed in getNodeGroup? */
1564
+ freeNodeGroups() {
1565
+ // old:
1566
+ let ngf = this.nodeGroupsFree;
1567
+ for (let ng of this.nodeGroupsInUse) {
1568
+ ngf.add(ng.exactKey, ng);
1569
+ ngf.add(ng.closeKey, ng);
1570
+ }
1571
+ this.nodeGroupsInUse = [];
1572
+
1573
+ // new:
1574
+ // for (let key in this.nodeGroupsFree.data)
1575
+ // for (let item of this.nodeGroupsFree.data[key])
1576
+ // this.nodeGroupsInUse.add(key, item);
1577
+ //
1578
+ // this.nodeGroupsFree = this.nodeGroupsInUse;
1579
+ // this.nodeGroupsInUse = new MultiValueMap();
1187
1580
  }
1188
1581
 
1189
1582
 
@@ -1203,26 +1596,27 @@ function setValue(root, path, node) {
1203
1596
  val = parseFloat(val);
1204
1597
 
1205
1598
  delve(root, path, val);
1206
-
1207
- //this.render();
1208
1599
  }
1209
1600
 
1210
- /** @enum {string} */
1601
+ /** @enum {int} */
1211
1602
  const PathType = {
1212
1603
  /** Child of a node */
1213
- Content: 'content',
1604
+ Content: 1,
1214
1605
 
1215
1606
  /** One or more whole attributes */
1216
- Multiple: 'attrName',
1607
+ Multiple: 2,
1217
1608
 
1218
1609
  /** Value of an attribute. */
1219
- Value: 'attrValue',
1610
+ Value: 3,
1220
1611
 
1221
1612
  /** Value of an attribute being passed to a component. */
1222
- Component: 'component',
1613
+ Component: 4,
1223
1614
 
1224
1615
  /** Expressions inside Html comments. */
1225
- Comment: 'comment',
1616
+ Comment: 5,
1617
+
1618
+ /** Value of an attribute. */
1619
+ Event: 6,
1226
1620
  };
1227
1621
 
1228
1622
 
@@ -1248,28 +1642,24 @@ function resolveNodePath(root, path) {
1248
1642
  for (let i=path.length-1; i>=0; i--)
1249
1643
  root = root.childNodes[path[i]];
1250
1644
  return root;
1251
- }
1252
-
1253
-
1254
- // TODO: Memory from this is never freed. Use a WeakMap<Node, Object<eventName:string, function[]>>
1255
- let nodeEvents = {};
1256
- let nodeEventArgs = {};
1645
+ }
1257
1646
 
1258
1647
  /**
1259
1648
  * A Shell is created from a tagged template expression instantiated as Nodes,
1260
- * but without any expressions filled in. */
1649
+ * but without any expressions filled in.
1650
+ * Only one Shell is created for all the items in a loop.
1651
+ *
1652
+ * When a NodeGroup is created from a Template's html strings,
1653
+ * the NodeGroup then clones the Shell's fragmentn to be its nodes. */
1261
1654
  class Shell {
1262
1655
 
1263
1656
  /**
1264
- * @type {DocumentFragment} Parent of the shell nodes. */
1657
+ * @type {DocumentFragment} DOM parent of the shell nodes. */
1265
1658
  fragment;
1266
1659
 
1267
1660
  /** @type {ExprPath[]} Paths to where expressions should go. */
1268
1661
  paths = [];
1269
1662
 
1270
- /** @type {?Template} Template that created this element. */
1271
- template;
1272
-
1273
1663
  // Embeds and ids
1274
1664
  events = [];
1275
1665
 
@@ -1281,6 +1671,7 @@ class Shell {
1281
1671
  staticComponents = [];
1282
1672
 
1283
1673
 
1674
+
1284
1675
  /**
1285
1676
  * Create the nodes but without filling in the expressions.
1286
1677
  * This is useful because the expression-less nodes created by a template can be cached.
@@ -1372,7 +1763,9 @@ class Shell {
1372
1763
  let parts = attr.value.split(/[\ue000-\uf8ff]/g);
1373
1764
  if (parts.length > 1) {
1374
1765
  let nonEmptyParts = (parts.length === 2 && !parts[0].length && !parts[1].length) ? null : parts;
1375
- this.paths.push(new ExprPath(null, node, PathType.Value, attr.name, nonEmptyParts));
1766
+ let type = isEvent(attr.name) ? PathType.Event : PathType.Value;
1767
+
1768
+ this.paths.push(new ExprPath(null, node, type, attr.name, nonEmptyParts));
1376
1769
  node.setAttribute(attr.name, parts.join(''));
1377
1770
  }
1378
1771
  }
@@ -1384,7 +1777,7 @@ class Shell {
1384
1777
  // Get or create nodeBefore.
1385
1778
  let nodeBefore = node.previousSibling; // Can be the same as another Path's nodeMarker.
1386
1779
  if (!nodeBefore) {
1387
- nodeBefore = document.createComment('PathStart:'+this.paths.length);
1780
+ nodeBefore = document.createComment('ExprPath:'+this.paths.length);
1388
1781
  node.parentNode.insertBefore(nodeBefore, node);
1389
1782
  }
1390
1783
 
@@ -1400,14 +1793,14 @@ class Shell {
1400
1793
  // Re-use existing comment placeholder.
1401
1794
  else {
1402
1795
  nodeMarker = node;
1403
- nodeMarker.textContent = 'PathEnd:'+ this.paths.length;
1796
+ nodeMarker.textContent = 'ExprPathEnd:'+ this.paths.length;
1404
1797
  }
1405
1798
 
1406
1799
 
1407
1800
 
1408
1801
 
1409
1802
  let path = new ExprPath(nodeBefore, nodeMarker, PathType.Content);
1410
-
1803
+
1411
1804
  this.paths.push(path);
1412
1805
  }
1413
1806
 
@@ -1421,7 +1814,6 @@ class Shell {
1421
1814
  for (let i=0; i<parts.length-1; i++) {
1422
1815
  let path = new ExprPath(node.previousSibling, node);
1423
1816
  path.type = PathType.Comment;
1424
-
1425
1817
  this.paths.push(path);
1426
1818
  }
1427
1819
  }
@@ -1441,7 +1833,6 @@ class Shell {
1441
1833
 
1442
1834
  for (let i=0, node; node=placeholders[i]; i++) {
1443
1835
  let path = new ExprPath(node.previousSibling, node, PathType.Content);
1444
-
1445
1836
  this.paths.push(path);
1446
1837
 
1447
1838
 
@@ -1472,22 +1863,25 @@ class Shell {
1472
1863
  path.nodeMarkerPath = getNodePath(path.nodeMarker);
1473
1864
 
1474
1865
  // Cache so we don't have to calculate this later inside NodeGroup.applyExprs()
1475
- if (path.type === PathType.Value && path.nodeMarker.nodeType === 1 &&
1866
+ if (path.type === PathType.Value && path.nodeMarker.nodeType === 1 && /*path.nodeMarker !== template.content.children[0] &&*/
1476
1867
  (path.nodeMarker.tagName.includes('-') || path.nodeMarker.hasAttribute('is'))) {
1477
1868
  path.type = PathType.Component;
1478
1869
  }
1479
1870
  }
1480
1871
 
1481
-
1482
1872
  this.findEmbeds();
1483
1873
 
1484
-
1485
1874
 
1486
1875
  } // end constructor
1487
1876
 
1488
1877
  /**
1489
1878
  * We find the path to every embed here once in the Shell, instead of every time a NodeGroup is instantiated.
1490
- * When a Nodegroup is created, it calls NodeGroup.activateEmbeds() that uses these paths. */
1879
+ * When a Nodegroup is created, it calls NodeGroup.activateEmbeds() that uses these paths.
1880
+ * Populates:
1881
+ * this.scripts
1882
+ * this.styles
1883
+ * this.ids
1884
+ * this.staticComponents */
1491
1885
  findEmbeds() {
1492
1886
  this.scripts = Array.prototype.map.call(this.fragment.querySelectorAll('scripts'), el => getNodePath(el));
1493
1887
  this.styles = Array.prototype.map.call(this.fragment.querySelectorAll('style'), el => getNodePath(el));
@@ -1499,7 +1893,7 @@ class Shell {
1499
1893
  for (let el of idEls) {
1500
1894
  let id = el.getAttribute('data-id') || el.getAttribute('id');
1501
1895
  if (div.hasOwnProperty(id))
1502
- throw new Error(`<${el.tagName.toLowerCase()} id="${id}"> can't override existing HTMLElement property.`)
1896
+ throw new Error(`<${el.tagName.toLowerCase()} id="${id}"> can't override existing HTMLElement id property.`)
1503
1897
  }
1504
1898
 
1505
1899
 
@@ -1511,228 +1905,36 @@ class Shell {
1511
1905
  if (isEvent(attrib.name))
1512
1906
  this.events.push([attrib.name, getNodePath(el)]);
1513
1907
 
1514
- if (el.tagName.includes('-') || el.hasAttribute('_is'))
1515
-
1516
- // Dynamic components have attributes with expression values.
1517
- // They are created from applyExprs()
1518
- // But static components are created in a separate path inside the NodeGroup constructor.
1519
- if (!this.paths.find(path => path.nodeMarker === el))
1520
- this.staticComponents.push(getNodePath(el));
1521
- }
1522
-
1523
- }
1524
-
1525
- /**
1526
- * Get the shell for the html strings.
1527
- * @param htmlStrings {string[]}
1528
- * @returns {Shell} */
1529
- static get(htmlStrings) {
1530
- let result = shells.get(htmlStrings);
1531
- if (!result) {
1532
- result = new Shell(htmlStrings);
1533
- shells.set(htmlStrings, result); // cache
1534
- }
1535
-
1536
-
1537
- return result;
1538
- }
1539
-
1540
-
1541
- }
1542
-
1543
- let shells = new WeakMap();
1544
-
1545
- /**
1546
- * ISC License
1547
- *
1548
- * Copyright (c) 2020, Andrea Giammarchi, @WebReflection
1549
- *
1550
- * Permission to use, copy, modify, and/or distribute this software for any
1551
- * purpose with or without fee is hereby granted, provided that the above
1552
- * copyright notice and this permission notice appear in all copies.
1553
- *
1554
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
1555
- * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
1556
- * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
1557
- * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
1558
- * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
1559
- * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
1560
- * PERFORMANCE OF THIS SOFTWARE.
1561
- */
1562
-
1563
- /**
1564
- * @param {Node} parentNode The container where children live
1565
- * @param {Node[]} a The list of current/live children
1566
- * @param {Node[]} b The list of future children
1567
- * @param {(entry: Node, action: number) => Node} get
1568
- * The callback invoked per each entry related DOM operation.
1569
- * @param {Node} [before] The optional node used as anchor to insert before.
1570
- * @returns {Node[]} The same list of future children.
1571
- */
1572
- const udomdiff = (parentNode, a, b, before) => {
1573
-
1574
-
1575
- const bLength = b.length;
1576
- let aEnd = a.length;
1577
- let bEnd = bLength;
1578
- let aStart = 0;
1579
- let bStart = 0;
1580
- let map = null;
1581
- while (aStart < aEnd || bStart < bEnd) {
1582
- // append head, tail, or nodes in between: fast path
1583
- if (aEnd === aStart) {
1584
- // we could be in a situation where the rest of nodes that
1585
- // need to be added are not at the end, and in such case
1586
- // the node to `insertBefore`, if the index is more than 0
1587
- // must be retrieved, otherwise it's gonna be the first item.
1588
- const node = bEnd < bLength
1589
- ? (bStart
1590
- ? (b[bStart - 1].nextSibling)
1591
- : b[bEnd - bStart])
1592
- : before;
1593
- while (bStart < bEnd) {
1594
- let bNode = b[bStart++];
1595
- parentNode.insertBefore(bNode, node);
1596
-
1597
-
1598
- }
1599
- }
1600
- // remove head or tail: fast path
1601
- else if (bEnd === bStart) {
1602
- while (aStart < aEnd) {
1603
- // remove the node only if it's unknown or not live
1604
- let aNode = a[aStart];
1605
- if (!map || !map.has(aNode)) {
1606
- parentNode.removeChild(aNode);
1607
-
1608
-
1609
- }
1610
- aStart++;
1611
- }
1612
- }
1613
- // same node: fast path
1614
- else if (a[aStart] === b[bStart]) {
1615
- aStart++;
1616
- bStart++;
1617
- }
1618
- // same tail: fast path
1619
- else if (a[aEnd - 1] === b[bEnd - 1]) {
1620
- aEnd--;
1621
- bEnd--;
1622
- }
1623
- // The once here single last swap "fast path" has been removed in v1.1.0
1624
- // https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85
1625
- // reverse swap: also fast path
1626
- else if (
1627
- a[aStart] === b[bEnd - 1] &&
1628
- b[bStart] === a[aEnd - 1]
1629
- ) {
1630
- // this is a "shrink" operation that could happen in these cases:
1631
- // [1, 2, 3, 4, 5]
1632
- // [1, 4, 3, 2, 5]
1633
- // or asymmetric too
1634
- // [1, 2, 3, 4, 5]
1635
- // [1, 2, 3, 5, 6, 4]
1636
- const node = a[--aEnd].nextSibling;
1637
-
1638
-
1639
- let a2 = b[bStart++];
1640
- let b2 = a[aStart++];
1641
- parentNode.insertBefore(
1642
- a2,
1643
- b2.nextSibling
1644
- );
1645
-
1646
-
1647
- let bNode = b[--bEnd];
1648
- parentNode.insertBefore(bNode, node);
1649
-
1650
-
1651
-
1652
- // mark the future index as identical (yeah, it's dirty, but cheap 👍)
1653
- // The main reason to do this, is that when a[aEnd] will be reached,
1654
- // the loop will likely be on the fast path, as identical to b[bEnd].
1655
- // In the best case scenario, the next loop will skip the tail,
1656
- // but in the worst one, this node will be considered as already
1657
- // processed, bailing out pretty quickly from the map index check
1658
- a[aEnd] = b[bEnd];
1659
- }
1660
- // map based fallback, "slow" path
1661
- else {
1662
- // the map requires an O(bEnd - bStart) operation once
1663
- // to store all future nodes indexes for later purposes.
1664
- // In the worst case scenario, this is a full O(N) cost,
1665
- // and such scenario happens at least when all nodes are different,
1666
- // but also if both first and last items of the lists are different
1667
- if (!map) {
1668
- map = new Map;
1669
- let i = bStart;
1670
- while (i < bEnd)
1671
- map.set(b[i], i++);
1672
- }
1673
- // if it's a future node, hence it needs some handling
1674
- if (map.has(a[aStart])) {
1675
- // grab the index of such node, 'cause it might have been processed
1676
- const index = map.get(a[aStart]);
1677
- // if it's not already processed, look on demand for the next LCS
1678
- if (bStart < index && index < bEnd) {
1679
- let i = aStart;
1680
- // counts the amount of nodes that are the same in the future
1681
- let sequence = 1;
1682
- while (++i < aEnd && i < bEnd && map.get(a[i]) === (index + sequence))
1683
- sequence++;
1684
- // effort decision here: if the sequence is longer than replaces
1685
- // needed to reach such sequence, which would brings again this loop
1686
- // to the fast path, prepend the difference before a sequence,
1687
- // and move only the future list index forward, so that aStart
1688
- // and bStart will be aligned again, hence on the fast path.
1689
- // An example considering aStart and bStart are both 0:
1690
- // a: [1, 2, 3, 4]
1691
- // b: [7, 1, 2, 3, 6]
1692
- // this would place 7 before 1 and, from that time on, 1, 2, and 3
1693
- // will be processed at zero cost
1694
- if (sequence > (index - bStart)) {
1695
- const node = a[aStart];
1696
- while (bStart < index) {
1697
- let bNode = b[bStart++];
1698
- parentNode.insertBefore(bNode, node);
1699
-
1700
-
1701
- }
1702
- }
1703
- // if the effort wasn't good enough, fallback to a replace,
1704
- // moving both source and target indexes forward, hoping that some
1705
- // similar node will be found later on, to go back to the fast path
1706
- else {
1707
- let aNode = a[aStart++];
1708
- let bNode = b[bStart++];
1709
- parentNode.replaceChild(
1710
- bNode,
1711
- aNode
1712
- );
1713
-
1714
-
1715
- }
1716
- }
1717
- // otherwise move the source forward, 'cause there's nothing to do
1718
- else
1719
- aStart++;
1720
- }
1721
- // this node has no meaning in the future list, so it's more than safe
1722
- // to remove it, and check the next live node out instead, meaning
1723
- // that only the live list index should be forwarded
1724
- else {
1725
- let aNode = a[aStart++];
1726
- parentNode.removeChild(aNode);
1727
-
1728
-
1729
- }
1908
+ if (el.tagName.includes('-') || el.hasAttribute('_is'))
1909
+
1910
+ // Dynamic components have attributes with expression values.
1911
+ // They are created from applyExprs()
1912
+ // But static components are created in a separate path inside the NodeGroup constructor.
1913
+ if (!this.paths.find(path => path.nodeMarker === el))
1914
+ this.staticComponents.push(getNodePath(el));
1730
1915
  }
1916
+
1731
1917
  }
1732
- return b;
1733
- };
1918
+
1919
+ /**
1920
+ * Get the shell for the html strings.
1921
+ * @param htmlStrings {string[]} Typically comes from a Template.
1922
+ * @returns {Shell} */
1923
+ static get(htmlStrings) {
1924
+ let result = Globals.shells.get(htmlStrings);
1925
+ if (!result) {
1926
+ result = new Shell(htmlStrings);
1927
+ Globals.shells.set(htmlStrings, result); // cache
1928
+ }
1929
+
1930
+
1931
+ return result;
1932
+ }
1933
+
1934
+
1935
+ }
1734
1936
 
1735
- /** @typedef {boolean|string|number|function|Object|Array|Date|Node} Expr */
1937
+ /** @typedef {boolean|string|number|function|Object|Array|Date|Node|Template} Expr */
1736
1938
 
1737
1939
  /**
1738
1940
  * A group of Nodes instantiated from a Shell, with Expr's filled in.
@@ -1745,16 +1947,17 @@ const udomdiff = (parentNode, a, b, before) => {
1745
1947
  * */
1746
1948
  class NodeGroup {
1747
1949
 
1748
- /** @Type {NodeGroupManager} */
1749
- manager;
1950
+ /**
1951
+ * @Type {RootNodeGroup} */
1952
+ rootNg;
1750
1953
 
1751
1954
  /** @type {ExprPath} */
1752
1955
  parentPath;
1753
1956
 
1754
- /** @type {Node} First node of NodeGroup. Should never be null. */
1957
+ /** @type {Node|HTMLElement} First node of NodeGroup. Should never be null. */
1755
1958
  startNode;
1756
1959
 
1757
- /** @type {Node} A node that never changes that this NodeGroup should always insert its nodes before.
1960
+ /** @type {Node|HTMLElement} A node that never changes that this NodeGroup should always insert its nodes before.
1758
1961
  * An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.*/
1759
1962
  endNode;
1760
1963
 
@@ -1764,13 +1967,9 @@ class NodeGroup {
1764
1967
  /** @type {string} Key that matches the template and the expressions. */
1765
1968
  exactKey;
1766
1969
 
1767
- /** @type {string} Key that only matches the template. */
1970
+ /** @type {string} Key that only matches the template. */
1768
1971
  closeKey;
1769
1972
 
1770
- /** @type {boolean} Used by NodeGroupManager. */
1771
- inUse;
1772
-
1773
-
1774
1973
  /**
1775
1974
  * @internal
1776
1975
  * @type {Node[]} Cached result of getNodes() used only for improving performance.*/
@@ -1780,133 +1979,58 @@ class NodeGroup {
1780
1979
  * @type {?Map<HTMLStyleElement, string>} */
1781
1980
  styles;
1782
1981
 
1783
- /**
1784
- * If rendering a Template with replaceMode=true, pseudoRoot points to the element where the attributes are rendered.
1785
- * But pseudoRoot is outside of this.getNodes().
1786
- * NodeGroupManager.render() copies the attributes from pseudoRoot to the actual web component root element.
1787
- * @type {?HTMLElement} */
1788
- pseudoRoot;
1789
-
1790
1982
  currentComponentProps = {};
1791
1983
 
1792
1984
 
1793
1985
  /**
1794
1986
  * Create an "instantiated" NodeGroup from a Template and add it to an element.
1795
1987
  * @param template {Template} Create it from the html strings and expressions in this template.
1796
- * @param manager {?NodeGroupManager}
1797
- * @returns {NodeGroup} */
1798
- constructor(template, manager=null) {
1799
-
1800
- /** @type {Template} */
1801
- this.template = template;
1802
-
1803
- /** @type {NodeGroupManager} */
1804
- this.manager = manager;
1805
-
1806
- // new!
1807
- template.nodeGroup = this;
1808
-
1809
- // Get a cached version of the parsed and instantiated html, and ExprPaths.
1810
- let shell = Shell.get(template.html);
1811
-
1812
- let fragment = shell.fragment.cloneNode(true);
1813
-
1814
- // Figure out value of replaceMode option if it isn't set,
1815
- // Assume replaceMode if there's only one child element and its tagname matches the root el.
1816
- let replaceMode = typeof template.replaceMode === 'boolean'
1817
- ? template.replaceMode
1818
- : fragment.children.length===1 &&
1819
- fragment.firstElementChild?.tagName.replace(/-SOLARITE-PLACEHOLDER$/, '')
1820
- === manager?.rootEl?.tagName;
1821
- if (replaceMode) {
1822
- this.pseudoRoot = fragment.firstElementChild;
1823
- // if (!manager.rootEl)
1824
- // manager.rootEl = this.pseudoRoot;
1825
-
1826
- }
1827
-
1828
- let childNodes = replaceMode
1829
- ? fragment.firstElementChild.childNodes
1830
- : fragment.childNodes;
1988
+ * @param parentPath {?ExprPath} */
1989
+ constructor(template, parentPath=null) {
1990
+ if (!(this instanceof RootNodeGroup)) {
1991
+ let [fragment, shell] = this.init(template, parentPath);
1831
1992
 
1993
+ this.updatePaths(fragment, shell.paths);
1832
1994
 
1833
- this.startNode = childNodes[0];
1834
- this.endNode = childNodes[childNodes.length - 1];
1995
+ this.activateEmbeds(fragment, shell);
1835
1996
 
1836
-
1837
- // Update paths
1838
- for (let oldPath of shell.paths) {
1839
- let path = oldPath.clone(fragment);
1840
- path.parentNg = this;
1841
- this.paths.push(path);
1997
+ // Apply exprs
1998
+ this.applyExprs(template.exprs);
1842
1999
  }
1843
-
1844
-
1845
- // Update web component placeholders.
1846
- // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
1847
- // Is this list needed at all?
1848
- //for (let component of shell.components)
1849
- // this.components.push(resolveNodePath(this.startNode.parentNode, getNodePath(component)))
1850
-
1851
-
1852
-
1853
- this.activateEmbeds(fragment, shell);
1854
-
1855
-
1856
-
1857
- // Apply exprs
1858
- this.applyExprs(template.exprs);
1859
-
1860
-
1861
2000
  }
1862
2001
 
1863
- activateEmbeds(root, shell) {
1864
-
1865
- // static components
1866
- // Must happen before ids.
1867
- for (let path of shell.staticComponents) {
1868
- let el = resolveNodePath(root, path);
2002
+ /**
2003
+ * Common init shared by RootNodeGroup and NodeGroup constructors.
2004
+ * But in a separate function because they need to do this at a different step.
2005
+ * @param template {Template} Create it from the html strings and expressions in this template.
2006
+ * @param parentPath {?ExprPath}
2007
+ * @param exactKey {?string} Optional, if already calculated.
2008
+ * @param closeKey {?string}
2009
+ * @returns {[DocumentFragment, Shell]} */
2010
+ init(template, parentPath=null, exactKey=null, closeKey=null) {
2011
+ this.exactKey = exactKey || template.getExactKey();
2012
+ this.closeKey = closeKey || template.getCloseKey();
1869
2013
 
1870
- // Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
1871
- if (el.tagName !== this.pseudoRoot?.tagName)
1872
- this.createNewComponent(el);
1873
- }
2014
+ this.parentPath = parentPath;
2015
+ this.rootNg = parentPath?.parentNg?.rootNg || this;
1874
2016
 
1875
- if (this.manager?.rootEl) {
2017
+
1876
2018
 
1877
- // ids
1878
- if (this.manager.options.ids !== false)
1879
- for (let path of shell.ids) {
1880
- let el = resolveNodePath(root, path);
1881
- let id = el.getAttribute('data-id') || el.getAttribute('id');
2019
+ /** @type {Template} */
2020
+ this.template = template;
1882
2021
 
1883
- // Don't allow overwriting existing class properties if they already have a non-Node value.
1884
- if (this.manager.rootEl[id] && !(this.manager.rootEl[id] instanceof Node))
1885
- throw new Error(`${this.manager.rootEl.constructor.name}.${id} already has a value. `+
1886
- `Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
2022
+ // new! Is this needed?
2023
+ template.nodeGroup = this;
1887
2024
 
1888
- this.manager.rootEl[id] = el;
1889
- }
2025
+ // Get a cached version of the parsed and instantiated html, and ExprPaths.
2026
+ let shell = Shell.get(template.html);
2027
+ let fragment = shell.fragment.cloneNode(true);
1890
2028
 
1891
- // styles
1892
- if (this.manager.options.styles !== false) {
1893
- if (shell.styles.length)
1894
- this.styles = new Map();
1895
- for (let path of shell.styles) {
1896
- let style = resolveNodePath(root, path);
1897
- Util.bindStyles(style, this.manager.rootEl);
1898
- this.styles.set(style, style.textContent);
1899
- }
2029
+ let childNodes = fragment.childNodes;
2030
+ this.startNode = childNodes[0];
2031
+ this.endNode = childNodes[childNodes.length - 1];
1900
2032
 
1901
- }
1902
- // scripts
1903
- if (this.manager.options.scripts !== false) {
1904
- for (let path of shell.scripts) {
1905
- let script = resolveNodePath(root, path);
1906
- eval(script.textContent);
1907
- }
1908
- }
1909
- }
2033
+ return [fragment, shell];
1910
2034
  }
1911
2035
 
1912
2036
  /**
@@ -1916,8 +2040,9 @@ class NodeGroup {
1916
2040
  * @param paths {?ExprPath[]} Optional. */
1917
2041
  applyExprs(exprs, paths=null) {
1918
2042
  paths = paths || this.paths;
2043
+
1919
2044
 
1920
-
2045
+
1921
2046
  // Update exprs at paths.
1922
2047
  let exprIndex = exprs.length-1, expr, lastNode;
1923
2048
 
@@ -1927,54 +2052,24 @@ class NodeGroup {
1927
2052
  expr = exprs[exprIndex];
1928
2053
 
1929
2054
  // Nodes
1930
- if (path.type === PathType.Content) {
1931
- this.applyNodeExpr(path, expr);
1932
-
1933
- }
1934
-
1935
- // Attributes
1936
- else {
1937
- let node = path.nodeMarker;
1938
- let el = (this.manager?.rootEl && node === this.pseudoRoot) ? this.manager.rootEl : node;
1939
-
1940
-
1941
- // This is necessary both here and below.
1942
- if (lastNode && lastNode !== this.pseudoRoot && lastNode !== node && Object.keys(this.currentComponentProps).length) {
1943
- this.applyComponentExprs(lastNode, this.currentComponentProps);
1944
- this.currentComponentProps = {};
1945
- }
1946
-
1947
- if (path.type === PathType.Multiple)
1948
- path.applyMultipleAttribs(el, expr);
1949
2055
 
1950
- // Capture attribute expressions to later send to the constructor of a web component.
1951
- // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
1952
- else if (path.nodeMarker !== this.pseudoRoot && path.type === PathType.Component)
1953
- this.currentComponentProps[path.attrName] = expr;
1954
-
1955
- else if (path.type === PathType.Comment) ;
1956
- else {
2056
+ // This is necessary both here and below.
2057
+ if (lastNode && lastNode !== this.rootNg.root && lastNode !== path.nodeMarker && Object.keys(this.currentComponentProps).length) {
2058
+ this.applyComponentExprs(lastNode, this.currentComponentProps);
2059
+ this.currentComponentProps = {};
2060
+ }
1957
2061
 
1958
- // Event attribute value
1959
- if (path.attrValue===null && (typeof expr === 'function' || Array.isArray(expr)) && isEvent(path.attrName)) {
1960
- let root = this.manager?.rootEl || this.startNode.parentNode; // latter is used when constructing a whole element.
1961
- path.applyEventAttrib(el, expr, root);
1962
- }
2062
+ exprIndex = path.apply(expr, exprs, exprIndex, this.currentComponentProps);
1963
2063
 
1964
- // Regular attribute value.
1965
- else // One node value may have multiple expressions. Here we apply them all at once.
1966
- exprIndex = path.applyValueAttrib(el, exprs, exprIndex);
1967
- }
2064
+ lastNode = path.nodeMarker;
1968
2065
 
1969
- lastNode = path.nodeMarker;
1970
- }
1971
2066
 
1972
2067
  exprIndex--;
1973
2068
  } // end for(path of this.paths)
1974
2069
 
1975
2070
 
1976
2071
  // Check again after we iterate through all paths to apply to a component.
1977
- if (lastNode && lastNode !== this.pseudoRoot && Object.keys(this.currentComponentProps).length) {
2072
+ if (lastNode && lastNode !== this.rootNg.root && Object.keys(this.currentComponentProps).length) {
1978
2073
  this.applyComponentExprs(lastNode, this.currentComponentProps);
1979
2074
  this.currentComponentProps = {};
1980
2075
  }
@@ -1990,125 +2085,10 @@ class NodeGroup {
1990
2085
 
1991
2086
 
1992
2087
 
1993
- }
1994
-
1995
- applyExpr(path, expr) {
1996
- // TODO: Use this if I can figure out how to adapt applyValueAttrib() to it.
1997
- }
1998
-
1999
- /**
2000
- * Insert/replace the nodes created by a single expression.
2001
- * Called by applyExprs()
2002
- * This function is recursive, as the functions it calls also call it.
2003
- * TODO: Move this to ExprPath?
2004
- * @param path {ExprPath}
2005
- * @param expr {Expr}
2006
- * @return {Node[]} New Nodes created. */
2007
- applyNodeExpr(path, expr) {
2008
-
2009
-
2010
- /** @type {(Node|NodeGroup|Expr)[]} */
2011
- let newNodes = [];
2012
- let oldNodeGroups = path.nodeGroups;
2013
-
2014
- let secondPass = []; // indices
2015
-
2016
- // First Pass
2017
- //for (let ng of path.nodeGroups) // TODO: Is this necessary?
2018
- // ng.parentPath = null;
2019
- path.nodeGroups = [];
2020
- path.apply(expr, newNodes, secondPass);
2021
- this.existingTextNodes = null;
2022
-
2023
- // TODO: Create an array of old vs Nodes and NodeGroups together.
2024
- // If they're all the same, skip the next steps.
2025
- // Or calculate it in the loop above as we go? Have a path.lastNodeGroups property?
2026
-
2027
- // Second pass to find close-match NodeGroups.
2028
- let flatten = false;
2029
- if (secondPass.length) {
2030
- for (let [nodesIndex, ngIndex] of secondPass) {
2031
- let ng = this.manager.getNodeGroup(newNodes[nodesIndex], false);
2032
-
2033
- ng.parentPath = path;
2034
- let ngNodes = ng.getNodes();
2035
-
2036
-
2037
-
2038
- if (ngNodes.length === 1)
2039
- newNodes[nodesIndex] = ngNodes[0];
2040
-
2041
- else {
2042
- newNodes[nodesIndex] = ngNodes;
2043
- flatten = true;
2044
- }
2045
- path.nodeGroups[ngIndex] = ng;
2046
- }
2047
-
2048
- if (flatten)
2049
- newNodes = newNodes.flat(); // TODO: Only if second pass happens?
2050
- }
2051
-
2052
-
2053
-
2054
-
2055
-
2056
- let oldNodes = path.getNodes();
2057
- path.nodesCache = newNodes; // Replaces value set by path.getNodes()
2058
-
2059
-
2060
- // This pre-check makes it a few percent faster?
2061
- let diff = findArrayDiff(oldNodes, newNodes);
2062
- if (diff !== false) {
2063
-
2064
- if (this.parentPath)
2065
- this.parentPath.clearNodesCache();
2066
-
2067
- // Fast clear method
2068
- let isNowEmpty = oldNodes.length && !newNodes.length;
2069
- if (!isNowEmpty || !path.fastClear(oldNodes, newNodes))
2070
-
2071
- // Rearrange nodes.
2072
- udomdiff(path.parentNode, oldNodes, newNodes, path.nodeMarker);
2073
-
2074
- this.saveOrphans(oldNodeGroups, oldNodes);
2075
- }
2076
-
2077
- }
2078
-
2079
- /**
2080
- * Find NodeGroups that had their nodes removed and add those nodes to a Fragment so
2081
- * they're not lost forever and the NodeGroup's internal structure is still consistent.
2082
- * Called from NodeGroup.applyNodeExpr().
2083
- * @param oldNodeGroups {NodeGroup[]}
2084
- * @param oldNodes {Node[]} */
2085
- saveOrphans(oldNodeGroups, oldNodes) {
2086
- let oldNgMap = new Map();
2087
- for (let ng of oldNodeGroups) {
2088
- oldNgMap.set(ng.startNode, ng);
2089
-
2090
- // TODO: Is this necessary?
2091
- // if (ng.parentPath)
2092
- // ng.parentPath.clearNodesCache();
2093
- }
2094
-
2095
- for (let i=0, node; node = oldNodes[i]; i++) {
2096
- let ng;
2097
- if (!node.parentNode && (ng = oldNgMap.get(node))) {
2098
- let fragment = document.createDocumentFragment();
2099
- let endNode = ng.endNode;
2100
- while (node !== endNode) {
2101
- fragment.append(node);
2102
- i++;
2103
- node = oldNodes[i];
2104
- }
2105
- fragment.append(endNode);
2106
- }
2107
- }
2108
2088
  }
2109
2089
 
2110
2090
  /**
2111
- * Create a nested RedComponent or call render with the new props.
2091
+ * Create a nested Component or call render with the new props.
2112
2092
  * @param el {Solarite:HTMLElement}
2113
2093
  * @param props {Object} */
2114
2094
  applyComponentExprs(el, props) {
@@ -2126,14 +2106,14 @@ class NodeGroup {
2126
2106
  if (isPreHtmlElement || isPreIsElement)
2127
2107
  el = this.createNewComponent(el, isPreHtmlElement, props);
2128
2108
 
2129
- // Update params of placeholder.
2109
+ // Call render() with the same params that would've been passed to the constructor.
2130
2110
  else if (el.render) {
2131
- let oldHash = componentHash.get(el);
2111
+ let oldHash = Globals.componentHash.get(el);
2132
2112
  if (oldHash !== newHash)
2133
2113
  el.render(props); // Pass new values of props to render so it can decide how it wants to respond.
2134
2114
  }
2135
2115
 
2136
- componentHash.set(el, newHash);
2116
+ Globals.componentHash.set(el, newHash);
2137
2117
  }
2138
2118
 
2139
2119
  /**
@@ -2176,33 +2156,43 @@ class NodeGroup {
2176
2156
  // We pass the childNodes to the constructor so it can know about them,
2177
2157
  // instead of only afterward when they're appended to the slot below.
2178
2158
  // This is useful for a custom selectbox, for example.
2179
- // NodeGroupManager.pendingChildren stores the childen so the super construtor call to Solarite's constructor
2159
+ // Globals.pendingChildren stores the childen so the super construtor call to Solarite's constructor
2180
2160
  // can add them as children before the rest of the constructor code executes.
2181
2161
  let ch = [... el.childNodes];
2182
- NodeGroupManager.pendingChildren.push(ch); // pop() is called in Solarite constructor.
2162
+ Globals.pendingChildren.push(ch); // pop() is called in Solarite constructor.
2183
2163
  let newEl = new Constructor(props, ch);
2184
2164
 
2185
2165
  if (!isPreHtmlElement)
2186
2166
  newEl.setAttribute('is', el.getAttribute('is').toLowerCase());
2187
2167
  el.replaceWith(newEl);
2188
-
2168
+
2189
2169
  // Set children / slot children
2190
2170
  // TODO: Match named slots.
2191
2171
  // TODO: This only appends to slot if render() is called in the constructor.
2192
2172
  //let slot = newEl.querySelector('slot') || newEl;
2193
2173
  //slot.append(...el.childNodes);
2194
-
2174
+
2195
2175
  // Copy over event attributes.
2196
2176
  for (let propName in props) {
2197
2177
  let val = props[propName];
2198
2178
  if (propName.startsWith('on') && typeof val === 'function')
2199
2179
  newEl.addEventListener(propName.slice(2), e => val(e, newEl));
2180
+
2181
+ // Bind array based event attributes on value.
2182
+ // This same logic is in ExprPath.applyValueAttrib() for non-components.
2183
+ if ((propName === 'value' || propName === 'data-value') && Util.isPath(val)) {
2184
+ let [obj, path] = [val[0], val.slice(1)];
2185
+ newEl.value = delve(obj, path);
2186
+ newEl.addEventListener('input', e => {
2187
+ delve(obj, path, Util.getInputValue(newEl));
2188
+ }, true); // We use capture so we update the values before other events added by the user.
2189
+ }
2200
2190
  }
2201
2191
 
2202
2192
  // If an id pointed at the placeholder, update it to point to the new element.
2203
2193
  let id = el.getAttribute('data-id') || el.getAttribute('id');
2204
2194
  if (id)
2205
- this.manager.rootEl[id] = newEl;
2195
+ delve(this.getRootNode(), id.split(/\./g), newEl);
2206
2196
 
2207
2197
 
2208
2198
  // Update paths to use replaced element.
@@ -2267,373 +2257,347 @@ class NodeGroup {
2267
2257
  return result;
2268
2258
  }
2269
2259
 
2270
- getParentNode() {
2271
- return this.startNode?.parentNode
2272
- }
2273
-
2274
-
2275
-
2276
- updateStyles() {
2277
- if (this.styles)
2278
- for (let [style, oldText] of this.styles) {
2279
- let newText = style.textContent;
2280
- if (oldText !== newText)
2281
- Util.bindStyles(style, this.manager.rootEl);
2282
- }
2283
- }
2284
-
2285
-
2286
-
2287
- }
2288
-
2289
-
2290
- let componentHash = new WeakMap();
2291
-
2292
- /**
2293
- * Tools for watch variables and performing precise renders.
2294
- */
2295
-
2296
- function serializePath(path) {
2297
- // Convert any array indices to strings, so serialized comparisons work.
2298
- return JSON.stringify([getObjectId(path[0]), ...path.slice(1).map(item => item+'')])
2299
-
2300
- }
2301
-
2302
- /**
2303
- * @typedef {Object} RenderOptions
2304
- * @property {boolean=} styles - Indicates whether the Courage component is present.
2305
- * @property {boolean=} scripts - Indicates whether the Power component is present.
2306
- * @property {boolean=} ids *
2307
- * @property {?boolean} render
2308
- * Used only when options are given to a class super constructor inheriting from Solarite.
2309
- * True to call render() immediately in super constructor.
2310
- * False to automatically call render() at all.
2311
- * Undefined (default) to call render() when added to the DOM, unless already rendered.
2312
- */
2313
-
2314
-
2315
- /**
2316
- * Manage all the NodeGroups for a single WebComponent or root HTMLElement
2317
- * There's one NodeGroup for the root of the WebComponent, and one for every ${...} expression that creates Node children.
2318
- * And each NodeGroup manages the one or more nodes created by the expression.
2319
- *
2320
- * An instance of this class exists for each element that r() renders to. */
2321
- class NodeGroupManager {
2322
-
2323
- /** @type {HTMLElement|DocumentFragment} */
2324
- rootEl;
2325
-
2326
- /** @type {NodeGroup} */
2327
- rootNg;
2328
-
2329
- /** @type {Change[]} */
2330
- changes = [];
2331
-
2332
-
2260
+ getParentNode() {
2261
+ return this.startNode?.parentNode
2262
+ }
2333
2263
 
2334
-
2264
+ /**
2265
+ * Get the root element of the NodeGroup's RootNodeGroup.
2266
+ * @returns {HTMLElement|DocumentFragment} */
2267
+ getRootNode() {
2268
+ return this.rootNg.root;
2269
+ }
2335
2270
 
2336
2271
  /**
2337
- * A map from the html strings and exprs that created a node group, to the NodeGroup.
2338
- * Also stores a map from just the html strings to the NodeGroup, so we can still find a similar match if the exprs changed.
2339
- *
2340
- * @type {MultiValueMap<string, (string|Template)[], NodeGroup>} */
2341
- nodeGroupsAvailable = new MultiValueMap();
2342
- nodeGroupsInUse = [];
2272
+ * @returns {RootNodeGroup} */
2273
+ getRootNodeGroup() {
2274
+ return this.rootNg;
2275
+ }
2343
2276
 
2344
2277
 
2345
- /** @type {RenderOptions} */
2346
- options = {};
2278
+ updatePaths(fragment, paths, offset) {
2279
+ // Update paths to point to the fragment.
2280
+ this.paths.length = paths.length;
2281
+ for (let i=0; i<paths.length; i++) {
2282
+ let path = paths[i].clone(fragment, offset);
2283
+ path.parentNg = this;
2284
+ this.paths[i] = path;
2285
+ }
2286
+ }
2287
+
2288
+ updateStyles() {
2289
+ if (this.styles)
2290
+ for (let [style, oldText] of this.styles) {
2291
+ let newText = style.textContent;
2292
+ if (oldText !== newText)
2293
+ Util.bindStyles(style, this.getRootNodeGroup().root);
2294
+ }
2295
+ }
2347
2296
 
2348
2297
 
2349
-
2298
+
2350
2299
 
2351
2300
  /**
2352
- * @param rootEl {HTMLElement|DocumentFragment} If not specified, the first element of the html will be the rootEl. */
2353
- constructor(rootEl=null) {
2354
- this.rootEl = rootEl;
2301
+ * @param root {HTMLElement}
2302
+ * @param shell {Shell}
2303
+ * @param pathOffset {int} */
2304
+ activateEmbeds(root, shell, pathOffset=0) {
2355
2305
 
2356
- /*
2357
-
2358
- */
2359
- }
2306
+ // static components. These are WebComponents not created by an expression.
2307
+ // Must happen before ids.
2308
+ for (let path of shell.staticComponents) {
2309
+ if (pathOffset)
2310
+ path = path.slice(0, -pathOffset);
2311
+ let el = resolveNodePath(root, path);
2360
2312
 
2313
+ // Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
2314
+ if (root !== el/* && !isReplaceEl(root, el)*/) // TODO: is isReplaceEl necessary?
2315
+ this.createNewComponent(el);
2316
+ }
2361
2317
 
2362
- /**
2363
- *
2364
- * 1. Delete a NodeGroup from this.nodeGroupsAvailable that matches this exactKey.
2365
- * 2. Then delete all of that NodeGroup's parents' exactKey entries
2366
- * We don't move them to in-use because we plucked the NodeGroup from them, they no longer match their exactKeys.
2367
- * 3. Then we move all the NodeGroup's exact+close keyed children to inUse because we don't want future calls
2368
- * to getNodeGroup() to borrow the children now that the whole NodeGroup is in-use.
2369
- *
2370
- * TODO: Have NodeGroups keep track of whether they're inUse.
2371
- * That way when we go up or down we don't have to remove those with .inUse===true
2372
- *
2373
- * @param exactKey
2374
- * @param goUp
2375
- * @param child
2376
- * @returns {?NodeGroup} */
2377
- findAndDeleteExact(exactKey, goUp=true, child=undefined) {
2378
-
2379
- let ng = this.nodeGroupsAvailable.delete(exactKey, child);
2380
- if (ng) {
2381
-
2382
-
2383
- // Mark close-key version as in-use.
2384
- let closeNg = this.nodeGroupsAvailable.delete(ng.closeKey, ng);
2385
-
2318
+ let rootEl = this.rootNg.root;
2319
+ if (rootEl) {
2386
2320
 
2387
- // Mark our self as in-use.
2388
- this.nodeGroupsInUse.push(ng);
2389
-
2390
- ng.inUse = true;
2391
- closeNg.inUse = true;
2392
-
2393
- // Mark all parents that have this NodeGroup as a child as in-use.
2394
- // So that way we don't use this parent again
2395
- if (goUp) {
2396
- let ng2 = ng;
2397
- while (ng2 = ng2?.parentPath?.parentNg) {
2398
- if (!ng2.inUse) {
2399
- ng2.inUse = true;
2400
- let success = this.nodeGroupsAvailable.delete(ng2.exactKey, ng2);
2401
- // assert(success);
2402
- let success2 = this.nodeGroupsAvailable.delete(ng2.closeKey, ng2);
2403
- // assert(success);
2404
-
2321
+ // ids
2322
+ if (this.options?.ids !== false)
2323
+ for (let path of shell.ids) {
2324
+ if (pathOffset)
2325
+ path = path.slice(0, -pathOffset);
2326
+ let el = resolveNodePath(root, path);
2327
+ let id = el.getAttribute('data-id') || el.getAttribute('id');
2328
+ if (id) { // If something hasn't removed the id.
2405
2329
 
2406
- // console.log(getHtml(ng2))
2407
- if (success) {
2408
- this.nodeGroupsInUse.push(ng2);
2409
- }
2330
+ // Don't allow overwriting existing class properties if they already have a non-Node value.
2331
+ if (rootEl[id] && !(rootEl[id] instanceof Node))
2332
+ throw new Error(`${rootEl.constructor.name}.${id} already has a value. ` +
2333
+ `Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
2334
+
2335
+ delve(rootEl, id.split(/\./g), el);
2410
2336
  }
2411
2337
  }
2412
- }
2413
2338
 
2414
- // Recurse to mark all child NodeGroups as in-use.
2415
- for (let path of ng.paths)
2416
- for (let childNg of path.nodeGroups) {
2417
- if (!childNg.inUse)
2418
- this.findAndDeleteExact(childNg.exactKey, false, childNg);
2419
- childNg.inUse = true;
2339
+ // styles
2340
+ if (this.options?.styles !== false) {
2341
+ if (shell.styles.length)
2342
+ this.styles = new Map();
2343
+ for (let path of shell.styles) {
2344
+ if (pathOffset)
2345
+ path = path.slice(0, -pathOffset);
2346
+ let style = resolveNodePath(root, path);
2347
+ Util.bindStyles(style, rootEl);
2348
+ this.styles.set(style, style.textContent);
2420
2349
  }
2421
-
2422
- if (ng.parentPath) ;
2423
-
2424
- return ng;
2425
- }
2426
- return null;
2427
- }
2428
-
2429
- /**
2430
- * @param closeKey {string}
2431
- * @param exactKey {string}
2432
- * @param goUp {boolean}
2433
- * @returns {NodeGroup} */
2434
- findAndDeleteClose(closeKey, exactKey, goUp=true) {
2435
- let ng = this.nodeGroupsAvailable.delete(closeKey);
2436
- if (ng) {
2437
-
2438
- // We matched on a new key, so delete the old exactKey.
2439
- let exactNg = this.nodeGroupsAvailable.delete(ng.exactKey, ng);
2440
-
2441
-
2442
-
2443
-
2444
-
2445
- ng.inUse = true;
2446
- if (goUp) {
2447
- let ng2 = ng;
2448
-
2449
- // We borrowed a node from another node group so make sure its parent isn't still an exact match.
2450
- while (ng2 = ng2?.parentPath?.parentNg) {
2451
- if (!ng2.inUse) {
2452
- ng2.inUse = true; // Might speed it up slightly?
2453
- let success = this.nodeGroupsAvailable.delete(ng2.exactKey, ng2);
2454
-
2455
-
2456
- // But it can still be a close match, so we don't use this code.
2457
- success = this.nodeGroupsAvailable.delete(ng2.closeKey, ng2);
2458
-
2459
2350
 
2460
- this.nodeGroupsInUse.push(ng2);
2461
- }
2351
+ }
2352
+ // scripts
2353
+ if (this.options?.scripts !== false) {
2354
+ for (let path of shell.scripts) {
2355
+ if (pathOffset)
2356
+ path = path.slice(0, -pathOffset);
2357
+ let script = resolveNodePath(root, path);
2358
+ eval(script.textContent);
2462
2359
  }
2463
2360
  }
2361
+ }
2362
+ }
2363
+ }
2464
2364
 
2465
- // Recursively mark all child NodeGroups as in-use.
2466
- // We actually DON't want to do this becuse applyExprs is going to swap out the child NodeGroups
2467
- // and mark them as in-use as it goes.
2468
- // that's probably why uncommenting this causes tests to fail.
2469
- // for (let path of ng.paths)
2470
- // for (let childNg of path.nodeGroups)
2471
- // this.findAndDeleteExact(childNg.exactKey, false, childNg);
2472
2365
 
2366
+ class RootNodeGroup extends NodeGroup {
2473
2367
 
2474
- ng.exactKey = exactKey;
2475
- ng.closeKey = closeKey;
2476
- this.nodeGroupsInUse.push(ng);
2477
-
2478
-
2479
- if (ng.parentPath) ;
2480
- }
2481
-
2482
-
2483
- return ng;
2484
- }
2368
+ /**
2369
+ * Root node at the top of the hierarchy.
2370
+ * @type {HTMLElement} */
2371
+ root;
2485
2372
 
2486
2373
  /**
2487
- * Get an existing or create a new NodeGroup that matches the template,
2488
- * but don't reparent it if it's somewhere else.
2489
- * @param template {Template}
2490
- * @param exact {?boolean}
2491
- * @param createForWatch Deprecated.
2492
- * @return {?NodeGroup} */
2493
- getNodeGroup(template, exact=null, createForWatch=false) {
2374
+ *
2375
+ * @param template
2376
+ * @param el
2377
+ * @param options {?object}
2378
+ */
2379
+ constructor(template, el, options) {
2380
+ super(template);
2494
2381
 
2495
- let exactKey = getObjectHash(template);
2382
+ this.options = options;
2496
2383
 
2497
- // 1. Try to find an exact match.
2498
- let ng;
2499
- if (exact === true) {
2500
- ng = this.findAndDeleteExact(exactKey);
2384
+ this.rootNg = this;
2385
+ let [fragment, shell] = this.init(template);
2501
2386
 
2502
- if (!ng) {
2503
-
2504
- return null;
2387
+ // If adding NodeGroup to an element.
2388
+ let offset = 0;
2389
+ let root = fragment; // TODO: Rename so it's not confused with this.root.
2390
+ if (el) {
2391
+
2392
+ // Save slot children
2393
+ let slotFragment;
2394
+ if (el.childNodes.length) {
2395
+ slotFragment = document.createDocumentFragment();
2396
+ slotFragment.append(...el.childNodes);
2505
2397
  }
2506
- }
2507
2398
 
2508
- // 2. Try to find a close match.
2509
- else {
2510
- // We don't need to delete the exact match bc it's already been deleted in the prev pass.
2511
- let closeKey = template.getCloseKey();
2512
- ng = createForWatch ? null : this.findAndDeleteClose(closeKey, exactKey);
2399
+ this.root = el;
2513
2400
 
2514
- // 2. Update expression values if they've changed.
2515
- if (ng) {
2516
-
2517
- // Temporary for debugging:
2518
- if (window.debug && !window.ng)
2519
- window.ng = ng;
2520
-
2521
-
2522
- ng.applyExprs(template.exprs);
2401
+ // If el should replace the root node of the fragment.
2402
+ if (isReplaceEl(fragment, el)) {
2403
+ el.append(...fragment.children[0].childNodes);
2523
2404
 
2524
-
2525
-
2526
- }
2405
+ // Copy attributes
2406
+ for (let attrib of fragment.children[0].attributes)
2407
+ if (!el.hasAttribute(attrib.name))
2408
+ el.setAttribute(attrib.name, attrib.value);
2527
2409
 
2528
- // 3. Or if not found, create a new NodeGroup
2410
+ // Go one level deeper into all of shell's paths.
2411
+ offset = 1;
2412
+ }
2529
2413
  else {
2530
-
2531
- ng = new NodeGroup(template, this);
2532
-
2533
-
2534
-
2535
-
2414
+ let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
2415
+ if (!isEmpty)
2416
+ el.append(...fragment.childNodes);
2417
+ }
2536
2418
 
2537
- // 4. Mark NodeGroup as being in-use.
2538
- // TODO: Moving from one group to another thrashes the gc. Is there a faster way?
2539
- // Could I have just a single WeakSet of those in use?
2540
- // Perhaps also result could cache its last exprKey and then we'd use only one map?
2541
- ng.exactKey = exactKey;
2542
- ng.closeKey = closeKey;
2543
- if (createForWatch)
2544
- this.nodeGroupsAvailable.add(ng.exactKey, ng);
2419
+ // Setup slots
2420
+ if (slotFragment) {
2421
+ for (let slot of el.querySelectorAll('slot[name]')) {
2422
+ let name = slot.getAttribute('name');
2423
+ if (name) {
2424
+ let slotChildren = slotFragment.querySelectorAll(`[slot='${name}']`);
2425
+ slot.append(...slotChildren);
2426
+ }
2427
+ }
2428
+ let unamedSlot = el.querySelector('slot:not([name])');
2429
+ if (unamedSlot)
2430
+ unamedSlot.append(slotFragment);
2545
2431
  else
2546
- this.nodeGroupsInUse.push(ng);
2432
+ el.append(slotFragment);
2547
2433
  }
2434
+
2435
+ root = el;
2436
+ this.startNode = el;
2437
+ this.endNode = el;
2548
2438
  }
2549
-
2550
- // New!
2551
- // We clear the parent PathExpr's nodesCache when we remove ourselves from it.
2552
- // Benchmarking shows this doesn't slow down the partialUpdate benchmark.
2553
- if (ng.parentPath) {
2554
- // ng.parentPath.clearNodesCache(); // Makes partialUpdate benchmark 10x slower!
2555
- ng.parentPath = null;
2439
+ else {
2440
+ let singleEl = getSingleEl(fragment);
2441
+ this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
2442
+ if (singleEl) {
2443
+ root = singleEl;
2444
+ offset = 1;
2445
+ }
2556
2446
  }
2557
2447
 
2448
+ this.updatePaths(root, shell.paths, offset);
2558
2449
 
2559
-
2560
-
2561
- return ng;
2450
+ this.activateEmbeds(root, shell, offset);
2451
+
2452
+ // Apply exprs
2453
+ this.applyExprs(template.exprs);
2562
2454
  }
2455
+ }
2563
2456
 
2564
- reset() {
2565
- //this.changes = [];
2566
- let available = this.nodeGroupsAvailable;
2567
- for (let ng of this.nodeGroupsInUse) {
2568
- ng.inUse = false;
2569
- available.add(ng.exactKey, ng);
2570
- available.add(ng.closeKey, ng);
2457
+ function getSingleEl(fragment) {
2458
+ let nonempty = [];
2459
+ for (let n of fragment.childNodes) {
2460
+ if (n.nodeType === 1 || n.nodeType === 3 && n.textContent.trim().length) {
2461
+ if (nonempty.length)
2462
+ return null;
2463
+ nonempty.push(n);
2571
2464
  }
2572
- this.nodeGroupsInUse = [];
2465
+ }
2466
+ return nonempty[0];
2467
+ }
2468
+
2469
+ /**
2470
+ * Does the fragment have one child that's an element matching the tagname of el?
2471
+ * @param fragment {DocumentFragment}
2472
+ * @param el {HTMLElement}
2473
+ * @returns {boolean} */
2474
+ function isReplaceEl(fragment, el) {
2475
+ return el.tagName.includes('-')
2476
+ && fragment.children.length===1
2477
+ && fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') === el.tagName;
2478
+ }
2479
+
2480
+ /**
2481
+ * The html strings and evaluated expressions from an html tagged template.
2482
+ * A unique Template is created for each item in a loop.
2483
+ * Although the reference to the html strings is shared among templates. */
2484
+ class Template {
2573
2485
 
2574
- // Used for watches
2575
- this.changes = [];
2486
+ /** @type {(Template|string|function)|(Template|string|function)[]} Evaulated expressions. */
2487
+ exprs = []
2576
2488
 
2577
-
2578
- // TODO: free the memory from any nodeGroupsAvailable() after render is done, since they weren't used?
2579
- }
2489
+ /** @type {string[]} */
2490
+ html = [];
2580
2491
 
2492
+ /** @type {Array} Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
2493
+ hashedFields;
2581
2494
 
2582
- // deprecated
2583
- //pathToLoopInfo = new MultiValueMap(); // uses a Set() for each value.
2584
- clearSubscribers = false;
2495
+ /**
2496
+ * @deprecated
2497
+ * @type {ExprPath} Used with forEach() from watch.js
2498
+ * Set in ExprPath.apply() */
2499
+ parentPath;
2585
2500
 
2586
-
2501
+ /** @type {NodeGroup} */
2502
+ nodeGroup;
2587
2503
 
2588
-
2589
2504
  /**
2590
- * @deprecated
2591
- * Store the functions used to create items for each loop.
2592
- * TODO: Can this be combined with pathToTemplates?
2593
- * @type {MultiValueMap<string, Subscriber>} */
2594
- pathToLoopInfo = new MultiValueMap();
2595
-
2505
+ * @type {string[][]} */
2506
+ paths = [];
2507
+
2596
2508
  /**
2597
- * Maps variable paths to the templates used to create NodeGroups
2598
- * @type {MultiValueMap<string, Subscriber>} */
2599
- subscribers = new MultiValueMap();
2600
-
2601
- clearSubscribersIfNeeded() {
2602
- if (this.clearSubscribers) {
2603
- this.pathToLoopInfo = new MultiValueMap();
2604
- this.subscribers = new MultiValueMap();
2605
- this.clearSubscribers = false;
2606
- }
2509
+ *
2510
+ * @param htmlStrings {string[]}
2511
+ * @param exprs {*[]} */
2512
+ constructor(htmlStrings, exprs) {
2513
+ this.html = htmlStrings;
2514
+ this.exprs = exprs;
2515
+
2516
+ //this.trace = new Error().stack.split(/\n/g)
2517
+
2518
+ // Multiple templates can share the same htmlStrings array.
2519
+ //this.hashedFields = [getObjectId(htmlStrings), exprs]
2520
+
2521
+
2607
2522
  }
2608
-
2609
2523
 
2610
2524
  /**
2611
- * Get the NodeGroupManager for a Web Component.
2612
- * @param rootEl {Solarite|HTMLElement}
2613
- * @return {NodeGroupManager} */
2614
- static get(rootEl=null) {
2615
- if (!rootEl)
2616
- return new NodeGroupManager();
2525
+ * Called by JSON.serialize when it encounters a Template.
2526
+ * This prevents the hashed version from being too large. */
2527
+ toJSON() {
2528
+ if (!this.hashedFields)
2529
+ this.hashedFields = [getObjectId(this.html), this.exprs];
2530
+
2531
+ return this.hashedFields
2532
+ }
2533
+
2534
+ /**
2535
+ * Render the main template, which may indirectly call renderTemplate() to create children.
2536
+ * @param el {HTMLElement}
2537
+ * @param options {RenderOptions}
2538
+ * @return {?DocumentFragment|HTMLElement} */
2539
+ render(el=null, options={}) {
2540
+ let ng;
2541
+ let standalone = !el;
2542
+ let firstTime = false;
2543
+
2544
+ // Rendering a standalone element.
2545
+ // TODO: figure out when to not use RootNodeGroup
2546
+ if (standalone) {
2547
+ ng = new RootNodeGroup(this, null, options);
2548
+ el = ng.getRootNode();
2549
+ Globals.nodeGroups.set(el, ng);
2550
+ firstTime = true;
2551
+ }
2552
+ else {
2553
+ ng = Globals.nodeGroups.get(el);
2554
+ if (!ng) {
2555
+ ng = new RootNodeGroup(this, el, options);
2556
+ Globals.nodeGroups.set(el, ng);
2557
+ firstTime = true;
2558
+ }
2559
+ }
2617
2560
 
2618
- let ngm = nodeGroupManagers.get(rootEl);
2619
- if (!ngm) {
2620
- ngm = new NodeGroupManager(rootEl);
2621
- nodeGroupManagers.set(rootEl, ngm);
2561
+ // Creating the root nodegroup also renders it.
2562
+ // If we didn't just create it, we need to render it.
2563
+ if (!firstTime) {
2564
+ if (this.html?.length === 1 && !this.html[0])
2565
+ el.innerHTML = ''; // Fast path for empty component.
2566
+ else
2567
+ ng.applyExprs(this.exprs);
2622
2568
  }
2623
2569
 
2624
- return ngm;
2570
+ return el;
2571
+ }
2572
+
2573
+ getExactKey() {
2574
+ if (!this.exactKey)
2575
+ this.exactKey = getObjectHash(this); // calls this.toJSON().
2576
+ return this.exactKey;
2625
2577
  }
2626
2578
 
2579
+ getCloseKey() {
2580
+ if (!this.closeKey)
2581
+ this.closeKey = '@'+this.toJSON()[0];
2582
+ // Use the joined html when debugging? But it breaks some tests.
2583
+ //return '@'+this.html.join('|')
2627
2584
 
2628
-
2585
+ return this.closeKey;
2586
+ }
2629
2587
  }
2630
2588
 
2631
- NodeGroupManager.pendingChildren = [];
2632
2589
 
2633
2590
  /**
2634
- * Each Element that has Expr children has an associated NodeGroupManager here.
2635
- * @type {WeakMap<HTMLElement, NodeGroupManager>} */
2636
- let nodeGroupManagers = new WeakMap();
2591
+ * @typedef {Object} RenderOptions
2592
+ * @property {boolean=} styles - Replace :host in style tags to scope them locally.
2593
+ * @property {boolean=} scripts - Execute script tags.
2594
+ * @property {boolean=} ids - Create references to elements with id or data-id attributes.
2595
+ * @property {?boolean} render - Deprecated.
2596
+ * Used only when options are given to a class super constructor inheriting from Solarite.
2597
+ * True to call render() immediately in super constructor.
2598
+ * False to automatically call render() at all.
2599
+ * Undefined (default) to call render() when added to the DOM, unless already rendered.
2600
+ */
2637
2601
 
2638
2602
  /**
2639
2603
  * Convert strings to HTMLNodes.
@@ -2648,142 +2612,142 @@ let nodeGroupManagers = new WeakMap();
2648
2612
  * 5. TODO: list more
2649
2613
  *
2650
2614
  * Currently supported:
2651
- * 1. r`<b>Hello${'World'}!` // Create Template that can later be used to create nodes.
2615
+ * 1. r(el, options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
2616
+ * 2. r(el, template, ?options) // Render the Template created by #1 to element.
2652
2617
  *
2653
- * 2. r(el, template, ?options) // Render the template created by #1 to element.
2654
- * 3. r(el, options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
2618
+ * 3. r`<b>Hello</b> ${'World'}!` // Create Template that can later be used to create nodes.
2655
2619
  *
2656
2620
  * 4. r('Hello'); // Create single text node.
2657
2621
  * 5. r('<b>Hello</b>'); // Create single HTMLElement
2658
2622
  * 6. r('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
2659
- * 7. r()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which includes properly handling nested components and r`` sub-expressions.
2623
+ * 7. r()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which
2624
+ * // includes properly handling nested components and r`` sub-expressions.
2660
2625
  * 8. r(template) // Render Template created by #1.
2661
- * 9. r(() => r`<b>Hello</b>`, {...}); // Create dynamic element that has a render() function.
2662
2626
  *
2663
- * @param htmlStrings {?HTMLElement|string|string[]|function():Template}
2627
+ * 9. r({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
2628
+ *
2629
+ * @param htmlStrings {?HTMLElement|string|string[]|function():Template|{render:function()}}
2664
2630
  * @param exprs {*[]|string|Template|Object}
2665
2631
  * @return {Node|HTMLElement|Template} */
2666
2632
  function r(htmlStrings=undefined, ...exprs) {
2667
2633
 
2668
- // 1. Path if used as a template tag.
2669
- if (Array.isArray(htmlStrings)) {
2670
- return new Template(htmlStrings, exprs);
2671
- }
2672
-
2673
- else if (htmlStrings instanceof Node) {
2674
- let parent = htmlStrings, template = exprs[0];
2675
-
2676
- // 2. Render template created by #4 to element.
2677
- if (exprs[0] instanceof Template) {
2678
- let options = exprs[1];
2679
- template.render(parent, options);
2680
-
2681
- // Append on the first go.
2682
- if (!parent.childNodes.length && this) {
2683
- // TODO: Is htis ever executed?
2684
- debugger;
2685
- parent.append(this.rootNg.getParentNode());
2686
- }
2687
- }
2688
-
2689
- // 3
2690
- else if (!exprs.length || exprs[0]) {
2691
- if (parent.shadowRoot)
2692
- parent.innerHTML = ''; // Remove shadowroot. TODO: This could mess up paths?
2693
-
2694
- let options = exprs[0];
2695
- return (htmlStrings, ...exprs) => {
2696
- rendered.add(parent);
2697
- let template = r(htmlStrings, ...exprs);
2698
- return template.render(parent, options);
2699
- }
2700
- }
2701
-
2702
- // null for expr[0], remove whole element.
2703
- // This path never happens?
2704
- else {
2705
- throw new Error('unsupported');
2706
- //let ngm = NodeGroupManager.get(parent);
2707
- //ngm.render(null, exprs[1])
2708
- }
2709
- }
2710
-
2711
- else if (typeof htmlStrings === 'string' || htmlStrings instanceof String) {
2712
- // If it starts with a string, trim both ends.
2713
- // TODO: Also trim if it ends with whitespace?
2714
- if (htmlStrings.match(/^\s^</))
2715
- htmlStrings = htmlStrings.trim();
2716
-
2717
- // We create a new one each time because otherwise
2718
- // the returned fragment will have its content replaced by a subsequent call.
2719
- let templateEl = document.createElement('template');
2720
- templateEl.innerHTML = htmlStrings;
2721
-
2722
- // 4+5. Return Node if there's one child.
2723
- if (templateEl.content.childNodes.length === 1)
2724
- return templateEl.content.firstChild;
2725
-
2726
- // 6. Otherwise return DocumentFragment.
2727
- return templateEl.content;
2728
- }
2729
-
2730
- // 7. Create a static element
2731
- else if (htmlStrings === undefined) {
2732
- return (htmlStrings, ...exprs) => {
2733
- //rendered.add(parent)
2734
- let template = r(htmlStrings, ...exprs);
2735
- return template.render();
2736
- }
2737
- }
2738
-
2739
- // 8.
2740
- else if (htmlStrings instanceof Template) {
2741
- return htmlStrings.render();
2742
- }
2743
-
2744
-
2745
- // 9. Create dynamic element with render() function.
2746
- else if (typeof htmlStrings === 'function') {
2747
- let getTemplate = htmlStrings;
2748
- let template = getTemplate();
2749
-
2750
- if (typeof template === 'string')
2751
- throw new Error(`Please add the "r" prefix before the string "${template}"`)
2752
-
2753
- template.replaceMode = true;
2754
- let el = template.render();
2755
-
2756
- // Create the render() function from the function we were given.
2757
- el.render = (function() {
2758
- template = getTemplate();
2759
- template.render(el);
2760
- }).bind(el);
2761
-
2762
-
2763
- // The second argument was an object of additional properties to add.
2764
- let props = exprs[0];
2765
- for (let name in props)
2766
- if (typeof props[name] === 'function')
2767
- el[name] = props[name].bind(el);
2768
- else
2769
- el[name] = props[name];
2770
-
2771
- return el;
2772
- }
2773
-
2774
- else
2775
- throw new Error('Unsupported arguments.')
2776
- }
2634
+ // TODO: Make this a more flat if/else and call other functions for the logic.
2635
+ if (htmlStrings instanceof Node) {
2636
+ let parent = htmlStrings, template = exprs[0];
2777
2637
 
2638
+ // 1
2639
+ if (!(exprs[0] instanceof Template)) {
2640
+ if (parent.shadowRoot)
2641
+ parent.innerHTML = ''; // Remove shadowroot. TODO: This could mess up paths?
2778
2642
 
2643
+ let options = exprs[0];
2779
2644
 
2645
+ // Return a tagged template function that applies the tagged themplate to parent.
2646
+ let taggedTemplate = (htmlStrings, ...exprs) => {
2647
+ Globals.rendered.add(parent);
2648
+ let template = new Template(htmlStrings, exprs);
2649
+ return template.render(parent, options);
2650
+ };
2651
+ return taggedTemplate;
2652
+ }
2780
2653
 
2654
+ // 2. Render template created by #4 to element.
2655
+ else if (exprs[0] instanceof Template) {
2656
+ let options = exprs[1];
2657
+ template.render(parent, options);
2781
2658
 
2659
+ // Append on the first go.
2660
+ if (!parent.childNodes.length && this) {
2661
+ // TODO: Is this ever executed?
2662
+ debugger;
2663
+ parent.append(this.rootNg.getParentNode());
2664
+ }
2665
+ }
2782
2666
 
2783
- /**
2784
- * Elements that have been rendered to by r() at least once.
2785
- * @type {WeakSet<HTMLElement>} */
2786
- let rendered = new WeakSet();
2667
+
2668
+
2669
+ // null for expr[0], remove whole element.
2670
+ // This path never happens?
2671
+ else {
2672
+ throw new Error('unsupported');
2673
+ //let ngm = NodeGroupManager.get(parent);
2674
+ //ngm.render(null, exprs[1])
2675
+ }
2676
+ }
2677
+
2678
+ // 3. Path if used as a template tag.
2679
+ else if (Array.isArray(htmlStrings)) {
2680
+ return new Template(htmlStrings, exprs);
2681
+ }
2682
+
2683
+ else if (typeof htmlStrings === 'string' || htmlStrings instanceof String) {
2684
+ // If it starts with a string, trim both ends.
2685
+ // TODO: Also trim if it ends with whitespace?
2686
+ if (htmlStrings.match(/^\s^</))
2687
+ htmlStrings = htmlStrings.trim();
2688
+
2689
+ // We create a new one each time because otherwise
2690
+ // the returned fragment will have its content replaced by a subsequent call.
2691
+ let templateEl = document.createElement('template');
2692
+ templateEl.innerHTML = htmlStrings;
2693
+
2694
+ // 4+5. Return Node if there's one child.
2695
+ let relevantNodes = Util.trimEmptyNodes(templateEl.content.childNodes);
2696
+ if (relevantNodes.length === 1)
2697
+ return relevantNodes[0];
2698
+
2699
+ // 6. Otherwise return DocumentFragment.
2700
+ return templateEl.content;
2701
+ }
2702
+
2703
+ // 7. Create a static element
2704
+ else if (htmlStrings === undefined) {
2705
+ return (htmlStrings, ...exprs) => {
2706
+ //Globals.rendered.add(parent)
2707
+ let template = r(htmlStrings, ...exprs);
2708
+ return template.render();
2709
+ }
2710
+ }
2711
+
2712
+ // 8.
2713
+ else if (htmlStrings instanceof Template) {
2714
+ return htmlStrings.render();
2715
+ }
2716
+
2717
+
2718
+ // 9. Create dynamic element with render() function.
2719
+ else if (typeof htmlStrings === 'object') {
2720
+ let obj = htmlStrings;
2721
+
2722
+ // Special rebound render path, called by normal path.
2723
+ if (Globals.objToEl.has(obj)) {
2724
+ return function(...args) {
2725
+ let template = r(...args);
2726
+ let el = template.render();
2727
+ Globals.objToEl.set(obj, el);
2728
+ }.bind(obj);
2729
+ }
2730
+
2731
+ // Normal path
2732
+ else {
2733
+ Globals.objToEl.set(obj, null);
2734
+ obj.render(); // Calls the Special rebound render path above, when the render function calls r(this)
2735
+ let el = Globals.objToEl.get(obj);
2736
+ Globals.objToEl.delete(obj);
2737
+
2738
+ for (let name in obj)
2739
+ if (typeof obj[name] === 'function')
2740
+ el[name] = obj[name].bind(el);
2741
+ else
2742
+ el[name] = obj[name];
2743
+
2744
+ return el;
2745
+ }
2746
+ }
2747
+
2748
+ else
2749
+ throw new Error('Unsupported arguments.')
2750
+ }
2787
2751
 
2788
2752
  //import {watchGet, watchSet} from "./watch.js";
2789
2753
 
@@ -2803,14 +2767,9 @@ function defineClass(Class, tagName, extendsTag) {
2803
2767
  }
2804
2768
  }
2805
2769
 
2806
- /**
2807
- * @type {Object<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
2808
- let elementClasses = {};
2809
2770
 
2810
- /**
2811
- * Store which instances of Solarite have already been added to the DOM. * @type {WeakSet<HTMLElement>}
2812
- */
2813
- let connected = new WeakSet();
2771
+
2772
+
2814
2773
 
2815
2774
  /**
2816
2775
  * Create a version of the Solarite class that extends from the given tag name.
@@ -2822,6 +2781,7 @@ let connected = new WeakSet();
2822
2781
  * 5. We have the onConnect, onFirstConnect, and onDisconnect methods.
2823
2782
  * Can't figure out how to have these work standalone though, and still be synchronous.
2824
2783
  * 6. Can we extend from other element types like TR?
2784
+ * 7. Shows default text if render() function isn't defined.
2825
2785
  *
2826
2786
  * Advantages to inheriting from HTMLElement
2827
2787
  * 1. Minimization won't break when it renames the Class and we call customElements.define() on the wrong name.
@@ -2836,10 +2796,10 @@ function createSolarite(extendsTag=null) {
2836
2796
  if (extendsTag && !extendsTag.includes('-')) {
2837
2797
  extendsTag = extendsTag.toLowerCase();
2838
2798
 
2839
- BaseClass = elementClasses[extendsTag];
2799
+ BaseClass = Globals.elementClasses[extendsTag];
2840
2800
  if (!BaseClass) { // TODO: Use Cache
2841
2801
  BaseClass = document.createElement(extendsTag).constructor;
2842
- elementClasses[extendsTag] = BaseClass;
2802
+ Globals.elementClasses[extendsTag] = BaseClass;
2843
2803
  }
2844
2804
  }
2845
2805
 
@@ -2875,26 +2835,24 @@ function createSolarite(extendsTag=null) {
2875
2835
  constructor(options={}) {
2876
2836
  super();
2877
2837
 
2878
-
2879
-
2880
2838
  // TODO: Is options.render ever used?
2881
2839
  if (options.render===true)
2882
2840
  this.render();
2883
2841
 
2884
2842
  else if (options.render===false)
2885
- rendered.add(this); // Don't render on connectedCallback()
2843
+ Globals.rendered.add(this); // Don't render on connectedCallback()
2886
2844
 
2887
2845
  // Add children before constructor code executes.
2888
2846
  // PendingChildren is setup in NodeGroup.createNewComponent()
2889
2847
  // TODO: Match named slots.
2890
- let ch = NodeGroupManager.pendingChildren.pop();
2848
+ let ch = Globals.pendingChildren.pop();
2891
2849
  if (ch)
2892
2850
  (this.querySelector('slot') || this).append(...ch);
2893
2851
 
2894
2852
  /** @deprecated */
2895
2853
  Object.defineProperty(this, 'html', {
2896
2854
  set(html) {
2897
- rendered.add(this);
2855
+ Globals.rendered.add(this);
2898
2856
  if (typeof html === 'string') {
2899
2857
  console.warn("Assigning to this.html without the r template prefix.");
2900
2858
  this.innerHTML = html;
@@ -2917,7 +2875,7 @@ function createSolarite(extendsTag=null) {
2917
2875
  /**
2918
2876
  * Call render() only if it hasn't already been called. */
2919
2877
  renderFirstTime() {
2920
- if (!rendered.has(this) && this.render)
2878
+ if (!Globals.rendered.has(this) && this.render)
2921
2879
  this.render();
2922
2880
  }
2923
2881
 
@@ -2925,8 +2883,8 @@ function createSolarite(extendsTag=null) {
2925
2883
  * Called automatically by the browser. */
2926
2884
  connectedCallback() {
2927
2885
  this.renderFirstTime();
2928
- if (!connected.has(this)) {
2929
- connected.add(this);
2886
+ if (!Globals.connected.has(this)) {
2887
+ Globals.connected.add(this);
2930
2888
  this.onFirstConnect();
2931
2889
  }
2932
2890
  this.onConnect();
@@ -2945,6 +2903,12 @@ function createSolarite(extendsTag=null) {
2945
2903
  }
2946
2904
  }
2947
2905
 
2906
+ /**
2907
+ * Solarite JavasCript UI library.
2908
+ * MIT License
2909
+ * https://vorticode.github.io/solarite/
2910
+ */
2911
+
2948
2912
  /**
2949
2913
  * TODO: The Proxy and the multiple base classes mess up 'instanceof Solarite'
2950
2914
  * @type {Node|Class<HTMLElement>|function(tagName:string):Node|Class<HTMLElement>} */
@@ -2953,10 +2917,10 @@ let Solarite = new Proxy(createSolarite(), {
2953
2917
  return createSolarite(...args)
2954
2918
  }
2955
2919
  });
2956
-
2920
+ let getInputValue = Util.getInputValue;
2957
2921
 
2958
2922
  //Experimental:
2959
2923
  //export {forEach, watchGet, watchSet} from './watch.js' // old, unfinished
2960
2924
  //export {watch} from './watch2.js'; // unfinished
2961
2925
 
2962
- export { ArgType, Solarite, Template, getArg, r };
2926
+ export { ArgType, Globals, Solarite, Template, delve, getArg, getInputValue, r };