solarite 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/benchmarks/naive/Solarite.min.js +4 -0
  2. package/benchmarks/naive/index.html +14 -0
  3. package/benchmarks/naive/main.js +339 -0
  4. package/benchmarks/naive/package-lock.json +13 -0
  5. package/benchmarks/naive/package.json +23 -0
  6. package/benchmarks/readme.md +48 -0
  7. package/build/build.bat +2 -2
  8. package/build/build.js +1 -1
  9. package/dist/Solarite-debug.js +1344 -1532
  10. package/dist/Solarite.js +1298 -1334
  11. package/dist/Solarite.min.js +3 -3
  12. package/dist/udomdiff-license.txt +18 -0
  13. package/docs/index.md +440 -130
  14. package/docs/js/Playground.js +1 -1
  15. package/docs/js/codemirror/codemirror6.js +3683 -3206
  16. package/docs/js/codemirror/themeSolarIce.js +1 -1
  17. package/docs/js/documentation.js +1 -1
  18. package/docs/js/ui/CodeEditor.js +183 -45
  19. package/docs/js/ui/FlexResizer.js +14 -3
  20. package/docs/js/util/Errors.js +11 -0
  21. package/docs/media/documentation.css +6 -3
  22. package/index.html +449 -30
  23. package/package.json +1 -1
  24. package/readme.md +1 -1
  25. package/src/solarite/ExprPath.js +349 -151
  26. package/src/solarite/Globals.js +43 -1
  27. package/src/solarite/NodeGroup.js +275 -293
  28. package/src/solarite/Shell.js +26 -28
  29. package/src/solarite/Solarite.js +12 -0
  30. package/src/solarite/Template.js +55 -128
  31. package/src/solarite/Util.js +131 -7
  32. package/src/solarite/createSolarite.js +14 -19
  33. package/src/solarite/getArg.js +1 -1
  34. package/src/solarite/hash.js +18 -35
  35. package/src/solarite/r.js +127 -127
  36. package/src/solarite/watch3.js +18 -11
  37. package/src/{solarite → unused}/NodeGroupManager.js +81 -109
  38. package/src/{solarite → unused}/watch.js +1 -1
  39. package/src/{solarite → unused}/watch2.js +1 -2
  40. package/src/{solarite → util}/MultiValueMap.js +18 -11
  41. package/src/util/WeakArray.js +33 -0
  42. package/tests/Solarite.test.js +862 -167
  43. package/tests/index.html +4 -2
  44. package/tests/run.bat +1 -1
  45. package/deno.lock +0 -176
@@ -1,13 +1,14 @@
1
1
  import {assert} from "../util/Errors.js";
2
2
  import delve from "../util/delve.js";
3
- import {getObjectId} from "./hash.js";
4
3
  import NodeGroup from "./NodeGroup.js";
5
- import {div, findArrayDiff, setIndent} from "./Util.js";
4
+ import Util, {arraySame, setIndent} from "./Util.js";
6
5
  import Template from "./Template.js";
7
6
  import Globals from "./Globals.js";
7
+ import MultiValueMap from "../util/MultiValueMap.js";
8
+ import udomdiff from "./udomdiff.js";
8
9
 
9
10
  /**
10
- * Path to where an expression should be evaluated within a Shell.
11
+ * Path to where an expression should be evaluated within a Shell or NodeGroup.
11
12
  * Path is only valid until the expressions before it are evaluated.
12
13
  * TODO: Make this based on parent and node instead of path? */
13
14
  export default class ExprPath {
@@ -46,14 +47,9 @@ export default class ExprPath {
46
47
  * @type {Node|HTMLElement} */
47
48
  nodeMarker;
48
49
 
49
- /** @deprecated */
50
- get parentNode() {
51
- return this.nodeMarker.parentNode;
52
- }
53
50
 
54
51
  // These are set after an expression is assigned:
55
52
 
56
-
57
53
  /** @type {NodeGroup} */
58
54
  parentNg;
59
55
 
@@ -61,8 +57,6 @@ export default class ExprPath {
61
57
  nodeGroups = [];
62
58
 
63
59
 
64
-
65
-
66
60
  // Caches to make things faster
67
61
 
68
62
  /**
@@ -70,50 +64,23 @@ export default class ExprPath {
70
64
  * @type {Node[]} Cached result of getNodes() */
71
65
  nodesCache;
72
66
 
73
- // What are these?
67
+ /**
68
+ * @type {int} Index of nodeBefore among its parentNode's children. */
74
69
  nodeBeforeIndex;
70
+
71
+ /**
72
+ * @type {int[]} Path to the node marker, in reverse for performance reasons. */
75
73
  nodeMarkerPath;
76
74
 
77
- // TODO: Keep this cached?
78
- expr;
79
-
80
- // for debugging
81
- //#IFDEV
82
- parentIndex;
83
- //#ENDIF
84
75
 
85
76
  /**
86
77
  * @param nodeBefore {Node}
87
78
  * @param nodeMarker {?Node}
88
- * @param type {string}
79
+ * @param type {PathType}
89
80
  * @param attrName {?string}
90
81
  * @param attrValue {string[]} */
91
82
  constructor(nodeBefore, nodeMarker, type=PathType.Content, attrName=null, attrValue=null) {
92
83
 
93
- //#IFDEV
94
- /*
95
- Object.defineProperty(this, 'debug', {
96
- get() {
97
- return [
98
- `parentNode: ${this.nodeBefore.parentNode?.tagName?.toLowerCase()}`,
99
- 'nodes:',
100
- ...setIndent(this.getNodes().map(item => {
101
- if (item instanceof Node)
102
- return item.outerHTML || item.textContent
103
- else if (item instanceof NodeGroup)
104
- return item.debug
105
- }), 1).flat()
106
- ]
107
- }
108
- })
109
-
110
- Object.defineProperty(this, 'debugNodes', {
111
- get: () =>
112
- this.getNodes()
113
- })
114
- */
115
- //#ENDIF
116
-
117
84
  // If path is a node.
118
85
  this.nodeBefore = nodeBefore;
119
86
  this.nodeMarker = nodeMarker;
@@ -125,25 +92,142 @@ export default class ExprPath {
125
92
  }
126
93
 
127
94
  /**
95
+ * Apply any type of expression.
96
+ * This calls other apply functions.
97
+ *
98
+ * One very messy part of this function is that it may apply multiple expressions if they're all part
99
+ * of the same attribute value.
100
+ *
101
+ * We should modify path.applyValueAttrib so it stores the procssed parts and then only calls
102
+ * setAttribute() once all the pieces are in place.
128
103
  *
104
+ * @param expr {Expr}
105
+ * @param exprs {Expr[]}
106
+ * @param exprIndex {int}
107
+ * @param componentExprs {object}
108
+ * @returns {int} */
109
+ apply(expr, exprs=null, exprIndex=0, componentExprs={}) {
110
+ switch (this.type) {
111
+ case 1: // PathType.Content:
112
+ this.applyNodes(expr);
113
+ break;
114
+ case 2: // PathType.Multiple:
115
+ this.applyMultipleAttribs(this.nodeMarker, expr);
116
+ break;
117
+ case 5: // PathType.Comment:
118
+ // Expressions inside Html comments. Deliberately empty because we won't waste time updating them.
119
+ break;
120
+ case 6: // PathType.Event:
121
+ this.applyEventAttrib(this.nodeMarker, expr, this.parentNg.rootNg.root);
122
+ break;
123
+ default:
124
+ if (this.type === 4 /*PathType.Component*/ && this.nodeMarker !== this.parentNg.rootNg.root)
125
+ componentExprs[this.attrName] = expr;
126
+ else {
127
+ // One attribute value may have multiple expressions. Here we apply them all at once.
128
+ exprIndex = this.applyValueAttrib(this.nodeMarker, exprs || [expr], exprIndex);
129
+ }
130
+ break;
131
+ }
132
+
133
+ return exprIndex;
134
+ }
135
+
136
+ /**
137
+ * Insert/replace the nodes created by a single expression.
138
+ * Called by applyExprs()
139
+ * This function is recursive, as the functions it calls also call it.
140
+ * @param expr {Expr}
141
+ * @return {Node[]} New Nodes created. */
142
+ applyNodes(expr) {
143
+ let path = this;
144
+
145
+ /*#IFDEV*/path.verify();/*#ENDIF*/
146
+
147
+ /** @type {(Node|NodeGroup|Expr)[]} */
148
+ let newNodes = [];
149
+ let oldNodeGroups = path.nodeGroups;
150
+ /*#IFDEV*/assert(!oldNodeGroups.includes(null))/*#ENDIF*/
151
+ let secondPass = []; // indices
152
+
153
+ path.nodeGroups = []; // Reset before applyExact and the code below rebuilds it.
154
+ path.applyExact(expr, newNodes, secondPass);
155
+
156
+ this.existingTextNodes = null;
157
+
158
+ // TODO: Create an array of old vs Nodes and NodeGroups together.
159
+ // If they're all the same, skip the next steps.
160
+ // Or calculate it in the loop above as we go? Have a path.lastNodeGroups property?
161
+
162
+ // Second pass to find close-match NodeGroups.
163
+ let flatten = false;
164
+ if (secondPass.length) {
165
+ for (let [nodesIndex, ngIndex] of secondPass) {
166
+ let ng = path.getNodeGroup(newNodes[nodesIndex], false);
167
+
168
+ let ngNodes = ng.getNodes();
169
+
170
+ /*#IFDEV*/assert(!(newNodes[nodesIndex] instanceof NodeGroup))/*#ENDIF*/
171
+
172
+ if (ngNodes.length === 1) // flatten manually so we can skip flattening below.
173
+ newNodes[nodesIndex] = ngNodes[0];
174
+
175
+ else {
176
+ newNodes[nodesIndex] = ngNodes;
177
+ flatten = true;
178
+ }
179
+ path.nodeGroups[ngIndex] = ng;
180
+ }
181
+
182
+ if (flatten)
183
+ newNodes = newNodes.flat(); // Only if second pass happens.
184
+ }
185
+
186
+ /*#IFDEV*/assert(!path.nodeGroups.includes(null))/*#ENDIF*/
187
+
188
+
189
+
190
+ let oldNodes = path.getNodes();
191
+
192
+
193
+ // This pre-check makes it a few percent faster?
194
+ let same = arraySame(oldNodes, newNodes);
195
+ if (!same) {
196
+
197
+ path.nodesCache = newNodes; // Replaces value set by path.getNodes()
198
+
199
+ if (this.parentNg.parentPath)
200
+ this.parentNg.parentPath.clearNodesCache();
201
+
202
+ // Fast clear method
203
+ let isNowEmpty = oldNodes.length && !newNodes.length;
204
+ if (!isNowEmpty || !path.fastClear())
205
+
206
+ // Rearrange nodes.
207
+ udomdiff(path.nodeMarker.parentNode, oldNodes, newNodes, path.nodeMarker)
208
+
209
+ Util.saveOrphans(oldNodeGroups, oldNodes);
210
+ }
211
+
212
+ // Must happen after second pass.
213
+ path.freeNodeGroups();
214
+
215
+ /*#IFDEV*/path.verify();/*#ENDIF*/
216
+ }
217
+
218
+
219
+
220
+ /**
221
+ * Apply Nodes that are an exact match.
129
222
  * @param expr {Template|Node|Array|function|*}
130
223
  * @param newNodes {(Node|Template)[]}
131
- * @param secondPass {Array} Locations within newNodes to evaluate later. */
132
- apply(expr, newNodes, secondPass) {
224
+ * @param secondPass {Array} Locations within newNodes to evaluate later. */
225
+ applyExact(expr, newNodes, secondPass) {
133
226
 
134
227
  if (expr instanceof Template) {
135
- expr.nodegroup = this.parentNg; // All tests pass w/o this.
136
-
137
- let ng = this.parentNg.manager.getNodeGroup(expr, true);
138
-
139
228
 
229
+ let ng = this.getNodeGroup(expr, true);
140
230
  if (ng) {
141
- //#IFDEV
142
- // Make sure the nodeCache of the ExprPath we took it from is sitll valid.
143
- if (ng.parentPath)
144
- ng.parentPath.verify();
145
- //#ENDIF
146
-
147
231
 
148
232
  // TODO: Track ranges of changed nodes and only pass those to udomdiff?
149
233
  // But will that break the swap benchmark?
@@ -151,7 +235,7 @@ export default class ExprPath {
151
235
  this.nodeGroups.push(ng);
152
236
  }
153
237
 
154
- // If expression, evaluate later to find partial match.
238
+ // If expression, mark it to be evaluated later in ExprPath.apply() to find partial match.
155
239
  else {
156
240
  secondPass.push([newNodes.length, this.nodeGroups.length])
157
241
  newNodes.push(expr)
@@ -169,25 +253,26 @@ export default class ExprPath {
169
253
  newNodes.push(expr);
170
254
  }
171
255
 
256
+ // Arrays and functions.
257
+ // I tried iterating over the result of a generator function to avoid this recursion and simplify the code,
258
+ // but that consistently made the js-framework-benchmarks a few percentage points slower.
172
259
  else if (Array.isArray(expr))
173
260
  for (let subExpr of expr)
174
- this.apply(subExpr, newNodes, secondPass)
261
+ this.applyExact(subExpr, newNodes, secondPass);
175
262
 
176
263
  else if (typeof expr === 'function') {
177
- //expr = watchFunction(expr, this.parentNg.manager); // part of watch2() experiment.
178
-
179
264
  Globals.currentExprPath = [this, expr]; // Used by watch3()
180
265
  let result = expr();
181
266
  Globals.currentExprPath = null;
182
267
 
183
- this.apply(result, newNodes, secondPass)
268
+ this.applyExact(result, newNodes, secondPass);
184
269
  }
185
270
 
186
271
  // Text
187
272
  else {
188
273
  // Convert falsy values (but not 0) to empty string.
189
274
  // Convert numbers to string so they compare the same.
190
- let text = (expr === undefined || expr === false || expr === null) ? '' : expr + '';
275
+ let text = (expr === undefined || expr === false || expr === null) ? '' : (expr + '');
191
276
 
192
277
  // Fast path for updating the text of a single text node.
193
278
  let first = this.nodeBefore.nextSibling;
@@ -207,7 +292,7 @@ export default class ExprPath {
207
292
  if (idx !== -1)
208
293
  newNodes.push(...this.existingTextNodes.splice(idx, 1))
209
294
  else
210
- newNodes.push(this.parentNode.ownerDocument.createTextNode(text));
295
+ newNodes.push(this.nodeMarker.ownerDocument.createTextNode(text));
211
296
  }
212
297
  }
213
298
  }
@@ -244,86 +329,103 @@ export default class ExprPath {
244
329
  /**
245
330
  * Handle attributes for event binding, such as:
246
331
  * onclick=${(e, el) => this.doSomething(el, 'meow')}
247
- * onclick=${[this.doSomething, 'meow']}
332
+ * oninput=${[this.doSomething, 'meow']}
248
333
  * onclick=${[this, 'doSomething', 'meow']}
249
334
  *
250
335
  * @param node
251
336
  * @param expr
252
337
  * @param root */
253
338
  applyEventAttrib(node, expr, root) {
254
- /*#IFDEV*/assert(this.type === PathType.Value || this.type === PathType.Component);/*#ENDIF*/
339
+ /*#IFDEV*/
340
+ assert(this.type === PathType.Event/* || this.type === PathType.Component*/);
341
+ assert(root instanceof HTMLElement);
342
+ /*#ENDIF*/
255
343
 
256
- let eventName = this.attrName.slice(2);
344
+ let eventName = this.attrName.slice(2); // remove "on-" prefix.
257
345
  let func;
258
346
 
259
347
  // Convert array to function.
260
- // TODO: This doesn't work for [this, 'doSomething', 'meow']
261
348
  let args = [];
262
349
  if (Array.isArray(expr)) {
263
- for (let i=0; i<expr.length; i++)
264
- if (typeof expr[i] === 'function') {
265
- func = expr[i];
266
- args = expr.slice(i+1);
267
- break;
268
- }
269
350
 
351
+ // oninput=${[this.doSomething, 'meow']}
352
+ if (typeof expr[0] === 'function') {
353
+ func = expr[0];
354
+ args = expr.slice(1);
355
+ }
356
+
357
+ // Undocumented.
270
358
  // oninput=${[this, 'value']}
271
- if (!func) {
272
- func = setValue
359
+ else {
360
+ func = setValue;
273
361
  args = [expr[0], expr.slice(1), node]
274
362
  node.value = delve(expr[0], expr.slice(1));
363
+ // root.render(); // TODO: This causes infinite recursion.
275
364
  }
276
365
  }
277
366
  else
278
367
  func = expr;
279
368
 
280
- let eventKey = getObjectId(node) + eventName;
281
- let [existing, existingBound] = nodeEvents[eventKey] || [];
282
- nodeEventArgs[eventKey] = args; // TODO: Put this in nodeEvents[]
369
+ let nodeEvents = Globals.nodeEvents.get(node);
370
+ if (!nodeEvents) {
371
+ nodeEvents = {[eventName]: new Array(3)};
372
+ Globals.nodeEvents.set(node, nodeEvents);
373
+ }
374
+ let nodeEvent = nodeEvents[eventName];
283
375
 
284
376
 
285
- if (existing !== func) {
377
+
378
+ // If function has changed, remove and rebind the event.
379
+ if (nodeEvent[0] !== func) {
380
+ let [existing, existingBound, _] = nodeEvent;
286
381
  if (existing)
287
382
  node.removeEventListener(eventName, existingBound);
288
383
 
289
384
  let originalFunc = func;
290
385
 
291
386
  // BoundFunc sets the "this" variable to be the current Solarite component.
292
- let boundFunc = event => originalFunc.call(root, ...args, event, node);
387
+ let boundFunc = (event) => {
388
+ let args = nodeEvent[2];
389
+ return originalFunc.call(root, ...args, event, node);
390
+ }
293
391
 
294
392
  // Save both the original and bound functions.
295
393
  // Original so we can compare it against a newly assigned function.
296
394
  // Bound so we can use it with removeEventListner().
297
- nodeEvents[eventKey] = [originalFunc, boundFunc];
395
+ nodeEvent[0] = originalFunc;
396
+ nodeEvent[1] = boundFunc;
298
397
 
299
398
  node.addEventListener(eventName, boundFunc);
300
399
 
301
- // TODO: classic event attribs:
400
+ // TODO: classic event attribs?
302
401
  //el[attr.name] = e => // e.g. el.onclick = ...
303
402
  // (new Function('event', 'el', attr.value)).bind(this.manager.rootEl)(e, el) // put "event", "el", and "this" in scope for the event code.
304
403
  }
404
+
405
+ // Otherwise just update the args to the function.
406
+ nodeEvents[eventName][2] = args;
305
407
  }
306
408
 
307
409
  applyValueAttrib(node, exprs, exprIndex) {
308
410
  let expr = exprs[exprIndex];
309
-
310
- // Array for form element data binding.
311
- // TODO: This never worked, and was moved to applyEventAttrib.
312
- // let isArrayValue = Array.isArray(expr);
313
- // if (isArrayValue && expr.length >= 2 && !expr.slice(1).find(v => !['string', 'number'].includes(typeof v))) {
314
- // node.value = delve(expr[0], expr.slice(1));
315
- // node.addEventListener('input', e => {
316
- // delve(expr[0], expr.slice(1), node.value) // TODO: support other properties like checked
317
- // });
318
- // }
319
411
 
320
412
  // Values to toggle an attribute
321
413
  if (!this.attrValue && (expr === false || expr === null || expr === undefined))
322
414
  node.removeAttribute(this.attrName);
323
-
415
+
324
416
  else if (!this.attrValue && expr === true)
325
417
  node.setAttribute(this.attrName, '');
326
418
 
419
+ // Passing a path to the value attribute.
420
+ // This same logic is in NodeGroup.createNewComponent() for components.
421
+ else if ((this.attrName === 'value' || this.attrName === 'data-value') && Util.isPath(expr)) {
422
+ let [obj, path] = [expr[0], expr.slice(1)];
423
+ node.value = delve(obj, path);
424
+ node.addEventListener('input', () => {
425
+ delve(obj, path, Util.getInputValue(node));
426
+ }, true); // We use capture so we update the values before other events added by the user.
427
+ }
428
+
327
429
  // Regular attribute
328
430
  else {
329
431
  let value = [];
@@ -339,21 +441,25 @@ export default class ExprPath {
339
441
  exprIndex--;
340
442
  }
341
443
  }
342
-
343
444
  exprIndex ++;
344
445
  }
345
446
  else
346
447
  value.unshift(expr);
347
448
 
348
449
  let joinedValue = value.join('')
349
- node.setAttribute(this.attrName, joinedValue);
450
+
451
+ // Only update attributes if the value has changed.
452
+ // The .value property is special. If it changes we don't update the attribute.
453
+ let oldVal = this.attrName === 'value' ? node.value : node.getAttribute(this.attrName);
454
+ if (oldVal !== joinedValue) {
455
+ node.setAttribute(this.attrName, joinedValue);
456
+ }
350
457
 
351
458
  // This is needed for setting input.value, .checked, option.selected, etc.
352
459
  // But in some cases setting the attribute is enough. such as div.setAttribute('title') updates div.title.
353
460
  // TODO: How to tell which is which?
354
461
  if (this.attrName in node)
355
462
  node[this.attrName] = joinedValue;
356
-
357
463
  }
358
464
 
359
465
  return exprIndex;
@@ -363,31 +469,35 @@ export default class ExprPath {
363
469
  /**
364
470
  *
365
471
  * @param newRoot {HTMLElement}
472
+ * @param pathOffset {int}
366
473
  * @return {ExprPath} */
367
- clone(newRoot) {
474
+ clone(newRoot, pathOffset=0) {
368
475
  /*#IFDEV*/this.verify();/*#ENDIF*/
369
476
 
370
- // Resolve node paths.
477
+ // Resolve node paths.
371
478
  let nodeMarker, nodeBefore;
372
- let root = newRoot;
373
- let path = this.nodeMarkerPath;
374
- for (let i=path.length-1; i>0; i--)
375
- root = root.childNodes[path[i]];
479
+ let root = newRoot;
480
+ let path = pathOffset ? this.nodeMarkerPath.slice(0, -pathOffset) : this.nodeMarkerPath;
481
+ for (let i=path.length-1; i>0; i--) // Resolve the path.
482
+ root = root.childNodes[path[i]];
376
483
  let childNodes = root.childNodes;
377
- nodeMarker = childNodes[path[0]]
378
- if (this.nodeBefore)
379
- nodeBefore = childNodes[this.nodeBeforeIndex];
484
+
485
+ nodeMarker = path.length ? childNodes[path[0]] : newRoot;
486
+ if (this.nodeBefore)
487
+ nodeBefore = childNodes[this.nodeBeforeIndex];
380
488
 
381
489
  let result = new ExprPath(nodeBefore, nodeMarker, this.type, this.attrName, this.attrValue);
382
490
 
383
491
  //#IFDEV
492
+ result.nodeMarker.exprPath = result;
493
+ if (result.nodeBefore)
494
+ result.nodeBefore.prevExprPath = result;
384
495
  result.verify();
385
- result.parentIndex = this.parentIndex; // used for debugging?
386
496
  //#ENDIF
387
497
 
388
498
  return result;
389
499
  }
390
-
500
+
391
501
  /**
392
502
  * Clear the nodeCache of this ExprPath, as well as all parent and child ExprPaths that
393
503
  * share the same DOM parent node.
@@ -395,19 +505,19 @@ export default class ExprPath {
395
505
  * TODO: Is recursive clearing ever necessary? */
396
506
  clearNodesCache() {
397
507
  let path = this;
398
-
508
+
399
509
  // Clear cache parent ExprPaths that have the same parentNode
400
- let parentNode = this.parentNode;
401
- while (path && path.parentNode === parentNode) {
510
+ let parentNode = this.nodeMarker.parentNode;
511
+ while (path && path.nodeMarker.parentNode === parentNode) {
402
512
  path.nodesCache = null;
403
513
  path = path.parentNg?.parentPath
404
-
514
+
405
515
  // If stuck in an infinite loop here, the problem is likely due to Template hash colisions.
406
516
  // Which cause one path to be the descendant of itself, creating a cycle.
407
517
  }
408
-
518
+
409
519
  function clearChildNodeCache(path) {
410
-
520
+
411
521
  // Clear cache of child ExprPaths that have the same parentNode
412
522
  for (let ng of path.nodeGroups) {
413
523
  if (ng) // Can be null from apply()'s push(null) call.
@@ -419,8 +529,9 @@ export default class ExprPath {
419
529
  }
420
530
  }
421
531
  }
422
-
423
- clearChildNodeCache(this);
532
+
533
+ // Commented out on Sep 30, 2024 b/c it was making the benchmark never finish when adding 10k rows.
534
+ //clearChildNodeCache(this);
424
535
  }
425
536
 
426
537
 
@@ -433,46 +544,48 @@ export default class ExprPath {
433
544
 
434
545
  // If parent is the only child of the grandparent, replace the whole parent.
435
546
  // And if it has no siblings, it's not created by a NodeGroup/path.
436
- let grandparent = parent.parentNode
437
- if (grandparent && parent === grandparent.firstChild && parent === grandparent.lastChild && !parent.hasAttribute('id')) {
438
- let replacement = document.createElement(parent.tagName)
439
- replacement.append(this.nodeBefore, this.nodeMarker)
440
- for (let attrib of parent.attributes)
441
- replacement.setAttribute(attrib.name, attrib.value)
442
- parent.replaceWith(replacement)
443
- }
444
- else {
547
+ // Commented out because this will break any references.
548
+ // And because I don't see much performance difference.
549
+ // let grandparent = parent.parentNode
550
+ // if (grandparent && parent === grandparent.firstChild && parent === grandparent.lastChild && !parent.hasAttribute('id')) {
551
+ // let replacement = document.createElement(parent.tagName)
552
+ // replacement.append(this.nodeBefore, this.nodeMarker)
553
+ // for (let attrib of parent.attributes)
554
+ // replacement.setAttribute(attrib.name, attrib.value)
555
+ // parent.replaceWith(replacement)
556
+ // }
557
+ // else {
445
558
  parent.innerHTML = ''; // Faster than calling .removeChild() a thousand times.
446
559
  parent.append(this.nodeBefore, this.nodeMarker)
447
- }
560
+ //}
448
561
  return true;
449
562
  }
450
563
  return false;
451
564
  }
452
-
565
+
453
566
  /**
454
567
  * @return {(Node|HTMLElement)[]} */
455
568
  getNodes() {
456
-
569
+
457
570
  // Why doesn't this work?
458
571
  // let result2 = [];
459
572
  // for (let ng of this.nodeGroups)
460
573
  // result2.push(...ng.getNodes())
461
574
  // return result2;
462
-
463
-
575
+
576
+
464
577
  let result
465
578
 
466
579
  // This shaves about 5ms off the partialUpdate benchmark.
467
- /*result = this.nodesCache;
580
+ result = this.nodesCache;
468
581
  if (result) {
469
-
582
+
470
583
  //#IFDEV
471
584
  this.checkNodesCache();
472
585
  //#ENDIF
473
-
586
+
474
587
  return result
475
- }*/
588
+ }
476
589
 
477
590
  result = [];
478
591
  let current = this.nodeBefore.nextSibling;
@@ -489,13 +602,100 @@ export default class ExprPath {
489
602
  getParentNode() { // Same as this.parentNode
490
603
  return this.nodeMarker.parentNode
491
604
  }
492
-
493
- removeNodeGroup(ng) {
494
- let idx = this.nodeGroups.indexOf(ng);
495
- /*#IFDEV*/assert(idx !== -1);/*#ENDIF*/
496
- this.nodeGroups.splice(idx);
497
- ng.parentPath = null;
498
- this.clearNodesCache();
605
+
606
+ /**
607
+ * Get an unused NodeGroup that matches the template's html and expressions (exact=true)
608
+ * or at least the html (exact=false).
609
+ * Remove it from nodeGroupsFree if it exists, or create it if not.
610
+ * Then add it to nodeGroupsInUse.
611
+ *
612
+ * @param template {Template}
613
+ * @param exact {boolean}
614
+ * If true, return an exact match, or null.
615
+ * If false, either find a match for the template's html and then apply the template's expressions,
616
+ * or createa new NodeGroup from the template.
617
+ * @return {NodeGroup} */
618
+ getNodeGroup(template, exact=true) {
619
+ //if (exact && this.nodeGroupsFree.isEmpty())
620
+ // return null;
621
+
622
+ let result;
623
+
624
+ if (exact) {
625
+ result = this.nodeGroupsFree.delete(template.getExactKey());
626
+ if (result) // also delete the matching close key.
627
+ this.nodeGroupsFree.delete(template.getCloseKey(), result);
628
+ else
629
+ return null;
630
+ }
631
+
632
+ // Find a close match.
633
+ // This is a match that has matching html, but different expressions applied.
634
+ // We can then apply the expressions to make it an exact match.
635
+ else {
636
+ result = this.nodeGroupsFree.delete(template.getCloseKey())
637
+ if (result) {
638
+ /*#IFDEV*/assert(result.exactKey);/*#ENDIF*/
639
+ this.nodeGroupsFree.delete(result.exactKey, result);
640
+
641
+ // Update this close match with the new expression values.
642
+ result.applyExprs(template.exprs);
643
+ result.exactKey = template.getExactKey(); // TODO: Should this be set elsewhere?
644
+ }
645
+ }
646
+
647
+ if (!result)
648
+ result = new NodeGroup(template, this);
649
+
650
+ // old:
651
+ this.nodeGroupsInUse.push(result);
652
+
653
+ // new:
654
+ // let ngiu = this.nodeGroupsInUse;
655
+ // ngiu.add(result.exactKey, result);
656
+ // ngiu.add(result.closeKey, result);
657
+
658
+ /*#IFDEV*/assert(result.parentPath);/*#ENDIF*/
659
+ return result;
660
+ }
661
+
662
+
663
+ /**
664
+ * Used with getNodeGroup() and freeNodeGroups().
665
+ * TODO: Use an array of WeakRef so the gc can collect them?
666
+ * TODO: Put items back in nodeGroupsInUse after applyExpr() is called, not before.
667
+ * @type {NodeGroup[]} */
668
+ nodeGroupsInUse = [];
669
+
670
+ /** @type {MultiValueMap<key:string, value:NodeGroup>} */
671
+ //nodeGroupsInUse = new MultiValueMap();
672
+
673
+ /**
674
+ * Used with getNodeGroup() and freeNodeGroups().
675
+ * Each NodeGroup is here twice, once under an exact key, and once under the close key.
676
+ * @type {MultiValueMap<key:string, value:NodeGroup>} */
677
+ nodeGroupsFree = new MultiValueMap();
678
+
679
+
680
+ /**
681
+ * Move everything from this.nodeGroupsInUse to this.nodeGroupsFree.
682
+ * TODO: this could run as needed in getNodeGroup? */
683
+ freeNodeGroups() {
684
+ // old:
685
+ let ngf = this.nodeGroupsFree;
686
+ for (let ng of this.nodeGroupsInUse) {
687
+ ngf.add(ng.exactKey, ng);
688
+ ngf.add(ng.closeKey, ng);
689
+ }
690
+ this.nodeGroupsInUse = [];
691
+
692
+ // new:
693
+ // for (let key in this.nodeGroupsFree.data)
694
+ // for (let item of this.nodeGroupsFree.data[key])
695
+ // this.nodeGroupsInUse.add(key, item);
696
+ //
697
+ // this.nodeGroupsFree = this.nodeGroupsInUse;
698
+ // this.nodeGroupsInUse = new MultiValueMap();
499
699
  }
500
700
 
501
701
  //#IFDEV
@@ -570,7 +770,7 @@ export default class ExprPath {
570
770
  nodes.push(current)
571
771
  current = current.nextSibling
572
772
  }
573
- assert(findArrayDiff(this.nodesCache, nodes) === false);
773
+ assert(arraySame(this.nodesCache, nodes) === true);
574
774
  }
575
775
  }
576
776
  //#ENDIF
@@ -590,26 +790,27 @@ function setValue(root, path, node) {
590
790
  val = parseFloat(val);
591
791
 
592
792
  delve(root, path, val);
593
-
594
- //this.render();
595
793
  }
596
794
 
597
- /** @enum {string} */
795
+ /** @enum {int} */
598
796
  export const PathType = {
599
797
  /** Child of a node */
600
- Content: 'content',
798
+ Content: 1,
601
799
 
602
800
  /** One or more whole attributes */
603
- Multiple: 'attrName',
801
+ Multiple: 2,
604
802
 
605
803
  /** Value of an attribute. */
606
- Value: 'attrValue',
804
+ Value: 3,
607
805
 
608
806
  /** Value of an attribute being passed to a component. */
609
- Component: 'component',
807
+ Component: 4,
610
808
 
611
809
  /** Expressions inside Html comments. */
612
- Comment: 'comment',
810
+ Comment: 5,
811
+
812
+ /** Value of an attribute. */
813
+ Event: 6,
613
814
  }
614
815
 
615
816
 
@@ -638,6 +839,3 @@ export function resolveNodePath(root, path) {
638
839
  }
639
840
 
640
841
 
641
- // TODO: Memory from this is never freed. Use a WeakMap<Node, Object<eventName:string, function[]>>
642
- let nodeEvents = {};
643
- let nodeEventArgs = {};