solarite 0.3.2 → 0.5.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 +2149 -2090
- package/dist/Solarite.js +1895 -1821
- package/dist/Solarite.min.js +2 -2
- package/package.json +12 -2
- package/readme.md +2 -2
- package/src/Globals.js +18 -24
- package/src/HtmlParser.js +1 -1
- package/src/MultiValueMap.js +3 -3
- package/src/NodeGroup.js +238 -483
- package/src/Path.js +212 -0
- package/src/PathToAttribValue.js +259 -0
- package/src/PathToAttribs.js +77 -0
- package/src/PathToComment.js +8 -0
- package/src/PathToComponent.js +169 -0
- package/src/PathToEvent.js +57 -0
- package/src/PathToNodes.js +566 -0
- package/src/RootNodeGroup.js +8 -0
- package/src/Shell.js +92 -83
- package/src/Solarite.d.ts +73 -29
- package/src/Solarite.js +133 -21
- package/src/Template.js +173 -42
- package/src/Util.js +74 -52
- package/src/assert.js +5 -5
- package/src/getArg.js +26 -24
- package/src/h.js +83 -128
- package/src/hash.js +11 -9
- package/src/toEl.js +83 -0
- package/src/udomdiff.js +0 -57
- package/src/watch.js +78 -81
- package/src/ExprPath.js +0 -1109
- package/src/createSolarite.js +0 -154
package/src/ExprPath.js
DELETED
|
@@ -1,1109 +0,0 @@
|
|
|
1
|
-
import {assert} from "./assert.js";
|
|
2
|
-
import delve from "./delve.js";
|
|
3
|
-
import NodeGroup from "./NodeGroup.js";
|
|
4
|
-
import Util, {setIndent} from "./Util.js";
|
|
5
|
-
import Template from "./Template.js";
|
|
6
|
-
import Globals from "./Globals.js";
|
|
7
|
-
import MultiValueMap from "./MultiValueMap.js";
|
|
8
|
-
import udomdiff from "./udomdiff.js";
|
|
9
|
-
//import {ArraySpliceOp} from "./watch.js";
|
|
10
|
-
//#IFDEV
|
|
11
|
-
var exprPathId = 0;
|
|
12
|
-
//#ENDIF
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Path to where an expression should be evaluated within a Shell or NodeGroup.
|
|
16
|
-
* Path is only valid until the expressions before it are evaluated.
|
|
17
|
-
* TODO: Make this based on parent and node instead of path? */
|
|
18
|
-
export default class ExprPath {
|
|
19
|
-
|
|
20
|
-
//#IFDEV
|
|
21
|
-
eid = exprPathId++;
|
|
22
|
-
//#ENDIF
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* @type {ExprPathType} */
|
|
26
|
-
type;
|
|
27
|
-
|
|
28
|
-
// Used for attributes:
|
|
29
|
-
|
|
30
|
-
/** @type {?string} Used only if type=AttribType.Value. */
|
|
31
|
-
attrName;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* @type {?string[]} Used only if type=AttribType.Value. If null, use one expr to set the whole attribute value. */
|
|
35
|
-
attrValue;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* @type {Set<string>} Used for type=AttribType.Multiple to remember the attributes that were added. */
|
|
39
|
-
attrNames;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* @type {Node} Node that occurs before this ExprPath's first Node.
|
|
43
|
-
* This is necessary because udomdiff() can steal nodes from another ExprPath.
|
|
44
|
-
* If we had a pointer to our own startNode then that node could be moved somewhere else w/o us knowing it.
|
|
45
|
-
* Used only for type='content'
|
|
46
|
-
* Will be null if ExprPath has no Nodes. */
|
|
47
|
-
nodeBefore;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* If type is AttribType.Multiple or AttribType.Value, points to the node having the attribute.
|
|
51
|
-
* If type is 'content', points to a node that never changes that this NodeGroup should always insert its nodes before.
|
|
52
|
-
* An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.
|
|
53
|
-
* @type {Node|HTMLElement} */
|
|
54
|
-
nodeMarker;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// These are set after an expression is assigned:
|
|
58
|
-
|
|
59
|
-
/** @type {NodeGroup} */
|
|
60
|
-
parentNg;
|
|
61
|
-
|
|
62
|
-
/** @type {NodeGroup[]} */
|
|
63
|
-
nodeGroups = [];
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// Caches to make things faster
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* @private
|
|
70
|
-
* @type {Node[]} Cached result of getNodes() */
|
|
71
|
-
nodesCache;
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* @type {int} Index of nodeBefore among its parentNode's children. */
|
|
75
|
-
nodeBeforeIndex;
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* @type {int[]} Path to the node marker, in reverse for performance reasons. */
|
|
79
|
-
nodeMarkerPath;
|
|
80
|
-
|
|
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
|
-
|
|
92
|
-
/**
|
|
93
|
-
* @param nodeBefore {Node}
|
|
94
|
-
* @param nodeMarker {?Node}
|
|
95
|
-
* @param type {ExprPathType}
|
|
96
|
-
* @param attrName {?string}
|
|
97
|
-
* @param attrValue {string[]} */
|
|
98
|
-
constructor(nodeBefore, nodeMarker, type=ExprPathType.Content, attrName=null, attrValue=null) {
|
|
99
|
-
|
|
100
|
-
// If path is a node.
|
|
101
|
-
this.nodeBefore = nodeBefore;
|
|
102
|
-
this.nodeMarker = nodeMarker;
|
|
103
|
-
this.type = type;
|
|
104
|
-
this.attrName = attrName;
|
|
105
|
-
this.attrValue = attrValue;
|
|
106
|
-
if (type === ExprPathType.AttribMultiple)
|
|
107
|
-
this.attrNames = new Set();
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Apply any type of expression.
|
|
112
|
-
* This calls other apply functions.
|
|
113
|
-
*
|
|
114
|
-
* One very messy part of this function is that it may apply multiple expressions if they're all part
|
|
115
|
-
* of the same attribute value.
|
|
116
|
-
*
|
|
117
|
-
* We should modify path.applyValueAttrib so it stores the procssed parts and then only calls
|
|
118
|
-
* setAttribute() once all the pieces are in place.
|
|
119
|
-
*
|
|
120
|
-
* @param exprs {Expr[]}
|
|
121
|
-
* @param freeNodeGroups {boolean} */
|
|
122
|
-
apply(exprs, freeNodeGroups=true) {
|
|
123
|
-
switch (this.type) {
|
|
124
|
-
case 1: // PathType.Content:
|
|
125
|
-
this.applyNodes(exprs[0], freeNodeGroups);
|
|
126
|
-
break;
|
|
127
|
-
case 2: // PathType.Multiple:
|
|
128
|
-
this.applyMultipleAttribs(this.nodeMarker, exprs[0]);
|
|
129
|
-
break;
|
|
130
|
-
case 5: // PathType.Comment:
|
|
131
|
-
// Expressions inside Html comments. Deliberately empty because we won't waste time updating them.
|
|
132
|
-
break;
|
|
133
|
-
case 6: // PathType.Event:
|
|
134
|
-
this.applyEventAttrib(this.nodeMarker, exprs[0], this.parentNg.rootNg.root);
|
|
135
|
-
break;
|
|
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);
|
|
139
|
-
break;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Insert/replace the nodes created by a single expression.
|
|
145
|
-
* Called by applyExprs()
|
|
146
|
-
* This function is recursive, as the functions it calls also call it.
|
|
147
|
-
* @param expr {Expr}
|
|
148
|
-
* @param freeNodeGroups {boolean}
|
|
149
|
-
* @return {Node[]} New Nodes created. */
|
|
150
|
-
applyNodes(expr, freeNodeGroups=true) {
|
|
151
|
-
let path = this;
|
|
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
|
-
|
|
159
|
-
/*#IFDEV*/path.verify();/*#ENDIF*/
|
|
160
|
-
|
|
161
|
-
/** @type {(Node|NodeGroup|Expr)[]} */
|
|
162
|
-
let newNodes = [];
|
|
163
|
-
let oldNodeGroups = path.nodeGroups;
|
|
164
|
-
/*#IFDEV*/assert(!oldNodeGroups.includes(null))/*#ENDIF*/
|
|
165
|
-
let secondPass = []; // indices
|
|
166
|
-
|
|
167
|
-
path.nodeGroups = []; // Reset before applyExactNodes and the code below rebuilds it.
|
|
168
|
-
path.applyExactNodes(expr, newNodes, secondPass);
|
|
169
|
-
|
|
170
|
-
//this.existingTextNodes = null;
|
|
171
|
-
|
|
172
|
-
// TODO: Create an array of old vs Nodes and NodeGroups together.
|
|
173
|
-
// If they're all the same, skip the next steps.
|
|
174
|
-
// Or calculate it in the loop above as we go? Have a path.lastNodeGroups property?
|
|
175
|
-
|
|
176
|
-
// Second pass to find close-match NodeGroups.
|
|
177
|
-
let flatten = false;
|
|
178
|
-
if (secondPass.length) {
|
|
179
|
-
for (let [nodesIndex, ngIndex] of secondPass) {
|
|
180
|
-
let ng = path.getNodeGroup(newNodes[nodesIndex], false);
|
|
181
|
-
|
|
182
|
-
let ngNodes = ng.getNodes();
|
|
183
|
-
|
|
184
|
-
/*#IFDEV*/assert(!(newNodes[nodesIndex] instanceof NodeGroup))/*#ENDIF*/
|
|
185
|
-
|
|
186
|
-
if (ngNodes.length === 1) // flatten manually so we can skip flattening below.
|
|
187
|
-
newNodes[nodesIndex] = ngNodes[0];
|
|
188
|
-
|
|
189
|
-
else {
|
|
190
|
-
newNodes[nodesIndex] = ngNodes;
|
|
191
|
-
flatten = true;
|
|
192
|
-
}
|
|
193
|
-
path.nodeGroups[ngIndex] = ng;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (flatten)
|
|
197
|
-
newNodes = newNodes.flat(); // Only if second pass happens.
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/*#IFDEV*/assert(!path.nodeGroups.includes(null))/*#ENDIF*/
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
let oldNodes = path.getNodes();
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
// This pre-check makes it a few percent faster?
|
|
208
|
-
let same = Util.arraySame(oldNodes, newNodes);
|
|
209
|
-
if (!same) {
|
|
210
|
-
|
|
211
|
-
path.nodesCache = newNodes; // Replaces value set by path.getNodes()
|
|
212
|
-
|
|
213
|
-
if (this.parentNg.parentPath)
|
|
214
|
-
this.parentNg.parentPath.clearNodesCache();
|
|
215
|
-
|
|
216
|
-
// Fast clear method
|
|
217
|
-
let isNowEmpty = oldNodes.length && !newNodes.length;
|
|
218
|
-
if (!isNowEmpty || !path.fastClear())
|
|
219
|
-
|
|
220
|
-
// Rearrange nodes.
|
|
221
|
-
udomdiff(path.nodeMarker.parentNode, oldNodes, newNodes, path.nodeMarker)
|
|
222
|
-
|
|
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();
|
|
230
|
-
|
|
231
|
-
// Instantiate components created within ${...} expressions.
|
|
232
|
-
// Embedded style tags are handled elsewhere, but where?
|
|
233
|
-
for (let el of newNodes) {
|
|
234
|
-
if (el instanceof HTMLElement) {
|
|
235
|
-
if (el.hasAttribute('solarite-placeholder'))
|
|
236
|
-
this.parentNg.instantiateComponent(el);
|
|
237
|
-
for (let child of el.querySelectorAll('[solarite-placeholder]'))
|
|
238
|
-
this.parentNg.instantiateComponent(child);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
/*#IFDEV*/path.verify();/*#ENDIF*/
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Used by watch() for inserting/removing/replacing individual loop items.
|
|
249
|
-
* @param op {ArraySpliceOp} */
|
|
250
|
-
applyArrayOp(op) {
|
|
251
|
-
|
|
252
|
-
// Replace NodeGroups
|
|
253
|
-
let replaceCount = Math.min(op.deleteCount, op.items.length);
|
|
254
|
-
let deleteCount = op.deleteCount - replaceCount;
|
|
255
|
-
for (let i=0; i<replaceCount; i++) {
|
|
256
|
-
let oldNg = this.nodeGroups[op.index + i]; // TODO: One expr can create multiple nodegroups.
|
|
257
|
-
|
|
258
|
-
// Try to find an exact match
|
|
259
|
-
let func = this.mapCallback || this.watchFunction;
|
|
260
|
-
let expr = func(op.items[i]);
|
|
261
|
-
|
|
262
|
-
// If the result of func isn't a template, conver it to one or more templates.
|
|
263
|
-
this.exprToTemplates(expr, template => { // TODO: An expr can create multiple NodeGroups. I need a way to group them.
|
|
264
|
-
|
|
265
|
-
let ng = this.getNodeGroup(template, true); // Removes from nodeGroupsAttached and adds to nodeGroupsRendered()
|
|
266
|
-
if (ng && ng === oldNg) {
|
|
267
|
-
// It's an exact match, so replace nothing.
|
|
268
|
-
// TODO: What if the found NodeGroup as at a differnet place?
|
|
269
|
-
} else {
|
|
270
|
-
|
|
271
|
-
// Find a close match or create a new node group
|
|
272
|
-
if (!ng)
|
|
273
|
-
ng = this.getNodeGroup(template, false); // adds back to nodeGroupsRendered()
|
|
274
|
-
this.nodeGroups[op.index + i] = ng; // TODO: Remove old one to nodeGroupsDetached?
|
|
275
|
-
|
|
276
|
-
// Splice in the new nodes.
|
|
277
|
-
let insertBefore = oldNg.startNode;
|
|
278
|
-
for (let node of ng.getNodes())
|
|
279
|
-
insertBefore.parentNode.insertBefore(node, insertBefore);
|
|
280
|
-
|
|
281
|
-
// Remove the old nodes.
|
|
282
|
-
if (ng !== oldNg)
|
|
283
|
-
oldNg.removeAndSaveOrphans();
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Delete extra at the end.
|
|
289
|
-
if (deleteCount > 0) {
|
|
290
|
-
for (let i=0; i<deleteCount; i++) {
|
|
291
|
-
let oldNg = this.nodeGroups[op.index + replaceCount + i];
|
|
292
|
-
oldNg.removeAndSaveOrphans();
|
|
293
|
-
}
|
|
294
|
-
this.nodeGroups.splice(op.index + replaceCount, deleteCount);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Add extra at the end.
|
|
298
|
-
else {
|
|
299
|
-
let newItems = op.items.slice(replaceCount);
|
|
300
|
-
|
|
301
|
-
let insertBefore = this.nodeGroups[op.index + replaceCount]?.startNode || this.nodeMarker;
|
|
302
|
-
for (let i = 0; i < newItems.length; i++) { // We use nodeMarker if the subequent (or all) nodeGroups have been removed.
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
// Try to find exact match
|
|
306
|
-
let template = this.mapCallback(newItems[i]);
|
|
307
|
-
let ng = this.getNodeGroup(template, true); // Removes from nodeGroupsAttached and adds to nodeGroupsRendered()
|
|
308
|
-
if (!ng) // Find a close match or create a new node group
|
|
309
|
-
ng = this.getNodeGroup(template, false); // adds back to nodeGroupsRendered()
|
|
310
|
-
|
|
311
|
-
this.nodeGroups.push(ng);
|
|
312
|
-
|
|
313
|
-
// Splice in the new nodes.
|
|
314
|
-
for (let node of ng.getNodes())
|
|
315
|
-
insertBefore.parentNode.insertBefore(node, insertBefore);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
//#IFDEV
|
|
320
|
-
assert(this.nodeGroups.length === op.array.length);
|
|
321
|
-
//#ENDIF
|
|
322
|
-
|
|
323
|
-
// TODO: update or invalidate the nodes cache?
|
|
324
|
-
this.nodesCache = null;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Recursively traverse expr.
|
|
329
|
-
* If a value is a function, evaluate it.
|
|
330
|
-
* If a value is an array, recurse on each item.
|
|
331
|
-
* If it's a primitive, convert it to a Template.
|
|
332
|
-
* Otherwise pass the item (which is now either a Template or a Node) to callback.
|
|
333
|
-
* @param expr
|
|
334
|
-
* @param callback {function(Node|Template)}
|
|
335
|
-
*
|
|
336
|
-
* TODO: have applyExactNodes() use this function. */
|
|
337
|
-
exprToTemplates(expr, callback) {
|
|
338
|
-
if (Array.isArray(expr))
|
|
339
|
-
for (let subExpr of expr)
|
|
340
|
-
this.exprToTemplates(subExpr, callback);
|
|
341
|
-
|
|
342
|
-
else if (typeof expr === 'function') {
|
|
343
|
-
// TODO: One ExprPath can have multiple expr functions.
|
|
344
|
-
// But if using it as a watch, it should only have one at the top level.
|
|
345
|
-
// So maybe this is ok.
|
|
346
|
-
Globals.currentExprPath = this; // Used by watch()
|
|
347
|
-
|
|
348
|
-
this.watchFunction = expr; // TODO: Only do this if it's a top level function.
|
|
349
|
-
expr = expr(); // As expr accesses watched variables, watch() uses Globals.currentExprPath to mark where those watched variables are being used.
|
|
350
|
-
Globals.currentExprPath = null;
|
|
351
|
-
|
|
352
|
-
this.exprToTemplates(expr, callback);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// String/Number/Date/Boolean
|
|
356
|
-
else if (!(expr instanceof Template) && !(expr instanceof Node)){
|
|
357
|
-
// Convert expression to a string.
|
|
358
|
-
if (expr === undefined || expr === false || expr === null) // Util.isFalsy() inlined
|
|
359
|
-
expr = '';
|
|
360
|
-
else if (typeof expr !== 'string')
|
|
361
|
-
expr += '';
|
|
362
|
-
|
|
363
|
-
// Get the same Template for the same string each time.
|
|
364
|
-
// let template = Globals.stringTemplates[expr];
|
|
365
|
-
// if (!template) {
|
|
366
|
-
let template = new Template([expr], []);
|
|
367
|
-
// Globals.stringTemplates[expr] = template;
|
|
368
|
-
//}
|
|
369
|
-
|
|
370
|
-
// Recurse.
|
|
371
|
-
this.exprToTemplates(template, callback);
|
|
372
|
-
}
|
|
373
|
-
else
|
|
374
|
-
callback(expr);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Try to apply Nodes that are an exact match, by finding existing nodes from the last render
|
|
380
|
-
* that have the same value as created by the expr.
|
|
381
|
-
* This is called from ExprPath.applyNodes().
|
|
382
|
-
*
|
|
383
|
-
* @param expr {Template|Node|Array|function|*}
|
|
384
|
-
* @param newNodes {(Node|Template)[]} An inout parameter; we add the nodes here as we go.
|
|
385
|
-
* @param secondPass {[int, int][]} Locations within newNodes for ExprPath.applyNodes() to evaluate later,
|
|
386
|
-
* when it tries to find partial matches. */
|
|
387
|
-
applyExactNodes(expr, newNodes, secondPass) {
|
|
388
|
-
|
|
389
|
-
if (expr instanceof Template) {
|
|
390
|
-
let ng = this.getNodeGroup(expr, true);
|
|
391
|
-
if (ng) {
|
|
392
|
-
|
|
393
|
-
// TODO: Track ranges of changed nodes and only pass those to udomdiff?
|
|
394
|
-
// But will that break the swap benchmark?
|
|
395
|
-
newNodes.push(...ng.getNodes());
|
|
396
|
-
this.nodeGroups.push(ng);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// If expression, mark it to be evaluated later in ExprPath.apply() to find partial match.
|
|
400
|
-
else {
|
|
401
|
-
secondPass.push([newNodes.length, this.nodeGroups.length])
|
|
402
|
-
newNodes.push(expr)
|
|
403
|
-
this.nodeGroups.push(null); // placeholder
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Node(s) created by an expression.
|
|
408
|
-
else if (expr instanceof Node) {
|
|
409
|
-
|
|
410
|
-
// DocumentFragment created by an expression.
|
|
411
|
-
if (expr instanceof DocumentFragment)
|
|
412
|
-
newNodes.push(...expr.childNodes);
|
|
413
|
-
else
|
|
414
|
-
newNodes.push(expr);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Arrays and functions.
|
|
418
|
-
// I tried iterating over the result of a generator function to avoid this recursion and simplify the code,
|
|
419
|
-
// but that consistently made the js-framework-benchmarks a few percentage points slower.
|
|
420
|
-
else
|
|
421
|
-
this.exprToTemplates(expr, template => {
|
|
422
|
-
this.applyExactNodes(template, newNodes, secondPass);
|
|
423
|
-
})
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
applyMultipleAttribs(node, expr) {
|
|
427
|
-
/*#IFDEV*/assert(this.type === ExprPathType.AttribMultiple);/*#ENDIF*/
|
|
428
|
-
|
|
429
|
-
if (Array.isArray(expr))
|
|
430
|
-
expr = expr.flat().join(' '); // flat and join so we can accept arrays of arrays of strings.
|
|
431
|
-
|
|
432
|
-
// Add new attributes
|
|
433
|
-
let oldNames = this.attrNames;
|
|
434
|
-
this.attrNames = new Set();
|
|
435
|
-
if (expr) {
|
|
436
|
-
if (typeof expr === 'function') {
|
|
437
|
-
Globals.currentExprPath = this; // Used by watch()
|
|
438
|
-
this.watchFunction = expr; // used by renderWatched()
|
|
439
|
-
expr = expr();
|
|
440
|
-
Globals.currentExprPath = null;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Attribute as name: value object.
|
|
444
|
-
if (typeof expr === 'object') {
|
|
445
|
-
for (let name in expr) {
|
|
446
|
-
let value = expr[name];
|
|
447
|
-
if (value === undefined || value === false || value === null)
|
|
448
|
-
continue;
|
|
449
|
-
node.setAttribute(name, value);
|
|
450
|
-
this.attrNames.add(name)
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Attributes as string
|
|
455
|
-
else {
|
|
456
|
-
let attrs = (expr + '') // Split string into multiple attributes.
|
|
457
|
-
.split(/([\w-]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s]+))/g)
|
|
458
|
-
.map(text => text.trim())
|
|
459
|
-
.filter(text => text.length);
|
|
460
|
-
|
|
461
|
-
for (let attr of attrs) {
|
|
462
|
-
let [name, value] = attr.split(/\s*=\s*/); // split on first equals.
|
|
463
|
-
value = (value || '').replace(/^(['"])(.*)\1$/, '$2'); // trim value quotes if they match.
|
|
464
|
-
node.setAttribute(name, value);
|
|
465
|
-
this.attrNames.add(name)
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Remove old attributes.
|
|
471
|
-
for (let oldName of oldNames)
|
|
472
|
-
if (!this.attrNames.has(oldName))
|
|
473
|
-
node.removeAttribute(oldName);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* Handle attributes for event binding, such as:
|
|
478
|
-
* onclick=${(e, el) => this.doSomething(el, 'meow')}
|
|
479
|
-
* oninput=${[this.doSomething, 'meow']}
|
|
480
|
-
* onclick=${[this, 'doSomething', 'meow']}
|
|
481
|
-
*
|
|
482
|
-
* @param node
|
|
483
|
-
* @param expr
|
|
484
|
-
* @param root */
|
|
485
|
-
applyEventAttrib(node, expr, root) {
|
|
486
|
-
/*#IFDEV*/
|
|
487
|
-
assert(this.type === ExprPathType.Event/* || this.type === PathType.Component*/);
|
|
488
|
-
assert(root instanceof HTMLElement);
|
|
489
|
-
/*#ENDIF*/
|
|
490
|
-
|
|
491
|
-
let eventName = this.attrName.slice(2); // remove "on-" prefix.
|
|
492
|
-
let func;
|
|
493
|
-
let args = [];
|
|
494
|
-
|
|
495
|
-
// Convert array to function.
|
|
496
|
-
// oninput=${[this.doSomething, 'meow']}
|
|
497
|
-
if (Array.isArray(expr) && typeof expr[0] === 'function') {
|
|
498
|
-
func = expr[0];
|
|
499
|
-
args = expr.slice(1);
|
|
500
|
-
}
|
|
501
|
-
else if (typeof expr === 'function')
|
|
502
|
-
func = expr;
|
|
503
|
-
else
|
|
504
|
-
throw new Error(`Invalid event binding: <${node.tagName.toLowerCase()} ${this.attrName}=\${${JSON.stringify(expr)}}>`);
|
|
505
|
-
|
|
506
|
-
this.bindEvent(node, root, eventName, eventName, func, args);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Call function when eventName is triggerd on node.
|
|
512
|
-
* @param node {HTMLElement}
|
|
513
|
-
* @param root {HTMLElement}
|
|
514
|
-
* @param key {string}
|
|
515
|
-
* @param eventName {string}
|
|
516
|
-
* @param func {function}
|
|
517
|
-
* @param args {array}
|
|
518
|
-
* @param capture {boolean} */
|
|
519
|
-
bindEvent(node, root, key, eventName, func, args, capture=false) {
|
|
520
|
-
let nodeEvents = Globals.nodeEvents.get(node);
|
|
521
|
-
if (!nodeEvents) {
|
|
522
|
-
nodeEvents = {[key]: new Array(3)};
|
|
523
|
-
Globals.nodeEvents.set(node, nodeEvents);
|
|
524
|
-
}
|
|
525
|
-
let nodeEvent = nodeEvents[key];
|
|
526
|
-
if (!nodeEvent)
|
|
527
|
-
nodeEvents[key] = nodeEvent = new Array(3);
|
|
528
|
-
|
|
529
|
-
if (typeof func !== 'function')
|
|
530
|
-
throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${${func}}> because it's not a function.`);
|
|
531
|
-
|
|
532
|
-
// If function has changed, remove and rebind the event.
|
|
533
|
-
if (nodeEvent[0] !== func) {
|
|
534
|
-
|
|
535
|
-
// TODO: We should be removing event listeners when calling getNodeGroup(),
|
|
536
|
-
// when we get the node from the list of nodeGroupsAttached/nodeGroupsDetached,
|
|
537
|
-
// instead of only when we rebind an event.
|
|
538
|
-
let [existing, existingBound, _] = nodeEvent;
|
|
539
|
-
if (existing)
|
|
540
|
-
node.removeEventListener(eventName, existingBound, capture);
|
|
541
|
-
|
|
542
|
-
let originalFunc = func;
|
|
543
|
-
|
|
544
|
-
// BoundFunc sets the "this" variable to be the current Solarite component.
|
|
545
|
-
let boundFunc = (event) => {
|
|
546
|
-
let args = nodeEvent[2];
|
|
547
|
-
return originalFunc.call(root, ...args, event, node);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// Save both the original and bound functions.
|
|
551
|
-
// Original so we can compare it against a newly assigned function.
|
|
552
|
-
// Bound so we can use it with removeEventListner().
|
|
553
|
-
nodeEvent[0] = originalFunc;
|
|
554
|
-
nodeEvent[1] = boundFunc;
|
|
555
|
-
|
|
556
|
-
node.addEventListener(eventName, boundFunc, capture);
|
|
557
|
-
|
|
558
|
-
// TODO: classic event attribs?
|
|
559
|
-
//el[attr.name] = e => // e.g. el.onclick = ... // put "event", "el", and "this" in scope for the event code.
|
|
560
|
-
// (new Function('event', 'el', attr.value)).bind(this.manager.rootEl)(e, el)
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// Otherwise just update the args to the function.
|
|
564
|
-
nodeEvents[key][2] = args;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
/**
|
|
568
|
-
* Handle values, including two-way binding.
|
|
569
|
-
* @param node
|
|
570
|
-
* @param exprs */
|
|
571
|
-
// TODO: node is always this.nodeMarker?
|
|
572
|
-
applyValueAttrib(node, exprs) {
|
|
573
|
-
let expr = exprs[0];
|
|
574
|
-
|
|
575
|
-
// Two-way binding between attributes
|
|
576
|
-
// Passing a path to the value attribute.
|
|
577
|
-
// Copies the attribute to the property when the input event fires.
|
|
578
|
-
// value=${[this, 'value]'}
|
|
579
|
-
// checked=${[this, 'isAgree']}
|
|
580
|
-
// This same logic is in NodeGroup.instantiateComponent() for components.
|
|
581
|
-
if (Util.isPath(expr)) {
|
|
582
|
-
let [obj, path] = [expr[0], expr.slice(1)];
|
|
583
|
-
|
|
584
|
-
if (!obj)
|
|
585
|
-
throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${[${expr.map(item => item ? `'${item}'` : item+'').join(', ')}]}>.`);
|
|
586
|
-
|
|
587
|
-
let value = delve(obj, path);
|
|
588
|
-
|
|
589
|
-
// Special case to allow setting select-multiple value from an array
|
|
590
|
-
if (this.attrName === 'value' && node.type === 'select-multiple' && Array.isArray(value)) {
|
|
591
|
-
// Set the .selected property on the options having a value within value.
|
|
592
|
-
let strValues = value.map(v => v + '');
|
|
593
|
-
for (let option of node.options)
|
|
594
|
-
option.selected = strValues.includes(option.value)
|
|
595
|
-
}
|
|
596
|
-
else {
|
|
597
|
-
// TODO: should we remove isFalsy, since these are always props?
|
|
598
|
-
let strValue = Util.isFalsy(value) ? '' : value;
|
|
599
|
-
|
|
600
|
-
// If we don't have this condition, when we call render(), the browser will scroll to the currently
|
|
601
|
-
// selected item in a <select> and mess up manually scrolling to a different value.
|
|
602
|
-
if (strValue !== node[this.attrName])
|
|
603
|
-
node[this.attrName] = strValue;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
// TODO: We need to remove any old listeners, like in bindEventAttribute.
|
|
607
|
-
// Does bindEvent() now handle that?
|
|
608
|
-
let func = () => {
|
|
609
|
-
let value = (this.attrName === 'value')
|
|
610
|
-
? Util.getInputValue(node)
|
|
611
|
-
: node[this.attrName];
|
|
612
|
-
delve(obj, path, value);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// We use capture so we update the values before other events added by the user.
|
|
616
|
-
// TODO: Bind to scroll events also?
|
|
617
|
-
// What about resize events and width/height?
|
|
618
|
-
this.bindEvent(node, path[0], this.attrName, 'input', func, [], true);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// Regular attribute
|
|
622
|
-
else {
|
|
623
|
-
// TODO: Cache this on ExprPath.isProp when Shell creates the props. Have ExprPath.clone() copy .isProp
|
|
624
|
-
// Or make it a new PathType.
|
|
625
|
-
//if (this.attrName === 'disabled')
|
|
626
|
-
// debugger;
|
|
627
|
-
|
|
628
|
-
// hasOwnProperty() checks only the object, not the parents
|
|
629
|
-
// this.attrName in node checks the node and the parents.
|
|
630
|
-
// This version checks the html element it extends from, to see if has a setter set:
|
|
631
|
-
// Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), this.attrName)?.set
|
|
632
|
-
//let isProp = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), this.attrName)?.set;
|
|
633
|
-
let isProp = this.isHtmlProperty;
|
|
634
|
-
if (isProp === undefined)
|
|
635
|
-
isProp = this.isHtmlProperty = Util.isHtmlProp(node, this.attrName);
|
|
636
|
-
|
|
637
|
-
// Values to toggle an attribute
|
|
638
|
-
let multiple = this.attrValue;
|
|
639
|
-
if (!multiple) {
|
|
640
|
-
Globals.currentExprPath = this; // Used by watch()
|
|
641
|
-
if (typeof expr === 'function') {
|
|
642
|
-
if (this.type === 4) { // Don't evaluate functions before passing them to components
|
|
643
|
-
return
|
|
644
|
-
}
|
|
645
|
-
this.watchFunction = expr; // The function that gets the expression, used for renderWatched()
|
|
646
|
-
expr = expr();
|
|
647
|
-
}
|
|
648
|
-
else
|
|
649
|
-
expr = Util.makePrimitive(expr);
|
|
650
|
-
Globals.currentExprPath = null;
|
|
651
|
-
}
|
|
652
|
-
if (!multiple && (expr === undefined || expr === false || expr === null)) { // Util.isFalsy() inlined.
|
|
653
|
-
if (isProp)
|
|
654
|
-
node[this.attrName] = false;
|
|
655
|
-
node.removeAttribute(this.attrName);
|
|
656
|
-
}
|
|
657
|
-
else if (!multiple && expr === true) {
|
|
658
|
-
if (isProp)
|
|
659
|
-
node[this.attrName] = true;
|
|
660
|
-
node.setAttribute(this.attrName, '');
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// A non-toggled attribute
|
|
664
|
-
else {
|
|
665
|
-
|
|
666
|
-
// If it's a series of expressions among strings, join them together.
|
|
667
|
-
let joinedValue;
|
|
668
|
-
if (multiple) {
|
|
669
|
-
let value = [];
|
|
670
|
-
for (let i = 0; i < this.attrValue.length; i++) {
|
|
671
|
-
value.push(this.attrValue[i]);
|
|
672
|
-
if (i < this.attrValue.length - 1) {
|
|
673
|
-
Globals.currentExprPath = this; // Used by watch()
|
|
674
|
-
let val = Util.makePrimitive(exprs[i]);
|
|
675
|
-
Globals.currentExprPath = null;
|
|
676
|
-
if (!Util.isFalsy(val))
|
|
677
|
-
value.push(val);
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
joinedValue = value.join('')
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
// If the attribute is one expression with no strings:
|
|
684
|
-
else
|
|
685
|
-
joinedValue = expr;
|
|
686
|
-
|
|
687
|
-
// Only update attributes if the value has changed.
|
|
688
|
-
// This is needed for setting input.value, .checked, option.selected, etc.
|
|
689
|
-
|
|
690
|
-
let oldVal = isProp
|
|
691
|
-
? node[this.attrName]
|
|
692
|
-
: node.getAttribute(this.attrName);
|
|
693
|
-
if (oldVal !== joinedValue) {
|
|
694
|
-
|
|
695
|
-
// <textarea value=${expr}></textarea>
|
|
696
|
-
// Without this branch we have no way to set the value of a textarea,
|
|
697
|
-
// since we also prohibit expressions that are a child of textarea.
|
|
698
|
-
if (isProp)
|
|
699
|
-
node[this.attrName] = joinedValue;
|
|
700
|
-
// TODO: Putting an 'else' here would be more performant
|
|
701
|
-
node.setAttribute(this.attrName, joinedValue);
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
/**
|
|
709
|
-
*
|
|
710
|
-
* @param newRoot {HTMLElement}
|
|
711
|
-
* @param pathOffset {int}
|
|
712
|
-
* @return {ExprPath} */
|
|
713
|
-
clone(newRoot, pathOffset=0) {
|
|
714
|
-
/*#IFDEV*/this.verify();/*#ENDIF*/
|
|
715
|
-
|
|
716
|
-
// Resolve node paths.
|
|
717
|
-
let nodeMarker, nodeBefore;
|
|
718
|
-
let root = newRoot;
|
|
719
|
-
let path = pathOffset ? this.nodeMarkerPath.slice(0, -pathOffset) : this.nodeMarkerPath;
|
|
720
|
-
let length = path.length-1;
|
|
721
|
-
for (let i=length; i>0; i--) // Resolve the path.
|
|
722
|
-
root = root.childNodes[path[i]];
|
|
723
|
-
let childNodes = root.childNodes;
|
|
724
|
-
|
|
725
|
-
nodeMarker = path.length ? childNodes[path[0]] : newRoot;
|
|
726
|
-
if (this.nodeBefore)
|
|
727
|
-
nodeBefore = childNodes[this.nodeBeforeIndex];
|
|
728
|
-
|
|
729
|
-
let result = new ExprPath(nodeBefore, nodeMarker, this.type, this.attrName, this.attrValue);
|
|
730
|
-
|
|
731
|
-
//#IFDEV
|
|
732
|
-
result.nodeMarker.exprPath = result;
|
|
733
|
-
if (result.nodeBefore)
|
|
734
|
-
result.nodeBefore.prevExprPath = result;
|
|
735
|
-
result.verify();
|
|
736
|
-
//#ENDIF
|
|
737
|
-
|
|
738
|
-
return result;
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
/**
|
|
742
|
-
* Clear the nodeCache of this ExprPath, as well as all parent and child ExprPaths that
|
|
743
|
-
* share the same DOM parent node.
|
|
744
|
-
*
|
|
745
|
-
* TODO: Is recursive clearing ever necessary? */
|
|
746
|
-
clearNodesCache() {
|
|
747
|
-
let path = this;
|
|
748
|
-
|
|
749
|
-
// Clear cache parent ExprPaths that have the same parentNode
|
|
750
|
-
let parentNode = this.nodeMarker.parentNode;
|
|
751
|
-
while (path && path.nodeMarker.parentNode === parentNode) {
|
|
752
|
-
path.nodesCache = null;
|
|
753
|
-
path = path.parentNg?.parentPath
|
|
754
|
-
|
|
755
|
-
// If stuck in an infinite loop here, the problem is likely due to Template hash colisions.
|
|
756
|
-
// Which cause one path to be the descendant of itself, creating a cycle.
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
function clearChildNodeCache(path) {
|
|
760
|
-
|
|
761
|
-
// Clear cache of child ExprPaths that have the same parentNode
|
|
762
|
-
for (let ng of path.nodeGroups) {
|
|
763
|
-
if (ng) // Can be null from apply()'s push(null) call.
|
|
764
|
-
for (let path2 of ng.paths) {
|
|
765
|
-
if (path2.type === ExprPathType.Content && path2.parentNode === parentNode) {
|
|
766
|
-
path2.nodesCache = null;
|
|
767
|
-
clearChildNodeCache(path2);
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
// Commented out on Sep 30, 2024 b/c it was making the benchmark never finish when adding 10k rows.
|
|
774
|
-
//clearChildNodeCache(this);
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
/**
|
|
779
|
-
* Attempt to remove all of this ExprPath's nodes from the DOM, if it can be done using a special fast method.
|
|
780
|
-
* @returns {boolean} Returns false if Nodes weren't removed, and they should instead be removed manually. */
|
|
781
|
-
fastClear() {
|
|
782
|
-
let parent = this.nodeBefore.parentNode;
|
|
783
|
-
if (this.nodeBefore === parent.firstChild && this.nodeMarker === parent.lastChild) {
|
|
784
|
-
|
|
785
|
-
// If parent is the only child of the grandparent, replace the whole parent.
|
|
786
|
-
// And if it has no siblings, it's not created by a NodeGroup/path.
|
|
787
|
-
// Commented out because this will break any references.
|
|
788
|
-
// And because I don't see much performance difference.
|
|
789
|
-
// let grandparent = parent.parentNode
|
|
790
|
-
// if (grandparent && parent === grandparent.firstChild && parent === grandparent.lastChild && !parent.hasAttribute('id')) {
|
|
791
|
-
// let replacement = document.createElement(parent.tagName)
|
|
792
|
-
// replacement.append(this.nodeBefore, this.nodeMarker)
|
|
793
|
-
// for (let attrib of parent.attributes)
|
|
794
|
-
// replacement.setAttribute(attrib.name, attrib.value)
|
|
795
|
-
// parent.replaceWith(replacement)
|
|
796
|
-
// }
|
|
797
|
-
// else {
|
|
798
|
-
parent.innerHTML = ''; // Faster than calling .removeChild() a thousand times.
|
|
799
|
-
parent.append(this.nodeBefore, this.nodeMarker)
|
|
800
|
-
//}
|
|
801
|
-
return true;
|
|
802
|
-
}
|
|
803
|
-
return false;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
/**
|
|
807
|
-
* @return {(Node|HTMLElement)[]} */
|
|
808
|
-
getNodes() {
|
|
809
|
-
|
|
810
|
-
// Why doesn't this work?
|
|
811
|
-
// let result2 = [];
|
|
812
|
-
// for (let ng of this.nodeGroups)
|
|
813
|
-
// result2.push(...ng.getNodes())
|
|
814
|
-
// return result2;
|
|
815
|
-
|
|
816
|
-
if (this.type === ExprPathType.AttribValue || this.type === ExprPathType.AttribMultiple || this.type === ExprPathType.ComponentAttribValue) {
|
|
817
|
-
return [this.nodeMarker];
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
let result
|
|
822
|
-
|
|
823
|
-
// This shaves about 5ms off the partialUpdate benchmark.
|
|
824
|
-
result = this.nodesCache;
|
|
825
|
-
if (result) {
|
|
826
|
-
|
|
827
|
-
//#IFDEV
|
|
828
|
-
this.checkNodesCache();
|
|
829
|
-
//#ENDIF
|
|
830
|
-
|
|
831
|
-
return result
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
result = [];
|
|
835
|
-
let current = this.nodeBefore.nextSibling;
|
|
836
|
-
let nodeMarker = this.nodeMarker;
|
|
837
|
-
while (current && current !== nodeMarker) {
|
|
838
|
-
result.push(current)
|
|
839
|
-
current = current.nextSibling
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
this.nodesCache = result;
|
|
843
|
-
return result;
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
/** @return {HTMLElement|ParentNode} */
|
|
847
|
-
getParentNode() {
|
|
848
|
-
return this.nodeMarker.parentNode
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
/**
|
|
852
|
-
* Get an unused NodeGroup that matches the template's html and expressions (exact=true)
|
|
853
|
-
* or at least the html (exact=false).
|
|
854
|
-
* Remove it from nodeGroupsFree if it exists, or create it if not.
|
|
855
|
-
* Then add it to nodeGroupsInUse.
|
|
856
|
-
*
|
|
857
|
-
* @param template {Template}
|
|
858
|
-
* @param exact {boolean}
|
|
859
|
-
* If true, return an exact match, or null.
|
|
860
|
-
* If false, either find a match for the template's html and then apply the template's expressions,
|
|
861
|
-
* or createa new NodeGroup from the template.
|
|
862
|
-
* @return {NodeGroup} */
|
|
863
|
-
getNodeGroup(template, exact=true) {
|
|
864
|
-
|
|
865
|
-
let result;
|
|
866
|
-
let collection = this.nodeGroupsAttachedAvailable;
|
|
867
|
-
|
|
868
|
-
// TODO: Would it be faster to maintain a separate list of detached nodegroups?
|
|
869
|
-
if (exact) { // [below] parentElement will be null if the parent is a DocumentFragment
|
|
870
|
-
result = collection.deleteAny(template.getExactKey());
|
|
871
|
-
if (!result) { // try searching detached
|
|
872
|
-
collection = this.nodeGroupsDetachedAvailable;
|
|
873
|
-
result = collection.deleteAny(template.getExactKey());
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
if (result) // also delete the matching close key.
|
|
877
|
-
collection.deleteSpecific(template.getCloseKey(), result);
|
|
878
|
-
else {
|
|
879
|
-
return null;
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
// Find a close match.
|
|
884
|
-
// This is a match that has matching html, but different expressions applied.
|
|
885
|
-
// We can then apply the expressions to make it an exact match.
|
|
886
|
-
// 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.
|
|
887
|
-
else if (template.exprs.length) {
|
|
888
|
-
result = collection.deleteAny(template.getCloseKey());
|
|
889
|
-
if (!result) { // try searching detached
|
|
890
|
-
collection = this.nodeGroupsDetachedAvailable;
|
|
891
|
-
result = collection.deleteAny(template.getCloseKey());
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
if (result) {
|
|
895
|
-
/*#IFDEV*/assert(result.exactKey);/*#ENDIF*/
|
|
896
|
-
collection.deleteSpecific(result.exactKey, result);
|
|
897
|
-
|
|
898
|
-
// Update this close match with the new expression values.
|
|
899
|
-
result.applyExprs(template.exprs);
|
|
900
|
-
result.exactKey = template.getExactKey(); // TODO: Should this be set elsewhere?
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
if (!result)
|
|
905
|
-
result = new NodeGroup(template, this);
|
|
906
|
-
|
|
907
|
-
// old:
|
|
908
|
-
this.nodeGroupsRendered.push(result);
|
|
909
|
-
|
|
910
|
-
/*#IFDEV*/assert(result.parentPath);/*#ENDIF*/
|
|
911
|
-
return result;
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
isComponent() {
|
|
915
|
-
// Events won't have type===Component.
|
|
916
|
-
// TODO: Have a special flag for components instead of it being on the type?
|
|
917
|
-
return this.type === ExprPathType.ComponentAttribValue || (this.attrName && this.nodeMarker.tagName && this.nodeMarker.tagName.includes('-'));
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
/**
|
|
921
|
-
* TODO: Rename this to nodeGroupsInUse, nodeGroupsAvialableAttached and nodeGroupsAvailableDetached?
|
|
922
|
-
* Nodes that have been used during the current render().
|
|
923
|
-
* Used with getNodeGroup() and freeNodeGroups().
|
|
924
|
-
* TODO: Use an array of WeakRef so the gc can collect them?
|
|
925
|
-
* TODO: Put items back in nodeGroupsInUse after applyExpr() is called, not before.
|
|
926
|
-
* @type {NodeGroup[]} */
|
|
927
|
-
nodeGroupsRendered = [];
|
|
928
|
-
|
|
929
|
-
/**
|
|
930
|
-
* Nodes that were added to the web component during the last render(), but are available to be used again.
|
|
931
|
-
* Used with getNodeGroup() and freeNodeGroups().
|
|
932
|
-
* Each NodeGroup is here twice, once under an exact key, and once under the close key.
|
|
933
|
-
* @type {MultiValueMap<key:string, value:NodeGroup>} */
|
|
934
|
-
nodeGroupsAttachedAvailable = new MultiValueMap();
|
|
935
|
-
|
|
936
|
-
/**
|
|
937
|
-
* Nodes that were not added to the web component during the last render(), and available to be used again.
|
|
938
|
-
* @type {MultiValueMap} */
|
|
939
|
-
nodeGroupsDetachedAvailable = new MultiValueMap();
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
/**
|
|
943
|
-
* Move everything from this.nodeGroupsRendered to this.nodeGroupsAttached and nodeGroupsDetached.
|
|
944
|
-
* Called at the beginning of applyNodes() so it can have NodeGroups to use.
|
|
945
|
-
* TODO: this could run as needed in getNodeGroup? */
|
|
946
|
-
freeNodeGroups() {
|
|
947
|
-
// Add nodes that weren't used during render() to nodeGroupsDetached
|
|
948
|
-
let previouslyAttached = this.nodeGroupsAttachedAvailable.data;
|
|
949
|
-
let detached = this.nodeGroupsDetachedAvailable.data;
|
|
950
|
-
for (let key in previouslyAttached) {
|
|
951
|
-
let set = detached[key];
|
|
952
|
-
if (!set)
|
|
953
|
-
detached[key] = previouslyAttached[key]
|
|
954
|
-
else
|
|
955
|
-
for (let ng of previouslyAttached[key])
|
|
956
|
-
set.add(ng);
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
// Add nodes that were used during render() to nodeGroupsRendered.
|
|
960
|
-
this.nodeGroupsAttachedAvailable = new MultiValueMap();
|
|
961
|
-
let nga = this.nodeGroupsAttachedAvailable;
|
|
962
|
-
for (let ng of this.nodeGroupsRendered) {
|
|
963
|
-
nga.add(ng.exactKey, ng);
|
|
964
|
-
nga.add(ng.closeKey, ng);
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
this.nodeGroupsRendered = [];
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
//#IFDEV
|
|
971
|
-
|
|
972
|
-
get debug() {
|
|
973
|
-
return [
|
|
974
|
-
`parentNode: ${this.nodeBefore.parentNode?.tagName?.toLowerCase()}`,
|
|
975
|
-
'nodes:',
|
|
976
|
-
...setIndent(this.getNodes().map(item => {
|
|
977
|
-
if (item instanceof Node)
|
|
978
|
-
return item.outerHTML || item.textContent
|
|
979
|
-
else if (item instanceof NodeGroup)
|
|
980
|
-
return item.debug
|
|
981
|
-
}), 1).flat()
|
|
982
|
-
]
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
get debugNodes() {
|
|
986
|
-
// Clear nodesCache so that getNodes() manually gets the nodes.
|
|
987
|
-
let nc = this.nodesCache;
|
|
988
|
-
this.nodesCache = null;
|
|
989
|
-
let result = this.getNodes()
|
|
990
|
-
this.nodesCache = nc;
|
|
991
|
-
return result;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
verify() {
|
|
995
|
-
if (!window.verify)
|
|
996
|
-
return;
|
|
997
|
-
|
|
998
|
-
assert(this.type!==ExprPathType.Content || this.nodeBefore)
|
|
999
|
-
assert(this.type!==ExprPathType.Content || this.nodeBefore.parentNode)
|
|
1000
|
-
|
|
1001
|
-
// Need either nodeMarker or parentNode
|
|
1002
|
-
assert(this.nodeMarker)
|
|
1003
|
-
|
|
1004
|
-
// nodeMarker must be attached.
|
|
1005
|
-
assert(!this.nodeMarker || this.nodeMarker.parentNode)
|
|
1006
|
-
|
|
1007
|
-
// nodeBefore and nodeMarker must have same parent.
|
|
1008
|
-
assert(this.type!==ExprPathType.Content || this.nodeBefore.parentNode === this.nodeMarker.parentNode)
|
|
1009
|
-
|
|
1010
|
-
assert(this.nodeBefore !== this.nodeMarker)
|
|
1011
|
-
assert(this.type!==ExprPathType.Content|| !this.nodeBefore.parentNode || this.nodeBefore.compareDocumentPosition(this.nodeMarker) === Node.DOCUMENT_POSITION_FOLLOWING)
|
|
1012
|
-
|
|
1013
|
-
// Detect cyclic parent and grandparent references.
|
|
1014
|
-
assert(this.parentNg?.parentPath !== this)
|
|
1015
|
-
assert(this.parentNg?.parentPath?.parentNg?.parentPath !== this)
|
|
1016
|
-
assert(this.parentNg?.parentPath?.parentNg?.parentPath?.parentNg?.parentPath !== this)
|
|
1017
|
-
|
|
1018
|
-
for (let ng of this.nodeGroups)
|
|
1019
|
-
ng.verify();
|
|
1020
|
-
|
|
1021
|
-
// Make sure the nodesCache matches the nodes.
|
|
1022
|
-
this.checkNodesCache();
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
checkNodesCache() {
|
|
1026
|
-
return;
|
|
1027
|
-
|
|
1028
|
-
// Make sure cache is accurate.
|
|
1029
|
-
// If this is invalid, then perhaps another component append()'d one of our nodes to itself.
|
|
1030
|
-
// Or perhaps one of our nodes is used in an expression more than once.
|
|
1031
|
-
// TODO: Find a way to check for and warn when this happens.
|
|
1032
|
-
// MutationObserver is too slow since it's asynchronous.
|
|
1033
|
-
// My own MutationWatcher has to modify DOM prototypes, which is rather invasive.
|
|
1034
|
-
if (this.nodesCache) {
|
|
1035
|
-
let nodes = [];
|
|
1036
|
-
let current = this.nodeBefore.nextSibling;
|
|
1037
|
-
let nodeMarker = this.nodeMarker;
|
|
1038
|
-
while (current && current !== nodeMarker) {
|
|
1039
|
-
nodes.push(current)
|
|
1040
|
-
current = current.nextSibling
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
if (!Util.arraySame(this.nodesCache, nodes))
|
|
1044
|
-
console.log(this.nodesCache, nodes)
|
|
1045
|
-
assert(Util.arraySame(this.nodesCache, nodes) === true);
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
//#ENDIF
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
/**
|
|
1052
|
-
*
|
|
1053
|
-
* @param root
|
|
1054
|
-
* @param path {string[]}
|
|
1055
|
-
* @param node {HTMLElement}
|
|
1056
|
-
*/
|
|
1057
|
-
function setValue(root, path, node) {
|
|
1058
|
-
let val = node.value;
|
|
1059
|
-
if (node.type === 'number')
|
|
1060
|
-
val = parseFloat(val);
|
|
1061
|
-
|
|
1062
|
-
delve(root, path, val);
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
/** @enum {int} */
|
|
1066
|
-
export const ExprPathType = {
|
|
1067
|
-
/** Child of a node */
|
|
1068
|
-
Content: 1,
|
|
1069
|
-
|
|
1070
|
-
/** One or more whole attributes */
|
|
1071
|
-
AttribMultiple: 2,
|
|
1072
|
-
|
|
1073
|
-
/** Value of an attribute. */
|
|
1074
|
-
AttribValue: 3,
|
|
1075
|
-
|
|
1076
|
-
/** Value of an attribute being passed to a component. */
|
|
1077
|
-
ComponentAttribValue: 4,
|
|
1078
|
-
|
|
1079
|
-
/** Expressions inside Html comments. */
|
|
1080
|
-
Comment: 5,
|
|
1081
|
-
|
|
1082
|
-
/** Value of an attribute. */
|
|
1083
|
-
Event: 6,
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
/** @return {int[]} Returns indices in reverse order, because doing it that way is faster. */
|
|
1088
|
-
export function getNodePath(node) {
|
|
1089
|
-
let result = [];
|
|
1090
|
-
while(true) {
|
|
1091
|
-
let parent = node.parentNode
|
|
1092
|
-
if (!parent)
|
|
1093
|
-
break;
|
|
1094
|
-
result.push(Array.prototype.indexOf.call(node.parentNode.childNodes, node))
|
|
1095
|
-
node = parent;
|
|
1096
|
-
}
|
|
1097
|
-
return result;
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
/**
|
|
1101
|
-
* Note that the path is backward, with the outermost element at the end.
|
|
1102
|
-
* @param root {HTMLElement|Document|DocumentFragment|ParentNode}
|
|
1103
|
-
* @param path {int[]}
|
|
1104
|
-
* @returns {Node|HTMLElement|HTMLStyleElement} */
|
|
1105
|
-
export function resolveNodePath(root, path) {
|
|
1106
|
-
for (let i=path.length-1; i>=0; i--)
|
|
1107
|
-
root = root.childNodes[path[i]];
|
|
1108
|
-
return root;
|
|
1109
|
-
}
|