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