solarite 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) 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 +3 -2
  8. package/build/build.js +1 -1
  9. package/dist/Solarite-debug.js +1941 -2791
  10. package/dist/Solarite.js +1697 -2511
  11. package/dist/Solarite.min.js +3 -3
  12. package/dist/udomdiff-license.txt +18 -0
  13. package/docs/index.md +695 -235
  14. package/docs/js/Playground.js +1 -1
  15. package/docs/js/codemirror/codemirror6.js +3683 -3206
  16. package/docs/js/codemirror/themeSolarIce.js +2 -2
  17. package/docs/js/documentation.js +2 -2
  18. package/docs/js/ui/CodeEditor.js +183 -45
  19. package/docs/js/ui/FlexResizer.js +15 -5
  20. package/docs/js/util/Errors.js +11 -0
  21. package/docs/media/documentation.css +6 -3
  22. package/index.html +462 -26
  23. package/package.json +1 -1
  24. package/readme.md +11 -1
  25. package/src/solarite/ExprPath.js +422 -135
  26. package/src/solarite/Globals.js +53 -0
  27. package/src/solarite/NodeGroup.js +300 -388
  28. package/src/solarite/Shell.js +31 -33
  29. package/src/solarite/Solarite.js +17 -2
  30. package/src/solarite/Template.js +75 -25
  31. package/src/solarite/Util.js +131 -7
  32. package/src/solarite/createSolarite.js +32 -25
  33. package/src/solarite/getArg.js +12 -12
  34. package/src/solarite/hash.js +18 -35
  35. package/src/solarite/r.js +128 -118
  36. package/src/solarite/watch3.js +98 -0
  37. package/src/{solarite → unused}/NodeGroupManager.js +86 -224
  38. package/src/unused/onConnect.js +79 -0
  39. package/src/{solarite → unused}/watch.js +2 -2
  40. package/src/{solarite → unused}/watch2.js +4 -4
  41. package/src/{solarite → util}/MultiValueMap.js +23 -12
  42. package/src/util/WeakArray.js +33 -0
  43. package/tests/Solarite.test.js +1108 -166
  44. package/tests/Testimony.js +274 -67
  45. package/tests/index.html +6 -35
  46. package/tests/run.bat +2 -0
  47. package/tests/NodeGroup.test.js +0 -115
  48. package/tests/Shell.test.js +0 -75
package/dist/Solarite.js CHANGED
@@ -81,8 +81,6 @@ var Util$1 = {
81
81
 
82
82
  };
83
83
 
84
-
85
-
86
84
  /**
87
85
  * Follow a path into an object.
88
86
  * @param obj {object}
@@ -130,7 +128,7 @@ let delveDontCreate = {};
130
128
  /**
131
129
  * There are three ways to create an instance of a Solarite Component:
132
130
  * 1. new ComponentName(); // direct class instantiation
133
- * 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.
134
132
  * 3. <body><component-name></component-name></body> // in the Document html.
135
133
  *
136
134
  * When created via #3, Solarite has no way to pass attributes as arguments to the constructor. So to make
@@ -180,19 +178,19 @@ function getArg(el, name, val=null, type=ArgType.String) {
180
178
  case ArgType.Float:
181
179
  return parseFloat(val);
182
180
  case ArgType.String:
183
- return val || '';
181
+ return [undefined, null, false].includes(val) ? '' : val+'';
184
182
  case ArgType.JSON:
185
- try {
186
- return JSON.parse(val);
187
- } catch (e) {
188
- return val;
189
- }
190
183
  case ArgType.Eval:
191
- try {
192
- return eval(`(${val})`);
193
- } catch (e) {
194
- return val;
195
- }
184
+ if (typeof val === 'string' && val.length)
185
+ try {
186
+ if (type === ArgType.JSON)
187
+ return JSON.parse(val);
188
+ else
189
+ return eval(`(${val})`);
190
+ } catch (e) {
191
+ return val;
192
+ }
193
+ else return val;
196
194
  default:
197
195
  return val;
198
196
  }
@@ -228,22 +226,14 @@ let objectIds = new WeakMap();
228
226
 
229
227
  /**
230
228
  * @param obj {Object|string|Node}
231
- * @param prefix
232
229
  * @returns {string} */
233
- function getObjectId(obj, prefix=null) {
234
-
235
-
236
-
237
-
238
-
239
- prefix = prefix || '~\f';
240
-
230
+ function getObjectId(obj) {
241
231
  // if (typeof obj === 'function')
242
- // return obj.toString();
232
+ // return obj.toString(); // This fails to detect when a function's bound variables changes.
243
233
 
244
234
  let result = objectIds.get(obj);
245
235
  if (!result) { // convert to string, store in result, then add 1 to lastObjectId.
246
- 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()
247
237
  objectIds.set(obj, result);
248
238
  }
249
239
  return result;
@@ -256,12 +246,20 @@ function getObjectId(obj, prefix=null) {
256
246
  * Adding a toJSON method globally on these object prototypes doesn't incur that performance penalty. */
257
247
  let isHashing = true;
258
248
  function toJSON() {
259
- //return (isHashing && !Array.isArray(this)) ? getObjectId(this) : this
260
249
  return isHashing ? getObjectId(this) : this
261
250
  }
251
+
252
+
262
253
  // Node.prototype.toJSON = toJSON;
263
254
  // Function.prototype.toJSON = toJSON;
264
-
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
+ }
265
263
 
266
264
  /**
267
265
  * Get a string that uniquely maps to the values of the given object.
@@ -270,25 +268,18 @@ function toJSON() {
270
268
  *
271
269
  * Relies on the Node and Function prototypes being overridden above.
272
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
+ *
273
274
  * @param obj {*}
274
275
  * @returns {string} */
275
276
  function getObjectHash(obj) {
276
-
277
- // Sometimes these get unassigned by Chrome and Brave 119, as well as Firefox, seemingly randomly!
278
- // The same tests sometimes pass, sometimes fail, even after browser and OS restarts.
279
- // So we check the assignments on every run of getObjectHash()
280
- if (Node.prototype.toJSON !== toJSON) {
281
- Node.prototype.toJSON = toJSON;
282
- if (Function.prototype.toJSON !== toJSON) // Will it only unmap one but not the other?
283
- Function.prototype.toJSON = toJSON;
284
- }
285
-
286
277
  let result;
287
278
  isHashing = true;
288
279
  try {
289
280
  result = JSON.stringify(obj);
290
281
  }
291
- catch(e){
282
+ catch(e) {
292
283
  result = getObjectHashCircular(obj);
293
284
  }
294
285
  isHashing = false;
@@ -296,7 +287,7 @@ function getObjectHash(obj) {
296
287
  }
297
288
 
298
289
  /**
299
- * Having this separate might help the optimzer for getObjectHash() ?
290
+ * Slower hashing method that supports.
300
291
  * @param obj
301
292
  * @returns {string} */
302
293
  function getObjectHashCircular(obj) {
@@ -315,71 +306,59 @@ function getObjectHashCircular(obj) {
315
306
  });
316
307
  }
317
308
 
318
- class MultiValueMap {
309
+
310
+
311
+ var Globals = {
319
312
 
320
- /** @type {Object<string, Set>} */
321
- data = {};
313
+ /**
314
+ * Used by NodeGroup.applyComponentExprs() */
315
+ componentHash: new WeakMap(),
322
316
 
323
- // Set a new value for a key
324
- add(key, value) {
325
- let data = this.data;
326
- let set = data[key];
327
- if (!set) {
328
- set = new Set();
329
- data[key] = set;
330
- }
331
- set.add(value);
332
- }
317
+ /**
318
+ * Store which instances of Solarite have already been added to the DOM.
319
+ * @type {WeakSet<HTMLElement>} */
320
+ connected: new WeakSet(),
333
321
 
334
- // Get all values for a key
335
- getAll(key) {
336
- return this.data[key] || [];
337
- }
322
+ /**
323
+ * Elements that have been rendered to by r() at least once.
324
+ * This is used by the Solarite class to know when to call onFirstConnect()
325
+ * @type {WeakSet<HTMLElement>} */
326
+ rendered: new WeakSet(),
338
327
 
339
- // Remove one value from a key, and return it
340
- delete(key, val=undefined) {
341
- // if (key === '["Html2",[[["Html3",["F1","A"]],["Html3",["F1","B"]]]]]')
342
- // debugger;
343
-
344
- let data = this.data;
345
- // The partialUpdate benchmark shows having this check first makes the function slightly faster.
346
- // if (!data.hasOwnProperty(key))
347
- // return undefined;
328
+ /**
329
+ * Used by watch3 to see which expressions are being accessed. */
330
+ currentExprPath: [],
348
331
 
349
- // Delete a specific value.
350
- let result;
351
- let set = data[key];
352
- if (!set) // slower than pre-check.
353
- return undefined;
332
+ /**
333
+ * @type {Object<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
334
+ elementClasses: {},
354
335
 
355
- if (val !== undefined) {
356
- set.delete(val);
357
- result = val;
358
- }
336
+ /**
337
+ * Used by ExprPath.applyEventAttrib()
338
+ * @type {WeakMap<Node, Object<eventName:string, [original:function, bound:function, args:*[]]>>} */
339
+ nodeEvents: new WeakMap(),
359
340
 
360
- // Delete any value.
361
- else {
362
- result = set.values().next().value;
363
- // [result] = set; // Does the same as above. is about the same speed?
364
- set.delete(result);
365
- }
341
+ /**
342
+ * Get the RootNodeGroup for an element.
343
+ * @type {WeakMap<HTMLElement, RootNodeGroup>} */
344
+ nodeGroups: new WeakMap(),
366
345
 
367
- // TODO: Will this make it slower?
368
- if (set.size === 0)
369
- delete data[key];
370
-
371
- return result;
372
- }
346
+ /**
347
+ * Used by r() path 9. */
348
+ objToEl: new WeakMap(),
373
349
 
374
- hasValue(val) {
375
- let data = this.data;
376
- let names = [];
377
- for (let name in data)
378
- if (data[name].has(val)) // TODO: iterate twice to pre-size array?
379
- names.push(name);
380
- return names;
381
- }
382
- }
350
+ pendingChildren: [],
351
+
352
+ /**
353
+ * Elements that are currently rendering via the r() function.
354
+ * @type {WeakSet<HTMLElement>} */
355
+ rendering: new WeakSet(),
356
+
357
+ /**
358
+ * Map from array of Html strings to a Shell created from them.
359
+ * @type {WeakMap<string[], Shell>} */
360
+ shells: new WeakMap()
361
+ };
383
362
 
384
363
  let Util = {
385
364
 
@@ -404,9 +383,133 @@ let Util = {
404
383
  child.textContent = newText;
405
384
  }
406
385
  }
407
- }
386
+ },
387
+
388
+ /**
389
+ * A generator function that recursively traverses and flattens a value.
390
+ *
391
+ * - If the input is an array, it recursively traverses and flattens the array.
392
+ * - If the input is a function, it calls the function, replaces the function
393
+ * with its result, and flattens the result if necessary. It will recursively
394
+ * call functions that return other functions.
395
+ * - Otherwise it yields the value as is.
396
+ *
397
+ * This function does not create a new array for the flattened values. Instead,
398
+ * it lazily yields each item as it is encountered. This can be more memory-efficient
399
+ * for large or deeply nested structures.
400
+ *
401
+ * @param {any} value - The value to flatten. Can be an array, object, function, or primitive.
402
+ * @yields {any} - The next item in the flattened structure.
403
+ *
404
+ * @example
405
+ * const complexArray = [
406
+ * 1,
407
+ * [2, () => 3, [4, () => [5, 6]], { a: 'object' }],
408
+ * () => () => 7,
409
+ * () => [() => 8, 9],
410
+ * ]; *
411
+ * for (const item of flatten(complexArray))
412
+ * console.log(item); // Outputs: 1, 2, 3, 4, 5, 6, { a: 'object' }, 7, 8, 9
413
+ */
414
+ *flatten(value) {
415
+ if (Array.isArray(value)) {
416
+ for (const item of value) {
417
+ yield* Util.flatten(item); // Recursively flatten arrays
418
+ }
419
+ } else if (typeof value === 'function') {
420
+ const result = value();
421
+ yield* Util.flatten(result); // Recursively flatten the result of a function
422
+ } else
423
+ yield value; // Yield primitive values as is
424
+ },
425
+
426
+ /**
427
+ * Get the value of an input as the most appropriate JavaScript type.
428
+ * @param node {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|HTMLDivElement}
429
+ * @return {string|string[]|number|[]|File[]|Date|boolean} */
430
+ getInputValue(node) {
431
+ if (node.type === 'checkbox' || node.type === 'radio')
432
+ return node.checked; // Boolean
433
+ if (node.type === 'file')
434
+ return [...node.files]; // FileList
435
+ if (node.type === 'number' || node.type === 'range')
436
+ return node.valueAsNumber; // Number
437
+ if (node.type === 'date' || node.type === 'time' || node.type === 'datetime-local')
438
+ return node.valueAsDate; // Date Object
439
+ if (node.type === 'select-multiple') // <select multiple>
440
+ return [...node.selectedOptions].map(option => option.value); // Array of Strings
441
+
442
+ return node.value; // String
443
+ },
444
+
445
+ /**
446
+ * Is it an array and a path that can be evaluated by delve() ?
447
+ * @param arr {Array|*}
448
+ * @returns {boolean} */
449
+ isPath(arr) {
450
+ return Array.isArray(arr) && typeof arr[0] === 'object' && !arr.slice(1).find(p => typeof p !== 'string' && typeof p !== 'number');
451
+ },
452
+
453
+ /**
454
+ * Find NodeGroups that had their nodes removed and add those nodes to a Fragment so
455
+ * they're not lost forever and the NodeGroup's internal structure is still consistent.
456
+ * This saves all of a NodeGroup's nodes in order, so that nextChildNode still works.
457
+ * This is necessary because a NodeGroup normally only stores the first and last node.
458
+ * Called from ExprPath.apply().
459
+ * @param oldNodeGroups {NodeGroup[]}
460
+ * @param oldNodes {Node[]} */
461
+ saveOrphans(oldNodeGroups, oldNodes) {
462
+ let oldNgMap = new Map();
463
+ for (let ng of oldNodeGroups) {
464
+ oldNgMap.set(ng.startNode, ng);
465
+
466
+ // TODO: Is this necessary?
467
+ // if (ng.parentPath)
468
+ // ng.parentPath.clearNodesCache();
469
+ }
470
+
471
+ for (let i=0, node; node = oldNodes[i]; i++) {
472
+ let ng;
473
+ if (!node.parentNode && (ng = oldNgMap.get(node))) {
474
+ //ng.nodesCache = [];
475
+ let fragment = document.createDocumentFragment();
476
+ let endNode = ng.endNode;
477
+ while (node !== endNode) {
478
+ fragment.append(node);
479
+ //ng.nodesCache.push(node);
480
+ i++;
481
+ node = oldNodes[i];
482
+ }
483
+ fragment.append(endNode);
484
+ //ng.nodesCache.push(endNode);
485
+ }
486
+ }
487
+ },
408
488
 
489
+ /**
490
+ * Remove nodes from the beginning and end that are not:
491
+ * 1. Elements.
492
+ * 2. Non-whitespace text nodes.
493
+ * @param nodes {Node[]|NodeList}
494
+ * @returns {Node[]} */
495
+ trimEmptyNodes(nodes) {
496
+ const shouldTrimNode = node =>
497
+ node.nodeType !== Node.ELEMENT_NODE &&
498
+ (node.nodeType !== Node.TEXT_NODE || node.textContent.trim() === '');
499
+
500
+ // Convert nodeList to an array for easier manipulation
501
+ const result = [...nodes];
502
+
503
+ // Trim from the start
504
+ while (result.length > 0 && shouldTrimNode(result[0]))
505
+ result.shift();
506
+
507
+ // Trim from the end
508
+ while (result.length > 0 && shouldTrimNode(result[result.length - 1]))
509
+ result.pop();
409
510
 
511
+ return result;
512
+ }
410
513
  };
411
514
 
412
515
 
@@ -451,15 +554,15 @@ function camelToDashes(str) {
451
554
  * Returns false if they're the same. Or the first index where they differ.
452
555
  * @param a
453
556
  * @param b
454
- * @returns {int|false} */
455
- function findArrayDiff(a, b) {
456
- if (a.length !== b.length)
457
- return -1;
557
+ * @returns {boolean} */
558
+ function arraySame(a, b) {
458
559
  let aLength = a.length;
560
+ if (aLength !== b.length)
561
+ return false;
459
562
  for (let i=0; i<aLength; i++)
460
563
  if (a[i] !== b[i])
461
- return i;
462
- return false; // the same.
564
+ return false;
565
+ return true; // the same.
463
566
  }
464
567
 
465
568
 
@@ -542,61 +645,321 @@ let state = {...defaultState};
542
645
  // For debugging only
543
646
 
544
647
 
545
- /**
546
- * Path to where an expression should be evaluated within a Shell.
547
- * Path is only valid until the expressions before it are evaluated.
548
- * TODO: Make this based on parent and node instead of path? */
549
- class ExprPath {
550
-
551
- /**
552
- * @type {PathType} */
553
- type;
554
-
555
- // Used for attributes:
556
-
557
- /** @type {?string} Used only if type=AttribType.Value. */
558
- attrName;
648
+ class MultiValueMap {
559
649
 
560
- /**
561
- * @type {?string[]} Used only if type=AttribType.Value. If null, use one expr to set the whole attribute value. */
562
- attrValue;
650
+ /** @type {Object<string, Set>} */
651
+ data = {};
563
652
 
564
- /**
565
- * @type {Set<string>} Used for type=AttribType.Multiple to remember the attributes that were added. */
566
- attrNames;
653
+ // Set a new value for a key
654
+ add(key, value) {
655
+ let data = this.data;
656
+ let set = data[key];
657
+ if (!set) {
658
+ set = new Set();
659
+ data[key] = set;
660
+ }
661
+ set.add(value);
662
+ }
567
663
 
664
+ isEmpty() {
665
+ for (let key in this.data)
666
+ return true;
667
+ return false;
668
+ }
568
669
 
670
+ // Get all values for a key
671
+ getAll(key) {
672
+ return this.data[key] || [];
673
+ }
569
674
 
570
675
  /**
571
- * @type {Node} Node that occurs before this ExprPath's first Node.
572
- * This is necessary because udomdiff() can steal nodes from another ExprPath.
573
- * If we had a pointer to our own startNode then that node could be moved somewhere else w/o us knowing it.
574
- * Used only for type='content'
575
- * Will be null if ExprPath has no Nodes. */
576
- nodeBefore;
676
+ * Remove one value from a key, and return it.
677
+ * @param key {string}
678
+ * @param val If specified, make sure we delete this specific value, if a key exists more than once.
679
+ * @returns {*} */
680
+ delete(key, val=undefined) {
681
+ // if (key === '["Html2",[[["Html3",["F1","A"]],["Html3",["F1","B"]]]]]')
682
+ // debugger;
577
683
 
578
- /**
579
- * If type is AttribType.Multiple or AttribType.Value, points to the node having the attribute.
580
- * If type is 'content', points to a node that never changes that this NodeGroup should always insert its nodes before.
581
- * An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.
582
- * @type {Node|HTMLElement} */
583
- nodeMarker;
684
+ let data = this.data;
584
685
 
585
- /** @deprecated */
586
- get parentNode() {
587
- return this.nodeMarker.parentNode;
588
- }
686
+ // if (!data.hasOwnProperty(key))
687
+ // return undefined;
589
688
 
590
- // These are set after an expression is assigned:
689
+ // Delete a specific value.
690
+ let result;
691
+ let set = data[key];
692
+ if (!set) // slower than pre-check.
693
+ return undefined;
591
694
 
695
+ // Delete any value.
696
+ if (val === undefined) {
697
+ //result = set.values().next().value; // get first item from set.
698
+ [result] = set; // Does the same as above and seems to be about the same speed.
699
+ set.delete(result);
700
+ }
592
701
 
593
- /** @type {NodeGroup} */
594
- parentNg;
702
+ // Delete a specific value.
703
+ else {
704
+ set.delete(val);
705
+ result = val;
706
+ }
595
707
 
596
- /** @type {NodeGroup[]} */
597
- nodeGroups = [];
708
+ // TODO: Will this make it slower?
709
+ if (set.size === 0)
710
+ delete data[key];
598
711
 
712
+ return result;
713
+ }
599
714
 
715
+ hasValue(val) {
716
+ let data = this.data;
717
+ let names = [];
718
+ for (let name in data)
719
+ if (data[name].has(val)) // TODO: iterate twice to pre-size array?
720
+ names.push(name);
721
+ return names;
722
+ }
723
+ }
724
+
725
+ /**
726
+ * ISC License
727
+ *
728
+ * Copyright (c) 2020, Andrea Giammarchi, @WebReflection
729
+ *
730
+ * Permission to use, copy, modify, and/or distribute this software for any
731
+ * purpose with or without fee is hereby granted, provided that the above
732
+ * copyright notice and this permission notice appear in all copies.
733
+ *
734
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
735
+ * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
736
+ * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
737
+ * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
738
+ * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
739
+ * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
740
+ * PERFORMANCE OF THIS SOFTWARE.
741
+ */
742
+
743
+ /**
744
+ * @param {Node} parentNode The container where children live
745
+ * @param {Node[]} a The list of current/live children
746
+ * @param {Node[]} b The list of future children
747
+ * @param {(entry: Node, action: number) => Node} get
748
+ * The callback invoked per each entry related DOM operation.
749
+ * @param {Node} [before] The optional node used as anchor to insert before.
750
+ * @returns {Node[]} The same list of future children.
751
+ */
752
+ const udomdiff = (parentNode, a, b, before) => {
753
+
754
+
755
+ const bLength = b.length;
756
+ let aEnd = a.length;
757
+ let bEnd = bLength;
758
+ let aStart = 0;
759
+ let bStart = 0;
760
+ let map = null;
761
+ while (aStart < aEnd || bStart < bEnd) {
762
+ // append head, tail, or nodes in between: fast path
763
+ if (aEnd === aStart) {
764
+ // we could be in a situation where the rest of nodes that
765
+ // need to be added are not at the end, and in such case
766
+ // the node to `insertBefore`, if the index is more than 0
767
+ // must be retrieved, otherwise it's gonna be the first item.
768
+ const node = bEnd < bLength
769
+ ? (bStart
770
+ ? (b[bStart - 1].nextSibling)
771
+ : b[bEnd - bStart])
772
+ : before;
773
+ while (bStart < bEnd) {
774
+ let bNode = b[bStart++];
775
+ parentNode.insertBefore(bNode, node);
776
+
777
+
778
+ }
779
+ }
780
+ // remove head or tail: fast path
781
+ else if (bEnd === bStart) {
782
+ while (aStart < aEnd) {
783
+ // remove the node only if it's unknown or not live
784
+ let aNode = a[aStart];
785
+ if (!map || !map.has(aNode)) {
786
+ parentNode.removeChild(aNode);
787
+
788
+
789
+ }
790
+ aStart++;
791
+ }
792
+ }
793
+ // same node: fast path
794
+ else if (a[aStart] === b[bStart]) {
795
+ aStart++;
796
+ bStart++;
797
+ }
798
+ // same tail: fast path
799
+ else if (a[aEnd - 1] === b[bEnd - 1]) {
800
+ aEnd--;
801
+ bEnd--;
802
+ }
803
+ // The once here single last swap "fast path" has been removed in v1.1.0
804
+ // https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85
805
+ // reverse swap: also fast path
806
+ else if (
807
+ a[aStart] === b[bEnd - 1] &&
808
+ b[bStart] === a[aEnd - 1]
809
+ ) {
810
+ // this is a "shrink" operation that could happen in these cases:
811
+ // [1, 2, 3, 4, 5]
812
+ // [1, 4, 3, 2, 5]
813
+ // or asymmetric too
814
+ // [1, 2, 3, 4, 5]
815
+ // [1, 2, 3, 5, 6, 4]
816
+ const node = a[--aEnd].nextSibling;
817
+
818
+
819
+ let a2 = b[bStart++];
820
+ let b2 = a[aStart++];
821
+ parentNode.insertBefore(
822
+ a2,
823
+ b2.nextSibling
824
+ );
825
+
826
+
827
+ let bNode = b[--bEnd];
828
+ parentNode.insertBefore(bNode, node);
829
+
830
+
831
+
832
+ // mark the future index as identical (yeah, it's dirty, but cheap 👍)
833
+ // The main reason to do this, is that when a[aEnd] will be reached,
834
+ // the loop will likely be on the fast path, as identical to b[bEnd].
835
+ // In the best case scenario, the next loop will skip the tail,
836
+ // but in the worst one, this node will be considered as already
837
+ // processed, bailing out pretty quickly from the map index check
838
+ a[aEnd] = b[bEnd];
839
+ }
840
+ // map based fallback, "slow" path
841
+ else {
842
+ // the map requires an O(bEnd - bStart) operation once
843
+ // to store all future nodes indexes for later purposes.
844
+ // In the worst case scenario, this is a full O(N) cost,
845
+ // and such scenario happens at least when all nodes are different,
846
+ // but also if both first and last items of the lists are different
847
+ if (!map) {
848
+ map = new Map;
849
+ let i = bStart;
850
+ while (i < bEnd)
851
+ map.set(b[i], i++);
852
+ }
853
+ // if it's a future node, hence it needs some handling
854
+ if (map.has(a[aStart])) {
855
+ // grab the index of such node, 'cause it might have been processed
856
+ const index = map.get(a[aStart]);
857
+ // if it's not already processed, look on demand for the next LCS
858
+ if (bStart < index && index < bEnd) {
859
+ let i = aStart;
860
+ // counts the amount of nodes that are the same in the future
861
+ let sequence = 1;
862
+ while (++i < aEnd && i < bEnd && map.get(a[i]) === (index + sequence))
863
+ sequence++;
864
+ // effort decision here: if the sequence is longer than replaces
865
+ // needed to reach such sequence, which would brings again this loop
866
+ // to the fast path, prepend the difference before a sequence,
867
+ // and move only the future list index forward, so that aStart
868
+ // and bStart will be aligned again, hence on the fast path.
869
+ // An example considering aStart and bStart are both 0:
870
+ // a: [1, 2, 3, 4]
871
+ // b: [7, 1, 2, 3, 6]
872
+ // this would place 7 before 1 and, from that time on, 1, 2, and 3
873
+ // will be processed at zero cost
874
+ if (sequence > (index - bStart)) {
875
+ const node = a[aStart];
876
+ while (bStart < index) {
877
+ let bNode = b[bStart++];
878
+ parentNode.insertBefore(bNode, node);
879
+
880
+
881
+ }
882
+ }
883
+ // if the effort wasn't good enough, fallback to a replace,
884
+ // moving both source and target indexes forward, hoping that some
885
+ // similar node will be found later on, to go back to the fast path
886
+ else {
887
+ let aNode = a[aStart++];
888
+ let bNode = b[bStart++];
889
+ parentNode.replaceChild(
890
+ bNode,
891
+ aNode
892
+ );
893
+
894
+
895
+ }
896
+ }
897
+ // otherwise move the source forward, 'cause there's nothing to do
898
+ else
899
+ aStart++;
900
+ }
901
+ // this node has no meaning in the future list, so it's more than safe
902
+ // to remove it, and check the next live node out instead, meaning
903
+ // that only the live list index should be forwarded
904
+ else {
905
+ let aNode = a[aStart++];
906
+ parentNode.removeChild(aNode);
907
+
908
+
909
+ }
910
+ }
911
+ }
912
+ return b;
913
+ };
914
+
915
+ /**
916
+ * Path to where an expression should be evaluated within a Shell or NodeGroup.
917
+ * Path is only valid until the expressions before it are evaluated.
918
+ * TODO: Make this based on parent and node instead of path? */
919
+ class ExprPath {
920
+
921
+ /**
922
+ * @type {PathType} */
923
+ type;
924
+
925
+ // Used for attributes:
926
+
927
+ /** @type {?string} Used only if type=AttribType.Value. */
928
+ attrName;
929
+
930
+ /**
931
+ * @type {?string[]} Used only if type=AttribType.Value. If null, use one expr to set the whole attribute value. */
932
+ attrValue;
933
+
934
+ /**
935
+ * @type {Set<string>} Used for type=AttribType.Multiple to remember the attributes that were added. */
936
+ attrNames;
937
+
938
+
939
+
940
+ /**
941
+ * @type {Node} Node that occurs before this ExprPath's first Node.
942
+ * This is necessary because udomdiff() can steal nodes from another ExprPath.
943
+ * If we had a pointer to our own startNode then that node could be moved somewhere else w/o us knowing it.
944
+ * Used only for type='content'
945
+ * Will be null if ExprPath has no Nodes. */
946
+ nodeBefore;
947
+
948
+ /**
949
+ * If type is AttribType.Multiple or AttribType.Value, points to the node having the attribute.
950
+ * If type is 'content', points to a node that never changes that this NodeGroup should always insert its nodes before.
951
+ * An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.
952
+ * @type {Node|HTMLElement} */
953
+ nodeMarker;
954
+
955
+
956
+ // These are set after an expression is assigned:
957
+
958
+ /** @type {NodeGroup} */
959
+ parentNg;
960
+
961
+ /** @type {NodeGroup[]} */
962
+ nodeGroups = [];
600
963
 
601
964
 
602
965
  // Caches to make things faster
@@ -606,26 +969,23 @@ class ExprPath {
606
969
  * @type {Node[]} Cached result of getNodes() */
607
970
  nodesCache;
608
971
 
609
- // What are these?
972
+ /**
973
+ * @type {int} Index of nodeBefore among its parentNode's children. */
610
974
  nodeBeforeIndex;
611
- nodeMarkerPath;
612
975
 
613
- // TODO: Keep this cached?
614
- expr;
976
+ /**
977
+ * @type {int[]} Path to the node marker, in reverse for performance reasons. */
978
+ nodeMarkerPath;
615
979
 
616
- // for debugging
617
-
618
980
 
619
981
  /**
620
982
  * @param nodeBefore {Node}
621
983
  * @param nodeMarker {?Node}
622
- * @param type {string}
984
+ * @param type {PathType}
623
985
  * @param attrName {?string}
624
986
  * @param attrValue {string[]} */
625
987
  constructor(nodeBefore, nodeMarker, type=PathType.Content, attrName=null, attrValue=null) {
626
988
 
627
-
628
-
629
989
  // If path is a node.
630
990
  this.nodeBefore = nodeBefore;
631
991
  this.nodeMarker = nodeMarker;
@@ -636,39 +996,245 @@ class ExprPath {
636
996
  this.attrNames = new Set();
637
997
  }
638
998
 
639
- applyMultipleAttribs(node, expr) {
640
-
641
-
642
- if (Array.isArray(expr))
643
- expr = expr.flat().join(' '); // flat and join so we can accept arrays of arrays of strings.
644
-
645
- // Add new attributes
646
- let oldNames = this.attrNames;
647
- this.attrNames = new Set();
648
- if (expr) {
649
- let attrs = (expr +'') // Split string into multiple attributes.
650
- .split(/([\w-]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s]+))/g)
651
- .map(text => text.trim())
652
- .filter(text => text.length);
653
-
654
- for (let attr of attrs) {
655
- let [name, value] = attr.split(/\s*=\s*/); // split on first equals.
656
- value = (value || '').replace(/^(['"])(.*)\1$/, '$2'); // trim value quotes if they match.
657
- node.setAttribute(name, value);
658
- this.attrNames.add(name);
659
- }
999
+ /**
1000
+ * Apply any type of expression.
1001
+ * This calls other apply functions.
1002
+ *
1003
+ * One very messy part of this function is that it may apply multiple expressions if they're all part
1004
+ * of the same attribute value.
1005
+ *
1006
+ * We should modify path.applyValueAttrib so it stores the procssed parts and then only calls
1007
+ * setAttribute() once all the pieces are in place.
1008
+ *
1009
+ * @param expr {Expr}
1010
+ * @param exprs {Expr[]}
1011
+ * @param exprIndex {int}
1012
+ * @param componentExprs {object}
1013
+ * @returns {int} */
1014
+ apply(expr, exprs=null, exprIndex=0, componentExprs={}) {
1015
+ switch (this.type) {
1016
+ case 1: // PathType.Content:
1017
+ this.applyNodes(expr);
1018
+ break;
1019
+ case 2: // PathType.Multiple:
1020
+ this.applyMultipleAttribs(this.nodeMarker, expr);
1021
+ break;
1022
+ case 5: // PathType.Comment:
1023
+ // Expressions inside Html comments. Deliberately empty because we won't waste time updating them.
1024
+ break;
1025
+ case 6: // PathType.Event:
1026
+ this.applyEventAttrib(this.nodeMarker, expr, this.parentNg.rootNg.root);
1027
+ break;
1028
+ default:
1029
+ if (this.type === 4 /*PathType.Component*/ && this.nodeMarker !== this.parentNg.rootNg.root)
1030
+ componentExprs[this.attrName] = expr;
1031
+ else {
1032
+ // One attribute value may have multiple expressions. Here we apply them all at once.
1033
+ exprIndex = this.applyValueAttrib(this.nodeMarker, exprs || [expr], exprIndex);
1034
+ }
1035
+ break;
660
1036
  }
661
1037
 
662
- // Remove old attributes.
663
- for (let oldName of oldNames)
664
- if (!this.attrNames.has(oldName))
665
- node.removeAttribute(oldName);
1038
+ return exprIndex;
666
1039
  }
667
1040
 
668
1041
  /**
669
- * Handle attributes for event binding, such as:
670
- * onclick=${(e, el) => this.doSomething(el, 'meow')}
671
- * onclick=${[this.doSomething, 'meow']}
1042
+ * Insert/replace the nodes created by a single expression.
1043
+ * Called by applyExprs()
1044
+ * This function is recursive, as the functions it calls also call it.
1045
+ * @param expr {Expr}
1046
+ * @return {Node[]} New Nodes created. */
1047
+ applyNodes(expr) {
1048
+ let path = this;
1049
+
1050
+
1051
+
1052
+ /** @type {(Node|NodeGroup|Expr)[]} */
1053
+ let newNodes = [];
1054
+ let oldNodeGroups = path.nodeGroups;
1055
+
1056
+ let secondPass = []; // indices
1057
+
1058
+ path.nodeGroups = []; // Reset before applyExact and the code below rebuilds it.
1059
+ path.applyExact(expr, newNodes, secondPass);
1060
+
1061
+ this.existingTextNodes = null;
1062
+
1063
+ // TODO: Create an array of old vs Nodes and NodeGroups together.
1064
+ // If they're all the same, skip the next steps.
1065
+ // Or calculate it in the loop above as we go? Have a path.lastNodeGroups property?
1066
+
1067
+ // Second pass to find close-match NodeGroups.
1068
+ let flatten = false;
1069
+ if (secondPass.length) {
1070
+ for (let [nodesIndex, ngIndex] of secondPass) {
1071
+ let ng = path.getNodeGroup(newNodes[nodesIndex], false);
1072
+
1073
+ let ngNodes = ng.getNodes();
1074
+
1075
+
1076
+
1077
+ if (ngNodes.length === 1) // flatten manually so we can skip flattening below.
1078
+ newNodes[nodesIndex] = ngNodes[0];
1079
+
1080
+ else {
1081
+ newNodes[nodesIndex] = ngNodes;
1082
+ flatten = true;
1083
+ }
1084
+ path.nodeGroups[ngIndex] = ng;
1085
+ }
1086
+
1087
+ if (flatten)
1088
+ newNodes = newNodes.flat(); // Only if second pass happens.
1089
+ }
1090
+
1091
+
1092
+
1093
+
1094
+
1095
+ let oldNodes = path.getNodes();
1096
+
1097
+
1098
+ // This pre-check makes it a few percent faster?
1099
+ let same = arraySame(oldNodes, newNodes);
1100
+ if (!same) {
1101
+
1102
+ path.nodesCache = newNodes; // Replaces value set by path.getNodes()
1103
+
1104
+ if (this.parentNg.parentPath)
1105
+ this.parentNg.parentPath.clearNodesCache();
1106
+
1107
+ // Fast clear method
1108
+ let isNowEmpty = oldNodes.length && !newNodes.length;
1109
+ if (!isNowEmpty || !path.fastClear())
1110
+
1111
+ // Rearrange nodes.
1112
+ udomdiff(path.nodeMarker.parentNode, oldNodes, newNodes, path.nodeMarker);
1113
+
1114
+ Util.saveOrphans(oldNodeGroups, oldNodes);
1115
+ }
1116
+
1117
+ // Must happen after second pass.
1118
+ path.freeNodeGroups();
1119
+
1120
+
1121
+ }
1122
+
1123
+
1124
+
1125
+ /**
1126
+ * Apply Nodes that are an exact match.
1127
+ * @param expr {Template|Node|Array|function|*}
1128
+ * @param newNodes {(Node|Template)[]}
1129
+ * @param secondPass {Array} Locations within newNodes to evaluate later. */
1130
+ applyExact(expr, newNodes, secondPass) {
1131
+
1132
+ if (expr instanceof Template) {
1133
+
1134
+ let ng = this.getNodeGroup(expr, true);
1135
+ if (ng) {
1136
+
1137
+ // TODO: Track ranges of changed nodes and only pass those to udomdiff?
1138
+ // But will that break the swap benchmark?
1139
+ newNodes.push(...ng.getNodes());
1140
+ this.nodeGroups.push(ng);
1141
+ }
1142
+
1143
+ // If expression, mark it to be evaluated later in ExprPath.apply() to find partial match.
1144
+ else {
1145
+ secondPass.push([newNodes.length, this.nodeGroups.length]);
1146
+ newNodes.push(expr);
1147
+ this.nodeGroups.push(null); // placeholder
1148
+ }
1149
+ }
1150
+
1151
+ // Node created by an expression.
1152
+ else if (expr instanceof Node) {
1153
+
1154
+ // DocumentFragment created by an expression.
1155
+ if (expr instanceof DocumentFragment)
1156
+ newNodes.push(...expr.childNodes);
1157
+ else
1158
+ newNodes.push(expr);
1159
+ }
1160
+
1161
+ // Arrays and functions.
1162
+ // I tried iterating over the result of a generator function to avoid this recursion and simplify the code,
1163
+ // but that consistently made the js-framework-benchmarks a few percentage points slower.
1164
+ else if (Array.isArray(expr))
1165
+ for (let subExpr of expr)
1166
+ this.applyExact(subExpr, newNodes, secondPass);
1167
+
1168
+ else if (typeof expr === 'function') {
1169
+ Globals.currentExprPath = [this, expr]; // Used by watch3()
1170
+ let result = expr();
1171
+ Globals.currentExprPath = null;
1172
+
1173
+ this.applyExact(result, newNodes, secondPass);
1174
+ }
1175
+
1176
+ // Text
1177
+ else {
1178
+ // Convert falsy values (but not 0) to empty string.
1179
+ // Convert numbers to string so they compare the same.
1180
+ let text = (expr === undefined || expr === false || expr === null) ? '' : (expr + '');
1181
+
1182
+ // Fast path for updating the text of a single text node.
1183
+ let first = this.nodeBefore.nextSibling;
1184
+ if (first.nodeType === 3 && first.nextSibling === this.nodeMarker && !newNodes.includes(first)) {
1185
+ if (first.textContent !== text)
1186
+ first.textContent = text;
1187
+
1188
+ newNodes.push(first);
1189
+ }
1190
+
1191
+ else {
1192
+ // TODO: Optimize this into a Set or Map or something?
1193
+ if (!this.existingTextNodes)
1194
+ this.existingTextNodes = this.getNodes().filter(n => n.nodeType === 3);
1195
+
1196
+ let idx = this.existingTextNodes.findIndex(n => n.textContent === text);
1197
+ if (idx !== -1)
1198
+ newNodes.push(...this.existingTextNodes.splice(idx, 1));
1199
+ else
1200
+ newNodes.push(this.nodeMarker.ownerDocument.createTextNode(text));
1201
+ }
1202
+ }
1203
+ }
1204
+
1205
+ applyMultipleAttribs(node, expr) {
1206
+
1207
+
1208
+ if (Array.isArray(expr))
1209
+ expr = expr.flat().join(' '); // flat and join so we can accept arrays of arrays of strings.
1210
+
1211
+ // Add new attributes
1212
+ let oldNames = this.attrNames;
1213
+ this.attrNames = new Set();
1214
+ if (expr) {
1215
+ let attrs = (expr +'') // Split string into multiple attributes.
1216
+ .split(/([\w-]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s]+))/g)
1217
+ .map(text => text.trim())
1218
+ .filter(text => text.length);
1219
+
1220
+ for (let attr of attrs) {
1221
+ let [name, value] = attr.split(/\s*=\s*/); // split on first equals.
1222
+ value = (value || '').replace(/^(['"])(.*)\1$/, '$2'); // trim value quotes if they match.
1223
+ node.setAttribute(name, value);
1224
+ this.attrNames.add(name);
1225
+ }
1226
+ }
1227
+
1228
+ // Remove old attributes.
1229
+ for (let oldName of oldNames)
1230
+ if (!this.attrNames.has(oldName))
1231
+ node.removeAttribute(oldName);
1232
+ }
1233
+
1234
+ /**
1235
+ * Handle attributes for event binding, such as:
1236
+ * onclick=${(e, el) => this.doSomething(el, 'meow')}
1237
+ * oninput=${[this.doSomething, 'meow']}
672
1238
  * onclick=${[this, 'doSomething', 'meow']}
673
1239
  *
674
1240
  * @param node
@@ -677,78 +1243,91 @@ class ExprPath {
677
1243
  applyEventAttrib(node, expr, root) {
678
1244
 
679
1245
 
680
- let eventName = this.attrName.slice(2);
1246
+ let eventName = this.attrName.slice(2); // remove "on-" prefix.
681
1247
  let func;
682
1248
 
683
1249
  // Convert array to function.
684
- // TODO: This doesn't work for [this, 'doSomething', 'meow']
685
1250
  let args = [];
686
1251
  if (Array.isArray(expr)) {
687
- for (let i=0; i<expr.length; i++)
688
- if (typeof expr[i] === 'function') {
689
- func = expr[i];
690
- args = expr.slice(i+1);
691
- break;
692
- }
693
1252
 
1253
+ // oninput=${[this.doSomething, 'meow']}
1254
+ if (typeof expr[0] === 'function') {
1255
+ func = expr[0];
1256
+ args = expr.slice(1);
1257
+ }
1258
+
1259
+ // Undocumented.
694
1260
  // oninput=${[this, 'value']}
695
- if (!func) {
1261
+ else {
696
1262
  func = setValue;
697
1263
  args = [expr[0], expr.slice(1), node];
698
1264
  node.value = delve(expr[0], expr.slice(1));
699
- //debugger;
1265
+ // root.render(); // TODO: This causes infinite recursion.
700
1266
  }
701
1267
  }
702
1268
  else
703
1269
  func = expr;
704
1270
 
705
- let eventKey = getObjectId(node) + eventName;
706
- let [existing, existingBound] = nodeEvents[eventKey] || [];
707
- nodeEventArgs[eventKey] = args; // TODO: Put this in nodeEvents[]
1271
+ let nodeEvents = Globals.nodeEvents.get(node);
1272
+ if (!nodeEvents) {
1273
+ nodeEvents = {[eventName]: new Array(3)};
1274
+ Globals.nodeEvents.set(node, nodeEvents);
1275
+ }
1276
+ let nodeEvent = nodeEvents[eventName];
1277
+
708
1278
 
709
1279
 
710
- if (existing !== func) {
1280
+ // If function has changed, remove and rebind the event.
1281
+ if (nodeEvent[0] !== func) {
1282
+ let [existing, existingBound, _] = nodeEvent;
711
1283
  if (existing)
712
1284
  node.removeEventListener(eventName, existingBound);
713
1285
 
714
1286
  let originalFunc = func;
715
1287
 
716
1288
  // BoundFunc sets the "this" variable to be the current Solarite component.
717
- let boundFunc = event => originalFunc.call(root, ...args, event, node);
1289
+ let boundFunc = (event) => {
1290
+ let args = nodeEvent[2];
1291
+ return originalFunc.call(root, ...args, event, node);
1292
+ };
718
1293
 
719
1294
  // Save both the original and bound functions.
720
1295
  // Original so we can compare it against a newly assigned function.
721
1296
  // Bound so we can use it with removeEventListner().
722
- nodeEvents[eventKey] = [originalFunc, boundFunc];
1297
+ nodeEvent[0] = originalFunc;
1298
+ nodeEvent[1] = boundFunc;
723
1299
 
724
1300
  node.addEventListener(eventName, boundFunc);
725
1301
 
726
- // TODO: classic event attribs:
1302
+ // TODO: classic event attribs?
727
1303
  //el[attr.name] = e => // e.g. el.onclick = ...
728
1304
  // (new Function('event', 'el', attr.value)).bind(this.manager.rootEl)(e, el) // put "event", "el", and "this" in scope for the event code.
729
1305
  }
1306
+
1307
+ // Otherwise just update the args to the function.
1308
+ nodeEvents[eventName][2] = args;
730
1309
  }
731
1310
 
732
1311
  applyValueAttrib(node, exprs, exprIndex) {
733
1312
  let expr = exprs[exprIndex];
734
-
735
- // Array for form element data binding.
736
- // TODO: This never worked, and was moved to applyEventAttrib.
737
- // let isArrayValue = Array.isArray(expr);
738
- // if (isArrayValue && expr.length >= 2 && !expr.slice(1).find(v => !['string', 'number'].includes(typeof v))) {
739
- // node.value = delve(expr[0], expr.slice(1));
740
- // node.addEventListener('input', e => {
741
- // delve(expr[0], expr.slice(1), node.value) // TODO: support other properties like checked
742
- // });
743
- // }
744
1313
 
745
1314
  // Values to toggle an attribute
746
1315
  if (!this.attrValue && (expr === false || expr === null || expr === undefined))
747
1316
  node.removeAttribute(this.attrName);
748
-
1317
+
749
1318
  else if (!this.attrValue && expr === true)
750
1319
  node.setAttribute(this.attrName, '');
751
1320
 
1321
+ // Passing a path to the value attribute.
1322
+ // This same logic is in NodeGroup.createNewComponent() for components.
1323
+ else if ((this.attrName === 'value' || this.attrName === 'data-value') && Util.isPath(expr)) {
1324
+ let [obj, path] = [expr[0], expr.slice(1)];
1325
+ node.value = delve(obj, path);
1326
+ node.addEventListener('input', () => {
1327
+ delve(obj, path, Util.getInputValue(node));
1328
+ }, true); // We use capture so we update the values before other events added by the user.
1329
+ }
1330
+
752
1331
  // Regular attribute
753
1332
  else {
754
1333
  let value = [];
@@ -764,21 +1343,25 @@ class ExprPath {
764
1343
  exprIndex--;
765
1344
  }
766
1345
  }
767
-
768
1346
  exprIndex ++;
769
1347
  }
770
1348
  else
771
1349
  value.unshift(expr);
772
1350
 
773
1351
  let joinedValue = value.join('');
774
- node.setAttribute(this.attrName, joinedValue);
1352
+
1353
+ // Only update attributes if the value has changed.
1354
+ // The .value property is special. If it changes we don't update the attribute.
1355
+ let oldVal = this.attrName === 'value' ? node.value : node.getAttribute(this.attrName);
1356
+ if (oldVal !== joinedValue) {
1357
+ node.setAttribute(this.attrName, joinedValue);
1358
+ }
775
1359
 
776
1360
  // This is needed for setting input.value, .checked, option.selected, etc.
777
1361
  // But in some cases setting the attribute is enough. such as div.setAttribute('title') updates div.title.
778
1362
  // TODO: How to tell which is which?
779
1363
  if (this.attrName in node)
780
1364
  node[this.attrName] = joinedValue;
781
-
782
1365
  }
783
1366
 
784
1367
  return exprIndex;
@@ -788,20 +1371,22 @@ class ExprPath {
788
1371
  /**
789
1372
  *
790
1373
  * @param newRoot {HTMLElement}
1374
+ * @param pathOffset {int}
791
1375
  * @return {ExprPath} */
792
- clone(newRoot) {
1376
+ clone(newRoot, pathOffset=0) {
793
1377
 
794
1378
 
795
- // Resolve node paths.
1379
+ // Resolve node paths.
796
1380
  let nodeMarker, nodeBefore;
797
- let root = newRoot;
798
- let path = this.nodeMarkerPath;
799
- for (let i=path.length-1; i>0; i--)
800
- root = root.childNodes[path[i]];
1381
+ let root = newRoot;
1382
+ let path = pathOffset ? this.nodeMarkerPath.slice(0, -pathOffset) : this.nodeMarkerPath;
1383
+ for (let i=path.length-1; i>0; i--) // Resolve the path.
1384
+ root = root.childNodes[path[i]];
801
1385
  let childNodes = root.childNodes;
802
- nodeMarker = childNodes[path[0]];
803
- if (this.nodeBefore)
804
- nodeBefore = childNodes[this.nodeBeforeIndex];
1386
+
1387
+ nodeMarker = path.length ? childNodes[path[0]] : newRoot;
1388
+ if (this.nodeBefore)
1389
+ nodeBefore = childNodes[this.nodeBeforeIndex];
805
1390
 
806
1391
  let result = new ExprPath(nodeBefore, nodeMarker, this.type, this.attrName, this.attrValue);
807
1392
 
@@ -809,41 +1394,27 @@ class ExprPath {
809
1394
 
810
1395
  return result;
811
1396
  }
812
-
1397
+
813
1398
  /**
814
1399
  * Clear the nodeCache of this ExprPath, as well as all parent and child ExprPaths that
815
1400
  * share the same DOM parent node.
816
1401
  *
817
- * TODO: Is recursive clearing ever necessary?
818
- */
1402
+ * TODO: Is recursive clearing ever necessary? */
819
1403
  clearNodesCache() {
820
1404
  let path = this;
821
-
1405
+
822
1406
  // Clear cache parent ExprPaths that have the same parentNode
823
- let parentNode = this.parentNode;
824
- while (path && path.parentNode === parentNode) {
1407
+ let parentNode = this.nodeMarker.parentNode;
1408
+ while (path && path.nodeMarker.parentNode === parentNode) {
825
1409
  path.nodesCache = null;
826
1410
  path = path.parentNg?.parentPath;
827
-
1411
+
828
1412
  // If stuck in an infinite loop here, the problem is likely due to Template hash colisions.
829
1413
  // Which cause one path to be the descendant of itself, creating a cycle.
830
1414
  }
831
-
832
- function clearChildNodeCache(path) {
833
-
834
- // Clear cache of child ExprPaths that have the same parentNode
835
- for (let ng of path.nodeGroups) {
836
- if (ng) // Can be null from applyOneExpr()'s push(null) call.
837
- for (let path2 of ng.paths) {
838
- if (path2.type === PathType.Content && path2.parentNode === parentNode) {
839
- path2.nodesCache = null;
840
- clearChildNodeCache(path2);
841
- }
842
- }
843
- }
844
- }
845
-
846
- clearChildNodeCache(this);
1415
+
1416
+ // Commented out on Sep 30, 2024 b/c it was making the benchmark never finish when adding 10k rows.
1417
+ //clearChildNodeCache(this);
847
1418
  }
848
1419
 
849
1420
 
@@ -856,44 +1427,46 @@ class ExprPath {
856
1427
 
857
1428
  // If parent is the only child of the grandparent, replace the whole parent.
858
1429
  // And if it has no siblings, it's not created by a NodeGroup/path.
859
- let grandparent = parent.parentNode;
860
- if (grandparent && parent === grandparent.firstChild && parent === grandparent.lastChild && !parent.hasAttribute('id')) {
861
- let replacement = document.createElement(parent.tagName);
862
- replacement.append(this.nodeBefore, this.nodeMarker);
863
- for (let attrib of parent.attributes)
864
- replacement.setAttribute(attrib.name, attrib.value);
865
- parent.replaceWith(replacement);
866
- }
867
- else {
1430
+ // Commented out because this will break any references.
1431
+ // And because I don't see much performance difference.
1432
+ // let grandparent = parent.parentNode
1433
+ // if (grandparent && parent === grandparent.firstChild && parent === grandparent.lastChild && !parent.hasAttribute('id')) {
1434
+ // let replacement = document.createElement(parent.tagName)
1435
+ // replacement.append(this.nodeBefore, this.nodeMarker)
1436
+ // for (let attrib of parent.attributes)
1437
+ // replacement.setAttribute(attrib.name, attrib.value)
1438
+ // parent.replaceWith(replacement)
1439
+ // }
1440
+ // else {
868
1441
  parent.innerHTML = ''; // Faster than calling .removeChild() a thousand times.
869
1442
  parent.append(this.nodeBefore, this.nodeMarker);
870
- }
1443
+ //}
871
1444
  return true;
872
1445
  }
873
1446
  return false;
874
1447
  }
875
-
1448
+
876
1449
  /**
877
1450
  * @return {(Node|HTMLElement)[]} */
878
1451
  getNodes() {
879
-
1452
+
880
1453
  // Why doesn't this work?
881
1454
  // let result2 = [];
882
1455
  // for (let ng of this.nodeGroups)
883
1456
  // result2.push(...ng.getNodes())
884
1457
  // return result2;
885
-
886
-
1458
+
1459
+
887
1460
  let result;
888
1461
 
889
1462
  // This shaves about 5ms off the partialUpdate benchmark.
890
- /*result = this.nodesCache;
1463
+ result = this.nodesCache;
891
1464
  if (result) {
1465
+
892
1466
 
893
-
894
-
1467
+
895
1468
  return result
896
- }*/
1469
+ }
897
1470
 
898
1471
  result = [];
899
1472
  let current = this.nodeBefore.nextSibling;
@@ -910,13 +1483,100 @@ class ExprPath {
910
1483
  getParentNode() { // Same as this.parentNode
911
1484
  return this.nodeMarker.parentNode
912
1485
  }
913
-
914
- removeNodeGroup(ng) {
915
- let idx = this.nodeGroups.indexOf(ng);
1486
+
1487
+ /**
1488
+ * Get an unused NodeGroup that matches the template's html and expressions (exact=true)
1489
+ * or at least the html (exact=false).
1490
+ * Remove it from nodeGroupsFree if it exists, or create it if not.
1491
+ * Then add it to nodeGroupsInUse.
1492
+ *
1493
+ * @param template {Template}
1494
+ * @param exact {boolean}
1495
+ * If true, return an exact match, or null.
1496
+ * If false, either find a match for the template's html and then apply the template's expressions,
1497
+ * or createa new NodeGroup from the template.
1498
+ * @return {NodeGroup} */
1499
+ getNodeGroup(template, exact=true) {
1500
+ //if (exact && this.nodeGroupsFree.isEmpty())
1501
+ // return null;
1502
+
1503
+ let result;
1504
+
1505
+ if (exact) {
1506
+ result = this.nodeGroupsFree.delete(template.getExactKey());
1507
+ if (result) // also delete the matching close key.
1508
+ this.nodeGroupsFree.delete(template.getCloseKey(), result);
1509
+ else
1510
+ return null;
1511
+ }
1512
+
1513
+ // Find a close match.
1514
+ // This is a match that has matching html, but different expressions applied.
1515
+ // We can then apply the expressions to make it an exact match.
1516
+ else {
1517
+ result = this.nodeGroupsFree.delete(template.getCloseKey());
1518
+ if (result) {
1519
+
1520
+ this.nodeGroupsFree.delete(result.exactKey, result);
1521
+
1522
+ // Update this close match with the new expression values.
1523
+ result.applyExprs(template.exprs);
1524
+ result.exactKey = template.getExactKey(); // TODO: Should this be set elsewhere?
1525
+ }
1526
+ }
1527
+
1528
+ if (!result)
1529
+ result = new NodeGroup(template, this);
1530
+
1531
+ // old:
1532
+ this.nodeGroupsInUse.push(result);
1533
+
1534
+ // new:
1535
+ // let ngiu = this.nodeGroupsInUse;
1536
+ // ngiu.add(result.exactKey, result);
1537
+ // ngiu.add(result.closeKey, result);
1538
+
916
1539
 
917
- this.nodeGroups.splice(idx);
918
- ng.parentPath = null;
919
- this.clearNodesCache();
1540
+ return result;
1541
+ }
1542
+
1543
+
1544
+ /**
1545
+ * Used with getNodeGroup() and freeNodeGroups().
1546
+ * TODO: Use an array of WeakRef so the gc can collect them?
1547
+ * TODO: Put items back in nodeGroupsInUse after applyExpr() is called, not before.
1548
+ * @type {NodeGroup[]} */
1549
+ nodeGroupsInUse = [];
1550
+
1551
+ /** @type {MultiValueMap<key:string, value:NodeGroup>} */
1552
+ //nodeGroupsInUse = new MultiValueMap();
1553
+
1554
+ /**
1555
+ * Used with getNodeGroup() and freeNodeGroups().
1556
+ * Each NodeGroup is here twice, once under an exact key, and once under the close key.
1557
+ * @type {MultiValueMap<key:string, value:NodeGroup>} */
1558
+ nodeGroupsFree = new MultiValueMap();
1559
+
1560
+
1561
+ /**
1562
+ * Move everything from this.nodeGroupsInUse to this.nodeGroupsFree.
1563
+ * TODO: this could run as needed in getNodeGroup? */
1564
+ freeNodeGroups() {
1565
+ // old:
1566
+ let ngf = this.nodeGroupsFree;
1567
+ for (let ng of this.nodeGroupsInUse) {
1568
+ ngf.add(ng.exactKey, ng);
1569
+ ngf.add(ng.closeKey, ng);
1570
+ }
1571
+ this.nodeGroupsInUse = [];
1572
+
1573
+ // new:
1574
+ // for (let key in this.nodeGroupsFree.data)
1575
+ // for (let item of this.nodeGroupsFree.data[key])
1576
+ // this.nodeGroupsInUse.add(key, item);
1577
+ //
1578
+ // this.nodeGroupsFree = this.nodeGroupsInUse;
1579
+ // this.nodeGroupsInUse = new MultiValueMap();
920
1580
  }
921
1581
 
922
1582
 
@@ -936,26 +1596,27 @@ function setValue(root, path, node) {
936
1596
  val = parseFloat(val);
937
1597
 
938
1598
  delve(root, path, val);
939
-
940
- //this.render();
941
1599
  }
942
1600
 
943
- /** @enum {string} */
1601
+ /** @enum {int} */
944
1602
  const PathType = {
945
1603
  /** Child of a node */
946
- Content: 'content',
1604
+ Content: 1,
947
1605
 
948
1606
  /** One or more whole attributes */
949
- Multiple: 'attrName',
1607
+ Multiple: 2,
950
1608
 
951
1609
  /** Value of an attribute. */
952
- Value: 'attrValue',
1610
+ Value: 3,
953
1611
 
954
1612
  /** Value of an attribute being passed to a component. */
955
- Component: 'component',
1613
+ Component: 4,
956
1614
 
957
1615
  /** Expressions inside Html comments. */
958
- Comment: 'comment',
1616
+ Comment: 5,
1617
+
1618
+ /** Value of an attribute. */
1619
+ Event: 6,
959
1620
  };
960
1621
 
961
1622
 
@@ -981,1278 +1642,299 @@ function resolveNodePath(root, path) {
981
1642
  for (let i=path.length-1; i>=0; i--)
982
1643
  root = root.childNodes[path[i]];
983
1644
  return root;
984
- }
985
-
986
-
987
- // TODO: Memory from this is never freed. Use a WeakMap<Node, Object<eventName:string, function[]>>
988
- let nodeEvents = {};
989
- let nodeEventArgs = {};
990
-
991
- /**
992
- * A Shell is created from a tagged template expression instantiated as Nodes,
993
- * but without any expressions filled in. */
994
- class Shell {
995
-
996
- /**
997
- * @type {DocumentFragment} Parent of the shell nodes. */
998
- fragment;
999
-
1000
- /** @type {ExprPath[]} Paths to where expressions should go. */
1001
- paths = [];
1002
-
1003
- /** @type {?Template} Template that created this element. */
1004
- template;
1005
-
1006
- // Embeds and ids
1007
- events = [];
1008
-
1009
- /** @type {int[][]} Array of paths */
1010
- ids = [];
1011
- scripts = [];
1012
- styles = [];
1013
-
1014
- staticComponents = [];
1015
-
1016
-
1017
- /**
1018
- * Create the nodes but without filling in the expressions.
1019
- * This is useful because the expression-less nodes created by a template can be cached.
1020
- * @param html {string[]} */
1021
- constructor(html=null) {
1022
- if (!html)
1023
- return;
1024
-
1025
-
1026
-
1027
- // 1. Add placeholders
1028
- // We increment the placeholder char as we go because nodes can't have the same attribute more than once.
1029
- let placeholder = 0xe000; // https://en.wikipedia.org/wiki/Private_Use_Areas 6400.
1030
-
1031
- let buffer = [];
1032
- let commentPlaceholder = `<!--!✨!-->`;
1033
- let componentNames = {};
1034
-
1035
- htmlContext(null); // Reset the context.
1036
- for (let i=0; i<html.length; i++) {
1037
- let lastHtml = html[i];
1038
- let context = htmlContext(lastHtml);
1039
-
1040
- // Swap out Embedded Solarite Components with ${} attributes.
1041
- // Later, NodeGroup.render() will search for these and replace them with the real components.
1042
- // Ctrl+F "redcomponent-placeholder" in project to find all code that manages subcomponents.
1043
- if (context === htmlContext.Attribute) {
1044
-
1045
- let lastIndex, lastMatch;
1046
- lastHtml.replace(/<[a-z][a-z0-9]*-[a-z0-9-]+/ig, (match, index) => {
1047
- lastIndex = index+1; // +1 for after opening <
1048
- lastMatch = match.slice(1);
1049
- });
1050
-
1051
- if (lastMatch) {
1052
- let newTagName = lastMatch + '-redcomponent-placeholder';
1053
- lastHtml = lastHtml.slice(0, lastIndex) + newTagName + lastHtml.slice(lastIndex + lastMatch.length);
1054
- componentNames[lastMatch] = newTagName;
1055
- }
1056
- }
1057
-
1058
- buffer.push(lastHtml);
1059
- //console.log(lastHtml, context)
1060
- if (i < html.length-1)
1061
- if (context === htmlContext.Text)
1062
- buffer.push(commentPlaceholder); // Comment Placeholder. because we can't put text in between <tr> tags for example.
1063
- else
1064
- buffer.push(String.fromCharCode(placeholder+i));
1065
- }
1066
-
1067
- // 2. Create elements from html with placeholders.
1068
- let template = document.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
1069
- let joinedHtml = buffer.join('');
1070
-
1071
- // Replace '-redcomponent-placeholder' close tags.
1072
- // TODO: is there a better way? What if the close tag is inside a comment?
1073
- for (let name in componentNames)
1074
- joinedHtml = joinedHtml.replaceAll(`</${name}>`, `</${componentNames[name]}>`);
1075
-
1076
- if (joinedHtml)
1077
- template.innerHTML = joinedHtml;
1078
- else // Create one text node, so shell isn't empty and NodeGroups created from it have something to point the startNode and endNode at.
1079
- template.content.append(document.createTextNode(''));
1080
- this.fragment = template.content;
1081
-
1082
- // 3. Find placeholders
1083
- let node;
1084
- let toRemove = [];
1085
- const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
1086
- while (node = walker.nextNode()) {
1087
-
1088
- // Remove previous after each iteration, so paths will still be calculated correctly.
1089
- toRemove.map(el => el.remove());
1090
- toRemove = [];
1091
-
1092
- // Replace attributes
1093
- if (node.nodeType === 1) {
1094
- for (let attr of node.attributes) {
1095
-
1096
- // Whole attribute
1097
- let matches = attr.name.match(/^[\ue000-\uf8ff]$/);
1098
- if (matches) {
1099
- this.paths.push(new ExprPath(null, node, PathType.Multiple));
1100
- node.removeAttribute(matches[0]);
1101
- }
1102
-
1103
- // Just the attribute value.
1104
- else {
1105
- let parts = attr.value.split(/[\ue000-\uf8ff]/g);
1106
- if (parts.length > 1) {
1107
- let nonEmptyParts = (parts.length === 2 && !parts[0].length && !parts[1].length) ? null : parts;
1108
- this.paths.push(new ExprPath(null, node, PathType.Value, attr.name, nonEmptyParts));
1109
- node.setAttribute(attr.name, parts.join(''));
1110
- }
1111
- }
1112
- }
1113
- }
1114
- // Replace comment placeholders
1115
- else if (node.nodeType === Node.COMMENT_NODE && node.nodeValue === '!✨!') {
1116
-
1117
- // Get or create nodeBefore.
1118
- let nodeBefore = node.previousSibling; // Can be the same as another Path's nodeMarker.
1119
- if (!nodeBefore) {
1120
- nodeBefore = document.createComment('PathStart:'+this.paths.length);
1121
- node.parentNode.insertBefore(nodeBefore, node);
1122
- }
1123
-
1124
-
1125
- // Get the next node.
1126
- let nodeMarker;
1127
-
1128
- // A subsequent node is available to be a nodeMarker.
1129
- if (node.nextSibling && (node.nextSibling.nodeType !== 8 || node.nextSibling.textContent !== '!✨!')) {
1130
- nodeMarker = node.nextSibling;
1131
- toRemove.push(node); // Removing them here will mess up the treeWalker.
1132
- }
1133
- // Re-use existing comment placeholder.
1134
- else {
1135
- nodeMarker = node;
1136
- nodeMarker.textContent = 'PathEnd:'+ this.paths.length;
1137
- }
1138
-
1139
-
1140
-
1141
-
1142
- let path = new ExprPath(nodeBefore, nodeMarker, PathType.Content);
1143
-
1144
- this.paths.push(path);
1145
- }
1146
-
1147
-
1148
- // Sometimes users will comment out a block of html code that has expressions.
1149
- // Here we look for expressions in comments.
1150
- // We don't actually update them dynamically, but we still add paths for them.
1151
- // That way the expression count still matches.
1152
- else if (node.nodeType === Node.COMMENT_NODE) {
1153
- let parts = node.textContent.split(/[\ue000-\uf8ff]/g);
1154
- for (let i=0; i<parts.length-1; i++) {
1155
- let path = new ExprPath(node.previousSibling, node);
1156
- path.type = PathType.Comment;
1157
-
1158
- this.paths.push(path);
1159
- }
1160
- }
1161
-
1162
- // Replace comment placeholders inside script and style tags, which have become text nodes.
1163
- else if (node.nodeType === Node.TEXT_NODE && ['SCRIPT', 'STYLE'].includes(node.parentNode?.nodeName)) {
1164
- let parts = node.textContent.split(commentPlaceholder);
1165
- if (parts.length > 1) {
1166
-
1167
- let placeholders = [];
1168
- for (let i = 0; i<parts.length; i++) {
1169
- let current = document.createTextNode(parts[i]);
1170
- node.parentNode.insertBefore(current, node);
1171
- if (i > 0)
1172
- placeholders.push(current);
1173
- }
1174
-
1175
- for (let i=0, node; node=placeholders[i]; i++) {
1176
- let path = new ExprPath(node.previousSibling, node, PathType.Content);
1177
-
1178
- this.paths.push(path);
1179
-
1180
-
1181
- }
1182
-
1183
- // Removing them here will mess up the treeWalker.
1184
- toRemove.push(node);
1185
- }
1186
- }
1187
- }
1188
- toRemove.map(el => el.remove());
1189
-
1190
- // Handle redcomponent-placeholder's.
1191
- // Ctrl+F "redcomponent-placeholder" in project to find all code that manages subcomponents.
1192
- //if (componentNames.size)
1193
- // this.components = [...this.fragment.querySelectorAll([...componentNames].join(','))]
1194
-
1195
- // Rename "is" attributes so the Web Components don't instantiate until we have the values of their PathExpr arguments.
1196
- // that happens in NodeGroup.applyComponentExprs()
1197
- for (let el of this.fragment.querySelectorAll('[is]')) {
1198
- el.setAttribute('_is', el.getAttribute('is'));
1199
- // this.components.push(el);
1200
- }
1201
-
1202
- for (let path of this.paths) {
1203
- if (path.nodeBefore)
1204
- path.nodeBeforeIndex = Array.prototype.indexOf.call(path.nodeBefore.parentNode.childNodes, path.nodeBefore);
1205
- path.nodeMarkerPath = getNodePath(path.nodeMarker);
1206
-
1207
- // Cache so we don't have to calculate this later inside NodeGroup.applyExprs()
1208
- if (path.type === PathType.Value && path.nodeMarker.nodeType === 1 &&
1209
- (path.nodeMarker.tagName.includes('-') || path.nodeMarker.hasAttribute('is'))) {
1210
- path.type = PathType.Component;
1211
- }
1212
- }
1213
-
1214
-
1215
- this.findEmbeds();
1216
-
1217
-
1218
-
1219
- } // end constructor
1220
-
1221
- /**
1222
- * We find the path to every embed here once in the Shell, instead of every time a NodeGroup is instantiated.
1223
- * When a Nodegroup is created, it calls NodeGroup.activateEmbeds() that uses these paths. */
1224
- findEmbeds() {
1225
- this.scripts = Array.prototype.map.call(this.fragment.querySelectorAll('scripts'), el => getNodePath(el));
1226
- this.styles = Array.prototype.map.call(this.fragment.querySelectorAll('style'), el => getNodePath(el));
1227
-
1228
- let idEls = this.fragment.querySelectorAll('[id],[data-id]');
1229
-
1230
-
1231
- // Check for valid id names.
1232
- for (let el of idEls) {
1233
- let id = el.getAttribute('data-id') || el.getAttribute('id');
1234
- if (div.hasOwnProperty(id))
1235
- throw new Error(`<${el.tagName.toLowerCase()} id="${id}"> can't override existing HTMLElement property.`)
1236
- }
1237
-
1238
-
1239
- this.ids = Array.prototype.map.call(idEls, el => getNodePath(el));
1240
-
1241
- // Events (not yet used)
1242
- for (let el of this.fragment.querySelectorAll('*')) {
1243
- for (let attrib of el.attributes)
1244
- if (isEvent(attrib.name))
1245
- this.events.push([attrib.name, getNodePath(el)]);
1246
-
1247
- if (el.tagName.includes('-') || el.hasAttribute('_is'))
1248
-
1249
- // Dynamic components have attributes with expression values.
1250
- // They are created from applyExprs()
1251
- // But static components are created in a separate path inside the NodeGroup constructor.
1252
- if (!this.paths.find(path => path.nodeMarker === el))
1253
- this.staticComponents.push(getNodePath(el));
1254
- }
1255
-
1256
- }
1257
-
1258
- /**
1259
- * Get the shell for the html strings.
1260
- * @param htmlStrings {string[]}
1261
- * @returns {Shell} */
1262
- static get(htmlStrings) {
1263
- let result = shells.get(htmlStrings);
1264
- if (!result) {
1265
- result = new Shell(htmlStrings);
1266
- shells.set(htmlStrings, result); // cache
1267
- }
1268
-
1269
-
1270
- return result;
1271
- }
1272
-
1273
-
1274
- }
1275
-
1276
- let shells = new WeakMap();
1277
-
1278
- /**
1279
- * The html strings and evaluated expressions from an html tagged template.
1280
- * A unique Template is created for each item in a loop.
1281
- * Although the reference to the html strings is shared among templates. */
1282
- class Template {
1283
-
1284
- /** @type {(Template|string|function)|(Template|string|function)[]} Evaulated expressions. */
1285
- exprs = []
1286
-
1287
- /** @type {string[]} */
1288
- html = [];
1289
-
1290
- /**
1291
- * If true, use this template to replace an existing element, instead of appending children to it.
1292
- * @type {?boolean} */
1293
- replaceMode;
1294
-
1295
- /** Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
1296
- hashedFields;
1297
-
1298
- /**
1299
- * @deprecated
1300
- * @type {ExprPath} Used with forEach() from watch.js
1301
- * Set in NodeGroup.applyOneExpr() */
1302
- parentPath;
1303
-
1304
- /** @type {NodeGroup} */
1305
- nodeGroup;
1306
-
1307
- /**
1308
- * @type {string[][]} */
1309
- paths = [];
1310
-
1311
- /**
1312
- *
1313
- * @param htmlStrings {string[]}
1314
- * @param exprs {*[]} */
1315
- constructor(htmlStrings, exprs) {
1316
- this.html = htmlStrings;
1317
- this.exprs = exprs;
1318
-
1319
- //this.trace = new Error().stack.split(/\n/g)
1320
-
1321
- // Multiple templates can share the same htmlStrings array.
1322
- //this.hashedFields = [getObjectId(htmlStrings), exprs]
1323
-
1324
-
1325
- }
1326
-
1327
- /**
1328
- * Called by JSON.serialize when it encounters a Template.
1329
- * This prevents the hashed version from being too large. */
1330
- toJSON() {
1331
- if (!this.hashedFields)
1332
- this.hashedFields = [getObjectId(this.html, 'Html'), this.exprs];
1333
-
1334
- return this.hashedFields
1335
- }
1336
-
1337
- toNode() {
1338
- let ngm = new NodeGroupManager();
1339
- return ngm.render(this);
1340
- }
1341
-
1342
- getCloseKey() {
1343
- // Use the joined html when debugging?
1344
- //return '@'+this.html.join('|')
1345
-
1346
- return '@'+this.hashedFields[0];
1347
- }
1348
- }
1349
-
1350
- /**
1351
- * ISC License
1352
- *
1353
- * Copyright (c) 2020, Andrea Giammarchi, @WebReflection
1354
- *
1355
- * Permission to use, copy, modify, and/or distribute this software for any
1356
- * purpose with or without fee is hereby granted, provided that the above
1357
- * copyright notice and this permission notice appear in all copies.
1358
- *
1359
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
1360
- * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
1361
- * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
1362
- * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
1363
- * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
1364
- * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
1365
- * PERFORMANCE OF THIS SOFTWARE.
1366
- */
1367
-
1368
- /**
1369
- * @param {Node} parentNode The container where children live
1370
- * @param {Node[]} a The list of current/live children
1371
- * @param {Node[]} b The list of future children
1372
- * @param {(entry: Node, action: number) => Node} get
1373
- * The callback invoked per each entry related DOM operation.
1374
- * @param {Node} [before] The optional node used as anchor to insert before.
1375
- * @returns {Node[]} The same list of future children.
1376
- */
1377
- const udomdiff = (parentNode, a, b, before) => {
1378
-
1379
-
1380
- const bLength = b.length;
1381
- let aEnd = a.length;
1382
- let bEnd = bLength;
1383
- let aStart = 0;
1384
- let bStart = 0;
1385
- let map = null;
1386
- while (aStart < aEnd || bStart < bEnd) {
1387
- // append head, tail, or nodes in between: fast path
1388
- if (aEnd === aStart) {
1389
- // we could be in a situation where the rest of nodes that
1390
- // need to be added are not at the end, and in such case
1391
- // the node to `insertBefore`, if the index is more than 0
1392
- // must be retrieved, otherwise it's gonna be the first item.
1393
- const node = bEnd < bLength
1394
- ? (bStart
1395
- ? (b[bStart - 1].nextSibling)
1396
- : b[bEnd - bStart])
1397
- : before;
1398
- while (bStart < bEnd) {
1399
- let bNode = b[bStart++];
1400
- parentNode.insertBefore(bNode, node);
1401
-
1402
-
1403
- }
1404
- }
1405
- // remove head or tail: fast path
1406
- else if (bEnd === bStart) {
1407
- while (aStart < aEnd) {
1408
- // remove the node only if it's unknown or not live
1409
- let aNode = a[aStart];
1410
- if (!map || !map.has(aNode)) {
1411
- parentNode.removeChild(aNode);
1412
-
1413
-
1414
- }
1415
- aStart++;
1416
- }
1417
- }
1418
- // same node: fast path
1419
- else if (a[aStart] === b[bStart]) {
1420
- aStart++;
1421
- bStart++;
1422
- }
1423
- // same tail: fast path
1424
- else if (a[aEnd - 1] === b[bEnd - 1]) {
1425
- aEnd--;
1426
- bEnd--;
1427
- }
1428
- // The once here single last swap "fast path" has been removed in v1.1.0
1429
- // https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85
1430
- // reverse swap: also fast path
1431
- else if (
1432
- a[aStart] === b[bEnd - 1] &&
1433
- b[bStart] === a[aEnd - 1]
1434
- ) {
1435
- // this is a "shrink" operation that could happen in these cases:
1436
- // [1, 2, 3, 4, 5]
1437
- // [1, 4, 3, 2, 5]
1438
- // or asymmetric too
1439
- // [1, 2, 3, 4, 5]
1440
- // [1, 2, 3, 5, 6, 4]
1441
- const node = a[--aEnd].nextSibling;
1442
-
1443
-
1444
- let a2 = b[bStart++];
1445
- let b2 = a[aStart++];
1446
- parentNode.insertBefore(
1447
- a2,
1448
- b2.nextSibling
1449
- );
1450
-
1451
-
1452
- let bNode = b[--bEnd];
1453
- parentNode.insertBefore(bNode, node);
1454
-
1455
-
1456
-
1457
- // mark the future index as identical (yeah, it's dirty, but cheap 👍)
1458
- // The main reason to do this, is that when a[aEnd] will be reached,
1459
- // the loop will likely be on the fast path, as identical to b[bEnd].
1460
- // In the best case scenario, the next loop will skip the tail,
1461
- // but in the worst one, this node will be considered as already
1462
- // processed, bailing out pretty quickly from the map index check
1463
- a[aEnd] = b[bEnd];
1464
- }
1465
- // map based fallback, "slow" path
1466
- else {
1467
- // the map requires an O(bEnd - bStart) operation once
1468
- // to store all future nodes indexes for later purposes.
1469
- // In the worst case scenario, this is a full O(N) cost,
1470
- // and such scenario happens at least when all nodes are different,
1471
- // but also if both first and last items of the lists are different
1472
- if (!map) {
1473
- map = new Map;
1474
- let i = bStart;
1475
- while (i < bEnd)
1476
- map.set(b[i], i++);
1477
- }
1478
- // if it's a future node, hence it needs some handling
1479
- if (map.has(a[aStart])) {
1480
- // grab the index of such node, 'cause it might have been processed
1481
- const index = map.get(a[aStart]);
1482
- // if it's not already processed, look on demand for the next LCS
1483
- if (bStart < index && index < bEnd) {
1484
- let i = aStart;
1485
- // counts the amount of nodes that are the same in the future
1486
- let sequence = 1;
1487
- while (++i < aEnd && i < bEnd && map.get(a[i]) === (index + sequence))
1488
- sequence++;
1489
- // effort decision here: if the sequence is longer than replaces
1490
- // needed to reach such sequence, which would brings again this loop
1491
- // to the fast path, prepend the difference before a sequence,
1492
- // and move only the future list index forward, so that aStart
1493
- // and bStart will be aligned again, hence on the fast path.
1494
- // An example considering aStart and bStart are both 0:
1495
- // a: [1, 2, 3, 4]
1496
- // b: [7, 1, 2, 3, 6]
1497
- // this would place 7 before 1 and, from that time on, 1, 2, and 3
1498
- // will be processed at zero cost
1499
- if (sequence > (index - bStart)) {
1500
- const node = a[aStart];
1501
- while (bStart < index) {
1502
- let bNode = b[bStart++];
1503
- parentNode.insertBefore(bNode, node);
1504
-
1505
-
1506
- }
1507
- }
1508
- // if the effort wasn't good enough, fallback to a replace,
1509
- // moving both source and target indexes forward, hoping that some
1510
- // similar node will be found later on, to go back to the fast path
1511
- else {
1512
- let aNode = a[aStart++];
1513
- let bNode = b[bStart++];
1514
- parentNode.replaceChild(
1515
- bNode,
1516
- aNode
1517
- );
1518
-
1519
-
1520
- }
1521
- }
1522
- // otherwise move the source forward, 'cause there's nothing to do
1523
- else
1524
- aStart++;
1525
- }
1526
- // this node has no meaning in the future list, so it's more than safe
1527
- // to remove it, and check the next live node out instead, meaning
1528
- // that only the live list index should be forwarded
1529
- else {
1530
- let aNode = a[aStart++];
1531
- parentNode.removeChild(aNode);
1532
-
1533
-
1534
- }
1535
- }
1536
- }
1537
- return b;
1538
- };
1645
+ }
1539
1646
 
1540
1647
  /**
1541
- * Tools for watch variables and performing precise renders.
1542
- */
1648
+ * A Shell is created from a tagged template expression instantiated as Nodes,
1649
+ * but without any expressions filled in.
1650
+ * Only one Shell is created for all the items in a loop.
1651
+ *
1652
+ * When a NodeGroup is created from a Template's html strings,
1653
+ * the NodeGroup then clones the Shell's fragmentn to be its nodes. */
1654
+ class Shell {
1543
1655
 
1656
+ /**
1657
+ * @type {DocumentFragment} DOM parent of the shell nodes. */
1658
+ fragment;
1544
1659
 
1545
- /**
1546
- * Stores info how to transform a path to a template. */
1547
- class TransformerInfo {
1548
- constructor(path, transformer, hash) {
1549
- this.path = path;
1550
- this.transformer = transformer;
1551
- this.hash = hash;
1552
- }
1553
- }
1660
+ /** @type {ExprPath[]} Paths to where expressions should go. */
1661
+ paths = [];
1554
1662
 
1663
+ // Embeds and ids
1664
+ events = [];
1555
1665
 
1556
- /**
1557
- * Maps an object path to the function that converts it to a Template.
1558
- * Once it's convert to a template, we can get the hash of that Tempate.
1559
- * Then that hash tells us what NodeGroups are affected by the object.
1560
- *
1561
- * We store the function to get the Template, instead of the Template itself,
1562
- * so we can call that function again when the object has a new value.
1563
- * @type {MultiValueMap<Object, function(...Object):Template>} */
1564
- let pathToTransformer = new MultiValueMap(); // uses a Set() for each value.
1666
+ /** @type {int[][]} Array of paths */
1667
+ ids = [];
1668
+ scripts = [];
1669
+ styles = [];
1565
1670
 
1566
- /**
1567
- *
1568
- * @param objectPaths {(*|function)[]}
1569
- * @returns {Template} */
1570
- function watchGet(...objectPaths) {
1571
-
1572
- /** @type {function} */
1573
- let transformer = objectPaths.at(-1);
1574
- let paths;
1575
- if (typeof transformer === 'function') {
1576
- paths = objectPaths.slice(0, -1);
1577
- }
1671
+ staticComponents = [];
1578
1672
 
1579
- // No transformer provided, so we create our own.
1580
- else if (objectPaths.length === 1) {
1581
- paths = [objectPaths[0].slice(0, -1)];
1582
- let prop = objectPaths[0].at(-1);
1583
- transformer = (...args) => (args[0][prop]);
1584
- }
1585
1673
 
1586
-
1587
- // Save arguments used to call the template, so we can call it again when those args have their values change.
1588
- let args = [];
1589
- for (let path of paths) {
1590
1674
 
1675
+ /**
1676
+ * Create the nodes but without filling in the expressions.
1677
+ * This is useful because the expression-less nodes created by a template can be cached.
1678
+ * @param html {string[]} */
1679
+ constructor(html=null) {
1680
+ if (!html)
1681
+ return;
1591
1682
 
1592
- let obj = delve(watchSet(path[0]), path.slice(1));
1593
- args.push(obj);
1594
- }
1595
-
1596
- let template = transformer(...args);
1597
-
1598
- // If the result isn't a Template, convert the function to return a Template that wraps the result.
1599
- // This way NodeGroupManager.findAndDelete() can find a NodeGroup that matches this Template's hash.
1600
- if (!(template instanceof Template)) {
1601
- let oldToTemplate = transformer;
1602
- transformer = function() {
1603
- return new Template(['', ''], [oldToTemplate(...arguments)]);
1604
- };
1605
- template = transformer(...args);
1606
- }
1607
-
1608
- // Map the object paths to the function that creates a template.
1609
- let hash = getObjectHash(template);
1610
- for (let path of paths) {
1611
- let serializedPath = serializePath(path);
1612
- pathToTransformer.add(serializedPath, new TransformerInfo(path, transformer, hash)); // Uses a Set() to ensure no duplicates.
1613
- }
1683
+
1614
1684
 
1615
- return template;
1616
- }
1685
+ // 1. Add placeholders
1686
+ // We increment the placeholder char as we go because nodes can't have the same attribute more than once.
1687
+ let placeholder = 0xe000; // https://en.wikipedia.org/wiki/Private_Use_Areas 6400.
1617
1688
 
1618
- //let proxyCache = new WeakMap();
1689
+ let buffer = [];
1690
+ let commentPlaceholder = `<!--!✨!-->`;
1691
+ let componentNames = {};
1619
1692
 
1693
+ htmlContext(null); // Reset the context.
1694
+ for (let i=0; i<html.length; i++) {
1695
+ let lastHtml = html[i];
1696
+ let context = htmlContext(lastHtml);
1620
1697
 
1621
- /**
1622
- * Set the value of a variable in a way that's watched, so later when we call .renderWatched()
1623
- * We can find what NodeGroups to update.*/
1624
- function watchSet(obj) {
1625
- if (obj?.$isProxy===true)
1626
- return obj; // It's already a Proxy.
1627
-
1628
- // This cache doesn't make things faster.
1629
- // let result = proxyCache.get(obj);
1630
- // if (!result) {
1631
- // result = new Proxy(obj, new ProxyHandler(obj));
1632
- // proxyCache.set(obj, result);
1633
- // }
1634
- // return result;
1635
- return new Proxy(obj, new ProxyHandler$1(obj));
1636
- }
1698
+ // Swap out Embedded Solarite Components with ${} attributes.
1699
+ // Later, NodeGroup.render() will search for these and replace them with the real components.
1700
+ // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
1701
+ if (context === htmlContext.Attribute) {
1637
1702
 
1638
- /**
1639
- * Loop over each item and apply watchGet() to each item.
1640
- * @param arrayPath {*[]}
1641
- * @param callback {function(obj:Object, index:int):Template}
1642
- * @returns {Template} */
1643
- function forEach(arrayPath, callback) {
1644
- let array = delve(arrayPath[0], arrayPath.slice(1));
1645
-
1646
- // This is retrieved on the 'insert' path inside renderWatched()
1647
- let ngm = NodeGroupManager.get(arrayPath[0]);
1648
- if (ngm.clearSubscribers) {
1649
- ngm.clearSubscribers = false;
1650
- ngm.pathToLoopInfo = new MultiValueMap();
1651
- } // TODO: Move tis into NodeGroupMAnager.get() without breaking things?
1652
-
1653
-
1654
- let newItems = [...array.map((item, i) => {
1655
- // TODO: This needs to wrap callback so we can pass it the index also.
1656
- return watchGet([...arrayPath, i], callback); // calls callback(array[i], i)
1657
- })
1658
- ];
1659
-
1660
- // We return a template that wraps the array
1661
- // So that NodeGroup.applyOneExpr can set the ExprPath and nextSibling on the template.
1662
- // Then the 'insert' path in renderWatched() uses that data fora dding more nodes.
1663
- let result = new Template(['', ''], [newItems]);
1664
-
1665
-
1666
- // We get a unique hash for each foreach template because the [''] array is unique each time.
1667
- let loopInfo = new LoopInfo(result, callback);
1668
- ngm.pathToLoopInfo.add(serializePath(arrayPath), loopInfo);
1669
- return result;
1670
- }
1703
+ let lastIndex, lastMatch;
1704
+ lastHtml.replace(/<[a-z][a-z0-9]*-[a-z0-9-]+/ig, (match, index) => {
1705
+ lastIndex = index+1; // +1 for after opening <
1706
+ lastMatch = match.slice(1);
1707
+ });
1671
1708
 
1672
- function serializePath(path) {
1673
- // Convert any array indices to strings, so serialized comparisons work.
1674
- return JSON.stringify([getObjectId(path[0]), ...path.slice(1).map(item => item+'')])
1709
+ if (lastMatch) {
1710
+ let newTagName = lastMatch + '-solarite-placeholder';
1711
+ lastHtml = lastHtml.slice(0, lastIndex) + newTagName + lastHtml.slice(lastIndex + lastMatch.length);
1712
+ componentNames[lastMatch] = newTagName;
1713
+ }
1714
+ }
1675
1715
 
1676
- }
1716
+ buffer.push(lastHtml);
1717
+ //console.log(lastHtml, context)
1718
+ if (i < html.length-1)
1719
+ if (context === htmlContext.Text)
1720
+ buffer.push(commentPlaceholder); // Comment Placeholder. because we can't put text in between <tr> tags for example.
1721
+ else
1722
+ buffer.push(String.fromCharCode(placeholder+i));
1723
+ }
1677
1724
 
1725
+ // 2. Create elements from html with placeholders.
1726
+ let template = document.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
1727
+ let joinedHtml = buffer.join('');
1678
1728
 
1679
- /**
1680
- * When an object property is accessed, a new Proxy with a new instance of this handler class is created,
1681
- * but it tracks the path from the root to the property.
1682
- * That way when a property is set, it can report the changed path. */
1683
- class ProxyHandler$1 {
1684
-
1685
- /**
1686
- * @param root An element managed by a NodeGroupManager. The same as the NodeGroupManager's rootEl.
1687
- * @param path {string[]} Used internally. */
1688
- constructor(root, path=[]) {
1729
+ // Replace '-solarite-placeholder' close tags.
1730
+ // TODO: is there a better way? What if the close tag is inside a comment?
1731
+ for (let name in componentNames)
1732
+ joinedHtml = joinedHtml.replaceAll(`</${name}>`, `</${componentNames[name]}>`);
1689
1733
 
1690
- this.root = root;
1691
- this.path = path; // path from root to this Proxy.
1692
- }
1734
+ if (joinedHtml)
1735
+ template.innerHTML = joinedHtml;
1736
+ else // Create one text node, so shell isn't empty and NodeGroups created from it have something to point the startNode and endNode at.
1737
+ template.content.append(document.createTextNode(''));
1738
+ this.fragment = template.content;
1693
1739
 
1694
- /**
1695
- * @param obj {Object}
1696
- * @param prop {string} */
1697
- get(obj, prop) {
1698
-
1699
- // Special props. Currently unused.
1700
- // if (prop === '$path')
1701
- // return this.path;
1702
- // if (prop === '$root')
1703
- // return this.root;
1704
- if (prop === '$isProxy')
1705
- return true;
1706
-
1707
-
1708
- // 1. Array.splice()
1709
- if (prop === 'splice' && Array.isArray(obj)) {
1710
- return (index, deleteCount, ...items) => {
1711
- let ngm = NodeGroupManager.get(this.root);
1712
-
1713
- if (deleteCount) {
1714
-
1715
- // Get the hash of each object along the delete range. The process to get the hash is:
1716
- // Serialized Path -> transformer -> Template -> hash.
1717
- let hashes = [];
1718
- for (let i=index; i<index+deleteCount; i++) {
1719
- let serializedPath = serializePath([this.root, ...this.path, i+'']);
1720
-
1721
- let obj = delve(this.root, [...this.path, i]);
1722
- for (let transformerInfo of pathToTransformer.getAll(serializedPath)) {
1723
- let template = transformerInfo.transformer(obj);
1724
- let hash = getObjectHash(template);
1725
- hashes.push(hash); // Hashes may go to nodes in more than one loop.
1726
- }
1740
+ // 3. Find placeholders
1741
+ let node;
1742
+ let toRemove = [];
1743
+ const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
1744
+ while (node = walker.nextNode()) {
1745
+
1746
+ // Remove previous after each iteration, so paths will still be calculated correctly.
1747
+ toRemove.map(el => el.remove());
1748
+ toRemove = [];
1749
+
1750
+ // Replace attributes
1751
+ if (node.nodeType === 1) {
1752
+ for (let attr of [...node.attributes]) { // Copy the attributes array b/c we remove attributes as we go.
1753
+
1754
+ // Whole attribute
1755
+ let matches = attr.name.match(/^[\ue000-\uf8ff]$/);
1756
+ if (matches) {
1757
+ this.paths.push(new ExprPath(null, node, PathType.Multiple));
1758
+ node.removeAttribute(matches[0]);
1727
1759
  }
1728
1760
 
1729
- ngm.changes.push(new Change('delete', this.root, [...this.path, index+''], hashes));
1730
- }
1761
+ // Just the attribute value.
1762
+ else {
1763
+ let parts = attr.value.split(/[\ue000-\uf8ff]/g);
1764
+ if (parts.length > 1) {
1765
+ let nonEmptyParts = (parts.length === 2 && !parts[0].length && !parts[1].length) ? null : parts;
1766
+ let type = isEvent(attr.name) ? PathType.Event : PathType.Value;
1731
1767
 
1732
- //let oldArray = [...obj];
1733
- let result = obj.splice(index, deleteCount);
1734
-
1735
- // Inserting
1736
- if (items.length) {
1737
-
1738
- let beforeNgs;
1739
- for (let loopInfo of ngm.getLoopInfo([this.root, ...this.path])) {
1740
- let beforeObj = delve(this.root, [...this.path, index]);
1741
-
1742
- // Find where to insert before.
1743
- if (beforeObj) {
1744
- let beforeTemplate = loopInfo.itemTransformer(beforeObj);
1745
- let beforeHash = getObjectHash(beforeTemplate);
1746
- beforeNgs = ngm.nodeGroupsAvailable.data[beforeHash];
1747
-
1748
- if (beforeNgs) {
1749
- let hash = getObjectHash(loopInfo.template);
1750
- let loopNgs = ngm.nodeGroupsAvailable.data[hash] || [];
1751
- for (let loopNg of loopNgs)
1752
- for (let beforeNg of beforeNgs)
1753
- if (beforeNg.startNode.parentNode === loopNg.startNode.parentNode)
1754
- ngm.changes.push(new Change('insert', this.root, [...this.path, index + ''], items, beforeTemplate));
1755
- }
1768
+ this.paths.push(new ExprPath(null, node, type, attr.name, nonEmptyParts));
1769
+ node.setAttribute(attr.name, parts.join(''));
1756
1770
  }
1757
- if (!beforeNgs)
1758
- ngm.changes.push(new Change('insert', this.root, [...this.path, index + ''], items));
1759
1771
  }
1760
- obj.splice(index, 0, ...items);
1761
1772
  }
1762
- return result;
1763
1773
  }
1764
- }
1765
-
1766
-
1767
-
1768
- // 2. Get property
1769
- // If we're getting an object or array property, apply watch() to it recursively.
1770
- let result = Reflect.get(obj, prop);
1771
- if (result && typeof result === 'object') {
1772
- let handler = new ProxyHandler$1(this.root, [...this.path, prop]); // same root, one level deeper on the path.
1773
- return new Proxy(result, handler);
1774
- }
1775
-
1776
- return result;
1777
- }
1778
-
1779
-
1780
- set(obj, prop, newValue) {
1781
- let ngm = NodeGroupManager.get(this.root);
1782
- ngm.changes.push(new Change('set', this.root, [...this.path, prop], newValue));
1783
- return Reflect.set(obj, prop, newValue)
1784
- }
1785
- }
1786
-
1787
-
1788
- /**
1789
- *
1790
- */
1791
- class Change {
1792
-
1793
- /**
1794
- * @param action {string}
1795
- * @param root {Object|Array}
1796
- * @param path {string[]}
1797
- * @param value
1798
- * If setting a value, this is the new value.
1799
- * If deleting from an array, this is an array of all the NodeGroups to delete.
1800
- *
1801
- * @param beforeTemplate
1802
- * */
1803
- constructor(action, root, path, value, beforeTemplate=null) {
1804
- this.action = action;
1774
+ // Replace comment placeholders
1775
+ else if (node.nodeType === Node.COMMENT_NODE && node.nodeValue === '!✨!') {
1805
1776
 
1806
- // TODO: Store root as first item of path, to be consistent with code elsewhere.
1807
- this.root = root;
1808
- this.path = path;
1809
- this.value = value;
1810
- this.beforeTemplate = beforeTemplate;
1777
+ // Get or create nodeBefore.
1778
+ let nodeBefore = node.previousSibling; // Can be the same as another Path's nodeMarker.
1779
+ if (!nodeBefore) {
1780
+ nodeBefore = document.createComment('ExprPath:'+this.paths.length);
1781
+ node.parentNode.insertBefore(nodeBefore, node);
1782
+ }
1783
+
1811
1784
 
1812
- /** @type {TransformerInfo[]} */
1813
- this.transformerInfo = [];
1785
+ // Get the next node.
1786
+ let nodeMarker;
1814
1787
 
1815
- // Traverse up the path.
1816
- for (let i=this.path.length; i>0; i--) {
1817
- let path = this.path.slice(0, i);
1818
- let fullPath = [this.root, ...path];
1788
+ // A subsequent node is available to be a nodeMarker.
1789
+ if (node.nextSibling && (node.nextSibling.nodeType !== 8 || node.nextSibling.textContent !== '!✨!')) {
1790
+ nodeMarker = node.nextSibling;
1791
+ toRemove.push(node); // Removing them here will mess up the treeWalker.
1792
+ }
1793
+ // Re-use existing comment placeholder.
1794
+ else {
1795
+ nodeMarker = node;
1796
+ nodeMarker.textContent = 'ExprPathEnd:'+ this.paths.length;
1797
+ }
1798
+
1819
1799
 
1820
- let serializedPath = getObjectHash(fullPath); // TODO: Why not serializedPath() ?
1821
- this.transformerInfo.push(...pathToTransformer.getAll(serializedPath));
1822
- }
1823
- }
1824
- }
1825
-
1826
- let logGets = false;
1827
- let gets = [];
1828
1800
 
1829
- let withinSet = 0;
1830
1801
 
1831
- /**
1832
- * Turn the props on obj into JavasCript properties that return Proxies when accessed.
1833
- * If called more than once, return the already-converted object.
1834
- * @param obj {Object}
1835
- * @param props {string}
1836
- * @returns {*|{$proxyHandler}}
1837
- */
1838
- function watch(obj, ...props) {
1839
-
1840
- if (props.length) {
1841
- let internalProps = {};
1842
- for (let prop of props) {
1843
- internalProps[prop] = obj[prop];
1844
- Object.defineProperty(obj, prop, {
1845
- get() {
1846
- return new Proxy(obj, new ProxyHandler(obj, [], internalProps))[prop];
1847
- },
1848
- set(value) {
1849
- return watch(this)[prop] = value;
1850
- }
1851
- });
1852
- }
1853
- return;
1854
- }
1855
-
1856
-
1857
- if (obj?.$proxyHandler)
1858
- return obj; // It's already a Proxy.
1859
-
1860
- // This cache doesn't make things faster.
1861
- // But could it save memory?
1862
- // let result = proxyCache.get(obj);
1863
- // if (!result) {
1864
- // result = new Proxy(obj, new ProxyHandler(obj));
1865
- // proxyCache.set(obj, result);
1866
- // }
1867
- // return result;
1868
-
1869
- return new Proxy(obj, new ProxyHandler(obj));
1870
- }
1802
+ let path = new ExprPath(nodeBefore, nodeMarker, PathType.Content);
1871
1803
 
1872
- /**
1873
- * Provides methods used when a Proxied version of a property is accessed on an object returned by watch() */
1874
- class ProxyHandler {
1875
-
1876
- serializedPath;
1877
-
1878
- /**
1879
- * @param root An element managed by a NodeGroupManager. The same as the NodeGroupManager's rootEl.
1880
- * @param path {string[]} Used internally.
1881
- * @param props */
1882
- constructor(root, path=[], props=null) {
1883
-
1884
- this.root = root;
1885
- this.path = path; // path from root to this Proxy.
1886
- this.props = props;
1887
- }
1888
-
1889
- /**
1890
- * Get the full path to this property from the root watched object.
1891
- * @param atIndex
1892
- * @returns {string} */
1893
- getSerializedPath(atIndex=null) {
1894
- if (!this.serializedPath)
1895
- this.serializedPath = JSON.stringify([getObjectId(this.root), ...this.path.map(item => item + '')]);
1896
-
1897
- if (atIndex!== null)
1898
- return this.serializedPath.slice(0, -1) + ',"' + atIndex + '"]';
1899
- return this.serializedPath;
1900
- }
1901
-
1902
- /**
1903
- * Return a ProxyHandler for a property one level deeper at pathItem.
1904
- * @param pathItem {string}
1905
- * @returns {ProxyHandler} */
1906
- extend(pathItem) {
1907
- pathItem += '';
1908
- assert(!this.root.$proxyHandler);
1909
- let result = new ProxyHandler(this.root, [...this.path, pathItem], this.props);
1910
- if (this.serializedPath)
1911
- result.serializedPath = this.getSerializedPath(pathItem);
1912
-
1913
- return result;
1914
- }
1915
-
1916
- /**
1917
- * Called directly by JavaScript when accessing the value of a property.
1918
- * @param obj {Object}
1919
- * @param prop {string} */
1920
- get(obj, prop) {
1921
-
1922
- // 1. Special props.
1923
- if (prop === '$proxyHandler')
1924
- return this;
1925
- else if (prop === '$removeProxy')
1926
- return delve(this.props || this.root, this.path);
1927
-
1928
- // 2. Array functions.
1929
- else if (prop === 'map' && Array.isArray(obj)) {
1804
+ this.paths.push(path);
1805
+ }
1930
1806
 
1931
- let ngm = NodeGroupManager.get(this.root);
1932
- ngm.clearSubscribersIfNeeded();
1933
1807
 
1934
- return callback => {
1935
- let loopInfo;
1936
-
1937
- let children = [];
1938
- let transformer = obj => {
1939
- let templates = [];
1940
-
1941
- for (let i = 0; i < obj.length; i++) {
1942
-
1943
- // Watch obj[i].
1944
- let handler = this.extend(i);
1945
- let item = obj[i];
1946
- if (!item.$proxyHandler)
1947
- item = new Proxy(item, handler);
1948
-
1949
- let template = callback(item, i, obj);
1950
- templates.push(template);
1951
-
1952
- // If the loop is re-evaluted via Set() then we add duplicate TemplateInfo's
1953
- //if (!withinSet) {
1954
- let spath = this.getSerializedPath(i);
1955
- let subscriber = new Subscriber(callback, template);
1956
- subscriber.parent = loopInfo;
1957
- ngm.subscribers.add(spath, subscriber);
1958
- children.push([spath, subscriber]);
1959
- //}
1960
- }
1961
-
1962
- // A parent Template that surrounds all the items in the loop.
1963
- // This lets us get template.nodeGroup.endNode so we can insertBefore().
1964
- return new Template(['', ''], [templates]);
1965
- };
1966
-
1967
- if (!withinSet)
1968
- loopInfo = new Subscriber(transformer, null, callback);
1969
- let wholeLoopTemplate = transformer(obj);
1970
- if (!withinSet) {
1971
- loopInfo.template = wholeLoopTemplate;
1972
- loopInfo.children = children;
1973
- ngm.subscribers.add(this.getSerializedPath(), loopInfo);
1808
+ // Sometimes users will comment out a block of html code that has expressions.
1809
+ // Here we look for expressions in comments.
1810
+ // We don't actually update them dynamically, but we still add paths for them.
1811
+ // That way the expression count still matches.
1812
+ else if (node.nodeType === Node.COMMENT_NODE) {
1813
+ let parts = node.textContent.split(/[\ue000-\uf8ff]/g);
1814
+ for (let i=0; i<parts.length-1; i++) {
1815
+ let path = new ExprPath(node.previousSibling, node);
1816
+ path.type = PathType.Comment;
1817
+ this.paths.push(path);
1974
1818
  }
1975
- return wholeLoopTemplate;
1976
1819
  }
1977
- }
1978
1820
 
1979
- else if ((prop ==='splice' || prop === 'fastSplice') && Array.isArray(obj)) {
1980
- let ngm = NodeGroupManager.get(this.root);
1981
- return (index, deleteCount, ...items) => {
1982
- let diff = items.length - deleteCount;
1983
- let objLength = obj.length;
1984
-
1985
- // Delete
1986
- if (deleteCount) {
1987
- for (let i=index; i<index+deleteCount; i++) {
1988
-
1989
- // Update pathToTemplates
1990
- let spath = this.getSerializedPath(i);
1991
-
1992
- // Delete nodes of associated NodeGroups.
1993
- for (let subscriber of ngm.subscribers.data[spath] || []) {
1994
- let ng = subscriber.template.nodeGroup;
1995
- for (let node of ng.getNodes())
1996
- node.remove();
1997
-
1998
- // Delete NodeGroup from NodeGroupManager.
1999
- ngm.nodeGroupsAvailable.delete(ng.exactKey, ng);
2000
- }
2001
-
2002
- delete ngm.subscribers.data[spath]; // Deletes templates associated with every loop where this is used.
1821
+ // Replace comment placeholders inside script and style tags, which have become text nodes.
1822
+ else if (node.nodeType === Node.TEXT_NODE && ['SCRIPT', 'STYLE'].includes(node.parentNode?.nodeName)) {
1823
+ let parts = node.textContent.split(commentPlaceholder);
1824
+ if (parts.length > 1) {
1825
+
1826
+ let placeholders = [];
1827
+ for (let i = 0; i<parts.length; i++) {
1828
+ let current = document.createTextNode(parts[i]);
1829
+ node.parentNode.insertBefore(current, node);
1830
+ if (i > 0)
1831
+ placeholders.push(current);
2003
1832
  }
2004
- }
2005
-
2006
- // Update indices of subsequent items.
2007
- if (diff) {
2008
- let loopPath = this.getSerializedPath();
2009
- let loopInfo = [...ngm.subscribers.getAll(loopPath)][0]; // TODO: Handle multiple loops.
2010
-
2011
- let move = (oldIndex) => {
2012
- let newIndex = oldIndex+diff;
2013
- let oldPath = this.getSerializedPath(oldIndex);
2014
- let newPath = this.getSerializedPath(newIndex);
2015
1833
 
2016
- let subscribers = ngm.subscribers.data[oldPath];
2017
- delete ngm.subscribers.data[oldPath]; // TODO: Some can be overwritten w/o being deleted?
2018
- ngm.subscribers.data[newPath] = subscribers;
1834
+ for (let i=0, node; node=placeholders[i]; i++) {
1835
+ let path = new ExprPath(node.previousSibling, node, PathType.Content);
1836
+ this.paths.push(path);
2019
1837
 
2020
-
2021
- // Update associated NodeGroups by passing them newIndex.
2022
- // This is unnecessary for most loops since they don't use the index.
2023
- // fastSplice skips this path, it skips updating item indices.
2024
- if (prop === 'splice') {
2025
- let array = delve(this.root, this.path);
2026
- for (let subscriber of subscribers) {
2027
- let ng = subscriber.template.nodeGroup;
2028
-
2029
- let item = array[oldIndex];
2030
-
2031
- //assert(!item.$proxyHandler)
2032
- let proxyItem = getProxy(item, this, this.path, newIndex); //new Proxy(item, this.extend(newIndex));
2033
- let exprs = loopInfo.itemTransformer(proxyItem, newIndex).exprs; // TODO: Pass updated array as third argument to transformer.
2034
- ng.applyExprs(exprs); // this is the slow part.
2035
- }
2036
- }
2037
- };
2038
-
2039
- // Iterate in different directions depending on whether diff is positive or negative.
2040
- if (diff > 0) // Moving items to the right, so we iterate backward from the end.
2041
- for (let i = objLength-1; i >= index + items.length + deleteCount; i--)
2042
- move(i);
2043
-
2044
- else // Moving items to the left, so we iterate forward.
2045
- for (let i = index + items.length + deleteCount; i < objLength; i++)
2046
- move(i);
2047
- }
2048
-
2049
-
2050
- // Add new items
2051
- if (items.length) {
2052
- let loopPath = this.getSerializedPath();
2053
-
2054
- let beforePath = this.getSerializedPath(index);
2055
- let ngm = NodeGroupManager.get(this.root);
2056
-
2057
- for (let loopInfo of ngm.subscribers.getAll(loopPath)) {
2058
- let beforeNodes = index < objLength - deleteCount
2059
- ? [...ngm.subscribers.getAll(beforePath)].map(t => t.template.nodeGroup.startNode)
2060
- : [loopInfo.template.nodeGroup.endNode];
2061
- for (let beforeNode of beforeNodes) { // TODO: Need to match the beforeNg with the loopInfo instead of iterating.
2062
- for (let i = 0; i < items.length; i++) {
2063
-
2064
- // Create NodeGroup of new item.
2065
- let itemHandler = this.extend(index + i);
2066
- assert(!items[i].$proxyHandler);
2067
- let proxyItem = new Proxy(items[i], itemHandler);
2068
- let template = loopInfo.itemTransformer(proxyItem);
2069
- let ng = ngm.getNodeGroup(template, null, true);
2070
-
2071
-
2072
- for (let node of ng.getNodes()) {
2073
- beforeNode.parentNode.insertBefore(node, beforeNode);
2074
- //loopInfo.template.nodeGroup.endNode = node; // The loop's end node is actually an empty text node, so don't do this.
2075
- }
2076
-
2077
-
2078
- // Add new items to ngm.templateInfo
2079
- let spath = itemHandler.getSerializedPath();
2080
- let subscriber = new Subscriber(loopInfo.itemTransformer, template);
2081
- subscriber.parent = loopInfo;
2082
- ngm.subscribers.add(spath, subscriber);
2083
- }
2084
- }
2085
- loopInfo.template.nodeGroup.parentPath.clearNodesCache();
2086
- loopInfo.template.nodeGroup.nodesCache = null;
2087
- }
2088
- }
2089
-
2090
- let result = obj.splice(index, deleteCount, ...items);
2091
-
2092
- //this.notify(this.path);
2093
-
2094
- return result;
2095
- }
2096
- }
1838
+
1839
+ }
2097
1840
 
2098
- // Allow these functions to use proxied objects as arguments.
2099
- else if (prop ==='indexOf' && Array.isArray(obj)) {
2100
- return item => {
2101
- return obj.indexOf(item.$removeProxy || item)
1841
+ // Removing them here will mess up the treeWalker.
1842
+ toRemove.push(node);
1843
+ }
2102
1844
  }
2103
1845
  }
1846
+ toRemove.map(el => el.remove());
2104
1847
 
2105
- // 3. Get property
2106
- else {
1848
+ // Handle solarite-placeholder's.
1849
+ // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
1850
+ //if (componentNames.size)
1851
+ // this.components = [...this.fragment.querySelectorAll([...componentNames].join(','))]
2107
1852
 
2108
- let obj2 = obj === this.root && this.props ? this.props : obj;
2109
- let result = Reflect.get(obj2, prop);
1853
+ // Rename "is" attributes so the Web Components don't instantiate until we have the values of their PathExpr arguments.
1854
+ // that happens in NodeGroup.applyComponentExprs()
1855
+ for (let el of this.fragment.querySelectorAll('[is]')) {
1856
+ el.setAttribute('_is', el.getAttribute('is'));
1857
+ // this.components.push(el);
1858
+ }
2110
1859
 
2111
- // This is read by watchFunction() which is called in NodeGroup.applyOneExpr().
2112
- // It's used to see what variables contribute to an expression.
2113
- if (logGets)
2114
- gets.push([this.root, ...this.path, prop]);
1860
+ for (let path of this.paths) {
1861
+ if (path.nodeBefore)
1862
+ path.nodeBeforeIndex = Array.prototype.indexOf.call(path.nodeBefore.parentNode.childNodes, path.nodeBefore);
1863
+ path.nodeMarkerPath = getNodePath(path.nodeMarker);
2115
1864
 
2116
- // If we're getting an object or array property, apply watch() to it recursively.
2117
- if (result && typeof result === 'object') {
2118
- let handler = this.extend(prop); // same root, one level deeper on the path.
2119
-
2120
- return new Proxy(result, handler);
1865
+ // Cache so we don't have to calculate this later inside NodeGroup.applyExprs()
1866
+ if (path.type === PathType.Value && path.nodeMarker.nodeType === 1 && /*path.nodeMarker !== template.content.children[0] &&*/
1867
+ (path.nodeMarker.tagName.includes('-') || path.nodeMarker.hasAttribute('is'))) {
1868
+ path.type = PathType.Component;
2121
1869
  }
2122
-
2123
- return result;
2124
1870
  }
2125
- }
2126
-
2127
-
2128
- /**
2129
- * Called directly by JavaScript when setting the value of a property via equals.
2130
- * @param obj
2131
- * @param prop
2132
- * @param value
2133
- * @returns {boolean} */
2134
- set(obj, prop, value) {
2135
- withinSet++;
2136
1871
 
2137
- let obj2 = obj === this.root && this.props ? this.props : obj;
2138
- let result = Reflect.set(obj2, prop, value);
2139
- let fullPath = [...this.path, prop+''];
2140
- this.notify(fullPath);
1872
+ this.findEmbeds();
1873
+
2141
1874
 
2142
- withinSet --;
1875
+ } // end constructor
2143
1876
 
2144
- return result;
2145
- }
2146
-
2147
1877
  /**
2148
- * Find every subscriber for fullPath, and above, and call applyExprs() for it.
2149
- * @param fullPath {string[]}
2150
- * @param excluded {Set} */
2151
- notify(fullPath, excluded = new Set()) {
2152
-
2153
- // Traverse upward through the path, looking for pathToTemplates.
2154
- let ngm = NodeGroupManager.get(this.root);
1878
+ * We find the path to every embed here once in the Shell, instead of every time a NodeGroup is instantiated.
1879
+ * When a Nodegroup is created, it calls NodeGroup.activateEmbeds() that uses these paths.
1880
+ * Populates:
1881
+ * this.scripts
1882
+ * this.styles
1883
+ * this.ids
1884
+ * this.staticComponents */
1885
+ findEmbeds() {
1886
+ this.scripts = Array.prototype.map.call(this.fragment.querySelectorAll('scripts'), el => getNodePath(el));
1887
+ this.styles = Array.prototype.map.call(this.fragment.querySelectorAll('style'), el => getNodePath(el));
1888
+
1889
+ let idEls = this.fragment.querySelectorAll('[id],[data-id]');
2155
1890
 
2156
- let rootHash = getObjectId(this.root);
2157
-
2158
- let len = fullPath.length;
2159
- while (len >= 1) {
2160
- let path = fullPath.slice(0, len);
2161
- let val = delve(this.root, path);
2162
- let serializedPath = JSON.stringify([rootHash, ...path]);
2163
- for (let subscriber of ngm.subscribers.getAll(serializedPath)) {
2164
- // We already applied expressions for a single item within this loop.
2165
- if (excluded.has(subscriber))
2166
- continue;
2167
-
2168
-
2169
- // Delete child subscriptions so we don't have duplicate subscriptions when we call applyExprs() directly below.
2170
- if (subscriber.children) {
2171
- for (let [spath, childInfo] of subscriber.children)
2172
- ngm.subscribers.delete(spath, childInfo);
2173
- subscriber.children = undefined;
2174
- }
2175
1891
 
2176
- let proxyVal = getProxy(val, this, path);
2177
- let exprs = subscriber.transformer(proxyVal).exprs; // TODO: Pass updated array as third argument to transformer.
2178
- for (let path of subscriber.template.nodeGroup.paths)
2179
- path.clearNodesCache();
2180
-
2181
- // Apply expressions.
2182
- subscriber.template.nodeGroup.applyExprs(exprs);
2183
-
2184
- // Don't also process parent loop after updating a single item within it.
2185
- if (subscriber.parent)
2186
- excluded.add(subscriber.parent);
2187
- }
2188
- len--;
1892
+ // Check for valid id names.
1893
+ for (let el of idEls) {
1894
+ let id = el.getAttribute('data-id') || el.getAttribute('id');
1895
+ if (div.hasOwnProperty(id))
1896
+ throw new Error(`<${el.tagName.toLowerCase()} id="${id}"> can't override existing HTMLElement id property.`)
2189
1897
  }
2190
- }
2191
- }
2192
1898
 
2193
- function getProxy(obj, ph, path, path2) {
2194
- if (!obj || !typeof obj !== 'object')
2195
- return obj;
2196
-
2197
- if (obj.$proxyHandler) {
2198
-
2199
- assert(obj.$proxyHandler.root === ph.root && JSON.stringify(obj.$proxyHandler.path) === JSON.stringify(path));
2200
- return obj;
2201
- }
2202
- if (path2)
2203
- path = [...path, path2+''];
2204
- return new Proxy(obj, new ProxyHandler(ph.root, path, ph.props));
2205
- }
2206
1899
 
2207
- /**
2208
- * Call a function and record which watched variables it accesess, storing their paths in pathToTemplates.
2209
- * Used by NodeGroup.applyOneExpr().
2210
- * TODO: only allow this to be called once per callback.
2211
- * @param callback {function}
2212
- * @param ngm {NodeGroupManager}
2213
- * @returns {Template} */
2214
- function watchFunction(callback, ngm) {
2215
- ngm.clearSubscribersIfNeeded();
2216
-
2217
- logGets = true;
2218
-
2219
- let transformer = () => new Template(['', ''], [callback()]);
2220
- let template = transformer();
2221
- for (let path of gets) {
2222
- let subscriber = new Subscriber(transformer, template);
2223
- ngm.subscribers.add(serializePath(path), subscriber);
2224
- }
1900
+ this.ids = Array.prototype.map.call(idEls, el => getNodePath(el));
2225
1901
 
2226
- gets = [];
2227
- logGets = false;
2228
- return template;
2229
- }
1902
+ // Events (not yet used)
1903
+ for (let el of this.fragment.querySelectorAll('*')) {
1904
+ for (let attrib of el.attributes)
1905
+ if (isEvent(attrib.name))
1906
+ this.events.push([attrib.name, getNodePath(el)]);
1907
+
1908
+ if (el.tagName.includes('-') || el.hasAttribute('_is'))
1909
+
1910
+ // Dynamic components have attributes with expression values.
1911
+ // They are created from applyExprs()
1912
+ // But static components are created in a separate path inside the NodeGroup constructor.
1913
+ if (!this.paths.find(path => path.nodeMarker === el))
1914
+ this.staticComponents.push(getNodePath(el));
1915
+ }
1916
+
1917
+ }
2230
1918
 
2231
- /**
2232
- * Represents a place where nodes will be updated.
2233
- * TODO: Merge this with Template, or ExprPath? */
2234
- class Subscriber {
2235
-
2236
- /** @type {Subscriber} Used only for children of a loop. */
2237
- parent;
2238
-
2239
- /** @type {Subscriber[]} TemplateInfo for each child of a loop. */
2240
- children;
2241
-
2242
1919
  /**
2243
- * @param transformer {function} Function that turns the object at the path into a template.
2244
- * @param template {Template}
2245
- * @param itemTransformer {function} If a loop, this transforms each item in the loop. */
2246
- constructor(transformer, template, itemTransformer=null) {
2247
- this.transformer = transformer;
2248
- this.template = template;
1920
+ * Get the shell for the html strings.
1921
+ * @param htmlStrings {string[]} Typically comes from a Template.
1922
+ * @returns {Shell} */
1923
+ static get(htmlStrings) {
1924
+ let result = Globals.shells.get(htmlStrings);
1925
+ if (!result) {
1926
+ result = new Shell(htmlStrings);
1927
+ Globals.shells.set(htmlStrings, result); // cache
1928
+ }
1929
+
2249
1930
 
2250
- // Used only for loops
2251
- this.itemTransformer = itemTransformer;
1931
+ return result;
2252
1932
  }
1933
+
1934
+
2253
1935
  }
2254
1936
 
2255
- /** @typedef {boolean|string|number|function|Object|Array|Date|Node} Expr */
1937
+ /** @typedef {boolean|string|number|function|Object|Array|Date|Node|Template} Expr */
2256
1938
 
2257
1939
  /**
2258
1940
  * A group of Nodes instantiated from a Shell, with Expr's filled in.
@@ -2265,32 +1947,29 @@ class Subscriber {
2265
1947
  * */
2266
1948
  class NodeGroup {
2267
1949
 
2268
- /** @Type {NodeGroupManager} */
2269
- manager;
1950
+ /**
1951
+ * @Type {RootNodeGroup} */
1952
+ rootNg;
2270
1953
 
2271
1954
  /** @type {ExprPath} */
2272
1955
  parentPath;
2273
1956
 
2274
- /** @type {Node} First node of NodeGroup. Should never be null. */
1957
+ /** @type {Node|HTMLElement} First node of NodeGroup. Should never be null. */
2275
1958
  startNode;
2276
1959
 
2277
- /** @type {Node} A node that never changes that this NodeGroup should always insert its nodes before.
1960
+ /** @type {Node|HTMLElement} A node that never changes that this NodeGroup should always insert its nodes before.
2278
1961
  * An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.*/
2279
1962
  endNode;
2280
1963
 
2281
1964
  /** @type {ExprPath[]} */
2282
1965
  paths = [];
2283
1966
 
2284
- /** @type {string} */
1967
+ /** @type {string} Key that matches the template and the expressions. */
2285
1968
  exactKey;
2286
1969
 
2287
- /** @type {string} */
1970
+ /** @type {string} Key that only matches the template. */
2288
1971
  closeKey;
2289
1972
 
2290
- /** @type {boolean} Used by NodeGroupManager. */
2291
- inUse;
2292
-
2293
-
2294
1973
  /**
2295
1974
  * @internal
2296
1975
  * @type {Node[]} Cached result of getNodes() used only for improving performance.*/
@@ -2300,436 +1979,141 @@ class NodeGroup {
2300
1979
  * @type {?Map<HTMLStyleElement, string>} */
2301
1980
  styles;
2302
1981
 
2303
- /**
2304
- * If rendering a Template with replaceMode=true, pseudoRoot points to the element where the attributes are rendered.
2305
- * But pseudoRoot is outside of this.getNodes().
2306
- * NodeGroupManager.render() copies the attributes from pseudoRoot to the actual web component root element.
2307
- * @type {?HTMLElement} */
2308
- pseudoRoot;
2309
-
2310
- currentComponentProps = {};
2311
-
2312
-
2313
- /**
2314
- * Create an "instantiated" NodeGroup from a Template and add it to an element.
2315
- * @param template {Template} Create it from the html strings and expressions in this template.
2316
- * @param manager {?NodeGroupManager}
2317
- * @returns {NodeGroup} */
2318
- constructor(template, manager=null) {
2319
-
2320
- // Used for forEach()
2321
- this.template = template;
2322
- this.manager = manager;
2323
-
2324
- // new!
2325
- template.nodeGroup = this;
2326
-
2327
- // Get a cached version of the parsed and instantiated html, and ExprPaths.
2328
- let shell = Shell.get(template.html);
2329
-
2330
- let fragment = shell.fragment.cloneNode(true);
2331
-
2332
- // Figure out value of replaceMode option if it isn't set,
2333
- // Assume replaceMode if there's only one child element and its tagname matches the root el.
2334
- let replaceMode = typeof template.replaceMode === 'boolean'
2335
- ? template.replaceMode
2336
- : fragment.children.length===1 &&
2337
- fragment.firstElementChild?.tagName.replace(/-REDCOMPONENT-PLACEHOLDER$/, '')
2338
- === manager?.rootEl?.tagName;
2339
- if (replaceMode) {
2340
- this.pseudoRoot = fragment.firstElementChild;
2341
- // if (!manager.rootEl)
2342
- // manager.rootEl = this.pseudoRoot;
2343
-
2344
- }
2345
-
2346
- let childNodes = replaceMode
2347
- ? fragment.firstElementChild.childNodes
2348
- : fragment.childNodes;
2349
-
2350
-
2351
- this.startNode = childNodes[0];
2352
- this.endNode = childNodes[childNodes.length - 1];
2353
-
2354
-
2355
- // Update paths
2356
- for (let oldPath of shell.paths) {
2357
- let path = oldPath.clone(fragment);
2358
- path.parentNg = this;
2359
- this.paths.push(path);
2360
- }
2361
-
2362
-
2363
- // Update web component placeholders.
2364
- // Ctrl+F "redcomponent-placeholder" in project to find all code that manages subcomponents.
2365
- // Is this list needed at all?
2366
- //for (let component of shell.components)
2367
- // this.components.push(resolveNodePath(this.startNode.parentNode, getNodePath(component)))
2368
-
2369
-
2370
-
2371
- this.activateEmbeds(fragment, shell);
2372
-
2373
-
2374
-
2375
- // Apply exprs
2376
- this.applyExprs(template.exprs);
2377
-
2378
-
2379
- }
2380
-
2381
- activateEmbeds(root, shell) {
2382
-
2383
- // static components
2384
- // Must happen before ids.
2385
- for (let path of shell.staticComponents) {
2386
- let el = resolveNodePath(root, path);
2387
-
2388
- // Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
2389
- if (el.tagName !== this.pseudoRoot?.tagName)
2390
- this.createNewComponent(el);
2391
- }
2392
-
2393
- if (this.manager.rootEl) {
2394
-
2395
- // ids
2396
- if (this.manager.options.ids !== false)
2397
- for (let path of shell.ids) {
2398
- let el = resolveNodePath(root, path);
2399
- let id = el.getAttribute('data-id') || el.getAttribute('id');
2400
- this.manager.rootEl[id] = el;
2401
- }
2402
-
2403
- // styles
2404
- if (this.manager.options.styles !== false) {
2405
- if (shell.styles.length)
2406
- this.styles = new Map();
2407
- for (let path of shell.styles) {
2408
- let style = resolveNodePath(root, path);
2409
- Util.bindStyles(style, this.manager.rootEl);
2410
- this.styles.set(style, style.textContent);
2411
- }
2412
-
2413
- }
2414
- // scripts
2415
- if (this.manager.options.scripts !== false) {
2416
- for (let path of shell.scripts) {
2417
- let script = resolveNodePath(root, path);
2418
- eval(script.textContent);
2419
- }
2420
- }
2421
- }
2422
- }
2423
-
2424
- updateStyles() {
2425
- if (this.styles)
2426
- for (let [style, oldText] of this.styles) {
2427
- let newText = style.textContent;
2428
- if (oldText !== newText)
2429
- Util.bindStyles(style, this.manager.rootEl);
2430
- }
2431
- }
2432
-
2433
- /**
2434
- * Use the paths to insert the given expressions.
2435
- * Dispatches expression handling to other functions depending on the path type.
2436
- * @param exprs {(*|*[]|function|Template)[]} */
2437
- applyExprs(exprs) {
2438
-
2439
-
2440
- // Update exprs at paths.
2441
- let exprIndex = exprs.length-1, expr, lastNode;
2442
-
2443
- // We apply them in reverse order so that a <select> box has its options created from an expression
2444
- // before its value attribute is set via an expression.
2445
- for (let path of this.paths.toReversed()) {
2446
- expr = exprs[exprIndex];
2447
-
2448
- // Nodes
2449
- if (path.type === PathType.Content) {
2450
- this.applyNodeExpr(path, expr);
2451
-
2452
- }
2453
-
2454
- // Attributes
2455
- else {
2456
- let node = path.nodeMarker; // path.resolve(result);
2457
- let node2 = (this.manager.rootEl && node === this.pseudoRoot) ? this.manager.rootEl : node;
2458
-
2459
-
2460
- // This is necessary both here and below.
2461
- if (lastNode && lastNode !== this.pseudoRoot && lastNode !== node && Object.keys(this.currentComponentProps).length) {
2462
- this.applyComponentExprs(lastNode, this.currentComponentProps);
2463
- this.currentComponentProps = {};
2464
- }
2465
-
2466
- if (path.type === PathType.Multiple)
2467
- path.applyMultipleAttribs(node2, expr);
2468
-
2469
- // Capture attribute expressions to later send to the constructor of a web component.
2470
- // Ctrl+F "redcomponent-placeholder" in project to find all code that manages subcomponents.
2471
- else if (path.nodeMarker !== this.pseudoRoot && path.type === PathType.Component)
2472
- this.currentComponentProps[path.attrName] = expr;
2473
-
2474
- else if (path.type === PathType.Comment) ;
2475
- else {
2476
-
2477
- // Event attribute value
2478
- if (path.attrValue===null && (typeof expr === 'function' || Array.isArray(expr)) && isEvent(path.attrName)) {
2479
- let root = this.manager.rootEl || this.startNode.parentNode; // latter is used when constructing a whole element.
2480
- path.applyEventAttrib(node2, expr, root);
2481
- }
2482
-
2483
- // Regular attribute value.
2484
- else
2485
- exprIndex = path.applyValueAttrib(node2, exprs, exprIndex);
2486
- }
2487
-
2488
- lastNode = path.nodeMarker;
2489
- }
2490
-
2491
- exprIndex--;
2492
- } // end for(path of this.paths)
2493
-
2494
-
2495
- // Check again after we iterate through all paths to apply to a component.
2496
- if (lastNode && lastNode !== this.pseudoRoot && Object.keys(this.currentComponentProps).length) {
2497
- this.applyComponentExprs(lastNode, this.currentComponentProps);
2498
- this.currentComponentProps = {};
2499
- }
2500
-
2501
- this.updateStyles();
2502
-
2503
- // Invalidate the nodes cache because we just changed it.
2504
- this.nodesCache = null;
2505
-
2506
- // If there's leftover expressions, there's probably an issue with the Shell that created this NodeGroup,
2507
- // and the number of paths not matching.
2508
-
2509
-
2510
-
2511
-
2512
- }
2513
-
2514
- /**
2515
- * Insert/replace the nodes created by a single expression.
2516
- * Called by applyExprs()
2517
- * This function is recursive, as the functions it calls also call it.
2518
- * TODO: Move this to ExprPath?
2519
- * @param path {ExprPath}
2520
- * @param expr {Expr}
2521
- * @return {Node[]} New Nodes created. */
2522
- applyNodeExpr(path, expr) {
2523
-
2524
-
2525
- /** @type {(Node|NodeGroup|Expr)[]} */
2526
- let newNodes = [];
2527
- let oldNodeGroups = path.nodeGroups;
2528
-
2529
- let secondPass = []; // indices
2530
-
2531
- // First Pass
2532
- //for (let ng of path.nodeGroups) // TODO: Is this necessary?
2533
- // ng.parentPath = null;
2534
- path.nodeGroups = [];
2535
- this.applyOneExpr(expr, path, newNodes, secondPass);
2536
- this.existingTextNodes = null;
1982
+ currentComponentProps = {};
1983
+
2537
1984
 
2538
- // TODO: Create an array of old vs Nodes and NodeGroups together.
2539
- // If they're all the same, skip the next steps.
2540
- // Or calculate it in the loop above as we go? Have a path.lastNodeGroups property?
1985
+ /**
1986
+ * Create an "instantiated" NodeGroup from a Template and add it to an element.
1987
+ * @param template {Template} Create it from the html strings and expressions in this template.
1988
+ * @param parentPath {?ExprPath} */
1989
+ constructor(template, parentPath=null) {
1990
+ if (!(this instanceof RootNodeGroup)) {
1991
+ let [fragment, shell] = this.init(template, parentPath);
2541
1992
 
2542
- // Second pass to find close-match NodeGroups.
2543
- let flatten = false;
2544
- if (secondPass.length) {
2545
- for (let [nodesIndex, ngIndex] of secondPass) {
2546
- let ng = this.manager.getNodeGroup(newNodes[nodesIndex], false);
2547
-
2548
- ng.parentPath = path;
2549
- let ngNodes = ng.getNodes();
1993
+ this.updatePaths(fragment, shell.paths);
2550
1994
 
2551
-
2552
-
2553
- if (ngNodes.length === 1)
2554
- newNodes[nodesIndex] = ngNodes[0];
2555
-
2556
- else {
2557
- newNodes[nodesIndex] = ngNodes;
2558
- flatten = true;
2559
- }
2560
- path.nodeGroups[ngIndex] = ng;
2561
- }
1995
+ this.activateEmbeds(fragment, shell);
2562
1996
 
2563
- if (flatten)
2564
- newNodes = newNodes.flat(); // TODO: Only if second pass happens?
1997
+ // Apply exprs
1998
+ this.applyExprs(template.exprs);
2565
1999
  }
2000
+ }
2566
2001
 
2567
-
2568
-
2002
+ /**
2003
+ * Common init shared by RootNodeGroup and NodeGroup constructors.
2004
+ * But in a separate function because they need to do this at a different step.
2005
+ * @param template {Template} Create it from the html strings and expressions in this template.
2006
+ * @param parentPath {?ExprPath}
2007
+ * @param exactKey {?string} Optional, if already calculated.
2008
+ * @param closeKey {?string}
2009
+ * @returns {[DocumentFragment, Shell]} */
2010
+ init(template, parentPath=null, exactKey=null, closeKey=null) {
2011
+ this.exactKey = exactKey || template.getExactKey();
2012
+ this.closeKey = closeKey || template.getCloseKey();
2569
2013
 
2570
-
2571
- let oldNodes = path.getNodes();
2572
- path.nodesCache = newNodes; // Replaces value set by path.getNodes()
2014
+ this.parentPath = parentPath;
2015
+ this.rootNg = parentPath?.parentNg?.rootNg || this;
2573
2016
 
2017
+
2574
2018
 
2575
- // This pre-check makes it a few percent faster?
2576
- let diff = findArrayDiff(oldNodes, newNodes);
2577
- if (diff !== false) {
2019
+ /** @type {Template} */
2020
+ this.template = template;
2578
2021
 
2579
- if (this.parentPath)
2580
- this.parentPath.clearNodesCache();
2022
+ // new! Is this needed?
2023
+ template.nodeGroup = this;
2581
2024
 
2582
- // Fast clear method
2583
- let isNowEmpty = oldNodes.length && !newNodes.length;
2584
- if (!isNowEmpty || !path.fastClear(oldNodes, newNodes))
2025
+ // Get a cached version of the parsed and instantiated html, and ExprPaths.
2026
+ let shell = Shell.get(template.html);
2027
+ let fragment = shell.fragment.cloneNode(true);
2585
2028
 
2586
- // Rearrange nodes.
2587
- udomdiff(path.parentNode, oldNodes, newNodes, path.nodeMarker);
2029
+ let childNodes = fragment.childNodes;
2030
+ this.startNode = childNodes[0];
2031
+ this.endNode = childNodes[childNodes.length - 1];
2588
2032
 
2589
- this.saveOrphans(oldNodeGroups, oldNodes);
2590
- }
2591
-
2033
+ return [fragment, shell];
2592
2034
  }
2593
-
2594
- /**
2595
- * Find NodeGroups that had their nodes removed and add those nodes to a Fragment so
2596
- * they're not lost forever and the NodeGroup's internal structure is still consistent.
2597
- * Called from NodeGroup.applyNodeExpr().
2598
- * @param oldNodeGroups {NodeGroup[]}
2599
- * @param oldNodes {Node[]} */
2600
- saveOrphans(oldNodeGroups, oldNodes) {
2601
- let oldNgMap = new Map();
2602
- for (let ng of oldNodeGroups) {
2603
- oldNgMap.set(ng.startNode, ng);
2604
-
2605
- // TODO: Is this necessary?
2606
- // if (ng.parentPath)
2607
- // ng.parentPath.clearNodesCache();
2608
- }
2609
2035
 
2610
- for (let i=0, node; node = oldNodes[i]; i++) {
2611
- let ng;
2612
- if (!node.parentNode && (ng = oldNgMap.get(node))) {
2613
- let fragment = document.createDocumentFragment();
2614
- let endNode = ng.endNode;
2615
- while (node !== endNode) {
2616
- fragment.append(node);
2617
- i++;
2618
- node = oldNodes[i];
2619
- }
2620
- fragment.append(endNode);
2621
- }
2622
- }
2623
- }
2036
+ /**
2037
+ * Use the paths to insert the given expressions.
2038
+ * Dispatches expression handling to other functions depending on the path type.
2039
+ * @param exprs {(*|*[]|function|Template)[]}
2040
+ * @param paths {?ExprPath[]} Optional. */
2041
+ applyExprs(exprs, paths=null) {
2042
+ paths = paths || this.paths;
2624
2043
 
2625
- // TODO: Move to ExprPath?
2626
- applyOneExpr(expr, path, newNodes, secondPass) {
2627
2044
 
2628
- if (expr instanceof Template) {
2629
- expr.parentPath = path;
2630
- expr.nodegroup = this;
2631
-
2632
- //if (window.debug && expr.exprs[0] === 'Banana' && path.nodeGroups.length === 0)
2633
- //if (window.debug && expr.exprs[0] === 'Banana')
2634
- // debugger;
2635
-
2636
- let ng = this.manager.getNodeGroup(expr, true);
2637
-
2638
-
2639
- if (ng) {
2640
-
2641
-
2642
-
2643
- // TODO: Track ranges of changed nodes and only pass those to udomdiff?
2644
- // But will that break the swap benchmark?
2645
- newNodes.push(...ng.getNodes());
2646
- path.nodeGroups.push(ng);
2647
- }
2648
-
2649
- // If expression, evaluate later to find partial match.
2650
- else {
2651
- secondPass.push([newNodes.length, path.nodeGroups.length]);
2652
- newNodes.push(expr);
2653
- path.nodeGroups.push(null); // placeholder
2045
+
2046
+ // Update exprs at paths.
2047
+ let exprIndex = exprs.length-1, expr, lastNode;
2048
+
2049
+ // We apply them in reverse order so that a <select> box has its options created from an expression
2050
+ // before its value attribute is set via an expression.
2051
+ for (let path of paths.toReversed()) {
2052
+ expr = exprs[exprIndex];
2053
+
2054
+ // Nodes
2055
+
2056
+ // This is necessary both here and below.
2057
+ if (lastNode && lastNode !== this.rootNg.root && lastNode !== path.nodeMarker && Object.keys(this.currentComponentProps).length) {
2058
+ this.applyComponentExprs(lastNode, this.currentComponentProps);
2059
+ this.currentComponentProps = {};
2654
2060
  }
2655
- }
2656
2061
 
2657
- // Node created by an expression.
2658
- else if (expr instanceof Node) {
2062
+ exprIndex = path.apply(expr, exprs, exprIndex, this.currentComponentProps);
2659
2063
 
2660
- // DocumentFragment created by an expression.
2661
- if (expr instanceof DocumentFragment)
2662
- newNodes.push(...expr.childNodes);
2663
- else
2664
- newNodes.push(expr);
2665
- }
2064
+ lastNode = path.nodeMarker;
2666
2065
 
2667
- else if (Array.isArray(expr))
2668
- for (let subExpr of expr)
2669
- this.applyOneExpr(subExpr, path, newNodes, secondPass);
2670
2066
 
2671
- else if (typeof expr === 'function') {
2672
- expr = watchFunction(expr, this.manager);
2673
-
2674
- this.applyOneExpr(expr, path, newNodes, secondPass);
2067
+ exprIndex--;
2068
+ } // end for(path of this.paths)
2069
+
2070
+
2071
+ // Check again after we iterate through all paths to apply to a component.
2072
+ if (lastNode && lastNode !== this.rootNg.root && Object.keys(this.currentComponentProps).length) {
2073
+ this.applyComponentExprs(lastNode, this.currentComponentProps);
2074
+ this.currentComponentProps = {};
2675
2075
  }
2676
2076
 
2677
- // Text
2678
- else {
2679
- // Convert falsy values (but not 0) to empty string.
2680
- // Convert numbers to string so they compare the same.
2681
- let text = (expr === undefined || expr === false || expr === null) ? '' : expr + '';
2077
+ this.updateStyles();
2682
2078
 
2683
- // Fast path for updating the text of a single text node.
2684
- let first = path.nodeBefore.nextSibling;
2685
- if (first.nodeType === 3 && first.nextSibling === path.nodeMarker && !newNodes.includes(first)) {
2686
- if (first.textContent !== text)
2687
- first.textContent = text;
2688
-
2689
- newNodes.push(first);
2690
- }
2079
+ // Invalidate the nodes cache because we just changed it.
2080
+ this.nodesCache = null;
2691
2081
 
2692
- else {
2693
- // TODO: Optimize this into a Set or Map or something?
2694
- if (!this.existingTextNodes)
2695
- this.existingTextNodes = path.getNodes().filter(n => n.nodeType === 3);
2696
-
2697
- let idx = this.existingTextNodes.findIndex(n => n.textContent === text);
2698
- if (idx !== -1)
2699
- newNodes.push(...this.existingTextNodes.splice(idx, 1));
2700
- else
2701
- newNodes.push(path.parentNode.ownerDocument.createTextNode(text));
2702
- }
2703
- }
2082
+ // If there's leftover expressions, there's probably an issue with the Shell that created this NodeGroup,
2083
+ // and the number of paths not matching.
2084
+
2085
+
2086
+
2087
+
2704
2088
  }
2705
2089
 
2706
2090
  /**
2707
- * Create a nested RedComponent or call render with the new props.
2091
+ * Create a nested Component or call render with the new props.
2708
2092
  * @param el {Solarite:HTMLElement}
2709
2093
  * @param props {Object} */
2710
2094
  applyComponentExprs(el, props) {
2711
-
2095
+
2712
2096
  // TODO: Does a hash of this already exist somewhere?
2713
2097
  // Perhaps if Components were treated as child NodeGroups, which would need to be the child of an ExprPath,
2714
2098
  // then we could re-use the hash and logic from NodeManager?
2715
2099
  let newHash = getObjectHash(props);
2716
-
2717
- let isPreHtmlElement = el.tagName.endsWith('-REDCOMPONENT-PLACEHOLDER');
2100
+
2101
+ let isPreHtmlElement = el.tagName.endsWith('-SOLARITE-PLACEHOLDER');
2718
2102
  let isPreIsElement = el.hasAttribute('_is');
2719
-
2720
-
2103
+
2104
+
2721
2105
  // Instantiate a placeholder.
2722
2106
  if (isPreHtmlElement || isPreIsElement)
2723
2107
  el = this.createNewComponent(el, isPreHtmlElement, props);
2724
-
2725
- // Update params of placeholder.
2108
+
2109
+ // Call render() with the same params that would've been passed to the constructor.
2726
2110
  else if (el.render) {
2727
- let oldHash = componentHash.get(el);
2111
+ let oldHash = Globals.componentHash.get(el);
2728
2112
  if (oldHash !== newHash)
2729
2113
  el.render(props); // Pass new values of props to render so it can decide how it wants to respond.
2730
2114
  }
2731
-
2732
- componentHash.set(el, newHash);
2115
+
2116
+ Globals.componentHash.set(el, newHash);
2733
2117
  }
2734
2118
 
2735
2119
  /**
@@ -2747,8 +2131,8 @@ class NodeGroup {
2747
2131
  isPreHtmlElement = !el.hasAttribute('_is');
2748
2132
 
2749
2133
  let tagName = (isPreHtmlElement
2750
- ? el.tagName.endsWith('-REDCOMPONENT-PLACEHOLDER')
2751
- ? el.tagName.slice(0, -25)
2134
+ ? el.tagName.endsWith('-SOLARITE-PLACEHOLDER')
2135
+ ? el.tagName.slice(0, -21)
2752
2136
  : el.tagName
2753
2137
  : el.getAttribute('is')).toLowerCase();
2754
2138
 
@@ -2772,33 +2156,43 @@ class NodeGroup {
2772
2156
  // We pass the childNodes to the constructor so it can know about them,
2773
2157
  // instead of only afterward when they're appended to the slot below.
2774
2158
  // This is useful for a custom selectbox, for example.
2775
- // NodeGroupManager.pendingChildren stores the childen so the super construtor call to Solarite's constructor
2159
+ // Globals.pendingChildren stores the childen so the super construtor call to Solarite's constructor
2776
2160
  // can add them as children before the rest of the constructor code executes.
2777
2161
  let ch = [... el.childNodes];
2778
- NodeGroupManager.pendingChildren.push(ch); // pop() is called in Solarite constructor.
2162
+ Globals.pendingChildren.push(ch); // pop() is called in Solarite constructor.
2779
2163
  let newEl = new Constructor(props, ch);
2780
2164
 
2781
2165
  if (!isPreHtmlElement)
2782
2166
  newEl.setAttribute('is', el.getAttribute('is').toLowerCase());
2783
2167
  el.replaceWith(newEl);
2784
-
2168
+
2785
2169
  // Set children / slot children
2786
2170
  // TODO: Match named slots.
2787
2171
  // TODO: This only appends to slot if render() is called in the constructor.
2788
2172
  //let slot = newEl.querySelector('slot') || newEl;
2789
2173
  //slot.append(...el.childNodes);
2790
-
2174
+
2791
2175
  // Copy over event attributes.
2792
2176
  for (let propName in props) {
2793
2177
  let val = props[propName];
2794
2178
  if (propName.startsWith('on') && typeof val === 'function')
2795
2179
  newEl.addEventListener(propName.slice(2), e => val(e, newEl));
2180
+
2181
+ // Bind array based event attributes on value.
2182
+ // This same logic is in ExprPath.applyValueAttrib() for non-components.
2183
+ if ((propName === 'value' || propName === 'data-value') && Util.isPath(val)) {
2184
+ let [obj, path] = [val[0], val.slice(1)];
2185
+ newEl.value = delve(obj, path);
2186
+ newEl.addEventListener('input', e => {
2187
+ delve(obj, path, Util.getInputValue(newEl));
2188
+ }, true); // We use capture so we update the values before other events added by the user.
2189
+ }
2796
2190
  }
2797
2191
 
2798
2192
  // If an id pointed at the placeholder, update it to point to the new element.
2799
2193
  let id = el.getAttribute('data-id') || el.getAttribute('id');
2800
2194
  if (id)
2801
- this.manager.rootEl[id] = newEl;
2195
+ delve(this.getRootNode(), id.split(/\./g), newEl);
2802
2196
 
2803
2197
 
2804
2198
  // Update paths to use replaced element.
@@ -2867,470 +2261,343 @@ class NodeGroup {
2867
2261
  return this.startNode?.parentNode
2868
2262
  }
2869
2263
 
2870
-
2871
-
2872
- }
2873
-
2874
-
2875
- let componentHash = new WeakMap();
2876
-
2877
- /**
2878
- * @typedef {Object} RenderOptions
2879
- * @property {boolean=} styles - Indicates whether the Courage component is present.
2880
- * @property {boolean=} scripts - Indicates whether the Power component is present.
2881
- * @property {boolean=} ids
2882
- *
2883
- * @property {?boolean} render
2884
- * Used only when options are given to a class super constructor inheriting from Solarite.
2885
- * True to call render() immediately in super constructor.
2886
- * False to automatically call render() at all.
2887
- * Undefined (default) to call render() when added to the DOM, unless already rendered.
2888
- */
2889
-
2890
-
2891
- /**
2892
- * Manage all the NodeGroups for a single WebComponent or root HTMLElement
2893
- * There's one NodeGroup for the root of the WebComponent, and one for every ${...} expression that creates Node children.
2894
- * And each NodeGroup manages the one or more nodes created by the expression.
2895
- *
2896
- * An instance of this class exists for each element that r() renders to. */
2897
- class NodeGroupManager {
2898
-
2899
- /** @type {HTMLElement|DocumentFragment} */
2900
- rootEl;
2901
-
2902
- /** @type {NodeGroup} */
2903
- rootNg;
2904
-
2905
- /** @type {Change[]} */
2906
- changes = [];
2907
-
2908
-
2909
-
2910
-
2911
-
2912
2264
  /**
2913
- * A map from the html strings and exprs that created a node group, to the NodeGroup.
2914
- * Also stores a map from just the html strings to the NodeGroup, so we can still find a similar match if the exprs changed.
2915
- *
2916
- * @type {MultiValueMap<string, (string|Template)[], NodeGroup>} */
2917
- nodeGroupsAvailable = new MultiValueMap();
2918
- nodeGroupsInUse = [];
2919
-
2920
-
2921
- /** @type {RenderOptions} */
2922
- options = {};
2923
-
2924
-
2925
-
2265
+ * Get the root element of the NodeGroup's RootNodeGroup.
2266
+ * @returns {HTMLElement|DocumentFragment} */
2267
+ getRootNode() {
2268
+ return this.rootNg.root;
2269
+ }
2926
2270
 
2927
2271
  /**
2928
- * @param rootEl {HTMLElement|DocumentFragment} If not specified, the first element of the html will be the rootEl. */
2929
- constructor(rootEl=null) {
2930
- this.rootEl = rootEl;
2931
- /*
2932
-
2933
- */
2272
+ * @returns {RootNodeGroup} */
2273
+ getRootNodeGroup() {
2274
+ return this.rootNg;
2934
2275
  }
2935
2276
 
2936
- /**
2937
- * Render the main template, which may indirectly call renderTemplate() to create children.
2938
- * @param template {Template}
2939
- * @param options {RenderOptions}
2940
- * @return {?DocumentFragment} */
2941
- render(template, options={}) {
2942
- this.mutationWatcherEnabled = false;
2943
- this.options = options;
2944
- this.clearSubscribers = false;
2945
-
2946
-
2947
-
2948
- if (!template && template !== '') {
2949
- this.rootEl.outerHTML = '';
2950
- this.mutationWatcherEnabled = true;
2951
- return null;
2952
- }
2953
-
2954
- // Fast path for empty component.
2955
- if (template.html?.length === 1 && !template.html[0]) {
2956
- this.rootEl.innerHTML = '';
2277
+
2278
+ updatePaths(fragment, paths, offset) {
2279
+ // Update paths to point to the fragment.
2280
+ this.paths.length = paths.length;
2281
+ for (let i=0; i<paths.length; i++) {
2282
+ let path = paths[i].clone(fragment, offset);
2283
+ path.parentNg = this;
2284
+ this.paths[i] = path;
2957
2285
  }
2958
- else {
2286
+ }
2959
2287
 
2960
- // Find or create a NodeGroup for the template.
2961
- // This updates all nodes from the template.
2962
- let close;
2963
- let exact = this.getNodeGroup(template, true);
2964
- if (!exact) {
2965
- close = this.getNodeGroup(template, false);
2288
+ updateStyles() {
2289
+ if (this.styles)
2290
+ for (let [style, oldText] of this.styles) {
2291
+ let newText = style.textContent;
2292
+ if (oldText !== newText)
2293
+ Util.bindStyles(style, this.getRootNodeGroup().root);
2966
2294
  }
2967
-
2968
-
2969
- let firstTime = !this.rootNg;
2970
- this.rootNg = exact || close;
2971
-
2972
- // Reparent NodeGroup
2973
- // TODO: Move this to NodeGroup?
2974
- let parent = this.rootNg.getParentNode();
2975
- if (!this.rootEl)
2976
- this.rootEl = parent;
2977
-
2978
- // If this is the first time rendering this element.
2979
- else if (firstTime) {
2980
-
2981
- // Save slot children
2982
- let fragment;
2983
- if (this.rootEl.childNodes.length) {
2984
- fragment = document.createDocumentFragment();
2985
- fragment.append(...this.rootEl.childNodes);
2986
- }
2295
+ }
2987
2296
 
2988
- // Add rendered elements.
2989
- if (parent instanceof DocumentFragment)
2990
- this.rootEl.append(parent);
2991
- else if (parent)
2992
- this.rootEl.append(...parent.childNodes);
2993
-
2994
- // Apply slot children
2995
- if (fragment) {
2996
- for (let slot of this.rootEl.querySelectorAll('slot[name]')) {
2997
- let name = slot.getAttribute('name');
2998
- if (name)
2999
- slot.append(...fragment.querySelectorAll(`[slot='${name}']`));
3000
- }
3001
- let unamedSlot = this.rootEl.querySelector('slot:not([name])');
3002
- if (unamedSlot)
3003
- unamedSlot.append(fragment);
3004
- }
2297
+
3005
2298
 
3006
- }
3007
2299
 
3008
- // this.rootNg was rendered as childrenOnly=true
3009
- // Apply attributes from a root element to the real root element.
3010
- let ng = this.rootNg;
3011
- if (ng.pseudoRoot && ng.pseudoRoot !== this.rootEl) {
3012
-
2300
+ /**
2301
+ * @param root {HTMLElement}
2302
+ * @param shell {Shell}
2303
+ * @param pathOffset {int} */
2304
+ activateEmbeds(root, shell, pathOffset=0) {
3013
2305
 
3014
- // Remove old attributes
3015
- // for (let attrib of this.rootEl.attributes)
3016
- // if (attrib.name !== 'is' && attrib.name !== 'data-style' && !ng.pseudoRoot.hasAttribute(attrib.name))
3017
- // this.rootEl.removeAttribute(attrib.name)
3018
-
3019
- // Add/set new attributes
3020
- if (firstTime)
3021
- for (let attrib of ng.pseudoRoot.attributes)
3022
- if (!this.rootEl.hasAttribute(attrib.name))
3023
- this.rootEl.setAttribute(attrib.name, attrib.value);
3024
-
3025
- // ng.startNode = ng.endNode = this.rootEl;
3026
- // ng.nodesCache = [ng.startNode]
3027
- // for (let path of ng.paths) {
3028
- // if (path.nodeMarker === ng.rootEl)
3029
- // path.nodeMarker = this.rootEl;
3030
- // path.nodesCache = null;
3031
- //
3032
- // }
3033
- //
3034
- // ng.rootEl = this.rootEl;
3035
- }
2306
+ // static components. These are WebComponents not created by an expression.
2307
+ // Must happen before ids.
2308
+ for (let path of shell.staticComponents) {
2309
+ if (pathOffset)
2310
+ path = path.slice(0, -pathOffset);
2311
+ let el = resolveNodePath(root, path);
3036
2312
 
3037
-
3038
- this.reset();
3039
-
2313
+ // Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
2314
+ if (root !== el/* && !isReplaceEl(root, el)*/) // TODO: is isReplaceEl necessary?
2315
+ this.createNewComponent(el);
3040
2316
  }
3041
2317
 
3042
- this.mutationWatcherEnabled = true;
3043
- return this.rootEl;
3044
-
3045
- }
3046
-
3047
-
3048
- /**
3049
- *
3050
- * 1. Delete a NodeGroup from this.nodeGroupsAvailable that matches this exactKey.
3051
- * 2. Then delete all of that NodeGroup's parents' exactKey entries
3052
- * We don't move them to in-use because we plucked the NodeGroup from them, they no longer match their exactKeys.
3053
- * 3. Then we move all the NodeGroup's exact+close keyed children to inUse because we don't want future calls
3054
- * to getNodeGroup() to borrow the children now that the whole NodeGroup is in-use.
3055
- *
3056
- * TODO: Have NodeGroups keep track of whether they're inUse.
3057
- * That way when we go up or down we don't have to remove those with .inUse===true
3058
- *
3059
- * @param exactKey
3060
- * @param goUp
3061
- * @param child
3062
- * @returns {?NodeGroup} */
3063
- findAndDeleteExact(exactKey, goUp=true, child=undefined) {
3064
-
3065
- let ng = this.nodeGroupsAvailable.delete(exactKey, child);
3066
- if (ng) {
3067
-
3068
-
3069
- // Mark close-key version as in-use.
3070
- let closeNg = this.nodeGroupsAvailable.delete(ng.closeKey, ng);
3071
-
2318
+ let rootEl = this.rootNg.root;
2319
+ if (rootEl) {
3072
2320
 
3073
- // Mark our self as in-use.
3074
- this.nodeGroupsInUse.push(ng);
3075
-
3076
- ng.inUse = true;
3077
- closeNg.inUse = true;
3078
-
3079
- // Mark all parents that have this NodeGroup as a child as in-use.
3080
- // So that way we don't use this parent again
3081
- if (goUp) {
3082
- let ng2 = ng;
3083
- while (ng2 = ng2?.parentPath?.parentNg) {
3084
- if (!ng2.inUse) {
3085
- ng2.inUse = true;
3086
- let success = this.nodeGroupsAvailable.delete(ng2.exactKey, ng2);
3087
- // assert(success);
3088
- let success2 = this.nodeGroupsAvailable.delete(ng2.closeKey, ng2);
3089
- // assert(success);
3090
-
2321
+ // ids
2322
+ if (this.options?.ids !== false)
2323
+ for (let path of shell.ids) {
2324
+ if (pathOffset)
2325
+ path = path.slice(0, -pathOffset);
2326
+ let el = resolveNodePath(root, path);
2327
+ let id = el.getAttribute('data-id') || el.getAttribute('id');
2328
+ if (id) { // If something hasn't removed the id.
3091
2329
 
3092
- // console.log(getHtml(ng2))
3093
- if (success) {
3094
- this.nodeGroupsInUse.push(ng2);
3095
- }
2330
+ // Don't allow overwriting existing class properties if they already have a non-Node value.
2331
+ if (rootEl[id] && !(rootEl[id] instanceof Node))
2332
+ throw new Error(`${rootEl.constructor.name}.${id} already has a value. ` +
2333
+ `Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
2334
+
2335
+ delve(rootEl, id.split(/\./g), el);
3096
2336
  }
3097
2337
  }
3098
- }
3099
2338
 
3100
- // Recurse to mark all child NodeGroups as in-use.
3101
- for (let path of ng.paths)
3102
- for (let childNg of path.nodeGroups) {
3103
- if (!childNg.inUse)
3104
- this.findAndDeleteExact(childNg.exactKey, false, childNg);
3105
- childNg.inUse = true;
2339
+ // styles
2340
+ if (this.options?.styles !== false) {
2341
+ if (shell.styles.length)
2342
+ this.styles = new Map();
2343
+ for (let path of shell.styles) {
2344
+ if (pathOffset)
2345
+ path = path.slice(0, -pathOffset);
2346
+ let style = resolveNodePath(root, path);
2347
+ Util.bindStyles(style, rootEl);
2348
+ this.styles.set(style, style.textContent);
3106
2349
  }
3107
-
3108
- if (ng.parentPath) ;
3109
-
3110
- return ng;
3111
- }
3112
- return null;
3113
- }
3114
-
3115
- /**
3116
- * @param closeKey {string}
3117
- * @param exactKey {string}
3118
- * @param goUp {boolean}
3119
- * @returns {NodeGroup} */
3120
- findAndDeleteClose(closeKey, exactKey, goUp=true) {
3121
- let ng = this.nodeGroupsAvailable.delete(closeKey);
3122
- if (ng) {
3123
-
3124
- // We matched on a new key, so delete the old exactKey.
3125
- let exactNg = this.nodeGroupsAvailable.delete(ng.exactKey, ng);
3126
-
3127
-
3128
-
3129
-
3130
-
3131
- ng.inUse = true;
3132
- if (goUp) {
3133
- let ng2 = ng;
3134
-
3135
- // We borrowed a node from another node group so make sure its parent isn't still an exact match.
3136
- while (ng2 = ng2?.parentPath?.parentNg) {
3137
- if (!ng2.inUse) {
3138
- ng2.inUse = true; // Might speed it up slightly?
3139
- let success = this.nodeGroupsAvailable.delete(ng2.exactKey, ng2);
3140
-
3141
2350
 
3142
- // But it can still be a close match, so we don't use this code.
3143
- success = this.nodeGroupsAvailable.delete(ng2.closeKey, ng2);
3144
-
3145
- }
2351
+ }
2352
+ // scripts
2353
+ if (this.options?.scripts !== false) {
2354
+ for (let path of shell.scripts) {
2355
+ if (pathOffset)
2356
+ path = path.slice(0, -pathOffset);
2357
+ let script = resolveNodePath(root, path);
2358
+ eval(script.textContent);
3146
2359
  }
3147
2360
  }
2361
+ }
2362
+ }
2363
+ }
3148
2364
 
3149
- // Recursively mark all child NodeGroups as in-use.
3150
- // We actually DON't want to do this becuse applyExprs is going to swap out the child NodeGroups
3151
- // and mark them as in-use as it goes.
3152
- // that's probably why uncommenting this causes tests to fail.
3153
- // for (let path of ng.paths)
3154
- // for (let childNg of path.nodeGroups)
3155
- // this.findAndDeleteExact(childNg.exactKey, false, childNg);
3156
2365
 
2366
+ class RootNodeGroup extends NodeGroup {
3157
2367
 
3158
- ng.exactKey = exactKey;
3159
- ng.closeKey = closeKey;
3160
- this.nodeGroupsInUse.push(ng);
3161
-
3162
-
3163
- if (ng.parentPath) ;
3164
- }
3165
-
3166
-
3167
- return ng;
3168
- }
2368
+ /**
2369
+ * Root node at the top of the hierarchy.
2370
+ * @type {HTMLElement} */
2371
+ root;
3169
2372
 
3170
2373
  /**
3171
- * Get an existing or create a new NodeGroup that matches the template,
3172
- * but don't reparent it if it's somewhere else.
3173
- * @param template {Template}
3174
- * @param exact {?boolean}
3175
- * @param createForWatch
3176
- * @return {?NodeGroup} */
3177
- getNodeGroup(template, exact=null, createForWatch=false) {
2374
+ *
2375
+ * @param template
2376
+ * @param el
2377
+ * @param options {?object}
2378
+ */
2379
+ constructor(template, el, options) {
2380
+ super(template);
3178
2381
 
3179
- let exactKey = getObjectHash(template);
2382
+ this.options = options;
3180
2383
 
3181
- // 1. Try to find an exact match.
3182
- let ng;
3183
- if (exact === true) {
3184
- ng = this.findAndDeleteExact(exactKey);
2384
+ this.rootNg = this;
2385
+ let [fragment, shell] = this.init(template);
3185
2386
 
3186
- if (!ng) {
3187
-
3188
- return null;
2387
+ // If adding NodeGroup to an element.
2388
+ let offset = 0;
2389
+ let root = fragment; // TODO: Rename so it's not confused with this.root.
2390
+ if (el) {
2391
+
2392
+ // Save slot children
2393
+ let slotFragment;
2394
+ if (el.childNodes.length) {
2395
+ slotFragment = document.createDocumentFragment();
2396
+ slotFragment.append(...el.childNodes);
3189
2397
  }
3190
- }
3191
2398
 
3192
- // 2. Try to find a close match.
3193
- else {
3194
- // We don't need to delete the exact match bc it's already been deleted in the prev pass.
3195
- let closeKey = template.getCloseKey();
3196
- ng = createForWatch ? null : this.findAndDeleteClose(closeKey, exactKey);
2399
+ this.root = el;
3197
2400
 
3198
- // 2. Update expression values if they've changed.
3199
- if (ng) {
3200
-
3201
- // Temporary for debugging:
3202
- if (window.debug && !window.ng)
3203
- window.ng = ng;
3204
-
3205
-
3206
- ng.applyExprs(template.exprs);
2401
+ // If el should replace the root node of the fragment.
2402
+ if (isReplaceEl(fragment, el)) {
2403
+ el.append(...fragment.children[0].childNodes);
3207
2404
 
3208
-
3209
-
3210
- }
2405
+ // Copy attributes
2406
+ for (let attrib of fragment.children[0].attributes)
2407
+ if (!el.hasAttribute(attrib.name))
2408
+ el.setAttribute(attrib.name, attrib.value);
3211
2409
 
3212
- // 3. Or if not found, create a new NodeGroup
2410
+ // Go one level deeper into all of shell's paths.
2411
+ offset = 1;
2412
+ }
3213
2413
  else {
3214
-
3215
- ng = new NodeGroup(template, this);
3216
-
3217
-
3218
-
3219
-
2414
+ let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
2415
+ if (!isEmpty)
2416
+ el.append(...fragment.childNodes);
2417
+ }
3220
2418
 
3221
- // 4. Mark NodeGroup as being in-use.
3222
- // TODO: Moving from one group to another thrashes the gc. Is there a faster way?
3223
- // Could I have just a single WeakSet of those in use?
3224
- // Perhaps also result could cache its last exprKey and then we'd use only one map?
3225
- ng.exactKey = exactKey;
3226
- ng.closeKey = closeKey;
3227
- if (createForWatch) // TODO: Have this path be a separate function?
3228
- this.nodeGroupsAvailable.add(ng.exactKey, ng);
2419
+ // Setup slots
2420
+ if (slotFragment) {
2421
+ for (let slot of el.querySelectorAll('slot[name]')) {
2422
+ let name = slot.getAttribute('name');
2423
+ if (name) {
2424
+ let slotChildren = slotFragment.querySelectorAll(`[slot='${name}']`);
2425
+ slot.append(...slotChildren);
2426
+ }
2427
+ }
2428
+ let unamedSlot = el.querySelector('slot:not([name])');
2429
+ if (unamedSlot)
2430
+ unamedSlot.append(slotFragment);
3229
2431
  else
3230
- this.nodeGroupsInUse.push(ng);
2432
+ el.append(slotFragment);
3231
2433
  }
2434
+
2435
+ root = el;
2436
+ this.startNode = el;
2437
+ this.endNode = el;
3232
2438
  }
3233
-
3234
- // New!
3235
- // We clear the parent PathExpr's nodesCache when we remove ourselves from it.
3236
- // Benchmarking shows this doesn't slow down the partialUpdate benchmark.
3237
- if (ng.parentPath) {
3238
- // ng.parentPath.clearNodesCache(); // Makes partialUpdate benchmark 10x slower!
3239
- ng.parentPath = null;
2439
+ else {
2440
+ let singleEl = getSingleEl(fragment);
2441
+ this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
2442
+ if (singleEl) {
2443
+ root = singleEl;
2444
+ offset = 1;
2445
+ }
3240
2446
  }
3241
2447
 
2448
+ this.updatePaths(root, shell.paths, offset);
3242
2449
 
3243
-
3244
-
3245
- return ng;
2450
+ this.activateEmbeds(root, shell, offset);
2451
+
2452
+ // Apply exprs
2453
+ this.applyExprs(template.exprs);
3246
2454
  }
2455
+ }
3247
2456
 
3248
- reset() {
3249
- //this.changes = [];
3250
- let available = this.nodeGroupsAvailable;
3251
- for (let ng of this.nodeGroupsInUse) {
3252
- ng.inUse = false;
3253
- available.add(ng.exactKey, ng);
3254
- available.add(ng.closeKey, ng);
2457
+ function getSingleEl(fragment) {
2458
+ let nonempty = [];
2459
+ for (let n of fragment.childNodes) {
2460
+ if (n.nodeType === 1 || n.nodeType === 3 && n.textContent.trim().length) {
2461
+ if (nonempty.length)
2462
+ return null;
2463
+ nonempty.push(n);
3255
2464
  }
3256
- this.nodeGroupsInUse = [];
2465
+ }
2466
+ return nonempty[0];
2467
+ }
3257
2468
 
3258
- // Used for watches
3259
- this.changes = [];
2469
+ /**
2470
+ * Does the fragment have one child that's an element matching the tagname of el?
2471
+ * @param fragment {DocumentFragment}
2472
+ * @param el {HTMLElement}
2473
+ * @returns {boolean} */
2474
+ function isReplaceEl(fragment, el) {
2475
+ return el.tagName.includes('-')
2476
+ && fragment.children.length===1
2477
+ && fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') === el.tagName;
2478
+ }
2479
+
2480
+ /**
2481
+ * The html strings and evaluated expressions from an html tagged template.
2482
+ * A unique Template is created for each item in a loop.
2483
+ * Although the reference to the html strings is shared among templates. */
2484
+ class Template {
3260
2485
 
3261
-
3262
- // TODO: free the memory from any nodeGroupsAvailable() after render is done, since they weren't used?
3263
- }
2486
+ /** @type {(Template|string|function)|(Template|string|function)[]} Evaulated expressions. */
2487
+ exprs = []
3264
2488
 
2489
+ /** @type {string[]} */
2490
+ html = [];
3265
2491
 
3266
- // deprecated
3267
- //pathToLoopInfo = new MultiValueMap(); // uses a Set() for each value.
3268
- clearSubscribers = false;
2492
+ /** @type {Array} Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
2493
+ hashedFields;
3269
2494
 
3270
2495
  /**
3271
- * One path may be used to loop in more than one place, so we use this to get every anchor from each loop.
3272
- * @param path {Array}
3273
- * @return {LoopInfo[]} A function that gets the loop anchor NodeGroup */
3274
- getLoopInfo(path) {
3275
- let serializedArrayPath = serializePath(path);
3276
- return [...this.pathToLoopInfo.getAll(serializedArrayPath)]; // This is set inside forEach()
3277
- }
2496
+ * @deprecated
2497
+ * @type {ExprPath} Used with forEach() from watch.js
2498
+ * Set in ExprPath.apply() */
2499
+ parentPath;
2500
+
2501
+ /** @type {NodeGroup} */
2502
+ nodeGroup;
3278
2503
 
3279
-
3280
2504
  /**
3281
- * @deprecated
3282
- * Store the functions used to create items for each loop.
3283
- * TODO: Can this be combined with pathToTemplates?
3284
- * @type {MultiValueMap<string, Subscriber>} */
3285
- pathToLoopInfo = new MultiValueMap();
3286
-
2505
+ * @type {string[][]} */
2506
+ paths = [];
2507
+
3287
2508
  /**
3288
- * Maps variable paths to the templates used to create NodeGroups
3289
- * @type {MultiValueMap<string, Subscriber>} */
3290
- subscribers = new MultiValueMap();
3291
-
3292
- clearSubscribersIfNeeded() {
3293
- if (this.clearSubscribers) {
3294
- this.pathToLoopInfo = new MultiValueMap();
3295
- this.subscribers = new MultiValueMap();
3296
- this.clearSubscribers = false;
3297
- }
2509
+ *
2510
+ * @param htmlStrings {string[]}
2511
+ * @param exprs {*[]} */
2512
+ constructor(htmlStrings, exprs) {
2513
+ this.html = htmlStrings;
2514
+ this.exprs = exprs;
2515
+
2516
+ //this.trace = new Error().stack.split(/\n/g)
2517
+
2518
+ // Multiple templates can share the same htmlStrings array.
2519
+ //this.hashedFields = [getObjectId(htmlStrings), exprs]
2520
+
2521
+
3298
2522
  }
3299
-
3300
2523
 
3301
2524
  /**
3302
- * Get the NodeGroupManager for a Web Component.
3303
- * @param rootEl {Solarite|HTMLElement}
3304
- * @return {NodeGroupManager} */
3305
- static get(rootEl) {
3306
- let ngm = nodeGroupManagers.get(rootEl);
3307
- if (!ngm) {
3308
- ngm = new NodeGroupManager(rootEl);
3309
- nodeGroupManagers.set(rootEl, ngm);
3310
- }
2525
+ * Called by JSON.serialize when it encounters a Template.
2526
+ * This prevents the hashed version from being too large. */
2527
+ toJSON() {
2528
+ if (!this.hashedFields)
2529
+ this.hashedFields = [getObjectId(this.html), this.exprs];
3311
2530
 
3312
- return ngm;
2531
+ return this.hashedFields
3313
2532
  }
3314
2533
 
2534
+ /**
2535
+ * Render the main template, which may indirectly call renderTemplate() to create children.
2536
+ * @param el {HTMLElement}
2537
+ * @param options {RenderOptions}
2538
+ * @return {?DocumentFragment|HTMLElement} */
2539
+ render(el=null, options={}) {
2540
+ let ng;
2541
+ let standalone = !el;
2542
+ let firstTime = false;
3315
2543
 
3316
-
3317
- }
2544
+ // Rendering a standalone element.
2545
+ // TODO: figure out when to not use RootNodeGroup
2546
+ if (standalone) {
2547
+ ng = new RootNodeGroup(this, null, options);
2548
+ el = ng.getRootNode();
2549
+ Globals.nodeGroups.set(el, ng);
2550
+ firstTime = true;
2551
+ }
2552
+ else {
2553
+ ng = Globals.nodeGroups.get(el);
2554
+ if (!ng) {
2555
+ ng = new RootNodeGroup(this, el, options);
2556
+ Globals.nodeGroups.set(el, ng);
2557
+ firstTime = true;
2558
+ }
2559
+ }
3318
2560
 
3319
- NodeGroupManager.pendingChildren = [];
2561
+ // Creating the root nodegroup also renders it.
2562
+ // If we didn't just create it, we need to render it.
2563
+ if (!firstTime) {
2564
+ if (this.html?.length === 1 && !this.html[0])
2565
+ el.innerHTML = ''; // Fast path for empty component.
2566
+ else
2567
+ ng.applyExprs(this.exprs);
2568
+ }
3320
2569
 
3321
- /**
3322
- * Each Element that has Expr children has an associated NodeGroupManager here.
3323
- * @type {WeakMap<HTMLElement, NodeGroupManager>} */
3324
- let nodeGroupManagers = new WeakMap();
2570
+ return el;
2571
+ }
3325
2572
 
2573
+ getExactKey() {
2574
+ if (!this.exactKey)
2575
+ this.exactKey = getObjectHash(this); // calls this.toJSON().
2576
+ return this.exactKey;
2577
+ }
3326
2578
 
2579
+ getCloseKey() {
2580
+ if (!this.closeKey)
2581
+ this.closeKey = '@'+this.toJSON()[0];
2582
+ // Use the joined html when debugging? But it breaks some tests.
2583
+ //return '@'+this.html.join('|')
3327
2584
 
3328
- class LoopInfo {
3329
- constructor(loopTemplate, itemTransformer) {
3330
- this.template = loopTemplate;
3331
- this.itemTransformer = itemTransformer;
2585
+ return this.closeKey;
3332
2586
  }
3333
- }
2587
+ }
2588
+
2589
+
2590
+ /**
2591
+ * @typedef {Object} RenderOptions
2592
+ * @property {boolean=} styles - Replace :host in style tags to scope them locally.
2593
+ * @property {boolean=} scripts - Execute script tags.
2594
+ * @property {boolean=} ids - Create references to elements with id or data-id attributes.
2595
+ * @property {?boolean} render - Deprecated.
2596
+ * Used only when options are given to a class super constructor inheriting from Solarite.
2597
+ * True to call render() immediately in super constructor.
2598
+ * False to automatically call render() at all.
2599
+ * Undefined (default) to call render() when added to the DOM, unless already rendered.
2600
+ */
3334
2601
 
3335
2602
  /**
3336
2603
  * Convert strings to HTMLNodes.
@@ -3345,133 +2612,147 @@ class LoopInfo {
3345
2612
  * 5. TODO: list more
3346
2613
  *
3347
2614
  * Currently supported:
3348
- * 1. r`<b>Hello${'World'}!` // Create Template that can later be used to create nodes.
2615
+ * 1. r(el, options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
2616
+ * 2. r(el, template, ?options) // Render the Template created by #1 to element.
3349
2617
  *
3350
- * 2. r(el, template, ?options) // Render the template created by #1 to element.
3351
- * 3. r(el, options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
2618
+ * 3. r`<b>Hello</b> ${'World'}!` // Create Template that can later be used to create nodes.
3352
2619
  *
3353
2620
  * 4. r('Hello'); // Create single text node.
3354
2621
  * 5. r('<b>Hello</b>'); // Create single HTMLElement
3355
2622
  * 6. r('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
3356
- * 7. r()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which includes properly handling nested components and r`` sub-expressions.
2623
+ * 7. r()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which
2624
+ * // includes properly handling nested components and r`` sub-expressions.
3357
2625
  * 8. r(template) // Render Template created by #1.
3358
- * 9. r(() => r`<b>Hello</b>`); // Create dynamic element that has a render() function.
3359
2626
  *
3360
- * @param htmlStrings {?HTMLElement|string|string[]|function():Template}
3361
- * @param exprs {*[]|string|Template}
2627
+ * 9. r({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
2628
+ *
2629
+ * @param htmlStrings {?HTMLElement|string|string[]|function():Template|{render:function()}}
2630
+ * @param exprs {*[]|string|Template|Object}
3362
2631
  * @return {Node|HTMLElement|Template} */
3363
2632
  function r(htmlStrings=undefined, ...exprs) {
3364
2633
 
3365
- // 1. Path if used as a template tag.
3366
- if (Array.isArray(htmlStrings)) {
3367
- return new Template(htmlStrings, exprs);
3368
- }
3369
-
3370
- else if (htmlStrings instanceof Node) {
3371
- let parent = htmlStrings, template = exprs[0];
3372
-
3373
- // 2. Render template created by #4 to element.
3374
- if (exprs[0] instanceof Template) {
3375
- let ngm = NodeGroupManager.get(parent);
3376
- let options = exprs[1];
3377
- ngm.render(template, options);
3378
-
3379
- // Append on the first go.
3380
- if (!parent.childNodes.length && this) {
3381
- // TODO: Is htis ever executed?
3382
- debugger;
3383
- parent.append(this.rootNg.getParentNode());
3384
- }
3385
- }
3386
-
3387
- // 3
3388
- else if (!exprs.length || exprs[0]) {
3389
- if (parent.shadowRoot)
3390
- parent.innerHTML = ''; // Remove shadowroot. TODO: This could mess up paths?
3391
-
3392
- let options = exprs[0];
3393
- return (htmlStrings, ...exprs) => {
3394
- rendered.add(parent);
3395
- let template = r(htmlStrings, ...exprs);
3396
- let ngm = NodeGroupManager.get(parent);
3397
- return ngm.render(template, options);
3398
- }
3399
- }
3400
-
3401
- // null for expr[0], remove whole element.
3402
- else {
3403
- let ngm = NodeGroupManager.get(parent);
3404
- ngm.render(null, exprs[1]);
3405
- }
3406
- }
3407
-
3408
- else if (typeof htmlStrings === 'string' || htmlStrings instanceof String) {
3409
- // If it starts with a string, trim both ends.
3410
- // TODO: Also trim if it ends with whitespace?
3411
- if (htmlStrings.match(/^\s^</))
3412
- htmlStrings = htmlStrings.trim();
3413
-
3414
- // We create a new one each time because otherwise
3415
- // the returned fragment will have its content replaced by a subsequent call.
3416
- let templateEl = document.createElement('template');
3417
- templateEl.innerHTML = htmlStrings;
3418
-
3419
- // 4+5. Return Node if there's one child.
3420
- if (templateEl.content.childNodes.length === 1)
3421
- return templateEl.content.firstChild;
3422
-
3423
- // 6. Otherwise return DocumentFragment.
3424
- return templateEl.content;
3425
- }
3426
-
3427
- // 7. Create a static element
3428
- else if (htmlStrings === undefined) {
3429
- return (htmlStrings, ...exprs) => {
3430
- //rendered.add(parent)
3431
- let template = r(htmlStrings, ...exprs);
3432
- return template.toNode();
3433
- }
3434
- }
3435
-
3436
- // 8.
3437
- else if (htmlStrings instanceof Template) {
3438
- let ngm = new NodeGroupManager();
3439
- return ngm.render(htmlStrings);
3440
- }
3441
-
3442
- // 9. Create dynamic element with render() function.
3443
- else if (typeof htmlStrings === 'function') {
3444
- let getTemplate = htmlStrings;
3445
- let template = getTemplate();
3446
-
3447
- if (typeof template === 'string')
3448
- throw new Error(`Please add the "r" prefix before the string "${template}"`)
3449
-
3450
- let ngm = new NodeGroupManager();
3451
- template.replaceMode = true;
3452
- let el = ngm.render(template);
3453
-
3454
- el.render = (function() {
3455
- template = getTemplate();
3456
- ngm.render(template);
3457
- }).bind(el);
3458
-
3459
- return el;
3460
- }
3461
- else
3462
- throw new Error('Unsupported arguments.')
3463
- }
2634
+ // TODO: Make this a more flat if/else and call other functions for the logic.
2635
+ if (htmlStrings instanceof Node) {
2636
+ let parent = htmlStrings, template = exprs[0];
2637
+
2638
+ // 1
2639
+ if (!(exprs[0] instanceof Template)) {
2640
+ if (parent.shadowRoot)
2641
+ parent.innerHTML = ''; // Remove shadowroot. TODO: This could mess up paths?
2642
+
2643
+ let options = exprs[0];
2644
+
2645
+ // Return a tagged template function that applies the tagged themplate to parent.
2646
+ let taggedTemplate = (htmlStrings, ...exprs) => {
2647
+ Globals.rendered.add(parent);
2648
+ let template = new Template(htmlStrings, exprs);
2649
+ return template.render(parent, options);
2650
+ };
2651
+ return taggedTemplate;
2652
+ }
3464
2653
 
2654
+ // 2. Render template created by #4 to element.
2655
+ else if (exprs[0] instanceof Template) {
2656
+ let options = exprs[1];
2657
+ template.render(parent, options);
3465
2658
 
2659
+ // Append on the first go.
2660
+ if (!parent.childNodes.length && this) {
2661
+ // TODO: Is this ever executed?
2662
+ debugger;
2663
+ parent.append(this.rootNg.getParentNode());
2664
+ }
2665
+ }
3466
2666
 
3467
2667
 
3468
2668
 
2669
+ // null for expr[0], remove whole element.
2670
+ // This path never happens?
2671
+ else {
2672
+ throw new Error('unsupported');
2673
+ //let ngm = NodeGroupManager.get(parent);
2674
+ //ngm.render(null, exprs[1])
2675
+ }
2676
+ }
3469
2677
 
3470
- /**
3471
- * Elements that have been rendered to by r() at least once.
3472
- * @type {WeakSet<HTMLElement>} */
3473
- let rendered = new WeakSet();
2678
+ // 3. Path if used as a template tag.
2679
+ else if (Array.isArray(htmlStrings)) {
2680
+ return new Template(htmlStrings, exprs);
2681
+ }
2682
+
2683
+ else if (typeof htmlStrings === 'string' || htmlStrings instanceof String) {
2684
+ // If it starts with a string, trim both ends.
2685
+ // TODO: Also trim if it ends with whitespace?
2686
+ if (htmlStrings.match(/^\s^</))
2687
+ htmlStrings = htmlStrings.trim();
2688
+
2689
+ // We create a new one each time because otherwise
2690
+ // the returned fragment will have its content replaced by a subsequent call.
2691
+ let templateEl = document.createElement('template');
2692
+ templateEl.innerHTML = htmlStrings;
2693
+
2694
+ // 4+5. Return Node if there's one child.
2695
+ let relevantNodes = Util.trimEmptyNodes(templateEl.content.childNodes);
2696
+ if (relevantNodes.length === 1)
2697
+ return relevantNodes[0];
2698
+
2699
+ // 6. Otherwise return DocumentFragment.
2700
+ return templateEl.content;
2701
+ }
2702
+
2703
+ // 7. Create a static element
2704
+ else if (htmlStrings === undefined) {
2705
+ return (htmlStrings, ...exprs) => {
2706
+ //Globals.rendered.add(parent)
2707
+ let template = r(htmlStrings, ...exprs);
2708
+ return template.render();
2709
+ }
2710
+ }
2711
+
2712
+ // 8.
2713
+ else if (htmlStrings instanceof Template) {
2714
+ return htmlStrings.render();
2715
+ }
2716
+
2717
+
2718
+ // 9. Create dynamic element with render() function.
2719
+ else if (typeof htmlStrings === 'object') {
2720
+ let obj = htmlStrings;
2721
+
2722
+ // Special rebound render path, called by normal path.
2723
+ if (Globals.objToEl.has(obj)) {
2724
+ return function(...args) {
2725
+ let template = r(...args);
2726
+ let el = template.render();
2727
+ Globals.objToEl.set(obj, el);
2728
+ }.bind(obj);
2729
+ }
2730
+
2731
+ // Normal path
2732
+ else {
2733
+ Globals.objToEl.set(obj, null);
2734
+ obj.render(); // Calls the Special rebound render path above, when the render function calls r(this)
2735
+ let el = Globals.objToEl.get(obj);
2736
+ Globals.objToEl.delete(obj);
2737
+
2738
+ for (let name in obj)
2739
+ if (typeof obj[name] === 'function')
2740
+ el[name] = obj[name].bind(el);
2741
+ else
2742
+ el[name] = obj[name];
2743
+
2744
+ return el;
2745
+ }
2746
+ }
2747
+
2748
+ else
2749
+ throw new Error('Unsupported arguments.')
2750
+ }
3474
2751
 
2752
+ //import {watchGet, watchSet} from "./watch.js";
2753
+
2754
+
2755
+
3475
2756
  function defineClass(Class, tagName, extendsTag) {
3476
2757
  if (!customElements.getName(Class)) { // If not previously defined.
3477
2758
  tagName = tagName || camelToDashes(Class.name);
@@ -3486,14 +2767,9 @@ function defineClass(Class, tagName, extendsTag) {
3486
2767
  }
3487
2768
  }
3488
2769
 
3489
- /**
3490
- * @type {Object<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
3491
- let elementClasses = {};
3492
2770
 
3493
- /**
3494
- * Store which instances of Solarite have already been added to the DOM. * @type {WeakSet<HTMLElement>}
3495
- */
3496
- let connected = new WeakSet();
2771
+
2772
+
3497
2773
 
3498
2774
  /**
3499
2775
  * Create a version of the Solarite class that extends from the given tag name.
@@ -3502,8 +2778,15 @@ let connected = new WeakSet();
3502
2778
  * 2. Calls render() when added to the DOM, if it hasn't been called already.
3503
2779
  * 3. Child elements are added before constructor is called. But they're also passed to the constructor.
3504
2780
  * 4. We can use this.html = r`...` to set html.
3505
- * 5. We have the onConnect, onFirstConnect, and onDisconnect methods. These could be standalone though.
2781
+ * 5. We have the onConnect, onFirstConnect, and onDisconnect methods.
2782
+ * Can't figure out how to have these work standalone though, and still be synchronous.
3506
2783
  * 6. Can we extend from other element types like TR?
2784
+ * 7. Shows default text if render() function isn't defined.
2785
+ *
2786
+ * Advantages to inheriting from HTMLElement
2787
+ * 1. Minimization won't break when it renames the Class and we call customElements.define() on the wrong name.
2788
+ * 2. We can inherit from things like HTMLTableRowElement directly.
2789
+ * 3. There's less magic, since everyone is familiar with defining custom elements.
3507
2790
  *
3508
2791
  * @param extendsTag {?string}
3509
2792
  * @return {Class} */
@@ -3513,10 +2796,10 @@ function createSolarite(extendsTag=null) {
3513
2796
  if (extendsTag && !extendsTag.includes('-')) {
3514
2797
  extendsTag = extendsTag.toLowerCase();
3515
2798
 
3516
- BaseClass = elementClasses[extendsTag];
2799
+ BaseClass = Globals.elementClasses[extendsTag];
3517
2800
  if (!BaseClass) { // TODO: Use Cache
3518
2801
  BaseClass = document.createElement(extendsTag).constructor;
3519
- elementClasses[extendsTag] = BaseClass;
2802
+ Globals.elementClasses[extendsTag] = BaseClass;
3520
2803
  }
3521
2804
  }
3522
2805
 
@@ -3552,26 +2835,24 @@ function createSolarite(extendsTag=null) {
3552
2835
  constructor(options={}) {
3553
2836
  super();
3554
2837
 
3555
-
3556
-
3557
2838
  // TODO: Is options.render ever used?
3558
2839
  if (options.render===true)
3559
2840
  this.render();
3560
2841
 
3561
2842
  else if (options.render===false)
3562
- rendered.add(this); // Don't render on connectedCallback()
2843
+ Globals.rendered.add(this); // Don't render on connectedCallback()
3563
2844
 
3564
2845
  // Add children before constructor code executes.
3565
2846
  // PendingChildren is setup in NodeGroup.createNewComponent()
3566
2847
  // TODO: Match named slots.
3567
- let ch = NodeGroupManager.pendingChildren.pop();
2848
+ let ch = Globals.pendingChildren.pop();
3568
2849
  if (ch)
3569
2850
  (this.querySelector('slot') || this).append(...ch);
3570
2851
 
3571
-
2852
+ /** @deprecated */
3572
2853
  Object.defineProperty(this, 'html', {
3573
2854
  set(html) {
3574
- rendered.add(this);
2855
+ Globals.rendered.add(this);
3575
2856
  if (typeof html === 'string') {
3576
2857
  console.warn("Assigning to this.html without the r template prefix.");
3577
2858
  this.innerHTML = html;
@@ -3594,7 +2875,7 @@ function createSolarite(extendsTag=null) {
3594
2875
  /**
3595
2876
  * Call render() only if it hasn't already been called. */
3596
2877
  renderFirstTime() {
3597
- if (!rendered.has(this) && this.render)
2878
+ if (!Globals.rendered.has(this) && this.render)
3598
2879
  this.render();
3599
2880
  }
3600
2881
 
@@ -3602,8 +2883,8 @@ function createSolarite(extendsTag=null) {
3602
2883
  * Called automatically by the browser. */
3603
2884
  connectedCallback() {
3604
2885
  this.renderFirstTime();
3605
- if (!connected.has(this)) {
3606
- connected.add(this);
2886
+ if (!Globals.connected.has(this)) {
2887
+ Globals.connected.add(this);
3607
2888
  this.onFirstConnect();
3608
2889
  }
3609
2890
  this.onConnect();
@@ -3618,115 +2899,16 @@ function createSolarite(extendsTag=null) {
3618
2899
  defineClass(this, tagName, extendsTag);
3619
2900
  }
3620
2901
 
3621
-
3622
- renderWatched() {
3623
- let ngm = NodeGroupManager.get(this);
3624
-
3625
- let nodeGroupUpdates = [];
3626
-
3627
- for (let change of ngm.changes) {
3628
- if (change.action === 'set') {
3629
- for (let transformerInfo of change.transformerInfo) {
3630
-
3631
- let oldHash = transformerInfo.hash;
3632
-
3633
- let newObj = delve(watchSet(transformerInfo.path[0]), transformerInfo.path.slice(1));
3634
- let newTemplate = transformerInfo.transformer(newObj);
3635
- let newHash = getObjectHash(newTemplate);
3636
- let ngs = [...ngm.nodeGroupsAvailable.data[oldHash]];
3637
- for (let ng of ngs) {
3638
- nodeGroupUpdates.push([ng, oldHash, newHash, newTemplate.exprs, transformerInfo]);
3639
- }
3640
- }
3641
- }
3642
-
3643
- else if (change.action === 'delete') {
3644
- for (let hash of change.value) {
3645
- let ngs = [...ngm.nodeGroupsAvailable.getAll(hash)]; // deletes from nodeGroupsAvailable.
3646
-
3647
- for (let ng of ngs) {
3648
- if (ng.parentPath)
3649
- ng.parentPath.clearNodesCache();
3650
-
3651
- for (let node of ng.getNodes())
3652
- node.remove();
3653
-
3654
- // TODO: Update ancestor NodeGroup exactKeys
3655
- }
3656
- }
3657
- }
3658
- else if (change.action === 'insert') {
3659
-
3660
- let beforeNg = change.beforeTemplate ? ngm.getNodeGroup(change.beforeTemplate, true) : null;
3661
- let arrayPath = [change.root, ...change.path];
3662
-
3663
- // Get anchor so we can use it to get the parent
3664
- // TODO: Should this be watchGet(change.root) ?
3665
- for (let loopInfo of ngm.getLoopInfo([change.root, ...change.path.slice(0, -1)])) {
3666
-
3667
- // Change.extra is aTemplate telling us where to insert before.
3668
- let beforeNode = beforeNg?.startNode || loopInfo.template.parentPath.nodeMarker;
3669
-
3670
- // Loop over every item added to the array.
3671
- let i = 0; // TODO: How to get real insert index.
3672
- for (let obj of change.value) {
3673
-
3674
- // Same logic as forEach() function.
3675
-
3676
- let callback = loopInfo.itemTransformer;
3677
- let path = [...arrayPath.slice(0, -1), (arrayPath.at(-1) * 1 + i) + ''];
3678
-
3679
- // Shortened logic found in watchGet(), but not any faster?
3680
- // the watchSet() is what makes this slower!
3681
- // let obj = delve(watchSet(path[0]), path.slice(1));
3682
- // let template = callback(obj);
3683
- // let serializedPath = serializePath(path);
3684
- // pathToTransformer.add(serializedPath, new TransformerInfo(path, callback, template)); // Uses a Set() to ensure no duplicates.
3685
-
3686
- let template = watchGet(path, callback);
3687
- i++;
3688
-
3689
-
3690
- //let template = loopInfo.itemTransformer(obj); // What if it takes more than one obj argument?
3691
-
3692
- // Create new NodeGroup
3693
- let ng = ngm.getNodeGroup(template, false, true);
3694
- ng.parentPath = beforeNg?.parentPath || loopInfo.template.parentPath;
3695
-
3696
- for (let node of ng.getNodes())
3697
- beforeNode.parentNode.insertBefore(node, beforeNode);
3698
-
3699
- if (ng.parentPath) // This check is needed for the forEachSpliceInsert test, but why?
3700
- ng.parentPath.clearNodesCache();
3701
- }
3702
-
3703
- // TODO: Update ancestor NodeGroup exactKeys
3704
- }
3705
- }
3706
- }
3707
-
3708
- // Update them all at once, that way we can reassign the same value twice.
3709
- for (let [ng, oldHash, newHash, exprs, ti] of nodeGroupUpdates) {
3710
- ng.applyExprs(exprs);
3711
- ngm.nodeGroupsAvailable.data[oldHash].delete(ng);
3712
- ng.exactKey = ti.hash = newHash;
3713
- ngm.nodeGroupsAvailable.add(ng.exactKey, ng); // Add back to Map with new key.
3714
- }
3715
-
3716
-
3717
- ngm.changes = [];
3718
-
3719
- return []; // TODO
3720
- }
3721
2902
 
3722
- /**
3723
- * @deprecated Use the getArg() function instead. */
3724
- getArg(name, val=null, type=ArgType.String) {
3725
- return getArg(this, name, val, type);
3726
- }
3727
2903
  }
3728
2904
  }
3729
2905
 
2906
+ /**
2907
+ * Solarite JavasCript UI library.
2908
+ * MIT License
2909
+ * https://vorticode.github.io/solarite/
2910
+ */
2911
+
3730
2912
  /**
3731
2913
  * TODO: The Proxy and the multiple base classes mess up 'instanceof Solarite'
3732
2914
  * @type {Node|Class<HTMLElement>|function(tagName:string):Node|Class<HTMLElement>} */
@@ -3735,6 +2917,10 @@ let Solarite = new Proxy(createSolarite(), {
3735
2917
  return createSolarite(...args)
3736
2918
  }
3737
2919
  });
3738
- // unfinished
2920
+ let getInputValue = Util.getInputValue;
2921
+
2922
+ //Experimental:
2923
+ //export {forEach, watchGet, watchSet} from './watch.js' // old, unfinished
2924
+ //export {watch} from './watch2.js'; // unfinished
3739
2925
 
3740
- export { ArgType, Solarite, forEach, getArg, r, watch, watchGet, watchSet };
2926
+ export { ArgType, Globals, Solarite, Template, delve, getArg, getInputValue, r };