solarite 0.1.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/build/build.bat +3 -0
- package/build/build.js +139 -0
- package/build/lib/rollup.min.js +11 -0
- package/build/lib/source-map.min.js +1 -0
- package/build/lib/terser.min.js +1 -0
- package/dist/Solarite-debug.js +4143 -0
- package/dist/Solarite.js +3740 -0
- package/dist/Solarite.min.js +4 -0
- package/docs/index.md +423 -0
- package/docs/js/Playground.js +184 -0
- package/docs/js/codemirror/codemirror6.js +32036 -0
- package/docs/js/codemirror/themeSolarIce.js +312 -0
- package/docs/js/documentation.js +32 -0
- package/docs/js/ui/CodeEditor.js +840 -0
- package/docs/js/ui/DarkToggle.js +52 -0
- package/docs/js/ui/FlexResizer.js +142 -0
- package/docs/js/util/Draggable2.js +151 -0
- package/docs/js/util/Errors.js +9 -0
- package/docs/js/util/Html.js +147 -0
- package/docs/js/util/Icons.js +623 -0
- package/docs/js/util/Input.js +253 -0
- package/docs/js/util/Util.js +88 -0
- package/docs/js/util/delve.js +43 -0
- package/docs/media/FiraCode400.woff2 +0 -0
- package/docs/media/cabin-latin-700.woff2 +0 -0
- package/docs/media/documentation.css +93 -0
- package/docs/media/eternium.css +1123 -0
- package/docs/media/solarite-machine.webp +0 -0
- package/index.html +325 -0
- package/package.json +33 -0
- package/readme.md +3 -0
- package/src/solarite/ExprPath.js +554 -0
- package/src/solarite/MultiValueMap.js +65 -0
- package/src/solarite/NodeGroup.js +706 -0
- package/src/solarite/NodeGroupManager.js +582 -0
- package/src/solarite/Shell.js +307 -0
- package/src/solarite/Solarite.js +19 -0
- package/src/solarite/Template.js +85 -0
- package/src/solarite/Util.js +264 -0
- package/src/solarite/createSolarite.js +267 -0
- package/src/solarite/getArg.js +99 -0
- package/src/solarite/hash.js +101 -0
- package/src/solarite/r.js +143 -0
- package/src/solarite/udomdiff.js +233 -0
- package/src/solarite/watch.js +302 -0
- package/src/solarite/watch2.js +439 -0
- 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/Perf.js +47 -0
- package/src/unused/Template.js +108 -0
- package/src/util/Errors.js +9 -0
- package/src/util/Util.js +88 -0
- package/src/util/delve.js +43 -0
- package/tests/Benchmark.test.js +319 -0
- package/tests/NodeGroup.test.js +115 -0
- package/tests/Shell.test.js +75 -0
- package/tests/Solarite.test.js +2896 -0
- package/tests/Testimony.js +602 -0
- package/tests/index.html +75 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import MultiValueMap from "./MultiValueMap.js";
|
|
2
|
+
import NodeGroup from "./NodeGroup.js";
|
|
3
|
+
import {getObjectHash} from "./hash.js";
|
|
4
|
+
import {serializePath} from "./watch.js";
|
|
5
|
+
|
|
6
|
+
import {assert} from "../util/Errors.js";
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} RenderOptions
|
|
11
|
+
* @property {boolean=} styles - Indicates whether the Courage component is present.
|
|
12
|
+
* @property {boolean=} scripts - Indicates whether the Power component is present.
|
|
13
|
+
* @property {boolean=} ids
|
|
14
|
+
*
|
|
15
|
+
* @property {?boolean} render
|
|
16
|
+
* Used only when options are given to a class super constructor inheriting from Solarite.
|
|
17
|
+
* True to call render() immediately in super constructor.
|
|
18
|
+
* False to automatically call render() at all.
|
|
19
|
+
* Undefined (default) to call render() when added to the DOM, unless already rendered.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Manage all the NodeGroups for a single WebComponent or root HTMLElement
|
|
25
|
+
* There's one NodeGroup for the root of the WebComponent, and one for every ${...} expression that creates Node children.
|
|
26
|
+
* And each NodeGroup manages the one or more nodes created by the expression.
|
|
27
|
+
*
|
|
28
|
+
* An instance of this class exists for each element that r() renders to. */
|
|
29
|
+
export default class NodeGroupManager {
|
|
30
|
+
|
|
31
|
+
/** @type {HTMLElement|DocumentFragment} */
|
|
32
|
+
rootEl;
|
|
33
|
+
|
|
34
|
+
/** @type {NodeGroup} */
|
|
35
|
+
rootNg;
|
|
36
|
+
|
|
37
|
+
/** @type {Change[]} */
|
|
38
|
+
changes = [];
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
//#IFDEV
|
|
43
|
+
modifications;
|
|
44
|
+
logDepth=0
|
|
45
|
+
//#ENDIF
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A map from the html strings and exprs that created a node group, to the NodeGroup.
|
|
49
|
+
* Also stores a map from just the html strings to the NodeGroup, so we can still find a similar match if the exprs changed.
|
|
50
|
+
*
|
|
51
|
+
* @type {MultiValueMap<string, (string|Template)[], NodeGroup>} */
|
|
52
|
+
nodeGroupsAvailable = new MultiValueMap();
|
|
53
|
+
nodeGroupsInUse = [];
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
/** @type {RenderOptions} */
|
|
57
|
+
options = {};
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
//#IFDEV
|
|
61
|
+
mutationWatcher;
|
|
62
|
+
mutationWatcherEnabled = true;
|
|
63
|
+
//#ENDIF
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param rootEl {HTMLElement|DocumentFragment} If not specified, the first element of the html will be the rootEl. */
|
|
67
|
+
constructor(rootEl=null) {
|
|
68
|
+
this.rootEl = rootEl;
|
|
69
|
+
/*
|
|
70
|
+
//#IFDEV
|
|
71
|
+
|
|
72
|
+
function closestCustomElement(node) {
|
|
73
|
+
do {
|
|
74
|
+
if (node.tagName && node.tagName.includes('-'))
|
|
75
|
+
return node;
|
|
76
|
+
} while (node = node.parentNode);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// TODO: Only trigger if we modify nodes inside an ExprPath.
|
|
80
|
+
// TODO: Enable this even when not in dev mode, because it's so useful for debugging?
|
|
81
|
+
// But it modifies the top level prototypes.
|
|
82
|
+
// TODO: Remove the onBeforeMutation callback when this.rootEl is not in the document.
|
|
83
|
+
// Because we won't get notified of document changes then anyway.
|
|
84
|
+
if (this.rootEl && this.rootEl.ownerDocument?.defaultView) { // TODO: Bind whenever we have rootEl.
|
|
85
|
+
this.mutationWatcher = MutationWatcher.getFromDocument(this.rootEl.ownerDocument);
|
|
86
|
+
this.mutationWatcher.onBeforeMutation.push((node, action, args) => {
|
|
87
|
+
|
|
88
|
+
// If a modification was made
|
|
89
|
+
if (this.mutationWatcherEnabled) {
|
|
90
|
+
if (this.rootEl.contains(node) && closestCustomElement(node) === this.rootEl) {
|
|
91
|
+
//console.log(node, action, args);
|
|
92
|
+
//throw new Error('DOM modification');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// If another DOM node steals one of ours by adding it to itself.
|
|
96
|
+
// TODO: append can use multiple arguments.
|
|
97
|
+
if (['insertBefore', 'append', 'appendChild'].includes(action)
|
|
98
|
+
&& this.rootEl.contains(args[0]) && closestCustomElement(args[0]) === this.rootEl) {
|
|
99
|
+
|
|
100
|
+
//console.log(node, action, args);
|
|
101
|
+
//throw new Error('Another element attempted to steal one of our nodes.');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
//#ENDIF
|
|
107
|
+
*/
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Render the main template, which may indirectly call renderTemplate() to create children.
|
|
112
|
+
* @param template {Template}
|
|
113
|
+
* @param options {RenderOptions}
|
|
114
|
+
* @return {?DocumentFragment} */
|
|
115
|
+
render(template, options={}) {
|
|
116
|
+
this.mutationWatcherEnabled = false;
|
|
117
|
+
this.options = options;
|
|
118
|
+
this.clearSubscribers = false;
|
|
119
|
+
|
|
120
|
+
//#IFDEV
|
|
121
|
+
this.modifications = {
|
|
122
|
+
created: [],
|
|
123
|
+
updated: [],
|
|
124
|
+
moved: [],
|
|
125
|
+
deleted: []
|
|
126
|
+
};
|
|
127
|
+
//#ENDIF
|
|
128
|
+
|
|
129
|
+
if (!template && template !== '') {
|
|
130
|
+
this.rootEl.outerHTML = '';
|
|
131
|
+
this.mutationWatcherEnabled = true;
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Fast path for empty component.
|
|
136
|
+
if (template.html?.length === 1 && !template.html[0]) {
|
|
137
|
+
this.rootEl.innerHTML = '';
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
|
|
141
|
+
// Find or create a NodeGroup for the template.
|
|
142
|
+
// This updates all nodes from the template.
|
|
143
|
+
let close;
|
|
144
|
+
let exact = this.getNodeGroup(template, true);
|
|
145
|
+
if (!exact) {
|
|
146
|
+
close = this.getNodeGroup(template, false);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
let firstTime = !this.rootNg;
|
|
151
|
+
this.rootNg = exact || close;
|
|
152
|
+
|
|
153
|
+
// Reparent NodeGroup
|
|
154
|
+
// TODO: Move this to NodeGroup?
|
|
155
|
+
let parent = this.rootNg.getParentNode()
|
|
156
|
+
if (!this.rootEl)
|
|
157
|
+
this.rootEl = parent;
|
|
158
|
+
|
|
159
|
+
// If this is the first time rendering this element.
|
|
160
|
+
else if (firstTime) {
|
|
161
|
+
|
|
162
|
+
// Save slot children
|
|
163
|
+
let fragment;
|
|
164
|
+
if (this.rootEl.childNodes.length) {
|
|
165
|
+
fragment = document.createDocumentFragment();
|
|
166
|
+
fragment.append(...this.rootEl.childNodes);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Add rendered elements.
|
|
170
|
+
if (parent instanceof DocumentFragment)
|
|
171
|
+
this.rootEl.append(parent);
|
|
172
|
+
else if (parent)
|
|
173
|
+
this.rootEl.append(...parent.childNodes)
|
|
174
|
+
|
|
175
|
+
// Apply slot children
|
|
176
|
+
if (fragment) {
|
|
177
|
+
for (let slot of this.rootEl.querySelectorAll('slot[name]')) {
|
|
178
|
+
let name = slot.getAttribute('name')
|
|
179
|
+
if (name)
|
|
180
|
+
slot.append(...fragment.querySelectorAll(`[slot='${name}']`))
|
|
181
|
+
}
|
|
182
|
+
let unamedSlot = this.rootEl.querySelector('slot:not([name])')
|
|
183
|
+
if (unamedSlot)
|
|
184
|
+
unamedSlot.append(fragment)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// this.rootNg was rendered as childrenOnly=true
|
|
190
|
+
// Apply attributes from a root element to the real root element.
|
|
191
|
+
let ng = this.rootNg;
|
|
192
|
+
if (ng.pseudoRoot && ng.pseudoRoot !== this.rootEl) {
|
|
193
|
+
/*#IFDEV*/assert(this.rootEl)/*#ENDIF*/
|
|
194
|
+
|
|
195
|
+
// Remove old attributes
|
|
196
|
+
// for (let attrib of this.rootEl.attributes)
|
|
197
|
+
// if (attrib.name !== 'is' && attrib.name !== 'data-style' && !ng.pseudoRoot.hasAttribute(attrib.name))
|
|
198
|
+
// this.rootEl.removeAttribute(attrib.name)
|
|
199
|
+
|
|
200
|
+
// Add/set new attributes
|
|
201
|
+
if (firstTime)
|
|
202
|
+
for (let attrib of ng.pseudoRoot.attributes)
|
|
203
|
+
if (!this.rootEl.hasAttribute(attrib.name))
|
|
204
|
+
this.rootEl.setAttribute(attrib.name, attrib.value);
|
|
205
|
+
|
|
206
|
+
// ng.startNode = ng.endNode = this.rootEl;
|
|
207
|
+
// ng.nodesCache = [ng.startNode]
|
|
208
|
+
// for (let path of ng.paths) {
|
|
209
|
+
// if (path.nodeMarker === ng.rootEl)
|
|
210
|
+
// path.nodeMarker = this.rootEl;
|
|
211
|
+
// path.nodesCache = null;
|
|
212
|
+
// /*#IFDEV*/assert(path.nodeBefore !== ng.rootEl)/*#ENDIF*/
|
|
213
|
+
// }
|
|
214
|
+
//
|
|
215
|
+
// ng.rootEl = this.rootEl;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/*#IFDEV*/this.rootNg.verify();/*#ENDIF*/
|
|
219
|
+
this.reset();
|
|
220
|
+
/*#IFDEV*/this.rootNg.verify();/*#ENDIF*/
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
this.mutationWatcherEnabled = true;
|
|
224
|
+
return this.rootEl;
|
|
225
|
+
//#IFDEV
|
|
226
|
+
//return this.modifications;
|
|
227
|
+
//#ENDIF
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
*
|
|
233
|
+
* 1. Delete a NodeGroup from this.nodeGroupsAvailable that matches this exactKey.
|
|
234
|
+
* 2. Then delete all of that NodeGroup's parents' exactKey entries
|
|
235
|
+
* We don't move them to in-use because we plucked the NodeGroup from them, they no longer match their exactKeys.
|
|
236
|
+
* 3. Then we move all the NodeGroup's exact+close keyed children to inUse because we don't want future calls
|
|
237
|
+
* to getNodeGroup() to borrow the children now that the whole NodeGroup is in-use.
|
|
238
|
+
*
|
|
239
|
+
* TODO: Have NodeGroups keep track of whether they're inUse.
|
|
240
|
+
* That way when we go up or down we don't have to remove those with .inUse===true
|
|
241
|
+
*
|
|
242
|
+
* @param exactKey
|
|
243
|
+
* @param goUp
|
|
244
|
+
* @param child
|
|
245
|
+
* @returns {?NodeGroup} */
|
|
246
|
+
findAndDeleteExact(exactKey, goUp=true, child=undefined) {
|
|
247
|
+
|
|
248
|
+
let ng = this.nodeGroupsAvailable.delete(exactKey, child);
|
|
249
|
+
if (ng) {
|
|
250
|
+
/*#IFDEV*/assert(ng.exactKey === exactKey);/*#ENDIF*/
|
|
251
|
+
|
|
252
|
+
// Mark close-key version as in-use.
|
|
253
|
+
let closeNg = this.nodeGroupsAvailable.delete(ng.closeKey, ng);
|
|
254
|
+
/*#IFDEV*/assert(closeNg);/*#ENDIF*/
|
|
255
|
+
|
|
256
|
+
// Mark our self as in-use.
|
|
257
|
+
this.nodeGroupsInUse.push(ng)
|
|
258
|
+
|
|
259
|
+
ng.inUse = true;
|
|
260
|
+
closeNg.inUse = true;
|
|
261
|
+
|
|
262
|
+
// Mark all parents that have this NodeGroup as a child as in-use.
|
|
263
|
+
// So that way we don't use this parent again
|
|
264
|
+
if (goUp) {
|
|
265
|
+
let ng2 = ng;
|
|
266
|
+
while (ng2 = ng2?.parentPath?.parentNg) {
|
|
267
|
+
if (!ng2.inUse) {
|
|
268
|
+
ng2.inUse = true;
|
|
269
|
+
let success = this.nodeGroupsAvailable.delete(ng2.exactKey, ng2);
|
|
270
|
+
// assert(success);
|
|
271
|
+
let success2 = this.nodeGroupsAvailable.delete(ng2.closeKey, ng2);
|
|
272
|
+
// assert(success);
|
|
273
|
+
/*#IFDEV*/assert(success === success2)/*#ENDIF*/
|
|
274
|
+
|
|
275
|
+
// console.log(getHtml(ng2))
|
|
276
|
+
if (success) {
|
|
277
|
+
this.nodeGroupsInUse.push(ng2)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Recurse to mark all child NodeGroups as in-use.
|
|
284
|
+
for (let path of ng.paths)
|
|
285
|
+
for (let childNg of path.nodeGroups) {
|
|
286
|
+
if (!childNg.inUse)
|
|
287
|
+
this.findAndDeleteExact(childNg.exactKey, false, childNg);
|
|
288
|
+
childNg.inUse = true;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (ng.parentPath) {
|
|
292
|
+
//ng.parentPath.clearNodesCache();
|
|
293
|
+
// ng.parentPath = null;
|
|
294
|
+
//ng.parentPath.removeNodeGroup(ng);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return ng;
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* @param closeKey {string}
|
|
304
|
+
* @param exactKey {string}
|
|
305
|
+
* @param goUp {boolean}
|
|
306
|
+
* @returns {NodeGroup} */
|
|
307
|
+
findAndDeleteClose(closeKey, exactKey, goUp=true) {
|
|
308
|
+
let ng = this.nodeGroupsAvailable.delete(closeKey);
|
|
309
|
+
if (ng) {
|
|
310
|
+
|
|
311
|
+
// We matched on a new key, so delete the old exactKey.
|
|
312
|
+
let exactNg = this.nodeGroupsAvailable.delete(ng.exactKey, ng);
|
|
313
|
+
|
|
314
|
+
/*#IFDEV*/assert(exactNg);/*#ENDIF*/
|
|
315
|
+
/*#IFDEV*/assert(ng === exactNg)/*#ENDIF*/
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
ng.inUse = true;
|
|
319
|
+
if (goUp) {
|
|
320
|
+
let ng2 = ng;
|
|
321
|
+
|
|
322
|
+
// We borrowed a node from another node group so make sure its parent isn't still an exact match.
|
|
323
|
+
while (ng2 = ng2?.parentPath?.parentNg) {
|
|
324
|
+
if (!ng2.inUse) {
|
|
325
|
+
ng2.inUse = true; // Might speed it up slightly?
|
|
326
|
+
let success = this.nodeGroupsAvailable.delete(ng2.exactKey, ng2);
|
|
327
|
+
/*#IFDEV*/assert(success);/*#ENDIF*/
|
|
328
|
+
|
|
329
|
+
// But it can still be a close match, so we don't use this code.
|
|
330
|
+
success = this.nodeGroupsAvailable.delete(ng2.closeKey, ng2);
|
|
331
|
+
/*#IFDEV*/assert(success);/*#ENDIF*/
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Recursively mark all child NodeGroups as in-use.
|
|
337
|
+
// We actually DON't want to do this becuse applyExprs is going to swap out the child NodeGroups
|
|
338
|
+
// and mark them as in-use as it goes.
|
|
339
|
+
// that's probably why uncommenting this causes tests to fail.
|
|
340
|
+
// for (let path of ng.paths)
|
|
341
|
+
// for (let childNg of path.nodeGroups)
|
|
342
|
+
// this.findAndDeleteExact(childNg.exactKey, false, childNg);
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
ng.exactKey = exactKey;
|
|
346
|
+
ng.closeKey = closeKey;
|
|
347
|
+
this.nodeGroupsInUse.push(ng)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
if (ng.parentPath) {
|
|
351
|
+
//ng.parentPath.clearNodesCache();
|
|
352
|
+
//ng.parentPath = null;
|
|
353
|
+
//ng.parentPath.removeNodeGroup(ng);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
return ng;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Get an existing or create a new NodeGroup that matches the template,
|
|
363
|
+
* but don't reparent it if it's somewhere else.
|
|
364
|
+
* @param template {Template}
|
|
365
|
+
* @param exact {?boolean}
|
|
366
|
+
* @param createForWatch
|
|
367
|
+
* @return {?NodeGroup} */
|
|
368
|
+
getNodeGroup(template, exact=null, createForWatch=false) {
|
|
369
|
+
|
|
370
|
+
let exactKey = getObjectHash(template)
|
|
371
|
+
|
|
372
|
+
/*#IFDEV*/if(NodeGroupManager.logEnabled) this.log(`Looking for ${exact ? 'exact' : 'close'} match: ` + template.debug)/*#ENDIF*/
|
|
373
|
+
|
|
374
|
+
// 1. Try to find an exact match.
|
|
375
|
+
let ng;
|
|
376
|
+
if (exact === true) {
|
|
377
|
+
ng = this.findAndDeleteExact(exactKey);
|
|
378
|
+
|
|
379
|
+
if (!ng) {
|
|
380
|
+
/*#IFDEV*/this.log(`Not found.`)/*#ENDIF*/
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
/*#IFDEV*/if(NodeGroupManager.logEnabled) this.log(`Found exact: ` + ng.debug)/*#ENDIF*/
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// 2. Try to find a close match.
|
|
387
|
+
else {
|
|
388
|
+
// We don't need to delete the exact match bc it's already been deleted in the prev pass.
|
|
389
|
+
let closeKey = template.getCloseKey();
|
|
390
|
+
ng = createForWatch ? null : this.findAndDeleteClose(closeKey, exactKey);
|
|
391
|
+
|
|
392
|
+
// 2. Update expression values if they've changed.
|
|
393
|
+
if (ng) {
|
|
394
|
+
|
|
395
|
+
// Temporary for debugging:
|
|
396
|
+
if (window.debug && !window.ng)
|
|
397
|
+
window.ng = ng;
|
|
398
|
+
|
|
399
|
+
/*#IFDEV*/if(NodeGroupManager.logEnabled) this.log(`Found close: ` + closeKey + ' ' + ng.debug)/*#ENDIF*/
|
|
400
|
+
/*#IFDEV*/this.incrementLogDepth(1);/*#ENDIF*/
|
|
401
|
+
/*#IFDEV*/ng.verify();/*#ENDIF*/
|
|
402
|
+
ng.applyExprs(template.exprs);
|
|
403
|
+
|
|
404
|
+
/*#IFDEV*/ng.verify()/*#ENDIF*/
|
|
405
|
+
/*#IFDEV*/this.incrementLogDepth(-1);/*#ENDIF*/
|
|
406
|
+
/*#IFDEV*/if(NodeGroupManager.logEnabled) this.log(`Updated close to: ` + ng.debug)/*#ENDIF*/
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// 3. Or if not found, create a new NodeGroup
|
|
410
|
+
else {
|
|
411
|
+
/*#IFDEV*/this.incrementLogDepth(1);/*#ENDIF*/
|
|
412
|
+
ng = new NodeGroup(template, this);
|
|
413
|
+
/*#IFDEV*/this.incrementLogDepth(-1);/*#ENDIF*/
|
|
414
|
+
|
|
415
|
+
//#IFDEV
|
|
416
|
+
this.modifications.created.push(...ng.getNodes())
|
|
417
|
+
//#ENDIF
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
// 4. Mark NodeGroup as being in-use.
|
|
421
|
+
// TODO: Moving from one group to another thrashes the gc. Is there a faster way?
|
|
422
|
+
// Could I have just a single WeakSet of those in use?
|
|
423
|
+
// Perhaps also result could cache its last exprKey and then we'd use only one map?
|
|
424
|
+
ng.exactKey = exactKey;
|
|
425
|
+
ng.closeKey = closeKey;
|
|
426
|
+
if (createForWatch) // TODO: Have this path be a separate function?
|
|
427
|
+
this.nodeGroupsAvailable.add(ng.exactKey, ng);
|
|
428
|
+
else
|
|
429
|
+
this.nodeGroupsInUse.push(ng)
|
|
430
|
+
|
|
431
|
+
/*#IFDEV*/if(NodeGroupManager.logEnabled) this.log(`Created new ` + ng.debug)/*#ENDIF*/
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// New!
|
|
436
|
+
// We clear the parent PathExpr's nodesCache when we remove ourselves from it.
|
|
437
|
+
// Benchmarking shows this doesn't slow down the partialUpdate benchmark.
|
|
438
|
+
if (ng.parentPath) {
|
|
439
|
+
// ng.parentPath.clearNodesCache(); // Makes partialUpdate benchmark 10x slower!
|
|
440
|
+
ng.parentPath = null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
/*#IFDEV*/ng.verify()/*#ENDIF*/
|
|
445
|
+
|
|
446
|
+
return ng;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
reset() {
|
|
450
|
+
//this.changes = [];
|
|
451
|
+
let available = this.nodeGroupsAvailable
|
|
452
|
+
for (let ng of this.nodeGroupsInUse) {
|
|
453
|
+
ng.inUse = false;
|
|
454
|
+
available.add(ng.exactKey, ng)
|
|
455
|
+
available.add(ng.closeKey, ng)
|
|
456
|
+
}
|
|
457
|
+
this.nodeGroupsInUse = [];
|
|
458
|
+
|
|
459
|
+
// Used for watches
|
|
460
|
+
this.changes = [];
|
|
461
|
+
|
|
462
|
+
/*#IFDEV*/this.log('----------------------')/*#ENDIF*/
|
|
463
|
+
// TODO: free the memory from any nodeGroupsAvailable() after render is done, since they weren't used?
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
// deprecated
|
|
468
|
+
//pathToLoopInfo = new MultiValueMap(); // uses a Set() for each value.
|
|
469
|
+
clearSubscribers = false;
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* One path may be used to loop in more than one place, so we use this to get every anchor from each loop.
|
|
473
|
+
* @param path {Array}
|
|
474
|
+
* @return {LoopInfo[]} A function that gets the loop anchor NodeGroup */
|
|
475
|
+
getLoopInfo(path) {
|
|
476
|
+
let serializedArrayPath = serializePath(path);
|
|
477
|
+
return [...this.pathToLoopInfo.getAll(serializedArrayPath)]; // This is set inside forEach()
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* @deprecated
|
|
483
|
+
* Store the functions used to create items for each loop.
|
|
484
|
+
* TODO: Can this be combined with pathToTemplates?
|
|
485
|
+
* @type {MultiValueMap<string, Subscriber>} */
|
|
486
|
+
pathToLoopInfo = new MultiValueMap();
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Maps variable paths to the templates used to create NodeGroups
|
|
490
|
+
* @type {MultiValueMap<string, Subscriber>} */
|
|
491
|
+
subscribers = new MultiValueMap();
|
|
492
|
+
|
|
493
|
+
clearSubscribersIfNeeded() {
|
|
494
|
+
if (this.clearSubscribers) {
|
|
495
|
+
this.pathToLoopInfo = new MultiValueMap();
|
|
496
|
+
this.subscribers = new MultiValueMap();
|
|
497
|
+
this.clearSubscribers = false;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Get the NodeGroupManager for a Web Component.
|
|
504
|
+
* @param rootEl {Solarite|HTMLElement}
|
|
505
|
+
* @return {NodeGroupManager} */
|
|
506
|
+
static get(rootEl) {
|
|
507
|
+
let ngm = nodeGroupManagers.get(rootEl);
|
|
508
|
+
if (!ngm) {
|
|
509
|
+
ngm = new NodeGroupManager(rootEl);
|
|
510
|
+
nodeGroupManagers.set(rootEl, ngm);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return ngm;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
//#IFDEV
|
|
518
|
+
static logEnabled = false;
|
|
519
|
+
incrementLogDepth(level) {
|
|
520
|
+
this.logDepth += level;
|
|
521
|
+
}
|
|
522
|
+
log(msg, level=0) {
|
|
523
|
+
this.logDepth += level;
|
|
524
|
+
if (NodeGroupManager.logEnabled) {
|
|
525
|
+
let indent = ' '.repeat(this.logDepth);
|
|
526
|
+
console.log(indent + msg);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* @returns {NodeGroup[]} */
|
|
532
|
+
getAllAvailableGroups() {
|
|
533
|
+
let result = new Set();
|
|
534
|
+
for (let values of Object.values(this.nodeGroupsAvailable.data))
|
|
535
|
+
result.add(...values)
|
|
536
|
+
return [...result];
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
verify() {
|
|
540
|
+
if (!window.verify)
|
|
541
|
+
return;
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
let findCloseMatch = item => {
|
|
545
|
+
let names = this.nodeGroupsAvailable.hasValue(item);
|
|
546
|
+
for (let name of names)
|
|
547
|
+
if (name.startsWith('@'))
|
|
548
|
+
return true;
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Check to make sure every exact match is also in close matches.
|
|
553
|
+
for (let name in this.nodeGroupsAvailable)
|
|
554
|
+
if (!name.startsWith('@)'))
|
|
555
|
+
for (let item of this.nodeGroupsAvailable.getAll(name))
|
|
556
|
+
assert(findCloseMatch(item))
|
|
557
|
+
|
|
558
|
+
// Recursively traverse through all node Groups
|
|
559
|
+
if (this.rootNg)
|
|
560
|
+
this.rootNg.verify();
|
|
561
|
+
|
|
562
|
+
for (let ng of this.getAllAvailableGroups())
|
|
563
|
+
ng.verify();
|
|
564
|
+
}
|
|
565
|
+
//#ENDIF
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
NodeGroupManager.pendingChildren = [];
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Each Element that has Expr children has an associated NodeGroupManager here.
|
|
572
|
+
* @type {WeakMap<HTMLElement, NodeGroupManager>} */
|
|
573
|
+
let nodeGroupManagers = new WeakMap();
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
export class LoopInfo {
|
|
578
|
+
constructor(loopTemplate, itemTransformer) {
|
|
579
|
+
this.template = loopTemplate
|
|
580
|
+
this.itemTransformer = itemTransformer;
|
|
581
|
+
}
|
|
582
|
+
}
|