solarite 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/benchmarks/naive/Solarite.min.js +4 -0
- package/benchmarks/naive/index.html +14 -0
- package/benchmarks/naive/main.js +339 -0
- package/benchmarks/naive/package-lock.json +13 -0
- package/benchmarks/naive/package.json +23 -0
- package/benchmarks/readme.md +48 -0
- package/build/build.bat +3 -2
- package/build/build.js +1 -1
- package/dist/Solarite-debug.js +1941 -2791
- package/dist/Solarite.js +1697 -2511
- package/dist/Solarite.min.js +3 -3
- package/dist/udomdiff-license.txt +18 -0
- package/docs/index.md +695 -235
- package/docs/js/Playground.js +1 -1
- package/docs/js/codemirror/codemirror6.js +3683 -3206
- package/docs/js/codemirror/themeSolarIce.js +2 -2
- package/docs/js/documentation.js +2 -2
- package/docs/js/ui/CodeEditor.js +183 -45
- package/docs/js/ui/FlexResizer.js +15 -5
- package/docs/js/util/Errors.js +11 -0
- package/docs/media/documentation.css +6 -3
- package/index.html +462 -26
- package/package.json +1 -1
- package/readme.md +11 -1
- package/src/solarite/ExprPath.js +422 -135
- package/src/solarite/Globals.js +53 -0
- package/src/solarite/NodeGroup.js +300 -388
- package/src/solarite/Shell.js +31 -33
- package/src/solarite/Solarite.js +17 -2
- package/src/solarite/Template.js +75 -25
- package/src/solarite/Util.js +131 -7
- package/src/solarite/createSolarite.js +32 -25
- package/src/solarite/getArg.js +12 -12
- package/src/solarite/hash.js +18 -35
- package/src/solarite/r.js +128 -118
- package/src/solarite/watch3.js +98 -0
- package/src/{solarite → unused}/NodeGroupManager.js +86 -224
- package/src/unused/onConnect.js +79 -0
- package/src/{solarite → unused}/watch.js +2 -2
- package/src/{solarite → unused}/watch2.js +4 -4
- package/src/{solarite → util}/MultiValueMap.js +23 -12
- package/src/util/WeakArray.js +33 -0
- package/tests/Solarite.test.js +1108 -166
- package/tests/Testimony.js +274 -67
- package/tests/index.html +6 -35
- package/tests/run.bat +2 -0
- package/tests/NodeGroup.test.js +0 -115
- package/tests/Shell.test.js +0 -75
|
@@ -2,13 +2,14 @@ import {assert} from "../util/Errors.js";
|
|
|
2
2
|
import ExprPath, {PathType, resolveNodePath} from "./ExprPath.js";
|
|
3
3
|
import {getObjectHash} from "./hash.js";
|
|
4
4
|
import Shell from "./Shell.js";
|
|
5
|
-
import Template from "./Template.js";
|
|
6
5
|
import udomdiff from "./udomdiff.js";
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import
|
|
6
|
+
import Util, {arraySame, flattenAndIndent, isEvent, nodeToArrayTree, setIndent} from "./Util.js";
|
|
7
|
+
//import NodeGroupManager from "./NodeGroupManager.js";
|
|
8
|
+
import delve from "../util/delve.js";
|
|
9
|
+
import Globals from "./Globals.js";
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
/** @typedef {boolean|string|number|function|Object|Array|Date|Node|Template} Expr */
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* A group of Nodes instantiated from a Shell, with Expr's filled in.
|
|
@@ -21,32 +22,29 @@ import NodeGroupManager from "./NodeGroupManager.js";
|
|
|
21
22
|
* */
|
|
22
23
|
export default class NodeGroup {
|
|
23
24
|
|
|
24
|
-
/**
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* @Type {RootNodeGroup} */
|
|
27
|
+
rootNg;
|
|
26
28
|
|
|
27
29
|
/** @type {ExprPath} */
|
|
28
30
|
parentPath;
|
|
29
31
|
|
|
30
|
-
/** @type {Node} First node of NodeGroup. Should never be null. */
|
|
32
|
+
/** @type {Node|HTMLElement} First node of NodeGroup. Should never be null. */
|
|
31
33
|
startNode;
|
|
32
34
|
|
|
33
|
-
/** @type {Node} A node that never changes that this NodeGroup should always insert its nodes before.
|
|
35
|
+
/** @type {Node|HTMLElement} A node that never changes that this NodeGroup should always insert its nodes before.
|
|
34
36
|
* An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.*/
|
|
35
37
|
endNode;
|
|
36
38
|
|
|
37
39
|
/** @type {ExprPath[]} */
|
|
38
40
|
paths = [];
|
|
39
41
|
|
|
40
|
-
/** @type {string} */
|
|
42
|
+
/** @type {string} Key that matches the template and the expressions. */
|
|
41
43
|
exactKey;
|
|
42
44
|
|
|
43
|
-
/** @type {string} */
|
|
45
|
+
/** @type {string} Key that only matches the template. */
|
|
44
46
|
closeKey;
|
|
45
47
|
|
|
46
|
-
/** @type {boolean} Used by NodeGroupManager. */
|
|
47
|
-
inUse;
|
|
48
|
-
|
|
49
|
-
|
|
50
48
|
/**
|
|
51
49
|
* @internal
|
|
52
50
|
* @type {Node[]} Cached result of getNodes() used only for improving performance.*/
|
|
@@ -56,206 +54,97 @@ export default class NodeGroup {
|
|
|
56
54
|
* @type {?Map<HTMLStyleElement, string>} */
|
|
57
55
|
styles;
|
|
58
56
|
|
|
59
|
-
/**
|
|
60
|
-
* If rendering a Template with replaceMode=true, pseudoRoot points to the element where the attributes are rendered.
|
|
61
|
-
* But pseudoRoot is outside of this.getNodes().
|
|
62
|
-
* NodeGroupManager.render() copies the attributes from pseudoRoot to the actual web component root element.
|
|
63
|
-
* @type {?HTMLElement} */
|
|
64
|
-
pseudoRoot;
|
|
65
|
-
|
|
66
57
|
currentComponentProps = {};
|
|
67
58
|
|
|
68
59
|
|
|
69
60
|
/**
|
|
70
61
|
* Create an "instantiated" NodeGroup from a Template and add it to an element.
|
|
71
62
|
* @param template {Template} Create it from the html strings and expressions in this template.
|
|
72
|
-
* @param
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// Used for forEach()
|
|
77
|
-
this.template = template;
|
|
78
|
-
this.manager = manager;
|
|
79
|
-
|
|
80
|
-
// new!
|
|
81
|
-
template.nodeGroup = this;
|
|
82
|
-
|
|
83
|
-
// Get a cached version of the parsed and instantiated html, and ExprPaths.
|
|
84
|
-
let shell = Shell.get(template.html);
|
|
85
|
-
|
|
86
|
-
let fragment = shell.fragment.cloneNode(true);
|
|
63
|
+
* @param parentPath {?ExprPath} */
|
|
64
|
+
constructor(template, parentPath=null) {
|
|
65
|
+
if (!(this instanceof RootNodeGroup)) {
|
|
66
|
+
let [fragment, shell] = this.init(template, parentPath);
|
|
87
67
|
|
|
88
|
-
|
|
89
|
-
// Assume replaceMode if there's only one child element and its tagname matches the root el.
|
|
90
|
-
let replaceMode = typeof template.replaceMode === 'boolean'
|
|
91
|
-
? template.replaceMode
|
|
92
|
-
: fragment.children.length===1 &&
|
|
93
|
-
fragment.firstElementChild?.tagName.replace(/-REDCOMPONENT-PLACEHOLDER$/, '')
|
|
94
|
-
=== manager?.rootEl?.tagName
|
|
95
|
-
if (replaceMode) {
|
|
96
|
-
this.pseudoRoot = fragment.firstElementChild;
|
|
97
|
-
// if (!manager.rootEl)
|
|
98
|
-
// manager.rootEl = this.pseudoRoot;
|
|
99
|
-
/*#IFDEV*/assert(this.pseudoRoot)/*#ENDIF*/
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
let childNodes = replaceMode
|
|
103
|
-
? fragment.firstElementChild.childNodes
|
|
104
|
-
: fragment.childNodes;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
this.startNode = childNodes[0]
|
|
108
|
-
this.endNode = childNodes[childNodes.length - 1]
|
|
68
|
+
this.updatePaths(fragment, shell.paths);
|
|
109
69
|
|
|
70
|
+
this.activateEmbeds(fragment, shell);
|
|
110
71
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
let path = oldPath.clone(fragment)
|
|
114
|
-
path.parentNg = this;
|
|
115
|
-
this.paths.push(path);
|
|
72
|
+
// Apply exprs
|
|
73
|
+
this.applyExprs(template.exprs);
|
|
116
74
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
// Update web component placeholders.
|
|
120
|
-
// Ctrl+F "redcomponent-placeholder" in project to find all code that manages subcomponents.
|
|
121
|
-
// Is this list needed at all?
|
|
122
|
-
//for (let component of shell.components)
|
|
123
|
-
// this.components.push(resolveNodePath(this.startNode.parentNode, getNodePath(component)))
|
|
124
|
-
|
|
125
|
-
/*#IFDEV*/this.verify();/*#ENDIF*/
|
|
126
|
-
|
|
127
|
-
this.activateEmbeds(fragment, shell);
|
|
128
|
-
|
|
129
|
-
/*#IFDEV*/
|
|
130
|
-
assert(this.paths.length <= template.exprs.length);
|
|
131
|
-
if (template.exprs.length)
|
|
132
|
-
assert(this.paths.length);
|
|
133
|
-
/*#ENDIF*/
|
|
134
|
-
|
|
135
|
-
// Apply exprs
|
|
136
|
-
this.applyExprs(template.exprs);
|
|
137
|
-
|
|
138
|
-
/*#IFDEV*/this.verify();/*#ENDIF*/
|
|
139
75
|
}
|
|
140
76
|
|
|
141
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Common init shared by RootNodeGroup and NodeGroup constructors.
|
|
79
|
+
* But in a separate function because they need to do this at a different step.
|
|
80
|
+
* @param template {Template} Create it from the html strings and expressions in this template.
|
|
81
|
+
* @param parentPath {?ExprPath}
|
|
82
|
+
* @param exactKey {?string} Optional, if already calculated.
|
|
83
|
+
* @param closeKey {?string}
|
|
84
|
+
* @returns {[DocumentFragment, Shell]} */
|
|
85
|
+
init(template, parentPath=null, exactKey=null, closeKey=null) {
|
|
86
|
+
this.exactKey = exactKey || template.getExactKey();
|
|
87
|
+
this.closeKey = closeKey || template.getCloseKey();
|
|
142
88
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
for (let path of shell.staticComponents) {
|
|
146
|
-
let el = resolveNodePath(root, path)
|
|
89
|
+
this.parentPath = parentPath;
|
|
90
|
+
this.rootNg = parentPath?.parentNg?.rootNg || this;
|
|
147
91
|
|
|
148
|
-
|
|
149
|
-
if (el.tagName !== this.pseudoRoot?.tagName)
|
|
150
|
-
this.createNewComponent(el)
|
|
151
|
-
}
|
|
92
|
+
/*#IFDEV*/assert(this.rootNg);/*#ENDIF*/
|
|
152
93
|
|
|
153
|
-
|
|
94
|
+
/** @type {Template} */
|
|
95
|
+
this.template = template;
|
|
154
96
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
for (let path of shell.ids) {
|
|
158
|
-
let el = resolveNodePath(root, path);
|
|
159
|
-
let id = el.getAttribute('data-id') || el.getAttribute('id')
|
|
160
|
-
this.manager.rootEl[id] = el;
|
|
161
|
-
}
|
|
97
|
+
// new! Is this needed?
|
|
98
|
+
template.nodeGroup = this;
|
|
162
99
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
this.styles = new Map();
|
|
167
|
-
for (let path of shell.styles) {
|
|
168
|
-
let style = resolveNodePath(root, path);
|
|
169
|
-
Util.bindStyles(style, this.manager.rootEl);
|
|
170
|
-
this.styles.set(style, style.textContent);
|
|
171
|
-
}
|
|
100
|
+
// Get a cached version of the parsed and instantiated html, and ExprPaths.
|
|
101
|
+
let shell = Shell.get(template.html);
|
|
102
|
+
let fragment = shell.fragment.cloneNode(true);
|
|
172
103
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
for (let path of shell.scripts) {
|
|
177
|
-
let script = resolveNodePath(root, path);
|
|
178
|
-
eval(script.textContent)
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
104
|
+
let childNodes = fragment.childNodes;
|
|
105
|
+
this.startNode = childNodes[0];
|
|
106
|
+
this.endNode = childNodes[childNodes.length - 1];
|
|
183
107
|
|
|
184
|
-
|
|
185
|
-
if (this.styles)
|
|
186
|
-
for (let [style, oldText] of this.styles) {
|
|
187
|
-
let newText = style.textContent;
|
|
188
|
-
if (oldText !== newText)
|
|
189
|
-
Util.bindStyles(style, this.manager.rootEl);
|
|
190
|
-
}
|
|
108
|
+
return [fragment, shell];
|
|
191
109
|
}
|
|
192
110
|
|
|
193
111
|
/**
|
|
194
112
|
* Use the paths to insert the given expressions.
|
|
195
113
|
* Dispatches expression handling to other functions depending on the path type.
|
|
196
|
-
* @param exprs {(*|*[]|function|Template)[]}
|
|
197
|
-
|
|
198
|
-
|
|
114
|
+
* @param exprs {(*|*[]|function|Template)[]}
|
|
115
|
+
* @param paths {?ExprPath[]} Optional. */
|
|
116
|
+
applyExprs(exprs, paths=null) {
|
|
117
|
+
paths = paths || this.paths;
|
|
118
|
+
|
|
199
119
|
/*#IFDEV*/this.verify();/*#ENDIF*/
|
|
120
|
+
|
|
200
121
|
// Update exprs at paths.
|
|
201
122
|
let exprIndex = exprs.length-1, expr, lastNode;
|
|
202
123
|
|
|
203
124
|
// We apply them in reverse order so that a <select> box has its options created from an expression
|
|
204
125
|
// before its value attribute is set via an expression.
|
|
205
|
-
for (let path of
|
|
126
|
+
for (let path of paths.toReversed()) {
|
|
206
127
|
expr = exprs[exprIndex];
|
|
207
128
|
|
|
208
129
|
// Nodes
|
|
209
|
-
if (path.type === PathType.Content) {
|
|
210
|
-
this.applyNodeExpr(path, expr);
|
|
211
|
-
/*#IFDEV*/path.verify()/*#ENDIF*/
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Attributes
|
|
215
|
-
else {
|
|
216
|
-
let node = path.nodeMarker; // path.resolve(result);
|
|
217
|
-
let node2 = (this.manager.rootEl && node === this.pseudoRoot) ? this.manager.rootEl : node;
|
|
218
|
-
/*#IFDEV*/assert(node);/*#ENDIF*/
|
|
219
|
-
|
|
220
|
-
// This is necessary both here and below.
|
|
221
|
-
if (lastNode && lastNode !== this.pseudoRoot && lastNode !== node && Object.keys(this.currentComponentProps).length) {
|
|
222
|
-
this.applyComponentExprs(lastNode, this.currentComponentProps);
|
|
223
|
-
this.currentComponentProps = {};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (path.type === PathType.Multiple)
|
|
227
|
-
path.applyMultipleAttribs(node2, expr)
|
|
228
130
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
else if (path.type === PathType.Comment) {
|
|
235
|
-
// Expressions inside Html comments. Deliberately empty because we won't waste time updating them.
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
131
|
+
// This is necessary both here and below.
|
|
132
|
+
if (lastNode && lastNode !== this.rootNg.root && lastNode !== path.nodeMarker && Object.keys(this.currentComponentProps).length) {
|
|
133
|
+
this.applyComponentExprs(lastNode, this.currentComponentProps);
|
|
134
|
+
this.currentComponentProps = {};
|
|
135
|
+
}
|
|
238
136
|
|
|
239
|
-
|
|
240
|
-
if (path.attrValue===null && (typeof expr === 'function' || Array.isArray(expr)) && isEvent(path.attrName)) {
|
|
241
|
-
let root = this.manager.rootEl || this.startNode.parentNode; // latter is used when constructing a whole element.
|
|
242
|
-
path.applyEventAttrib(node2, expr, root);
|
|
243
|
-
}
|
|
137
|
+
exprIndex = path.apply(expr, exprs, exprIndex, this.currentComponentProps);
|
|
244
138
|
|
|
245
|
-
|
|
246
|
-
else
|
|
247
|
-
exprIndex = path.applyValueAttrib(node2, exprs, exprIndex);
|
|
248
|
-
}
|
|
139
|
+
lastNode = path.nodeMarker;
|
|
249
140
|
|
|
250
|
-
lastNode = path.nodeMarker;
|
|
251
|
-
}
|
|
252
141
|
|
|
253
142
|
exprIndex--;
|
|
254
143
|
} // end for(path of this.paths)
|
|
255
144
|
|
|
256
145
|
|
|
257
146
|
// Check again after we iterate through all paths to apply to a component.
|
|
258
|
-
if (lastNode && lastNode !== this.
|
|
147
|
+
if (lastNode && lastNode !== this.rootNg.root && Object.keys(this.currentComponentProps).length) {
|
|
259
148
|
this.applyComponentExprs(lastNode, this.currentComponentProps);
|
|
260
149
|
this.currentComponentProps = {};
|
|
261
150
|
}
|
|
@@ -274,228 +163,32 @@ export default class NodeGroup {
|
|
|
274
163
|
}
|
|
275
164
|
|
|
276
165
|
/**
|
|
277
|
-
*
|
|
278
|
-
* Called by applyExprs()
|
|
279
|
-
* This function is recursive, as the functions it calls also call it.
|
|
280
|
-
* TODO: Move this to ExprPath?
|
|
281
|
-
* @param path {ExprPath}
|
|
282
|
-
* @param expr {Expr}
|
|
283
|
-
* @return {Node[]} New Nodes created. */
|
|
284
|
-
applyNodeExpr(path, expr) {
|
|
285
|
-
/*#IFDEV*/path.verify();/*#ENDIF*/
|
|
286
|
-
|
|
287
|
-
/** @type {(Node|NodeGroup|Expr)[]} */
|
|
288
|
-
let newNodes = [];
|
|
289
|
-
let oldNodeGroups = path.nodeGroups;
|
|
290
|
-
/*#IFDEV*/assert(!oldNodeGroups.includes(null))/*#ENDIF*/
|
|
291
|
-
let secondPass = []; // indices
|
|
292
|
-
|
|
293
|
-
// First Pass
|
|
294
|
-
//for (let ng of path.nodeGroups) // TODO: Is this necessary?
|
|
295
|
-
// ng.parentPath = null;
|
|
296
|
-
path.nodeGroups = [];
|
|
297
|
-
this.applyOneExpr(expr, path, newNodes, secondPass);
|
|
298
|
-
this.existingTextNodes = null;
|
|
299
|
-
|
|
300
|
-
// TODO: Create an array of old vs Nodes and NodeGroups together.
|
|
301
|
-
// If they're all the same, skip the next steps.
|
|
302
|
-
// Or calculate it in the loop above as we go? Have a path.lastNodeGroups property?
|
|
303
|
-
|
|
304
|
-
// Second pass to find close-match NodeGroups.
|
|
305
|
-
let flatten = false;
|
|
306
|
-
if (secondPass.length) {
|
|
307
|
-
for (let [nodesIndex, ngIndex] of secondPass) {
|
|
308
|
-
let ng = this.manager.getNodeGroup(newNodes[nodesIndex], false);
|
|
309
|
-
|
|
310
|
-
ng.parentPath = path;
|
|
311
|
-
let ngNodes = ng.getNodes();
|
|
312
|
-
|
|
313
|
-
/*#IFDEV*/assert(!(newNodes[nodesIndex] instanceof NodeGroup))/*#ENDIF*/
|
|
314
|
-
|
|
315
|
-
if (ngNodes.length === 1)
|
|
316
|
-
newNodes[nodesIndex] = ngNodes[0];
|
|
317
|
-
|
|
318
|
-
else {
|
|
319
|
-
newNodes[nodesIndex] = ngNodes;
|
|
320
|
-
flatten = true;
|
|
321
|
-
}
|
|
322
|
-
path.nodeGroups[ngIndex] = ng;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (flatten)
|
|
326
|
-
newNodes = newNodes.flat(); // TODO: Only if second pass happens?
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/*#IFDEV*/assert(!path.nodeGroups.includes(null))/*#ENDIF*/
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
let oldNodes = path.getNodes();
|
|
334
|
-
path.nodesCache = newNodes; // Replaces value set by path.getNodes()
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
// This pre-check makes it a few percent faster?
|
|
338
|
-
let diff = findArrayDiff(oldNodes, newNodes);
|
|
339
|
-
if (diff !== false) {
|
|
340
|
-
|
|
341
|
-
if (this.parentPath)
|
|
342
|
-
this.parentPath.clearNodesCache();
|
|
343
|
-
|
|
344
|
-
// Fast clear method
|
|
345
|
-
let isNowEmpty = oldNodes.length && !newNodes.length;
|
|
346
|
-
if (!isNowEmpty || !path.fastClear(oldNodes, newNodes))
|
|
347
|
-
|
|
348
|
-
// Rearrange nodes.
|
|
349
|
-
udomdiff(path.parentNode, oldNodes, newNodes, path.nodeMarker)
|
|
350
|
-
|
|
351
|
-
this.saveOrphans(oldNodeGroups, oldNodes);
|
|
352
|
-
}
|
|
353
|
-
/*#IFDEV*/path.verify();/*#ENDIF*/
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Find NodeGroups that had their nodes removed and add those nodes to a Fragment so
|
|
358
|
-
* they're not lost forever and the NodeGroup's internal structure is still consistent.
|
|
359
|
-
* Called from NodeGroup.applyNodeExpr().
|
|
360
|
-
* @param oldNodeGroups {NodeGroup[]}
|
|
361
|
-
* @param oldNodes {Node[]} */
|
|
362
|
-
saveOrphans(oldNodeGroups, oldNodes) {
|
|
363
|
-
let oldNgMap = new Map();
|
|
364
|
-
for (let ng of oldNodeGroups) {
|
|
365
|
-
oldNgMap.set(ng.startNode, ng)
|
|
366
|
-
|
|
367
|
-
// TODO: Is this necessary?
|
|
368
|
-
// if (ng.parentPath)
|
|
369
|
-
// ng.parentPath.clearNodesCache();
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
for (let i=0, node; node = oldNodes[i]; i++) {
|
|
373
|
-
let ng;
|
|
374
|
-
if (!node.parentNode && (ng = oldNgMap.get(node))) {
|
|
375
|
-
let fragment = document.createDocumentFragment();
|
|
376
|
-
let endNode = ng.endNode;
|
|
377
|
-
while (node !== endNode) {
|
|
378
|
-
fragment.append(node)
|
|
379
|
-
i++;
|
|
380
|
-
node = oldNodes[i];
|
|
381
|
-
}
|
|
382
|
-
fragment.append(endNode);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// TODO: Move to ExprPath?
|
|
388
|
-
applyOneExpr(expr, path, newNodes, secondPass) {
|
|
389
|
-
|
|
390
|
-
if (expr instanceof Template) {
|
|
391
|
-
expr.parentPath = path;
|
|
392
|
-
expr.nodegroup = this;
|
|
393
|
-
|
|
394
|
-
//if (window.debug && expr.exprs[0] === 'Banana' && path.nodeGroups.length === 0)
|
|
395
|
-
//if (window.debug && expr.exprs[0] === 'Banana')
|
|
396
|
-
// debugger;
|
|
397
|
-
|
|
398
|
-
let ng = this.manager.getNodeGroup(expr, true);
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
if (ng) {
|
|
402
|
-
//#IFDEV
|
|
403
|
-
// Make sure the nodeCache of the ExprPath we took it from is sitll valid.
|
|
404
|
-
if (ng.parentPath)
|
|
405
|
-
ng.parentPath.verify();
|
|
406
|
-
//#ENDIF
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
// TODO: Track ranges of changed nodes and only pass those to udomdiff?
|
|
410
|
-
// But will that break the swap benchmark?
|
|
411
|
-
newNodes.push(...ng.getNodes());
|
|
412
|
-
path.nodeGroups.push(ng);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// If expression, evaluate later to find partial match.
|
|
416
|
-
else {
|
|
417
|
-
secondPass.push([newNodes.length, path.nodeGroups.length])
|
|
418
|
-
newNodes.push(expr)
|
|
419
|
-
path.nodeGroups.push(null); // placeholder
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// Node created by an expression.
|
|
424
|
-
else if (expr instanceof Node) {
|
|
425
|
-
|
|
426
|
-
// DocumentFragment created by an expression.
|
|
427
|
-
if (expr instanceof DocumentFragment)
|
|
428
|
-
newNodes.push(...expr.childNodes);
|
|
429
|
-
else
|
|
430
|
-
newNodes.push(expr);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
else if (Array.isArray(expr))
|
|
434
|
-
for (let subExpr of expr)
|
|
435
|
-
this.applyOneExpr(subExpr, path, newNodes, secondPass)
|
|
436
|
-
|
|
437
|
-
else if (typeof expr === 'function') {
|
|
438
|
-
expr = watchFunction(expr, this.manager)
|
|
439
|
-
|
|
440
|
-
this.applyOneExpr(expr, path, newNodes, secondPass)
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Text
|
|
444
|
-
else {
|
|
445
|
-
// Convert falsy values (but not 0) to empty string.
|
|
446
|
-
// Convert numbers to string so they compare the same.
|
|
447
|
-
let text = (expr === undefined || expr === false || expr === null) ? '' : expr + '';
|
|
448
|
-
|
|
449
|
-
// Fast path for updating the text of a single text node.
|
|
450
|
-
let first = path.nodeBefore.nextSibling;
|
|
451
|
-
if (first.nodeType === 3 && first.nextSibling === path.nodeMarker && !newNodes.includes(first)) {
|
|
452
|
-
if (first.textContent !== text)
|
|
453
|
-
first.textContent = text;
|
|
454
|
-
|
|
455
|
-
newNodes.push(first);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
else {
|
|
459
|
-
// TODO: Optimize this into a Set or Map or something?
|
|
460
|
-
if (!this.existingTextNodes)
|
|
461
|
-
this.existingTextNodes = path.getNodes().filter(n => n.nodeType === 3);
|
|
462
|
-
|
|
463
|
-
let idx = this.existingTextNodes.findIndex(n => n.textContent === text);
|
|
464
|
-
if (idx !== -1)
|
|
465
|
-
newNodes.push(...this.existingTextNodes.splice(idx, 1))
|
|
466
|
-
else
|
|
467
|
-
newNodes.push(path.parentNode.ownerDocument.createTextNode(text));
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
/**
|
|
473
|
-
* Create a nested RedComponent or call render with the new props.
|
|
166
|
+
* Create a nested Component or call render with the new props.
|
|
474
167
|
* @param el {Solarite:HTMLElement}
|
|
475
168
|
* @param props {Object} */
|
|
476
169
|
applyComponentExprs(el, props) {
|
|
477
|
-
|
|
170
|
+
|
|
478
171
|
// TODO: Does a hash of this already exist somewhere?
|
|
479
172
|
// Perhaps if Components were treated as child NodeGroups, which would need to be the child of an ExprPath,
|
|
480
173
|
// then we could re-use the hash and logic from NodeManager?
|
|
481
174
|
let newHash = getObjectHash(props);
|
|
482
|
-
|
|
483
|
-
let isPreHtmlElement = el.tagName.endsWith('-
|
|
175
|
+
|
|
176
|
+
let isPreHtmlElement = el.tagName.endsWith('-SOLARITE-PLACEHOLDER');
|
|
484
177
|
let isPreIsElement = el.hasAttribute('_is')
|
|
485
|
-
|
|
486
|
-
|
|
178
|
+
|
|
179
|
+
|
|
487
180
|
// Instantiate a placeholder.
|
|
488
181
|
if (isPreHtmlElement || isPreIsElement)
|
|
489
182
|
el = this.createNewComponent(el, isPreHtmlElement, props);
|
|
490
|
-
|
|
491
|
-
//
|
|
183
|
+
|
|
184
|
+
// Call render() with the same params that would've been passed to the constructor.
|
|
492
185
|
else if (el.render) {
|
|
493
|
-
let oldHash = componentHash.get(el);
|
|
186
|
+
let oldHash = Globals.componentHash.get(el);
|
|
494
187
|
if (oldHash !== newHash)
|
|
495
188
|
el.render(props); // Pass new values of props to render so it can decide how it wants to respond.
|
|
496
189
|
}
|
|
497
|
-
|
|
498
|
-
componentHash.set(el, newHash);
|
|
190
|
+
|
|
191
|
+
Globals.componentHash.set(el, newHash);
|
|
499
192
|
}
|
|
500
193
|
|
|
501
194
|
/**
|
|
@@ -513,8 +206,8 @@ export default class NodeGroup {
|
|
|
513
206
|
isPreHtmlElement = !el.hasAttribute('_is');
|
|
514
207
|
|
|
515
208
|
let tagName = (isPreHtmlElement
|
|
516
|
-
? el.tagName.endsWith('-
|
|
517
|
-
? el.tagName.slice(0, -
|
|
209
|
+
? el.tagName.endsWith('-SOLARITE-PLACEHOLDER')
|
|
210
|
+
? el.tagName.slice(0, -21)
|
|
518
211
|
: el.tagName
|
|
519
212
|
: el.getAttribute('is')).toLowerCase();
|
|
520
213
|
|
|
@@ -538,33 +231,43 @@ export default class NodeGroup {
|
|
|
538
231
|
// We pass the childNodes to the constructor so it can know about them,
|
|
539
232
|
// instead of only afterward when they're appended to the slot below.
|
|
540
233
|
// This is useful for a custom selectbox, for example.
|
|
541
|
-
//
|
|
234
|
+
// Globals.pendingChildren stores the childen so the super construtor call to Solarite's constructor
|
|
542
235
|
// can add them as children before the rest of the constructor code executes.
|
|
543
236
|
let ch = [... el.childNodes];
|
|
544
|
-
|
|
237
|
+
Globals.pendingChildren.push(ch); // pop() is called in Solarite constructor.
|
|
545
238
|
let newEl = new Constructor(props, ch);
|
|
546
239
|
|
|
547
240
|
if (!isPreHtmlElement)
|
|
548
241
|
newEl.setAttribute('is', el.getAttribute('is').toLowerCase())
|
|
549
242
|
el.replaceWith(newEl);
|
|
550
|
-
|
|
243
|
+
|
|
551
244
|
// Set children / slot children
|
|
552
245
|
// TODO: Match named slots.
|
|
553
246
|
// TODO: This only appends to slot if render() is called in the constructor.
|
|
554
247
|
//let slot = newEl.querySelector('slot') || newEl;
|
|
555
248
|
//slot.append(...el.childNodes);
|
|
556
|
-
|
|
249
|
+
|
|
557
250
|
// Copy over event attributes.
|
|
558
251
|
for (let propName in props) {
|
|
559
252
|
let val = props[propName];
|
|
560
253
|
if (propName.startsWith('on') && typeof val === 'function')
|
|
561
254
|
newEl.addEventListener(propName.slice(2), e => val(e, newEl));
|
|
255
|
+
|
|
256
|
+
// Bind array based event attributes on value.
|
|
257
|
+
// This same logic is in ExprPath.applyValueAttrib() for non-components.
|
|
258
|
+
if ((propName === 'value' || propName === 'data-value') && Util.isPath(val)) {
|
|
259
|
+
let [obj, path] = [val[0], val.slice(1)];
|
|
260
|
+
newEl.value = delve(obj, path);
|
|
261
|
+
newEl.addEventListener('input', e => {
|
|
262
|
+
delve(obj, path, Util.getInputValue(newEl));
|
|
263
|
+
}, true); // We use capture so we update the values before other events added by the user.
|
|
264
|
+
}
|
|
562
265
|
}
|
|
563
266
|
|
|
564
267
|
// If an id pointed at the placeholder, update it to point to the new element.
|
|
565
268
|
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
566
269
|
if (id)
|
|
567
|
-
this.
|
|
270
|
+
delve(this.getRootNode(), id.split(/\./g), newEl);
|
|
568
271
|
|
|
569
272
|
|
|
570
273
|
// Update paths to use replaced element.
|
|
@@ -633,6 +336,38 @@ export default class NodeGroup {
|
|
|
633
336
|
return this.startNode?.parentNode
|
|
634
337
|
}
|
|
635
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Get the root element of the NodeGroup's RootNodeGroup.
|
|
341
|
+
* @returns {HTMLElement|DocumentFragment} */
|
|
342
|
+
getRootNode() {
|
|
343
|
+
return this.rootNg.root;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* @returns {RootNodeGroup} */
|
|
348
|
+
getRootNodeGroup() {
|
|
349
|
+
return this.rootNg;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
updatePaths(fragment, paths, offset) {
|
|
354
|
+
// Update paths to point to the fragment.
|
|
355
|
+
this.paths.length = paths.length;
|
|
356
|
+
for (let i=0; i<paths.length; i++) {
|
|
357
|
+
let path = paths[i].clone(fragment, offset)
|
|
358
|
+
path.parentNg = this;
|
|
359
|
+
this.paths[i] = path;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
updateStyles() {
|
|
364
|
+
if (this.styles)
|
|
365
|
+
for (let [style, oldText] of this.styles) {
|
|
366
|
+
let newText = style.textContent;
|
|
367
|
+
if (oldText !== newText)
|
|
368
|
+
Util.bindStyles(style, this.getRootNodeGroup().root);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
636
371
|
|
|
637
372
|
//#IFDEV
|
|
638
373
|
/**
|
|
@@ -652,7 +387,7 @@ export default class NodeGroup {
|
|
|
652
387
|
|
|
653
388
|
let path = this.paths.find(path=>path.type === PathType.Content && path.getNodes().includes(nextNode));
|
|
654
389
|
if (path)
|
|
655
|
-
return [`Path
|
|
390
|
+
return [`Path.nodes:`]
|
|
656
391
|
|
|
657
392
|
return [];
|
|
658
393
|
})
|
|
@@ -700,7 +435,184 @@ export default class NodeGroup {
|
|
|
700
435
|
return true;
|
|
701
436
|
}
|
|
702
437
|
//#ENDIF
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* @param root {HTMLElement}
|
|
442
|
+
* @param shell {Shell}
|
|
443
|
+
* @param pathOffset {int} */
|
|
444
|
+
activateEmbeds(root, shell, pathOffset=0) {
|
|
445
|
+
|
|
446
|
+
// static components. These are WebComponents not created by an expression.
|
|
447
|
+
// Must happen before ids.
|
|
448
|
+
for (let path of shell.staticComponents) {
|
|
449
|
+
if (pathOffset)
|
|
450
|
+
path = path.slice(0, -pathOffset);
|
|
451
|
+
let el = resolveNodePath(root, path);
|
|
452
|
+
|
|
453
|
+
// Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
|
|
454
|
+
if (root !== el/* && !isReplaceEl(root, el)*/) // TODO: is isReplaceEl necessary?
|
|
455
|
+
this.createNewComponent(el)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
let rootEl = this.rootNg.root;
|
|
459
|
+
if (rootEl) {
|
|
460
|
+
|
|
461
|
+
// ids
|
|
462
|
+
if (this.options?.ids !== false)
|
|
463
|
+
for (let path of shell.ids) {
|
|
464
|
+
if (pathOffset)
|
|
465
|
+
path = path.slice(0, -pathOffset);
|
|
466
|
+
let el = resolveNodePath(root, path);
|
|
467
|
+
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
468
|
+
if (id) { // If something hasn't removed the id.
|
|
469
|
+
|
|
470
|
+
// Don't allow overwriting existing class properties if they already have a non-Node value.
|
|
471
|
+
if (rootEl[id] && !(rootEl[id] instanceof Node))
|
|
472
|
+
throw new Error(`${rootEl.constructor.name}.${id} already has a value. ` +
|
|
473
|
+
`Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
|
|
474
|
+
|
|
475
|
+
delve(rootEl, id.split(/\./g), el);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// styles
|
|
480
|
+
if (this.options?.styles !== false) {
|
|
481
|
+
if (shell.styles.length)
|
|
482
|
+
this.styles = new Map();
|
|
483
|
+
for (let path of shell.styles) {
|
|
484
|
+
if (pathOffset)
|
|
485
|
+
path = path.slice(0, -pathOffset);
|
|
486
|
+
let style = resolveNodePath(root, path);
|
|
487
|
+
Util.bindStyles(style, rootEl);
|
|
488
|
+
this.styles.set(style, style.textContent);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
}
|
|
492
|
+
// scripts
|
|
493
|
+
if (this.options?.scripts !== false) {
|
|
494
|
+
for (let path of shell.scripts) {
|
|
495
|
+
if (pathOffset)
|
|
496
|
+
path = path.slice(0, -pathOffset);
|
|
497
|
+
let script = resolveNodePath(root, path);
|
|
498
|
+
eval(script.textContent)
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
703
503
|
}
|
|
704
504
|
|
|
705
505
|
|
|
706
|
-
|
|
506
|
+
export class RootNodeGroup extends NodeGroup {
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Root node at the top of the hierarchy.
|
|
510
|
+
* @type {HTMLElement} */
|
|
511
|
+
root;
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
*
|
|
515
|
+
* @param template
|
|
516
|
+
* @param el
|
|
517
|
+
* @param options {?object}
|
|
518
|
+
*/
|
|
519
|
+
constructor(template, el, options) {
|
|
520
|
+
super(template);
|
|
521
|
+
|
|
522
|
+
this.options = options;
|
|
523
|
+
|
|
524
|
+
this.rootNg = this;
|
|
525
|
+
let [fragment, shell] = this.init(template);
|
|
526
|
+
|
|
527
|
+
// If adding NodeGroup to an element.
|
|
528
|
+
let offset = 0;
|
|
529
|
+
let root = fragment; // TODO: Rename so it's not confused with this.root.
|
|
530
|
+
if (el) {
|
|
531
|
+
|
|
532
|
+
// Save slot children
|
|
533
|
+
let slotFragment;
|
|
534
|
+
if (el.childNodes.length) {
|
|
535
|
+
slotFragment = document.createDocumentFragment();
|
|
536
|
+
slotFragment.append(...el.childNodes);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
this.root = el;
|
|
540
|
+
|
|
541
|
+
// If el should replace the root node of the fragment.
|
|
542
|
+
if (isReplaceEl(fragment, el)) {
|
|
543
|
+
el.append(...fragment.children[0].childNodes);
|
|
544
|
+
|
|
545
|
+
// Copy attributes
|
|
546
|
+
for (let attrib of fragment.children[0].attributes)
|
|
547
|
+
if (!el.hasAttribute(attrib.name))
|
|
548
|
+
el.setAttribute(attrib.name, attrib.value);
|
|
549
|
+
|
|
550
|
+
// Go one level deeper into all of shell's paths.
|
|
551
|
+
offset = 1;
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
|
|
555
|
+
if (!isEmpty)
|
|
556
|
+
el.append(...fragment.childNodes);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Setup slots
|
|
560
|
+
if (slotFragment) {
|
|
561
|
+
for (let slot of el.querySelectorAll('slot[name]')) {
|
|
562
|
+
let name = slot.getAttribute('name')
|
|
563
|
+
if (name) {
|
|
564
|
+
let slotChildren = slotFragment.querySelectorAll(`[slot='${name}']`);
|
|
565
|
+
slot.append(...slotChildren);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
let unamedSlot = el.querySelector('slot:not([name])')
|
|
569
|
+
if (unamedSlot)
|
|
570
|
+
unamedSlot.append(slotFragment);
|
|
571
|
+
else
|
|
572
|
+
el.append(slotFragment);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
root = el;
|
|
576
|
+
this.startNode = el;
|
|
577
|
+
this.endNode = el;
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
let singleEl = getSingleEl(fragment);
|
|
581
|
+
this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
|
|
582
|
+
if (singleEl) {
|
|
583
|
+
root = singleEl;
|
|
584
|
+
offset = 1;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
this.updatePaths(root, shell.paths, offset);
|
|
589
|
+
|
|
590
|
+
this.activateEmbeds(root, shell, offset);
|
|
591
|
+
|
|
592
|
+
// Apply exprs
|
|
593
|
+
this.applyExprs(template.exprs);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function getSingleEl(fragment) {
|
|
598
|
+
let nonempty = [];
|
|
599
|
+
for (let n of fragment.childNodes) {
|
|
600
|
+
if (n.nodeType === 1 || n.nodeType === 3 && n.textContent.trim().length) {
|
|
601
|
+
if (nonempty.length)
|
|
602
|
+
return null;
|
|
603
|
+
nonempty.push(n);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return nonempty[0];
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Does the fragment have one child that's an element matching the tagname of el?
|
|
611
|
+
* @param fragment {DocumentFragment}
|
|
612
|
+
* @param el {HTMLElement}
|
|
613
|
+
* @returns {boolean} */
|
|
614
|
+
function isReplaceEl(fragment, el) {
|
|
615
|
+
return el.tagName.includes('-')
|
|
616
|
+
&& fragment.children.length===1
|
|
617
|
+
&& fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') === el.tagName;
|
|
618
|
+
}
|