solarite 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/benchmarks/naive/Solarite.min.js +4 -0
- package/benchmarks/naive/index.html +14 -0
- package/benchmarks/naive/main.js +339 -0
- package/benchmarks/naive/package-lock.json +13 -0
- package/benchmarks/naive/package.json +23 -0
- package/benchmarks/readme.md +48 -0
- package/build/build.bat +3 -2
- package/build/build.js +1 -1
- package/dist/Solarite-debug.js +1941 -2791
- package/dist/Solarite.js +1697 -2511
- package/dist/Solarite.min.js +3 -3
- package/dist/udomdiff-license.txt +18 -0
- package/docs/index.md +695 -235
- package/docs/js/Playground.js +1 -1
- package/docs/js/codemirror/codemirror6.js +3683 -3206
- package/docs/js/codemirror/themeSolarIce.js +2 -2
- package/docs/js/documentation.js +2 -2
- package/docs/js/ui/CodeEditor.js +183 -45
- package/docs/js/ui/FlexResizer.js +15 -5
- package/docs/js/util/Errors.js +11 -0
- package/docs/media/documentation.css +6 -3
- package/index.html +462 -26
- package/package.json +1 -1
- package/readme.md +11 -1
- package/src/solarite/ExprPath.js +422 -135
- package/src/solarite/Globals.js +53 -0
- package/src/solarite/NodeGroup.js +300 -388
- package/src/solarite/Shell.js +31 -33
- package/src/solarite/Solarite.js +17 -2
- package/src/solarite/Template.js +75 -25
- package/src/solarite/Util.js +131 -7
- package/src/solarite/createSolarite.js +32 -25
- package/src/solarite/getArg.js +12 -12
- package/src/solarite/hash.js +18 -35
- package/src/solarite/r.js +128 -118
- package/src/solarite/watch3.js +98 -0
- package/src/{solarite → unused}/NodeGroupManager.js +86 -224
- package/src/unused/onConnect.js +79 -0
- package/src/{solarite → unused}/watch.js +2 -2
- package/src/{solarite → unused}/watch2.js +4 -4
- package/src/{solarite → util}/MultiValueMap.js +23 -12
- package/src/util/WeakArray.js +33 -0
- package/tests/Solarite.test.js +1108 -166
- package/tests/Testimony.js +274 -67
- package/tests/index.html +6 -35
- package/tests/run.bat +2 -0
- package/tests/NodeGroup.test.js +0 -115
- package/tests/Shell.test.js +0 -75
package/dist/Solarite.js
CHANGED
|
@@ -81,8 +81,6 @@ var Util$1 = {
|
|
|
81
81
|
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
84
|
/**
|
|
87
85
|
* Follow a path into an object.
|
|
88
86
|
* @param obj {object}
|
|
@@ -130,7 +128,7 @@ let delveDontCreate = {};
|
|
|
130
128
|
/**
|
|
131
129
|
* There are three ways to create an instance of a Solarite Component:
|
|
132
130
|
* 1. new ComponentName(); // direct class instantiation
|
|
133
|
-
* 2. this.html = r`<div><component-name></component-name></div>; // as a child of another
|
|
131
|
+
* 2. this.html = r`<div><component-name></component-name></div>; // as a child of another Component.
|
|
134
132
|
* 3. <body><component-name></component-name></body> // in the Document html.
|
|
135
133
|
*
|
|
136
134
|
* When created via #3, Solarite has no way to pass attributes as arguments to the constructor. So to make
|
|
@@ -180,19 +178,19 @@ function getArg(el, name, val=null, type=ArgType.String) {
|
|
|
180
178
|
case ArgType.Float:
|
|
181
179
|
return parseFloat(val);
|
|
182
180
|
case ArgType.String:
|
|
183
|
-
return val
|
|
181
|
+
return [undefined, null, false].includes(val) ? '' : val+'';
|
|
184
182
|
case ArgType.JSON:
|
|
185
|
-
try {
|
|
186
|
-
return JSON.parse(val);
|
|
187
|
-
} catch (e) {
|
|
188
|
-
return val;
|
|
189
|
-
}
|
|
190
183
|
case ArgType.Eval:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
184
|
+
if (typeof val === 'string' && val.length)
|
|
185
|
+
try {
|
|
186
|
+
if (type === ArgType.JSON)
|
|
187
|
+
return JSON.parse(val);
|
|
188
|
+
else
|
|
189
|
+
return eval(`(${val})`);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
return val;
|
|
192
|
+
}
|
|
193
|
+
else return val;
|
|
196
194
|
default:
|
|
197
195
|
return val;
|
|
198
196
|
}
|
|
@@ -228,22 +226,14 @@ let objectIds = new WeakMap();
|
|
|
228
226
|
|
|
229
227
|
/**
|
|
230
228
|
* @param obj {Object|string|Node}
|
|
231
|
-
* @param prefix
|
|
232
229
|
* @returns {string} */
|
|
233
|
-
function getObjectId(obj
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
prefix = prefix || '~\f';
|
|
240
|
-
|
|
230
|
+
function getObjectId(obj) {
|
|
241
231
|
// if (typeof obj === 'function')
|
|
242
|
-
// return obj.toString();
|
|
232
|
+
// return obj.toString(); // This fails to detect when a function's bound variables changes.
|
|
243
233
|
|
|
244
234
|
let result = objectIds.get(obj);
|
|
245
235
|
if (!result) { // convert to string, store in result, then add 1 to lastObjectId.
|
|
246
|
-
result =
|
|
236
|
+
result = (lastObjectId++); // We use a unique prefix to ensure it doesn't collide w/ strings not from getObjectId()
|
|
247
237
|
objectIds.set(obj, result);
|
|
248
238
|
}
|
|
249
239
|
return result;
|
|
@@ -256,12 +246,20 @@ function getObjectId(obj, prefix=null) {
|
|
|
256
246
|
* Adding a toJSON method globally on these object prototypes doesn't incur that performance penalty. */
|
|
257
247
|
let isHashing = true;
|
|
258
248
|
function toJSON() {
|
|
259
|
-
//return (isHashing && !Array.isArray(this)) ? getObjectId(this) : this
|
|
260
249
|
return isHashing ? getObjectId(this) : this
|
|
261
250
|
}
|
|
251
|
+
|
|
252
|
+
|
|
262
253
|
// Node.prototype.toJSON = toJSON;
|
|
263
254
|
// Function.prototype.toJSON = toJSON;
|
|
264
|
-
|
|
255
|
+
// Sometimes these get unassigned by Chrome and Brave 119, as well as Firefox, seemingly randomly!
|
|
256
|
+
// The same tests sometimes pass, sometimes fail, even after browser and OS restarts.
|
|
257
|
+
// So we check the assignments on every run of getObjectHash()
|
|
258
|
+
if (Node.prototype.toJSON !== toJSON) {
|
|
259
|
+
Node.prototype.toJSON = toJSON;
|
|
260
|
+
if (Function.prototype.toJSON !== toJSON) // Will it only unmap one but not the other?
|
|
261
|
+
Function.prototype.toJSON = toJSON;
|
|
262
|
+
}
|
|
265
263
|
|
|
266
264
|
/**
|
|
267
265
|
* Get a string that uniquely maps to the values of the given object.
|
|
@@ -270,25 +268,18 @@ function toJSON() {
|
|
|
270
268
|
*
|
|
271
269
|
* Relies on the Node and Function prototypes being overridden above.
|
|
272
270
|
*
|
|
271
|
+
* Note that passing an integer may collide with the number we get from hashing an object.
|
|
272
|
+
* But we don't handle that case because we need max performance and Solarite never passes integers to this function.
|
|
273
|
+
*
|
|
273
274
|
* @param obj {*}
|
|
274
275
|
* @returns {string} */
|
|
275
276
|
function getObjectHash(obj) {
|
|
276
|
-
|
|
277
|
-
// Sometimes these get unassigned by Chrome and Brave 119, as well as Firefox, seemingly randomly!
|
|
278
|
-
// The same tests sometimes pass, sometimes fail, even after browser and OS restarts.
|
|
279
|
-
// So we check the assignments on every run of getObjectHash()
|
|
280
|
-
if (Node.prototype.toJSON !== toJSON) {
|
|
281
|
-
Node.prototype.toJSON = toJSON;
|
|
282
|
-
if (Function.prototype.toJSON !== toJSON) // Will it only unmap one but not the other?
|
|
283
|
-
Function.prototype.toJSON = toJSON;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
277
|
let result;
|
|
287
278
|
isHashing = true;
|
|
288
279
|
try {
|
|
289
280
|
result = JSON.stringify(obj);
|
|
290
281
|
}
|
|
291
|
-
catch(e){
|
|
282
|
+
catch(e) {
|
|
292
283
|
result = getObjectHashCircular(obj);
|
|
293
284
|
}
|
|
294
285
|
isHashing = false;
|
|
@@ -296,7 +287,7 @@ function getObjectHash(obj) {
|
|
|
296
287
|
}
|
|
297
288
|
|
|
298
289
|
/**
|
|
299
|
-
*
|
|
290
|
+
* Slower hashing method that supports.
|
|
300
291
|
* @param obj
|
|
301
292
|
* @returns {string} */
|
|
302
293
|
function getObjectHashCircular(obj) {
|
|
@@ -315,71 +306,59 @@ function getObjectHashCircular(obj) {
|
|
|
315
306
|
});
|
|
316
307
|
}
|
|
317
308
|
|
|
318
|
-
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
var Globals = {
|
|
319
312
|
|
|
320
|
-
/**
|
|
321
|
-
|
|
313
|
+
/**
|
|
314
|
+
* Used by NodeGroup.applyComponentExprs() */
|
|
315
|
+
componentHash: new WeakMap(),
|
|
322
316
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (!set) {
|
|
328
|
-
set = new Set();
|
|
329
|
-
data[key] = set;
|
|
330
|
-
}
|
|
331
|
-
set.add(value);
|
|
332
|
-
}
|
|
317
|
+
/**
|
|
318
|
+
* Store which instances of Solarite have already been added to the DOM.
|
|
319
|
+
* @type {WeakSet<HTMLElement>} */
|
|
320
|
+
connected: new WeakSet(),
|
|
333
321
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
322
|
+
/**
|
|
323
|
+
* Elements that have been rendered to by r() at least once.
|
|
324
|
+
* This is used by the Solarite class to know when to call onFirstConnect()
|
|
325
|
+
* @type {WeakSet<HTMLElement>} */
|
|
326
|
+
rendered: new WeakSet(),
|
|
338
327
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
// debugger;
|
|
343
|
-
|
|
344
|
-
let data = this.data;
|
|
345
|
-
// The partialUpdate benchmark shows having this check first makes the function slightly faster.
|
|
346
|
-
// if (!data.hasOwnProperty(key))
|
|
347
|
-
// return undefined;
|
|
328
|
+
/**
|
|
329
|
+
* Used by watch3 to see which expressions are being accessed. */
|
|
330
|
+
currentExprPath: [],
|
|
348
331
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
if (!set) // slower than pre-check.
|
|
353
|
-
return undefined;
|
|
332
|
+
/**
|
|
333
|
+
* @type {Object<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
|
|
334
|
+
elementClasses: {},
|
|
354
335
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
336
|
+
/**
|
|
337
|
+
* Used by ExprPath.applyEventAttrib()
|
|
338
|
+
* @type {WeakMap<Node, Object<eventName:string, [original:function, bound:function, args:*[]]>>} */
|
|
339
|
+
nodeEvents: new WeakMap(),
|
|
359
340
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
set.delete(result);
|
|
365
|
-
}
|
|
341
|
+
/**
|
|
342
|
+
* Get the RootNodeGroup for an element.
|
|
343
|
+
* @type {WeakMap<HTMLElement, RootNodeGroup>} */
|
|
344
|
+
nodeGroups: new WeakMap(),
|
|
366
345
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
return result;
|
|
372
|
-
}
|
|
346
|
+
/**
|
|
347
|
+
* Used by r() path 9. */
|
|
348
|
+
objToEl: new WeakMap(),
|
|
373
349
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
350
|
+
pendingChildren: [],
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Elements that are currently rendering via the r() function.
|
|
354
|
+
* @type {WeakSet<HTMLElement>} */
|
|
355
|
+
rendering: new WeakSet(),
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Map from array of Html strings to a Shell created from them.
|
|
359
|
+
* @type {WeakMap<string[], Shell>} */
|
|
360
|
+
shells: new WeakMap()
|
|
361
|
+
};
|
|
383
362
|
|
|
384
363
|
let Util = {
|
|
385
364
|
|
|
@@ -404,9 +383,133 @@ let Util = {
|
|
|
404
383
|
child.textContent = newText;
|
|
405
384
|
}
|
|
406
385
|
}
|
|
407
|
-
}
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* A generator function that recursively traverses and flattens a value.
|
|
390
|
+
*
|
|
391
|
+
* - If the input is an array, it recursively traverses and flattens the array.
|
|
392
|
+
* - If the input is a function, it calls the function, replaces the function
|
|
393
|
+
* with its result, and flattens the result if necessary. It will recursively
|
|
394
|
+
* call functions that return other functions.
|
|
395
|
+
* - Otherwise it yields the value as is.
|
|
396
|
+
*
|
|
397
|
+
* This function does not create a new array for the flattened values. Instead,
|
|
398
|
+
* it lazily yields each item as it is encountered. This can be more memory-efficient
|
|
399
|
+
* for large or deeply nested structures.
|
|
400
|
+
*
|
|
401
|
+
* @param {any} value - The value to flatten. Can be an array, object, function, or primitive.
|
|
402
|
+
* @yields {any} - The next item in the flattened structure.
|
|
403
|
+
*
|
|
404
|
+
* @example
|
|
405
|
+
* const complexArray = [
|
|
406
|
+
* 1,
|
|
407
|
+
* [2, () => 3, [4, () => [5, 6]], { a: 'object' }],
|
|
408
|
+
* () => () => 7,
|
|
409
|
+
* () => [() => 8, 9],
|
|
410
|
+
* ]; *
|
|
411
|
+
* for (const item of flatten(complexArray))
|
|
412
|
+
* console.log(item); // Outputs: 1, 2, 3, 4, 5, 6, { a: 'object' }, 7, 8, 9
|
|
413
|
+
*/
|
|
414
|
+
*flatten(value) {
|
|
415
|
+
if (Array.isArray(value)) {
|
|
416
|
+
for (const item of value) {
|
|
417
|
+
yield* Util.flatten(item); // Recursively flatten arrays
|
|
418
|
+
}
|
|
419
|
+
} else if (typeof value === 'function') {
|
|
420
|
+
const result = value();
|
|
421
|
+
yield* Util.flatten(result); // Recursively flatten the result of a function
|
|
422
|
+
} else
|
|
423
|
+
yield value; // Yield primitive values as is
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Get the value of an input as the most appropriate JavaScript type.
|
|
428
|
+
* @param node {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|HTMLDivElement}
|
|
429
|
+
* @return {string|string[]|number|[]|File[]|Date|boolean} */
|
|
430
|
+
getInputValue(node) {
|
|
431
|
+
if (node.type === 'checkbox' || node.type === 'radio')
|
|
432
|
+
return node.checked; // Boolean
|
|
433
|
+
if (node.type === 'file')
|
|
434
|
+
return [...node.files]; // FileList
|
|
435
|
+
if (node.type === 'number' || node.type === 'range')
|
|
436
|
+
return node.valueAsNumber; // Number
|
|
437
|
+
if (node.type === 'date' || node.type === 'time' || node.type === 'datetime-local')
|
|
438
|
+
return node.valueAsDate; // Date Object
|
|
439
|
+
if (node.type === 'select-multiple') // <select multiple>
|
|
440
|
+
return [...node.selectedOptions].map(option => option.value); // Array of Strings
|
|
441
|
+
|
|
442
|
+
return node.value; // String
|
|
443
|
+
},
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Is it an array and a path that can be evaluated by delve() ?
|
|
447
|
+
* @param arr {Array|*}
|
|
448
|
+
* @returns {boolean} */
|
|
449
|
+
isPath(arr) {
|
|
450
|
+
return Array.isArray(arr) && typeof arr[0] === 'object' && !arr.slice(1).find(p => typeof p !== 'string' && typeof p !== 'number');
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Find NodeGroups that had their nodes removed and add those nodes to a Fragment so
|
|
455
|
+
* they're not lost forever and the NodeGroup's internal structure is still consistent.
|
|
456
|
+
* This saves all of a NodeGroup's nodes in order, so that nextChildNode still works.
|
|
457
|
+
* This is necessary because a NodeGroup normally only stores the first and last node.
|
|
458
|
+
* Called from ExprPath.apply().
|
|
459
|
+
* @param oldNodeGroups {NodeGroup[]}
|
|
460
|
+
* @param oldNodes {Node[]} */
|
|
461
|
+
saveOrphans(oldNodeGroups, oldNodes) {
|
|
462
|
+
let oldNgMap = new Map();
|
|
463
|
+
for (let ng of oldNodeGroups) {
|
|
464
|
+
oldNgMap.set(ng.startNode, ng);
|
|
465
|
+
|
|
466
|
+
// TODO: Is this necessary?
|
|
467
|
+
// if (ng.parentPath)
|
|
468
|
+
// ng.parentPath.clearNodesCache();
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
for (let i=0, node; node = oldNodes[i]; i++) {
|
|
472
|
+
let ng;
|
|
473
|
+
if (!node.parentNode && (ng = oldNgMap.get(node))) {
|
|
474
|
+
//ng.nodesCache = [];
|
|
475
|
+
let fragment = document.createDocumentFragment();
|
|
476
|
+
let endNode = ng.endNode;
|
|
477
|
+
while (node !== endNode) {
|
|
478
|
+
fragment.append(node);
|
|
479
|
+
//ng.nodesCache.push(node);
|
|
480
|
+
i++;
|
|
481
|
+
node = oldNodes[i];
|
|
482
|
+
}
|
|
483
|
+
fragment.append(endNode);
|
|
484
|
+
//ng.nodesCache.push(endNode);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
},
|
|
408
488
|
|
|
489
|
+
/**
|
|
490
|
+
* Remove nodes from the beginning and end that are not:
|
|
491
|
+
* 1. Elements.
|
|
492
|
+
* 2. Non-whitespace text nodes.
|
|
493
|
+
* @param nodes {Node[]|NodeList}
|
|
494
|
+
* @returns {Node[]} */
|
|
495
|
+
trimEmptyNodes(nodes) {
|
|
496
|
+
const shouldTrimNode = node =>
|
|
497
|
+
node.nodeType !== Node.ELEMENT_NODE &&
|
|
498
|
+
(node.nodeType !== Node.TEXT_NODE || node.textContent.trim() === '');
|
|
499
|
+
|
|
500
|
+
// Convert nodeList to an array for easier manipulation
|
|
501
|
+
const result = [...nodes];
|
|
502
|
+
|
|
503
|
+
// Trim from the start
|
|
504
|
+
while (result.length > 0 && shouldTrimNode(result[0]))
|
|
505
|
+
result.shift();
|
|
506
|
+
|
|
507
|
+
// Trim from the end
|
|
508
|
+
while (result.length > 0 && shouldTrimNode(result[result.length - 1]))
|
|
509
|
+
result.pop();
|
|
409
510
|
|
|
511
|
+
return result;
|
|
512
|
+
}
|
|
410
513
|
};
|
|
411
514
|
|
|
412
515
|
|
|
@@ -451,15 +554,15 @@ function camelToDashes(str) {
|
|
|
451
554
|
* Returns false if they're the same. Or the first index where they differ.
|
|
452
555
|
* @param a
|
|
453
556
|
* @param b
|
|
454
|
-
* @returns {
|
|
455
|
-
function
|
|
456
|
-
if (a.length !== b.length)
|
|
457
|
-
return -1;
|
|
557
|
+
* @returns {boolean} */
|
|
558
|
+
function arraySame(a, b) {
|
|
458
559
|
let aLength = a.length;
|
|
560
|
+
if (aLength !== b.length)
|
|
561
|
+
return false;
|
|
459
562
|
for (let i=0; i<aLength; i++)
|
|
460
563
|
if (a[i] !== b[i])
|
|
461
|
-
return
|
|
462
|
-
return
|
|
564
|
+
return false;
|
|
565
|
+
return true; // the same.
|
|
463
566
|
}
|
|
464
567
|
|
|
465
568
|
|
|
@@ -542,61 +645,321 @@ let state = {...defaultState};
|
|
|
542
645
|
// For debugging only
|
|
543
646
|
|
|
544
647
|
|
|
545
|
-
|
|
546
|
-
* Path to where an expression should be evaluated within a Shell.
|
|
547
|
-
* Path is only valid until the expressions before it are evaluated.
|
|
548
|
-
* TODO: Make this based on parent and node instead of path? */
|
|
549
|
-
class ExprPath {
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* @type {PathType} */
|
|
553
|
-
type;
|
|
554
|
-
|
|
555
|
-
// Used for attributes:
|
|
556
|
-
|
|
557
|
-
/** @type {?string} Used only if type=AttribType.Value. */
|
|
558
|
-
attrName;
|
|
648
|
+
class MultiValueMap {
|
|
559
649
|
|
|
560
|
-
/**
|
|
561
|
-
|
|
562
|
-
attrValue;
|
|
650
|
+
/** @type {Object<string, Set>} */
|
|
651
|
+
data = {};
|
|
563
652
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
653
|
+
// Set a new value for a key
|
|
654
|
+
add(key, value) {
|
|
655
|
+
let data = this.data;
|
|
656
|
+
let set = data[key];
|
|
657
|
+
if (!set) {
|
|
658
|
+
set = new Set();
|
|
659
|
+
data[key] = set;
|
|
660
|
+
}
|
|
661
|
+
set.add(value);
|
|
662
|
+
}
|
|
567
663
|
|
|
664
|
+
isEmpty() {
|
|
665
|
+
for (let key in this.data)
|
|
666
|
+
return true;
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
568
669
|
|
|
670
|
+
// Get all values for a key
|
|
671
|
+
getAll(key) {
|
|
672
|
+
return this.data[key] || [];
|
|
673
|
+
}
|
|
569
674
|
|
|
570
675
|
/**
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
|
|
576
|
-
|
|
676
|
+
* Remove one value from a key, and return it.
|
|
677
|
+
* @param key {string}
|
|
678
|
+
* @param val If specified, make sure we delete this specific value, if a key exists more than once.
|
|
679
|
+
* @returns {*} */
|
|
680
|
+
delete(key, val=undefined) {
|
|
681
|
+
// if (key === '["Html2",[[["Html3",["F1","A"]],["Html3",["F1","B"]]]]]')
|
|
682
|
+
// debugger;
|
|
577
683
|
|
|
578
|
-
|
|
579
|
-
* If type is AttribType.Multiple or AttribType.Value, points to the node having the attribute.
|
|
580
|
-
* If type is 'content', points to a node that never changes that this NodeGroup should always insert its nodes before.
|
|
581
|
-
* An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.
|
|
582
|
-
* @type {Node|HTMLElement} */
|
|
583
|
-
nodeMarker;
|
|
684
|
+
let data = this.data;
|
|
584
685
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
return this.nodeMarker.parentNode;
|
|
588
|
-
}
|
|
686
|
+
// if (!data.hasOwnProperty(key))
|
|
687
|
+
// return undefined;
|
|
589
688
|
|
|
590
|
-
|
|
689
|
+
// Delete a specific value.
|
|
690
|
+
let result;
|
|
691
|
+
let set = data[key];
|
|
692
|
+
if (!set) // slower than pre-check.
|
|
693
|
+
return undefined;
|
|
591
694
|
|
|
695
|
+
// Delete any value.
|
|
696
|
+
if (val === undefined) {
|
|
697
|
+
//result = set.values().next().value; // get first item from set.
|
|
698
|
+
[result] = set; // Does the same as above and seems to be about the same speed.
|
|
699
|
+
set.delete(result);
|
|
700
|
+
}
|
|
592
701
|
|
|
593
|
-
|
|
594
|
-
|
|
702
|
+
// Delete a specific value.
|
|
703
|
+
else {
|
|
704
|
+
set.delete(val);
|
|
705
|
+
result = val;
|
|
706
|
+
}
|
|
595
707
|
|
|
596
|
-
|
|
597
|
-
|
|
708
|
+
// TODO: Will this make it slower?
|
|
709
|
+
if (set.size === 0)
|
|
710
|
+
delete data[key];
|
|
598
711
|
|
|
712
|
+
return result;
|
|
713
|
+
}
|
|
599
714
|
|
|
715
|
+
hasValue(val) {
|
|
716
|
+
let data = this.data;
|
|
717
|
+
let names = [];
|
|
718
|
+
for (let name in data)
|
|
719
|
+
if (data[name].has(val)) // TODO: iterate twice to pre-size array?
|
|
720
|
+
names.push(name);
|
|
721
|
+
return names;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* ISC License
|
|
727
|
+
*
|
|
728
|
+
* Copyright (c) 2020, Andrea Giammarchi, @WebReflection
|
|
729
|
+
*
|
|
730
|
+
* Permission to use, copy, modify, and/or distribute this software for any
|
|
731
|
+
* purpose with or without fee is hereby granted, provided that the above
|
|
732
|
+
* copyright notice and this permission notice appear in all copies.
|
|
733
|
+
*
|
|
734
|
+
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
735
|
+
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
736
|
+
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
737
|
+
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
738
|
+
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
|
739
|
+
* OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
740
|
+
* PERFORMANCE OF THIS SOFTWARE.
|
|
741
|
+
*/
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* @param {Node} parentNode The container where children live
|
|
745
|
+
* @param {Node[]} a The list of current/live children
|
|
746
|
+
* @param {Node[]} b The list of future children
|
|
747
|
+
* @param {(entry: Node, action: number) => Node} get
|
|
748
|
+
* The callback invoked per each entry related DOM operation.
|
|
749
|
+
* @param {Node} [before] The optional node used as anchor to insert before.
|
|
750
|
+
* @returns {Node[]} The same list of future children.
|
|
751
|
+
*/
|
|
752
|
+
const udomdiff = (parentNode, a, b, before) => {
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
const bLength = b.length;
|
|
756
|
+
let aEnd = a.length;
|
|
757
|
+
let bEnd = bLength;
|
|
758
|
+
let aStart = 0;
|
|
759
|
+
let bStart = 0;
|
|
760
|
+
let map = null;
|
|
761
|
+
while (aStart < aEnd || bStart < bEnd) {
|
|
762
|
+
// append head, tail, or nodes in between: fast path
|
|
763
|
+
if (aEnd === aStart) {
|
|
764
|
+
// we could be in a situation where the rest of nodes that
|
|
765
|
+
// need to be added are not at the end, and in such case
|
|
766
|
+
// the node to `insertBefore`, if the index is more than 0
|
|
767
|
+
// must be retrieved, otherwise it's gonna be the first item.
|
|
768
|
+
const node = bEnd < bLength
|
|
769
|
+
? (bStart
|
|
770
|
+
? (b[bStart - 1].nextSibling)
|
|
771
|
+
: b[bEnd - bStart])
|
|
772
|
+
: before;
|
|
773
|
+
while (bStart < bEnd) {
|
|
774
|
+
let bNode = b[bStart++];
|
|
775
|
+
parentNode.insertBefore(bNode, node);
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
// remove head or tail: fast path
|
|
781
|
+
else if (bEnd === bStart) {
|
|
782
|
+
while (aStart < aEnd) {
|
|
783
|
+
// remove the node only if it's unknown or not live
|
|
784
|
+
let aNode = a[aStart];
|
|
785
|
+
if (!map || !map.has(aNode)) {
|
|
786
|
+
parentNode.removeChild(aNode);
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
}
|
|
790
|
+
aStart++;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
// same node: fast path
|
|
794
|
+
else if (a[aStart] === b[bStart]) {
|
|
795
|
+
aStart++;
|
|
796
|
+
bStart++;
|
|
797
|
+
}
|
|
798
|
+
// same tail: fast path
|
|
799
|
+
else if (a[aEnd - 1] === b[bEnd - 1]) {
|
|
800
|
+
aEnd--;
|
|
801
|
+
bEnd--;
|
|
802
|
+
}
|
|
803
|
+
// The once here single last swap "fast path" has been removed in v1.1.0
|
|
804
|
+
// https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85
|
|
805
|
+
// reverse swap: also fast path
|
|
806
|
+
else if (
|
|
807
|
+
a[aStart] === b[bEnd - 1] &&
|
|
808
|
+
b[bStart] === a[aEnd - 1]
|
|
809
|
+
) {
|
|
810
|
+
// this is a "shrink" operation that could happen in these cases:
|
|
811
|
+
// [1, 2, 3, 4, 5]
|
|
812
|
+
// [1, 4, 3, 2, 5]
|
|
813
|
+
// or asymmetric too
|
|
814
|
+
// [1, 2, 3, 4, 5]
|
|
815
|
+
// [1, 2, 3, 5, 6, 4]
|
|
816
|
+
const node = a[--aEnd].nextSibling;
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
let a2 = b[bStart++];
|
|
820
|
+
let b2 = a[aStart++];
|
|
821
|
+
parentNode.insertBefore(
|
|
822
|
+
a2,
|
|
823
|
+
b2.nextSibling
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
let bNode = b[--bEnd];
|
|
828
|
+
parentNode.insertBefore(bNode, node);
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
// mark the future index as identical (yeah, it's dirty, but cheap 👍)
|
|
833
|
+
// The main reason to do this, is that when a[aEnd] will be reached,
|
|
834
|
+
// the loop will likely be on the fast path, as identical to b[bEnd].
|
|
835
|
+
// In the best case scenario, the next loop will skip the tail,
|
|
836
|
+
// but in the worst one, this node will be considered as already
|
|
837
|
+
// processed, bailing out pretty quickly from the map index check
|
|
838
|
+
a[aEnd] = b[bEnd];
|
|
839
|
+
}
|
|
840
|
+
// map based fallback, "slow" path
|
|
841
|
+
else {
|
|
842
|
+
// the map requires an O(bEnd - bStart) operation once
|
|
843
|
+
// to store all future nodes indexes for later purposes.
|
|
844
|
+
// In the worst case scenario, this is a full O(N) cost,
|
|
845
|
+
// and such scenario happens at least when all nodes are different,
|
|
846
|
+
// but also if both first and last items of the lists are different
|
|
847
|
+
if (!map) {
|
|
848
|
+
map = new Map;
|
|
849
|
+
let i = bStart;
|
|
850
|
+
while (i < bEnd)
|
|
851
|
+
map.set(b[i], i++);
|
|
852
|
+
}
|
|
853
|
+
// if it's a future node, hence it needs some handling
|
|
854
|
+
if (map.has(a[aStart])) {
|
|
855
|
+
// grab the index of such node, 'cause it might have been processed
|
|
856
|
+
const index = map.get(a[aStart]);
|
|
857
|
+
// if it's not already processed, look on demand for the next LCS
|
|
858
|
+
if (bStart < index && index < bEnd) {
|
|
859
|
+
let i = aStart;
|
|
860
|
+
// counts the amount of nodes that are the same in the future
|
|
861
|
+
let sequence = 1;
|
|
862
|
+
while (++i < aEnd && i < bEnd && map.get(a[i]) === (index + sequence))
|
|
863
|
+
sequence++;
|
|
864
|
+
// effort decision here: if the sequence is longer than replaces
|
|
865
|
+
// needed to reach such sequence, which would brings again this loop
|
|
866
|
+
// to the fast path, prepend the difference before a sequence,
|
|
867
|
+
// and move only the future list index forward, so that aStart
|
|
868
|
+
// and bStart will be aligned again, hence on the fast path.
|
|
869
|
+
// An example considering aStart and bStart are both 0:
|
|
870
|
+
// a: [1, 2, 3, 4]
|
|
871
|
+
// b: [7, 1, 2, 3, 6]
|
|
872
|
+
// this would place 7 before 1 and, from that time on, 1, 2, and 3
|
|
873
|
+
// will be processed at zero cost
|
|
874
|
+
if (sequence > (index - bStart)) {
|
|
875
|
+
const node = a[aStart];
|
|
876
|
+
while (bStart < index) {
|
|
877
|
+
let bNode = b[bStart++];
|
|
878
|
+
parentNode.insertBefore(bNode, node);
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
// if the effort wasn't good enough, fallback to a replace,
|
|
884
|
+
// moving both source and target indexes forward, hoping that some
|
|
885
|
+
// similar node will be found later on, to go back to the fast path
|
|
886
|
+
else {
|
|
887
|
+
let aNode = a[aStart++];
|
|
888
|
+
let bNode = b[bStart++];
|
|
889
|
+
parentNode.replaceChild(
|
|
890
|
+
bNode,
|
|
891
|
+
aNode
|
|
892
|
+
);
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
// otherwise move the source forward, 'cause there's nothing to do
|
|
898
|
+
else
|
|
899
|
+
aStart++;
|
|
900
|
+
}
|
|
901
|
+
// this node has no meaning in the future list, so it's more than safe
|
|
902
|
+
// to remove it, and check the next live node out instead, meaning
|
|
903
|
+
// that only the live list index should be forwarded
|
|
904
|
+
else {
|
|
905
|
+
let aNode = a[aStart++];
|
|
906
|
+
parentNode.removeChild(aNode);
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return b;
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Path to where an expression should be evaluated within a Shell or NodeGroup.
|
|
917
|
+
* Path is only valid until the expressions before it are evaluated.
|
|
918
|
+
* TODO: Make this based on parent and node instead of path? */
|
|
919
|
+
class ExprPath {
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* @type {PathType} */
|
|
923
|
+
type;
|
|
924
|
+
|
|
925
|
+
// Used for attributes:
|
|
926
|
+
|
|
927
|
+
/** @type {?string} Used only if type=AttribType.Value. */
|
|
928
|
+
attrName;
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* @type {?string[]} Used only if type=AttribType.Value. If null, use one expr to set the whole attribute value. */
|
|
932
|
+
attrValue;
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* @type {Set<string>} Used for type=AttribType.Multiple to remember the attributes that were added. */
|
|
936
|
+
attrNames;
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* @type {Node} Node that occurs before this ExprPath's first Node.
|
|
942
|
+
* This is necessary because udomdiff() can steal nodes from another ExprPath.
|
|
943
|
+
* If we had a pointer to our own startNode then that node could be moved somewhere else w/o us knowing it.
|
|
944
|
+
* Used only for type='content'
|
|
945
|
+
* Will be null if ExprPath has no Nodes. */
|
|
946
|
+
nodeBefore;
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* If type is AttribType.Multiple or AttribType.Value, points to the node having the attribute.
|
|
950
|
+
* If type is 'content', points to a node that never changes that this NodeGroup should always insert its nodes before.
|
|
951
|
+
* An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.
|
|
952
|
+
* @type {Node|HTMLElement} */
|
|
953
|
+
nodeMarker;
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
// These are set after an expression is assigned:
|
|
957
|
+
|
|
958
|
+
/** @type {NodeGroup} */
|
|
959
|
+
parentNg;
|
|
960
|
+
|
|
961
|
+
/** @type {NodeGroup[]} */
|
|
962
|
+
nodeGroups = [];
|
|
600
963
|
|
|
601
964
|
|
|
602
965
|
// Caches to make things faster
|
|
@@ -606,26 +969,23 @@ class ExprPath {
|
|
|
606
969
|
* @type {Node[]} Cached result of getNodes() */
|
|
607
970
|
nodesCache;
|
|
608
971
|
|
|
609
|
-
|
|
972
|
+
/**
|
|
973
|
+
* @type {int} Index of nodeBefore among its parentNode's children. */
|
|
610
974
|
nodeBeforeIndex;
|
|
611
|
-
nodeMarkerPath;
|
|
612
975
|
|
|
613
|
-
|
|
614
|
-
|
|
976
|
+
/**
|
|
977
|
+
* @type {int[]} Path to the node marker, in reverse for performance reasons. */
|
|
978
|
+
nodeMarkerPath;
|
|
615
979
|
|
|
616
|
-
// for debugging
|
|
617
|
-
|
|
618
980
|
|
|
619
981
|
/**
|
|
620
982
|
* @param nodeBefore {Node}
|
|
621
983
|
* @param nodeMarker {?Node}
|
|
622
|
-
* @param type {
|
|
984
|
+
* @param type {PathType}
|
|
623
985
|
* @param attrName {?string}
|
|
624
986
|
* @param attrValue {string[]} */
|
|
625
987
|
constructor(nodeBefore, nodeMarker, type=PathType.Content, attrName=null, attrValue=null) {
|
|
626
988
|
|
|
627
|
-
|
|
628
|
-
|
|
629
989
|
// If path is a node.
|
|
630
990
|
this.nodeBefore = nodeBefore;
|
|
631
991
|
this.nodeMarker = nodeMarker;
|
|
@@ -636,39 +996,245 @@ class ExprPath {
|
|
|
636
996
|
this.attrNames = new Set();
|
|
637
997
|
}
|
|
638
998
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
999
|
+
/**
|
|
1000
|
+
* Apply any type of expression.
|
|
1001
|
+
* This calls other apply functions.
|
|
1002
|
+
*
|
|
1003
|
+
* One very messy part of this function is that it may apply multiple expressions if they're all part
|
|
1004
|
+
* of the same attribute value.
|
|
1005
|
+
*
|
|
1006
|
+
* We should modify path.applyValueAttrib so it stores the procssed parts and then only calls
|
|
1007
|
+
* setAttribute() once all the pieces are in place.
|
|
1008
|
+
*
|
|
1009
|
+
* @param expr {Expr}
|
|
1010
|
+
* @param exprs {Expr[]}
|
|
1011
|
+
* @param exprIndex {int}
|
|
1012
|
+
* @param componentExprs {object}
|
|
1013
|
+
* @returns {int} */
|
|
1014
|
+
apply(expr, exprs=null, exprIndex=0, componentExprs={}) {
|
|
1015
|
+
switch (this.type) {
|
|
1016
|
+
case 1: // PathType.Content:
|
|
1017
|
+
this.applyNodes(expr);
|
|
1018
|
+
break;
|
|
1019
|
+
case 2: // PathType.Multiple:
|
|
1020
|
+
this.applyMultipleAttribs(this.nodeMarker, expr);
|
|
1021
|
+
break;
|
|
1022
|
+
case 5: // PathType.Comment:
|
|
1023
|
+
// Expressions inside Html comments. Deliberately empty because we won't waste time updating them.
|
|
1024
|
+
break;
|
|
1025
|
+
case 6: // PathType.Event:
|
|
1026
|
+
this.applyEventAttrib(this.nodeMarker, expr, this.parentNg.rootNg.root);
|
|
1027
|
+
break;
|
|
1028
|
+
default:
|
|
1029
|
+
if (this.type === 4 /*PathType.Component*/ && this.nodeMarker !== this.parentNg.rootNg.root)
|
|
1030
|
+
componentExprs[this.attrName] = expr;
|
|
1031
|
+
else {
|
|
1032
|
+
// One attribute value may have multiple expressions. Here we apply them all at once.
|
|
1033
|
+
exprIndex = this.applyValueAttrib(this.nodeMarker, exprs || [expr], exprIndex);
|
|
1034
|
+
}
|
|
1035
|
+
break;
|
|
660
1036
|
}
|
|
661
1037
|
|
|
662
|
-
|
|
663
|
-
for (let oldName of oldNames)
|
|
664
|
-
if (!this.attrNames.has(oldName))
|
|
665
|
-
node.removeAttribute(oldName);
|
|
1038
|
+
return exprIndex;
|
|
666
1039
|
}
|
|
667
1040
|
|
|
668
1041
|
/**
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
1042
|
+
* Insert/replace the nodes created by a single expression.
|
|
1043
|
+
* Called by applyExprs()
|
|
1044
|
+
* This function is recursive, as the functions it calls also call it.
|
|
1045
|
+
* @param expr {Expr}
|
|
1046
|
+
* @return {Node[]} New Nodes created. */
|
|
1047
|
+
applyNodes(expr) {
|
|
1048
|
+
let path = this;
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
/** @type {(Node|NodeGroup|Expr)[]} */
|
|
1053
|
+
let newNodes = [];
|
|
1054
|
+
let oldNodeGroups = path.nodeGroups;
|
|
1055
|
+
|
|
1056
|
+
let secondPass = []; // indices
|
|
1057
|
+
|
|
1058
|
+
path.nodeGroups = []; // Reset before applyExact and the code below rebuilds it.
|
|
1059
|
+
path.applyExact(expr, newNodes, secondPass);
|
|
1060
|
+
|
|
1061
|
+
this.existingTextNodes = null;
|
|
1062
|
+
|
|
1063
|
+
// TODO: Create an array of old vs Nodes and NodeGroups together.
|
|
1064
|
+
// If they're all the same, skip the next steps.
|
|
1065
|
+
// Or calculate it in the loop above as we go? Have a path.lastNodeGroups property?
|
|
1066
|
+
|
|
1067
|
+
// Second pass to find close-match NodeGroups.
|
|
1068
|
+
let flatten = false;
|
|
1069
|
+
if (secondPass.length) {
|
|
1070
|
+
for (let [nodesIndex, ngIndex] of secondPass) {
|
|
1071
|
+
let ng = path.getNodeGroup(newNodes[nodesIndex], false);
|
|
1072
|
+
|
|
1073
|
+
let ngNodes = ng.getNodes();
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
if (ngNodes.length === 1) // flatten manually so we can skip flattening below.
|
|
1078
|
+
newNodes[nodesIndex] = ngNodes[0];
|
|
1079
|
+
|
|
1080
|
+
else {
|
|
1081
|
+
newNodes[nodesIndex] = ngNodes;
|
|
1082
|
+
flatten = true;
|
|
1083
|
+
}
|
|
1084
|
+
path.nodeGroups[ngIndex] = ng;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if (flatten)
|
|
1088
|
+
newNodes = newNodes.flat(); // Only if second pass happens.
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
let oldNodes = path.getNodes();
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
// This pre-check makes it a few percent faster?
|
|
1099
|
+
let same = arraySame(oldNodes, newNodes);
|
|
1100
|
+
if (!same) {
|
|
1101
|
+
|
|
1102
|
+
path.nodesCache = newNodes; // Replaces value set by path.getNodes()
|
|
1103
|
+
|
|
1104
|
+
if (this.parentNg.parentPath)
|
|
1105
|
+
this.parentNg.parentPath.clearNodesCache();
|
|
1106
|
+
|
|
1107
|
+
// Fast clear method
|
|
1108
|
+
let isNowEmpty = oldNodes.length && !newNodes.length;
|
|
1109
|
+
if (!isNowEmpty || !path.fastClear())
|
|
1110
|
+
|
|
1111
|
+
// Rearrange nodes.
|
|
1112
|
+
udomdiff(path.nodeMarker.parentNode, oldNodes, newNodes, path.nodeMarker);
|
|
1113
|
+
|
|
1114
|
+
Util.saveOrphans(oldNodeGroups, oldNodes);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Must happen after second pass.
|
|
1118
|
+
path.freeNodeGroups();
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* Apply Nodes that are an exact match.
|
|
1127
|
+
* @param expr {Template|Node|Array|function|*}
|
|
1128
|
+
* @param newNodes {(Node|Template)[]}
|
|
1129
|
+
* @param secondPass {Array} Locations within newNodes to evaluate later. */
|
|
1130
|
+
applyExact(expr, newNodes, secondPass) {
|
|
1131
|
+
|
|
1132
|
+
if (expr instanceof Template) {
|
|
1133
|
+
|
|
1134
|
+
let ng = this.getNodeGroup(expr, true);
|
|
1135
|
+
if (ng) {
|
|
1136
|
+
|
|
1137
|
+
// TODO: Track ranges of changed nodes and only pass those to udomdiff?
|
|
1138
|
+
// But will that break the swap benchmark?
|
|
1139
|
+
newNodes.push(...ng.getNodes());
|
|
1140
|
+
this.nodeGroups.push(ng);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// If expression, mark it to be evaluated later in ExprPath.apply() to find partial match.
|
|
1144
|
+
else {
|
|
1145
|
+
secondPass.push([newNodes.length, this.nodeGroups.length]);
|
|
1146
|
+
newNodes.push(expr);
|
|
1147
|
+
this.nodeGroups.push(null); // placeholder
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// Node created by an expression.
|
|
1152
|
+
else if (expr instanceof Node) {
|
|
1153
|
+
|
|
1154
|
+
// DocumentFragment created by an expression.
|
|
1155
|
+
if (expr instanceof DocumentFragment)
|
|
1156
|
+
newNodes.push(...expr.childNodes);
|
|
1157
|
+
else
|
|
1158
|
+
newNodes.push(expr);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// Arrays and functions.
|
|
1162
|
+
// I tried iterating over the result of a generator function to avoid this recursion and simplify the code,
|
|
1163
|
+
// but that consistently made the js-framework-benchmarks a few percentage points slower.
|
|
1164
|
+
else if (Array.isArray(expr))
|
|
1165
|
+
for (let subExpr of expr)
|
|
1166
|
+
this.applyExact(subExpr, newNodes, secondPass);
|
|
1167
|
+
|
|
1168
|
+
else if (typeof expr === 'function') {
|
|
1169
|
+
Globals.currentExprPath = [this, expr]; // Used by watch3()
|
|
1170
|
+
let result = expr();
|
|
1171
|
+
Globals.currentExprPath = null;
|
|
1172
|
+
|
|
1173
|
+
this.applyExact(result, newNodes, secondPass);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// Text
|
|
1177
|
+
else {
|
|
1178
|
+
// Convert falsy values (but not 0) to empty string.
|
|
1179
|
+
// Convert numbers to string so they compare the same.
|
|
1180
|
+
let text = (expr === undefined || expr === false || expr === null) ? '' : (expr + '');
|
|
1181
|
+
|
|
1182
|
+
// Fast path for updating the text of a single text node.
|
|
1183
|
+
let first = this.nodeBefore.nextSibling;
|
|
1184
|
+
if (first.nodeType === 3 && first.nextSibling === this.nodeMarker && !newNodes.includes(first)) {
|
|
1185
|
+
if (first.textContent !== text)
|
|
1186
|
+
first.textContent = text;
|
|
1187
|
+
|
|
1188
|
+
newNodes.push(first);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
else {
|
|
1192
|
+
// TODO: Optimize this into a Set or Map or something?
|
|
1193
|
+
if (!this.existingTextNodes)
|
|
1194
|
+
this.existingTextNodes = this.getNodes().filter(n => n.nodeType === 3);
|
|
1195
|
+
|
|
1196
|
+
let idx = this.existingTextNodes.findIndex(n => n.textContent === text);
|
|
1197
|
+
if (idx !== -1)
|
|
1198
|
+
newNodes.push(...this.existingTextNodes.splice(idx, 1));
|
|
1199
|
+
else
|
|
1200
|
+
newNodes.push(this.nodeMarker.ownerDocument.createTextNode(text));
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
applyMultipleAttribs(node, expr) {
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
if (Array.isArray(expr))
|
|
1209
|
+
expr = expr.flat().join(' '); // flat and join so we can accept arrays of arrays of strings.
|
|
1210
|
+
|
|
1211
|
+
// Add new attributes
|
|
1212
|
+
let oldNames = this.attrNames;
|
|
1213
|
+
this.attrNames = new Set();
|
|
1214
|
+
if (expr) {
|
|
1215
|
+
let attrs = (expr +'') // Split string into multiple attributes.
|
|
1216
|
+
.split(/([\w-]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s]+))/g)
|
|
1217
|
+
.map(text => text.trim())
|
|
1218
|
+
.filter(text => text.length);
|
|
1219
|
+
|
|
1220
|
+
for (let attr of attrs) {
|
|
1221
|
+
let [name, value] = attr.split(/\s*=\s*/); // split on first equals.
|
|
1222
|
+
value = (value || '').replace(/^(['"])(.*)\1$/, '$2'); // trim value quotes if they match.
|
|
1223
|
+
node.setAttribute(name, value);
|
|
1224
|
+
this.attrNames.add(name);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// Remove old attributes.
|
|
1229
|
+
for (let oldName of oldNames)
|
|
1230
|
+
if (!this.attrNames.has(oldName))
|
|
1231
|
+
node.removeAttribute(oldName);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/**
|
|
1235
|
+
* Handle attributes for event binding, such as:
|
|
1236
|
+
* onclick=${(e, el) => this.doSomething(el, 'meow')}
|
|
1237
|
+
* oninput=${[this.doSomething, 'meow']}
|
|
672
1238
|
* onclick=${[this, 'doSomething', 'meow']}
|
|
673
1239
|
*
|
|
674
1240
|
* @param node
|
|
@@ -677,78 +1243,91 @@ class ExprPath {
|
|
|
677
1243
|
applyEventAttrib(node, expr, root) {
|
|
678
1244
|
|
|
679
1245
|
|
|
680
|
-
let eventName = this.attrName.slice(2);
|
|
1246
|
+
let eventName = this.attrName.slice(2); // remove "on-" prefix.
|
|
681
1247
|
let func;
|
|
682
1248
|
|
|
683
1249
|
// Convert array to function.
|
|
684
|
-
// TODO: This doesn't work for [this, 'doSomething', 'meow']
|
|
685
1250
|
let args = [];
|
|
686
1251
|
if (Array.isArray(expr)) {
|
|
687
|
-
for (let i=0; i<expr.length; i++)
|
|
688
|
-
if (typeof expr[i] === 'function') {
|
|
689
|
-
func = expr[i];
|
|
690
|
-
args = expr.slice(i+1);
|
|
691
|
-
break;
|
|
692
|
-
}
|
|
693
1252
|
|
|
1253
|
+
// oninput=${[this.doSomething, 'meow']}
|
|
1254
|
+
if (typeof expr[0] === 'function') {
|
|
1255
|
+
func = expr[0];
|
|
1256
|
+
args = expr.slice(1);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// Undocumented.
|
|
694
1260
|
// oninput=${[this, 'value']}
|
|
695
|
-
|
|
1261
|
+
else {
|
|
696
1262
|
func = setValue;
|
|
697
1263
|
args = [expr[0], expr.slice(1), node];
|
|
698
1264
|
node.value = delve(expr[0], expr.slice(1));
|
|
699
|
-
//
|
|
1265
|
+
// root.render(); // TODO: This causes infinite recursion.
|
|
700
1266
|
}
|
|
701
1267
|
}
|
|
702
1268
|
else
|
|
703
1269
|
func = expr;
|
|
704
1270
|
|
|
705
|
-
let
|
|
706
|
-
|
|
707
|
-
|
|
1271
|
+
let nodeEvents = Globals.nodeEvents.get(node);
|
|
1272
|
+
if (!nodeEvents) {
|
|
1273
|
+
nodeEvents = {[eventName]: new Array(3)};
|
|
1274
|
+
Globals.nodeEvents.set(node, nodeEvents);
|
|
1275
|
+
}
|
|
1276
|
+
let nodeEvent = nodeEvents[eventName];
|
|
1277
|
+
|
|
708
1278
|
|
|
709
1279
|
|
|
710
|
-
|
|
1280
|
+
// If function has changed, remove and rebind the event.
|
|
1281
|
+
if (nodeEvent[0] !== func) {
|
|
1282
|
+
let [existing, existingBound, _] = nodeEvent;
|
|
711
1283
|
if (existing)
|
|
712
1284
|
node.removeEventListener(eventName, existingBound);
|
|
713
1285
|
|
|
714
1286
|
let originalFunc = func;
|
|
715
1287
|
|
|
716
1288
|
// BoundFunc sets the "this" variable to be the current Solarite component.
|
|
717
|
-
let boundFunc = event =>
|
|
1289
|
+
let boundFunc = (event) => {
|
|
1290
|
+
let args = nodeEvent[2];
|
|
1291
|
+
return originalFunc.call(root, ...args, event, node);
|
|
1292
|
+
};
|
|
718
1293
|
|
|
719
1294
|
// Save both the original and bound functions.
|
|
720
1295
|
// Original so we can compare it against a newly assigned function.
|
|
721
1296
|
// Bound so we can use it with removeEventListner().
|
|
722
|
-
|
|
1297
|
+
nodeEvent[0] = originalFunc;
|
|
1298
|
+
nodeEvent[1] = boundFunc;
|
|
723
1299
|
|
|
724
1300
|
node.addEventListener(eventName, boundFunc);
|
|
725
1301
|
|
|
726
|
-
// TODO: classic event attribs
|
|
1302
|
+
// TODO: classic event attribs?
|
|
727
1303
|
//el[attr.name] = e => // e.g. el.onclick = ...
|
|
728
1304
|
// (new Function('event', 'el', attr.value)).bind(this.manager.rootEl)(e, el) // put "event", "el", and "this" in scope for the event code.
|
|
729
1305
|
}
|
|
1306
|
+
|
|
1307
|
+
// Otherwise just update the args to the function.
|
|
1308
|
+
nodeEvents[eventName][2] = args;
|
|
730
1309
|
}
|
|
731
1310
|
|
|
732
1311
|
applyValueAttrib(node, exprs, exprIndex) {
|
|
733
1312
|
let expr = exprs[exprIndex];
|
|
734
|
-
|
|
735
|
-
// Array for form element data binding.
|
|
736
|
-
// TODO: This never worked, and was moved to applyEventAttrib.
|
|
737
|
-
// let isArrayValue = Array.isArray(expr);
|
|
738
|
-
// if (isArrayValue && expr.length >= 2 && !expr.slice(1).find(v => !['string', 'number'].includes(typeof v))) {
|
|
739
|
-
// node.value = delve(expr[0], expr.slice(1));
|
|
740
|
-
// node.addEventListener('input', e => {
|
|
741
|
-
// delve(expr[0], expr.slice(1), node.value) // TODO: support other properties like checked
|
|
742
|
-
// });
|
|
743
|
-
// }
|
|
744
1313
|
|
|
745
1314
|
// Values to toggle an attribute
|
|
746
1315
|
if (!this.attrValue && (expr === false || expr === null || expr === undefined))
|
|
747
1316
|
node.removeAttribute(this.attrName);
|
|
748
|
-
|
|
1317
|
+
|
|
749
1318
|
else if (!this.attrValue && expr === true)
|
|
750
1319
|
node.setAttribute(this.attrName, '');
|
|
751
1320
|
|
|
1321
|
+
// Passing a path to the value attribute.
|
|
1322
|
+
// This same logic is in NodeGroup.createNewComponent() for components.
|
|
1323
|
+
else if ((this.attrName === 'value' || this.attrName === 'data-value') && Util.isPath(expr)) {
|
|
1324
|
+
let [obj, path] = [expr[0], expr.slice(1)];
|
|
1325
|
+
node.value = delve(obj, path);
|
|
1326
|
+
node.addEventListener('input', () => {
|
|
1327
|
+
delve(obj, path, Util.getInputValue(node));
|
|
1328
|
+
}, true); // We use capture so we update the values before other events added by the user.
|
|
1329
|
+
}
|
|
1330
|
+
|
|
752
1331
|
// Regular attribute
|
|
753
1332
|
else {
|
|
754
1333
|
let value = [];
|
|
@@ -764,21 +1343,25 @@ class ExprPath {
|
|
|
764
1343
|
exprIndex--;
|
|
765
1344
|
}
|
|
766
1345
|
}
|
|
767
|
-
|
|
768
1346
|
exprIndex ++;
|
|
769
1347
|
}
|
|
770
1348
|
else
|
|
771
1349
|
value.unshift(expr);
|
|
772
1350
|
|
|
773
1351
|
let joinedValue = value.join('');
|
|
774
|
-
|
|
1352
|
+
|
|
1353
|
+
// Only update attributes if the value has changed.
|
|
1354
|
+
// The .value property is special. If it changes we don't update the attribute.
|
|
1355
|
+
let oldVal = this.attrName === 'value' ? node.value : node.getAttribute(this.attrName);
|
|
1356
|
+
if (oldVal !== joinedValue) {
|
|
1357
|
+
node.setAttribute(this.attrName, joinedValue);
|
|
1358
|
+
}
|
|
775
1359
|
|
|
776
1360
|
// This is needed for setting input.value, .checked, option.selected, etc.
|
|
777
1361
|
// But in some cases setting the attribute is enough. such as div.setAttribute('title') updates div.title.
|
|
778
1362
|
// TODO: How to tell which is which?
|
|
779
1363
|
if (this.attrName in node)
|
|
780
1364
|
node[this.attrName] = joinedValue;
|
|
781
|
-
|
|
782
1365
|
}
|
|
783
1366
|
|
|
784
1367
|
return exprIndex;
|
|
@@ -788,20 +1371,22 @@ class ExprPath {
|
|
|
788
1371
|
/**
|
|
789
1372
|
*
|
|
790
1373
|
* @param newRoot {HTMLElement}
|
|
1374
|
+
* @param pathOffset {int}
|
|
791
1375
|
* @return {ExprPath} */
|
|
792
|
-
clone(newRoot) {
|
|
1376
|
+
clone(newRoot, pathOffset=0) {
|
|
793
1377
|
|
|
794
1378
|
|
|
795
|
-
|
|
1379
|
+
// Resolve node paths.
|
|
796
1380
|
let nodeMarker, nodeBefore;
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1381
|
+
let root = newRoot;
|
|
1382
|
+
let path = pathOffset ? this.nodeMarkerPath.slice(0, -pathOffset) : this.nodeMarkerPath;
|
|
1383
|
+
for (let i=path.length-1; i>0; i--) // Resolve the path.
|
|
1384
|
+
root = root.childNodes[path[i]];
|
|
801
1385
|
let childNodes = root.childNodes;
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
1386
|
+
|
|
1387
|
+
nodeMarker = path.length ? childNodes[path[0]] : newRoot;
|
|
1388
|
+
if (this.nodeBefore)
|
|
1389
|
+
nodeBefore = childNodes[this.nodeBeforeIndex];
|
|
805
1390
|
|
|
806
1391
|
let result = new ExprPath(nodeBefore, nodeMarker, this.type, this.attrName, this.attrValue);
|
|
807
1392
|
|
|
@@ -809,41 +1394,27 @@ class ExprPath {
|
|
|
809
1394
|
|
|
810
1395
|
return result;
|
|
811
1396
|
}
|
|
812
|
-
|
|
1397
|
+
|
|
813
1398
|
/**
|
|
814
1399
|
* Clear the nodeCache of this ExprPath, as well as all parent and child ExprPaths that
|
|
815
1400
|
* share the same DOM parent node.
|
|
816
1401
|
*
|
|
817
|
-
* TODO: Is recursive clearing ever necessary?
|
|
818
|
-
*/
|
|
1402
|
+
* TODO: Is recursive clearing ever necessary? */
|
|
819
1403
|
clearNodesCache() {
|
|
820
1404
|
let path = this;
|
|
821
|
-
|
|
1405
|
+
|
|
822
1406
|
// Clear cache parent ExprPaths that have the same parentNode
|
|
823
|
-
let parentNode = this.parentNode;
|
|
824
|
-
while (path && path.parentNode === parentNode) {
|
|
1407
|
+
let parentNode = this.nodeMarker.parentNode;
|
|
1408
|
+
while (path && path.nodeMarker.parentNode === parentNode) {
|
|
825
1409
|
path.nodesCache = null;
|
|
826
1410
|
path = path.parentNg?.parentPath;
|
|
827
|
-
|
|
1411
|
+
|
|
828
1412
|
// If stuck in an infinite loop here, the problem is likely due to Template hash colisions.
|
|
829
1413
|
// Which cause one path to be the descendant of itself, creating a cycle.
|
|
830
1414
|
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
// Clear cache of child ExprPaths that have the same parentNode
|
|
835
|
-
for (let ng of path.nodeGroups) {
|
|
836
|
-
if (ng) // Can be null from applyOneExpr()'s push(null) call.
|
|
837
|
-
for (let path2 of ng.paths) {
|
|
838
|
-
if (path2.type === PathType.Content && path2.parentNode === parentNode) {
|
|
839
|
-
path2.nodesCache = null;
|
|
840
|
-
clearChildNodeCache(path2);
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
clearChildNodeCache(this);
|
|
1415
|
+
|
|
1416
|
+
// Commented out on Sep 30, 2024 b/c it was making the benchmark never finish when adding 10k rows.
|
|
1417
|
+
//clearChildNodeCache(this);
|
|
847
1418
|
}
|
|
848
1419
|
|
|
849
1420
|
|
|
@@ -856,44 +1427,46 @@ class ExprPath {
|
|
|
856
1427
|
|
|
857
1428
|
// If parent is the only child of the grandparent, replace the whole parent.
|
|
858
1429
|
// And if it has no siblings, it's not created by a NodeGroup/path.
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1430
|
+
// Commented out because this will break any references.
|
|
1431
|
+
// And because I don't see much performance difference.
|
|
1432
|
+
// let grandparent = parent.parentNode
|
|
1433
|
+
// if (grandparent && parent === grandparent.firstChild && parent === grandparent.lastChild && !parent.hasAttribute('id')) {
|
|
1434
|
+
// let replacement = document.createElement(parent.tagName)
|
|
1435
|
+
// replacement.append(this.nodeBefore, this.nodeMarker)
|
|
1436
|
+
// for (let attrib of parent.attributes)
|
|
1437
|
+
// replacement.setAttribute(attrib.name, attrib.value)
|
|
1438
|
+
// parent.replaceWith(replacement)
|
|
1439
|
+
// }
|
|
1440
|
+
// else {
|
|
868
1441
|
parent.innerHTML = ''; // Faster than calling .removeChild() a thousand times.
|
|
869
1442
|
parent.append(this.nodeBefore, this.nodeMarker);
|
|
870
|
-
}
|
|
1443
|
+
//}
|
|
871
1444
|
return true;
|
|
872
1445
|
}
|
|
873
1446
|
return false;
|
|
874
1447
|
}
|
|
875
|
-
|
|
1448
|
+
|
|
876
1449
|
/**
|
|
877
1450
|
* @return {(Node|HTMLElement)[]} */
|
|
878
1451
|
getNodes() {
|
|
879
|
-
|
|
1452
|
+
|
|
880
1453
|
// Why doesn't this work?
|
|
881
1454
|
// let result2 = [];
|
|
882
1455
|
// for (let ng of this.nodeGroups)
|
|
883
1456
|
// result2.push(...ng.getNodes())
|
|
884
1457
|
// return result2;
|
|
885
|
-
|
|
886
|
-
|
|
1458
|
+
|
|
1459
|
+
|
|
887
1460
|
let result;
|
|
888
1461
|
|
|
889
1462
|
// This shaves about 5ms off the partialUpdate benchmark.
|
|
890
|
-
|
|
1463
|
+
result = this.nodesCache;
|
|
891
1464
|
if (result) {
|
|
1465
|
+
|
|
892
1466
|
|
|
893
|
-
|
|
894
|
-
|
|
1467
|
+
|
|
895
1468
|
return result
|
|
896
|
-
}
|
|
1469
|
+
}
|
|
897
1470
|
|
|
898
1471
|
result = [];
|
|
899
1472
|
let current = this.nodeBefore.nextSibling;
|
|
@@ -910,13 +1483,100 @@ class ExprPath {
|
|
|
910
1483
|
getParentNode() { // Same as this.parentNode
|
|
911
1484
|
return this.nodeMarker.parentNode
|
|
912
1485
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* Get an unused NodeGroup that matches the template's html and expressions (exact=true)
|
|
1489
|
+
* or at least the html (exact=false).
|
|
1490
|
+
* Remove it from nodeGroupsFree if it exists, or create it if not.
|
|
1491
|
+
* Then add it to nodeGroupsInUse.
|
|
1492
|
+
*
|
|
1493
|
+
* @param template {Template}
|
|
1494
|
+
* @param exact {boolean}
|
|
1495
|
+
* If true, return an exact match, or null.
|
|
1496
|
+
* If false, either find a match for the template's html and then apply the template's expressions,
|
|
1497
|
+
* or createa new NodeGroup from the template.
|
|
1498
|
+
* @return {NodeGroup} */
|
|
1499
|
+
getNodeGroup(template, exact=true) {
|
|
1500
|
+
//if (exact && this.nodeGroupsFree.isEmpty())
|
|
1501
|
+
// return null;
|
|
1502
|
+
|
|
1503
|
+
let result;
|
|
1504
|
+
|
|
1505
|
+
if (exact) {
|
|
1506
|
+
result = this.nodeGroupsFree.delete(template.getExactKey());
|
|
1507
|
+
if (result) // also delete the matching close key.
|
|
1508
|
+
this.nodeGroupsFree.delete(template.getCloseKey(), result);
|
|
1509
|
+
else
|
|
1510
|
+
return null;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// Find a close match.
|
|
1514
|
+
// This is a match that has matching html, but different expressions applied.
|
|
1515
|
+
// We can then apply the expressions to make it an exact match.
|
|
1516
|
+
else {
|
|
1517
|
+
result = this.nodeGroupsFree.delete(template.getCloseKey());
|
|
1518
|
+
if (result) {
|
|
1519
|
+
|
|
1520
|
+
this.nodeGroupsFree.delete(result.exactKey, result);
|
|
1521
|
+
|
|
1522
|
+
// Update this close match with the new expression values.
|
|
1523
|
+
result.applyExprs(template.exprs);
|
|
1524
|
+
result.exactKey = template.getExactKey(); // TODO: Should this be set elsewhere?
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
if (!result)
|
|
1529
|
+
result = new NodeGroup(template, this);
|
|
1530
|
+
|
|
1531
|
+
// old:
|
|
1532
|
+
this.nodeGroupsInUse.push(result);
|
|
1533
|
+
|
|
1534
|
+
// new:
|
|
1535
|
+
// let ngiu = this.nodeGroupsInUse;
|
|
1536
|
+
// ngiu.add(result.exactKey, result);
|
|
1537
|
+
// ngiu.add(result.closeKey, result);
|
|
1538
|
+
|
|
916
1539
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1540
|
+
return result;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
|
|
1544
|
+
/**
|
|
1545
|
+
* Used with getNodeGroup() and freeNodeGroups().
|
|
1546
|
+
* TODO: Use an array of WeakRef so the gc can collect them?
|
|
1547
|
+
* TODO: Put items back in nodeGroupsInUse after applyExpr() is called, not before.
|
|
1548
|
+
* @type {NodeGroup[]} */
|
|
1549
|
+
nodeGroupsInUse = [];
|
|
1550
|
+
|
|
1551
|
+
/** @type {MultiValueMap<key:string, value:NodeGroup>} */
|
|
1552
|
+
//nodeGroupsInUse = new MultiValueMap();
|
|
1553
|
+
|
|
1554
|
+
/**
|
|
1555
|
+
* Used with getNodeGroup() and freeNodeGroups().
|
|
1556
|
+
* Each NodeGroup is here twice, once under an exact key, and once under the close key.
|
|
1557
|
+
* @type {MultiValueMap<key:string, value:NodeGroup>} */
|
|
1558
|
+
nodeGroupsFree = new MultiValueMap();
|
|
1559
|
+
|
|
1560
|
+
|
|
1561
|
+
/**
|
|
1562
|
+
* Move everything from this.nodeGroupsInUse to this.nodeGroupsFree.
|
|
1563
|
+
* TODO: this could run as needed in getNodeGroup? */
|
|
1564
|
+
freeNodeGroups() {
|
|
1565
|
+
// old:
|
|
1566
|
+
let ngf = this.nodeGroupsFree;
|
|
1567
|
+
for (let ng of this.nodeGroupsInUse) {
|
|
1568
|
+
ngf.add(ng.exactKey, ng);
|
|
1569
|
+
ngf.add(ng.closeKey, ng);
|
|
1570
|
+
}
|
|
1571
|
+
this.nodeGroupsInUse = [];
|
|
1572
|
+
|
|
1573
|
+
// new:
|
|
1574
|
+
// for (let key in this.nodeGroupsFree.data)
|
|
1575
|
+
// for (let item of this.nodeGroupsFree.data[key])
|
|
1576
|
+
// this.nodeGroupsInUse.add(key, item);
|
|
1577
|
+
//
|
|
1578
|
+
// this.nodeGroupsFree = this.nodeGroupsInUse;
|
|
1579
|
+
// this.nodeGroupsInUse = new MultiValueMap();
|
|
920
1580
|
}
|
|
921
1581
|
|
|
922
1582
|
|
|
@@ -936,26 +1596,27 @@ function setValue(root, path, node) {
|
|
|
936
1596
|
val = parseFloat(val);
|
|
937
1597
|
|
|
938
1598
|
delve(root, path, val);
|
|
939
|
-
|
|
940
|
-
//this.render();
|
|
941
1599
|
}
|
|
942
1600
|
|
|
943
|
-
/** @enum {
|
|
1601
|
+
/** @enum {int} */
|
|
944
1602
|
const PathType = {
|
|
945
1603
|
/** Child of a node */
|
|
946
|
-
Content:
|
|
1604
|
+
Content: 1,
|
|
947
1605
|
|
|
948
1606
|
/** One or more whole attributes */
|
|
949
|
-
Multiple:
|
|
1607
|
+
Multiple: 2,
|
|
950
1608
|
|
|
951
1609
|
/** Value of an attribute. */
|
|
952
|
-
Value:
|
|
1610
|
+
Value: 3,
|
|
953
1611
|
|
|
954
1612
|
/** Value of an attribute being passed to a component. */
|
|
955
|
-
Component:
|
|
1613
|
+
Component: 4,
|
|
956
1614
|
|
|
957
1615
|
/** Expressions inside Html comments. */
|
|
958
|
-
Comment:
|
|
1616
|
+
Comment: 5,
|
|
1617
|
+
|
|
1618
|
+
/** Value of an attribute. */
|
|
1619
|
+
Event: 6,
|
|
959
1620
|
};
|
|
960
1621
|
|
|
961
1622
|
|
|
@@ -981,1278 +1642,299 @@ function resolveNodePath(root, path) {
|
|
|
981
1642
|
for (let i=path.length-1; i>=0; i--)
|
|
982
1643
|
root = root.childNodes[path[i]];
|
|
983
1644
|
return root;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
// TODO: Memory from this is never freed. Use a WeakMap<Node, Object<eventName:string, function[]>>
|
|
988
|
-
let nodeEvents = {};
|
|
989
|
-
let nodeEventArgs = {};
|
|
990
|
-
|
|
991
|
-
/**
|
|
992
|
-
* A Shell is created from a tagged template expression instantiated as Nodes,
|
|
993
|
-
* but without any expressions filled in. */
|
|
994
|
-
class Shell {
|
|
995
|
-
|
|
996
|
-
/**
|
|
997
|
-
* @type {DocumentFragment} Parent of the shell nodes. */
|
|
998
|
-
fragment;
|
|
999
|
-
|
|
1000
|
-
/** @type {ExprPath[]} Paths to where expressions should go. */
|
|
1001
|
-
paths = [];
|
|
1002
|
-
|
|
1003
|
-
/** @type {?Template} Template that created this element. */
|
|
1004
|
-
template;
|
|
1005
|
-
|
|
1006
|
-
// Embeds and ids
|
|
1007
|
-
events = [];
|
|
1008
|
-
|
|
1009
|
-
/** @type {int[][]} Array of paths */
|
|
1010
|
-
ids = [];
|
|
1011
|
-
scripts = [];
|
|
1012
|
-
styles = [];
|
|
1013
|
-
|
|
1014
|
-
staticComponents = [];
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
/**
|
|
1018
|
-
* Create the nodes but without filling in the expressions.
|
|
1019
|
-
* This is useful because the expression-less nodes created by a template can be cached.
|
|
1020
|
-
* @param html {string[]} */
|
|
1021
|
-
constructor(html=null) {
|
|
1022
|
-
if (!html)
|
|
1023
|
-
return;
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
// 1. Add placeholders
|
|
1028
|
-
// We increment the placeholder char as we go because nodes can't have the same attribute more than once.
|
|
1029
|
-
let placeholder = 0xe000; // https://en.wikipedia.org/wiki/Private_Use_Areas 6400.
|
|
1030
|
-
|
|
1031
|
-
let buffer = [];
|
|
1032
|
-
let commentPlaceholder = `<!--!✨!-->`;
|
|
1033
|
-
let componentNames = {};
|
|
1034
|
-
|
|
1035
|
-
htmlContext(null); // Reset the context.
|
|
1036
|
-
for (let i=0; i<html.length; i++) {
|
|
1037
|
-
let lastHtml = html[i];
|
|
1038
|
-
let context = htmlContext(lastHtml);
|
|
1039
|
-
|
|
1040
|
-
// Swap out Embedded Solarite Components with ${} attributes.
|
|
1041
|
-
// Later, NodeGroup.render() will search for these and replace them with the real components.
|
|
1042
|
-
// Ctrl+F "redcomponent-placeholder" in project to find all code that manages subcomponents.
|
|
1043
|
-
if (context === htmlContext.Attribute) {
|
|
1044
|
-
|
|
1045
|
-
let lastIndex, lastMatch;
|
|
1046
|
-
lastHtml.replace(/<[a-z][a-z0-9]*-[a-z0-9-]+/ig, (match, index) => {
|
|
1047
|
-
lastIndex = index+1; // +1 for after opening <
|
|
1048
|
-
lastMatch = match.slice(1);
|
|
1049
|
-
});
|
|
1050
|
-
|
|
1051
|
-
if (lastMatch) {
|
|
1052
|
-
let newTagName = lastMatch + '-redcomponent-placeholder';
|
|
1053
|
-
lastHtml = lastHtml.slice(0, lastIndex) + newTagName + lastHtml.slice(lastIndex + lastMatch.length);
|
|
1054
|
-
componentNames[lastMatch] = newTagName;
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
buffer.push(lastHtml);
|
|
1059
|
-
//console.log(lastHtml, context)
|
|
1060
|
-
if (i < html.length-1)
|
|
1061
|
-
if (context === htmlContext.Text)
|
|
1062
|
-
buffer.push(commentPlaceholder); // Comment Placeholder. because we can't put text in between <tr> tags for example.
|
|
1063
|
-
else
|
|
1064
|
-
buffer.push(String.fromCharCode(placeholder+i));
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
// 2. Create elements from html with placeholders.
|
|
1068
|
-
let template = document.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
|
|
1069
|
-
let joinedHtml = buffer.join('');
|
|
1070
|
-
|
|
1071
|
-
// Replace '-redcomponent-placeholder' close tags.
|
|
1072
|
-
// TODO: is there a better way? What if the close tag is inside a comment?
|
|
1073
|
-
for (let name in componentNames)
|
|
1074
|
-
joinedHtml = joinedHtml.replaceAll(`</${name}>`, `</${componentNames[name]}>`);
|
|
1075
|
-
|
|
1076
|
-
if (joinedHtml)
|
|
1077
|
-
template.innerHTML = joinedHtml;
|
|
1078
|
-
else // Create one text node, so shell isn't empty and NodeGroups created from it have something to point the startNode and endNode at.
|
|
1079
|
-
template.content.append(document.createTextNode(''));
|
|
1080
|
-
this.fragment = template.content;
|
|
1081
|
-
|
|
1082
|
-
// 3. Find placeholders
|
|
1083
|
-
let node;
|
|
1084
|
-
let toRemove = [];
|
|
1085
|
-
const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
|
|
1086
|
-
while (node = walker.nextNode()) {
|
|
1087
|
-
|
|
1088
|
-
// Remove previous after each iteration, so paths will still be calculated correctly.
|
|
1089
|
-
toRemove.map(el => el.remove());
|
|
1090
|
-
toRemove = [];
|
|
1091
|
-
|
|
1092
|
-
// Replace attributes
|
|
1093
|
-
if (node.nodeType === 1) {
|
|
1094
|
-
for (let attr of node.attributes) {
|
|
1095
|
-
|
|
1096
|
-
// Whole attribute
|
|
1097
|
-
let matches = attr.name.match(/^[\ue000-\uf8ff]$/);
|
|
1098
|
-
if (matches) {
|
|
1099
|
-
this.paths.push(new ExprPath(null, node, PathType.Multiple));
|
|
1100
|
-
node.removeAttribute(matches[0]);
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
// Just the attribute value.
|
|
1104
|
-
else {
|
|
1105
|
-
let parts = attr.value.split(/[\ue000-\uf8ff]/g);
|
|
1106
|
-
if (parts.length > 1) {
|
|
1107
|
-
let nonEmptyParts = (parts.length === 2 && !parts[0].length && !parts[1].length) ? null : parts;
|
|
1108
|
-
this.paths.push(new ExprPath(null, node, PathType.Value, attr.name, nonEmptyParts));
|
|
1109
|
-
node.setAttribute(attr.name, parts.join(''));
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
// Replace comment placeholders
|
|
1115
|
-
else if (node.nodeType === Node.COMMENT_NODE && node.nodeValue === '!✨!') {
|
|
1116
|
-
|
|
1117
|
-
// Get or create nodeBefore.
|
|
1118
|
-
let nodeBefore = node.previousSibling; // Can be the same as another Path's nodeMarker.
|
|
1119
|
-
if (!nodeBefore) {
|
|
1120
|
-
nodeBefore = document.createComment('PathStart:'+this.paths.length);
|
|
1121
|
-
node.parentNode.insertBefore(nodeBefore, node);
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
// Get the next node.
|
|
1126
|
-
let nodeMarker;
|
|
1127
|
-
|
|
1128
|
-
// A subsequent node is available to be a nodeMarker.
|
|
1129
|
-
if (node.nextSibling && (node.nextSibling.nodeType !== 8 || node.nextSibling.textContent !== '!✨!')) {
|
|
1130
|
-
nodeMarker = node.nextSibling;
|
|
1131
|
-
toRemove.push(node); // Removing them here will mess up the treeWalker.
|
|
1132
|
-
}
|
|
1133
|
-
// Re-use existing comment placeholder.
|
|
1134
|
-
else {
|
|
1135
|
-
nodeMarker = node;
|
|
1136
|
-
nodeMarker.textContent = 'PathEnd:'+ this.paths.length;
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
let path = new ExprPath(nodeBefore, nodeMarker, PathType.Content);
|
|
1143
|
-
|
|
1144
|
-
this.paths.push(path);
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
// Sometimes users will comment out a block of html code that has expressions.
|
|
1149
|
-
// Here we look for expressions in comments.
|
|
1150
|
-
// We don't actually update them dynamically, but we still add paths for them.
|
|
1151
|
-
// That way the expression count still matches.
|
|
1152
|
-
else if (node.nodeType === Node.COMMENT_NODE) {
|
|
1153
|
-
let parts = node.textContent.split(/[\ue000-\uf8ff]/g);
|
|
1154
|
-
for (let i=0; i<parts.length-1; i++) {
|
|
1155
|
-
let path = new ExprPath(node.previousSibling, node);
|
|
1156
|
-
path.type = PathType.Comment;
|
|
1157
|
-
|
|
1158
|
-
this.paths.push(path);
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
// Replace comment placeholders inside script and style tags, which have become text nodes.
|
|
1163
|
-
else if (node.nodeType === Node.TEXT_NODE && ['SCRIPT', 'STYLE'].includes(node.parentNode?.nodeName)) {
|
|
1164
|
-
let parts = node.textContent.split(commentPlaceholder);
|
|
1165
|
-
if (parts.length > 1) {
|
|
1166
|
-
|
|
1167
|
-
let placeholders = [];
|
|
1168
|
-
for (let i = 0; i<parts.length; i++) {
|
|
1169
|
-
let current = document.createTextNode(parts[i]);
|
|
1170
|
-
node.parentNode.insertBefore(current, node);
|
|
1171
|
-
if (i > 0)
|
|
1172
|
-
placeholders.push(current);
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
for (let i=0, node; node=placeholders[i]; i++) {
|
|
1176
|
-
let path = new ExprPath(node.previousSibling, node, PathType.Content);
|
|
1177
|
-
|
|
1178
|
-
this.paths.push(path);
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
// Removing them here will mess up the treeWalker.
|
|
1184
|
-
toRemove.push(node);
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
toRemove.map(el => el.remove());
|
|
1189
|
-
|
|
1190
|
-
// Handle redcomponent-placeholder's.
|
|
1191
|
-
// Ctrl+F "redcomponent-placeholder" in project to find all code that manages subcomponents.
|
|
1192
|
-
//if (componentNames.size)
|
|
1193
|
-
// this.components = [...this.fragment.querySelectorAll([...componentNames].join(','))]
|
|
1194
|
-
|
|
1195
|
-
// Rename "is" attributes so the Web Components don't instantiate until we have the values of their PathExpr arguments.
|
|
1196
|
-
// that happens in NodeGroup.applyComponentExprs()
|
|
1197
|
-
for (let el of this.fragment.querySelectorAll('[is]')) {
|
|
1198
|
-
el.setAttribute('_is', el.getAttribute('is'));
|
|
1199
|
-
// this.components.push(el);
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
for (let path of this.paths) {
|
|
1203
|
-
if (path.nodeBefore)
|
|
1204
|
-
path.nodeBeforeIndex = Array.prototype.indexOf.call(path.nodeBefore.parentNode.childNodes, path.nodeBefore);
|
|
1205
|
-
path.nodeMarkerPath = getNodePath(path.nodeMarker);
|
|
1206
|
-
|
|
1207
|
-
// Cache so we don't have to calculate this later inside NodeGroup.applyExprs()
|
|
1208
|
-
if (path.type === PathType.Value && path.nodeMarker.nodeType === 1 &&
|
|
1209
|
-
(path.nodeMarker.tagName.includes('-') || path.nodeMarker.hasAttribute('is'))) {
|
|
1210
|
-
path.type = PathType.Component;
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
this.findEmbeds();
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
} // end constructor
|
|
1220
|
-
|
|
1221
|
-
/**
|
|
1222
|
-
* We find the path to every embed here once in the Shell, instead of every time a NodeGroup is instantiated.
|
|
1223
|
-
* When a Nodegroup is created, it calls NodeGroup.activateEmbeds() that uses these paths. */
|
|
1224
|
-
findEmbeds() {
|
|
1225
|
-
this.scripts = Array.prototype.map.call(this.fragment.querySelectorAll('scripts'), el => getNodePath(el));
|
|
1226
|
-
this.styles = Array.prototype.map.call(this.fragment.querySelectorAll('style'), el => getNodePath(el));
|
|
1227
|
-
|
|
1228
|
-
let idEls = this.fragment.querySelectorAll('[id],[data-id]');
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
// Check for valid id names.
|
|
1232
|
-
for (let el of idEls) {
|
|
1233
|
-
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
1234
|
-
if (div.hasOwnProperty(id))
|
|
1235
|
-
throw new Error(`<${el.tagName.toLowerCase()} id="${id}"> can't override existing HTMLElement property.`)
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
this.ids = Array.prototype.map.call(idEls, el => getNodePath(el));
|
|
1240
|
-
|
|
1241
|
-
// Events (not yet used)
|
|
1242
|
-
for (let el of this.fragment.querySelectorAll('*')) {
|
|
1243
|
-
for (let attrib of el.attributes)
|
|
1244
|
-
if (isEvent(attrib.name))
|
|
1245
|
-
this.events.push([attrib.name, getNodePath(el)]);
|
|
1246
|
-
|
|
1247
|
-
if (el.tagName.includes('-') || el.hasAttribute('_is'))
|
|
1248
|
-
|
|
1249
|
-
// Dynamic components have attributes with expression values.
|
|
1250
|
-
// They are created from applyExprs()
|
|
1251
|
-
// But static components are created in a separate path inside the NodeGroup constructor.
|
|
1252
|
-
if (!this.paths.find(path => path.nodeMarker === el))
|
|
1253
|
-
this.staticComponents.push(getNodePath(el));
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
/**
|
|
1259
|
-
* Get the shell for the html strings.
|
|
1260
|
-
* @param htmlStrings {string[]}
|
|
1261
|
-
* @returns {Shell} */
|
|
1262
|
-
static get(htmlStrings) {
|
|
1263
|
-
let result = shells.get(htmlStrings);
|
|
1264
|
-
if (!result) {
|
|
1265
|
-
result = new Shell(htmlStrings);
|
|
1266
|
-
shells.set(htmlStrings, result); // cache
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
return result;
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
let shells = new WeakMap();
|
|
1277
|
-
|
|
1278
|
-
/**
|
|
1279
|
-
* The html strings and evaluated expressions from an html tagged template.
|
|
1280
|
-
* A unique Template is created for each item in a loop.
|
|
1281
|
-
* Although the reference to the html strings is shared among templates. */
|
|
1282
|
-
class Template {
|
|
1283
|
-
|
|
1284
|
-
/** @type {(Template|string|function)|(Template|string|function)[]} Evaulated expressions. */
|
|
1285
|
-
exprs = []
|
|
1286
|
-
|
|
1287
|
-
/** @type {string[]} */
|
|
1288
|
-
html = [];
|
|
1289
|
-
|
|
1290
|
-
/**
|
|
1291
|
-
* If true, use this template to replace an existing element, instead of appending children to it.
|
|
1292
|
-
* @type {?boolean} */
|
|
1293
|
-
replaceMode;
|
|
1294
|
-
|
|
1295
|
-
/** Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
|
|
1296
|
-
hashedFields;
|
|
1297
|
-
|
|
1298
|
-
/**
|
|
1299
|
-
* @deprecated
|
|
1300
|
-
* @type {ExprPath} Used with forEach() from watch.js
|
|
1301
|
-
* Set in NodeGroup.applyOneExpr() */
|
|
1302
|
-
parentPath;
|
|
1303
|
-
|
|
1304
|
-
/** @type {NodeGroup} */
|
|
1305
|
-
nodeGroup;
|
|
1306
|
-
|
|
1307
|
-
/**
|
|
1308
|
-
* @type {string[][]} */
|
|
1309
|
-
paths = [];
|
|
1310
|
-
|
|
1311
|
-
/**
|
|
1312
|
-
*
|
|
1313
|
-
* @param htmlStrings {string[]}
|
|
1314
|
-
* @param exprs {*[]} */
|
|
1315
|
-
constructor(htmlStrings, exprs) {
|
|
1316
|
-
this.html = htmlStrings;
|
|
1317
|
-
this.exprs = exprs;
|
|
1318
|
-
|
|
1319
|
-
//this.trace = new Error().stack.split(/\n/g)
|
|
1320
|
-
|
|
1321
|
-
// Multiple templates can share the same htmlStrings array.
|
|
1322
|
-
//this.hashedFields = [getObjectId(htmlStrings), exprs]
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
/**
|
|
1328
|
-
* Called by JSON.serialize when it encounters a Template.
|
|
1329
|
-
* This prevents the hashed version from being too large. */
|
|
1330
|
-
toJSON() {
|
|
1331
|
-
if (!this.hashedFields)
|
|
1332
|
-
this.hashedFields = [getObjectId(this.html, 'Html'), this.exprs];
|
|
1333
|
-
|
|
1334
|
-
return this.hashedFields
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
toNode() {
|
|
1338
|
-
let ngm = new NodeGroupManager();
|
|
1339
|
-
return ngm.render(this);
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
getCloseKey() {
|
|
1343
|
-
// Use the joined html when debugging?
|
|
1344
|
-
//return '@'+this.html.join('|')
|
|
1345
|
-
|
|
1346
|
-
return '@'+this.hashedFields[0];
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
/**
|
|
1351
|
-
* ISC License
|
|
1352
|
-
*
|
|
1353
|
-
* Copyright (c) 2020, Andrea Giammarchi, @WebReflection
|
|
1354
|
-
*
|
|
1355
|
-
* Permission to use, copy, modify, and/or distribute this software for any
|
|
1356
|
-
* purpose with or without fee is hereby granted, provided that the above
|
|
1357
|
-
* copyright notice and this permission notice appear in all copies.
|
|
1358
|
-
*
|
|
1359
|
-
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
1360
|
-
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
1361
|
-
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
1362
|
-
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
1363
|
-
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
|
1364
|
-
* OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
1365
|
-
* PERFORMANCE OF THIS SOFTWARE.
|
|
1366
|
-
*/
|
|
1367
|
-
|
|
1368
|
-
/**
|
|
1369
|
-
* @param {Node} parentNode The container where children live
|
|
1370
|
-
* @param {Node[]} a The list of current/live children
|
|
1371
|
-
* @param {Node[]} b The list of future children
|
|
1372
|
-
* @param {(entry: Node, action: number) => Node} get
|
|
1373
|
-
* The callback invoked per each entry related DOM operation.
|
|
1374
|
-
* @param {Node} [before] The optional node used as anchor to insert before.
|
|
1375
|
-
* @returns {Node[]} The same list of future children.
|
|
1376
|
-
*/
|
|
1377
|
-
const udomdiff = (parentNode, a, b, before) => {
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
const bLength = b.length;
|
|
1381
|
-
let aEnd = a.length;
|
|
1382
|
-
let bEnd = bLength;
|
|
1383
|
-
let aStart = 0;
|
|
1384
|
-
let bStart = 0;
|
|
1385
|
-
let map = null;
|
|
1386
|
-
while (aStart < aEnd || bStart < bEnd) {
|
|
1387
|
-
// append head, tail, or nodes in between: fast path
|
|
1388
|
-
if (aEnd === aStart) {
|
|
1389
|
-
// we could be in a situation where the rest of nodes that
|
|
1390
|
-
// need to be added are not at the end, and in such case
|
|
1391
|
-
// the node to `insertBefore`, if the index is more than 0
|
|
1392
|
-
// must be retrieved, otherwise it's gonna be the first item.
|
|
1393
|
-
const node = bEnd < bLength
|
|
1394
|
-
? (bStart
|
|
1395
|
-
? (b[bStart - 1].nextSibling)
|
|
1396
|
-
: b[bEnd - bStart])
|
|
1397
|
-
: before;
|
|
1398
|
-
while (bStart < bEnd) {
|
|
1399
|
-
let bNode = b[bStart++];
|
|
1400
|
-
parentNode.insertBefore(bNode, node);
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
// remove head or tail: fast path
|
|
1406
|
-
else if (bEnd === bStart) {
|
|
1407
|
-
while (aStart < aEnd) {
|
|
1408
|
-
// remove the node only if it's unknown or not live
|
|
1409
|
-
let aNode = a[aStart];
|
|
1410
|
-
if (!map || !map.has(aNode)) {
|
|
1411
|
-
parentNode.removeChild(aNode);
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
}
|
|
1415
|
-
aStart++;
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
// same node: fast path
|
|
1419
|
-
else if (a[aStart] === b[bStart]) {
|
|
1420
|
-
aStart++;
|
|
1421
|
-
bStart++;
|
|
1422
|
-
}
|
|
1423
|
-
// same tail: fast path
|
|
1424
|
-
else if (a[aEnd - 1] === b[bEnd - 1]) {
|
|
1425
|
-
aEnd--;
|
|
1426
|
-
bEnd--;
|
|
1427
|
-
}
|
|
1428
|
-
// The once here single last swap "fast path" has been removed in v1.1.0
|
|
1429
|
-
// https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85
|
|
1430
|
-
// reverse swap: also fast path
|
|
1431
|
-
else if (
|
|
1432
|
-
a[aStart] === b[bEnd - 1] &&
|
|
1433
|
-
b[bStart] === a[aEnd - 1]
|
|
1434
|
-
) {
|
|
1435
|
-
// this is a "shrink" operation that could happen in these cases:
|
|
1436
|
-
// [1, 2, 3, 4, 5]
|
|
1437
|
-
// [1, 4, 3, 2, 5]
|
|
1438
|
-
// or asymmetric too
|
|
1439
|
-
// [1, 2, 3, 4, 5]
|
|
1440
|
-
// [1, 2, 3, 5, 6, 4]
|
|
1441
|
-
const node = a[--aEnd].nextSibling;
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
let a2 = b[bStart++];
|
|
1445
|
-
let b2 = a[aStart++];
|
|
1446
|
-
parentNode.insertBefore(
|
|
1447
|
-
a2,
|
|
1448
|
-
b2.nextSibling
|
|
1449
|
-
);
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
let bNode = b[--bEnd];
|
|
1453
|
-
parentNode.insertBefore(bNode, node);
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
// mark the future index as identical (yeah, it's dirty, but cheap 👍)
|
|
1458
|
-
// The main reason to do this, is that when a[aEnd] will be reached,
|
|
1459
|
-
// the loop will likely be on the fast path, as identical to b[bEnd].
|
|
1460
|
-
// In the best case scenario, the next loop will skip the tail,
|
|
1461
|
-
// but in the worst one, this node will be considered as already
|
|
1462
|
-
// processed, bailing out pretty quickly from the map index check
|
|
1463
|
-
a[aEnd] = b[bEnd];
|
|
1464
|
-
}
|
|
1465
|
-
// map based fallback, "slow" path
|
|
1466
|
-
else {
|
|
1467
|
-
// the map requires an O(bEnd - bStart) operation once
|
|
1468
|
-
// to store all future nodes indexes for later purposes.
|
|
1469
|
-
// In the worst case scenario, this is a full O(N) cost,
|
|
1470
|
-
// and such scenario happens at least when all nodes are different,
|
|
1471
|
-
// but also if both first and last items of the lists are different
|
|
1472
|
-
if (!map) {
|
|
1473
|
-
map = new Map;
|
|
1474
|
-
let i = bStart;
|
|
1475
|
-
while (i < bEnd)
|
|
1476
|
-
map.set(b[i], i++);
|
|
1477
|
-
}
|
|
1478
|
-
// if it's a future node, hence it needs some handling
|
|
1479
|
-
if (map.has(a[aStart])) {
|
|
1480
|
-
// grab the index of such node, 'cause it might have been processed
|
|
1481
|
-
const index = map.get(a[aStart]);
|
|
1482
|
-
// if it's not already processed, look on demand for the next LCS
|
|
1483
|
-
if (bStart < index && index < bEnd) {
|
|
1484
|
-
let i = aStart;
|
|
1485
|
-
// counts the amount of nodes that are the same in the future
|
|
1486
|
-
let sequence = 1;
|
|
1487
|
-
while (++i < aEnd && i < bEnd && map.get(a[i]) === (index + sequence))
|
|
1488
|
-
sequence++;
|
|
1489
|
-
// effort decision here: if the sequence is longer than replaces
|
|
1490
|
-
// needed to reach such sequence, which would brings again this loop
|
|
1491
|
-
// to the fast path, prepend the difference before a sequence,
|
|
1492
|
-
// and move only the future list index forward, so that aStart
|
|
1493
|
-
// and bStart will be aligned again, hence on the fast path.
|
|
1494
|
-
// An example considering aStart and bStart are both 0:
|
|
1495
|
-
// a: [1, 2, 3, 4]
|
|
1496
|
-
// b: [7, 1, 2, 3, 6]
|
|
1497
|
-
// this would place 7 before 1 and, from that time on, 1, 2, and 3
|
|
1498
|
-
// will be processed at zero cost
|
|
1499
|
-
if (sequence > (index - bStart)) {
|
|
1500
|
-
const node = a[aStart];
|
|
1501
|
-
while (bStart < index) {
|
|
1502
|
-
let bNode = b[bStart++];
|
|
1503
|
-
parentNode.insertBefore(bNode, node);
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
// if the effort wasn't good enough, fallback to a replace,
|
|
1509
|
-
// moving both source and target indexes forward, hoping that some
|
|
1510
|
-
// similar node will be found later on, to go back to the fast path
|
|
1511
|
-
else {
|
|
1512
|
-
let aNode = a[aStart++];
|
|
1513
|
-
let bNode = b[bStart++];
|
|
1514
|
-
parentNode.replaceChild(
|
|
1515
|
-
bNode,
|
|
1516
|
-
aNode
|
|
1517
|
-
);
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
// otherwise move the source forward, 'cause there's nothing to do
|
|
1523
|
-
else
|
|
1524
|
-
aStart++;
|
|
1525
|
-
}
|
|
1526
|
-
// this node has no meaning in the future list, so it's more than safe
|
|
1527
|
-
// to remove it, and check the next live node out instead, meaning
|
|
1528
|
-
// that only the live list index should be forwarded
|
|
1529
|
-
else {
|
|
1530
|
-
let aNode = a[aStart++];
|
|
1531
|
-
parentNode.removeChild(aNode);
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
return b;
|
|
1538
|
-
};
|
|
1645
|
+
}
|
|
1539
1646
|
|
|
1540
1647
|
/**
|
|
1541
|
-
*
|
|
1542
|
-
|
|
1648
|
+
* A Shell is created from a tagged template expression instantiated as Nodes,
|
|
1649
|
+
* but without any expressions filled in.
|
|
1650
|
+
* Only one Shell is created for all the items in a loop.
|
|
1651
|
+
*
|
|
1652
|
+
* When a NodeGroup is created from a Template's html strings,
|
|
1653
|
+
* the NodeGroup then clones the Shell's fragmentn to be its nodes. */
|
|
1654
|
+
class Shell {
|
|
1543
1655
|
|
|
1656
|
+
/**
|
|
1657
|
+
* @type {DocumentFragment} DOM parent of the shell nodes. */
|
|
1658
|
+
fragment;
|
|
1544
1659
|
|
|
1545
|
-
/**
|
|
1546
|
-
|
|
1547
|
-
class TransformerInfo {
|
|
1548
|
-
constructor(path, transformer, hash) {
|
|
1549
|
-
this.path = path;
|
|
1550
|
-
this.transformer = transformer;
|
|
1551
|
-
this.hash = hash;
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1660
|
+
/** @type {ExprPath[]} Paths to where expressions should go. */
|
|
1661
|
+
paths = [];
|
|
1554
1662
|
|
|
1663
|
+
// Embeds and ids
|
|
1664
|
+
events = [];
|
|
1555
1665
|
|
|
1556
|
-
/**
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
*
|
|
1561
|
-
* We store the function to get the Template, instead of the Template itself,
|
|
1562
|
-
* so we can call that function again when the object has a new value.
|
|
1563
|
-
* @type {MultiValueMap<Object, function(...Object):Template>} */
|
|
1564
|
-
let pathToTransformer = new MultiValueMap(); // uses a Set() for each value.
|
|
1666
|
+
/** @type {int[][]} Array of paths */
|
|
1667
|
+
ids = [];
|
|
1668
|
+
scripts = [];
|
|
1669
|
+
styles = [];
|
|
1565
1670
|
|
|
1566
|
-
|
|
1567
|
-
*
|
|
1568
|
-
* @param objectPaths {(*|function)[]}
|
|
1569
|
-
* @returns {Template} */
|
|
1570
|
-
function watchGet(...objectPaths) {
|
|
1571
|
-
|
|
1572
|
-
/** @type {function} */
|
|
1573
|
-
let transformer = objectPaths.at(-1);
|
|
1574
|
-
let paths;
|
|
1575
|
-
if (typeof transformer === 'function') {
|
|
1576
|
-
paths = objectPaths.slice(0, -1);
|
|
1577
|
-
}
|
|
1671
|
+
staticComponents = [];
|
|
1578
1672
|
|
|
1579
|
-
// No transformer provided, so we create our own.
|
|
1580
|
-
else if (objectPaths.length === 1) {
|
|
1581
|
-
paths = [objectPaths[0].slice(0, -1)];
|
|
1582
|
-
let prop = objectPaths[0].at(-1);
|
|
1583
|
-
transformer = (...args) => (args[0][prop]);
|
|
1584
|
-
}
|
|
1585
1673
|
|
|
1586
|
-
|
|
1587
|
-
// Save arguments used to call the template, so we can call it again when those args have their values change.
|
|
1588
|
-
let args = [];
|
|
1589
|
-
for (let path of paths) {
|
|
1590
1674
|
|
|
1675
|
+
/**
|
|
1676
|
+
* Create the nodes but without filling in the expressions.
|
|
1677
|
+
* This is useful because the expression-less nodes created by a template can be cached.
|
|
1678
|
+
* @param html {string[]} */
|
|
1679
|
+
constructor(html=null) {
|
|
1680
|
+
if (!html)
|
|
1681
|
+
return;
|
|
1591
1682
|
|
|
1592
|
-
|
|
1593
|
-
args.push(obj);
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
let template = transformer(...args);
|
|
1597
|
-
|
|
1598
|
-
// If the result isn't a Template, convert the function to return a Template that wraps the result.
|
|
1599
|
-
// This way NodeGroupManager.findAndDelete() can find a NodeGroup that matches this Template's hash.
|
|
1600
|
-
if (!(template instanceof Template)) {
|
|
1601
|
-
let oldToTemplate = transformer;
|
|
1602
|
-
transformer = function() {
|
|
1603
|
-
return new Template(['', ''], [oldToTemplate(...arguments)]);
|
|
1604
|
-
};
|
|
1605
|
-
template = transformer(...args);
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
// Map the object paths to the function that creates a template.
|
|
1609
|
-
let hash = getObjectHash(template);
|
|
1610
|
-
for (let path of paths) {
|
|
1611
|
-
let serializedPath = serializePath(path);
|
|
1612
|
-
pathToTransformer.add(serializedPath, new TransformerInfo(path, transformer, hash)); // Uses a Set() to ensure no duplicates.
|
|
1613
|
-
}
|
|
1683
|
+
|
|
1614
1684
|
|
|
1615
|
-
|
|
1616
|
-
|
|
1685
|
+
// 1. Add placeholders
|
|
1686
|
+
// We increment the placeholder char as we go because nodes can't have the same attribute more than once.
|
|
1687
|
+
let placeholder = 0xe000; // https://en.wikipedia.org/wiki/Private_Use_Areas 6400.
|
|
1617
1688
|
|
|
1618
|
-
|
|
1689
|
+
let buffer = [];
|
|
1690
|
+
let commentPlaceholder = `<!--!✨!-->`;
|
|
1691
|
+
let componentNames = {};
|
|
1619
1692
|
|
|
1693
|
+
htmlContext(null); // Reset the context.
|
|
1694
|
+
for (let i=0; i<html.length; i++) {
|
|
1695
|
+
let lastHtml = html[i];
|
|
1696
|
+
let context = htmlContext(lastHtml);
|
|
1620
1697
|
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
if (obj?.$isProxy===true)
|
|
1626
|
-
return obj; // It's already a Proxy.
|
|
1627
|
-
|
|
1628
|
-
// This cache doesn't make things faster.
|
|
1629
|
-
// let result = proxyCache.get(obj);
|
|
1630
|
-
// if (!result) {
|
|
1631
|
-
// result = new Proxy(obj, new ProxyHandler(obj));
|
|
1632
|
-
// proxyCache.set(obj, result);
|
|
1633
|
-
// }
|
|
1634
|
-
// return result;
|
|
1635
|
-
return new Proxy(obj, new ProxyHandler$1(obj));
|
|
1636
|
-
}
|
|
1698
|
+
// Swap out Embedded Solarite Components with ${} attributes.
|
|
1699
|
+
// Later, NodeGroup.render() will search for these and replace them with the real components.
|
|
1700
|
+
// Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
|
|
1701
|
+
if (context === htmlContext.Attribute) {
|
|
1637
1702
|
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
function forEach(arrayPath, callback) {
|
|
1644
|
-
let array = delve(arrayPath[0], arrayPath.slice(1));
|
|
1645
|
-
|
|
1646
|
-
// This is retrieved on the 'insert' path inside renderWatched()
|
|
1647
|
-
let ngm = NodeGroupManager.get(arrayPath[0]);
|
|
1648
|
-
if (ngm.clearSubscribers) {
|
|
1649
|
-
ngm.clearSubscribers = false;
|
|
1650
|
-
ngm.pathToLoopInfo = new MultiValueMap();
|
|
1651
|
-
} // TODO: Move tis into NodeGroupMAnager.get() without breaking things?
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
let newItems = [...array.map((item, i) => {
|
|
1655
|
-
// TODO: This needs to wrap callback so we can pass it the index also.
|
|
1656
|
-
return watchGet([...arrayPath, i], callback); // calls callback(array[i], i)
|
|
1657
|
-
})
|
|
1658
|
-
];
|
|
1659
|
-
|
|
1660
|
-
// We return a template that wraps the array
|
|
1661
|
-
// So that NodeGroup.applyOneExpr can set the ExprPath and nextSibling on the template.
|
|
1662
|
-
// Then the 'insert' path in renderWatched() uses that data fora dding more nodes.
|
|
1663
|
-
let result = new Template(['', ''], [newItems]);
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
// We get a unique hash for each foreach template because the [''] array is unique each time.
|
|
1667
|
-
let loopInfo = new LoopInfo(result, callback);
|
|
1668
|
-
ngm.pathToLoopInfo.add(serializePath(arrayPath), loopInfo);
|
|
1669
|
-
return result;
|
|
1670
|
-
}
|
|
1703
|
+
let lastIndex, lastMatch;
|
|
1704
|
+
lastHtml.replace(/<[a-z][a-z0-9]*-[a-z0-9-]+/ig, (match, index) => {
|
|
1705
|
+
lastIndex = index+1; // +1 for after opening <
|
|
1706
|
+
lastMatch = match.slice(1);
|
|
1707
|
+
});
|
|
1671
1708
|
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1709
|
+
if (lastMatch) {
|
|
1710
|
+
let newTagName = lastMatch + '-solarite-placeholder';
|
|
1711
|
+
lastHtml = lastHtml.slice(0, lastIndex) + newTagName + lastHtml.slice(lastIndex + lastMatch.length);
|
|
1712
|
+
componentNames[lastMatch] = newTagName;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1675
1715
|
|
|
1676
|
-
|
|
1716
|
+
buffer.push(lastHtml);
|
|
1717
|
+
//console.log(lastHtml, context)
|
|
1718
|
+
if (i < html.length-1)
|
|
1719
|
+
if (context === htmlContext.Text)
|
|
1720
|
+
buffer.push(commentPlaceholder); // Comment Placeholder. because we can't put text in between <tr> tags for example.
|
|
1721
|
+
else
|
|
1722
|
+
buffer.push(String.fromCharCode(placeholder+i));
|
|
1723
|
+
}
|
|
1677
1724
|
|
|
1725
|
+
// 2. Create elements from html with placeholders.
|
|
1726
|
+
let template = document.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
|
|
1727
|
+
let joinedHtml = buffer.join('');
|
|
1678
1728
|
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
class ProxyHandler$1 {
|
|
1684
|
-
|
|
1685
|
-
/**
|
|
1686
|
-
* @param root An element managed by a NodeGroupManager. The same as the NodeGroupManager's rootEl.
|
|
1687
|
-
* @param path {string[]} Used internally. */
|
|
1688
|
-
constructor(root, path=[]) {
|
|
1729
|
+
// Replace '-solarite-placeholder' close tags.
|
|
1730
|
+
// TODO: is there a better way? What if the close tag is inside a comment?
|
|
1731
|
+
for (let name in componentNames)
|
|
1732
|
+
joinedHtml = joinedHtml.replaceAll(`</${name}>`, `</${componentNames[name]}>`);
|
|
1689
1733
|
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1734
|
+
if (joinedHtml)
|
|
1735
|
+
template.innerHTML = joinedHtml;
|
|
1736
|
+
else // Create one text node, so shell isn't empty and NodeGroups created from it have something to point the startNode and endNode at.
|
|
1737
|
+
template.content.append(document.createTextNode(''));
|
|
1738
|
+
this.fragment = template.content;
|
|
1693
1739
|
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
if (deleteCount) {
|
|
1714
|
-
|
|
1715
|
-
// Get the hash of each object along the delete range. The process to get the hash is:
|
|
1716
|
-
// Serialized Path -> transformer -> Template -> hash.
|
|
1717
|
-
let hashes = [];
|
|
1718
|
-
for (let i=index; i<index+deleteCount; i++) {
|
|
1719
|
-
let serializedPath = serializePath([this.root, ...this.path, i+'']);
|
|
1720
|
-
|
|
1721
|
-
let obj = delve(this.root, [...this.path, i]);
|
|
1722
|
-
for (let transformerInfo of pathToTransformer.getAll(serializedPath)) {
|
|
1723
|
-
let template = transformerInfo.transformer(obj);
|
|
1724
|
-
let hash = getObjectHash(template);
|
|
1725
|
-
hashes.push(hash); // Hashes may go to nodes in more than one loop.
|
|
1726
|
-
}
|
|
1740
|
+
// 3. Find placeholders
|
|
1741
|
+
let node;
|
|
1742
|
+
let toRemove = [];
|
|
1743
|
+
const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
|
|
1744
|
+
while (node = walker.nextNode()) {
|
|
1745
|
+
|
|
1746
|
+
// Remove previous after each iteration, so paths will still be calculated correctly.
|
|
1747
|
+
toRemove.map(el => el.remove());
|
|
1748
|
+
toRemove = [];
|
|
1749
|
+
|
|
1750
|
+
// Replace attributes
|
|
1751
|
+
if (node.nodeType === 1) {
|
|
1752
|
+
for (let attr of [...node.attributes]) { // Copy the attributes array b/c we remove attributes as we go.
|
|
1753
|
+
|
|
1754
|
+
// Whole attribute
|
|
1755
|
+
let matches = attr.name.match(/^[\ue000-\uf8ff]$/);
|
|
1756
|
+
if (matches) {
|
|
1757
|
+
this.paths.push(new ExprPath(null, node, PathType.Multiple));
|
|
1758
|
+
node.removeAttribute(matches[0]);
|
|
1727
1759
|
}
|
|
1728
1760
|
|
|
1729
|
-
|
|
1730
|
-
|
|
1761
|
+
// Just the attribute value.
|
|
1762
|
+
else {
|
|
1763
|
+
let parts = attr.value.split(/[\ue000-\uf8ff]/g);
|
|
1764
|
+
if (parts.length > 1) {
|
|
1765
|
+
let nonEmptyParts = (parts.length === 2 && !parts[0].length && !parts[1].length) ? null : parts;
|
|
1766
|
+
let type = isEvent(attr.name) ? PathType.Event : PathType.Value;
|
|
1731
1767
|
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
// Inserting
|
|
1736
|
-
if (items.length) {
|
|
1737
|
-
|
|
1738
|
-
let beforeNgs;
|
|
1739
|
-
for (let loopInfo of ngm.getLoopInfo([this.root, ...this.path])) {
|
|
1740
|
-
let beforeObj = delve(this.root, [...this.path, index]);
|
|
1741
|
-
|
|
1742
|
-
// Find where to insert before.
|
|
1743
|
-
if (beforeObj) {
|
|
1744
|
-
let beforeTemplate = loopInfo.itemTransformer(beforeObj);
|
|
1745
|
-
let beforeHash = getObjectHash(beforeTemplate);
|
|
1746
|
-
beforeNgs = ngm.nodeGroupsAvailable.data[beforeHash];
|
|
1747
|
-
|
|
1748
|
-
if (beforeNgs) {
|
|
1749
|
-
let hash = getObjectHash(loopInfo.template);
|
|
1750
|
-
let loopNgs = ngm.nodeGroupsAvailable.data[hash] || [];
|
|
1751
|
-
for (let loopNg of loopNgs)
|
|
1752
|
-
for (let beforeNg of beforeNgs)
|
|
1753
|
-
if (beforeNg.startNode.parentNode === loopNg.startNode.parentNode)
|
|
1754
|
-
ngm.changes.push(new Change('insert', this.root, [...this.path, index + ''], items, beforeTemplate));
|
|
1755
|
-
}
|
|
1768
|
+
this.paths.push(new ExprPath(null, node, type, attr.name, nonEmptyParts));
|
|
1769
|
+
node.setAttribute(attr.name, parts.join(''));
|
|
1756
1770
|
}
|
|
1757
|
-
if (!beforeNgs)
|
|
1758
|
-
ngm.changes.push(new Change('insert', this.root, [...this.path, index + ''], items));
|
|
1759
1771
|
}
|
|
1760
|
-
obj.splice(index, 0, ...items);
|
|
1761
1772
|
}
|
|
1762
|
-
return result;
|
|
1763
1773
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
// 2. Get property
|
|
1769
|
-
// If we're getting an object or array property, apply watch() to it recursively.
|
|
1770
|
-
let result = Reflect.get(obj, prop);
|
|
1771
|
-
if (result && typeof result === 'object') {
|
|
1772
|
-
let handler = new ProxyHandler$1(this.root, [...this.path, prop]); // same root, one level deeper on the path.
|
|
1773
|
-
return new Proxy(result, handler);
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
return result;
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
set(obj, prop, newValue) {
|
|
1781
|
-
let ngm = NodeGroupManager.get(this.root);
|
|
1782
|
-
ngm.changes.push(new Change('set', this.root, [...this.path, prop], newValue));
|
|
1783
|
-
return Reflect.set(obj, prop, newValue)
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
/**
|
|
1789
|
-
*
|
|
1790
|
-
*/
|
|
1791
|
-
class Change {
|
|
1792
|
-
|
|
1793
|
-
/**
|
|
1794
|
-
* @param action {string}
|
|
1795
|
-
* @param root {Object|Array}
|
|
1796
|
-
* @param path {string[]}
|
|
1797
|
-
* @param value
|
|
1798
|
-
* If setting a value, this is the new value.
|
|
1799
|
-
* If deleting from an array, this is an array of all the NodeGroups to delete.
|
|
1800
|
-
*
|
|
1801
|
-
* @param beforeTemplate
|
|
1802
|
-
* */
|
|
1803
|
-
constructor(action, root, path, value, beforeTemplate=null) {
|
|
1804
|
-
this.action = action;
|
|
1774
|
+
// Replace comment placeholders
|
|
1775
|
+
else if (node.nodeType === Node.COMMENT_NODE && node.nodeValue === '!✨!') {
|
|
1805
1776
|
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1777
|
+
// Get or create nodeBefore.
|
|
1778
|
+
let nodeBefore = node.previousSibling; // Can be the same as another Path's nodeMarker.
|
|
1779
|
+
if (!nodeBefore) {
|
|
1780
|
+
nodeBefore = document.createComment('ExprPath:'+this.paths.length);
|
|
1781
|
+
node.parentNode.insertBefore(nodeBefore, node);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1811
1784
|
|
|
1812
|
-
|
|
1813
|
-
|
|
1785
|
+
// Get the next node.
|
|
1786
|
+
let nodeMarker;
|
|
1814
1787
|
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1788
|
+
// A subsequent node is available to be a nodeMarker.
|
|
1789
|
+
if (node.nextSibling && (node.nextSibling.nodeType !== 8 || node.nextSibling.textContent !== '!✨!')) {
|
|
1790
|
+
nodeMarker = node.nextSibling;
|
|
1791
|
+
toRemove.push(node); // Removing them here will mess up the treeWalker.
|
|
1792
|
+
}
|
|
1793
|
+
// Re-use existing comment placeholder.
|
|
1794
|
+
else {
|
|
1795
|
+
nodeMarker = node;
|
|
1796
|
+
nodeMarker.textContent = 'ExprPathEnd:'+ this.paths.length;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1819
1799
|
|
|
1820
|
-
let serializedPath = getObjectHash(fullPath); // TODO: Why not serializedPath() ?
|
|
1821
|
-
this.transformerInfo.push(...pathToTransformer.getAll(serializedPath));
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
let logGets = false;
|
|
1827
|
-
let gets = [];
|
|
1828
1800
|
|
|
1829
|
-
let withinSet = 0;
|
|
1830
1801
|
|
|
1831
|
-
|
|
1832
|
-
* Turn the props on obj into JavasCript properties that return Proxies when accessed.
|
|
1833
|
-
* If called more than once, return the already-converted object.
|
|
1834
|
-
* @param obj {Object}
|
|
1835
|
-
* @param props {string}
|
|
1836
|
-
* @returns {*|{$proxyHandler}}
|
|
1837
|
-
*/
|
|
1838
|
-
function watch(obj, ...props) {
|
|
1839
|
-
|
|
1840
|
-
if (props.length) {
|
|
1841
|
-
let internalProps = {};
|
|
1842
|
-
for (let prop of props) {
|
|
1843
|
-
internalProps[prop] = obj[prop];
|
|
1844
|
-
Object.defineProperty(obj, prop, {
|
|
1845
|
-
get() {
|
|
1846
|
-
return new Proxy(obj, new ProxyHandler(obj, [], internalProps))[prop];
|
|
1847
|
-
},
|
|
1848
|
-
set(value) {
|
|
1849
|
-
return watch(this)[prop] = value;
|
|
1850
|
-
}
|
|
1851
|
-
});
|
|
1852
|
-
}
|
|
1853
|
-
return;
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
if (obj?.$proxyHandler)
|
|
1858
|
-
return obj; // It's already a Proxy.
|
|
1859
|
-
|
|
1860
|
-
// This cache doesn't make things faster.
|
|
1861
|
-
// But could it save memory?
|
|
1862
|
-
// let result = proxyCache.get(obj);
|
|
1863
|
-
// if (!result) {
|
|
1864
|
-
// result = new Proxy(obj, new ProxyHandler(obj));
|
|
1865
|
-
// proxyCache.set(obj, result);
|
|
1866
|
-
// }
|
|
1867
|
-
// return result;
|
|
1868
|
-
|
|
1869
|
-
return new Proxy(obj, new ProxyHandler(obj));
|
|
1870
|
-
}
|
|
1802
|
+
let path = new ExprPath(nodeBefore, nodeMarker, PathType.Content);
|
|
1871
1803
|
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
class ProxyHandler {
|
|
1875
|
-
|
|
1876
|
-
serializedPath;
|
|
1877
|
-
|
|
1878
|
-
/**
|
|
1879
|
-
* @param root An element managed by a NodeGroupManager. The same as the NodeGroupManager's rootEl.
|
|
1880
|
-
* @param path {string[]} Used internally.
|
|
1881
|
-
* @param props */
|
|
1882
|
-
constructor(root, path=[], props=null) {
|
|
1883
|
-
|
|
1884
|
-
this.root = root;
|
|
1885
|
-
this.path = path; // path from root to this Proxy.
|
|
1886
|
-
this.props = props;
|
|
1887
|
-
}
|
|
1888
|
-
|
|
1889
|
-
/**
|
|
1890
|
-
* Get the full path to this property from the root watched object.
|
|
1891
|
-
* @param atIndex
|
|
1892
|
-
* @returns {string} */
|
|
1893
|
-
getSerializedPath(atIndex=null) {
|
|
1894
|
-
if (!this.serializedPath)
|
|
1895
|
-
this.serializedPath = JSON.stringify([getObjectId(this.root), ...this.path.map(item => item + '')]);
|
|
1896
|
-
|
|
1897
|
-
if (atIndex!== null)
|
|
1898
|
-
return this.serializedPath.slice(0, -1) + ',"' + atIndex + '"]';
|
|
1899
|
-
return this.serializedPath;
|
|
1900
|
-
}
|
|
1901
|
-
|
|
1902
|
-
/**
|
|
1903
|
-
* Return a ProxyHandler for a property one level deeper at pathItem.
|
|
1904
|
-
* @param pathItem {string}
|
|
1905
|
-
* @returns {ProxyHandler} */
|
|
1906
|
-
extend(pathItem) {
|
|
1907
|
-
pathItem += '';
|
|
1908
|
-
assert(!this.root.$proxyHandler);
|
|
1909
|
-
let result = new ProxyHandler(this.root, [...this.path, pathItem], this.props);
|
|
1910
|
-
if (this.serializedPath)
|
|
1911
|
-
result.serializedPath = this.getSerializedPath(pathItem);
|
|
1912
|
-
|
|
1913
|
-
return result;
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
/**
|
|
1917
|
-
* Called directly by JavaScript when accessing the value of a property.
|
|
1918
|
-
* @param obj {Object}
|
|
1919
|
-
* @param prop {string} */
|
|
1920
|
-
get(obj, prop) {
|
|
1921
|
-
|
|
1922
|
-
// 1. Special props.
|
|
1923
|
-
if (prop === '$proxyHandler')
|
|
1924
|
-
return this;
|
|
1925
|
-
else if (prop === '$removeProxy')
|
|
1926
|
-
return delve(this.props || this.root, this.path);
|
|
1927
|
-
|
|
1928
|
-
// 2. Array functions.
|
|
1929
|
-
else if (prop === 'map' && Array.isArray(obj)) {
|
|
1804
|
+
this.paths.push(path);
|
|
1805
|
+
}
|
|
1930
1806
|
|
|
1931
|
-
let ngm = NodeGroupManager.get(this.root);
|
|
1932
|
-
ngm.clearSubscribersIfNeeded();
|
|
1933
1807
|
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
let handler = this.extend(i);
|
|
1945
|
-
let item = obj[i];
|
|
1946
|
-
if (!item.$proxyHandler)
|
|
1947
|
-
item = new Proxy(item, handler);
|
|
1948
|
-
|
|
1949
|
-
let template = callback(item, i, obj);
|
|
1950
|
-
templates.push(template);
|
|
1951
|
-
|
|
1952
|
-
// If the loop is re-evaluted via Set() then we add duplicate TemplateInfo's
|
|
1953
|
-
//if (!withinSet) {
|
|
1954
|
-
let spath = this.getSerializedPath(i);
|
|
1955
|
-
let subscriber = new Subscriber(callback, template);
|
|
1956
|
-
subscriber.parent = loopInfo;
|
|
1957
|
-
ngm.subscribers.add(spath, subscriber);
|
|
1958
|
-
children.push([spath, subscriber]);
|
|
1959
|
-
//}
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
// A parent Template that surrounds all the items in the loop.
|
|
1963
|
-
// This lets us get template.nodeGroup.endNode so we can insertBefore().
|
|
1964
|
-
return new Template(['', ''], [templates]);
|
|
1965
|
-
};
|
|
1966
|
-
|
|
1967
|
-
if (!withinSet)
|
|
1968
|
-
loopInfo = new Subscriber(transformer, null, callback);
|
|
1969
|
-
let wholeLoopTemplate = transformer(obj);
|
|
1970
|
-
if (!withinSet) {
|
|
1971
|
-
loopInfo.template = wholeLoopTemplate;
|
|
1972
|
-
loopInfo.children = children;
|
|
1973
|
-
ngm.subscribers.add(this.getSerializedPath(), loopInfo);
|
|
1808
|
+
// Sometimes users will comment out a block of html code that has expressions.
|
|
1809
|
+
// Here we look for expressions in comments.
|
|
1810
|
+
// We don't actually update them dynamically, but we still add paths for them.
|
|
1811
|
+
// That way the expression count still matches.
|
|
1812
|
+
else if (node.nodeType === Node.COMMENT_NODE) {
|
|
1813
|
+
let parts = node.textContent.split(/[\ue000-\uf8ff]/g);
|
|
1814
|
+
for (let i=0; i<parts.length-1; i++) {
|
|
1815
|
+
let path = new ExprPath(node.previousSibling, node);
|
|
1816
|
+
path.type = PathType.Comment;
|
|
1817
|
+
this.paths.push(path);
|
|
1974
1818
|
}
|
|
1975
|
-
return wholeLoopTemplate;
|
|
1976
1819
|
}
|
|
1977
|
-
}
|
|
1978
1820
|
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
let spath = this.getSerializedPath(i);
|
|
1991
|
-
|
|
1992
|
-
// Delete nodes of associated NodeGroups.
|
|
1993
|
-
for (let subscriber of ngm.subscribers.data[spath] || []) {
|
|
1994
|
-
let ng = subscriber.template.nodeGroup;
|
|
1995
|
-
for (let node of ng.getNodes())
|
|
1996
|
-
node.remove();
|
|
1997
|
-
|
|
1998
|
-
// Delete NodeGroup from NodeGroupManager.
|
|
1999
|
-
ngm.nodeGroupsAvailable.delete(ng.exactKey, ng);
|
|
2000
|
-
}
|
|
2001
|
-
|
|
2002
|
-
delete ngm.subscribers.data[spath]; // Deletes templates associated with every loop where this is used.
|
|
1821
|
+
// Replace comment placeholders inside script and style tags, which have become text nodes.
|
|
1822
|
+
else if (node.nodeType === Node.TEXT_NODE && ['SCRIPT', 'STYLE'].includes(node.parentNode?.nodeName)) {
|
|
1823
|
+
let parts = node.textContent.split(commentPlaceholder);
|
|
1824
|
+
if (parts.length > 1) {
|
|
1825
|
+
|
|
1826
|
+
let placeholders = [];
|
|
1827
|
+
for (let i = 0; i<parts.length; i++) {
|
|
1828
|
+
let current = document.createTextNode(parts[i]);
|
|
1829
|
+
node.parentNode.insertBefore(current, node);
|
|
1830
|
+
if (i > 0)
|
|
1831
|
+
placeholders.push(current);
|
|
2003
1832
|
}
|
|
2004
|
-
}
|
|
2005
|
-
|
|
2006
|
-
// Update indices of subsequent items.
|
|
2007
|
-
if (diff) {
|
|
2008
|
-
let loopPath = this.getSerializedPath();
|
|
2009
|
-
let loopInfo = [...ngm.subscribers.getAll(loopPath)][0]; // TODO: Handle multiple loops.
|
|
2010
|
-
|
|
2011
|
-
let move = (oldIndex) => {
|
|
2012
|
-
let newIndex = oldIndex+diff;
|
|
2013
|
-
let oldPath = this.getSerializedPath(oldIndex);
|
|
2014
|
-
let newPath = this.getSerializedPath(newIndex);
|
|
2015
1833
|
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
1834
|
+
for (let i=0, node; node=placeholders[i]; i++) {
|
|
1835
|
+
let path = new ExprPath(node.previousSibling, node, PathType.Content);
|
|
1836
|
+
this.paths.push(path);
|
|
2019
1837
|
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
// This is unnecessary for most loops since they don't use the index.
|
|
2023
|
-
// fastSplice skips this path, it skips updating item indices.
|
|
2024
|
-
if (prop === 'splice') {
|
|
2025
|
-
let array = delve(this.root, this.path);
|
|
2026
|
-
for (let subscriber of subscribers) {
|
|
2027
|
-
let ng = subscriber.template.nodeGroup;
|
|
2028
|
-
|
|
2029
|
-
let item = array[oldIndex];
|
|
2030
|
-
|
|
2031
|
-
//assert(!item.$proxyHandler)
|
|
2032
|
-
let proxyItem = getProxy(item, this, this.path, newIndex); //new Proxy(item, this.extend(newIndex));
|
|
2033
|
-
let exprs = loopInfo.itemTransformer(proxyItem, newIndex).exprs; // TODO: Pass updated array as third argument to transformer.
|
|
2034
|
-
ng.applyExprs(exprs); // this is the slow part.
|
|
2035
|
-
}
|
|
2036
|
-
}
|
|
2037
|
-
};
|
|
2038
|
-
|
|
2039
|
-
// Iterate in different directions depending on whether diff is positive or negative.
|
|
2040
|
-
if (diff > 0) // Moving items to the right, so we iterate backward from the end.
|
|
2041
|
-
for (let i = objLength-1; i >= index + items.length + deleteCount; i--)
|
|
2042
|
-
move(i);
|
|
2043
|
-
|
|
2044
|
-
else // Moving items to the left, so we iterate forward.
|
|
2045
|
-
for (let i = index + items.length + deleteCount; i < objLength; i++)
|
|
2046
|
-
move(i);
|
|
2047
|
-
}
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
// Add new items
|
|
2051
|
-
if (items.length) {
|
|
2052
|
-
let loopPath = this.getSerializedPath();
|
|
2053
|
-
|
|
2054
|
-
let beforePath = this.getSerializedPath(index);
|
|
2055
|
-
let ngm = NodeGroupManager.get(this.root);
|
|
2056
|
-
|
|
2057
|
-
for (let loopInfo of ngm.subscribers.getAll(loopPath)) {
|
|
2058
|
-
let beforeNodes = index < objLength - deleteCount
|
|
2059
|
-
? [...ngm.subscribers.getAll(beforePath)].map(t => t.template.nodeGroup.startNode)
|
|
2060
|
-
: [loopInfo.template.nodeGroup.endNode];
|
|
2061
|
-
for (let beforeNode of beforeNodes) { // TODO: Need to match the beforeNg with the loopInfo instead of iterating.
|
|
2062
|
-
for (let i = 0; i < items.length; i++) {
|
|
2063
|
-
|
|
2064
|
-
// Create NodeGroup of new item.
|
|
2065
|
-
let itemHandler = this.extend(index + i);
|
|
2066
|
-
assert(!items[i].$proxyHandler);
|
|
2067
|
-
let proxyItem = new Proxy(items[i], itemHandler);
|
|
2068
|
-
let template = loopInfo.itemTransformer(proxyItem);
|
|
2069
|
-
let ng = ngm.getNodeGroup(template, null, true);
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
for (let node of ng.getNodes()) {
|
|
2073
|
-
beforeNode.parentNode.insertBefore(node, beforeNode);
|
|
2074
|
-
//loopInfo.template.nodeGroup.endNode = node; // The loop's end node is actually an empty text node, so don't do this.
|
|
2075
|
-
}
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
// Add new items to ngm.templateInfo
|
|
2079
|
-
let spath = itemHandler.getSerializedPath();
|
|
2080
|
-
let subscriber = new Subscriber(loopInfo.itemTransformer, template);
|
|
2081
|
-
subscriber.parent = loopInfo;
|
|
2082
|
-
ngm.subscribers.add(spath, subscriber);
|
|
2083
|
-
}
|
|
2084
|
-
}
|
|
2085
|
-
loopInfo.template.nodeGroup.parentPath.clearNodesCache();
|
|
2086
|
-
loopInfo.template.nodeGroup.nodesCache = null;
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
|
-
let result = obj.splice(index, deleteCount, ...items);
|
|
2091
|
-
|
|
2092
|
-
//this.notify(this.path);
|
|
2093
|
-
|
|
2094
|
-
return result;
|
|
2095
|
-
}
|
|
2096
|
-
}
|
|
1838
|
+
|
|
1839
|
+
}
|
|
2097
1840
|
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
return obj.indexOf(item.$removeProxy || item)
|
|
1841
|
+
// Removing them here will mess up the treeWalker.
|
|
1842
|
+
toRemove.push(node);
|
|
1843
|
+
}
|
|
2102
1844
|
}
|
|
2103
1845
|
}
|
|
1846
|
+
toRemove.map(el => el.remove());
|
|
2104
1847
|
|
|
2105
|
-
//
|
|
2106
|
-
|
|
1848
|
+
// Handle solarite-placeholder's.
|
|
1849
|
+
// Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
|
|
1850
|
+
//if (componentNames.size)
|
|
1851
|
+
// this.components = [...this.fragment.querySelectorAll([...componentNames].join(','))]
|
|
2107
1852
|
|
|
2108
|
-
|
|
2109
|
-
|
|
1853
|
+
// Rename "is" attributes so the Web Components don't instantiate until we have the values of their PathExpr arguments.
|
|
1854
|
+
// that happens in NodeGroup.applyComponentExprs()
|
|
1855
|
+
for (let el of this.fragment.querySelectorAll('[is]')) {
|
|
1856
|
+
el.setAttribute('_is', el.getAttribute('is'));
|
|
1857
|
+
// this.components.push(el);
|
|
1858
|
+
}
|
|
2110
1859
|
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
1860
|
+
for (let path of this.paths) {
|
|
1861
|
+
if (path.nodeBefore)
|
|
1862
|
+
path.nodeBeforeIndex = Array.prototype.indexOf.call(path.nodeBefore.parentNode.childNodes, path.nodeBefore);
|
|
1863
|
+
path.nodeMarkerPath = getNodePath(path.nodeMarker);
|
|
2115
1864
|
|
|
2116
|
-
//
|
|
2117
|
-
if (
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
return new Proxy(result, handler);
|
|
1865
|
+
// Cache so we don't have to calculate this later inside NodeGroup.applyExprs()
|
|
1866
|
+
if (path.type === PathType.Value && path.nodeMarker.nodeType === 1 && /*path.nodeMarker !== template.content.children[0] &&*/
|
|
1867
|
+
(path.nodeMarker.tagName.includes('-') || path.nodeMarker.hasAttribute('is'))) {
|
|
1868
|
+
path.type = PathType.Component;
|
|
2121
1869
|
}
|
|
2122
|
-
|
|
2123
|
-
return result;
|
|
2124
1870
|
}
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
/**
|
|
2129
|
-
* Called directly by JavaScript when setting the value of a property via equals.
|
|
2130
|
-
* @param obj
|
|
2131
|
-
* @param prop
|
|
2132
|
-
* @param value
|
|
2133
|
-
* @returns {boolean} */
|
|
2134
|
-
set(obj, prop, value) {
|
|
2135
|
-
withinSet++;
|
|
2136
1871
|
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
let fullPath = [...this.path, prop+''];
|
|
2140
|
-
this.notify(fullPath);
|
|
1872
|
+
this.findEmbeds();
|
|
1873
|
+
|
|
2141
1874
|
|
|
2142
|
-
|
|
1875
|
+
} // end constructor
|
|
2143
1876
|
|
|
2144
|
-
return result;
|
|
2145
|
-
}
|
|
2146
|
-
|
|
2147
1877
|
/**
|
|
2148
|
-
*
|
|
2149
|
-
*
|
|
2150
|
-
*
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
1878
|
+
* We find the path to every embed here once in the Shell, instead of every time a NodeGroup is instantiated.
|
|
1879
|
+
* When a Nodegroup is created, it calls NodeGroup.activateEmbeds() that uses these paths.
|
|
1880
|
+
* Populates:
|
|
1881
|
+
* this.scripts
|
|
1882
|
+
* this.styles
|
|
1883
|
+
* this.ids
|
|
1884
|
+
* this.staticComponents */
|
|
1885
|
+
findEmbeds() {
|
|
1886
|
+
this.scripts = Array.prototype.map.call(this.fragment.querySelectorAll('scripts'), el => getNodePath(el));
|
|
1887
|
+
this.styles = Array.prototype.map.call(this.fragment.querySelectorAll('style'), el => getNodePath(el));
|
|
1888
|
+
|
|
1889
|
+
let idEls = this.fragment.querySelectorAll('[id],[data-id]');
|
|
2155
1890
|
|
|
2156
|
-
let rootHash = getObjectId(this.root);
|
|
2157
|
-
|
|
2158
|
-
let len = fullPath.length;
|
|
2159
|
-
while (len >= 1) {
|
|
2160
|
-
let path = fullPath.slice(0, len);
|
|
2161
|
-
let val = delve(this.root, path);
|
|
2162
|
-
let serializedPath = JSON.stringify([rootHash, ...path]);
|
|
2163
|
-
for (let subscriber of ngm.subscribers.getAll(serializedPath)) {
|
|
2164
|
-
// We already applied expressions for a single item within this loop.
|
|
2165
|
-
if (excluded.has(subscriber))
|
|
2166
|
-
continue;
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
// Delete child subscriptions so we don't have duplicate subscriptions when we call applyExprs() directly below.
|
|
2170
|
-
if (subscriber.children) {
|
|
2171
|
-
for (let [spath, childInfo] of subscriber.children)
|
|
2172
|
-
ngm.subscribers.delete(spath, childInfo);
|
|
2173
|
-
subscriber.children = undefined;
|
|
2174
|
-
}
|
|
2175
1891
|
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
// Apply expressions.
|
|
2182
|
-
subscriber.template.nodeGroup.applyExprs(exprs);
|
|
2183
|
-
|
|
2184
|
-
// Don't also process parent loop after updating a single item within it.
|
|
2185
|
-
if (subscriber.parent)
|
|
2186
|
-
excluded.add(subscriber.parent);
|
|
2187
|
-
}
|
|
2188
|
-
len--;
|
|
1892
|
+
// Check for valid id names.
|
|
1893
|
+
for (let el of idEls) {
|
|
1894
|
+
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
1895
|
+
if (div.hasOwnProperty(id))
|
|
1896
|
+
throw new Error(`<${el.tagName.toLowerCase()} id="${id}"> can't override existing HTMLElement id property.`)
|
|
2189
1897
|
}
|
|
2190
|
-
}
|
|
2191
|
-
}
|
|
2192
1898
|
|
|
2193
|
-
function getProxy(obj, ph, path, path2) {
|
|
2194
|
-
if (!obj || !typeof obj !== 'object')
|
|
2195
|
-
return obj;
|
|
2196
|
-
|
|
2197
|
-
if (obj.$proxyHandler) {
|
|
2198
|
-
|
|
2199
|
-
assert(obj.$proxyHandler.root === ph.root && JSON.stringify(obj.$proxyHandler.path) === JSON.stringify(path));
|
|
2200
|
-
return obj;
|
|
2201
|
-
}
|
|
2202
|
-
if (path2)
|
|
2203
|
-
path = [...path, path2+''];
|
|
2204
|
-
return new Proxy(obj, new ProxyHandler(ph.root, path, ph.props));
|
|
2205
|
-
}
|
|
2206
1899
|
|
|
2207
|
-
|
|
2208
|
-
* Call a function and record which watched variables it accesess, storing their paths in pathToTemplates.
|
|
2209
|
-
* Used by NodeGroup.applyOneExpr().
|
|
2210
|
-
* TODO: only allow this to be called once per callback.
|
|
2211
|
-
* @param callback {function}
|
|
2212
|
-
* @param ngm {NodeGroupManager}
|
|
2213
|
-
* @returns {Template} */
|
|
2214
|
-
function watchFunction(callback, ngm) {
|
|
2215
|
-
ngm.clearSubscribersIfNeeded();
|
|
2216
|
-
|
|
2217
|
-
logGets = true;
|
|
2218
|
-
|
|
2219
|
-
let transformer = () => new Template(['', ''], [callback()]);
|
|
2220
|
-
let template = transformer();
|
|
2221
|
-
for (let path of gets) {
|
|
2222
|
-
let subscriber = new Subscriber(transformer, template);
|
|
2223
|
-
ngm.subscribers.add(serializePath(path), subscriber);
|
|
2224
|
-
}
|
|
1900
|
+
this.ids = Array.prototype.map.call(idEls, el => getNodePath(el));
|
|
2225
1901
|
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
1902
|
+
// Events (not yet used)
|
|
1903
|
+
for (let el of this.fragment.querySelectorAll('*')) {
|
|
1904
|
+
for (let attrib of el.attributes)
|
|
1905
|
+
if (isEvent(attrib.name))
|
|
1906
|
+
this.events.push([attrib.name, getNodePath(el)]);
|
|
1907
|
+
|
|
1908
|
+
if (el.tagName.includes('-') || el.hasAttribute('_is'))
|
|
1909
|
+
|
|
1910
|
+
// Dynamic components have attributes with expression values.
|
|
1911
|
+
// They are created from applyExprs()
|
|
1912
|
+
// But static components are created in a separate path inside the NodeGroup constructor.
|
|
1913
|
+
if (!this.paths.find(path => path.nodeMarker === el))
|
|
1914
|
+
this.staticComponents.push(getNodePath(el));
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
}
|
|
2230
1918
|
|
|
2231
|
-
/**
|
|
2232
|
-
* Represents a place where nodes will be updated.
|
|
2233
|
-
* TODO: Merge this with Template, or ExprPath? */
|
|
2234
|
-
class Subscriber {
|
|
2235
|
-
|
|
2236
|
-
/** @type {Subscriber} Used only for children of a loop. */
|
|
2237
|
-
parent;
|
|
2238
|
-
|
|
2239
|
-
/** @type {Subscriber[]} TemplateInfo for each child of a loop. */
|
|
2240
|
-
children;
|
|
2241
|
-
|
|
2242
1919
|
/**
|
|
2243
|
-
*
|
|
2244
|
-
* @param
|
|
2245
|
-
* @
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
1920
|
+
* Get the shell for the html strings.
|
|
1921
|
+
* @param htmlStrings {string[]} Typically comes from a Template.
|
|
1922
|
+
* @returns {Shell} */
|
|
1923
|
+
static get(htmlStrings) {
|
|
1924
|
+
let result = Globals.shells.get(htmlStrings);
|
|
1925
|
+
if (!result) {
|
|
1926
|
+
result = new Shell(htmlStrings);
|
|
1927
|
+
Globals.shells.set(htmlStrings, result); // cache
|
|
1928
|
+
}
|
|
1929
|
+
|
|
2249
1930
|
|
|
2250
|
-
|
|
2251
|
-
this.itemTransformer = itemTransformer;
|
|
1931
|
+
return result;
|
|
2252
1932
|
}
|
|
1933
|
+
|
|
1934
|
+
|
|
2253
1935
|
}
|
|
2254
1936
|
|
|
2255
|
-
/** @typedef {boolean|string|number|function|Object|Array|Date|Node} Expr */
|
|
1937
|
+
/** @typedef {boolean|string|number|function|Object|Array|Date|Node|Template} Expr */
|
|
2256
1938
|
|
|
2257
1939
|
/**
|
|
2258
1940
|
* A group of Nodes instantiated from a Shell, with Expr's filled in.
|
|
@@ -2265,32 +1947,29 @@ class Subscriber {
|
|
|
2265
1947
|
* */
|
|
2266
1948
|
class NodeGroup {
|
|
2267
1949
|
|
|
2268
|
-
/**
|
|
2269
|
-
|
|
1950
|
+
/**
|
|
1951
|
+
* @Type {RootNodeGroup} */
|
|
1952
|
+
rootNg;
|
|
2270
1953
|
|
|
2271
1954
|
/** @type {ExprPath} */
|
|
2272
1955
|
parentPath;
|
|
2273
1956
|
|
|
2274
|
-
/** @type {Node} First node of NodeGroup. Should never be null. */
|
|
1957
|
+
/** @type {Node|HTMLElement} First node of NodeGroup. Should never be null. */
|
|
2275
1958
|
startNode;
|
|
2276
1959
|
|
|
2277
|
-
/** @type {Node} A node that never changes that this NodeGroup should always insert its nodes before.
|
|
1960
|
+
/** @type {Node|HTMLElement} A node that never changes that this NodeGroup should always insert its nodes before.
|
|
2278
1961
|
* An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.*/
|
|
2279
1962
|
endNode;
|
|
2280
1963
|
|
|
2281
1964
|
/** @type {ExprPath[]} */
|
|
2282
1965
|
paths = [];
|
|
2283
1966
|
|
|
2284
|
-
/** @type {string} */
|
|
1967
|
+
/** @type {string} Key that matches the template and the expressions. */
|
|
2285
1968
|
exactKey;
|
|
2286
1969
|
|
|
2287
|
-
/** @type {string} */
|
|
1970
|
+
/** @type {string} Key that only matches the template. */
|
|
2288
1971
|
closeKey;
|
|
2289
1972
|
|
|
2290
|
-
/** @type {boolean} Used by NodeGroupManager. */
|
|
2291
|
-
inUse;
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
1973
|
/**
|
|
2295
1974
|
* @internal
|
|
2296
1975
|
* @type {Node[]} Cached result of getNodes() used only for improving performance.*/
|
|
@@ -2300,436 +1979,141 @@ class NodeGroup {
|
|
|
2300
1979
|
* @type {?Map<HTMLStyleElement, string>} */
|
|
2301
1980
|
styles;
|
|
2302
1981
|
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
* But pseudoRoot is outside of this.getNodes().
|
|
2306
|
-
* NodeGroupManager.render() copies the attributes from pseudoRoot to the actual web component root element.
|
|
2307
|
-
* @type {?HTMLElement} */
|
|
2308
|
-
pseudoRoot;
|
|
2309
|
-
|
|
2310
|
-
currentComponentProps = {};
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
/**
|
|
2314
|
-
* Create an "instantiated" NodeGroup from a Template and add it to an element.
|
|
2315
|
-
* @param template {Template} Create it from the html strings and expressions in this template.
|
|
2316
|
-
* @param manager {?NodeGroupManager}
|
|
2317
|
-
* @returns {NodeGroup} */
|
|
2318
|
-
constructor(template, manager=null) {
|
|
2319
|
-
|
|
2320
|
-
// Used for forEach()
|
|
2321
|
-
this.template = template;
|
|
2322
|
-
this.manager = manager;
|
|
2323
|
-
|
|
2324
|
-
// new!
|
|
2325
|
-
template.nodeGroup = this;
|
|
2326
|
-
|
|
2327
|
-
// Get a cached version of the parsed and instantiated html, and ExprPaths.
|
|
2328
|
-
let shell = Shell.get(template.html);
|
|
2329
|
-
|
|
2330
|
-
let fragment = shell.fragment.cloneNode(true);
|
|
2331
|
-
|
|
2332
|
-
// Figure out value of replaceMode option if it isn't set,
|
|
2333
|
-
// Assume replaceMode if there's only one child element and its tagname matches the root el.
|
|
2334
|
-
let replaceMode = typeof template.replaceMode === 'boolean'
|
|
2335
|
-
? template.replaceMode
|
|
2336
|
-
: fragment.children.length===1 &&
|
|
2337
|
-
fragment.firstElementChild?.tagName.replace(/-REDCOMPONENT-PLACEHOLDER$/, '')
|
|
2338
|
-
=== manager?.rootEl?.tagName;
|
|
2339
|
-
if (replaceMode) {
|
|
2340
|
-
this.pseudoRoot = fragment.firstElementChild;
|
|
2341
|
-
// if (!manager.rootEl)
|
|
2342
|
-
// manager.rootEl = this.pseudoRoot;
|
|
2343
|
-
|
|
2344
|
-
}
|
|
2345
|
-
|
|
2346
|
-
let childNodes = replaceMode
|
|
2347
|
-
? fragment.firstElementChild.childNodes
|
|
2348
|
-
: fragment.childNodes;
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
this.startNode = childNodes[0];
|
|
2352
|
-
this.endNode = childNodes[childNodes.length - 1];
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
// Update paths
|
|
2356
|
-
for (let oldPath of shell.paths) {
|
|
2357
|
-
let path = oldPath.clone(fragment);
|
|
2358
|
-
path.parentNg = this;
|
|
2359
|
-
this.paths.push(path);
|
|
2360
|
-
}
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
// Update web component placeholders.
|
|
2364
|
-
// Ctrl+F "redcomponent-placeholder" in project to find all code that manages subcomponents.
|
|
2365
|
-
// Is this list needed at all?
|
|
2366
|
-
//for (let component of shell.components)
|
|
2367
|
-
// this.components.push(resolveNodePath(this.startNode.parentNode, getNodePath(component)))
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
this.activateEmbeds(fragment, shell);
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
// Apply exprs
|
|
2376
|
-
this.applyExprs(template.exprs);
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
}
|
|
2380
|
-
|
|
2381
|
-
activateEmbeds(root, shell) {
|
|
2382
|
-
|
|
2383
|
-
// static components
|
|
2384
|
-
// Must happen before ids.
|
|
2385
|
-
for (let path of shell.staticComponents) {
|
|
2386
|
-
let el = resolveNodePath(root, path);
|
|
2387
|
-
|
|
2388
|
-
// Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
|
|
2389
|
-
if (el.tagName !== this.pseudoRoot?.tagName)
|
|
2390
|
-
this.createNewComponent(el);
|
|
2391
|
-
}
|
|
2392
|
-
|
|
2393
|
-
if (this.manager.rootEl) {
|
|
2394
|
-
|
|
2395
|
-
// ids
|
|
2396
|
-
if (this.manager.options.ids !== false)
|
|
2397
|
-
for (let path of shell.ids) {
|
|
2398
|
-
let el = resolveNodePath(root, path);
|
|
2399
|
-
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
2400
|
-
this.manager.rootEl[id] = el;
|
|
2401
|
-
}
|
|
2402
|
-
|
|
2403
|
-
// styles
|
|
2404
|
-
if (this.manager.options.styles !== false) {
|
|
2405
|
-
if (shell.styles.length)
|
|
2406
|
-
this.styles = new Map();
|
|
2407
|
-
for (let path of shell.styles) {
|
|
2408
|
-
let style = resolveNodePath(root, path);
|
|
2409
|
-
Util.bindStyles(style, this.manager.rootEl);
|
|
2410
|
-
this.styles.set(style, style.textContent);
|
|
2411
|
-
}
|
|
2412
|
-
|
|
2413
|
-
}
|
|
2414
|
-
// scripts
|
|
2415
|
-
if (this.manager.options.scripts !== false) {
|
|
2416
|
-
for (let path of shell.scripts) {
|
|
2417
|
-
let script = resolveNodePath(root, path);
|
|
2418
|
-
eval(script.textContent);
|
|
2419
|
-
}
|
|
2420
|
-
}
|
|
2421
|
-
}
|
|
2422
|
-
}
|
|
2423
|
-
|
|
2424
|
-
updateStyles() {
|
|
2425
|
-
if (this.styles)
|
|
2426
|
-
for (let [style, oldText] of this.styles) {
|
|
2427
|
-
let newText = style.textContent;
|
|
2428
|
-
if (oldText !== newText)
|
|
2429
|
-
Util.bindStyles(style, this.manager.rootEl);
|
|
2430
|
-
}
|
|
2431
|
-
}
|
|
2432
|
-
|
|
2433
|
-
/**
|
|
2434
|
-
* Use the paths to insert the given expressions.
|
|
2435
|
-
* Dispatches expression handling to other functions depending on the path type.
|
|
2436
|
-
* @param exprs {(*|*[]|function|Template)[]} */
|
|
2437
|
-
applyExprs(exprs) {
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
// Update exprs at paths.
|
|
2441
|
-
let exprIndex = exprs.length-1, expr, lastNode;
|
|
2442
|
-
|
|
2443
|
-
// We apply them in reverse order so that a <select> box has its options created from an expression
|
|
2444
|
-
// before its value attribute is set via an expression.
|
|
2445
|
-
for (let path of this.paths.toReversed()) {
|
|
2446
|
-
expr = exprs[exprIndex];
|
|
2447
|
-
|
|
2448
|
-
// Nodes
|
|
2449
|
-
if (path.type === PathType.Content) {
|
|
2450
|
-
this.applyNodeExpr(path, expr);
|
|
2451
|
-
|
|
2452
|
-
}
|
|
2453
|
-
|
|
2454
|
-
// Attributes
|
|
2455
|
-
else {
|
|
2456
|
-
let node = path.nodeMarker; // path.resolve(result);
|
|
2457
|
-
let node2 = (this.manager.rootEl && node === this.pseudoRoot) ? this.manager.rootEl : node;
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
// This is necessary both here and below.
|
|
2461
|
-
if (lastNode && lastNode !== this.pseudoRoot && lastNode !== node && Object.keys(this.currentComponentProps).length) {
|
|
2462
|
-
this.applyComponentExprs(lastNode, this.currentComponentProps);
|
|
2463
|
-
this.currentComponentProps = {};
|
|
2464
|
-
}
|
|
2465
|
-
|
|
2466
|
-
if (path.type === PathType.Multiple)
|
|
2467
|
-
path.applyMultipleAttribs(node2, expr);
|
|
2468
|
-
|
|
2469
|
-
// Capture attribute expressions to later send to the constructor of a web component.
|
|
2470
|
-
// Ctrl+F "redcomponent-placeholder" in project to find all code that manages subcomponents.
|
|
2471
|
-
else if (path.nodeMarker !== this.pseudoRoot && path.type === PathType.Component)
|
|
2472
|
-
this.currentComponentProps[path.attrName] = expr;
|
|
2473
|
-
|
|
2474
|
-
else if (path.type === PathType.Comment) ;
|
|
2475
|
-
else {
|
|
2476
|
-
|
|
2477
|
-
// Event attribute value
|
|
2478
|
-
if (path.attrValue===null && (typeof expr === 'function' || Array.isArray(expr)) && isEvent(path.attrName)) {
|
|
2479
|
-
let root = this.manager.rootEl || this.startNode.parentNode; // latter is used when constructing a whole element.
|
|
2480
|
-
path.applyEventAttrib(node2, expr, root);
|
|
2481
|
-
}
|
|
2482
|
-
|
|
2483
|
-
// Regular attribute value.
|
|
2484
|
-
else
|
|
2485
|
-
exprIndex = path.applyValueAttrib(node2, exprs, exprIndex);
|
|
2486
|
-
}
|
|
2487
|
-
|
|
2488
|
-
lastNode = path.nodeMarker;
|
|
2489
|
-
}
|
|
2490
|
-
|
|
2491
|
-
exprIndex--;
|
|
2492
|
-
} // end for(path of this.paths)
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
// Check again after we iterate through all paths to apply to a component.
|
|
2496
|
-
if (lastNode && lastNode !== this.pseudoRoot && Object.keys(this.currentComponentProps).length) {
|
|
2497
|
-
this.applyComponentExprs(lastNode, this.currentComponentProps);
|
|
2498
|
-
this.currentComponentProps = {};
|
|
2499
|
-
}
|
|
2500
|
-
|
|
2501
|
-
this.updateStyles();
|
|
2502
|
-
|
|
2503
|
-
// Invalidate the nodes cache because we just changed it.
|
|
2504
|
-
this.nodesCache = null;
|
|
2505
|
-
|
|
2506
|
-
// If there's leftover expressions, there's probably an issue with the Shell that created this NodeGroup,
|
|
2507
|
-
// and the number of paths not matching.
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
}
|
|
2513
|
-
|
|
2514
|
-
/**
|
|
2515
|
-
* Insert/replace the nodes created by a single expression.
|
|
2516
|
-
* Called by applyExprs()
|
|
2517
|
-
* This function is recursive, as the functions it calls also call it.
|
|
2518
|
-
* TODO: Move this to ExprPath?
|
|
2519
|
-
* @param path {ExprPath}
|
|
2520
|
-
* @param expr {Expr}
|
|
2521
|
-
* @return {Node[]} New Nodes created. */
|
|
2522
|
-
applyNodeExpr(path, expr) {
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
/** @type {(Node|NodeGroup|Expr)[]} */
|
|
2526
|
-
let newNodes = [];
|
|
2527
|
-
let oldNodeGroups = path.nodeGroups;
|
|
2528
|
-
|
|
2529
|
-
let secondPass = []; // indices
|
|
2530
|
-
|
|
2531
|
-
// First Pass
|
|
2532
|
-
//for (let ng of path.nodeGroups) // TODO: Is this necessary?
|
|
2533
|
-
// ng.parentPath = null;
|
|
2534
|
-
path.nodeGroups = [];
|
|
2535
|
-
this.applyOneExpr(expr, path, newNodes, secondPass);
|
|
2536
|
-
this.existingTextNodes = null;
|
|
1982
|
+
currentComponentProps = {};
|
|
1983
|
+
|
|
2537
1984
|
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
1985
|
+
/**
|
|
1986
|
+
* Create an "instantiated" NodeGroup from a Template and add it to an element.
|
|
1987
|
+
* @param template {Template} Create it from the html strings and expressions in this template.
|
|
1988
|
+
* @param parentPath {?ExprPath} */
|
|
1989
|
+
constructor(template, parentPath=null) {
|
|
1990
|
+
if (!(this instanceof RootNodeGroup)) {
|
|
1991
|
+
let [fragment, shell] = this.init(template, parentPath);
|
|
2541
1992
|
|
|
2542
|
-
|
|
2543
|
-
let flatten = false;
|
|
2544
|
-
if (secondPass.length) {
|
|
2545
|
-
for (let [nodesIndex, ngIndex] of secondPass) {
|
|
2546
|
-
let ng = this.manager.getNodeGroup(newNodes[nodesIndex], false);
|
|
2547
|
-
|
|
2548
|
-
ng.parentPath = path;
|
|
2549
|
-
let ngNodes = ng.getNodes();
|
|
1993
|
+
this.updatePaths(fragment, shell.paths);
|
|
2550
1994
|
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
if (ngNodes.length === 1)
|
|
2554
|
-
newNodes[nodesIndex] = ngNodes[0];
|
|
2555
|
-
|
|
2556
|
-
else {
|
|
2557
|
-
newNodes[nodesIndex] = ngNodes;
|
|
2558
|
-
flatten = true;
|
|
2559
|
-
}
|
|
2560
|
-
path.nodeGroups[ngIndex] = ng;
|
|
2561
|
-
}
|
|
1995
|
+
this.activateEmbeds(fragment, shell);
|
|
2562
1996
|
|
|
2563
|
-
|
|
2564
|
-
|
|
1997
|
+
// Apply exprs
|
|
1998
|
+
this.applyExprs(template.exprs);
|
|
2565
1999
|
}
|
|
2000
|
+
}
|
|
2566
2001
|
|
|
2567
|
-
|
|
2568
|
-
|
|
2002
|
+
/**
|
|
2003
|
+
* Common init shared by RootNodeGroup and NodeGroup constructors.
|
|
2004
|
+
* But in a separate function because they need to do this at a different step.
|
|
2005
|
+
* @param template {Template} Create it from the html strings and expressions in this template.
|
|
2006
|
+
* @param parentPath {?ExprPath}
|
|
2007
|
+
* @param exactKey {?string} Optional, if already calculated.
|
|
2008
|
+
* @param closeKey {?string}
|
|
2009
|
+
* @returns {[DocumentFragment, Shell]} */
|
|
2010
|
+
init(template, parentPath=null, exactKey=null, closeKey=null) {
|
|
2011
|
+
this.exactKey = exactKey || template.getExactKey();
|
|
2012
|
+
this.closeKey = closeKey || template.getCloseKey();
|
|
2569
2013
|
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
path.nodesCache = newNodes; // Replaces value set by path.getNodes()
|
|
2014
|
+
this.parentPath = parentPath;
|
|
2015
|
+
this.rootNg = parentPath?.parentNg?.rootNg || this;
|
|
2573
2016
|
|
|
2017
|
+
|
|
2574
2018
|
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
if (diff !== false) {
|
|
2019
|
+
/** @type {Template} */
|
|
2020
|
+
this.template = template;
|
|
2578
2021
|
|
|
2579
|
-
|
|
2580
|
-
|
|
2022
|
+
// new! Is this needed?
|
|
2023
|
+
template.nodeGroup = this;
|
|
2581
2024
|
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2025
|
+
// Get a cached version of the parsed and instantiated html, and ExprPaths.
|
|
2026
|
+
let shell = Shell.get(template.html);
|
|
2027
|
+
let fragment = shell.fragment.cloneNode(true);
|
|
2585
2028
|
|
|
2586
|
-
|
|
2587
|
-
|
|
2029
|
+
let childNodes = fragment.childNodes;
|
|
2030
|
+
this.startNode = childNodes[0];
|
|
2031
|
+
this.endNode = childNodes[childNodes.length - 1];
|
|
2588
2032
|
|
|
2589
|
-
|
|
2590
|
-
}
|
|
2591
|
-
|
|
2033
|
+
return [fragment, shell];
|
|
2592
2034
|
}
|
|
2593
|
-
|
|
2594
|
-
/**
|
|
2595
|
-
* Find NodeGroups that had their nodes removed and add those nodes to a Fragment so
|
|
2596
|
-
* they're not lost forever and the NodeGroup's internal structure is still consistent.
|
|
2597
|
-
* Called from NodeGroup.applyNodeExpr().
|
|
2598
|
-
* @param oldNodeGroups {NodeGroup[]}
|
|
2599
|
-
* @param oldNodes {Node[]} */
|
|
2600
|
-
saveOrphans(oldNodeGroups, oldNodes) {
|
|
2601
|
-
let oldNgMap = new Map();
|
|
2602
|
-
for (let ng of oldNodeGroups) {
|
|
2603
|
-
oldNgMap.set(ng.startNode, ng);
|
|
2604
|
-
|
|
2605
|
-
// TODO: Is this necessary?
|
|
2606
|
-
// if (ng.parentPath)
|
|
2607
|
-
// ng.parentPath.clearNodesCache();
|
|
2608
|
-
}
|
|
2609
2035
|
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
i++;
|
|
2618
|
-
node = oldNodes[i];
|
|
2619
|
-
}
|
|
2620
|
-
fragment.append(endNode);
|
|
2621
|
-
}
|
|
2622
|
-
}
|
|
2623
|
-
}
|
|
2036
|
+
/**
|
|
2037
|
+
* Use the paths to insert the given expressions.
|
|
2038
|
+
* Dispatches expression handling to other functions depending on the path type.
|
|
2039
|
+
* @param exprs {(*|*[]|function|Template)[]}
|
|
2040
|
+
* @param paths {?ExprPath[]} Optional. */
|
|
2041
|
+
applyExprs(exprs, paths=null) {
|
|
2042
|
+
paths = paths || this.paths;
|
|
2624
2043
|
|
|
2625
|
-
// TODO: Move to ExprPath?
|
|
2626
|
-
applyOneExpr(expr, path, newNodes, secondPass) {
|
|
2627
2044
|
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
// TODO: Track ranges of changed nodes and only pass those to udomdiff?
|
|
2644
|
-
// But will that break the swap benchmark?
|
|
2645
|
-
newNodes.push(...ng.getNodes());
|
|
2646
|
-
path.nodeGroups.push(ng);
|
|
2647
|
-
}
|
|
2648
|
-
|
|
2649
|
-
// If expression, evaluate later to find partial match.
|
|
2650
|
-
else {
|
|
2651
|
-
secondPass.push([newNodes.length, path.nodeGroups.length]);
|
|
2652
|
-
newNodes.push(expr);
|
|
2653
|
-
path.nodeGroups.push(null); // placeholder
|
|
2045
|
+
|
|
2046
|
+
// Update exprs at paths.
|
|
2047
|
+
let exprIndex = exprs.length-1, expr, lastNode;
|
|
2048
|
+
|
|
2049
|
+
// We apply them in reverse order so that a <select> box has its options created from an expression
|
|
2050
|
+
// before its value attribute is set via an expression.
|
|
2051
|
+
for (let path of paths.toReversed()) {
|
|
2052
|
+
expr = exprs[exprIndex];
|
|
2053
|
+
|
|
2054
|
+
// Nodes
|
|
2055
|
+
|
|
2056
|
+
// This is necessary both here and below.
|
|
2057
|
+
if (lastNode && lastNode !== this.rootNg.root && lastNode !== path.nodeMarker && Object.keys(this.currentComponentProps).length) {
|
|
2058
|
+
this.applyComponentExprs(lastNode, this.currentComponentProps);
|
|
2059
|
+
this.currentComponentProps = {};
|
|
2654
2060
|
}
|
|
2655
|
-
}
|
|
2656
2061
|
|
|
2657
|
-
|
|
2658
|
-
else if (expr instanceof Node) {
|
|
2062
|
+
exprIndex = path.apply(expr, exprs, exprIndex, this.currentComponentProps);
|
|
2659
2063
|
|
|
2660
|
-
|
|
2661
|
-
if (expr instanceof DocumentFragment)
|
|
2662
|
-
newNodes.push(...expr.childNodes);
|
|
2663
|
-
else
|
|
2664
|
-
newNodes.push(expr);
|
|
2665
|
-
}
|
|
2064
|
+
lastNode = path.nodeMarker;
|
|
2666
2065
|
|
|
2667
|
-
else if (Array.isArray(expr))
|
|
2668
|
-
for (let subExpr of expr)
|
|
2669
|
-
this.applyOneExpr(subExpr, path, newNodes, secondPass);
|
|
2670
2066
|
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2067
|
+
exprIndex--;
|
|
2068
|
+
} // end for(path of this.paths)
|
|
2069
|
+
|
|
2070
|
+
|
|
2071
|
+
// Check again after we iterate through all paths to apply to a component.
|
|
2072
|
+
if (lastNode && lastNode !== this.rootNg.root && Object.keys(this.currentComponentProps).length) {
|
|
2073
|
+
this.applyComponentExprs(lastNode, this.currentComponentProps);
|
|
2074
|
+
this.currentComponentProps = {};
|
|
2675
2075
|
}
|
|
2676
2076
|
|
|
2677
|
-
|
|
2678
|
-
else {
|
|
2679
|
-
// Convert falsy values (but not 0) to empty string.
|
|
2680
|
-
// Convert numbers to string so they compare the same.
|
|
2681
|
-
let text = (expr === undefined || expr === false || expr === null) ? '' : expr + '';
|
|
2077
|
+
this.updateStyles();
|
|
2682
2078
|
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
if (first.nodeType === 3 && first.nextSibling === path.nodeMarker && !newNodes.includes(first)) {
|
|
2686
|
-
if (first.textContent !== text)
|
|
2687
|
-
first.textContent = text;
|
|
2688
|
-
|
|
2689
|
-
newNodes.push(first);
|
|
2690
|
-
}
|
|
2079
|
+
// Invalidate the nodes cache because we just changed it.
|
|
2080
|
+
this.nodesCache = null;
|
|
2691
2081
|
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
if (idx !== -1)
|
|
2699
|
-
newNodes.push(...this.existingTextNodes.splice(idx, 1));
|
|
2700
|
-
else
|
|
2701
|
-
newNodes.push(path.parentNode.ownerDocument.createTextNode(text));
|
|
2702
|
-
}
|
|
2703
|
-
}
|
|
2082
|
+
// If there's leftover expressions, there's probably an issue with the Shell that created this NodeGroup,
|
|
2083
|
+
// and the number of paths not matching.
|
|
2084
|
+
|
|
2085
|
+
|
|
2086
|
+
|
|
2087
|
+
|
|
2704
2088
|
}
|
|
2705
2089
|
|
|
2706
2090
|
/**
|
|
2707
|
-
* Create a nested
|
|
2091
|
+
* Create a nested Component or call render with the new props.
|
|
2708
2092
|
* @param el {Solarite:HTMLElement}
|
|
2709
2093
|
* @param props {Object} */
|
|
2710
2094
|
applyComponentExprs(el, props) {
|
|
2711
|
-
|
|
2095
|
+
|
|
2712
2096
|
// TODO: Does a hash of this already exist somewhere?
|
|
2713
2097
|
// Perhaps if Components were treated as child NodeGroups, which would need to be the child of an ExprPath,
|
|
2714
2098
|
// then we could re-use the hash and logic from NodeManager?
|
|
2715
2099
|
let newHash = getObjectHash(props);
|
|
2716
|
-
|
|
2717
|
-
let isPreHtmlElement = el.tagName.endsWith('-
|
|
2100
|
+
|
|
2101
|
+
let isPreHtmlElement = el.tagName.endsWith('-SOLARITE-PLACEHOLDER');
|
|
2718
2102
|
let isPreIsElement = el.hasAttribute('_is');
|
|
2719
|
-
|
|
2720
|
-
|
|
2103
|
+
|
|
2104
|
+
|
|
2721
2105
|
// Instantiate a placeholder.
|
|
2722
2106
|
if (isPreHtmlElement || isPreIsElement)
|
|
2723
2107
|
el = this.createNewComponent(el, isPreHtmlElement, props);
|
|
2724
|
-
|
|
2725
|
-
//
|
|
2108
|
+
|
|
2109
|
+
// Call render() with the same params that would've been passed to the constructor.
|
|
2726
2110
|
else if (el.render) {
|
|
2727
|
-
let oldHash = componentHash.get(el);
|
|
2111
|
+
let oldHash = Globals.componentHash.get(el);
|
|
2728
2112
|
if (oldHash !== newHash)
|
|
2729
2113
|
el.render(props); // Pass new values of props to render so it can decide how it wants to respond.
|
|
2730
2114
|
}
|
|
2731
|
-
|
|
2732
|
-
componentHash.set(el, newHash);
|
|
2115
|
+
|
|
2116
|
+
Globals.componentHash.set(el, newHash);
|
|
2733
2117
|
}
|
|
2734
2118
|
|
|
2735
2119
|
/**
|
|
@@ -2747,8 +2131,8 @@ class NodeGroup {
|
|
|
2747
2131
|
isPreHtmlElement = !el.hasAttribute('_is');
|
|
2748
2132
|
|
|
2749
2133
|
let tagName = (isPreHtmlElement
|
|
2750
|
-
? el.tagName.endsWith('-
|
|
2751
|
-
? el.tagName.slice(0, -
|
|
2134
|
+
? el.tagName.endsWith('-SOLARITE-PLACEHOLDER')
|
|
2135
|
+
? el.tagName.slice(0, -21)
|
|
2752
2136
|
: el.tagName
|
|
2753
2137
|
: el.getAttribute('is')).toLowerCase();
|
|
2754
2138
|
|
|
@@ -2772,33 +2156,43 @@ class NodeGroup {
|
|
|
2772
2156
|
// We pass the childNodes to the constructor so it can know about them,
|
|
2773
2157
|
// instead of only afterward when they're appended to the slot below.
|
|
2774
2158
|
// This is useful for a custom selectbox, for example.
|
|
2775
|
-
//
|
|
2159
|
+
// Globals.pendingChildren stores the childen so the super construtor call to Solarite's constructor
|
|
2776
2160
|
// can add them as children before the rest of the constructor code executes.
|
|
2777
2161
|
let ch = [... el.childNodes];
|
|
2778
|
-
|
|
2162
|
+
Globals.pendingChildren.push(ch); // pop() is called in Solarite constructor.
|
|
2779
2163
|
let newEl = new Constructor(props, ch);
|
|
2780
2164
|
|
|
2781
2165
|
if (!isPreHtmlElement)
|
|
2782
2166
|
newEl.setAttribute('is', el.getAttribute('is').toLowerCase());
|
|
2783
2167
|
el.replaceWith(newEl);
|
|
2784
|
-
|
|
2168
|
+
|
|
2785
2169
|
// Set children / slot children
|
|
2786
2170
|
// TODO: Match named slots.
|
|
2787
2171
|
// TODO: This only appends to slot if render() is called in the constructor.
|
|
2788
2172
|
//let slot = newEl.querySelector('slot') || newEl;
|
|
2789
2173
|
//slot.append(...el.childNodes);
|
|
2790
|
-
|
|
2174
|
+
|
|
2791
2175
|
// Copy over event attributes.
|
|
2792
2176
|
for (let propName in props) {
|
|
2793
2177
|
let val = props[propName];
|
|
2794
2178
|
if (propName.startsWith('on') && typeof val === 'function')
|
|
2795
2179
|
newEl.addEventListener(propName.slice(2), e => val(e, newEl));
|
|
2180
|
+
|
|
2181
|
+
// Bind array based event attributes on value.
|
|
2182
|
+
// This same logic is in ExprPath.applyValueAttrib() for non-components.
|
|
2183
|
+
if ((propName === 'value' || propName === 'data-value') && Util.isPath(val)) {
|
|
2184
|
+
let [obj, path] = [val[0], val.slice(1)];
|
|
2185
|
+
newEl.value = delve(obj, path);
|
|
2186
|
+
newEl.addEventListener('input', e => {
|
|
2187
|
+
delve(obj, path, Util.getInputValue(newEl));
|
|
2188
|
+
}, true); // We use capture so we update the values before other events added by the user.
|
|
2189
|
+
}
|
|
2796
2190
|
}
|
|
2797
2191
|
|
|
2798
2192
|
// If an id pointed at the placeholder, update it to point to the new element.
|
|
2799
2193
|
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
2800
2194
|
if (id)
|
|
2801
|
-
this.
|
|
2195
|
+
delve(this.getRootNode(), id.split(/\./g), newEl);
|
|
2802
2196
|
|
|
2803
2197
|
|
|
2804
2198
|
// Update paths to use replaced element.
|
|
@@ -2867,470 +2261,343 @@ class NodeGroup {
|
|
|
2867
2261
|
return this.startNode?.parentNode
|
|
2868
2262
|
}
|
|
2869
2263
|
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
}
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
let componentHash = new WeakMap();
|
|
2876
|
-
|
|
2877
|
-
/**
|
|
2878
|
-
* @typedef {Object} RenderOptions
|
|
2879
|
-
* @property {boolean=} styles - Indicates whether the Courage component is present.
|
|
2880
|
-
* @property {boolean=} scripts - Indicates whether the Power component is present.
|
|
2881
|
-
* @property {boolean=} ids
|
|
2882
|
-
*
|
|
2883
|
-
* @property {?boolean} render
|
|
2884
|
-
* Used only when options are given to a class super constructor inheriting from Solarite.
|
|
2885
|
-
* True to call render() immediately in super constructor.
|
|
2886
|
-
* False to automatically call render() at all.
|
|
2887
|
-
* Undefined (default) to call render() when added to the DOM, unless already rendered.
|
|
2888
|
-
*/
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
/**
|
|
2892
|
-
* Manage all the NodeGroups for a single WebComponent or root HTMLElement
|
|
2893
|
-
* There's one NodeGroup for the root of the WebComponent, and one for every ${...} expression that creates Node children.
|
|
2894
|
-
* And each NodeGroup manages the one or more nodes created by the expression.
|
|
2895
|
-
*
|
|
2896
|
-
* An instance of this class exists for each element that r() renders to. */
|
|
2897
|
-
class NodeGroupManager {
|
|
2898
|
-
|
|
2899
|
-
/** @type {HTMLElement|DocumentFragment} */
|
|
2900
|
-
rootEl;
|
|
2901
|
-
|
|
2902
|
-
/** @type {NodeGroup} */
|
|
2903
|
-
rootNg;
|
|
2904
|
-
|
|
2905
|
-
/** @type {Change[]} */
|
|
2906
|
-
changes = [];
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
2264
|
/**
|
|
2913
|
-
*
|
|
2914
|
-
*
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
nodeGroupsInUse = [];
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
/** @type {RenderOptions} */
|
|
2922
|
-
options = {};
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2265
|
+
* Get the root element of the NodeGroup's RootNodeGroup.
|
|
2266
|
+
* @returns {HTMLElement|DocumentFragment} */
|
|
2267
|
+
getRootNode() {
|
|
2268
|
+
return this.rootNg.root;
|
|
2269
|
+
}
|
|
2926
2270
|
|
|
2927
2271
|
/**
|
|
2928
|
-
* @
|
|
2929
|
-
|
|
2930
|
-
this.
|
|
2931
|
-
/*
|
|
2932
|
-
|
|
2933
|
-
*/
|
|
2272
|
+
* @returns {RootNodeGroup} */
|
|
2273
|
+
getRootNodeGroup() {
|
|
2274
|
+
return this.rootNg;
|
|
2934
2275
|
}
|
|
2935
2276
|
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
this.clearSubscribers = false;
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
if (!template && template !== '') {
|
|
2949
|
-
this.rootEl.outerHTML = '';
|
|
2950
|
-
this.mutationWatcherEnabled = true;
|
|
2951
|
-
return null;
|
|
2952
|
-
}
|
|
2953
|
-
|
|
2954
|
-
// Fast path for empty component.
|
|
2955
|
-
if (template.html?.length === 1 && !template.html[0]) {
|
|
2956
|
-
this.rootEl.innerHTML = '';
|
|
2277
|
+
|
|
2278
|
+
updatePaths(fragment, paths, offset) {
|
|
2279
|
+
// Update paths to point to the fragment.
|
|
2280
|
+
this.paths.length = paths.length;
|
|
2281
|
+
for (let i=0; i<paths.length; i++) {
|
|
2282
|
+
let path = paths[i].clone(fragment, offset);
|
|
2283
|
+
path.parentNg = this;
|
|
2284
|
+
this.paths[i] = path;
|
|
2957
2285
|
}
|
|
2958
|
-
|
|
2286
|
+
}
|
|
2959
2287
|
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
let
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2288
|
+
updateStyles() {
|
|
2289
|
+
if (this.styles)
|
|
2290
|
+
for (let [style, oldText] of this.styles) {
|
|
2291
|
+
let newText = style.textContent;
|
|
2292
|
+
if (oldText !== newText)
|
|
2293
|
+
Util.bindStyles(style, this.getRootNodeGroup().root);
|
|
2966
2294
|
}
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
let firstTime = !this.rootNg;
|
|
2970
|
-
this.rootNg = exact || close;
|
|
2971
|
-
|
|
2972
|
-
// Reparent NodeGroup
|
|
2973
|
-
// TODO: Move this to NodeGroup?
|
|
2974
|
-
let parent = this.rootNg.getParentNode();
|
|
2975
|
-
if (!this.rootEl)
|
|
2976
|
-
this.rootEl = parent;
|
|
2977
|
-
|
|
2978
|
-
// If this is the first time rendering this element.
|
|
2979
|
-
else if (firstTime) {
|
|
2980
|
-
|
|
2981
|
-
// Save slot children
|
|
2982
|
-
let fragment;
|
|
2983
|
-
if (this.rootEl.childNodes.length) {
|
|
2984
|
-
fragment = document.createDocumentFragment();
|
|
2985
|
-
fragment.append(...this.rootEl.childNodes);
|
|
2986
|
-
}
|
|
2295
|
+
}
|
|
2987
2296
|
|
|
2988
|
-
|
|
2989
|
-
if (parent instanceof DocumentFragment)
|
|
2990
|
-
this.rootEl.append(parent);
|
|
2991
|
-
else if (parent)
|
|
2992
|
-
this.rootEl.append(...parent.childNodes);
|
|
2993
|
-
|
|
2994
|
-
// Apply slot children
|
|
2995
|
-
if (fragment) {
|
|
2996
|
-
for (let slot of this.rootEl.querySelectorAll('slot[name]')) {
|
|
2997
|
-
let name = slot.getAttribute('name');
|
|
2998
|
-
if (name)
|
|
2999
|
-
slot.append(...fragment.querySelectorAll(`[slot='${name}']`));
|
|
3000
|
-
}
|
|
3001
|
-
let unamedSlot = this.rootEl.querySelector('slot:not([name])');
|
|
3002
|
-
if (unamedSlot)
|
|
3003
|
-
unamedSlot.append(fragment);
|
|
3004
|
-
}
|
|
2297
|
+
|
|
3005
2298
|
|
|
3006
|
-
}
|
|
3007
2299
|
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
2300
|
+
/**
|
|
2301
|
+
* @param root {HTMLElement}
|
|
2302
|
+
* @param shell {Shell}
|
|
2303
|
+
* @param pathOffset {int} */
|
|
2304
|
+
activateEmbeds(root, shell, pathOffset=0) {
|
|
3013
2305
|
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
if (firstTime)
|
|
3021
|
-
for (let attrib of ng.pseudoRoot.attributes)
|
|
3022
|
-
if (!this.rootEl.hasAttribute(attrib.name))
|
|
3023
|
-
this.rootEl.setAttribute(attrib.name, attrib.value);
|
|
3024
|
-
|
|
3025
|
-
// ng.startNode = ng.endNode = this.rootEl;
|
|
3026
|
-
// ng.nodesCache = [ng.startNode]
|
|
3027
|
-
// for (let path of ng.paths) {
|
|
3028
|
-
// if (path.nodeMarker === ng.rootEl)
|
|
3029
|
-
// path.nodeMarker = this.rootEl;
|
|
3030
|
-
// path.nodesCache = null;
|
|
3031
|
-
//
|
|
3032
|
-
// }
|
|
3033
|
-
//
|
|
3034
|
-
// ng.rootEl = this.rootEl;
|
|
3035
|
-
}
|
|
2306
|
+
// static components. These are WebComponents not created by an expression.
|
|
2307
|
+
// Must happen before ids.
|
|
2308
|
+
for (let path of shell.staticComponents) {
|
|
2309
|
+
if (pathOffset)
|
|
2310
|
+
path = path.slice(0, -pathOffset);
|
|
2311
|
+
let el = resolveNodePath(root, path);
|
|
3036
2312
|
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
2313
|
+
// Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
|
|
2314
|
+
if (root !== el/* && !isReplaceEl(root, el)*/) // TODO: is isReplaceEl necessary?
|
|
2315
|
+
this.createNewComponent(el);
|
|
3040
2316
|
}
|
|
3041
2317
|
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
}
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
/**
|
|
3049
|
-
*
|
|
3050
|
-
* 1. Delete a NodeGroup from this.nodeGroupsAvailable that matches this exactKey.
|
|
3051
|
-
* 2. Then delete all of that NodeGroup's parents' exactKey entries
|
|
3052
|
-
* We don't move them to in-use because we plucked the NodeGroup from them, they no longer match their exactKeys.
|
|
3053
|
-
* 3. Then we move all the NodeGroup's exact+close keyed children to inUse because we don't want future calls
|
|
3054
|
-
* to getNodeGroup() to borrow the children now that the whole NodeGroup is in-use.
|
|
3055
|
-
*
|
|
3056
|
-
* TODO: Have NodeGroups keep track of whether they're inUse.
|
|
3057
|
-
* That way when we go up or down we don't have to remove those with .inUse===true
|
|
3058
|
-
*
|
|
3059
|
-
* @param exactKey
|
|
3060
|
-
* @param goUp
|
|
3061
|
-
* @param child
|
|
3062
|
-
* @returns {?NodeGroup} */
|
|
3063
|
-
findAndDeleteExact(exactKey, goUp=true, child=undefined) {
|
|
3064
|
-
|
|
3065
|
-
let ng = this.nodeGroupsAvailable.delete(exactKey, child);
|
|
3066
|
-
if (ng) {
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
// Mark close-key version as in-use.
|
|
3070
|
-
let closeNg = this.nodeGroupsAvailable.delete(ng.closeKey, ng);
|
|
3071
|
-
|
|
2318
|
+
let rootEl = this.rootNg.root;
|
|
2319
|
+
if (rootEl) {
|
|
3072
2320
|
|
|
3073
|
-
//
|
|
3074
|
-
this.
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
if (goUp) {
|
|
3082
|
-
let ng2 = ng;
|
|
3083
|
-
while (ng2 = ng2?.parentPath?.parentNg) {
|
|
3084
|
-
if (!ng2.inUse) {
|
|
3085
|
-
ng2.inUse = true;
|
|
3086
|
-
let success = this.nodeGroupsAvailable.delete(ng2.exactKey, ng2);
|
|
3087
|
-
// assert(success);
|
|
3088
|
-
let success2 = this.nodeGroupsAvailable.delete(ng2.closeKey, ng2);
|
|
3089
|
-
// assert(success);
|
|
3090
|
-
|
|
2321
|
+
// ids
|
|
2322
|
+
if (this.options?.ids !== false)
|
|
2323
|
+
for (let path of shell.ids) {
|
|
2324
|
+
if (pathOffset)
|
|
2325
|
+
path = path.slice(0, -pathOffset);
|
|
2326
|
+
let el = resolveNodePath(root, path);
|
|
2327
|
+
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
2328
|
+
if (id) { // If something hasn't removed the id.
|
|
3091
2329
|
|
|
3092
|
-
//
|
|
3093
|
-
if (
|
|
3094
|
-
|
|
3095
|
-
|
|
2330
|
+
// Don't allow overwriting existing class properties if they already have a non-Node value.
|
|
2331
|
+
if (rootEl[id] && !(rootEl[id] instanceof Node))
|
|
2332
|
+
throw new Error(`${rootEl.constructor.name}.${id} already has a value. ` +
|
|
2333
|
+
`Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
|
|
2334
|
+
|
|
2335
|
+
delve(rootEl, id.split(/\./g), el);
|
|
3096
2336
|
}
|
|
3097
2337
|
}
|
|
3098
|
-
}
|
|
3099
2338
|
|
|
3100
|
-
//
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
2339
|
+
// styles
|
|
2340
|
+
if (this.options?.styles !== false) {
|
|
2341
|
+
if (shell.styles.length)
|
|
2342
|
+
this.styles = new Map();
|
|
2343
|
+
for (let path of shell.styles) {
|
|
2344
|
+
if (pathOffset)
|
|
2345
|
+
path = path.slice(0, -pathOffset);
|
|
2346
|
+
let style = resolveNodePath(root, path);
|
|
2347
|
+
Util.bindStyles(style, rootEl);
|
|
2348
|
+
this.styles.set(style, style.textContent);
|
|
3106
2349
|
}
|
|
3107
|
-
|
|
3108
|
-
if (ng.parentPath) ;
|
|
3109
|
-
|
|
3110
|
-
return ng;
|
|
3111
|
-
}
|
|
3112
|
-
return null;
|
|
3113
|
-
}
|
|
3114
|
-
|
|
3115
|
-
/**
|
|
3116
|
-
* @param closeKey {string}
|
|
3117
|
-
* @param exactKey {string}
|
|
3118
|
-
* @param goUp {boolean}
|
|
3119
|
-
* @returns {NodeGroup} */
|
|
3120
|
-
findAndDeleteClose(closeKey, exactKey, goUp=true) {
|
|
3121
|
-
let ng = this.nodeGroupsAvailable.delete(closeKey);
|
|
3122
|
-
if (ng) {
|
|
3123
|
-
|
|
3124
|
-
// We matched on a new key, so delete the old exactKey.
|
|
3125
|
-
let exactNg = this.nodeGroupsAvailable.delete(ng.exactKey, ng);
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
ng.inUse = true;
|
|
3132
|
-
if (goUp) {
|
|
3133
|
-
let ng2 = ng;
|
|
3134
|
-
|
|
3135
|
-
// We borrowed a node from another node group so make sure its parent isn't still an exact match.
|
|
3136
|
-
while (ng2 = ng2?.parentPath?.parentNg) {
|
|
3137
|
-
if (!ng2.inUse) {
|
|
3138
|
-
ng2.inUse = true; // Might speed it up slightly?
|
|
3139
|
-
let success = this.nodeGroupsAvailable.delete(ng2.exactKey, ng2);
|
|
3140
|
-
|
|
3141
2350
|
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
2351
|
+
}
|
|
2352
|
+
// scripts
|
|
2353
|
+
if (this.options?.scripts !== false) {
|
|
2354
|
+
for (let path of shell.scripts) {
|
|
2355
|
+
if (pathOffset)
|
|
2356
|
+
path = path.slice(0, -pathOffset);
|
|
2357
|
+
let script = resolveNodePath(root, path);
|
|
2358
|
+
eval(script.textContent);
|
|
3146
2359
|
}
|
|
3147
2360
|
}
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
3148
2364
|
|
|
3149
|
-
// Recursively mark all child NodeGroups as in-use.
|
|
3150
|
-
// We actually DON't want to do this becuse applyExprs is going to swap out the child NodeGroups
|
|
3151
|
-
// and mark them as in-use as it goes.
|
|
3152
|
-
// that's probably why uncommenting this causes tests to fail.
|
|
3153
|
-
// for (let path of ng.paths)
|
|
3154
|
-
// for (let childNg of path.nodeGroups)
|
|
3155
|
-
// this.findAndDeleteExact(childNg.exactKey, false, childNg);
|
|
3156
2365
|
|
|
2366
|
+
class RootNodeGroup extends NodeGroup {
|
|
3157
2367
|
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
if (ng.parentPath) ;
|
|
3164
|
-
}
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
return ng;
|
|
3168
|
-
}
|
|
2368
|
+
/**
|
|
2369
|
+
* Root node at the top of the hierarchy.
|
|
2370
|
+
* @type {HTMLElement} */
|
|
2371
|
+
root;
|
|
3169
2372
|
|
|
3170
2373
|
/**
|
|
3171
|
-
*
|
|
3172
|
-
*
|
|
3173
|
-
* @param
|
|
3174
|
-
* @param
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
2374
|
+
*
|
|
2375
|
+
* @param template
|
|
2376
|
+
* @param el
|
|
2377
|
+
* @param options {?object}
|
|
2378
|
+
*/
|
|
2379
|
+
constructor(template, el, options) {
|
|
2380
|
+
super(template);
|
|
3178
2381
|
|
|
3179
|
-
|
|
2382
|
+
this.options = options;
|
|
3180
2383
|
|
|
3181
|
-
|
|
3182
|
-
let
|
|
3183
|
-
if (exact === true) {
|
|
3184
|
-
ng = this.findAndDeleteExact(exactKey);
|
|
2384
|
+
this.rootNg = this;
|
|
2385
|
+
let [fragment, shell] = this.init(template);
|
|
3185
2386
|
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
2387
|
+
// If adding NodeGroup to an element.
|
|
2388
|
+
let offset = 0;
|
|
2389
|
+
let root = fragment; // TODO: Rename so it's not confused with this.root.
|
|
2390
|
+
if (el) {
|
|
2391
|
+
|
|
2392
|
+
// Save slot children
|
|
2393
|
+
let slotFragment;
|
|
2394
|
+
if (el.childNodes.length) {
|
|
2395
|
+
slotFragment = document.createDocumentFragment();
|
|
2396
|
+
slotFragment.append(...el.childNodes);
|
|
3189
2397
|
}
|
|
3190
|
-
}
|
|
3191
2398
|
|
|
3192
|
-
|
|
3193
|
-
else {
|
|
3194
|
-
// We don't need to delete the exact match bc it's already been deleted in the prev pass.
|
|
3195
|
-
let closeKey = template.getCloseKey();
|
|
3196
|
-
ng = createForWatch ? null : this.findAndDeleteClose(closeKey, exactKey);
|
|
2399
|
+
this.root = el;
|
|
3197
2400
|
|
|
3198
|
-
//
|
|
3199
|
-
if (
|
|
3200
|
-
|
|
3201
|
-
// Temporary for debugging:
|
|
3202
|
-
if (window.debug && !window.ng)
|
|
3203
|
-
window.ng = ng;
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
ng.applyExprs(template.exprs);
|
|
2401
|
+
// If el should replace the root node of the fragment.
|
|
2402
|
+
if (isReplaceEl(fragment, el)) {
|
|
2403
|
+
el.append(...fragment.children[0].childNodes);
|
|
3207
2404
|
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
2405
|
+
// Copy attributes
|
|
2406
|
+
for (let attrib of fragment.children[0].attributes)
|
|
2407
|
+
if (!el.hasAttribute(attrib.name))
|
|
2408
|
+
el.setAttribute(attrib.name, attrib.value);
|
|
3211
2409
|
|
|
3212
|
-
|
|
2410
|
+
// Go one level deeper into all of shell's paths.
|
|
2411
|
+
offset = 1;
|
|
2412
|
+
}
|
|
3213
2413
|
else {
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
2414
|
+
let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
|
|
2415
|
+
if (!isEmpty)
|
|
2416
|
+
el.append(...fragment.childNodes);
|
|
2417
|
+
}
|
|
3220
2418
|
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
2419
|
+
// Setup slots
|
|
2420
|
+
if (slotFragment) {
|
|
2421
|
+
for (let slot of el.querySelectorAll('slot[name]')) {
|
|
2422
|
+
let name = slot.getAttribute('name');
|
|
2423
|
+
if (name) {
|
|
2424
|
+
let slotChildren = slotFragment.querySelectorAll(`[slot='${name}']`);
|
|
2425
|
+
slot.append(...slotChildren);
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
let unamedSlot = el.querySelector('slot:not([name])');
|
|
2429
|
+
if (unamedSlot)
|
|
2430
|
+
unamedSlot.append(slotFragment);
|
|
3229
2431
|
else
|
|
3230
|
-
|
|
2432
|
+
el.append(slotFragment);
|
|
3231
2433
|
}
|
|
2434
|
+
|
|
2435
|
+
root = el;
|
|
2436
|
+
this.startNode = el;
|
|
2437
|
+
this.endNode = el;
|
|
3232
2438
|
}
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
2439
|
+
else {
|
|
2440
|
+
let singleEl = getSingleEl(fragment);
|
|
2441
|
+
this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
|
|
2442
|
+
if (singleEl) {
|
|
2443
|
+
root = singleEl;
|
|
2444
|
+
offset = 1;
|
|
2445
|
+
}
|
|
3240
2446
|
}
|
|
3241
2447
|
|
|
2448
|
+
this.updatePaths(root, shell.paths, offset);
|
|
3242
2449
|
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
2450
|
+
this.activateEmbeds(root, shell, offset);
|
|
2451
|
+
|
|
2452
|
+
// Apply exprs
|
|
2453
|
+
this.applyExprs(template.exprs);
|
|
3246
2454
|
}
|
|
2455
|
+
}
|
|
3247
2456
|
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
2457
|
+
function getSingleEl(fragment) {
|
|
2458
|
+
let nonempty = [];
|
|
2459
|
+
for (let n of fragment.childNodes) {
|
|
2460
|
+
if (n.nodeType === 1 || n.nodeType === 3 && n.textContent.trim().length) {
|
|
2461
|
+
if (nonempty.length)
|
|
2462
|
+
return null;
|
|
2463
|
+
nonempty.push(n);
|
|
3255
2464
|
}
|
|
3256
|
-
|
|
2465
|
+
}
|
|
2466
|
+
return nonempty[0];
|
|
2467
|
+
}
|
|
3257
2468
|
|
|
3258
|
-
|
|
3259
|
-
|
|
2469
|
+
/**
|
|
2470
|
+
* Does the fragment have one child that's an element matching the tagname of el?
|
|
2471
|
+
* @param fragment {DocumentFragment}
|
|
2472
|
+
* @param el {HTMLElement}
|
|
2473
|
+
* @returns {boolean} */
|
|
2474
|
+
function isReplaceEl(fragment, el) {
|
|
2475
|
+
return el.tagName.includes('-')
|
|
2476
|
+
&& fragment.children.length===1
|
|
2477
|
+
&& fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') === el.tagName;
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
/**
|
|
2481
|
+
* The html strings and evaluated expressions from an html tagged template.
|
|
2482
|
+
* A unique Template is created for each item in a loop.
|
|
2483
|
+
* Although the reference to the html strings is shared among templates. */
|
|
2484
|
+
class Template {
|
|
3260
2485
|
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
}
|
|
2486
|
+
/** @type {(Template|string|function)|(Template|string|function)[]} Evaulated expressions. */
|
|
2487
|
+
exprs = []
|
|
3264
2488
|
|
|
2489
|
+
/** @type {string[]} */
|
|
2490
|
+
html = [];
|
|
3265
2491
|
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
clearSubscribers = false;
|
|
2492
|
+
/** @type {Array} Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
|
|
2493
|
+
hashedFields;
|
|
3269
2494
|
|
|
3270
2495
|
/**
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
*
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
2496
|
+
* @deprecated
|
|
2497
|
+
* @type {ExprPath} Used with forEach() from watch.js
|
|
2498
|
+
* Set in ExprPath.apply() */
|
|
2499
|
+
parentPath;
|
|
2500
|
+
|
|
2501
|
+
/** @type {NodeGroup} */
|
|
2502
|
+
nodeGroup;
|
|
3278
2503
|
|
|
3279
|
-
|
|
3280
2504
|
/**
|
|
3281
|
-
* @
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
* @type {MultiValueMap<string, Subscriber>} */
|
|
3285
|
-
pathToLoopInfo = new MultiValueMap();
|
|
3286
|
-
|
|
2505
|
+
* @type {string[][]} */
|
|
2506
|
+
paths = [];
|
|
2507
|
+
|
|
3287
2508
|
/**
|
|
3288
|
-
*
|
|
3289
|
-
* @
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
2509
|
+
*
|
|
2510
|
+
* @param htmlStrings {string[]}
|
|
2511
|
+
* @param exprs {*[]} */
|
|
2512
|
+
constructor(htmlStrings, exprs) {
|
|
2513
|
+
this.html = htmlStrings;
|
|
2514
|
+
this.exprs = exprs;
|
|
2515
|
+
|
|
2516
|
+
//this.trace = new Error().stack.split(/\n/g)
|
|
2517
|
+
|
|
2518
|
+
// Multiple templates can share the same htmlStrings array.
|
|
2519
|
+
//this.hashedFields = [getObjectId(htmlStrings), exprs]
|
|
2520
|
+
|
|
2521
|
+
|
|
3298
2522
|
}
|
|
3299
|
-
|
|
3300
2523
|
|
|
3301
2524
|
/**
|
|
3302
|
-
*
|
|
3303
|
-
*
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
if (!ngm) {
|
|
3308
|
-
ngm = new NodeGroupManager(rootEl);
|
|
3309
|
-
nodeGroupManagers.set(rootEl, ngm);
|
|
3310
|
-
}
|
|
2525
|
+
* Called by JSON.serialize when it encounters a Template.
|
|
2526
|
+
* This prevents the hashed version from being too large. */
|
|
2527
|
+
toJSON() {
|
|
2528
|
+
if (!this.hashedFields)
|
|
2529
|
+
this.hashedFields = [getObjectId(this.html), this.exprs];
|
|
3311
2530
|
|
|
3312
|
-
return
|
|
2531
|
+
return this.hashedFields
|
|
3313
2532
|
}
|
|
3314
2533
|
|
|
2534
|
+
/**
|
|
2535
|
+
* Render the main template, which may indirectly call renderTemplate() to create children.
|
|
2536
|
+
* @param el {HTMLElement}
|
|
2537
|
+
* @param options {RenderOptions}
|
|
2538
|
+
* @return {?DocumentFragment|HTMLElement} */
|
|
2539
|
+
render(el=null, options={}) {
|
|
2540
|
+
let ng;
|
|
2541
|
+
let standalone = !el;
|
|
2542
|
+
let firstTime = false;
|
|
3315
2543
|
|
|
3316
|
-
|
|
3317
|
-
|
|
2544
|
+
// Rendering a standalone element.
|
|
2545
|
+
// TODO: figure out when to not use RootNodeGroup
|
|
2546
|
+
if (standalone) {
|
|
2547
|
+
ng = new RootNodeGroup(this, null, options);
|
|
2548
|
+
el = ng.getRootNode();
|
|
2549
|
+
Globals.nodeGroups.set(el, ng);
|
|
2550
|
+
firstTime = true;
|
|
2551
|
+
}
|
|
2552
|
+
else {
|
|
2553
|
+
ng = Globals.nodeGroups.get(el);
|
|
2554
|
+
if (!ng) {
|
|
2555
|
+
ng = new RootNodeGroup(this, el, options);
|
|
2556
|
+
Globals.nodeGroups.set(el, ng);
|
|
2557
|
+
firstTime = true;
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
3318
2560
|
|
|
3319
|
-
|
|
2561
|
+
// Creating the root nodegroup also renders it.
|
|
2562
|
+
// If we didn't just create it, we need to render it.
|
|
2563
|
+
if (!firstTime) {
|
|
2564
|
+
if (this.html?.length === 1 && !this.html[0])
|
|
2565
|
+
el.innerHTML = ''; // Fast path for empty component.
|
|
2566
|
+
else
|
|
2567
|
+
ng.applyExprs(this.exprs);
|
|
2568
|
+
}
|
|
3320
2569
|
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
* @type {WeakMap<HTMLElement, NodeGroupManager>} */
|
|
3324
|
-
let nodeGroupManagers = new WeakMap();
|
|
2570
|
+
return el;
|
|
2571
|
+
}
|
|
3325
2572
|
|
|
2573
|
+
getExactKey() {
|
|
2574
|
+
if (!this.exactKey)
|
|
2575
|
+
this.exactKey = getObjectHash(this); // calls this.toJSON().
|
|
2576
|
+
return this.exactKey;
|
|
2577
|
+
}
|
|
3326
2578
|
|
|
2579
|
+
getCloseKey() {
|
|
2580
|
+
if (!this.closeKey)
|
|
2581
|
+
this.closeKey = '@'+this.toJSON()[0];
|
|
2582
|
+
// Use the joined html when debugging? But it breaks some tests.
|
|
2583
|
+
//return '@'+this.html.join('|')
|
|
3327
2584
|
|
|
3328
|
-
|
|
3329
|
-
constructor(loopTemplate, itemTransformer) {
|
|
3330
|
-
this.template = loopTemplate;
|
|
3331
|
-
this.itemTransformer = itemTransformer;
|
|
2585
|
+
return this.closeKey;
|
|
3332
2586
|
}
|
|
3333
|
-
}
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
|
|
2590
|
+
/**
|
|
2591
|
+
* @typedef {Object} RenderOptions
|
|
2592
|
+
* @property {boolean=} styles - Replace :host in style tags to scope them locally.
|
|
2593
|
+
* @property {boolean=} scripts - Execute script tags.
|
|
2594
|
+
* @property {boolean=} ids - Create references to elements with id or data-id attributes.
|
|
2595
|
+
* @property {?boolean} render - Deprecated.
|
|
2596
|
+
* Used only when options are given to a class super constructor inheriting from Solarite.
|
|
2597
|
+
* True to call render() immediately in super constructor.
|
|
2598
|
+
* False to automatically call render() at all.
|
|
2599
|
+
* Undefined (default) to call render() when added to the DOM, unless already rendered.
|
|
2600
|
+
*/
|
|
3334
2601
|
|
|
3335
2602
|
/**
|
|
3336
2603
|
* Convert strings to HTMLNodes.
|
|
@@ -3345,133 +2612,147 @@ class LoopInfo {
|
|
|
3345
2612
|
* 5. TODO: list more
|
|
3346
2613
|
*
|
|
3347
2614
|
* Currently supported:
|
|
3348
|
-
* 1. r`<b
|
|
2615
|
+
* 1. r(el, options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
|
|
2616
|
+
* 2. r(el, template, ?options) // Render the Template created by #1 to element.
|
|
3349
2617
|
*
|
|
3350
|
-
*
|
|
3351
|
-
* 3. r(el, options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
|
|
2618
|
+
* 3. r`<b>Hello</b> ${'World'}!` // Create Template that can later be used to create nodes.
|
|
3352
2619
|
*
|
|
3353
2620
|
* 4. r('Hello'); // Create single text node.
|
|
3354
2621
|
* 5. r('<b>Hello</b>'); // Create single HTMLElement
|
|
3355
2622
|
* 6. r('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
|
|
3356
|
-
* 7. r()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which
|
|
2623
|
+
* 7. r()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which
|
|
2624
|
+
* // includes properly handling nested components and r`` sub-expressions.
|
|
3357
2625
|
* 8. r(template) // Render Template created by #1.
|
|
3358
|
-
* 9. r(() => r`<b>Hello</b>`); // Create dynamic element that has a render() function.
|
|
3359
2626
|
*
|
|
3360
|
-
*
|
|
3361
|
-
*
|
|
2627
|
+
* 9. r({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
|
|
2628
|
+
*
|
|
2629
|
+
* @param htmlStrings {?HTMLElement|string|string[]|function():Template|{render:function()}}
|
|
2630
|
+
* @param exprs {*[]|string|Template|Object}
|
|
3362
2631
|
* @return {Node|HTMLElement|Template} */
|
|
3363
2632
|
function r(htmlStrings=undefined, ...exprs) {
|
|
3364
2633
|
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
}
|
|
3385
|
-
}
|
|
3386
|
-
|
|
3387
|
-
// 3
|
|
3388
|
-
else if (!exprs.length || exprs[0]) {
|
|
3389
|
-
if (parent.shadowRoot)
|
|
3390
|
-
parent.innerHTML = ''; // Remove shadowroot. TODO: This could mess up paths?
|
|
3391
|
-
|
|
3392
|
-
let options = exprs[0];
|
|
3393
|
-
return (htmlStrings, ...exprs) => {
|
|
3394
|
-
rendered.add(parent);
|
|
3395
|
-
let template = r(htmlStrings, ...exprs);
|
|
3396
|
-
let ngm = NodeGroupManager.get(parent);
|
|
3397
|
-
return ngm.render(template, options);
|
|
3398
|
-
}
|
|
3399
|
-
}
|
|
3400
|
-
|
|
3401
|
-
// null for expr[0], remove whole element.
|
|
3402
|
-
else {
|
|
3403
|
-
let ngm = NodeGroupManager.get(parent);
|
|
3404
|
-
ngm.render(null, exprs[1]);
|
|
3405
|
-
}
|
|
3406
|
-
}
|
|
3407
|
-
|
|
3408
|
-
else if (typeof htmlStrings === 'string' || htmlStrings instanceof String) {
|
|
3409
|
-
// If it starts with a string, trim both ends.
|
|
3410
|
-
// TODO: Also trim if it ends with whitespace?
|
|
3411
|
-
if (htmlStrings.match(/^\s^</))
|
|
3412
|
-
htmlStrings = htmlStrings.trim();
|
|
3413
|
-
|
|
3414
|
-
// We create a new one each time because otherwise
|
|
3415
|
-
// the returned fragment will have its content replaced by a subsequent call.
|
|
3416
|
-
let templateEl = document.createElement('template');
|
|
3417
|
-
templateEl.innerHTML = htmlStrings;
|
|
3418
|
-
|
|
3419
|
-
// 4+5. Return Node if there's one child.
|
|
3420
|
-
if (templateEl.content.childNodes.length === 1)
|
|
3421
|
-
return templateEl.content.firstChild;
|
|
3422
|
-
|
|
3423
|
-
// 6. Otherwise return DocumentFragment.
|
|
3424
|
-
return templateEl.content;
|
|
3425
|
-
}
|
|
3426
|
-
|
|
3427
|
-
// 7. Create a static element
|
|
3428
|
-
else if (htmlStrings === undefined) {
|
|
3429
|
-
return (htmlStrings, ...exprs) => {
|
|
3430
|
-
//rendered.add(parent)
|
|
3431
|
-
let template = r(htmlStrings, ...exprs);
|
|
3432
|
-
return template.toNode();
|
|
3433
|
-
}
|
|
3434
|
-
}
|
|
3435
|
-
|
|
3436
|
-
// 8.
|
|
3437
|
-
else if (htmlStrings instanceof Template) {
|
|
3438
|
-
let ngm = new NodeGroupManager();
|
|
3439
|
-
return ngm.render(htmlStrings);
|
|
3440
|
-
}
|
|
3441
|
-
|
|
3442
|
-
// 9. Create dynamic element with render() function.
|
|
3443
|
-
else if (typeof htmlStrings === 'function') {
|
|
3444
|
-
let getTemplate = htmlStrings;
|
|
3445
|
-
let template = getTemplate();
|
|
3446
|
-
|
|
3447
|
-
if (typeof template === 'string')
|
|
3448
|
-
throw new Error(`Please add the "r" prefix before the string "${template}"`)
|
|
3449
|
-
|
|
3450
|
-
let ngm = new NodeGroupManager();
|
|
3451
|
-
template.replaceMode = true;
|
|
3452
|
-
let el = ngm.render(template);
|
|
3453
|
-
|
|
3454
|
-
el.render = (function() {
|
|
3455
|
-
template = getTemplate();
|
|
3456
|
-
ngm.render(template);
|
|
3457
|
-
}).bind(el);
|
|
3458
|
-
|
|
3459
|
-
return el;
|
|
3460
|
-
}
|
|
3461
|
-
else
|
|
3462
|
-
throw new Error('Unsupported arguments.')
|
|
3463
|
-
}
|
|
2634
|
+
// TODO: Make this a more flat if/else and call other functions for the logic.
|
|
2635
|
+
if (htmlStrings instanceof Node) {
|
|
2636
|
+
let parent = htmlStrings, template = exprs[0];
|
|
2637
|
+
|
|
2638
|
+
// 1
|
|
2639
|
+
if (!(exprs[0] instanceof Template)) {
|
|
2640
|
+
if (parent.shadowRoot)
|
|
2641
|
+
parent.innerHTML = ''; // Remove shadowroot. TODO: This could mess up paths?
|
|
2642
|
+
|
|
2643
|
+
let options = exprs[0];
|
|
2644
|
+
|
|
2645
|
+
// Return a tagged template function that applies the tagged themplate to parent.
|
|
2646
|
+
let taggedTemplate = (htmlStrings, ...exprs) => {
|
|
2647
|
+
Globals.rendered.add(parent);
|
|
2648
|
+
let template = new Template(htmlStrings, exprs);
|
|
2649
|
+
return template.render(parent, options);
|
|
2650
|
+
};
|
|
2651
|
+
return taggedTemplate;
|
|
2652
|
+
}
|
|
3464
2653
|
|
|
2654
|
+
// 2. Render template created by #4 to element.
|
|
2655
|
+
else if (exprs[0] instanceof Template) {
|
|
2656
|
+
let options = exprs[1];
|
|
2657
|
+
template.render(parent, options);
|
|
3465
2658
|
|
|
2659
|
+
// Append on the first go.
|
|
2660
|
+
if (!parent.childNodes.length && this) {
|
|
2661
|
+
// TODO: Is this ever executed?
|
|
2662
|
+
debugger;
|
|
2663
|
+
parent.append(this.rootNg.getParentNode());
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
3466
2666
|
|
|
3467
2667
|
|
|
3468
2668
|
|
|
2669
|
+
// null for expr[0], remove whole element.
|
|
2670
|
+
// This path never happens?
|
|
2671
|
+
else {
|
|
2672
|
+
throw new Error('unsupported');
|
|
2673
|
+
//let ngm = NodeGroupManager.get(parent);
|
|
2674
|
+
//ngm.render(null, exprs[1])
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
3469
2677
|
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
2678
|
+
// 3. Path if used as a template tag.
|
|
2679
|
+
else if (Array.isArray(htmlStrings)) {
|
|
2680
|
+
return new Template(htmlStrings, exprs);
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
else if (typeof htmlStrings === 'string' || htmlStrings instanceof String) {
|
|
2684
|
+
// If it starts with a string, trim both ends.
|
|
2685
|
+
// TODO: Also trim if it ends with whitespace?
|
|
2686
|
+
if (htmlStrings.match(/^\s^</))
|
|
2687
|
+
htmlStrings = htmlStrings.trim();
|
|
2688
|
+
|
|
2689
|
+
// We create a new one each time because otherwise
|
|
2690
|
+
// the returned fragment will have its content replaced by a subsequent call.
|
|
2691
|
+
let templateEl = document.createElement('template');
|
|
2692
|
+
templateEl.innerHTML = htmlStrings;
|
|
2693
|
+
|
|
2694
|
+
// 4+5. Return Node if there's one child.
|
|
2695
|
+
let relevantNodes = Util.trimEmptyNodes(templateEl.content.childNodes);
|
|
2696
|
+
if (relevantNodes.length === 1)
|
|
2697
|
+
return relevantNodes[0];
|
|
2698
|
+
|
|
2699
|
+
// 6. Otherwise return DocumentFragment.
|
|
2700
|
+
return templateEl.content;
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
// 7. Create a static element
|
|
2704
|
+
else if (htmlStrings === undefined) {
|
|
2705
|
+
return (htmlStrings, ...exprs) => {
|
|
2706
|
+
//Globals.rendered.add(parent)
|
|
2707
|
+
let template = r(htmlStrings, ...exprs);
|
|
2708
|
+
return template.render();
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
// 8.
|
|
2713
|
+
else if (htmlStrings instanceof Template) {
|
|
2714
|
+
return htmlStrings.render();
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
|
|
2718
|
+
// 9. Create dynamic element with render() function.
|
|
2719
|
+
else if (typeof htmlStrings === 'object') {
|
|
2720
|
+
let obj = htmlStrings;
|
|
2721
|
+
|
|
2722
|
+
// Special rebound render path, called by normal path.
|
|
2723
|
+
if (Globals.objToEl.has(obj)) {
|
|
2724
|
+
return function(...args) {
|
|
2725
|
+
let template = r(...args);
|
|
2726
|
+
let el = template.render();
|
|
2727
|
+
Globals.objToEl.set(obj, el);
|
|
2728
|
+
}.bind(obj);
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
// Normal path
|
|
2732
|
+
else {
|
|
2733
|
+
Globals.objToEl.set(obj, null);
|
|
2734
|
+
obj.render(); // Calls the Special rebound render path above, when the render function calls r(this)
|
|
2735
|
+
let el = Globals.objToEl.get(obj);
|
|
2736
|
+
Globals.objToEl.delete(obj);
|
|
2737
|
+
|
|
2738
|
+
for (let name in obj)
|
|
2739
|
+
if (typeof obj[name] === 'function')
|
|
2740
|
+
el[name] = obj[name].bind(el);
|
|
2741
|
+
else
|
|
2742
|
+
el[name] = obj[name];
|
|
2743
|
+
|
|
2744
|
+
return el;
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
else
|
|
2749
|
+
throw new Error('Unsupported arguments.')
|
|
2750
|
+
}
|
|
3474
2751
|
|
|
2752
|
+
//import {watchGet, watchSet} from "./watch.js";
|
|
2753
|
+
|
|
2754
|
+
|
|
2755
|
+
|
|
3475
2756
|
function defineClass(Class, tagName, extendsTag) {
|
|
3476
2757
|
if (!customElements.getName(Class)) { // If not previously defined.
|
|
3477
2758
|
tagName = tagName || camelToDashes(Class.name);
|
|
@@ -3486,14 +2767,9 @@ function defineClass(Class, tagName, extendsTag) {
|
|
|
3486
2767
|
}
|
|
3487
2768
|
}
|
|
3488
2769
|
|
|
3489
|
-
/**
|
|
3490
|
-
* @type {Object<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
|
|
3491
|
-
let elementClasses = {};
|
|
3492
2770
|
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
*/
|
|
3496
|
-
let connected = new WeakSet();
|
|
2771
|
+
|
|
2772
|
+
|
|
3497
2773
|
|
|
3498
2774
|
/**
|
|
3499
2775
|
* Create a version of the Solarite class that extends from the given tag name.
|
|
@@ -3502,8 +2778,15 @@ let connected = new WeakSet();
|
|
|
3502
2778
|
* 2. Calls render() when added to the DOM, if it hasn't been called already.
|
|
3503
2779
|
* 3. Child elements are added before constructor is called. But they're also passed to the constructor.
|
|
3504
2780
|
* 4. We can use this.html = r`...` to set html.
|
|
3505
|
-
* 5. We have the onConnect, onFirstConnect, and onDisconnect methods.
|
|
2781
|
+
* 5. We have the onConnect, onFirstConnect, and onDisconnect methods.
|
|
2782
|
+
* Can't figure out how to have these work standalone though, and still be synchronous.
|
|
3506
2783
|
* 6. Can we extend from other element types like TR?
|
|
2784
|
+
* 7. Shows default text if render() function isn't defined.
|
|
2785
|
+
*
|
|
2786
|
+
* Advantages to inheriting from HTMLElement
|
|
2787
|
+
* 1. Minimization won't break when it renames the Class and we call customElements.define() on the wrong name.
|
|
2788
|
+
* 2. We can inherit from things like HTMLTableRowElement directly.
|
|
2789
|
+
* 3. There's less magic, since everyone is familiar with defining custom elements.
|
|
3507
2790
|
*
|
|
3508
2791
|
* @param extendsTag {?string}
|
|
3509
2792
|
* @return {Class} */
|
|
@@ -3513,10 +2796,10 @@ function createSolarite(extendsTag=null) {
|
|
|
3513
2796
|
if (extendsTag && !extendsTag.includes('-')) {
|
|
3514
2797
|
extendsTag = extendsTag.toLowerCase();
|
|
3515
2798
|
|
|
3516
|
-
BaseClass = elementClasses[extendsTag];
|
|
2799
|
+
BaseClass = Globals.elementClasses[extendsTag];
|
|
3517
2800
|
if (!BaseClass) { // TODO: Use Cache
|
|
3518
2801
|
BaseClass = document.createElement(extendsTag).constructor;
|
|
3519
|
-
elementClasses[extendsTag] = BaseClass;
|
|
2802
|
+
Globals.elementClasses[extendsTag] = BaseClass;
|
|
3520
2803
|
}
|
|
3521
2804
|
}
|
|
3522
2805
|
|
|
@@ -3552,26 +2835,24 @@ function createSolarite(extendsTag=null) {
|
|
|
3552
2835
|
constructor(options={}) {
|
|
3553
2836
|
super();
|
|
3554
2837
|
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
2838
|
// TODO: Is options.render ever used?
|
|
3558
2839
|
if (options.render===true)
|
|
3559
2840
|
this.render();
|
|
3560
2841
|
|
|
3561
2842
|
else if (options.render===false)
|
|
3562
|
-
rendered.add(this); // Don't render on connectedCallback()
|
|
2843
|
+
Globals.rendered.add(this); // Don't render on connectedCallback()
|
|
3563
2844
|
|
|
3564
2845
|
// Add children before constructor code executes.
|
|
3565
2846
|
// PendingChildren is setup in NodeGroup.createNewComponent()
|
|
3566
2847
|
// TODO: Match named slots.
|
|
3567
|
-
let ch =
|
|
2848
|
+
let ch = Globals.pendingChildren.pop();
|
|
3568
2849
|
if (ch)
|
|
3569
2850
|
(this.querySelector('slot') || this).append(...ch);
|
|
3570
2851
|
|
|
3571
|
-
|
|
2852
|
+
/** @deprecated */
|
|
3572
2853
|
Object.defineProperty(this, 'html', {
|
|
3573
2854
|
set(html) {
|
|
3574
|
-
rendered.add(this);
|
|
2855
|
+
Globals.rendered.add(this);
|
|
3575
2856
|
if (typeof html === 'string') {
|
|
3576
2857
|
console.warn("Assigning to this.html without the r template prefix.");
|
|
3577
2858
|
this.innerHTML = html;
|
|
@@ -3594,7 +2875,7 @@ function createSolarite(extendsTag=null) {
|
|
|
3594
2875
|
/**
|
|
3595
2876
|
* Call render() only if it hasn't already been called. */
|
|
3596
2877
|
renderFirstTime() {
|
|
3597
|
-
if (!rendered.has(this) && this.render)
|
|
2878
|
+
if (!Globals.rendered.has(this) && this.render)
|
|
3598
2879
|
this.render();
|
|
3599
2880
|
}
|
|
3600
2881
|
|
|
@@ -3602,8 +2883,8 @@ function createSolarite(extendsTag=null) {
|
|
|
3602
2883
|
* Called automatically by the browser. */
|
|
3603
2884
|
connectedCallback() {
|
|
3604
2885
|
this.renderFirstTime();
|
|
3605
|
-
if (!connected.has(this)) {
|
|
3606
|
-
connected.add(this);
|
|
2886
|
+
if (!Globals.connected.has(this)) {
|
|
2887
|
+
Globals.connected.add(this);
|
|
3607
2888
|
this.onFirstConnect();
|
|
3608
2889
|
}
|
|
3609
2890
|
this.onConnect();
|
|
@@ -3618,115 +2899,16 @@ function createSolarite(extendsTag=null) {
|
|
|
3618
2899
|
defineClass(this, tagName, extendsTag);
|
|
3619
2900
|
}
|
|
3620
2901
|
|
|
3621
|
-
|
|
3622
|
-
renderWatched() {
|
|
3623
|
-
let ngm = NodeGroupManager.get(this);
|
|
3624
|
-
|
|
3625
|
-
let nodeGroupUpdates = [];
|
|
3626
|
-
|
|
3627
|
-
for (let change of ngm.changes) {
|
|
3628
|
-
if (change.action === 'set') {
|
|
3629
|
-
for (let transformerInfo of change.transformerInfo) {
|
|
3630
|
-
|
|
3631
|
-
let oldHash = transformerInfo.hash;
|
|
3632
|
-
|
|
3633
|
-
let newObj = delve(watchSet(transformerInfo.path[0]), transformerInfo.path.slice(1));
|
|
3634
|
-
let newTemplate = transformerInfo.transformer(newObj);
|
|
3635
|
-
let newHash = getObjectHash(newTemplate);
|
|
3636
|
-
let ngs = [...ngm.nodeGroupsAvailable.data[oldHash]];
|
|
3637
|
-
for (let ng of ngs) {
|
|
3638
|
-
nodeGroupUpdates.push([ng, oldHash, newHash, newTemplate.exprs, transformerInfo]);
|
|
3639
|
-
}
|
|
3640
|
-
}
|
|
3641
|
-
}
|
|
3642
|
-
|
|
3643
|
-
else if (change.action === 'delete') {
|
|
3644
|
-
for (let hash of change.value) {
|
|
3645
|
-
let ngs = [...ngm.nodeGroupsAvailable.getAll(hash)]; // deletes from nodeGroupsAvailable.
|
|
3646
|
-
|
|
3647
|
-
for (let ng of ngs) {
|
|
3648
|
-
if (ng.parentPath)
|
|
3649
|
-
ng.parentPath.clearNodesCache();
|
|
3650
|
-
|
|
3651
|
-
for (let node of ng.getNodes())
|
|
3652
|
-
node.remove();
|
|
3653
|
-
|
|
3654
|
-
// TODO: Update ancestor NodeGroup exactKeys
|
|
3655
|
-
}
|
|
3656
|
-
}
|
|
3657
|
-
}
|
|
3658
|
-
else if (change.action === 'insert') {
|
|
3659
|
-
|
|
3660
|
-
let beforeNg = change.beforeTemplate ? ngm.getNodeGroup(change.beforeTemplate, true) : null;
|
|
3661
|
-
let arrayPath = [change.root, ...change.path];
|
|
3662
|
-
|
|
3663
|
-
// Get anchor so we can use it to get the parent
|
|
3664
|
-
// TODO: Should this be watchGet(change.root) ?
|
|
3665
|
-
for (let loopInfo of ngm.getLoopInfo([change.root, ...change.path.slice(0, -1)])) {
|
|
3666
|
-
|
|
3667
|
-
// Change.extra is aTemplate telling us where to insert before.
|
|
3668
|
-
let beforeNode = beforeNg?.startNode || loopInfo.template.parentPath.nodeMarker;
|
|
3669
|
-
|
|
3670
|
-
// Loop over every item added to the array.
|
|
3671
|
-
let i = 0; // TODO: How to get real insert index.
|
|
3672
|
-
for (let obj of change.value) {
|
|
3673
|
-
|
|
3674
|
-
// Same logic as forEach() function.
|
|
3675
|
-
|
|
3676
|
-
let callback = loopInfo.itemTransformer;
|
|
3677
|
-
let path = [...arrayPath.slice(0, -1), (arrayPath.at(-1) * 1 + i) + ''];
|
|
3678
|
-
|
|
3679
|
-
// Shortened logic found in watchGet(), but not any faster?
|
|
3680
|
-
// the watchSet() is what makes this slower!
|
|
3681
|
-
// let obj = delve(watchSet(path[0]), path.slice(1));
|
|
3682
|
-
// let template = callback(obj);
|
|
3683
|
-
// let serializedPath = serializePath(path);
|
|
3684
|
-
// pathToTransformer.add(serializedPath, new TransformerInfo(path, callback, template)); // Uses a Set() to ensure no duplicates.
|
|
3685
|
-
|
|
3686
|
-
let template = watchGet(path, callback);
|
|
3687
|
-
i++;
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
//let template = loopInfo.itemTransformer(obj); // What if it takes more than one obj argument?
|
|
3691
|
-
|
|
3692
|
-
// Create new NodeGroup
|
|
3693
|
-
let ng = ngm.getNodeGroup(template, false, true);
|
|
3694
|
-
ng.parentPath = beforeNg?.parentPath || loopInfo.template.parentPath;
|
|
3695
|
-
|
|
3696
|
-
for (let node of ng.getNodes())
|
|
3697
|
-
beforeNode.parentNode.insertBefore(node, beforeNode);
|
|
3698
|
-
|
|
3699
|
-
if (ng.parentPath) // This check is needed for the forEachSpliceInsert test, but why?
|
|
3700
|
-
ng.parentPath.clearNodesCache();
|
|
3701
|
-
}
|
|
3702
|
-
|
|
3703
|
-
// TODO: Update ancestor NodeGroup exactKeys
|
|
3704
|
-
}
|
|
3705
|
-
}
|
|
3706
|
-
}
|
|
3707
|
-
|
|
3708
|
-
// Update them all at once, that way we can reassign the same value twice.
|
|
3709
|
-
for (let [ng, oldHash, newHash, exprs, ti] of nodeGroupUpdates) {
|
|
3710
|
-
ng.applyExprs(exprs);
|
|
3711
|
-
ngm.nodeGroupsAvailable.data[oldHash].delete(ng);
|
|
3712
|
-
ng.exactKey = ti.hash = newHash;
|
|
3713
|
-
ngm.nodeGroupsAvailable.add(ng.exactKey, ng); // Add back to Map with new key.
|
|
3714
|
-
}
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
ngm.changes = [];
|
|
3718
|
-
|
|
3719
|
-
return []; // TODO
|
|
3720
|
-
}
|
|
3721
2902
|
|
|
3722
|
-
/**
|
|
3723
|
-
* @deprecated Use the getArg() function instead. */
|
|
3724
|
-
getArg(name, val=null, type=ArgType.String) {
|
|
3725
|
-
return getArg(this, name, val, type);
|
|
3726
|
-
}
|
|
3727
2903
|
}
|
|
3728
2904
|
}
|
|
3729
2905
|
|
|
2906
|
+
/**
|
|
2907
|
+
* Solarite JavasCript UI library.
|
|
2908
|
+
* MIT License
|
|
2909
|
+
* https://vorticode.github.io/solarite/
|
|
2910
|
+
*/
|
|
2911
|
+
|
|
3730
2912
|
/**
|
|
3731
2913
|
* TODO: The Proxy and the multiple base classes mess up 'instanceof Solarite'
|
|
3732
2914
|
* @type {Node|Class<HTMLElement>|function(tagName:string):Node|Class<HTMLElement>} */
|
|
@@ -3735,6 +2917,10 @@ let Solarite = new Proxy(createSolarite(), {
|
|
|
3735
2917
|
return createSolarite(...args)
|
|
3736
2918
|
}
|
|
3737
2919
|
});
|
|
3738
|
-
|
|
2920
|
+
let getInputValue = Util.getInputValue;
|
|
2921
|
+
|
|
2922
|
+
//Experimental:
|
|
2923
|
+
//export {forEach, watchGet, watchSet} from './watch.js' // old, unfinished
|
|
2924
|
+
//export {watch} from './watch2.js'; // unfinished
|
|
3739
2925
|
|
|
3740
|
-
export { ArgType,
|
|
2926
|
+
export { ArgType, Globals, Solarite, Template, delve, getArg, getInputValue, r };
|