solarite 0.2.4 → 0.3.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/dist/Solarite-debug.js +1457 -1402
- package/dist/Solarite.js +1425 -1272
- package/dist/Solarite.min.js +2 -2
- package/package.json +5 -6
- package/readme.md +2 -4
- package/src/{solarite/ExprPath.js → ExprPath.js} +421 -231
- package/src/Globals.js +79 -0
- package/src/HtmlParser.js +91 -0
- package/src/{util/MultiValueMap.js → MultiValueMap.js} +22 -26
- package/src/{solarite/NodeGroup.js → NodeGroup.js} +286 -226
- package/src/{solarite/Shell.js → Shell.js} +119 -92
- package/src/Solarite.d.ts +62 -0
- package/src/{solarite/Solarite.js → Solarite.js} +15 -13
- package/src/{solarite/Template.js → Template.js} +22 -19
- package/src/Util.js +330 -0
- package/src/{util/Errors.js → assert.js} +1 -0
- package/src/createSolarite.js +154 -0
- package/src/{util/delve.js → delve.js} +5 -4
- package/src/{solarite/getArg.js → getArg.js} +41 -15
- package/src/{solarite/r.js → h.js} +59 -29
- package/src/{solarite/hash.js → hash.js} +12 -9
- package/src/unused/FastLookupArray.js +54 -0
- package/src/unused/Hashes.js +339 -0
- package/src/unused/InUse.test.js +92 -0
- package/src/unused/InUseMap.js +98 -0
- package/src/unused/LinkedList.js +117 -0
- package/src/unused/LinkedList.test.js +115 -0
- package/src/unused/Misc.js +13 -0
- package/src/unused/Perf.js +47 -0
- package/src/unused/TrackedArray.js +54 -0
- package/src/watch.js +546 -0
- package/src/solarite/Globals.js +0 -54
- package/src/solarite/Util.js +0 -388
- package/src/solarite/createSolarite.js +0 -274
- package/src/solarite/watch3.js +0 -189
- package/src/util/Util.js +0 -113
- /package/src/{solarite/udomdiff.js → udomdiff.js} +0 -0
- /package/src/{util → unused}/WeakArray.js +0 -0
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import {assert} from "
|
|
2
|
-
import ExprPath, {
|
|
1
|
+
import {assert} from "./assert.js";
|
|
2
|
+
import ExprPath, {ExprPathType, resolveNodePath} from "./ExprPath.js";
|
|
3
3
|
import {getObjectHash} from "./hash.js";
|
|
4
4
|
import Shell from "./Shell.js";
|
|
5
|
-
import
|
|
6
|
-
import Util, {arraySame, flattenAndIndent, isEvent, nodeToArrayTree, setIndent} from "./Util.js";
|
|
5
|
+
import Util, {flattenAndIndent, nodeToArrayTree, setIndent} from "./Util.js";
|
|
7
6
|
//import NodeGroupManager from "./NodeGroupManager.js";
|
|
8
|
-
import delve from "
|
|
7
|
+
import delve from "./delve.js";
|
|
9
8
|
import Globals from "./Globals.js";
|
|
10
|
-
import MultiValueMap from "../util/MultiValueMap.js";
|
|
11
|
-
|
|
12
9
|
|
|
13
10
|
/** @typedef {boolean|string|number|function|Object|Array|Date|Node|Template} Expr */
|
|
14
11
|
|
|
@@ -34,7 +31,8 @@ export default class NodeGroup {
|
|
|
34
31
|
startNode;
|
|
35
32
|
|
|
36
33
|
/** @type {Node|HTMLElement} A node that never changes that this NodeGroup should always insert its nodes before.
|
|
37
|
-
* An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position
|
|
34
|
+
* An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.
|
|
35
|
+
* TODO: But sometimes startNode and endNode point to the same node. Document htis inconsistency. */
|
|
38
36
|
endNode;
|
|
39
37
|
|
|
40
38
|
/** @type {ExprPath[]} */
|
|
@@ -52,11 +50,11 @@ export default class NodeGroup {
|
|
|
52
50
|
nodesCache;
|
|
53
51
|
|
|
54
52
|
/**
|
|
53
|
+
* A map between <style> Elements and their text content.
|
|
54
|
+
* This lets NodeGroup.updateStyles() see when the style text has changed.
|
|
55
55
|
* @type {?Map<HTMLStyleElement, string>} */
|
|
56
56
|
styles;
|
|
57
57
|
|
|
58
|
-
currentComponentProps = {};
|
|
59
|
-
|
|
60
58
|
|
|
61
59
|
/**
|
|
62
60
|
* Create an "instantiated" NodeGroup from a Template and add it to an element.
|
|
@@ -64,14 +62,26 @@ export default class NodeGroup {
|
|
|
64
62
|
* @param parentPath {?ExprPath} */
|
|
65
63
|
constructor(template, parentPath=null) {
|
|
66
64
|
if (!(this instanceof RootNodeGroup)) {
|
|
65
|
+
|
|
67
66
|
let [fragment, shell] = this.init(template, parentPath);
|
|
68
67
|
|
|
69
|
-
|
|
68
|
+
if (fragment && template.exprs.length) {
|
|
69
|
+
this.updatePaths(fragment, shell.paths);
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
// Static web components can sometimes have children created via expressions.
|
|
72
|
+
// But calling applyExprs() will mess up the shell's path to them.
|
|
73
|
+
// So we find them first, then call activateStaticComponents() after their children have been created.
|
|
74
|
+
let staticComponents = this.findStaticComponents(fragment, shell);
|
|
72
75
|
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
this.activateEmbeds(fragment, shell);
|
|
77
|
+
|
|
78
|
+
// Apply exprs
|
|
79
|
+
this.applyExprs(template.exprs);
|
|
80
|
+
|
|
81
|
+
this.instantiateStaticComponents(staticComponents);
|
|
82
|
+
}
|
|
83
|
+
else if (shell)
|
|
84
|
+
this.activateEmbeds(fragment, shell);
|
|
75
85
|
}
|
|
76
86
|
}
|
|
77
87
|
|
|
@@ -99,68 +109,119 @@ export default class NodeGroup {
|
|
|
99
109
|
template.nodeGroup = this;
|
|
100
110
|
|
|
101
111
|
// Get a cached version of the parsed and instantiated html, and ExprPaths.
|
|
102
|
-
let shell = Shell.get(template.html);
|
|
103
|
-
let fragment = shell.fragment.cloneNode(true);
|
|
104
112
|
|
|
105
|
-
|
|
106
|
-
this.
|
|
107
|
-
|
|
113
|
+
// If it's just a text node, skip a bunch of unnecessary steps.
|
|
114
|
+
if (!(this instanceof RootNodeGroup) && !template.exprs.length && !template.html[0].includes('<')) {
|
|
115
|
+
//let doc = this.rootNg.startNode?.ownerDocument || document;
|
|
116
|
+
let textNode = document.createTextNode(template.html[0]);
|
|
108
117
|
|
|
109
|
-
|
|
118
|
+
this.startNode = this.endNode = textNode;
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
let shell = Shell.get(template.html);
|
|
123
|
+
let fragment = shell.fragment.cloneNode(true);
|
|
124
|
+
|
|
125
|
+
if (fragment instanceof DocumentFragment) {
|
|
126
|
+
let childNodes = fragment.childNodes;
|
|
127
|
+
this.startNode = childNodes[0];
|
|
128
|
+
this.endNode = childNodes[childNodes.length - 1];
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
this.startNode = this.endNode = fragment;
|
|
132
|
+
}
|
|
133
|
+
return [fragment, shell];
|
|
134
|
+
}
|
|
110
135
|
}
|
|
111
136
|
|
|
112
137
|
/**
|
|
113
138
|
* Use the paths to insert the given expressions.
|
|
114
139
|
* Dispatches expression handling to other functions depending on the path type.
|
|
115
140
|
* @param exprs {(*|*[]|function|Template)[]}
|
|
116
|
-
* @param paths {?ExprPath[]} Optional. */
|
|
141
|
+
* @param paths {?ExprPath[]} Optional. Only used for testing. Normally uses this.paths. */
|
|
117
142
|
applyExprs(exprs, paths=null) {
|
|
118
143
|
paths = paths || this.paths;
|
|
119
144
|
|
|
120
|
-
/*#IFDEV*/
|
|
145
|
+
/*#IFDEV*/
|
|
146
|
+
this.verify();/*#ENDIF*/
|
|
147
|
+
|
|
148
|
+
// Things to consider:
|
|
149
|
+
// 1. One path may use multipe expressions. E.g. <div class="${1} ${2}">
|
|
150
|
+
// 2. One component may need to use multiple attribute paths to be instantiated.
|
|
151
|
+
// 3. We apply them in reverse order so that a <select> box has its children created from an expression
|
|
152
|
+
// before its instantiated and its value attribute is set via an expression.
|
|
153
|
+
|
|
154
|
+
let exprIndex = exprs.length - 1; // Update exprs at paths.
|
|
155
|
+
let lastComponentPathIndex;
|
|
156
|
+
let pathExprs = new Array(paths.length); // Store all the expressions that map to a single path. Only paths to attribute values can have more than one.
|
|
157
|
+
for (let i = paths.length - 1, path; path = paths[i]; i--) {
|
|
158
|
+
let prevPath = paths[i - 1];
|
|
159
|
+
let nextPath = paths[i + 1];
|
|
160
|
+
|
|
161
|
+
// Get the expressions associated with this path.
|
|
162
|
+
if (path.attrValue?.length > 2) {
|
|
163
|
+
let startIndex = (exprIndex - (path.attrValue.length - 1)) + 1;
|
|
164
|
+
pathExprs[i] = exprs.slice(startIndex, exprIndex + 1); // probably doesn't allocate if the JS vm implements copy on write.
|
|
165
|
+
exprIndex -= pathExprs[i].length;
|
|
166
|
+
} else {
|
|
167
|
+
pathExprs[i] = [exprs[exprIndex]];
|
|
168
|
+
exprIndex--;
|
|
169
|
+
}
|
|
121
170
|
|
|
122
|
-
|
|
123
|
-
|
|
171
|
+
// TODO: Need to end and restart this block when going from one component to the next?
|
|
172
|
+
// Think of having two adjacent components.
|
|
173
|
+
// But the dynamicAttribsAdjacet test already passes.
|
|
124
174
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
175
|
+
// If expr is an attribute in a component:
|
|
176
|
+
// 1. Instantiate it if it hasn't already been, sending all expr's to its constructor.
|
|
177
|
+
// 2. Otherwise send them to its render function.
|
|
178
|
+
// Components with no expressions as attributes are instead activated in activateEmbeds().
|
|
179
|
+
if (path.nodeMarker !== this.rootNg.root && path.isComponent()) {
|
|
129
180
|
|
|
130
|
-
|
|
181
|
+
if (!nextPath || !nextPath.isComponent() || nextPath.nodeMarker !== path.nodeMarker)
|
|
182
|
+
lastComponentPathIndex = i;
|
|
183
|
+
let isFirstComponentPath = !prevPath || !prevPath.isComponent() || prevPath.nodeMarker !== path.nodeMarker;
|
|
131
184
|
|
|
132
|
-
|
|
133
|
-
if (lastNode && lastNode !== this.rootNg.root && lastNode !== path.nodeMarker && Object.keys(this.currentComponentProps).length) {
|
|
134
|
-
this.applyComponentExprs(lastNode, this.currentComponentProps);
|
|
135
|
-
this.currentComponentProps = {};
|
|
136
|
-
}
|
|
185
|
+
if (isFirstComponentPath) {
|
|
137
186
|
|
|
138
|
-
|
|
187
|
+
let componentProps = {}
|
|
188
|
+
for (let j=i; j<=lastComponentPathIndex; j++) {
|
|
189
|
+
let attrName = paths[j].attrName; // Util.dashesToCamel(paths[j].attrName);
|
|
190
|
+
componentProps[attrName] = pathExprs[j].length > 1 ? pathExprs[j].join('') : pathExprs[j][0];
|
|
191
|
+
}
|
|
139
192
|
|
|
140
|
-
|
|
193
|
+
this.applyComponentExprs(path.nodeMarker, componentProps);
|
|
141
194
|
|
|
195
|
+
// Set attributes on component.
|
|
196
|
+
for (let j=i; j<=lastComponentPathIndex; j++)
|
|
197
|
+
paths[j].apply(pathExprs[j]);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
142
200
|
|
|
143
|
-
|
|
144
|
-
|
|
201
|
+
// Else apply it normally
|
|
202
|
+
else
|
|
203
|
+
path.apply(pathExprs[i]);
|
|
145
204
|
|
|
146
205
|
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
this.applyComponentExprs(lastNode, this.currentComponentProps);
|
|
150
|
-
this.currentComponentProps = {};
|
|
151
|
-
}
|
|
206
|
+
} // end for(path of this.paths)
|
|
207
|
+
|
|
152
208
|
|
|
209
|
+
// TODO: Only do this if we have ExprPaths within styles?
|
|
153
210
|
this.updateStyles();
|
|
154
211
|
|
|
212
|
+
|
|
213
|
+
|
|
155
214
|
// Invalidate the nodes cache because we just changed it.
|
|
156
215
|
this.nodesCache = null;
|
|
157
216
|
|
|
158
217
|
// If there's leftover expressions, there's probably an issue with the Shell that created this NodeGroup,
|
|
159
218
|
// and the number of paths not matching.
|
|
160
|
-
/*#IFDEV*/
|
|
219
|
+
/*#IFDEV*/
|
|
220
|
+
assert(exprIndex === -1);/*#ENDIF*/
|
|
161
221
|
|
|
162
222
|
|
|
163
|
-
/*#IFDEV*/
|
|
223
|
+
/*#IFDEV*/
|
|
224
|
+
this.verify();/*#ENDIF*/
|
|
164
225
|
}
|
|
165
226
|
|
|
166
227
|
/**
|
|
@@ -174,24 +235,30 @@ export default class NodeGroup {
|
|
|
174
235
|
// then we could re-use the hash and logic from NodeManager?
|
|
175
236
|
let newHash = getObjectHash(props);
|
|
176
237
|
|
|
177
|
-
let isPreHtmlElement = el.
|
|
238
|
+
let isPreHtmlElement = el.hasAttribute('solarite-placeholder');
|
|
178
239
|
let isPreIsElement = el.hasAttribute('_is')
|
|
179
240
|
|
|
180
241
|
|
|
181
242
|
// Instantiate a placeholder.
|
|
182
243
|
if (isPreHtmlElement || isPreIsElement)
|
|
183
|
-
el = this.
|
|
244
|
+
el = this.instantiateComponent(el, isPreHtmlElement, props);
|
|
184
245
|
|
|
185
246
|
// Call render() with the same params that would've been passed to the constructor.
|
|
247
|
+
// We do this even if the arguments haven't changed, so we can let the child component
|
|
248
|
+
// compare the arguments and then decide for itself whether it wants to re-render.
|
|
186
249
|
else if (el.render) {
|
|
187
|
-
let oldHash = Globals.
|
|
188
|
-
if (oldHash !== newHash)
|
|
189
|
-
|
|
250
|
+
//let oldHash = Globals.componentArgsHash.get(el);
|
|
251
|
+
//if (oldHash !== newHash) { // Only if not changed.
|
|
252
|
+
let args = {};
|
|
253
|
+
for (let name in props || {})
|
|
254
|
+
args[Util.dashesToCamel(name)] = props[name];
|
|
255
|
+
el.render(args); // Pass new values of props to render so it can decide how it wants to respond.
|
|
256
|
+
//}
|
|
190
257
|
}
|
|
191
258
|
|
|
192
|
-
Globals.
|
|
259
|
+
Globals.componentArgsHash.set(el, newHash);
|
|
193
260
|
}
|
|
194
|
-
|
|
261
|
+
|
|
195
262
|
/**
|
|
196
263
|
* We swap the placeholder element for the real element so we can pass its dynamic attributes
|
|
197
264
|
* to its constructor.
|
|
@@ -199,78 +266,54 @@ export default class NodeGroup {
|
|
|
199
266
|
* The logic of this function is complex and could use cleaning up.
|
|
200
267
|
*
|
|
201
268
|
* @param el
|
|
202
|
-
* @param isPreHtmlElement
|
|
269
|
+
* @param isPreHtmlElement {?boolean} True if the element's tag name ends with -solarite-placeholder
|
|
203
270
|
* @param props {Object} Attributes with dynamic values.
|
|
204
271
|
* @return {HTMLElement} */
|
|
205
|
-
|
|
272
|
+
instantiateComponent(el, isPreHtmlElement=undefined, props=undefined) {
|
|
206
273
|
if (isPreHtmlElement === undefined)
|
|
207
274
|
isPreHtmlElement = !el.hasAttribute('_is');
|
|
208
|
-
|
|
275
|
+
|
|
209
276
|
let tagName = (isPreHtmlElement
|
|
210
|
-
? el.tagName.
|
|
211
|
-
? el.tagName.slice(0, -21)
|
|
212
|
-
: el.tagName
|
|
277
|
+
? el.tagName.slice(0, -21) // Remove -SOLARITE-PLACEHOLDER
|
|
213
278
|
: el.getAttribute('is')).toLowerCase();
|
|
214
279
|
|
|
215
|
-
|
|
216
|
-
|
|
280
|
+
|
|
281
|
+
// Throw if custom element isn't defined.
|
|
282
|
+
let Constructor = customElements.get(tagName);
|
|
283
|
+
if (!Constructor)
|
|
284
|
+
throw new Error(`The custom tag name ${tagName} is not registered.`)
|
|
285
|
+
|
|
286
|
+
let args = {};
|
|
287
|
+
for (let name in props || {})
|
|
288
|
+
args[Util.dashesToCamel(name)] = props[name];
|
|
289
|
+
|
|
217
290
|
// Pass other attribs to constructor, since otherwise they're not yet set on the element,
|
|
218
291
|
// and the constructor would otherwise have no way to see them.
|
|
219
292
|
if (el.attributes.length) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
293
|
+
for (let attrib of el.attributes) {
|
|
294
|
+
let attribName = Util.dashesToCamel(attrib.name);
|
|
295
|
+
if (!args.hasOwnProperty(attribName) && attribName !== 'solarite-placeholder')
|
|
296
|
+
args[attribName] = attrib.value;
|
|
297
|
+
}
|
|
225
298
|
}
|
|
226
|
-
|
|
227
|
-
// Create CustomElement and
|
|
228
|
-
let Constructor = customElements.get(tagName);
|
|
229
|
-
if (!Constructor)
|
|
230
|
-
throw new Error(`The custom tag name ${tagName} is not registered.`)
|
|
231
299
|
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
// can add them as children before the rest of the constructor code executes.
|
|
237
|
-
let ch = [... el.childNodes];
|
|
238
|
-
Globals.pendingChildren.push(ch); // pop() is called in Solarite constructor.
|
|
239
|
-
let newEl = new Constructor(props, ch);
|
|
300
|
+
// Create the web component.
|
|
301
|
+
// Get the children that aren't Solarite's comment placeholders.
|
|
302
|
+
let ch = [...el.childNodes].filter(node => node.nodeType !== Node.COMMENT_NODE || !node.nodeValue.startsWith('ExprPath'));
|
|
303
|
+
let newEl = new Constructor(args, ch);
|
|
240
304
|
|
|
241
305
|
if (!isPreHtmlElement)
|
|
242
|
-
newEl.setAttribute('is', el.getAttribute('is').toLowerCase())
|
|
306
|
+
newEl.setAttribute('is', el.getAttribute('is').toLowerCase());
|
|
307
|
+
|
|
308
|
+
// Replace the placeholder tag with the instantiated web component.
|
|
243
309
|
el.replaceWith(newEl);
|
|
244
310
|
|
|
245
|
-
// Set children / slot children
|
|
246
|
-
// TODO: Match named slots.
|
|
247
|
-
// TODO: This only appends to slot if render() is called in the constructor.
|
|
248
|
-
//let slot = newEl.querySelector('slot') || newEl;
|
|
249
|
-
//slot.append(...el.childNodes);
|
|
250
|
-
|
|
251
|
-
// Copy over event attributes.
|
|
252
|
-
for (let propName in props) {
|
|
253
|
-
let val = props[propName];
|
|
254
|
-
if (propName.startsWith('on') && typeof val === 'function')
|
|
255
|
-
newEl.addEventListener(propName.slice(2), e => val(e, newEl));
|
|
256
|
-
|
|
257
|
-
// Bind array based event attributes on value.
|
|
258
|
-
// This same logic is in ExprPath.applyValueAttrib() for non-components.
|
|
259
|
-
if ((propName === 'value' || propName === 'data-value') && Util.isPath(val)) {
|
|
260
|
-
let [obj, path] = [val[0], val.slice(1)];
|
|
261
|
-
newEl.value = delve(obj, path);
|
|
262
|
-
newEl.addEventListener('input', e => {
|
|
263
|
-
delve(obj, path, Util.getInputValue(newEl));
|
|
264
|
-
}, true); // We use capture so we update the values before other events added by the user.
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
311
|
// If an id pointed at the placeholder, update it to point to the new element.
|
|
269
312
|
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
270
313
|
if (id)
|
|
271
314
|
delve(this.getRootNode(), id.split(/\./g), newEl);
|
|
272
|
-
|
|
273
|
-
|
|
315
|
+
|
|
316
|
+
|
|
274
317
|
// Update paths to use replaced element.
|
|
275
318
|
for (let path of this.paths) {
|
|
276
319
|
if (path.nodeMarker === el)
|
|
@@ -282,31 +325,31 @@ export default class NodeGroup {
|
|
|
282
325
|
this.startNode = newEl;
|
|
283
326
|
if (this.endNode === el)
|
|
284
327
|
this.endNode = newEl;
|
|
285
|
-
|
|
286
|
-
|
|
328
|
+
|
|
329
|
+
|
|
287
330
|
// applyComponentExprs() is called because we're rendering.
|
|
288
331
|
// So we want to render the sub-component also.
|
|
289
332
|
if (newEl.renderFirstTime)
|
|
290
333
|
newEl.renderFirstTime();
|
|
291
|
-
|
|
334
|
+
|
|
292
335
|
// Copy attributes over.
|
|
293
336
|
for (let attrib of el.attributes)
|
|
294
|
-
if (attrib.name !== '_is')
|
|
337
|
+
if (attrib.name !== '_is' && attrib.name !== 'solarite-placeholder')
|
|
295
338
|
newEl.setAttribute(attrib.name, attrib.value);
|
|
296
339
|
|
|
297
340
|
// Set dynamic attributes if they are primitive types.
|
|
298
|
-
for (let name in
|
|
299
|
-
let val =
|
|
341
|
+
for (let name in props) {
|
|
342
|
+
let val = props[name];
|
|
300
343
|
if (typeof val === 'boolean') {
|
|
301
344
|
if (val !== false && val !== undefined && val !== null)
|
|
302
345
|
newEl.setAttribute(name, '');
|
|
303
346
|
}
|
|
304
347
|
|
|
305
|
-
// If type
|
|
348
|
+
// If type is a non-boolean primitive, set the attribute value.
|
|
306
349
|
else if (['number', 'bigint', 'string'].includes(typeof val))
|
|
307
350
|
newEl.setAttribute(name, val);
|
|
308
351
|
}
|
|
309
|
-
|
|
352
|
+
|
|
310
353
|
return newEl;
|
|
311
354
|
}
|
|
312
355
|
|
|
@@ -352,8 +395,7 @@ export default class NodeGroup {
|
|
|
352
395
|
|
|
353
396
|
/**
|
|
354
397
|
* Requires the nodeCache to be present. */
|
|
355
|
-
|
|
356
|
-
/*#IFDEV*/assert(!this.startNode.parentNode);/*#ENDIF*/
|
|
398
|
+
removeAndSaveOrphans() {
|
|
357
399
|
/*#IFDEV*/assert(this.nodesCache);/*#ENDIF*/
|
|
358
400
|
let fragment = document.createDocumentFragment();
|
|
359
401
|
for (let node of this.getNodes())
|
|
@@ -363,8 +405,9 @@ export default class NodeGroup {
|
|
|
363
405
|
|
|
364
406
|
updatePaths(fragment, paths, offset) {
|
|
365
407
|
// Update paths to point to the fragment.
|
|
366
|
-
|
|
367
|
-
|
|
408
|
+
let pathLength = paths.length;
|
|
409
|
+
this.paths.length = pathLength;
|
|
410
|
+
for (let i=0; i<pathLength; i++) {
|
|
368
411
|
let path = paths[i].clone(fragment, offset)
|
|
369
412
|
path.parentNg = this;
|
|
370
413
|
this.paths[i] = path;
|
|
@@ -386,23 +429,23 @@ export default class NodeGroup {
|
|
|
386
429
|
* An interleaved array of sets of nodes and top-level ExprPaths
|
|
387
430
|
* @type {(Node|HTMLElement|ExprPath)[]} */
|
|
388
431
|
get nodes() { throw new Error('')};
|
|
389
|
-
|
|
432
|
+
|
|
390
433
|
get debug() {
|
|
391
434
|
return [
|
|
392
435
|
`parentNode: ${this.parentNode?.tagName?.toLowerCase()}`,
|
|
393
436
|
'nodes:',
|
|
394
437
|
...setIndent(this.getNodes().map(item => {
|
|
395
438
|
if (item instanceof Node) {
|
|
396
|
-
|
|
439
|
+
|
|
397
440
|
let tree = nodeToArrayTree(item, nextNode => {
|
|
398
|
-
|
|
399
|
-
let path = this.paths.find(path=>path.type ===
|
|
441
|
+
|
|
442
|
+
let path = this.paths.find(path=>path.type === ExprPathType.Content && path.getNodes().includes(nextNode));
|
|
400
443
|
if (path)
|
|
401
444
|
return [`Path.nodes:`]
|
|
402
|
-
|
|
445
|
+
|
|
403
446
|
return [];
|
|
404
447
|
})
|
|
405
|
-
|
|
448
|
+
|
|
406
449
|
// TODO: How to indend nodes belonging to a path vs those that just occur after the path?
|
|
407
450
|
return flattenAndIndent(tree)
|
|
408
451
|
}
|
|
@@ -413,10 +456,10 @@ export default class NodeGroup {
|
|
|
413
456
|
}
|
|
414
457
|
|
|
415
458
|
get debugNodes() { return this.getNodes() }
|
|
416
|
-
|
|
417
|
-
|
|
459
|
+
|
|
460
|
+
|
|
418
461
|
get debugNodesHtml() { return this.getNodes().map(n => n.outerHTML || n.textContent) }
|
|
419
|
-
|
|
462
|
+
|
|
420
463
|
verify() {
|
|
421
464
|
if (!window.verify)
|
|
422
465
|
return;
|
|
@@ -431,7 +474,7 @@ export default class NodeGroup {
|
|
|
431
474
|
|
|
432
475
|
// if (this.parentPath)
|
|
433
476
|
// assert(this.parentPath.nodeGroups.includes(this));
|
|
434
|
-
|
|
477
|
+
|
|
435
478
|
for (let path of this.paths) {
|
|
436
479
|
assert(path.parentNg === this)
|
|
437
480
|
|
|
@@ -447,61 +490,70 @@ export default class NodeGroup {
|
|
|
447
490
|
}
|
|
448
491
|
//#ENDIF
|
|
449
492
|
|
|
493
|
+
findStaticComponents(root, shell, pathOffset=0) {
|
|
494
|
+
let result = [];
|
|
450
495
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
activateEmbeds(root, shell, pathOffset=0) {
|
|
456
|
-
|
|
457
|
-
// static components. These are WebComponents not created by an expression.
|
|
458
|
-
// Must happen before ids.
|
|
496
|
+
// static components. These are WebComponents that do not have any constructor arguments that are expressions.
|
|
497
|
+
// Those are instead created by applyExpr() which calls applyComponentExprs() which calls instantiateComponent().
|
|
498
|
+
// Maybe someday these two paths will be merged?
|
|
499
|
+
// Must happen before ids because instantiateComponent will replace the element.
|
|
459
500
|
for (let path of shell.staticComponents) {
|
|
460
501
|
if (pathOffset)
|
|
461
502
|
path = path.slice(0, -pathOffset);
|
|
462
503
|
let el = resolveNodePath(root, path);
|
|
463
504
|
|
|
464
505
|
// Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
|
|
506
|
+
// Recreating it is necessary so we can pass the constructor args to it.
|
|
465
507
|
if (root !== el/* && !isReplaceEl(root, el)*/) // TODO: is isReplaceEl necessary?
|
|
466
|
-
|
|
508
|
+
result.push(el);
|
|
467
509
|
}
|
|
510
|
+
return result;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
instantiateStaticComponents(staticComponents) {
|
|
514
|
+
for (let el of staticComponents)
|
|
515
|
+
this.instantiateComponent(el)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* @param root {HTMLElement|DocumentFragment}
|
|
520
|
+
* @param shell {Shell}
|
|
521
|
+
* @param pathOffset {int} */
|
|
522
|
+
activateEmbeds(root, shell, pathOffset=0) {
|
|
468
523
|
|
|
469
524
|
let rootEl = this.rootNg.root;
|
|
470
525
|
if (rootEl) {
|
|
526
|
+
let options = this.rootNg.options;
|
|
471
527
|
|
|
472
528
|
// ids
|
|
473
|
-
if (
|
|
529
|
+
if (options?.ids !== false) {
|
|
474
530
|
for (let path of shell.ids) {
|
|
475
531
|
if (pathOffset)
|
|
476
532
|
path = path.slice(0, -pathOffset);
|
|
477
533
|
let el = resolveNodePath(root, path);
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
// Don't allow overwriting existing class properties if they already have a non-Node value.
|
|
482
|
-
if (rootEl[id] && !(rootEl[id] instanceof Node))
|
|
483
|
-
throw new Error(`${rootEl.constructor.name}.${id} already has a value. ` +
|
|
484
|
-
`Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
|
|
485
|
-
|
|
486
|
-
delve(rootEl, id.split(/\./g), el);
|
|
487
|
-
}
|
|
534
|
+
Util.bindId(rootEl, el);
|
|
535
|
+
}
|
|
488
536
|
}
|
|
489
537
|
|
|
490
538
|
// styles
|
|
491
|
-
if (
|
|
539
|
+
if (options?.styles !== false) {
|
|
492
540
|
if (shell.styles.length)
|
|
493
541
|
this.styles = new Map();
|
|
494
542
|
for (let path of shell.styles) {
|
|
495
543
|
if (pathOffset)
|
|
496
544
|
path = path.slice(0, -pathOffset);
|
|
545
|
+
|
|
546
|
+
/** @type {HTMLStyleElement} */
|
|
497
547
|
let style = resolveNodePath(root, path);
|
|
498
|
-
|
|
499
|
-
|
|
548
|
+
if (rootEl.nodeType === 1) {
|
|
549
|
+
Util.bindStyles(style, rootEl);
|
|
550
|
+
this.styles.set(style, style.textContent);
|
|
551
|
+
}
|
|
500
552
|
}
|
|
501
553
|
|
|
502
554
|
}
|
|
503
555
|
// scripts
|
|
504
|
-
if (
|
|
556
|
+
if (options?.scripts !== false) {
|
|
505
557
|
for (let path of shell.scripts) {
|
|
506
558
|
if (pathOffset)
|
|
507
559
|
path = path.slice(0, -pathOffset);
|
|
@@ -522,20 +574,8 @@ export class RootNodeGroup extends NodeGroup {
|
|
|
522
574
|
root;
|
|
523
575
|
|
|
524
576
|
/**
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
* @type {Object<field:string, Set<ExprPath>>} */
|
|
528
|
-
watchedExprPaths = {};
|
|
529
|
-
|
|
530
|
-
/**
|
|
531
|
-
* Map from arrays where .map is called and their callback functions.
|
|
532
|
-
* TODO: One array might be called with two different map functions in different places!
|
|
533
|
-
* @type {Map<Array, function>} */
|
|
534
|
-
mapCallbacks = new Map();
|
|
535
|
-
|
|
536
|
-
/**
|
|
537
|
-
*
|
|
538
|
-
* @type {Map<ExprPath, boolean|Array>} */
|
|
577
|
+
* When we call renerWatched() we re-render these expressions, then clear this to a new Map()
|
|
578
|
+
* @type {Map<ExprPath, ValueOp|WholeArrayOp|ArraySpliceOp[]>} */
|
|
539
579
|
exprsToRender = new Map();
|
|
540
580
|
|
|
541
581
|
/**
|
|
@@ -552,82 +592,102 @@ export class RootNodeGroup extends NodeGroup {
|
|
|
552
592
|
this.rootNg = this;
|
|
553
593
|
let [fragment, shell] = this.init(template);
|
|
554
594
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
if (el.childNodes.length) {
|
|
564
|
-
slotFragment = document.createDocumentFragment();
|
|
565
|
-
slotFragment.append(...el.childNodes);
|
|
595
|
+
if (fragment instanceof Text) {
|
|
596
|
+
|
|
597
|
+
if (el) {
|
|
598
|
+
this.startNode = el;
|
|
599
|
+
this.endNode = el;
|
|
600
|
+
if (fragment.nodeValue.length)
|
|
601
|
+
el.append(fragment);
|
|
602
|
+
this.root = el;
|
|
566
603
|
}
|
|
604
|
+
Globals.nodeGroups.set(this.root, this);
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
567
607
|
|
|
568
|
-
|
|
608
|
+
// If adding NodeGroup to an element.
|
|
609
|
+
let offset = 0;
|
|
610
|
+
let root = fragment; // TODO: Rename so it's not confused with this.root.
|
|
611
|
+
if (el) {
|
|
612
|
+
Globals.nodeGroups.set(el, this);
|
|
613
|
+
|
|
614
|
+
// Save slot children
|
|
615
|
+
let slotChildren;
|
|
616
|
+
if (el.childNodes.length) {
|
|
617
|
+
slotChildren = document.createDocumentFragment();
|
|
618
|
+
slotChildren.append(...el.childNodes);
|
|
619
|
+
}
|
|
569
620
|
|
|
570
|
-
|
|
571
|
-
if (isReplaceEl(fragment, el)) {
|
|
572
|
-
el.append(...fragment.children[0].childNodes);
|
|
621
|
+
this.root = el;
|
|
573
622
|
|
|
574
|
-
//
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
el.setAttribute(attrib.name, attrib.value);
|
|
623
|
+
// If el should replace the root node of the fragment.
|
|
624
|
+
if (isReplaceEl(fragment, el)) {
|
|
625
|
+
el.append(...fragment.children[0].childNodes);
|
|
578
626
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
627
|
+
// Copy attributes
|
|
628
|
+
for (let attrib of fragment.children[0].attributes)
|
|
629
|
+
if (!el.hasAttribute(attrib.name) && attrib.name !== 'solarite-placeholder')
|
|
630
|
+
el.setAttribute(attrib.name, attrib.value);
|
|
631
|
+
|
|
632
|
+
// Go one level deeper into all of shell's paths.
|
|
633
|
+
offset = 1;
|
|
634
|
+
} else {
|
|
635
|
+
let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
|
|
636
|
+
if (!isEmpty)
|
|
637
|
+
el.append(...fragment.childNodes);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Setup children
|
|
641
|
+
if (slotChildren) {
|
|
587
642
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
643
|
+
// Named slots
|
|
644
|
+
for (let slot of el.querySelectorAll('slot[name]')) {
|
|
645
|
+
let name = slot.getAttribute('name')
|
|
646
|
+
if (name) {
|
|
647
|
+
let slotChildren2 = slotChildren.querySelectorAll(`[slot='${name}']`);
|
|
648
|
+
slot.append(...slotChildren2);
|
|
649
|
+
}
|
|
595
650
|
}
|
|
651
|
+
|
|
652
|
+
// Unnamed slots
|
|
653
|
+
let unamedSlot = el.querySelector('slot:not([name])')
|
|
654
|
+
if (unamedSlot)
|
|
655
|
+
unamedSlot.append(slotChildren);
|
|
656
|
+
|
|
657
|
+
// No slots
|
|
658
|
+
else
|
|
659
|
+
el.append(slotChildren);
|
|
596
660
|
}
|
|
597
|
-
let unamedSlot = el.querySelector('slot:not([name])')
|
|
598
|
-
if (unamedSlot)
|
|
599
|
-
unamedSlot.append(slotFragment);
|
|
600
|
-
else
|
|
601
|
-
el.append(slotFragment);
|
|
602
|
-
}
|
|
603
661
|
|
|
604
|
-
|
|
662
|
+
root = el;
|
|
605
663
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
|
|
664
|
+
this.startNode = el;
|
|
665
|
+
this.endNode = el;
|
|
666
|
+
} else {
|
|
667
|
+
let singleEl = getSingleEl(fragment);
|
|
668
|
+
this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
|
|
612
669
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
670
|
+
Globals.nodeGroups.set(this.root, this);
|
|
671
|
+
if (singleEl) {
|
|
672
|
+
root = singleEl;
|
|
673
|
+
offset = 1;
|
|
674
|
+
}
|
|
617
675
|
}
|
|
618
|
-
}
|
|
619
676
|
|
|
620
|
-
|
|
677
|
+
this.updatePaths(root, shell.paths, offset);
|
|
621
678
|
|
|
622
|
-
|
|
679
|
+
// Static web components can sometimes have children created via expressions.
|
|
680
|
+
// But calling applyExprs() will mess up the shell's path to them.
|
|
681
|
+
// So we find them first, then call activateStaticComponents() after their children have been created.
|
|
682
|
+
let staticComponents = this.findStaticComponents(root, shell, offset);
|
|
623
683
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
684
|
+
this.activateEmbeds(root, shell, offset);
|
|
685
|
+
|
|
686
|
+
// Apply exprs
|
|
687
|
+
this.applyExprs(template.exprs);
|
|
627
688
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
this.mapCallbacks = new Map();
|
|
689
|
+
this.instantiateStaticComponents(staticComponents);
|
|
690
|
+
}
|
|
631
691
|
}
|
|
632
692
|
}
|
|
633
693
|
|
|
@@ -649,7 +709,7 @@ function getSingleEl(fragment) {
|
|
|
649
709
|
* @param el {HTMLElement}
|
|
650
710
|
* @returns {boolean} */
|
|
651
711
|
function isReplaceEl(fragment, el) {
|
|
652
|
-
return
|
|
653
|
-
&&
|
|
712
|
+
return fragment.children.length===1
|
|
713
|
+
&& el.tagName.includes('-') // TODO: Check for solarite-placeholder attribute instead?
|
|
654
714
|
&& fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') === el.tagName;
|
|
655
715
|
}
|