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.
- 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 +3 -2
- package/build/build.js +1 -1
- package/dist/Solarite-debug.js +1941 -2791
- package/dist/Solarite.js +1697 -2511
- package/dist/Solarite.min.js +3 -3
- package/dist/udomdiff-license.txt +18 -0
- package/docs/index.md +695 -235
- package/docs/js/Playground.js +1 -1
- package/docs/js/codemirror/codemirror6.js +3683 -3206
- package/docs/js/codemirror/themeSolarIce.js +2 -2
- package/docs/js/documentation.js +2 -2
- package/docs/js/ui/CodeEditor.js +183 -45
- package/docs/js/ui/FlexResizer.js +15 -5
- package/docs/js/util/Errors.js +11 -0
- package/docs/media/documentation.css +6 -3
- package/index.html +462 -26
- package/package.json +1 -1
- package/readme.md +11 -1
- package/src/solarite/ExprPath.js +422 -135
- package/src/solarite/Globals.js +53 -0
- package/src/solarite/NodeGroup.js +300 -388
- package/src/solarite/Shell.js +31 -33
- package/src/solarite/Solarite.js +17 -2
- package/src/solarite/Template.js +75 -25
- package/src/solarite/Util.js +131 -7
- package/src/solarite/createSolarite.js +32 -25
- package/src/solarite/getArg.js +12 -12
- package/src/solarite/hash.js +18 -35
- package/src/solarite/r.js +128 -118
- package/src/solarite/watch3.js +98 -0
- package/src/{solarite → unused}/NodeGroupManager.js +86 -224
- package/src/unused/onConnect.js +79 -0
- package/src/{solarite → unused}/watch.js +2 -2
- package/src/{solarite → unused}/watch2.js +4 -4
- package/src/{solarite → util}/MultiValueMap.js +23 -12
- package/src/util/WeakArray.js +33 -0
- package/tests/Solarite.test.js +1108 -166
- package/tests/Testimony.js +274 -67
- package/tests/index.html +6 -35
- package/tests/run.bat +2 -0
- package/tests/NodeGroup.test.js +0 -115
- package/tests/Shell.test.js +0 -75
package/src/solarite/ExprPath.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
67
|
+
/**
|
|
68
|
+
* @type {int} Index of nodeBefore among its parentNode's children. */
|
|
72
69
|
nodeBeforeIndex;
|
|
73
|
-
nodeMarkerPath;
|
|
74
70
|
|
|
75
|
-
|
|
76
|
-
|
|
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 {
|
|
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
|
-
*
|
|
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*/
|
|
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
|
-
|
|
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
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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 =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
477
|
+
// Resolve node paths.
|
|
281
478
|
let nodeMarker, nodeBefore;
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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(
|
|
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 {
|
|
795
|
+
/** @enum {int} */
|
|
509
796
|
export const PathType = {
|
|
510
797
|
/** Child of a node */
|
|
511
|
-
Content:
|
|
798
|
+
Content: 1,
|
|
512
799
|
|
|
513
800
|
/** One or more whole attributes */
|
|
514
|
-
Multiple:
|
|
801
|
+
Multiple: 2,
|
|
515
802
|
|
|
516
803
|
/** Value of an attribute. */
|
|
517
|
-
Value:
|
|
804
|
+
Value: 3,
|
|
518
805
|
|
|
519
806
|
/** Value of an attribute being passed to a component. */
|
|
520
|
-
Component:
|
|
807
|
+
Component: 4,
|
|
521
808
|
|
|
522
809
|
/** Expressions inside Html comments. */
|
|
523
|
-
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 = {};
|