solarite 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/benchmarks/naive/Solarite.min.js +4 -0
- package/benchmarks/naive/index.html +14 -0
- package/benchmarks/naive/main.js +339 -0
- package/benchmarks/naive/package-lock.json +13 -0
- package/benchmarks/naive/package.json +23 -0
- package/benchmarks/readme.md +48 -0
- package/build/build.bat +2 -2
- package/build/build.js +1 -1
- package/dist/Solarite-debug.js +1344 -1532
- package/dist/Solarite.js +1298 -1334
- package/dist/Solarite.min.js +3 -3
- package/dist/udomdiff-license.txt +18 -0
- package/docs/index.md +440 -130
- package/docs/js/Playground.js +1 -1
- package/docs/js/codemirror/codemirror6.js +3683 -3206
- package/docs/js/codemirror/themeSolarIce.js +1 -1
- package/docs/js/documentation.js +1 -1
- package/docs/js/ui/CodeEditor.js +183 -45
- package/docs/js/ui/FlexResizer.js +14 -3
- package/docs/js/util/Errors.js +11 -0
- package/docs/media/documentation.css +6 -3
- package/index.html +449 -30
- package/package.json +1 -1
- package/readme.md +1 -1
- package/src/solarite/ExprPath.js +349 -151
- package/src/solarite/Globals.js +43 -1
- package/src/solarite/NodeGroup.js +275 -293
- package/src/solarite/Shell.js +26 -28
- package/src/solarite/Solarite.js +12 -0
- package/src/solarite/Template.js +55 -128
- package/src/solarite/Util.js +131 -7
- package/src/solarite/createSolarite.js +14 -19
- package/src/solarite/getArg.js +1 -1
- package/src/solarite/hash.js +18 -35
- package/src/solarite/r.js +127 -127
- package/src/solarite/watch3.js +18 -11
- package/src/{solarite → unused}/NodeGroupManager.js +81 -109
- package/src/{solarite → unused}/watch.js +1 -1
- package/src/{solarite → unused}/watch2.js +1 -2
- package/src/{solarite → util}/MultiValueMap.js +18 -11
- package/src/util/WeakArray.js +33 -0
- package/tests/Solarite.test.js +862 -167
- package/tests/index.html +4 -2
- package/tests/run.bat +1 -1
- package/deno.lock +0 -176
|
@@ -3,11 +3,13 @@ import ExprPath, {PathType, resolveNodePath} from "./ExprPath.js";
|
|
|
3
3
|
import {getObjectHash} from "./hash.js";
|
|
4
4
|
import Shell from "./Shell.js";
|
|
5
5
|
import udomdiff from "./udomdiff.js";
|
|
6
|
-
import Util, {
|
|
7
|
-
import NodeGroupManager from "./NodeGroupManager.js";
|
|
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";
|
|
8
10
|
|
|
9
11
|
|
|
10
|
-
/** @typedef {boolean|string|number|function|Object|Array|Date|Node} Expr */
|
|
12
|
+
/** @typedef {boolean|string|number|function|Object|Array|Date|Node|Template} Expr */
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* A group of Nodes instantiated from a Shell, with Expr's filled in.
|
|
@@ -20,16 +22,17 @@ import NodeGroupManager from "./NodeGroupManager.js";
|
|
|
20
22
|
* */
|
|
21
23
|
export default class NodeGroup {
|
|
22
24
|
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
+
/**
|
|
26
|
+
* @Type {RootNodeGroup} */
|
|
27
|
+
rootNg;
|
|
25
28
|
|
|
26
29
|
/** @type {ExprPath} */
|
|
27
30
|
parentPath;
|
|
28
31
|
|
|
29
|
-
/** @type {Node} First node of NodeGroup. Should never be null. */
|
|
32
|
+
/** @type {Node|HTMLElement} First node of NodeGroup. Should never be null. */
|
|
30
33
|
startNode;
|
|
31
34
|
|
|
32
|
-
/** @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.
|
|
33
36
|
* An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.*/
|
|
34
37
|
endNode;
|
|
35
38
|
|
|
@@ -39,13 +42,9 @@ export default class NodeGroup {
|
|
|
39
42
|
/** @type {string} Key that matches the template and the expressions. */
|
|
40
43
|
exactKey;
|
|
41
44
|
|
|
42
|
-
/** @type {string} Key that only matches the template.
|
|
45
|
+
/** @type {string} Key that only matches the template. */
|
|
43
46
|
closeKey;
|
|
44
47
|
|
|
45
|
-
/** @type {boolean} Used by NodeGroupManager. */
|
|
46
|
-
inUse;
|
|
47
|
-
|
|
48
|
-
|
|
49
48
|
/**
|
|
50
49
|
* @internal
|
|
51
50
|
* @type {Node[]} Cached result of getNodes() used only for improving performance.*/
|
|
@@ -55,137 +54,58 @@ export default class NodeGroup {
|
|
|
55
54
|
* @type {?Map<HTMLStyleElement, string>} */
|
|
56
55
|
styles;
|
|
57
56
|
|
|
58
|
-
/**
|
|
59
|
-
* If rendering a Template with replaceMode=true, pseudoRoot points to the element where the attributes are rendered.
|
|
60
|
-
* But pseudoRoot is outside of this.getNodes().
|
|
61
|
-
* NodeGroupManager.render() copies the attributes from pseudoRoot to the actual web component root element.
|
|
62
|
-
* @type {?HTMLElement} */
|
|
63
|
-
pseudoRoot;
|
|
64
|
-
|
|
65
57
|
currentComponentProps = {};
|
|
66
58
|
|
|
67
59
|
|
|
68
60
|
/**
|
|
69
61
|
* Create an "instantiated" NodeGroup from a Template and add it to an element.
|
|
70
62
|
* @param template {Template} Create it from the html strings and expressions in this template.
|
|
71
|
-
* @param
|
|
72
|
-
|
|
73
|
-
|
|
63
|
+
* @param parentPath {?ExprPath} */
|
|
64
|
+
constructor(template, parentPath=null) {
|
|
65
|
+
if (!(this instanceof RootNodeGroup)) {
|
|
66
|
+
let [fragment, shell] = this.init(template, parentPath);
|
|
74
67
|
|
|
75
|
-
|
|
76
|
-
this.template = template;
|
|
68
|
+
this.updatePaths(fragment, shell.paths);
|
|
77
69
|
|
|
78
|
-
|
|
79
|
-
this.manager = manager;
|
|
80
|
-
|
|
81
|
-
// new!
|
|
82
|
-
template.nodeGroup = this;
|
|
83
|
-
|
|
84
|
-
// Get a cached version of the parsed and instantiated html, and ExprPaths.
|
|
85
|
-
let shell = Shell.get(template.html);
|
|
70
|
+
this.activateEmbeds(fragment, shell);
|
|
86
71
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
// Figure out value of replaceMode option if it isn't set,
|
|
90
|
-
// Assume replaceMode if there's only one child element and its tagname matches the root el.
|
|
91
|
-
let replaceMode = typeof template.replaceMode === 'boolean'
|
|
92
|
-
? template.replaceMode
|
|
93
|
-
: fragment.children.length===1 &&
|
|
94
|
-
fragment.firstElementChild?.tagName.replace(/-SOLARITE-PLACEHOLDER$/, '')
|
|
95
|
-
=== manager?.rootEl?.tagName
|
|
96
|
-
if (replaceMode) {
|
|
97
|
-
this.pseudoRoot = fragment.firstElementChild;
|
|
98
|
-
// if (!manager.rootEl)
|
|
99
|
-
// manager.rootEl = this.pseudoRoot;
|
|
100
|
-
/*#IFDEV*/assert(this.pseudoRoot)/*#ENDIF*/
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
let childNodes = replaceMode
|
|
104
|
-
? fragment.firstElementChild.childNodes
|
|
105
|
-
: fragment.childNodes;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
this.startNode = childNodes[0]
|
|
109
|
-
this.endNode = childNodes[childNodes.length - 1]
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// Update paths
|
|
113
|
-
for (let oldPath of shell.paths) {
|
|
114
|
-
let path = oldPath.clone(fragment)
|
|
115
|
-
path.parentNg = this;
|
|
116
|
-
this.paths.push(path);
|
|
72
|
+
// Apply exprs
|
|
73
|
+
this.applyExprs(template.exprs);
|
|
117
74
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// Update web component placeholders.
|
|
121
|
-
// Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
|
|
122
|
-
// Is this list needed at all?
|
|
123
|
-
//for (let component of shell.components)
|
|
124
|
-
// this.components.push(resolveNodePath(this.startNode.parentNode, getNodePath(component)))
|
|
125
|
-
|
|
126
|
-
/*#IFDEV*/this.verify();/*#ENDIF*/
|
|
127
|
-
|
|
128
|
-
this.activateEmbeds(fragment, shell);
|
|
129
|
-
|
|
130
|
-
/*#IFDEV*/
|
|
131
|
-
assert(this.paths.length <= template.exprs.length);
|
|
132
|
-
if (template.exprs.length)
|
|
133
|
-
assert(this.paths.length);
|
|
134
|
-
/*#ENDIF*/
|
|
135
|
-
|
|
136
|
-
// Apply exprs
|
|
137
|
-
this.applyExprs(template.exprs);
|
|
138
|
-
|
|
139
|
-
/*#IFDEV*/this.verify();/*#ENDIF*/
|
|
140
75
|
}
|
|
141
76
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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();
|
|
148
88
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
this.createNewComponent(el)
|
|
152
|
-
}
|
|
89
|
+
this.parentPath = parentPath;
|
|
90
|
+
this.rootNg = parentPath?.parentNg?.rootNg || this;
|
|
153
91
|
|
|
154
|
-
|
|
92
|
+
/*#IFDEV*/assert(this.rootNg);/*#ENDIF*/
|
|
155
93
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
for (let path of shell.ids) {
|
|
159
|
-
let el = resolveNodePath(root, path);
|
|
160
|
-
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
94
|
+
/** @type {Template} */
|
|
95
|
+
this.template = template;
|
|
161
96
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
throw new Error(`${this.manager.rootEl.constructor.name}.${id} already has a value. `+
|
|
165
|
-
`Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
|
|
97
|
+
// new! Is this needed?
|
|
98
|
+
template.nodeGroup = this;
|
|
166
99
|
|
|
167
|
-
|
|
168
|
-
|
|
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);
|
|
169
103
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
this.styles = new Map();
|
|
174
|
-
for (let path of shell.styles) {
|
|
175
|
-
let style = resolveNodePath(root, path);
|
|
176
|
-
Util.bindStyles(style, this.manager.rootEl);
|
|
177
|
-
this.styles.set(style, style.textContent);
|
|
178
|
-
}
|
|
104
|
+
let childNodes = fragment.childNodes;
|
|
105
|
+
this.startNode = childNodes[0];
|
|
106
|
+
this.endNode = childNodes[childNodes.length - 1];
|
|
179
107
|
|
|
180
|
-
|
|
181
|
-
// scripts
|
|
182
|
-
if (this.manager.options.scripts !== false) {
|
|
183
|
-
for (let path of shell.scripts) {
|
|
184
|
-
let script = resolveNodePath(root, path);
|
|
185
|
-
eval(script.textContent)
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
108
|
+
return [fragment, shell];
|
|
189
109
|
}
|
|
190
110
|
|
|
191
111
|
/**
|
|
@@ -195,8 +115,9 @@ export default class NodeGroup {
|
|
|
195
115
|
* @param paths {?ExprPath[]} Optional. */
|
|
196
116
|
applyExprs(exprs, paths=null) {
|
|
197
117
|
paths = paths || this.paths;
|
|
198
|
-
|
|
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
|
|
|
@@ -206,56 +127,24 @@ export default class NodeGroup {
|
|
|
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;
|
|
217
|
-
let el = (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(el, 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(el, expr, root);
|
|
243
|
-
}
|
|
137
|
+
exprIndex = path.apply(expr, exprs, exprIndex, this.currentComponentProps);
|
|
244
138
|
|
|
245
|
-
|
|
246
|
-
else // One node value may have multiple expressions. Here we apply them all at once.
|
|
247
|
-
exprIndex = path.applyValueAttrib(el, 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
|
}
|
|
@@ -273,123 +162,8 @@ export default class NodeGroup {
|
|
|
273
162
|
/*#IFDEV*/this.verify();/*#ENDIF*/
|
|
274
163
|
}
|
|
275
164
|
|
|
276
|
-
applyExpr(path, expr) {
|
|
277
|
-
// TODO: Use this if I can figure out how to adapt applyValueAttrib() to it.
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Insert/replace the nodes created by a single expression.
|
|
282
|
-
* Called by applyExprs()
|
|
283
|
-
* This function is recursive, as the functions it calls also call it.
|
|
284
|
-
* TODO: Move this to ExprPath?
|
|
285
|
-
* @param path {ExprPath}
|
|
286
|
-
* @param expr {Expr}
|
|
287
|
-
* @return {Node[]} New Nodes created. */
|
|
288
|
-
applyNodeExpr(path, expr) {
|
|
289
|
-
/*#IFDEV*/path.verify();/*#ENDIF*/
|
|
290
|
-
|
|
291
|
-
/** @type {(Node|NodeGroup|Expr)[]} */
|
|
292
|
-
let newNodes = [];
|
|
293
|
-
let oldNodeGroups = path.nodeGroups;
|
|
294
|
-
/*#IFDEV*/assert(!oldNodeGroups.includes(null))/*#ENDIF*/
|
|
295
|
-
let secondPass = []; // indices
|
|
296
|
-
|
|
297
|
-
// First Pass
|
|
298
|
-
//for (let ng of path.nodeGroups) // TODO: Is this necessary?
|
|
299
|
-
// ng.parentPath = null;
|
|
300
|
-
path.nodeGroups = [];
|
|
301
|
-
path.apply(expr, newNodes, secondPass);
|
|
302
|
-
this.existingTextNodes = null;
|
|
303
|
-
|
|
304
|
-
// TODO: Create an array of old vs Nodes and NodeGroups together.
|
|
305
|
-
// If they're all the same, skip the next steps.
|
|
306
|
-
// Or calculate it in the loop above as we go? Have a path.lastNodeGroups property?
|
|
307
|
-
|
|
308
|
-
// Second pass to find close-match NodeGroups.
|
|
309
|
-
let flatten = false;
|
|
310
|
-
if (secondPass.length) {
|
|
311
|
-
for (let [nodesIndex, ngIndex] of secondPass) {
|
|
312
|
-
let ng = this.manager.getNodeGroup(newNodes[nodesIndex], false);
|
|
313
|
-
|
|
314
|
-
ng.parentPath = path;
|
|
315
|
-
let ngNodes = ng.getNodes();
|
|
316
|
-
|
|
317
|
-
/*#IFDEV*/assert(!(newNodes[nodesIndex] instanceof NodeGroup))/*#ENDIF*/
|
|
318
|
-
|
|
319
|
-
if (ngNodes.length === 1)
|
|
320
|
-
newNodes[nodesIndex] = ngNodes[0];
|
|
321
|
-
|
|
322
|
-
else {
|
|
323
|
-
newNodes[nodesIndex] = ngNodes;
|
|
324
|
-
flatten = true;
|
|
325
|
-
}
|
|
326
|
-
path.nodeGroups[ngIndex] = ng;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (flatten)
|
|
330
|
-
newNodes = newNodes.flat(); // TODO: Only if second pass happens?
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/*#IFDEV*/assert(!path.nodeGroups.includes(null))/*#ENDIF*/
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
let oldNodes = path.getNodes();
|
|
338
|
-
path.nodesCache = newNodes; // Replaces value set by path.getNodes()
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
// This pre-check makes it a few percent faster?
|
|
342
|
-
let diff = findArrayDiff(oldNodes, newNodes);
|
|
343
|
-
if (diff !== false) {
|
|
344
|
-
|
|
345
|
-
if (this.parentPath)
|
|
346
|
-
this.parentPath.clearNodesCache();
|
|
347
|
-
|
|
348
|
-
// Fast clear method
|
|
349
|
-
let isNowEmpty = oldNodes.length && !newNodes.length;
|
|
350
|
-
if (!isNowEmpty || !path.fastClear(oldNodes, newNodes))
|
|
351
|
-
|
|
352
|
-
// Rearrange nodes.
|
|
353
|
-
udomdiff(path.parentNode, oldNodes, newNodes, path.nodeMarker)
|
|
354
|
-
|
|
355
|
-
this.saveOrphans(oldNodeGroups, oldNodes);
|
|
356
|
-
}
|
|
357
|
-
/*#IFDEV*/path.verify();/*#ENDIF*/
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Find NodeGroups that had their nodes removed and add those nodes to a Fragment so
|
|
362
|
-
* they're not lost forever and the NodeGroup's internal structure is still consistent.
|
|
363
|
-
* Called from NodeGroup.applyNodeExpr().
|
|
364
|
-
* @param oldNodeGroups {NodeGroup[]}
|
|
365
|
-
* @param oldNodes {Node[]} */
|
|
366
|
-
saveOrphans(oldNodeGroups, oldNodes) {
|
|
367
|
-
let oldNgMap = new Map();
|
|
368
|
-
for (let ng of oldNodeGroups) {
|
|
369
|
-
oldNgMap.set(ng.startNode, ng)
|
|
370
|
-
|
|
371
|
-
// TODO: Is this necessary?
|
|
372
|
-
// if (ng.parentPath)
|
|
373
|
-
// ng.parentPath.clearNodesCache();
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
for (let i=0, node; node = oldNodes[i]; i++) {
|
|
377
|
-
let ng;
|
|
378
|
-
if (!node.parentNode && (ng = oldNgMap.get(node))) {
|
|
379
|
-
let fragment = document.createDocumentFragment();
|
|
380
|
-
let endNode = ng.endNode;
|
|
381
|
-
while (node !== endNode) {
|
|
382
|
-
fragment.append(node)
|
|
383
|
-
i++;
|
|
384
|
-
node = oldNodes[i];
|
|
385
|
-
}
|
|
386
|
-
fragment.append(endNode);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
165
|
/**
|
|
392
|
-
* Create a nested
|
|
166
|
+
* Create a nested Component or call render with the new props.
|
|
393
167
|
* @param el {Solarite:HTMLElement}
|
|
394
168
|
* @param props {Object} */
|
|
395
169
|
applyComponentExprs(el, props) {
|
|
@@ -407,14 +181,14 @@ export default class NodeGroup {
|
|
|
407
181
|
if (isPreHtmlElement || isPreIsElement)
|
|
408
182
|
el = this.createNewComponent(el, isPreHtmlElement, props);
|
|
409
183
|
|
|
410
|
-
//
|
|
184
|
+
// Call render() with the same params that would've been passed to the constructor.
|
|
411
185
|
else if (el.render) {
|
|
412
|
-
let oldHash = componentHash.get(el);
|
|
186
|
+
let oldHash = Globals.componentHash.get(el);
|
|
413
187
|
if (oldHash !== newHash)
|
|
414
188
|
el.render(props); // Pass new values of props to render so it can decide how it wants to respond.
|
|
415
189
|
}
|
|
416
190
|
|
|
417
|
-
componentHash.set(el, newHash);
|
|
191
|
+
Globals.componentHash.set(el, newHash);
|
|
418
192
|
}
|
|
419
193
|
|
|
420
194
|
/**
|
|
@@ -457,33 +231,43 @@ export default class NodeGroup {
|
|
|
457
231
|
// We pass the childNodes to the constructor so it can know about them,
|
|
458
232
|
// instead of only afterward when they're appended to the slot below.
|
|
459
233
|
// This is useful for a custom selectbox, for example.
|
|
460
|
-
//
|
|
234
|
+
// Globals.pendingChildren stores the childen so the super construtor call to Solarite's constructor
|
|
461
235
|
// can add them as children before the rest of the constructor code executes.
|
|
462
236
|
let ch = [... el.childNodes];
|
|
463
|
-
|
|
237
|
+
Globals.pendingChildren.push(ch); // pop() is called in Solarite constructor.
|
|
464
238
|
let newEl = new Constructor(props, ch);
|
|
465
239
|
|
|
466
240
|
if (!isPreHtmlElement)
|
|
467
241
|
newEl.setAttribute('is', el.getAttribute('is').toLowerCase())
|
|
468
242
|
el.replaceWith(newEl);
|
|
469
|
-
|
|
243
|
+
|
|
470
244
|
// Set children / slot children
|
|
471
245
|
// TODO: Match named slots.
|
|
472
246
|
// TODO: This only appends to slot if render() is called in the constructor.
|
|
473
247
|
//let slot = newEl.querySelector('slot') || newEl;
|
|
474
248
|
//slot.append(...el.childNodes);
|
|
475
|
-
|
|
249
|
+
|
|
476
250
|
// Copy over event attributes.
|
|
477
251
|
for (let propName in props) {
|
|
478
252
|
let val = props[propName];
|
|
479
253
|
if (propName.startsWith('on') && typeof val === 'function')
|
|
480
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
|
+
}
|
|
481
265
|
}
|
|
482
266
|
|
|
483
267
|
// If an id pointed at the placeholder, update it to point to the new element.
|
|
484
268
|
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
485
269
|
if (id)
|
|
486
|
-
this.
|
|
270
|
+
delve(this.getRootNode(), id.split(/\./g), newEl);
|
|
487
271
|
|
|
488
272
|
|
|
489
273
|
// Update paths to use replaced element.
|
|
@@ -552,18 +336,39 @@ export default class NodeGroup {
|
|
|
552
336
|
return this.startNode?.parentNode
|
|
553
337
|
}
|
|
554
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
|
+
|
|
555
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
|
+
}
|
|
556
362
|
|
|
557
363
|
updateStyles() {
|
|
558
364
|
if (this.styles)
|
|
559
365
|
for (let [style, oldText] of this.styles) {
|
|
560
366
|
let newText = style.textContent;
|
|
561
367
|
if (oldText !== newText)
|
|
562
|
-
Util.bindStyles(style, this.
|
|
368
|
+
Util.bindStyles(style, this.getRootNodeGroup().root);
|
|
563
369
|
}
|
|
564
370
|
}
|
|
565
371
|
|
|
566
|
-
|
|
567
372
|
//#IFDEV
|
|
568
373
|
/**
|
|
569
374
|
* @deprecated
|
|
@@ -582,7 +387,7 @@ export default class NodeGroup {
|
|
|
582
387
|
|
|
583
388
|
let path = this.paths.find(path=>path.type === PathType.Content && path.getNodes().includes(nextNode));
|
|
584
389
|
if (path)
|
|
585
|
-
return [`Path
|
|
390
|
+
return [`Path.nodes:`]
|
|
586
391
|
|
|
587
392
|
return [];
|
|
588
393
|
})
|
|
@@ -630,7 +435,184 @@ export default class NodeGroup {
|
|
|
630
435
|
return true;
|
|
631
436
|
}
|
|
632
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
|
+
}
|
|
633
503
|
}
|
|
634
504
|
|
|
635
505
|
|
|
636
|
-
|
|
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
|
+
}
|