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
package/dist/Solarite.js
ADDED
|
@@ -0,0 +1,3740 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Array|function(...*)} Callbacks
|
|
3
|
+
* @property {function(function)} push
|
|
4
|
+
* @property {function()} remove
|
|
5
|
+
* @property {function()} pause
|
|
6
|
+
* @property {function()} resume
|
|
7
|
+
* */
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A place for functions that have no other home. */
|
|
12
|
+
var Util$1 = {
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create an array-like object that stores a group of callbacks.
|
|
16
|
+
* Supports all array functions and properties like push() and .length.
|
|
17
|
+
* Can be called directly.
|
|
18
|
+
*
|
|
19
|
+
* @param functions {function[]}
|
|
20
|
+
* @return {Callbacks|function}
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* var c = Util.callback();
|
|
24
|
+
* var f = () => console.log(3);
|
|
25
|
+
* c.push(f);
|
|
26
|
+
* c();
|
|
27
|
+
* c.remove(f);
|
|
28
|
+
* c();
|
|
29
|
+
*/
|
|
30
|
+
callback(...functions) {
|
|
31
|
+
var paused = false;
|
|
32
|
+
|
|
33
|
+
// Make it callable. When we call it, call all callbacks() with the given args.
|
|
34
|
+
let result = async function(...args) {
|
|
35
|
+
let result2 = [];
|
|
36
|
+
if (!paused)
|
|
37
|
+
for (let i=0; i<result.length; i++)
|
|
38
|
+
result2.push(result[i](...args));
|
|
39
|
+
return await Promise.all(result2);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Make it iterable.
|
|
43
|
+
result[Symbol.iterator] = function() {
|
|
44
|
+
let index = 0;
|
|
45
|
+
return {
|
|
46
|
+
next: () => index < result.length
|
|
47
|
+
? {value: result[index++], done: false}
|
|
48
|
+
: {done: true}
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Use properties from Array
|
|
53
|
+
for (let prop of Object.getOwnPropertyNames(Array.prototype))
|
|
54
|
+
if (prop !== 'length' && prop !== 'constructor')
|
|
55
|
+
result[prop] = Array.prototype[prop];
|
|
56
|
+
|
|
57
|
+
result.l = 0; // Internal length
|
|
58
|
+
Object.defineProperty(result, 'length', {
|
|
59
|
+
get() { return result.l },
|
|
60
|
+
set(val) { result.l = val;}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Add the remove() function.
|
|
64
|
+
result.remove = func => {
|
|
65
|
+
let idx = result.findIndex(item => item === func);
|
|
66
|
+
if (idx !== -1)
|
|
67
|
+
result.splice(idx, 1);
|
|
68
|
+
};
|
|
69
|
+
result.pause = () => paused = true;
|
|
70
|
+
|
|
71
|
+
result.resume = () => paused = false;
|
|
72
|
+
|
|
73
|
+
// Add initial functions
|
|
74
|
+
for (let f of functions)
|
|
75
|
+
result.push(f);
|
|
76
|
+
|
|
77
|
+
return result;
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Follow a path into an object.
|
|
88
|
+
* @param obj {object}
|
|
89
|
+
* @param path {string[]}
|
|
90
|
+
* @param createVal {*} If set, non-existant paths will be created and value at path will be set to createVal.
|
|
91
|
+
* @return {*} The value, or undefined if it can't be reached. */
|
|
92
|
+
function delve(obj, path, createVal = delveDontCreate) {
|
|
93
|
+
let isCreate = createVal !== delveDontCreate;
|
|
94
|
+
|
|
95
|
+
let len = path.length;
|
|
96
|
+
if (!obj && !isCreate && len)
|
|
97
|
+
return undefined;
|
|
98
|
+
|
|
99
|
+
let i = 0;
|
|
100
|
+
for (let srcProp of path) {
|
|
101
|
+
|
|
102
|
+
// If the path is undefined and we're not to the end yet:
|
|
103
|
+
if (obj[srcProp] === undefined) {
|
|
104
|
+
|
|
105
|
+
// If the next index is an integer or integer string.
|
|
106
|
+
if (isCreate) {
|
|
107
|
+
if (i < len - 1) {
|
|
108
|
+
// If next level path is a number, create as an array
|
|
109
|
+
let isArray = (path[i + 1] + '').match(/^\d+$/);
|
|
110
|
+
obj[srcProp] = isArray ? [] : {};
|
|
111
|
+
}
|
|
112
|
+
} else
|
|
113
|
+
return undefined; // can't traverse
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// If last item in path
|
|
117
|
+
if (isCreate && i === len - 1)
|
|
118
|
+
obj[srcProp] = createVal;
|
|
119
|
+
|
|
120
|
+
// Traverse deeper along destination object.
|
|
121
|
+
obj = obj[srcProp];
|
|
122
|
+
i++;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return obj;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let delveDontCreate = {};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* There are three ways to create an instance of a Solarite Component:
|
|
132
|
+
* 1. new ComponentName(); // direct class instantiation
|
|
133
|
+
* 2. this.html = r`<div><component-name></component-name></div>; // as a child of another RedComponent.
|
|
134
|
+
* 3. <body><component-name></component-name></body> // in the Document html.
|
|
135
|
+
*
|
|
136
|
+
* When created via #3, Solarite has no way to pass attributes as arguments to the constructor. So to make
|
|
137
|
+
* sure we get the correct value via all three paths, we write our constructors according to the following
|
|
138
|
+
* example. Note that constructor args are embedded in an object, and must be all lower-case because
|
|
139
|
+
* Browsers make all html attribute names lowercase.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* constructor({name, userid=1}={}) {
|
|
143
|
+
* super();
|
|
144
|
+
*
|
|
145
|
+
* // Get value from "name" attriute if persent, otherwise from name constructor arg.
|
|
146
|
+
* this.name = getArg(this, 'name', name);
|
|
147
|
+
*
|
|
148
|
+
* // Optionally convert the value to an integer.
|
|
149
|
+
* this.userId = getArg(this, 'userid', userid, ArgType.Int);
|
|
150
|
+
* }
|
|
151
|
+
*
|
|
152
|
+
* @param el {HTMLElement}
|
|
153
|
+
* @param name {string} Attribute name. Not case-sensitive.
|
|
154
|
+
* @param val {*} Default value to use if attribute doesn't exist.
|
|
155
|
+
* @param type {ArgType|function|*[]}
|
|
156
|
+
* If an array, use the value if it's in the array, otherwise return undefined.
|
|
157
|
+
* If it's a function, pass the value to the function and return the result.
|
|
158
|
+
* @return {*} */
|
|
159
|
+
function getArg(el, name, val=null, type=ArgType.String) {
|
|
160
|
+
let attrVal = el.getAttribute(name);
|
|
161
|
+
if (attrVal !== null) // If attribute doesn't exist.
|
|
162
|
+
val = attrVal;
|
|
163
|
+
|
|
164
|
+
if (Array.isArray(type))
|
|
165
|
+
return type.includes(val) ? val : undefined;
|
|
166
|
+
|
|
167
|
+
if (typeof type === 'function')
|
|
168
|
+
return type(val);
|
|
169
|
+
|
|
170
|
+
// If bool, it's true as long as it exists and its value isn't falsey.
|
|
171
|
+
if (type===ArgType.Bool) {
|
|
172
|
+
let lAttrVal = typeof val === 'string' ? val.toLowerCase() : val;
|
|
173
|
+
return !['false', '0', false, 0, null, undefined].includes(lAttrVal);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Attribute doesn't exist
|
|
177
|
+
switch (type) {
|
|
178
|
+
case ArgType.Int:
|
|
179
|
+
return parseInt(val);
|
|
180
|
+
case ArgType.Float:
|
|
181
|
+
return parseFloat(val);
|
|
182
|
+
case ArgType.String:
|
|
183
|
+
return val || '';
|
|
184
|
+
case ArgType.JSON:
|
|
185
|
+
try {
|
|
186
|
+
return JSON.parse(val);
|
|
187
|
+
} catch (e) {
|
|
188
|
+
return val;
|
|
189
|
+
}
|
|
190
|
+
case ArgType.Eval:
|
|
191
|
+
try {
|
|
192
|
+
return eval(`(${val})`);
|
|
193
|
+
} catch (e) {
|
|
194
|
+
return val;
|
|
195
|
+
}
|
|
196
|
+
default:
|
|
197
|
+
return val;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* @enum */
|
|
203
|
+
var ArgType = {
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* false, 0, null, undefined, '0', and 'false' (case-insensitive) become false.
|
|
207
|
+
* Anything else, including empty string becomes true.
|
|
208
|
+
* Empty string is true because attributes with no value should be evaulated as true. */
|
|
209
|
+
Bool: 'Bool',
|
|
210
|
+
|
|
211
|
+
Int: 'Int',
|
|
212
|
+
Float: 'Float',
|
|
213
|
+
String: 'String',
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Parse the string value as JSON.
|
|
217
|
+
* If it's not parsable, return the value as a string. */
|
|
218
|
+
JSON: 'JSON',
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Evaluate the string as JavaScript using the eval() function.
|
|
222
|
+
* If it can't be evaluated, return the original string. */
|
|
223
|
+
Eval: 'Eval'
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
let lastObjectId = 1>>>0; // Is a 32-bit int faster to increment than JavaScript's Number, which is a 64-bit float?
|
|
227
|
+
let objectIds = new WeakMap();
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @param obj {Object|string|Node}
|
|
231
|
+
* @param prefix
|
|
232
|
+
* @returns {string} */
|
|
233
|
+
function getObjectId(obj, prefix=null) {
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
prefix = prefix || '~\f';
|
|
240
|
+
|
|
241
|
+
// if (typeof obj === 'function')
|
|
242
|
+
// return obj.toString();
|
|
243
|
+
|
|
244
|
+
let result = objectIds.get(obj);
|
|
245
|
+
if (!result) { // convert to string, store in result, then add 1 to lastObjectId.
|
|
246
|
+
result = prefix+(lastObjectId++); // We use a unique prefix to ensure it doesn't collide w/ strings not from getObjectId()
|
|
247
|
+
objectIds.set(obj, result);
|
|
248
|
+
}
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Control how JSON.stringify() handles Nodes and Functions.
|
|
254
|
+
* Normally, we'd pass a replacer() function argument to JSON.stringify() to handle Nodes and Functions.
|
|
255
|
+
* But that makes JSON.stringify() take twice as long to run.
|
|
256
|
+
* Adding a toJSON method globally on these object prototypes doesn't incur that performance penalty. */
|
|
257
|
+
let isHashing = true;
|
|
258
|
+
function toJSON() {
|
|
259
|
+
//return (isHashing && !Array.isArray(this)) ? getObjectId(this) : this
|
|
260
|
+
return isHashing ? getObjectId(this) : this
|
|
261
|
+
}
|
|
262
|
+
// Node.prototype.toJSON = toJSON;
|
|
263
|
+
// Function.prototype.toJSON = toJSON;
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Get a string that uniquely maps to the values of the given object.
|
|
268
|
+
* If a value in obj changes, calling getObjectHash(obj) will then return a different hash.
|
|
269
|
+
* This is used by NodeGroupManager to create a hash that represents the current values of a NodeGroup.
|
|
270
|
+
*
|
|
271
|
+
* Relies on the Node and Function prototypes being overridden above.
|
|
272
|
+
*
|
|
273
|
+
* @param obj {*}
|
|
274
|
+
* @returns {string} */
|
|
275
|
+
function getObjectHash(obj) {
|
|
276
|
+
|
|
277
|
+
// Sometimes these get unassigned by Chrome and Brave 119, as well as Firefox, seemingly randomly!
|
|
278
|
+
// The same tests sometimes pass, sometimes fail, even after browser and OS restarts.
|
|
279
|
+
// So we check the assignments on every run of getObjectHash()
|
|
280
|
+
if (Node.prototype.toJSON !== toJSON) {
|
|
281
|
+
Node.prototype.toJSON = toJSON;
|
|
282
|
+
if (Function.prototype.toJSON !== toJSON) // Will it only unmap one but not the other?
|
|
283
|
+
Function.prototype.toJSON = toJSON;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let result;
|
|
287
|
+
isHashing = true;
|
|
288
|
+
try {
|
|
289
|
+
result = JSON.stringify(obj);
|
|
290
|
+
}
|
|
291
|
+
catch(e){
|
|
292
|
+
result = getObjectHashCircular(obj);
|
|
293
|
+
}
|
|
294
|
+
isHashing = false;
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Having this separate might help the optimzer for getObjectHash() ?
|
|
300
|
+
* @param obj
|
|
301
|
+
* @returns {string} */
|
|
302
|
+
function getObjectHashCircular(obj) {
|
|
303
|
+
|
|
304
|
+
//console.log('circular')
|
|
305
|
+
// Slower version that handles circular references.
|
|
306
|
+
// Just adding any callback at all, even one that just returns the value, makes JSON.stringify() twice as slow.
|
|
307
|
+
const seen = new Set();
|
|
308
|
+
return JSON.stringify(obj, (key, value) => {
|
|
309
|
+
if (typeof value === 'object' && value !== null) {
|
|
310
|
+
if (seen.has(value))
|
|
311
|
+
return getObjectId(value);
|
|
312
|
+
seen.add(value);
|
|
313
|
+
}
|
|
314
|
+
return value;
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
class MultiValueMap {
|
|
319
|
+
|
|
320
|
+
/** @type {Object<string, Set>} */
|
|
321
|
+
data = {};
|
|
322
|
+
|
|
323
|
+
// Set a new value for a key
|
|
324
|
+
add(key, value) {
|
|
325
|
+
let data = this.data;
|
|
326
|
+
let set = data[key];
|
|
327
|
+
if (!set) {
|
|
328
|
+
set = new Set();
|
|
329
|
+
data[key] = set;
|
|
330
|
+
}
|
|
331
|
+
set.add(value);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Get all values for a key
|
|
335
|
+
getAll(key) {
|
|
336
|
+
return this.data[key] || [];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Remove one value from a key, and return it
|
|
340
|
+
delete(key, val=undefined) {
|
|
341
|
+
// if (key === '["Html2",[[["Html3",["F1","A"]],["Html3",["F1","B"]]]]]')
|
|
342
|
+
// debugger;
|
|
343
|
+
|
|
344
|
+
let data = this.data;
|
|
345
|
+
// The partialUpdate benchmark shows having this check first makes the function slightly faster.
|
|
346
|
+
// if (!data.hasOwnProperty(key))
|
|
347
|
+
// return undefined;
|
|
348
|
+
|
|
349
|
+
// Delete a specific value.
|
|
350
|
+
let result;
|
|
351
|
+
let set = data[key];
|
|
352
|
+
if (!set) // slower than pre-check.
|
|
353
|
+
return undefined;
|
|
354
|
+
|
|
355
|
+
if (val !== undefined) {
|
|
356
|
+
set.delete(val);
|
|
357
|
+
result = val;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Delete any value.
|
|
361
|
+
else {
|
|
362
|
+
result = set.values().next().value;
|
|
363
|
+
// [result] = set; // Does the same as above. is about the same speed?
|
|
364
|
+
set.delete(result);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// TODO: Will this make it slower?
|
|
368
|
+
if (set.size === 0)
|
|
369
|
+
delete data[key];
|
|
370
|
+
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
hasValue(val) {
|
|
375
|
+
let data = this.data;
|
|
376
|
+
let names = [];
|
|
377
|
+
for (let name in data)
|
|
378
|
+
if (data[name].has(val)) // TODO: iterate twice to pre-size array?
|
|
379
|
+
names.push(name);
|
|
380
|
+
return names;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
let Util = {
|
|
385
|
+
|
|
386
|
+
bindStyles(style, root) {
|
|
387
|
+
let styleId = root.getAttribute('data-style');
|
|
388
|
+
if (!styleId) {
|
|
389
|
+
// Keep track of one style id for each class.
|
|
390
|
+
// TODO: Put this outside the class in a map, so it doesn't conflict with static properties.
|
|
391
|
+
if (!root.constructor.styleId)
|
|
392
|
+
root.constructor.styleId = 1;
|
|
393
|
+
styleId = root.constructor.styleId++;
|
|
394
|
+
|
|
395
|
+
root.setAttribute('data-style', styleId);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
let tagName = root.tagName.toLowerCase();
|
|
399
|
+
for (let child of style.childNodes) {
|
|
400
|
+
if (child.nodeType === 3) {
|
|
401
|
+
let oldText = child.textContent;
|
|
402
|
+
let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, tagName + '[data-style="' + styleId + '"]');
|
|
403
|
+
if (oldText !== newText)
|
|
404
|
+
child.textContent = newText;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
let div = document.createElement('div');
|
|
415
|
+
|
|
416
|
+
let isEvent = attrName => attrName.startsWith('on') && attrName in div;
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Convert a Proper Case name to a name with dashes.
|
|
421
|
+
* Dashes will be placed between letters and numbers.
|
|
422
|
+
* If there are multiple consecutive capital letters followed by another chracater, a dash will be placed before the last capital letter.
|
|
423
|
+
* @param str {string}
|
|
424
|
+
* @return {string}
|
|
425
|
+
*
|
|
426
|
+
* @example
|
|
427
|
+
* 'ProperName' => 'proper-name'
|
|
428
|
+
* 'HTMLElement' => 'html-element'
|
|
429
|
+
* 'BigUI' => 'big-ui'
|
|
430
|
+
* 'UIForm' => 'ui-form'
|
|
431
|
+
* 'A100' => 'a-100' */
|
|
432
|
+
function camelToDashes(str) {
|
|
433
|
+
// Convert any capital letter that is preceded by a lowercase letter or number to lowercase and precede with a dash.
|
|
434
|
+
str = str.replace(/([a-z0-9])([A-Z])/g, '$1-$2');
|
|
435
|
+
|
|
436
|
+
// Convert any capital letter that is followed by a lowercase letter or number to lowercase and precede with a dash.
|
|
437
|
+
str = str.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2');
|
|
438
|
+
|
|
439
|
+
// Convert any number that is preceded by a lowercase or uppercase letter to be preceded by a dash.
|
|
440
|
+
str = str.replace(/([a-zA-Z])([0-9])/g, '$1-$2');
|
|
441
|
+
|
|
442
|
+
// Convert all the remaining capital letters to lowercase.
|
|
443
|
+
return str.toLowerCase();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Returns false if they're the same. Or the first index where they differ.
|
|
452
|
+
* @param a
|
|
453
|
+
* @param b
|
|
454
|
+
* @returns {int|false} */
|
|
455
|
+
function findArrayDiff(a, b) {
|
|
456
|
+
if (a.length !== b.length)
|
|
457
|
+
return -1;
|
|
458
|
+
let aLength = a.length;
|
|
459
|
+
for (let i=0; i<aLength; i++)
|
|
460
|
+
if (a[i] !== b[i])
|
|
461
|
+
return i;
|
|
462
|
+
return false; // the same.
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* TODO: Turn this into a class because it has internal state.
|
|
468
|
+
* TODO: Don't break on 3<a inside a <script> or <style> tag.
|
|
469
|
+
* @param html {?string} Pass null to reset context.
|
|
470
|
+
* @returns {string} */
|
|
471
|
+
function htmlContext(html) {
|
|
472
|
+
if (html === null) {
|
|
473
|
+
state = {...defaultState};
|
|
474
|
+
return state.context;
|
|
475
|
+
}
|
|
476
|
+
for (let i = 0; i < html.length; i++) {
|
|
477
|
+
const char = html[i];
|
|
478
|
+
switch (state.context) {
|
|
479
|
+
case htmlContext.Text:
|
|
480
|
+
if (char === '<' && html[i+1].match(/[a-z!]/i)) { // Start of a tag or comment.
|
|
481
|
+
// if (html.slice(i, i+4) === '<!--')
|
|
482
|
+
// state.context = htmlContext.Comment;
|
|
483
|
+
// else
|
|
484
|
+
state.context = htmlContext.Tag;
|
|
485
|
+
state.buffer = '';
|
|
486
|
+
}
|
|
487
|
+
break;
|
|
488
|
+
case htmlContext.Tag:
|
|
489
|
+
if (char === '>') {
|
|
490
|
+
state.context = htmlContext.Text;
|
|
491
|
+
state.quote = null;
|
|
492
|
+
state.buffer = '';
|
|
493
|
+
} else if (char === ' ' && !state.buffer) {
|
|
494
|
+
// No attribute name is present. Skipping the space.
|
|
495
|
+
continue;
|
|
496
|
+
} else if (char === ' ' || char === '/' || char === '?') {
|
|
497
|
+
state.buffer = ''; // Reset the buffer when a delimiter or potential self-closing sign is found.
|
|
498
|
+
} else if (char === '"' || char === "'" || char === '=') {
|
|
499
|
+
state.context = htmlContext.Attribute;
|
|
500
|
+
state.quote = char === '=' ? null : char;
|
|
501
|
+
state.buffer = '';
|
|
502
|
+
} else {
|
|
503
|
+
state.buffer += char;
|
|
504
|
+
}
|
|
505
|
+
break;
|
|
506
|
+
case htmlContext.Attribute:
|
|
507
|
+
if (!state.quote && !state.buffer.length && (char === '"' || char === "'"))
|
|
508
|
+
state.quote = char;
|
|
509
|
+
|
|
510
|
+
else if (char === state.quote || (!state.quote && state.buffer.length)) {
|
|
511
|
+
state.context = htmlContext.Tag;
|
|
512
|
+
state.quote = null;
|
|
513
|
+
state.buffer = '';
|
|
514
|
+
} else if (!state.quote && char === '>') {
|
|
515
|
+
state.context = htmlContext.Text;
|
|
516
|
+
state.quote = null;
|
|
517
|
+
state.buffer = '';
|
|
518
|
+
} else if (char !== ' ') {
|
|
519
|
+
state.buffer += char;
|
|
520
|
+
}
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
}
|
|
525
|
+
return state.context;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
htmlContext.Attribute = 'Attribute';
|
|
530
|
+
htmlContext.Text = 'Text';
|
|
531
|
+
htmlContext.Tag = 'Tag';
|
|
532
|
+
//htmlContext.Comment = 'Comment';
|
|
533
|
+
let defaultState = {
|
|
534
|
+
context: htmlContext.Text, // possible values: 'TEXT', 'TAG', 'ATTRIBUTE'
|
|
535
|
+
quote: null, // possible values: null, '"', "'"
|
|
536
|
+
buffer: '',
|
|
537
|
+
lastChar: null
|
|
538
|
+
};
|
|
539
|
+
let state = {...defaultState};
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
// For debugging only
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Path to where an expression should be evaluated within a Shell.
|
|
547
|
+
* Path is only valid until the expressions before it are evaluated.
|
|
548
|
+
* TODO: Make this based on parent and node instead of path? */
|
|
549
|
+
class ExprPath {
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* @type {PathType} */
|
|
553
|
+
type;
|
|
554
|
+
|
|
555
|
+
// Used for attributes:
|
|
556
|
+
|
|
557
|
+
/** @type {?string} Used only if type=AttribType.Value. */
|
|
558
|
+
attrName;
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* @type {?string[]} Used only if type=AttribType.Value. If null, use one expr to set the whole attribute value. */
|
|
562
|
+
attrValue;
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* @type {Set<string>} Used for type=AttribType.Multiple to remember the attributes that were added. */
|
|
566
|
+
attrNames;
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* @type {Node} Node that occurs before this ExprPath's first Node.
|
|
572
|
+
* This is necessary because udomdiff() can steal nodes from another ExprPath.
|
|
573
|
+
* If we had a pointer to our own startNode then that node could be moved somewhere else w/o us knowing it.
|
|
574
|
+
* Used only for type='content'
|
|
575
|
+
* Will be null if ExprPath has no Nodes. */
|
|
576
|
+
nodeBefore;
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* If type is AttribType.Multiple or AttribType.Value, points to the node having the attribute.
|
|
580
|
+
* If type is 'content', points to a node that never changes that this NodeGroup should always insert its nodes before.
|
|
581
|
+
* An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.
|
|
582
|
+
* @type {Node|HTMLElement} */
|
|
583
|
+
nodeMarker;
|
|
584
|
+
|
|
585
|
+
/** @deprecated */
|
|
586
|
+
get parentNode() {
|
|
587
|
+
return this.nodeMarker.parentNode;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// These are set after an expression is assigned:
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
/** @type {NodeGroup} */
|
|
594
|
+
parentNg;
|
|
595
|
+
|
|
596
|
+
/** @type {NodeGroup[]} */
|
|
597
|
+
nodeGroups = [];
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
// Caches to make things faster
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* @private
|
|
606
|
+
* @type {Node[]} Cached result of getNodes() */
|
|
607
|
+
nodesCache;
|
|
608
|
+
|
|
609
|
+
// What are these?
|
|
610
|
+
nodeBeforeIndex;
|
|
611
|
+
nodeMarkerPath;
|
|
612
|
+
|
|
613
|
+
// TODO: Keep this cached?
|
|
614
|
+
expr;
|
|
615
|
+
|
|
616
|
+
// for debugging
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* @param nodeBefore {Node}
|
|
621
|
+
* @param nodeMarker {?Node}
|
|
622
|
+
* @param type {string}
|
|
623
|
+
* @param attrName {?string}
|
|
624
|
+
* @param attrValue {string[]} */
|
|
625
|
+
constructor(nodeBefore, nodeMarker, type=PathType.Content, attrName=null, attrValue=null) {
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
// If path is a node.
|
|
630
|
+
this.nodeBefore = nodeBefore;
|
|
631
|
+
this.nodeMarker = nodeMarker;
|
|
632
|
+
this.type = type;
|
|
633
|
+
this.attrName = attrName;
|
|
634
|
+
this.attrValue = attrValue;
|
|
635
|
+
if (type === PathType.Multiple)
|
|
636
|
+
this.attrNames = new Set();
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
applyMultipleAttribs(node, expr) {
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
if (Array.isArray(expr))
|
|
643
|
+
expr = expr.flat().join(' '); // flat and join so we can accept arrays of arrays of strings.
|
|
644
|
+
|
|
645
|
+
// Add new attributes
|
|
646
|
+
let oldNames = this.attrNames;
|
|
647
|
+
this.attrNames = new Set();
|
|
648
|
+
if (expr) {
|
|
649
|
+
let attrs = (expr +'') // Split string into multiple attributes.
|
|
650
|
+
.split(/([\w-]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s]+))/g)
|
|
651
|
+
.map(text => text.trim())
|
|
652
|
+
.filter(text => text.length);
|
|
653
|
+
|
|
654
|
+
for (let attr of attrs) {
|
|
655
|
+
let [name, value] = attr.split(/\s*=\s*/); // split on first equals.
|
|
656
|
+
value = (value || '').replace(/^(['"])(.*)\1$/, '$2'); // trim value quotes if they match.
|
|
657
|
+
node.setAttribute(name, value);
|
|
658
|
+
this.attrNames.add(name);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Remove old attributes.
|
|
663
|
+
for (let oldName of oldNames)
|
|
664
|
+
if (!this.attrNames.has(oldName))
|
|
665
|
+
node.removeAttribute(oldName);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Handle attributes for event binding, such as:
|
|
670
|
+
* onclick=${(e, el) => this.doSomething(el, 'meow')}
|
|
671
|
+
* onclick=${[this.doSomething, 'meow']}
|
|
672
|
+
* onclick=${[this, 'doSomething', 'meow']}
|
|
673
|
+
*
|
|
674
|
+
* @param node
|
|
675
|
+
* @param expr
|
|
676
|
+
* @param root */
|
|
677
|
+
applyEventAttrib(node, expr, root) {
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
let eventName = this.attrName.slice(2);
|
|
681
|
+
let func;
|
|
682
|
+
|
|
683
|
+
// Convert array to function.
|
|
684
|
+
// TODO: This doesn't work for [this, 'doSomething', 'meow']
|
|
685
|
+
let args = [];
|
|
686
|
+
if (Array.isArray(expr)) {
|
|
687
|
+
for (let i=0; i<expr.length; i++)
|
|
688
|
+
if (typeof expr[i] === 'function') {
|
|
689
|
+
func = expr[i];
|
|
690
|
+
args = expr.slice(i+1);
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// oninput=${[this, 'value']}
|
|
695
|
+
if (!func) {
|
|
696
|
+
func = setValue;
|
|
697
|
+
args = [expr[0], expr.slice(1), node];
|
|
698
|
+
node.value = delve(expr[0], expr.slice(1));
|
|
699
|
+
//debugger;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
else
|
|
703
|
+
func = expr;
|
|
704
|
+
|
|
705
|
+
let eventKey = getObjectId(node) + eventName;
|
|
706
|
+
let [existing, existingBound] = nodeEvents[eventKey] || [];
|
|
707
|
+
nodeEventArgs[eventKey] = args; // TODO: Put this in nodeEvents[]
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
if (existing !== func) {
|
|
711
|
+
if (existing)
|
|
712
|
+
node.removeEventListener(eventName, existingBound);
|
|
713
|
+
|
|
714
|
+
let originalFunc = func;
|
|
715
|
+
|
|
716
|
+
// BoundFunc sets the "this" variable to be the current Solarite component.
|
|
717
|
+
let boundFunc = event => originalFunc.call(root, ...args, event, node);
|
|
718
|
+
|
|
719
|
+
// Save both the original and bound functions.
|
|
720
|
+
// Original so we can compare it against a newly assigned function.
|
|
721
|
+
// Bound so we can use it with removeEventListner().
|
|
722
|
+
nodeEvents[eventKey] = [originalFunc, boundFunc];
|
|
723
|
+
|
|
724
|
+
node.addEventListener(eventName, boundFunc);
|
|
725
|
+
|
|
726
|
+
// TODO: classic event attribs:
|
|
727
|
+
//el[attr.name] = e => // e.g. el.onclick = ...
|
|
728
|
+
// (new Function('event', 'el', attr.value)).bind(this.manager.rootEl)(e, el) // put "event", "el", and "this" in scope for the event code.
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
applyValueAttrib(node, exprs, exprIndex) {
|
|
733
|
+
let expr = exprs[exprIndex];
|
|
734
|
+
|
|
735
|
+
// Array for form element data binding.
|
|
736
|
+
// TODO: This never worked, and was moved to applyEventAttrib.
|
|
737
|
+
// let isArrayValue = Array.isArray(expr);
|
|
738
|
+
// if (isArrayValue && expr.length >= 2 && !expr.slice(1).find(v => !['string', 'number'].includes(typeof v))) {
|
|
739
|
+
// node.value = delve(expr[0], expr.slice(1));
|
|
740
|
+
// node.addEventListener('input', e => {
|
|
741
|
+
// delve(expr[0], expr.slice(1), node.value) // TODO: support other properties like checked
|
|
742
|
+
// });
|
|
743
|
+
// }
|
|
744
|
+
|
|
745
|
+
// Values to toggle an attribute
|
|
746
|
+
if (!this.attrValue && (expr === false || expr === null || expr === undefined))
|
|
747
|
+
node.removeAttribute(this.attrName);
|
|
748
|
+
|
|
749
|
+
else if (!this.attrValue && expr === true)
|
|
750
|
+
node.setAttribute(this.attrName, '');
|
|
751
|
+
|
|
752
|
+
// Regular attribute
|
|
753
|
+
else {
|
|
754
|
+
let value = [];
|
|
755
|
+
|
|
756
|
+
// We go backward because NodeGroup.applyExprs() calls this function, and it goes backward through the exprs.
|
|
757
|
+
if (this.attrValue) {
|
|
758
|
+
for (let i=this.attrValue.length-1; i>=0; i--) {
|
|
759
|
+
value.unshift(this.attrValue[i]);
|
|
760
|
+
if (i > 0) {
|
|
761
|
+
let val = exprs[exprIndex];
|
|
762
|
+
if (val !== false && val !== null && val !== undefined)
|
|
763
|
+
value.unshift(val);
|
|
764
|
+
exprIndex--;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
exprIndex ++;
|
|
769
|
+
}
|
|
770
|
+
else
|
|
771
|
+
value.unshift(expr);
|
|
772
|
+
|
|
773
|
+
let joinedValue = value.join('');
|
|
774
|
+
node.setAttribute(this.attrName, joinedValue);
|
|
775
|
+
|
|
776
|
+
// This is needed for setting input.value, .checked, option.selected, etc.
|
|
777
|
+
// But in some cases setting the attribute is enough. such as div.setAttribute('title') updates div.title.
|
|
778
|
+
// TODO: How to tell which is which?
|
|
779
|
+
if (this.attrName in node)
|
|
780
|
+
node[this.attrName] = joinedValue;
|
|
781
|
+
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
return exprIndex;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
*
|
|
790
|
+
* @param newRoot {HTMLElement}
|
|
791
|
+
* @return {ExprPath} */
|
|
792
|
+
clone(newRoot) {
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
// Resolve node paths.
|
|
796
|
+
let nodeMarker, nodeBefore;
|
|
797
|
+
let root = newRoot;
|
|
798
|
+
let path = this.nodeMarkerPath;
|
|
799
|
+
for (let i=path.length-1; i>0; i--)
|
|
800
|
+
root = root.childNodes[path[i]];
|
|
801
|
+
let childNodes = root.childNodes;
|
|
802
|
+
nodeMarker = childNodes[path[0]];
|
|
803
|
+
if (this.nodeBefore)
|
|
804
|
+
nodeBefore = childNodes[this.nodeBeforeIndex];
|
|
805
|
+
|
|
806
|
+
let result = new ExprPath(nodeBefore, nodeMarker, this.type, this.attrName, this.attrValue);
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
return result;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Clear the nodeCache of this ExprPath, as well as all parent and child ExprPaths that
|
|
815
|
+
* share the same DOM parent node.
|
|
816
|
+
*
|
|
817
|
+
* TODO: Is recursive clearing ever necessary?
|
|
818
|
+
*/
|
|
819
|
+
clearNodesCache() {
|
|
820
|
+
let path = this;
|
|
821
|
+
|
|
822
|
+
// Clear cache parent ExprPaths that have the same parentNode
|
|
823
|
+
let parentNode = this.parentNode;
|
|
824
|
+
while (path && path.parentNode === parentNode) {
|
|
825
|
+
path.nodesCache = null;
|
|
826
|
+
path = path.parentNg?.parentPath;
|
|
827
|
+
|
|
828
|
+
// If stuck in an infinite loop here, the problem is likely due to Template hash colisions.
|
|
829
|
+
// Which cause one path to be the descendant of itself, creating a cycle.
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function clearChildNodeCache(path) {
|
|
833
|
+
|
|
834
|
+
// Clear cache of child ExprPaths that have the same parentNode
|
|
835
|
+
for (let ng of path.nodeGroups) {
|
|
836
|
+
if (ng) // Can be null from applyOneExpr()'s push(null) call.
|
|
837
|
+
for (let path2 of ng.paths) {
|
|
838
|
+
if (path2.type === PathType.Content && path2.parentNode === parentNode) {
|
|
839
|
+
path2.nodesCache = null;
|
|
840
|
+
clearChildNodeCache(path2);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
clearChildNodeCache(this);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Attempt to remove all of this ExprPath's nodes from the DOM, if it can be done using a special fast method.
|
|
852
|
+
* @returns {boolean} Returns false if Nodes werne't removed, and they should instead be removed manually. */
|
|
853
|
+
fastClear() {
|
|
854
|
+
let parent = this.nodeBefore.parentNode;
|
|
855
|
+
if (this.nodeBefore === parent.firstChild && this.nodeMarker === parent.lastChild) {
|
|
856
|
+
|
|
857
|
+
// If parent is the only child of the grandparent, replace the whole parent.
|
|
858
|
+
// And if it has no siblings, it's not created by a NodeGroup/path.
|
|
859
|
+
let grandparent = parent.parentNode;
|
|
860
|
+
if (grandparent && parent === grandparent.firstChild && parent === grandparent.lastChild && !parent.hasAttribute('id')) {
|
|
861
|
+
let replacement = document.createElement(parent.tagName);
|
|
862
|
+
replacement.append(this.nodeBefore, this.nodeMarker);
|
|
863
|
+
for (let attrib of parent.attributes)
|
|
864
|
+
replacement.setAttribute(attrib.name, attrib.value);
|
|
865
|
+
parent.replaceWith(replacement);
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
parent.innerHTML = ''; // Faster than calling .removeChild() a thousand times.
|
|
869
|
+
parent.append(this.nodeBefore, this.nodeMarker);
|
|
870
|
+
}
|
|
871
|
+
return true;
|
|
872
|
+
}
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* @return {(Node|HTMLElement)[]} */
|
|
878
|
+
getNodes() {
|
|
879
|
+
|
|
880
|
+
// Why doesn't this work?
|
|
881
|
+
// let result2 = [];
|
|
882
|
+
// for (let ng of this.nodeGroups)
|
|
883
|
+
// result2.push(...ng.getNodes())
|
|
884
|
+
// return result2;
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
let result;
|
|
888
|
+
|
|
889
|
+
// This shaves about 5ms off the partialUpdate benchmark.
|
|
890
|
+
/*result = this.nodesCache;
|
|
891
|
+
if (result) {
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
return result
|
|
896
|
+
}*/
|
|
897
|
+
|
|
898
|
+
result = [];
|
|
899
|
+
let current = this.nodeBefore.nextSibling;
|
|
900
|
+
let nodeMarker = this.nodeMarker;
|
|
901
|
+
while (current && current !== nodeMarker) {
|
|
902
|
+
result.push(current);
|
|
903
|
+
current = current.nextSibling;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
this.nodesCache = result;
|
|
907
|
+
return result;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
getParentNode() { // Same as this.parentNode
|
|
911
|
+
return this.nodeMarker.parentNode
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
removeNodeGroup(ng) {
|
|
915
|
+
let idx = this.nodeGroups.indexOf(ng);
|
|
916
|
+
|
|
917
|
+
this.nodeGroups.splice(idx);
|
|
918
|
+
ng.parentPath = null;
|
|
919
|
+
this.clearNodesCache();
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
*
|
|
929
|
+
* @param root
|
|
930
|
+
* @param path {string[]}
|
|
931
|
+
* @param node {HTMLElement}
|
|
932
|
+
*/
|
|
933
|
+
function setValue(root, path, node) {
|
|
934
|
+
let val = node.value;
|
|
935
|
+
if (node.type === 'number')
|
|
936
|
+
val = parseFloat(val);
|
|
937
|
+
|
|
938
|
+
delve(root, path, val);
|
|
939
|
+
|
|
940
|
+
//this.render();
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/** @enum {string} */
|
|
944
|
+
const PathType = {
|
|
945
|
+
/** Child of a node */
|
|
946
|
+
Content: 'content',
|
|
947
|
+
|
|
948
|
+
/** One or more whole attributes */
|
|
949
|
+
Multiple: 'attrName',
|
|
950
|
+
|
|
951
|
+
/** Value of an attribute. */
|
|
952
|
+
Value: 'attrValue',
|
|
953
|
+
|
|
954
|
+
/** Value of an attribute being passed to a component. */
|
|
955
|
+
Component: 'component',
|
|
956
|
+
|
|
957
|
+
/** Expressions inside Html comments. */
|
|
958
|
+
Comment: 'comment',
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
/** @return {int[]} Returns indices in reverse order, because doing it that way is faster. */
|
|
963
|
+
function getNodePath(node) {
|
|
964
|
+
let result = [];
|
|
965
|
+
while(true) {
|
|
966
|
+
let parent = node.parentNode;
|
|
967
|
+
if (!parent)
|
|
968
|
+
break;
|
|
969
|
+
result.push(Array.prototype.indexOf.call(node.parentNode.childNodes, node));
|
|
970
|
+
node = parent;
|
|
971
|
+
}
|
|
972
|
+
return result;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Note that the path is backward, with the outermost element at the end.
|
|
977
|
+
* @param root {HTMLElement|Document|DocumentFragment|ParentNode}
|
|
978
|
+
* @param path {int[]}
|
|
979
|
+
* @returns {Node|HTMLElement} */
|
|
980
|
+
function resolveNodePath(root, path) {
|
|
981
|
+
for (let i=path.length-1; i>=0; i--)
|
|
982
|
+
root = root.childNodes[path[i]];
|
|
983
|
+
return root;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
// TODO: Memory from this is never freed. Use a WeakMap<Node, Object<eventName:string, function[]>>
|
|
988
|
+
let nodeEvents = {};
|
|
989
|
+
let nodeEventArgs = {};
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* A Shell is created from a tagged template expression instantiated as Nodes,
|
|
993
|
+
* but without any expressions filled in. */
|
|
994
|
+
class Shell {
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* @type {DocumentFragment} Parent of the shell nodes. */
|
|
998
|
+
fragment;
|
|
999
|
+
|
|
1000
|
+
/** @type {ExprPath[]} Paths to where expressions should go. */
|
|
1001
|
+
paths = [];
|
|
1002
|
+
|
|
1003
|
+
/** @type {?Template} Template that created this element. */
|
|
1004
|
+
template;
|
|
1005
|
+
|
|
1006
|
+
// Embeds and ids
|
|
1007
|
+
events = [];
|
|
1008
|
+
|
|
1009
|
+
/** @type {int[][]} Array of paths */
|
|
1010
|
+
ids = [];
|
|
1011
|
+
scripts = [];
|
|
1012
|
+
styles = [];
|
|
1013
|
+
|
|
1014
|
+
staticComponents = [];
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* Create the nodes but without filling in the expressions.
|
|
1019
|
+
* This is useful because the expression-less nodes created by a template can be cached.
|
|
1020
|
+
* @param html {string[]} */
|
|
1021
|
+
constructor(html=null) {
|
|
1022
|
+
if (!html)
|
|
1023
|
+
return;
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
// 1. Add placeholders
|
|
1028
|
+
// We increment the placeholder char as we go because nodes can't have the same attribute more than once.
|
|
1029
|
+
let placeholder = 0xe000; // https://en.wikipedia.org/wiki/Private_Use_Areas 6400.
|
|
1030
|
+
|
|
1031
|
+
let buffer = [];
|
|
1032
|
+
let commentPlaceholder = `<!--!✨!-->`;
|
|
1033
|
+
let componentNames = {};
|
|
1034
|
+
|
|
1035
|
+
htmlContext(null); // Reset the context.
|
|
1036
|
+
for (let i=0; i<html.length; i++) {
|
|
1037
|
+
let lastHtml = html[i];
|
|
1038
|
+
let context = htmlContext(lastHtml);
|
|
1039
|
+
|
|
1040
|
+
// Swap out Embedded Solarite Components with ${} attributes.
|
|
1041
|
+
// Later, NodeGroup.render() will search for these and replace them with the real components.
|
|
1042
|
+
// Ctrl+F "redcomponent-placeholder" in project to find all code that manages subcomponents.
|
|
1043
|
+
if (context === htmlContext.Attribute) {
|
|
1044
|
+
|
|
1045
|
+
let lastIndex, lastMatch;
|
|
1046
|
+
lastHtml.replace(/<[a-z][a-z0-9]*-[a-z0-9-]+/ig, (match, index) => {
|
|
1047
|
+
lastIndex = index+1; // +1 for after opening <
|
|
1048
|
+
lastMatch = match.slice(1);
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
if (lastMatch) {
|
|
1052
|
+
let newTagName = lastMatch + '-redcomponent-placeholder';
|
|
1053
|
+
lastHtml = lastHtml.slice(0, lastIndex) + newTagName + lastHtml.slice(lastIndex + lastMatch.length);
|
|
1054
|
+
componentNames[lastMatch] = newTagName;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
buffer.push(lastHtml);
|
|
1059
|
+
//console.log(lastHtml, context)
|
|
1060
|
+
if (i < html.length-1)
|
|
1061
|
+
if (context === htmlContext.Text)
|
|
1062
|
+
buffer.push(commentPlaceholder); // Comment Placeholder. because we can't put text in between <tr> tags for example.
|
|
1063
|
+
else
|
|
1064
|
+
buffer.push(String.fromCharCode(placeholder+i));
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// 2. Create elements from html with placeholders.
|
|
1068
|
+
let template = document.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
|
|
1069
|
+
let joinedHtml = buffer.join('');
|
|
1070
|
+
|
|
1071
|
+
// Replace '-redcomponent-placeholder' close tags.
|
|
1072
|
+
// TODO: is there a better way? What if the close tag is inside a comment?
|
|
1073
|
+
for (let name in componentNames)
|
|
1074
|
+
joinedHtml = joinedHtml.replaceAll(`</${name}>`, `</${componentNames[name]}>`);
|
|
1075
|
+
|
|
1076
|
+
if (joinedHtml)
|
|
1077
|
+
template.innerHTML = joinedHtml;
|
|
1078
|
+
else // Create one text node, so shell isn't empty and NodeGroups created from it have something to point the startNode and endNode at.
|
|
1079
|
+
template.content.append(document.createTextNode(''));
|
|
1080
|
+
this.fragment = template.content;
|
|
1081
|
+
|
|
1082
|
+
// 3. Find placeholders
|
|
1083
|
+
let node;
|
|
1084
|
+
let toRemove = [];
|
|
1085
|
+
const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
|
|
1086
|
+
while (node = walker.nextNode()) {
|
|
1087
|
+
|
|
1088
|
+
// Remove previous after each iteration, so paths will still be calculated correctly.
|
|
1089
|
+
toRemove.map(el => el.remove());
|
|
1090
|
+
toRemove = [];
|
|
1091
|
+
|
|
1092
|
+
// Replace attributes
|
|
1093
|
+
if (node.nodeType === 1) {
|
|
1094
|
+
for (let attr of node.attributes) {
|
|
1095
|
+
|
|
1096
|
+
// Whole attribute
|
|
1097
|
+
let matches = attr.name.match(/^[\ue000-\uf8ff]$/);
|
|
1098
|
+
if (matches) {
|
|
1099
|
+
this.paths.push(new ExprPath(null, node, PathType.Multiple));
|
|
1100
|
+
node.removeAttribute(matches[0]);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// Just the attribute value.
|
|
1104
|
+
else {
|
|
1105
|
+
let parts = attr.value.split(/[\ue000-\uf8ff]/g);
|
|
1106
|
+
if (parts.length > 1) {
|
|
1107
|
+
let nonEmptyParts = (parts.length === 2 && !parts[0].length && !parts[1].length) ? null : parts;
|
|
1108
|
+
this.paths.push(new ExprPath(null, node, PathType.Value, attr.name, nonEmptyParts));
|
|
1109
|
+
node.setAttribute(attr.name, parts.join(''));
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
// Replace comment placeholders
|
|
1115
|
+
else if (node.nodeType === Node.COMMENT_NODE && node.nodeValue === '!✨!') {
|
|
1116
|
+
|
|
1117
|
+
// Get or create nodeBefore.
|
|
1118
|
+
let nodeBefore = node.previousSibling; // Can be the same as another Path's nodeMarker.
|
|
1119
|
+
if (!nodeBefore) {
|
|
1120
|
+
nodeBefore = document.createComment('PathStart:'+this.paths.length);
|
|
1121
|
+
node.parentNode.insertBefore(nodeBefore, node);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
// Get the next node.
|
|
1126
|
+
let nodeMarker;
|
|
1127
|
+
|
|
1128
|
+
// A subsequent node is available to be a nodeMarker.
|
|
1129
|
+
if (node.nextSibling && (node.nextSibling.nodeType !== 8 || node.nextSibling.textContent !== '!✨!')) {
|
|
1130
|
+
nodeMarker = node.nextSibling;
|
|
1131
|
+
toRemove.push(node); // Removing them here will mess up the treeWalker.
|
|
1132
|
+
}
|
|
1133
|
+
// Re-use existing comment placeholder.
|
|
1134
|
+
else {
|
|
1135
|
+
nodeMarker = node;
|
|
1136
|
+
nodeMarker.textContent = 'PathEnd:'+ this.paths.length;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
let path = new ExprPath(nodeBefore, nodeMarker, PathType.Content);
|
|
1143
|
+
|
|
1144
|
+
this.paths.push(path);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
// Sometimes users will comment out a block of html code that has expressions.
|
|
1149
|
+
// Here we look for expressions in comments.
|
|
1150
|
+
// We don't actually update them dynamically, but we still add paths for them.
|
|
1151
|
+
// That way the expression count still matches.
|
|
1152
|
+
else if (node.nodeType === Node.COMMENT_NODE) {
|
|
1153
|
+
let parts = node.textContent.split(/[\ue000-\uf8ff]/g);
|
|
1154
|
+
for (let i=0; i<parts.length-1; i++) {
|
|
1155
|
+
let path = new ExprPath(node.previousSibling, node);
|
|
1156
|
+
path.type = PathType.Comment;
|
|
1157
|
+
|
|
1158
|
+
this.paths.push(path);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Replace comment placeholders inside script and style tags, which have become text nodes.
|
|
1163
|
+
else if (node.nodeType === Node.TEXT_NODE && ['SCRIPT', 'STYLE'].includes(node.parentNode?.nodeName)) {
|
|
1164
|
+
let parts = node.textContent.split(commentPlaceholder);
|
|
1165
|
+
if (parts.length > 1) {
|
|
1166
|
+
|
|
1167
|
+
let placeholders = [];
|
|
1168
|
+
for (let i = 0; i<parts.length; i++) {
|
|
1169
|
+
let current = document.createTextNode(parts[i]);
|
|
1170
|
+
node.parentNode.insertBefore(current, node);
|
|
1171
|
+
if (i > 0)
|
|
1172
|
+
placeholders.push(current);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
for (let i=0, node; node=placeholders[i]; i++) {
|
|
1176
|
+
let path = new ExprPath(node.previousSibling, node, PathType.Content);
|
|
1177
|
+
|
|
1178
|
+
this.paths.push(path);
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// Removing them here will mess up the treeWalker.
|
|
1184
|
+
toRemove.push(node);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
toRemove.map(el => el.remove());
|
|
1189
|
+
|
|
1190
|
+
// Handle redcomponent-placeholder's.
|
|
1191
|
+
// Ctrl+F "redcomponent-placeholder" in project to find all code that manages subcomponents.
|
|
1192
|
+
//if (componentNames.size)
|
|
1193
|
+
// this.components = [...this.fragment.querySelectorAll([...componentNames].join(','))]
|
|
1194
|
+
|
|
1195
|
+
// Rename "is" attributes so the Web Components don't instantiate until we have the values of their PathExpr arguments.
|
|
1196
|
+
// that happens in NodeGroup.applyComponentExprs()
|
|
1197
|
+
for (let el of this.fragment.querySelectorAll('[is]')) {
|
|
1198
|
+
el.setAttribute('_is', el.getAttribute('is'));
|
|
1199
|
+
// this.components.push(el);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
for (let path of this.paths) {
|
|
1203
|
+
if (path.nodeBefore)
|
|
1204
|
+
path.nodeBeforeIndex = Array.prototype.indexOf.call(path.nodeBefore.parentNode.childNodes, path.nodeBefore);
|
|
1205
|
+
path.nodeMarkerPath = getNodePath(path.nodeMarker);
|
|
1206
|
+
|
|
1207
|
+
// Cache so we don't have to calculate this later inside NodeGroup.applyExprs()
|
|
1208
|
+
if (path.type === PathType.Value && path.nodeMarker.nodeType === 1 &&
|
|
1209
|
+
(path.nodeMarker.tagName.includes('-') || path.nodeMarker.hasAttribute('is'))) {
|
|
1210
|
+
path.type = PathType.Component;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
this.findEmbeds();
|
|
1216
|
+
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
} // end constructor
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* We find the path to every embed here once in the Shell, instead of every time a NodeGroup is instantiated.
|
|
1223
|
+
* When a Nodegroup is created, it calls NodeGroup.activateEmbeds() that uses these paths. */
|
|
1224
|
+
findEmbeds() {
|
|
1225
|
+
this.scripts = Array.prototype.map.call(this.fragment.querySelectorAll('scripts'), el => getNodePath(el));
|
|
1226
|
+
this.styles = Array.prototype.map.call(this.fragment.querySelectorAll('style'), el => getNodePath(el));
|
|
1227
|
+
|
|
1228
|
+
let idEls = this.fragment.querySelectorAll('[id],[data-id]');
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
// Check for valid id names.
|
|
1232
|
+
for (let el of idEls) {
|
|
1233
|
+
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
1234
|
+
if (div.hasOwnProperty(id))
|
|
1235
|
+
throw new Error(`<${el.tagName.toLowerCase()} id="${id}"> can't override existing HTMLElement property.`)
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
this.ids = Array.prototype.map.call(idEls, el => getNodePath(el));
|
|
1240
|
+
|
|
1241
|
+
// Events (not yet used)
|
|
1242
|
+
for (let el of this.fragment.querySelectorAll('*')) {
|
|
1243
|
+
for (let attrib of el.attributes)
|
|
1244
|
+
if (isEvent(attrib.name))
|
|
1245
|
+
this.events.push([attrib.name, getNodePath(el)]);
|
|
1246
|
+
|
|
1247
|
+
if (el.tagName.includes('-') || el.hasAttribute('_is'))
|
|
1248
|
+
|
|
1249
|
+
// Dynamic components have attributes with expression values.
|
|
1250
|
+
// They are created from applyExprs()
|
|
1251
|
+
// But static components are created in a separate path inside the NodeGroup constructor.
|
|
1252
|
+
if (!this.paths.find(path => path.nodeMarker === el))
|
|
1253
|
+
this.staticComponents.push(getNodePath(el));
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* Get the shell for the html strings.
|
|
1260
|
+
* @param htmlStrings {string[]}
|
|
1261
|
+
* @returns {Shell} */
|
|
1262
|
+
static get(htmlStrings) {
|
|
1263
|
+
let result = shells.get(htmlStrings);
|
|
1264
|
+
if (!result) {
|
|
1265
|
+
result = new Shell(htmlStrings);
|
|
1266
|
+
shells.set(htmlStrings, result); // cache
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
|
|
1270
|
+
return result;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
let shells = new WeakMap();
|
|
1277
|
+
|
|
1278
|
+
/**
|
|
1279
|
+
* The html strings and evaluated expressions from an html tagged template.
|
|
1280
|
+
* A unique Template is created for each item in a loop.
|
|
1281
|
+
* Although the reference to the html strings is shared among templates. */
|
|
1282
|
+
class Template {
|
|
1283
|
+
|
|
1284
|
+
/** @type {(Template|string|function)|(Template|string|function)[]} Evaulated expressions. */
|
|
1285
|
+
exprs = []
|
|
1286
|
+
|
|
1287
|
+
/** @type {string[]} */
|
|
1288
|
+
html = [];
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* If true, use this template to replace an existing element, instead of appending children to it.
|
|
1292
|
+
* @type {?boolean} */
|
|
1293
|
+
replaceMode;
|
|
1294
|
+
|
|
1295
|
+
/** Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
|
|
1296
|
+
hashedFields;
|
|
1297
|
+
|
|
1298
|
+
/**
|
|
1299
|
+
* @deprecated
|
|
1300
|
+
* @type {ExprPath} Used with forEach() from watch.js
|
|
1301
|
+
* Set in NodeGroup.applyOneExpr() */
|
|
1302
|
+
parentPath;
|
|
1303
|
+
|
|
1304
|
+
/** @type {NodeGroup} */
|
|
1305
|
+
nodeGroup;
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* @type {string[][]} */
|
|
1309
|
+
paths = [];
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
*
|
|
1313
|
+
* @param htmlStrings {string[]}
|
|
1314
|
+
* @param exprs {*[]} */
|
|
1315
|
+
constructor(htmlStrings, exprs) {
|
|
1316
|
+
this.html = htmlStrings;
|
|
1317
|
+
this.exprs = exprs;
|
|
1318
|
+
|
|
1319
|
+
//this.trace = new Error().stack.split(/\n/g)
|
|
1320
|
+
|
|
1321
|
+
// Multiple templates can share the same htmlStrings array.
|
|
1322
|
+
//this.hashedFields = [getObjectId(htmlStrings), exprs]
|
|
1323
|
+
|
|
1324
|
+
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/**
|
|
1328
|
+
* Called by JSON.serialize when it encounters a Template.
|
|
1329
|
+
* This prevents the hashed version from being too large. */
|
|
1330
|
+
toJSON() {
|
|
1331
|
+
if (!this.hashedFields)
|
|
1332
|
+
this.hashedFields = [getObjectId(this.html, 'Html'), this.exprs];
|
|
1333
|
+
|
|
1334
|
+
return this.hashedFields
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
toNode() {
|
|
1338
|
+
let ngm = new NodeGroupManager();
|
|
1339
|
+
return ngm.render(this);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
getCloseKey() {
|
|
1343
|
+
// Use the joined html when debugging?
|
|
1344
|
+
//return '@'+this.html.join('|')
|
|
1345
|
+
|
|
1346
|
+
return '@'+this.hashedFields[0];
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
/**
|
|
1351
|
+
* ISC License
|
|
1352
|
+
*
|
|
1353
|
+
* Copyright (c) 2020, Andrea Giammarchi, @WebReflection
|
|
1354
|
+
*
|
|
1355
|
+
* Permission to use, copy, modify, and/or distribute this software for any
|
|
1356
|
+
* purpose with or without fee is hereby granted, provided that the above
|
|
1357
|
+
* copyright notice and this permission notice appear in all copies.
|
|
1358
|
+
*
|
|
1359
|
+
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
1360
|
+
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
1361
|
+
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
1362
|
+
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
1363
|
+
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
|
1364
|
+
* OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
1365
|
+
* PERFORMANCE OF THIS SOFTWARE.
|
|
1366
|
+
*/
|
|
1367
|
+
|
|
1368
|
+
/**
|
|
1369
|
+
* @param {Node} parentNode The container where children live
|
|
1370
|
+
* @param {Node[]} a The list of current/live children
|
|
1371
|
+
* @param {Node[]} b The list of future children
|
|
1372
|
+
* @param {(entry: Node, action: number) => Node} get
|
|
1373
|
+
* The callback invoked per each entry related DOM operation.
|
|
1374
|
+
* @param {Node} [before] The optional node used as anchor to insert before.
|
|
1375
|
+
* @returns {Node[]} The same list of future children.
|
|
1376
|
+
*/
|
|
1377
|
+
const udomdiff = (parentNode, a, b, before) => {
|
|
1378
|
+
|
|
1379
|
+
|
|
1380
|
+
const bLength = b.length;
|
|
1381
|
+
let aEnd = a.length;
|
|
1382
|
+
let bEnd = bLength;
|
|
1383
|
+
let aStart = 0;
|
|
1384
|
+
let bStart = 0;
|
|
1385
|
+
let map = null;
|
|
1386
|
+
while (aStart < aEnd || bStart < bEnd) {
|
|
1387
|
+
// append head, tail, or nodes in between: fast path
|
|
1388
|
+
if (aEnd === aStart) {
|
|
1389
|
+
// we could be in a situation where the rest of nodes that
|
|
1390
|
+
// need to be added are not at the end, and in such case
|
|
1391
|
+
// the node to `insertBefore`, if the index is more than 0
|
|
1392
|
+
// must be retrieved, otherwise it's gonna be the first item.
|
|
1393
|
+
const node = bEnd < bLength
|
|
1394
|
+
? (bStart
|
|
1395
|
+
? (b[bStart - 1].nextSibling)
|
|
1396
|
+
: b[bEnd - bStart])
|
|
1397
|
+
: before;
|
|
1398
|
+
while (bStart < bEnd) {
|
|
1399
|
+
let bNode = b[bStart++];
|
|
1400
|
+
parentNode.insertBefore(bNode, node);
|
|
1401
|
+
|
|
1402
|
+
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
// remove head or tail: fast path
|
|
1406
|
+
else if (bEnd === bStart) {
|
|
1407
|
+
while (aStart < aEnd) {
|
|
1408
|
+
// remove the node only if it's unknown or not live
|
|
1409
|
+
let aNode = a[aStart];
|
|
1410
|
+
if (!map || !map.has(aNode)) {
|
|
1411
|
+
parentNode.removeChild(aNode);
|
|
1412
|
+
|
|
1413
|
+
|
|
1414
|
+
}
|
|
1415
|
+
aStart++;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
// same node: fast path
|
|
1419
|
+
else if (a[aStart] === b[bStart]) {
|
|
1420
|
+
aStart++;
|
|
1421
|
+
bStart++;
|
|
1422
|
+
}
|
|
1423
|
+
// same tail: fast path
|
|
1424
|
+
else if (a[aEnd - 1] === b[bEnd - 1]) {
|
|
1425
|
+
aEnd--;
|
|
1426
|
+
bEnd--;
|
|
1427
|
+
}
|
|
1428
|
+
// The once here single last swap "fast path" has been removed in v1.1.0
|
|
1429
|
+
// https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85
|
|
1430
|
+
// reverse swap: also fast path
|
|
1431
|
+
else if (
|
|
1432
|
+
a[aStart] === b[bEnd - 1] &&
|
|
1433
|
+
b[bStart] === a[aEnd - 1]
|
|
1434
|
+
) {
|
|
1435
|
+
// this is a "shrink" operation that could happen in these cases:
|
|
1436
|
+
// [1, 2, 3, 4, 5]
|
|
1437
|
+
// [1, 4, 3, 2, 5]
|
|
1438
|
+
// or asymmetric too
|
|
1439
|
+
// [1, 2, 3, 4, 5]
|
|
1440
|
+
// [1, 2, 3, 5, 6, 4]
|
|
1441
|
+
const node = a[--aEnd].nextSibling;
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
let a2 = b[bStart++];
|
|
1445
|
+
let b2 = a[aStart++];
|
|
1446
|
+
parentNode.insertBefore(
|
|
1447
|
+
a2,
|
|
1448
|
+
b2.nextSibling
|
|
1449
|
+
);
|
|
1450
|
+
|
|
1451
|
+
|
|
1452
|
+
let bNode = b[--bEnd];
|
|
1453
|
+
parentNode.insertBefore(bNode, node);
|
|
1454
|
+
|
|
1455
|
+
|
|
1456
|
+
|
|
1457
|
+
// mark the future index as identical (yeah, it's dirty, but cheap 👍)
|
|
1458
|
+
// The main reason to do this, is that when a[aEnd] will be reached,
|
|
1459
|
+
// the loop will likely be on the fast path, as identical to b[bEnd].
|
|
1460
|
+
// In the best case scenario, the next loop will skip the tail,
|
|
1461
|
+
// but in the worst one, this node will be considered as already
|
|
1462
|
+
// processed, bailing out pretty quickly from the map index check
|
|
1463
|
+
a[aEnd] = b[bEnd];
|
|
1464
|
+
}
|
|
1465
|
+
// map based fallback, "slow" path
|
|
1466
|
+
else {
|
|
1467
|
+
// the map requires an O(bEnd - bStart) operation once
|
|
1468
|
+
// to store all future nodes indexes for later purposes.
|
|
1469
|
+
// In the worst case scenario, this is a full O(N) cost,
|
|
1470
|
+
// and such scenario happens at least when all nodes are different,
|
|
1471
|
+
// but also if both first and last items of the lists are different
|
|
1472
|
+
if (!map) {
|
|
1473
|
+
map = new Map;
|
|
1474
|
+
let i = bStart;
|
|
1475
|
+
while (i < bEnd)
|
|
1476
|
+
map.set(b[i], i++);
|
|
1477
|
+
}
|
|
1478
|
+
// if it's a future node, hence it needs some handling
|
|
1479
|
+
if (map.has(a[aStart])) {
|
|
1480
|
+
// grab the index of such node, 'cause it might have been processed
|
|
1481
|
+
const index = map.get(a[aStart]);
|
|
1482
|
+
// if it's not already processed, look on demand for the next LCS
|
|
1483
|
+
if (bStart < index && index < bEnd) {
|
|
1484
|
+
let i = aStart;
|
|
1485
|
+
// counts the amount of nodes that are the same in the future
|
|
1486
|
+
let sequence = 1;
|
|
1487
|
+
while (++i < aEnd && i < bEnd && map.get(a[i]) === (index + sequence))
|
|
1488
|
+
sequence++;
|
|
1489
|
+
// effort decision here: if the sequence is longer than replaces
|
|
1490
|
+
// needed to reach such sequence, which would brings again this loop
|
|
1491
|
+
// to the fast path, prepend the difference before a sequence,
|
|
1492
|
+
// and move only the future list index forward, so that aStart
|
|
1493
|
+
// and bStart will be aligned again, hence on the fast path.
|
|
1494
|
+
// An example considering aStart and bStart are both 0:
|
|
1495
|
+
// a: [1, 2, 3, 4]
|
|
1496
|
+
// b: [7, 1, 2, 3, 6]
|
|
1497
|
+
// this would place 7 before 1 and, from that time on, 1, 2, and 3
|
|
1498
|
+
// will be processed at zero cost
|
|
1499
|
+
if (sequence > (index - bStart)) {
|
|
1500
|
+
const node = a[aStart];
|
|
1501
|
+
while (bStart < index) {
|
|
1502
|
+
let bNode = b[bStart++];
|
|
1503
|
+
parentNode.insertBefore(bNode, node);
|
|
1504
|
+
|
|
1505
|
+
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
// if the effort wasn't good enough, fallback to a replace,
|
|
1509
|
+
// moving both source and target indexes forward, hoping that some
|
|
1510
|
+
// similar node will be found later on, to go back to the fast path
|
|
1511
|
+
else {
|
|
1512
|
+
let aNode = a[aStart++];
|
|
1513
|
+
let bNode = b[bStart++];
|
|
1514
|
+
parentNode.replaceChild(
|
|
1515
|
+
bNode,
|
|
1516
|
+
aNode
|
|
1517
|
+
);
|
|
1518
|
+
|
|
1519
|
+
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
// otherwise move the source forward, 'cause there's nothing to do
|
|
1523
|
+
else
|
|
1524
|
+
aStart++;
|
|
1525
|
+
}
|
|
1526
|
+
// this node has no meaning in the future list, so it's more than safe
|
|
1527
|
+
// to remove it, and check the next live node out instead, meaning
|
|
1528
|
+
// that only the live list index should be forwarded
|
|
1529
|
+
else {
|
|
1530
|
+
let aNode = a[aStart++];
|
|
1531
|
+
parentNode.removeChild(aNode);
|
|
1532
|
+
|
|
1533
|
+
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
return b;
|
|
1538
|
+
};
|
|
1539
|
+
|
|
1540
|
+
/**
|
|
1541
|
+
* Tools for watch variables and performing precise renders.
|
|
1542
|
+
*/
|
|
1543
|
+
|
|
1544
|
+
|
|
1545
|
+
/**
|
|
1546
|
+
* Stores info how to transform a path to a template. */
|
|
1547
|
+
class TransformerInfo {
|
|
1548
|
+
constructor(path, transformer, hash) {
|
|
1549
|
+
this.path = path;
|
|
1550
|
+
this.transformer = transformer;
|
|
1551
|
+
this.hash = hash;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
|
|
1556
|
+
/**
|
|
1557
|
+
* Maps an object path to the function that converts it to a Template.
|
|
1558
|
+
* Once it's convert to a template, we can get the hash of that Tempate.
|
|
1559
|
+
* Then that hash tells us what NodeGroups are affected by the object.
|
|
1560
|
+
*
|
|
1561
|
+
* We store the function to get the Template, instead of the Template itself,
|
|
1562
|
+
* so we can call that function again when the object has a new value.
|
|
1563
|
+
* @type {MultiValueMap<Object, function(...Object):Template>} */
|
|
1564
|
+
let pathToTransformer = new MultiValueMap(); // uses a Set() for each value.
|
|
1565
|
+
|
|
1566
|
+
/**
|
|
1567
|
+
*
|
|
1568
|
+
* @param objectPaths {(*|function)[]}
|
|
1569
|
+
* @returns {Template} */
|
|
1570
|
+
function watchGet(...objectPaths) {
|
|
1571
|
+
|
|
1572
|
+
/** @type {function} */
|
|
1573
|
+
let transformer = objectPaths.at(-1);
|
|
1574
|
+
let paths;
|
|
1575
|
+
if (typeof transformer === 'function') {
|
|
1576
|
+
paths = objectPaths.slice(0, -1);
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// No transformer provided, so we create our own.
|
|
1580
|
+
else if (objectPaths.length === 1) {
|
|
1581
|
+
paths = [objectPaths[0].slice(0, -1)];
|
|
1582
|
+
let prop = objectPaths[0].at(-1);
|
|
1583
|
+
transformer = (...args) => (args[0][prop]);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
|
|
1587
|
+
// Save arguments used to call the template, so we can call it again when those args have their values change.
|
|
1588
|
+
let args = [];
|
|
1589
|
+
for (let path of paths) {
|
|
1590
|
+
|
|
1591
|
+
|
|
1592
|
+
let obj = delve(watchSet(path[0]), path.slice(1));
|
|
1593
|
+
args.push(obj);
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
let template = transformer(...args);
|
|
1597
|
+
|
|
1598
|
+
// If the result isn't a Template, convert the function to return a Template that wraps the result.
|
|
1599
|
+
// This way NodeGroupManager.findAndDelete() can find a NodeGroup that matches this Template's hash.
|
|
1600
|
+
if (!(template instanceof Template)) {
|
|
1601
|
+
let oldToTemplate = transformer;
|
|
1602
|
+
transformer = function() {
|
|
1603
|
+
return new Template(['', ''], [oldToTemplate(...arguments)]);
|
|
1604
|
+
};
|
|
1605
|
+
template = transformer(...args);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// Map the object paths to the function that creates a template.
|
|
1609
|
+
let hash = getObjectHash(template);
|
|
1610
|
+
for (let path of paths) {
|
|
1611
|
+
let serializedPath = serializePath(path);
|
|
1612
|
+
pathToTransformer.add(serializedPath, new TransformerInfo(path, transformer, hash)); // Uses a Set() to ensure no duplicates.
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
return template;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
//let proxyCache = new WeakMap();
|
|
1619
|
+
|
|
1620
|
+
|
|
1621
|
+
/**
|
|
1622
|
+
* Set the value of a variable in a way that's watched, so later when we call .renderWatched()
|
|
1623
|
+
* We can find what NodeGroups to update.*/
|
|
1624
|
+
function watchSet(obj) {
|
|
1625
|
+
if (obj?.$isProxy===true)
|
|
1626
|
+
return obj; // It's already a Proxy.
|
|
1627
|
+
|
|
1628
|
+
// This cache doesn't make things faster.
|
|
1629
|
+
// let result = proxyCache.get(obj);
|
|
1630
|
+
// if (!result) {
|
|
1631
|
+
// result = new Proxy(obj, new ProxyHandler(obj));
|
|
1632
|
+
// proxyCache.set(obj, result);
|
|
1633
|
+
// }
|
|
1634
|
+
// return result;
|
|
1635
|
+
return new Proxy(obj, new ProxyHandler$1(obj));
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
/**
|
|
1639
|
+
* Loop over each item and apply watchGet() to each item.
|
|
1640
|
+
* @param arrayPath {*[]}
|
|
1641
|
+
* @param callback {function(obj:Object, index:int):Template}
|
|
1642
|
+
* @returns {Template} */
|
|
1643
|
+
function forEach(arrayPath, callback) {
|
|
1644
|
+
let array = delve(arrayPath[0], arrayPath.slice(1));
|
|
1645
|
+
|
|
1646
|
+
// This is retrieved on the 'insert' path inside renderWatched()
|
|
1647
|
+
let ngm = NodeGroupManager.get(arrayPath[0]);
|
|
1648
|
+
if (ngm.clearSubscribers) {
|
|
1649
|
+
ngm.clearSubscribers = false;
|
|
1650
|
+
ngm.pathToLoopInfo = new MultiValueMap();
|
|
1651
|
+
} // TODO: Move tis into NodeGroupMAnager.get() without breaking things?
|
|
1652
|
+
|
|
1653
|
+
|
|
1654
|
+
let newItems = [...array.map((item, i) => {
|
|
1655
|
+
// TODO: This needs to wrap callback so we can pass it the index also.
|
|
1656
|
+
return watchGet([...arrayPath, i], callback); // calls callback(array[i], i)
|
|
1657
|
+
})
|
|
1658
|
+
];
|
|
1659
|
+
|
|
1660
|
+
// We return a template that wraps the array
|
|
1661
|
+
// So that NodeGroup.applyOneExpr can set the ExprPath and nextSibling on the template.
|
|
1662
|
+
// Then the 'insert' path in renderWatched() uses that data fora dding more nodes.
|
|
1663
|
+
let result = new Template(['', ''], [newItems]);
|
|
1664
|
+
|
|
1665
|
+
|
|
1666
|
+
// We get a unique hash for each foreach template because the [''] array is unique each time.
|
|
1667
|
+
let loopInfo = new LoopInfo(result, callback);
|
|
1668
|
+
ngm.pathToLoopInfo.add(serializePath(arrayPath), loopInfo);
|
|
1669
|
+
return result;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
function serializePath(path) {
|
|
1673
|
+
// Convert any array indices to strings, so serialized comparisons work.
|
|
1674
|
+
return JSON.stringify([getObjectId(path[0]), ...path.slice(1).map(item => item+'')])
|
|
1675
|
+
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
|
|
1679
|
+
/**
|
|
1680
|
+
* When an object property is accessed, a new Proxy with a new instance of this handler class is created,
|
|
1681
|
+
* but it tracks the path from the root to the property.
|
|
1682
|
+
* That way when a property is set, it can report the changed path. */
|
|
1683
|
+
class ProxyHandler$1 {
|
|
1684
|
+
|
|
1685
|
+
/**
|
|
1686
|
+
* @param root An element managed by a NodeGroupManager. The same as the NodeGroupManager's rootEl.
|
|
1687
|
+
* @param path {string[]} Used internally. */
|
|
1688
|
+
constructor(root, path=[]) {
|
|
1689
|
+
|
|
1690
|
+
this.root = root;
|
|
1691
|
+
this.path = path; // path from root to this Proxy.
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
/**
|
|
1695
|
+
* @param obj {Object}
|
|
1696
|
+
* @param prop {string} */
|
|
1697
|
+
get(obj, prop) {
|
|
1698
|
+
|
|
1699
|
+
// Special props. Currently unused.
|
|
1700
|
+
// if (prop === '$path')
|
|
1701
|
+
// return this.path;
|
|
1702
|
+
// if (prop === '$root')
|
|
1703
|
+
// return this.root;
|
|
1704
|
+
if (prop === '$isProxy')
|
|
1705
|
+
return true;
|
|
1706
|
+
|
|
1707
|
+
|
|
1708
|
+
// 1. Array.splice()
|
|
1709
|
+
if (prop === 'splice' && Array.isArray(obj)) {
|
|
1710
|
+
return (index, deleteCount, ...items) => {
|
|
1711
|
+
let ngm = NodeGroupManager.get(this.root);
|
|
1712
|
+
|
|
1713
|
+
if (deleteCount) {
|
|
1714
|
+
|
|
1715
|
+
// Get the hash of each object along the delete range. The process to get the hash is:
|
|
1716
|
+
// Serialized Path -> transformer -> Template -> hash.
|
|
1717
|
+
let hashes = [];
|
|
1718
|
+
for (let i=index; i<index+deleteCount; i++) {
|
|
1719
|
+
let serializedPath = serializePath([this.root, ...this.path, i+'']);
|
|
1720
|
+
|
|
1721
|
+
let obj = delve(this.root, [...this.path, i]);
|
|
1722
|
+
for (let transformerInfo of pathToTransformer.getAll(serializedPath)) {
|
|
1723
|
+
let template = transformerInfo.transformer(obj);
|
|
1724
|
+
let hash = getObjectHash(template);
|
|
1725
|
+
hashes.push(hash); // Hashes may go to nodes in more than one loop.
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
ngm.changes.push(new Change('delete', this.root, [...this.path, index+''], hashes));
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
//let oldArray = [...obj];
|
|
1733
|
+
let result = obj.splice(index, deleteCount);
|
|
1734
|
+
|
|
1735
|
+
// Inserting
|
|
1736
|
+
if (items.length) {
|
|
1737
|
+
|
|
1738
|
+
let beforeNgs;
|
|
1739
|
+
for (let loopInfo of ngm.getLoopInfo([this.root, ...this.path])) {
|
|
1740
|
+
let beforeObj = delve(this.root, [...this.path, index]);
|
|
1741
|
+
|
|
1742
|
+
// Find where to insert before.
|
|
1743
|
+
if (beforeObj) {
|
|
1744
|
+
let beforeTemplate = loopInfo.itemTransformer(beforeObj);
|
|
1745
|
+
let beforeHash = getObjectHash(beforeTemplate);
|
|
1746
|
+
beforeNgs = ngm.nodeGroupsAvailable.data[beforeHash];
|
|
1747
|
+
|
|
1748
|
+
if (beforeNgs) {
|
|
1749
|
+
let hash = getObjectHash(loopInfo.template);
|
|
1750
|
+
let loopNgs = ngm.nodeGroupsAvailable.data[hash] || [];
|
|
1751
|
+
for (let loopNg of loopNgs)
|
|
1752
|
+
for (let beforeNg of beforeNgs)
|
|
1753
|
+
if (beforeNg.startNode.parentNode === loopNg.startNode.parentNode)
|
|
1754
|
+
ngm.changes.push(new Change('insert', this.root, [...this.path, index + ''], items, beforeTemplate));
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
if (!beforeNgs)
|
|
1758
|
+
ngm.changes.push(new Change('insert', this.root, [...this.path, index + ''], items));
|
|
1759
|
+
}
|
|
1760
|
+
obj.splice(index, 0, ...items);
|
|
1761
|
+
}
|
|
1762
|
+
return result;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
|
|
1767
|
+
|
|
1768
|
+
// 2. Get property
|
|
1769
|
+
// If we're getting an object or array property, apply watch() to it recursively.
|
|
1770
|
+
let result = Reflect.get(obj, prop);
|
|
1771
|
+
if (result && typeof result === 'object') {
|
|
1772
|
+
let handler = new ProxyHandler$1(this.root, [...this.path, prop]); // same root, one level deeper on the path.
|
|
1773
|
+
return new Proxy(result, handler);
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
return result;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
|
|
1780
|
+
set(obj, prop, newValue) {
|
|
1781
|
+
let ngm = NodeGroupManager.get(this.root);
|
|
1782
|
+
ngm.changes.push(new Change('set', this.root, [...this.path, prop], newValue));
|
|
1783
|
+
return Reflect.set(obj, prop, newValue)
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
|
|
1788
|
+
/**
|
|
1789
|
+
*
|
|
1790
|
+
*/
|
|
1791
|
+
class Change {
|
|
1792
|
+
|
|
1793
|
+
/**
|
|
1794
|
+
* @param action {string}
|
|
1795
|
+
* @param root {Object|Array}
|
|
1796
|
+
* @param path {string[]}
|
|
1797
|
+
* @param value
|
|
1798
|
+
* If setting a value, this is the new value.
|
|
1799
|
+
* If deleting from an array, this is an array of all the NodeGroups to delete.
|
|
1800
|
+
*
|
|
1801
|
+
* @param beforeTemplate
|
|
1802
|
+
* */
|
|
1803
|
+
constructor(action, root, path, value, beforeTemplate=null) {
|
|
1804
|
+
this.action = action;
|
|
1805
|
+
|
|
1806
|
+
// TODO: Store root as first item of path, to be consistent with code elsewhere.
|
|
1807
|
+
this.root = root;
|
|
1808
|
+
this.path = path;
|
|
1809
|
+
this.value = value;
|
|
1810
|
+
this.beforeTemplate = beforeTemplate;
|
|
1811
|
+
|
|
1812
|
+
/** @type {TransformerInfo[]} */
|
|
1813
|
+
this.transformerInfo = [];
|
|
1814
|
+
|
|
1815
|
+
// Traverse up the path.
|
|
1816
|
+
for (let i=this.path.length; i>0; i--) {
|
|
1817
|
+
let path = this.path.slice(0, i);
|
|
1818
|
+
let fullPath = [this.root, ...path];
|
|
1819
|
+
|
|
1820
|
+
let serializedPath = getObjectHash(fullPath); // TODO: Why not serializedPath() ?
|
|
1821
|
+
this.transformerInfo.push(...pathToTransformer.getAll(serializedPath));
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
let logGets = false;
|
|
1827
|
+
let gets = [];
|
|
1828
|
+
|
|
1829
|
+
let withinSet = 0;
|
|
1830
|
+
|
|
1831
|
+
/**
|
|
1832
|
+
* Turn the props on obj into JavasCript properties that return Proxies when accessed.
|
|
1833
|
+
* If called more than once, return the already-converted object.
|
|
1834
|
+
* @param obj {Object}
|
|
1835
|
+
* @param props {string}
|
|
1836
|
+
* @returns {*|{$proxyHandler}}
|
|
1837
|
+
*/
|
|
1838
|
+
function watch(obj, ...props) {
|
|
1839
|
+
|
|
1840
|
+
if (props.length) {
|
|
1841
|
+
let internalProps = {};
|
|
1842
|
+
for (let prop of props) {
|
|
1843
|
+
internalProps[prop] = obj[prop];
|
|
1844
|
+
Object.defineProperty(obj, prop, {
|
|
1845
|
+
get() {
|
|
1846
|
+
return new Proxy(obj, new ProxyHandler(obj, [], internalProps))[prop];
|
|
1847
|
+
},
|
|
1848
|
+
set(value) {
|
|
1849
|
+
return watch(this)[prop] = value;
|
|
1850
|
+
}
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
|
|
1857
|
+
if (obj?.$proxyHandler)
|
|
1858
|
+
return obj; // It's already a Proxy.
|
|
1859
|
+
|
|
1860
|
+
// This cache doesn't make things faster.
|
|
1861
|
+
// But could it save memory?
|
|
1862
|
+
// let result = proxyCache.get(obj);
|
|
1863
|
+
// if (!result) {
|
|
1864
|
+
// result = new Proxy(obj, new ProxyHandler(obj));
|
|
1865
|
+
// proxyCache.set(obj, result);
|
|
1866
|
+
// }
|
|
1867
|
+
// return result;
|
|
1868
|
+
|
|
1869
|
+
return new Proxy(obj, new ProxyHandler(obj));
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
/**
|
|
1873
|
+
* Provides methods used when a Proxied version of a property is accessed on an object returned by watch() */
|
|
1874
|
+
class ProxyHandler {
|
|
1875
|
+
|
|
1876
|
+
serializedPath;
|
|
1877
|
+
|
|
1878
|
+
/**
|
|
1879
|
+
* @param root An element managed by a NodeGroupManager. The same as the NodeGroupManager's rootEl.
|
|
1880
|
+
* @param path {string[]} Used internally.
|
|
1881
|
+
* @param props */
|
|
1882
|
+
constructor(root, path=[], props=null) {
|
|
1883
|
+
|
|
1884
|
+
this.root = root;
|
|
1885
|
+
this.path = path; // path from root to this Proxy.
|
|
1886
|
+
this.props = props;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
/**
|
|
1890
|
+
* Get the full path to this property from the root watched object.
|
|
1891
|
+
* @param atIndex
|
|
1892
|
+
* @returns {string} */
|
|
1893
|
+
getSerializedPath(atIndex=null) {
|
|
1894
|
+
if (!this.serializedPath)
|
|
1895
|
+
this.serializedPath = JSON.stringify([getObjectId(this.root), ...this.path.map(item => item + '')]);
|
|
1896
|
+
|
|
1897
|
+
if (atIndex!== null)
|
|
1898
|
+
return this.serializedPath.slice(0, -1) + ',"' + atIndex + '"]';
|
|
1899
|
+
return this.serializedPath;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
/**
|
|
1903
|
+
* Return a ProxyHandler for a property one level deeper at pathItem.
|
|
1904
|
+
* @param pathItem {string}
|
|
1905
|
+
* @returns {ProxyHandler} */
|
|
1906
|
+
extend(pathItem) {
|
|
1907
|
+
pathItem += '';
|
|
1908
|
+
assert(!this.root.$proxyHandler);
|
|
1909
|
+
let result = new ProxyHandler(this.root, [...this.path, pathItem], this.props);
|
|
1910
|
+
if (this.serializedPath)
|
|
1911
|
+
result.serializedPath = this.getSerializedPath(pathItem);
|
|
1912
|
+
|
|
1913
|
+
return result;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
/**
|
|
1917
|
+
* Called directly by JavaScript when accessing the value of a property.
|
|
1918
|
+
* @param obj {Object}
|
|
1919
|
+
* @param prop {string} */
|
|
1920
|
+
get(obj, prop) {
|
|
1921
|
+
|
|
1922
|
+
// 1. Special props.
|
|
1923
|
+
if (prop === '$proxyHandler')
|
|
1924
|
+
return this;
|
|
1925
|
+
else if (prop === '$removeProxy')
|
|
1926
|
+
return delve(this.props || this.root, this.path);
|
|
1927
|
+
|
|
1928
|
+
// 2. Array functions.
|
|
1929
|
+
else if (prop === 'map' && Array.isArray(obj)) {
|
|
1930
|
+
|
|
1931
|
+
let ngm = NodeGroupManager.get(this.root);
|
|
1932
|
+
ngm.clearSubscribersIfNeeded();
|
|
1933
|
+
|
|
1934
|
+
return callback => {
|
|
1935
|
+
let loopInfo;
|
|
1936
|
+
|
|
1937
|
+
let children = [];
|
|
1938
|
+
let transformer = obj => {
|
|
1939
|
+
let templates = [];
|
|
1940
|
+
|
|
1941
|
+
for (let i = 0; i < obj.length; i++) {
|
|
1942
|
+
|
|
1943
|
+
// Watch obj[i].
|
|
1944
|
+
let handler = this.extend(i);
|
|
1945
|
+
let item = obj[i];
|
|
1946
|
+
if (!item.$proxyHandler)
|
|
1947
|
+
item = new Proxy(item, handler);
|
|
1948
|
+
|
|
1949
|
+
let template = callback(item, i, obj);
|
|
1950
|
+
templates.push(template);
|
|
1951
|
+
|
|
1952
|
+
// If the loop is re-evaluted via Set() then we add duplicate TemplateInfo's
|
|
1953
|
+
//if (!withinSet) {
|
|
1954
|
+
let spath = this.getSerializedPath(i);
|
|
1955
|
+
let subscriber = new Subscriber(callback, template);
|
|
1956
|
+
subscriber.parent = loopInfo;
|
|
1957
|
+
ngm.subscribers.add(spath, subscriber);
|
|
1958
|
+
children.push([spath, subscriber]);
|
|
1959
|
+
//}
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
// A parent Template that surrounds all the items in the loop.
|
|
1963
|
+
// This lets us get template.nodeGroup.endNode so we can insertBefore().
|
|
1964
|
+
return new Template(['', ''], [templates]);
|
|
1965
|
+
};
|
|
1966
|
+
|
|
1967
|
+
if (!withinSet)
|
|
1968
|
+
loopInfo = new Subscriber(transformer, null, callback);
|
|
1969
|
+
let wholeLoopTemplate = transformer(obj);
|
|
1970
|
+
if (!withinSet) {
|
|
1971
|
+
loopInfo.template = wholeLoopTemplate;
|
|
1972
|
+
loopInfo.children = children;
|
|
1973
|
+
ngm.subscribers.add(this.getSerializedPath(), loopInfo);
|
|
1974
|
+
}
|
|
1975
|
+
return wholeLoopTemplate;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
else if ((prop ==='splice' || prop === 'fastSplice') && Array.isArray(obj)) {
|
|
1980
|
+
let ngm = NodeGroupManager.get(this.root);
|
|
1981
|
+
return (index, deleteCount, ...items) => {
|
|
1982
|
+
let diff = items.length - deleteCount;
|
|
1983
|
+
let objLength = obj.length;
|
|
1984
|
+
|
|
1985
|
+
// Delete
|
|
1986
|
+
if (deleteCount) {
|
|
1987
|
+
for (let i=index; i<index+deleteCount; i++) {
|
|
1988
|
+
|
|
1989
|
+
// Update pathToTemplates
|
|
1990
|
+
let spath = this.getSerializedPath(i);
|
|
1991
|
+
|
|
1992
|
+
// Delete nodes of associated NodeGroups.
|
|
1993
|
+
for (let subscriber of ngm.subscribers.data[spath] || []) {
|
|
1994
|
+
let ng = subscriber.template.nodeGroup;
|
|
1995
|
+
for (let node of ng.getNodes())
|
|
1996
|
+
node.remove();
|
|
1997
|
+
|
|
1998
|
+
// Delete NodeGroup from NodeGroupManager.
|
|
1999
|
+
ngm.nodeGroupsAvailable.delete(ng.exactKey, ng);
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
delete ngm.subscribers.data[spath]; // Deletes templates associated with every loop where this is used.
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
// Update indices of subsequent items.
|
|
2007
|
+
if (diff) {
|
|
2008
|
+
let loopPath = this.getSerializedPath();
|
|
2009
|
+
let loopInfo = [...ngm.subscribers.getAll(loopPath)][0]; // TODO: Handle multiple loops.
|
|
2010
|
+
|
|
2011
|
+
let move = (oldIndex) => {
|
|
2012
|
+
let newIndex = oldIndex+diff;
|
|
2013
|
+
let oldPath = this.getSerializedPath(oldIndex);
|
|
2014
|
+
let newPath = this.getSerializedPath(newIndex);
|
|
2015
|
+
|
|
2016
|
+
let subscribers = ngm.subscribers.data[oldPath];
|
|
2017
|
+
delete ngm.subscribers.data[oldPath]; // TODO: Some can be overwritten w/o being deleted?
|
|
2018
|
+
ngm.subscribers.data[newPath] = subscribers;
|
|
2019
|
+
|
|
2020
|
+
|
|
2021
|
+
// Update associated NodeGroups by passing them newIndex.
|
|
2022
|
+
// This is unnecessary for most loops since they don't use the index.
|
|
2023
|
+
// fastSplice skips this path, it skips updating item indices.
|
|
2024
|
+
if (prop === 'splice') {
|
|
2025
|
+
let array = delve(this.root, this.path);
|
|
2026
|
+
for (let subscriber of subscribers) {
|
|
2027
|
+
let ng = subscriber.template.nodeGroup;
|
|
2028
|
+
|
|
2029
|
+
let item = array[oldIndex];
|
|
2030
|
+
|
|
2031
|
+
//assert(!item.$proxyHandler)
|
|
2032
|
+
let proxyItem = getProxy(item, this, this.path, newIndex); //new Proxy(item, this.extend(newIndex));
|
|
2033
|
+
let exprs = loopInfo.itemTransformer(proxyItem, newIndex).exprs; // TODO: Pass updated array as third argument to transformer.
|
|
2034
|
+
ng.applyExprs(exprs); // this is the slow part.
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
};
|
|
2038
|
+
|
|
2039
|
+
// Iterate in different directions depending on whether diff is positive or negative.
|
|
2040
|
+
if (diff > 0) // Moving items to the right, so we iterate backward from the end.
|
|
2041
|
+
for (let i = objLength-1; i >= index + items.length + deleteCount; i--)
|
|
2042
|
+
move(i);
|
|
2043
|
+
|
|
2044
|
+
else // Moving items to the left, so we iterate forward.
|
|
2045
|
+
for (let i = index + items.length + deleteCount; i < objLength; i++)
|
|
2046
|
+
move(i);
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
|
|
2050
|
+
// Add new items
|
|
2051
|
+
if (items.length) {
|
|
2052
|
+
let loopPath = this.getSerializedPath();
|
|
2053
|
+
|
|
2054
|
+
let beforePath = this.getSerializedPath(index);
|
|
2055
|
+
let ngm = NodeGroupManager.get(this.root);
|
|
2056
|
+
|
|
2057
|
+
for (let loopInfo of ngm.subscribers.getAll(loopPath)) {
|
|
2058
|
+
let beforeNodes = index < objLength - deleteCount
|
|
2059
|
+
? [...ngm.subscribers.getAll(beforePath)].map(t => t.template.nodeGroup.startNode)
|
|
2060
|
+
: [loopInfo.template.nodeGroup.endNode];
|
|
2061
|
+
for (let beforeNode of beforeNodes) { // TODO: Need to match the beforeNg with the loopInfo instead of iterating.
|
|
2062
|
+
for (let i = 0; i < items.length; i++) {
|
|
2063
|
+
|
|
2064
|
+
// Create NodeGroup of new item.
|
|
2065
|
+
let itemHandler = this.extend(index + i);
|
|
2066
|
+
assert(!items[i].$proxyHandler);
|
|
2067
|
+
let proxyItem = new Proxy(items[i], itemHandler);
|
|
2068
|
+
let template = loopInfo.itemTransformer(proxyItem);
|
|
2069
|
+
let ng = ngm.getNodeGroup(template, null, true);
|
|
2070
|
+
|
|
2071
|
+
|
|
2072
|
+
for (let node of ng.getNodes()) {
|
|
2073
|
+
beforeNode.parentNode.insertBefore(node, beforeNode);
|
|
2074
|
+
//loopInfo.template.nodeGroup.endNode = node; // The loop's end node is actually an empty text node, so don't do this.
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
|
|
2078
|
+
// Add new items to ngm.templateInfo
|
|
2079
|
+
let spath = itemHandler.getSerializedPath();
|
|
2080
|
+
let subscriber = new Subscriber(loopInfo.itemTransformer, template);
|
|
2081
|
+
subscriber.parent = loopInfo;
|
|
2082
|
+
ngm.subscribers.add(spath, subscriber);
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
loopInfo.template.nodeGroup.parentPath.clearNodesCache();
|
|
2086
|
+
loopInfo.template.nodeGroup.nodesCache = null;
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
let result = obj.splice(index, deleteCount, ...items);
|
|
2091
|
+
|
|
2092
|
+
//this.notify(this.path);
|
|
2093
|
+
|
|
2094
|
+
return result;
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
// Allow these functions to use proxied objects as arguments.
|
|
2099
|
+
else if (prop ==='indexOf' && Array.isArray(obj)) {
|
|
2100
|
+
return item => {
|
|
2101
|
+
return obj.indexOf(item.$removeProxy || item)
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
// 3. Get property
|
|
2106
|
+
else {
|
|
2107
|
+
|
|
2108
|
+
let obj2 = obj === this.root && this.props ? this.props : obj;
|
|
2109
|
+
let result = Reflect.get(obj2, prop);
|
|
2110
|
+
|
|
2111
|
+
// This is read by watchFunction() which is called in NodeGroup.applyOneExpr().
|
|
2112
|
+
// It's used to see what variables contribute to an expression.
|
|
2113
|
+
if (logGets)
|
|
2114
|
+
gets.push([this.root, ...this.path, prop]);
|
|
2115
|
+
|
|
2116
|
+
// If we're getting an object or array property, apply watch() to it recursively.
|
|
2117
|
+
if (result && typeof result === 'object') {
|
|
2118
|
+
let handler = this.extend(prop); // same root, one level deeper on the path.
|
|
2119
|
+
|
|
2120
|
+
return new Proxy(result, handler);
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
return result;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
|
|
2128
|
+
/**
|
|
2129
|
+
* Called directly by JavaScript when setting the value of a property via equals.
|
|
2130
|
+
* @param obj
|
|
2131
|
+
* @param prop
|
|
2132
|
+
* @param value
|
|
2133
|
+
* @returns {boolean} */
|
|
2134
|
+
set(obj, prop, value) {
|
|
2135
|
+
withinSet++;
|
|
2136
|
+
|
|
2137
|
+
let obj2 = obj === this.root && this.props ? this.props : obj;
|
|
2138
|
+
let result = Reflect.set(obj2, prop, value);
|
|
2139
|
+
let fullPath = [...this.path, prop+''];
|
|
2140
|
+
this.notify(fullPath);
|
|
2141
|
+
|
|
2142
|
+
withinSet --;
|
|
2143
|
+
|
|
2144
|
+
return result;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
/**
|
|
2148
|
+
* Find every subscriber for fullPath, and above, and call applyExprs() for it.
|
|
2149
|
+
* @param fullPath {string[]}
|
|
2150
|
+
* @param excluded {Set} */
|
|
2151
|
+
notify(fullPath, excluded = new Set()) {
|
|
2152
|
+
|
|
2153
|
+
// Traverse upward through the path, looking for pathToTemplates.
|
|
2154
|
+
let ngm = NodeGroupManager.get(this.root);
|
|
2155
|
+
|
|
2156
|
+
let rootHash = getObjectId(this.root);
|
|
2157
|
+
|
|
2158
|
+
let len = fullPath.length;
|
|
2159
|
+
while (len >= 1) {
|
|
2160
|
+
let path = fullPath.slice(0, len);
|
|
2161
|
+
let val = delve(this.root, path);
|
|
2162
|
+
let serializedPath = JSON.stringify([rootHash, ...path]);
|
|
2163
|
+
for (let subscriber of ngm.subscribers.getAll(serializedPath)) {
|
|
2164
|
+
// We already applied expressions for a single item within this loop.
|
|
2165
|
+
if (excluded.has(subscriber))
|
|
2166
|
+
continue;
|
|
2167
|
+
|
|
2168
|
+
|
|
2169
|
+
// Delete child subscriptions so we don't have duplicate subscriptions when we call applyExprs() directly below.
|
|
2170
|
+
if (subscriber.children) {
|
|
2171
|
+
for (let [spath, childInfo] of subscriber.children)
|
|
2172
|
+
ngm.subscribers.delete(spath, childInfo);
|
|
2173
|
+
subscriber.children = undefined;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
let proxyVal = getProxy(val, this, path);
|
|
2177
|
+
let exprs = subscriber.transformer(proxyVal).exprs; // TODO: Pass updated array as third argument to transformer.
|
|
2178
|
+
for (let path of subscriber.template.nodeGroup.paths)
|
|
2179
|
+
path.clearNodesCache();
|
|
2180
|
+
|
|
2181
|
+
// Apply expressions.
|
|
2182
|
+
subscriber.template.nodeGroup.applyExprs(exprs);
|
|
2183
|
+
|
|
2184
|
+
// Don't also process parent loop after updating a single item within it.
|
|
2185
|
+
if (subscriber.parent)
|
|
2186
|
+
excluded.add(subscriber.parent);
|
|
2187
|
+
}
|
|
2188
|
+
len--;
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
function getProxy(obj, ph, path, path2) {
|
|
2194
|
+
if (!obj || !typeof obj !== 'object')
|
|
2195
|
+
return obj;
|
|
2196
|
+
|
|
2197
|
+
if (obj.$proxyHandler) {
|
|
2198
|
+
|
|
2199
|
+
assert(obj.$proxyHandler.root === ph.root && JSON.stringify(obj.$proxyHandler.path) === JSON.stringify(path));
|
|
2200
|
+
return obj;
|
|
2201
|
+
}
|
|
2202
|
+
if (path2)
|
|
2203
|
+
path = [...path, path2+''];
|
|
2204
|
+
return new Proxy(obj, new ProxyHandler(ph.root, path, ph.props));
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
/**
|
|
2208
|
+
* Call a function and record which watched variables it accesess, storing their paths in pathToTemplates.
|
|
2209
|
+
* Used by NodeGroup.applyOneExpr().
|
|
2210
|
+
* TODO: only allow this to be called once per callback.
|
|
2211
|
+
* @param callback {function}
|
|
2212
|
+
* @param ngm {NodeGroupManager}
|
|
2213
|
+
* @returns {Template} */
|
|
2214
|
+
function watchFunction(callback, ngm) {
|
|
2215
|
+
ngm.clearSubscribersIfNeeded();
|
|
2216
|
+
|
|
2217
|
+
logGets = true;
|
|
2218
|
+
|
|
2219
|
+
let transformer = () => new Template(['', ''], [callback()]);
|
|
2220
|
+
let template = transformer();
|
|
2221
|
+
for (let path of gets) {
|
|
2222
|
+
let subscriber = new Subscriber(transformer, template);
|
|
2223
|
+
ngm.subscribers.add(serializePath(path), subscriber);
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
gets = [];
|
|
2227
|
+
logGets = false;
|
|
2228
|
+
return template;
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
/**
|
|
2232
|
+
* Represents a place where nodes will be updated.
|
|
2233
|
+
* TODO: Merge this with Template, or ExprPath? */
|
|
2234
|
+
class Subscriber {
|
|
2235
|
+
|
|
2236
|
+
/** @type {Subscriber} Used only for children of a loop. */
|
|
2237
|
+
parent;
|
|
2238
|
+
|
|
2239
|
+
/** @type {Subscriber[]} TemplateInfo for each child of a loop. */
|
|
2240
|
+
children;
|
|
2241
|
+
|
|
2242
|
+
/**
|
|
2243
|
+
* @param transformer {function} Function that turns the object at the path into a template.
|
|
2244
|
+
* @param template {Template}
|
|
2245
|
+
* @param itemTransformer {function} If a loop, this transforms each item in the loop. */
|
|
2246
|
+
constructor(transformer, template, itemTransformer=null) {
|
|
2247
|
+
this.transformer = transformer;
|
|
2248
|
+
this.template = template;
|
|
2249
|
+
|
|
2250
|
+
// Used only for loops
|
|
2251
|
+
this.itemTransformer = itemTransformer;
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
/** @typedef {boolean|string|number|function|Object|Array|Date|Node} Expr */
|
|
2256
|
+
|
|
2257
|
+
/**
|
|
2258
|
+
* A group of Nodes instantiated from a Shell, with Expr's filled in.
|
|
2259
|
+
*
|
|
2260
|
+
* The range is determined by startNode and nodeMarker.
|
|
2261
|
+
* startNode - never null. An empty text node is created before the first path if none exists.
|
|
2262
|
+
* nodeMarker - null if this Nodegroup is at the end of its parents' nodes.
|
|
2263
|
+
*
|
|
2264
|
+
*
|
|
2265
|
+
* */
|
|
2266
|
+
class NodeGroup {
|
|
2267
|
+
|
|
2268
|
+
/** @Type {NodeGroupManager} */
|
|
2269
|
+
manager;
|
|
2270
|
+
|
|
2271
|
+
/** @type {ExprPath} */
|
|
2272
|
+
parentPath;
|
|
2273
|
+
|
|
2274
|
+
/** @type {Node} First node of NodeGroup. Should never be null. */
|
|
2275
|
+
startNode;
|
|
2276
|
+
|
|
2277
|
+
/** @type {Node} A node that never changes that this NodeGroup should always insert its nodes before.
|
|
2278
|
+
* An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.*/
|
|
2279
|
+
endNode;
|
|
2280
|
+
|
|
2281
|
+
/** @type {ExprPath[]} */
|
|
2282
|
+
paths = [];
|
|
2283
|
+
|
|
2284
|
+
/** @type {string} */
|
|
2285
|
+
exactKey;
|
|
2286
|
+
|
|
2287
|
+
/** @type {string} */
|
|
2288
|
+
closeKey;
|
|
2289
|
+
|
|
2290
|
+
/** @type {boolean} Used by NodeGroupManager. */
|
|
2291
|
+
inUse;
|
|
2292
|
+
|
|
2293
|
+
|
|
2294
|
+
/**
|
|
2295
|
+
* @internal
|
|
2296
|
+
* @type {Node[]} Cached result of getNodes() used only for improving performance.*/
|
|
2297
|
+
nodesCache;
|
|
2298
|
+
|
|
2299
|
+
/**
|
|
2300
|
+
* @type {?Map<HTMLStyleElement, string>} */
|
|
2301
|
+
styles;
|
|
2302
|
+
|
|
2303
|
+
/**
|
|
2304
|
+
* If rendering a Template with replaceMode=true, pseudoRoot points to the element where the attributes are rendered.
|
|
2305
|
+
* But pseudoRoot is outside of this.getNodes().
|
|
2306
|
+
* NodeGroupManager.render() copies the attributes from pseudoRoot to the actual web component root element.
|
|
2307
|
+
* @type {?HTMLElement} */
|
|
2308
|
+
pseudoRoot;
|
|
2309
|
+
|
|
2310
|
+
currentComponentProps = {};
|
|
2311
|
+
|
|
2312
|
+
|
|
2313
|
+
/**
|
|
2314
|
+
* Create an "instantiated" NodeGroup from a Template and add it to an element.
|
|
2315
|
+
* @param template {Template} Create it from the html strings and expressions in this template.
|
|
2316
|
+
* @param manager {?NodeGroupManager}
|
|
2317
|
+
* @returns {NodeGroup} */
|
|
2318
|
+
constructor(template, manager=null) {
|
|
2319
|
+
|
|
2320
|
+
// Used for forEach()
|
|
2321
|
+
this.template = template;
|
|
2322
|
+
this.manager = manager;
|
|
2323
|
+
|
|
2324
|
+
// new!
|
|
2325
|
+
template.nodeGroup = this;
|
|
2326
|
+
|
|
2327
|
+
// Get a cached version of the parsed and instantiated html, and ExprPaths.
|
|
2328
|
+
let shell = Shell.get(template.html);
|
|
2329
|
+
|
|
2330
|
+
let fragment = shell.fragment.cloneNode(true);
|
|
2331
|
+
|
|
2332
|
+
// Figure out value of replaceMode option if it isn't set,
|
|
2333
|
+
// Assume replaceMode if there's only one child element and its tagname matches the root el.
|
|
2334
|
+
let replaceMode = typeof template.replaceMode === 'boolean'
|
|
2335
|
+
? template.replaceMode
|
|
2336
|
+
: fragment.children.length===1 &&
|
|
2337
|
+
fragment.firstElementChild?.tagName.replace(/-REDCOMPONENT-PLACEHOLDER$/, '')
|
|
2338
|
+
=== manager?.rootEl?.tagName;
|
|
2339
|
+
if (replaceMode) {
|
|
2340
|
+
this.pseudoRoot = fragment.firstElementChild;
|
|
2341
|
+
// if (!manager.rootEl)
|
|
2342
|
+
// manager.rootEl = this.pseudoRoot;
|
|
2343
|
+
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
let childNodes = replaceMode
|
|
2347
|
+
? fragment.firstElementChild.childNodes
|
|
2348
|
+
: fragment.childNodes;
|
|
2349
|
+
|
|
2350
|
+
|
|
2351
|
+
this.startNode = childNodes[0];
|
|
2352
|
+
this.endNode = childNodes[childNodes.length - 1];
|
|
2353
|
+
|
|
2354
|
+
|
|
2355
|
+
// Update paths
|
|
2356
|
+
for (let oldPath of shell.paths) {
|
|
2357
|
+
let path = oldPath.clone(fragment);
|
|
2358
|
+
path.parentNg = this;
|
|
2359
|
+
this.paths.push(path);
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
|
|
2363
|
+
// Update web component placeholders.
|
|
2364
|
+
// Ctrl+F "redcomponent-placeholder" in project to find all code that manages subcomponents.
|
|
2365
|
+
// Is this list needed at all?
|
|
2366
|
+
//for (let component of shell.components)
|
|
2367
|
+
// this.components.push(resolveNodePath(this.startNode.parentNode, getNodePath(component)))
|
|
2368
|
+
|
|
2369
|
+
|
|
2370
|
+
|
|
2371
|
+
this.activateEmbeds(fragment, shell);
|
|
2372
|
+
|
|
2373
|
+
|
|
2374
|
+
|
|
2375
|
+
// Apply exprs
|
|
2376
|
+
this.applyExprs(template.exprs);
|
|
2377
|
+
|
|
2378
|
+
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
activateEmbeds(root, shell) {
|
|
2382
|
+
|
|
2383
|
+
// static components
|
|
2384
|
+
// Must happen before ids.
|
|
2385
|
+
for (let path of shell.staticComponents) {
|
|
2386
|
+
let el = resolveNodePath(root, path);
|
|
2387
|
+
|
|
2388
|
+
// Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
|
|
2389
|
+
if (el.tagName !== this.pseudoRoot?.tagName)
|
|
2390
|
+
this.createNewComponent(el);
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
if (this.manager.rootEl) {
|
|
2394
|
+
|
|
2395
|
+
// ids
|
|
2396
|
+
if (this.manager.options.ids !== false)
|
|
2397
|
+
for (let path of shell.ids) {
|
|
2398
|
+
let el = resolveNodePath(root, path);
|
|
2399
|
+
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
2400
|
+
this.manager.rootEl[id] = el;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
// styles
|
|
2404
|
+
if (this.manager.options.styles !== false) {
|
|
2405
|
+
if (shell.styles.length)
|
|
2406
|
+
this.styles = new Map();
|
|
2407
|
+
for (let path of shell.styles) {
|
|
2408
|
+
let style = resolveNodePath(root, path);
|
|
2409
|
+
Util.bindStyles(style, this.manager.rootEl);
|
|
2410
|
+
this.styles.set(style, style.textContent);
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
}
|
|
2414
|
+
// scripts
|
|
2415
|
+
if (this.manager.options.scripts !== false) {
|
|
2416
|
+
for (let path of shell.scripts) {
|
|
2417
|
+
let script = resolveNodePath(root, path);
|
|
2418
|
+
eval(script.textContent);
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
updateStyles() {
|
|
2425
|
+
if (this.styles)
|
|
2426
|
+
for (let [style, oldText] of this.styles) {
|
|
2427
|
+
let newText = style.textContent;
|
|
2428
|
+
if (oldText !== newText)
|
|
2429
|
+
Util.bindStyles(style, this.manager.rootEl);
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
/**
|
|
2434
|
+
* Use the paths to insert the given expressions.
|
|
2435
|
+
* Dispatches expression handling to other functions depending on the path type.
|
|
2436
|
+
* @param exprs {(*|*[]|function|Template)[]} */
|
|
2437
|
+
applyExprs(exprs) {
|
|
2438
|
+
|
|
2439
|
+
|
|
2440
|
+
// Update exprs at paths.
|
|
2441
|
+
let exprIndex = exprs.length-1, expr, lastNode;
|
|
2442
|
+
|
|
2443
|
+
// We apply them in reverse order so that a <select> box has its options created from an expression
|
|
2444
|
+
// before its value attribute is set via an expression.
|
|
2445
|
+
for (let path of this.paths.toReversed()) {
|
|
2446
|
+
expr = exprs[exprIndex];
|
|
2447
|
+
|
|
2448
|
+
// Nodes
|
|
2449
|
+
if (path.type === PathType.Content) {
|
|
2450
|
+
this.applyNodeExpr(path, expr);
|
|
2451
|
+
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
// Attributes
|
|
2455
|
+
else {
|
|
2456
|
+
let node = path.nodeMarker; // path.resolve(result);
|
|
2457
|
+
let node2 = (this.manager.rootEl && node === this.pseudoRoot) ? this.manager.rootEl : node;
|
|
2458
|
+
|
|
2459
|
+
|
|
2460
|
+
// This is necessary both here and below.
|
|
2461
|
+
if (lastNode && lastNode !== this.pseudoRoot && lastNode !== node && Object.keys(this.currentComponentProps).length) {
|
|
2462
|
+
this.applyComponentExprs(lastNode, this.currentComponentProps);
|
|
2463
|
+
this.currentComponentProps = {};
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
if (path.type === PathType.Multiple)
|
|
2467
|
+
path.applyMultipleAttribs(node2, expr);
|
|
2468
|
+
|
|
2469
|
+
// Capture attribute expressions to later send to the constructor of a web component.
|
|
2470
|
+
// Ctrl+F "redcomponent-placeholder" in project to find all code that manages subcomponents.
|
|
2471
|
+
else if (path.nodeMarker !== this.pseudoRoot && path.type === PathType.Component)
|
|
2472
|
+
this.currentComponentProps[path.attrName] = expr;
|
|
2473
|
+
|
|
2474
|
+
else if (path.type === PathType.Comment) ;
|
|
2475
|
+
else {
|
|
2476
|
+
|
|
2477
|
+
// Event attribute value
|
|
2478
|
+
if (path.attrValue===null && (typeof expr === 'function' || Array.isArray(expr)) && isEvent(path.attrName)) {
|
|
2479
|
+
let root = this.manager.rootEl || this.startNode.parentNode; // latter is used when constructing a whole element.
|
|
2480
|
+
path.applyEventAttrib(node2, expr, root);
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
// Regular attribute value.
|
|
2484
|
+
else
|
|
2485
|
+
exprIndex = path.applyValueAttrib(node2, exprs, exprIndex);
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
lastNode = path.nodeMarker;
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
exprIndex--;
|
|
2492
|
+
} // end for(path of this.paths)
|
|
2493
|
+
|
|
2494
|
+
|
|
2495
|
+
// Check again after we iterate through all paths to apply to a component.
|
|
2496
|
+
if (lastNode && lastNode !== this.pseudoRoot && Object.keys(this.currentComponentProps).length) {
|
|
2497
|
+
this.applyComponentExprs(lastNode, this.currentComponentProps);
|
|
2498
|
+
this.currentComponentProps = {};
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
this.updateStyles();
|
|
2502
|
+
|
|
2503
|
+
// Invalidate the nodes cache because we just changed it.
|
|
2504
|
+
this.nodesCache = null;
|
|
2505
|
+
|
|
2506
|
+
// If there's leftover expressions, there's probably an issue with the Shell that created this NodeGroup,
|
|
2507
|
+
// and the number of paths not matching.
|
|
2508
|
+
|
|
2509
|
+
|
|
2510
|
+
|
|
2511
|
+
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
/**
|
|
2515
|
+
* Insert/replace the nodes created by a single expression.
|
|
2516
|
+
* Called by applyExprs()
|
|
2517
|
+
* This function is recursive, as the functions it calls also call it.
|
|
2518
|
+
* TODO: Move this to ExprPath?
|
|
2519
|
+
* @param path {ExprPath}
|
|
2520
|
+
* @param expr {Expr}
|
|
2521
|
+
* @return {Node[]} New Nodes created. */
|
|
2522
|
+
applyNodeExpr(path, expr) {
|
|
2523
|
+
|
|
2524
|
+
|
|
2525
|
+
/** @type {(Node|NodeGroup|Expr)[]} */
|
|
2526
|
+
let newNodes = [];
|
|
2527
|
+
let oldNodeGroups = path.nodeGroups;
|
|
2528
|
+
|
|
2529
|
+
let secondPass = []; // indices
|
|
2530
|
+
|
|
2531
|
+
// First Pass
|
|
2532
|
+
//for (let ng of path.nodeGroups) // TODO: Is this necessary?
|
|
2533
|
+
// ng.parentPath = null;
|
|
2534
|
+
path.nodeGroups = [];
|
|
2535
|
+
this.applyOneExpr(expr, path, newNodes, secondPass);
|
|
2536
|
+
this.existingTextNodes = null;
|
|
2537
|
+
|
|
2538
|
+
// TODO: Create an array of old vs Nodes and NodeGroups together.
|
|
2539
|
+
// If they're all the same, skip the next steps.
|
|
2540
|
+
// Or calculate it in the loop above as we go? Have a path.lastNodeGroups property?
|
|
2541
|
+
|
|
2542
|
+
// Second pass to find close-match NodeGroups.
|
|
2543
|
+
let flatten = false;
|
|
2544
|
+
if (secondPass.length) {
|
|
2545
|
+
for (let [nodesIndex, ngIndex] of secondPass) {
|
|
2546
|
+
let ng = this.manager.getNodeGroup(newNodes[nodesIndex], false);
|
|
2547
|
+
|
|
2548
|
+
ng.parentPath = path;
|
|
2549
|
+
let ngNodes = ng.getNodes();
|
|
2550
|
+
|
|
2551
|
+
|
|
2552
|
+
|
|
2553
|
+
if (ngNodes.length === 1)
|
|
2554
|
+
newNodes[nodesIndex] = ngNodes[0];
|
|
2555
|
+
|
|
2556
|
+
else {
|
|
2557
|
+
newNodes[nodesIndex] = ngNodes;
|
|
2558
|
+
flatten = true;
|
|
2559
|
+
}
|
|
2560
|
+
path.nodeGroups[ngIndex] = ng;
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
if (flatten)
|
|
2564
|
+
newNodes = newNodes.flat(); // TODO: Only if second pass happens?
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
|
|
2568
|
+
|
|
2569
|
+
|
|
2570
|
+
|
|
2571
|
+
let oldNodes = path.getNodes();
|
|
2572
|
+
path.nodesCache = newNodes; // Replaces value set by path.getNodes()
|
|
2573
|
+
|
|
2574
|
+
|
|
2575
|
+
// This pre-check makes it a few percent faster?
|
|
2576
|
+
let diff = findArrayDiff(oldNodes, newNodes);
|
|
2577
|
+
if (diff !== false) {
|
|
2578
|
+
|
|
2579
|
+
if (this.parentPath)
|
|
2580
|
+
this.parentPath.clearNodesCache();
|
|
2581
|
+
|
|
2582
|
+
// Fast clear method
|
|
2583
|
+
let isNowEmpty = oldNodes.length && !newNodes.length;
|
|
2584
|
+
if (!isNowEmpty || !path.fastClear(oldNodes, newNodes))
|
|
2585
|
+
|
|
2586
|
+
// Rearrange nodes.
|
|
2587
|
+
udomdiff(path.parentNode, oldNodes, newNodes, path.nodeMarker);
|
|
2588
|
+
|
|
2589
|
+
this.saveOrphans(oldNodeGroups, oldNodes);
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
/**
|
|
2595
|
+
* Find NodeGroups that had their nodes removed and add those nodes to a Fragment so
|
|
2596
|
+
* they're not lost forever and the NodeGroup's internal structure is still consistent.
|
|
2597
|
+
* Called from NodeGroup.applyNodeExpr().
|
|
2598
|
+
* @param oldNodeGroups {NodeGroup[]}
|
|
2599
|
+
* @param oldNodes {Node[]} */
|
|
2600
|
+
saveOrphans(oldNodeGroups, oldNodes) {
|
|
2601
|
+
let oldNgMap = new Map();
|
|
2602
|
+
for (let ng of oldNodeGroups) {
|
|
2603
|
+
oldNgMap.set(ng.startNode, ng);
|
|
2604
|
+
|
|
2605
|
+
// TODO: Is this necessary?
|
|
2606
|
+
// if (ng.parentPath)
|
|
2607
|
+
// ng.parentPath.clearNodesCache();
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
for (let i=0, node; node = oldNodes[i]; i++) {
|
|
2611
|
+
let ng;
|
|
2612
|
+
if (!node.parentNode && (ng = oldNgMap.get(node))) {
|
|
2613
|
+
let fragment = document.createDocumentFragment();
|
|
2614
|
+
let endNode = ng.endNode;
|
|
2615
|
+
while (node !== endNode) {
|
|
2616
|
+
fragment.append(node);
|
|
2617
|
+
i++;
|
|
2618
|
+
node = oldNodes[i];
|
|
2619
|
+
}
|
|
2620
|
+
fragment.append(endNode);
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
// TODO: Move to ExprPath?
|
|
2626
|
+
applyOneExpr(expr, path, newNodes, secondPass) {
|
|
2627
|
+
|
|
2628
|
+
if (expr instanceof Template) {
|
|
2629
|
+
expr.parentPath = path;
|
|
2630
|
+
expr.nodegroup = this;
|
|
2631
|
+
|
|
2632
|
+
//if (window.debug && expr.exprs[0] === 'Banana' && path.nodeGroups.length === 0)
|
|
2633
|
+
//if (window.debug && expr.exprs[0] === 'Banana')
|
|
2634
|
+
// debugger;
|
|
2635
|
+
|
|
2636
|
+
let ng = this.manager.getNodeGroup(expr, true);
|
|
2637
|
+
|
|
2638
|
+
|
|
2639
|
+
if (ng) {
|
|
2640
|
+
|
|
2641
|
+
|
|
2642
|
+
|
|
2643
|
+
// TODO: Track ranges of changed nodes and only pass those to udomdiff?
|
|
2644
|
+
// But will that break the swap benchmark?
|
|
2645
|
+
newNodes.push(...ng.getNodes());
|
|
2646
|
+
path.nodeGroups.push(ng);
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
// If expression, evaluate later to find partial match.
|
|
2650
|
+
else {
|
|
2651
|
+
secondPass.push([newNodes.length, path.nodeGroups.length]);
|
|
2652
|
+
newNodes.push(expr);
|
|
2653
|
+
path.nodeGroups.push(null); // placeholder
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
// Node created by an expression.
|
|
2658
|
+
else if (expr instanceof Node) {
|
|
2659
|
+
|
|
2660
|
+
// DocumentFragment created by an expression.
|
|
2661
|
+
if (expr instanceof DocumentFragment)
|
|
2662
|
+
newNodes.push(...expr.childNodes);
|
|
2663
|
+
else
|
|
2664
|
+
newNodes.push(expr);
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
else if (Array.isArray(expr))
|
|
2668
|
+
for (let subExpr of expr)
|
|
2669
|
+
this.applyOneExpr(subExpr, path, newNodes, secondPass);
|
|
2670
|
+
|
|
2671
|
+
else if (typeof expr === 'function') {
|
|
2672
|
+
expr = watchFunction(expr, this.manager);
|
|
2673
|
+
|
|
2674
|
+
this.applyOneExpr(expr, path, newNodes, secondPass);
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
// Text
|
|
2678
|
+
else {
|
|
2679
|
+
// Convert falsy values (but not 0) to empty string.
|
|
2680
|
+
// Convert numbers to string so they compare the same.
|
|
2681
|
+
let text = (expr === undefined || expr === false || expr === null) ? '' : expr + '';
|
|
2682
|
+
|
|
2683
|
+
// Fast path for updating the text of a single text node.
|
|
2684
|
+
let first = path.nodeBefore.nextSibling;
|
|
2685
|
+
if (first.nodeType === 3 && first.nextSibling === path.nodeMarker && !newNodes.includes(first)) {
|
|
2686
|
+
if (first.textContent !== text)
|
|
2687
|
+
first.textContent = text;
|
|
2688
|
+
|
|
2689
|
+
newNodes.push(first);
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
else {
|
|
2693
|
+
// TODO: Optimize this into a Set or Map or something?
|
|
2694
|
+
if (!this.existingTextNodes)
|
|
2695
|
+
this.existingTextNodes = path.getNodes().filter(n => n.nodeType === 3);
|
|
2696
|
+
|
|
2697
|
+
let idx = this.existingTextNodes.findIndex(n => n.textContent === text);
|
|
2698
|
+
if (idx !== -1)
|
|
2699
|
+
newNodes.push(...this.existingTextNodes.splice(idx, 1));
|
|
2700
|
+
else
|
|
2701
|
+
newNodes.push(path.parentNode.ownerDocument.createTextNode(text));
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
/**
|
|
2707
|
+
* Create a nested RedComponent or call render with the new props.
|
|
2708
|
+
* @param el {Solarite:HTMLElement}
|
|
2709
|
+
* @param props {Object} */
|
|
2710
|
+
applyComponentExprs(el, props) {
|
|
2711
|
+
|
|
2712
|
+
// TODO: Does a hash of this already exist somewhere?
|
|
2713
|
+
// Perhaps if Components were treated as child NodeGroups, which would need to be the child of an ExprPath,
|
|
2714
|
+
// then we could re-use the hash and logic from NodeManager?
|
|
2715
|
+
let newHash = getObjectHash(props);
|
|
2716
|
+
|
|
2717
|
+
let isPreHtmlElement = el.tagName.endsWith('-REDCOMPONENT-PLACEHOLDER');
|
|
2718
|
+
let isPreIsElement = el.hasAttribute('_is');
|
|
2719
|
+
|
|
2720
|
+
|
|
2721
|
+
// Instantiate a placeholder.
|
|
2722
|
+
if (isPreHtmlElement || isPreIsElement)
|
|
2723
|
+
el = this.createNewComponent(el, isPreHtmlElement, props);
|
|
2724
|
+
|
|
2725
|
+
// Update params of placeholder.
|
|
2726
|
+
else if (el.render) {
|
|
2727
|
+
let oldHash = componentHash.get(el);
|
|
2728
|
+
if (oldHash !== newHash)
|
|
2729
|
+
el.render(props); // Pass new values of props to render so it can decide how it wants to respond.
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
componentHash.set(el, newHash);
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
/**
|
|
2736
|
+
* We swap the placeholder element for the real element so we can pass its dynamic attributes
|
|
2737
|
+
* to its constructor.
|
|
2738
|
+
*
|
|
2739
|
+
* The logic of this function is complex and could use cleaning up.
|
|
2740
|
+
*
|
|
2741
|
+
* @param el
|
|
2742
|
+
* @param isPreHtmlElement
|
|
2743
|
+
* @param props {Object} Attributes with dynamic values.
|
|
2744
|
+
* @return {HTMLElement} */
|
|
2745
|
+
createNewComponent(el, isPreHtmlElement=undefined, props=undefined) {
|
|
2746
|
+
if (isPreHtmlElement === undefined)
|
|
2747
|
+
isPreHtmlElement = !el.hasAttribute('_is');
|
|
2748
|
+
|
|
2749
|
+
let tagName = (isPreHtmlElement
|
|
2750
|
+
? el.tagName.endsWith('-REDCOMPONENT-PLACEHOLDER')
|
|
2751
|
+
? el.tagName.slice(0, -25)
|
|
2752
|
+
: el.tagName
|
|
2753
|
+
: el.getAttribute('is')).toLowerCase();
|
|
2754
|
+
|
|
2755
|
+
let dynamicProps = {...(props || {})};
|
|
2756
|
+
|
|
2757
|
+
// Pass other attribs to constructor, since otherwise they're not yet set on the element,
|
|
2758
|
+
// and the constructor would otherwise have no way to see them.
|
|
2759
|
+
if (el.attributes.length) {
|
|
2760
|
+
if (!props)
|
|
2761
|
+
props = {};
|
|
2762
|
+
for (let attrib of el.attributes)
|
|
2763
|
+
if (!props.hasOwnProperty(attrib.name))
|
|
2764
|
+
props[attrib.name] = attrib.value;
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
// Create CustomElement and
|
|
2768
|
+
let Constructor = customElements.get(tagName);
|
|
2769
|
+
if (!Constructor)
|
|
2770
|
+
throw new Error(`The custom tag name ${tagName} is not registered.`)
|
|
2771
|
+
|
|
2772
|
+
// We pass the childNodes to the constructor so it can know about them,
|
|
2773
|
+
// instead of only afterward when they're appended to the slot below.
|
|
2774
|
+
// This is useful for a custom selectbox, for example.
|
|
2775
|
+
// NodeGroupManager.pendingChildren stores the childen so the super construtor call to Solarite's constructor
|
|
2776
|
+
// can add them as children before the rest of the constructor code executes.
|
|
2777
|
+
let ch = [... el.childNodes];
|
|
2778
|
+
NodeGroupManager.pendingChildren.push(ch); // pop() is called in Solarite constructor.
|
|
2779
|
+
let newEl = new Constructor(props, ch);
|
|
2780
|
+
|
|
2781
|
+
if (!isPreHtmlElement)
|
|
2782
|
+
newEl.setAttribute('is', el.getAttribute('is').toLowerCase());
|
|
2783
|
+
el.replaceWith(newEl);
|
|
2784
|
+
|
|
2785
|
+
// Set children / slot children
|
|
2786
|
+
// TODO: Match named slots.
|
|
2787
|
+
// TODO: This only appends to slot if render() is called in the constructor.
|
|
2788
|
+
//let slot = newEl.querySelector('slot') || newEl;
|
|
2789
|
+
//slot.append(...el.childNodes);
|
|
2790
|
+
|
|
2791
|
+
// Copy over event attributes.
|
|
2792
|
+
for (let propName in props) {
|
|
2793
|
+
let val = props[propName];
|
|
2794
|
+
if (propName.startsWith('on') && typeof val === 'function')
|
|
2795
|
+
newEl.addEventListener(propName.slice(2), e => val(e, newEl));
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
// If an id pointed at the placeholder, update it to point to the new element.
|
|
2799
|
+
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
2800
|
+
if (id)
|
|
2801
|
+
this.manager.rootEl[id] = newEl;
|
|
2802
|
+
|
|
2803
|
+
|
|
2804
|
+
// Update paths to use replaced element.
|
|
2805
|
+
for (let path of this.paths) {
|
|
2806
|
+
if (path.nodeMarker === el)
|
|
2807
|
+
path.nodeMarker = newEl;
|
|
2808
|
+
if (path.nodeBefore === el)
|
|
2809
|
+
path.nodeBefore = newEl;
|
|
2810
|
+
}
|
|
2811
|
+
if (this.startNode === el)
|
|
2812
|
+
this.startNode = newEl;
|
|
2813
|
+
if (this.endNode === el)
|
|
2814
|
+
this.endNode = newEl;
|
|
2815
|
+
|
|
2816
|
+
|
|
2817
|
+
// applyComponentExprs() is called because we're rendering.
|
|
2818
|
+
// So we want to render the sub-component also.
|
|
2819
|
+
if (newEl.renderFirstTime)
|
|
2820
|
+
newEl.renderFirstTime();
|
|
2821
|
+
|
|
2822
|
+
// Copy attributes over.
|
|
2823
|
+
for (let attrib of el.attributes)
|
|
2824
|
+
if (attrib.name !== '_is')
|
|
2825
|
+
newEl.setAttribute(attrib.name, attrib.value);
|
|
2826
|
+
|
|
2827
|
+
// Set dynamic attributes if they are primitive types.
|
|
2828
|
+
for (let name in dynamicProps) {
|
|
2829
|
+
let val = dynamicProps[name];
|
|
2830
|
+
if (typeof val === 'boolean') {
|
|
2831
|
+
if (val !== false && val !== undefined && val !== null)
|
|
2832
|
+
newEl.setAttribute(name, '');
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
// If type isn't an object or array, set the attribute.
|
|
2836
|
+
else if (['number', 'bigint', 'string'].includes(typeof val))
|
|
2837
|
+
newEl.setAttribute(name, val);
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
return newEl;
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
/**
|
|
2844
|
+
* Get all the nodes inclusive between startNode and endNode.
|
|
2845
|
+
* TODO: when not using nodesCache, could this use less memory with yield?
|
|
2846
|
+
* But we'd need to save the reference to the next Node in case it's removed.
|
|
2847
|
+
* @return {(Node|HTMLElement)[]} */
|
|
2848
|
+
getNodes() {
|
|
2849
|
+
// applyExprs() invalidates this cache.
|
|
2850
|
+
let result = this.nodesCache;
|
|
2851
|
+
if (result) // This does speed up the partialUpdate benchmark by 10-15%.
|
|
2852
|
+
return result;
|
|
2853
|
+
|
|
2854
|
+
result = [];
|
|
2855
|
+
let current = this.startNode;
|
|
2856
|
+
let afterLast = this.endNode?.nextSibling;
|
|
2857
|
+
while (current && current !== afterLast) {
|
|
2858
|
+
result.push(current);
|
|
2859
|
+
current = current.nextSibling;
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
this.nodesCache = result;
|
|
2863
|
+
return result;
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
getParentNode() {
|
|
2867
|
+
return this.startNode?.parentNode
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
|
|
2871
|
+
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
|
|
2875
|
+
let componentHash = new WeakMap();
|
|
2876
|
+
|
|
2877
|
+
/**
|
|
2878
|
+
* @typedef {Object} RenderOptions
|
|
2879
|
+
* @property {boolean=} styles - Indicates whether the Courage component is present.
|
|
2880
|
+
* @property {boolean=} scripts - Indicates whether the Power component is present.
|
|
2881
|
+
* @property {boolean=} ids
|
|
2882
|
+
*
|
|
2883
|
+
* @property {?boolean} render
|
|
2884
|
+
* Used only when options are given to a class super constructor inheriting from Solarite.
|
|
2885
|
+
* True to call render() immediately in super constructor.
|
|
2886
|
+
* False to automatically call render() at all.
|
|
2887
|
+
* Undefined (default) to call render() when added to the DOM, unless already rendered.
|
|
2888
|
+
*/
|
|
2889
|
+
|
|
2890
|
+
|
|
2891
|
+
/**
|
|
2892
|
+
* Manage all the NodeGroups for a single WebComponent or root HTMLElement
|
|
2893
|
+
* There's one NodeGroup for the root of the WebComponent, and one for every ${...} expression that creates Node children.
|
|
2894
|
+
* And each NodeGroup manages the one or more nodes created by the expression.
|
|
2895
|
+
*
|
|
2896
|
+
* An instance of this class exists for each element that r() renders to. */
|
|
2897
|
+
class NodeGroupManager {
|
|
2898
|
+
|
|
2899
|
+
/** @type {HTMLElement|DocumentFragment} */
|
|
2900
|
+
rootEl;
|
|
2901
|
+
|
|
2902
|
+
/** @type {NodeGroup} */
|
|
2903
|
+
rootNg;
|
|
2904
|
+
|
|
2905
|
+
/** @type {Change[]} */
|
|
2906
|
+
changes = [];
|
|
2907
|
+
|
|
2908
|
+
|
|
2909
|
+
|
|
2910
|
+
|
|
2911
|
+
|
|
2912
|
+
/**
|
|
2913
|
+
* A map from the html strings and exprs that created a node group, to the NodeGroup.
|
|
2914
|
+
* Also stores a map from just the html strings to the NodeGroup, so we can still find a similar match if the exprs changed.
|
|
2915
|
+
*
|
|
2916
|
+
* @type {MultiValueMap<string, (string|Template)[], NodeGroup>} */
|
|
2917
|
+
nodeGroupsAvailable = new MultiValueMap();
|
|
2918
|
+
nodeGroupsInUse = [];
|
|
2919
|
+
|
|
2920
|
+
|
|
2921
|
+
/** @type {RenderOptions} */
|
|
2922
|
+
options = {};
|
|
2923
|
+
|
|
2924
|
+
|
|
2925
|
+
|
|
2926
|
+
|
|
2927
|
+
/**
|
|
2928
|
+
* @param rootEl {HTMLElement|DocumentFragment} If not specified, the first element of the html will be the rootEl. */
|
|
2929
|
+
constructor(rootEl=null) {
|
|
2930
|
+
this.rootEl = rootEl;
|
|
2931
|
+
/*
|
|
2932
|
+
|
|
2933
|
+
*/
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
/**
|
|
2937
|
+
* Render the main template, which may indirectly call renderTemplate() to create children.
|
|
2938
|
+
* @param template {Template}
|
|
2939
|
+
* @param options {RenderOptions}
|
|
2940
|
+
* @return {?DocumentFragment} */
|
|
2941
|
+
render(template, options={}) {
|
|
2942
|
+
this.mutationWatcherEnabled = false;
|
|
2943
|
+
this.options = options;
|
|
2944
|
+
this.clearSubscribers = false;
|
|
2945
|
+
|
|
2946
|
+
|
|
2947
|
+
|
|
2948
|
+
if (!template && template !== '') {
|
|
2949
|
+
this.rootEl.outerHTML = '';
|
|
2950
|
+
this.mutationWatcherEnabled = true;
|
|
2951
|
+
return null;
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
// Fast path for empty component.
|
|
2955
|
+
if (template.html?.length === 1 && !template.html[0]) {
|
|
2956
|
+
this.rootEl.innerHTML = '';
|
|
2957
|
+
}
|
|
2958
|
+
else {
|
|
2959
|
+
|
|
2960
|
+
// Find or create a NodeGroup for the template.
|
|
2961
|
+
// This updates all nodes from the template.
|
|
2962
|
+
let close;
|
|
2963
|
+
let exact = this.getNodeGroup(template, true);
|
|
2964
|
+
if (!exact) {
|
|
2965
|
+
close = this.getNodeGroup(template, false);
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
|
|
2969
|
+
let firstTime = !this.rootNg;
|
|
2970
|
+
this.rootNg = exact || close;
|
|
2971
|
+
|
|
2972
|
+
// Reparent NodeGroup
|
|
2973
|
+
// TODO: Move this to NodeGroup?
|
|
2974
|
+
let parent = this.rootNg.getParentNode();
|
|
2975
|
+
if (!this.rootEl)
|
|
2976
|
+
this.rootEl = parent;
|
|
2977
|
+
|
|
2978
|
+
// If this is the first time rendering this element.
|
|
2979
|
+
else if (firstTime) {
|
|
2980
|
+
|
|
2981
|
+
// Save slot children
|
|
2982
|
+
let fragment;
|
|
2983
|
+
if (this.rootEl.childNodes.length) {
|
|
2984
|
+
fragment = document.createDocumentFragment();
|
|
2985
|
+
fragment.append(...this.rootEl.childNodes);
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
// Add rendered elements.
|
|
2989
|
+
if (parent instanceof DocumentFragment)
|
|
2990
|
+
this.rootEl.append(parent);
|
|
2991
|
+
else if (parent)
|
|
2992
|
+
this.rootEl.append(...parent.childNodes);
|
|
2993
|
+
|
|
2994
|
+
// Apply slot children
|
|
2995
|
+
if (fragment) {
|
|
2996
|
+
for (let slot of this.rootEl.querySelectorAll('slot[name]')) {
|
|
2997
|
+
let name = slot.getAttribute('name');
|
|
2998
|
+
if (name)
|
|
2999
|
+
slot.append(...fragment.querySelectorAll(`[slot='${name}']`));
|
|
3000
|
+
}
|
|
3001
|
+
let unamedSlot = this.rootEl.querySelector('slot:not([name])');
|
|
3002
|
+
if (unamedSlot)
|
|
3003
|
+
unamedSlot.append(fragment);
|
|
3004
|
+
}
|
|
3005
|
+
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
// this.rootNg was rendered as childrenOnly=true
|
|
3009
|
+
// Apply attributes from a root element to the real root element.
|
|
3010
|
+
let ng = this.rootNg;
|
|
3011
|
+
if (ng.pseudoRoot && ng.pseudoRoot !== this.rootEl) {
|
|
3012
|
+
|
|
3013
|
+
|
|
3014
|
+
// Remove old attributes
|
|
3015
|
+
// for (let attrib of this.rootEl.attributes)
|
|
3016
|
+
// if (attrib.name !== 'is' && attrib.name !== 'data-style' && !ng.pseudoRoot.hasAttribute(attrib.name))
|
|
3017
|
+
// this.rootEl.removeAttribute(attrib.name)
|
|
3018
|
+
|
|
3019
|
+
// Add/set new attributes
|
|
3020
|
+
if (firstTime)
|
|
3021
|
+
for (let attrib of ng.pseudoRoot.attributes)
|
|
3022
|
+
if (!this.rootEl.hasAttribute(attrib.name))
|
|
3023
|
+
this.rootEl.setAttribute(attrib.name, attrib.value);
|
|
3024
|
+
|
|
3025
|
+
// ng.startNode = ng.endNode = this.rootEl;
|
|
3026
|
+
// ng.nodesCache = [ng.startNode]
|
|
3027
|
+
// for (let path of ng.paths) {
|
|
3028
|
+
// if (path.nodeMarker === ng.rootEl)
|
|
3029
|
+
// path.nodeMarker = this.rootEl;
|
|
3030
|
+
// path.nodesCache = null;
|
|
3031
|
+
//
|
|
3032
|
+
// }
|
|
3033
|
+
//
|
|
3034
|
+
// ng.rootEl = this.rootEl;
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
|
|
3038
|
+
this.reset();
|
|
3039
|
+
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
this.mutationWatcherEnabled = true;
|
|
3043
|
+
return this.rootEl;
|
|
3044
|
+
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
|
|
3048
|
+
/**
|
|
3049
|
+
*
|
|
3050
|
+
* 1. Delete a NodeGroup from this.nodeGroupsAvailable that matches this exactKey.
|
|
3051
|
+
* 2. Then delete all of that NodeGroup's parents' exactKey entries
|
|
3052
|
+
* We don't move them to in-use because we plucked the NodeGroup from them, they no longer match their exactKeys.
|
|
3053
|
+
* 3. Then we move all the NodeGroup's exact+close keyed children to inUse because we don't want future calls
|
|
3054
|
+
* to getNodeGroup() to borrow the children now that the whole NodeGroup is in-use.
|
|
3055
|
+
*
|
|
3056
|
+
* TODO: Have NodeGroups keep track of whether they're inUse.
|
|
3057
|
+
* That way when we go up or down we don't have to remove those with .inUse===true
|
|
3058
|
+
*
|
|
3059
|
+
* @param exactKey
|
|
3060
|
+
* @param goUp
|
|
3061
|
+
* @param child
|
|
3062
|
+
* @returns {?NodeGroup} */
|
|
3063
|
+
findAndDeleteExact(exactKey, goUp=true, child=undefined) {
|
|
3064
|
+
|
|
3065
|
+
let ng = this.nodeGroupsAvailable.delete(exactKey, child);
|
|
3066
|
+
if (ng) {
|
|
3067
|
+
|
|
3068
|
+
|
|
3069
|
+
// Mark close-key version as in-use.
|
|
3070
|
+
let closeNg = this.nodeGroupsAvailable.delete(ng.closeKey, ng);
|
|
3071
|
+
|
|
3072
|
+
|
|
3073
|
+
// Mark our self as in-use.
|
|
3074
|
+
this.nodeGroupsInUse.push(ng);
|
|
3075
|
+
|
|
3076
|
+
ng.inUse = true;
|
|
3077
|
+
closeNg.inUse = true;
|
|
3078
|
+
|
|
3079
|
+
// Mark all parents that have this NodeGroup as a child as in-use.
|
|
3080
|
+
// So that way we don't use this parent again
|
|
3081
|
+
if (goUp) {
|
|
3082
|
+
let ng2 = ng;
|
|
3083
|
+
while (ng2 = ng2?.parentPath?.parentNg) {
|
|
3084
|
+
if (!ng2.inUse) {
|
|
3085
|
+
ng2.inUse = true;
|
|
3086
|
+
let success = this.nodeGroupsAvailable.delete(ng2.exactKey, ng2);
|
|
3087
|
+
// assert(success);
|
|
3088
|
+
let success2 = this.nodeGroupsAvailable.delete(ng2.closeKey, ng2);
|
|
3089
|
+
// assert(success);
|
|
3090
|
+
|
|
3091
|
+
|
|
3092
|
+
// console.log(getHtml(ng2))
|
|
3093
|
+
if (success) {
|
|
3094
|
+
this.nodeGroupsInUse.push(ng2);
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
// Recurse to mark all child NodeGroups as in-use.
|
|
3101
|
+
for (let path of ng.paths)
|
|
3102
|
+
for (let childNg of path.nodeGroups) {
|
|
3103
|
+
if (!childNg.inUse)
|
|
3104
|
+
this.findAndDeleteExact(childNg.exactKey, false, childNg);
|
|
3105
|
+
childNg.inUse = true;
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
if (ng.parentPath) ;
|
|
3109
|
+
|
|
3110
|
+
return ng;
|
|
3111
|
+
}
|
|
3112
|
+
return null;
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
/**
|
|
3116
|
+
* @param closeKey {string}
|
|
3117
|
+
* @param exactKey {string}
|
|
3118
|
+
* @param goUp {boolean}
|
|
3119
|
+
* @returns {NodeGroup} */
|
|
3120
|
+
findAndDeleteClose(closeKey, exactKey, goUp=true) {
|
|
3121
|
+
let ng = this.nodeGroupsAvailable.delete(closeKey);
|
|
3122
|
+
if (ng) {
|
|
3123
|
+
|
|
3124
|
+
// We matched on a new key, so delete the old exactKey.
|
|
3125
|
+
let exactNg = this.nodeGroupsAvailable.delete(ng.exactKey, ng);
|
|
3126
|
+
|
|
3127
|
+
|
|
3128
|
+
|
|
3129
|
+
|
|
3130
|
+
|
|
3131
|
+
ng.inUse = true;
|
|
3132
|
+
if (goUp) {
|
|
3133
|
+
let ng2 = ng;
|
|
3134
|
+
|
|
3135
|
+
// We borrowed a node from another node group so make sure its parent isn't still an exact match.
|
|
3136
|
+
while (ng2 = ng2?.parentPath?.parentNg) {
|
|
3137
|
+
if (!ng2.inUse) {
|
|
3138
|
+
ng2.inUse = true; // Might speed it up slightly?
|
|
3139
|
+
let success = this.nodeGroupsAvailable.delete(ng2.exactKey, ng2);
|
|
3140
|
+
|
|
3141
|
+
|
|
3142
|
+
// But it can still be a close match, so we don't use this code.
|
|
3143
|
+
success = this.nodeGroupsAvailable.delete(ng2.closeKey, ng2);
|
|
3144
|
+
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
// Recursively mark all child NodeGroups as in-use.
|
|
3150
|
+
// We actually DON't want to do this becuse applyExprs is going to swap out the child NodeGroups
|
|
3151
|
+
// and mark them as in-use as it goes.
|
|
3152
|
+
// that's probably why uncommenting this causes tests to fail.
|
|
3153
|
+
// for (let path of ng.paths)
|
|
3154
|
+
// for (let childNg of path.nodeGroups)
|
|
3155
|
+
// this.findAndDeleteExact(childNg.exactKey, false, childNg);
|
|
3156
|
+
|
|
3157
|
+
|
|
3158
|
+
ng.exactKey = exactKey;
|
|
3159
|
+
ng.closeKey = closeKey;
|
|
3160
|
+
this.nodeGroupsInUse.push(ng);
|
|
3161
|
+
|
|
3162
|
+
|
|
3163
|
+
if (ng.parentPath) ;
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
|
|
3167
|
+
return ng;
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
/**
|
|
3171
|
+
* Get an existing or create a new NodeGroup that matches the template,
|
|
3172
|
+
* but don't reparent it if it's somewhere else.
|
|
3173
|
+
* @param template {Template}
|
|
3174
|
+
* @param exact {?boolean}
|
|
3175
|
+
* @param createForWatch
|
|
3176
|
+
* @return {?NodeGroup} */
|
|
3177
|
+
getNodeGroup(template, exact=null, createForWatch=false) {
|
|
3178
|
+
|
|
3179
|
+
let exactKey = getObjectHash(template);
|
|
3180
|
+
|
|
3181
|
+
// 1. Try to find an exact match.
|
|
3182
|
+
let ng;
|
|
3183
|
+
if (exact === true) {
|
|
3184
|
+
ng = this.findAndDeleteExact(exactKey);
|
|
3185
|
+
|
|
3186
|
+
if (!ng) {
|
|
3187
|
+
|
|
3188
|
+
return null;
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
// 2. Try to find a close match.
|
|
3193
|
+
else {
|
|
3194
|
+
// We don't need to delete the exact match bc it's already been deleted in the prev pass.
|
|
3195
|
+
let closeKey = template.getCloseKey();
|
|
3196
|
+
ng = createForWatch ? null : this.findAndDeleteClose(closeKey, exactKey);
|
|
3197
|
+
|
|
3198
|
+
// 2. Update expression values if they've changed.
|
|
3199
|
+
if (ng) {
|
|
3200
|
+
|
|
3201
|
+
// Temporary for debugging:
|
|
3202
|
+
if (window.debug && !window.ng)
|
|
3203
|
+
window.ng = ng;
|
|
3204
|
+
|
|
3205
|
+
|
|
3206
|
+
ng.applyExprs(template.exprs);
|
|
3207
|
+
|
|
3208
|
+
|
|
3209
|
+
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
// 3. Or if not found, create a new NodeGroup
|
|
3213
|
+
else {
|
|
3214
|
+
|
|
3215
|
+
ng = new NodeGroup(template, this);
|
|
3216
|
+
|
|
3217
|
+
|
|
3218
|
+
|
|
3219
|
+
|
|
3220
|
+
|
|
3221
|
+
// 4. Mark NodeGroup as being in-use.
|
|
3222
|
+
// TODO: Moving from one group to another thrashes the gc. Is there a faster way?
|
|
3223
|
+
// Could I have just a single WeakSet of those in use?
|
|
3224
|
+
// Perhaps also result could cache its last exprKey and then we'd use only one map?
|
|
3225
|
+
ng.exactKey = exactKey;
|
|
3226
|
+
ng.closeKey = closeKey;
|
|
3227
|
+
if (createForWatch) // TODO: Have this path be a separate function?
|
|
3228
|
+
this.nodeGroupsAvailable.add(ng.exactKey, ng);
|
|
3229
|
+
else
|
|
3230
|
+
this.nodeGroupsInUse.push(ng);
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
// New!
|
|
3235
|
+
// We clear the parent PathExpr's nodesCache when we remove ourselves from it.
|
|
3236
|
+
// Benchmarking shows this doesn't slow down the partialUpdate benchmark.
|
|
3237
|
+
if (ng.parentPath) {
|
|
3238
|
+
// ng.parentPath.clearNodesCache(); // Makes partialUpdate benchmark 10x slower!
|
|
3239
|
+
ng.parentPath = null;
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
|
|
3243
|
+
|
|
3244
|
+
|
|
3245
|
+
return ng;
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3248
|
+
reset() {
|
|
3249
|
+
//this.changes = [];
|
|
3250
|
+
let available = this.nodeGroupsAvailable;
|
|
3251
|
+
for (let ng of this.nodeGroupsInUse) {
|
|
3252
|
+
ng.inUse = false;
|
|
3253
|
+
available.add(ng.exactKey, ng);
|
|
3254
|
+
available.add(ng.closeKey, ng);
|
|
3255
|
+
}
|
|
3256
|
+
this.nodeGroupsInUse = [];
|
|
3257
|
+
|
|
3258
|
+
// Used for watches
|
|
3259
|
+
this.changes = [];
|
|
3260
|
+
|
|
3261
|
+
|
|
3262
|
+
// TODO: free the memory from any nodeGroupsAvailable() after render is done, since they weren't used?
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3265
|
+
|
|
3266
|
+
// deprecated
|
|
3267
|
+
//pathToLoopInfo = new MultiValueMap(); // uses a Set() for each value.
|
|
3268
|
+
clearSubscribers = false;
|
|
3269
|
+
|
|
3270
|
+
/**
|
|
3271
|
+
* One path may be used to loop in more than one place, so we use this to get every anchor from each loop.
|
|
3272
|
+
* @param path {Array}
|
|
3273
|
+
* @return {LoopInfo[]} A function that gets the loop anchor NodeGroup */
|
|
3274
|
+
getLoopInfo(path) {
|
|
3275
|
+
let serializedArrayPath = serializePath(path);
|
|
3276
|
+
return [...this.pathToLoopInfo.getAll(serializedArrayPath)]; // This is set inside forEach()
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
|
|
3280
|
+
/**
|
|
3281
|
+
* @deprecated
|
|
3282
|
+
* Store the functions used to create items for each loop.
|
|
3283
|
+
* TODO: Can this be combined with pathToTemplates?
|
|
3284
|
+
* @type {MultiValueMap<string, Subscriber>} */
|
|
3285
|
+
pathToLoopInfo = new MultiValueMap();
|
|
3286
|
+
|
|
3287
|
+
/**
|
|
3288
|
+
* Maps variable paths to the templates used to create NodeGroups
|
|
3289
|
+
* @type {MultiValueMap<string, Subscriber>} */
|
|
3290
|
+
subscribers = new MultiValueMap();
|
|
3291
|
+
|
|
3292
|
+
clearSubscribersIfNeeded() {
|
|
3293
|
+
if (this.clearSubscribers) {
|
|
3294
|
+
this.pathToLoopInfo = new MultiValueMap();
|
|
3295
|
+
this.subscribers = new MultiValueMap();
|
|
3296
|
+
this.clearSubscribers = false;
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
|
|
3301
|
+
/**
|
|
3302
|
+
* Get the NodeGroupManager for a Web Component.
|
|
3303
|
+
* @param rootEl {Solarite|HTMLElement}
|
|
3304
|
+
* @return {NodeGroupManager} */
|
|
3305
|
+
static get(rootEl) {
|
|
3306
|
+
let ngm = nodeGroupManagers.get(rootEl);
|
|
3307
|
+
if (!ngm) {
|
|
3308
|
+
ngm = new NodeGroupManager(rootEl);
|
|
3309
|
+
nodeGroupManagers.set(rootEl, ngm);
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
return ngm;
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
|
|
3316
|
+
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3319
|
+
NodeGroupManager.pendingChildren = [];
|
|
3320
|
+
|
|
3321
|
+
/**
|
|
3322
|
+
* Each Element that has Expr children has an associated NodeGroupManager here.
|
|
3323
|
+
* @type {WeakMap<HTMLElement, NodeGroupManager>} */
|
|
3324
|
+
let nodeGroupManagers = new WeakMap();
|
|
3325
|
+
|
|
3326
|
+
|
|
3327
|
+
|
|
3328
|
+
class LoopInfo {
|
|
3329
|
+
constructor(loopTemplate, itemTransformer) {
|
|
3330
|
+
this.template = loopTemplate;
|
|
3331
|
+
this.itemTransformer = itemTransformer;
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
/**
|
|
3336
|
+
* Convert strings to HTMLNodes.
|
|
3337
|
+
* Using r as a tag will always create a Template.
|
|
3338
|
+
* Using r() as a function() will always create a DOM element.
|
|
3339
|
+
*
|
|
3340
|
+
* Features beyond what standard js tagged template strings do:
|
|
3341
|
+
* 1. r`` sub-expressions
|
|
3342
|
+
* 2. functions, nodes, and arrays of nodes as sub-expressions.
|
|
3343
|
+
* 3. html-escape all expressions by default, unless wrapped in r()
|
|
3344
|
+
* 4. event binding
|
|
3345
|
+
* 5. TODO: list more
|
|
3346
|
+
*
|
|
3347
|
+
* Currently supported:
|
|
3348
|
+
* 1. r`<b>Hello${'World'}!` // Create Template that can later be used to create nodes.
|
|
3349
|
+
*
|
|
3350
|
+
* 2. r(el, template, ?options) // Render the template created by #1 to element.
|
|
3351
|
+
* 3. r(el, options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
|
|
3352
|
+
*
|
|
3353
|
+
* 4. r('Hello'); // Create single text node.
|
|
3354
|
+
* 5. r('<b>Hello</b>'); // Create single HTMLElement
|
|
3355
|
+
* 6. r('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
|
|
3356
|
+
* 7. r()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which includes properly handling nested components and r`` sub-expressions.
|
|
3357
|
+
* 8. r(template) // Render Template created by #1.
|
|
3358
|
+
* 9. r(() => r`<b>Hello</b>`); // Create dynamic element that has a render() function.
|
|
3359
|
+
*
|
|
3360
|
+
* @param htmlStrings {?HTMLElement|string|string[]|function():Template}
|
|
3361
|
+
* @param exprs {*[]|string|Template}
|
|
3362
|
+
* @return {Node|HTMLElement|Template} */
|
|
3363
|
+
function r(htmlStrings=undefined, ...exprs) {
|
|
3364
|
+
|
|
3365
|
+
// 1. Path if used as a template tag.
|
|
3366
|
+
if (Array.isArray(htmlStrings)) {
|
|
3367
|
+
return new Template(htmlStrings, exprs);
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
else if (htmlStrings instanceof Node) {
|
|
3371
|
+
let parent = htmlStrings, template = exprs[0];
|
|
3372
|
+
|
|
3373
|
+
// 2. Render template created by #4 to element.
|
|
3374
|
+
if (exprs[0] instanceof Template) {
|
|
3375
|
+
let ngm = NodeGroupManager.get(parent);
|
|
3376
|
+
let options = exprs[1];
|
|
3377
|
+
ngm.render(template, options);
|
|
3378
|
+
|
|
3379
|
+
// Append on the first go.
|
|
3380
|
+
if (!parent.childNodes.length && this) {
|
|
3381
|
+
// TODO: Is htis ever executed?
|
|
3382
|
+
debugger;
|
|
3383
|
+
parent.append(this.rootNg.getParentNode());
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
// 3
|
|
3388
|
+
else if (!exprs.length || exprs[0]) {
|
|
3389
|
+
if (parent.shadowRoot)
|
|
3390
|
+
parent.innerHTML = ''; // Remove shadowroot. TODO: This could mess up paths?
|
|
3391
|
+
|
|
3392
|
+
let options = exprs[0];
|
|
3393
|
+
return (htmlStrings, ...exprs) => {
|
|
3394
|
+
rendered.add(parent);
|
|
3395
|
+
let template = r(htmlStrings, ...exprs);
|
|
3396
|
+
let ngm = NodeGroupManager.get(parent);
|
|
3397
|
+
return ngm.render(template, options);
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
// null for expr[0], remove whole element.
|
|
3402
|
+
else {
|
|
3403
|
+
let ngm = NodeGroupManager.get(parent);
|
|
3404
|
+
ngm.render(null, exprs[1]);
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3408
|
+
else if (typeof htmlStrings === 'string' || htmlStrings instanceof String) {
|
|
3409
|
+
// If it starts with a string, trim both ends.
|
|
3410
|
+
// TODO: Also trim if it ends with whitespace?
|
|
3411
|
+
if (htmlStrings.match(/^\s^</))
|
|
3412
|
+
htmlStrings = htmlStrings.trim();
|
|
3413
|
+
|
|
3414
|
+
// We create a new one each time because otherwise
|
|
3415
|
+
// the returned fragment will have its content replaced by a subsequent call.
|
|
3416
|
+
let templateEl = document.createElement('template');
|
|
3417
|
+
templateEl.innerHTML = htmlStrings;
|
|
3418
|
+
|
|
3419
|
+
// 4+5. Return Node if there's one child.
|
|
3420
|
+
if (templateEl.content.childNodes.length === 1)
|
|
3421
|
+
return templateEl.content.firstChild;
|
|
3422
|
+
|
|
3423
|
+
// 6. Otherwise return DocumentFragment.
|
|
3424
|
+
return templateEl.content;
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3427
|
+
// 7. Create a static element
|
|
3428
|
+
else if (htmlStrings === undefined) {
|
|
3429
|
+
return (htmlStrings, ...exprs) => {
|
|
3430
|
+
//rendered.add(parent)
|
|
3431
|
+
let template = r(htmlStrings, ...exprs);
|
|
3432
|
+
return template.toNode();
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
// 8.
|
|
3437
|
+
else if (htmlStrings instanceof Template) {
|
|
3438
|
+
let ngm = new NodeGroupManager();
|
|
3439
|
+
return ngm.render(htmlStrings);
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
// 9. Create dynamic element with render() function.
|
|
3443
|
+
else if (typeof htmlStrings === 'function') {
|
|
3444
|
+
let getTemplate = htmlStrings;
|
|
3445
|
+
let template = getTemplate();
|
|
3446
|
+
|
|
3447
|
+
if (typeof template === 'string')
|
|
3448
|
+
throw new Error(`Please add the "r" prefix before the string "${template}"`)
|
|
3449
|
+
|
|
3450
|
+
let ngm = new NodeGroupManager();
|
|
3451
|
+
template.replaceMode = true;
|
|
3452
|
+
let el = ngm.render(template);
|
|
3453
|
+
|
|
3454
|
+
el.render = (function() {
|
|
3455
|
+
template = getTemplate();
|
|
3456
|
+
ngm.render(template);
|
|
3457
|
+
}).bind(el);
|
|
3458
|
+
|
|
3459
|
+
return el;
|
|
3460
|
+
}
|
|
3461
|
+
else
|
|
3462
|
+
throw new Error('Unsupported arguments.')
|
|
3463
|
+
}
|
|
3464
|
+
|
|
3465
|
+
|
|
3466
|
+
|
|
3467
|
+
|
|
3468
|
+
|
|
3469
|
+
|
|
3470
|
+
/**
|
|
3471
|
+
* Elements that have been rendered to by r() at least once.
|
|
3472
|
+
* @type {WeakSet<HTMLElement>} */
|
|
3473
|
+
let rendered = new WeakSet();
|
|
3474
|
+
|
|
3475
|
+
function defineClass(Class, tagName, extendsTag) {
|
|
3476
|
+
if (!customElements.getName(Class)) { // If not previously defined.
|
|
3477
|
+
tagName = tagName || camelToDashes(Class.name);
|
|
3478
|
+
if (!tagName.includes('-'))
|
|
3479
|
+
tagName += '-element';
|
|
3480
|
+
|
|
3481
|
+
let options = null;
|
|
3482
|
+
if (extendsTag)
|
|
3483
|
+
options = {extends: extendsTag};
|
|
3484
|
+
|
|
3485
|
+
customElements.define(tagName, Class, options);
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
/**
|
|
3490
|
+
* @type {Object<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
|
|
3491
|
+
let elementClasses = {};
|
|
3492
|
+
|
|
3493
|
+
/**
|
|
3494
|
+
* Store which instances of Solarite have already been added to the DOM. * @type {WeakSet<HTMLElement>}
|
|
3495
|
+
*/
|
|
3496
|
+
let connected = new WeakSet();
|
|
3497
|
+
|
|
3498
|
+
/**
|
|
3499
|
+
* Create a version of the Solarite class that extends from the given tag name.
|
|
3500
|
+
* Reasons to inherit from this instead of HTMLElement. None of these are all that useful.
|
|
3501
|
+
* 1. customElements.define() is called automatically when you create the first instance.
|
|
3502
|
+
* 2. Calls render() when added to the DOM, if it hasn't been called already.
|
|
3503
|
+
* 3. Child elements are added before constructor is called. But they're also passed to the constructor.
|
|
3504
|
+
* 4. We can use this.html = r`...` to set html.
|
|
3505
|
+
* 5. We have the onConnect, onFirstConnect, and onDisconnect methods. These could be standalone though.
|
|
3506
|
+
* 6. Can we extend from other element types like TR?
|
|
3507
|
+
*
|
|
3508
|
+
* @param extendsTag {?string}
|
|
3509
|
+
* @return {Class} */
|
|
3510
|
+
function createSolarite(extendsTag=null) {
|
|
3511
|
+
|
|
3512
|
+
let BaseClass = HTMLElement;
|
|
3513
|
+
if (extendsTag && !extendsTag.includes('-')) {
|
|
3514
|
+
extendsTag = extendsTag.toLowerCase();
|
|
3515
|
+
|
|
3516
|
+
BaseClass = elementClasses[extendsTag];
|
|
3517
|
+
if (!BaseClass) { // TODO: Use Cache
|
|
3518
|
+
BaseClass = document.createElement(extendsTag).constructor;
|
|
3519
|
+
elementClasses[extendsTag] = BaseClass;
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
/**
|
|
3524
|
+
* Intercept the construct call to auto-define the class before the constructor is called.
|
|
3525
|
+
* @type {HTMLElement} */
|
|
3526
|
+
let HTMLElementAutoDefine = new Proxy(BaseClass, {
|
|
3527
|
+
construct(Parent, args, Class) {
|
|
3528
|
+
defineClass(Class, null, extendsTag);
|
|
3529
|
+
|
|
3530
|
+
// This is a good place to manipulate any args before they're sent to the constructor.
|
|
3531
|
+
// Such as loading them from attributes, if I could find a way to do so.
|
|
3532
|
+
|
|
3533
|
+
// This line is equivalent the to super() call.
|
|
3534
|
+
return Reflect.construct(Parent, args, Class);
|
|
3535
|
+
}
|
|
3536
|
+
});
|
|
3537
|
+
|
|
3538
|
+
return class Solarite extends HTMLElementAutoDefine {
|
|
3539
|
+
|
|
3540
|
+
|
|
3541
|
+
/**
|
|
3542
|
+
* TODO: Make these standalone functions.
|
|
3543
|
+
* Callbacks.
|
|
3544
|
+
* Use onConnect.push(() => ...); to add new callbacks. */
|
|
3545
|
+
onConnect = Util$1.callback();
|
|
3546
|
+
|
|
3547
|
+
onFirstConnect = Util$1.callback();
|
|
3548
|
+
onDisconnect = Util$1.callback();
|
|
3549
|
+
|
|
3550
|
+
/**
|
|
3551
|
+
* @param options {RenderOptions} */
|
|
3552
|
+
constructor(options={}) {
|
|
3553
|
+
super();
|
|
3554
|
+
|
|
3555
|
+
|
|
3556
|
+
|
|
3557
|
+
// TODO: Is options.render ever used?
|
|
3558
|
+
if (options.render===true)
|
|
3559
|
+
this.render();
|
|
3560
|
+
|
|
3561
|
+
else if (options.render===false)
|
|
3562
|
+
rendered.add(this); // Don't render on connectedCallback()
|
|
3563
|
+
|
|
3564
|
+
// Add children before constructor code executes.
|
|
3565
|
+
// PendingChildren is setup in NodeGroup.createNewComponent()
|
|
3566
|
+
// TODO: Match named slots.
|
|
3567
|
+
let ch = NodeGroupManager.pendingChildren.pop();
|
|
3568
|
+
if (ch)
|
|
3569
|
+
(this.querySelector('slot') || this).append(...ch);
|
|
3570
|
+
|
|
3571
|
+
|
|
3572
|
+
Object.defineProperty(this, 'html', {
|
|
3573
|
+
set(html) {
|
|
3574
|
+
rendered.add(this);
|
|
3575
|
+
if (typeof html === 'string') {
|
|
3576
|
+
console.warn("Assigning to this.html without the r template prefix.");
|
|
3577
|
+
this.innerHTML = html;
|
|
3578
|
+
}
|
|
3579
|
+
else
|
|
3580
|
+
this.modifications = r(this, html, options);
|
|
3581
|
+
}
|
|
3582
|
+
});
|
|
3583
|
+
|
|
3584
|
+
/*
|
|
3585
|
+
let pthis = new Proxy(this, {
|
|
3586
|
+
get(obj, prop) {
|
|
3587
|
+
return Reflect.get(obj, prop)
|
|
3588
|
+
}
|
|
3589
|
+
});
|
|
3590
|
+
this.render = this.render.bind(pthis);
|
|
3591
|
+
*/
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
/**
|
|
3595
|
+
* Call render() only if it hasn't already been called. */
|
|
3596
|
+
renderFirstTime() {
|
|
3597
|
+
if (!rendered.has(this) && this.render)
|
|
3598
|
+
this.render();
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3601
|
+
/**
|
|
3602
|
+
* Called automatically by the browser. */
|
|
3603
|
+
connectedCallback() {
|
|
3604
|
+
this.renderFirstTime();
|
|
3605
|
+
if (!connected.has(this)) {
|
|
3606
|
+
connected.add(this);
|
|
3607
|
+
this.onFirstConnect();
|
|
3608
|
+
}
|
|
3609
|
+
this.onConnect();
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
disconnectedCallback() {
|
|
3613
|
+
this.onDisconnect();
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3616
|
+
|
|
3617
|
+
static define(tagName=null) {
|
|
3618
|
+
defineClass(this, tagName, extendsTag);
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
|
|
3622
|
+
renderWatched() {
|
|
3623
|
+
let ngm = NodeGroupManager.get(this);
|
|
3624
|
+
|
|
3625
|
+
let nodeGroupUpdates = [];
|
|
3626
|
+
|
|
3627
|
+
for (let change of ngm.changes) {
|
|
3628
|
+
if (change.action === 'set') {
|
|
3629
|
+
for (let transformerInfo of change.transformerInfo) {
|
|
3630
|
+
|
|
3631
|
+
let oldHash = transformerInfo.hash;
|
|
3632
|
+
|
|
3633
|
+
let newObj = delve(watchSet(transformerInfo.path[0]), transformerInfo.path.slice(1));
|
|
3634
|
+
let newTemplate = transformerInfo.transformer(newObj);
|
|
3635
|
+
let newHash = getObjectHash(newTemplate);
|
|
3636
|
+
let ngs = [...ngm.nodeGroupsAvailable.data[oldHash]];
|
|
3637
|
+
for (let ng of ngs) {
|
|
3638
|
+
nodeGroupUpdates.push([ng, oldHash, newHash, newTemplate.exprs, transformerInfo]);
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
else if (change.action === 'delete') {
|
|
3644
|
+
for (let hash of change.value) {
|
|
3645
|
+
let ngs = [...ngm.nodeGroupsAvailable.getAll(hash)]; // deletes from nodeGroupsAvailable.
|
|
3646
|
+
|
|
3647
|
+
for (let ng of ngs) {
|
|
3648
|
+
if (ng.parentPath)
|
|
3649
|
+
ng.parentPath.clearNodesCache();
|
|
3650
|
+
|
|
3651
|
+
for (let node of ng.getNodes())
|
|
3652
|
+
node.remove();
|
|
3653
|
+
|
|
3654
|
+
// TODO: Update ancestor NodeGroup exactKeys
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
else if (change.action === 'insert') {
|
|
3659
|
+
|
|
3660
|
+
let beforeNg = change.beforeTemplate ? ngm.getNodeGroup(change.beforeTemplate, true) : null;
|
|
3661
|
+
let arrayPath = [change.root, ...change.path];
|
|
3662
|
+
|
|
3663
|
+
// Get anchor so we can use it to get the parent
|
|
3664
|
+
// TODO: Should this be watchGet(change.root) ?
|
|
3665
|
+
for (let loopInfo of ngm.getLoopInfo([change.root, ...change.path.slice(0, -1)])) {
|
|
3666
|
+
|
|
3667
|
+
// Change.extra is aTemplate telling us where to insert before.
|
|
3668
|
+
let beforeNode = beforeNg?.startNode || loopInfo.template.parentPath.nodeMarker;
|
|
3669
|
+
|
|
3670
|
+
// Loop over every item added to the array.
|
|
3671
|
+
let i = 0; // TODO: How to get real insert index.
|
|
3672
|
+
for (let obj of change.value) {
|
|
3673
|
+
|
|
3674
|
+
// Same logic as forEach() function.
|
|
3675
|
+
|
|
3676
|
+
let callback = loopInfo.itemTransformer;
|
|
3677
|
+
let path = [...arrayPath.slice(0, -1), (arrayPath.at(-1) * 1 + i) + ''];
|
|
3678
|
+
|
|
3679
|
+
// Shortened logic found in watchGet(), but not any faster?
|
|
3680
|
+
// the watchSet() is what makes this slower!
|
|
3681
|
+
// let obj = delve(watchSet(path[0]), path.slice(1));
|
|
3682
|
+
// let template = callback(obj);
|
|
3683
|
+
// let serializedPath = serializePath(path);
|
|
3684
|
+
// pathToTransformer.add(serializedPath, new TransformerInfo(path, callback, template)); // Uses a Set() to ensure no duplicates.
|
|
3685
|
+
|
|
3686
|
+
let template = watchGet(path, callback);
|
|
3687
|
+
i++;
|
|
3688
|
+
|
|
3689
|
+
|
|
3690
|
+
//let template = loopInfo.itemTransformer(obj); // What if it takes more than one obj argument?
|
|
3691
|
+
|
|
3692
|
+
// Create new NodeGroup
|
|
3693
|
+
let ng = ngm.getNodeGroup(template, false, true);
|
|
3694
|
+
ng.parentPath = beforeNg?.parentPath || loopInfo.template.parentPath;
|
|
3695
|
+
|
|
3696
|
+
for (let node of ng.getNodes())
|
|
3697
|
+
beforeNode.parentNode.insertBefore(node, beforeNode);
|
|
3698
|
+
|
|
3699
|
+
if (ng.parentPath) // This check is needed for the forEachSpliceInsert test, but why?
|
|
3700
|
+
ng.parentPath.clearNodesCache();
|
|
3701
|
+
}
|
|
3702
|
+
|
|
3703
|
+
// TODO: Update ancestor NodeGroup exactKeys
|
|
3704
|
+
}
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3708
|
+
// Update them all at once, that way we can reassign the same value twice.
|
|
3709
|
+
for (let [ng, oldHash, newHash, exprs, ti] of nodeGroupUpdates) {
|
|
3710
|
+
ng.applyExprs(exprs);
|
|
3711
|
+
ngm.nodeGroupsAvailable.data[oldHash].delete(ng);
|
|
3712
|
+
ng.exactKey = ti.hash = newHash;
|
|
3713
|
+
ngm.nodeGroupsAvailable.add(ng.exactKey, ng); // Add back to Map with new key.
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3716
|
+
|
|
3717
|
+
ngm.changes = [];
|
|
3718
|
+
|
|
3719
|
+
return []; // TODO
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3722
|
+
/**
|
|
3723
|
+
* @deprecated Use the getArg() function instead. */
|
|
3724
|
+
getArg(name, val=null, type=ArgType.String) {
|
|
3725
|
+
return getArg(this, name, val, type);
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
/**
|
|
3731
|
+
* TODO: The Proxy and the multiple base classes mess up 'instanceof Solarite'
|
|
3732
|
+
* @type {Node|Class<HTMLElement>|function(tagName:string):Node|Class<HTMLElement>} */
|
|
3733
|
+
let Solarite = new Proxy(createSolarite(), {
|
|
3734
|
+
apply(self, _, args) {
|
|
3735
|
+
return createSolarite(...args)
|
|
3736
|
+
}
|
|
3737
|
+
});
|
|
3738
|
+
// unfinished
|
|
3739
|
+
|
|
3740
|
+
export { ArgType, Solarite, forEach, getArg, r, watch, watchGet, watchSet };
|