solarite 0.1.1 → 0.2.1

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