solarite 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/build.bat +1 -0
- package/deno.lock +176 -0
- package/dist/Solarite-debug.js +456 -1118
- package/dist/Solarite.js +438 -1216
- package/dist/Solarite.min.js +2 -2
- package/docs/index.md +366 -216
- package/docs/js/codemirror/themeSolarIce.js +1 -1
- package/docs/js/documentation.js +1 -1
- package/docs/js/ui/FlexResizer.js +1 -2
- package/docs/media/documentation.css +2 -2
- package/index.html +40 -23
- package/package.json +1 -1
- package/readme.md +11 -1
- package/src/solarite/ExprPath.js +92 -3
- package/src/solarite/Globals.js +11 -0
- package/src/solarite/MultiValueMap.js +5 -1
- package/src/solarite/NodeGroup.js +54 -124
- package/src/solarite/NodeGroupManager.js +15 -125
- package/src/solarite/Shell.js +5 -5
- package/src/solarite/Solarite.js +5 -2
- package/src/solarite/Template.js +131 -8
- package/src/solarite/createSolarite.js +18 -6
- package/src/solarite/getArg.js +11 -11
- package/src/solarite/hash.js +1 -1
- package/src/solarite/r.js +24 -14
- package/src/solarite/watch.js +1 -1
- package/src/solarite/watch2.js +3 -2
- package/src/solarite/watch3.js +91 -0
- package/src/unused/onConnect.js +79 -0
- package/tests/Solarite.test.js +281 -34
- package/tests/Testimony.js +274 -67
- package/tests/index.html +3 -34
- package/tests/run.bat +2 -0
- package/tests/NodeGroup.test.js +0 -115
- package/tests/Shell.test.js +0 -75
package/dist/Solarite.js
CHANGED
|
@@ -81,8 +81,6 @@ var Util$1 = {
|
|
|
81
81
|
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
84
|
/**
|
|
87
85
|
* Follow a path into an object.
|
|
88
86
|
* @param obj {object}
|
|
@@ -180,19 +178,19 @@ function getArg(el, name, val=null, type=ArgType.String) {
|
|
|
180
178
|
case ArgType.Float:
|
|
181
179
|
return parseFloat(val);
|
|
182
180
|
case ArgType.String:
|
|
183
|
-
return val
|
|
181
|
+
return [undefined, null, false].includes(val) ? '' : val+'';
|
|
184
182
|
case ArgType.JSON:
|
|
185
|
-
try {
|
|
186
|
-
return JSON.parse(val);
|
|
187
|
-
} catch (e) {
|
|
188
|
-
return val;
|
|
189
|
-
}
|
|
190
183
|
case ArgType.Eval:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
184
|
+
if (typeof val === 'string' && val.length)
|
|
185
|
+
try {
|
|
186
|
+
if (type === ArgType.JSON)
|
|
187
|
+
return JSON.parse(val);
|
|
188
|
+
else
|
|
189
|
+
return eval(`(${val})`);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
return val;
|
|
192
|
+
}
|
|
193
|
+
else return val;
|
|
196
194
|
default:
|
|
197
195
|
return val;
|
|
198
196
|
}
|
|
@@ -336,7 +334,11 @@ class MultiValueMap {
|
|
|
336
334
|
return this.data[key] || [];
|
|
337
335
|
}
|
|
338
336
|
|
|
339
|
-
|
|
337
|
+
/**
|
|
338
|
+
* Remove one value from a key, and return it.
|
|
339
|
+
* @param key {string}
|
|
340
|
+
* @param val If specified, make sure we delete this specific value, if a key exists more than once.
|
|
341
|
+
* @returns {*} */
|
|
340
342
|
delete(key, val=undefined) {
|
|
341
343
|
// if (key === '["Html2",[[["Html3",["F1","A"]],["Html3",["F1","B"]]]]]')
|
|
342
344
|
// debugger;
|
|
@@ -381,6 +383,8 @@ class MultiValueMap {
|
|
|
381
383
|
}
|
|
382
384
|
}
|
|
383
385
|
|
|
386
|
+
|
|
387
|
+
|
|
384
388
|
let Util = {
|
|
385
389
|
|
|
386
390
|
bindStyles(style, root) {
|
|
@@ -542,6 +546,191 @@ let state = {...defaultState};
|
|
|
542
546
|
// For debugging only
|
|
543
547
|
|
|
544
548
|
|
|
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 {
|
|
554
|
+
|
|
555
|
+
/** @type {(Template|string|function)|(Template|string|function)[]} Evaulated expressions. */
|
|
556
|
+
exprs = []
|
|
557
|
+
|
|
558
|
+
/** @type {string[]} */
|
|
559
|
+
html = [];
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* If true, use this template to replace an existing element, instead of appending children to it.
|
|
563
|
+
* @type {?boolean} */
|
|
564
|
+
replaceMode;
|
|
565
|
+
|
|
566
|
+
/** Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
|
|
567
|
+
hashedFields;
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* @deprecated
|
|
571
|
+
* @type {ExprPath} Used with forEach() from watch.js
|
|
572
|
+
* Set in ExprPath.apply() */
|
|
573
|
+
parentPath;
|
|
574
|
+
|
|
575
|
+
/** @type {NodeGroup} */
|
|
576
|
+
nodeGroup;
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* @type {string[][]} */
|
|
580
|
+
paths = [];
|
|
581
|
+
|
|
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)
|
|
591
|
+
|
|
592
|
+
// Multiple templates can share the same htmlStrings array.
|
|
593
|
+
//this.hashedFields = [getObjectId(htmlStrings), exprs]
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Called by JSON.serialize when it encounters a Template.
|
|
600
|
+
* This prevents the hashed version from being too large. */
|
|
601
|
+
toJSON() {
|
|
602
|
+
if (!this.hashedFields)
|
|
603
|
+
this.hashedFields = [getObjectId(this.html, 'Html'), this.exprs];
|
|
604
|
+
|
|
605
|
+
return this.hashedFields
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Render the main template, which may indirectly call renderTemplate() to create children.
|
|
610
|
+
* @param el {HTMLElement}
|
|
611
|
+
* @param options {RenderOptions}
|
|
612
|
+
* @return {?DocumentFragment|HTMLElement} */
|
|
613
|
+
render(el=null, options={}) {
|
|
614
|
+
|
|
615
|
+
let ng;
|
|
616
|
+
if (!el) {
|
|
617
|
+
ng = new NodeGroup(this);
|
|
618
|
+
el = ng.getParentNode();
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
let ngm = NodeGroupManager.get(el);
|
|
622
|
+
if (ng)
|
|
623
|
+
ng.manager = ngm;
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
ngm.options = options;
|
|
628
|
+
ngm.clearSubscribers = false; // Used for deprecated watch() path?
|
|
629
|
+
ngm.mutationWatcherEnabled = false;
|
|
630
|
+
|
|
631
|
+
// Fast path for empty component.
|
|
632
|
+
if (this.html?.length === 1 && !this.html[0]) {
|
|
633
|
+
el.innerHTML = '';
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
|
|
637
|
+
// Find or create a NodeGroup for the template.
|
|
638
|
+
// This updates all nodes from the template.
|
|
639
|
+
let close;
|
|
640
|
+
let exact = ngm.getNodeGroup(this, true);
|
|
641
|
+
if (!exact) {
|
|
642
|
+
close = ngm.getNodeGroup(this, false);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
let firstTime = !ngm.rootNg;
|
|
646
|
+
ngm.rootNg = exact || close;
|
|
647
|
+
|
|
648
|
+
// Reparent NodeGroup
|
|
649
|
+
// TODO: Move this to NodeGroup?
|
|
650
|
+
let parent = ngm.rootNg.getParentNode();
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
// If this is the first time rendering this element.
|
|
654
|
+
if (firstTime) {
|
|
655
|
+
|
|
656
|
+
// Save slot children
|
|
657
|
+
let fragment;
|
|
658
|
+
if (el.childNodes.length) {
|
|
659
|
+
fragment = document.createDocumentFragment();
|
|
660
|
+
fragment.append(...el.childNodes);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Add rendered elements.
|
|
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}']`));
|
|
675
|
+
}
|
|
676
|
+
let unamedSlot = el.querySelector('slot:not([name])');
|
|
677
|
+
if (unamedSlot)
|
|
678
|
+
unamedSlot.append(fragment);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
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
|
+
|
|
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
|
+
}
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
ngm.reset(); // Mark all NodeGroups as available, for next render.
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
window.ngm = ngm;
|
|
718
|
+
}
|
|
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
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
545
734
|
/**
|
|
546
735
|
* Path to where an expression should be evaluated within a Shell.
|
|
547
736
|
* Path is only valid until the expressions before it are evaluated.
|
|
@@ -636,6 +825,86 @@ class ExprPath {
|
|
|
636
825
|
this.attrNames = new Set();
|
|
637
826
|
}
|
|
638
827
|
|
|
828
|
+
/**
|
|
829
|
+
*
|
|
830
|
+
* @param expr {Template|Node|Array|function|*}
|
|
831
|
+
* @param newNodes {(Node|Template)[]}
|
|
832
|
+
* @param secondPass {Array} Locations within newNodes to evaluate later. */
|
|
833
|
+
apply(expr, newNodes, secondPass) {
|
|
834
|
+
|
|
835
|
+
if (expr instanceof Template) {
|
|
836
|
+
expr.nodegroup = this.parentNg; // All tests pass w/o this.
|
|
837
|
+
|
|
838
|
+
let ng = this.parentNg.manager.getNodeGroup(expr, true);
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
if (ng) {
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
// TODO: Track ranges of changed nodes and only pass those to udomdiff?
|
|
846
|
+
// But will that break the swap benchmark?
|
|
847
|
+
newNodes.push(...ng.getNodes());
|
|
848
|
+
this.nodeGroups.push(ng);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// If expression, evaluate later to find partial match.
|
|
852
|
+
else {
|
|
853
|
+
secondPass.push([newNodes.length, this.nodeGroups.length]);
|
|
854
|
+
newNodes.push(expr);
|
|
855
|
+
this.nodeGroups.push(null); // placeholder
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Node created by an expression.
|
|
860
|
+
else if (expr instanceof Node) {
|
|
861
|
+
|
|
862
|
+
// DocumentFragment created by an expression.
|
|
863
|
+
if (expr instanceof DocumentFragment)
|
|
864
|
+
newNodes.push(...expr.childNodes);
|
|
865
|
+
else
|
|
866
|
+
newNodes.push(expr);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
else if (Array.isArray(expr))
|
|
870
|
+
for (let subExpr of expr)
|
|
871
|
+
this.apply(subExpr, newNodes, secondPass);
|
|
872
|
+
|
|
873
|
+
else if (typeof expr === 'function') {
|
|
874
|
+
let result = expr();
|
|
875
|
+
|
|
876
|
+
this.apply(result, newNodes, secondPass);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Text
|
|
880
|
+
else {
|
|
881
|
+
// Convert falsy values (but not 0) to empty string.
|
|
882
|
+
// Convert numbers to string so they compare the same.
|
|
883
|
+
let text = (expr === undefined || expr === false || expr === null) ? '' : expr + '';
|
|
884
|
+
|
|
885
|
+
// Fast path for updating the text of a single text node.
|
|
886
|
+
let first = this.nodeBefore.nextSibling;
|
|
887
|
+
if (first.nodeType === 3 && first.nextSibling === this.nodeMarker && !newNodes.includes(first)) {
|
|
888
|
+
if (first.textContent !== text)
|
|
889
|
+
first.textContent = text;
|
|
890
|
+
|
|
891
|
+
newNodes.push(first);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
else {
|
|
895
|
+
// TODO: Optimize this into a Set or Map or something?
|
|
896
|
+
if (!this.existingTextNodes)
|
|
897
|
+
this.existingTextNodes = this.getNodes().filter(n => n.nodeType === 3);
|
|
898
|
+
|
|
899
|
+
let idx = this.existingTextNodes.findIndex(n => n.textContent === text);
|
|
900
|
+
if (idx !== -1)
|
|
901
|
+
newNodes.push(...this.existingTextNodes.splice(idx, 1));
|
|
902
|
+
else
|
|
903
|
+
newNodes.push(this.parentNode.ownerDocument.createTextNode(text));
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
639
908
|
applyMultipleAttribs(node, expr) {
|
|
640
909
|
|
|
641
910
|
|
|
@@ -696,7 +965,6 @@ class ExprPath {
|
|
|
696
965
|
func = setValue;
|
|
697
966
|
args = [expr[0], expr.slice(1), node];
|
|
698
967
|
node.value = delve(expr[0], expr.slice(1));
|
|
699
|
-
//debugger;
|
|
700
968
|
}
|
|
701
969
|
}
|
|
702
970
|
else
|
|
@@ -814,8 +1082,7 @@ class ExprPath {
|
|
|
814
1082
|
* Clear the nodeCache of this ExprPath, as well as all parent and child ExprPaths that
|
|
815
1083
|
* share the same DOM parent node.
|
|
816
1084
|
*
|
|
817
|
-
* TODO: Is recursive clearing ever necessary?
|
|
818
|
-
*/
|
|
1085
|
+
* TODO: Is recursive clearing ever necessary? */
|
|
819
1086
|
clearNodesCache() {
|
|
820
1087
|
let path = this;
|
|
821
1088
|
|
|
@@ -833,7 +1100,7 @@ class ExprPath {
|
|
|
833
1100
|
|
|
834
1101
|
// Clear cache of child ExprPaths that have the same parentNode
|
|
835
1102
|
for (let ng of path.nodeGroups) {
|
|
836
|
-
if (ng) // Can be null from
|
|
1103
|
+
if (ng) // Can be null from apply()'s push(null) call.
|
|
837
1104
|
for (let path2 of ng.paths) {
|
|
838
1105
|
if (path2.type === PathType.Content && path2.parentNode === parentNode) {
|
|
839
1106
|
path2.nodesCache = null;
|
|
@@ -1039,7 +1306,7 @@ class Shell {
|
|
|
1039
1306
|
|
|
1040
1307
|
// Swap out Embedded Solarite Components with ${} attributes.
|
|
1041
1308
|
// Later, NodeGroup.render() will search for these and replace them with the real components.
|
|
1042
|
-
// Ctrl+F "
|
|
1309
|
+
// Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
|
|
1043
1310
|
if (context === htmlContext.Attribute) {
|
|
1044
1311
|
|
|
1045
1312
|
let lastIndex, lastMatch;
|
|
@@ -1049,7 +1316,7 @@ class Shell {
|
|
|
1049
1316
|
});
|
|
1050
1317
|
|
|
1051
1318
|
if (lastMatch) {
|
|
1052
|
-
let newTagName = lastMatch + '-
|
|
1319
|
+
let newTagName = lastMatch + '-solarite-placeholder';
|
|
1053
1320
|
lastHtml = lastHtml.slice(0, lastIndex) + newTagName + lastHtml.slice(lastIndex + lastMatch.length);
|
|
1054
1321
|
componentNames[lastMatch] = newTagName;
|
|
1055
1322
|
}
|
|
@@ -1066,9 +1333,9 @@ class Shell {
|
|
|
1066
1333
|
|
|
1067
1334
|
// 2. Create elements from html with placeholders.
|
|
1068
1335
|
let template = document.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
// Replace '-
|
|
1336
|
+
let joinedHtml = buffer.join('');
|
|
1337
|
+
|
|
1338
|
+
// Replace '-solarite-placeholder' close tags.
|
|
1072
1339
|
// TODO: is there a better way? What if the close tag is inside a comment?
|
|
1073
1340
|
for (let name in componentNames)
|
|
1074
1341
|
joinedHtml = joinedHtml.replaceAll(`</${name}>`, `</${componentNames[name]}>`);
|
|
@@ -1091,7 +1358,7 @@ class Shell {
|
|
|
1091
1358
|
|
|
1092
1359
|
// Replace attributes
|
|
1093
1360
|
if (node.nodeType === 1) {
|
|
1094
|
-
for (let attr of node.attributes) {
|
|
1361
|
+
for (let attr of [...node.attributes]) { // Copy the attributes array b/c we remove attributes as we go.
|
|
1095
1362
|
|
|
1096
1363
|
// Whole attribute
|
|
1097
1364
|
let matches = attr.name.match(/^[\ue000-\uf8ff]$/);
|
|
@@ -1187,8 +1454,8 @@ class Shell {
|
|
|
1187
1454
|
}
|
|
1188
1455
|
toRemove.map(el => el.remove());
|
|
1189
1456
|
|
|
1190
|
-
// Handle
|
|
1191
|
-
// Ctrl+F "
|
|
1457
|
+
// Handle solarite-placeholder's.
|
|
1458
|
+
// Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
|
|
1192
1459
|
//if (componentNames.size)
|
|
1193
1460
|
// this.components = [...this.fragment.querySelectorAll([...componentNames].join(','))]
|
|
1194
1461
|
|
|
@@ -1276,106 +1543,34 @@ class Shell {
|
|
|
1276
1543
|
let shells = new WeakMap();
|
|
1277
1544
|
|
|
1278
1545
|
/**
|
|
1279
|
-
*
|
|
1280
|
-
*
|
|
1281
|
-
*
|
|
1282
|
-
|
|
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
|
+
*/
|
|
1283
1562
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
/** Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
|
|
1296
|
-
hashedFields;
|
|
1297
|
-
|
|
1298
|
-
/**
|
|
1299
|
-
* @deprecated
|
|
1300
|
-
* @type {ExprPath} Used with forEach() from watch.js
|
|
1301
|
-
* Set in NodeGroup.applyOneExpr() */
|
|
1302
|
-
parentPath;
|
|
1303
|
-
|
|
1304
|
-
/** @type {NodeGroup} */
|
|
1305
|
-
nodeGroup;
|
|
1306
|
-
|
|
1307
|
-
/**
|
|
1308
|
-
* @type {string[][]} */
|
|
1309
|
-
paths = [];
|
|
1310
|
-
|
|
1311
|
-
/**
|
|
1312
|
-
*
|
|
1313
|
-
* @param htmlStrings {string[]}
|
|
1314
|
-
* @param exprs {*[]} */
|
|
1315
|
-
constructor(htmlStrings, exprs) {
|
|
1316
|
-
this.html = htmlStrings;
|
|
1317
|
-
this.exprs = exprs;
|
|
1318
|
-
|
|
1319
|
-
//this.trace = new Error().stack.split(/\n/g)
|
|
1320
|
-
|
|
1321
|
-
// Multiple templates can share the same htmlStrings array.
|
|
1322
|
-
//this.hashedFields = [getObjectId(htmlStrings), exprs]
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
/**
|
|
1328
|
-
* Called by JSON.serialize when it encounters a Template.
|
|
1329
|
-
* This prevents the hashed version from being too large. */
|
|
1330
|
-
toJSON() {
|
|
1331
|
-
if (!this.hashedFields)
|
|
1332
|
-
this.hashedFields = [getObjectId(this.html, 'Html'), this.exprs];
|
|
1333
|
-
|
|
1334
|
-
return this.hashedFields
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
toNode() {
|
|
1338
|
-
let ngm = new NodeGroupManager();
|
|
1339
|
-
return ngm.render(this);
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
getCloseKey() {
|
|
1343
|
-
// Use the joined html when debugging?
|
|
1344
|
-
//return '@'+this.html.join('|')
|
|
1345
|
-
|
|
1346
|
-
return '@'+this.hashedFields[0];
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
/**
|
|
1351
|
-
* ISC License
|
|
1352
|
-
*
|
|
1353
|
-
* Copyright (c) 2020, Andrea Giammarchi, @WebReflection
|
|
1354
|
-
*
|
|
1355
|
-
* Permission to use, copy, modify, and/or distribute this software for any
|
|
1356
|
-
* purpose with or without fee is hereby granted, provided that the above
|
|
1357
|
-
* copyright notice and this permission notice appear in all copies.
|
|
1358
|
-
*
|
|
1359
|
-
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
1360
|
-
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
1361
|
-
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
1362
|
-
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
1363
|
-
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
|
1364
|
-
* OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
1365
|
-
* PERFORMANCE OF THIS SOFTWARE.
|
|
1366
|
-
*/
|
|
1367
|
-
|
|
1368
|
-
/**
|
|
1369
|
-
* @param {Node} parentNode The container where children live
|
|
1370
|
-
* @param {Node[]} a The list of current/live children
|
|
1371
|
-
* @param {Node[]} b The list of future children
|
|
1372
|
-
* @param {(entry: Node, action: number) => Node} get
|
|
1373
|
-
* The callback invoked per each entry related DOM operation.
|
|
1374
|
-
* @param {Node} [before] The optional node used as anchor to insert before.
|
|
1375
|
-
* @returns {Node[]} The same list of future children.
|
|
1376
|
-
*/
|
|
1377
|
-
const udomdiff = (parentNode, a, b, before) => {
|
|
1378
|
-
|
|
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
|
+
|
|
1379
1574
|
|
|
1380
1575
|
const bLength = b.length;
|
|
1381
1576
|
let aEnd = a.length;
|
|
@@ -1534,723 +1729,8 @@ const udomdiff = (parentNode, a, b, before) => {
|
|
|
1534
1729
|
}
|
|
1535
1730
|
}
|
|
1536
1731
|
}
|
|
1537
|
-
return b;
|
|
1538
|
-
};
|
|
1539
|
-
|
|
1540
|
-
/**
|
|
1541
|
-
* Tools for watch variables and performing precise renders.
|
|
1542
|
-
*/
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
/**
|
|
1546
|
-
* Stores info how to transform a path to a template. */
|
|
1547
|
-
class TransformerInfo {
|
|
1548
|
-
constructor(path, transformer, hash) {
|
|
1549
|
-
this.path = path;
|
|
1550
|
-
this.transformer = transformer;
|
|
1551
|
-
this.hash = hash;
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
/**
|
|
1557
|
-
* Maps an object path to the function that converts it to a Template.
|
|
1558
|
-
* Once it's convert to a template, we can get the hash of that Tempate.
|
|
1559
|
-
* Then that hash tells us what NodeGroups are affected by the object.
|
|
1560
|
-
*
|
|
1561
|
-
* We store the function to get the Template, instead of the Template itself,
|
|
1562
|
-
* so we can call that function again when the object has a new value.
|
|
1563
|
-
* @type {MultiValueMap<Object, function(...Object):Template>} */
|
|
1564
|
-
let pathToTransformer = new MultiValueMap(); // uses a Set() for each value.
|
|
1565
|
-
|
|
1566
|
-
/**
|
|
1567
|
-
*
|
|
1568
|
-
* @param objectPaths {(*|function)[]}
|
|
1569
|
-
* @returns {Template} */
|
|
1570
|
-
function watchGet(...objectPaths) {
|
|
1571
|
-
|
|
1572
|
-
/** @type {function} */
|
|
1573
|
-
let transformer = objectPaths.at(-1);
|
|
1574
|
-
let paths;
|
|
1575
|
-
if (typeof transformer === 'function') {
|
|
1576
|
-
paths = objectPaths.slice(0, -1);
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
// No transformer provided, so we create our own.
|
|
1580
|
-
else if (objectPaths.length === 1) {
|
|
1581
|
-
paths = [objectPaths[0].slice(0, -1)];
|
|
1582
|
-
let prop = objectPaths[0].at(-1);
|
|
1583
|
-
transformer = (...args) => (args[0][prop]);
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
// Save arguments used to call the template, so we can call it again when those args have their values change.
|
|
1588
|
-
let args = [];
|
|
1589
|
-
for (let path of paths) {
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
let obj = delve(watchSet(path[0]), path.slice(1));
|
|
1593
|
-
args.push(obj);
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
let template = transformer(...args);
|
|
1597
|
-
|
|
1598
|
-
// If the result isn't a Template, convert the function to return a Template that wraps the result.
|
|
1599
|
-
// This way NodeGroupManager.findAndDelete() can find a NodeGroup that matches this Template's hash.
|
|
1600
|
-
if (!(template instanceof Template)) {
|
|
1601
|
-
let oldToTemplate = transformer;
|
|
1602
|
-
transformer = function() {
|
|
1603
|
-
return new Template(['', ''], [oldToTemplate(...arguments)]);
|
|
1604
|
-
};
|
|
1605
|
-
template = transformer(...args);
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
// Map the object paths to the function that creates a template.
|
|
1609
|
-
let hash = getObjectHash(template);
|
|
1610
|
-
for (let path of paths) {
|
|
1611
|
-
let serializedPath = serializePath(path);
|
|
1612
|
-
pathToTransformer.add(serializedPath, new TransformerInfo(path, transformer, hash)); // Uses a Set() to ensure no duplicates.
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
return template;
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
//let proxyCache = new WeakMap();
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
/**
|
|
1622
|
-
* Set the value of a variable in a way that's watched, so later when we call .renderWatched()
|
|
1623
|
-
* We can find what NodeGroups to update.*/
|
|
1624
|
-
function watchSet(obj) {
|
|
1625
|
-
if (obj?.$isProxy===true)
|
|
1626
|
-
return obj; // It's already a Proxy.
|
|
1627
|
-
|
|
1628
|
-
// This cache doesn't make things faster.
|
|
1629
|
-
// let result = proxyCache.get(obj);
|
|
1630
|
-
// if (!result) {
|
|
1631
|
-
// result = new Proxy(obj, new ProxyHandler(obj));
|
|
1632
|
-
// proxyCache.set(obj, result);
|
|
1633
|
-
// }
|
|
1634
|
-
// return result;
|
|
1635
|
-
return new Proxy(obj, new ProxyHandler$1(obj));
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
/**
|
|
1639
|
-
* Loop over each item and apply watchGet() to each item.
|
|
1640
|
-
* @param arrayPath {*[]}
|
|
1641
|
-
* @param callback {function(obj:Object, index:int):Template}
|
|
1642
|
-
* @returns {Template} */
|
|
1643
|
-
function forEach(arrayPath, callback) {
|
|
1644
|
-
let array = delve(arrayPath[0], arrayPath.slice(1));
|
|
1645
|
-
|
|
1646
|
-
// This is retrieved on the 'insert' path inside renderWatched()
|
|
1647
|
-
let ngm = NodeGroupManager.get(arrayPath[0]);
|
|
1648
|
-
if (ngm.clearSubscribers) {
|
|
1649
|
-
ngm.clearSubscribers = false;
|
|
1650
|
-
ngm.pathToLoopInfo = new MultiValueMap();
|
|
1651
|
-
} // TODO: Move tis into NodeGroupMAnager.get() without breaking things?
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
let newItems = [...array.map((item, i) => {
|
|
1655
|
-
// TODO: This needs to wrap callback so we can pass it the index also.
|
|
1656
|
-
return watchGet([...arrayPath, i], callback); // calls callback(array[i], i)
|
|
1657
|
-
})
|
|
1658
|
-
];
|
|
1659
|
-
|
|
1660
|
-
// We return a template that wraps the array
|
|
1661
|
-
// So that NodeGroup.applyOneExpr can set the ExprPath and nextSibling on the template.
|
|
1662
|
-
// Then the 'insert' path in renderWatched() uses that data fora dding more nodes.
|
|
1663
|
-
let result = new Template(['', ''], [newItems]);
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
// We get a unique hash for each foreach template because the [''] array is unique each time.
|
|
1667
|
-
let loopInfo = new LoopInfo(result, callback);
|
|
1668
|
-
ngm.pathToLoopInfo.add(serializePath(arrayPath), loopInfo);
|
|
1669
|
-
return result;
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
function serializePath(path) {
|
|
1673
|
-
// Convert any array indices to strings, so serialized comparisons work.
|
|
1674
|
-
return JSON.stringify([getObjectId(path[0]), ...path.slice(1).map(item => item+'')])
|
|
1675
|
-
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
/**
|
|
1680
|
-
* When an object property is accessed, a new Proxy with a new instance of this handler class is created,
|
|
1681
|
-
* but it tracks the path from the root to the property.
|
|
1682
|
-
* That way when a property is set, it can report the changed path. */
|
|
1683
|
-
class ProxyHandler$1 {
|
|
1684
|
-
|
|
1685
|
-
/**
|
|
1686
|
-
* @param root An element managed by a NodeGroupManager. The same as the NodeGroupManager's rootEl.
|
|
1687
|
-
* @param path {string[]} Used internally. */
|
|
1688
|
-
constructor(root, path=[]) {
|
|
1689
|
-
|
|
1690
|
-
this.root = root;
|
|
1691
|
-
this.path = path; // path from root to this Proxy.
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
/**
|
|
1695
|
-
* @param obj {Object}
|
|
1696
|
-
* @param prop {string} */
|
|
1697
|
-
get(obj, prop) {
|
|
1698
|
-
|
|
1699
|
-
// Special props. Currently unused.
|
|
1700
|
-
// if (prop === '$path')
|
|
1701
|
-
// return this.path;
|
|
1702
|
-
// if (prop === '$root')
|
|
1703
|
-
// return this.root;
|
|
1704
|
-
if (prop === '$isProxy')
|
|
1705
|
-
return true;
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
// 1. Array.splice()
|
|
1709
|
-
if (prop === 'splice' && Array.isArray(obj)) {
|
|
1710
|
-
return (index, deleteCount, ...items) => {
|
|
1711
|
-
let ngm = NodeGroupManager.get(this.root);
|
|
1712
|
-
|
|
1713
|
-
if (deleteCount) {
|
|
1714
|
-
|
|
1715
|
-
// Get the hash of each object along the delete range. The process to get the hash is:
|
|
1716
|
-
// Serialized Path -> transformer -> Template -> hash.
|
|
1717
|
-
let hashes = [];
|
|
1718
|
-
for (let i=index; i<index+deleteCount; i++) {
|
|
1719
|
-
let serializedPath = serializePath([this.root, ...this.path, i+'']);
|
|
1720
|
-
|
|
1721
|
-
let obj = delve(this.root, [...this.path, i]);
|
|
1722
|
-
for (let transformerInfo of pathToTransformer.getAll(serializedPath)) {
|
|
1723
|
-
let template = transformerInfo.transformer(obj);
|
|
1724
|
-
let hash = getObjectHash(template);
|
|
1725
|
-
hashes.push(hash); // Hashes may go to nodes in more than one loop.
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1729
|
-
ngm.changes.push(new Change('delete', this.root, [...this.path, index+''], hashes));
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
//let oldArray = [...obj];
|
|
1733
|
-
let result = obj.splice(index, deleteCount);
|
|
1734
|
-
|
|
1735
|
-
// Inserting
|
|
1736
|
-
if (items.length) {
|
|
1737
|
-
|
|
1738
|
-
let beforeNgs;
|
|
1739
|
-
for (let loopInfo of ngm.getLoopInfo([this.root, ...this.path])) {
|
|
1740
|
-
let beforeObj = delve(this.root, [...this.path, index]);
|
|
1741
|
-
|
|
1742
|
-
// Find where to insert before.
|
|
1743
|
-
if (beforeObj) {
|
|
1744
|
-
let beforeTemplate = loopInfo.itemTransformer(beforeObj);
|
|
1745
|
-
let beforeHash = getObjectHash(beforeTemplate);
|
|
1746
|
-
beforeNgs = ngm.nodeGroupsAvailable.data[beforeHash];
|
|
1747
|
-
|
|
1748
|
-
if (beforeNgs) {
|
|
1749
|
-
let hash = getObjectHash(loopInfo.template);
|
|
1750
|
-
let loopNgs = ngm.nodeGroupsAvailable.data[hash] || [];
|
|
1751
|
-
for (let loopNg of loopNgs)
|
|
1752
|
-
for (let beforeNg of beforeNgs)
|
|
1753
|
-
if (beforeNg.startNode.parentNode === loopNg.startNode.parentNode)
|
|
1754
|
-
ngm.changes.push(new Change('insert', this.root, [...this.path, index + ''], items, beforeTemplate));
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
if (!beforeNgs)
|
|
1758
|
-
ngm.changes.push(new Change('insert', this.root, [...this.path, index + ''], items));
|
|
1759
|
-
}
|
|
1760
|
-
obj.splice(index, 0, ...items);
|
|
1761
|
-
}
|
|
1762
|
-
return result;
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
// 2. Get property
|
|
1769
|
-
// If we're getting an object or array property, apply watch() to it recursively.
|
|
1770
|
-
let result = Reflect.get(obj, prop);
|
|
1771
|
-
if (result && typeof result === 'object') {
|
|
1772
|
-
let handler = new ProxyHandler$1(this.root, [...this.path, prop]); // same root, one level deeper on the path.
|
|
1773
|
-
return new Proxy(result, handler);
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
return result;
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
set(obj, prop, newValue) {
|
|
1781
|
-
let ngm = NodeGroupManager.get(this.root);
|
|
1782
|
-
ngm.changes.push(new Change('set', this.root, [...this.path, prop], newValue));
|
|
1783
|
-
return Reflect.set(obj, prop, newValue)
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
/**
|
|
1789
|
-
*
|
|
1790
|
-
*/
|
|
1791
|
-
class Change {
|
|
1792
|
-
|
|
1793
|
-
/**
|
|
1794
|
-
* @param action {string}
|
|
1795
|
-
* @param root {Object|Array}
|
|
1796
|
-
* @param path {string[]}
|
|
1797
|
-
* @param value
|
|
1798
|
-
* If setting a value, this is the new value.
|
|
1799
|
-
* If deleting from an array, this is an array of all the NodeGroups to delete.
|
|
1800
|
-
*
|
|
1801
|
-
* @param beforeTemplate
|
|
1802
|
-
* */
|
|
1803
|
-
constructor(action, root, path, value, beforeTemplate=null) {
|
|
1804
|
-
this.action = action;
|
|
1805
|
-
|
|
1806
|
-
// TODO: Store root as first item of path, to be consistent with code elsewhere.
|
|
1807
|
-
this.root = root;
|
|
1808
|
-
this.path = path;
|
|
1809
|
-
this.value = value;
|
|
1810
|
-
this.beforeTemplate = beforeTemplate;
|
|
1811
|
-
|
|
1812
|
-
/** @type {TransformerInfo[]} */
|
|
1813
|
-
this.transformerInfo = [];
|
|
1814
|
-
|
|
1815
|
-
// Traverse up the path.
|
|
1816
|
-
for (let i=this.path.length; i>0; i--) {
|
|
1817
|
-
let path = this.path.slice(0, i);
|
|
1818
|
-
let fullPath = [this.root, ...path];
|
|
1819
|
-
|
|
1820
|
-
let serializedPath = getObjectHash(fullPath); // TODO: Why not serializedPath() ?
|
|
1821
|
-
this.transformerInfo.push(...pathToTransformer.getAll(serializedPath));
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
let logGets = false;
|
|
1827
|
-
let gets = [];
|
|
1828
|
-
|
|
1829
|
-
let withinSet = 0;
|
|
1830
|
-
|
|
1831
|
-
/**
|
|
1832
|
-
* Turn the props on obj into JavasCript properties that return Proxies when accessed.
|
|
1833
|
-
* If called more than once, return the already-converted object.
|
|
1834
|
-
* @param obj {Object}
|
|
1835
|
-
* @param props {string}
|
|
1836
|
-
* @returns {*|{$proxyHandler}}
|
|
1837
|
-
*/
|
|
1838
|
-
function watch(obj, ...props) {
|
|
1839
|
-
|
|
1840
|
-
if (props.length) {
|
|
1841
|
-
let internalProps = {};
|
|
1842
|
-
for (let prop of props) {
|
|
1843
|
-
internalProps[prop] = obj[prop];
|
|
1844
|
-
Object.defineProperty(obj, prop, {
|
|
1845
|
-
get() {
|
|
1846
|
-
return new Proxy(obj, new ProxyHandler(obj, [], internalProps))[prop];
|
|
1847
|
-
},
|
|
1848
|
-
set(value) {
|
|
1849
|
-
return watch(this)[prop] = value;
|
|
1850
|
-
}
|
|
1851
|
-
});
|
|
1852
|
-
}
|
|
1853
|
-
return;
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
if (obj?.$proxyHandler)
|
|
1858
|
-
return obj; // It's already a Proxy.
|
|
1859
|
-
|
|
1860
|
-
// This cache doesn't make things faster.
|
|
1861
|
-
// But could it save memory?
|
|
1862
|
-
// let result = proxyCache.get(obj);
|
|
1863
|
-
// if (!result) {
|
|
1864
|
-
// result = new Proxy(obj, new ProxyHandler(obj));
|
|
1865
|
-
// proxyCache.set(obj, result);
|
|
1866
|
-
// }
|
|
1867
|
-
// return result;
|
|
1868
|
-
|
|
1869
|
-
return new Proxy(obj, new ProxyHandler(obj));
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
|
-
/**
|
|
1873
|
-
* Provides methods used when a Proxied version of a property is accessed on an object returned by watch() */
|
|
1874
|
-
class ProxyHandler {
|
|
1875
|
-
|
|
1876
|
-
serializedPath;
|
|
1877
|
-
|
|
1878
|
-
/**
|
|
1879
|
-
* @param root An element managed by a NodeGroupManager. The same as the NodeGroupManager's rootEl.
|
|
1880
|
-
* @param path {string[]} Used internally.
|
|
1881
|
-
* @param props */
|
|
1882
|
-
constructor(root, path=[], props=null) {
|
|
1883
|
-
|
|
1884
|
-
this.root = root;
|
|
1885
|
-
this.path = path; // path from root to this Proxy.
|
|
1886
|
-
this.props = props;
|
|
1887
|
-
}
|
|
1888
|
-
|
|
1889
|
-
/**
|
|
1890
|
-
* Get the full path to this property from the root watched object.
|
|
1891
|
-
* @param atIndex
|
|
1892
|
-
* @returns {string} */
|
|
1893
|
-
getSerializedPath(atIndex=null) {
|
|
1894
|
-
if (!this.serializedPath)
|
|
1895
|
-
this.serializedPath = JSON.stringify([getObjectId(this.root), ...this.path.map(item => item + '')]);
|
|
1896
|
-
|
|
1897
|
-
if (atIndex!== null)
|
|
1898
|
-
return this.serializedPath.slice(0, -1) + ',"' + atIndex + '"]';
|
|
1899
|
-
return this.serializedPath;
|
|
1900
|
-
}
|
|
1901
|
-
|
|
1902
|
-
/**
|
|
1903
|
-
* Return a ProxyHandler for a property one level deeper at pathItem.
|
|
1904
|
-
* @param pathItem {string}
|
|
1905
|
-
* @returns {ProxyHandler} */
|
|
1906
|
-
extend(pathItem) {
|
|
1907
|
-
pathItem += '';
|
|
1908
|
-
assert(!this.root.$proxyHandler);
|
|
1909
|
-
let result = new ProxyHandler(this.root, [...this.path, pathItem], this.props);
|
|
1910
|
-
if (this.serializedPath)
|
|
1911
|
-
result.serializedPath = this.getSerializedPath(pathItem);
|
|
1912
|
-
|
|
1913
|
-
return result;
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
/**
|
|
1917
|
-
* Called directly by JavaScript when accessing the value of a property.
|
|
1918
|
-
* @param obj {Object}
|
|
1919
|
-
* @param prop {string} */
|
|
1920
|
-
get(obj, prop) {
|
|
1921
|
-
|
|
1922
|
-
// 1. Special props.
|
|
1923
|
-
if (prop === '$proxyHandler')
|
|
1924
|
-
return this;
|
|
1925
|
-
else if (prop === '$removeProxy')
|
|
1926
|
-
return delve(this.props || this.root, this.path);
|
|
1927
|
-
|
|
1928
|
-
// 2. Array functions.
|
|
1929
|
-
else if (prop === 'map' && Array.isArray(obj)) {
|
|
1930
|
-
|
|
1931
|
-
let ngm = NodeGroupManager.get(this.root);
|
|
1932
|
-
ngm.clearSubscribersIfNeeded();
|
|
1933
|
-
|
|
1934
|
-
return callback => {
|
|
1935
|
-
let loopInfo;
|
|
1936
|
-
|
|
1937
|
-
let children = [];
|
|
1938
|
-
let transformer = obj => {
|
|
1939
|
-
let templates = [];
|
|
1940
|
-
|
|
1941
|
-
for (let i = 0; i < obj.length; i++) {
|
|
1942
|
-
|
|
1943
|
-
// Watch obj[i].
|
|
1944
|
-
let handler = this.extend(i);
|
|
1945
|
-
let item = obj[i];
|
|
1946
|
-
if (!item.$proxyHandler)
|
|
1947
|
-
item = new Proxy(item, handler);
|
|
1948
|
-
|
|
1949
|
-
let template = callback(item, i, obj);
|
|
1950
|
-
templates.push(template);
|
|
1951
|
-
|
|
1952
|
-
// If the loop is re-evaluted via Set() then we add duplicate TemplateInfo's
|
|
1953
|
-
//if (!withinSet) {
|
|
1954
|
-
let spath = this.getSerializedPath(i);
|
|
1955
|
-
let subscriber = new Subscriber(callback, template);
|
|
1956
|
-
subscriber.parent = loopInfo;
|
|
1957
|
-
ngm.subscribers.add(spath, subscriber);
|
|
1958
|
-
children.push([spath, subscriber]);
|
|
1959
|
-
//}
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
// A parent Template that surrounds all the items in the loop.
|
|
1963
|
-
// This lets us get template.nodeGroup.endNode so we can insertBefore().
|
|
1964
|
-
return new Template(['', ''], [templates]);
|
|
1965
|
-
};
|
|
1966
|
-
|
|
1967
|
-
if (!withinSet)
|
|
1968
|
-
loopInfo = new Subscriber(transformer, null, callback);
|
|
1969
|
-
let wholeLoopTemplate = transformer(obj);
|
|
1970
|
-
if (!withinSet) {
|
|
1971
|
-
loopInfo.template = wholeLoopTemplate;
|
|
1972
|
-
loopInfo.children = children;
|
|
1973
|
-
ngm.subscribers.add(this.getSerializedPath(), loopInfo);
|
|
1974
|
-
}
|
|
1975
|
-
return wholeLoopTemplate;
|
|
1976
|
-
}
|
|
1977
|
-
}
|
|
1978
|
-
|
|
1979
|
-
else if ((prop ==='splice' || prop === 'fastSplice') && Array.isArray(obj)) {
|
|
1980
|
-
let ngm = NodeGroupManager.get(this.root);
|
|
1981
|
-
return (index, deleteCount, ...items) => {
|
|
1982
|
-
let diff = items.length - deleteCount;
|
|
1983
|
-
let objLength = obj.length;
|
|
1984
|
-
|
|
1985
|
-
// Delete
|
|
1986
|
-
if (deleteCount) {
|
|
1987
|
-
for (let i=index; i<index+deleteCount; i++) {
|
|
1988
|
-
|
|
1989
|
-
// Update pathToTemplates
|
|
1990
|
-
let spath = this.getSerializedPath(i);
|
|
1991
|
-
|
|
1992
|
-
// Delete nodes of associated NodeGroups.
|
|
1993
|
-
for (let subscriber of ngm.subscribers.data[spath] || []) {
|
|
1994
|
-
let ng = subscriber.template.nodeGroup;
|
|
1995
|
-
for (let node of ng.getNodes())
|
|
1996
|
-
node.remove();
|
|
1997
|
-
|
|
1998
|
-
// Delete NodeGroup from NodeGroupManager.
|
|
1999
|
-
ngm.nodeGroupsAvailable.delete(ng.exactKey, ng);
|
|
2000
|
-
}
|
|
2001
|
-
|
|
2002
|
-
delete ngm.subscribers.data[spath]; // Deletes templates associated with every loop where this is used.
|
|
2003
|
-
}
|
|
2004
|
-
}
|
|
2005
|
-
|
|
2006
|
-
// Update indices of subsequent items.
|
|
2007
|
-
if (diff) {
|
|
2008
|
-
let loopPath = this.getSerializedPath();
|
|
2009
|
-
let loopInfo = [...ngm.subscribers.getAll(loopPath)][0]; // TODO: Handle multiple loops.
|
|
2010
|
-
|
|
2011
|
-
let move = (oldIndex) => {
|
|
2012
|
-
let newIndex = oldIndex+diff;
|
|
2013
|
-
let oldPath = this.getSerializedPath(oldIndex);
|
|
2014
|
-
let newPath = this.getSerializedPath(newIndex);
|
|
2015
|
-
|
|
2016
|
-
let subscribers = ngm.subscribers.data[oldPath];
|
|
2017
|
-
delete ngm.subscribers.data[oldPath]; // TODO: Some can be overwritten w/o being deleted?
|
|
2018
|
-
ngm.subscribers.data[newPath] = subscribers;
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
// Update associated NodeGroups by passing them newIndex.
|
|
2022
|
-
// This is unnecessary for most loops since they don't use the index.
|
|
2023
|
-
// fastSplice skips this path, it skips updating item indices.
|
|
2024
|
-
if (prop === 'splice') {
|
|
2025
|
-
let array = delve(this.root, this.path);
|
|
2026
|
-
for (let subscriber of subscribers) {
|
|
2027
|
-
let ng = subscriber.template.nodeGroup;
|
|
2028
|
-
|
|
2029
|
-
let item = array[oldIndex];
|
|
2030
|
-
|
|
2031
|
-
//assert(!item.$proxyHandler)
|
|
2032
|
-
let proxyItem = getProxy(item, this, this.path, newIndex); //new Proxy(item, this.extend(newIndex));
|
|
2033
|
-
let exprs = loopInfo.itemTransformer(proxyItem, newIndex).exprs; // TODO: Pass updated array as third argument to transformer.
|
|
2034
|
-
ng.applyExprs(exprs); // this is the slow part.
|
|
2035
|
-
}
|
|
2036
|
-
}
|
|
2037
|
-
};
|
|
2038
|
-
|
|
2039
|
-
// Iterate in different directions depending on whether diff is positive or negative.
|
|
2040
|
-
if (diff > 0) // Moving items to the right, so we iterate backward from the end.
|
|
2041
|
-
for (let i = objLength-1; i >= index + items.length + deleteCount; i--)
|
|
2042
|
-
move(i);
|
|
2043
|
-
|
|
2044
|
-
else // Moving items to the left, so we iterate forward.
|
|
2045
|
-
for (let i = index + items.length + deleteCount; i < objLength; i++)
|
|
2046
|
-
move(i);
|
|
2047
|
-
}
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
// Add new items
|
|
2051
|
-
if (items.length) {
|
|
2052
|
-
let loopPath = this.getSerializedPath();
|
|
2053
|
-
|
|
2054
|
-
let beforePath = this.getSerializedPath(index);
|
|
2055
|
-
let ngm = NodeGroupManager.get(this.root);
|
|
2056
|
-
|
|
2057
|
-
for (let loopInfo of ngm.subscribers.getAll(loopPath)) {
|
|
2058
|
-
let beforeNodes = index < objLength - deleteCount
|
|
2059
|
-
? [...ngm.subscribers.getAll(beforePath)].map(t => t.template.nodeGroup.startNode)
|
|
2060
|
-
: [loopInfo.template.nodeGroup.endNode];
|
|
2061
|
-
for (let beforeNode of beforeNodes) { // TODO: Need to match the beforeNg with the loopInfo instead of iterating.
|
|
2062
|
-
for (let i = 0; i < items.length; i++) {
|
|
2063
|
-
|
|
2064
|
-
// Create NodeGroup of new item.
|
|
2065
|
-
let itemHandler = this.extend(index + i);
|
|
2066
|
-
assert(!items[i].$proxyHandler);
|
|
2067
|
-
let proxyItem = new Proxy(items[i], itemHandler);
|
|
2068
|
-
let template = loopInfo.itemTransformer(proxyItem);
|
|
2069
|
-
let ng = ngm.getNodeGroup(template, null, true);
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
for (let node of ng.getNodes()) {
|
|
2073
|
-
beforeNode.parentNode.insertBefore(node, beforeNode);
|
|
2074
|
-
//loopInfo.template.nodeGroup.endNode = node; // The loop's end node is actually an empty text node, so don't do this.
|
|
2075
|
-
}
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
// Add new items to ngm.templateInfo
|
|
2079
|
-
let spath = itemHandler.getSerializedPath();
|
|
2080
|
-
let subscriber = new Subscriber(loopInfo.itemTransformer, template);
|
|
2081
|
-
subscriber.parent = loopInfo;
|
|
2082
|
-
ngm.subscribers.add(spath, subscriber);
|
|
2083
|
-
}
|
|
2084
|
-
}
|
|
2085
|
-
loopInfo.template.nodeGroup.parentPath.clearNodesCache();
|
|
2086
|
-
loopInfo.template.nodeGroup.nodesCache = null;
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
|
-
let result = obj.splice(index, deleteCount, ...items);
|
|
2091
|
-
|
|
2092
|
-
//this.notify(this.path);
|
|
2093
|
-
|
|
2094
|
-
return result;
|
|
2095
|
-
}
|
|
2096
|
-
}
|
|
2097
|
-
|
|
2098
|
-
// Allow these functions to use proxied objects as arguments.
|
|
2099
|
-
else if (prop ==='indexOf' && Array.isArray(obj)) {
|
|
2100
|
-
return item => {
|
|
2101
|
-
return obj.indexOf(item.$removeProxy || item)
|
|
2102
|
-
}
|
|
2103
|
-
}
|
|
2104
|
-
|
|
2105
|
-
// 3. Get property
|
|
2106
|
-
else {
|
|
2107
|
-
|
|
2108
|
-
let obj2 = obj === this.root && this.props ? this.props : obj;
|
|
2109
|
-
let result = Reflect.get(obj2, prop);
|
|
2110
|
-
|
|
2111
|
-
// This is read by watchFunction() which is called in NodeGroup.applyOneExpr().
|
|
2112
|
-
// It's used to see what variables contribute to an expression.
|
|
2113
|
-
if (logGets)
|
|
2114
|
-
gets.push([this.root, ...this.path, prop]);
|
|
2115
|
-
|
|
2116
|
-
// If we're getting an object or array property, apply watch() to it recursively.
|
|
2117
|
-
if (result && typeof result === 'object') {
|
|
2118
|
-
let handler = this.extend(prop); // same root, one level deeper on the path.
|
|
2119
|
-
|
|
2120
|
-
return new Proxy(result, handler);
|
|
2121
|
-
}
|
|
2122
|
-
|
|
2123
|
-
return result;
|
|
2124
|
-
}
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
/**
|
|
2129
|
-
* Called directly by JavaScript when setting the value of a property via equals.
|
|
2130
|
-
* @param obj
|
|
2131
|
-
* @param prop
|
|
2132
|
-
* @param value
|
|
2133
|
-
* @returns {boolean} */
|
|
2134
|
-
set(obj, prop, value) {
|
|
2135
|
-
withinSet++;
|
|
2136
|
-
|
|
2137
|
-
let obj2 = obj === this.root && this.props ? this.props : obj;
|
|
2138
|
-
let result = Reflect.set(obj2, prop, value);
|
|
2139
|
-
let fullPath = [...this.path, prop+''];
|
|
2140
|
-
this.notify(fullPath);
|
|
2141
|
-
|
|
2142
|
-
withinSet --;
|
|
2143
|
-
|
|
2144
|
-
return result;
|
|
2145
|
-
}
|
|
2146
|
-
|
|
2147
|
-
/**
|
|
2148
|
-
* Find every subscriber for fullPath, and above, and call applyExprs() for it.
|
|
2149
|
-
* @param fullPath {string[]}
|
|
2150
|
-
* @param excluded {Set} */
|
|
2151
|
-
notify(fullPath, excluded = new Set()) {
|
|
2152
|
-
|
|
2153
|
-
// Traverse upward through the path, looking for pathToTemplates.
|
|
2154
|
-
let ngm = NodeGroupManager.get(this.root);
|
|
2155
|
-
|
|
2156
|
-
let rootHash = getObjectId(this.root);
|
|
2157
|
-
|
|
2158
|
-
let len = fullPath.length;
|
|
2159
|
-
while (len >= 1) {
|
|
2160
|
-
let path = fullPath.slice(0, len);
|
|
2161
|
-
let val = delve(this.root, path);
|
|
2162
|
-
let serializedPath = JSON.stringify([rootHash, ...path]);
|
|
2163
|
-
for (let subscriber of ngm.subscribers.getAll(serializedPath)) {
|
|
2164
|
-
// We already applied expressions for a single item within this loop.
|
|
2165
|
-
if (excluded.has(subscriber))
|
|
2166
|
-
continue;
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
// Delete child subscriptions so we don't have duplicate subscriptions when we call applyExprs() directly below.
|
|
2170
|
-
if (subscriber.children) {
|
|
2171
|
-
for (let [spath, childInfo] of subscriber.children)
|
|
2172
|
-
ngm.subscribers.delete(spath, childInfo);
|
|
2173
|
-
subscriber.children = undefined;
|
|
2174
|
-
}
|
|
2175
|
-
|
|
2176
|
-
let proxyVal = getProxy(val, this, path);
|
|
2177
|
-
let exprs = subscriber.transformer(proxyVal).exprs; // TODO: Pass updated array as third argument to transformer.
|
|
2178
|
-
for (let path of subscriber.template.nodeGroup.paths)
|
|
2179
|
-
path.clearNodesCache();
|
|
2180
|
-
|
|
2181
|
-
// Apply expressions.
|
|
2182
|
-
subscriber.template.nodeGroup.applyExprs(exprs);
|
|
2183
|
-
|
|
2184
|
-
// Don't also process parent loop after updating a single item within it.
|
|
2185
|
-
if (subscriber.parent)
|
|
2186
|
-
excluded.add(subscriber.parent);
|
|
2187
|
-
}
|
|
2188
|
-
len--;
|
|
2189
|
-
}
|
|
2190
|
-
}
|
|
2191
|
-
}
|
|
2192
|
-
|
|
2193
|
-
function getProxy(obj, ph, path, path2) {
|
|
2194
|
-
if (!obj || !typeof obj !== 'object')
|
|
2195
|
-
return obj;
|
|
2196
|
-
|
|
2197
|
-
if (obj.$proxyHandler) {
|
|
2198
|
-
|
|
2199
|
-
assert(obj.$proxyHandler.root === ph.root && JSON.stringify(obj.$proxyHandler.path) === JSON.stringify(path));
|
|
2200
|
-
return obj;
|
|
2201
|
-
}
|
|
2202
|
-
if (path2)
|
|
2203
|
-
path = [...path, path2+''];
|
|
2204
|
-
return new Proxy(obj, new ProxyHandler(ph.root, path, ph.props));
|
|
2205
|
-
}
|
|
2206
|
-
|
|
2207
|
-
/**
|
|
2208
|
-
* Call a function and record which watched variables it accesess, storing their paths in pathToTemplates.
|
|
2209
|
-
* Used by NodeGroup.applyOneExpr().
|
|
2210
|
-
* TODO: only allow this to be called once per callback.
|
|
2211
|
-
* @param callback {function}
|
|
2212
|
-
* @param ngm {NodeGroupManager}
|
|
2213
|
-
* @returns {Template} */
|
|
2214
|
-
function watchFunction(callback, ngm) {
|
|
2215
|
-
ngm.clearSubscribersIfNeeded();
|
|
2216
|
-
|
|
2217
|
-
logGets = true;
|
|
2218
|
-
|
|
2219
|
-
let transformer = () => new Template(['', ''], [callback()]);
|
|
2220
|
-
let template = transformer();
|
|
2221
|
-
for (let path of gets) {
|
|
2222
|
-
let subscriber = new Subscriber(transformer, template);
|
|
2223
|
-
ngm.subscribers.add(serializePath(path), subscriber);
|
|
2224
|
-
}
|
|
2225
|
-
|
|
2226
|
-
gets = [];
|
|
2227
|
-
logGets = false;
|
|
2228
|
-
return template;
|
|
2229
|
-
}
|
|
2230
|
-
|
|
2231
|
-
/**
|
|
2232
|
-
* Represents a place where nodes will be updated.
|
|
2233
|
-
* TODO: Merge this with Template, or ExprPath? */
|
|
2234
|
-
class Subscriber {
|
|
2235
|
-
|
|
2236
|
-
/** @type {Subscriber} Used only for children of a loop. */
|
|
2237
|
-
parent;
|
|
2238
|
-
|
|
2239
|
-
/** @type {Subscriber[]} TemplateInfo for each child of a loop. */
|
|
2240
|
-
children;
|
|
2241
|
-
|
|
2242
|
-
/**
|
|
2243
|
-
* @param transformer {function} Function that turns the object at the path into a template.
|
|
2244
|
-
* @param template {Template}
|
|
2245
|
-
* @param itemTransformer {function} If a loop, this transforms each item in the loop. */
|
|
2246
|
-
constructor(transformer, template, itemTransformer=null) {
|
|
2247
|
-
this.transformer = transformer;
|
|
2248
|
-
this.template = template;
|
|
2249
|
-
|
|
2250
|
-
// Used only for loops
|
|
2251
|
-
this.itemTransformer = itemTransformer;
|
|
2252
|
-
}
|
|
2253
|
-
}
|
|
1732
|
+
return b;
|
|
1733
|
+
};
|
|
2254
1734
|
|
|
2255
1735
|
/** @typedef {boolean|string|number|function|Object|Array|Date|Node} Expr */
|
|
2256
1736
|
|
|
@@ -2281,10 +1761,10 @@ class NodeGroup {
|
|
|
2281
1761
|
/** @type {ExprPath[]} */
|
|
2282
1762
|
paths = [];
|
|
2283
1763
|
|
|
2284
|
-
/** @type {string} */
|
|
1764
|
+
/** @type {string} Key that matches the template and the expressions. */
|
|
2285
1765
|
exactKey;
|
|
2286
1766
|
|
|
2287
|
-
/** @type {string} */
|
|
1767
|
+
/** @type {string} Key that only matches the template. */
|
|
2288
1768
|
closeKey;
|
|
2289
1769
|
|
|
2290
1770
|
/** @type {boolean} Used by NodeGroupManager. */
|
|
@@ -2317,8 +1797,10 @@ class NodeGroup {
|
|
|
2317
1797
|
* @returns {NodeGroup} */
|
|
2318
1798
|
constructor(template, manager=null) {
|
|
2319
1799
|
|
|
2320
|
-
|
|
1800
|
+
/** @type {Template} */
|
|
2321
1801
|
this.template = template;
|
|
1802
|
+
|
|
1803
|
+
/** @type {NodeGroupManager} */
|
|
2322
1804
|
this.manager = manager;
|
|
2323
1805
|
|
|
2324
1806
|
// new!
|
|
@@ -2334,7 +1816,7 @@ class NodeGroup {
|
|
|
2334
1816
|
let replaceMode = typeof template.replaceMode === 'boolean'
|
|
2335
1817
|
? template.replaceMode
|
|
2336
1818
|
: fragment.children.length===1 &&
|
|
2337
|
-
fragment.firstElementChild?.tagName.replace(/-
|
|
1819
|
+
fragment.firstElementChild?.tagName.replace(/-SOLARITE-PLACEHOLDER$/, '')
|
|
2338
1820
|
=== manager?.rootEl?.tagName;
|
|
2339
1821
|
if (replaceMode) {
|
|
2340
1822
|
this.pseudoRoot = fragment.firstElementChild;
|
|
@@ -2361,7 +1843,7 @@ class NodeGroup {
|
|
|
2361
1843
|
|
|
2362
1844
|
|
|
2363
1845
|
// Update web component placeholders.
|
|
2364
|
-
// Ctrl+F "
|
|
1846
|
+
// Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
|
|
2365
1847
|
// Is this list needed at all?
|
|
2366
1848
|
//for (let component of shell.components)
|
|
2367
1849
|
// this.components.push(resolveNodePath(this.startNode.parentNode, getNodePath(component)))
|
|
@@ -2390,13 +1872,19 @@ class NodeGroup {
|
|
|
2390
1872
|
this.createNewComponent(el);
|
|
2391
1873
|
}
|
|
2392
1874
|
|
|
2393
|
-
if (this.manager
|
|
1875
|
+
if (this.manager?.rootEl) {
|
|
2394
1876
|
|
|
2395
1877
|
// ids
|
|
2396
1878
|
if (this.manager.options.ids !== false)
|
|
2397
1879
|
for (let path of shell.ids) {
|
|
2398
1880
|
let el = resolveNodePath(root, path);
|
|
2399
1881
|
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
1882
|
+
|
|
1883
|
+
// Don't allow overwriting existing class properties if they already have a non-Node value.
|
|
1884
|
+
if (this.manager.rootEl[id] && !(this.manager.rootEl[id] instanceof Node))
|
|
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}">`);
|
|
1887
|
+
|
|
2400
1888
|
this.manager.rootEl[id] = el;
|
|
2401
1889
|
}
|
|
2402
1890
|
|
|
@@ -2421,20 +1909,13 @@ class NodeGroup {
|
|
|
2421
1909
|
}
|
|
2422
1910
|
}
|
|
2423
1911
|
|
|
2424
|
-
updateStyles() {
|
|
2425
|
-
if (this.styles)
|
|
2426
|
-
for (let [style, oldText] of this.styles) {
|
|
2427
|
-
let newText = style.textContent;
|
|
2428
|
-
if (oldText !== newText)
|
|
2429
|
-
Util.bindStyles(style, this.manager.rootEl);
|
|
2430
|
-
}
|
|
2431
|
-
}
|
|
2432
|
-
|
|
2433
1912
|
/**
|
|
2434
1913
|
* Use the paths to insert the given expressions.
|
|
2435
1914
|
* Dispatches expression handling to other functions depending on the path type.
|
|
2436
|
-
* @param exprs {(*|*[]|function|Template)[]}
|
|
2437
|
-
|
|
1915
|
+
* @param exprs {(*|*[]|function|Template)[]}
|
|
1916
|
+
* @param paths {?ExprPath[]} Optional. */
|
|
1917
|
+
applyExprs(exprs, paths=null) {
|
|
1918
|
+
paths = paths || this.paths;
|
|
2438
1919
|
|
|
2439
1920
|
|
|
2440
1921
|
// Update exprs at paths.
|
|
@@ -2442,7 +1923,7 @@ class NodeGroup {
|
|
|
2442
1923
|
|
|
2443
1924
|
// We apply them in reverse order so that a <select> box has its options created from an expression
|
|
2444
1925
|
// before its value attribute is set via an expression.
|
|
2445
|
-
for (let path of
|
|
1926
|
+
for (let path of paths.toReversed()) {
|
|
2446
1927
|
expr = exprs[exprIndex];
|
|
2447
1928
|
|
|
2448
1929
|
// Nodes
|
|
@@ -2453,8 +1934,8 @@ class NodeGroup {
|
|
|
2453
1934
|
|
|
2454
1935
|
// Attributes
|
|
2455
1936
|
else {
|
|
2456
|
-
let node = path.nodeMarker;
|
|
2457
|
-
let
|
|
1937
|
+
let node = path.nodeMarker;
|
|
1938
|
+
let el = (this.manager?.rootEl && node === this.pseudoRoot) ? this.manager.rootEl : node;
|
|
2458
1939
|
|
|
2459
1940
|
|
|
2460
1941
|
// This is necessary both here and below.
|
|
@@ -2464,10 +1945,10 @@ class NodeGroup {
|
|
|
2464
1945
|
}
|
|
2465
1946
|
|
|
2466
1947
|
if (path.type === PathType.Multiple)
|
|
2467
|
-
path.applyMultipleAttribs(
|
|
1948
|
+
path.applyMultipleAttribs(el, expr);
|
|
2468
1949
|
|
|
2469
1950
|
// Capture attribute expressions to later send to the constructor of a web component.
|
|
2470
|
-
// Ctrl+F "
|
|
1951
|
+
// Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
|
|
2471
1952
|
else if (path.nodeMarker !== this.pseudoRoot && path.type === PathType.Component)
|
|
2472
1953
|
this.currentComponentProps[path.attrName] = expr;
|
|
2473
1954
|
|
|
@@ -2476,13 +1957,13 @@ class NodeGroup {
|
|
|
2476
1957
|
|
|
2477
1958
|
// Event attribute value
|
|
2478
1959
|
if (path.attrValue===null && (typeof expr === 'function' || Array.isArray(expr)) && isEvent(path.attrName)) {
|
|
2479
|
-
let root = this.manager
|
|
2480
|
-
path.applyEventAttrib(
|
|
1960
|
+
let root = this.manager?.rootEl || this.startNode.parentNode; // latter is used when constructing a whole element.
|
|
1961
|
+
path.applyEventAttrib(el, expr, root);
|
|
2481
1962
|
}
|
|
2482
1963
|
|
|
2483
1964
|
// Regular attribute value.
|
|
2484
|
-
else
|
|
2485
|
-
exprIndex = path.applyValueAttrib(
|
|
1965
|
+
else // One node value may have multiple expressions. Here we apply them all at once.
|
|
1966
|
+
exprIndex = path.applyValueAttrib(el, exprs, exprIndex);
|
|
2486
1967
|
}
|
|
2487
1968
|
|
|
2488
1969
|
lastNode = path.nodeMarker;
|
|
@@ -2509,6 +1990,10 @@ class NodeGroup {
|
|
|
2509
1990
|
|
|
2510
1991
|
|
|
2511
1992
|
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
applyExpr(path, expr) {
|
|
1996
|
+
// TODO: Use this if I can figure out how to adapt applyValueAttrib() to it.
|
|
2512
1997
|
}
|
|
2513
1998
|
|
|
2514
1999
|
/**
|
|
@@ -2532,7 +2017,7 @@ class NodeGroup {
|
|
|
2532
2017
|
//for (let ng of path.nodeGroups) // TODO: Is this necessary?
|
|
2533
2018
|
// ng.parentPath = null;
|
|
2534
2019
|
path.nodeGroups = [];
|
|
2535
|
-
|
|
2020
|
+
path.apply(expr, newNodes, secondPass);
|
|
2536
2021
|
this.existingTextNodes = null;
|
|
2537
2022
|
|
|
2538
2023
|
// TODO: Create an array of old vs Nodes and NodeGroups together.
|
|
@@ -2622,113 +2107,32 @@ class NodeGroup {
|
|
|
2622
2107
|
}
|
|
2623
2108
|
}
|
|
2624
2109
|
|
|
2625
|
-
// TODO: Move to ExprPath?
|
|
2626
|
-
applyOneExpr(expr, path, newNodes, secondPass) {
|
|
2627
|
-
|
|
2628
|
-
if (expr instanceof Template) {
|
|
2629
|
-
expr.parentPath = path;
|
|
2630
|
-
expr.nodegroup = this;
|
|
2631
|
-
|
|
2632
|
-
//if (window.debug && expr.exprs[0] === 'Banana' && path.nodeGroups.length === 0)
|
|
2633
|
-
//if (window.debug && expr.exprs[0] === 'Banana')
|
|
2634
|
-
// debugger;
|
|
2635
|
-
|
|
2636
|
-
let ng = this.manager.getNodeGroup(expr, true);
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
if (ng) {
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
// TODO: Track ranges of changed nodes and only pass those to udomdiff?
|
|
2644
|
-
// But will that break the swap benchmark?
|
|
2645
|
-
newNodes.push(...ng.getNodes());
|
|
2646
|
-
path.nodeGroups.push(ng);
|
|
2647
|
-
}
|
|
2648
|
-
|
|
2649
|
-
// If expression, evaluate later to find partial match.
|
|
2650
|
-
else {
|
|
2651
|
-
secondPass.push([newNodes.length, path.nodeGroups.length]);
|
|
2652
|
-
newNodes.push(expr);
|
|
2653
|
-
path.nodeGroups.push(null); // placeholder
|
|
2654
|
-
}
|
|
2655
|
-
}
|
|
2656
|
-
|
|
2657
|
-
// Node created by an expression.
|
|
2658
|
-
else if (expr instanceof Node) {
|
|
2659
|
-
|
|
2660
|
-
// DocumentFragment created by an expression.
|
|
2661
|
-
if (expr instanceof DocumentFragment)
|
|
2662
|
-
newNodes.push(...expr.childNodes);
|
|
2663
|
-
else
|
|
2664
|
-
newNodes.push(expr);
|
|
2665
|
-
}
|
|
2666
|
-
|
|
2667
|
-
else if (Array.isArray(expr))
|
|
2668
|
-
for (let subExpr of expr)
|
|
2669
|
-
this.applyOneExpr(subExpr, path, newNodes, secondPass);
|
|
2670
|
-
|
|
2671
|
-
else if (typeof expr === 'function') {
|
|
2672
|
-
expr = watchFunction(expr, this.manager);
|
|
2673
|
-
|
|
2674
|
-
this.applyOneExpr(expr, path, newNodes, secondPass);
|
|
2675
|
-
}
|
|
2676
|
-
|
|
2677
|
-
// Text
|
|
2678
|
-
else {
|
|
2679
|
-
// Convert falsy values (but not 0) to empty string.
|
|
2680
|
-
// Convert numbers to string so they compare the same.
|
|
2681
|
-
let text = (expr === undefined || expr === false || expr === null) ? '' : expr + '';
|
|
2682
|
-
|
|
2683
|
-
// Fast path for updating the text of a single text node.
|
|
2684
|
-
let first = path.nodeBefore.nextSibling;
|
|
2685
|
-
if (first.nodeType === 3 && first.nextSibling === path.nodeMarker && !newNodes.includes(first)) {
|
|
2686
|
-
if (first.textContent !== text)
|
|
2687
|
-
first.textContent = text;
|
|
2688
|
-
|
|
2689
|
-
newNodes.push(first);
|
|
2690
|
-
}
|
|
2691
|
-
|
|
2692
|
-
else {
|
|
2693
|
-
// TODO: Optimize this into a Set or Map or something?
|
|
2694
|
-
if (!this.existingTextNodes)
|
|
2695
|
-
this.existingTextNodes = path.getNodes().filter(n => n.nodeType === 3);
|
|
2696
|
-
|
|
2697
|
-
let idx = this.existingTextNodes.findIndex(n => n.textContent === text);
|
|
2698
|
-
if (idx !== -1)
|
|
2699
|
-
newNodes.push(...this.existingTextNodes.splice(idx, 1));
|
|
2700
|
-
else
|
|
2701
|
-
newNodes.push(path.parentNode.ownerDocument.createTextNode(text));
|
|
2702
|
-
}
|
|
2703
|
-
}
|
|
2704
|
-
}
|
|
2705
|
-
|
|
2706
2110
|
/**
|
|
2707
2111
|
* Create a nested RedComponent or call render with the new props.
|
|
2708
2112
|
* @param el {Solarite:HTMLElement}
|
|
2709
2113
|
* @param props {Object} */
|
|
2710
2114
|
applyComponentExprs(el, props) {
|
|
2711
|
-
|
|
2115
|
+
|
|
2712
2116
|
// TODO: Does a hash of this already exist somewhere?
|
|
2713
2117
|
// Perhaps if Components were treated as child NodeGroups, which would need to be the child of an ExprPath,
|
|
2714
2118
|
// then we could re-use the hash and logic from NodeManager?
|
|
2715
2119
|
let newHash = getObjectHash(props);
|
|
2716
|
-
|
|
2717
|
-
let isPreHtmlElement = el.tagName.endsWith('-
|
|
2120
|
+
|
|
2121
|
+
let isPreHtmlElement = el.tagName.endsWith('-SOLARITE-PLACEHOLDER');
|
|
2718
2122
|
let isPreIsElement = el.hasAttribute('_is');
|
|
2719
|
-
|
|
2720
|
-
|
|
2123
|
+
|
|
2124
|
+
|
|
2721
2125
|
// Instantiate a placeholder.
|
|
2722
2126
|
if (isPreHtmlElement || isPreIsElement)
|
|
2723
2127
|
el = this.createNewComponent(el, isPreHtmlElement, props);
|
|
2724
|
-
|
|
2128
|
+
|
|
2725
2129
|
// Update params of placeholder.
|
|
2726
2130
|
else if (el.render) {
|
|
2727
2131
|
let oldHash = componentHash.get(el);
|
|
2728
2132
|
if (oldHash !== newHash)
|
|
2729
2133
|
el.render(props); // Pass new values of props to render so it can decide how it wants to respond.
|
|
2730
2134
|
}
|
|
2731
|
-
|
|
2135
|
+
|
|
2732
2136
|
componentHash.set(el, newHash);
|
|
2733
2137
|
}
|
|
2734
2138
|
|
|
@@ -2747,8 +2151,8 @@ class NodeGroup {
|
|
|
2747
2151
|
isPreHtmlElement = !el.hasAttribute('_is');
|
|
2748
2152
|
|
|
2749
2153
|
let tagName = (isPreHtmlElement
|
|
2750
|
-
? el.tagName.endsWith('-
|
|
2751
|
-
? el.tagName.slice(0, -
|
|
2154
|
+
? el.tagName.endsWith('-SOLARITE-PLACEHOLDER')
|
|
2155
|
+
? el.tagName.slice(0, -21)
|
|
2752
2156
|
: el.tagName
|
|
2753
2157
|
: el.getAttribute('is')).toLowerCase();
|
|
2754
2158
|
|
|
@@ -2868,18 +2272,38 @@ class NodeGroup {
|
|
|
2868
2272
|
}
|
|
2869
2273
|
|
|
2870
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
|
+
|
|
2871
2286
|
|
|
2872
2287
|
}
|
|
2873
2288
|
|
|
2874
2289
|
|
|
2875
2290
|
let componentHash = new WeakMap();
|
|
2876
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
|
+
|
|
2877
2302
|
/**
|
|
2878
2303
|
* @typedef {Object} RenderOptions
|
|
2879
2304
|
* @property {boolean=} styles - Indicates whether the Courage component is present.
|
|
2880
2305
|
* @property {boolean=} scripts - Indicates whether the Power component is present.
|
|
2881
|
-
* @property {boolean=} ids
|
|
2882
|
-
*
|
|
2306
|
+
* @property {boolean=} ids *
|
|
2883
2307
|
* @property {?boolean} render
|
|
2884
2308
|
* Used only when options are given to a class super constructor inheriting from Solarite.
|
|
2885
2309
|
* True to call render() immediately in super constructor.
|
|
@@ -2928,122 +2352,12 @@ class NodeGroupManager {
|
|
|
2928
2352
|
* @param rootEl {HTMLElement|DocumentFragment} If not specified, the first element of the html will be the rootEl. */
|
|
2929
2353
|
constructor(rootEl=null) {
|
|
2930
2354
|
this.rootEl = rootEl;
|
|
2355
|
+
|
|
2931
2356
|
/*
|
|
2932
2357
|
|
|
2933
2358
|
*/
|
|
2934
2359
|
}
|
|
2935
2360
|
|
|
2936
|
-
/**
|
|
2937
|
-
* Render the main template, which may indirectly call renderTemplate() to create children.
|
|
2938
|
-
* @param template {Template}
|
|
2939
|
-
* @param options {RenderOptions}
|
|
2940
|
-
* @return {?DocumentFragment} */
|
|
2941
|
-
render(template, options={}) {
|
|
2942
|
-
this.mutationWatcherEnabled = false;
|
|
2943
|
-
this.options = options;
|
|
2944
|
-
this.clearSubscribers = false;
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
if (!template && template !== '') {
|
|
2949
|
-
this.rootEl.outerHTML = '';
|
|
2950
|
-
this.mutationWatcherEnabled = true;
|
|
2951
|
-
return null;
|
|
2952
|
-
}
|
|
2953
|
-
|
|
2954
|
-
// Fast path for empty component.
|
|
2955
|
-
if (template.html?.length === 1 && !template.html[0]) {
|
|
2956
|
-
this.rootEl.innerHTML = '';
|
|
2957
|
-
}
|
|
2958
|
-
else {
|
|
2959
|
-
|
|
2960
|
-
// Find or create a NodeGroup for the template.
|
|
2961
|
-
// This updates all nodes from the template.
|
|
2962
|
-
let close;
|
|
2963
|
-
let exact = this.getNodeGroup(template, true);
|
|
2964
|
-
if (!exact) {
|
|
2965
|
-
close = this.getNodeGroup(template, false);
|
|
2966
|
-
}
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
let firstTime = !this.rootNg;
|
|
2970
|
-
this.rootNg = exact || close;
|
|
2971
|
-
|
|
2972
|
-
// Reparent NodeGroup
|
|
2973
|
-
// TODO: Move this to NodeGroup?
|
|
2974
|
-
let parent = this.rootNg.getParentNode();
|
|
2975
|
-
if (!this.rootEl)
|
|
2976
|
-
this.rootEl = parent;
|
|
2977
|
-
|
|
2978
|
-
// If this is the first time rendering this element.
|
|
2979
|
-
else if (firstTime) {
|
|
2980
|
-
|
|
2981
|
-
// Save slot children
|
|
2982
|
-
let fragment;
|
|
2983
|
-
if (this.rootEl.childNodes.length) {
|
|
2984
|
-
fragment = document.createDocumentFragment();
|
|
2985
|
-
fragment.append(...this.rootEl.childNodes);
|
|
2986
|
-
}
|
|
2987
|
-
|
|
2988
|
-
// Add rendered elements.
|
|
2989
|
-
if (parent instanceof DocumentFragment)
|
|
2990
|
-
this.rootEl.append(parent);
|
|
2991
|
-
else if (parent)
|
|
2992
|
-
this.rootEl.append(...parent.childNodes);
|
|
2993
|
-
|
|
2994
|
-
// Apply slot children
|
|
2995
|
-
if (fragment) {
|
|
2996
|
-
for (let slot of this.rootEl.querySelectorAll('slot[name]')) {
|
|
2997
|
-
let name = slot.getAttribute('name');
|
|
2998
|
-
if (name)
|
|
2999
|
-
slot.append(...fragment.querySelectorAll(`[slot='${name}']`));
|
|
3000
|
-
}
|
|
3001
|
-
let unamedSlot = this.rootEl.querySelector('slot:not([name])');
|
|
3002
|
-
if (unamedSlot)
|
|
3003
|
-
unamedSlot.append(fragment);
|
|
3004
|
-
}
|
|
3005
|
-
|
|
3006
|
-
}
|
|
3007
|
-
|
|
3008
|
-
// this.rootNg was rendered as childrenOnly=true
|
|
3009
|
-
// Apply attributes from a root element to the real root element.
|
|
3010
|
-
let ng = this.rootNg;
|
|
3011
|
-
if (ng.pseudoRoot && ng.pseudoRoot !== this.rootEl) {
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
// Remove old attributes
|
|
3015
|
-
// for (let attrib of this.rootEl.attributes)
|
|
3016
|
-
// if (attrib.name !== 'is' && attrib.name !== 'data-style' && !ng.pseudoRoot.hasAttribute(attrib.name))
|
|
3017
|
-
// this.rootEl.removeAttribute(attrib.name)
|
|
3018
|
-
|
|
3019
|
-
// Add/set new attributes
|
|
3020
|
-
if (firstTime)
|
|
3021
|
-
for (let attrib of ng.pseudoRoot.attributes)
|
|
3022
|
-
if (!this.rootEl.hasAttribute(attrib.name))
|
|
3023
|
-
this.rootEl.setAttribute(attrib.name, attrib.value);
|
|
3024
|
-
|
|
3025
|
-
// ng.startNode = ng.endNode = this.rootEl;
|
|
3026
|
-
// ng.nodesCache = [ng.startNode]
|
|
3027
|
-
// for (let path of ng.paths) {
|
|
3028
|
-
// if (path.nodeMarker === ng.rootEl)
|
|
3029
|
-
// path.nodeMarker = this.rootEl;
|
|
3030
|
-
// path.nodesCache = null;
|
|
3031
|
-
//
|
|
3032
|
-
// }
|
|
3033
|
-
//
|
|
3034
|
-
// ng.rootEl = this.rootEl;
|
|
3035
|
-
}
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
this.reset();
|
|
3039
|
-
|
|
3040
|
-
}
|
|
3041
|
-
|
|
3042
|
-
this.mutationWatcherEnabled = true;
|
|
3043
|
-
return this.rootEl;
|
|
3044
|
-
|
|
3045
|
-
}
|
|
3046
|
-
|
|
3047
2361
|
|
|
3048
2362
|
/**
|
|
3049
2363
|
*
|
|
@@ -3142,6 +2456,8 @@ class NodeGroupManager {
|
|
|
3142
2456
|
// But it can still be a close match, so we don't use this code.
|
|
3143
2457
|
success = this.nodeGroupsAvailable.delete(ng2.closeKey, ng2);
|
|
3144
2458
|
|
|
2459
|
+
|
|
2460
|
+
this.nodeGroupsInUse.push(ng2);
|
|
3145
2461
|
}
|
|
3146
2462
|
}
|
|
3147
2463
|
}
|
|
@@ -3172,7 +2488,7 @@ class NodeGroupManager {
|
|
|
3172
2488
|
* but don't reparent it if it's somewhere else.
|
|
3173
2489
|
* @param template {Template}
|
|
3174
2490
|
* @param exact {?boolean}
|
|
3175
|
-
* @param createForWatch
|
|
2491
|
+
* @param createForWatch Deprecated.
|
|
3176
2492
|
* @return {?NodeGroup} */
|
|
3177
2493
|
getNodeGroup(template, exact=null, createForWatch=false) {
|
|
3178
2494
|
|
|
@@ -3224,7 +2540,7 @@ class NodeGroupManager {
|
|
|
3224
2540
|
// Perhaps also result could cache its last exprKey and then we'd use only one map?
|
|
3225
2541
|
ng.exactKey = exactKey;
|
|
3226
2542
|
ng.closeKey = closeKey;
|
|
3227
|
-
if (createForWatch)
|
|
2543
|
+
if (createForWatch)
|
|
3228
2544
|
this.nodeGroupsAvailable.add(ng.exactKey, ng);
|
|
3229
2545
|
else
|
|
3230
2546
|
this.nodeGroupsInUse.push(ng);
|
|
@@ -3267,14 +2583,7 @@ class NodeGroupManager {
|
|
|
3267
2583
|
//pathToLoopInfo = new MultiValueMap(); // uses a Set() for each value.
|
|
3268
2584
|
clearSubscribers = false;
|
|
3269
2585
|
|
|
3270
|
-
|
|
3271
|
-
* One path may be used to loop in more than one place, so we use this to get every anchor from each loop.
|
|
3272
|
-
* @param path {Array}
|
|
3273
|
-
* @return {LoopInfo[]} A function that gets the loop anchor NodeGroup */
|
|
3274
|
-
getLoopInfo(path) {
|
|
3275
|
-
let serializedArrayPath = serializePath(path);
|
|
3276
|
-
return [...this.pathToLoopInfo.getAll(serializedArrayPath)]; // This is set inside forEach()
|
|
3277
|
-
}
|
|
2586
|
+
|
|
3278
2587
|
|
|
3279
2588
|
|
|
3280
2589
|
/**
|
|
@@ -3302,7 +2611,10 @@ class NodeGroupManager {
|
|
|
3302
2611
|
* Get the NodeGroupManager for a Web Component.
|
|
3303
2612
|
* @param rootEl {Solarite|HTMLElement}
|
|
3304
2613
|
* @return {NodeGroupManager} */
|
|
3305
|
-
static get(rootEl) {
|
|
2614
|
+
static get(rootEl=null) {
|
|
2615
|
+
if (!rootEl)
|
|
2616
|
+
return new NodeGroupManager();
|
|
2617
|
+
|
|
3306
2618
|
let ngm = nodeGroupManagers.get(rootEl);
|
|
3307
2619
|
if (!ngm) {
|
|
3308
2620
|
ngm = new NodeGroupManager(rootEl);
|
|
@@ -3321,16 +2633,7 @@ NodeGroupManager.pendingChildren = [];
|
|
|
3321
2633
|
/**
|
|
3322
2634
|
* Each Element that has Expr children has an associated NodeGroupManager here.
|
|
3323
2635
|
* @type {WeakMap<HTMLElement, NodeGroupManager>} */
|
|
3324
|
-
let nodeGroupManagers = new WeakMap();
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
class LoopInfo {
|
|
3329
|
-
constructor(loopTemplate, itemTransformer) {
|
|
3330
|
-
this.template = loopTemplate;
|
|
3331
|
-
this.itemTransformer = itemTransformer;
|
|
3332
|
-
}
|
|
3333
|
-
}
|
|
2636
|
+
let nodeGroupManagers = new WeakMap();
|
|
3334
2637
|
|
|
3335
2638
|
/**
|
|
3336
2639
|
* Convert strings to HTMLNodes.
|
|
@@ -3355,10 +2658,10 @@ class LoopInfo {
|
|
|
3355
2658
|
* 6. r('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
|
|
3356
2659
|
* 7. r()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which includes properly handling nested components and r`` sub-expressions.
|
|
3357
2660
|
* 8. r(template) // Render Template created by #1.
|
|
3358
|
-
* 9. r(() => r`<b>Hello</b
|
|
2661
|
+
* 9. r(() => r`<b>Hello</b>`, {...}); // Create dynamic element that has a render() function.
|
|
3359
2662
|
*
|
|
3360
2663
|
* @param htmlStrings {?HTMLElement|string|string[]|function():Template}
|
|
3361
|
-
* @param exprs {*[]|string|Template}
|
|
2664
|
+
* @param exprs {*[]|string|Template|Object}
|
|
3362
2665
|
* @return {Node|HTMLElement|Template} */
|
|
3363
2666
|
function r(htmlStrings=undefined, ...exprs) {
|
|
3364
2667
|
|
|
@@ -3372,9 +2675,8 @@ function r(htmlStrings=undefined, ...exprs) {
|
|
|
3372
2675
|
|
|
3373
2676
|
// 2. Render template created by #4 to element.
|
|
3374
2677
|
if (exprs[0] instanceof Template) {
|
|
3375
|
-
let ngm = NodeGroupManager.get(parent);
|
|
3376
2678
|
let options = exprs[1];
|
|
3377
|
-
|
|
2679
|
+
template.render(parent, options);
|
|
3378
2680
|
|
|
3379
2681
|
// Append on the first go.
|
|
3380
2682
|
if (!parent.childNodes.length && this) {
|
|
@@ -3393,15 +2695,16 @@ function r(htmlStrings=undefined, ...exprs) {
|
|
|
3393
2695
|
return (htmlStrings, ...exprs) => {
|
|
3394
2696
|
rendered.add(parent);
|
|
3395
2697
|
let template = r(htmlStrings, ...exprs);
|
|
3396
|
-
|
|
3397
|
-
return ngm.render(template, options);
|
|
2698
|
+
return template.render(parent, options);
|
|
3398
2699
|
}
|
|
3399
2700
|
}
|
|
3400
2701
|
|
|
3401
2702
|
// null for expr[0], remove whole element.
|
|
2703
|
+
// This path never happens?
|
|
3402
2704
|
else {
|
|
3403
|
-
|
|
3404
|
-
ngm.
|
|
2705
|
+
throw new Error('unsupported');
|
|
2706
|
+
//let ngm = NodeGroupManager.get(parent);
|
|
2707
|
+
//ngm.render(null, exprs[1])
|
|
3405
2708
|
}
|
|
3406
2709
|
}
|
|
3407
2710
|
|
|
@@ -3429,16 +2732,16 @@ function r(htmlStrings=undefined, ...exprs) {
|
|
|
3429
2732
|
return (htmlStrings, ...exprs) => {
|
|
3430
2733
|
//rendered.add(parent)
|
|
3431
2734
|
let template = r(htmlStrings, ...exprs);
|
|
3432
|
-
return template.
|
|
2735
|
+
return template.render();
|
|
3433
2736
|
}
|
|
3434
2737
|
}
|
|
3435
2738
|
|
|
3436
2739
|
// 8.
|
|
3437
2740
|
else if (htmlStrings instanceof Template) {
|
|
3438
|
-
|
|
3439
|
-
return ngm.render(htmlStrings);
|
|
2741
|
+
return htmlStrings.render();
|
|
3440
2742
|
}
|
|
3441
2743
|
|
|
2744
|
+
|
|
3442
2745
|
// 9. Create dynamic element with render() function.
|
|
3443
2746
|
else if (typeof htmlStrings === 'function') {
|
|
3444
2747
|
let getTemplate = htmlStrings;
|
|
@@ -3447,17 +2750,27 @@ function r(htmlStrings=undefined, ...exprs) {
|
|
|
3447
2750
|
if (typeof template === 'string')
|
|
3448
2751
|
throw new Error(`Please add the "r" prefix before the string "${template}"`)
|
|
3449
2752
|
|
|
3450
|
-
let ngm = new NodeGroupManager();
|
|
3451
2753
|
template.replaceMode = true;
|
|
3452
|
-
let el =
|
|
2754
|
+
let el = template.render();
|
|
3453
2755
|
|
|
2756
|
+
// Create the render() function from the function we were given.
|
|
3454
2757
|
el.render = (function() {
|
|
3455
2758
|
template = getTemplate();
|
|
3456
|
-
|
|
2759
|
+
template.render(el);
|
|
3457
2760
|
}).bind(el);
|
|
3458
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
|
+
|
|
3459
2771
|
return el;
|
|
3460
2772
|
}
|
|
2773
|
+
|
|
3461
2774
|
else
|
|
3462
2775
|
throw new Error('Unsupported arguments.')
|
|
3463
2776
|
}
|
|
@@ -3472,6 +2785,10 @@ function r(htmlStrings=undefined, ...exprs) {
|
|
|
3472
2785
|
* @type {WeakSet<HTMLElement>} */
|
|
3473
2786
|
let rendered = new WeakSet();
|
|
3474
2787
|
|
|
2788
|
+
//import {watchGet, watchSet} from "./watch.js";
|
|
2789
|
+
|
|
2790
|
+
|
|
2791
|
+
|
|
3475
2792
|
function defineClass(Class, tagName, extendsTag) {
|
|
3476
2793
|
if (!customElements.getName(Class)) { // If not previously defined.
|
|
3477
2794
|
tagName = tagName || camelToDashes(Class.name);
|
|
@@ -3502,9 +2819,15 @@ let connected = new WeakSet();
|
|
|
3502
2819
|
* 2. Calls render() when added to the DOM, if it hasn't been called already.
|
|
3503
2820
|
* 3. Child elements are added before constructor is called. But they're also passed to the constructor.
|
|
3504
2821
|
* 4. We can use this.html = r`...` to set html.
|
|
3505
|
-
* 5. We have the onConnect, onFirstConnect, and onDisconnect methods.
|
|
2822
|
+
* 5. We have the onConnect, onFirstConnect, and onDisconnect methods.
|
|
2823
|
+
* Can't figure out how to have these work standalone though, and still be synchronous.
|
|
3506
2824
|
* 6. Can we extend from other element types like TR?
|
|
3507
2825
|
*
|
|
2826
|
+
* Advantages to inheriting from HTMLElement
|
|
2827
|
+
* 1. Minimization won't break when it renames the Class and we call customElements.define() on the wrong name.
|
|
2828
|
+
* 2. We can inherit from things like HTMLTableRowElement directly.
|
|
2829
|
+
* 3. There's less magic, since everyone is familiar with defining custom elements.
|
|
2830
|
+
*
|
|
3508
2831
|
* @param extendsTag {?string}
|
|
3509
2832
|
* @return {Class} */
|
|
3510
2833
|
function createSolarite(extendsTag=null) {
|
|
@@ -3568,7 +2891,7 @@ function createSolarite(extendsTag=null) {
|
|
|
3568
2891
|
if (ch)
|
|
3569
2892
|
(this.querySelector('slot') || this).append(...ch);
|
|
3570
2893
|
|
|
3571
|
-
|
|
2894
|
+
/** @deprecated */
|
|
3572
2895
|
Object.defineProperty(this, 'html', {
|
|
3573
2896
|
set(html) {
|
|
3574
2897
|
rendered.add(this);
|
|
@@ -3618,112 +2941,7 @@ function createSolarite(extendsTag=null) {
|
|
|
3618
2941
|
defineClass(this, tagName, extendsTag);
|
|
3619
2942
|
}
|
|
3620
2943
|
|
|
3621
|
-
|
|
3622
|
-
renderWatched() {
|
|
3623
|
-
let ngm = NodeGroupManager.get(this);
|
|
3624
|
-
|
|
3625
|
-
let nodeGroupUpdates = [];
|
|
3626
|
-
|
|
3627
|
-
for (let change of ngm.changes) {
|
|
3628
|
-
if (change.action === 'set') {
|
|
3629
|
-
for (let transformerInfo of change.transformerInfo) {
|
|
3630
|
-
|
|
3631
|
-
let oldHash = transformerInfo.hash;
|
|
3632
|
-
|
|
3633
|
-
let newObj = delve(watchSet(transformerInfo.path[0]), transformerInfo.path.slice(1));
|
|
3634
|
-
let newTemplate = transformerInfo.transformer(newObj);
|
|
3635
|
-
let newHash = getObjectHash(newTemplate);
|
|
3636
|
-
let ngs = [...ngm.nodeGroupsAvailable.data[oldHash]];
|
|
3637
|
-
for (let ng of ngs) {
|
|
3638
|
-
nodeGroupUpdates.push([ng, oldHash, newHash, newTemplate.exprs, transformerInfo]);
|
|
3639
|
-
}
|
|
3640
|
-
}
|
|
3641
|
-
}
|
|
3642
|
-
|
|
3643
|
-
else if (change.action === 'delete') {
|
|
3644
|
-
for (let hash of change.value) {
|
|
3645
|
-
let ngs = [...ngm.nodeGroupsAvailable.getAll(hash)]; // deletes from nodeGroupsAvailable.
|
|
3646
|
-
|
|
3647
|
-
for (let ng of ngs) {
|
|
3648
|
-
if (ng.parentPath)
|
|
3649
|
-
ng.parentPath.clearNodesCache();
|
|
3650
|
-
|
|
3651
|
-
for (let node of ng.getNodes())
|
|
3652
|
-
node.remove();
|
|
3653
|
-
|
|
3654
|
-
// TODO: Update ancestor NodeGroup exactKeys
|
|
3655
|
-
}
|
|
3656
|
-
}
|
|
3657
|
-
}
|
|
3658
|
-
else if (change.action === 'insert') {
|
|
3659
|
-
|
|
3660
|
-
let beforeNg = change.beforeTemplate ? ngm.getNodeGroup(change.beforeTemplate, true) : null;
|
|
3661
|
-
let arrayPath = [change.root, ...change.path];
|
|
3662
|
-
|
|
3663
|
-
// Get anchor so we can use it to get the parent
|
|
3664
|
-
// TODO: Should this be watchGet(change.root) ?
|
|
3665
|
-
for (let loopInfo of ngm.getLoopInfo([change.root, ...change.path.slice(0, -1)])) {
|
|
3666
|
-
|
|
3667
|
-
// Change.extra is aTemplate telling us where to insert before.
|
|
3668
|
-
let beforeNode = beforeNg?.startNode || loopInfo.template.parentPath.nodeMarker;
|
|
3669
|
-
|
|
3670
|
-
// Loop over every item added to the array.
|
|
3671
|
-
let i = 0; // TODO: How to get real insert index.
|
|
3672
|
-
for (let obj of change.value) {
|
|
3673
|
-
|
|
3674
|
-
// Same logic as forEach() function.
|
|
3675
|
-
|
|
3676
|
-
let callback = loopInfo.itemTransformer;
|
|
3677
|
-
let path = [...arrayPath.slice(0, -1), (arrayPath.at(-1) * 1 + i) + ''];
|
|
3678
|
-
|
|
3679
|
-
// Shortened logic found in watchGet(), but not any faster?
|
|
3680
|
-
// the watchSet() is what makes this slower!
|
|
3681
|
-
// let obj = delve(watchSet(path[0]), path.slice(1));
|
|
3682
|
-
// let template = callback(obj);
|
|
3683
|
-
// let serializedPath = serializePath(path);
|
|
3684
|
-
// pathToTransformer.add(serializedPath, new TransformerInfo(path, callback, template)); // Uses a Set() to ensure no duplicates.
|
|
3685
|
-
|
|
3686
|
-
let template = watchGet(path, callback);
|
|
3687
|
-
i++;
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
//let template = loopInfo.itemTransformer(obj); // What if it takes more than one obj argument?
|
|
3691
|
-
|
|
3692
|
-
// Create new NodeGroup
|
|
3693
|
-
let ng = ngm.getNodeGroup(template, false, true);
|
|
3694
|
-
ng.parentPath = beforeNg?.parentPath || loopInfo.template.parentPath;
|
|
3695
|
-
|
|
3696
|
-
for (let node of ng.getNodes())
|
|
3697
|
-
beforeNode.parentNode.insertBefore(node, beforeNode);
|
|
3698
|
-
|
|
3699
|
-
if (ng.parentPath) // This check is needed for the forEachSpliceInsert test, but why?
|
|
3700
|
-
ng.parentPath.clearNodesCache();
|
|
3701
|
-
}
|
|
3702
|
-
|
|
3703
|
-
// TODO: Update ancestor NodeGroup exactKeys
|
|
3704
|
-
}
|
|
3705
|
-
}
|
|
3706
|
-
}
|
|
3707
|
-
|
|
3708
|
-
// Update them all at once, that way we can reassign the same value twice.
|
|
3709
|
-
for (let [ng, oldHash, newHash, exprs, ti] of nodeGroupUpdates) {
|
|
3710
|
-
ng.applyExprs(exprs);
|
|
3711
|
-
ngm.nodeGroupsAvailable.data[oldHash].delete(ng);
|
|
3712
|
-
ng.exactKey = ti.hash = newHash;
|
|
3713
|
-
ngm.nodeGroupsAvailable.add(ng.exactKey, ng); // Add back to Map with new key.
|
|
3714
|
-
}
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
ngm.changes = [];
|
|
3718
|
-
|
|
3719
|
-
return []; // TODO
|
|
3720
|
-
}
|
|
3721
2944
|
|
|
3722
|
-
/**
|
|
3723
|
-
* @deprecated Use the getArg() function instead. */
|
|
3724
|
-
getArg(name, val=null, type=ArgType.String) {
|
|
3725
|
-
return getArg(this, name, val, type);
|
|
3726
|
-
}
|
|
3727
2945
|
}
|
|
3728
2946
|
}
|
|
3729
2947
|
|
|
@@ -3735,6 +2953,10 @@ let Solarite = new Proxy(createSolarite(), {
|
|
|
3735
2953
|
return createSolarite(...args)
|
|
3736
2954
|
}
|
|
3737
2955
|
});
|
|
3738
|
-
|
|
2956
|
+
|
|
2957
|
+
|
|
2958
|
+
//Experimental:
|
|
2959
|
+
//export {forEach, watchGet, watchSet} from './watch.js' // old, unfinished
|
|
2960
|
+
//export {watch} from './watch2.js'; // unfinished
|
|
3739
2961
|
|
|
3740
|
-
export { ArgType, Solarite,
|
|
2962
|
+
export { ArgType, Solarite, Template, getArg, r };
|