solarite 0.2.3 → 0.3.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/dist/Solarite-debug.js +1708 -1211
- package/dist/Solarite.js +1642 -1047
- package/dist/Solarite.min.js +2 -2
- package/package.json +1 -1
- package/readme.md +1 -3
- package/src/solarite/ExprPath.js +479 -196
- package/src/solarite/Globals.js +77 -51
- package/src/solarite/HtmlParser.js +91 -0
- package/src/solarite/NodeGroup.js +288 -195
- package/src/solarite/Shell.js +117 -91
- package/src/solarite/Solarite.js +7 -3
- package/src/solarite/Template.js +23 -18
- package/src/solarite/Util.js +117 -179
- package/src/solarite/createSolarite.js +16 -138
- package/src/solarite/getArg.js +31 -16
- package/src/solarite/{r.js → h.js} +56 -18
- package/src/solarite/hash.js +12 -9
- package/src/solarite/watch.js +546 -0
- package/src/util/Errors.js +1 -0
- package/src/util/MultiValueMap.js +73 -13
- package/src/util/Util.js +15 -2
- package/src/util/delve.js +5 -4
- package/src/solarite/watch3.js +0 -98
- package/src/util/WeakArray.js +0 -33
package/src/solarite/ExprPath.js
CHANGED
|
@@ -6,6 +6,10 @@ import Template from "./Template.js";
|
|
|
6
6
|
import Globals from "./Globals.js";
|
|
7
7
|
import MultiValueMap from "../util/MultiValueMap.js";
|
|
8
8
|
import udomdiff from "./udomdiff.js";
|
|
9
|
+
//import {ArraySpliceOp} from "./watch.js";
|
|
10
|
+
//#IFDEV
|
|
11
|
+
var exprPathId = 0;
|
|
12
|
+
//#ENDIF
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
15
|
* Path to where an expression should be evaluated within a Shell or NodeGroup.
|
|
@@ -13,8 +17,12 @@ import udomdiff from "./udomdiff.js";
|
|
|
13
17
|
* TODO: Make this based on parent and node instead of path? */
|
|
14
18
|
export default class ExprPath {
|
|
15
19
|
|
|
20
|
+
//#IFDEV
|
|
21
|
+
eid = exprPathId++;
|
|
22
|
+
//#ENDIF
|
|
23
|
+
|
|
16
24
|
/**
|
|
17
|
-
* @type {
|
|
25
|
+
* @type {ExprPathType} */
|
|
18
26
|
type;
|
|
19
27
|
|
|
20
28
|
// Used for attributes:
|
|
@@ -30,8 +38,6 @@ export default class ExprPath {
|
|
|
30
38
|
* @type {Set<string>} Used for type=AttribType.Multiple to remember the attributes that were added. */
|
|
31
39
|
attrNames;
|
|
32
40
|
|
|
33
|
-
|
|
34
|
-
|
|
35
41
|
/**
|
|
36
42
|
* @type {Node} Node that occurs before this ExprPath's first Node.
|
|
37
43
|
* This is necessary because udomdiff() can steal nodes from another ExprPath.
|
|
@@ -73,13 +79,23 @@ export default class ExprPath {
|
|
|
73
79
|
nodeMarkerPath;
|
|
74
80
|
|
|
75
81
|
|
|
82
|
+
/** @type {?function} A function called by renderWatched() to update the value of this expression. */
|
|
83
|
+
watchFunction
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @type {?function} The most recent callback passed to a .map() function in this ExprPath.
|
|
87
|
+
* TODO: What if one ExprPath has two .map() calls? Maybe we just won't support that. */
|
|
88
|
+
mapCallback
|
|
89
|
+
|
|
90
|
+
isHtmlProperty = undefined;
|
|
91
|
+
|
|
76
92
|
/**
|
|
77
93
|
* @param nodeBefore {Node}
|
|
78
94
|
* @param nodeMarker {?Node}
|
|
79
|
-
* @param type {
|
|
95
|
+
* @param type {ExprPathType}
|
|
80
96
|
* @param attrName {?string}
|
|
81
97
|
* @param attrValue {string[]} */
|
|
82
|
-
constructor(nodeBefore, nodeMarker, type=
|
|
98
|
+
constructor(nodeBefore, nodeMarker, type=ExprPathType.Content, attrName=null, attrValue=null) {
|
|
83
99
|
|
|
84
100
|
// If path is a node.
|
|
85
101
|
this.nodeBefore = nodeBefore;
|
|
@@ -87,7 +103,7 @@ export default class ExprPath {
|
|
|
87
103
|
this.type = type;
|
|
88
104
|
this.attrName = attrName;
|
|
89
105
|
this.attrValue = attrValue;
|
|
90
|
-
if (type ===
|
|
106
|
+
if (type === ExprPathType.AttribMultiple)
|
|
91
107
|
this.attrNames = new Set();
|
|
92
108
|
}
|
|
93
109
|
|
|
@@ -101,36 +117,27 @@ export default class ExprPath {
|
|
|
101
117
|
* We should modify path.applyValueAttrib so it stores the procssed parts and then only calls
|
|
102
118
|
* setAttribute() once all the pieces are in place.
|
|
103
119
|
*
|
|
104
|
-
* @param expr {Expr}
|
|
105
120
|
* @param exprs {Expr[]}
|
|
106
|
-
* @param
|
|
107
|
-
|
|
108
|
-
* @returns {int} */
|
|
109
|
-
apply(expr, exprs=null, exprIndex=0, componentExprs={}) {
|
|
121
|
+
* @param freeNodeGroups {boolean} */
|
|
122
|
+
apply(exprs, freeNodeGroups=true) {
|
|
110
123
|
switch (this.type) {
|
|
111
124
|
case 1: // PathType.Content:
|
|
112
|
-
this.applyNodes(
|
|
125
|
+
this.applyNodes(exprs[0], freeNodeGroups);
|
|
113
126
|
break;
|
|
114
127
|
case 2: // PathType.Multiple:
|
|
115
|
-
this.applyMultipleAttribs(this.nodeMarker,
|
|
128
|
+
this.applyMultipleAttribs(this.nodeMarker, exprs[0]);
|
|
116
129
|
break;
|
|
117
130
|
case 5: // PathType.Comment:
|
|
118
131
|
// Expressions inside Html comments. Deliberately empty because we won't waste time updating them.
|
|
119
132
|
break;
|
|
120
133
|
case 6: // PathType.Event:
|
|
121
|
-
this.applyEventAttrib(this.nodeMarker,
|
|
134
|
+
this.applyEventAttrib(this.nodeMarker, exprs[0], this.parentNg.rootNg.root);
|
|
122
135
|
break;
|
|
123
|
-
default:
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
}
|
|
136
|
+
default: // TODO: Is this still used? Lots of tests fail without it.
|
|
137
|
+
// One attribute value may have multiple expressions. Here we apply them all at once.
|
|
138
|
+
this.applyValueAttrib(this.nodeMarker, exprs);
|
|
130
139
|
break;
|
|
131
140
|
}
|
|
132
|
-
|
|
133
|
-
return exprIndex;
|
|
134
141
|
}
|
|
135
142
|
|
|
136
143
|
/**
|
|
@@ -138,10 +145,17 @@ export default class ExprPath {
|
|
|
138
145
|
* Called by applyExprs()
|
|
139
146
|
* This function is recursive, as the functions it calls also call it.
|
|
140
147
|
* @param expr {Expr}
|
|
148
|
+
* @param freeNodeGroups {boolean}
|
|
141
149
|
* @return {Node[]} New Nodes created. */
|
|
142
|
-
applyNodes(expr) {
|
|
150
|
+
applyNodes(expr, freeNodeGroups=true) {
|
|
143
151
|
let path = this;
|
|
144
152
|
|
|
153
|
+
// This can be done at the beginning or the end of this function.
|
|
154
|
+
// If at the end, we may get rendering done faster.
|
|
155
|
+
// But when at the beginning, it leaves all the nodes in-use so we can do a renderWatched().
|
|
156
|
+
if (freeNodeGroups)
|
|
157
|
+
path.freeNodeGroups();
|
|
158
|
+
|
|
145
159
|
/*#IFDEV*/path.verify();/*#ENDIF*/
|
|
146
160
|
|
|
147
161
|
/** @type {(Node|NodeGroup|Expr)[]} */
|
|
@@ -150,10 +164,10 @@ export default class ExprPath {
|
|
|
150
164
|
/*#IFDEV*/assert(!oldNodeGroups.includes(null))/*#ENDIF*/
|
|
151
165
|
let secondPass = []; // indices
|
|
152
166
|
|
|
153
|
-
path.nodeGroups = []; // Reset before
|
|
154
|
-
path.
|
|
167
|
+
path.nodeGroups = []; // Reset before applyExactNodes and the code below rebuilds it.
|
|
168
|
+
path.applyExactNodes(expr, newNodes, secondPass);
|
|
155
169
|
|
|
156
|
-
this.existingTextNodes = null;
|
|
170
|
+
//this.existingTextNodes = null;
|
|
157
171
|
|
|
158
172
|
// TODO: Create an array of old vs Nodes and NodeGroups together.
|
|
159
173
|
// If they're all the same, skip the next steps.
|
|
@@ -206,26 +220,162 @@ export default class ExprPath {
|
|
|
206
220
|
// Rearrange nodes.
|
|
207
221
|
udomdiff(path.nodeMarker.parentNode, oldNodes, newNodes, path.nodeMarker)
|
|
208
222
|
|
|
209
|
-
|
|
223
|
+
// TODO: Put this in a remove() function of NodeGroup.
|
|
224
|
+
// Then only run it on the old nodeGroups that were actually removed.
|
|
225
|
+
//Util.saveOrphans(oldNodeGroups, oldNodes);
|
|
226
|
+
|
|
227
|
+
for (let ng of oldNodeGroups)
|
|
228
|
+
if (!ng.startNode.parentNode)
|
|
229
|
+
ng.removeAndSaveOrphans();
|
|
210
230
|
}
|
|
211
231
|
|
|
212
|
-
// Must happen after second pass.
|
|
213
|
-
path.freeNodeGroups();
|
|
214
232
|
|
|
215
233
|
/*#IFDEV*/path.verify();/*#ENDIF*/
|
|
216
234
|
}
|
|
217
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Used by watch() for inserting/removing/replacing individual loop items.
|
|
238
|
+
* @param op {ArraySpliceOp} */
|
|
239
|
+
applyArrayOp(op) {
|
|
240
|
+
|
|
241
|
+
// Replace NodeGroups
|
|
242
|
+
let replaceCount = Math.min(op.deleteCount, op.items.length);
|
|
243
|
+
let deleteCount = op.deleteCount - replaceCount;
|
|
244
|
+
for (let i=0; i<replaceCount; i++) {
|
|
245
|
+
let oldNg = this.nodeGroups[op.index + i]; // TODO: One expr can create multiple nodegroups.
|
|
246
|
+
|
|
247
|
+
// Try to find an exact match
|
|
248
|
+
let func = this.mapCallback || this.watchFunction;
|
|
249
|
+
let expr = func(op.items[i]);
|
|
250
|
+
|
|
251
|
+
// If the result of func isn't a template, conver it to one or more templates.
|
|
252
|
+
this.exprToTemplates(expr, template => { // TODO: An expr can create multiple NodeGroups. I need a way to group them.
|
|
253
|
+
|
|
254
|
+
let ng = this.getNodeGroup(template, true); // Removes from nodeGroupsAttached and adds to nodeGroupsRendered()
|
|
255
|
+
if (ng && ng === oldNg) {
|
|
256
|
+
// It's an exact match, so replace nothing.
|
|
257
|
+
// TODO: What if the found NodeGroup as at a differnet place?
|
|
258
|
+
} else {
|
|
259
|
+
|
|
260
|
+
// Find a close match or create a new node group
|
|
261
|
+
if (!ng)
|
|
262
|
+
ng = this.getNodeGroup(template, false); // adds back to nodeGroupsRendered()
|
|
263
|
+
this.nodeGroups[op.index + i] = ng; // TODO: Remove old one to nodeGroupsDetached?
|
|
264
|
+
|
|
265
|
+
// Splice in the new nodes.
|
|
266
|
+
let insertBefore = oldNg.startNode;
|
|
267
|
+
for (let node of ng.getNodes())
|
|
268
|
+
insertBefore.parentNode.insertBefore(node, insertBefore);
|
|
269
|
+
|
|
270
|
+
// Remove the old nodes.
|
|
271
|
+
if (ng !== oldNg)
|
|
272
|
+
oldNg.removeAndSaveOrphans();
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
218
276
|
|
|
277
|
+
// Delete extra at the end.
|
|
278
|
+
if (deleteCount > 0) {
|
|
279
|
+
for (let i=0; i<deleteCount; i++) {
|
|
280
|
+
let oldNg = this.nodeGroups[op.index + replaceCount + i];
|
|
281
|
+
oldNg.removeAndSaveOrphans();
|
|
282
|
+
}
|
|
283
|
+
this.nodeGroups.splice(op.index + replaceCount, deleteCount);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Add extra at the end.
|
|
287
|
+
else {
|
|
288
|
+
let newItems = op.items.slice(replaceCount);
|
|
289
|
+
|
|
290
|
+
let insertBefore = this.nodeGroups[op.index + replaceCount]?.startNode || this.nodeMarker;
|
|
291
|
+
for (let i = 0; i < newItems.length; i++) { // We use nodeMarker if the subequent (or all) nodeGroups have been removed.
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
// Try to find exact match
|
|
295
|
+
let template = this.mapCallback(newItems[i]);
|
|
296
|
+
let ng = this.getNodeGroup(template, true); // Removes from nodeGroupsAttached and adds to nodeGroupsRendered()
|
|
297
|
+
if (!ng) // Find a close match or create a new node group
|
|
298
|
+
ng = this.getNodeGroup(template, false); // adds back to nodeGroupsRendered()
|
|
299
|
+
|
|
300
|
+
this.nodeGroups.push(ng);
|
|
301
|
+
|
|
302
|
+
// Splice in the new nodes.
|
|
303
|
+
for (let node of ng.getNodes())
|
|
304
|
+
insertBefore.parentNode.insertBefore(node, insertBefore);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
//#IFDEV
|
|
309
|
+
assert(this.nodeGroups.length === op.array.length);
|
|
310
|
+
//#ENDIF
|
|
311
|
+
|
|
312
|
+
// TODO: update or invalidate the nodes cache?
|
|
313
|
+
this.nodesCache = null;
|
|
314
|
+
}
|
|
219
315
|
|
|
220
316
|
/**
|
|
221
|
-
*
|
|
317
|
+
* Recursively traverse expr.
|
|
318
|
+
* If a value is a function, evaluate it.
|
|
319
|
+
* If a value is an array, recurse on each item.
|
|
320
|
+
* If it's a primitive, convert it to a Template.
|
|
321
|
+
* Otherwise pass the item (which is now either a Template or a Node) to callback.
|
|
322
|
+
* @param expr
|
|
323
|
+
* @param callback {function(Node|Template)}
|
|
324
|
+
*
|
|
325
|
+
* TODO: have applyExactNodes() use this function. */
|
|
326
|
+
exprToTemplates(expr, callback) {
|
|
327
|
+
if (Array.isArray(expr))
|
|
328
|
+
for (let subExpr of expr)
|
|
329
|
+
this.exprToTemplates(subExpr, callback);
|
|
330
|
+
|
|
331
|
+
else if (typeof expr === 'function') {
|
|
332
|
+
// TODO: One ExprPath can have multiple expr functions.
|
|
333
|
+
// But if using it as a watch, it should only have one at the top level.
|
|
334
|
+
// So maybe this is ok.
|
|
335
|
+
Globals.currentExprPath = this; // Used by watch()
|
|
336
|
+
|
|
337
|
+
this.watchFunction = expr; // TODO: Only do this if it's a top level function.
|
|
338
|
+
expr = expr(); // As expr accesses watched variables, watch() uses Globals.currentExprPath to mark where those watched variables are being used.
|
|
339
|
+
Globals.currentExprPath = null;
|
|
340
|
+
|
|
341
|
+
this.exprToTemplates(expr, callback);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// String/Number/Date/Boolean
|
|
345
|
+
else if (!(expr instanceof Template) && !(expr instanceof Node)){
|
|
346
|
+
// Convert expression to a string.
|
|
347
|
+
if (expr === undefined || expr === false || expr === null) // Util.isFalsy() inlined
|
|
348
|
+
expr = '';
|
|
349
|
+
else if (typeof expr !== 'string')
|
|
350
|
+
expr += '';
|
|
351
|
+
|
|
352
|
+
// Get the same Template for the same string each time.
|
|
353
|
+
// let template = Globals.stringTemplates[expr];
|
|
354
|
+
// if (!template) {
|
|
355
|
+
let template = new Template([expr], []);
|
|
356
|
+
// Globals.stringTemplates[expr] = template;
|
|
357
|
+
//}
|
|
358
|
+
|
|
359
|
+
// Recurse.
|
|
360
|
+
this.exprToTemplates(template, callback);
|
|
361
|
+
}
|
|
362
|
+
else
|
|
363
|
+
callback(expr);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Try to apply Nodes that are an exact match, by finding existing nodes from the last render
|
|
369
|
+
* that have the same value as created by the expr.
|
|
370
|
+
* This is called from ExprPath.applyNodes().
|
|
371
|
+
*
|
|
222
372
|
* @param expr {Template|Node|Array|function|*}
|
|
223
|
-
* @param newNodes {(Node|Template)[]}
|
|
224
|
-
* @param secondPass {
|
|
225
|
-
|
|
373
|
+
* @param newNodes {(Node|Template)[]} An inout parameter; we add the nodes here as we go.
|
|
374
|
+
* @param secondPass {[int, int][]} Locations within newNodes for ExprPath.applyNodes() to evaluate later,
|
|
375
|
+
* when it tries to find partial matches. */
|
|
376
|
+
applyExactNodes(expr, newNodes, secondPass) {
|
|
226
377
|
|
|
227
378
|
if (expr instanceof Template) {
|
|
228
|
-
|
|
229
379
|
let ng = this.getNodeGroup(expr, true);
|
|
230
380
|
if (ng) {
|
|
231
381
|
|
|
@@ -243,7 +393,7 @@ export default class ExprPath {
|
|
|
243
393
|
}
|
|
244
394
|
}
|
|
245
395
|
|
|
246
|
-
// Node created by an expression.
|
|
396
|
+
// Node(s) created by an expression.
|
|
247
397
|
else if (expr instanceof Node) {
|
|
248
398
|
|
|
249
399
|
// DocumentFragment created by an expression.
|
|
@@ -256,49 +406,54 @@ export default class ExprPath {
|
|
|
256
406
|
// Arrays and functions.
|
|
257
407
|
// I tried iterating over the result of a generator function to avoid this recursion and simplify the code,
|
|
258
408
|
// but that consistently made the js-framework-benchmarks a few percentage points slower.
|
|
259
|
-
else
|
|
409
|
+
else {
|
|
410
|
+
this.exprToTemplates(expr, template => {
|
|
411
|
+
this.applyExactNodes(template, newNodes, secondPass);
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Old version
|
|
417
|
+
/*else if (Array.isArray(expr))
|
|
260
418
|
for (let subExpr of expr)
|
|
261
|
-
this.
|
|
419
|
+
this.applyExactNodes(subExpr, newNodes, secondPass);
|
|
262
420
|
|
|
263
421
|
else if (typeof expr === 'function') {
|
|
264
|
-
|
|
265
|
-
|
|
422
|
+
// TODO: One ExprPath can have multiple expr functions.
|
|
423
|
+
// But if using it as a watch, it should only have one at the top level.
|
|
424
|
+
// So maybe this is ok.
|
|
425
|
+
Globals.currentExprPath = this; // Used by watch()
|
|
426
|
+
|
|
427
|
+
this.watchFunction = expr; // TODO: Only do this if it's a top level function.
|
|
428
|
+
let result = expr(); // As expr accesses watched variables, watch() uses Globals.currentExprPath to mark where those watched variables are being used.
|
|
266
429
|
Globals.currentExprPath = null;
|
|
267
430
|
|
|
268
|
-
this.
|
|
431
|
+
this.applyExactNodes(result, newNodes, secondPass);
|
|
269
432
|
}
|
|
270
433
|
|
|
271
|
-
//
|
|
434
|
+
// String
|
|
272
435
|
else {
|
|
273
|
-
// Convert
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
436
|
+
// Convert expression to a string.
|
|
437
|
+
let stringExpr = expr;
|
|
438
|
+
if (expr === undefined || expr === false || expr === null) // Util.isFalsy()
|
|
439
|
+
stringExpr = '';
|
|
440
|
+
else if (typeof expr !== 'string')
|
|
441
|
+
stringExpr = expr + '';
|
|
442
|
+
|
|
443
|
+
// Get the same Template for the same string each time.
|
|
444
|
+
let template = Globals.stringTemplates[stringExpr];
|
|
445
|
+
if (!template) {
|
|
446
|
+
template = new Template([stringExpr], []);
|
|
447
|
+
Globals.stringTemplates[stringExpr] = template;
|
|
284
448
|
}
|
|
285
449
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
}
|
|
450
|
+
// Recurse.
|
|
451
|
+
this.applyExactNodes(template, newNodes, secondPass);
|
|
452
|
+
}*/
|
|
298
453
|
}
|
|
299
454
|
|
|
300
455
|
applyMultipleAttribs(node, expr) {
|
|
301
|
-
/*#IFDEV*/assert(this.type ===
|
|
456
|
+
/*#IFDEV*/assert(this.type === ExprPathType.AttribMultiple);/*#ENDIF*/
|
|
302
457
|
|
|
303
458
|
if (Array.isArray(expr))
|
|
304
459
|
expr = expr.flat().join(' '); // flat and join so we can accept arrays of arrays of strings.
|
|
@@ -307,6 +462,13 @@ export default class ExprPath {
|
|
|
307
462
|
let oldNames = this.attrNames;
|
|
308
463
|
this.attrNames = new Set();
|
|
309
464
|
if (expr) {
|
|
465
|
+
if (typeof expr === 'function') {
|
|
466
|
+
Globals.currentExprPath = this; // Used by watch()
|
|
467
|
+
this.watchFunction = expr; // used by renderWatched()
|
|
468
|
+
expr = expr();
|
|
469
|
+
Globals.currentExprPath = null;
|
|
470
|
+
}
|
|
471
|
+
|
|
310
472
|
let attrs = (expr +'') // Split string into multiple attributes.
|
|
311
473
|
.split(/([\w-]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s]+))/g)
|
|
312
474
|
.map(text => text.trim())
|
|
@@ -337,49 +499,60 @@ export default class ExprPath {
|
|
|
337
499
|
* @param root */
|
|
338
500
|
applyEventAttrib(node, expr, root) {
|
|
339
501
|
/*#IFDEV*/
|
|
340
|
-
assert(this.type ===
|
|
502
|
+
assert(this.type === ExprPathType.Event/* || this.type === PathType.Component*/);
|
|
341
503
|
assert(root instanceof HTMLElement);
|
|
342
504
|
/*#ENDIF*/
|
|
343
505
|
|
|
344
506
|
let eventName = this.attrName.slice(2); // remove "on-" prefix.
|
|
345
507
|
let func;
|
|
346
|
-
|
|
347
|
-
// Convert array to function.
|
|
348
508
|
let args = [];
|
|
349
|
-
if (Array.isArray(expr)) {
|
|
350
509
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
// Undocumented.
|
|
358
|
-
// oninput=${[this, 'value']}
|
|
359
|
-
else {
|
|
360
|
-
func = setValue;
|
|
361
|
-
args = [expr[0], expr.slice(1), node]
|
|
362
|
-
node.value = delve(expr[0], expr.slice(1));
|
|
363
|
-
// root.render(); // TODO: This causes infinite recursion.
|
|
364
|
-
}
|
|
510
|
+
// Convert array to function.
|
|
511
|
+
// oninput=${[this.doSomething, 'meow']}
|
|
512
|
+
if (Array.isArray(expr) && typeof expr[0] === 'function') {
|
|
513
|
+
func = expr[0];
|
|
514
|
+
args = expr.slice(1);
|
|
365
515
|
}
|
|
366
|
-
else
|
|
516
|
+
else if (typeof expr === 'function')
|
|
367
517
|
func = expr;
|
|
518
|
+
else
|
|
519
|
+
throw new Error(`Invalid event binding: <${node.tagName.toLowerCase()} ${this.attrName}=\${${JSON.stringify(expr)}}>`);
|
|
520
|
+
|
|
521
|
+
this.bindEvent(node, root, eventName, eventName, func, args);
|
|
522
|
+
}
|
|
523
|
+
|
|
368
524
|
|
|
525
|
+
/**
|
|
526
|
+
* Call function when eventName is triggerd on node.
|
|
527
|
+
* @param node {HTMLElement}
|
|
528
|
+
* @param root {HTMLElement}
|
|
529
|
+
* @param key {string}
|
|
530
|
+
* @param eventName {string}
|
|
531
|
+
* @param func {function}
|
|
532
|
+
* @param args {array}
|
|
533
|
+
* @param capture {boolean} */
|
|
534
|
+
bindEvent(node, root, key, eventName, func, args, capture=false) {
|
|
369
535
|
let nodeEvents = Globals.nodeEvents.get(node);
|
|
370
536
|
if (!nodeEvents) {
|
|
371
|
-
nodeEvents = {[
|
|
537
|
+
nodeEvents = {[key]: new Array(3)};
|
|
372
538
|
Globals.nodeEvents.set(node, nodeEvents);
|
|
373
539
|
}
|
|
374
|
-
let nodeEvent = nodeEvents[
|
|
375
|
-
|
|
540
|
+
let nodeEvent = nodeEvents[key];
|
|
541
|
+
if (!nodeEvent)
|
|
542
|
+
nodeEvents[key] = nodeEvent = new Array(3);
|
|
376
543
|
|
|
544
|
+
if (typeof func !== 'function')
|
|
545
|
+
throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${${func}}> because it's not a function.`);
|
|
377
546
|
|
|
378
547
|
// If function has changed, remove and rebind the event.
|
|
379
548
|
if (nodeEvent[0] !== func) {
|
|
549
|
+
|
|
550
|
+
// TODO: We should be removing event listeners when calling getNodeGroup(),
|
|
551
|
+
// when we get the node from the list of nodeGroupsAttached/nodeGroupsDetached,
|
|
552
|
+
// instead of only when we rebind an event.
|
|
380
553
|
let [existing, existingBound, _] = nodeEvent;
|
|
381
554
|
if (existing)
|
|
382
|
-
node.removeEventListener(eventName, existingBound);
|
|
555
|
+
node.removeEventListener(eventName, existingBound, capture);
|
|
383
556
|
|
|
384
557
|
let originalFunc = func;
|
|
385
558
|
|
|
@@ -395,74 +568,155 @@ export default class ExprPath {
|
|
|
395
568
|
nodeEvent[0] = originalFunc;
|
|
396
569
|
nodeEvent[1] = boundFunc;
|
|
397
570
|
|
|
398
|
-
node.addEventListener(eventName, boundFunc);
|
|
571
|
+
node.addEventListener(eventName, boundFunc, capture);
|
|
399
572
|
|
|
400
573
|
// TODO: classic event attribs?
|
|
401
|
-
//el[attr.name] = e => // e.g. el.onclick = ...
|
|
402
|
-
// (new Function('event', 'el', attr.value)).bind(this.manager.rootEl)(e, el)
|
|
574
|
+
//el[attr.name] = e => // e.g. el.onclick = ... // put "event", "el", and "this" in scope for the event code.
|
|
575
|
+
// (new Function('event', 'el', attr.value)).bind(this.manager.rootEl)(e, el)
|
|
403
576
|
}
|
|
404
577
|
|
|
405
578
|
// Otherwise just update the args to the function.
|
|
406
|
-
nodeEvents[
|
|
579
|
+
nodeEvents[key][2] = args;
|
|
407
580
|
}
|
|
408
581
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
else if (!this.attrValue && expr === true)
|
|
417
|
-
node.setAttribute(this.attrName, '');
|
|
582
|
+
/**
|
|
583
|
+
* Handle values, including two-way binding.
|
|
584
|
+
* @param node
|
|
585
|
+
* @param exprs */
|
|
586
|
+
// TODO: node is always this.nodeMarker?
|
|
587
|
+
applyValueAttrib(node, exprs) {
|
|
588
|
+
let expr = exprs[0];
|
|
418
589
|
|
|
590
|
+
// Two-way binding between attributes
|
|
419
591
|
// Passing a path to the value attribute.
|
|
592
|
+
// Copies the attribute to the property when the input event fires.
|
|
593
|
+
// value=${[this, 'value]'}
|
|
594
|
+
// checked=${[this, 'isAgree']}
|
|
420
595
|
// This same logic is in NodeGroup.createNewComponent() for components.
|
|
421
|
-
|
|
596
|
+
if (Util.isPath(expr)) {
|
|
422
597
|
let [obj, path] = [expr[0], expr.slice(1)];
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
598
|
+
|
|
599
|
+
if (!obj)
|
|
600
|
+
throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${[${expr.map(item => item ? `'${item}'` : item+'').join(', ')}]}>.`);
|
|
601
|
+
|
|
602
|
+
let value = delve(obj, path);
|
|
603
|
+
|
|
604
|
+
// Special case to allow setting select-multiple value from an array
|
|
605
|
+
if (this.attrName === 'value' && node.type === 'select-multiple' && Array.isArray(value)) {
|
|
606
|
+
// Set the .selected property on the options having a value within value.
|
|
607
|
+
let strValues = value.map(v => v + '');
|
|
608
|
+
for (let option of node.options)
|
|
609
|
+
option.selected = strValues.includes(option.value)
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
// TODO: should we remove isFalsy, since these are always props?
|
|
613
|
+
let strValue = Util.isFalsy(value) ? '' : value;
|
|
614
|
+
|
|
615
|
+
// If we don't have this condition, when we call render(), the browser will scroll to the currently
|
|
616
|
+
// selected item in a <select> and mess up manually scrolling to a different value.
|
|
617
|
+
if (strValue !== node[this.attrName])
|
|
618
|
+
node[this.attrName] = strValue;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// TODO: We need to remove any old listeners, like in bindEventAttribute.
|
|
622
|
+
// Does bindEvent() now handle that?
|
|
623
|
+
let func = () => {
|
|
624
|
+
let value = (this.attrName === 'value')
|
|
625
|
+
? Util.getInputValue(node)
|
|
626
|
+
: node[this.attrName];
|
|
627
|
+
delve(obj, path, value);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// We use capture so we update the values before other events added by the user.
|
|
631
|
+
// TODO: Bind to scroll events also?
|
|
632
|
+
// What about resize events and width/height?
|
|
633
|
+
this.bindEvent(node, path[0], this.attrName, 'input', func, [], true);
|
|
427
634
|
}
|
|
428
635
|
|
|
429
636
|
// Regular attribute
|
|
430
637
|
else {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
//
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
638
|
+
// TODO: Cache this on ExprPath.isProp when Shell creates the props. Have ExprPath.clone() copy .isProp
|
|
639
|
+
// Or make it a new PathType.
|
|
640
|
+
//if (this.attrName === 'disabled')
|
|
641
|
+
// debugger;
|
|
642
|
+
|
|
643
|
+
// hasOwnProperty() checks only the object, not the parents
|
|
644
|
+
// this.attrName in node checks the node and the parents.
|
|
645
|
+
// This version checks the html element it extends from, to see if has a setter set:
|
|
646
|
+
// Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), this.attrName)?.set
|
|
647
|
+
//let isProp = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), this.attrName)?.set;
|
|
648
|
+
let isProp = this.isHtmlProperty;
|
|
649
|
+
if (isProp === undefined)
|
|
650
|
+
isProp = this.isHtmlProperty = Util.isHtmlProp(node, this.attrName);
|
|
651
|
+
|
|
652
|
+
// Values to toggle an attribute
|
|
653
|
+
let multiple = this.attrValue;
|
|
654
|
+
if (!multiple) {
|
|
655
|
+
Globals.currentExprPath = this; // Used by watch()
|
|
656
|
+
if (typeof expr === 'function') {
|
|
657
|
+
if (this.type === 4) { // Don't evaluate functions before passing them to components
|
|
658
|
+
return
|
|
442
659
|
}
|
|
660
|
+
this.watchFunction = expr; // The function that gets the expression, used for renderWatched()
|
|
661
|
+
expr = expr();
|
|
443
662
|
}
|
|
444
|
-
|
|
663
|
+
else
|
|
664
|
+
expr = Util.makePrimitive(expr);
|
|
665
|
+
Globals.currentExprPath = null;
|
|
666
|
+
}
|
|
667
|
+
if (!multiple && (expr === undefined || expr === false || expr === null)) { // Util.isFalsy() inlined.
|
|
668
|
+
if (isProp)
|
|
669
|
+
node[this.attrName] = false;
|
|
670
|
+
node.removeAttribute(this.attrName);
|
|
671
|
+
}
|
|
672
|
+
else if (!multiple && expr === true) {
|
|
673
|
+
if (isProp)
|
|
674
|
+
node[this.attrName] = true;
|
|
675
|
+
node.setAttribute(this.attrName, '');
|
|
445
676
|
}
|
|
446
|
-
else
|
|
447
|
-
value.unshift(expr);
|
|
448
677
|
|
|
449
|
-
|
|
678
|
+
// A non-toggled attribute
|
|
679
|
+
else {
|
|
450
680
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
681
|
+
// If it's a series of expressions among strings, join them together.
|
|
682
|
+
let joinedValue;
|
|
683
|
+
if (multiple) {
|
|
684
|
+
let value = [];
|
|
685
|
+
for (let i = 0; i < this.attrValue.length; i++) {
|
|
686
|
+
value.push(this.attrValue[i]);
|
|
687
|
+
if (i < this.attrValue.length - 1) {
|
|
688
|
+
Globals.currentExprPath = this; // Used by watch()
|
|
689
|
+
let val = Util.makePrimitive(exprs[i]);
|
|
690
|
+
Globals.currentExprPath = null;
|
|
691
|
+
if (!Util.isFalsy(val))
|
|
692
|
+
value.push(val);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
joinedValue = value.join('')
|
|
696
|
+
}
|
|
457
697
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
698
|
+
// If the attribute is one expression with no strings:
|
|
699
|
+
else
|
|
700
|
+
joinedValue = expr;
|
|
701
|
+
|
|
702
|
+
// Only update attributes if the value has changed.
|
|
703
|
+
// This is needed for setting input.value, .checked, option.selected, etc.
|
|
704
|
+
|
|
705
|
+
let oldVal = isProp
|
|
706
|
+
? node[this.attrName]
|
|
707
|
+
: node.getAttribute(this.attrName);
|
|
708
|
+
if (oldVal !== joinedValue) {
|
|
709
|
+
|
|
710
|
+
// <textarea value=${expr}></textarea>
|
|
711
|
+
// Without this branch we have no way to set the value of a textarea,
|
|
712
|
+
// since we also prohibit expressions that are a child of textarea.
|
|
713
|
+
if (isProp)
|
|
714
|
+
node[this.attrName] = joinedValue;
|
|
715
|
+
// TODO: Putting an 'else' here would be more performant
|
|
716
|
+
node.setAttribute(this.attrName, joinedValue);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
463
719
|
}
|
|
464
|
-
|
|
465
|
-
return exprIndex;
|
|
466
720
|
}
|
|
467
721
|
|
|
468
722
|
|
|
@@ -478,7 +732,8 @@ export default class ExprPath {
|
|
|
478
732
|
let nodeMarker, nodeBefore;
|
|
479
733
|
let root = newRoot;
|
|
480
734
|
let path = pathOffset ? this.nodeMarkerPath.slice(0, -pathOffset) : this.nodeMarkerPath;
|
|
481
|
-
|
|
735
|
+
let length = path.length-1;
|
|
736
|
+
for (let i=length; i>0; i--) // Resolve the path.
|
|
482
737
|
root = root.childNodes[path[i]];
|
|
483
738
|
let childNodes = root.childNodes;
|
|
484
739
|
|
|
@@ -522,7 +777,7 @@ export default class ExprPath {
|
|
|
522
777
|
for (let ng of path.nodeGroups) {
|
|
523
778
|
if (ng) // Can be null from apply()'s push(null) call.
|
|
524
779
|
for (let path2 of ng.paths) {
|
|
525
|
-
if (path2.type ===
|
|
780
|
+
if (path2.type === ExprPathType.Content && path2.parentNode === parentNode) {
|
|
526
781
|
path2.nodesCache = null;
|
|
527
782
|
clearChildNodeCache(path2);
|
|
528
783
|
}
|
|
@@ -537,7 +792,7 @@ export default class ExprPath {
|
|
|
537
792
|
|
|
538
793
|
/**
|
|
539
794
|
* Attempt to remove all of this ExprPath's nodes from the DOM, if it can be done using a special fast method.
|
|
540
|
-
* @returns {boolean} Returns false if Nodes
|
|
795
|
+
* @returns {boolean} Returns false if Nodes weren't removed, and they should instead be removed manually. */
|
|
541
796
|
fastClear() {
|
|
542
797
|
let parent = this.nodeBefore.parentNode;
|
|
543
798
|
if (this.nodeBefore === parent.firstChild && this.nodeMarker === parent.lastChild) {
|
|
@@ -573,6 +828,10 @@ export default class ExprPath {
|
|
|
573
828
|
// result2.push(...ng.getNodes())
|
|
574
829
|
// return result2;
|
|
575
830
|
|
|
831
|
+
if (this.type === ExprPathType.AttribValue || this.type === ExprPathType.AttribMultiple || this.type === ExprPathType.ComponentAttribValue) {
|
|
832
|
+
return [this.nodeMarker];
|
|
833
|
+
}
|
|
834
|
+
|
|
576
835
|
|
|
577
836
|
let result
|
|
578
837
|
|
|
@@ -599,7 +858,8 @@ export default class ExprPath {
|
|
|
599
858
|
return result;
|
|
600
859
|
}
|
|
601
860
|
|
|
602
|
-
|
|
861
|
+
/** @return {HTMLElement|ParentNode} */
|
|
862
|
+
getParentNode() {
|
|
603
863
|
return this.nodeMarker.parentNode
|
|
604
864
|
}
|
|
605
865
|
|
|
@@ -616,27 +876,39 @@ export default class ExprPath {
|
|
|
616
876
|
* or createa new NodeGroup from the template.
|
|
617
877
|
* @return {NodeGroup} */
|
|
618
878
|
getNodeGroup(template, exact=true) {
|
|
619
|
-
//if (exact && this.nodeGroupsFree.isEmpty())
|
|
620
|
-
// return null;
|
|
621
879
|
|
|
622
880
|
let result;
|
|
881
|
+
let collection = this.nodeGroupsAttachedAvailable;
|
|
882
|
+
|
|
883
|
+
// TODO: Would it be faster to maintain a separate list of detached nodegroups?
|
|
884
|
+
if (exact) { // [below] parentElement will be null if the parent is a DocumentFragment
|
|
885
|
+
result = collection.deleteAny(template.getExactKey());
|
|
886
|
+
if (!result) { // try searching detached
|
|
887
|
+
collection = this.nodeGroupsDetachedAvailable;
|
|
888
|
+
result = collection.deleteAny(template.getExactKey());
|
|
889
|
+
}
|
|
623
890
|
|
|
624
|
-
if (exact) {
|
|
625
|
-
result = this.nodeGroupsFree.delete(template.getExactKey());
|
|
626
891
|
if (result) // also delete the matching close key.
|
|
627
|
-
|
|
628
|
-
else
|
|
892
|
+
collection.deleteSpecific(template.getCloseKey(), result);
|
|
893
|
+
else {
|
|
629
894
|
return null;
|
|
895
|
+
}
|
|
630
896
|
}
|
|
631
897
|
|
|
632
898
|
// Find a close match.
|
|
633
899
|
// This is a match that has matching html, but different expressions applied.
|
|
634
900
|
// We can then apply the expressions to make it an exact match.
|
|
635
|
-
|
|
636
|
-
|
|
901
|
+
// If the template has no expressions, the key is the html, and we've already searched for an exact match. There won't be an inexact match.
|
|
902
|
+
else if (template.exprs.length) {
|
|
903
|
+
result = collection.deleteAny(template.getCloseKey());
|
|
904
|
+
if (!result) { // try searching detached
|
|
905
|
+
collection = this.nodeGroupsDetachedAvailable;
|
|
906
|
+
result = collection.deleteAny(template.getCloseKey());
|
|
907
|
+
}
|
|
908
|
+
|
|
637
909
|
if (result) {
|
|
638
910
|
/*#IFDEV*/assert(result.exactKey);/*#ENDIF*/
|
|
639
|
-
|
|
911
|
+
collection.deleteSpecific(result.exactKey, result);
|
|
640
912
|
|
|
641
913
|
// Update this close match with the new expression values.
|
|
642
914
|
result.applyExprs(template.exprs);
|
|
@@ -648,58 +920,70 @@ export default class ExprPath {
|
|
|
648
920
|
result = new NodeGroup(template, this);
|
|
649
921
|
|
|
650
922
|
// old:
|
|
651
|
-
this.
|
|
652
|
-
|
|
653
|
-
// new:
|
|
654
|
-
// let ngiu = this.nodeGroupsInUse;
|
|
655
|
-
// ngiu.add(result.exactKey, result);
|
|
656
|
-
// ngiu.add(result.closeKey, result);
|
|
923
|
+
this.nodeGroupsRendered.push(result);
|
|
657
924
|
|
|
658
925
|
/*#IFDEV*/assert(result.parentPath);/*#ENDIF*/
|
|
659
926
|
return result;
|
|
660
927
|
}
|
|
661
928
|
|
|
929
|
+
isComponent() {
|
|
930
|
+
// Events won't have type===Component.
|
|
931
|
+
// TODO: Have a special flag for components instead of it being on the type?
|
|
932
|
+
return this.type === ExprPathType.ComponentAttribValue || (this.attrName && this.nodeMarker.tagName && this.nodeMarker.tagName.includes('-'));
|
|
933
|
+
}
|
|
662
934
|
|
|
663
935
|
/**
|
|
936
|
+
* TODO: Rename this to nodeGroupsInUse, nodeGroupsAvialableAttached and nodeGroupsAvailableDetached?
|
|
937
|
+
* Nodes that have been used during the current render().
|
|
664
938
|
* Used with getNodeGroup() and freeNodeGroups().
|
|
665
939
|
* TODO: Use an array of WeakRef so the gc can collect them?
|
|
666
940
|
* TODO: Put items back in nodeGroupsInUse after applyExpr() is called, not before.
|
|
667
941
|
* @type {NodeGroup[]} */
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
/** @type {MultiValueMap<key:string, value:NodeGroup>} */
|
|
671
|
-
//nodeGroupsInUse = new MultiValueMap();
|
|
942
|
+
nodeGroupsRendered = [];
|
|
672
943
|
|
|
673
944
|
/**
|
|
945
|
+
* Nodes that were added to the web component during the last render(), but are available to be used again.
|
|
674
946
|
* Used with getNodeGroup() and freeNodeGroups().
|
|
675
947
|
* Each NodeGroup is here twice, once under an exact key, and once under the close key.
|
|
676
948
|
* @type {MultiValueMap<key:string, value:NodeGroup>} */
|
|
677
|
-
|
|
949
|
+
nodeGroupsAttachedAvailable = new MultiValueMap();
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Nodes that were not added to the web component during the last render(), and available to be used again.
|
|
953
|
+
* @type {MultiValueMap} */
|
|
954
|
+
nodeGroupsDetachedAvailable = new MultiValueMap();
|
|
678
955
|
|
|
679
956
|
|
|
680
957
|
/**
|
|
681
|
-
* Move everything from this.
|
|
958
|
+
* Move everything from this.nodeGroupsRendered to this.nodeGroupsAttached and nodeGroupsDetached.
|
|
959
|
+
* Called at the beginning of applyNodes() so it can have NodeGroups to use.
|
|
682
960
|
* TODO: this could run as needed in getNodeGroup? */
|
|
683
961
|
freeNodeGroups() {
|
|
684
|
-
//
|
|
685
|
-
let
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
962
|
+
// Add nodes that weren't used during render() to nodeGroupsDetached
|
|
963
|
+
let previouslyAttached = this.nodeGroupsAttachedAvailable.data;
|
|
964
|
+
let detached = this.nodeGroupsDetachedAvailable.data;
|
|
965
|
+
for (let key in previouslyAttached) {
|
|
966
|
+
let set = detached[key];
|
|
967
|
+
if (!set)
|
|
968
|
+
detached[key] = previouslyAttached[key]
|
|
969
|
+
else
|
|
970
|
+
for (let ng of previouslyAttached[key])
|
|
971
|
+
set.add(ng);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Add nodes that were used during render() to nodeGroupsRendered.
|
|
975
|
+
this.nodeGroupsAttachedAvailable = new MultiValueMap();
|
|
976
|
+
let nga = this.nodeGroupsAttachedAvailable;
|
|
977
|
+
for (let ng of this.nodeGroupsRendered) {
|
|
978
|
+
nga.add(ng.exactKey, ng);
|
|
979
|
+
nga.add(ng.closeKey, ng);
|
|
689
980
|
}
|
|
690
|
-
|
|
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();
|
|
981
|
+
|
|
982
|
+
this.nodeGroupsRendered = [];
|
|
699
983
|
}
|
|
700
984
|
|
|
701
985
|
//#IFDEV
|
|
702
|
-
|
|
986
|
+
|
|
703
987
|
get debug() {
|
|
704
988
|
return [
|
|
705
989
|
`parentNode: ${this.nodeBefore.parentNode?.tagName?.toLowerCase()}`,
|
|
@@ -712,7 +996,7 @@ export default class ExprPath {
|
|
|
712
996
|
}), 1).flat()
|
|
713
997
|
]
|
|
714
998
|
}
|
|
715
|
-
|
|
999
|
+
|
|
716
1000
|
get debugNodes() {
|
|
717
1001
|
// Clear nodesCache so that getNodes() manually gets the nodes.
|
|
718
1002
|
let nc = this.nodesCache;
|
|
@@ -721,13 +1005,13 @@ export default class ExprPath {
|
|
|
721
1005
|
this.nodesCache = nc;
|
|
722
1006
|
return result;
|
|
723
1007
|
}
|
|
724
|
-
|
|
1008
|
+
|
|
725
1009
|
verify() {
|
|
726
1010
|
if (!window.verify)
|
|
727
1011
|
return;
|
|
728
1012
|
|
|
729
|
-
assert(this.type!==
|
|
730
|
-
assert(this.type!==
|
|
1013
|
+
assert(this.type!==ExprPathType.Content || this.nodeBefore)
|
|
1014
|
+
assert(this.type!==ExprPathType.Content || this.nodeBefore.parentNode)
|
|
731
1015
|
|
|
732
1016
|
// Need either nodeMarker or parentNode
|
|
733
1017
|
assert(this.nodeMarker)
|
|
@@ -736,10 +1020,10 @@ export default class ExprPath {
|
|
|
736
1020
|
assert(!this.nodeMarker || this.nodeMarker.parentNode)
|
|
737
1021
|
|
|
738
1022
|
// nodeBefore and nodeMarker must have same parent.
|
|
739
|
-
assert(this.type!==
|
|
1023
|
+
assert(this.type!==ExprPathType.Content || this.nodeBefore.parentNode === this.nodeMarker.parentNode)
|
|
740
1024
|
|
|
741
1025
|
assert(this.nodeBefore !== this.nodeMarker)
|
|
742
|
-
assert(this.type!==
|
|
1026
|
+
assert(this.type!==ExprPathType.Content|| !this.nodeBefore.parentNode || this.nodeBefore.compareDocumentPosition(this.nodeMarker) === Node.DOCUMENT_POSITION_FOLLOWING)
|
|
743
1027
|
|
|
744
1028
|
// Detect cyclic parent and grandparent references.
|
|
745
1029
|
assert(this.parentNg?.parentPath !== this)
|
|
@@ -752,10 +1036,10 @@ export default class ExprPath {
|
|
|
752
1036
|
// Make sure the nodesCache matches the nodes.
|
|
753
1037
|
this.checkNodesCache();
|
|
754
1038
|
}
|
|
755
|
-
|
|
1039
|
+
|
|
756
1040
|
checkNodesCache() {
|
|
757
1041
|
return;
|
|
758
|
-
|
|
1042
|
+
|
|
759
1043
|
// Make sure cache is accurate.
|
|
760
1044
|
// If this is invalid, then perhaps another component append()'d one of our nodes to itself.
|
|
761
1045
|
// Or perhaps one of our nodes is used in an expression more than once.
|
|
@@ -770,14 +1054,15 @@ export default class ExprPath {
|
|
|
770
1054
|
nodes.push(current)
|
|
771
1055
|
current = current.nextSibling
|
|
772
1056
|
}
|
|
1057
|
+
|
|
1058
|
+
if (!arraySame(this.nodesCache, nodes))
|
|
1059
|
+
console.log(this.nodesCache, nodes)
|
|
773
1060
|
assert(arraySame(this.nodesCache, nodes) === true);
|
|
774
1061
|
}
|
|
775
1062
|
}
|
|
776
1063
|
//#ENDIF
|
|
777
1064
|
}
|
|
778
1065
|
|
|
779
|
-
|
|
780
|
-
|
|
781
1066
|
/**
|
|
782
1067
|
*
|
|
783
1068
|
* @param root
|
|
@@ -793,19 +1078,19 @@ function setValue(root, path, node) {
|
|
|
793
1078
|
}
|
|
794
1079
|
|
|
795
1080
|
/** @enum {int} */
|
|
796
|
-
export const
|
|
1081
|
+
export const ExprPathType = {
|
|
797
1082
|
/** Child of a node */
|
|
798
1083
|
Content: 1,
|
|
799
|
-
|
|
1084
|
+
|
|
800
1085
|
/** One or more whole attributes */
|
|
801
|
-
|
|
802
|
-
|
|
1086
|
+
AttribMultiple: 2,
|
|
1087
|
+
|
|
803
1088
|
/** Value of an attribute. */
|
|
804
|
-
|
|
805
|
-
|
|
1089
|
+
AttribValue: 3,
|
|
1090
|
+
|
|
806
1091
|
/** Value of an attribute being passed to a component. */
|
|
807
|
-
|
|
808
|
-
|
|
1092
|
+
ComponentAttribValue: 4,
|
|
1093
|
+
|
|
809
1094
|
/** Expressions inside Html comments. */
|
|
810
1095
|
Comment: 5,
|
|
811
1096
|
|
|
@@ -831,11 +1116,9 @@ export function getNodePath(node) {
|
|
|
831
1116
|
* Note that the path is backward, with the outermost element at the end.
|
|
832
1117
|
* @param root {HTMLElement|Document|DocumentFragment|ParentNode}
|
|
833
1118
|
* @param path {int[]}
|
|
834
|
-
* @returns {Node|HTMLElement} */
|
|
1119
|
+
* @returns {Node|HTMLElement|HTMLStyleElement} */
|
|
835
1120
|
export function resolveNodePath(root, path) {
|
|
836
1121
|
for (let i=path.length-1; i>=0; i--)
|
|
837
1122
|
root = root.childNodes[path[i]];
|
|
838
1123
|
return root;
|
|
839
1124
|
}
|
|
840
|
-
|
|
841
|
-
|