solarite 0.2.4 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Solarite-debug.js +1457 -1402
- package/dist/Solarite.js +1425 -1272
- package/dist/Solarite.min.js +2 -2
- package/package.json +5 -6
- package/readme.md +2 -4
- package/src/{solarite/ExprPath.js → ExprPath.js} +421 -231
- package/src/Globals.js +79 -0
- package/src/HtmlParser.js +91 -0
- package/src/{util/MultiValueMap.js → MultiValueMap.js} +22 -26
- package/src/{solarite/NodeGroup.js → NodeGroup.js} +286 -226
- package/src/{solarite/Shell.js → Shell.js} +119 -92
- package/src/Solarite.d.ts +62 -0
- package/src/{solarite/Solarite.js → Solarite.js} +15 -13
- package/src/{solarite/Template.js → Template.js} +22 -19
- package/src/Util.js +330 -0
- package/src/{util/Errors.js → assert.js} +1 -0
- package/src/createSolarite.js +154 -0
- package/src/{util/delve.js → delve.js} +5 -4
- package/src/{solarite/getArg.js → getArg.js} +41 -15
- package/src/{solarite/r.js → h.js} +59 -29
- package/src/{solarite/hash.js → hash.js} +12 -9
- package/src/unused/FastLookupArray.js +54 -0
- package/src/unused/Hashes.js +339 -0
- package/src/unused/InUse.test.js +92 -0
- package/src/unused/InUseMap.js +98 -0
- package/src/unused/LinkedList.js +117 -0
- package/src/unused/LinkedList.test.js +115 -0
- package/src/unused/Misc.js +13 -0
- package/src/unused/Perf.js +47 -0
- package/src/unused/TrackedArray.js +54 -0
- package/src/watch.js +546 -0
- package/src/solarite/Globals.js +0 -54
- package/src/solarite/Util.js +0 -388
- package/src/solarite/createSolarite.js +0 -274
- package/src/solarite/watch3.js +0 -189
- package/src/util/Util.js +0 -113
- /package/src/{solarite/udomdiff.js → udomdiff.js} +0 -0
- /package/src/{util → unused}/WeakArray.js +0 -0
package/dist/Solarite.js
CHANGED
|
@@ -1,261 +1,4 @@
|
|
|
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
|
-
* @param map {Map|WeakMap|Object}
|
|
82
|
-
* @param key
|
|
83
|
-
* @param value */
|
|
84
|
-
mapAdd(map, key, value) {
|
|
85
|
-
let isMap = map instanceof Map || map instanceof WeakMap;
|
|
86
|
-
let result = isMap ? map.get(key) : map[key];
|
|
87
|
-
if (!result) {
|
|
88
|
-
result = [value];
|
|
89
|
-
if (isMap)
|
|
90
|
-
map.set(key, result);
|
|
91
|
-
else
|
|
92
|
-
map[key] = result;
|
|
93
|
-
}
|
|
94
|
-
else
|
|
95
|
-
result.push(value);
|
|
96
|
-
},
|
|
97
|
-
|
|
98
|
-
weakMemoize(obj, callback) {
|
|
99
|
-
let result = weakMemoizeInputs.get(obj);
|
|
100
|
-
if (!result) {
|
|
101
|
-
result = callback(obj);
|
|
102
|
-
weakMemoizeInputs.set(obj, result);
|
|
103
|
-
}
|
|
104
|
-
return result;
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
let weakMemoizeInputs = new WeakMap();
|
|
109
1
|
|
|
110
|
-
/**
|
|
111
|
-
* Follow a path into an object.
|
|
112
|
-
* @param obj {object}
|
|
113
|
-
* @param path {string[]}
|
|
114
|
-
* @param createVal {*} If set, non-existant paths will be created and value at path will be set to createVal.
|
|
115
|
-
* @return {*} The value, or undefined if it can't be reached. */
|
|
116
|
-
function delve(obj, path, createVal = delveDontCreate) {
|
|
117
|
-
let isCreate = createVal !== delveDontCreate;
|
|
118
|
-
|
|
119
|
-
let len = path.length;
|
|
120
|
-
if (!obj && !isCreate && len)
|
|
121
|
-
return undefined;
|
|
122
|
-
|
|
123
|
-
let i = 0;
|
|
124
|
-
for (let srcProp of path) {
|
|
125
|
-
|
|
126
|
-
// If the path is undefined and we're not to the end yet:
|
|
127
|
-
if (obj[srcProp] === undefined) {
|
|
128
|
-
|
|
129
|
-
// If the next index is an integer or integer string.
|
|
130
|
-
if (isCreate) {
|
|
131
|
-
if (i < len - 1) {
|
|
132
|
-
// If next level path is a number, create as an array
|
|
133
|
-
let isArray = (path[i + 1] + '').match(/^\d+$/);
|
|
134
|
-
obj[srcProp] = isArray ? [] : {};
|
|
135
|
-
}
|
|
136
|
-
} else
|
|
137
|
-
return undefined; // can't traverse
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// If last item in path
|
|
141
|
-
if (isCreate && i === len - 1)
|
|
142
|
-
obj[srcProp] = createVal;
|
|
143
|
-
|
|
144
|
-
// Traverse deeper along destination object.
|
|
145
|
-
obj = obj[srcProp];
|
|
146
|
-
i++;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return obj;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
let delveDontCreate = {};
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* There are three ways to create an instance of a Solarite Component:
|
|
156
|
-
* 1. new ComponentName(); // direct class instantiation
|
|
157
|
-
* 2. this.html = r`<div><component-name></component-name></div>; // as a child of another RedComponent.
|
|
158
|
-
* 3. <body><component-name></component-name></body> // in the Document html.
|
|
159
|
-
*
|
|
160
|
-
* When created via #3, Solarite has no way to pass attributes as arguments to the constructor. So to make
|
|
161
|
-
* sure we get the correct value via all three paths, we write our constructors according to the following
|
|
162
|
-
* example. Note that constructor args are embedded in an object, and must be all lower-case because
|
|
163
|
-
* Browsers make all html attribute names lowercase.
|
|
164
|
-
*
|
|
165
|
-
* @example
|
|
166
|
-
* constructor({name, userid=1}={}) {
|
|
167
|
-
* super();
|
|
168
|
-
*
|
|
169
|
-
* // Get value from "name" attriute if persent, otherwise from name constructor arg.
|
|
170
|
-
* this.name = getArg(this, 'name', name);
|
|
171
|
-
*
|
|
172
|
-
* // Optionally convert the value to an integer.
|
|
173
|
-
* this.userId = getArg(this, 'userid', userid, ArgType.Int);
|
|
174
|
-
* }
|
|
175
|
-
*
|
|
176
|
-
* @param el {HTMLElement}
|
|
177
|
-
* @param name {string} Attribute name. Not case-sensitive.
|
|
178
|
-
* @param val {*} Default value to use if attribute doesn't exist.
|
|
179
|
-
* @param type {ArgType|function|*[]}
|
|
180
|
-
* If an array, use the value if it's in the array, otherwise return undefined.
|
|
181
|
-
* If it's a function, pass the value to the function and return the result.
|
|
182
|
-
* @param fallback {*} If the type can't be parsed as the given type, use this value.
|
|
183
|
-
* @return {*} Undefined if attribute isn't set. */
|
|
184
|
-
function getArg(el, name, val=undefined, type=ArgType.String, fallback=undefined) {
|
|
185
|
-
let attrVal = el.getAttribute(name);
|
|
186
|
-
if (attrVal !== null) // If attribute doesn't exist.
|
|
187
|
-
val = attrVal;
|
|
188
|
-
|
|
189
|
-
if (Array.isArray(type))
|
|
190
|
-
return type.includes(val) ? val : fallback;
|
|
191
|
-
|
|
192
|
-
if (typeof type === 'function')
|
|
193
|
-
return type(val);
|
|
194
|
-
|
|
195
|
-
// If bool, it's true as long as it exists and its value isn't falsey.
|
|
196
|
-
if (type===ArgType.Bool) {
|
|
197
|
-
let lAttrVal = typeof val === 'string' ? val.toLowerCase() : val;
|
|
198
|
-
if (['false', '0', false, 0, null, undefined, NaN].includes(lAttrVal))
|
|
199
|
-
return false;
|
|
200
|
-
if (['true', true].includes(lAttrVal) || parseFloat(lAttrVal) !== 0)
|
|
201
|
-
return true;
|
|
202
|
-
return fallback;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Attribute doesn't exist
|
|
206
|
-
let result;
|
|
207
|
-
switch (type) {
|
|
208
|
-
case ArgType.Int:
|
|
209
|
-
result = parseInt(val);
|
|
210
|
-
return isNaN(result) ? fallback : result;
|
|
211
|
-
case ArgType.Float:
|
|
212
|
-
result = parseFloat(val);
|
|
213
|
-
return isNaN(result) ? fallback : result;
|
|
214
|
-
case ArgType.String:
|
|
215
|
-
return [undefined, null, false].includes(val) ? '' : val+'';
|
|
216
|
-
case ArgType.JSON:
|
|
217
|
-
case ArgType.Eval:
|
|
218
|
-
if (typeof val === 'string' && val.length)
|
|
219
|
-
try {
|
|
220
|
-
if (type === ArgType.JSON)
|
|
221
|
-
return JSON.parse(val);
|
|
222
|
-
else
|
|
223
|
-
return eval(`(${val})`);
|
|
224
|
-
} catch (e) {
|
|
225
|
-
return val;
|
|
226
|
-
}
|
|
227
|
-
else return fallback;
|
|
228
|
-
|
|
229
|
-
// type not provided
|
|
230
|
-
default:
|
|
231
|
-
return val;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* @enum */
|
|
237
|
-
var ArgType = {
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* false, 0, null, undefined, '0', and 'false' (case-insensitive) become false.
|
|
241
|
-
* Anything else, including empty string becomes true.
|
|
242
|
-
* Empty string is true because attributes with no value should be evaulated as true. */
|
|
243
|
-
Bool: 'Bool',
|
|
244
|
-
|
|
245
|
-
Int: 'Int',
|
|
246
|
-
Float: 'Float',
|
|
247
|
-
String: 'String',
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Parse the string value as JSON.
|
|
251
|
-
* If it's not parsable, return the value as a string. */
|
|
252
|
-
JSON: 'JSON',
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Evaluate the string as JavaScript using the eval() function.
|
|
256
|
-
* If it can't be evaluated, return the original string. */
|
|
257
|
-
Eval: 'Eval'
|
|
258
|
-
};
|
|
259
2
|
|
|
260
3
|
let lastObjectId = 1>>>0; // Is a 32-bit int faster to increment than JavaScript's Number, which is a 64-bit float?
|
|
261
4
|
let objectIds = new WeakMap();
|
|
@@ -269,7 +12,7 @@ function getObjectId(obj) {
|
|
|
269
12
|
|
|
270
13
|
let result = objectIds.get(obj);
|
|
271
14
|
if (!result) { // convert to string, store in result, then add 1 to lastObjectId.
|
|
272
|
-
result = (lastObjectId++); // We use a unique prefix to ensure it doesn't collide w/ strings not from getObjectId()
|
|
15
|
+
result = '~\f' + (lastObjectId++); // We use a unique, 2-byte prefix to ensure it doesn't collide w/ strings not from getObjectId()
|
|
273
16
|
objectIds.set(obj, result);
|
|
274
17
|
}
|
|
275
18
|
return result;
|
|
@@ -288,14 +31,7 @@ function toJSON() {
|
|
|
288
31
|
|
|
289
32
|
// Node.prototype.toJSON = toJSON;
|
|
290
33
|
// Function.prototype.toJSON = toJSON;
|
|
291
|
-
|
|
292
|
-
// The same tests sometimes pass, sometimes fail, even after browser and OS restarts.
|
|
293
|
-
// So we check the assignments on every run of getObjectHash()
|
|
294
|
-
if (Node.prototype.toJSON !== toJSON) {
|
|
295
|
-
Node.prototype.toJSON = toJSON;
|
|
296
|
-
if (Function.prototype.toJSON !== toJSON) // Will it only unmap one but not the other?
|
|
297
|
-
Function.prototype.toJSON = toJSON;
|
|
298
|
-
}
|
|
34
|
+
|
|
299
35
|
|
|
300
36
|
/**
|
|
301
37
|
* Get a string that uniquely maps to the values of the given object.
|
|
@@ -310,6 +46,16 @@ if (Node.prototype.toJSON !== toJSON) {
|
|
|
310
46
|
* @param obj {*}
|
|
311
47
|
* @returns {string} */
|
|
312
48
|
function getObjectHash(obj) {
|
|
49
|
+
|
|
50
|
+
// Sometimes these get unassigned by Chrome and Brave 119, as well as Firefox, seemingly randomly!
|
|
51
|
+
// The same tests sometimes pass, sometimes fail, even after browser and OS restarts.
|
|
52
|
+
// So we check the assignments on every run of getObjectHash()
|
|
53
|
+
if (Node.prototype.toJSON !== toJSON) {
|
|
54
|
+
Node.prototype.toJSON = toJSON;
|
|
55
|
+
if (Function.prototype.toJSON !== toJSON) // Will it only unmap one but not the other?
|
|
56
|
+
Function.prototype.toJSON = toJSON;
|
|
57
|
+
}
|
|
58
|
+
|
|
313
59
|
let result;
|
|
314
60
|
isHashing = true;
|
|
315
61
|
try {
|
|
@@ -342,63 +88,164 @@ function getObjectHashCircular(obj) {
|
|
|
342
88
|
});
|
|
343
89
|
}
|
|
344
90
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
var Globals = {
|
|
91
|
+
var Globals;
|
|
348
92
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Created with a reset() function because it's useful for testing. */
|
|
95
|
+
function reset() {
|
|
96
|
+
Globals = {
|
|
352
97
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
connected: new WeakSet(),
|
|
98
|
+
/**
|
|
99
|
+
* Used by NodeGroup.applyComponentExprs() */
|
|
100
|
+
componentArgsHash: new WeakMap(),
|
|
357
101
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
rendered: new WeakSet(),
|
|
102
|
+
/**
|
|
103
|
+
* Store which instances of Solarite have already been added to the DOM.
|
|
104
|
+
* @type {WeakSet<HTMLElement>} */
|
|
105
|
+
connected: new WeakSet(),
|
|
363
106
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
107
|
+
/**
|
|
108
|
+
* ExprPath.applyExactNodes() sets this property when an expression is being accessed.
|
|
109
|
+
* watch() then adds the ExprPath to the list of ExprPaths that should be re-rendered when the value changes.
|
|
110
|
+
* @type {ExprPath}*/
|
|
111
|
+
currentExprPath: null,
|
|
368
112
|
|
|
369
|
-
|
|
370
|
-
* @type {Object<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
|
|
371
|
-
elementClasses: {},
|
|
113
|
+
div: document.createElement("div"),
|
|
372
114
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
nodeEvents: new WeakMap(),
|
|
115
|
+
/**
|
|
116
|
+
* @type {Record<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
|
|
117
|
+
elementClasses: {},
|
|
377
118
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
* @type {WeakMap<HTMLElement, RootNodeGroup>} */
|
|
381
|
-
nodeGroups: new WeakMap(),
|
|
119
|
+
/** @type {Record<string, boolean>} Key is tag-name.propName. Value is whether it's an attribute.*/
|
|
120
|
+
htmlProps: {},
|
|
382
121
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Used by ExprPath.applyEventAttrib()
|
|
124
|
+
* @type {WeakMap<Node, Record<eventName:string, [original:function, bound:function, args:*[]]>>} */
|
|
125
|
+
nodeEvents: new WeakMap(),
|
|
386
126
|
|
|
387
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Get the RootNodeGroup for an element.
|
|
129
|
+
* @type {WeakMap<HTMLElement, RootNodeGroup>} */
|
|
130
|
+
nodeGroups: new WeakMap(),
|
|
388
131
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
rendering: new WeakSet(),
|
|
132
|
+
/**
|
|
133
|
+
* Used by r() path 9. */
|
|
134
|
+
objToEl: new WeakMap(),
|
|
393
135
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
136
|
+
//pendingChildren: [],
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Elements that have been rendered to by r() at least once.
|
|
141
|
+
* This is used by the Solarite class to know when to call onFirstConnect()
|
|
142
|
+
* @type {WeakSet<HTMLElement>} */
|
|
143
|
+
rendered: new WeakSet(),
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Elements that are currently rendering via the r() function.
|
|
147
|
+
* @type {WeakSet<HTMLElement>} */
|
|
148
|
+
rendering: new WeakSet(),
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Map from array of Html strings to a Shell created from them.
|
|
152
|
+
* @type {WeakMap<string[], Shell>} */
|
|
153
|
+
shells: new WeakMap(),
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* A map of individual untagged strings to their Templates.
|
|
157
|
+
* This way we don't keep creating new Templates for the same string when re-rendering.
|
|
158
|
+
* This is used by ExprPath.applyExactNodes()
|
|
159
|
+
* @type {Record<string, Template>} */
|
|
160
|
+
//stringTemplates: {},
|
|
161
|
+
|
|
162
|
+
reset,
|
|
163
|
+
|
|
164
|
+
count: 0
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
reset();
|
|
168
|
+
|
|
169
|
+
var Globals$1 = Globals;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Follow a path into an object.
|
|
173
|
+
* @param obj {object}
|
|
174
|
+
* @param path {string[]}
|
|
175
|
+
* @param createVal {*} If set, non-existent paths will be created and value at path will be set to createVal.
|
|
176
|
+
* @return {*} The value, or undefined if it can't be reached. */
|
|
177
|
+
function delve(obj, path, createVal = d) {
|
|
178
|
+
let isCreate = createVal !== d;
|
|
179
|
+
|
|
180
|
+
let len = path.length;
|
|
181
|
+
if (!obj && !isCreate && len)
|
|
182
|
+
return undefined;
|
|
183
|
+
|
|
184
|
+
let i = 0;
|
|
185
|
+
for (let srcProp of path) {
|
|
186
|
+
|
|
187
|
+
// If the path is undefined and we're not to the end yet:
|
|
188
|
+
if (obj[srcProp] === undefined) {
|
|
189
|
+
|
|
190
|
+
// If the next index is an integer or integer string.
|
|
191
|
+
if (isCreate) {
|
|
192
|
+
if (i < len - 1) {
|
|
193
|
+
// If next level path is a number, create as an array
|
|
194
|
+
let isArray = (path[i + 1] + '').match(/^\d+$/);
|
|
195
|
+
obj[srcProp] = isArray ? [] : {};
|
|
196
|
+
}
|
|
197
|
+
} else
|
|
198
|
+
return undefined; // can't traverse
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// If last item in path
|
|
202
|
+
if (isCreate && i === len - 1)
|
|
203
|
+
obj[srcProp] = createVal;
|
|
204
|
+
|
|
205
|
+
// Traverse deeper along destination object.
|
|
206
|
+
obj = obj[srcProp];
|
|
207
|
+
i++;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return obj;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// d means "don't create"
|
|
214
|
+
let d = {};
|
|
399
215
|
|
|
400
216
|
let Util = {
|
|
401
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Returns true if they're the same.
|
|
220
|
+
* @param a
|
|
221
|
+
* @param b
|
|
222
|
+
* @returns {boolean} */
|
|
223
|
+
arraySame(a, b) {
|
|
224
|
+
let aLength = a.length;
|
|
225
|
+
if (aLength !== b.length)
|
|
226
|
+
return false;
|
|
227
|
+
for (let i=0; i<aLength; i++)
|
|
228
|
+
if (a[i] !== b[i])
|
|
229
|
+
return false;
|
|
230
|
+
return true; // the same.
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
bindId(root, el) {
|
|
234
|
+
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
235
|
+
if (id) { // If something hasn't removed the id.
|
|
236
|
+
|
|
237
|
+
// Don't allow overwriting existing class properties if they already have a non-Node value.
|
|
238
|
+
if (root[id] && !(root[id] instanceof Node))
|
|
239
|
+
throw new Error(`${root.constructor.name}.${id} already has a value. ` +
|
|
240
|
+
`Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
|
|
241
|
+
|
|
242
|
+
delve(root, id.split(/\./g), el);
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* @param style {HTMLStyleElement}
|
|
248
|
+
* @param root {HTMLElement} */
|
|
402
249
|
bindStyles(style, root) {
|
|
403
250
|
let styleId = root.getAttribute('data-style');
|
|
404
251
|
if (!styleId) {
|
|
@@ -411,17 +258,59 @@ let Util = {
|
|
|
411
258
|
root.setAttribute('data-style', styleId);
|
|
412
259
|
}
|
|
413
260
|
|
|
261
|
+
// Replace ":host" with "tagName[data-style=...]" in the css.
|
|
414
262
|
let tagName = root.tagName.toLowerCase();
|
|
415
263
|
for (let child of style.childNodes) {
|
|
416
264
|
if (child.nodeType === 3) {
|
|
417
265
|
let oldText = child.textContent;
|
|
418
|
-
let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, tagName
|
|
266
|
+
let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, `${tagName}[data-style="${styleId}"]`);
|
|
419
267
|
if (oldText !== newText)
|
|
420
268
|
child.textContent = newText;
|
|
421
269
|
}
|
|
422
270
|
}
|
|
423
271
|
},
|
|
424
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Convert a Proper Case name to a name with dashes.
|
|
275
|
+
* Dashes will be placed between letters and numbers.
|
|
276
|
+
* If there are multiple consecutive capital letters followed by another chracater, a dash will be placed before the last capital letter.
|
|
277
|
+
* @param str {string}
|
|
278
|
+
* @return {string}
|
|
279
|
+
*
|
|
280
|
+
* @example
|
|
281
|
+
* 'ProperName' => 'proper-name'
|
|
282
|
+
* 'HTMLElement' => 'html-element'
|
|
283
|
+
* 'BigUI' => 'big-ui'
|
|
284
|
+
* 'UIForm' => 'ui-form'
|
|
285
|
+
* 'A100' => 'a-100' */
|
|
286
|
+
camelToDashes(str) {
|
|
287
|
+
// Convert any capital letter that is preceded by a lowercase letter or number to lowercase and precede with a dash.
|
|
288
|
+
str = str.replace(/([a-z0-9])([A-Z])/g, '$1-$2');
|
|
289
|
+
|
|
290
|
+
// Convert any capital letter that is followed by a lowercase letter or number to lowercase and precede with a dash.
|
|
291
|
+
str = str.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2');
|
|
292
|
+
|
|
293
|
+
// Convert any number that is preceded by a lowercase or uppercase letter to be preceded by a dash.
|
|
294
|
+
str = str.replace(/([a-zA-Z])([0-9])/g, '$1-$2');
|
|
295
|
+
|
|
296
|
+
// Convert all the remaining capital letters to lowercase.
|
|
297
|
+
return str.toLowerCase();
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Converts a string written in kebab-case to camelCase.
|
|
302
|
+
*
|
|
303
|
+
* @param {string} str - The input string written in kebab-case.
|
|
304
|
+
* @return {string} - The resulting camelCase string.
|
|
305
|
+
*
|
|
306
|
+
* @example
|
|
307
|
+
* dashesToCamel('example-string') // Returns 'exampleString'
|
|
308
|
+
* dashesToCamel('another-example-test') // Returns 'anotherExampleTest' */
|
|
309
|
+
dashesToCamel(str) {
|
|
310
|
+
return str.replace(/-([a-z])/g, g => g[1].toUpperCase());
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
|
|
425
314
|
/**
|
|
426
315
|
* A generator function that recursively traverses and flattens a value.
|
|
427
316
|
*
|
|
@@ -448,23 +337,24 @@ let Util = {
|
|
|
448
337
|
* for (const item of flatten(complexArray))
|
|
449
338
|
* console.log(item); // Outputs: 1, 2, 3, 4, 5, 6, { a: 'object' }, 7, 8, 9
|
|
450
339
|
*/
|
|
451
|
-
*flatten(value) {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
},
|
|
340
|
+
// *flatten(value) {
|
|
341
|
+
// if (Array.isArray(value)) {
|
|
342
|
+
// for (const item of value) {
|
|
343
|
+
// yield* Util.flatten(item); // Recursively flatten arrays
|
|
344
|
+
// }
|
|
345
|
+
// } else if (typeof value === 'function') {
|
|
346
|
+
// const result = value();
|
|
347
|
+
// yield* Util.flatten(result); // Recursively flatten the result of a function
|
|
348
|
+
// } else
|
|
349
|
+
// yield value; // Yield primitive values as is
|
|
350
|
+
// },
|
|
462
351
|
|
|
463
352
|
/**
|
|
464
353
|
* Get the value of an input as the most appropriate JavaScript type.
|
|
465
354
|
* @param node {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|HTMLDivElement}
|
|
466
355
|
* @return {string|string[]|number|[]|File[]|Date|boolean} */
|
|
467
356
|
getInputValue(node) {
|
|
357
|
+
// .type is a built-in DOM property
|
|
468
358
|
if (node.type === 'checkbox' || node.type === 'radio')
|
|
469
359
|
return node.checked; // Boolean
|
|
470
360
|
if (node.type === 'file')
|
|
@@ -479,48 +369,84 @@ let Util = {
|
|
|
479
369
|
return node.value; // String
|
|
480
370
|
},
|
|
481
371
|
|
|
372
|
+
isEvent(attrName) {
|
|
373
|
+
return attrName.startsWith('on') && attrName in Globals$1.div;
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* @param el {HTMLElement}
|
|
378
|
+
* @param prop {string}
|
|
379
|
+
* @returns {boolean} */
|
|
380
|
+
isHtmlProp(el, prop) {
|
|
381
|
+
let key = el.tagName + '.' + prop;
|
|
382
|
+
let result = Globals$1.htmlProps[key];
|
|
383
|
+
if (result === undefined) { // Caching just barely makes this slightly faster.
|
|
384
|
+
let proto = Object.getPrototypeOf(el);
|
|
385
|
+
|
|
386
|
+
// Find the first HTMLElement that we inherit from (not our own classes)
|
|
387
|
+
while (proto) {
|
|
388
|
+
const ctorName = proto.constructor.name;
|
|
389
|
+
if (ctorName.startsWith('HTML') && ctorName.endsWith('Element'))
|
|
390
|
+
break
|
|
391
|
+
proto = Object.getPrototypeOf(proto);
|
|
392
|
+
}
|
|
393
|
+
Globals$1.htmlProps[key] = result = (proto
|
|
394
|
+
? !!Object.getOwnPropertyDescriptor(proto, prop)?.set
|
|
395
|
+
: false);
|
|
396
|
+
}
|
|
397
|
+
return result;
|
|
398
|
+
},
|
|
399
|
+
|
|
482
400
|
/**
|
|
483
401
|
* Is it an array and a path that can be evaluated by delve() ?
|
|
402
|
+
* We allow the first element to be null/undefined so binding can report errors.
|
|
484
403
|
* @param arr {Array|*}
|
|
485
404
|
* @returns {boolean} */
|
|
486
405
|
isPath(arr) {
|
|
487
|
-
return Array.isArray(arr) &&
|
|
406
|
+
return Array.isArray(arr) && arr.length >=2 // An array of at least two elements.
|
|
407
|
+
&& (typeof arr[0] === 'object' || arr[0] === undefined) // Where the first element is an object, null, or undefined.
|
|
408
|
+
&& !arr.slice(1).find(p => typeof p !== 'string' && typeof p !== 'number'); // Path 1..x is only numbers and strings.
|
|
409
|
+
},
|
|
410
|
+
|
|
411
|
+
isFalsy(val) {
|
|
412
|
+
return val === undefined || val === false || val === null;
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
/*
|
|
416
|
+
isPrimitive(val) {
|
|
417
|
+
return typeof val === 'string' || typeof val === 'number'
|
|
418
|
+
},*/
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* If val is a function, evaluate it recursively until the result is not a function.
|
|
422
|
+
* If it's an array or an object, convert it to Json.
|
|
423
|
+
* If it's a Date, format it as Y-m-d H:i:s
|
|
424
|
+
* @param val
|
|
425
|
+
* @returns {string|number|boolean} */
|
|
426
|
+
makePrimitive(val) {
|
|
427
|
+
if (typeof val === 'function')
|
|
428
|
+
return Util.makePrimitive(val());
|
|
429
|
+
else if (val instanceof Date)
|
|
430
|
+
return val.toISOString().replace(/T/, ' ');
|
|
431
|
+
else if (Array.isArray(val) || typeof val === 'object')
|
|
432
|
+
return ''; // JSON.stringify(val);
|
|
433
|
+
return val;
|
|
488
434
|
},
|
|
489
435
|
|
|
490
436
|
/**
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
oldNgMap.set(ng.startNode, ng);
|
|
502
|
-
|
|
503
|
-
// TODO: Is this necessary?
|
|
504
|
-
// if (ng.parentPath)
|
|
505
|
-
// ng.parentPath.clearNodesCache();
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
for (let i=0, node; node = oldNodes[i]; i++) {
|
|
509
|
-
let ng;
|
|
510
|
-
if (!node.parentNode && (ng = oldNgMap.get(node))) {
|
|
511
|
-
//ng.nodesCache = [];
|
|
512
|
-
let fragment = document.createDocumentFragment();
|
|
513
|
-
let endNode = ng.endNode;
|
|
514
|
-
while (node !== endNode) {
|
|
515
|
-
fragment.append(node);
|
|
516
|
-
//ng.nodesCache.push(node);
|
|
517
|
-
i++;
|
|
518
|
-
node = oldNodes[i];
|
|
519
|
-
}
|
|
520
|
-
fragment.append(endNode);
|
|
521
|
-
//ng.nodesCache.push(endNode);
|
|
522
|
-
}
|
|
437
|
+
* Use an array as the value of a map, appending to it when we add.
|
|
438
|
+
* Used by watch.js.
|
|
439
|
+
* @param map {Map|WeakMap|Object}
|
|
440
|
+
* @param key
|
|
441
|
+
* @param value */
|
|
442
|
+
mapArrayAdd(map, key, value) {
|
|
443
|
+
let result = map.get(key);
|
|
444
|
+
if (!result) {
|
|
445
|
+
result = [value];
|
|
446
|
+
map.set(key, result);
|
|
523
447
|
}
|
|
448
|
+
else
|
|
449
|
+
result.push(value);
|
|
524
450
|
},
|
|
525
451
|
|
|
526
452
|
/**
|
|
@@ -551,140 +477,12 @@ let Util = {
|
|
|
551
477
|
|
|
552
478
|
|
|
553
479
|
|
|
554
|
-
let div = document.createElement('div');
|
|
555
|
-
|
|
556
|
-
let isEvent = attrName => attrName.startsWith('on') && attrName in div;
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
/**
|
|
560
|
-
* Convert a Proper Case name to a name with dashes.
|
|
561
|
-
* Dashes will be placed between letters and numbers.
|
|
562
|
-
* If there are multiple consecutive capital letters followed by another chracater, a dash will be placed before the last capital letter.
|
|
563
|
-
* @param str {string}
|
|
564
|
-
* @return {string}
|
|
565
|
-
*
|
|
566
|
-
* @example
|
|
567
|
-
* 'ProperName' => 'proper-name'
|
|
568
|
-
* 'HTMLElement' => 'html-element'
|
|
569
|
-
* 'BigUI' => 'big-ui'
|
|
570
|
-
* 'UIForm' => 'ui-form'
|
|
571
|
-
* 'A100' => 'a-100' */
|
|
572
|
-
function camelToDashes(str) {
|
|
573
|
-
// Convert any capital letter that is preceded by a lowercase letter or number to lowercase and precede with a dash.
|
|
574
|
-
str = str.replace(/([a-z0-9])([A-Z])/g, '$1-$2');
|
|
575
|
-
|
|
576
|
-
// Convert any capital letter that is followed by a lowercase letter or number to lowercase and precede with a dash.
|
|
577
|
-
str = str.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2');
|
|
578
|
-
|
|
579
|
-
// Convert any number that is preceded by a lowercase or uppercase letter to be preceded by a dash.
|
|
580
|
-
str = str.replace(/([a-zA-Z])([0-9])/g, '$1-$2');
|
|
581
|
-
|
|
582
|
-
// Convert all the remaining capital letters to lowercase.
|
|
583
|
-
return str.toLowerCase();
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
/**
|
|
591
|
-
* Returns false if they're the same. Or the first index where they differ.
|
|
592
|
-
* @param a
|
|
593
|
-
* @param b
|
|
594
|
-
* @returns {boolean} */
|
|
595
|
-
function arraySame(a, b) {
|
|
596
|
-
let aLength = a.length;
|
|
597
|
-
if (aLength !== b.length)
|
|
598
|
-
return false;
|
|
599
|
-
for (let i=0; i<aLength; i++)
|
|
600
|
-
if (a[i] !== b[i])
|
|
601
|
-
return false;
|
|
602
|
-
return true; // the same.
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
/**
|
|
607
|
-
* TODO: Turn this into a class because it has internal state.
|
|
608
|
-
* TODO: Don't break on 3<a inside a <script> or <style> tag.
|
|
609
|
-
* @param html {?string} Pass null to reset context.
|
|
610
|
-
* @returns {string} */
|
|
611
|
-
function htmlContext(html) {
|
|
612
|
-
if (html === null) {
|
|
613
|
-
state = {...defaultState};
|
|
614
|
-
return state.context;
|
|
615
|
-
}
|
|
616
|
-
for (let i = 0; i < html.length; i++) {
|
|
617
|
-
const char = html[i];
|
|
618
|
-
switch (state.context) {
|
|
619
|
-
case htmlContext.Text:
|
|
620
|
-
if (char === '<' && html[i+1].match(/[a-z!]/i)) { // Start of a tag or comment.
|
|
621
|
-
// if (html.slice(i, i+4) === '<!--')
|
|
622
|
-
// state.context = htmlContext.Comment;
|
|
623
|
-
// else
|
|
624
|
-
state.context = htmlContext.Tag;
|
|
625
|
-
state.buffer = '';
|
|
626
|
-
}
|
|
627
|
-
break;
|
|
628
|
-
case htmlContext.Tag:
|
|
629
|
-
if (char === '>') {
|
|
630
|
-
state.context = htmlContext.Text;
|
|
631
|
-
state.quote = null;
|
|
632
|
-
state.buffer = '';
|
|
633
|
-
} else if (char === ' ' && !state.buffer) {
|
|
634
|
-
// No attribute name is present. Skipping the space.
|
|
635
|
-
continue;
|
|
636
|
-
} else if (char === ' ' || char === '/' || char === '?') {
|
|
637
|
-
state.buffer = ''; // Reset the buffer when a delimiter or potential self-closing sign is found.
|
|
638
|
-
} else if (char === '"' || char === "'" || char === '=') {
|
|
639
|
-
state.context = htmlContext.Attribute;
|
|
640
|
-
state.quote = char === '=' ? null : char;
|
|
641
|
-
state.buffer = '';
|
|
642
|
-
} else {
|
|
643
|
-
state.buffer += char;
|
|
644
|
-
}
|
|
645
|
-
break;
|
|
646
|
-
case htmlContext.Attribute:
|
|
647
|
-
if (!state.quote && !state.buffer.length && (char === '"' || char === "'"))
|
|
648
|
-
state.quote = char;
|
|
649
|
-
|
|
650
|
-
else if (char === state.quote || (!state.quote && state.buffer.length)) {
|
|
651
|
-
state.context = htmlContext.Tag;
|
|
652
|
-
state.quote = null;
|
|
653
|
-
state.buffer = '';
|
|
654
|
-
} else if (!state.quote && char === '>') {
|
|
655
|
-
state.context = htmlContext.Text;
|
|
656
|
-
state.quote = null;
|
|
657
|
-
state.buffer = '';
|
|
658
|
-
} else if (char !== ' ') {
|
|
659
|
-
state.buffer += char;
|
|
660
|
-
}
|
|
661
|
-
break;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
}
|
|
665
|
-
return state.context;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
htmlContext.Attribute = 'Attribute';
|
|
670
|
-
htmlContext.Text = 'Text';
|
|
671
|
-
htmlContext.Tag = 'Tag';
|
|
672
|
-
//htmlContext.Comment = 'Comment';
|
|
673
|
-
let defaultState = {
|
|
674
|
-
context: htmlContext.Text, // possible values: 'TEXT', 'TAG', 'ATTRIBUTE'
|
|
675
|
-
quote: null, // possible values: null, '"', "'"
|
|
676
|
-
buffer: '',
|
|
677
|
-
lastChar: null
|
|
678
|
-
};
|
|
679
|
-
let state = {...defaultState};
|
|
680
|
-
|
|
681
|
-
|
|
682
480
|
// For debugging only
|
|
683
481
|
|
|
684
482
|
|
|
685
483
|
class MultiValueMap {
|
|
686
484
|
|
|
687
|
-
/** @type {
|
|
485
|
+
/** @type {Record<string, Set>} */
|
|
688
486
|
data = {};
|
|
689
487
|
|
|
690
488
|
// Set a new value for a key
|
|
@@ -718,23 +516,14 @@ class MultiValueMap {
|
|
|
718
516
|
* @param val If specified, make sure we delete this specific value, if a key exists more than once.
|
|
719
517
|
* @returns {*|undefined} The deleted item. */
|
|
720
518
|
delete(key, val=undefined) {
|
|
721
|
-
// if (key === '["Html2",[[["Html3",["F1","A"]],["Html3",["F1","B"]]]]]')
|
|
722
|
-
// debugger;
|
|
723
|
-
|
|
724
519
|
let data = this.data;
|
|
725
|
-
|
|
726
|
-
// if (!data.hasOwnProperty(key))
|
|
727
|
-
// return undefined;
|
|
728
|
-
|
|
729
|
-
// Delete a specific value.
|
|
730
520
|
let result;
|
|
731
521
|
let set = data[key];
|
|
732
|
-
if (!set)
|
|
522
|
+
if (!set)
|
|
733
523
|
return undefined;
|
|
734
524
|
|
|
735
525
|
// Delete any value.
|
|
736
526
|
if (val === undefined) {
|
|
737
|
-
//result = set.values().next().value; // get first item from set.
|
|
738
527
|
[result] = set; // Does the same as above and seems to be about the same speed.
|
|
739
528
|
set.delete(result);
|
|
740
529
|
}
|
|
@@ -745,7 +534,6 @@ class MultiValueMap {
|
|
|
745
534
|
result = val;
|
|
746
535
|
}
|
|
747
536
|
|
|
748
|
-
// TODO: Will this make it slower?
|
|
749
537
|
if (set.size === 0)
|
|
750
538
|
delete data[key];
|
|
751
539
|
|
|
@@ -753,28 +541,34 @@ class MultiValueMap {
|
|
|
753
541
|
}
|
|
754
542
|
|
|
755
543
|
/**
|
|
756
|
-
*
|
|
757
|
-
* if not the latter, just delete any item that matches the key.
|
|
544
|
+
* Remove one value from a key, and return it.
|
|
758
545
|
* @param key {string}
|
|
759
|
-
* @param isPreferred {function}
|
|
760
546
|
* @returns {*|undefined} The deleted item. */
|
|
761
|
-
|
|
547
|
+
deleteAny(key) {
|
|
548
|
+
let data = this.data;
|
|
762
549
|
let result;
|
|
550
|
+
let set = data[key];
|
|
551
|
+
if (!set) // slower than pre-check.
|
|
552
|
+
return undefined;
|
|
553
|
+
|
|
554
|
+
[result] = set; // Does the same as above and seems to be about the same speed.
|
|
555
|
+
set.delete(result);
|
|
556
|
+
|
|
557
|
+
if (set.size === 0)
|
|
558
|
+
delete data[key];
|
|
559
|
+
|
|
560
|
+
return result;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
deleteSpecific(key, val) {
|
|
763
564
|
let data = this.data;
|
|
565
|
+
let result;
|
|
764
566
|
let set = data[key];
|
|
765
567
|
if (!set)
|
|
766
568
|
return undefined;
|
|
767
569
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
set.delete(val);
|
|
771
|
-
result = val;
|
|
772
|
-
break;
|
|
773
|
-
}
|
|
774
|
-
if (!result) {
|
|
775
|
-
[result] = set;
|
|
776
|
-
set.delete(result);
|
|
777
|
-
}
|
|
570
|
+
set.delete(val);
|
|
571
|
+
result = val;
|
|
778
572
|
|
|
779
573
|
if (set.size === 0)
|
|
780
574
|
delete data[key];
|
|
@@ -982,14 +776,19 @@ const udomdiff = (parentNode, a, b, before) => {
|
|
|
982
776
|
return b;
|
|
983
777
|
};
|
|
984
778
|
|
|
779
|
+
//import {ArraySpliceOp} from "./watch.js";
|
|
780
|
+
|
|
781
|
+
|
|
985
782
|
/**
|
|
986
783
|
* Path to where an expression should be evaluated within a Shell or NodeGroup.
|
|
987
784
|
* Path is only valid until the expressions before it are evaluated.
|
|
988
785
|
* TODO: Make this based on parent and node instead of path? */
|
|
989
786
|
class ExprPath {
|
|
990
787
|
|
|
788
|
+
|
|
789
|
+
|
|
991
790
|
/**
|
|
992
|
-
* @type {
|
|
791
|
+
* @type {ExprPathType} */
|
|
993
792
|
type;
|
|
994
793
|
|
|
995
794
|
// Used for attributes:
|
|
@@ -1005,8 +804,6 @@ class ExprPath {
|
|
|
1005
804
|
* @type {Set<string>} Used for type=AttribType.Multiple to remember the attributes that were added. */
|
|
1006
805
|
attrNames;
|
|
1007
806
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
807
|
/**
|
|
1011
808
|
* @type {Node} Node that occurs before this ExprPath's first Node.
|
|
1012
809
|
* This is necessary because udomdiff() can steal nodes from another ExprPath.
|
|
@@ -1048,16 +845,23 @@ class ExprPath {
|
|
|
1048
845
|
nodeMarkerPath;
|
|
1049
846
|
|
|
1050
847
|
|
|
1051
|
-
/** @type {?function} */
|
|
848
|
+
/** @type {?function} A function called by renderWatched() to update the value of this expression. */
|
|
1052
849
|
watchFunction
|
|
1053
850
|
|
|
851
|
+
/**
|
|
852
|
+
* @type {?function} The most recent callback passed to a .map() function in this ExprPath.
|
|
853
|
+
* TODO: What if one ExprPath has two .map() calls? Maybe we just won't support that. */
|
|
854
|
+
mapCallback
|
|
855
|
+
|
|
856
|
+
isHtmlProperty = undefined;
|
|
857
|
+
|
|
1054
858
|
/**
|
|
1055
859
|
* @param nodeBefore {Node}
|
|
1056
860
|
* @param nodeMarker {?Node}
|
|
1057
|
-
* @param type {
|
|
861
|
+
* @param type {ExprPathType}
|
|
1058
862
|
* @param attrName {?string}
|
|
1059
863
|
* @param attrValue {string[]} */
|
|
1060
|
-
constructor(nodeBefore, nodeMarker, type=
|
|
864
|
+
constructor(nodeBefore, nodeMarker, type=ExprPathType.Content, attrName=null, attrValue=null) {
|
|
1061
865
|
|
|
1062
866
|
// If path is a node.
|
|
1063
867
|
this.nodeBefore = nodeBefore;
|
|
@@ -1065,7 +869,7 @@ class ExprPath {
|
|
|
1065
869
|
this.type = type;
|
|
1066
870
|
this.attrName = attrName;
|
|
1067
871
|
this.attrValue = attrValue;
|
|
1068
|
-
if (type ===
|
|
872
|
+
if (type === ExprPathType.AttribMultiple)
|
|
1069
873
|
this.attrNames = new Set();
|
|
1070
874
|
}
|
|
1071
875
|
|
|
@@ -1079,36 +883,27 @@ class ExprPath {
|
|
|
1079
883
|
* We should modify path.applyValueAttrib so it stores the procssed parts and then only calls
|
|
1080
884
|
* setAttribute() once all the pieces are in place.
|
|
1081
885
|
*
|
|
1082
|
-
* @param expr {Expr}
|
|
1083
886
|
* @param exprs {Expr[]}
|
|
1084
|
-
* @param
|
|
1085
|
-
|
|
1086
|
-
* @returns {int} */
|
|
1087
|
-
apply(expr, exprs=null, exprIndex=0, componentExprs={}) {
|
|
887
|
+
* @param freeNodeGroups {boolean} */
|
|
888
|
+
apply(exprs, freeNodeGroups=true) {
|
|
1088
889
|
switch (this.type) {
|
|
1089
890
|
case 1: // PathType.Content:
|
|
1090
|
-
this.applyNodes(
|
|
891
|
+
this.applyNodes(exprs[0], freeNodeGroups);
|
|
1091
892
|
break;
|
|
1092
893
|
case 2: // PathType.Multiple:
|
|
1093
|
-
this.applyMultipleAttribs(this.nodeMarker,
|
|
894
|
+
this.applyMultipleAttribs(this.nodeMarker, exprs[0]);
|
|
1094
895
|
break;
|
|
1095
896
|
case 5: // PathType.Comment:
|
|
1096
897
|
// Expressions inside Html comments. Deliberately empty because we won't waste time updating them.
|
|
1097
898
|
break;
|
|
1098
899
|
case 6: // PathType.Event:
|
|
1099
|
-
this.applyEventAttrib(this.nodeMarker,
|
|
900
|
+
this.applyEventAttrib(this.nodeMarker, exprs[0], this.parentNg.rootNg.root);
|
|
1100
901
|
break;
|
|
1101
|
-
default:
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
else {
|
|
1105
|
-
// One attribute value may have multiple expressions. Here we apply them all at once.
|
|
1106
|
-
exprIndex = this.applyValueAttrib(this.nodeMarker, exprs || [expr], exprIndex);
|
|
1107
|
-
}
|
|
902
|
+
default: // TODO: Is this still used? Lots of tests fail without it.
|
|
903
|
+
// One attribute value may have multiple expressions. Here we apply them all at once.
|
|
904
|
+
this.applyValueAttrib(this.nodeMarker, exprs);
|
|
1108
905
|
break;
|
|
1109
906
|
}
|
|
1110
|
-
|
|
1111
|
-
return exprIndex;
|
|
1112
907
|
}
|
|
1113
908
|
|
|
1114
909
|
/**
|
|
@@ -1116,14 +911,16 @@ class ExprPath {
|
|
|
1116
911
|
* Called by applyExprs()
|
|
1117
912
|
* This function is recursive, as the functions it calls also call it.
|
|
1118
913
|
* @param expr {Expr}
|
|
914
|
+
* @param freeNodeGroups {boolean}
|
|
1119
915
|
* @return {Node[]} New Nodes created. */
|
|
1120
|
-
applyNodes(expr) {
|
|
916
|
+
applyNodes(expr, freeNodeGroups=true) {
|
|
1121
917
|
let path = this;
|
|
1122
918
|
|
|
1123
919
|
// This can be done at the beginning or the end of this function.
|
|
1124
920
|
// If at the end, we may get rendering done faster.
|
|
1125
921
|
// But when at the beginning, it leaves all the nodes in-use so we can do a renderWatched().
|
|
1126
|
-
|
|
922
|
+
if (freeNodeGroups)
|
|
923
|
+
path.freeNodeGroups();
|
|
1127
924
|
|
|
1128
925
|
|
|
1129
926
|
|
|
@@ -1133,10 +930,10 @@ class ExprPath {
|
|
|
1133
930
|
|
|
1134
931
|
let secondPass = []; // indices
|
|
1135
932
|
|
|
1136
|
-
path.nodeGroups = []; // Reset before
|
|
1137
|
-
path.
|
|
933
|
+
path.nodeGroups = []; // Reset before applyExactNodes and the code below rebuilds it.
|
|
934
|
+
path.applyExactNodes(expr, newNodes, secondPass);
|
|
1138
935
|
|
|
1139
|
-
this.existingTextNodes = null;
|
|
936
|
+
//this.existingTextNodes = null;
|
|
1140
937
|
|
|
1141
938
|
// TODO: Create an array of old vs Nodes and NodeGroups together.
|
|
1142
939
|
// If they're all the same, skip the next steps.
|
|
@@ -1174,7 +971,7 @@ class ExprPath {
|
|
|
1174
971
|
|
|
1175
972
|
|
|
1176
973
|
// This pre-check makes it a few percent faster?
|
|
1177
|
-
let same = arraySame(oldNodes, newNodes);
|
|
974
|
+
let same = Util.arraySame(oldNodes, newNodes);
|
|
1178
975
|
if (!same) {
|
|
1179
976
|
|
|
1180
977
|
path.nodesCache = newNodes; // Replaces value set by path.getNodes()
|
|
@@ -1193,59 +990,167 @@ class ExprPath {
|
|
|
1193
990
|
// Then only run it on the old nodeGroups that were actually removed.
|
|
1194
991
|
//Util.saveOrphans(oldNodeGroups, oldNodes);
|
|
1195
992
|
|
|
1196
|
-
for (let ng of oldNodeGroups)
|
|
1197
|
-
if (!ng.startNode.parentNode)
|
|
1198
|
-
ng.
|
|
993
|
+
for (let ng of oldNodeGroups)
|
|
994
|
+
if (!ng.startNode.parentNode)
|
|
995
|
+
ng.removeAndSaveOrphans();
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
// Instantiate components created within ${...} expressions.
|
|
1001
|
+
// Embedded style tags are handled elsewhere, but where?
|
|
1002
|
+
for (let el of newNodes) {
|
|
1003
|
+
if (el instanceof HTMLElement) {
|
|
1004
|
+
if (el.hasAttribute('solarite-placeholder'))
|
|
1005
|
+
this.parentNg.instantiateComponent(el);
|
|
1006
|
+
for (let child of el.querySelectorAll('[solarite-placeholder]'))
|
|
1007
|
+
this.parentNg.instantiateComponent(child);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* Used by watch() for inserting/removing/replacing individual loop items.
|
|
1018
|
+
* @param op {ArraySpliceOp} */
|
|
1019
|
+
applyArrayOp(op) {
|
|
1020
|
+
|
|
1021
|
+
// Replace NodeGroups
|
|
1022
|
+
let replaceCount = Math.min(op.deleteCount, op.items.length);
|
|
1023
|
+
let deleteCount = op.deleteCount - replaceCount;
|
|
1024
|
+
for (let i=0; i<replaceCount; i++) {
|
|
1025
|
+
let oldNg = this.nodeGroups[op.index + i]; // TODO: One expr can create multiple nodegroups.
|
|
1026
|
+
|
|
1027
|
+
// Try to find an exact match
|
|
1028
|
+
let func = this.mapCallback || this.watchFunction;
|
|
1029
|
+
let expr = func(op.items[i]);
|
|
1030
|
+
|
|
1031
|
+
// If the result of func isn't a template, conver it to one or more templates.
|
|
1032
|
+
this.exprToTemplates(expr, template => { // TODO: An expr can create multiple NodeGroups. I need a way to group them.
|
|
1033
|
+
|
|
1034
|
+
let ng = this.getNodeGroup(template, true); // Removes from nodeGroupsAttached and adds to nodeGroupsRendered()
|
|
1035
|
+
if (ng && ng === oldNg) ; else {
|
|
1036
|
+
|
|
1037
|
+
// Find a close match or create a new node group
|
|
1038
|
+
if (!ng)
|
|
1039
|
+
ng = this.getNodeGroup(template, false); // adds back to nodeGroupsRendered()
|
|
1040
|
+
this.nodeGroups[op.index + i] = ng; // TODO: Remove old one to nodeGroupsDetached?
|
|
1041
|
+
|
|
1042
|
+
// Splice in the new nodes.
|
|
1043
|
+
let insertBefore = oldNg.startNode;
|
|
1044
|
+
for (let node of ng.getNodes())
|
|
1045
|
+
insertBefore.parentNode.insertBefore(node, insertBefore);
|
|
1046
|
+
|
|
1047
|
+
// Remove the old nodes.
|
|
1048
|
+
if (ng !== oldNg)
|
|
1049
|
+
oldNg.removeAndSaveOrphans();
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// Delete extra at the end.
|
|
1055
|
+
if (deleteCount > 0) {
|
|
1056
|
+
for (let i=0; i<deleteCount; i++) {
|
|
1057
|
+
let oldNg = this.nodeGroups[op.index + replaceCount + i];
|
|
1058
|
+
oldNg.removeAndSaveOrphans();
|
|
1059
|
+
}
|
|
1060
|
+
this.nodeGroups.splice(op.index + replaceCount, deleteCount);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Add extra at the end.
|
|
1064
|
+
else {
|
|
1065
|
+
let newItems = op.items.slice(replaceCount);
|
|
1066
|
+
|
|
1067
|
+
let insertBefore = this.nodeGroups[op.index + replaceCount]?.startNode || this.nodeMarker;
|
|
1068
|
+
for (let i = 0; i < newItems.length; i++) { // We use nodeMarker if the subequent (or all) nodeGroups have been removed.
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
// Try to find exact match
|
|
1072
|
+
let template = this.mapCallback(newItems[i]);
|
|
1073
|
+
let ng = this.getNodeGroup(template, true); // Removes from nodeGroupsAttached and adds to nodeGroupsRendered()
|
|
1074
|
+
if (!ng) // Find a close match or create a new node group
|
|
1075
|
+
ng = this.getNodeGroup(template, false); // adds back to nodeGroupsRendered()
|
|
1076
|
+
|
|
1077
|
+
this.nodeGroups.push(ng);
|
|
1078
|
+
|
|
1079
|
+
// Splice in the new nodes.
|
|
1080
|
+
for (let node of ng.getNodes())
|
|
1081
|
+
insertBefore.parentNode.insertBefore(node, insertBefore);
|
|
1082
|
+
}
|
|
1199
1083
|
}
|
|
1200
1084
|
|
|
1201
|
-
|
|
1202
1085
|
|
|
1086
|
+
|
|
1087
|
+
// TODO: update or invalidate the nodes cache?
|
|
1088
|
+
this.nodesCache = null;
|
|
1203
1089
|
}
|
|
1204
1090
|
|
|
1205
1091
|
/**
|
|
1206
|
-
*
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1092
|
+
* Recursively traverse expr.
|
|
1093
|
+
* If a value is a function, evaluate it.
|
|
1094
|
+
* If a value is an array, recurse on each item.
|
|
1095
|
+
* If it's a primitive, convert it to a Template.
|
|
1096
|
+
* Otherwise pass the item (which is now either a Template or a Node) to callback.
|
|
1097
|
+
* @param expr
|
|
1098
|
+
* @param callback {function(Node|Template)}
|
|
1099
|
+
*
|
|
1100
|
+
* TODO: have applyExactNodes() use this function. */
|
|
1101
|
+
exprToTemplates(expr, callback) {
|
|
1102
|
+
if (Array.isArray(expr))
|
|
1103
|
+
for (let subExpr of expr)
|
|
1104
|
+
this.exprToTemplates(subExpr, callback);
|
|
1217
1105
|
|
|
1106
|
+
else if (typeof expr === 'function') {
|
|
1107
|
+
// TODO: One ExprPath can have multiple expr functions.
|
|
1108
|
+
// But if using it as a watch, it should only have one at the top level.
|
|
1109
|
+
// So maybe this is ok.
|
|
1110
|
+
Globals$1.currentExprPath = this; // Used by watch()
|
|
1218
1111
|
|
|
1112
|
+
this.watchFunction = expr; // TODO: Only do this if it's a top level function.
|
|
1113
|
+
expr = expr(); // As expr accesses watched variables, watch() uses Globals.currentExprPath to mark where those watched variables are being used.
|
|
1114
|
+
Globals$1.currentExprPath = null;
|
|
1219
1115
|
|
|
1220
|
-
|
|
1116
|
+
this.exprToTemplates(expr, callback);
|
|
1117
|
+
}
|
|
1221
1118
|
|
|
1222
|
-
|
|
1119
|
+
// String/Number/Date/Boolean
|
|
1120
|
+
else if (!(expr instanceof Template) && !(expr instanceof Node)){
|
|
1121
|
+
// Convert expression to a string.
|
|
1122
|
+
if (expr === undefined || expr === false || expr === null) // Util.isFalsy() inlined
|
|
1123
|
+
expr = '';
|
|
1124
|
+
else if (typeof expr !== 'string')
|
|
1125
|
+
expr += '';
|
|
1223
1126
|
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1127
|
+
// Get the same Template for the same string each time.
|
|
1128
|
+
// let template = Globals.stringTemplates[expr];
|
|
1129
|
+
// if (!template) {
|
|
1130
|
+
let template = new Template([expr], []);
|
|
1131
|
+
// Globals.stringTemplates[expr] = template;
|
|
1132
|
+
//}
|
|
1228
1133
|
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
node.remove();
|
|
1232
|
-
oldNg.saveOrphans();
|
|
1134
|
+
// Recurse.
|
|
1135
|
+
this.exprToTemplates(template, callback);
|
|
1233
1136
|
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
this.nodesCache = null;
|
|
1137
|
+
else
|
|
1138
|
+
callback(expr);
|
|
1237
1139
|
}
|
|
1238
1140
|
|
|
1239
1141
|
|
|
1240
1142
|
/**
|
|
1241
|
-
*
|
|
1143
|
+
* Try to apply Nodes that are an exact match, by finding existing nodes from the last render
|
|
1144
|
+
* that have the same value as created by the expr.
|
|
1145
|
+
* This is called from ExprPath.applyNodes().
|
|
1146
|
+
*
|
|
1242
1147
|
* @param expr {Template|Node|Array|function|*}
|
|
1243
|
-
* @param newNodes {(Node|Template)[]}
|
|
1244
|
-
* @param secondPass {
|
|
1245
|
-
|
|
1148
|
+
* @param newNodes {(Node|Template)[]} An inout parameter; we add the nodes here as we go.
|
|
1149
|
+
* @param secondPass {[int, int][]} Locations within newNodes for ExprPath.applyNodes() to evaluate later,
|
|
1150
|
+
* when it tries to find partial matches. */
|
|
1151
|
+
applyExactNodes(expr, newNodes, secondPass) {
|
|
1246
1152
|
|
|
1247
1153
|
if (expr instanceof Template) {
|
|
1248
|
-
|
|
1249
1154
|
let ng = this.getNodeGroup(expr, true);
|
|
1250
1155
|
if (ng) {
|
|
1251
1156
|
|
|
@@ -1263,7 +1168,7 @@ class ExprPath {
|
|
|
1263
1168
|
}
|
|
1264
1169
|
}
|
|
1265
1170
|
|
|
1266
|
-
// Node created by an expression.
|
|
1171
|
+
// Node(s) created by an expression.
|
|
1267
1172
|
else if (expr instanceof Node) {
|
|
1268
1173
|
|
|
1269
1174
|
// DocumentFragment created by an expression.
|
|
@@ -1276,49 +1181,10 @@ class ExprPath {
|
|
|
1276
1181
|
// Arrays and functions.
|
|
1277
1182
|
// I tried iterating over the result of a generator function to avoid this recursion and simplify the code,
|
|
1278
1183
|
// but that consistently made the js-framework-benchmarks a few percentage points slower.
|
|
1279
|
-
else
|
|
1280
|
-
|
|
1281
|
-
this.
|
|
1282
|
-
|
|
1283
|
-
else if (typeof expr === 'function') {
|
|
1284
|
-
// TODO: One ExprPath can have multiple expr functions.
|
|
1285
|
-
// But if using it as a watch, it should only have one at the top level.
|
|
1286
|
-
// So maybe this is ok.
|
|
1287
|
-
Globals.currentExprPath = [this, expr]; // Used by watch3()
|
|
1288
|
-
this.watchFunction = expr; // TODO: Only do this if it's a top level function.
|
|
1289
|
-
let result = expr();
|
|
1290
|
-
Globals.currentExprPath = null;
|
|
1291
|
-
|
|
1292
|
-
this.applyExact(result, newNodes, secondPass);
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
// Text
|
|
1296
|
-
else {
|
|
1297
|
-
// Convert falsy values (but not 0) to empty string.
|
|
1298
|
-
// Convert numbers to string so they compare the same.
|
|
1299
|
-
let text = (expr === undefined || expr === false || expr === null) ? '' : (expr + '');
|
|
1300
|
-
|
|
1301
|
-
// Fast path for updating the text of a single text node.
|
|
1302
|
-
let first = this.nodeBefore.nextSibling;
|
|
1303
|
-
if (first.nodeType === 3 && first.nextSibling === this.nodeMarker && !newNodes.includes(first)) {
|
|
1304
|
-
if (first.textContent !== text)
|
|
1305
|
-
first.textContent = text;
|
|
1306
|
-
|
|
1307
|
-
newNodes.push(first);
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
else {
|
|
1311
|
-
// TODO: Optimize this into a Set or Map or something?
|
|
1312
|
-
if (!this.existingTextNodes)
|
|
1313
|
-
this.existingTextNodes = this.getNodes().filter(n => n.nodeType === 3);
|
|
1314
|
-
|
|
1315
|
-
let idx = this.existingTextNodes.findIndex(n => n.textContent === text);
|
|
1316
|
-
if (idx !== -1)
|
|
1317
|
-
newNodes.push(...this.existingTextNodes.splice(idx, 1));
|
|
1318
|
-
else
|
|
1319
|
-
newNodes.push(this.nodeMarker.ownerDocument.createTextNode(text));
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1184
|
+
else
|
|
1185
|
+
this.exprToTemplates(expr, template => {
|
|
1186
|
+
this.applyExactNodes(template, newNodes, secondPass);
|
|
1187
|
+
});
|
|
1322
1188
|
}
|
|
1323
1189
|
|
|
1324
1190
|
applyMultipleAttribs(node, expr) {
|
|
@@ -1331,6 +1197,13 @@ class ExprPath {
|
|
|
1331
1197
|
let oldNames = this.attrNames;
|
|
1332
1198
|
this.attrNames = new Set();
|
|
1333
1199
|
if (expr) {
|
|
1200
|
+
if (typeof expr === 'function') {
|
|
1201
|
+
Globals$1.currentExprPath = this; // Used by watch()
|
|
1202
|
+
this.watchFunction = expr; // used by renderWatched()
|
|
1203
|
+
expr = expr();
|
|
1204
|
+
Globals$1.currentExprPath = null;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1334
1207
|
let attrs = (expr +'') // Split string into multiple attributes.
|
|
1335
1208
|
.split(/([\w-]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s]+))/g)
|
|
1336
1209
|
.map(text => text.trim())
|
|
@@ -1364,46 +1237,51 @@ class ExprPath {
|
|
|
1364
1237
|
|
|
1365
1238
|
let eventName = this.attrName.slice(2); // remove "on-" prefix.
|
|
1366
1239
|
let func;
|
|
1367
|
-
|
|
1368
|
-
// Convert array to function.
|
|
1369
1240
|
let args = [];
|
|
1370
|
-
if (Array.isArray(expr)) {
|
|
1371
|
-
|
|
1372
|
-
// oninput=${[this.doSomething, 'meow']}
|
|
1373
|
-
if (typeof expr[0] === 'function') {
|
|
1374
|
-
func = expr[0];
|
|
1375
|
-
args = expr.slice(1);
|
|
1376
|
-
}
|
|
1377
1241
|
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
// root.render(); // TODO: This causes infinite recursion.
|
|
1384
|
-
}
|
|
1242
|
+
// Convert array to function.
|
|
1243
|
+
// oninput=${[this.doSomething, 'meow']}
|
|
1244
|
+
if (Array.isArray(expr) && typeof expr[0] === 'function') {
|
|
1245
|
+
func = expr[0];
|
|
1246
|
+
args = expr.slice(1);
|
|
1385
1247
|
}
|
|
1386
|
-
else
|
|
1248
|
+
else if (typeof expr === 'function')
|
|
1387
1249
|
func = expr;
|
|
1250
|
+
else
|
|
1251
|
+
throw new Error(`Invalid event binding: <${node.tagName.toLowerCase()} ${this.attrName}=\${${JSON.stringify(expr)}}>`);
|
|
1388
1252
|
|
|
1389
1253
|
this.bindEvent(node, root, eventName, eventName, func, args);
|
|
1390
1254
|
}
|
|
1391
1255
|
|
|
1392
1256
|
|
|
1257
|
+
/**
|
|
1258
|
+
* Call function when eventName is triggerd on node.
|
|
1259
|
+
* @param node {HTMLElement}
|
|
1260
|
+
* @param root {HTMLElement}
|
|
1261
|
+
* @param key {string}
|
|
1262
|
+
* @param eventName {string}
|
|
1263
|
+
* @param func {function}
|
|
1264
|
+
* @param args {array}
|
|
1265
|
+
* @param capture {boolean} */
|
|
1393
1266
|
bindEvent(node, root, key, eventName, func, args, capture=false) {
|
|
1394
|
-
let nodeEvents = Globals.nodeEvents.get(node);
|
|
1267
|
+
let nodeEvents = Globals$1.nodeEvents.get(node);
|
|
1395
1268
|
if (!nodeEvents) {
|
|
1396
1269
|
nodeEvents = {[key]: new Array(3)};
|
|
1397
|
-
Globals.nodeEvents.set(node, nodeEvents);
|
|
1270
|
+
Globals$1.nodeEvents.set(node, nodeEvents);
|
|
1398
1271
|
}
|
|
1399
1272
|
let nodeEvent = nodeEvents[key];
|
|
1400
1273
|
if (!nodeEvent)
|
|
1401
1274
|
nodeEvents[key] = nodeEvent = new Array(3);
|
|
1402
1275
|
|
|
1276
|
+
if (typeof func !== 'function')
|
|
1277
|
+
throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${${func}}> because it's not a function.`);
|
|
1403
1278
|
|
|
1404
1279
|
// If function has changed, remove and rebind the event.
|
|
1405
1280
|
if (nodeEvent[0] !== func) {
|
|
1406
1281
|
|
|
1282
|
+
// TODO: We should be removing event listeners when calling getNodeGroup(),
|
|
1283
|
+
// when we get the node from the list of nodeGroupsAttached/nodeGroupsDetached,
|
|
1284
|
+
// instead of only when we rebind an event.
|
|
1407
1285
|
let [existing, existingBound, _] = nodeEvent;
|
|
1408
1286
|
if (existing)
|
|
1409
1287
|
node.removeEventListener(eventName, existingBound, capture);
|
|
@@ -1433,68 +1311,144 @@ class ExprPath {
|
|
|
1433
1311
|
nodeEvents[key][2] = args;
|
|
1434
1312
|
}
|
|
1435
1313
|
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
else if (!this.attrValue && expr === true)
|
|
1444
|
-
node.setAttribute(this.attrName, '');
|
|
1314
|
+
/**
|
|
1315
|
+
* Handle values, including two-way binding.
|
|
1316
|
+
* @param node
|
|
1317
|
+
* @param exprs */
|
|
1318
|
+
// TODO: node is always this.nodeMarker?
|
|
1319
|
+
applyValueAttrib(node, exprs) {
|
|
1320
|
+
let expr = exprs[0];
|
|
1445
1321
|
|
|
1322
|
+
// Two-way binding between attributes
|
|
1446
1323
|
// Passing a path to the value attribute.
|
|
1447
|
-
//
|
|
1448
|
-
|
|
1324
|
+
// Copies the attribute to the property when the input event fires.
|
|
1325
|
+
// value=${[this, 'value]'}
|
|
1326
|
+
// checked=${[this, 'isAgree']}
|
|
1327
|
+
// This same logic is in NodeGroup.instantiateComponent() for components.
|
|
1328
|
+
if (Util.isPath(expr)) {
|
|
1449
1329
|
let [obj, path] = [expr[0], expr.slice(1)];
|
|
1450
|
-
node.value = delve(obj, path);
|
|
1451
|
-
// TODO: We need to remove any old listeners, like in bindEventAttribute
|
|
1452
1330
|
|
|
1331
|
+
if (!obj)
|
|
1332
|
+
throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${[${expr.map(item => item ? `'${item}'` : item+'').join(', ')}]}>.`);
|
|
1333
|
+
|
|
1334
|
+
let value = delve(obj, path);
|
|
1335
|
+
|
|
1336
|
+
// Special case to allow setting select-multiple value from an array
|
|
1337
|
+
if (this.attrName === 'value' && node.type === 'select-multiple' && Array.isArray(value)) {
|
|
1338
|
+
// Set the .selected property on the options having a value within value.
|
|
1339
|
+
let strValues = value.map(v => v + '');
|
|
1340
|
+
for (let option of node.options)
|
|
1341
|
+
option.selected = strValues.includes(option.value);
|
|
1342
|
+
}
|
|
1343
|
+
else {
|
|
1344
|
+
// TODO: should we remove isFalsy, since these are always props?
|
|
1345
|
+
let strValue = Util.isFalsy(value) ? '' : value;
|
|
1346
|
+
|
|
1347
|
+
// If we don't have this condition, when we call render(), the browser will scroll to the currently
|
|
1348
|
+
// selected item in a <select> and mess up manually scrolling to a different value.
|
|
1349
|
+
if (strValue !== node[this.attrName])
|
|
1350
|
+
node[this.attrName] = strValue;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// TODO: We need to remove any old listeners, like in bindEventAttribute.
|
|
1354
|
+
// Does bindEvent() now handle that?
|
|
1453
1355
|
let func = () => {
|
|
1454
|
-
|
|
1356
|
+
let value = (this.attrName === 'value')
|
|
1357
|
+
? Util.getInputValue(node)
|
|
1358
|
+
: node[this.attrName];
|
|
1359
|
+
delve(obj, path, value);
|
|
1455
1360
|
};
|
|
1456
1361
|
|
|
1457
1362
|
// We use capture so we update the values before other events added by the user.
|
|
1458
|
-
|
|
1363
|
+
// TODO: Bind to scroll events also?
|
|
1364
|
+
// What about resize events and width/height?
|
|
1365
|
+
this.bindEvent(node, path[0], this.attrName, 'input', func, [], true);
|
|
1459
1366
|
}
|
|
1460
1367
|
|
|
1461
1368
|
// Regular attribute
|
|
1462
1369
|
else {
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
//
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1370
|
+
// TODO: Cache this on ExprPath.isProp when Shell creates the props. Have ExprPath.clone() copy .isProp
|
|
1371
|
+
// Or make it a new PathType.
|
|
1372
|
+
//if (this.attrName === 'disabled')
|
|
1373
|
+
// debugger;
|
|
1374
|
+
|
|
1375
|
+
// hasOwnProperty() checks only the object, not the parents
|
|
1376
|
+
// this.attrName in node checks the node and the parents.
|
|
1377
|
+
// This version checks the html element it extends from, to see if has a setter set:
|
|
1378
|
+
// Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), this.attrName)?.set
|
|
1379
|
+
//let isProp = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), this.attrName)?.set;
|
|
1380
|
+
let isProp = this.isHtmlProperty;
|
|
1381
|
+
if (isProp === undefined)
|
|
1382
|
+
isProp = this.isHtmlProperty = Util.isHtmlProp(node, this.attrName);
|
|
1383
|
+
|
|
1384
|
+
// Values to toggle an attribute
|
|
1385
|
+
let multiple = this.attrValue;
|
|
1386
|
+
if (!multiple) {
|
|
1387
|
+
Globals$1.currentExprPath = this; // Used by watch()
|
|
1388
|
+
if (typeof expr === 'function') {
|
|
1389
|
+
if (this.type === 4) { // Don't evaluate functions before passing them to components
|
|
1390
|
+
return
|
|
1474
1391
|
}
|
|
1392
|
+
this.watchFunction = expr; // The function that gets the expression, used for renderWatched()
|
|
1393
|
+
expr = expr();
|
|
1475
1394
|
}
|
|
1476
|
-
|
|
1395
|
+
else
|
|
1396
|
+
expr = Util.makePrimitive(expr);
|
|
1397
|
+
Globals$1.currentExprPath = null;
|
|
1398
|
+
}
|
|
1399
|
+
if (!multiple && (expr === undefined || expr === false || expr === null)) { // Util.isFalsy() inlined.
|
|
1400
|
+
if (isProp)
|
|
1401
|
+
node[this.attrName] = false;
|
|
1402
|
+
node.removeAttribute(this.attrName);
|
|
1403
|
+
}
|
|
1404
|
+
else if (!multiple && expr === true) {
|
|
1405
|
+
if (isProp)
|
|
1406
|
+
node[this.attrName] = true;
|
|
1407
|
+
node.setAttribute(this.attrName, '');
|
|
1477
1408
|
}
|
|
1478
|
-
else
|
|
1479
|
-
value.unshift(expr);
|
|
1480
1409
|
|
|
1481
|
-
|
|
1410
|
+
// A non-toggled attribute
|
|
1411
|
+
else {
|
|
1482
1412
|
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1413
|
+
// If it's a series of expressions among strings, join them together.
|
|
1414
|
+
let joinedValue;
|
|
1415
|
+
if (multiple) {
|
|
1416
|
+
let value = [];
|
|
1417
|
+
for (let i = 0; i < this.attrValue.length; i++) {
|
|
1418
|
+
value.push(this.attrValue[i]);
|
|
1419
|
+
if (i < this.attrValue.length - 1) {
|
|
1420
|
+
Globals$1.currentExprPath = this; // Used by watch()
|
|
1421
|
+
let val = Util.makePrimitive(exprs[i]);
|
|
1422
|
+
Globals$1.currentExprPath = null;
|
|
1423
|
+
if (!Util.isFalsy(val))
|
|
1424
|
+
value.push(val);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
joinedValue = value.join('');
|
|
1428
|
+
}
|
|
1489
1429
|
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1430
|
+
// If the attribute is one expression with no strings:
|
|
1431
|
+
else
|
|
1432
|
+
joinedValue = expr;
|
|
1433
|
+
|
|
1434
|
+
// Only update attributes if the value has changed.
|
|
1435
|
+
// This is needed for setting input.value, .checked, option.selected, etc.
|
|
1436
|
+
|
|
1437
|
+
let oldVal = isProp
|
|
1438
|
+
? node[this.attrName]
|
|
1439
|
+
: node.getAttribute(this.attrName);
|
|
1440
|
+
if (oldVal !== joinedValue) {
|
|
1441
|
+
|
|
1442
|
+
// <textarea value=${expr}></textarea>
|
|
1443
|
+
// Without this branch we have no way to set the value of a textarea,
|
|
1444
|
+
// since we also prohibit expressions that are a child of textarea.
|
|
1445
|
+
if (isProp)
|
|
1446
|
+
node[this.attrName] = joinedValue;
|
|
1447
|
+
// TODO: Putting an 'else' here would be more performant
|
|
1448
|
+
node.setAttribute(this.attrName, joinedValue);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1495
1451
|
}
|
|
1496
|
-
|
|
1497
|
-
return exprIndex;
|
|
1498
1452
|
}
|
|
1499
1453
|
|
|
1500
1454
|
|
|
@@ -1510,7 +1464,8 @@ class ExprPath {
|
|
|
1510
1464
|
let nodeMarker, nodeBefore;
|
|
1511
1465
|
let root = newRoot;
|
|
1512
1466
|
let path = pathOffset ? this.nodeMarkerPath.slice(0, -pathOffset) : this.nodeMarkerPath;
|
|
1513
|
-
|
|
1467
|
+
let length = path.length-1;
|
|
1468
|
+
for (let i=length; i>0; i--) // Resolve the path.
|
|
1514
1469
|
root = root.childNodes[path[i]];
|
|
1515
1470
|
let childNodes = root.childNodes;
|
|
1516
1471
|
|
|
@@ -1550,7 +1505,7 @@ class ExprPath {
|
|
|
1550
1505
|
|
|
1551
1506
|
/**
|
|
1552
1507
|
* Attempt to remove all of this ExprPath's nodes from the DOM, if it can be done using a special fast method.
|
|
1553
|
-
* @returns {boolean} Returns false if Nodes
|
|
1508
|
+
* @returns {boolean} Returns false if Nodes weren't removed, and they should instead be removed manually. */
|
|
1554
1509
|
fastClear() {
|
|
1555
1510
|
let parent = this.nodeBefore.parentNode;
|
|
1556
1511
|
if (this.nodeBefore === parent.firstChild && this.nodeMarker === parent.lastChild) {
|
|
@@ -1586,6 +1541,10 @@ class ExprPath {
|
|
|
1586
1541
|
// result2.push(...ng.getNodes())
|
|
1587
1542
|
// return result2;
|
|
1588
1543
|
|
|
1544
|
+
if (this.type === ExprPathType.AttribValue || this.type === ExprPathType.AttribMultiple || this.type === ExprPathType.ComponentAttribValue) {
|
|
1545
|
+
return [this.nodeMarker];
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1589
1548
|
|
|
1590
1549
|
let result;
|
|
1591
1550
|
|
|
@@ -1610,7 +1569,8 @@ class ExprPath {
|
|
|
1610
1569
|
return result;
|
|
1611
1570
|
}
|
|
1612
1571
|
|
|
1613
|
-
|
|
1572
|
+
/** @return {HTMLElement|ParentNode} */
|
|
1573
|
+
getParentNode() {
|
|
1614
1574
|
return this.nodeMarker.parentNode
|
|
1615
1575
|
}
|
|
1616
1576
|
|
|
@@ -1627,28 +1587,39 @@ class ExprPath {
|
|
|
1627
1587
|
* or createa new NodeGroup from the template.
|
|
1628
1588
|
* @return {NodeGroup} */
|
|
1629
1589
|
getNodeGroup(template, exact=true) {
|
|
1630
|
-
//if (exact && this.nodeGroupsFree.isEmpty())
|
|
1631
|
-
// return null;
|
|
1632
1590
|
|
|
1633
1591
|
let result;
|
|
1592
|
+
let collection = this.nodeGroupsAttachedAvailable;
|
|
1634
1593
|
|
|
1635
1594
|
// TODO: Would it be faster to maintain a separate list of detached nodegroups?
|
|
1636
1595
|
if (exact) { // [below] parentElement will be null if the parent is a DocumentFragment
|
|
1637
|
-
result =
|
|
1596
|
+
result = collection.deleteAny(template.getExactKey());
|
|
1597
|
+
if (!result) { // try searching detached
|
|
1598
|
+
collection = this.nodeGroupsDetachedAvailable;
|
|
1599
|
+
result = collection.deleteAny(template.getExactKey());
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1638
1602
|
if (result) // also delete the matching close key.
|
|
1639
|
-
|
|
1640
|
-
else
|
|
1603
|
+
collection.deleteSpecific(template.getCloseKey(), result);
|
|
1604
|
+
else {
|
|
1641
1605
|
return null;
|
|
1606
|
+
}
|
|
1642
1607
|
}
|
|
1643
1608
|
|
|
1644
1609
|
// Find a close match.
|
|
1645
1610
|
// This is a match that has matching html, but different expressions applied.
|
|
1646
1611
|
// We can then apply the expressions to make it an exact match.
|
|
1647
|
-
|
|
1648
|
-
|
|
1612
|
+
// If the template has no expressions, the key is the html, and we've already searched for an exact match. There won't be an inexact match.
|
|
1613
|
+
else if (template.exprs.length) {
|
|
1614
|
+
result = collection.deleteAny(template.getCloseKey());
|
|
1615
|
+
if (!result) { // try searching detached
|
|
1616
|
+
collection = this.nodeGroupsDetachedAvailable;
|
|
1617
|
+
result = collection.deleteAny(template.getCloseKey());
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1649
1620
|
if (result) {
|
|
1650
1621
|
|
|
1651
|
-
|
|
1622
|
+
collection.deleteSpecific(result.exactKey, result);
|
|
1652
1623
|
|
|
1653
1624
|
// Update this close match with the new expression values.
|
|
1654
1625
|
result.applyExprs(template.exprs);
|
|
@@ -1660,95 +1631,85 @@ class ExprPath {
|
|
|
1660
1631
|
result = new NodeGroup(template, this);
|
|
1661
1632
|
|
|
1662
1633
|
// old:
|
|
1663
|
-
this.
|
|
1664
|
-
|
|
1665
|
-
// new:
|
|
1666
|
-
// let ngiu = this.nodeGroupsInUse;
|
|
1667
|
-
// ngiu.add(result.exactKey, result);
|
|
1668
|
-
// ngiu.add(result.closeKey, result);
|
|
1634
|
+
this.nodeGroupsRendered.push(result);
|
|
1669
1635
|
|
|
1670
1636
|
|
|
1671
1637
|
return result;
|
|
1672
1638
|
}
|
|
1673
1639
|
|
|
1640
|
+
isComponent() {
|
|
1641
|
+
// Events won't have type===Component.
|
|
1642
|
+
// TODO: Have a special flag for components instead of it being on the type?
|
|
1643
|
+
return this.type === ExprPathType.ComponentAttribValue || (this.attrName && this.nodeMarker.tagName && this.nodeMarker.tagName.includes('-'));
|
|
1644
|
+
}
|
|
1674
1645
|
|
|
1675
1646
|
/**
|
|
1647
|
+
* TODO: Rename this to nodeGroupsInUse, nodeGroupsAvialableAttached and nodeGroupsAvailableDetached?
|
|
1648
|
+
* Nodes that have been used during the current render().
|
|
1676
1649
|
* Used with getNodeGroup() and freeNodeGroups().
|
|
1677
1650
|
* TODO: Use an array of WeakRef so the gc can collect them?
|
|
1678
1651
|
* TODO: Put items back in nodeGroupsInUse after applyExpr() is called, not before.
|
|
1679
1652
|
* @type {NodeGroup[]} */
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
/** @type {MultiValueMap<key:string, value:NodeGroup>} */
|
|
1683
|
-
//nodeGroupsInUse = new MultiValueMap();
|
|
1653
|
+
nodeGroupsRendered = [];
|
|
1684
1654
|
|
|
1685
1655
|
/**
|
|
1656
|
+
* Nodes that were added to the web component during the last render(), but are available to be used again.
|
|
1686
1657
|
* Used with getNodeGroup() and freeNodeGroups().
|
|
1687
1658
|
* Each NodeGroup is here twice, once under an exact key, and once under the close key.
|
|
1688
1659
|
* @type {MultiValueMap<key:string, value:NodeGroup>} */
|
|
1689
|
-
|
|
1660
|
+
nodeGroupsAttachedAvailable = new MultiValueMap();
|
|
1690
1661
|
|
|
1691
|
-
|
|
1662
|
+
/**
|
|
1663
|
+
* Nodes that were not added to the web component during the last render(), and available to be used again.
|
|
1664
|
+
* @type {MultiValueMap} */
|
|
1665
|
+
nodeGroupsDetachedAvailable = new MultiValueMap();
|
|
1692
1666
|
|
|
1693
1667
|
|
|
1694
1668
|
/**
|
|
1695
|
-
* Move everything from this.
|
|
1669
|
+
* Move everything from this.nodeGroupsRendered to this.nodeGroupsAttached and nodeGroupsDetached.
|
|
1670
|
+
* Called at the beginning of applyNodes() so it can have NodeGroups to use.
|
|
1696
1671
|
* TODO: this could run as needed in getNodeGroup? */
|
|
1697
1672
|
freeNodeGroups() {
|
|
1698
|
-
//
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1673
|
+
// Add nodes that weren't used during render() to nodeGroupsDetached
|
|
1674
|
+
let previouslyAttached = this.nodeGroupsAttachedAvailable.data;
|
|
1675
|
+
let detached = this.nodeGroupsDetachedAvailable.data;
|
|
1676
|
+
for (let key in previouslyAttached) {
|
|
1677
|
+
let set = detached[key];
|
|
1678
|
+
if (!set)
|
|
1679
|
+
detached[key] = previouslyAttached[key];
|
|
1680
|
+
else
|
|
1681
|
+
for (let ng of previouslyAttached[key])
|
|
1682
|
+
set.add(ng);
|
|
1683
|
+
}
|
|
1702
1684
|
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1685
|
+
// Add nodes that were used during render() to nodeGroupsRendered.
|
|
1686
|
+
this.nodeGroupsAttachedAvailable = new MultiValueMap();
|
|
1687
|
+
let nga = this.nodeGroupsAttachedAvailable;
|
|
1688
|
+
for (let ng of this.nodeGroupsRendered) {
|
|
1689
|
+
nga.add(ng.exactKey, ng);
|
|
1690
|
+
nga.add(ng.closeKey, ng);
|
|
1707
1691
|
}
|
|
1708
|
-
this.nodeGroupsInUse = [];
|
|
1709
1692
|
|
|
1710
|
-
|
|
1711
|
-
// for (let key in this.nodeGroupsFree.data)
|
|
1712
|
-
// for (let item of this.nodeGroupsFree.data[key])
|
|
1713
|
-
// this.nodeGroupsInUse.add(key, item);
|
|
1714
|
-
//
|
|
1715
|
-
// this.nodeGroupsFree = this.nodeGroupsInUse;
|
|
1716
|
-
// this.nodeGroupsInUse = new MultiValueMap();
|
|
1693
|
+
this.nodeGroupsRendered = [];
|
|
1717
1694
|
}
|
|
1718
1695
|
|
|
1719
1696
|
|
|
1720
1697
|
}
|
|
1721
1698
|
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
/**
|
|
1725
|
-
*
|
|
1726
|
-
* @param root
|
|
1727
|
-
* @param path {string[]}
|
|
1728
|
-
* @param node {HTMLElement}
|
|
1729
|
-
*/
|
|
1730
|
-
function setValue(root, path, node) {
|
|
1731
|
-
let val = node.value;
|
|
1732
|
-
if (node.type === 'number')
|
|
1733
|
-
val = parseFloat(val);
|
|
1734
|
-
|
|
1735
|
-
delve(root, path, val);
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
1699
|
/** @enum {int} */
|
|
1739
|
-
const
|
|
1700
|
+
const ExprPathType = {
|
|
1740
1701
|
/** Child of a node */
|
|
1741
1702
|
Content: 1,
|
|
1742
|
-
|
|
1703
|
+
|
|
1743
1704
|
/** One or more whole attributes */
|
|
1744
|
-
|
|
1745
|
-
|
|
1705
|
+
AttribMultiple: 2,
|
|
1706
|
+
|
|
1746
1707
|
/** Value of an attribute. */
|
|
1747
|
-
|
|
1748
|
-
|
|
1708
|
+
AttribValue: 3,
|
|
1709
|
+
|
|
1749
1710
|
/** Value of an attribute being passed to a component. */
|
|
1750
|
-
|
|
1751
|
-
|
|
1711
|
+
ComponentAttribValue: 4,
|
|
1712
|
+
|
|
1752
1713
|
/** Expressions inside Html comments. */
|
|
1753
1714
|
Comment: 5,
|
|
1754
1715
|
|
|
@@ -1774,116 +1735,177 @@ function getNodePath(node) {
|
|
|
1774
1735
|
* Note that the path is backward, with the outermost element at the end.
|
|
1775
1736
|
* @param root {HTMLElement|Document|DocumentFragment|ParentNode}
|
|
1776
1737
|
* @param path {int[]}
|
|
1777
|
-
* @returns {Node|HTMLElement} */
|
|
1738
|
+
* @returns {Node|HTMLElement|HTMLStyleElement} */
|
|
1778
1739
|
function resolveNodePath(root, path) {
|
|
1779
1740
|
for (let i=path.length-1; i>=0; i--)
|
|
1780
1741
|
root = root.childNodes[path[i]];
|
|
1781
1742
|
return root;
|
|
1782
1743
|
}
|
|
1783
1744
|
|
|
1745
|
+
class HtmlParser {
|
|
1746
|
+
constructor() {
|
|
1747
|
+
this.defaultState = {
|
|
1748
|
+
context: HtmlParser.Text, // possible values: 'TEXT', 'TAG', 'ATTRIBUTE'
|
|
1749
|
+
quote: null, // possible values: null, '"', "'"
|
|
1750
|
+
buffer: '',
|
|
1751
|
+
lastChar: null
|
|
1752
|
+
};
|
|
1753
|
+
this.state = {...this.defaultState};
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
reset() {
|
|
1757
|
+
this.state = {...this.defaultState};
|
|
1758
|
+
return this.state.context;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
/**
|
|
1762
|
+
* Parse the next chunk of html, starting with the same context we left off with from the previous chunk.
|
|
1763
|
+
* @param html {string}
|
|
1764
|
+
* @param onContextChange {?function(html:string, index:int, oldContext:string, newContext:string)}
|
|
1765
|
+
* Called every time the context changes, and again at the last context.
|
|
1766
|
+
* @return {('Attribute','Text','Tag')} The context at the end of html. */
|
|
1767
|
+
parse(html, onContextChange=null) {
|
|
1768
|
+
if (html === null)
|
|
1769
|
+
return this.reset();
|
|
1770
|
+
|
|
1771
|
+
for (let i = 0; i < html.length; i++) {
|
|
1772
|
+
const char = html[i];
|
|
1773
|
+
switch (this.state.context) {
|
|
1774
|
+
case HtmlParser.Text:
|
|
1775
|
+
if (char === '<' && html[i + 1].match(/[/a-z!]/i)) { // Start of a tag or comment.
|
|
1776
|
+
onContextChange?.(html, i, this.state.context, HtmlParser.Tag);
|
|
1777
|
+
this.state.context = HtmlParser.Tag;
|
|
1778
|
+
this.state.buffer = '';
|
|
1779
|
+
}
|
|
1780
|
+
break;
|
|
1781
|
+
case HtmlParser.Tag:
|
|
1782
|
+
if (char === '>') {
|
|
1783
|
+
onContextChange?.(html, i+1, this.state.context, HtmlParser.Text);
|
|
1784
|
+
this.state.context = HtmlParser.Text;
|
|
1785
|
+
this.state.quote = null;
|
|
1786
|
+
this.state.buffer = '';
|
|
1787
|
+
}
|
|
1788
|
+
else if (char === ' ' && !this.state.buffer) {
|
|
1789
|
+
// No attribute name is present. Skipping the space.
|
|
1790
|
+
continue;
|
|
1791
|
+
}
|
|
1792
|
+
else if (char === ' ' || char === '/' || char === '?') {
|
|
1793
|
+
this.state.buffer = ''; // Reset the buffer when a delimiter or potential self-closing sign is found.
|
|
1794
|
+
}
|
|
1795
|
+
else if (char === '"' || char === "'" || char === '=') {
|
|
1796
|
+
onContextChange?.(html, i, this.state.context, HtmlParser.Attribute);
|
|
1797
|
+
this.state.context = HtmlParser.Attribute;
|
|
1798
|
+
this.state.quote = char === '=' ? null : char;
|
|
1799
|
+
this.state.buffer = '';
|
|
1800
|
+
}
|
|
1801
|
+
else
|
|
1802
|
+
this.state.buffer += char;
|
|
1803
|
+
break;
|
|
1804
|
+
case HtmlParser.Attribute:
|
|
1805
|
+
// Start an attribute quote.
|
|
1806
|
+
if (!this.state.quote && !this.state.buffer.length && (char === '"' || char === "'")) {
|
|
1807
|
+
this.state.quote = char;
|
|
1808
|
+
}
|
|
1809
|
+
else if (char === this.state.quote || (!this.state.quote && this.state.buffer.length)) {
|
|
1810
|
+
onContextChange?.(html, i, this.state.context, HtmlParser.Tag);
|
|
1811
|
+
this.state.context = HtmlParser.Tag;
|
|
1812
|
+
this.state.quote = null;
|
|
1813
|
+
this.state.buffer = '';
|
|
1814
|
+
}
|
|
1815
|
+
else if (!this.state.quote && char === '>') {
|
|
1816
|
+
onContextChange?.(html, i+1, this.state.context, HtmlParser.Text);
|
|
1817
|
+
this.state.context = HtmlParser.Text;
|
|
1818
|
+
this.state.quote = null;
|
|
1819
|
+
this.state.buffer = '';
|
|
1820
|
+
}
|
|
1821
|
+
else if (char !== ' ')
|
|
1822
|
+
this.state.buffer += char;
|
|
1823
|
+
|
|
1824
|
+
break;
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
onContextChange?.(html, html.length, this.state.context, null);
|
|
1828
|
+
return this.state.context;
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
HtmlParser.Attribute = 'Attribute';
|
|
1833
|
+
HtmlParser.Text = 'Text';
|
|
1834
|
+
HtmlParser.Tag = 'Tag';
|
|
1835
|
+
|
|
1784
1836
|
/**
|
|
1785
1837
|
* A Shell is created from a tagged template expression instantiated as Nodes,
|
|
1786
1838
|
* but without any expressions filled in.
|
|
1787
1839
|
* Only one Shell is created for all the items in a loop.
|
|
1788
1840
|
*
|
|
1789
1841
|
* When a NodeGroup is created from a Template's html strings,
|
|
1790
|
-
* the NodeGroup then clones the Shell's
|
|
1842
|
+
* the NodeGroup then clones the Shell's fragment to be its nodes. */
|
|
1791
1843
|
class Shell {
|
|
1792
1844
|
|
|
1793
1845
|
/**
|
|
1794
|
-
* @type {DocumentFragment} DOM parent of the shell nodes. */
|
|
1846
|
+
* @type {DocumentFragment|Text} DOM parent of the shell's nodes. */
|
|
1795
1847
|
fragment;
|
|
1796
1848
|
|
|
1797
1849
|
/** @type {ExprPath[]} Paths to where expressions should go. */
|
|
1798
1850
|
paths = [];
|
|
1799
1851
|
|
|
1800
|
-
//
|
|
1801
|
-
events = [];
|
|
1852
|
+
// Elements with events. Not yet used.
|
|
1853
|
+
// events = [];
|
|
1802
1854
|
|
|
1803
1855
|
/** @type {int[][]} Array of paths */
|
|
1804
1856
|
ids = [];
|
|
1857
|
+
|
|
1858
|
+
/** @type {int[][]} Array of paths */
|
|
1805
1859
|
scripts = [];
|
|
1860
|
+
|
|
1861
|
+
/** @type {int[][]} Array of paths */
|
|
1806
1862
|
styles = [];
|
|
1807
1863
|
|
|
1864
|
+
/** @type {int[][]} Array of paths. Used by activateEmbeds() to quickly find components. */
|
|
1808
1865
|
staticComponents = [];
|
|
1809
1866
|
|
|
1867
|
+
/** @type {{path:int[], attribs:Record<string, string>}[]} */
|
|
1868
|
+
//componentAttribs = [];
|
|
1869
|
+
|
|
1810
1870
|
|
|
1811
1871
|
|
|
1812
1872
|
/**
|
|
1813
1873
|
* Create the nodes but without filling in the expressions.
|
|
1814
1874
|
* This is useful because the expression-less nodes created by a template can be cached.
|
|
1815
|
-
* @param html {string[]} */
|
|
1875
|
+
* @param html {string[]} Html strings, split on places where an expression exists. */
|
|
1816
1876
|
constructor(html=null) {
|
|
1817
1877
|
if (!html)
|
|
1818
1878
|
return;
|
|
1819
1879
|
|
|
1820
1880
|
|
|
1821
1881
|
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
let buffer = [];
|
|
1827
|
-
let commentPlaceholder = `<!--!✨!-->`;
|
|
1828
|
-
let componentNames = {};
|
|
1829
|
-
|
|
1830
|
-
htmlContext(null); // Reset the context.
|
|
1831
|
-
for (let i=0; i<html.length; i++) {
|
|
1832
|
-
let lastHtml = html[i];
|
|
1833
|
-
let context = htmlContext(lastHtml);
|
|
1834
|
-
|
|
1835
|
-
// Swap out Embedded Solarite Components with ${} attributes.
|
|
1836
|
-
// Later, NodeGroup.render() will search for these and replace them with the real components.
|
|
1837
|
-
// Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
|
|
1838
|
-
if (context === htmlContext.Attribute) {
|
|
1839
|
-
|
|
1840
|
-
let lastIndex, lastMatch;
|
|
1841
|
-
lastHtml.replace(/<[a-z][a-z0-9]*-[a-z0-9-]+/ig, (match, index) => {
|
|
1842
|
-
lastIndex = index+1; // +1 for after opening <
|
|
1843
|
-
lastMatch = match.slice(1);
|
|
1844
|
-
});
|
|
1845
|
-
|
|
1846
|
-
if (lastMatch) {
|
|
1847
|
-
let newTagName = lastMatch + '-solarite-placeholder';
|
|
1848
|
-
lastHtml = lastHtml.slice(0, lastIndex) + newTagName + lastHtml.slice(lastIndex + lastMatch.length);
|
|
1849
|
-
componentNames[lastMatch] = newTagName;
|
|
1850
|
-
}
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
buffer.push(lastHtml);
|
|
1854
|
-
//console.log(lastHtml, context)
|
|
1855
|
-
if (i < html.length-1)
|
|
1856
|
-
if (context === htmlContext.Text)
|
|
1857
|
-
buffer.push(commentPlaceholder); // Comment Placeholder. because we can't put text in between <tr> tags for example.
|
|
1858
|
-
else
|
|
1859
|
-
buffer.push(String.fromCharCode(placeholder+i));
|
|
1882
|
+
if (html.length === 1 && !html[0].match(/[<&]/)) {
|
|
1883
|
+
this.fragment = document.createTextNode(html[0]);
|
|
1884
|
+
return;
|
|
1860
1885
|
}
|
|
1861
1886
|
|
|
1862
|
-
// 2. Create elements from html with placeholders.
|
|
1863
|
-
let template = document.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
|
|
1864
|
-
let joinedHtml = buffer.join('');
|
|
1865
1887
|
|
|
1866
|
-
//
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
template.content.append(document.createTextNode(''));
|
|
1888
|
+
// 1. Add placeholders
|
|
1889
|
+
let joinedHtml = Shell.addPlaceholders(html);
|
|
1890
|
+
|
|
1891
|
+
let template = document.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
|
|
1892
|
+
if (joinedHtml)
|
|
1893
|
+
template.innerHTML = joinedHtml;
|
|
1894
|
+
else // Create one text node, so shell isn't empty and NodeGroups created from it have something to point the startNode and endNode at.
|
|
1895
|
+
template.content.append(document.createTextNode(''));
|
|
1875
1896
|
this.fragment = template.content;
|
|
1876
1897
|
|
|
1877
|
-
//
|
|
1898
|
+
// 2. Find placeholders
|
|
1878
1899
|
let node;
|
|
1879
1900
|
let toRemove = [];
|
|
1880
|
-
|
|
1901
|
+
let placeholdersUsed = 0;
|
|
1902
|
+
const walker = document.createTreeWalker(this.fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
|
|
1881
1903
|
while (node = walker.nextNode()) {
|
|
1882
1904
|
|
|
1883
1905
|
// Remove previous after each iteration, so paths will still be calculated correctly.
|
|
1884
1906
|
toRemove.map(el => el.remove());
|
|
1885
1907
|
toRemove = [];
|
|
1886
|
-
|
|
1908
|
+
|
|
1887
1909
|
// Replace attributes
|
|
1888
1910
|
if (node.nodeType === 1) {
|
|
1889
1911
|
for (let attr of [...node.attributes]) { // Copy the attributes array b/c we remove attributes as we go.
|
|
@@ -1891,7 +1913,8 @@ class Shell {
|
|
|
1891
1913
|
// Whole attribute
|
|
1892
1914
|
let matches = attr.name.match(/^[\ue000-\uf8ff]$/);
|
|
1893
1915
|
if (matches) {
|
|
1894
|
-
this.paths.push(new ExprPath(null, node,
|
|
1916
|
+
this.paths.push(new ExprPath(null, node, ExprPathType.AttribMultiple));
|
|
1917
|
+
placeholdersUsed ++;
|
|
1895
1918
|
node.removeAttribute(matches[0]);
|
|
1896
1919
|
}
|
|
1897
1920
|
|
|
@@ -1900,16 +1923,17 @@ class Shell {
|
|
|
1900
1923
|
let parts = attr.value.split(/[\ue000-\uf8ff]/g);
|
|
1901
1924
|
if (parts.length > 1) {
|
|
1902
1925
|
let nonEmptyParts = (parts.length === 2 && !parts[0].length && !parts[1].length) ? null : parts;
|
|
1903
|
-
let type = isEvent(attr.name) ?
|
|
1926
|
+
let type = Util.isEvent(attr.name) ? ExprPathType.Event : ExprPathType.AttribValue;
|
|
1904
1927
|
|
|
1905
1928
|
this.paths.push(new ExprPath(null, node, type, attr.name, nonEmptyParts));
|
|
1929
|
+
placeholdersUsed += parts.length - 1;
|
|
1906
1930
|
node.setAttribute(attr.name, parts.join(''));
|
|
1907
1931
|
}
|
|
1908
1932
|
}
|
|
1909
1933
|
}
|
|
1910
1934
|
}
|
|
1911
1935
|
// Replace comment placeholders
|
|
1912
|
-
else if (node.nodeType ===
|
|
1936
|
+
else if (node.nodeType === 8 && node.nodeValue === '!✨!') {
|
|
1913
1937
|
|
|
1914
1938
|
// Get or create nodeBefore.
|
|
1915
1939
|
let nodeBefore = node.previousSibling; // Can be the same as another Path's nodeMarker.
|
|
@@ -1934,12 +1958,14 @@ class Shell {
|
|
|
1934
1958
|
}
|
|
1935
1959
|
|
|
1936
1960
|
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
let path = new ExprPath(nodeBefore, nodeMarker, PathType.Content);
|
|
1940
|
-
|
|
1961
|
+
let path = new ExprPath(nodeBefore, nodeMarker, ExprPathType.Content);
|
|
1941
1962
|
this.paths.push(path);
|
|
1963
|
+
placeholdersUsed ++;
|
|
1942
1964
|
}
|
|
1965
|
+
|
|
1966
|
+
else if (node.nodeType === 3 && node.parentNode?.tagName === 'TEXTAREA' && node.textContent.includes('<!--!✨!-->'))
|
|
1967
|
+
throw new Error(`Textarea can't have expressions inside them. Use <textarea value="\${...}"> instead.`);
|
|
1968
|
+
|
|
1943
1969
|
|
|
1944
1970
|
|
|
1945
1971
|
// Sometimes users will comment out a block of html code that has expressions.
|
|
@@ -1950,8 +1976,9 @@ class Shell {
|
|
|
1950
1976
|
let parts = node.textContent.split(/[\ue000-\uf8ff]/g);
|
|
1951
1977
|
for (let i=0; i<parts.length-1; i++) {
|
|
1952
1978
|
let path = new ExprPath(node.previousSibling, node);
|
|
1953
|
-
path.type =
|
|
1979
|
+
path.type = ExprPathType.Comment;
|
|
1954
1980
|
this.paths.push(path);
|
|
1981
|
+
placeholdersUsed ++;
|
|
1955
1982
|
}
|
|
1956
1983
|
}
|
|
1957
1984
|
|
|
@@ -1969,8 +1996,9 @@ class Shell {
|
|
|
1969
1996
|
}
|
|
1970
1997
|
|
|
1971
1998
|
for (let i=0, node; node=placeholders[i]; i++) {
|
|
1972
|
-
let path = new ExprPath(node.previousSibling, node,
|
|
1999
|
+
let path = new ExprPath(node.previousSibling, node, ExprPathType.Content);
|
|
1973
2000
|
this.paths.push(path);
|
|
2001
|
+
placeholdersUsed ++;
|
|
1974
2002
|
|
|
1975
2003
|
|
|
1976
2004
|
}
|
|
@@ -1982,17 +2010,17 @@ class Shell {
|
|
|
1982
2010
|
}
|
|
1983
2011
|
toRemove.map(el => el.remove());
|
|
1984
2012
|
|
|
2013
|
+
// Less than or equal because there can be one path to multiple expressions
|
|
2014
|
+
// if those expressions are in the same attribute value.
|
|
2015
|
+
if (placeholdersUsed !== html.length-1)
|
|
2016
|
+
throw new Error(`Could not parse expressions in template. Check for duplicate attributes or malformed html: ${html.join('${...}')}`);
|
|
2017
|
+
|
|
1985
2018
|
// Handle solarite-placeholder's.
|
|
1986
|
-
// Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
|
|
1987
|
-
//if (componentNames.size)
|
|
1988
|
-
// this.components = [...this.fragment.querySelectorAll([...componentNames].join(','))]
|
|
1989
2019
|
|
|
1990
|
-
// Rename "is" attributes so the Web Components don't instantiate until we have the values of their PathExpr arguments.
|
|
2020
|
+
// 3. Rename "is" attributes so the Web Components don't instantiate until we have the values of their PathExpr arguments.
|
|
1991
2021
|
// that happens in NodeGroup.applyComponentExprs()
|
|
1992
|
-
for (let el of this.fragment.querySelectorAll('[is]'))
|
|
2022
|
+
for (let el of this.fragment.querySelectorAll('[is]'))
|
|
1993
2023
|
el.setAttribute('_is', el.getAttribute('is'));
|
|
1994
|
-
// this.components.push(el);
|
|
1995
|
-
}
|
|
1996
2024
|
|
|
1997
2025
|
for (let path of this.paths) {
|
|
1998
2026
|
if (path.nodeBefore)
|
|
@@ -2000,16 +2028,64 @@ class Shell {
|
|
|
2000
2028
|
path.nodeMarkerPath = getNodePath(path.nodeMarker);
|
|
2001
2029
|
|
|
2002
2030
|
// Cache so we don't have to calculate this later inside NodeGroup.applyExprs()
|
|
2003
|
-
if (path.type ===
|
|
2031
|
+
if (path.type === ExprPathType.AttribValue && path.nodeMarker.nodeType === 1 &&
|
|
2004
2032
|
(path.nodeMarker.tagName.includes('-') || path.nodeMarker.hasAttribute('is'))) {
|
|
2005
|
-
path.type =
|
|
2033
|
+
path.type = ExprPathType.ComponentAttribValue;
|
|
2006
2034
|
}
|
|
2007
2035
|
}
|
|
2008
2036
|
|
|
2009
2037
|
this.findEmbeds();
|
|
2010
2038
|
|
|
2011
2039
|
|
|
2012
|
-
}
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
/**
|
|
2043
|
+
* 1. Add a Unicode placeholder char for where expressions go within attributes.
|
|
2044
|
+
* 2. Add a comment placeholder for where expressions are children of other nodes.
|
|
2045
|
+
* 3. Append -solarite-placeholder to the tag names of custom components so that we can wait to instantiate them later.
|
|
2046
|
+
* @param htmlChunks {string[]}
|
|
2047
|
+
* @returns {string} */
|
|
2048
|
+
static addPlaceholders(htmlChunks) {
|
|
2049
|
+
let tokens = [];
|
|
2050
|
+
|
|
2051
|
+
function addToken(token, context) {
|
|
2052
|
+
|
|
2053
|
+
if (context === HtmlParser.Tag) {
|
|
2054
|
+
// Find Solarite Components tags and append -solarite-placeholder to their tag names
|
|
2055
|
+
// and give them a solarite-placeholder attribute so we can easily find them later.
|
|
2056
|
+
// This way we can gather their constructor arguments and their children before we call their constructor.
|
|
2057
|
+
// Later, NodeGroup.instantiateComponent() will replace them with the real components.
|
|
2058
|
+
// Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
|
|
2059
|
+
token = token.replace(/^<\/?[a-z][a-z0-9]*-[a-z0-9-]+/i, match => match + '-solarite-placeholder solarite-placeholder');
|
|
2060
|
+
}
|
|
2061
|
+
tokens.push(token);
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
let htmlParser = new HtmlParser(); // Reset the context.
|
|
2065
|
+
for (let i = 0; i < htmlChunks.length; i++) {
|
|
2066
|
+
let lastHtml = htmlChunks[i];
|
|
2067
|
+
|
|
2068
|
+
// Append -solarite-placholder to web component tags, so we can pass args to them when they're instantiated.
|
|
2069
|
+
let lastIndex = 0;
|
|
2070
|
+
let context = htmlParser.parse(lastHtml, (html, index, oldContext, newContext) => {
|
|
2071
|
+
if (lastIndex !== index) {
|
|
2072
|
+
let token = html.slice(lastIndex, index);
|
|
2073
|
+
addToken(token, oldContext);
|
|
2074
|
+
}
|
|
2075
|
+
lastIndex = index;
|
|
2076
|
+
});
|
|
2077
|
+
|
|
2078
|
+
// Insert placeholders
|
|
2079
|
+
if (i < htmlChunks.length - 1) {
|
|
2080
|
+
if (context === HtmlParser.Text)
|
|
2081
|
+
tokens.push(commentPlaceholder); // Comment Placeholder. because we can't put text in between <tr> tags for example.
|
|
2082
|
+
else
|
|
2083
|
+
tokens.push(String.fromCharCode(attribPlaceholder + i));
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
return tokens.join('');
|
|
2088
|
+
}
|
|
2013
2089
|
|
|
2014
2090
|
/**
|
|
2015
2091
|
* We find the path to every embed here once in the Shell, instead of every time a NodeGroup is instantiated.
|
|
@@ -2021,36 +2097,30 @@ class Shell {
|
|
|
2021
2097
|
* this.staticComponents */
|
|
2022
2098
|
findEmbeds() {
|
|
2023
2099
|
this.scripts = Array.prototype.map.call(this.fragment.querySelectorAll('scripts'), el => getNodePath(el));
|
|
2100
|
+
|
|
2101
|
+
// TODO: only find styles that have ExprPaths in them?
|
|
2024
2102
|
this.styles = Array.prototype.map.call(this.fragment.querySelectorAll('style'), el => getNodePath(el));
|
|
2025
2103
|
|
|
2026
2104
|
let idEls = this.fragment.querySelectorAll('[id],[data-id]');
|
|
2027
|
-
|
|
2028
2105
|
|
|
2029
2106
|
// Check for valid id names.
|
|
2030
2107
|
for (let el of idEls) {
|
|
2031
2108
|
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
2032
|
-
if (div.hasOwnProperty(id))
|
|
2109
|
+
if (Globals$1.div.hasOwnProperty(id))
|
|
2033
2110
|
throw new Error(`<${el.tagName.toLowerCase()} id="${id}"> can't override existing HTMLElement id property.`)
|
|
2034
2111
|
}
|
|
2035
2112
|
|
|
2036
|
-
|
|
2037
2113
|
this.ids = Array.prototype.map.call(idEls, el => getNodePath(el));
|
|
2038
2114
|
|
|
2039
|
-
// Events (not yet used)
|
|
2040
2115
|
for (let el of this.fragment.querySelectorAll('*')) {
|
|
2041
|
-
for (let attrib of el.attributes)
|
|
2042
|
-
if (isEvent(attrib.name))
|
|
2043
|
-
this.events.push([attrib.name, getNodePath(el)]);
|
|
2044
|
-
|
|
2045
2116
|
if (el.tagName.includes('-') || el.hasAttribute('_is'))
|
|
2046
2117
|
|
|
2047
|
-
// Dynamic components have attributes with expression values.
|
|
2118
|
+
// Dynamic components are components that have attributes with expression values.
|
|
2048
2119
|
// They are created from applyExprs()
|
|
2049
2120
|
// But static components are created in a separate path inside the NodeGroup constructor.
|
|
2050
2121
|
if (!this.paths.find(path => path.nodeMarker === el))
|
|
2051
2122
|
this.staticComponents.push(getNodePath(el));
|
|
2052
2123
|
}
|
|
2053
|
-
|
|
2054
2124
|
}
|
|
2055
2125
|
|
|
2056
2126
|
/**
|
|
@@ -2058,10 +2128,10 @@ class Shell {
|
|
|
2058
2128
|
* @param htmlStrings {string[]} Typically comes from a Template.
|
|
2059
2129
|
* @returns {Shell} */
|
|
2060
2130
|
static get(htmlStrings) {
|
|
2061
|
-
let result = Globals.shells.get(htmlStrings);
|
|
2131
|
+
let result = Globals$1.shells.get(htmlStrings);
|
|
2062
2132
|
if (!result) {
|
|
2063
2133
|
result = new Shell(htmlStrings);
|
|
2064
|
-
Globals.shells.set(htmlStrings, result); // cache
|
|
2134
|
+
Globals$1.shells.set(htmlStrings, result); // cache
|
|
2065
2135
|
}
|
|
2066
2136
|
|
|
2067
2137
|
|
|
@@ -2069,7 +2139,14 @@ class Shell {
|
|
|
2069
2139
|
}
|
|
2070
2140
|
|
|
2071
2141
|
|
|
2072
|
-
}
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
|
|
2145
|
+
const commentPlaceholder = `<!--!✨!-->`;
|
|
2146
|
+
|
|
2147
|
+
|
|
2148
|
+
// We increment the placeholder char as we go because nodes can't have the same attribute more than once.
|
|
2149
|
+
const attribPlaceholder = 0xe000; // https://en.wikipedia.org/wiki/Private_Use_Areas 6400.
|
|
2073
2150
|
|
|
2074
2151
|
/** @typedef {boolean|string|number|function|Object|Array|Date|Node|Template} Expr */
|
|
2075
2152
|
|
|
@@ -2095,7 +2172,8 @@ class NodeGroup {
|
|
|
2095
2172
|
startNode;
|
|
2096
2173
|
|
|
2097
2174
|
/** @type {Node|HTMLElement} A node that never changes that this NodeGroup should always insert its nodes before.
|
|
2098
|
-
* An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position
|
|
2175
|
+
* An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.
|
|
2176
|
+
* TODO: But sometimes startNode and endNode point to the same node. Document htis inconsistency. */
|
|
2099
2177
|
endNode;
|
|
2100
2178
|
|
|
2101
2179
|
/** @type {ExprPath[]} */
|
|
@@ -2113,11 +2191,11 @@ class NodeGroup {
|
|
|
2113
2191
|
nodesCache;
|
|
2114
2192
|
|
|
2115
2193
|
/**
|
|
2194
|
+
* A map between <style> Elements and their text content.
|
|
2195
|
+
* This lets NodeGroup.updateStyles() see when the style text has changed.
|
|
2116
2196
|
* @type {?Map<HTMLStyleElement, string>} */
|
|
2117
2197
|
styles;
|
|
2118
2198
|
|
|
2119
|
-
currentComponentProps = {};
|
|
2120
|
-
|
|
2121
2199
|
|
|
2122
2200
|
/**
|
|
2123
2201
|
* Create an "instantiated" NodeGroup from a Template and add it to an element.
|
|
@@ -2125,14 +2203,26 @@ class NodeGroup {
|
|
|
2125
2203
|
* @param parentPath {?ExprPath} */
|
|
2126
2204
|
constructor(template, parentPath=null) {
|
|
2127
2205
|
if (!(this instanceof RootNodeGroup)) {
|
|
2206
|
+
|
|
2128
2207
|
let [fragment, shell] = this.init(template, parentPath);
|
|
2129
2208
|
|
|
2130
|
-
|
|
2209
|
+
if (fragment && template.exprs.length) {
|
|
2210
|
+
this.updatePaths(fragment, shell.paths);
|
|
2131
2211
|
|
|
2132
|
-
|
|
2212
|
+
// Static web components can sometimes have children created via expressions.
|
|
2213
|
+
// But calling applyExprs() will mess up the shell's path to them.
|
|
2214
|
+
// So we find them first, then call activateStaticComponents() after their children have been created.
|
|
2215
|
+
let staticComponents = this.findStaticComponents(fragment, shell);
|
|
2133
2216
|
|
|
2134
|
-
|
|
2135
|
-
|
|
2217
|
+
this.activateEmbeds(fragment, shell);
|
|
2218
|
+
|
|
2219
|
+
// Apply exprs
|
|
2220
|
+
this.applyExprs(template.exprs);
|
|
2221
|
+
|
|
2222
|
+
this.instantiateStaticComponents(staticComponents);
|
|
2223
|
+
}
|
|
2224
|
+
else if (shell)
|
|
2225
|
+
this.activateEmbeds(fragment, shell);
|
|
2136
2226
|
}
|
|
2137
2227
|
}
|
|
2138
2228
|
|
|
@@ -2160,59 +2250,107 @@ class NodeGroup {
|
|
|
2160
2250
|
template.nodeGroup = this;
|
|
2161
2251
|
|
|
2162
2252
|
// Get a cached version of the parsed and instantiated html, and ExprPaths.
|
|
2163
|
-
let shell = Shell.get(template.html);
|
|
2164
|
-
let fragment = shell.fragment.cloneNode(true);
|
|
2165
2253
|
|
|
2166
|
-
|
|
2167
|
-
this.
|
|
2168
|
-
|
|
2254
|
+
// If it's just a text node, skip a bunch of unnecessary steps.
|
|
2255
|
+
if (!(this instanceof RootNodeGroup) && !template.exprs.length && !template.html[0].includes('<')) {
|
|
2256
|
+
//let doc = this.rootNg.startNode?.ownerDocument || document;
|
|
2257
|
+
let textNode = document.createTextNode(template.html[0]);
|
|
2258
|
+
|
|
2259
|
+
this.startNode = this.endNode = textNode;
|
|
2260
|
+
return [];
|
|
2261
|
+
}
|
|
2262
|
+
else {
|
|
2263
|
+
let shell = Shell.get(template.html);
|
|
2264
|
+
let fragment = shell.fragment.cloneNode(true);
|
|
2169
2265
|
|
|
2170
|
-
|
|
2266
|
+
if (fragment instanceof DocumentFragment) {
|
|
2267
|
+
let childNodes = fragment.childNodes;
|
|
2268
|
+
this.startNode = childNodes[0];
|
|
2269
|
+
this.endNode = childNodes[childNodes.length - 1];
|
|
2270
|
+
}
|
|
2271
|
+
else {
|
|
2272
|
+
this.startNode = this.endNode = fragment;
|
|
2273
|
+
}
|
|
2274
|
+
return [fragment, shell];
|
|
2275
|
+
}
|
|
2171
2276
|
}
|
|
2172
2277
|
|
|
2173
2278
|
/**
|
|
2174
2279
|
* Use the paths to insert the given expressions.
|
|
2175
2280
|
* Dispatches expression handling to other functions depending on the path type.
|
|
2176
2281
|
* @param exprs {(*|*[]|function|Template)[]}
|
|
2177
|
-
* @param paths {?ExprPath[]} Optional. */
|
|
2282
|
+
* @param paths {?ExprPath[]} Optional. Only used for testing. Normally uses this.paths. */
|
|
2178
2283
|
applyExprs(exprs, paths=null) {
|
|
2179
2284
|
paths = paths || this.paths;
|
|
2180
2285
|
|
|
2181
2286
|
|
|
2182
2287
|
|
|
2183
|
-
//
|
|
2184
|
-
|
|
2288
|
+
// Things to consider:
|
|
2289
|
+
// 1. One path may use multipe expressions. E.g. <div class="${1} ${2}">
|
|
2290
|
+
// 2. One component may need to use multiple attribute paths to be instantiated.
|
|
2291
|
+
// 3. We apply them in reverse order so that a <select> box has its children created from an expression
|
|
2292
|
+
// before its instantiated and its value attribute is set via an expression.
|
|
2293
|
+
|
|
2294
|
+
let exprIndex = exprs.length - 1; // Update exprs at paths.
|
|
2295
|
+
let lastComponentPathIndex;
|
|
2296
|
+
let pathExprs = new Array(paths.length); // Store all the expressions that map to a single path. Only paths to attribute values can have more than one.
|
|
2297
|
+
for (let i = paths.length - 1, path; path = paths[i]; i--) {
|
|
2298
|
+
let prevPath = paths[i - 1];
|
|
2299
|
+
let nextPath = paths[i + 1];
|
|
2300
|
+
|
|
2301
|
+
// Get the expressions associated with this path.
|
|
2302
|
+
if (path.attrValue?.length > 2) {
|
|
2303
|
+
let startIndex = (exprIndex - (path.attrValue.length - 1)) + 1;
|
|
2304
|
+
pathExprs[i] = exprs.slice(startIndex, exprIndex + 1); // probably doesn't allocate if the JS vm implements copy on write.
|
|
2305
|
+
exprIndex -= pathExprs[i].length;
|
|
2306
|
+
} else {
|
|
2307
|
+
pathExprs[i] = [exprs[exprIndex]];
|
|
2308
|
+
exprIndex--;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
// TODO: Need to end and restart this block when going from one component to the next?
|
|
2312
|
+
// Think of having two adjacent components.
|
|
2313
|
+
// But the dynamicAttribsAdjacet test already passes.
|
|
2185
2314
|
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2315
|
+
// If expr is an attribute in a component:
|
|
2316
|
+
// 1. Instantiate it if it hasn't already been, sending all expr's to its constructor.
|
|
2317
|
+
// 2. Otherwise send them to its render function.
|
|
2318
|
+
// Components with no expressions as attributes are instead activated in activateEmbeds().
|
|
2319
|
+
if (path.nodeMarker !== this.rootNg.root && path.isComponent()) {
|
|
2190
2320
|
|
|
2191
|
-
|
|
2321
|
+
if (!nextPath || !nextPath.isComponent() || nextPath.nodeMarker !== path.nodeMarker)
|
|
2322
|
+
lastComponentPathIndex = i;
|
|
2323
|
+
let isFirstComponentPath = !prevPath || !prevPath.isComponent() || prevPath.nodeMarker !== path.nodeMarker;
|
|
2192
2324
|
|
|
2193
|
-
|
|
2194
|
-
if (lastNode && lastNode !== this.rootNg.root && lastNode !== path.nodeMarker && Object.keys(this.currentComponentProps).length) {
|
|
2195
|
-
this.applyComponentExprs(lastNode, this.currentComponentProps);
|
|
2196
|
-
this.currentComponentProps = {};
|
|
2197
|
-
}
|
|
2325
|
+
if (isFirstComponentPath) {
|
|
2198
2326
|
|
|
2199
|
-
|
|
2327
|
+
let componentProps = {};
|
|
2328
|
+
for (let j=i; j<=lastComponentPathIndex; j++) {
|
|
2329
|
+
let attrName = paths[j].attrName; // Util.dashesToCamel(paths[j].attrName);
|
|
2330
|
+
componentProps[attrName] = pathExprs[j].length > 1 ? pathExprs[j].join('') : pathExprs[j][0];
|
|
2331
|
+
}
|
|
2200
2332
|
|
|
2201
|
-
|
|
2333
|
+
this.applyComponentExprs(path.nodeMarker, componentProps);
|
|
2202
2334
|
|
|
2335
|
+
// Set attributes on component.
|
|
2336
|
+
for (let j=i; j<=lastComponentPathIndex; j++)
|
|
2337
|
+
paths[j].apply(pathExprs[j]);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2203
2340
|
|
|
2204
|
-
|
|
2205
|
-
|
|
2341
|
+
// Else apply it normally
|
|
2342
|
+
else
|
|
2343
|
+
path.apply(pathExprs[i]);
|
|
2206
2344
|
|
|
2207
2345
|
|
|
2208
|
-
//
|
|
2209
|
-
|
|
2210
|
-
this.applyComponentExprs(lastNode, this.currentComponentProps);
|
|
2211
|
-
this.currentComponentProps = {};
|
|
2212
|
-
}
|
|
2346
|
+
} // end for(path of this.paths)
|
|
2347
|
+
|
|
2213
2348
|
|
|
2349
|
+
// TODO: Only do this if we have ExprPaths within styles?
|
|
2214
2350
|
this.updateStyles();
|
|
2215
2351
|
|
|
2352
|
+
|
|
2353
|
+
|
|
2216
2354
|
// Invalidate the nodes cache because we just changed it.
|
|
2217
2355
|
this.nodesCache = null;
|
|
2218
2356
|
|
|
@@ -2235,24 +2373,30 @@ class NodeGroup {
|
|
|
2235
2373
|
// then we could re-use the hash and logic from NodeManager?
|
|
2236
2374
|
let newHash = getObjectHash(props);
|
|
2237
2375
|
|
|
2238
|
-
let isPreHtmlElement = el.
|
|
2376
|
+
let isPreHtmlElement = el.hasAttribute('solarite-placeholder');
|
|
2239
2377
|
let isPreIsElement = el.hasAttribute('_is');
|
|
2240
2378
|
|
|
2241
2379
|
|
|
2242
2380
|
// Instantiate a placeholder.
|
|
2243
2381
|
if (isPreHtmlElement || isPreIsElement)
|
|
2244
|
-
el = this.
|
|
2382
|
+
el = this.instantiateComponent(el, isPreHtmlElement, props);
|
|
2245
2383
|
|
|
2246
2384
|
// Call render() with the same params that would've been passed to the constructor.
|
|
2385
|
+
// We do this even if the arguments haven't changed, so we can let the child component
|
|
2386
|
+
// compare the arguments and then decide for itself whether it wants to re-render.
|
|
2247
2387
|
else if (el.render) {
|
|
2248
|
-
let oldHash = Globals.
|
|
2249
|
-
if (oldHash !== newHash)
|
|
2250
|
-
|
|
2388
|
+
//let oldHash = Globals.componentArgsHash.get(el);
|
|
2389
|
+
//if (oldHash !== newHash) { // Only if not changed.
|
|
2390
|
+
let args = {};
|
|
2391
|
+
for (let name in props || {})
|
|
2392
|
+
args[Util.dashesToCamel(name)] = props[name];
|
|
2393
|
+
el.render(args); // Pass new values of props to render so it can decide how it wants to respond.
|
|
2394
|
+
//}
|
|
2251
2395
|
}
|
|
2252
2396
|
|
|
2253
|
-
Globals.
|
|
2397
|
+
Globals$1.componentArgsHash.set(el, newHash);
|
|
2254
2398
|
}
|
|
2255
|
-
|
|
2399
|
+
|
|
2256
2400
|
/**
|
|
2257
2401
|
* We swap the placeholder element for the real element so we can pass its dynamic attributes
|
|
2258
2402
|
* to its constructor.
|
|
@@ -2260,78 +2404,54 @@ class NodeGroup {
|
|
|
2260
2404
|
* The logic of this function is complex and could use cleaning up.
|
|
2261
2405
|
*
|
|
2262
2406
|
* @param el
|
|
2263
|
-
* @param isPreHtmlElement
|
|
2407
|
+
* @param isPreHtmlElement {?boolean} True if the element's tag name ends with -solarite-placeholder
|
|
2264
2408
|
* @param props {Object} Attributes with dynamic values.
|
|
2265
2409
|
* @return {HTMLElement} */
|
|
2266
|
-
|
|
2410
|
+
instantiateComponent(el, isPreHtmlElement=undefined, props=undefined) {
|
|
2267
2411
|
if (isPreHtmlElement === undefined)
|
|
2268
2412
|
isPreHtmlElement = !el.hasAttribute('_is');
|
|
2269
|
-
|
|
2413
|
+
|
|
2270
2414
|
let tagName = (isPreHtmlElement
|
|
2271
|
-
? el.tagName.
|
|
2272
|
-
? el.tagName.slice(0, -21)
|
|
2273
|
-
: el.tagName
|
|
2415
|
+
? el.tagName.slice(0, -21) // Remove -SOLARITE-PLACEHOLDER
|
|
2274
2416
|
: el.getAttribute('is')).toLowerCase();
|
|
2275
2417
|
|
|
2276
|
-
|
|
2277
|
-
|
|
2418
|
+
|
|
2419
|
+
// Throw if custom element isn't defined.
|
|
2420
|
+
let Constructor = customElements.get(tagName);
|
|
2421
|
+
if (!Constructor)
|
|
2422
|
+
throw new Error(`The custom tag name ${tagName} is not registered.`)
|
|
2423
|
+
|
|
2424
|
+
let args = {};
|
|
2425
|
+
for (let name in props || {})
|
|
2426
|
+
args[Util.dashesToCamel(name)] = props[name];
|
|
2427
|
+
|
|
2278
2428
|
// Pass other attribs to constructor, since otherwise they're not yet set on the element,
|
|
2279
2429
|
// and the constructor would otherwise have no way to see them.
|
|
2280
2430
|
if (el.attributes.length) {
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2431
|
+
for (let attrib of el.attributes) {
|
|
2432
|
+
let attribName = Util.dashesToCamel(attrib.name);
|
|
2433
|
+
if (!args.hasOwnProperty(attribName) && attribName !== 'solarite-placeholder')
|
|
2434
|
+
args[attribName] = attrib.value;
|
|
2435
|
+
}
|
|
2286
2436
|
}
|
|
2287
|
-
|
|
2288
|
-
// Create CustomElement and
|
|
2289
|
-
let Constructor = customElements.get(tagName);
|
|
2290
|
-
if (!Constructor)
|
|
2291
|
-
throw new Error(`The custom tag name ${tagName} is not registered.`)
|
|
2292
2437
|
|
|
2293
|
-
//
|
|
2294
|
-
//
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
// can add them as children before the rest of the constructor code executes.
|
|
2298
|
-
let ch = [... el.childNodes];
|
|
2299
|
-
Globals.pendingChildren.push(ch); // pop() is called in Solarite constructor.
|
|
2300
|
-
let newEl = new Constructor(props, ch);
|
|
2438
|
+
// Create the web component.
|
|
2439
|
+
// Get the children that aren't Solarite's comment placeholders.
|
|
2440
|
+
let ch = [...el.childNodes].filter(node => node.nodeType !== Node.COMMENT_NODE || !node.nodeValue.startsWith('ExprPath'));
|
|
2441
|
+
let newEl = new Constructor(args, ch);
|
|
2301
2442
|
|
|
2302
2443
|
if (!isPreHtmlElement)
|
|
2303
2444
|
newEl.setAttribute('is', el.getAttribute('is').toLowerCase());
|
|
2445
|
+
|
|
2446
|
+
// Replace the placeholder tag with the instantiated web component.
|
|
2304
2447
|
el.replaceWith(newEl);
|
|
2305
2448
|
|
|
2306
|
-
// Set children / slot children
|
|
2307
|
-
// TODO: Match named slots.
|
|
2308
|
-
// TODO: This only appends to slot if render() is called in the constructor.
|
|
2309
|
-
//let slot = newEl.querySelector('slot') || newEl;
|
|
2310
|
-
//slot.append(...el.childNodes);
|
|
2311
|
-
|
|
2312
|
-
// Copy over event attributes.
|
|
2313
|
-
for (let propName in props) {
|
|
2314
|
-
let val = props[propName];
|
|
2315
|
-
if (propName.startsWith('on') && typeof val === 'function')
|
|
2316
|
-
newEl.addEventListener(propName.slice(2), e => val(e, newEl));
|
|
2317
|
-
|
|
2318
|
-
// Bind array based event attributes on value.
|
|
2319
|
-
// This same logic is in ExprPath.applyValueAttrib() for non-components.
|
|
2320
|
-
if ((propName === 'value' || propName === 'data-value') && Util.isPath(val)) {
|
|
2321
|
-
let [obj, path] = [val[0], val.slice(1)];
|
|
2322
|
-
newEl.value = delve(obj, path);
|
|
2323
|
-
newEl.addEventListener('input', e => {
|
|
2324
|
-
delve(obj, path, Util.getInputValue(newEl));
|
|
2325
|
-
}, true); // We use capture so we update the values before other events added by the user.
|
|
2326
|
-
}
|
|
2327
|
-
}
|
|
2328
|
-
|
|
2329
2449
|
// If an id pointed at the placeholder, update it to point to the new element.
|
|
2330
2450
|
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
2331
2451
|
if (id)
|
|
2332
2452
|
delve(this.getRootNode(), id.split(/\./g), newEl);
|
|
2333
|
-
|
|
2334
|
-
|
|
2453
|
+
|
|
2454
|
+
|
|
2335
2455
|
// Update paths to use replaced element.
|
|
2336
2456
|
for (let path of this.paths) {
|
|
2337
2457
|
if (path.nodeMarker === el)
|
|
@@ -2343,31 +2463,31 @@ class NodeGroup {
|
|
|
2343
2463
|
this.startNode = newEl;
|
|
2344
2464
|
if (this.endNode === el)
|
|
2345
2465
|
this.endNode = newEl;
|
|
2346
|
-
|
|
2347
|
-
|
|
2466
|
+
|
|
2467
|
+
|
|
2348
2468
|
// applyComponentExprs() is called because we're rendering.
|
|
2349
2469
|
// So we want to render the sub-component also.
|
|
2350
2470
|
if (newEl.renderFirstTime)
|
|
2351
2471
|
newEl.renderFirstTime();
|
|
2352
|
-
|
|
2472
|
+
|
|
2353
2473
|
// Copy attributes over.
|
|
2354
2474
|
for (let attrib of el.attributes)
|
|
2355
|
-
if (attrib.name !== '_is')
|
|
2475
|
+
if (attrib.name !== '_is' && attrib.name !== 'solarite-placeholder')
|
|
2356
2476
|
newEl.setAttribute(attrib.name, attrib.value);
|
|
2357
2477
|
|
|
2358
2478
|
// Set dynamic attributes if they are primitive types.
|
|
2359
|
-
for (let name in
|
|
2360
|
-
let val =
|
|
2479
|
+
for (let name in props) {
|
|
2480
|
+
let val = props[name];
|
|
2361
2481
|
if (typeof val === 'boolean') {
|
|
2362
2482
|
if (val !== false && val !== undefined && val !== null)
|
|
2363
2483
|
newEl.setAttribute(name, '');
|
|
2364
2484
|
}
|
|
2365
2485
|
|
|
2366
|
-
// If type
|
|
2486
|
+
// If type is a non-boolean primitive, set the attribute value.
|
|
2367
2487
|
else if (['number', 'bigint', 'string'].includes(typeof val))
|
|
2368
2488
|
newEl.setAttribute(name, val);
|
|
2369
2489
|
}
|
|
2370
|
-
|
|
2490
|
+
|
|
2371
2491
|
return newEl;
|
|
2372
2492
|
}
|
|
2373
2493
|
|
|
@@ -2413,8 +2533,7 @@ class NodeGroup {
|
|
|
2413
2533
|
|
|
2414
2534
|
/**
|
|
2415
2535
|
* Requires the nodeCache to be present. */
|
|
2416
|
-
|
|
2417
|
-
|
|
2536
|
+
removeAndSaveOrphans() {
|
|
2418
2537
|
|
|
2419
2538
|
let fragment = document.createDocumentFragment();
|
|
2420
2539
|
for (let node of this.getNodes())
|
|
@@ -2424,8 +2543,9 @@ class NodeGroup {
|
|
|
2424
2543
|
|
|
2425
2544
|
updatePaths(fragment, paths, offset) {
|
|
2426
2545
|
// Update paths to point to the fragment.
|
|
2427
|
-
|
|
2428
|
-
|
|
2546
|
+
let pathLength = paths.length;
|
|
2547
|
+
this.paths.length = pathLength;
|
|
2548
|
+
for (let i=0; i<pathLength; i++) {
|
|
2429
2549
|
let path = paths[i].clone(fragment, offset);
|
|
2430
2550
|
path.parentNg = this;
|
|
2431
2551
|
this.paths[i] = path;
|
|
@@ -2443,61 +2563,70 @@ class NodeGroup {
|
|
|
2443
2563
|
|
|
2444
2564
|
|
|
2445
2565
|
|
|
2566
|
+
findStaticComponents(root, shell, pathOffset=0) {
|
|
2567
|
+
let result = [];
|
|
2446
2568
|
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
activateEmbeds(root, shell, pathOffset=0) {
|
|
2452
|
-
|
|
2453
|
-
// static components. These are WebComponents not created by an expression.
|
|
2454
|
-
// Must happen before ids.
|
|
2569
|
+
// static components. These are WebComponents that do not have any constructor arguments that are expressions.
|
|
2570
|
+
// Those are instead created by applyExpr() which calls applyComponentExprs() which calls instantiateComponent().
|
|
2571
|
+
// Maybe someday these two paths will be merged?
|
|
2572
|
+
// Must happen before ids because instantiateComponent will replace the element.
|
|
2455
2573
|
for (let path of shell.staticComponents) {
|
|
2456
2574
|
if (pathOffset)
|
|
2457
2575
|
path = path.slice(0, -pathOffset);
|
|
2458
2576
|
let el = resolveNodePath(root, path);
|
|
2459
2577
|
|
|
2460
2578
|
// Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
|
|
2579
|
+
// Recreating it is necessary so we can pass the constructor args to it.
|
|
2461
2580
|
if (root !== el/* && !isReplaceEl(root, el)*/) // TODO: is isReplaceEl necessary?
|
|
2462
|
-
|
|
2581
|
+
result.push(el);
|
|
2463
2582
|
}
|
|
2583
|
+
return result;
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
instantiateStaticComponents(staticComponents) {
|
|
2587
|
+
for (let el of staticComponents)
|
|
2588
|
+
this.instantiateComponent(el);
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
/**
|
|
2592
|
+
* @param root {HTMLElement|DocumentFragment}
|
|
2593
|
+
* @param shell {Shell}
|
|
2594
|
+
* @param pathOffset {int} */
|
|
2595
|
+
activateEmbeds(root, shell, pathOffset=0) {
|
|
2464
2596
|
|
|
2465
2597
|
let rootEl = this.rootNg.root;
|
|
2466
2598
|
if (rootEl) {
|
|
2599
|
+
let options = this.rootNg.options;
|
|
2467
2600
|
|
|
2468
2601
|
// ids
|
|
2469
|
-
if (
|
|
2602
|
+
if (options?.ids !== false) {
|
|
2470
2603
|
for (let path of shell.ids) {
|
|
2471
2604
|
if (pathOffset)
|
|
2472
2605
|
path = path.slice(0, -pathOffset);
|
|
2473
2606
|
let el = resolveNodePath(root, path);
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
// Don't allow overwriting existing class properties if they already have a non-Node value.
|
|
2478
|
-
if (rootEl[id] && !(rootEl[id] instanceof Node))
|
|
2479
|
-
throw new Error(`${rootEl.constructor.name}.${id} already has a value. ` +
|
|
2480
|
-
`Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
|
|
2481
|
-
|
|
2482
|
-
delve(rootEl, id.split(/\./g), el);
|
|
2483
|
-
}
|
|
2607
|
+
Util.bindId(rootEl, el);
|
|
2608
|
+
}
|
|
2484
2609
|
}
|
|
2485
2610
|
|
|
2486
2611
|
// styles
|
|
2487
|
-
if (
|
|
2612
|
+
if (options?.styles !== false) {
|
|
2488
2613
|
if (shell.styles.length)
|
|
2489
2614
|
this.styles = new Map();
|
|
2490
2615
|
for (let path of shell.styles) {
|
|
2491
2616
|
if (pathOffset)
|
|
2492
2617
|
path = path.slice(0, -pathOffset);
|
|
2618
|
+
|
|
2619
|
+
/** @type {HTMLStyleElement} */
|
|
2493
2620
|
let style = resolveNodePath(root, path);
|
|
2494
|
-
|
|
2495
|
-
|
|
2621
|
+
if (rootEl.nodeType === 1) {
|
|
2622
|
+
Util.bindStyles(style, rootEl);
|
|
2623
|
+
this.styles.set(style, style.textContent);
|
|
2624
|
+
}
|
|
2496
2625
|
}
|
|
2497
2626
|
|
|
2498
2627
|
}
|
|
2499
2628
|
// scripts
|
|
2500
|
-
if (
|
|
2629
|
+
if (options?.scripts !== false) {
|
|
2501
2630
|
for (let path of shell.scripts) {
|
|
2502
2631
|
if (pathOffset)
|
|
2503
2632
|
path = path.slice(0, -pathOffset);
|
|
@@ -2518,20 +2647,8 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2518
2647
|
root;
|
|
2519
2648
|
|
|
2520
2649
|
/**
|
|
2521
|
-
*
|
|
2522
|
-
*
|
|
2523
|
-
* @type {Object<field:string, Set<ExprPath>>} */
|
|
2524
|
-
watchedExprPaths = {};
|
|
2525
|
-
|
|
2526
|
-
/**
|
|
2527
|
-
* Map from arrays where .map is called and their callback functions.
|
|
2528
|
-
* TODO: One array might be called with two different map functions in different places!
|
|
2529
|
-
* @type {Map<Array, function>} */
|
|
2530
|
-
mapCallbacks = new Map();
|
|
2531
|
-
|
|
2532
|
-
/**
|
|
2533
|
-
*
|
|
2534
|
-
* @type {Map<ExprPath, boolean|Array>} */
|
|
2650
|
+
* When we call renerWatched() we re-render these expressions, then clear this to a new Map()
|
|
2651
|
+
* @type {Map<ExprPath, ValueOp|WholeArrayOp|ArraySpliceOp[]>} */
|
|
2535
2652
|
exprsToRender = new Map();
|
|
2536
2653
|
|
|
2537
2654
|
/**
|
|
@@ -2548,82 +2665,102 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2548
2665
|
this.rootNg = this;
|
|
2549
2666
|
let [fragment, shell] = this.init(template);
|
|
2550
2667
|
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
if (el.childNodes.length) {
|
|
2560
|
-
slotFragment = document.createDocumentFragment();
|
|
2561
|
-
slotFragment.append(...el.childNodes);
|
|
2668
|
+
if (fragment instanceof Text) {
|
|
2669
|
+
|
|
2670
|
+
if (el) {
|
|
2671
|
+
this.startNode = el;
|
|
2672
|
+
this.endNode = el;
|
|
2673
|
+
if (fragment.nodeValue.length)
|
|
2674
|
+
el.append(fragment);
|
|
2675
|
+
this.root = el;
|
|
2562
2676
|
}
|
|
2677
|
+
Globals$1.nodeGroups.set(this.root, this);
|
|
2678
|
+
}
|
|
2679
|
+
else {
|
|
2563
2680
|
|
|
2564
|
-
|
|
2681
|
+
// If adding NodeGroup to an element.
|
|
2682
|
+
let offset = 0;
|
|
2683
|
+
let root = fragment; // TODO: Rename so it's not confused with this.root.
|
|
2684
|
+
if (el) {
|
|
2685
|
+
Globals$1.nodeGroups.set(el, this);
|
|
2686
|
+
|
|
2687
|
+
// Save slot children
|
|
2688
|
+
let slotChildren;
|
|
2689
|
+
if (el.childNodes.length) {
|
|
2690
|
+
slotChildren = document.createDocumentFragment();
|
|
2691
|
+
slotChildren.append(...el.childNodes);
|
|
2692
|
+
}
|
|
2565
2693
|
|
|
2566
|
-
|
|
2567
|
-
if (isReplaceEl(fragment, el)) {
|
|
2568
|
-
el.append(...fragment.children[0].childNodes);
|
|
2694
|
+
this.root = el;
|
|
2569
2695
|
|
|
2570
|
-
//
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
el.setAttribute(attrib.name, attrib.value);
|
|
2696
|
+
// If el should replace the root node of the fragment.
|
|
2697
|
+
if (isReplaceEl(fragment, el)) {
|
|
2698
|
+
el.append(...fragment.children[0].childNodes);
|
|
2574
2699
|
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2700
|
+
// Copy attributes
|
|
2701
|
+
for (let attrib of fragment.children[0].attributes)
|
|
2702
|
+
if (!el.hasAttribute(attrib.name) && attrib.name !== 'solarite-placeholder')
|
|
2703
|
+
el.setAttribute(attrib.name, attrib.value);
|
|
2704
|
+
|
|
2705
|
+
// Go one level deeper into all of shell's paths.
|
|
2706
|
+
offset = 1;
|
|
2707
|
+
} else {
|
|
2708
|
+
let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
|
|
2709
|
+
if (!isEmpty)
|
|
2710
|
+
el.append(...fragment.childNodes);
|
|
2711
|
+
}
|
|
2583
2712
|
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
let
|
|
2590
|
-
|
|
2713
|
+
// Setup children
|
|
2714
|
+
if (slotChildren) {
|
|
2715
|
+
|
|
2716
|
+
// Named slots
|
|
2717
|
+
for (let slot of el.querySelectorAll('slot[name]')) {
|
|
2718
|
+
let name = slot.getAttribute('name');
|
|
2719
|
+
if (name) {
|
|
2720
|
+
let slotChildren2 = slotChildren.querySelectorAll(`[slot='${name}']`);
|
|
2721
|
+
slot.append(...slotChildren2);
|
|
2722
|
+
}
|
|
2591
2723
|
}
|
|
2724
|
+
|
|
2725
|
+
// Unnamed slots
|
|
2726
|
+
let unamedSlot = el.querySelector('slot:not([name])');
|
|
2727
|
+
if (unamedSlot)
|
|
2728
|
+
unamedSlot.append(slotChildren);
|
|
2729
|
+
|
|
2730
|
+
// No slots
|
|
2731
|
+
else
|
|
2732
|
+
el.append(slotChildren);
|
|
2592
2733
|
}
|
|
2593
|
-
let unamedSlot = el.querySelector('slot:not([name])');
|
|
2594
|
-
if (unamedSlot)
|
|
2595
|
-
unamedSlot.append(slotFragment);
|
|
2596
|
-
else
|
|
2597
|
-
el.append(slotFragment);
|
|
2598
|
-
}
|
|
2599
2734
|
|
|
2600
|
-
|
|
2735
|
+
root = el;
|
|
2601
2736
|
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
|
|
2737
|
+
this.startNode = el;
|
|
2738
|
+
this.endNode = el;
|
|
2739
|
+
} else {
|
|
2740
|
+
let singleEl = getSingleEl(fragment);
|
|
2741
|
+
this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
|
|
2608
2742
|
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2743
|
+
Globals$1.nodeGroups.set(this.root, this);
|
|
2744
|
+
if (singleEl) {
|
|
2745
|
+
root = singleEl;
|
|
2746
|
+
offset = 1;
|
|
2747
|
+
}
|
|
2613
2748
|
}
|
|
2614
|
-
}
|
|
2615
2749
|
|
|
2616
|
-
|
|
2750
|
+
this.updatePaths(root, shell.paths, offset);
|
|
2617
2751
|
|
|
2618
|
-
|
|
2752
|
+
// Static web components can sometimes have children created via expressions.
|
|
2753
|
+
// But calling applyExprs() will mess up the shell's path to them.
|
|
2754
|
+
// So we find them first, then call activateStaticComponents() after their children have been created.
|
|
2755
|
+
let staticComponents = this.findStaticComponents(root, shell, offset);
|
|
2619
2756
|
|
|
2620
|
-
|
|
2621
|
-
this.applyExprs(template.exprs);
|
|
2622
|
-
}
|
|
2757
|
+
this.activateEmbeds(root, shell, offset);
|
|
2623
2758
|
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2759
|
+
// Apply exprs
|
|
2760
|
+
this.applyExprs(template.exprs);
|
|
2761
|
+
|
|
2762
|
+
this.instantiateStaticComponents(staticComponents);
|
|
2763
|
+
}
|
|
2627
2764
|
}
|
|
2628
2765
|
}
|
|
2629
2766
|
|
|
@@ -2645,8 +2782,8 @@ function getSingleEl(fragment) {
|
|
|
2645
2782
|
* @param el {HTMLElement}
|
|
2646
2783
|
* @returns {boolean} */
|
|
2647
2784
|
function isReplaceEl(fragment, el) {
|
|
2648
|
-
return
|
|
2649
|
-
&&
|
|
2785
|
+
return fragment.children.length===1
|
|
2786
|
+
&& el.tagName.includes('-') // TODO: Check for solarite-placeholder attribute instead?
|
|
2650
2787
|
&& fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') === el.tagName;
|
|
2651
2788
|
}
|
|
2652
2789
|
|
|
@@ -2665,19 +2802,9 @@ class Template {
|
|
|
2665
2802
|
/** @type {Array} Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
|
|
2666
2803
|
hashedFields;
|
|
2667
2804
|
|
|
2668
|
-
/**
|
|
2669
|
-
* @deprecated
|
|
2670
|
-
* @type {ExprPath} Used with forEach() from watch.js
|
|
2671
|
-
* Set in ExprPath.apply() */
|
|
2672
|
-
parentPath;
|
|
2673
|
-
|
|
2674
2805
|
/** @type {NodeGroup} */
|
|
2675
2806
|
nodeGroup;
|
|
2676
2807
|
|
|
2677
|
-
/**
|
|
2678
|
-
* @type {string[][]} */
|
|
2679
|
-
paths = [];
|
|
2680
|
-
|
|
2681
2808
|
/**
|
|
2682
2809
|
*
|
|
2683
2810
|
* @param htmlStrings {string[]}
|
|
@@ -2719,17 +2846,21 @@ class Template {
|
|
|
2719
2846
|
if (standalone) {
|
|
2720
2847
|
ng = new RootNodeGroup(this, null, options);
|
|
2721
2848
|
el = ng.getRootNode();
|
|
2722
|
-
|
|
2849
|
+
Globals$1.nodeGroups.set(el, ng); // Why was this commented out?
|
|
2723
2850
|
firstTime = true;
|
|
2724
2851
|
}
|
|
2725
2852
|
else {
|
|
2726
|
-
ng = Globals.nodeGroups.get(el);
|
|
2853
|
+
ng = Globals$1.nodeGroups.get(el);
|
|
2727
2854
|
if (!ng) {
|
|
2728
2855
|
ng = new RootNodeGroup(this, el, options);
|
|
2729
|
-
|
|
2856
|
+
Globals$1.nodeGroups.set(el, ng); // Why was this commented out?
|
|
2730
2857
|
firstTime = true;
|
|
2731
2858
|
}
|
|
2732
|
-
|
|
2859
|
+
|
|
2860
|
+
// This can happen if we try manually rendering one template to a NodeGroup that was created expecting a different template.
|
|
2861
|
+
// These don't always have the same length, for example if one attribute has multiple expressions.
|
|
2862
|
+
if (ng.paths.length === 0 && this.exprs.length || ng.paths.length > this.exprs.length)
|
|
2863
|
+
throw new Error(`Solarite Error: Parent HTMLElement ${ng.template.html.join('${...}')} and ${ng.paths.length} \${value} placeholders can't accomodate a Template with ${this.exprs.length} values.`); }
|
|
2733
2864
|
|
|
2734
2865
|
// Creating the root nodegroup also renders it.
|
|
2735
2866
|
// If we didn't just create it, we need to render it.
|
|
@@ -2737,23 +2868,32 @@ class Template {
|
|
|
2737
2868
|
if (this.html?.length === 1 && !this.html[0])
|
|
2738
2869
|
el.innerHTML = ''; // Fast path for empty component.
|
|
2739
2870
|
else {
|
|
2740
|
-
ng.clearRenderWatched();
|
|
2741
2871
|
ng.applyExprs(this.exprs);
|
|
2742
2872
|
}
|
|
2743
2873
|
}
|
|
2744
2874
|
|
|
2875
|
+
ng.exprsToRender = new Map();
|
|
2745
2876
|
return el;
|
|
2746
2877
|
}
|
|
2747
2878
|
|
|
2748
2879
|
getExactKey() {
|
|
2749
|
-
if (!this.exactKey)
|
|
2750
|
-
|
|
2880
|
+
if (!this.exactKey) {
|
|
2881
|
+
if (this.exprs.length)
|
|
2882
|
+
this.exactKey = getObjectHash(this);// calls this.toJSON().
|
|
2883
|
+
else // Don't hash plain html.
|
|
2884
|
+
this.exactKey = this.html[0];
|
|
2885
|
+
}
|
|
2751
2886
|
return this.exactKey;
|
|
2752
2887
|
}
|
|
2753
2888
|
|
|
2754
2889
|
getCloseKey() {
|
|
2755
|
-
|
|
2756
|
-
|
|
2890
|
+
//console.log(this.exprs.length)
|
|
2891
|
+
if (!this.closeKey) {
|
|
2892
|
+
if (this.exprs.length)
|
|
2893
|
+
this.closeKey = /*'@' + */this.toJSON()[0];
|
|
2894
|
+
else
|
|
2895
|
+
this.closeKey = this.html[0];
|
|
2896
|
+
}
|
|
2757
2897
|
// Use the joined html when debugging? But it breaks some tests.
|
|
2758
2898
|
//return '@'+this.html.join('|')
|
|
2759
2899
|
|
|
@@ -2776,8 +2916,8 @@ class Template {
|
|
|
2776
2916
|
|
|
2777
2917
|
/**
|
|
2778
2918
|
* Convert strings to HTMLNodes.
|
|
2779
|
-
* Using
|
|
2780
|
-
* Using
|
|
2919
|
+
* Using h`...` as a tag will always create a Template.
|
|
2920
|
+
* Using h() as a function() will always create a DOM element.
|
|
2781
2921
|
*
|
|
2782
2922
|
* Features beyond what standard js tagged template strings do:
|
|
2783
2923
|
* 1. r`` sub-expressions
|
|
@@ -2787,24 +2927,27 @@ class Template {
|
|
|
2787
2927
|
* 5. TODO: list more
|
|
2788
2928
|
*
|
|
2789
2929
|
* Currently supported:
|
|
2790
|
-
* 1.
|
|
2791
|
-
* 2.
|
|
2930
|
+
* 1. h(el, options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
|
|
2931
|
+
* 2. h(el, template, ?options) // Render the Template created by #1 to element.
|
|
2792
2932
|
*
|
|
2793
|
-
* 3.
|
|
2933
|
+
* 3. h`<b>Hello</b> ${'World'}!` // Create Template that can later be used to create nodes.
|
|
2794
2934
|
*
|
|
2795
|
-
* 4.
|
|
2796
|
-
* 5.
|
|
2797
|
-
* 6.
|
|
2798
|
-
* 7.
|
|
2935
|
+
* 4. h('Hello'); // Create single text node.
|
|
2936
|
+
* 5. h('<b>Hello</b>'); // Create single HTMLElement
|
|
2937
|
+
* 6. h('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
|
|
2938
|
+
* 7. h()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which
|
|
2799
2939
|
* // includes properly handling nested components and r`` sub-expressions.
|
|
2800
|
-
* 8.
|
|
2801
|
-
*
|
|
2802
|
-
* 9. r({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
|
|
2940
|
+
* 8. h(template) // Render Template created by #1.
|
|
2803
2941
|
*
|
|
2942
|
+
* 9. h({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
|
|
2943
|
+
* 10. h(string, object, ...) // JSX TODO
|
|
2804
2944
|
* @param htmlStrings {?HTMLElement|string|string[]|function():Template|{render:function()}}
|
|
2805
2945
|
* @param exprs {*[]|string|Template|Object}
|
|
2806
2946
|
* @return {Node|HTMLElement|Template} */
|
|
2807
|
-
function
|
|
2947
|
+
function h(htmlStrings=undefined, ...exprs) {
|
|
2948
|
+
|
|
2949
|
+
if (htmlStrings === undefined && !exprs.length && arguments.length)
|
|
2950
|
+
throw new Error('h() cannot be called with undefined.');
|
|
2808
2951
|
|
|
2809
2952
|
// TODO: Make this a more flat if/else and call other functions for the logic.
|
|
2810
2953
|
if (htmlStrings instanceof Node) {
|
|
@@ -2819,7 +2962,7 @@ function r(htmlStrings=undefined, ...exprs) {
|
|
|
2819
2962
|
|
|
2820
2963
|
// Return a tagged template function that applies the tagged themplate to parent.
|
|
2821
2964
|
let taggedTemplate = (htmlStrings, ...exprs) => {
|
|
2822
|
-
Globals.rendered.add(parent);
|
|
2965
|
+
Globals$1.rendered.add(parent);
|
|
2823
2966
|
let template = new Template(htmlStrings, exprs);
|
|
2824
2967
|
return template.render(parent, options);
|
|
2825
2968
|
};
|
|
@@ -2827,7 +2970,7 @@ function r(htmlStrings=undefined, ...exprs) {
|
|
|
2827
2970
|
}
|
|
2828
2971
|
|
|
2829
2972
|
// 2. Render template created by #4 to element.
|
|
2830
|
-
else
|
|
2973
|
+
else { // instanceof Template
|
|
2831
2974
|
let options = exprs[1];
|
|
2832
2975
|
template.render(parent, options);
|
|
2833
2976
|
|
|
@@ -2838,16 +2981,6 @@ function r(htmlStrings=undefined, ...exprs) {
|
|
|
2838
2981
|
parent.append(this.rootNg.getParentNode());
|
|
2839
2982
|
}
|
|
2840
2983
|
}
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
// null for expr[0], remove whole element.
|
|
2845
|
-
// This path never happens?
|
|
2846
|
-
else {
|
|
2847
|
-
throw new Error('unsupported');
|
|
2848
|
-
//let ngm = NodeGroupManager.get(parent);
|
|
2849
|
-
//ngm.render(null, exprs[1])
|
|
2850
|
-
}
|
|
2851
2984
|
}
|
|
2852
2985
|
|
|
2853
2986
|
// 3. Path if used as a template tag.
|
|
@@ -2856,6 +2989,22 @@ function r(htmlStrings=undefined, ...exprs) {
|
|
|
2856
2989
|
}
|
|
2857
2990
|
|
|
2858
2991
|
else if (typeof htmlStrings === 'string' || htmlStrings instanceof String) {
|
|
2992
|
+
// 10. JSX
|
|
2993
|
+
if (typeof exprs[0] === 'object') {
|
|
2994
|
+
exprs[0] || {};
|
|
2995
|
+
exprs.slice(1);
|
|
2996
|
+
|
|
2997
|
+
let templateHtmlStrings = [];
|
|
2998
|
+
let templateExprs = [];
|
|
2999
|
+
|
|
3000
|
+
// TODO How to know which children are static html and which are expression placeholders?
|
|
3001
|
+
// Perhaps we have to treat every text child as a string?
|
|
3002
|
+
|
|
3003
|
+
assert(templateHtmlStrings.length === templateExprs.length+1);
|
|
3004
|
+
return new Template(templateHtmlStrings, templateExprs);
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
|
|
2859
3008
|
// If it starts with a string, trim both ends.
|
|
2860
3009
|
// TODO: Also trim if it ends with whitespace?
|
|
2861
3010
|
if (htmlStrings.match(/^\s^</))
|
|
@@ -2879,7 +3028,7 @@ function r(htmlStrings=undefined, ...exprs) {
|
|
|
2879
3028
|
else if (htmlStrings === undefined) {
|
|
2880
3029
|
return (htmlStrings, ...exprs) => {
|
|
2881
3030
|
//Globals.rendered.add(parent)
|
|
2882
|
-
let template =
|
|
3031
|
+
let template = h(htmlStrings, ...exprs);
|
|
2883
3032
|
return template.render();
|
|
2884
3033
|
}
|
|
2885
3034
|
}
|
|
@@ -2891,46 +3040,193 @@ function r(htmlStrings=undefined, ...exprs) {
|
|
|
2891
3040
|
|
|
2892
3041
|
|
|
2893
3042
|
// 9. Create dynamic element with render() function.
|
|
3043
|
+
// TODO: This path doesn't handle embeds like data-id="..."
|
|
2894
3044
|
else if (typeof htmlStrings === 'object') {
|
|
2895
3045
|
let obj = htmlStrings;
|
|
2896
3046
|
|
|
3047
|
+
if (obj.constructor.name !== 'Object')
|
|
3048
|
+
throw new Error(`Solarate Web Component class ${obj.constructor?.name} must extend HTMLElement.`);
|
|
3049
|
+
|
|
3050
|
+
|
|
2897
3051
|
// Special rebound render path, called by normal path.
|
|
2898
|
-
|
|
3052
|
+
// Intercepts the main r`...` function call inside render().
|
|
3053
|
+
if (Globals$1.objToEl.has(obj)) {
|
|
2899
3054
|
return function(...args) {
|
|
2900
|
-
let template =
|
|
3055
|
+
let template = h(...args);
|
|
2901
3056
|
let el = template.render();
|
|
2902
|
-
Globals.objToEl.set(obj, el);
|
|
3057
|
+
Globals$1.objToEl.set(obj, el);
|
|
2903
3058
|
}.bind(obj);
|
|
2904
3059
|
}
|
|
2905
3060
|
|
|
2906
3061
|
// Normal path
|
|
2907
3062
|
else {
|
|
2908
|
-
Globals.objToEl.set(obj, null);
|
|
2909
|
-
obj
|
|
2910
|
-
let el = Globals.objToEl.get(obj);
|
|
2911
|
-
Globals.objToEl.delete(obj);
|
|
3063
|
+
Globals$1.objToEl.set(obj, null);
|
|
3064
|
+
obj[renderF](); // Calls the Special rebound render path above, when the render function calls r(this)
|
|
3065
|
+
let el = Globals$1.objToEl.get(obj);
|
|
3066
|
+
Globals$1.objToEl.delete(obj);
|
|
2912
3067
|
|
|
2913
3068
|
for (let name in obj)
|
|
2914
3069
|
if (typeof obj[name] === 'function')
|
|
2915
|
-
el[name] = obj[name].bind(el);
|
|
3070
|
+
el[name] = obj[name].bind(el); // Make the "this" of functions be el.
|
|
3071
|
+
// TODO: But this doesn't work for passing an object with functions as a constructor arg via an attribute:
|
|
3072
|
+
// <my-element arg=${{myFunc() { return this }}}
|
|
2916
3073
|
else
|
|
2917
3074
|
el[name] = obj[name];
|
|
2918
3075
|
|
|
3076
|
+
// Bind id's
|
|
3077
|
+
// This doesn't work for id's referenced by attributes.
|
|
3078
|
+
// for (let idEl of el.querySelectorAll('[id],[data-id]')) {
|
|
3079
|
+
// Util.bindId(el, idEl);
|
|
3080
|
+
// Util.bindId(obj, idEl);
|
|
3081
|
+
// }
|
|
3082
|
+
// TODO: Bind styles
|
|
3083
|
+
|
|
2919
3084
|
return el;
|
|
2920
3085
|
}
|
|
2921
3086
|
}
|
|
2922
3087
|
|
|
2923
3088
|
else
|
|
2924
3089
|
throw new Error('Unsupported arguments.')
|
|
2925
|
-
}
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
// Trick to prevent minifier from renaming this function.
|
|
3093
|
+
let renderF = 'render';
|
|
2926
3094
|
|
|
2927
|
-
|
|
3095
|
+
/**
|
|
3096
|
+
* There are three ways to create an instance of a Solarite Component:
|
|
3097
|
+
* 1. new ComponentName(); // direct class instantiation
|
|
3098
|
+
* 2. this.html = r`<div><component-name></component-name></div>; // as a child of another RedComponent.
|
|
3099
|
+
* 3. <body><component-name></component-name></body> // in the Document html.
|
|
3100
|
+
*
|
|
3101
|
+
* When created via #3, Solarite has no way to pass attributes as arguments to the constructor. So to make
|
|
3102
|
+
* sure we get the correct value via all three paths, we write our constructors according to the following
|
|
3103
|
+
* example. Note that constructor args are embedded in an object, and must be all lower-case because
|
|
3104
|
+
* Browsers make all html attribute names lowercase.
|
|
3105
|
+
*
|
|
3106
|
+
* @example
|
|
3107
|
+
* constructor({name, userid=1}={}) {
|
|
3108
|
+
* super();
|
|
3109
|
+
*
|
|
3110
|
+
* // Get value from "name" attriute if persent, otherwise from name constructor arg.
|
|
3111
|
+
* this.name = getArg(this, 'name', name);
|
|
3112
|
+
*
|
|
3113
|
+
* // Optionally convert the value to an integer.
|
|
3114
|
+
* this.userId = getArg(this, 'userid', userid, ArgType.Int);
|
|
3115
|
+
* }
|
|
3116
|
+
*
|
|
3117
|
+
* @param el {HTMLElement}
|
|
3118
|
+
* @param attributeName {string} Attribute name. Not case-sensitive.
|
|
3119
|
+
* @param defaultValue {*} Default value to use if attribute doesn't exist.
|
|
3120
|
+
* @param type {ArgType|function|Class|*[]}
|
|
3121
|
+
* If an array, use the value if it's in the array, otherwise return undefined.
|
|
3122
|
+
* If it's a function, pass the value to the function and return the result.
|
|
3123
|
+
* @param fallback {*} If the defaultValue is undefiend and type can't be parsed as the given type, use this value.
|
|
3124
|
+
* TODO: Should this be merged with the defaultValue argument?
|
|
3125
|
+
* @return {*} Undefined if attribute isn't set. */
|
|
3126
|
+
function getArg(el, attributeName, defaultValue=undefined, type=ArgType.String, fallback=undefined) {
|
|
3127
|
+
let val = defaultValue;
|
|
3128
|
+
let attrVal = el.getAttribute(attributeName) || el.getAttribute(Util.camelToDashes(attributeName));
|
|
3129
|
+
if (attrVal !== null) // If attribute doesn't exist.
|
|
3130
|
+
val = attrVal;
|
|
3131
|
+
|
|
3132
|
+
if (Array.isArray(type))
|
|
3133
|
+
return type.includes(val) ? val : fallback;
|
|
3134
|
+
|
|
3135
|
+
if (typeof type === 'function') {
|
|
3136
|
+
return type.constructor
|
|
3137
|
+
? new type(val) // arg type is custom Class
|
|
3138
|
+
: type(val); // arg type is custom function
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
// If bool, it's true as long as it exists and its value isn't falsey.
|
|
3142
|
+
if (type===ArgType.Bool) {
|
|
3143
|
+
let lAttrVal = typeof val === 'string' ? val.toLowerCase() : val;
|
|
3144
|
+
if (['false', '0', false, 0, null, undefined, NaN].includes(lAttrVal))
|
|
3145
|
+
return false;
|
|
3146
|
+
if (['true', true].includes(lAttrVal) || parseFloat(lAttrVal) !== 0)
|
|
3147
|
+
return true;
|
|
3148
|
+
return fallback;
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
// Attribute doesn't exist
|
|
3152
|
+
let result;
|
|
3153
|
+
switch (type) {
|
|
3154
|
+
case ArgType.Int:
|
|
3155
|
+
result = parseInt(val);
|
|
3156
|
+
return isNaN(result) ? fallback : result;
|
|
3157
|
+
case ArgType.Float:
|
|
3158
|
+
result = parseFloat(val);
|
|
3159
|
+
return isNaN(result) ? fallback : result;
|
|
3160
|
+
case ArgType.String:
|
|
3161
|
+
return [undefined, null, false].includes(val) ? '' : val+'';
|
|
3162
|
+
case ArgType.Json:
|
|
3163
|
+
case ArgType.Eval:
|
|
3164
|
+
if (typeof val === 'string' && val.length)
|
|
3165
|
+
try {
|
|
3166
|
+
if (type === ArgType.Json)
|
|
3167
|
+
return JSON.parse(val);
|
|
3168
|
+
else
|
|
3169
|
+
return eval(`(${val})`);
|
|
3170
|
+
} catch (e) {
|
|
3171
|
+
return val;
|
|
3172
|
+
}
|
|
3173
|
+
else return val;
|
|
3174
|
+
|
|
3175
|
+
// type not provided
|
|
3176
|
+
default:
|
|
3177
|
+
return val;
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
|
|
3182
|
+
/**
|
|
3183
|
+
* Experimental. Set multiple arguments/attributes all at once.
|
|
3184
|
+
* @param el {HTMLElement}
|
|
3185
|
+
* @param args {Record<string, any>}
|
|
3186
|
+
* @param types {Record<string, ArgType|function|Class>}
|
|
3187
|
+
*
|
|
3188
|
+
* @example
|
|
3189
|
+
* constructor({user, path}={}) {
|
|
3190
|
+
* setArgs(this, arguments[0], {user: User, path: ArgType.String});
|
|
3191
|
+
* }
|
|
3192
|
+
*/
|
|
3193
|
+
function setArgs(el, args, types) {
|
|
3194
|
+
for (let name in args)
|
|
3195
|
+
this[name] = getArg(el, name, args[name], types[name] || ArgType.String);
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
|
|
3199
|
+
/**
|
|
3200
|
+
* @enum */
|
|
3201
|
+
var ArgType = {
|
|
3202
|
+
|
|
3203
|
+
/**
|
|
3204
|
+
* false, 0, null, undefined, '0', and 'false' (case-insensitive) become false.
|
|
3205
|
+
* Anything else, including empty string becomes true.
|
|
3206
|
+
* Empty string is true because attributes with no value should be evaulated as true. */
|
|
3207
|
+
Bool: 'Bool',
|
|
3208
|
+
|
|
3209
|
+
Int: 'Int',
|
|
3210
|
+
Float: 'Float',
|
|
3211
|
+
String: 'String',
|
|
2928
3212
|
|
|
3213
|
+
/** @deprecated for Json */
|
|
3214
|
+
JSON: 'Json',
|
|
2929
3215
|
|
|
3216
|
+
/**
|
|
3217
|
+
* Parse the string value as JSON.
|
|
3218
|
+
* If it's not parsable, return the value as a string. */
|
|
3219
|
+
Json: 'Json',
|
|
2930
3220
|
|
|
3221
|
+
/**
|
|
3222
|
+
* Evaluate the string as JavaScript using the eval() function.
|
|
3223
|
+
* If it can't be evaluated, return the original string. */
|
|
3224
|
+
Eval: 'Eval'
|
|
3225
|
+
};
|
|
3226
|
+
|
|
2931
3227
|
function defineClass(Class, tagName, extendsTag) {
|
|
2932
|
-
if (!customElements
|
|
2933
|
-
tagName = tagName || camelToDashes(Class.name);
|
|
3228
|
+
if (!customElements[getName](Class)) { // If not previously defined.
|
|
3229
|
+
tagName = tagName || Util.camelToDashes(Class.name);
|
|
2934
3230
|
if (!tagName.includes('-'))
|
|
2935
3231
|
tagName += '-element';
|
|
2936
3232
|
|
|
@@ -2938,21 +3234,17 @@ function defineClass(Class, tagName, extendsTag) {
|
|
|
2938
3234
|
if (extendsTag)
|
|
2939
3235
|
options = {extends: extendsTag};
|
|
2940
3236
|
|
|
2941
|
-
customElements
|
|
3237
|
+
customElements[define](tagName, Class, options);
|
|
2942
3238
|
}
|
|
2943
3239
|
}
|
|
2944
3240
|
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
3241
|
/**
|
|
2950
3242
|
* Create a version of the Solarite class that extends from the given tag name.
|
|
2951
3243
|
* Reasons to inherit from this instead of HTMLElement. None of these are all that useful.
|
|
2952
3244
|
* 1. customElements.define() is called automatically when you create the first instance.
|
|
2953
3245
|
* 2. Calls render() when added to the DOM, if it hasn't been called already.
|
|
2954
|
-
* 3. Child elements are added before constructor is called. But they're also passed to the constructor.
|
|
2955
|
-
* 4. We can use this.html = r`...` to set html.
|
|
3246
|
+
* 3. Child elements are added before constructor is called. But they're also passed to the constructor. (deprecated?)
|
|
3247
|
+
* 4. We can use this.html = r`...` to set html. (deprecated)
|
|
2956
3248
|
* 5. We have the onConnect, onFirstConnect, and onDisconnect methods.
|
|
2957
3249
|
* Can't figure out how to have these work standalone though, and still be synchronous.
|
|
2958
3250
|
* 6. Can we extend from other element types like TR?
|
|
@@ -2971,10 +3263,10 @@ function createSolarite(extendsTag=null) {
|
|
|
2971
3263
|
if (extendsTag && !extendsTag.includes('-')) {
|
|
2972
3264
|
extendsTag = extendsTag.toLowerCase();
|
|
2973
3265
|
|
|
2974
|
-
BaseClass = Globals.elementClasses[extendsTag];
|
|
3266
|
+
BaseClass = Globals$1.elementClasses[extendsTag];
|
|
2975
3267
|
if (!BaseClass) { // TODO: Use Cache
|
|
2976
3268
|
BaseClass = document.createElement(extendsTag).constructor;
|
|
2977
|
-
Globals.elementClasses[extendsTag] = BaseClass;
|
|
3269
|
+
Globals$1.elementClasses[extendsTag] = BaseClass;
|
|
2978
3270
|
}
|
|
2979
3271
|
}
|
|
2980
3272
|
|
|
@@ -3000,10 +3292,10 @@ function createSolarite(extendsTag=null) {
|
|
|
3000
3292
|
* TODO: Make these standalone functions.
|
|
3001
3293
|
* Callbacks.
|
|
3002
3294
|
* Use onConnect.push(() => ...); to add new callbacks. */
|
|
3003
|
-
onConnect
|
|
3295
|
+
onConnect;
|
|
3004
3296
|
|
|
3005
|
-
onFirstConnect
|
|
3006
|
-
onDisconnect
|
|
3297
|
+
onFirstConnect;
|
|
3298
|
+
onDisconnect;
|
|
3007
3299
|
|
|
3008
3300
|
/**
|
|
3009
3301
|
* @param options {RenderOptions} */
|
|
@@ -3015,27 +3307,28 @@ function createSolarite(extendsTag=null) {
|
|
|
3015
3307
|
this.render();
|
|
3016
3308
|
|
|
3017
3309
|
else if (options.render===false)
|
|
3018
|
-
Globals.rendered.add(this); // Don't render on connectedCallback()
|
|
3310
|
+
Globals$1.rendered.add(this); // Don't render on connectedCallback()
|
|
3019
3311
|
|
|
3020
|
-
// Add children before constructor code executes.
|
|
3021
|
-
//
|
|
3312
|
+
// Add slot children before constructor code executes.
|
|
3313
|
+
// This breaks the styleStaticNested test.
|
|
3314
|
+
// PendingChildren is setup in NodeGroup.instantiateComponent()
|
|
3022
3315
|
// TODO: Match named slots.
|
|
3023
|
-
let ch = Globals.pendingChildren.pop();
|
|
3024
|
-
if (ch)
|
|
3025
|
-
|
|
3316
|
+
//let ch = Globals.pendingChildren.pop();
|
|
3317
|
+
//if (ch) // TODO: how could there be a slot before render is called?
|
|
3318
|
+
// (this.querySelector('slot') || this).append(...ch);
|
|
3026
3319
|
|
|
3027
|
-
/** @deprecated
|
|
3320
|
+
/** @deprecated
|
|
3028
3321
|
Object.defineProperty(this, 'html', {
|
|
3029
3322
|
set(html) {
|
|
3030
3323
|
Globals.rendered.add(this);
|
|
3031
3324
|
if (typeof html === 'string') {
|
|
3032
|
-
console.warn("Assigning to this.html without the r template prefix.")
|
|
3325
|
+
console.warn("Assigning to this.html without the r template prefix.")
|
|
3033
3326
|
this.innerHTML = html;
|
|
3034
3327
|
}
|
|
3035
3328
|
else
|
|
3036
3329
|
this.modifications = r(this, html, options);
|
|
3037
3330
|
}
|
|
3038
|
-
})
|
|
3331
|
+
})*/
|
|
3039
3332
|
|
|
3040
3333
|
/*
|
|
3041
3334
|
let pthis = new Proxy(this, {
|
|
@@ -3050,7 +3343,7 @@ function createSolarite(extendsTag=null) {
|
|
|
3050
3343
|
/**
|
|
3051
3344
|
* Call render() only if it hasn't already been called. */
|
|
3052
3345
|
renderFirstTime() {
|
|
3053
|
-
if (!Globals.rendered.has(this) && this.render)
|
|
3346
|
+
if (!Globals$1.rendered.has(this) && this.render)
|
|
3054
3347
|
this.render();
|
|
3055
3348
|
}
|
|
3056
3349
|
|
|
@@ -3058,171 +3351,30 @@ function createSolarite(extendsTag=null) {
|
|
|
3058
3351
|
* Called automatically by the browser. */
|
|
3059
3352
|
connectedCallback() {
|
|
3060
3353
|
this.renderFirstTime();
|
|
3061
|
-
if (!Globals.connected.has(this)) {
|
|
3062
|
-
Globals.connected.add(this);
|
|
3063
|
-
this.onFirstConnect
|
|
3354
|
+
if (!Globals$1.connected.has(this)) {
|
|
3355
|
+
Globals$1.connected.add(this);
|
|
3356
|
+
if (this.onFirstConnect)
|
|
3357
|
+
this.onFirstConnect();
|
|
3064
3358
|
}
|
|
3065
|
-
this.onConnect
|
|
3359
|
+
if (this.onConnect)
|
|
3360
|
+
this.onConnect();
|
|
3066
3361
|
}
|
|
3067
3362
|
|
|
3068
3363
|
disconnectedCallback() {
|
|
3069
|
-
this.onDisconnect
|
|
3364
|
+
if (this.onDisconnect)
|
|
3365
|
+
this.onDisconnect();
|
|
3070
3366
|
}
|
|
3071
3367
|
|
|
3072
3368
|
|
|
3073
3369
|
static define(tagName=null) {
|
|
3074
3370
|
defineClass(this, tagName, extendsTag);
|
|
3075
3371
|
}
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
3372
|
}
|
|
3079
|
-
}
|
|
3080
|
-
|
|
3081
|
-
/**
|
|
3082
|
-
* Trying to be able to automatically watch primitive values.
|
|
3083
|
-
* TODO:
|
|
3084
|
-
* 1. Have get() return Proxies for nested updates.
|
|
3085
|
-
* 2. Override .map() for loops to capture changes.
|
|
3086
|
-
*/
|
|
3087
|
-
|
|
3088
|
-
let unusedArg = Symbol('unusedArg');
|
|
3089
|
-
|
|
3090
|
-
/**
|
|
3091
|
-
* Custom map function triggers the get() Proxy.
|
|
3092
|
-
* @param array {Array}
|
|
3093
|
-
* @param callback {function}
|
|
3094
|
-
* @returns {*[]} */
|
|
3095
|
-
function map(array, callback) {
|
|
3096
|
-
let result = [];
|
|
3097
|
-
for (let i=0; i<array.length; i++)
|
|
3098
|
-
result.push(callback(array[i], i, array));
|
|
3099
|
-
return result;
|
|
3100
|
-
}
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
/**
|
|
3104
|
-
*
|
|
3105
|
-
* @param root {HTMLElement}
|
|
3106
|
-
* @param field {string}
|
|
3107
|
-
* @param value {string|Symbol} */
|
|
3108
|
-
function watch3(root, field, value=unusedArg) {
|
|
3109
|
-
// Store internal value used by get/set.
|
|
3110
|
-
if (value !== unusedArg)
|
|
3111
|
-
root[field] = value;
|
|
3112
|
-
else
|
|
3113
|
-
value = root[field];
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
// use a single object for both defineProperty and new Proxy's handler.
|
|
3117
|
-
const handler = {
|
|
3118
|
-
get(obj, prop, receiver) {
|
|
3119
|
-
|
|
3120
|
-
let result = (obj === receiver && field === prop)
|
|
3121
|
-
? value // top-level value.
|
|
3122
|
-
: Reflect.get(obj, prop, receiver); // avoid infinite recursion.
|
|
3123
|
-
|
|
3124
|
-
if (prop === 'map')
|
|
3125
|
-
|
|
3126
|
-
// Double function so the ExprPath calls it as a function,
|
|
3127
|
-
// instead of it being evaluated immediately when the Templat eis created.
|
|
3128
|
-
return (callback) => () => {
|
|
3129
|
-
let rootNg = Globals.nodeGroups.get(root);
|
|
3130
|
-
rootNg.mapCallbacks.set(obj, callback);
|
|
3131
|
-
return map(new Proxy(obj, handler), callback);
|
|
3132
|
-
}
|
|
3133
|
-
|
|
3134
|
-
// Track which ExprPath is using this variable.
|
|
3135
|
-
if (Globals.currentExprPath) {
|
|
3136
|
-
let [exprPath, exprFunction] = Globals.currentExprPath; // Set in ExprPath.applyExact()
|
|
3137
|
-
|
|
3138
|
-
let rootNg = Globals.nodeGroups.get(root);
|
|
3139
|
-
|
|
3140
|
-
// Init for field.
|
|
3141
|
-
rootNg.watchedExprPaths[field] = rootNg.watchedExprPaths[field] || new Set();
|
|
3142
|
-
rootNg.watchedExprPaths[field].add(exprPath);
|
|
3143
|
-
}
|
|
3144
|
-
|
|
3145
|
-
if (result && typeof result === 'object')
|
|
3146
|
-
return new Proxy(result, handler);
|
|
3147
|
-
|
|
3148
|
-
return result;
|
|
3149
|
-
},
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
// TODO: Will fail for attribute w/ a value having multiple ExprPaths.
|
|
3153
|
-
// TODO: This won't update a component's expressions.
|
|
3154
|
-
set(obj, prop, val, receiver) {
|
|
3155
|
-
|
|
3156
|
-
// 1. Set the value.
|
|
3157
|
-
if (obj === receiver && field === prop)
|
|
3158
|
-
value = val; // top-level value.
|
|
3159
|
-
else // avoid infinite recursion.
|
|
3160
|
-
Reflect.set(obj, prop, val, receiver);
|
|
3161
|
-
|
|
3162
|
-
// 2. Add to the list of ExprPaths to re-render.
|
|
3163
|
-
let rootNg = Globals.nodeGroups.get(root);
|
|
3164
|
-
for (let exprPath of rootNg.watchedExprPaths[field]) {
|
|
3165
|
-
|
|
3166
|
-
// Update a single NodeGroup created by array.map()
|
|
3167
|
-
if (Array.isArray(obj) && parseInt(prop) == prop) {
|
|
3168
|
-
let exprsToRender = rootNg.exprsToRender.get(exprPath);
|
|
3169
|
-
|
|
3170
|
-
// If we're not re-rendering the whole thing.
|
|
3171
|
-
if (exprsToRender !== true)
|
|
3172
|
-
Util$1.mapAdd(rootNg.exprsToRender, exprPath, [obj, prop, val]);
|
|
3173
|
-
}
|
|
3174
|
-
|
|
3175
|
-
// Reapply the whole expression.
|
|
3176
|
-
else
|
|
3177
|
-
rootNg.exprsToRender.set(exprPath, true);
|
|
3178
|
-
}
|
|
3179
|
-
return true;
|
|
3180
|
-
}
|
|
3181
|
-
};
|
|
3182
|
-
|
|
3183
|
-
Object.defineProperty(root, field, {
|
|
3184
|
-
get: () => handler.get(root, field, root),
|
|
3185
|
-
set: (val) => handler.set(root, field, val, root)
|
|
3186
|
-
});
|
|
3187
3373
|
}
|
|
3188
3374
|
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
* @returns {*[]} */
|
|
3193
|
-
function renderWatched(root) {
|
|
3194
|
-
let rootNg = Globals.nodeGroups.get(root);
|
|
3195
|
-
let modified = [];
|
|
3196
|
-
|
|
3197
|
-
for (let [exprPath, params] of rootNg.exprsToRender) {
|
|
3198
|
-
|
|
3199
|
-
// Reapply the whole expression.
|
|
3200
|
-
if (params === true) {
|
|
3201
|
-
exprPath.apply(exprPath.watchFunction);
|
|
3202
|
-
|
|
3203
|
-
// TODO: freeNodeGroups() could be skipped if applyExprs() never marked them as in-use.
|
|
3204
|
-
exprPath.freeNodeGroups();
|
|
3205
|
-
|
|
3206
|
-
modified.push(...exprPath.getNodes());
|
|
3207
|
-
}
|
|
3208
|
-
|
|
3209
|
-
// Update a single NodeGroup created by array.map()
|
|
3210
|
-
else {
|
|
3211
|
-
for (let row of params) {
|
|
3212
|
-
let [obj, prop, value] = row;
|
|
3213
|
-
let callback = rootNg.mapCallbacks.get(obj);
|
|
3214
|
-
let template = callback(value);
|
|
3215
|
-
exprPath.applyLoopItemUpdate(prop, template);
|
|
3216
|
-
|
|
3217
|
-
modified.push(...exprPath.nodeGroups[prop].getNodes());
|
|
3218
|
-
}
|
|
3219
|
-
}
|
|
3220
|
-
}
|
|
3221
|
-
|
|
3222
|
-
rootNg.exprsToRender = new Map(); // clear
|
|
3223
|
-
|
|
3224
|
-
return modified;
|
|
3225
|
-
}
|
|
3375
|
+
// Trick to prevent minifier from renaming this method.
|
|
3376
|
+
let define = 'define';
|
|
3377
|
+
let getName = 'getName';
|
|
3226
3378
|
|
|
3227
3379
|
/**
|
|
3228
3380
|
* Solarite JavasCript UI library.
|
|
@@ -3233,12 +3385,13 @@ function renderWatched(root) {
|
|
|
3233
3385
|
/**
|
|
3234
3386
|
* TODO: The Proxy and the multiple base classes mess up 'instanceof Solarite'
|
|
3235
3387
|
* @type {Node|Class<HTMLElement>|function(tagName:string):Node|Class<HTMLElement>} */
|
|
3236
|
-
|
|
3388
|
+
const Solarite = new Proxy(createSolarite(), {
|
|
3237
3389
|
apply(self, _, args) {
|
|
3238
3390
|
return createSolarite(...args)
|
|
3239
3391
|
}
|
|
3240
3392
|
});
|
|
3241
|
-
|
|
3242
|
-
// unfinished
|
|
3393
|
+
|
|
3394
|
+
//export {default as watch, renderWatched} from './watch.js'; // unfinished
|
|
3243
3395
|
|
|
3244
|
-
export
|
|
3396
|
+
export default h;
|
|
3397
|
+
export { ArgType, Globals$1 as Globals, Solarite, Util as SolariteUtil, Template, delve, getArg, h, h as r, setArgs };
|