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