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.
- package/benchmarks/naive/Solarite.min.js +4 -0
- package/benchmarks/naive/index.html +14 -0
- package/benchmarks/naive/main.js +339 -0
- package/benchmarks/naive/package-lock.json +13 -0
- package/benchmarks/naive/package.json +23 -0
- package/benchmarks/readme.md +48 -0
- package/build/build.bat +2 -2
- package/build/build.js +1 -1
- package/dist/Solarite-debug.js +1344 -1532
- package/dist/Solarite.js +1298 -1334
- package/dist/Solarite.min.js +3 -3
- package/dist/udomdiff-license.txt +18 -0
- package/docs/index.md +440 -130
- package/docs/js/Playground.js +1 -1
- package/docs/js/codemirror/codemirror6.js +3683 -3206
- package/docs/js/codemirror/themeSolarIce.js +1 -1
- package/docs/js/documentation.js +1 -1
- package/docs/js/ui/CodeEditor.js +183 -45
- package/docs/js/ui/FlexResizer.js +14 -3
- package/docs/js/util/Errors.js +11 -0
- package/docs/media/documentation.css +6 -3
- package/index.html +449 -30
- package/package.json +1 -1
- package/readme.md +1 -1
- package/src/solarite/ExprPath.js +349 -151
- package/src/solarite/Globals.js +43 -1
- package/src/solarite/NodeGroup.js +275 -293
- package/src/solarite/Shell.js +26 -28
- package/src/solarite/Solarite.js +12 -0
- package/src/solarite/Template.js +55 -128
- package/src/solarite/Util.js +131 -7
- package/src/solarite/createSolarite.js +14 -19
- package/src/solarite/getArg.js +1 -1
- package/src/solarite/hash.js +18 -35
- package/src/solarite/r.js +127 -127
- package/src/solarite/watch3.js +18 -11
- package/src/{solarite → unused}/NodeGroupManager.js +81 -109
- package/src/{solarite → unused}/watch.js +1 -1
- package/src/{solarite → unused}/watch2.js +1 -2
- package/src/{solarite → util}/MultiValueMap.js +18 -11
- package/src/util/WeakArray.js +33 -0
- package/tests/Solarite.test.js +862 -167
- package/tests/index.html +4 -2
- package/tests/run.bat +1 -1
- package/deno.lock +0 -176
package/src/solarite/ExprPath.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
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*/
|
|
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
|
-
|
|
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
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
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 =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
477
|
+
// Resolve node paths.
|
|
371
478
|
let nodeMarker, nodeBefore;
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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(
|
|
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 {
|
|
795
|
+
/** @enum {int} */
|
|
598
796
|
export const PathType = {
|
|
599
797
|
/** Child of a node */
|
|
600
|
-
Content:
|
|
798
|
+
Content: 1,
|
|
601
799
|
|
|
602
800
|
/** One or more whole attributes */
|
|
603
|
-
Multiple:
|
|
801
|
+
Multiple: 2,
|
|
604
802
|
|
|
605
803
|
/** Value of an attribute. */
|
|
606
|
-
Value:
|
|
804
|
+
Value: 3,
|
|
607
805
|
|
|
608
806
|
/** Value of an attribute being passed to a component. */
|
|
609
|
-
Component:
|
|
807
|
+
Component: 4,
|
|
610
808
|
|
|
611
809
|
/** Expressions inside Html comments. */
|
|
612
|
-
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 = {};
|