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
@@ -1,11 +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";
5
+ import Template from "./Template.js";
6
+ import Globals from "./Globals.js";
7
+ import MultiValueMap from "../util/MultiValueMap.js";
8
+ import udomdiff from "./udomdiff.js";
6
9
 
7
10
  /**
8
- * 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.
9
12
  * Path is only valid until the expressions before it are evaluated.
10
13
  * TODO: Make this based on parent and node instead of path? */
11
14
  export default class ExprPath {
@@ -44,14 +47,9 @@ export default class ExprPath {
44
47
  * @type {Node|HTMLElement} */
45
48
  nodeMarker;
46
49
 
47
- /** @deprecated */
48
- get parentNode() {
49
- return this.nodeMarker.parentNode;
50
- }
51
50
 
52
51
  // These are set after an expression is assigned:
53
52
 
54
-
55
53
  /** @type {NodeGroup} */
56
54
  parentNg;
57
55
 
@@ -59,8 +57,6 @@ export default class ExprPath {
59
57
  nodeGroups = [];
60
58
 
61
59
 
62
-
63
-
64
60
  // Caches to make things faster
65
61
 
66
62
  /**
@@ -68,50 +64,23 @@ export default class ExprPath {
68
64
  * @type {Node[]} Cached result of getNodes() */
69
65
  nodesCache;
70
66
 
71
- // What are these?
67
+ /**
68
+ * @type {int} Index of nodeBefore among its parentNode's children. */
72
69
  nodeBeforeIndex;
73
- nodeMarkerPath;
74
70
 
75
- // TODO: Keep this cached?
76
- expr;
71
+ /**
72
+ * @type {int[]} Path to the node marker, in reverse for performance reasons. */
73
+ nodeMarkerPath;
77
74
 
78
- // for debugging
79
- //#IFDEV
80
- parentIndex;
81
- //#ENDIF
82
75
 
83
76
  /**
84
77
  * @param nodeBefore {Node}
85
78
  * @param nodeMarker {?Node}
86
- * @param type {string}
79
+ * @param type {PathType}
87
80
  * @param attrName {?string}
88
81
  * @param attrValue {string[]} */
89
82
  constructor(nodeBefore, nodeMarker, type=PathType.Content, attrName=null, attrValue=null) {
90
83
 
91
- //#IFDEV
92
- /*
93
- Object.defineProperty(this, 'debug', {
94
- get() {
95
- return [
96
- `parentNode: ${this.nodeBefore.parentNode?.tagName?.toLowerCase()}`,
97
- 'nodes:',
98
- ...setIndent(this.getNodes().map(item => {
99
- if (item instanceof Node)
100
- return item.outerHTML || item.textContent
101
- else if (item instanceof NodeGroup)
102
- return item.debug
103
- }), 1).flat()
104
- ]
105
- }
106
- })
107
-
108
- Object.defineProperty(this, 'debugNodes', {
109
- get: () =>
110
- this.getNodes()
111
- })
112
- */
113
- //#ENDIF
114
-
115
84
  // If path is a node.
116
85
  this.nodeBefore = nodeBefore;
117
86
  this.nodeMarker = nodeMarker;
@@ -122,6 +91,212 @@ export default class ExprPath {
122
91
  this.attrNames = new Set();
123
92
  }
124
93
 
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.
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.
222
+ * @param expr {Template|Node|Array|function|*}
223
+ * @param newNodes {(Node|Template)[]}
224
+ * @param secondPass {Array} Locations within newNodes to evaluate later. */
225
+ applyExact(expr, newNodes, secondPass) {
226
+
227
+ if (expr instanceof Template) {
228
+
229
+ let ng = this.getNodeGroup(expr, true);
230
+ if (ng) {
231
+
232
+ // TODO: Track ranges of changed nodes and only pass those to udomdiff?
233
+ // But will that break the swap benchmark?
234
+ newNodes.push(...ng.getNodes());
235
+ this.nodeGroups.push(ng);
236
+ }
237
+
238
+ // If expression, mark it to be evaluated later in ExprPath.apply() to find partial match.
239
+ else {
240
+ secondPass.push([newNodes.length, this.nodeGroups.length])
241
+ newNodes.push(expr)
242
+ this.nodeGroups.push(null); // placeholder
243
+ }
244
+ }
245
+
246
+ // Node created by an expression.
247
+ else if (expr instanceof Node) {
248
+
249
+ // DocumentFragment created by an expression.
250
+ if (expr instanceof DocumentFragment)
251
+ newNodes.push(...expr.childNodes);
252
+ else
253
+ newNodes.push(expr);
254
+ }
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.
259
+ else if (Array.isArray(expr))
260
+ for (let subExpr of expr)
261
+ this.applyExact(subExpr, newNodes, secondPass);
262
+
263
+ else if (typeof expr === 'function') {
264
+ Globals.currentExprPath = [this, expr]; // Used by watch3()
265
+ let result = expr();
266
+ Globals.currentExprPath = null;
267
+
268
+ this.applyExact(result, newNodes, secondPass);
269
+ }
270
+
271
+ // Text
272
+ else {
273
+ // Convert falsy values (but not 0) to empty string.
274
+ // Convert numbers to string so they compare the same.
275
+ let text = (expr === undefined || expr === false || expr === null) ? '' : (expr + '');
276
+
277
+ // Fast path for updating the text of a single text node.
278
+ let first = this.nodeBefore.nextSibling;
279
+ if (first.nodeType === 3 && first.nextSibling === this.nodeMarker && !newNodes.includes(first)) {
280
+ if (first.textContent !== text)
281
+ first.textContent = text;
282
+
283
+ newNodes.push(first);
284
+ }
285
+
286
+ else {
287
+ // TODO: Optimize this into a Set or Map or something?
288
+ if (!this.existingTextNodes)
289
+ this.existingTextNodes = this.getNodes().filter(n => n.nodeType === 3);
290
+
291
+ let idx = this.existingTextNodes.findIndex(n => n.textContent === text);
292
+ if (idx !== -1)
293
+ newNodes.push(...this.existingTextNodes.splice(idx, 1))
294
+ else
295
+ newNodes.push(this.nodeMarker.ownerDocument.createTextNode(text));
296
+ }
297
+ }
298
+ }
299
+
125
300
  applyMultipleAttribs(node, expr) {
126
301
  /*#IFDEV*/assert(this.type === PathType.Multiple);/*#ENDIF*/
127
302
 
@@ -154,86 +329,103 @@ export default class ExprPath {
154
329
  /**
155
330
  * Handle attributes for event binding, such as:
156
331
  * onclick=${(e, el) => this.doSomething(el, 'meow')}
157
- * onclick=${[this.doSomething, 'meow']}
332
+ * oninput=${[this.doSomething, 'meow']}
158
333
  * onclick=${[this, 'doSomething', 'meow']}
159
334
  *
160
335
  * @param node
161
336
  * @param expr
162
337
  * @param root */
163
338
  applyEventAttrib(node, expr, root) {
164
- /*#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*/
165
343
 
166
- let eventName = this.attrName.slice(2);
344
+ let eventName = this.attrName.slice(2); // remove "on-" prefix.
167
345
  let func;
168
346
 
169
347
  // Convert array to function.
170
- // TODO: This doesn't work for [this, 'doSomething', 'meow']
171
348
  let args = [];
172
349
  if (Array.isArray(expr)) {
173
- for (let i=0; i<expr.length; i++)
174
- if (typeof expr[i] === 'function') {
175
- func = expr[i];
176
- args = expr.slice(i+1);
177
- break;
178
- }
179
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.
180
358
  // oninput=${[this, 'value']}
181
- if (!func) {
182
- func = setValue
359
+ else {
360
+ func = setValue;
183
361
  args = [expr[0], expr.slice(1), node]
184
362
  node.value = delve(expr[0], expr.slice(1));
363
+ // root.render(); // TODO: This causes infinite recursion.
185
364
  }
186
365
  }
187
366
  else
188
367
  func = expr;
189
368
 
190
- let eventKey = getObjectId(node) + eventName;
191
- let [existing, existingBound] = nodeEvents[eventKey] || [];
192
- 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];
193
375
 
194
376
 
195
- if (existing !== func) {
377
+
378
+ // If function has changed, remove and rebind the event.
379
+ if (nodeEvent[0] !== func) {
380
+ let [existing, existingBound, _] = nodeEvent;
196
381
  if (existing)
197
382
  node.removeEventListener(eventName, existingBound);
198
383
 
199
384
  let originalFunc = func;
200
385
 
201
386
  // BoundFunc sets the "this" variable to be the current Solarite component.
202
- 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
+ }
203
391
 
204
392
  // Save both the original and bound functions.
205
393
  // Original so we can compare it against a newly assigned function.
206
394
  // Bound so we can use it with removeEventListner().
207
- nodeEvents[eventKey] = [originalFunc, boundFunc];
395
+ nodeEvent[0] = originalFunc;
396
+ nodeEvent[1] = boundFunc;
208
397
 
209
398
  node.addEventListener(eventName, boundFunc);
210
399
 
211
- // TODO: classic event attribs:
400
+ // TODO: classic event attribs?
212
401
  //el[attr.name] = e => // e.g. el.onclick = ...
213
402
  // (new Function('event', 'el', attr.value)).bind(this.manager.rootEl)(e, el) // put "event", "el", and "this" in scope for the event code.
214
403
  }
404
+
405
+ // Otherwise just update the args to the function.
406
+ nodeEvents[eventName][2] = args;
215
407
  }
216
408
 
217
409
  applyValueAttrib(node, exprs, exprIndex) {
218
410
  let expr = exprs[exprIndex];
219
-
220
- // Array for form element data binding.
221
- // TODO: This never worked, and was moved to applyEventAttrib.
222
- // let isArrayValue = Array.isArray(expr);
223
- // if (isArrayValue && expr.length >= 2 && !expr.slice(1).find(v => !['string', 'number'].includes(typeof v))) {
224
- // node.value = delve(expr[0], expr.slice(1));
225
- // node.addEventListener('input', e => {
226
- // delve(expr[0], expr.slice(1), node.value) // TODO: support other properties like checked
227
- // });
228
- // }
229
411
 
230
412
  // Values to toggle an attribute
231
413
  if (!this.attrValue && (expr === false || expr === null || expr === undefined))
232
414
  node.removeAttribute(this.attrName);
233
-
415
+
234
416
  else if (!this.attrValue && expr === true)
235
417
  node.setAttribute(this.attrName, '');
236
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
+
237
429
  // Regular attribute
238
430
  else {
239
431
  let value = [];
@@ -249,21 +441,25 @@ export default class ExprPath {
249
441
  exprIndex--;
250
442
  }
251
443
  }
252
-
253
444
  exprIndex ++;
254
445
  }
255
446
  else
256
447
  value.unshift(expr);
257
448
 
258
449
  let joinedValue = value.join('')
259
- 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
+ }
260
457
 
261
458
  // This is needed for setting input.value, .checked, option.selected, etc.
262
459
  // But in some cases setting the attribute is enough. such as div.setAttribute('title') updates div.title.
263
460
  // TODO: How to tell which is which?
264
461
  if (this.attrName in node)
265
462
  node[this.attrName] = joinedValue;
266
-
267
463
  }
268
464
 
269
465
  return exprIndex;
@@ -273,55 +469,58 @@ export default class ExprPath {
273
469
  /**
274
470
  *
275
471
  * @param newRoot {HTMLElement}
472
+ * @param pathOffset {int}
276
473
  * @return {ExprPath} */
277
- clone(newRoot) {
474
+ clone(newRoot, pathOffset=0) {
278
475
  /*#IFDEV*/this.verify();/*#ENDIF*/
279
476
 
280
- // Resolve node paths.
477
+ // Resolve node paths.
281
478
  let nodeMarker, nodeBefore;
282
- let root = newRoot;
283
- let path = this.nodeMarkerPath;
284
- for (let i=path.length-1; i>0; i--)
285
- 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]];
286
483
  let childNodes = root.childNodes;
287
- nodeMarker = childNodes[path[0]]
288
- if (this.nodeBefore)
289
- nodeBefore = childNodes[this.nodeBeforeIndex];
484
+
485
+ nodeMarker = path.length ? childNodes[path[0]] : newRoot;
486
+ if (this.nodeBefore)
487
+ nodeBefore = childNodes[this.nodeBeforeIndex];
290
488
 
291
489
  let result = new ExprPath(nodeBefore, nodeMarker, this.type, this.attrName, this.attrValue);
292
490
 
293
491
  //#IFDEV
492
+ result.nodeMarker.exprPath = result;
493
+ if (result.nodeBefore)
494
+ result.nodeBefore.prevExprPath = result;
294
495
  result.verify();
295
- result.parentIndex = this.parentIndex; // used for debugging?
296
496
  //#ENDIF
297
497
 
298
498
  return result;
299
499
  }
300
-
500
+
301
501
  /**
302
502
  * Clear the nodeCache of this ExprPath, as well as all parent and child ExprPaths that
303
503
  * share the same DOM parent node.
304
504
  *
305
- * TODO: Is recursive clearing ever necessary?
306
- */
505
+ * TODO: Is recursive clearing ever necessary? */
307
506
  clearNodesCache() {
308
507
  let path = this;
309
-
508
+
310
509
  // Clear cache parent ExprPaths that have the same parentNode
311
- let parentNode = this.parentNode;
312
- while (path && path.parentNode === parentNode) {
510
+ let parentNode = this.nodeMarker.parentNode;
511
+ while (path && path.nodeMarker.parentNode === parentNode) {
313
512
  path.nodesCache = null;
314
513
  path = path.parentNg?.parentPath
315
-
514
+
316
515
  // If stuck in an infinite loop here, the problem is likely due to Template hash colisions.
317
516
  // Which cause one path to be the descendant of itself, creating a cycle.
318
517
  }
319
-
518
+
320
519
  function clearChildNodeCache(path) {
321
-
520
+
322
521
  // Clear cache of child ExprPaths that have the same parentNode
323
522
  for (let ng of path.nodeGroups) {
324
- if (ng) // Can be null from applyOneExpr()'s push(null) call.
523
+ if (ng) // Can be null from apply()'s push(null) call.
325
524
  for (let path2 of ng.paths) {
326
525
  if (path2.type === PathType.Content && path2.parentNode === parentNode) {
327
526
  path2.nodesCache = null;
@@ -330,8 +529,9 @@ export default class ExprPath {
330
529
  }
331
530
  }
332
531
  }
333
-
334
- 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);
335
535
  }
336
536
 
337
537
 
@@ -344,46 +544,48 @@ export default class ExprPath {
344
544
 
345
545
  // If parent is the only child of the grandparent, replace the whole parent.
346
546
  // And if it has no siblings, it's not created by a NodeGroup/path.
347
- let grandparent = parent.parentNode
348
- if (grandparent && parent === grandparent.firstChild && parent === grandparent.lastChild && !parent.hasAttribute('id')) {
349
- let replacement = document.createElement(parent.tagName)
350
- replacement.append(this.nodeBefore, this.nodeMarker)
351
- for (let attrib of parent.attributes)
352
- replacement.setAttribute(attrib.name, attrib.value)
353
- parent.replaceWith(replacement)
354
- }
355
- 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 {
356
558
  parent.innerHTML = ''; // Faster than calling .removeChild() a thousand times.
357
559
  parent.append(this.nodeBefore, this.nodeMarker)
358
- }
560
+ //}
359
561
  return true;
360
562
  }
361
563
  return false;
362
564
  }
363
-
565
+
364
566
  /**
365
567
  * @return {(Node|HTMLElement)[]} */
366
568
  getNodes() {
367
-
569
+
368
570
  // Why doesn't this work?
369
571
  // let result2 = [];
370
572
  // for (let ng of this.nodeGroups)
371
573
  // result2.push(...ng.getNodes())
372
574
  // return result2;
373
-
374
-
575
+
576
+
375
577
  let result
376
578
 
377
579
  // This shaves about 5ms off the partialUpdate benchmark.
378
- /*result = this.nodesCache;
580
+ result = this.nodesCache;
379
581
  if (result) {
380
-
582
+
381
583
  //#IFDEV
382
584
  this.checkNodesCache();
383
585
  //#ENDIF
384
-
586
+
385
587
  return result
386
- }*/
588
+ }
387
589
 
388
590
  result = [];
389
591
  let current = this.nodeBefore.nextSibling;
@@ -400,13 +602,100 @@ export default class ExprPath {
400
602
  getParentNode() { // Same as this.parentNode
401
603
  return this.nodeMarker.parentNode
402
604
  }
403
-
404
- removeNodeGroup(ng) {
405
- let idx = this.nodeGroups.indexOf(ng);
406
- /*#IFDEV*/assert(idx !== -1);/*#ENDIF*/
407
- this.nodeGroups.splice(idx);
408
- ng.parentPath = null;
409
- 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();
410
699
  }
411
700
 
412
701
  //#IFDEV
@@ -481,7 +770,7 @@ export default class ExprPath {
481
770
  nodes.push(current)
482
771
  current = current.nextSibling
483
772
  }
484
- assert(findArrayDiff(this.nodesCache, nodes) === false);
773
+ assert(arraySame(this.nodesCache, nodes) === true);
485
774
  }
486
775
  }
487
776
  //#ENDIF
@@ -501,26 +790,27 @@ function setValue(root, path, node) {
501
790
  val = parseFloat(val);
502
791
 
503
792
  delve(root, path, val);
504
-
505
- //this.render();
506
793
  }
507
794
 
508
- /** @enum {string} */
795
+ /** @enum {int} */
509
796
  export const PathType = {
510
797
  /** Child of a node */
511
- Content: 'content',
798
+ Content: 1,
512
799
 
513
800
  /** One or more whole attributes */
514
- Multiple: 'attrName',
801
+ Multiple: 2,
515
802
 
516
803
  /** Value of an attribute. */
517
- Value: 'attrValue',
804
+ Value: 3,
518
805
 
519
806
  /** Value of an attribute being passed to a component. */
520
- Component: 'component',
807
+ Component: 4,
521
808
 
522
809
  /** Expressions inside Html comments. */
523
- Comment: 'comment',
810
+ Comment: 5,
811
+
812
+ /** Value of an attribute. */
813
+ Event: 6,
524
814
  }
525
815
 
526
816
 
@@ -549,6 +839,3 @@ export function resolveNodePath(root, path) {
549
839
  }
550
840
 
551
841
 
552
- // TODO: Memory from this is never freed. Use a WeakMap<Node, Object<eventName:string, function[]>>
553
- let nodeEvents = {};
554
- let nodeEventArgs = {};