solarite 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +2 -2
- package/build/build.js +1 -1
- package/dist/Solarite-debug.js +1344 -1532
- package/dist/Solarite.js +1298 -1334
- package/dist/Solarite.min.js +3 -3
- package/dist/udomdiff-license.txt +18 -0
- package/docs/index.md +440 -130
- package/docs/js/Playground.js +1 -1
- package/docs/js/codemirror/codemirror6.js +3683 -3206
- package/docs/js/codemirror/themeSolarIce.js +1 -1
- package/docs/js/documentation.js +1 -1
- package/docs/js/ui/CodeEditor.js +183 -45
- package/docs/js/ui/FlexResizer.js +14 -3
- package/docs/js/util/Errors.js +11 -0
- package/docs/media/documentation.css +6 -3
- package/index.html +449 -30
- package/package.json +1 -1
- package/readme.md +1 -1
- package/src/solarite/ExprPath.js +349 -151
- package/src/solarite/Globals.js +43 -1
- package/src/solarite/NodeGroup.js +275 -293
- package/src/solarite/Shell.js +26 -28
- package/src/solarite/Solarite.js +12 -0
- package/src/solarite/Template.js +55 -128
- package/src/solarite/Util.js +131 -7
- package/src/solarite/createSolarite.js +14 -19
- package/src/solarite/getArg.js +1 -1
- package/src/solarite/hash.js +18 -35
- package/src/solarite/r.js +127 -127
- package/src/solarite/watch3.js +18 -11
- package/src/{solarite → unused}/NodeGroupManager.js +81 -109
- package/src/{solarite → unused}/watch.js +1 -1
- package/src/{solarite → unused}/watch2.js +1 -2
- package/src/{solarite → util}/MultiValueMap.js +18 -11
- package/src/util/WeakArray.js +33 -0
- package/tests/Solarite.test.js +862 -167
- package/tests/index.html +4 -2
- package/tests/run.bat +1 -1
- package/deno.lock +0 -176
package/dist/Solarite-debug.js
CHANGED
|
@@ -128,7 +128,7 @@ let delveDontCreate = {};
|
|
|
128
128
|
/**
|
|
129
129
|
* There are three ways to create an instance of a Solarite Component:
|
|
130
130
|
* 1. new ComponentName(); // direct class instantiation
|
|
131
|
-
* 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.
|
|
132
132
|
* 3. <body><component-name></component-name></body> // in the Document html.
|
|
133
133
|
*
|
|
134
134
|
* When created via #3, Solarite has no way to pass attributes as arguments to the constructor. So to make
|
|
@@ -226,32 +226,14 @@ let objectIds = new WeakMap();
|
|
|
226
226
|
|
|
227
227
|
/**
|
|
228
228
|
* @param obj {Object|string|Node}
|
|
229
|
-
* @param prefix
|
|
230
229
|
* @returns {string} */
|
|
231
|
-
function getObjectId(obj
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
//#IFDEV
|
|
236
|
-
// Slower but useful for debugging:
|
|
237
|
-
if (!prefix) {
|
|
238
|
-
if (Array.isArray(obj))
|
|
239
|
-
prefix = 'Array';
|
|
240
|
-
else if (typeof obj === 'function')
|
|
241
|
-
prefix = 'Func';
|
|
242
|
-
else if (typeof obj === 'object')
|
|
243
|
-
prefix = 'Obj';
|
|
244
|
-
}
|
|
245
|
-
//#ENDIF
|
|
246
|
-
|
|
247
|
-
prefix = prefix || '~\f';
|
|
248
|
-
|
|
230
|
+
function getObjectId(obj) {
|
|
249
231
|
// if (typeof obj === 'function')
|
|
250
|
-
// return obj.toString();
|
|
232
|
+
// return obj.toString(); // This fails to detect when a function's bound variables changes.
|
|
251
233
|
|
|
252
234
|
let result = objectIds.get(obj);
|
|
253
235
|
if (!result) { // convert to string, store in result, then add 1 to lastObjectId.
|
|
254
|
-
result =
|
|
236
|
+
result = (lastObjectId++); // We use a unique prefix to ensure it doesn't collide w/ strings not from getObjectId()
|
|
255
237
|
objectIds.set(obj, result);
|
|
256
238
|
}
|
|
257
239
|
return result;
|
|
@@ -264,12 +246,20 @@ function getObjectId(obj, prefix=null) {
|
|
|
264
246
|
* Adding a toJSON method globally on these object prototypes doesn't incur that performance penalty. */
|
|
265
247
|
let isHashing = true;
|
|
266
248
|
function toJSON() {
|
|
267
|
-
//return (isHashing && !Array.isArray(this)) ? getObjectId(this) : this
|
|
268
249
|
return isHashing ? getObjectId(this) : this
|
|
269
250
|
}
|
|
251
|
+
|
|
252
|
+
|
|
270
253
|
// Node.prototype.toJSON = toJSON;
|
|
271
254
|
// Function.prototype.toJSON = toJSON;
|
|
272
|
-
|
|
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
|
+
}
|
|
273
263
|
|
|
274
264
|
/**
|
|
275
265
|
* Get a string that uniquely maps to the values of the given object.
|
|
@@ -278,25 +268,18 @@ function toJSON() {
|
|
|
278
268
|
*
|
|
279
269
|
* Relies on the Node and Function prototypes being overridden above.
|
|
280
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
|
+
*
|
|
281
274
|
* @param obj {*}
|
|
282
275
|
* @returns {string} */
|
|
283
276
|
function getObjectHash(obj) {
|
|
284
|
-
|
|
285
|
-
// Sometimes these get unassigned by Chrome and Brave 119, as well as Firefox, seemingly randomly!
|
|
286
|
-
// The same tests sometimes pass, sometimes fail, even after browser and OS restarts.
|
|
287
|
-
// So we check the assignments on every run of getObjectHash()
|
|
288
|
-
if (Node.prototype.toJSON !== toJSON) {
|
|
289
|
-
Node.prototype.toJSON = toJSON;
|
|
290
|
-
if (Function.prototype.toJSON !== toJSON) // Will it only unmap one but not the other?
|
|
291
|
-
Function.prototype.toJSON = toJSON;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
277
|
let result;
|
|
295
278
|
isHashing = true;
|
|
296
279
|
try {
|
|
297
280
|
result = JSON.stringify(obj);
|
|
298
281
|
}
|
|
299
|
-
catch(e){
|
|
282
|
+
catch(e) {
|
|
300
283
|
result = getObjectHashCircular(obj);
|
|
301
284
|
}
|
|
302
285
|
isHashing = false;
|
|
@@ -304,7 +287,7 @@ function getObjectHash(obj) {
|
|
|
304
287
|
}
|
|
305
288
|
|
|
306
289
|
/**
|
|
307
|
-
*
|
|
290
|
+
* Slower hashing method that supports.
|
|
308
291
|
* @param obj
|
|
309
292
|
* @returns {string} */
|
|
310
293
|
function getObjectHashCircular(obj) {
|
|
@@ -323,85 +306,67 @@ function getObjectHashCircular(obj) {
|
|
|
323
306
|
});
|
|
324
307
|
}
|
|
325
308
|
|
|
326
|
-
|
|
309
|
+
//#IFDEV
|
|
310
|
+
/*@__NO_SIDE_EFFECTS__*/
|
|
311
|
+
function assert(val) {
|
|
312
|
+
if (!val) {
|
|
313
|
+
debugger;
|
|
314
|
+
throw new Error('Assertion failed: ' + val);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
//#ENDIF
|
|
318
|
+
|
|
319
|
+
var Globals = {
|
|
327
320
|
|
|
328
|
-
/**
|
|
329
|
-
|
|
321
|
+
/**
|
|
322
|
+
* Used by NodeGroup.applyComponentExprs() */
|
|
323
|
+
componentHash: new WeakMap(),
|
|
330
324
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (!set) {
|
|
336
|
-
set = new Set();
|
|
337
|
-
data[key] = set;
|
|
338
|
-
}
|
|
339
|
-
set.add(value);
|
|
340
|
-
}
|
|
325
|
+
/**
|
|
326
|
+
* Store which instances of Solarite have already been added to the DOM.
|
|
327
|
+
* @type {WeakSet<HTMLElement>} */
|
|
328
|
+
connected: new WeakSet(),
|
|
341
329
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
330
|
+
/**
|
|
331
|
+
* Elements that have been rendered to by r() at least once.
|
|
332
|
+
* This is used by the Solarite class to know when to call onFirstConnect()
|
|
333
|
+
* @type {WeakSet<HTMLElement>} */
|
|
334
|
+
rendered: new WeakSet(),
|
|
346
335
|
|
|
347
336
|
/**
|
|
348
|
-
*
|
|
349
|
-
|
|
350
|
-
* @param val If specified, make sure we delete this specific value, if a key exists more than once.
|
|
351
|
-
* @returns {*} */
|
|
352
|
-
delete(key, val=undefined) {
|
|
353
|
-
// if (key === '["Html2",[[["Html3",["F1","A"]],["Html3",["F1","B"]]]]]')
|
|
354
|
-
// debugger;
|
|
355
|
-
|
|
356
|
-
let data = this.data;
|
|
357
|
-
// The partialUpdate benchmark shows having this check first makes the function slightly faster.
|
|
358
|
-
// if (!data.hasOwnProperty(key))
|
|
359
|
-
// return undefined;
|
|
337
|
+
* Used by watch3 to see which expressions are being accessed. */
|
|
338
|
+
currentExprPath: [],
|
|
360
339
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if (!set) // slower than pre-check.
|
|
365
|
-
return undefined;
|
|
340
|
+
/**
|
|
341
|
+
* @type {Object<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
|
|
342
|
+
elementClasses: {},
|
|
366
343
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
344
|
+
/**
|
|
345
|
+
* Used by ExprPath.applyEventAttrib()
|
|
346
|
+
* @type {WeakMap<Node, Object<eventName:string, [original:function, bound:function, args:*[]]>>} */
|
|
347
|
+
nodeEvents: new WeakMap(),
|
|
371
348
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
set.delete(result);
|
|
377
|
-
}
|
|
349
|
+
/**
|
|
350
|
+
* Get the RootNodeGroup for an element.
|
|
351
|
+
* @type {WeakMap<HTMLElement, RootNodeGroup>} */
|
|
352
|
+
nodeGroups: new WeakMap(),
|
|
378
353
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
return result;
|
|
384
|
-
}
|
|
354
|
+
/**
|
|
355
|
+
* Used by r() path 9. */
|
|
356
|
+
objToEl: new WeakMap(),
|
|
385
357
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
function assert(val) {
|
|
399
|
-
if (!val) {
|
|
400
|
-
debugger;
|
|
401
|
-
throw new Error('Assertion failed: ' + val);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
//#ENDIF
|
|
358
|
+
pendingChildren: [],
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Elements that are currently rendering via the r() function.
|
|
362
|
+
* @type {WeakSet<HTMLElement>} */
|
|
363
|
+
rendering: new WeakSet(),
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Map from array of Html strings to a Shell created from them.
|
|
367
|
+
* @type {WeakMap<string[], Shell>} */
|
|
368
|
+
shells: new WeakMap()
|
|
369
|
+
};
|
|
405
370
|
|
|
406
371
|
let Util = {
|
|
407
372
|
|
|
@@ -426,9 +391,133 @@ let Util = {
|
|
|
426
391
|
child.textContent = newText;
|
|
427
392
|
}
|
|
428
393
|
}
|
|
429
|
-
}
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* A generator function that recursively traverses and flattens a value.
|
|
398
|
+
*
|
|
399
|
+
* - If the input is an array, it recursively traverses and flattens the array.
|
|
400
|
+
* - If the input is a function, it calls the function, replaces the function
|
|
401
|
+
* with its result, and flattens the result if necessary. It will recursively
|
|
402
|
+
* call functions that return other functions.
|
|
403
|
+
* - Otherwise it yields the value as is.
|
|
404
|
+
*
|
|
405
|
+
* This function does not create a new array for the flattened values. Instead,
|
|
406
|
+
* it lazily yields each item as it is encountered. This can be more memory-efficient
|
|
407
|
+
* for large or deeply nested structures.
|
|
408
|
+
*
|
|
409
|
+
* @param {any} value - The value to flatten. Can be an array, object, function, or primitive.
|
|
410
|
+
* @yields {any} - The next item in the flattened structure.
|
|
411
|
+
*
|
|
412
|
+
* @example
|
|
413
|
+
* const complexArray = [
|
|
414
|
+
* 1,
|
|
415
|
+
* [2, () => 3, [4, () => [5, 6]], { a: 'object' }],
|
|
416
|
+
* () => () => 7,
|
|
417
|
+
* () => [() => 8, 9],
|
|
418
|
+
* ]; *
|
|
419
|
+
* for (const item of flatten(complexArray))
|
|
420
|
+
* console.log(item); // Outputs: 1, 2, 3, 4, 5, 6, { a: 'object' }, 7, 8, 9
|
|
421
|
+
*/
|
|
422
|
+
*flatten(value) {
|
|
423
|
+
if (Array.isArray(value)) {
|
|
424
|
+
for (const item of value) {
|
|
425
|
+
yield* Util.flatten(item); // Recursively flatten arrays
|
|
426
|
+
}
|
|
427
|
+
} else if (typeof value === 'function') {
|
|
428
|
+
const result = value();
|
|
429
|
+
yield* Util.flatten(result); // Recursively flatten the result of a function
|
|
430
|
+
} else
|
|
431
|
+
yield value; // Yield primitive values as is
|
|
432
|
+
},
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Get the value of an input as the most appropriate JavaScript type.
|
|
436
|
+
* @param node {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|HTMLDivElement}
|
|
437
|
+
* @return {string|string[]|number|[]|File[]|Date|boolean} */
|
|
438
|
+
getInputValue(node) {
|
|
439
|
+
if (node.type === 'checkbox' || node.type === 'radio')
|
|
440
|
+
return node.checked; // Boolean
|
|
441
|
+
if (node.type === 'file')
|
|
442
|
+
return [...node.files]; // FileList
|
|
443
|
+
if (node.type === 'number' || node.type === 'range')
|
|
444
|
+
return node.valueAsNumber; // Number
|
|
445
|
+
if (node.type === 'date' || node.type === 'time' || node.type === 'datetime-local')
|
|
446
|
+
return node.valueAsDate; // Date Object
|
|
447
|
+
if (node.type === 'select-multiple') // <select multiple>
|
|
448
|
+
return [...node.selectedOptions].map(option => option.value); // Array of Strings
|
|
449
|
+
|
|
450
|
+
return node.value; // String
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Is it an array and a path that can be evaluated by delve() ?
|
|
455
|
+
* @param arr {Array|*}
|
|
456
|
+
* @returns {boolean} */
|
|
457
|
+
isPath(arr) {
|
|
458
|
+
return Array.isArray(arr) && typeof arr[0] === 'object' && !arr.slice(1).find(p => typeof p !== 'string' && typeof p !== 'number');
|
|
459
|
+
},
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Find NodeGroups that had their nodes removed and add those nodes to a Fragment so
|
|
463
|
+
* they're not lost forever and the NodeGroup's internal structure is still consistent.
|
|
464
|
+
* This saves all of a NodeGroup's nodes in order, so that nextChildNode still works.
|
|
465
|
+
* This is necessary because a NodeGroup normally only stores the first and last node.
|
|
466
|
+
* Called from ExprPath.apply().
|
|
467
|
+
* @param oldNodeGroups {NodeGroup[]}
|
|
468
|
+
* @param oldNodes {Node[]} */
|
|
469
|
+
saveOrphans(oldNodeGroups, oldNodes) {
|
|
470
|
+
let oldNgMap = new Map();
|
|
471
|
+
for (let ng of oldNodeGroups) {
|
|
472
|
+
oldNgMap.set(ng.startNode, ng);
|
|
473
|
+
|
|
474
|
+
// TODO: Is this necessary?
|
|
475
|
+
// if (ng.parentPath)
|
|
476
|
+
// ng.parentPath.clearNodesCache();
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
for (let i=0, node; node = oldNodes[i]; i++) {
|
|
480
|
+
let ng;
|
|
481
|
+
if (!node.parentNode && (ng = oldNgMap.get(node))) {
|
|
482
|
+
//ng.nodesCache = [];
|
|
483
|
+
let fragment = document.createDocumentFragment();
|
|
484
|
+
let endNode = ng.endNode;
|
|
485
|
+
while (node !== endNode) {
|
|
486
|
+
fragment.append(node);
|
|
487
|
+
//ng.nodesCache.push(node);
|
|
488
|
+
i++;
|
|
489
|
+
node = oldNodes[i];
|
|
490
|
+
}
|
|
491
|
+
fragment.append(endNode);
|
|
492
|
+
//ng.nodesCache.push(endNode);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
},
|
|
430
496
|
|
|
497
|
+
/**
|
|
498
|
+
* Remove nodes from the beginning and end that are not:
|
|
499
|
+
* 1. Elements.
|
|
500
|
+
* 2. Non-whitespace text nodes.
|
|
501
|
+
* @param nodes {Node[]|NodeList}
|
|
502
|
+
* @returns {Node[]} */
|
|
503
|
+
trimEmptyNodes(nodes) {
|
|
504
|
+
const shouldTrimNode = node =>
|
|
505
|
+
node.nodeType !== Node.ELEMENT_NODE &&
|
|
506
|
+
(node.nodeType !== Node.TEXT_NODE || node.textContent.trim() === '');
|
|
507
|
+
|
|
508
|
+
// Convert nodeList to an array for easier manipulation
|
|
509
|
+
const result = [...nodes];
|
|
510
|
+
|
|
511
|
+
// Trim from the start
|
|
512
|
+
while (result.length > 0 && shouldTrimNode(result[0]))
|
|
513
|
+
result.shift();
|
|
514
|
+
|
|
515
|
+
// Trim from the end
|
|
516
|
+
while (result.length > 0 && shouldTrimNode(result[result.length - 1]))
|
|
517
|
+
result.pop();
|
|
431
518
|
|
|
519
|
+
return result;
|
|
520
|
+
}
|
|
432
521
|
};
|
|
433
522
|
|
|
434
523
|
|
|
@@ -473,15 +562,15 @@ function camelToDashes(str) {
|
|
|
473
562
|
* Returns false if they're the same. Or the first index where they differ.
|
|
474
563
|
* @param a
|
|
475
564
|
* @param b
|
|
476
|
-
* @returns {
|
|
477
|
-
function
|
|
478
|
-
if (a.length !== b.length)
|
|
479
|
-
return -1;
|
|
565
|
+
* @returns {boolean} */
|
|
566
|
+
function arraySame(a, b) {
|
|
480
567
|
let aLength = a.length;
|
|
568
|
+
if (aLength !== b.length)
|
|
569
|
+
return false;
|
|
481
570
|
for (let i=0; i<aLength; i++)
|
|
482
571
|
if (a[i] !== b[i])
|
|
483
|
-
return
|
|
484
|
-
return
|
|
572
|
+
return false;
|
|
573
|
+
return true; // the same.
|
|
485
574
|
}
|
|
486
575
|
|
|
487
576
|
|
|
@@ -623,211 +712,315 @@ function flattenAndIndent(inputArray, indent = "") {
|
|
|
623
712
|
}
|
|
624
713
|
//#ENDIF
|
|
625
714
|
|
|
626
|
-
|
|
627
|
-
* The html strings and evaluated expressions from an html tagged template.
|
|
628
|
-
* A unique Template is created for each item in a loop.
|
|
629
|
-
* Although the reference to the html strings is shared among templates. */
|
|
630
|
-
class Template {
|
|
631
|
-
|
|
632
|
-
/** @type {(Template|string|function)|(Template|string|function)[]} Evaulated expressions. */
|
|
633
|
-
exprs = []
|
|
634
|
-
|
|
635
|
-
/** @type {string[]} */
|
|
636
|
-
html = [];
|
|
637
|
-
|
|
638
|
-
/**
|
|
639
|
-
* If true, use this template to replace an existing element, instead of appending children to it.
|
|
640
|
-
* @type {?boolean} */
|
|
641
|
-
replaceMode;
|
|
642
|
-
|
|
643
|
-
/** Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
|
|
644
|
-
hashedFields;
|
|
645
|
-
|
|
646
|
-
/**
|
|
647
|
-
* @deprecated
|
|
648
|
-
* @type {ExprPath} Used with forEach() from watch.js
|
|
649
|
-
* Set in ExprPath.apply() */
|
|
650
|
-
parentPath;
|
|
651
|
-
|
|
652
|
-
/** @type {NodeGroup} */
|
|
653
|
-
nodeGroup;
|
|
654
|
-
|
|
655
|
-
/**
|
|
656
|
-
* @type {string[][]} */
|
|
657
|
-
paths = [];
|
|
715
|
+
class MultiValueMap {
|
|
658
716
|
|
|
659
|
-
/**
|
|
660
|
-
|
|
661
|
-
* @param htmlStrings {string[]}
|
|
662
|
-
* @param exprs {*[]} */
|
|
663
|
-
constructor(htmlStrings, exprs) {
|
|
664
|
-
this.html = htmlStrings;
|
|
665
|
-
this.exprs = exprs;
|
|
666
|
-
|
|
667
|
-
//this.trace = new Error().stack.split(/\n/g)
|
|
717
|
+
/** @type {Object<string, Set>} */
|
|
718
|
+
data = {};
|
|
668
719
|
|
|
669
|
-
|
|
670
|
-
|
|
720
|
+
// Set a new value for a key
|
|
721
|
+
add(key, value) {
|
|
722
|
+
let data = this.data;
|
|
723
|
+
let set = data[key];
|
|
724
|
+
if (!set) {
|
|
725
|
+
set = new Set();
|
|
726
|
+
data[key] = set;
|
|
727
|
+
}
|
|
728
|
+
set.add(value);
|
|
729
|
+
}
|
|
671
730
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
Object.defineProperty(this, 'debug', {
|
|
677
|
-
get() {
|
|
678
|
-
return JSON.stringify([this.html, this.exprs]);
|
|
679
|
-
}
|
|
680
|
-
});
|
|
681
|
-
//#ENDIF
|
|
731
|
+
isEmpty() {
|
|
732
|
+
for (let key in this.data)
|
|
733
|
+
return true;
|
|
734
|
+
return false;
|
|
682
735
|
}
|
|
683
736
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
toJSON() {
|
|
688
|
-
if (!this.hashedFields)
|
|
689
|
-
this.hashedFields = [getObjectId(this.html, 'Html'), this.exprs];
|
|
690
|
-
|
|
691
|
-
return this.hashedFields
|
|
737
|
+
// Get all values for a key
|
|
738
|
+
getAll(key) {
|
|
739
|
+
return this.data[key] || [];
|
|
692
740
|
}
|
|
693
741
|
|
|
694
742
|
/**
|
|
695
|
-
*
|
|
696
|
-
* @param
|
|
697
|
-
* @param
|
|
698
|
-
* @
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
if (!el) {
|
|
703
|
-
ng = new NodeGroup(this);
|
|
704
|
-
el = ng.getParentNode();
|
|
705
|
-
}
|
|
743
|
+
* Remove one value from a key, and return it.
|
|
744
|
+
* @param key {string}
|
|
745
|
+
* @param val If specified, make sure we delete this specific value, if a key exists more than once.
|
|
746
|
+
* @returns {*} */
|
|
747
|
+
delete(key, val=undefined) {
|
|
748
|
+
// if (key === '["Html2",[[["Html3",["F1","A"]],["Html3",["F1","B"]]]]]')
|
|
749
|
+
// debugger;
|
|
706
750
|
|
|
707
|
-
let
|
|
708
|
-
if (ng)
|
|
709
|
-
ng.manager = ngm;
|
|
751
|
+
let data = this.data;
|
|
710
752
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
created: [],
|
|
714
|
-
updated: [],
|
|
715
|
-
moved: [],
|
|
716
|
-
deleted: []
|
|
717
|
-
};
|
|
718
|
-
//#ENDIF
|
|
753
|
+
// if (!data.hasOwnProperty(key))
|
|
754
|
+
// return undefined;
|
|
719
755
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
756
|
+
// Delete a specific value.
|
|
757
|
+
let result;
|
|
758
|
+
let set = data[key];
|
|
759
|
+
if (!set) // slower than pre-check.
|
|
760
|
+
return undefined;
|
|
723
761
|
|
|
724
|
-
//
|
|
725
|
-
if (
|
|
726
|
-
|
|
762
|
+
// Delete any value.
|
|
763
|
+
if (val === undefined) {
|
|
764
|
+
//result = set.values().next().value; // get first item from set.
|
|
765
|
+
[result] = set; // Does the same as above and seems to be about the same speed.
|
|
766
|
+
set.delete(result);
|
|
727
767
|
}
|
|
768
|
+
|
|
769
|
+
// Delete a specific value.
|
|
728
770
|
else {
|
|
771
|
+
set.delete(val);
|
|
772
|
+
result = val;
|
|
773
|
+
}
|
|
729
774
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
let exact = ngm.getNodeGroup(this, true);
|
|
734
|
-
if (!exact) {
|
|
735
|
-
close = ngm.getNodeGroup(this, false);
|
|
736
|
-
}
|
|
775
|
+
// TODO: Will this make it slower?
|
|
776
|
+
if (set.size === 0)
|
|
777
|
+
delete data[key];
|
|
737
778
|
|
|
738
|
-
|
|
739
|
-
|
|
779
|
+
return result;
|
|
780
|
+
}
|
|
740
781
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
782
|
+
hasValue(val) {
|
|
783
|
+
let data = this.data;
|
|
784
|
+
let names = [];
|
|
785
|
+
for (let name in data)
|
|
786
|
+
if (data[name].has(val)) // TODO: iterate twice to pre-size array?
|
|
787
|
+
names.push(name);
|
|
788
|
+
return names;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* ISC License
|
|
794
|
+
*
|
|
795
|
+
* Copyright (c) 2020, Andrea Giammarchi, @WebReflection
|
|
796
|
+
*
|
|
797
|
+
* Permission to use, copy, modify, and/or distribute this software for any
|
|
798
|
+
* purpose with or without fee is hereby granted, provided that the above
|
|
799
|
+
* copyright notice and this permission notice appear in all copies.
|
|
800
|
+
*
|
|
801
|
+
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
802
|
+
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
803
|
+
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
804
|
+
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
805
|
+
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
|
806
|
+
* OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
807
|
+
* PERFORMANCE OF THIS SOFTWARE.
|
|
808
|
+
*/
|
|
744
809
|
|
|
810
|
+
/**
|
|
811
|
+
* @param {Node} parentNode The container where children live
|
|
812
|
+
* @param {Node[]} a The list of current/live children
|
|
813
|
+
* @param {Node[]} b The list of future children
|
|
814
|
+
* @param {(entry: Node, action: number) => Node} get
|
|
815
|
+
* The callback invoked per each entry related DOM operation.
|
|
816
|
+
* @param {Node} [before] The optional node used as anchor to insert before.
|
|
817
|
+
* @returns {Node[]} The same list of future children.
|
|
818
|
+
*/
|
|
819
|
+
const udomdiff = (parentNode, a, b, before) => {
|
|
820
|
+
//#IFDEV
|
|
821
|
+
// if (parentNode instanceof ExprPath)
|
|
822
|
+
// parentNode.verify();
|
|
823
|
+
//#ENDIF
|
|
745
824
|
|
|
746
|
-
|
|
747
|
-
|
|
825
|
+
const bLength = b.length;
|
|
826
|
+
let aEnd = a.length;
|
|
827
|
+
let bEnd = bLength;
|
|
828
|
+
let aStart = 0;
|
|
829
|
+
let bStart = 0;
|
|
830
|
+
let map = null;
|
|
831
|
+
while (aStart < aEnd || bStart < bEnd) {
|
|
832
|
+
// append head, tail, or nodes in between: fast path
|
|
833
|
+
if (aEnd === aStart) {
|
|
834
|
+
// we could be in a situation where the rest of nodes that
|
|
835
|
+
// need to be added are not at the end, and in such case
|
|
836
|
+
// the node to `insertBefore`, if the index is more than 0
|
|
837
|
+
// must be retrieved, otherwise it's gonna be the first item.
|
|
838
|
+
const node = bEnd < bLength
|
|
839
|
+
? (bStart
|
|
840
|
+
? (b[bStart - 1].nextSibling)
|
|
841
|
+
: b[bEnd - bStart])
|
|
842
|
+
: before;
|
|
843
|
+
while (bStart < bEnd) {
|
|
844
|
+
let bNode = b[bStart++];
|
|
845
|
+
parentNode.insertBefore(bNode, node);
|
|
748
846
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
847
|
+
//#IFDEV
|
|
848
|
+
if (bNode instanceof NodeGroup)
|
|
849
|
+
bNode.verify();
|
|
850
|
+
// if (parentNode instanceof ExprPath)
|
|
851
|
+
// parentNode.verify();
|
|
852
|
+
//#ENDIF
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
// remove head or tail: fast path
|
|
856
|
+
else if (bEnd === bStart) {
|
|
857
|
+
while (aStart < aEnd) {
|
|
858
|
+
// remove the node only if it's unknown or not live
|
|
859
|
+
let aNode = a[aStart];
|
|
860
|
+
if (!map || !map.has(aNode)) {
|
|
861
|
+
parentNode.removeChild(aNode);
|
|
755
862
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
// Apply slot children
|
|
763
|
-
if (fragment) {
|
|
764
|
-
for (let slot of el.querySelectorAll('slot[name]')) {
|
|
765
|
-
let name = slot.getAttribute('name');
|
|
766
|
-
if (name)
|
|
767
|
-
slot.append(...fragment.querySelectorAll(`[slot='${name}']`));
|
|
768
|
-
}
|
|
769
|
-
let unamedSlot = el.querySelector('slot:not([name])');
|
|
770
|
-
if (unamedSlot)
|
|
771
|
-
unamedSlot.append(fragment);
|
|
863
|
+
//#IFDEV
|
|
864
|
+
if (aNode instanceof NodeGroup)
|
|
865
|
+
aNode.verify();
|
|
866
|
+
// if (parentNode instanceof ExprPath)
|
|
867
|
+
// parentNode.verify();
|
|
868
|
+
//#ENDIF
|
|
772
869
|
}
|
|
870
|
+
aStart++;
|
|
773
871
|
}
|
|
872
|
+
}
|
|
873
|
+
// same node: fast path
|
|
874
|
+
else if (a[aStart] === b[bStart]) {
|
|
875
|
+
aStart++;
|
|
876
|
+
bStart++;
|
|
877
|
+
}
|
|
878
|
+
// same tail: fast path
|
|
879
|
+
else if (a[aEnd - 1] === b[bEnd - 1]) {
|
|
880
|
+
aEnd--;
|
|
881
|
+
bEnd--;
|
|
882
|
+
}
|
|
883
|
+
// The once here single last swap "fast path" has been removed in v1.1.0
|
|
884
|
+
// https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85
|
|
885
|
+
// reverse swap: also fast path
|
|
886
|
+
else if (
|
|
887
|
+
a[aStart] === b[bEnd - 1] &&
|
|
888
|
+
b[bStart] === a[aEnd - 1]
|
|
889
|
+
) {
|
|
890
|
+
// this is a "shrink" operation that could happen in these cases:
|
|
891
|
+
// [1, 2, 3, 4, 5]
|
|
892
|
+
// [1, 4, 3, 2, 5]
|
|
893
|
+
// or asymmetric too
|
|
894
|
+
// [1, 2, 3, 4, 5]
|
|
895
|
+
// [1, 2, 3, 5, 6, 4]
|
|
896
|
+
const node = a[--aEnd].nextSibling;
|
|
774
897
|
|
|
775
|
-
ngm.rootEl = el;
|
|
776
|
-
|
|
777
|
-
// this.rootNg was rendered as childrenOnly=true
|
|
778
|
-
// Apply attributes from a root element to the real root element.
|
|
779
|
-
let ng = ngm.rootNg;
|
|
780
|
-
if (ng.pseudoRoot && ng.pseudoRoot !== el) {
|
|
781
|
-
/*#IFDEV*/assert(el);/*#ENDIF*/
|
|
782
|
-
|
|
783
|
-
// Remove old attributes
|
|
784
|
-
// for (let attrib of this.rootEl.attributes)
|
|
785
|
-
// if (attrib.name !== 'is' && attrib.name !== 'data-style' && !ng.pseudoRoot.hasAttribute(attrib.name))
|
|
786
|
-
// this.rootEl.removeAttribute(attrib.name)
|
|
787
|
-
|
|
788
|
-
// Add/set new attributes
|
|
789
|
-
if (firstTime)
|
|
790
|
-
for (let attrib of ng.pseudoRoot.attributes)
|
|
791
|
-
if (!el.hasAttribute(attrib.name))
|
|
792
|
-
el.setAttribute(attrib.name, attrib.value);
|
|
793
|
-
|
|
794
|
-
// ng.startNode = ng.endNode = this.rootEl;
|
|
795
|
-
// ng.nodesCache = [ng.startNode]
|
|
796
|
-
// for (let path of ng.paths) {
|
|
797
|
-
// if (path.nodeMarker === ng.rootEl)
|
|
798
|
-
// path.nodeMarker = this.rootEl;
|
|
799
|
-
// path.nodesCache = null;
|
|
800
|
-
// /*#IFDEV*/assert(path.nodeBefore !== ng.rootEl)/*#ENDIF*/
|
|
801
|
-
// }
|
|
802
|
-
//
|
|
803
|
-
// ng.rootEl = this.rootEl;
|
|
804
|
-
}
|
|
805
898
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
899
|
+
let a2 = b[bStart++];
|
|
900
|
+
let b2 = a[aStart++];
|
|
901
|
+
parentNode.insertBefore(
|
|
902
|
+
a2,
|
|
903
|
+
b2.nextSibling
|
|
904
|
+
);
|
|
905
|
+
//#IFDEV
|
|
906
|
+
if (a2 instanceof NodeGroup)
|
|
907
|
+
a2.verify();
|
|
908
|
+
// if (parentNode instanceof ExprPath)
|
|
909
|
+
// parentNode.verify();
|
|
910
|
+
//#ENDIF
|
|
911
|
+
|
|
912
|
+
let bNode = b[--bEnd];
|
|
913
|
+
parentNode.insertBefore(bNode, node);
|
|
809
914
|
|
|
810
|
-
|
|
915
|
+
//#IFDEV
|
|
916
|
+
if (bNode instanceof NodeGroup)
|
|
917
|
+
bNode.verify();
|
|
918
|
+
// if (parentNode instanceof ExprPath)
|
|
919
|
+
// parentNode.verify();
|
|
920
|
+
|
|
921
|
+
//#ENDIF
|
|
922
|
+
|
|
923
|
+
// mark the future index as identical (yeah, it's dirty, but cheap 👍)
|
|
924
|
+
// The main reason to do this, is that when a[aEnd] will be reached,
|
|
925
|
+
// the loop will likely be on the fast path, as identical to b[bEnd].
|
|
926
|
+
// In the best case scenario, the next loop will skip the tail,
|
|
927
|
+
// but in the worst one, this node will be considered as already
|
|
928
|
+
// processed, bailing out pretty quickly from the map index check
|
|
929
|
+
a[aEnd] = b[bEnd];
|
|
811
930
|
}
|
|
931
|
+
// map based fallback, "slow" path
|
|
932
|
+
else {
|
|
933
|
+
// the map requires an O(bEnd - bStart) operation once
|
|
934
|
+
// to store all future nodes indexes for later purposes.
|
|
935
|
+
// In the worst case scenario, this is a full O(N) cost,
|
|
936
|
+
// and such scenario happens at least when all nodes are different,
|
|
937
|
+
// but also if both first and last items of the lists are different
|
|
938
|
+
if (!map) {
|
|
939
|
+
map = new Map;
|
|
940
|
+
let i = bStart;
|
|
941
|
+
while (i < bEnd)
|
|
942
|
+
map.set(b[i], i++);
|
|
943
|
+
}
|
|
944
|
+
// if it's a future node, hence it needs some handling
|
|
945
|
+
if (map.has(a[aStart])) {
|
|
946
|
+
// grab the index of such node, 'cause it might have been processed
|
|
947
|
+
const index = map.get(a[aStart]);
|
|
948
|
+
// if it's not already processed, look on demand for the next LCS
|
|
949
|
+
if (bStart < index && index < bEnd) {
|
|
950
|
+
let i = aStart;
|
|
951
|
+
// counts the amount of nodes that are the same in the future
|
|
952
|
+
let sequence = 1;
|
|
953
|
+
while (++i < aEnd && i < bEnd && map.get(a[i]) === (index + sequence))
|
|
954
|
+
sequence++;
|
|
955
|
+
// effort decision here: if the sequence is longer than replaces
|
|
956
|
+
// needed to reach such sequence, which would brings again this loop
|
|
957
|
+
// to the fast path, prepend the difference before a sequence,
|
|
958
|
+
// and move only the future list index forward, so that aStart
|
|
959
|
+
// and bStart will be aligned again, hence on the fast path.
|
|
960
|
+
// An example considering aStart and bStart are both 0:
|
|
961
|
+
// a: [1, 2, 3, 4]
|
|
962
|
+
// b: [7, 1, 2, 3, 6]
|
|
963
|
+
// this would place 7 before 1 and, from that time on, 1, 2, and 3
|
|
964
|
+
// will be processed at zero cost
|
|
965
|
+
if (sequence > (index - bStart)) {
|
|
966
|
+
const node = a[aStart];
|
|
967
|
+
while (bStart < index) {
|
|
968
|
+
let bNode = b[bStart++];
|
|
969
|
+
parentNode.insertBefore(bNode, node);
|
|
812
970
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
}
|
|
971
|
+
//#IFDEV
|
|
972
|
+
if (bNode instanceof NodeGroup)
|
|
973
|
+
bNode.verify();
|
|
974
|
+
// if (parentNode instanceof ExprPath)
|
|
975
|
+
// parentNode.verify();
|
|
819
976
|
|
|
977
|
+
//#ENDIF
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
// if the effort wasn't good enough, fallback to a replace,
|
|
981
|
+
// moving both source and target indexes forward, hoping that some
|
|
982
|
+
// similar node will be found later on, to go back to the fast path
|
|
983
|
+
else {
|
|
984
|
+
let aNode = a[aStart++];
|
|
985
|
+
let bNode = b[bStart++];
|
|
986
|
+
parentNode.replaceChild(
|
|
987
|
+
bNode,
|
|
988
|
+
aNode
|
|
989
|
+
);
|
|
820
990
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
991
|
+
//#IFDEV
|
|
992
|
+
if (aNode instanceof NodeGroup)
|
|
993
|
+
aNode.verify();
|
|
994
|
+
// if (parentNode instanceof ExprPath)
|
|
995
|
+
// parentNode.verify();
|
|
996
|
+
//#ENDIF
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
// otherwise move the source forward, 'cause there's nothing to do
|
|
1000
|
+
else
|
|
1001
|
+
aStart++;
|
|
1002
|
+
}
|
|
1003
|
+
// this node has no meaning in the future list, so it's more than safe
|
|
1004
|
+
// to remove it, and check the next live node out instead, meaning
|
|
1005
|
+
// that only the live list index should be forwarded
|
|
1006
|
+
else {
|
|
1007
|
+
let aNode = a[aStart++];
|
|
1008
|
+
parentNode.removeChild(aNode);
|
|
824
1009
|
|
|
825
|
-
|
|
1010
|
+
//#IFDEV
|
|
1011
|
+
if (aNode instanceof NodeGroup)
|
|
1012
|
+
aNode.verify();
|
|
1013
|
+
// if (parentNode instanceof ExprPath)
|
|
1014
|
+
// parentNode.verify();
|
|
1015
|
+
//#ENDIF
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
826
1018
|
}
|
|
827
|
-
|
|
1019
|
+
return b;
|
|
1020
|
+
};
|
|
828
1021
|
|
|
829
1022
|
/**
|
|
830
|
-
* Path to where an expression should be evaluated within a Shell.
|
|
1023
|
+
* Path to where an expression should be evaluated within a Shell or NodeGroup.
|
|
831
1024
|
* Path is only valid until the expressions before it are evaluated.
|
|
832
1025
|
* TODO: Make this based on parent and node instead of path? */
|
|
833
1026
|
class ExprPath {
|
|
@@ -866,14 +1059,9 @@ class ExprPath {
|
|
|
866
1059
|
* @type {Node|HTMLElement} */
|
|
867
1060
|
nodeMarker;
|
|
868
1061
|
|
|
869
|
-
/** @deprecated */
|
|
870
|
-
get parentNode() {
|
|
871
|
-
return this.nodeMarker.parentNode;
|
|
872
|
-
}
|
|
873
1062
|
|
|
874
1063
|
// These are set after an expression is assigned:
|
|
875
1064
|
|
|
876
|
-
|
|
877
1065
|
/** @type {NodeGroup} */
|
|
878
1066
|
parentNg;
|
|
879
1067
|
|
|
@@ -881,8 +1069,6 @@ class ExprPath {
|
|
|
881
1069
|
nodeGroups = [];
|
|
882
1070
|
|
|
883
1071
|
|
|
884
|
-
|
|
885
|
-
|
|
886
1072
|
// Caches to make things faster
|
|
887
1073
|
|
|
888
1074
|
/**
|
|
@@ -890,50 +1076,23 @@ class ExprPath {
|
|
|
890
1076
|
* @type {Node[]} Cached result of getNodes() */
|
|
891
1077
|
nodesCache;
|
|
892
1078
|
|
|
893
|
-
|
|
1079
|
+
/**
|
|
1080
|
+
* @type {int} Index of nodeBefore among its parentNode's children. */
|
|
894
1081
|
nodeBeforeIndex;
|
|
895
|
-
nodeMarkerPath;
|
|
896
1082
|
|
|
897
|
-
|
|
898
|
-
|
|
1083
|
+
/**
|
|
1084
|
+
* @type {int[]} Path to the node marker, in reverse for performance reasons. */
|
|
1085
|
+
nodeMarkerPath;
|
|
899
1086
|
|
|
900
|
-
// for debugging
|
|
901
|
-
//#IFDEV
|
|
902
|
-
parentIndex;
|
|
903
|
-
//#ENDIF
|
|
904
1087
|
|
|
905
1088
|
/**
|
|
906
1089
|
* @param nodeBefore {Node}
|
|
907
1090
|
* @param nodeMarker {?Node}
|
|
908
|
-
* @param type {
|
|
1091
|
+
* @param type {PathType}
|
|
909
1092
|
* @param attrName {?string}
|
|
910
1093
|
* @param attrValue {string[]} */
|
|
911
1094
|
constructor(nodeBefore, nodeMarker, type=PathType.Content, attrName=null, attrValue=null) {
|
|
912
1095
|
|
|
913
|
-
//#IFDEV
|
|
914
|
-
/*
|
|
915
|
-
Object.defineProperty(this, 'debug', {
|
|
916
|
-
get() {
|
|
917
|
-
return [
|
|
918
|
-
`parentNode: ${this.nodeBefore.parentNode?.tagName?.toLowerCase()}`,
|
|
919
|
-
'nodes:',
|
|
920
|
-
...setIndent(this.getNodes().map(item => {
|
|
921
|
-
if (item instanceof Node)
|
|
922
|
-
return item.outerHTML || item.textContent
|
|
923
|
-
else if (item instanceof NodeGroup)
|
|
924
|
-
return item.debug
|
|
925
|
-
}), 1).flat()
|
|
926
|
-
]
|
|
927
|
-
}
|
|
928
|
-
})
|
|
929
|
-
|
|
930
|
-
Object.defineProperty(this, 'debugNodes', {
|
|
931
|
-
get: () =>
|
|
932
|
-
this.getNodes()
|
|
933
|
-
})
|
|
934
|
-
*/
|
|
935
|
-
//#ENDIF
|
|
936
|
-
|
|
937
1096
|
// If path is a node.
|
|
938
1097
|
this.nodeBefore = nodeBefore;
|
|
939
1098
|
this.nodeMarker = nodeMarker;
|
|
@@ -945,25 +1104,142 @@ class ExprPath {
|
|
|
945
1104
|
}
|
|
946
1105
|
|
|
947
1106
|
/**
|
|
1107
|
+
* Apply any type of expression.
|
|
1108
|
+
* This calls other apply functions.
|
|
948
1109
|
*
|
|
1110
|
+
* One very messy part of this function is that it may apply multiple expressions if they're all part
|
|
1111
|
+
* of the same attribute value.
|
|
1112
|
+
*
|
|
1113
|
+
* We should modify path.applyValueAttrib so it stores the procssed parts and then only calls
|
|
1114
|
+
* setAttribute() once all the pieces are in place.
|
|
1115
|
+
*
|
|
1116
|
+
* @param expr {Expr}
|
|
1117
|
+
* @param exprs {Expr[]}
|
|
1118
|
+
* @param exprIndex {int}
|
|
1119
|
+
* @param componentExprs {object}
|
|
1120
|
+
* @returns {int} */
|
|
1121
|
+
apply(expr, exprs=null, exprIndex=0, componentExprs={}) {
|
|
1122
|
+
switch (this.type) {
|
|
1123
|
+
case 1: // PathType.Content:
|
|
1124
|
+
this.applyNodes(expr);
|
|
1125
|
+
break;
|
|
1126
|
+
case 2: // PathType.Multiple:
|
|
1127
|
+
this.applyMultipleAttribs(this.nodeMarker, expr);
|
|
1128
|
+
break;
|
|
1129
|
+
case 5: // PathType.Comment:
|
|
1130
|
+
// Expressions inside Html comments. Deliberately empty because we won't waste time updating them.
|
|
1131
|
+
break;
|
|
1132
|
+
case 6: // PathType.Event:
|
|
1133
|
+
this.applyEventAttrib(this.nodeMarker, expr, this.parentNg.rootNg.root);
|
|
1134
|
+
break;
|
|
1135
|
+
default:
|
|
1136
|
+
if (this.type === 4 /*PathType.Component*/ && this.nodeMarker !== this.parentNg.rootNg.root)
|
|
1137
|
+
componentExprs[this.attrName] = expr;
|
|
1138
|
+
else {
|
|
1139
|
+
// One attribute value may have multiple expressions. Here we apply them all at once.
|
|
1140
|
+
exprIndex = this.applyValueAttrib(this.nodeMarker, exprs || [expr], exprIndex);
|
|
1141
|
+
}
|
|
1142
|
+
break;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
return exprIndex;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/**
|
|
1149
|
+
* Insert/replace the nodes created by a single expression.
|
|
1150
|
+
* Called by applyExprs()
|
|
1151
|
+
* This function is recursive, as the functions it calls also call it.
|
|
1152
|
+
* @param expr {Expr}
|
|
1153
|
+
* @return {Node[]} New Nodes created. */
|
|
1154
|
+
applyNodes(expr) {
|
|
1155
|
+
let path = this;
|
|
1156
|
+
|
|
1157
|
+
/*#IFDEV*/path.verify();/*#ENDIF*/
|
|
1158
|
+
|
|
1159
|
+
/** @type {(Node|NodeGroup|Expr)[]} */
|
|
1160
|
+
let newNodes = [];
|
|
1161
|
+
let oldNodeGroups = path.nodeGroups;
|
|
1162
|
+
/*#IFDEV*/assert(!oldNodeGroups.includes(null));/*#ENDIF*/
|
|
1163
|
+
let secondPass = []; // indices
|
|
1164
|
+
|
|
1165
|
+
path.nodeGroups = []; // Reset before applyExact and the code below rebuilds it.
|
|
1166
|
+
path.applyExact(expr, newNodes, secondPass);
|
|
1167
|
+
|
|
1168
|
+
this.existingTextNodes = null;
|
|
1169
|
+
|
|
1170
|
+
// TODO: Create an array of old vs Nodes and NodeGroups together.
|
|
1171
|
+
// If they're all the same, skip the next steps.
|
|
1172
|
+
// Or calculate it in the loop above as we go? Have a path.lastNodeGroups property?
|
|
1173
|
+
|
|
1174
|
+
// Second pass to find close-match NodeGroups.
|
|
1175
|
+
let flatten = false;
|
|
1176
|
+
if (secondPass.length) {
|
|
1177
|
+
for (let [nodesIndex, ngIndex] of secondPass) {
|
|
1178
|
+
let ng = path.getNodeGroup(newNodes[nodesIndex], false);
|
|
1179
|
+
|
|
1180
|
+
let ngNodes = ng.getNodes();
|
|
1181
|
+
|
|
1182
|
+
/*#IFDEV*/assert(!(newNodes[nodesIndex] instanceof NodeGroup));/*#ENDIF*/
|
|
1183
|
+
|
|
1184
|
+
if (ngNodes.length === 1) // flatten manually so we can skip flattening below.
|
|
1185
|
+
newNodes[nodesIndex] = ngNodes[0];
|
|
1186
|
+
|
|
1187
|
+
else {
|
|
1188
|
+
newNodes[nodesIndex] = ngNodes;
|
|
1189
|
+
flatten = true;
|
|
1190
|
+
}
|
|
1191
|
+
path.nodeGroups[ngIndex] = ng;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
if (flatten)
|
|
1195
|
+
newNodes = newNodes.flat(); // Only if second pass happens.
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/*#IFDEV*/assert(!path.nodeGroups.includes(null));/*#ENDIF*/
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
|
|
1202
|
+
let oldNodes = path.getNodes();
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
// This pre-check makes it a few percent faster?
|
|
1206
|
+
let same = arraySame(oldNodes, newNodes);
|
|
1207
|
+
if (!same) {
|
|
1208
|
+
|
|
1209
|
+
path.nodesCache = newNodes; // Replaces value set by path.getNodes()
|
|
1210
|
+
|
|
1211
|
+
if (this.parentNg.parentPath)
|
|
1212
|
+
this.parentNg.parentPath.clearNodesCache();
|
|
1213
|
+
|
|
1214
|
+
// Fast clear method
|
|
1215
|
+
let isNowEmpty = oldNodes.length && !newNodes.length;
|
|
1216
|
+
if (!isNowEmpty || !path.fastClear())
|
|
1217
|
+
|
|
1218
|
+
// Rearrange nodes.
|
|
1219
|
+
udomdiff(path.nodeMarker.parentNode, oldNodes, newNodes, path.nodeMarker);
|
|
1220
|
+
|
|
1221
|
+
Util.saveOrphans(oldNodeGroups, oldNodes);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// Must happen after second pass.
|
|
1225
|
+
path.freeNodeGroups();
|
|
1226
|
+
|
|
1227
|
+
/*#IFDEV*/path.verify();/*#ENDIF*/
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Apply Nodes that are an exact match.
|
|
949
1234
|
* @param expr {Template|Node|Array|function|*}
|
|
950
1235
|
* @param newNodes {(Node|Template)[]}
|
|
951
|
-
* @param secondPass {Array} Locations within newNodes to evaluate later.
|
|
952
|
-
|
|
1236
|
+
* @param secondPass {Array} Locations within newNodes to evaluate later. */
|
|
1237
|
+
applyExact(expr, newNodes, secondPass) {
|
|
953
1238
|
|
|
954
1239
|
if (expr instanceof Template) {
|
|
955
|
-
expr.nodegroup = this.parentNg; // All tests pass w/o this.
|
|
956
|
-
|
|
957
|
-
let ng = this.parentNg.manager.getNodeGroup(expr, true);
|
|
958
|
-
|
|
959
1240
|
|
|
1241
|
+
let ng = this.getNodeGroup(expr, true);
|
|
960
1242
|
if (ng) {
|
|
961
|
-
//#IFDEV
|
|
962
|
-
// Make sure the nodeCache of the ExprPath we took it from is sitll valid.
|
|
963
|
-
if (ng.parentPath)
|
|
964
|
-
ng.parentPath.verify();
|
|
965
|
-
//#ENDIF
|
|
966
|
-
|
|
967
1243
|
|
|
968
1244
|
// TODO: Track ranges of changed nodes and only pass those to udomdiff?
|
|
969
1245
|
// But will that break the swap benchmark?
|
|
@@ -971,7 +1247,7 @@ class ExprPath {
|
|
|
971
1247
|
this.nodeGroups.push(ng);
|
|
972
1248
|
}
|
|
973
1249
|
|
|
974
|
-
// If expression,
|
|
1250
|
+
// If expression, mark it to be evaluated later in ExprPath.apply() to find partial match.
|
|
975
1251
|
else {
|
|
976
1252
|
secondPass.push([newNodes.length, this.nodeGroups.length]);
|
|
977
1253
|
newNodes.push(expr);
|
|
@@ -989,21 +1265,26 @@ class ExprPath {
|
|
|
989
1265
|
newNodes.push(expr);
|
|
990
1266
|
}
|
|
991
1267
|
|
|
1268
|
+
// Arrays and functions.
|
|
1269
|
+
// I tried iterating over the result of a generator function to avoid this recursion and simplify the code,
|
|
1270
|
+
// but that consistently made the js-framework-benchmarks a few percentage points slower.
|
|
992
1271
|
else if (Array.isArray(expr))
|
|
993
1272
|
for (let subExpr of expr)
|
|
994
|
-
this.
|
|
1273
|
+
this.applyExact(subExpr, newNodes, secondPass);
|
|
995
1274
|
|
|
996
1275
|
else if (typeof expr === 'function') {
|
|
1276
|
+
Globals.currentExprPath = [this, expr]; // Used by watch3()
|
|
997
1277
|
let result = expr();
|
|
1278
|
+
Globals.currentExprPath = null;
|
|
998
1279
|
|
|
999
|
-
this.
|
|
1280
|
+
this.applyExact(result, newNodes, secondPass);
|
|
1000
1281
|
}
|
|
1001
1282
|
|
|
1002
1283
|
// Text
|
|
1003
1284
|
else {
|
|
1004
1285
|
// Convert falsy values (but not 0) to empty string.
|
|
1005
1286
|
// Convert numbers to string so they compare the same.
|
|
1006
|
-
let text = (expr === undefined || expr === false || expr === null) ? '' : expr + '';
|
|
1287
|
+
let text = (expr === undefined || expr === false || expr === null) ? '' : (expr + '');
|
|
1007
1288
|
|
|
1008
1289
|
// Fast path for updating the text of a single text node.
|
|
1009
1290
|
let first = this.nodeBefore.nextSibling;
|
|
@@ -1023,7 +1304,7 @@ class ExprPath {
|
|
|
1023
1304
|
if (idx !== -1)
|
|
1024
1305
|
newNodes.push(...this.existingTextNodes.splice(idx, 1));
|
|
1025
1306
|
else
|
|
1026
|
-
newNodes.push(this.
|
|
1307
|
+
newNodes.push(this.nodeMarker.ownerDocument.createTextNode(text));
|
|
1027
1308
|
}
|
|
1028
1309
|
}
|
|
1029
1310
|
}
|
|
@@ -1060,86 +1341,103 @@ class ExprPath {
|
|
|
1060
1341
|
/**
|
|
1061
1342
|
* Handle attributes for event binding, such as:
|
|
1062
1343
|
* onclick=${(e, el) => this.doSomething(el, 'meow')}
|
|
1063
|
-
*
|
|
1344
|
+
* oninput=${[this.doSomething, 'meow']}
|
|
1064
1345
|
* onclick=${[this, 'doSomething', 'meow']}
|
|
1065
1346
|
*
|
|
1066
1347
|
* @param node
|
|
1067
1348
|
* @param expr
|
|
1068
1349
|
* @param root */
|
|
1069
1350
|
applyEventAttrib(node, expr, root) {
|
|
1070
|
-
/*#IFDEV*/
|
|
1351
|
+
/*#IFDEV*/
|
|
1352
|
+
assert(this.type === PathType.Event/* || this.type === PathType.Component*/);
|
|
1353
|
+
assert(root instanceof HTMLElement);
|
|
1354
|
+
/*#ENDIF*/
|
|
1071
1355
|
|
|
1072
|
-
let eventName = this.attrName.slice(2);
|
|
1356
|
+
let eventName = this.attrName.slice(2); // remove "on-" prefix.
|
|
1073
1357
|
let func;
|
|
1074
1358
|
|
|
1075
1359
|
// Convert array to function.
|
|
1076
|
-
// TODO: This doesn't work for [this, 'doSomething', 'meow']
|
|
1077
1360
|
let args = [];
|
|
1078
1361
|
if (Array.isArray(expr)) {
|
|
1079
|
-
for (let i=0; i<expr.length; i++)
|
|
1080
|
-
if (typeof expr[i] === 'function') {
|
|
1081
|
-
func = expr[i];
|
|
1082
|
-
args = expr.slice(i+1);
|
|
1083
|
-
break;
|
|
1084
|
-
}
|
|
1085
1362
|
|
|
1363
|
+
// oninput=${[this.doSomething, 'meow']}
|
|
1364
|
+
if (typeof expr[0] === 'function') {
|
|
1365
|
+
func = expr[0];
|
|
1366
|
+
args = expr.slice(1);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// Undocumented.
|
|
1086
1370
|
// oninput=${[this, 'value']}
|
|
1087
|
-
|
|
1371
|
+
else {
|
|
1088
1372
|
func = setValue;
|
|
1089
1373
|
args = [expr[0], expr.slice(1), node];
|
|
1090
1374
|
node.value = delve(expr[0], expr.slice(1));
|
|
1375
|
+
// root.render(); // TODO: This causes infinite recursion.
|
|
1091
1376
|
}
|
|
1092
1377
|
}
|
|
1093
1378
|
else
|
|
1094
1379
|
func = expr;
|
|
1095
1380
|
|
|
1096
|
-
let
|
|
1097
|
-
|
|
1098
|
-
|
|
1381
|
+
let nodeEvents = Globals.nodeEvents.get(node);
|
|
1382
|
+
if (!nodeEvents) {
|
|
1383
|
+
nodeEvents = {[eventName]: new Array(3)};
|
|
1384
|
+
Globals.nodeEvents.set(node, nodeEvents);
|
|
1385
|
+
}
|
|
1386
|
+
let nodeEvent = nodeEvents[eventName];
|
|
1099
1387
|
|
|
1100
1388
|
|
|
1101
|
-
|
|
1389
|
+
|
|
1390
|
+
// If function has changed, remove and rebind the event.
|
|
1391
|
+
if (nodeEvent[0] !== func) {
|
|
1392
|
+
let [existing, existingBound, _] = nodeEvent;
|
|
1102
1393
|
if (existing)
|
|
1103
1394
|
node.removeEventListener(eventName, existingBound);
|
|
1104
1395
|
|
|
1105
1396
|
let originalFunc = func;
|
|
1106
1397
|
|
|
1107
1398
|
// BoundFunc sets the "this" variable to be the current Solarite component.
|
|
1108
|
-
let boundFunc = event =>
|
|
1399
|
+
let boundFunc = (event) => {
|
|
1400
|
+
let args = nodeEvent[2];
|
|
1401
|
+
return originalFunc.call(root, ...args, event, node);
|
|
1402
|
+
};
|
|
1109
1403
|
|
|
1110
1404
|
// Save both the original and bound functions.
|
|
1111
1405
|
// Original so we can compare it against a newly assigned function.
|
|
1112
1406
|
// Bound so we can use it with removeEventListner().
|
|
1113
|
-
|
|
1407
|
+
nodeEvent[0] = originalFunc;
|
|
1408
|
+
nodeEvent[1] = boundFunc;
|
|
1114
1409
|
|
|
1115
1410
|
node.addEventListener(eventName, boundFunc);
|
|
1116
1411
|
|
|
1117
|
-
// TODO: classic event attribs
|
|
1412
|
+
// TODO: classic event attribs?
|
|
1118
1413
|
//el[attr.name] = e => // e.g. el.onclick = ...
|
|
1119
1414
|
// (new Function('event', 'el', attr.value)).bind(this.manager.rootEl)(e, el) // put "event", "el", and "this" in scope for the event code.
|
|
1120
1415
|
}
|
|
1416
|
+
|
|
1417
|
+
// Otherwise just update the args to the function.
|
|
1418
|
+
nodeEvents[eventName][2] = args;
|
|
1121
1419
|
}
|
|
1122
1420
|
|
|
1123
1421
|
applyValueAttrib(node, exprs, exprIndex) {
|
|
1124
1422
|
let expr = exprs[exprIndex];
|
|
1125
|
-
|
|
1126
|
-
// Array for form element data binding.
|
|
1127
|
-
// TODO: This never worked, and was moved to applyEventAttrib.
|
|
1128
|
-
// let isArrayValue = Array.isArray(expr);
|
|
1129
|
-
// if (isArrayValue && expr.length >= 2 && !expr.slice(1).find(v => !['string', 'number'].includes(typeof v))) {
|
|
1130
|
-
// node.value = delve(expr[0], expr.slice(1));
|
|
1131
|
-
// node.addEventListener('input', e => {
|
|
1132
|
-
// delve(expr[0], expr.slice(1), node.value) // TODO: support other properties like checked
|
|
1133
|
-
// });
|
|
1134
|
-
// }
|
|
1135
1423
|
|
|
1136
1424
|
// Values to toggle an attribute
|
|
1137
1425
|
if (!this.attrValue && (expr === false || expr === null || expr === undefined))
|
|
1138
1426
|
node.removeAttribute(this.attrName);
|
|
1139
|
-
|
|
1427
|
+
|
|
1140
1428
|
else if (!this.attrValue && expr === true)
|
|
1141
1429
|
node.setAttribute(this.attrName, '');
|
|
1142
1430
|
|
|
1431
|
+
// Passing a path to the value attribute.
|
|
1432
|
+
// This same logic is in NodeGroup.createNewComponent() for components.
|
|
1433
|
+
else if ((this.attrName === 'value' || this.attrName === 'data-value') && Util.isPath(expr)) {
|
|
1434
|
+
let [obj, path] = [expr[0], expr.slice(1)];
|
|
1435
|
+
node.value = delve(obj, path);
|
|
1436
|
+
node.addEventListener('input', () => {
|
|
1437
|
+
delve(obj, path, Util.getInputValue(node));
|
|
1438
|
+
}, true); // We use capture so we update the values before other events added by the user.
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1143
1441
|
// Regular attribute
|
|
1144
1442
|
else {
|
|
1145
1443
|
let value = [];
|
|
@@ -1155,21 +1453,25 @@ class ExprPath {
|
|
|
1155
1453
|
exprIndex--;
|
|
1156
1454
|
}
|
|
1157
1455
|
}
|
|
1158
|
-
|
|
1159
1456
|
exprIndex ++;
|
|
1160
1457
|
}
|
|
1161
1458
|
else
|
|
1162
1459
|
value.unshift(expr);
|
|
1163
1460
|
|
|
1164
1461
|
let joinedValue = value.join('');
|
|
1165
|
-
|
|
1462
|
+
|
|
1463
|
+
// Only update attributes if the value has changed.
|
|
1464
|
+
// The .value property is special. If it changes we don't update the attribute.
|
|
1465
|
+
let oldVal = this.attrName === 'value' ? node.value : node.getAttribute(this.attrName);
|
|
1466
|
+
if (oldVal !== joinedValue) {
|
|
1467
|
+
node.setAttribute(this.attrName, joinedValue);
|
|
1468
|
+
}
|
|
1166
1469
|
|
|
1167
1470
|
// This is needed for setting input.value, .checked, option.selected, etc.
|
|
1168
1471
|
// But in some cases setting the attribute is enough. such as div.setAttribute('title') updates div.title.
|
|
1169
1472
|
// TODO: How to tell which is which?
|
|
1170
1473
|
if (this.attrName in node)
|
|
1171
1474
|
node[this.attrName] = joinedValue;
|
|
1172
|
-
|
|
1173
1475
|
}
|
|
1174
1476
|
|
|
1175
1477
|
return exprIndex;
|
|
@@ -1179,31 +1481,35 @@ class ExprPath {
|
|
|
1179
1481
|
/**
|
|
1180
1482
|
*
|
|
1181
1483
|
* @param newRoot {HTMLElement}
|
|
1484
|
+
* @param pathOffset {int}
|
|
1182
1485
|
* @return {ExprPath} */
|
|
1183
|
-
clone(newRoot) {
|
|
1486
|
+
clone(newRoot, pathOffset=0) {
|
|
1184
1487
|
/*#IFDEV*/this.verify();/*#ENDIF*/
|
|
1185
1488
|
|
|
1186
|
-
|
|
1489
|
+
// Resolve node paths.
|
|
1187
1490
|
let nodeMarker, nodeBefore;
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1491
|
+
let root = newRoot;
|
|
1492
|
+
let path = pathOffset ? this.nodeMarkerPath.slice(0, -pathOffset) : this.nodeMarkerPath;
|
|
1493
|
+
for (let i=path.length-1; i>0; i--) // Resolve the path.
|
|
1494
|
+
root = root.childNodes[path[i]];
|
|
1192
1495
|
let childNodes = root.childNodes;
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1496
|
+
|
|
1497
|
+
nodeMarker = path.length ? childNodes[path[0]] : newRoot;
|
|
1498
|
+
if (this.nodeBefore)
|
|
1499
|
+
nodeBefore = childNodes[this.nodeBeforeIndex];
|
|
1196
1500
|
|
|
1197
1501
|
let result = new ExprPath(nodeBefore, nodeMarker, this.type, this.attrName, this.attrValue);
|
|
1198
1502
|
|
|
1199
1503
|
//#IFDEV
|
|
1504
|
+
result.nodeMarker.exprPath = result;
|
|
1505
|
+
if (result.nodeBefore)
|
|
1506
|
+
result.nodeBefore.prevExprPath = result;
|
|
1200
1507
|
result.verify();
|
|
1201
|
-
result.parentIndex = this.parentIndex; // used for debugging?
|
|
1202
1508
|
//#ENDIF
|
|
1203
1509
|
|
|
1204
1510
|
return result;
|
|
1205
1511
|
}
|
|
1206
|
-
|
|
1512
|
+
|
|
1207
1513
|
/**
|
|
1208
1514
|
* Clear the nodeCache of this ExprPath, as well as all parent and child ExprPaths that
|
|
1209
1515
|
* share the same DOM parent node.
|
|
@@ -1211,32 +1517,19 @@ class ExprPath {
|
|
|
1211
1517
|
* TODO: Is recursive clearing ever necessary? */
|
|
1212
1518
|
clearNodesCache() {
|
|
1213
1519
|
let path = this;
|
|
1214
|
-
|
|
1520
|
+
|
|
1215
1521
|
// Clear cache parent ExprPaths that have the same parentNode
|
|
1216
|
-
let parentNode = this.parentNode;
|
|
1217
|
-
while (path && path.parentNode === parentNode) {
|
|
1522
|
+
let parentNode = this.nodeMarker.parentNode;
|
|
1523
|
+
while (path && path.nodeMarker.parentNode === parentNode) {
|
|
1218
1524
|
path.nodesCache = null;
|
|
1219
1525
|
path = path.parentNg?.parentPath;
|
|
1220
|
-
|
|
1526
|
+
|
|
1221
1527
|
// If stuck in an infinite loop here, the problem is likely due to Template hash colisions.
|
|
1222
1528
|
// Which cause one path to be the descendant of itself, creating a cycle.
|
|
1223
1529
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
// Clear cache of child ExprPaths that have the same parentNode
|
|
1228
|
-
for (let ng of path.nodeGroups) {
|
|
1229
|
-
if (ng) // Can be null from apply()'s push(null) call.
|
|
1230
|
-
for (let path2 of ng.paths) {
|
|
1231
|
-
if (path2.type === PathType.Content && path2.parentNode === parentNode) {
|
|
1232
|
-
path2.nodesCache = null;
|
|
1233
|
-
clearChildNodeCache(path2);
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
clearChildNodeCache(this);
|
|
1530
|
+
|
|
1531
|
+
// Commented out on Sep 30, 2024 b/c it was making the benchmark never finish when adding 10k rows.
|
|
1532
|
+
//clearChildNodeCache(this);
|
|
1240
1533
|
}
|
|
1241
1534
|
|
|
1242
1535
|
|
|
@@ -1249,46 +1542,48 @@ class ExprPath {
|
|
|
1249
1542
|
|
|
1250
1543
|
// If parent is the only child of the grandparent, replace the whole parent.
|
|
1251
1544
|
// And if it has no siblings, it's not created by a NodeGroup/path.
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1545
|
+
// Commented out because this will break any references.
|
|
1546
|
+
// And because I don't see much performance difference.
|
|
1547
|
+
// let grandparent = parent.parentNode
|
|
1548
|
+
// if (grandparent && parent === grandparent.firstChild && parent === grandparent.lastChild && !parent.hasAttribute('id')) {
|
|
1549
|
+
// let replacement = document.createElement(parent.tagName)
|
|
1550
|
+
// replacement.append(this.nodeBefore, this.nodeMarker)
|
|
1551
|
+
// for (let attrib of parent.attributes)
|
|
1552
|
+
// replacement.setAttribute(attrib.name, attrib.value)
|
|
1553
|
+
// parent.replaceWith(replacement)
|
|
1554
|
+
// }
|
|
1555
|
+
// else {
|
|
1261
1556
|
parent.innerHTML = ''; // Faster than calling .removeChild() a thousand times.
|
|
1262
1557
|
parent.append(this.nodeBefore, this.nodeMarker);
|
|
1263
|
-
}
|
|
1558
|
+
//}
|
|
1264
1559
|
return true;
|
|
1265
1560
|
}
|
|
1266
1561
|
return false;
|
|
1267
1562
|
}
|
|
1268
|
-
|
|
1563
|
+
|
|
1269
1564
|
/**
|
|
1270
1565
|
* @return {(Node|HTMLElement)[]} */
|
|
1271
1566
|
getNodes() {
|
|
1272
|
-
|
|
1567
|
+
|
|
1273
1568
|
// Why doesn't this work?
|
|
1274
1569
|
// let result2 = [];
|
|
1275
1570
|
// for (let ng of this.nodeGroups)
|
|
1276
1571
|
// result2.push(...ng.getNodes())
|
|
1277
1572
|
// return result2;
|
|
1278
|
-
|
|
1279
|
-
|
|
1573
|
+
|
|
1574
|
+
|
|
1280
1575
|
let result;
|
|
1281
1576
|
|
|
1282
1577
|
// This shaves about 5ms off the partialUpdate benchmark.
|
|
1283
|
-
|
|
1578
|
+
result = this.nodesCache;
|
|
1284
1579
|
if (result) {
|
|
1285
|
-
|
|
1580
|
+
|
|
1286
1581
|
//#IFDEV
|
|
1287
1582
|
this.checkNodesCache();
|
|
1288
1583
|
//#ENDIF
|
|
1289
|
-
|
|
1584
|
+
|
|
1290
1585
|
return result
|
|
1291
|
-
}
|
|
1586
|
+
}
|
|
1292
1587
|
|
|
1293
1588
|
result = [];
|
|
1294
1589
|
let current = this.nodeBefore.nextSibling;
|
|
@@ -1305,13 +1600,100 @@ class ExprPath {
|
|
|
1305
1600
|
getParentNode() { // Same as this.parentNode
|
|
1306
1601
|
return this.nodeMarker.parentNode
|
|
1307
1602
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1603
|
+
|
|
1604
|
+
/**
|
|
1605
|
+
* Get an unused NodeGroup that matches the template's html and expressions (exact=true)
|
|
1606
|
+
* or at least the html (exact=false).
|
|
1607
|
+
* Remove it from nodeGroupsFree if it exists, or create it if not.
|
|
1608
|
+
* Then add it to nodeGroupsInUse.
|
|
1609
|
+
*
|
|
1610
|
+
* @param template {Template}
|
|
1611
|
+
* @param exact {boolean}
|
|
1612
|
+
* If true, return an exact match, or null.
|
|
1613
|
+
* If false, either find a match for the template's html and then apply the template's expressions,
|
|
1614
|
+
* or createa new NodeGroup from the template.
|
|
1615
|
+
* @return {NodeGroup} */
|
|
1616
|
+
getNodeGroup(template, exact=true) {
|
|
1617
|
+
//if (exact && this.nodeGroupsFree.isEmpty())
|
|
1618
|
+
// return null;
|
|
1619
|
+
|
|
1620
|
+
let result;
|
|
1621
|
+
|
|
1622
|
+
if (exact) {
|
|
1623
|
+
result = this.nodeGroupsFree.delete(template.getExactKey());
|
|
1624
|
+
if (result) // also delete the matching close key.
|
|
1625
|
+
this.nodeGroupsFree.delete(template.getCloseKey(), result);
|
|
1626
|
+
else
|
|
1627
|
+
return null;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// Find a close match.
|
|
1631
|
+
// This is a match that has matching html, but different expressions applied.
|
|
1632
|
+
// We can then apply the expressions to make it an exact match.
|
|
1633
|
+
else {
|
|
1634
|
+
result = this.nodeGroupsFree.delete(template.getCloseKey());
|
|
1635
|
+
if (result) {
|
|
1636
|
+
/*#IFDEV*/assert(result.exactKey);/*#ENDIF*/
|
|
1637
|
+
this.nodeGroupsFree.delete(result.exactKey, result);
|
|
1638
|
+
|
|
1639
|
+
// Update this close match with the new expression values.
|
|
1640
|
+
result.applyExprs(template.exprs);
|
|
1641
|
+
result.exactKey = template.getExactKey(); // TODO: Should this be set elsewhere?
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
if (!result)
|
|
1646
|
+
result = new NodeGroup(template, this);
|
|
1647
|
+
|
|
1648
|
+
// old:
|
|
1649
|
+
this.nodeGroupsInUse.push(result);
|
|
1650
|
+
|
|
1651
|
+
// new:
|
|
1652
|
+
// let ngiu = this.nodeGroupsInUse;
|
|
1653
|
+
// ngiu.add(result.exactKey, result);
|
|
1654
|
+
// ngiu.add(result.closeKey, result);
|
|
1655
|
+
|
|
1656
|
+
/*#IFDEV*/assert(result.parentPath);/*#ENDIF*/
|
|
1657
|
+
return result;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
|
|
1661
|
+
/**
|
|
1662
|
+
* Used with getNodeGroup() and freeNodeGroups().
|
|
1663
|
+
* TODO: Use an array of WeakRef so the gc can collect them?
|
|
1664
|
+
* TODO: Put items back in nodeGroupsInUse after applyExpr() is called, not before.
|
|
1665
|
+
* @type {NodeGroup[]} */
|
|
1666
|
+
nodeGroupsInUse = [];
|
|
1667
|
+
|
|
1668
|
+
/** @type {MultiValueMap<key:string, value:NodeGroup>} */
|
|
1669
|
+
//nodeGroupsInUse = new MultiValueMap();
|
|
1670
|
+
|
|
1671
|
+
/**
|
|
1672
|
+
* Used with getNodeGroup() and freeNodeGroups().
|
|
1673
|
+
* Each NodeGroup is here twice, once under an exact key, and once under the close key.
|
|
1674
|
+
* @type {MultiValueMap<key:string, value:NodeGroup>} */
|
|
1675
|
+
nodeGroupsFree = new MultiValueMap();
|
|
1676
|
+
|
|
1677
|
+
|
|
1678
|
+
/**
|
|
1679
|
+
* Move everything from this.nodeGroupsInUse to this.nodeGroupsFree.
|
|
1680
|
+
* TODO: this could run as needed in getNodeGroup? */
|
|
1681
|
+
freeNodeGroups() {
|
|
1682
|
+
// old:
|
|
1683
|
+
let ngf = this.nodeGroupsFree;
|
|
1684
|
+
for (let ng of this.nodeGroupsInUse) {
|
|
1685
|
+
ngf.add(ng.exactKey, ng);
|
|
1686
|
+
ngf.add(ng.closeKey, ng);
|
|
1687
|
+
}
|
|
1688
|
+
this.nodeGroupsInUse = [];
|
|
1689
|
+
|
|
1690
|
+
// new:
|
|
1691
|
+
// for (let key in this.nodeGroupsFree.data)
|
|
1692
|
+
// for (let item of this.nodeGroupsFree.data[key])
|
|
1693
|
+
// this.nodeGroupsInUse.add(key, item);
|
|
1694
|
+
//
|
|
1695
|
+
// this.nodeGroupsFree = this.nodeGroupsInUse;
|
|
1696
|
+
// this.nodeGroupsInUse = new MultiValueMap();
|
|
1315
1697
|
}
|
|
1316
1698
|
|
|
1317
1699
|
//#IFDEV
|
|
@@ -1389,26 +1771,27 @@ function setValue(root, path, node) {
|
|
|
1389
1771
|
val = parseFloat(val);
|
|
1390
1772
|
|
|
1391
1773
|
delve(root, path, val);
|
|
1392
|
-
|
|
1393
|
-
//this.render();
|
|
1394
1774
|
}
|
|
1395
1775
|
|
|
1396
|
-
/** @enum {
|
|
1776
|
+
/** @enum {int} */
|
|
1397
1777
|
const PathType = {
|
|
1398
1778
|
/** Child of a node */
|
|
1399
|
-
Content:
|
|
1779
|
+
Content: 1,
|
|
1400
1780
|
|
|
1401
1781
|
/** One or more whole attributes */
|
|
1402
|
-
Multiple:
|
|
1782
|
+
Multiple: 2,
|
|
1403
1783
|
|
|
1404
1784
|
/** Value of an attribute. */
|
|
1405
|
-
Value:
|
|
1785
|
+
Value: 3,
|
|
1406
1786
|
|
|
1407
1787
|
/** Value of an attribute being passed to a component. */
|
|
1408
|
-
Component:
|
|
1788
|
+
Component: 4,
|
|
1409
1789
|
|
|
1410
1790
|
/** Expressions inside Html comments. */
|
|
1411
|
-
Comment:
|
|
1791
|
+
Comment: 5,
|
|
1792
|
+
|
|
1793
|
+
/** Value of an attribute. */
|
|
1794
|
+
Event: 6,
|
|
1412
1795
|
};
|
|
1413
1796
|
|
|
1414
1797
|
|
|
@@ -1434,28 +1817,24 @@ function resolveNodePath(root, path) {
|
|
|
1434
1817
|
for (let i=path.length-1; i>=0; i--)
|
|
1435
1818
|
root = root.childNodes[path[i]];
|
|
1436
1819
|
return root;
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
// TODO: Memory from this is never freed. Use a WeakMap<Node, Object<eventName:string, function[]>>
|
|
1441
|
-
let nodeEvents = {};
|
|
1442
|
-
let nodeEventArgs = {};
|
|
1820
|
+
}
|
|
1443
1821
|
|
|
1444
1822
|
/**
|
|
1445
1823
|
* A Shell is created from a tagged template expression instantiated as Nodes,
|
|
1446
|
-
* but without any expressions filled in.
|
|
1824
|
+
* but without any expressions filled in.
|
|
1825
|
+
* Only one Shell is created for all the items in a loop.
|
|
1826
|
+
*
|
|
1827
|
+
* When a NodeGroup is created from a Template's html strings,
|
|
1828
|
+
* the NodeGroup then clones the Shell's fragmentn to be its nodes. */
|
|
1447
1829
|
class Shell {
|
|
1448
1830
|
|
|
1449
1831
|
/**
|
|
1450
|
-
* @type {DocumentFragment}
|
|
1832
|
+
* @type {DocumentFragment} DOM parent of the shell nodes. */
|
|
1451
1833
|
fragment;
|
|
1452
1834
|
|
|
1453
1835
|
/** @type {ExprPath[]} Paths to where expressions should go. */
|
|
1454
1836
|
paths = [];
|
|
1455
1837
|
|
|
1456
|
-
/** @type {?Template} Template that created this element. */
|
|
1457
|
-
template;
|
|
1458
|
-
|
|
1459
1838
|
// Embeds and ids
|
|
1460
1839
|
events = [];
|
|
1461
1840
|
|
|
@@ -1467,6 +1846,7 @@ class Shell {
|
|
|
1467
1846
|
staticComponents = [];
|
|
1468
1847
|
|
|
1469
1848
|
|
|
1849
|
+
|
|
1470
1850
|
/**
|
|
1471
1851
|
* Create the nodes but without filling in the expressions.
|
|
1472
1852
|
* This is useful because the expression-less nodes created by a template can be cached.
|
|
@@ -1560,7 +1940,9 @@ class Shell {
|
|
|
1560
1940
|
let parts = attr.value.split(/[\ue000-\uf8ff]/g);
|
|
1561
1941
|
if (parts.length > 1) {
|
|
1562
1942
|
let nonEmptyParts = (parts.length === 2 && !parts[0].length && !parts[1].length) ? null : parts;
|
|
1563
|
-
|
|
1943
|
+
let type = isEvent(attr.name) ? PathType.Event : PathType.Value;
|
|
1944
|
+
|
|
1945
|
+
this.paths.push(new ExprPath(null, node, type, attr.name, nonEmptyParts));
|
|
1564
1946
|
node.setAttribute(attr.name, parts.join(''));
|
|
1565
1947
|
}
|
|
1566
1948
|
}
|
|
@@ -1572,7 +1954,7 @@ class Shell {
|
|
|
1572
1954
|
// Get or create nodeBefore.
|
|
1573
1955
|
let nodeBefore = node.previousSibling; // Can be the same as another Path's nodeMarker.
|
|
1574
1956
|
if (!nodeBefore) {
|
|
1575
|
-
nodeBefore = document.createComment('
|
|
1957
|
+
nodeBefore = document.createComment('ExprPath:'+this.paths.length);
|
|
1576
1958
|
node.parentNode.insertBefore(nodeBefore, node);
|
|
1577
1959
|
}
|
|
1578
1960
|
/*#IFDEV*/assert(nodeBefore);/*#ENDIF*/
|
|
@@ -1588,16 +1970,14 @@ class Shell {
|
|
|
1588
1970
|
// Re-use existing comment placeholder.
|
|
1589
1971
|
else {
|
|
1590
1972
|
nodeMarker = node;
|
|
1591
|
-
nodeMarker.textContent = '
|
|
1973
|
+
nodeMarker.textContent = 'ExprPathEnd:'+ this.paths.length;
|
|
1592
1974
|
}
|
|
1593
1975
|
/*#IFDEV*/assert(nodeMarker);/*#ENDIF*/
|
|
1594
1976
|
|
|
1595
1977
|
|
|
1596
1978
|
|
|
1597
1979
|
let path = new ExprPath(nodeBefore, nodeMarker, PathType.Content);
|
|
1598
|
-
|
|
1599
|
-
path.parentIndex = this.paths.length; // For debugging.
|
|
1600
|
-
//#ENDIF
|
|
1980
|
+
|
|
1601
1981
|
this.paths.push(path);
|
|
1602
1982
|
}
|
|
1603
1983
|
|
|
@@ -1611,9 +1991,6 @@ class Shell {
|
|
|
1611
1991
|
for (let i=0; i<parts.length-1; i++) {
|
|
1612
1992
|
let path = new ExprPath(node.previousSibling, node);
|
|
1613
1993
|
path.type = PathType.Comment;
|
|
1614
|
-
//#IFDEV
|
|
1615
|
-
path.parentIndex = i; // For debugging.
|
|
1616
|
-
//#ENDIF
|
|
1617
1994
|
this.paths.push(path);
|
|
1618
1995
|
}
|
|
1619
1996
|
}
|
|
@@ -1633,9 +2010,6 @@ class Shell {
|
|
|
1633
2010
|
|
|
1634
2011
|
for (let i=0, node; node=placeholders[i]; i++) {
|
|
1635
2012
|
let path = new ExprPath(node.previousSibling, node, PathType.Content);
|
|
1636
|
-
//#IFDEV
|
|
1637
|
-
path.parentIndex = i; // For debugging.
|
|
1638
|
-
//#ENDIF
|
|
1639
2013
|
this.paths.push(path);
|
|
1640
2014
|
|
|
1641
2015
|
/*#IFDEV*/path.verify();/*#ENDIF*/
|
|
@@ -1666,22 +2040,25 @@ class Shell {
|
|
|
1666
2040
|
path.nodeMarkerPath = getNodePath(path.nodeMarker);
|
|
1667
2041
|
|
|
1668
2042
|
// Cache so we don't have to calculate this later inside NodeGroup.applyExprs()
|
|
1669
|
-
if (path.type === PathType.Value && path.nodeMarker.nodeType === 1 &&
|
|
2043
|
+
if (path.type === PathType.Value && path.nodeMarker.nodeType === 1 && /*path.nodeMarker !== template.content.children[0] &&*/
|
|
1670
2044
|
(path.nodeMarker.tagName.includes('-') || path.nodeMarker.hasAttribute('is'))) {
|
|
1671
2045
|
path.type = PathType.Component;
|
|
1672
2046
|
}
|
|
1673
2047
|
}
|
|
1674
2048
|
|
|
1675
|
-
|
|
1676
2049
|
this.findEmbeds();
|
|
1677
2050
|
|
|
1678
|
-
|
|
1679
2051
|
/*#IFDEV*/this.verify();/*#ENDIF*/
|
|
1680
2052
|
} // end constructor
|
|
1681
2053
|
|
|
1682
2054
|
/**
|
|
1683
2055
|
* We find the path to every embed here once in the Shell, instead of every time a NodeGroup is instantiated.
|
|
1684
|
-
* When a Nodegroup is created, it calls NodeGroup.activateEmbeds() that uses these paths.
|
|
2056
|
+
* When a Nodegroup is created, it calls NodeGroup.activateEmbeds() that uses these paths.
|
|
2057
|
+
* Populates:
|
|
2058
|
+
* this.scripts
|
|
2059
|
+
* this.styles
|
|
2060
|
+
* this.ids
|
|
2061
|
+
* this.staticComponents */
|
|
1685
2062
|
findEmbeds() {
|
|
1686
2063
|
this.scripts = Array.prototype.map.call(this.fragment.querySelectorAll('scripts'), el => getNodePath(el));
|
|
1687
2064
|
this.styles = Array.prototype.map.call(this.fragment.querySelectorAll('style'), el => getNodePath(el));
|
|
@@ -1693,7 +2070,7 @@ class Shell {
|
|
|
1693
2070
|
for (let el of idEls) {
|
|
1694
2071
|
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
1695
2072
|
if (div.hasOwnProperty(id))
|
|
1696
|
-
throw new Error(`<${el.tagName.toLowerCase()} id="${id}"> can't override existing HTMLElement property.`)
|
|
2073
|
+
throw new Error(`<${el.tagName.toLowerCase()} id="${id}"> can't override existing HTMLElement id property.`)
|
|
1697
2074
|
}
|
|
1698
2075
|
|
|
1699
2076
|
|
|
@@ -1718,263 +2095,31 @@ class Shell {
|
|
|
1718
2095
|
|
|
1719
2096
|
/**
|
|
1720
2097
|
* Get the shell for the html strings.
|
|
1721
|
-
* @param htmlStrings {string[]}
|
|
2098
|
+
* @param htmlStrings {string[]} Typically comes from a Template.
|
|
1722
2099
|
* @returns {Shell} */
|
|
1723
|
-
static get(htmlStrings) {
|
|
1724
|
-
let result = shells.get(htmlStrings);
|
|
1725
|
-
if (!result) {
|
|
1726
|
-
result = new Shell(htmlStrings);
|
|
1727
|
-
shells.set(htmlStrings, result); // cache
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
/*#IFDEV*/result.verify();/*#ENDIF*/
|
|
1731
|
-
return result;
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
//#IFDEV
|
|
1735
|
-
// For debugging only:
|
|
1736
|
-
verify() {
|
|
1737
|
-
for (let path of this.paths) {
|
|
1738
|
-
assert(this.fragment.contains(path.getParentNode()));
|
|
1739
|
-
path.verify();
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
//#ENDIF
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
let shells = new WeakMap();
|
|
1746
|
-
|
|
1747
|
-
/**
|
|
1748
|
-
* ISC License
|
|
1749
|
-
*
|
|
1750
|
-
* Copyright (c) 2020, Andrea Giammarchi, @WebReflection
|
|
1751
|
-
*
|
|
1752
|
-
* Permission to use, copy, modify, and/or distribute this software for any
|
|
1753
|
-
* purpose with or without fee is hereby granted, provided that the above
|
|
1754
|
-
* copyright notice and this permission notice appear in all copies.
|
|
1755
|
-
*
|
|
1756
|
-
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
1757
|
-
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
1758
|
-
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
1759
|
-
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
1760
|
-
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
|
1761
|
-
* OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
1762
|
-
* PERFORMANCE OF THIS SOFTWARE.
|
|
1763
|
-
*/
|
|
1764
|
-
|
|
1765
|
-
/**
|
|
1766
|
-
* @param {Node} parentNode The container where children live
|
|
1767
|
-
* @param {Node[]} a The list of current/live children
|
|
1768
|
-
* @param {Node[]} b The list of future children
|
|
1769
|
-
* @param {(entry: Node, action: number) => Node} get
|
|
1770
|
-
* The callback invoked per each entry related DOM operation.
|
|
1771
|
-
* @param {Node} [before] The optional node used as anchor to insert before.
|
|
1772
|
-
* @returns {Node[]} The same list of future children.
|
|
1773
|
-
*/
|
|
1774
|
-
const udomdiff = (parentNode, a, b, before) => {
|
|
1775
|
-
//#IFDEV
|
|
1776
|
-
// if (parentNode instanceof ExprPath)
|
|
1777
|
-
// parentNode.verify();
|
|
1778
|
-
//#ENDIF
|
|
1779
|
-
|
|
1780
|
-
const bLength = b.length;
|
|
1781
|
-
let aEnd = a.length;
|
|
1782
|
-
let bEnd = bLength;
|
|
1783
|
-
let aStart = 0;
|
|
1784
|
-
let bStart = 0;
|
|
1785
|
-
let map = null;
|
|
1786
|
-
while (aStart < aEnd || bStart < bEnd) {
|
|
1787
|
-
// append head, tail, or nodes in between: fast path
|
|
1788
|
-
if (aEnd === aStart) {
|
|
1789
|
-
// we could be in a situation where the rest of nodes that
|
|
1790
|
-
// need to be added are not at the end, and in such case
|
|
1791
|
-
// the node to `insertBefore`, if the index is more than 0
|
|
1792
|
-
// must be retrieved, otherwise it's gonna be the first item.
|
|
1793
|
-
const node = bEnd < bLength
|
|
1794
|
-
? (bStart
|
|
1795
|
-
? (b[bStart - 1].nextSibling)
|
|
1796
|
-
: b[bEnd - bStart])
|
|
1797
|
-
: before;
|
|
1798
|
-
while (bStart < bEnd) {
|
|
1799
|
-
let bNode = b[bStart++];
|
|
1800
|
-
parentNode.insertBefore(bNode, node);
|
|
1801
|
-
|
|
1802
|
-
//#IFDEV
|
|
1803
|
-
if (bNode instanceof NodeGroup)
|
|
1804
|
-
bNode.verify();
|
|
1805
|
-
// if (parentNode instanceof ExprPath)
|
|
1806
|
-
// parentNode.verify();
|
|
1807
|
-
//#ENDIF
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1810
|
-
// remove head or tail: fast path
|
|
1811
|
-
else if (bEnd === bStart) {
|
|
1812
|
-
while (aStart < aEnd) {
|
|
1813
|
-
// remove the node only if it's unknown or not live
|
|
1814
|
-
let aNode = a[aStart];
|
|
1815
|
-
if (!map || !map.has(aNode)) {
|
|
1816
|
-
parentNode.removeChild(aNode);
|
|
1817
|
-
|
|
1818
|
-
//#IFDEV
|
|
1819
|
-
if (aNode instanceof NodeGroup)
|
|
1820
|
-
aNode.verify();
|
|
1821
|
-
// if (parentNode instanceof ExprPath)
|
|
1822
|
-
// parentNode.verify();
|
|
1823
|
-
//#ENDIF
|
|
1824
|
-
}
|
|
1825
|
-
aStart++;
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
// same node: fast path
|
|
1829
|
-
else if (a[aStart] === b[bStart]) {
|
|
1830
|
-
aStart++;
|
|
1831
|
-
bStart++;
|
|
1832
|
-
}
|
|
1833
|
-
// same tail: fast path
|
|
1834
|
-
else if (a[aEnd - 1] === b[bEnd - 1]) {
|
|
1835
|
-
aEnd--;
|
|
1836
|
-
bEnd--;
|
|
1837
|
-
}
|
|
1838
|
-
// The once here single last swap "fast path" has been removed in v1.1.0
|
|
1839
|
-
// https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85
|
|
1840
|
-
// reverse swap: also fast path
|
|
1841
|
-
else if (
|
|
1842
|
-
a[aStart] === b[bEnd - 1] &&
|
|
1843
|
-
b[bStart] === a[aEnd - 1]
|
|
1844
|
-
) {
|
|
1845
|
-
// this is a "shrink" operation that could happen in these cases:
|
|
1846
|
-
// [1, 2, 3, 4, 5]
|
|
1847
|
-
// [1, 4, 3, 2, 5]
|
|
1848
|
-
// or asymmetric too
|
|
1849
|
-
// [1, 2, 3, 4, 5]
|
|
1850
|
-
// [1, 2, 3, 5, 6, 4]
|
|
1851
|
-
const node = a[--aEnd].nextSibling;
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
let a2 = b[bStart++];
|
|
1855
|
-
let b2 = a[aStart++];
|
|
1856
|
-
parentNode.insertBefore(
|
|
1857
|
-
a2,
|
|
1858
|
-
b2.nextSibling
|
|
1859
|
-
);
|
|
1860
|
-
//#IFDEV
|
|
1861
|
-
if (a2 instanceof NodeGroup)
|
|
1862
|
-
a2.verify();
|
|
1863
|
-
// if (parentNode instanceof ExprPath)
|
|
1864
|
-
// parentNode.verify();
|
|
1865
|
-
//#ENDIF
|
|
1866
|
-
|
|
1867
|
-
let bNode = b[--bEnd];
|
|
1868
|
-
parentNode.insertBefore(bNode, node);
|
|
1869
|
-
|
|
1870
|
-
//#IFDEV
|
|
1871
|
-
if (bNode instanceof NodeGroup)
|
|
1872
|
-
bNode.verify();
|
|
1873
|
-
// if (parentNode instanceof ExprPath)
|
|
1874
|
-
// parentNode.verify();
|
|
1875
|
-
|
|
1876
|
-
//#ENDIF
|
|
1877
|
-
|
|
1878
|
-
// mark the future index as identical (yeah, it's dirty, but cheap 👍)
|
|
1879
|
-
// The main reason to do this, is that when a[aEnd] will be reached,
|
|
1880
|
-
// the loop will likely be on the fast path, as identical to b[bEnd].
|
|
1881
|
-
// In the best case scenario, the next loop will skip the tail,
|
|
1882
|
-
// but in the worst one, this node will be considered as already
|
|
1883
|
-
// processed, bailing out pretty quickly from the map index check
|
|
1884
|
-
a[aEnd] = b[bEnd];
|
|
1885
|
-
}
|
|
1886
|
-
// map based fallback, "slow" path
|
|
1887
|
-
else {
|
|
1888
|
-
// the map requires an O(bEnd - bStart) operation once
|
|
1889
|
-
// to store all future nodes indexes for later purposes.
|
|
1890
|
-
// In the worst case scenario, this is a full O(N) cost,
|
|
1891
|
-
// and such scenario happens at least when all nodes are different,
|
|
1892
|
-
// but also if both first and last items of the lists are different
|
|
1893
|
-
if (!map) {
|
|
1894
|
-
map = new Map;
|
|
1895
|
-
let i = bStart;
|
|
1896
|
-
while (i < bEnd)
|
|
1897
|
-
map.set(b[i], i++);
|
|
1898
|
-
}
|
|
1899
|
-
// if it's a future node, hence it needs some handling
|
|
1900
|
-
if (map.has(a[aStart])) {
|
|
1901
|
-
// grab the index of such node, 'cause it might have been processed
|
|
1902
|
-
const index = map.get(a[aStart]);
|
|
1903
|
-
// if it's not already processed, look on demand for the next LCS
|
|
1904
|
-
if (bStart < index && index < bEnd) {
|
|
1905
|
-
let i = aStart;
|
|
1906
|
-
// counts the amount of nodes that are the same in the future
|
|
1907
|
-
let sequence = 1;
|
|
1908
|
-
while (++i < aEnd && i < bEnd && map.get(a[i]) === (index + sequence))
|
|
1909
|
-
sequence++;
|
|
1910
|
-
// effort decision here: if the sequence is longer than replaces
|
|
1911
|
-
// needed to reach such sequence, which would brings again this loop
|
|
1912
|
-
// to the fast path, prepend the difference before a sequence,
|
|
1913
|
-
// and move only the future list index forward, so that aStart
|
|
1914
|
-
// and bStart will be aligned again, hence on the fast path.
|
|
1915
|
-
// An example considering aStart and bStart are both 0:
|
|
1916
|
-
// a: [1, 2, 3, 4]
|
|
1917
|
-
// b: [7, 1, 2, 3, 6]
|
|
1918
|
-
// this would place 7 before 1 and, from that time on, 1, 2, and 3
|
|
1919
|
-
// will be processed at zero cost
|
|
1920
|
-
if (sequence > (index - bStart)) {
|
|
1921
|
-
const node = a[aStart];
|
|
1922
|
-
while (bStart < index) {
|
|
1923
|
-
let bNode = b[bStart++];
|
|
1924
|
-
parentNode.insertBefore(bNode, node);
|
|
1925
|
-
|
|
1926
|
-
//#IFDEV
|
|
1927
|
-
if (bNode instanceof NodeGroup)
|
|
1928
|
-
bNode.verify();
|
|
1929
|
-
// if (parentNode instanceof ExprPath)
|
|
1930
|
-
// parentNode.verify();
|
|
1931
|
-
|
|
1932
|
-
//#ENDIF
|
|
1933
|
-
}
|
|
1934
|
-
}
|
|
1935
|
-
// if the effort wasn't good enough, fallback to a replace,
|
|
1936
|
-
// moving both source and target indexes forward, hoping that some
|
|
1937
|
-
// similar node will be found later on, to go back to the fast path
|
|
1938
|
-
else {
|
|
1939
|
-
let aNode = a[aStart++];
|
|
1940
|
-
let bNode = b[bStart++];
|
|
1941
|
-
parentNode.replaceChild(
|
|
1942
|
-
bNode,
|
|
1943
|
-
aNode
|
|
1944
|
-
);
|
|
1945
|
-
|
|
1946
|
-
//#IFDEV
|
|
1947
|
-
if (aNode instanceof NodeGroup)
|
|
1948
|
-
aNode.verify();
|
|
1949
|
-
// if (parentNode instanceof ExprPath)
|
|
1950
|
-
// parentNode.verify();
|
|
1951
|
-
//#ENDIF
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
1954
|
-
// otherwise move the source forward, 'cause there's nothing to do
|
|
1955
|
-
else
|
|
1956
|
-
aStart++;
|
|
1957
|
-
}
|
|
1958
|
-
// this node has no meaning in the future list, so it's more than safe
|
|
1959
|
-
// to remove it, and check the next live node out instead, meaning
|
|
1960
|
-
// that only the live list index should be forwarded
|
|
1961
|
-
else {
|
|
1962
|
-
let aNode = a[aStart++];
|
|
1963
|
-
parentNode.removeChild(aNode);
|
|
2100
|
+
static get(htmlStrings) {
|
|
2101
|
+
let result = Globals.shells.get(htmlStrings);
|
|
2102
|
+
if (!result) {
|
|
2103
|
+
result = new Shell(htmlStrings);
|
|
2104
|
+
Globals.shells.set(htmlStrings, result); // cache
|
|
2105
|
+
}
|
|
1964
2106
|
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
2107
|
+
/*#IFDEV*/result.verify();/*#ENDIF*/
|
|
2108
|
+
return result;
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
//#IFDEV
|
|
2112
|
+
// For debugging only:
|
|
2113
|
+
verify() {
|
|
2114
|
+
for (let path of this.paths) {
|
|
2115
|
+
assert(this.fragment.contains(path.getParentNode()));
|
|
2116
|
+
path.verify();
|
|
1972
2117
|
}
|
|
1973
2118
|
}
|
|
1974
|
-
|
|
1975
|
-
}
|
|
2119
|
+
//#ENDIF
|
|
2120
|
+
}
|
|
1976
2121
|
|
|
1977
|
-
/** @typedef {boolean|string|number|function|Object|Array|Date|Node} Expr */
|
|
2122
|
+
/** @typedef {boolean|string|number|function|Object|Array|Date|Node|Template} Expr */
|
|
1978
2123
|
|
|
1979
2124
|
/**
|
|
1980
2125
|
* A group of Nodes instantiated from a Shell, with Expr's filled in.
|
|
@@ -1987,16 +2132,17 @@ const udomdiff = (parentNode, a, b, before) => {
|
|
|
1987
2132
|
* */
|
|
1988
2133
|
class NodeGroup {
|
|
1989
2134
|
|
|
1990
|
-
/**
|
|
1991
|
-
|
|
2135
|
+
/**
|
|
2136
|
+
* @Type {RootNodeGroup} */
|
|
2137
|
+
rootNg;
|
|
1992
2138
|
|
|
1993
2139
|
/** @type {ExprPath} */
|
|
1994
2140
|
parentPath;
|
|
1995
2141
|
|
|
1996
|
-
/** @type {Node} First node of NodeGroup. Should never be null. */
|
|
2142
|
+
/** @type {Node|HTMLElement} First node of NodeGroup. Should never be null. */
|
|
1997
2143
|
startNode;
|
|
1998
2144
|
|
|
1999
|
-
/** @type {Node} A node that never changes that this NodeGroup should always insert its nodes before.
|
|
2145
|
+
/** @type {Node|HTMLElement} A node that never changes that this NodeGroup should always insert its nodes before.
|
|
2000
2146
|
* An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.*/
|
|
2001
2147
|
endNode;
|
|
2002
2148
|
|
|
@@ -2006,13 +2152,9 @@ class NodeGroup {
|
|
|
2006
2152
|
/** @type {string} Key that matches the template and the expressions. */
|
|
2007
2153
|
exactKey;
|
|
2008
2154
|
|
|
2009
|
-
/** @type {string} Key that only matches the template.
|
|
2155
|
+
/** @type {string} Key that only matches the template. */
|
|
2010
2156
|
closeKey;
|
|
2011
2157
|
|
|
2012
|
-
/** @type {boolean} Used by NodeGroupManager. */
|
|
2013
|
-
inUse;
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
2158
|
/**
|
|
2017
2159
|
* @internal
|
|
2018
2160
|
* @type {Node[]} Cached result of getNodes() used only for improving performance.*/
|
|
@@ -2022,137 +2164,58 @@ class NodeGroup {
|
|
|
2022
2164
|
* @type {?Map<HTMLStyleElement, string>} */
|
|
2023
2165
|
styles;
|
|
2024
2166
|
|
|
2025
|
-
/**
|
|
2026
|
-
* If rendering a Template with replaceMode=true, pseudoRoot points to the element where the attributes are rendered.
|
|
2027
|
-
* But pseudoRoot is outside of this.getNodes().
|
|
2028
|
-
* NodeGroupManager.render() copies the attributes from pseudoRoot to the actual web component root element.
|
|
2029
|
-
* @type {?HTMLElement} */
|
|
2030
|
-
pseudoRoot;
|
|
2031
|
-
|
|
2032
2167
|
currentComponentProps = {};
|
|
2033
2168
|
|
|
2034
2169
|
|
|
2035
2170
|
/**
|
|
2036
2171
|
* Create an "instantiated" NodeGroup from a Template and add it to an element.
|
|
2037
2172
|
* @param template {Template} Create it from the html strings and expressions in this template.
|
|
2038
|
-
* @param
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
/** @type {Template} */
|
|
2043
|
-
this.template = template;
|
|
2044
|
-
|
|
2045
|
-
/** @type {NodeGroupManager} */
|
|
2046
|
-
this.manager = manager;
|
|
2047
|
-
|
|
2048
|
-
// new!
|
|
2049
|
-
template.nodeGroup = this;
|
|
2050
|
-
|
|
2051
|
-
// Get a cached version of the parsed and instantiated html, and ExprPaths.
|
|
2052
|
-
let shell = Shell.get(template.html);
|
|
2173
|
+
* @param parentPath {?ExprPath} */
|
|
2174
|
+
constructor(template, parentPath=null) {
|
|
2175
|
+
if (!(this instanceof RootNodeGroup)) {
|
|
2176
|
+
let [fragment, shell] = this.init(template, parentPath);
|
|
2053
2177
|
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
// Figure out value of replaceMode option if it isn't set,
|
|
2057
|
-
// Assume replaceMode if there's only one child element and its tagname matches the root el.
|
|
2058
|
-
let replaceMode = typeof template.replaceMode === 'boolean'
|
|
2059
|
-
? template.replaceMode
|
|
2060
|
-
: fragment.children.length===1 &&
|
|
2061
|
-
fragment.firstElementChild?.tagName.replace(/-SOLARITE-PLACEHOLDER$/, '')
|
|
2062
|
-
=== manager?.rootEl?.tagName;
|
|
2063
|
-
if (replaceMode) {
|
|
2064
|
-
this.pseudoRoot = fragment.firstElementChild;
|
|
2065
|
-
// if (!manager.rootEl)
|
|
2066
|
-
// manager.rootEl = this.pseudoRoot;
|
|
2067
|
-
/*#IFDEV*/assert(this.pseudoRoot);/*#ENDIF*/
|
|
2068
|
-
}
|
|
2069
|
-
|
|
2070
|
-
let childNodes = replaceMode
|
|
2071
|
-
? fragment.firstElementChild.childNodes
|
|
2072
|
-
: fragment.childNodes;
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
this.startNode = childNodes[0];
|
|
2076
|
-
this.endNode = childNodes[childNodes.length - 1];
|
|
2178
|
+
this.updatePaths(fragment, shell.paths);
|
|
2077
2179
|
|
|
2180
|
+
this.activateEmbeds(fragment, shell);
|
|
2078
2181
|
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
let path = oldPath.clone(fragment);
|
|
2082
|
-
path.parentNg = this;
|
|
2083
|
-
this.paths.push(path);
|
|
2182
|
+
// Apply exprs
|
|
2183
|
+
this.applyExprs(template.exprs);
|
|
2084
2184
|
}
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
// Update web component placeholders.
|
|
2088
|
-
// Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
|
|
2089
|
-
// Is this list needed at all?
|
|
2090
|
-
//for (let component of shell.components)
|
|
2091
|
-
// this.components.push(resolveNodePath(this.startNode.parentNode, getNodePath(component)))
|
|
2092
|
-
|
|
2093
|
-
/*#IFDEV*/this.verify();/*#ENDIF*/
|
|
2094
|
-
|
|
2095
|
-
this.activateEmbeds(fragment, shell);
|
|
2096
|
-
|
|
2097
|
-
/*#IFDEV*/
|
|
2098
|
-
assert(this.paths.length <= template.exprs.length);
|
|
2099
|
-
if (template.exprs.length)
|
|
2100
|
-
assert(this.paths.length);
|
|
2101
|
-
/*#ENDIF*/
|
|
2102
|
-
|
|
2103
|
-
// Apply exprs
|
|
2104
|
-
this.applyExprs(template.exprs);
|
|
2105
|
-
|
|
2106
|
-
/*#IFDEV*/this.verify();/*#ENDIF*/
|
|
2107
2185
|
}
|
|
2108
2186
|
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2187
|
+
/**
|
|
2188
|
+
* Common init shared by RootNodeGroup and NodeGroup constructors.
|
|
2189
|
+
* But in a separate function because they need to do this at a different step.
|
|
2190
|
+
* @param template {Template} Create it from the html strings and expressions in this template.
|
|
2191
|
+
* @param parentPath {?ExprPath}
|
|
2192
|
+
* @param exactKey {?string} Optional, if already calculated.
|
|
2193
|
+
* @param closeKey {?string}
|
|
2194
|
+
* @returns {[DocumentFragment, Shell]} */
|
|
2195
|
+
init(template, parentPath=null, exactKey=null, closeKey=null) {
|
|
2196
|
+
this.exactKey = exactKey || template.getExactKey();
|
|
2197
|
+
this.closeKey = closeKey || template.getCloseKey();
|
|
2115
2198
|
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
this.createNewComponent(el);
|
|
2119
|
-
}
|
|
2199
|
+
this.parentPath = parentPath;
|
|
2200
|
+
this.rootNg = parentPath?.parentNg?.rootNg || this;
|
|
2120
2201
|
|
|
2121
|
-
|
|
2202
|
+
/*#IFDEV*/assert(this.rootNg);/*#ENDIF*/
|
|
2122
2203
|
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
for (let path of shell.ids) {
|
|
2126
|
-
let el = resolveNodePath(root, path);
|
|
2127
|
-
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
2204
|
+
/** @type {Template} */
|
|
2205
|
+
this.template = template;
|
|
2128
2206
|
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
throw new Error(`${this.manager.rootEl.constructor.name}.${id} already has a value. `+
|
|
2132
|
-
`Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
|
|
2207
|
+
// new! Is this needed?
|
|
2208
|
+
template.nodeGroup = this;
|
|
2133
2209
|
|
|
2134
|
-
|
|
2135
|
-
|
|
2210
|
+
// Get a cached version of the parsed and instantiated html, and ExprPaths.
|
|
2211
|
+
let shell = Shell.get(template.html);
|
|
2212
|
+
let fragment = shell.fragment.cloneNode(true);
|
|
2136
2213
|
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
this.styles = new Map();
|
|
2141
|
-
for (let path of shell.styles) {
|
|
2142
|
-
let style = resolveNodePath(root, path);
|
|
2143
|
-
Util.bindStyles(style, this.manager.rootEl);
|
|
2144
|
-
this.styles.set(style, style.textContent);
|
|
2145
|
-
}
|
|
2214
|
+
let childNodes = fragment.childNodes;
|
|
2215
|
+
this.startNode = childNodes[0];
|
|
2216
|
+
this.endNode = childNodes[childNodes.length - 1];
|
|
2146
2217
|
|
|
2147
|
-
|
|
2148
|
-
// scripts
|
|
2149
|
-
if (this.manager.options.scripts !== false) {
|
|
2150
|
-
for (let path of shell.scripts) {
|
|
2151
|
-
let script = resolveNodePath(root, path);
|
|
2152
|
-
eval(script.textContent);
|
|
2153
|
-
}
|
|
2154
|
-
}
|
|
2155
|
-
}
|
|
2218
|
+
return [fragment, shell];
|
|
2156
2219
|
}
|
|
2157
2220
|
|
|
2158
2221
|
/**
|
|
@@ -2162,8 +2225,9 @@ class NodeGroup {
|
|
|
2162
2225
|
* @param paths {?ExprPath[]} Optional. */
|
|
2163
2226
|
applyExprs(exprs, paths=null) {
|
|
2164
2227
|
paths = paths || this.paths;
|
|
2165
|
-
|
|
2228
|
+
|
|
2166
2229
|
/*#IFDEV*/this.verify();/*#ENDIF*/
|
|
2230
|
+
|
|
2167
2231
|
// Update exprs at paths.
|
|
2168
2232
|
let exprIndex = exprs.length-1, expr, lastNode;
|
|
2169
2233
|
|
|
@@ -2173,54 +2237,24 @@ class NodeGroup {
|
|
|
2173
2237
|
expr = exprs[exprIndex];
|
|
2174
2238
|
|
|
2175
2239
|
// Nodes
|
|
2176
|
-
if (path.type === PathType.Content) {
|
|
2177
|
-
this.applyNodeExpr(path, expr);
|
|
2178
|
-
/*#IFDEV*/path.verify();/*#ENDIF*/
|
|
2179
|
-
}
|
|
2180
2240
|
|
|
2181
|
-
//
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
// This is necessary both here and below.
|
|
2188
|
-
if (lastNode && lastNode !== this.pseudoRoot && lastNode !== node && Object.keys(this.currentComponentProps).length) {
|
|
2189
|
-
this.applyComponentExprs(lastNode, this.currentComponentProps);
|
|
2190
|
-
this.currentComponentProps = {};
|
|
2191
|
-
}
|
|
2192
|
-
|
|
2193
|
-
if (path.type === PathType.Multiple)
|
|
2194
|
-
path.applyMultipleAttribs(el, expr);
|
|
2195
|
-
|
|
2196
|
-
// Capture attribute expressions to later send to the constructor of a web component.
|
|
2197
|
-
// Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
|
|
2198
|
-
else if (path.nodeMarker !== this.pseudoRoot && path.type === PathType.Component)
|
|
2199
|
-
this.currentComponentProps[path.attrName] = expr;
|
|
2200
|
-
|
|
2201
|
-
else if (path.type === PathType.Comment) ;
|
|
2202
|
-
else {
|
|
2241
|
+
// This is necessary both here and below.
|
|
2242
|
+
if (lastNode && lastNode !== this.rootNg.root && lastNode !== path.nodeMarker && Object.keys(this.currentComponentProps).length) {
|
|
2243
|
+
this.applyComponentExprs(lastNode, this.currentComponentProps);
|
|
2244
|
+
this.currentComponentProps = {};
|
|
2245
|
+
}
|
|
2203
2246
|
|
|
2204
|
-
|
|
2205
|
-
if (path.attrValue===null && (typeof expr === 'function' || Array.isArray(expr)) && isEvent(path.attrName)) {
|
|
2206
|
-
let root = this.manager?.rootEl || this.startNode.parentNode; // latter is used when constructing a whole element.
|
|
2207
|
-
path.applyEventAttrib(el, expr, root);
|
|
2208
|
-
}
|
|
2247
|
+
exprIndex = path.apply(expr, exprs, exprIndex, this.currentComponentProps);
|
|
2209
2248
|
|
|
2210
|
-
|
|
2211
|
-
else // One node value may have multiple expressions. Here we apply them all at once.
|
|
2212
|
-
exprIndex = path.applyValueAttrib(el, exprs, exprIndex);
|
|
2213
|
-
}
|
|
2249
|
+
lastNode = path.nodeMarker;
|
|
2214
2250
|
|
|
2215
|
-
lastNode = path.nodeMarker;
|
|
2216
|
-
}
|
|
2217
2251
|
|
|
2218
2252
|
exprIndex--;
|
|
2219
2253
|
} // end for(path of this.paths)
|
|
2220
2254
|
|
|
2221
2255
|
|
|
2222
2256
|
// Check again after we iterate through all paths to apply to a component.
|
|
2223
|
-
if (lastNode && lastNode !== this.
|
|
2257
|
+
if (lastNode && lastNode !== this.rootNg.root && Object.keys(this.currentComponentProps).length) {
|
|
2224
2258
|
this.applyComponentExprs(lastNode, this.currentComponentProps);
|
|
2225
2259
|
this.currentComponentProps = {};
|
|
2226
2260
|
}
|
|
@@ -2238,123 +2272,8 @@ class NodeGroup {
|
|
|
2238
2272
|
/*#IFDEV*/this.verify();/*#ENDIF*/
|
|
2239
2273
|
}
|
|
2240
2274
|
|
|
2241
|
-
applyExpr(path, expr) {
|
|
2242
|
-
// TODO: Use this if I can figure out how to adapt applyValueAttrib() to it.
|
|
2243
|
-
}
|
|
2244
|
-
|
|
2245
|
-
/**
|
|
2246
|
-
* Insert/replace the nodes created by a single expression.
|
|
2247
|
-
* Called by applyExprs()
|
|
2248
|
-
* This function is recursive, as the functions it calls also call it.
|
|
2249
|
-
* TODO: Move this to ExprPath?
|
|
2250
|
-
* @param path {ExprPath}
|
|
2251
|
-
* @param expr {Expr}
|
|
2252
|
-
* @return {Node[]} New Nodes created. */
|
|
2253
|
-
applyNodeExpr(path, expr) {
|
|
2254
|
-
/*#IFDEV*/path.verify();/*#ENDIF*/
|
|
2255
|
-
|
|
2256
|
-
/** @type {(Node|NodeGroup|Expr)[]} */
|
|
2257
|
-
let newNodes = [];
|
|
2258
|
-
let oldNodeGroups = path.nodeGroups;
|
|
2259
|
-
/*#IFDEV*/assert(!oldNodeGroups.includes(null));/*#ENDIF*/
|
|
2260
|
-
let secondPass = []; // indices
|
|
2261
|
-
|
|
2262
|
-
// First Pass
|
|
2263
|
-
//for (let ng of path.nodeGroups) // TODO: Is this necessary?
|
|
2264
|
-
// ng.parentPath = null;
|
|
2265
|
-
path.nodeGroups = [];
|
|
2266
|
-
path.apply(expr, newNodes, secondPass);
|
|
2267
|
-
this.existingTextNodes = null;
|
|
2268
|
-
|
|
2269
|
-
// TODO: Create an array of old vs Nodes and NodeGroups together.
|
|
2270
|
-
// If they're all the same, skip the next steps.
|
|
2271
|
-
// Or calculate it in the loop above as we go? Have a path.lastNodeGroups property?
|
|
2272
|
-
|
|
2273
|
-
// Second pass to find close-match NodeGroups.
|
|
2274
|
-
let flatten = false;
|
|
2275
|
-
if (secondPass.length) {
|
|
2276
|
-
for (let [nodesIndex, ngIndex] of secondPass) {
|
|
2277
|
-
let ng = this.manager.getNodeGroup(newNodes[nodesIndex], false);
|
|
2278
|
-
|
|
2279
|
-
ng.parentPath = path;
|
|
2280
|
-
let ngNodes = ng.getNodes();
|
|
2281
|
-
|
|
2282
|
-
/*#IFDEV*/assert(!(newNodes[nodesIndex] instanceof NodeGroup));/*#ENDIF*/
|
|
2283
|
-
|
|
2284
|
-
if (ngNodes.length === 1)
|
|
2285
|
-
newNodes[nodesIndex] = ngNodes[0];
|
|
2286
|
-
|
|
2287
|
-
else {
|
|
2288
|
-
newNodes[nodesIndex] = ngNodes;
|
|
2289
|
-
flatten = true;
|
|
2290
|
-
}
|
|
2291
|
-
path.nodeGroups[ngIndex] = ng;
|
|
2292
|
-
}
|
|
2293
|
-
|
|
2294
|
-
if (flatten)
|
|
2295
|
-
newNodes = newNodes.flat(); // TODO: Only if second pass happens?
|
|
2296
|
-
}
|
|
2297
|
-
|
|
2298
|
-
/*#IFDEV*/assert(!path.nodeGroups.includes(null));/*#ENDIF*/
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
let oldNodes = path.getNodes();
|
|
2303
|
-
path.nodesCache = newNodes; // Replaces value set by path.getNodes()
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
// This pre-check makes it a few percent faster?
|
|
2307
|
-
let diff = findArrayDiff(oldNodes, newNodes);
|
|
2308
|
-
if (diff !== false) {
|
|
2309
|
-
|
|
2310
|
-
if (this.parentPath)
|
|
2311
|
-
this.parentPath.clearNodesCache();
|
|
2312
|
-
|
|
2313
|
-
// Fast clear method
|
|
2314
|
-
let isNowEmpty = oldNodes.length && !newNodes.length;
|
|
2315
|
-
if (!isNowEmpty || !path.fastClear(oldNodes, newNodes))
|
|
2316
|
-
|
|
2317
|
-
// Rearrange nodes.
|
|
2318
|
-
udomdiff(path.parentNode, oldNodes, newNodes, path.nodeMarker);
|
|
2319
|
-
|
|
2320
|
-
this.saveOrphans(oldNodeGroups, oldNodes);
|
|
2321
|
-
}
|
|
2322
|
-
/*#IFDEV*/path.verify();/*#ENDIF*/
|
|
2323
|
-
}
|
|
2324
|
-
|
|
2325
|
-
/**
|
|
2326
|
-
* Find NodeGroups that had their nodes removed and add those nodes to a Fragment so
|
|
2327
|
-
* they're not lost forever and the NodeGroup's internal structure is still consistent.
|
|
2328
|
-
* Called from NodeGroup.applyNodeExpr().
|
|
2329
|
-
* @param oldNodeGroups {NodeGroup[]}
|
|
2330
|
-
* @param oldNodes {Node[]} */
|
|
2331
|
-
saveOrphans(oldNodeGroups, oldNodes) {
|
|
2332
|
-
let oldNgMap = new Map();
|
|
2333
|
-
for (let ng of oldNodeGroups) {
|
|
2334
|
-
oldNgMap.set(ng.startNode, ng);
|
|
2335
|
-
|
|
2336
|
-
// TODO: Is this necessary?
|
|
2337
|
-
// if (ng.parentPath)
|
|
2338
|
-
// ng.parentPath.clearNodesCache();
|
|
2339
|
-
}
|
|
2340
|
-
|
|
2341
|
-
for (let i=0, node; node = oldNodes[i]; i++) {
|
|
2342
|
-
let ng;
|
|
2343
|
-
if (!node.parentNode && (ng = oldNgMap.get(node))) {
|
|
2344
|
-
let fragment = document.createDocumentFragment();
|
|
2345
|
-
let endNode = ng.endNode;
|
|
2346
|
-
while (node !== endNode) {
|
|
2347
|
-
fragment.append(node);
|
|
2348
|
-
i++;
|
|
2349
|
-
node = oldNodes[i];
|
|
2350
|
-
}
|
|
2351
|
-
fragment.append(endNode);
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
}
|
|
2355
|
-
|
|
2356
2275
|
/**
|
|
2357
|
-
* Create a nested
|
|
2276
|
+
* Create a nested Component or call render with the new props.
|
|
2358
2277
|
* @param el {Solarite:HTMLElement}
|
|
2359
2278
|
* @param props {Object} */
|
|
2360
2279
|
applyComponentExprs(el, props) {
|
|
@@ -2372,14 +2291,14 @@ class NodeGroup {
|
|
|
2372
2291
|
if (isPreHtmlElement || isPreIsElement)
|
|
2373
2292
|
el = this.createNewComponent(el, isPreHtmlElement, props);
|
|
2374
2293
|
|
|
2375
|
-
//
|
|
2294
|
+
// Call render() with the same params that would've been passed to the constructor.
|
|
2376
2295
|
else if (el.render) {
|
|
2377
|
-
let oldHash = componentHash.get(el);
|
|
2296
|
+
let oldHash = Globals.componentHash.get(el);
|
|
2378
2297
|
if (oldHash !== newHash)
|
|
2379
2298
|
el.render(props); // Pass new values of props to render so it can decide how it wants to respond.
|
|
2380
2299
|
}
|
|
2381
2300
|
|
|
2382
|
-
componentHash.set(el, newHash);
|
|
2301
|
+
Globals.componentHash.set(el, newHash);
|
|
2383
2302
|
}
|
|
2384
2303
|
|
|
2385
2304
|
/**
|
|
@@ -2422,33 +2341,43 @@ class NodeGroup {
|
|
|
2422
2341
|
// We pass the childNodes to the constructor so it can know about them,
|
|
2423
2342
|
// instead of only afterward when they're appended to the slot below.
|
|
2424
2343
|
// This is useful for a custom selectbox, for example.
|
|
2425
|
-
//
|
|
2344
|
+
// Globals.pendingChildren stores the childen so the super construtor call to Solarite's constructor
|
|
2426
2345
|
// can add them as children before the rest of the constructor code executes.
|
|
2427
2346
|
let ch = [... el.childNodes];
|
|
2428
|
-
|
|
2347
|
+
Globals.pendingChildren.push(ch); // pop() is called in Solarite constructor.
|
|
2429
2348
|
let newEl = new Constructor(props, ch);
|
|
2430
2349
|
|
|
2431
2350
|
if (!isPreHtmlElement)
|
|
2432
2351
|
newEl.setAttribute('is', el.getAttribute('is').toLowerCase());
|
|
2433
2352
|
el.replaceWith(newEl);
|
|
2434
|
-
|
|
2353
|
+
|
|
2435
2354
|
// Set children / slot children
|
|
2436
2355
|
// TODO: Match named slots.
|
|
2437
2356
|
// TODO: This only appends to slot if render() is called in the constructor.
|
|
2438
2357
|
//let slot = newEl.querySelector('slot') || newEl;
|
|
2439
2358
|
//slot.append(...el.childNodes);
|
|
2440
|
-
|
|
2359
|
+
|
|
2441
2360
|
// Copy over event attributes.
|
|
2442
2361
|
for (let propName in props) {
|
|
2443
2362
|
let val = props[propName];
|
|
2444
2363
|
if (propName.startsWith('on') && typeof val === 'function')
|
|
2445
2364
|
newEl.addEventListener(propName.slice(2), e => val(e, newEl));
|
|
2365
|
+
|
|
2366
|
+
// Bind array based event attributes on value.
|
|
2367
|
+
// This same logic is in ExprPath.applyValueAttrib() for non-components.
|
|
2368
|
+
if ((propName === 'value' || propName === 'data-value') && Util.isPath(val)) {
|
|
2369
|
+
let [obj, path] = [val[0], val.slice(1)];
|
|
2370
|
+
newEl.value = delve(obj, path);
|
|
2371
|
+
newEl.addEventListener('input', e => {
|
|
2372
|
+
delve(obj, path, Util.getInputValue(newEl));
|
|
2373
|
+
}, true); // We use capture so we update the values before other events added by the user.
|
|
2374
|
+
}
|
|
2446
2375
|
}
|
|
2447
2376
|
|
|
2448
2377
|
// If an id pointed at the placeholder, update it to point to the new element.
|
|
2449
2378
|
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
2450
2379
|
if (id)
|
|
2451
|
-
this.
|
|
2380
|
+
delve(this.getRootNode(), id.split(/\./g), newEl);
|
|
2452
2381
|
|
|
2453
2382
|
|
|
2454
2383
|
// Update paths to use replaced element.
|
|
@@ -2517,18 +2446,39 @@ class NodeGroup {
|
|
|
2517
2446
|
return this.startNode?.parentNode
|
|
2518
2447
|
}
|
|
2519
2448
|
|
|
2449
|
+
/**
|
|
2450
|
+
* Get the root element of the NodeGroup's RootNodeGroup.
|
|
2451
|
+
* @returns {HTMLElement|DocumentFragment} */
|
|
2452
|
+
getRootNode() {
|
|
2453
|
+
return this.rootNg.root;
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
/**
|
|
2457
|
+
* @returns {RootNodeGroup} */
|
|
2458
|
+
getRootNodeGroup() {
|
|
2459
|
+
return this.rootNg;
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2520
2462
|
|
|
2463
|
+
updatePaths(fragment, paths, offset) {
|
|
2464
|
+
// Update paths to point to the fragment.
|
|
2465
|
+
this.paths.length = paths.length;
|
|
2466
|
+
for (let i=0; i<paths.length; i++) {
|
|
2467
|
+
let path = paths[i].clone(fragment, offset);
|
|
2468
|
+
path.parentNg = this;
|
|
2469
|
+
this.paths[i] = path;
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2521
2472
|
|
|
2522
2473
|
updateStyles() {
|
|
2523
2474
|
if (this.styles)
|
|
2524
2475
|
for (let [style, oldText] of this.styles) {
|
|
2525
2476
|
let newText = style.textContent;
|
|
2526
2477
|
if (oldText !== newText)
|
|
2527
|
-
Util.bindStyles(style, this.
|
|
2478
|
+
Util.bindStyles(style, this.getRootNodeGroup().root);
|
|
2528
2479
|
}
|
|
2529
2480
|
}
|
|
2530
2481
|
|
|
2531
|
-
|
|
2532
2482
|
//#IFDEV
|
|
2533
2483
|
/**
|
|
2534
2484
|
* @deprecated
|
|
@@ -2547,7 +2497,7 @@ class NodeGroup {
|
|
|
2547
2497
|
|
|
2548
2498
|
let path = this.paths.find(path=>path.type === PathType.Content && path.getNodes().includes(nextNode));
|
|
2549
2499
|
if (path)
|
|
2550
|
-
return [`Path
|
|
2500
|
+
return [`Path.nodes:`]
|
|
2551
2501
|
|
|
2552
2502
|
return [];
|
|
2553
2503
|
});
|
|
@@ -2586,465 +2536,327 @@ class NodeGroup {
|
|
|
2586
2536
|
|
|
2587
2537
|
// Fails for detached NodeGroups.
|
|
2588
2538
|
// NodeGroups get detached when their nodes are removed by udomdiff()
|
|
2589
|
-
let parentNode = this.getParentNode();
|
|
2590
|
-
if (parentNode)
|
|
2591
|
-
assert(this.getParentNode().contains(path.getParentNode()));
|
|
2592
|
-
path.verify();
|
|
2593
|
-
// TODO: Make sure path nodes are all within our own node range.
|
|
2594
|
-
}
|
|
2595
|
-
return true;
|
|
2596
|
-
}
|
|
2597
|
-
//#ENDIF
|
|
2598
|
-
}
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
let componentHash = new WeakMap();
|
|
2602
|
-
|
|
2603
|
-
/**
|
|
2604
|
-
* Tools for watch variables and performing precise renders.
|
|
2605
|
-
*/
|
|
2606
|
-
|
|
2607
|
-
function serializePath(path) {
|
|
2608
|
-
// Convert any array indices to strings, so serialized comparisons work.
|
|
2609
|
-
return JSON.stringify([getObjectId(path[0]), ...path.slice(1).map(item => item+'')])
|
|
2610
|
-
|
|
2611
|
-
}
|
|
2612
|
-
|
|
2613
|
-
/**
|
|
2614
|
-
* @typedef {Object} RenderOptions
|
|
2615
|
-
* @property {boolean=} styles - Indicates whether the Courage component is present.
|
|
2616
|
-
* @property {boolean=} scripts - Indicates whether the Power component is present.
|
|
2617
|
-
* @property {boolean=} ids *
|
|
2618
|
-
* @property {?boolean} render
|
|
2619
|
-
* Used only when options are given to a class super constructor inheriting from Solarite.
|
|
2620
|
-
* True to call render() immediately in super constructor.
|
|
2621
|
-
* False to automatically call render() at all.
|
|
2622
|
-
* Undefined (default) to call render() when added to the DOM, unless already rendered.
|
|
2623
|
-
*/
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
/**
|
|
2627
|
-
* Manage all the NodeGroups for a single WebComponent or root HTMLElement
|
|
2628
|
-
* There's one NodeGroup for the root of the WebComponent, and one for every ${...} expression that creates Node children.
|
|
2629
|
-
* And each NodeGroup manages the one or more nodes created by the expression.
|
|
2630
|
-
*
|
|
2631
|
-
* An instance of this class exists for each element that r() renders to. */
|
|
2632
|
-
class NodeGroupManager {
|
|
2633
|
-
|
|
2634
|
-
/** @type {HTMLElement|DocumentFragment} */
|
|
2635
|
-
rootEl;
|
|
2636
|
-
|
|
2637
|
-
/** @type {NodeGroup} */
|
|
2638
|
-
rootNg;
|
|
2639
|
-
|
|
2640
|
-
/** @type {Change[]} */
|
|
2641
|
-
changes = [];
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
//#IFDEV
|
|
2646
|
-
modifications;
|
|
2647
|
-
logDepth=0
|
|
2539
|
+
let parentNode = this.getParentNode();
|
|
2540
|
+
if (parentNode)
|
|
2541
|
+
assert(this.getParentNode().contains(path.getParentNode()));
|
|
2542
|
+
path.verify();
|
|
2543
|
+
// TODO: Make sure path nodes are all within our own node range.
|
|
2544
|
+
}
|
|
2545
|
+
return true;
|
|
2546
|
+
}
|
|
2648
2547
|
//#ENDIF
|
|
2649
2548
|
|
|
2650
|
-
/**
|
|
2651
|
-
* A map from the html strings and exprs that created a node group, to the NodeGroup.
|
|
2652
|
-
* Also stores a map from just the html strings to the NodeGroup, so we can still find a similar match if the exprs changed.
|
|
2653
|
-
*
|
|
2654
|
-
* @type {MultiValueMap<string, (string|Template)[], NodeGroup>} */
|
|
2655
|
-
nodeGroupsAvailable = new MultiValueMap();
|
|
2656
|
-
nodeGroupsInUse = [];
|
|
2657
2549
|
|
|
2550
|
+
/**
|
|
2551
|
+
* @param root {HTMLElement}
|
|
2552
|
+
* @param shell {Shell}
|
|
2553
|
+
* @param pathOffset {int} */
|
|
2554
|
+
activateEmbeds(root, shell, pathOffset=0) {
|
|
2658
2555
|
|
|
2659
|
-
|
|
2660
|
-
|
|
2556
|
+
// static components. These are WebComponents not created by an expression.
|
|
2557
|
+
// Must happen before ids.
|
|
2558
|
+
for (let path of shell.staticComponents) {
|
|
2559
|
+
if (pathOffset)
|
|
2560
|
+
path = path.slice(0, -pathOffset);
|
|
2561
|
+
let el = resolveNodePath(root, path);
|
|
2661
2562
|
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
//#ENDIF
|
|
2563
|
+
// Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
|
|
2564
|
+
if (root !== el/* && !isReplaceEl(root, el)*/) // TODO: is isReplaceEl necessary?
|
|
2565
|
+
this.createNewComponent(el);
|
|
2566
|
+
}
|
|
2667
2567
|
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
constructor(rootEl=null) {
|
|
2671
|
-
this.rootEl = rootEl;
|
|
2568
|
+
let rootEl = this.rootNg.root;
|
|
2569
|
+
if (rootEl) {
|
|
2672
2570
|
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
}
|
|
2682
|
-
|
|
2683
|
-
// TODO: Only trigger if we modify nodes inside an ExprPath.
|
|
2684
|
-
// TODO: Enable this even when not in dev mode, because it's so useful for debugging?
|
|
2685
|
-
// But it modifies the top level prototypes.
|
|
2686
|
-
// TODO: Remove the onBeforeMutation callback when this.rootEl is not in the document.
|
|
2687
|
-
// Because we won't get notified of document changes then anyway.
|
|
2688
|
-
if (this.rootEl && this.rootEl.ownerDocument?.defaultView) { // TODO: Bind whenever we have rootEl.
|
|
2689
|
-
this.mutationWatcher = MutationWatcher.getFromDocument(this.rootEl.ownerDocument);
|
|
2690
|
-
this.mutationWatcher.onBeforeMutation.push((node, action, args) => {
|
|
2691
|
-
|
|
2692
|
-
// If a modification was made
|
|
2693
|
-
if (this.mutationWatcherEnabled) {
|
|
2694
|
-
if (this.rootEl.contains(node) && closestCustomElement(node) === this.rootEl) {
|
|
2695
|
-
//console.log(node, action, args);
|
|
2696
|
-
//throw new Error('DOM modification');
|
|
2697
|
-
}
|
|
2698
|
-
|
|
2699
|
-
// If another DOM node steals one of ours by adding it to itself.
|
|
2700
|
-
// TODO: append can use multiple arguments.
|
|
2701
|
-
if (['insertBefore', 'append', 'appendChild'].includes(action)
|
|
2702
|
-
&& this.rootEl.contains(args[0]) && closestCustomElement(args[0]) === this.rootEl) {
|
|
2703
|
-
|
|
2704
|
-
//console.log(node, action, args);
|
|
2705
|
-
//throw new Error('Another element attempted to steal one of our nodes.');
|
|
2706
|
-
}
|
|
2707
|
-
}
|
|
2708
|
-
});
|
|
2709
|
-
}
|
|
2710
|
-
//#ENDIF
|
|
2711
|
-
*/
|
|
2712
|
-
}
|
|
2571
|
+
// ids
|
|
2572
|
+
if (this.options?.ids !== false)
|
|
2573
|
+
for (let path of shell.ids) {
|
|
2574
|
+
if (pathOffset)
|
|
2575
|
+
path = path.slice(0, -pathOffset);
|
|
2576
|
+
let el = resolveNodePath(root, path);
|
|
2577
|
+
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
2578
|
+
if (id) { // If something hasn't removed the id.
|
|
2713
2579
|
|
|
2580
|
+
// Don't allow overwriting existing class properties if they already have a non-Node value.
|
|
2581
|
+
if (rootEl[id] && !(rootEl[id] instanceof Node))
|
|
2582
|
+
throw new Error(`${rootEl.constructor.name}.${id} already has a value. ` +
|
|
2583
|
+
`Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
|
|
2714
2584
|
|
|
2715
|
-
|
|
2716
|
-
*
|
|
2717
|
-
* 1. Delete a NodeGroup from this.nodeGroupsAvailable that matches this exactKey.
|
|
2718
|
-
* 2. Then delete all of that NodeGroup's parents' exactKey entries
|
|
2719
|
-
* We don't move them to in-use because we plucked the NodeGroup from them, they no longer match their exactKeys.
|
|
2720
|
-
* 3. Then we move all the NodeGroup's exact+close keyed children to inUse because we don't want future calls
|
|
2721
|
-
* to getNodeGroup() to borrow the children now that the whole NodeGroup is in-use.
|
|
2722
|
-
*
|
|
2723
|
-
* TODO: Have NodeGroups keep track of whether they're inUse.
|
|
2724
|
-
* That way when we go up or down we don't have to remove those with .inUse===true
|
|
2725
|
-
*
|
|
2726
|
-
* @param exactKey
|
|
2727
|
-
* @param goUp
|
|
2728
|
-
* @param child
|
|
2729
|
-
* @returns {?NodeGroup} */
|
|
2730
|
-
findAndDeleteExact(exactKey, goUp=true, child=undefined) {
|
|
2731
|
-
|
|
2732
|
-
let ng = this.nodeGroupsAvailable.delete(exactKey, child);
|
|
2733
|
-
if (ng) {
|
|
2734
|
-
/*#IFDEV*/assert(ng.exactKey === exactKey);/*#ENDIF*/
|
|
2735
|
-
|
|
2736
|
-
// Mark close-key version as in-use.
|
|
2737
|
-
let closeNg = this.nodeGroupsAvailable.delete(ng.closeKey, ng);
|
|
2738
|
-
/*#IFDEV*/assert(closeNg);/*#ENDIF*/
|
|
2739
|
-
|
|
2740
|
-
// Mark our self as in-use.
|
|
2741
|
-
this.nodeGroupsInUse.push(ng);
|
|
2742
|
-
|
|
2743
|
-
ng.inUse = true;
|
|
2744
|
-
closeNg.inUse = true;
|
|
2745
|
-
|
|
2746
|
-
// Mark all parents that have this NodeGroup as a child as in-use.
|
|
2747
|
-
// So that way we don't use this parent again
|
|
2748
|
-
if (goUp) {
|
|
2749
|
-
let ng2 = ng;
|
|
2750
|
-
while (ng2 = ng2?.parentPath?.parentNg) {
|
|
2751
|
-
if (!ng2.inUse) {
|
|
2752
|
-
ng2.inUse = true;
|
|
2753
|
-
let success = this.nodeGroupsAvailable.delete(ng2.exactKey, ng2);
|
|
2754
|
-
// assert(success);
|
|
2755
|
-
let success2 = this.nodeGroupsAvailable.delete(ng2.closeKey, ng2);
|
|
2756
|
-
// assert(success);
|
|
2757
|
-
/*#IFDEV*/assert(success === success2);/*#ENDIF*/
|
|
2758
|
-
|
|
2759
|
-
// console.log(getHtml(ng2))
|
|
2760
|
-
if (success) {
|
|
2761
|
-
this.nodeGroupsInUse.push(ng2);
|
|
2762
|
-
}
|
|
2585
|
+
delve(rootEl, id.split(/\./g), el);
|
|
2763
2586
|
}
|
|
2764
2587
|
}
|
|
2765
|
-
}
|
|
2766
2588
|
|
|
2767
|
-
//
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2589
|
+
// styles
|
|
2590
|
+
if (this.options?.styles !== false) {
|
|
2591
|
+
if (shell.styles.length)
|
|
2592
|
+
this.styles = new Map();
|
|
2593
|
+
for (let path of shell.styles) {
|
|
2594
|
+
if (pathOffset)
|
|
2595
|
+
path = path.slice(0, -pathOffset);
|
|
2596
|
+
let style = resolveNodePath(root, path);
|
|
2597
|
+
Util.bindStyles(style, rootEl);
|
|
2598
|
+
this.styles.set(style, style.textContent);
|
|
2773
2599
|
}
|
|
2774
|
-
|
|
2775
|
-
if (ng.parentPath) ;
|
|
2776
2600
|
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
* @param goUp {boolean}
|
|
2786
|
-
* @returns {NodeGroup} */
|
|
2787
|
-
findAndDeleteClose(closeKey, exactKey, goUp=true) {
|
|
2788
|
-
let ng = this.nodeGroupsAvailable.delete(closeKey);
|
|
2789
|
-
if (ng) {
|
|
2790
|
-
|
|
2791
|
-
// We matched on a new key, so delete the old exactKey.
|
|
2792
|
-
let exactNg = this.nodeGroupsAvailable.delete(ng.exactKey, ng);
|
|
2793
|
-
|
|
2794
|
-
/*#IFDEV*/assert(exactNg);/*#ENDIF*/
|
|
2795
|
-
/*#IFDEV*/assert(ng === exactNg);/*#ENDIF*/
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
ng.inUse = true;
|
|
2799
|
-
if (goUp) {
|
|
2800
|
-
let ng2 = ng;
|
|
2801
|
-
|
|
2802
|
-
// We borrowed a node from another node group so make sure its parent isn't still an exact match.
|
|
2803
|
-
while (ng2 = ng2?.parentPath?.parentNg) {
|
|
2804
|
-
if (!ng2.inUse) {
|
|
2805
|
-
ng2.inUse = true; // Might speed it up slightly?
|
|
2806
|
-
let success = this.nodeGroupsAvailable.delete(ng2.exactKey, ng2);
|
|
2807
|
-
/*#IFDEV*/assert(success);/*#ENDIF*/
|
|
2808
|
-
|
|
2809
|
-
// But it can still be a close match, so we don't use this code.
|
|
2810
|
-
success = this.nodeGroupsAvailable.delete(ng2.closeKey, ng2);
|
|
2811
|
-
/*#IFDEV*/assert(success);/*#ENDIF*/
|
|
2812
|
-
|
|
2813
|
-
this.nodeGroupsInUse.push(ng2);
|
|
2814
|
-
}
|
|
2601
|
+
}
|
|
2602
|
+
// scripts
|
|
2603
|
+
if (this.options?.scripts !== false) {
|
|
2604
|
+
for (let path of shell.scripts) {
|
|
2605
|
+
if (pathOffset)
|
|
2606
|
+
path = path.slice(0, -pathOffset);
|
|
2607
|
+
let script = resolveNodePath(root, path);
|
|
2608
|
+
eval(script.textContent);
|
|
2815
2609
|
}
|
|
2816
2610
|
}
|
|
2817
|
-
|
|
2818
|
-
// Recursively mark all child NodeGroups as in-use.
|
|
2819
|
-
// We actually DON't want to do this becuse applyExprs is going to swap out the child NodeGroups
|
|
2820
|
-
// and mark them as in-use as it goes.
|
|
2821
|
-
// that's probably why uncommenting this causes tests to fail.
|
|
2822
|
-
// for (let path of ng.paths)
|
|
2823
|
-
// for (let childNg of path.nodeGroups)
|
|
2824
|
-
// this.findAndDeleteExact(childNg.exactKey, false, childNg);
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
ng.exactKey = exactKey;
|
|
2828
|
-
ng.closeKey = closeKey;
|
|
2829
|
-
this.nodeGroupsInUse.push(ng);
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
if (ng.parentPath) ;
|
|
2833
2611
|
}
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
return ng;
|
|
2837
2612
|
}
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
|
|
2616
|
+
class RootNodeGroup extends NodeGroup {
|
|
2838
2617
|
|
|
2839
2618
|
/**
|
|
2840
|
-
*
|
|
2841
|
-
*
|
|
2842
|
-
|
|
2843
|
-
* @param exact {?boolean}
|
|
2844
|
-
* @param createForWatch Deprecated.
|
|
2845
|
-
* @return {?NodeGroup} */
|
|
2846
|
-
getNodeGroup(template, exact=null, createForWatch=false) {
|
|
2619
|
+
* Root node at the top of the hierarchy.
|
|
2620
|
+
* @type {HTMLElement} */
|
|
2621
|
+
root;
|
|
2847
2622
|
|
|
2848
|
-
|
|
2623
|
+
/**
|
|
2624
|
+
*
|
|
2625
|
+
* @param template
|
|
2626
|
+
* @param el
|
|
2627
|
+
* @param options {?object}
|
|
2628
|
+
*/
|
|
2629
|
+
constructor(template, el, options) {
|
|
2630
|
+
super(template);
|
|
2849
2631
|
|
|
2850
|
-
|
|
2851
|
-
let ng;
|
|
2852
|
-
if (exact === true) {
|
|
2853
|
-
ng = this.findAndDeleteExact(exactKey);
|
|
2632
|
+
this.options = options;
|
|
2854
2633
|
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
return null;
|
|
2858
|
-
}
|
|
2859
|
-
}
|
|
2634
|
+
this.rootNg = this;
|
|
2635
|
+
let [fragment, shell] = this.init(template);
|
|
2860
2636
|
|
|
2861
|
-
//
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
ng = createForWatch ? null : this.findAndDeleteClose(closeKey, exactKey);
|
|
2637
|
+
// If adding NodeGroup to an element.
|
|
2638
|
+
let offset = 0;
|
|
2639
|
+
let root = fragment; // TODO: Rename so it's not confused with this.root.
|
|
2640
|
+
if (el) {
|
|
2866
2641
|
|
|
2867
|
-
//
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
window.ng = ng;
|
|
2873
|
-
/*#IFDEV*/this.incrementLogDepth(1);/*#ENDIF*/
|
|
2874
|
-
/*#IFDEV*/ng.verify();/*#ENDIF*/
|
|
2875
|
-
ng.applyExprs(template.exprs);
|
|
2876
|
-
|
|
2877
|
-
/*#IFDEV*/ng.verify();/*#ENDIF*/
|
|
2878
|
-
/*#IFDEV*/this.incrementLogDepth(-1);/*#ENDIF*/
|
|
2642
|
+
// Save slot children
|
|
2643
|
+
let slotFragment;
|
|
2644
|
+
if (el.childNodes.length) {
|
|
2645
|
+
slotFragment = document.createDocumentFragment();
|
|
2646
|
+
slotFragment.append(...el.childNodes);
|
|
2879
2647
|
}
|
|
2880
2648
|
|
|
2881
|
-
|
|
2882
|
-
else {
|
|
2883
|
-
/*#IFDEV*/this.incrementLogDepth(1);/*#ENDIF*/
|
|
2884
|
-
ng = new NodeGroup(template, this);
|
|
2885
|
-
/*#IFDEV*/this.incrementLogDepth(-1);/*#ENDIF*/
|
|
2649
|
+
this.root = el;
|
|
2886
2650
|
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2651
|
+
// If el should replace the root node of the fragment.
|
|
2652
|
+
if (isReplaceEl(fragment, el)) {
|
|
2653
|
+
el.append(...fragment.children[0].childNodes);
|
|
2654
|
+
|
|
2655
|
+
// Copy attributes
|
|
2656
|
+
for (let attrib of fragment.children[0].attributes)
|
|
2657
|
+
if (!el.hasAttribute(attrib.name))
|
|
2658
|
+
el.setAttribute(attrib.name, attrib.value);
|
|
2890
2659
|
|
|
2660
|
+
// Go one level deeper into all of shell's paths.
|
|
2661
|
+
offset = 1;
|
|
2662
|
+
}
|
|
2663
|
+
else {
|
|
2664
|
+
let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
|
|
2665
|
+
if (!isEmpty)
|
|
2666
|
+
el.append(...fragment.childNodes);
|
|
2667
|
+
}
|
|
2891
2668
|
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2669
|
+
// Setup slots
|
|
2670
|
+
if (slotFragment) {
|
|
2671
|
+
for (let slot of el.querySelectorAll('slot[name]')) {
|
|
2672
|
+
let name = slot.getAttribute('name');
|
|
2673
|
+
if (name) {
|
|
2674
|
+
let slotChildren = slotFragment.querySelectorAll(`[slot='${name}']`);
|
|
2675
|
+
slot.append(...slotChildren);
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
let unamedSlot = el.querySelector('slot:not([name])');
|
|
2679
|
+
if (unamedSlot)
|
|
2680
|
+
unamedSlot.append(slotFragment);
|
|
2900
2681
|
else
|
|
2901
|
-
|
|
2682
|
+
el.append(slotFragment);
|
|
2902
2683
|
}
|
|
2684
|
+
|
|
2685
|
+
root = el;
|
|
2686
|
+
this.startNode = el;
|
|
2687
|
+
this.endNode = el;
|
|
2903
2688
|
}
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2689
|
+
else {
|
|
2690
|
+
let singleEl = getSingleEl(fragment);
|
|
2691
|
+
this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
|
|
2692
|
+
if (singleEl) {
|
|
2693
|
+
root = singleEl;
|
|
2694
|
+
offset = 1;
|
|
2695
|
+
}
|
|
2911
2696
|
}
|
|
2912
2697
|
|
|
2698
|
+
this.updatePaths(root, shell.paths, offset);
|
|
2913
2699
|
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2700
|
+
this.activateEmbeds(root, shell, offset);
|
|
2701
|
+
|
|
2702
|
+
// Apply exprs
|
|
2703
|
+
this.applyExprs(template.exprs);
|
|
2917
2704
|
}
|
|
2705
|
+
}
|
|
2918
2706
|
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2707
|
+
function getSingleEl(fragment) {
|
|
2708
|
+
let nonempty = [];
|
|
2709
|
+
for (let n of fragment.childNodes) {
|
|
2710
|
+
if (n.nodeType === 1 || n.nodeType === 3 && n.textContent.trim().length) {
|
|
2711
|
+
if (nonempty.length)
|
|
2712
|
+
return null;
|
|
2713
|
+
nonempty.push(n);
|
|
2926
2714
|
}
|
|
2927
|
-
this.nodeGroupsInUse = [];
|
|
2928
|
-
|
|
2929
|
-
// Used for watches
|
|
2930
|
-
this.changes = [];
|
|
2931
|
-
|
|
2932
|
-
/*#IFDEV*/this.log('----------------------');/*#ENDIF*/
|
|
2933
|
-
// TODO: free the memory from any nodeGroupsAvailable() after render is done, since they weren't used?
|
|
2934
2715
|
}
|
|
2716
|
+
return nonempty[0];
|
|
2717
|
+
}
|
|
2935
2718
|
|
|
2719
|
+
/**
|
|
2720
|
+
* Does the fragment have one child that's an element matching the tagname of el?
|
|
2721
|
+
* @param fragment {DocumentFragment}
|
|
2722
|
+
* @param el {HTMLElement}
|
|
2723
|
+
* @returns {boolean} */
|
|
2724
|
+
function isReplaceEl(fragment, el) {
|
|
2725
|
+
return el.tagName.includes('-')
|
|
2726
|
+
&& fragment.children.length===1
|
|
2727
|
+
&& fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') === el.tagName;
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
/**
|
|
2731
|
+
* The html strings and evaluated expressions from an html tagged template.
|
|
2732
|
+
* A unique Template is created for each item in a loop.
|
|
2733
|
+
* Although the reference to the html strings is shared among templates. */
|
|
2734
|
+
class Template {
|
|
2936
2735
|
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
clearSubscribers = false;
|
|
2736
|
+
/** @type {(Template|string|function)|(Template|string|function)[]} Evaulated expressions. */
|
|
2737
|
+
exprs = []
|
|
2940
2738
|
|
|
2941
|
-
|
|
2739
|
+
/** @type {string[]} */
|
|
2740
|
+
html = [];
|
|
2741
|
+
|
|
2742
|
+
/** @type {Array} Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
|
|
2743
|
+
hashedFields;
|
|
2942
2744
|
|
|
2943
2745
|
/**
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
*
|
|
2947
|
-
|
|
2948
|
-
getLoopInfo(path) {
|
|
2949
|
-
let serializedArrayPath = serializePath(path);
|
|
2950
|
-
return [...this.pathToLoopInfo.getAll(serializedArrayPath)]; // This is set inside forEach()
|
|
2951
|
-
}
|
|
2746
|
+
* @deprecated
|
|
2747
|
+
* @type {ExprPath} Used with forEach() from watch.js
|
|
2748
|
+
* Set in ExprPath.apply() */
|
|
2749
|
+
parentPath;
|
|
2952
2750
|
|
|
2953
|
-
|
|
2751
|
+
/** @type {NodeGroup} */
|
|
2752
|
+
nodeGroup;
|
|
2954
2753
|
|
|
2955
|
-
|
|
2956
|
-
/**
|
|
2957
|
-
* @deprecated
|
|
2958
|
-
* Store the functions used to create items for each loop.
|
|
2959
|
-
* TODO: Can this be combined with pathToTemplates?
|
|
2960
|
-
* @type {MultiValueMap<string, Subscriber>} */
|
|
2961
|
-
pathToLoopInfo = new MultiValueMap();
|
|
2962
|
-
|
|
2963
2754
|
/**
|
|
2964
|
-
*
|
|
2965
|
-
|
|
2966
|
-
subscribers = new MultiValueMap();
|
|
2967
|
-
|
|
2968
|
-
clearSubscribersIfNeeded() {
|
|
2969
|
-
if (this.clearSubscribers) {
|
|
2970
|
-
this.pathToLoopInfo = new MultiValueMap();
|
|
2971
|
-
this.subscribers = new MultiValueMap();
|
|
2972
|
-
this.clearSubscribers = false;
|
|
2973
|
-
}
|
|
2974
|
-
}
|
|
2975
|
-
|
|
2755
|
+
* @type {string[][]} */
|
|
2756
|
+
paths = [];
|
|
2976
2757
|
|
|
2977
2758
|
/**
|
|
2978
|
-
*
|
|
2979
|
-
* @param
|
|
2980
|
-
* @
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2759
|
+
*
|
|
2760
|
+
* @param htmlStrings {string[]}
|
|
2761
|
+
* @param exprs {*[]} */
|
|
2762
|
+
constructor(htmlStrings, exprs) {
|
|
2763
|
+
this.html = htmlStrings;
|
|
2764
|
+
this.exprs = exprs;
|
|
2984
2765
|
|
|
2985
|
-
|
|
2986
|
-
if (!ngm) {
|
|
2987
|
-
ngm = new NodeGroupManager(rootEl);
|
|
2988
|
-
nodeGroupManagers.set(rootEl, ngm);
|
|
2989
|
-
}
|
|
2766
|
+
//this.trace = new Error().stack.split(/\n/g)
|
|
2990
2767
|
|
|
2991
|
-
|
|
2992
|
-
|
|
2768
|
+
// Multiple templates can share the same htmlStrings array.
|
|
2769
|
+
//this.hashedFields = [getObjectId(htmlStrings), exprs]
|
|
2993
2770
|
|
|
2771
|
+
//#IFDEV
|
|
2772
|
+
assert(Array.isArray(htmlStrings));
|
|
2773
|
+
assert(Array.isArray(exprs));
|
|
2994
2774
|
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
this.logDepth += level;
|
|
2775
|
+
Object.defineProperty(this, 'debug', {
|
|
2776
|
+
get() {
|
|
2777
|
+
return JSON.stringify([this.html, this.exprs]);
|
|
2778
|
+
}
|
|
2779
|
+
});
|
|
2780
|
+
//#ENDIF
|
|
3002
2781
|
}
|
|
3003
2782
|
|
|
3004
2783
|
/**
|
|
3005
|
-
*
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
2784
|
+
* Called by JSON.serialize when it encounters a Template.
|
|
2785
|
+
* This prevents the hashed version from being too large. */
|
|
2786
|
+
toJSON() {
|
|
2787
|
+
if (!this.hashedFields)
|
|
2788
|
+
this.hashedFields = [getObjectId(this.html), this.exprs];
|
|
2789
|
+
|
|
2790
|
+
return this.hashedFields
|
|
3011
2791
|
}
|
|
3012
2792
|
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
2793
|
+
/**
|
|
2794
|
+
* Render the main template, which may indirectly call renderTemplate() to create children.
|
|
2795
|
+
* @param el {HTMLElement}
|
|
2796
|
+
* @param options {RenderOptions}
|
|
2797
|
+
* @return {?DocumentFragment|HTMLElement} */
|
|
2798
|
+
render(el=null, options={}) {
|
|
2799
|
+
let ng;
|
|
2800
|
+
let standalone = !el;
|
|
2801
|
+
let firstTime = false;
|
|
3016
2802
|
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
}
|
|
2803
|
+
// Rendering a standalone element.
|
|
2804
|
+
// TODO: figure out when to not use RootNodeGroup
|
|
2805
|
+
if (standalone) {
|
|
2806
|
+
ng = new RootNodeGroup(this, null, options);
|
|
2807
|
+
el = ng.getRootNode();
|
|
2808
|
+
Globals.nodeGroups.set(el, ng);
|
|
2809
|
+
firstTime = true;
|
|
2810
|
+
}
|
|
2811
|
+
else {
|
|
2812
|
+
ng = Globals.nodeGroups.get(el);
|
|
2813
|
+
if (!ng) {
|
|
2814
|
+
ng = new RootNodeGroup(this, el, options);
|
|
2815
|
+
Globals.nodeGroups.set(el, ng);
|
|
2816
|
+
firstTime = true;
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
3025
2819
|
|
|
3026
|
-
//
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
2820
|
+
// Creating the root nodegroup also renders it.
|
|
2821
|
+
// If we didn't just create it, we need to render it.
|
|
2822
|
+
if (!firstTime) {
|
|
2823
|
+
if (this.html?.length === 1 && !this.html[0])
|
|
2824
|
+
el.innerHTML = ''; // Fast path for empty component.
|
|
2825
|
+
else
|
|
2826
|
+
ng.applyExprs(this.exprs);
|
|
2827
|
+
}
|
|
3031
2828
|
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
this.rootNg.verify();
|
|
2829
|
+
return el;
|
|
2830
|
+
}
|
|
3035
2831
|
|
|
3036
|
-
|
|
3037
|
-
|
|
2832
|
+
getExactKey() {
|
|
2833
|
+
if (!this.exactKey)
|
|
2834
|
+
this.exactKey = getObjectHash(this); // calls this.toJSON().
|
|
2835
|
+
return this.exactKey;
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
getCloseKey() {
|
|
2839
|
+
if (!this.closeKey)
|
|
2840
|
+
this.closeKey = '@'+this.toJSON()[0];
|
|
2841
|
+
// Use the joined html when debugging? But it breaks some tests.
|
|
2842
|
+
//return '@'+this.html.join('|')
|
|
2843
|
+
|
|
2844
|
+
return this.closeKey;
|
|
3038
2845
|
}
|
|
3039
|
-
//#ENDIF
|
|
3040
2846
|
}
|
|
3041
2847
|
|
|
3042
|
-
NodeGroupManager.pendingChildren = [];
|
|
3043
2848
|
|
|
3044
2849
|
/**
|
|
3045
|
-
*
|
|
3046
|
-
* @
|
|
3047
|
-
|
|
2850
|
+
* @typedef {Object} RenderOptions
|
|
2851
|
+
* @property {boolean=} styles - Replace :host in style tags to scope them locally.
|
|
2852
|
+
* @property {boolean=} scripts - Execute script tags.
|
|
2853
|
+
* @property {boolean=} ids - Create references to elements with id or data-id attributes.
|
|
2854
|
+
* @property {?boolean} render - Deprecated.
|
|
2855
|
+
* Used only when options are given to a class super constructor inheriting from Solarite.
|
|
2856
|
+
* True to call render() immediately in super constructor.
|
|
2857
|
+
* False to automatically call render() at all.
|
|
2858
|
+
* Undefined (default) to call render() when added to the DOM, unless already rendered.
|
|
2859
|
+
*/
|
|
3048
2860
|
|
|
3049
2861
|
/**
|
|
3050
2862
|
* Convert strings to HTMLNodes.
|
|
@@ -3059,142 +2871,142 @@ let nodeGroupManagers = new WeakMap();
|
|
|
3059
2871
|
* 5. TODO: list more
|
|
3060
2872
|
*
|
|
3061
2873
|
* Currently supported:
|
|
3062
|
-
* 1. r`<b
|
|
2874
|
+
* 1. r(el, options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
|
|
2875
|
+
* 2. r(el, template, ?options) // Render the Template created by #1 to element.
|
|
3063
2876
|
*
|
|
3064
|
-
*
|
|
3065
|
-
* 3. r(el, options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
|
|
2877
|
+
* 3. r`<b>Hello</b> ${'World'}!` // Create Template that can later be used to create nodes.
|
|
3066
2878
|
*
|
|
3067
2879
|
* 4. r('Hello'); // Create single text node.
|
|
3068
2880
|
* 5. r('<b>Hello</b>'); // Create single HTMLElement
|
|
3069
2881
|
* 6. r('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
|
|
3070
|
-
* 7. r()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which
|
|
2882
|
+
* 7. r()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which
|
|
2883
|
+
* // includes properly handling nested components and r`` sub-expressions.
|
|
3071
2884
|
* 8. r(template) // Render Template created by #1.
|
|
3072
|
-
* 9. r(() => r`<b>Hello</b>`, {...}); // Create dynamic element that has a render() function.
|
|
3073
2885
|
*
|
|
3074
|
-
*
|
|
2886
|
+
* 9. r({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
|
|
2887
|
+
*
|
|
2888
|
+
* @param htmlStrings {?HTMLElement|string|string[]|function():Template|{render:function()}}
|
|
3075
2889
|
* @param exprs {*[]|string|Template|Object}
|
|
3076
2890
|
* @return {Node|HTMLElement|Template} */
|
|
3077
2891
|
function r(htmlStrings=undefined, ...exprs) {
|
|
3078
2892
|
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
}
|
|
3083
|
-
|
|
3084
|
-
else if (htmlStrings instanceof Node) {
|
|
3085
|
-
let parent = htmlStrings, template = exprs[0];
|
|
3086
|
-
|
|
3087
|
-
// 2. Render template created by #4 to element.
|
|
3088
|
-
if (exprs[0] instanceof Template) {
|
|
3089
|
-
let options = exprs[1];
|
|
3090
|
-
template.render(parent, options);
|
|
3091
|
-
|
|
3092
|
-
// Append on the first go.
|
|
3093
|
-
if (!parent.childNodes.length && this) {
|
|
3094
|
-
// TODO: Is htis ever executed?
|
|
3095
|
-
debugger;
|
|
3096
|
-
parent.append(this.rootNg.getParentNode());
|
|
3097
|
-
}
|
|
3098
|
-
}
|
|
3099
|
-
|
|
3100
|
-
// 3
|
|
3101
|
-
else if (!exprs.length || exprs[0]) {
|
|
3102
|
-
if (parent.shadowRoot)
|
|
3103
|
-
parent.innerHTML = ''; // Remove shadowroot. TODO: This could mess up paths?
|
|
3104
|
-
|
|
3105
|
-
let options = exprs[0];
|
|
3106
|
-
return (htmlStrings, ...exprs) => {
|
|
3107
|
-
rendered.add(parent);
|
|
3108
|
-
let template = r(htmlStrings, ...exprs);
|
|
3109
|
-
return template.render(parent, options);
|
|
3110
|
-
}
|
|
3111
|
-
}
|
|
3112
|
-
|
|
3113
|
-
// null for expr[0], remove whole element.
|
|
3114
|
-
// This path never happens?
|
|
3115
|
-
else {
|
|
3116
|
-
throw new Error('unsupported');
|
|
3117
|
-
//let ngm = NodeGroupManager.get(parent);
|
|
3118
|
-
//ngm.render(null, exprs[1])
|
|
3119
|
-
}
|
|
3120
|
-
}
|
|
3121
|
-
|
|
3122
|
-
else if (typeof htmlStrings === 'string' || htmlStrings instanceof String) {
|
|
3123
|
-
// If it starts with a string, trim both ends.
|
|
3124
|
-
// TODO: Also trim if it ends with whitespace?
|
|
3125
|
-
if (htmlStrings.match(/^\s^</))
|
|
3126
|
-
htmlStrings = htmlStrings.trim();
|
|
3127
|
-
|
|
3128
|
-
// We create a new one each time because otherwise
|
|
3129
|
-
// the returned fragment will have its content replaced by a subsequent call.
|
|
3130
|
-
let templateEl = document.createElement('template');
|
|
3131
|
-
templateEl.innerHTML = htmlStrings;
|
|
3132
|
-
|
|
3133
|
-
// 4+5. Return Node if there's one child.
|
|
3134
|
-
if (templateEl.content.childNodes.length === 1)
|
|
3135
|
-
return templateEl.content.firstChild;
|
|
3136
|
-
|
|
3137
|
-
// 6. Otherwise return DocumentFragment.
|
|
3138
|
-
return templateEl.content;
|
|
3139
|
-
}
|
|
3140
|
-
|
|
3141
|
-
// 7. Create a static element
|
|
3142
|
-
else if (htmlStrings === undefined) {
|
|
3143
|
-
return (htmlStrings, ...exprs) => {
|
|
3144
|
-
//rendered.add(parent)
|
|
3145
|
-
let template = r(htmlStrings, ...exprs);
|
|
3146
|
-
return template.render();
|
|
3147
|
-
}
|
|
3148
|
-
}
|
|
3149
|
-
|
|
3150
|
-
// 8.
|
|
3151
|
-
else if (htmlStrings instanceof Template) {
|
|
3152
|
-
return htmlStrings.render();
|
|
3153
|
-
}
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
// 9. Create dynamic element with render() function.
|
|
3157
|
-
else if (typeof htmlStrings === 'function') {
|
|
3158
|
-
let getTemplate = htmlStrings;
|
|
3159
|
-
let template = getTemplate();
|
|
3160
|
-
|
|
3161
|
-
if (typeof template === 'string')
|
|
3162
|
-
throw new Error(`Please add the "r" prefix before the string "${template}"`)
|
|
3163
|
-
|
|
3164
|
-
template.replaceMode = true;
|
|
3165
|
-
let el = template.render();
|
|
3166
|
-
|
|
3167
|
-
// Create the render() function from the function we were given.
|
|
3168
|
-
el.render = (function() {
|
|
3169
|
-
template = getTemplate();
|
|
3170
|
-
template.render(el);
|
|
3171
|
-
}).bind(el);
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
// The second argument was an object of additional properties to add.
|
|
3175
|
-
let props = exprs[0];
|
|
3176
|
-
for (let name in props)
|
|
3177
|
-
if (typeof props[name] === 'function')
|
|
3178
|
-
el[name] = props[name].bind(el);
|
|
3179
|
-
else
|
|
3180
|
-
el[name] = props[name];
|
|
3181
|
-
|
|
3182
|
-
return el;
|
|
3183
|
-
}
|
|
3184
|
-
|
|
3185
|
-
else
|
|
3186
|
-
throw new Error('Unsupported arguments.')
|
|
3187
|
-
}
|
|
2893
|
+
// TODO: Make this a more flat if/else and call other functions for the logic.
|
|
2894
|
+
if (htmlStrings instanceof Node) {
|
|
2895
|
+
let parent = htmlStrings, template = exprs[0];
|
|
3188
2896
|
|
|
2897
|
+
// 1
|
|
2898
|
+
if (!(exprs[0] instanceof Template)) {
|
|
2899
|
+
if (parent.shadowRoot)
|
|
2900
|
+
parent.innerHTML = ''; // Remove shadowroot. TODO: This could mess up paths?
|
|
3189
2901
|
|
|
2902
|
+
let options = exprs[0];
|
|
3190
2903
|
|
|
2904
|
+
// Return a tagged template function that applies the tagged themplate to parent.
|
|
2905
|
+
let taggedTemplate = (htmlStrings, ...exprs) => {
|
|
2906
|
+
Globals.rendered.add(parent);
|
|
2907
|
+
let template = new Template(htmlStrings, exprs);
|
|
2908
|
+
return template.render(parent, options);
|
|
2909
|
+
};
|
|
2910
|
+
return taggedTemplate;
|
|
2911
|
+
}
|
|
3191
2912
|
|
|
2913
|
+
// 2. Render template created by #4 to element.
|
|
2914
|
+
else if (exprs[0] instanceof Template) {
|
|
2915
|
+
let options = exprs[1];
|
|
2916
|
+
template.render(parent, options);
|
|
3192
2917
|
|
|
2918
|
+
// Append on the first go.
|
|
2919
|
+
if (!parent.childNodes.length && this) {
|
|
2920
|
+
// TODO: Is this ever executed?
|
|
2921
|
+
debugger;
|
|
2922
|
+
parent.append(this.rootNg.getParentNode());
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
3193
2925
|
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
2926
|
+
|
|
2927
|
+
|
|
2928
|
+
// null for expr[0], remove whole element.
|
|
2929
|
+
// This path never happens?
|
|
2930
|
+
else {
|
|
2931
|
+
throw new Error('unsupported');
|
|
2932
|
+
//let ngm = NodeGroupManager.get(parent);
|
|
2933
|
+
//ngm.render(null, exprs[1])
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
// 3. Path if used as a template tag.
|
|
2938
|
+
else if (Array.isArray(htmlStrings)) {
|
|
2939
|
+
return new Template(htmlStrings, exprs);
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
else if (typeof htmlStrings === 'string' || htmlStrings instanceof String) {
|
|
2943
|
+
// If it starts with a string, trim both ends.
|
|
2944
|
+
// TODO: Also trim if it ends with whitespace?
|
|
2945
|
+
if (htmlStrings.match(/^\s^</))
|
|
2946
|
+
htmlStrings = htmlStrings.trim();
|
|
2947
|
+
|
|
2948
|
+
// We create a new one each time because otherwise
|
|
2949
|
+
// the returned fragment will have its content replaced by a subsequent call.
|
|
2950
|
+
let templateEl = document.createElement('template');
|
|
2951
|
+
templateEl.innerHTML = htmlStrings;
|
|
2952
|
+
|
|
2953
|
+
// 4+5. Return Node if there's one child.
|
|
2954
|
+
let relevantNodes = Util.trimEmptyNodes(templateEl.content.childNodes);
|
|
2955
|
+
if (relevantNodes.length === 1)
|
|
2956
|
+
return relevantNodes[0];
|
|
2957
|
+
|
|
2958
|
+
// 6. Otherwise return DocumentFragment.
|
|
2959
|
+
return templateEl.content;
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
// 7. Create a static element
|
|
2963
|
+
else if (htmlStrings === undefined) {
|
|
2964
|
+
return (htmlStrings, ...exprs) => {
|
|
2965
|
+
//Globals.rendered.add(parent)
|
|
2966
|
+
let template = r(htmlStrings, ...exprs);
|
|
2967
|
+
return template.render();
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
// 8.
|
|
2972
|
+
else if (htmlStrings instanceof Template) {
|
|
2973
|
+
return htmlStrings.render();
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
|
|
2977
|
+
// 9. Create dynamic element with render() function.
|
|
2978
|
+
else if (typeof htmlStrings === 'object') {
|
|
2979
|
+
let obj = htmlStrings;
|
|
2980
|
+
|
|
2981
|
+
// Special rebound render path, called by normal path.
|
|
2982
|
+
if (Globals.objToEl.has(obj)) {
|
|
2983
|
+
return function(...args) {
|
|
2984
|
+
let template = r(...args);
|
|
2985
|
+
let el = template.render();
|
|
2986
|
+
Globals.objToEl.set(obj, el);
|
|
2987
|
+
}.bind(obj);
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
// Normal path
|
|
2991
|
+
else {
|
|
2992
|
+
Globals.objToEl.set(obj, null);
|
|
2993
|
+
obj.render(); // Calls the Special rebound render path above, when the render function calls r(this)
|
|
2994
|
+
let el = Globals.objToEl.get(obj);
|
|
2995
|
+
Globals.objToEl.delete(obj);
|
|
2996
|
+
|
|
2997
|
+
for (let name in obj)
|
|
2998
|
+
if (typeof obj[name] === 'function')
|
|
2999
|
+
el[name] = obj[name].bind(el);
|
|
3000
|
+
else
|
|
3001
|
+
el[name] = obj[name];
|
|
3002
|
+
|
|
3003
|
+
return el;
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
else
|
|
3008
|
+
throw new Error('Unsupported arguments.')
|
|
3009
|
+
}
|
|
3198
3010
|
|
|
3199
3011
|
//import {watchGet, watchSet} from "./watch.js";
|
|
3200
3012
|
|
|
@@ -3214,14 +3026,9 @@ function defineClass(Class, tagName, extendsTag) {
|
|
|
3214
3026
|
}
|
|
3215
3027
|
}
|
|
3216
3028
|
|
|
3217
|
-
/**
|
|
3218
|
-
* @type {Object<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
|
|
3219
|
-
let elementClasses = {};
|
|
3220
3029
|
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
*/
|
|
3224
|
-
let connected = new WeakSet();
|
|
3030
|
+
|
|
3031
|
+
|
|
3225
3032
|
|
|
3226
3033
|
/**
|
|
3227
3034
|
* Create a version of the Solarite class that extends from the given tag name.
|
|
@@ -3233,6 +3040,7 @@ let connected = new WeakSet();
|
|
|
3233
3040
|
* 5. We have the onConnect, onFirstConnect, and onDisconnect methods.
|
|
3234
3041
|
* Can't figure out how to have these work standalone though, and still be synchronous.
|
|
3235
3042
|
* 6. Can we extend from other element types like TR?
|
|
3043
|
+
* 7. Shows default text if render() function isn't defined.
|
|
3236
3044
|
*
|
|
3237
3045
|
* Advantages to inheriting from HTMLElement
|
|
3238
3046
|
* 1. Minimization won't break when it renames the Class and we call customElements.define() on the wrong name.
|
|
@@ -3247,10 +3055,10 @@ function createSolarite(extendsTag=null) {
|
|
|
3247
3055
|
if (extendsTag && !extendsTag.includes('-')) {
|
|
3248
3056
|
extendsTag = extendsTag.toLowerCase();
|
|
3249
3057
|
|
|
3250
|
-
BaseClass = elementClasses[extendsTag];
|
|
3058
|
+
BaseClass = Globals.elementClasses[extendsTag];
|
|
3251
3059
|
if (!BaseClass) { // TODO: Use Cache
|
|
3252
3060
|
BaseClass = document.createElement(extendsTag).constructor;
|
|
3253
|
-
elementClasses[extendsTag] = BaseClass;
|
|
3061
|
+
Globals.elementClasses[extendsTag] = BaseClass;
|
|
3254
3062
|
}
|
|
3255
3063
|
}
|
|
3256
3064
|
|
|
@@ -3286,26 +3094,24 @@ function createSolarite(extendsTag=null) {
|
|
|
3286
3094
|
constructor(options={}) {
|
|
3287
3095
|
super();
|
|
3288
3096
|
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
3097
|
// TODO: Is options.render ever used?
|
|
3292
3098
|
if (options.render===true)
|
|
3293
3099
|
this.render();
|
|
3294
3100
|
|
|
3295
3101
|
else if (options.render===false)
|
|
3296
|
-
rendered.add(this); // Don't render on connectedCallback()
|
|
3102
|
+
Globals.rendered.add(this); // Don't render on connectedCallback()
|
|
3297
3103
|
|
|
3298
3104
|
// Add children before constructor code executes.
|
|
3299
3105
|
// PendingChildren is setup in NodeGroup.createNewComponent()
|
|
3300
3106
|
// TODO: Match named slots.
|
|
3301
|
-
let ch =
|
|
3107
|
+
let ch = Globals.pendingChildren.pop();
|
|
3302
3108
|
if (ch)
|
|
3303
3109
|
(this.querySelector('slot') || this).append(...ch);
|
|
3304
3110
|
|
|
3305
3111
|
/** @deprecated */
|
|
3306
3112
|
Object.defineProperty(this, 'html', {
|
|
3307
3113
|
set(html) {
|
|
3308
|
-
rendered.add(this);
|
|
3114
|
+
Globals.rendered.add(this);
|
|
3309
3115
|
if (typeof html === 'string') {
|
|
3310
3116
|
console.warn("Assigning to this.html without the r template prefix.");
|
|
3311
3117
|
this.innerHTML = html;
|
|
@@ -3328,7 +3134,7 @@ function createSolarite(extendsTag=null) {
|
|
|
3328
3134
|
/**
|
|
3329
3135
|
* Call render() only if it hasn't already been called. */
|
|
3330
3136
|
renderFirstTime() {
|
|
3331
|
-
if (!rendered.has(this) && this.render)
|
|
3137
|
+
if (!Globals.rendered.has(this) && this.render)
|
|
3332
3138
|
this.render();
|
|
3333
3139
|
}
|
|
3334
3140
|
|
|
@@ -3336,8 +3142,8 @@ function createSolarite(extendsTag=null) {
|
|
|
3336
3142
|
* Called automatically by the browser. */
|
|
3337
3143
|
connectedCallback() {
|
|
3338
3144
|
this.renderFirstTime();
|
|
3339
|
-
if (!connected.has(this)) {
|
|
3340
|
-
connected.add(this);
|
|
3145
|
+
if (!Globals.connected.has(this)) {
|
|
3146
|
+
Globals.connected.add(this);
|
|
3341
3147
|
this.onFirstConnect();
|
|
3342
3148
|
}
|
|
3343
3149
|
this.onConnect();
|
|
@@ -3464,6 +3270,12 @@ function createSolarite(extendsTag=null) {
|
|
|
3464
3270
|
}
|
|
3465
3271
|
}
|
|
3466
3272
|
|
|
3273
|
+
/**
|
|
3274
|
+
* Solarite JavasCript UI library.
|
|
3275
|
+
* MIT License
|
|
3276
|
+
* https://vorticode.github.io/solarite/
|
|
3277
|
+
*/
|
|
3278
|
+
|
|
3467
3279
|
/**
|
|
3468
3280
|
* TODO: The Proxy and the multiple base classes mess up 'instanceof Solarite'
|
|
3469
3281
|
* @type {Node|Class<HTMLElement>|function(tagName:string):Node|Class<HTMLElement>} */
|
|
@@ -3472,10 +3284,10 @@ let Solarite = new Proxy(createSolarite(), {
|
|
|
3472
3284
|
return createSolarite(...args)
|
|
3473
3285
|
}
|
|
3474
3286
|
});
|
|
3475
|
-
|
|
3287
|
+
let getInputValue = Util.getInputValue;
|
|
3476
3288
|
|
|
3477
3289
|
//Experimental:
|
|
3478
3290
|
//export {forEach, watchGet, watchSet} from './watch.js' // old, unfinished
|
|
3479
3291
|
//export {watch} from './watch2.js'; // unfinished
|
|
3480
3292
|
|
|
3481
|
-
export { ArgType, Solarite, Template, getArg, r };
|
|
3293
|
+
export { ArgType, Globals, Solarite, Template, delve, getArg, getInputValue, r };
|