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-debug.js
CHANGED
|
@@ -1,261 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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;
|
|
1
|
+
//#IFDEV
|
|
2
|
+
/*@__NO_SIDE_EFFECTS__*/
|
|
3
|
+
function assert(val) {
|
|
4
|
+
if (!val) {
|
|
5
|
+
debugger;
|
|
6
|
+
throw new Error('Assertion failed: ' + val);
|
|
232
7
|
}
|
|
233
8
|
}
|
|
234
9
|
|
|
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
|
-
};
|
|
10
|
+
//#ENDIF
|
|
259
11
|
|
|
260
12
|
let lastObjectId = 1>>>0; // Is a 32-bit int faster to increment than JavaScript's Number, which is a 64-bit float?
|
|
261
13
|
let objectIds = new WeakMap();
|
|
@@ -269,7 +21,7 @@ function getObjectId(obj) {
|
|
|
269
21
|
|
|
270
22
|
let result = objectIds.get(obj);
|
|
271
23
|
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()
|
|
24
|
+
result = '~\f' + (lastObjectId++); // We use a unique, 2-byte prefix to ensure it doesn't collide w/ strings not from getObjectId()
|
|
273
25
|
objectIds.set(obj, result);
|
|
274
26
|
}
|
|
275
27
|
return result;
|
|
@@ -288,14 +40,7 @@ function toJSON() {
|
|
|
288
40
|
|
|
289
41
|
// Node.prototype.toJSON = toJSON;
|
|
290
42
|
// 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
|
-
}
|
|
43
|
+
|
|
299
44
|
|
|
300
45
|
/**
|
|
301
46
|
* Get a string that uniquely maps to the values of the given object.
|
|
@@ -310,6 +55,16 @@ if (Node.prototype.toJSON !== toJSON) {
|
|
|
310
55
|
* @param obj {*}
|
|
311
56
|
* @returns {string} */
|
|
312
57
|
function getObjectHash(obj) {
|
|
58
|
+
|
|
59
|
+
// Sometimes these get unassigned by Chrome and Brave 119, as well as Firefox, seemingly randomly!
|
|
60
|
+
// The same tests sometimes pass, sometimes fail, even after browser and OS restarts.
|
|
61
|
+
// So we check the assignments on every run of getObjectHash()
|
|
62
|
+
if (Node.prototype.toJSON !== toJSON) {
|
|
63
|
+
Node.prototype.toJSON = toJSON;
|
|
64
|
+
if (Function.prototype.toJSON !== toJSON) // Will it only unmap one but not the other?
|
|
65
|
+
Function.prototype.toJSON = toJSON;
|
|
66
|
+
}
|
|
67
|
+
|
|
313
68
|
let result;
|
|
314
69
|
isHashing = true;
|
|
315
70
|
try {
|
|
@@ -342,71 +97,164 @@ function getObjectHashCircular(obj) {
|
|
|
342
97
|
});
|
|
343
98
|
}
|
|
344
99
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
100
|
+
var Globals;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Created with a reset() function because it's useful for testing. */
|
|
104
|
+
function reset() {
|
|
105
|
+
Globals = {
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Used by NodeGroup.applyComponentExprs() */
|
|
109
|
+
componentArgsHash: new WeakMap(),
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Store which instances of Solarite have already been added to the DOM.
|
|
113
|
+
* @type {WeakSet<HTMLElement>} */
|
|
114
|
+
connected: new WeakSet(),
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* ExprPath.applyExactNodes() sets this property when an expression is being accessed.
|
|
118
|
+
* watch() then adds the ExprPath to the list of ExprPaths that should be re-rendered when the value changes.
|
|
119
|
+
* @type {ExprPath}*/
|
|
120
|
+
currentExprPath: null,
|
|
121
|
+
|
|
122
|
+
div: document.createElement("div"),
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @type {Record<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
|
|
126
|
+
elementClasses: {},
|
|
127
|
+
|
|
128
|
+
/** @type {Record<string, boolean>} Key is tag-name.propName. Value is whether it's an attribute.*/
|
|
129
|
+
htmlProps: {},
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Used by ExprPath.applyEventAttrib()
|
|
133
|
+
* @type {WeakMap<Node, Record<eventName:string, [original:function, bound:function, args:*[]]>>} */
|
|
134
|
+
nodeEvents: new WeakMap(),
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get the RootNodeGroup for an element.
|
|
138
|
+
* @type {WeakMap<HTMLElement, RootNodeGroup>} */
|
|
139
|
+
nodeGroups: new WeakMap(),
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Used by r() path 9. */
|
|
143
|
+
objToEl: new WeakMap(),
|
|
144
|
+
|
|
145
|
+
//pendingChildren: [],
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Elements that have been rendered to by r() at least once.
|
|
150
|
+
* This is used by the Solarite class to know when to call onFirstConnect()
|
|
151
|
+
* @type {WeakSet<HTMLElement>} */
|
|
152
|
+
rendered: new WeakSet(),
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Elements that are currently rendering via the r() function.
|
|
156
|
+
* @type {WeakSet<HTMLElement>} */
|
|
157
|
+
rendering: new WeakSet(),
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Map from array of Html strings to a Shell created from them.
|
|
161
|
+
* @type {WeakMap<string[], Shell>} */
|
|
162
|
+
shells: new WeakMap(),
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* A map of individual untagged strings to their Templates.
|
|
166
|
+
* This way we don't keep creating new Templates for the same string when re-rendering.
|
|
167
|
+
* This is used by ExprPath.applyExactNodes()
|
|
168
|
+
* @type {Record<string, Template>} */
|
|
169
|
+
//stringTemplates: {},
|
|
170
|
+
|
|
171
|
+
reset,
|
|
172
|
+
|
|
173
|
+
count: 0
|
|
174
|
+
};
|
|
352
175
|
}
|
|
353
|
-
|
|
176
|
+
reset();
|
|
177
|
+
|
|
178
|
+
var Globals$1 = Globals;
|
|
354
179
|
|
|
355
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Follow a path into an object.
|
|
182
|
+
* @param obj {object}
|
|
183
|
+
* @param path {string[]}
|
|
184
|
+
* @param createVal {*} If set, non-existent paths will be created and value at path will be set to createVal.
|
|
185
|
+
* @return {*} The value, or undefined if it can't be reached. */
|
|
186
|
+
function delve(obj, path, createVal = d) {
|
|
187
|
+
let isCreate = createVal !== d;
|
|
356
188
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
189
|
+
let len = path.length;
|
|
190
|
+
if (!obj && !isCreate && len)
|
|
191
|
+
return undefined;
|
|
360
192
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
* @type {WeakSet<HTMLElement>} */
|
|
364
|
-
connected: new WeakSet(),
|
|
193
|
+
let i = 0;
|
|
194
|
+
for (let srcProp of path) {
|
|
365
195
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
* This is used by the Solarite class to know when to call onFirstConnect()
|
|
369
|
-
* @type {WeakSet<HTMLElement>} */
|
|
370
|
-
rendered: new WeakSet(),
|
|
196
|
+
// If the path is undefined and we're not to the end yet:
|
|
197
|
+
if (obj[srcProp] === undefined) {
|
|
371
198
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
199
|
+
// If the next index is an integer or integer string.
|
|
200
|
+
if (isCreate) {
|
|
201
|
+
if (i < len - 1) {
|
|
202
|
+
// If next level path is a number, create as an array
|
|
203
|
+
let isArray = (path[i + 1] + '').match(/^\d+$/);
|
|
204
|
+
obj[srcProp] = isArray ? [] : {};
|
|
205
|
+
}
|
|
206
|
+
} else
|
|
207
|
+
return undefined; // can't traverse
|
|
208
|
+
}
|
|
376
209
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
210
|
+
// If last item in path
|
|
211
|
+
if (isCreate && i === len - 1)
|
|
212
|
+
obj[srcProp] = createVal;
|
|
380
213
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
214
|
+
// Traverse deeper along destination object.
|
|
215
|
+
obj = obj[srcProp];
|
|
216
|
+
i++;
|
|
217
|
+
}
|
|
385
218
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
219
|
+
return obj;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// d means "don't create"
|
|
223
|
+
let d = {};
|
|
224
|
+
|
|
225
|
+
let Util = {
|
|
390
226
|
|
|
391
227
|
/**
|
|
392
|
-
*
|
|
393
|
-
|
|
228
|
+
* Returns true if they're the same.
|
|
229
|
+
* @param a
|
|
230
|
+
* @param b
|
|
231
|
+
* @returns {boolean} */
|
|
232
|
+
arraySame(a, b) {
|
|
233
|
+
let aLength = a.length;
|
|
234
|
+
if (aLength !== b.length)
|
|
235
|
+
return false;
|
|
236
|
+
for (let i=0; i<aLength; i++)
|
|
237
|
+
if (a[i] !== b[i])
|
|
238
|
+
return false;
|
|
239
|
+
return true; // the same.
|
|
240
|
+
},
|
|
394
241
|
|
|
395
|
-
|
|
242
|
+
bindId(root, el) {
|
|
243
|
+
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
244
|
+
if (id) { // If something hasn't removed the id.
|
|
396
245
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
246
|
+
// Don't allow overwriting existing class properties if they already have a non-Node value.
|
|
247
|
+
if (root[id] && !(root[id] instanceof Node))
|
|
248
|
+
throw new Error(`${root.constructor.name}.${id} already has a value. ` +
|
|
249
|
+
`Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
|
|
401
250
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
shells: new WeakMap()
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
let Util = {
|
|
251
|
+
delve(root, id.split(/\./g), el);
|
|
252
|
+
}
|
|
253
|
+
},
|
|
409
254
|
|
|
255
|
+
/**
|
|
256
|
+
* @param style {HTMLStyleElement}
|
|
257
|
+
* @param root {HTMLElement} */
|
|
410
258
|
bindStyles(style, root) {
|
|
411
259
|
let styleId = root.getAttribute('data-style');
|
|
412
260
|
if (!styleId) {
|
|
@@ -419,17 +267,59 @@ let Util = {
|
|
|
419
267
|
root.setAttribute('data-style', styleId);
|
|
420
268
|
}
|
|
421
269
|
|
|
270
|
+
// Replace ":host" with "tagName[data-style=...]" in the css.
|
|
422
271
|
let tagName = root.tagName.toLowerCase();
|
|
423
272
|
for (let child of style.childNodes) {
|
|
424
273
|
if (child.nodeType === 3) {
|
|
425
274
|
let oldText = child.textContent;
|
|
426
|
-
let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, tagName
|
|
275
|
+
let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, `${tagName}[data-style="${styleId}"]`);
|
|
427
276
|
if (oldText !== newText)
|
|
428
277
|
child.textContent = newText;
|
|
429
278
|
}
|
|
430
279
|
}
|
|
431
280
|
},
|
|
432
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Convert a Proper Case name to a name with dashes.
|
|
284
|
+
* Dashes will be placed between letters and numbers.
|
|
285
|
+
* If there are multiple consecutive capital letters followed by another chracater, a dash will be placed before the last capital letter.
|
|
286
|
+
* @param str {string}
|
|
287
|
+
* @return {string}
|
|
288
|
+
*
|
|
289
|
+
* @example
|
|
290
|
+
* 'ProperName' => 'proper-name'
|
|
291
|
+
* 'HTMLElement' => 'html-element'
|
|
292
|
+
* 'BigUI' => 'big-ui'
|
|
293
|
+
* 'UIForm' => 'ui-form'
|
|
294
|
+
* 'A100' => 'a-100' */
|
|
295
|
+
camelToDashes(str) {
|
|
296
|
+
// Convert any capital letter that is preceded by a lowercase letter or number to lowercase and precede with a dash.
|
|
297
|
+
str = str.replace(/([a-z0-9])([A-Z])/g, '$1-$2');
|
|
298
|
+
|
|
299
|
+
// Convert any capital letter that is followed by a lowercase letter or number to lowercase and precede with a dash.
|
|
300
|
+
str = str.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2');
|
|
301
|
+
|
|
302
|
+
// Convert any number that is preceded by a lowercase or uppercase letter to be preceded by a dash.
|
|
303
|
+
str = str.replace(/([a-zA-Z])([0-9])/g, '$1-$2');
|
|
304
|
+
|
|
305
|
+
// Convert all the remaining capital letters to lowercase.
|
|
306
|
+
return str.toLowerCase();
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Converts a string written in kebab-case to camelCase.
|
|
311
|
+
*
|
|
312
|
+
* @param {string} str - The input string written in kebab-case.
|
|
313
|
+
* @return {string} - The resulting camelCase string.
|
|
314
|
+
*
|
|
315
|
+
* @example
|
|
316
|
+
* dashesToCamel('example-string') // Returns 'exampleString'
|
|
317
|
+
* dashesToCamel('another-example-test') // Returns 'anotherExampleTest' */
|
|
318
|
+
dashesToCamel(str) {
|
|
319
|
+
return str.replace(/-([a-z])/g, g => g[1].toUpperCase());
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
|
|
433
323
|
/**
|
|
434
324
|
* A generator function that recursively traverses and flattens a value.
|
|
435
325
|
*
|
|
@@ -456,23 +346,24 @@ let Util = {
|
|
|
456
346
|
* for (const item of flatten(complexArray))
|
|
457
347
|
* console.log(item); // Outputs: 1, 2, 3, 4, 5, 6, { a: 'object' }, 7, 8, 9
|
|
458
348
|
*/
|
|
459
|
-
*flatten(value) {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
},
|
|
349
|
+
// *flatten(value) {
|
|
350
|
+
// if (Array.isArray(value)) {
|
|
351
|
+
// for (const item of value) {
|
|
352
|
+
// yield* Util.flatten(item); // Recursively flatten arrays
|
|
353
|
+
// }
|
|
354
|
+
// } else if (typeof value === 'function') {
|
|
355
|
+
// const result = value();
|
|
356
|
+
// yield* Util.flatten(result); // Recursively flatten the result of a function
|
|
357
|
+
// } else
|
|
358
|
+
// yield value; // Yield primitive values as is
|
|
359
|
+
// },
|
|
470
360
|
|
|
471
361
|
/**
|
|
472
362
|
* Get the value of an input as the most appropriate JavaScript type.
|
|
473
363
|
* @param node {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|HTMLDivElement}
|
|
474
364
|
* @return {string|string[]|number|[]|File[]|Date|boolean} */
|
|
475
365
|
getInputValue(node) {
|
|
366
|
+
// .type is a built-in DOM property
|
|
476
367
|
if (node.type === 'checkbox' || node.type === 'radio')
|
|
477
368
|
return node.checked; // Boolean
|
|
478
369
|
if (node.type === 'file')
|
|
@@ -487,48 +378,84 @@ let Util = {
|
|
|
487
378
|
return node.value; // String
|
|
488
379
|
},
|
|
489
380
|
|
|
381
|
+
isEvent(attrName) {
|
|
382
|
+
return attrName.startsWith('on') && attrName in Globals$1.div;
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* @param el {HTMLElement}
|
|
387
|
+
* @param prop {string}
|
|
388
|
+
* @returns {boolean} */
|
|
389
|
+
isHtmlProp(el, prop) {
|
|
390
|
+
let key = el.tagName + '.' + prop;
|
|
391
|
+
let result = Globals$1.htmlProps[key];
|
|
392
|
+
if (result === undefined) { // Caching just barely makes this slightly faster.
|
|
393
|
+
let proto = Object.getPrototypeOf(el);
|
|
394
|
+
|
|
395
|
+
// Find the first HTMLElement that we inherit from (not our own classes)
|
|
396
|
+
while (proto) {
|
|
397
|
+
const ctorName = proto.constructor.name;
|
|
398
|
+
if (ctorName.startsWith('HTML') && ctorName.endsWith('Element'))
|
|
399
|
+
break
|
|
400
|
+
proto = Object.getPrototypeOf(proto);
|
|
401
|
+
}
|
|
402
|
+
Globals$1.htmlProps[key] = result = (proto
|
|
403
|
+
? !!Object.getOwnPropertyDescriptor(proto, prop)?.set
|
|
404
|
+
: false);
|
|
405
|
+
}
|
|
406
|
+
return result;
|
|
407
|
+
},
|
|
408
|
+
|
|
490
409
|
/**
|
|
491
410
|
* Is it an array and a path that can be evaluated by delve() ?
|
|
411
|
+
* We allow the first element to be null/undefined so binding can report errors.
|
|
492
412
|
* @param arr {Array|*}
|
|
493
413
|
* @returns {boolean} */
|
|
494
414
|
isPath(arr) {
|
|
495
|
-
return Array.isArray(arr) &&
|
|
415
|
+
return Array.isArray(arr) && arr.length >=2 // An array of at least two elements.
|
|
416
|
+
&& (typeof arr[0] === 'object' || arr[0] === undefined) // Where the first element is an object, null, or undefined.
|
|
417
|
+
&& !arr.slice(1).find(p => typeof p !== 'string' && typeof p !== 'number'); // Path 1..x is only numbers and strings.
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
isFalsy(val) {
|
|
421
|
+
return val === undefined || val === false || val === null;
|
|
422
|
+
},
|
|
423
|
+
|
|
424
|
+
/*
|
|
425
|
+
isPrimitive(val) {
|
|
426
|
+
return typeof val === 'string' || typeof val === 'number'
|
|
427
|
+
},*/
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* If val is a function, evaluate it recursively until the result is not a function.
|
|
431
|
+
* If it's an array or an object, convert it to Json.
|
|
432
|
+
* If it's a Date, format it as Y-m-d H:i:s
|
|
433
|
+
* @param val
|
|
434
|
+
* @returns {string|number|boolean} */
|
|
435
|
+
makePrimitive(val) {
|
|
436
|
+
if (typeof val === 'function')
|
|
437
|
+
return Util.makePrimitive(val());
|
|
438
|
+
else if (val instanceof Date)
|
|
439
|
+
return val.toISOString().replace(/T/, ' ');
|
|
440
|
+
else if (Array.isArray(val) || typeof val === 'object')
|
|
441
|
+
return ''; // JSON.stringify(val);
|
|
442
|
+
return val;
|
|
496
443
|
},
|
|
497
444
|
|
|
498
445
|
/**
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
oldNgMap.set(ng.startNode, ng);
|
|
510
|
-
|
|
511
|
-
// TODO: Is this necessary?
|
|
512
|
-
// if (ng.parentPath)
|
|
513
|
-
// ng.parentPath.clearNodesCache();
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
for (let i=0, node; node = oldNodes[i]; i++) {
|
|
517
|
-
let ng;
|
|
518
|
-
if (!node.parentNode && (ng = oldNgMap.get(node))) {
|
|
519
|
-
//ng.nodesCache = [];
|
|
520
|
-
let fragment = document.createDocumentFragment();
|
|
521
|
-
let endNode = ng.endNode;
|
|
522
|
-
while (node !== endNode) {
|
|
523
|
-
fragment.append(node);
|
|
524
|
-
//ng.nodesCache.push(node);
|
|
525
|
-
i++;
|
|
526
|
-
node = oldNodes[i];
|
|
527
|
-
}
|
|
528
|
-
fragment.append(endNode);
|
|
529
|
-
//ng.nodesCache.push(endNode);
|
|
530
|
-
}
|
|
446
|
+
* Use an array as the value of a map, appending to it when we add.
|
|
447
|
+
* Used by watch.js.
|
|
448
|
+
* @param map {Map|WeakMap|Object}
|
|
449
|
+
* @param key
|
|
450
|
+
* @param value */
|
|
451
|
+
mapArrayAdd(map, key, value) {
|
|
452
|
+
let result = map.get(key);
|
|
453
|
+
if (!result) {
|
|
454
|
+
result = [value];
|
|
455
|
+
map.set(key, result);
|
|
531
456
|
}
|
|
457
|
+
else
|
|
458
|
+
result.push(value);
|
|
532
459
|
},
|
|
533
460
|
|
|
534
461
|
/**
|
|
@@ -559,134 +486,6 @@ let Util = {
|
|
|
559
486
|
|
|
560
487
|
|
|
561
488
|
|
|
562
|
-
let div = document.createElement('div');
|
|
563
|
-
|
|
564
|
-
let isEvent = attrName => attrName.startsWith('on') && attrName in div;
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
/**
|
|
568
|
-
* Convert a Proper Case name to a name with dashes.
|
|
569
|
-
* Dashes will be placed between letters and numbers.
|
|
570
|
-
* If there are multiple consecutive capital letters followed by another chracater, a dash will be placed before the last capital letter.
|
|
571
|
-
* @param str {string}
|
|
572
|
-
* @return {string}
|
|
573
|
-
*
|
|
574
|
-
* @example
|
|
575
|
-
* 'ProperName' => 'proper-name'
|
|
576
|
-
* 'HTMLElement' => 'html-element'
|
|
577
|
-
* 'BigUI' => 'big-ui'
|
|
578
|
-
* 'UIForm' => 'ui-form'
|
|
579
|
-
* 'A100' => 'a-100' */
|
|
580
|
-
function camelToDashes(str) {
|
|
581
|
-
// Convert any capital letter that is preceded by a lowercase letter or number to lowercase and precede with a dash.
|
|
582
|
-
str = str.replace(/([a-z0-9])([A-Z])/g, '$1-$2');
|
|
583
|
-
|
|
584
|
-
// Convert any capital letter that is followed by a lowercase letter or number to lowercase and precede with a dash.
|
|
585
|
-
str = str.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2');
|
|
586
|
-
|
|
587
|
-
// Convert any number that is preceded by a lowercase or uppercase letter to be preceded by a dash.
|
|
588
|
-
str = str.replace(/([a-zA-Z])([0-9])/g, '$1-$2');
|
|
589
|
-
|
|
590
|
-
// Convert all the remaining capital letters to lowercase.
|
|
591
|
-
return str.toLowerCase();
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
/**
|
|
599
|
-
* Returns false if they're the same. Or the first index where they differ.
|
|
600
|
-
* @param a
|
|
601
|
-
* @param b
|
|
602
|
-
* @returns {boolean} */
|
|
603
|
-
function arraySame(a, b) {
|
|
604
|
-
let aLength = a.length;
|
|
605
|
-
if (aLength !== b.length)
|
|
606
|
-
return false;
|
|
607
|
-
for (let i=0; i<aLength; i++)
|
|
608
|
-
if (a[i] !== b[i])
|
|
609
|
-
return false;
|
|
610
|
-
return true; // the same.
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
/**
|
|
615
|
-
* TODO: Turn this into a class because it has internal state.
|
|
616
|
-
* TODO: Don't break on 3<a inside a <script> or <style> tag.
|
|
617
|
-
* @param html {?string} Pass null to reset context.
|
|
618
|
-
* @returns {string} */
|
|
619
|
-
function htmlContext(html) {
|
|
620
|
-
if (html === null) {
|
|
621
|
-
state = {...defaultState};
|
|
622
|
-
return state.context;
|
|
623
|
-
}
|
|
624
|
-
for (let i = 0; i < html.length; i++) {
|
|
625
|
-
const char = html[i];
|
|
626
|
-
switch (state.context) {
|
|
627
|
-
case htmlContext.Text:
|
|
628
|
-
if (char === '<' && html[i+1].match(/[a-z!]/i)) { // Start of a tag or comment.
|
|
629
|
-
// if (html.slice(i, i+4) === '<!--')
|
|
630
|
-
// state.context = htmlContext.Comment;
|
|
631
|
-
// else
|
|
632
|
-
state.context = htmlContext.Tag;
|
|
633
|
-
state.buffer = '';
|
|
634
|
-
}
|
|
635
|
-
break;
|
|
636
|
-
case htmlContext.Tag:
|
|
637
|
-
if (char === '>') {
|
|
638
|
-
state.context = htmlContext.Text;
|
|
639
|
-
state.quote = null;
|
|
640
|
-
state.buffer = '';
|
|
641
|
-
} else if (char === ' ' && !state.buffer) {
|
|
642
|
-
// No attribute name is present. Skipping the space.
|
|
643
|
-
continue;
|
|
644
|
-
} else if (char === ' ' || char === '/' || char === '?') {
|
|
645
|
-
state.buffer = ''; // Reset the buffer when a delimiter or potential self-closing sign is found.
|
|
646
|
-
} else if (char === '"' || char === "'" || char === '=') {
|
|
647
|
-
state.context = htmlContext.Attribute;
|
|
648
|
-
state.quote = char === '=' ? null : char;
|
|
649
|
-
state.buffer = '';
|
|
650
|
-
} else {
|
|
651
|
-
state.buffer += char;
|
|
652
|
-
}
|
|
653
|
-
break;
|
|
654
|
-
case htmlContext.Attribute:
|
|
655
|
-
if (!state.quote && !state.buffer.length && (char === '"' || char === "'"))
|
|
656
|
-
state.quote = char;
|
|
657
|
-
|
|
658
|
-
else if (char === state.quote || (!state.quote && state.buffer.length)) {
|
|
659
|
-
state.context = htmlContext.Tag;
|
|
660
|
-
state.quote = null;
|
|
661
|
-
state.buffer = '';
|
|
662
|
-
} else if (!state.quote && char === '>') {
|
|
663
|
-
state.context = htmlContext.Text;
|
|
664
|
-
state.quote = null;
|
|
665
|
-
state.buffer = '';
|
|
666
|
-
} else if (char !== ' ') {
|
|
667
|
-
state.buffer += char;
|
|
668
|
-
}
|
|
669
|
-
break;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
}
|
|
673
|
-
return state.context;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
htmlContext.Attribute = 'Attribute';
|
|
678
|
-
htmlContext.Text = 'Text';
|
|
679
|
-
htmlContext.Tag = 'Tag';
|
|
680
|
-
//htmlContext.Comment = 'Comment';
|
|
681
|
-
let defaultState = {
|
|
682
|
-
context: htmlContext.Text, // possible values: 'TEXT', 'TAG', 'ATTRIBUTE'
|
|
683
|
-
quote: null, // possible values: null, '"', "'"
|
|
684
|
-
buffer: '',
|
|
685
|
-
lastChar: null
|
|
686
|
-
};
|
|
687
|
-
let state = {...defaultState};
|
|
688
|
-
|
|
689
|
-
|
|
690
489
|
// For debugging only
|
|
691
490
|
//#IFDEV
|
|
692
491
|
function setIndent(items, level=1) {
|
|
@@ -751,7 +550,7 @@ function flattenAndIndent(inputArray, indent = "") {
|
|
|
751
550
|
|
|
752
551
|
class MultiValueMap {
|
|
753
552
|
|
|
754
|
-
/** @type {
|
|
553
|
+
/** @type {Record<string, Set>} */
|
|
755
554
|
data = {};
|
|
756
555
|
|
|
757
556
|
// Set a new value for a key
|
|
@@ -785,23 +584,14 @@ class MultiValueMap {
|
|
|
785
584
|
* @param val If specified, make sure we delete this specific value, if a key exists more than once.
|
|
786
585
|
* @returns {*|undefined} The deleted item. */
|
|
787
586
|
delete(key, val=undefined) {
|
|
788
|
-
// if (key === '["Html2",[[["Html3",["F1","A"]],["Html3",["F1","B"]]]]]')
|
|
789
|
-
// debugger;
|
|
790
|
-
|
|
791
587
|
let data = this.data;
|
|
792
|
-
|
|
793
|
-
// if (!data.hasOwnProperty(key))
|
|
794
|
-
// return undefined;
|
|
795
|
-
|
|
796
|
-
// Delete a specific value.
|
|
797
588
|
let result;
|
|
798
589
|
let set = data[key];
|
|
799
|
-
if (!set)
|
|
590
|
+
if (!set)
|
|
800
591
|
return undefined;
|
|
801
592
|
|
|
802
593
|
// Delete any value.
|
|
803
594
|
if (val === undefined) {
|
|
804
|
-
//result = set.values().next().value; // get first item from set.
|
|
805
595
|
[result] = set; // Does the same as above and seems to be about the same speed.
|
|
806
596
|
set.delete(result);
|
|
807
597
|
}
|
|
@@ -812,7 +602,6 @@ class MultiValueMap {
|
|
|
812
602
|
result = val;
|
|
813
603
|
}
|
|
814
604
|
|
|
815
|
-
// TODO: Will this make it slower?
|
|
816
605
|
if (set.size === 0)
|
|
817
606
|
delete data[key];
|
|
818
607
|
|
|
@@ -820,28 +609,34 @@ class MultiValueMap {
|
|
|
820
609
|
}
|
|
821
610
|
|
|
822
611
|
/**
|
|
823
|
-
*
|
|
824
|
-
* if not the latter, just delete any item that matches the key.
|
|
612
|
+
* Remove one value from a key, and return it.
|
|
825
613
|
* @param key {string}
|
|
826
|
-
* @param isPreferred {function}
|
|
827
614
|
* @returns {*|undefined} The deleted item. */
|
|
828
|
-
|
|
615
|
+
deleteAny(key) {
|
|
616
|
+
let data = this.data;
|
|
829
617
|
let result;
|
|
618
|
+
let set = data[key];
|
|
619
|
+
if (!set) // slower than pre-check.
|
|
620
|
+
return undefined;
|
|
621
|
+
|
|
622
|
+
[result] = set; // Does the same as above and seems to be about the same speed.
|
|
623
|
+
set.delete(result);
|
|
624
|
+
|
|
625
|
+
if (set.size === 0)
|
|
626
|
+
delete data[key];
|
|
627
|
+
|
|
628
|
+
return result;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
deleteSpecific(key, val) {
|
|
830
632
|
let data = this.data;
|
|
633
|
+
let result;
|
|
831
634
|
let set = data[key];
|
|
832
635
|
if (!set)
|
|
833
636
|
return undefined;
|
|
834
637
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
set.delete(val);
|
|
838
|
-
result = val;
|
|
839
|
-
break;
|
|
840
|
-
}
|
|
841
|
-
if (!result) {
|
|
842
|
-
[result] = set;
|
|
843
|
-
set.delete(result);
|
|
844
|
-
}
|
|
638
|
+
set.delete(val);
|
|
639
|
+
result = val;
|
|
845
640
|
|
|
846
641
|
if (set.size === 0)
|
|
847
642
|
delete data[key];
|
|
@@ -1089,14 +884,23 @@ const udomdiff = (parentNode, a, b, before) => {
|
|
|
1089
884
|
return b;
|
|
1090
885
|
};
|
|
1091
886
|
|
|
887
|
+
//import {ArraySpliceOp} from "./watch.js";
|
|
888
|
+
//#IFDEV
|
|
889
|
+
var exprPathId = 0;
|
|
890
|
+
//#ENDIF
|
|
891
|
+
|
|
1092
892
|
/**
|
|
1093
893
|
* Path to where an expression should be evaluated within a Shell or NodeGroup.
|
|
1094
894
|
* Path is only valid until the expressions before it are evaluated.
|
|
1095
895
|
* TODO: Make this based on parent and node instead of path? */
|
|
1096
896
|
class ExprPath {
|
|
1097
897
|
|
|
898
|
+
//#IFDEV
|
|
899
|
+
eid = exprPathId++;
|
|
900
|
+
//#ENDIF
|
|
901
|
+
|
|
1098
902
|
/**
|
|
1099
|
-
* @type {
|
|
903
|
+
* @type {ExprPathType} */
|
|
1100
904
|
type;
|
|
1101
905
|
|
|
1102
906
|
// Used for attributes:
|
|
@@ -1112,8 +916,6 @@ class ExprPath {
|
|
|
1112
916
|
* @type {Set<string>} Used for type=AttribType.Multiple to remember the attributes that were added. */
|
|
1113
917
|
attrNames;
|
|
1114
918
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
919
|
/**
|
|
1118
920
|
* @type {Node} Node that occurs before this ExprPath's first Node.
|
|
1119
921
|
* This is necessary because udomdiff() can steal nodes from another ExprPath.
|
|
@@ -1155,16 +957,23 @@ class ExprPath {
|
|
|
1155
957
|
nodeMarkerPath;
|
|
1156
958
|
|
|
1157
959
|
|
|
1158
|
-
/** @type {?function} */
|
|
960
|
+
/** @type {?function} A function called by renderWatched() to update the value of this expression. */
|
|
1159
961
|
watchFunction
|
|
1160
962
|
|
|
963
|
+
/**
|
|
964
|
+
* @type {?function} The most recent callback passed to a .map() function in this ExprPath.
|
|
965
|
+
* TODO: What if one ExprPath has two .map() calls? Maybe we just won't support that. */
|
|
966
|
+
mapCallback
|
|
967
|
+
|
|
968
|
+
isHtmlProperty = undefined;
|
|
969
|
+
|
|
1161
970
|
/**
|
|
1162
971
|
* @param nodeBefore {Node}
|
|
1163
972
|
* @param nodeMarker {?Node}
|
|
1164
|
-
* @param type {
|
|
973
|
+
* @param type {ExprPathType}
|
|
1165
974
|
* @param attrName {?string}
|
|
1166
975
|
* @param attrValue {string[]} */
|
|
1167
|
-
constructor(nodeBefore, nodeMarker, type=
|
|
976
|
+
constructor(nodeBefore, nodeMarker, type=ExprPathType.Content, attrName=null, attrValue=null) {
|
|
1168
977
|
|
|
1169
978
|
// If path is a node.
|
|
1170
979
|
this.nodeBefore = nodeBefore;
|
|
@@ -1172,7 +981,7 @@ class ExprPath {
|
|
|
1172
981
|
this.type = type;
|
|
1173
982
|
this.attrName = attrName;
|
|
1174
983
|
this.attrValue = attrValue;
|
|
1175
|
-
if (type ===
|
|
984
|
+
if (type === ExprPathType.AttribMultiple)
|
|
1176
985
|
this.attrNames = new Set();
|
|
1177
986
|
}
|
|
1178
987
|
|
|
@@ -1186,36 +995,27 @@ class ExprPath {
|
|
|
1186
995
|
* We should modify path.applyValueAttrib so it stores the procssed parts and then only calls
|
|
1187
996
|
* setAttribute() once all the pieces are in place.
|
|
1188
997
|
*
|
|
1189
|
-
* @param expr {Expr}
|
|
1190
998
|
* @param exprs {Expr[]}
|
|
1191
|
-
* @param
|
|
1192
|
-
|
|
1193
|
-
* @returns {int} */
|
|
1194
|
-
apply(expr, exprs=null, exprIndex=0, componentExprs={}) {
|
|
999
|
+
* @param freeNodeGroups {boolean} */
|
|
1000
|
+
apply(exprs, freeNodeGroups=true) {
|
|
1195
1001
|
switch (this.type) {
|
|
1196
1002
|
case 1: // PathType.Content:
|
|
1197
|
-
this.applyNodes(
|
|
1003
|
+
this.applyNodes(exprs[0], freeNodeGroups);
|
|
1198
1004
|
break;
|
|
1199
1005
|
case 2: // PathType.Multiple:
|
|
1200
|
-
this.applyMultipleAttribs(this.nodeMarker,
|
|
1006
|
+
this.applyMultipleAttribs(this.nodeMarker, exprs[0]);
|
|
1201
1007
|
break;
|
|
1202
1008
|
case 5: // PathType.Comment:
|
|
1203
1009
|
// Expressions inside Html comments. Deliberately empty because we won't waste time updating them.
|
|
1204
1010
|
break;
|
|
1205
1011
|
case 6: // PathType.Event:
|
|
1206
|
-
this.applyEventAttrib(this.nodeMarker,
|
|
1012
|
+
this.applyEventAttrib(this.nodeMarker, exprs[0], this.parentNg.rootNg.root);
|
|
1207
1013
|
break;
|
|
1208
|
-
default:
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
else {
|
|
1212
|
-
// One attribute value may have multiple expressions. Here we apply them all at once.
|
|
1213
|
-
exprIndex = this.applyValueAttrib(this.nodeMarker, exprs || [expr], exprIndex);
|
|
1214
|
-
}
|
|
1014
|
+
default: // TODO: Is this still used? Lots of tests fail without it.
|
|
1015
|
+
// One attribute value may have multiple expressions. Here we apply them all at once.
|
|
1016
|
+
this.applyValueAttrib(this.nodeMarker, exprs);
|
|
1215
1017
|
break;
|
|
1216
1018
|
}
|
|
1217
|
-
|
|
1218
|
-
return exprIndex;
|
|
1219
1019
|
}
|
|
1220
1020
|
|
|
1221
1021
|
/**
|
|
@@ -1223,14 +1023,16 @@ class ExprPath {
|
|
|
1223
1023
|
* Called by applyExprs()
|
|
1224
1024
|
* This function is recursive, as the functions it calls also call it.
|
|
1225
1025
|
* @param expr {Expr}
|
|
1026
|
+
* @param freeNodeGroups {boolean}
|
|
1226
1027
|
* @return {Node[]} New Nodes created. */
|
|
1227
|
-
applyNodes(expr) {
|
|
1028
|
+
applyNodes(expr, freeNodeGroups=true) {
|
|
1228
1029
|
let path = this;
|
|
1229
1030
|
|
|
1230
1031
|
// This can be done at the beginning or the end of this function.
|
|
1231
1032
|
// If at the end, we may get rendering done faster.
|
|
1232
1033
|
// But when at the beginning, it leaves all the nodes in-use so we can do a renderWatched().
|
|
1233
|
-
|
|
1034
|
+
if (freeNodeGroups)
|
|
1035
|
+
path.freeNodeGroups();
|
|
1234
1036
|
|
|
1235
1037
|
/*#IFDEV*/path.verify();/*#ENDIF*/
|
|
1236
1038
|
|
|
@@ -1240,10 +1042,10 @@ class ExprPath {
|
|
|
1240
1042
|
/*#IFDEV*/assert(!oldNodeGroups.includes(null));/*#ENDIF*/
|
|
1241
1043
|
let secondPass = []; // indices
|
|
1242
1044
|
|
|
1243
|
-
path.nodeGroups = []; // Reset before
|
|
1244
|
-
path.
|
|
1045
|
+
path.nodeGroups = []; // Reset before applyExactNodes and the code below rebuilds it.
|
|
1046
|
+
path.applyExactNodes(expr, newNodes, secondPass);
|
|
1245
1047
|
|
|
1246
|
-
this.existingTextNodes = null;
|
|
1048
|
+
//this.existingTextNodes = null;
|
|
1247
1049
|
|
|
1248
1050
|
// TODO: Create an array of old vs Nodes and NodeGroups together.
|
|
1249
1051
|
// If they're all the same, skip the next steps.
|
|
@@ -1281,7 +1083,7 @@ class ExprPath {
|
|
|
1281
1083
|
|
|
1282
1084
|
|
|
1283
1085
|
// This pre-check makes it a few percent faster?
|
|
1284
|
-
let same = arraySame(oldNodes, newNodes);
|
|
1086
|
+
let same = Util.arraySame(oldNodes, newNodes);
|
|
1285
1087
|
if (!same) {
|
|
1286
1088
|
|
|
1287
1089
|
path.nodesCache = newNodes; // Replaces value set by path.getNodes()
|
|
@@ -1302,7 +1104,21 @@ class ExprPath {
|
|
|
1302
1104
|
|
|
1303
1105
|
for (let ng of oldNodeGroups)
|
|
1304
1106
|
if (!ng.startNode.parentNode)
|
|
1305
|
-
ng.
|
|
1107
|
+
ng.removeAndSaveOrphans();
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
// Instantiate components created within ${...} expressions.
|
|
1113
|
+
// Embedded style tags are handled elsewhere, but where?
|
|
1114
|
+
for (let el of newNodes) {
|
|
1115
|
+
if (el instanceof HTMLElement) {
|
|
1116
|
+
if (el.hasAttribute('solarite-placeholder'))
|
|
1117
|
+
this.parentNg.instantiateComponent(el);
|
|
1118
|
+
for (let child of el.querySelectorAll('[solarite-placeholder]'))
|
|
1119
|
+
this.parentNg.instantiateComponent(child);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1306
1122
|
}
|
|
1307
1123
|
|
|
1308
1124
|
|
|
@@ -1310,49 +1126,145 @@ class ExprPath {
|
|
|
1310
1126
|
}
|
|
1311
1127
|
|
|
1312
1128
|
/**
|
|
1313
|
-
* Used by watch() for replacing individual loop items.
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
let oldNg = this.nodeGroups[index];
|
|
1317
|
-
this.nodeGroupsFree.add(oldNg.exactKey, oldNg);
|
|
1318
|
-
this.nodeGroupsFree.add(oldNg.closeKey, oldNg);
|
|
1129
|
+
* Used by watch() for inserting/removing/replacing individual loop items.
|
|
1130
|
+
* @param op {ArraySpliceOp} */
|
|
1131
|
+
applyArrayOp(op) {
|
|
1319
1132
|
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1133
|
+
// Replace NodeGroups
|
|
1134
|
+
let replaceCount = Math.min(op.deleteCount, op.items.length);
|
|
1135
|
+
let deleteCount = op.deleteCount - replaceCount;
|
|
1136
|
+
for (let i=0; i<replaceCount; i++) {
|
|
1137
|
+
let oldNg = this.nodeGroups[op.index + i]; // TODO: One expr can create multiple nodegroups.
|
|
1324
1138
|
|
|
1139
|
+
// Try to find an exact match
|
|
1140
|
+
let func = this.mapCallback || this.watchFunction;
|
|
1141
|
+
let expr = func(op.items[i]);
|
|
1325
1142
|
|
|
1143
|
+
// If the result of func isn't a template, conver it to one or more templates.
|
|
1144
|
+
this.exprToTemplates(expr, template => { // TODO: An expr can create multiple NodeGroups. I need a way to group them.
|
|
1326
1145
|
|
|
1327
|
-
|
|
1146
|
+
let ng = this.getNodeGroup(template, true); // Removes from nodeGroupsAttached and adds to nodeGroupsRendered()
|
|
1147
|
+
if (ng && ng === oldNg) ; else {
|
|
1328
1148
|
|
|
1329
|
-
|
|
1149
|
+
// Find a close match or create a new node group
|
|
1150
|
+
if (!ng)
|
|
1151
|
+
ng = this.getNodeGroup(template, false); // adds back to nodeGroupsRendered()
|
|
1152
|
+
this.nodeGroups[op.index + i] = ng; // TODO: Remove old one to nodeGroupsDetached?
|
|
1330
1153
|
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1154
|
+
// Splice in the new nodes.
|
|
1155
|
+
let insertBefore = oldNg.startNode;
|
|
1156
|
+
for (let node of ng.getNodes())
|
|
1157
|
+
insertBefore.parentNode.insertBefore(node, insertBefore);
|
|
1158
|
+
|
|
1159
|
+
// Remove the old nodes.
|
|
1160
|
+
if (ng !== oldNg)
|
|
1161
|
+
oldNg.removeAndSaveOrphans();
|
|
1162
|
+
}
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// Delete extra at the end.
|
|
1167
|
+
if (deleteCount > 0) {
|
|
1168
|
+
for (let i=0; i<deleteCount; i++) {
|
|
1169
|
+
let oldNg = this.nodeGroups[op.index + replaceCount + i];
|
|
1170
|
+
oldNg.removeAndSaveOrphans();
|
|
1171
|
+
}
|
|
1172
|
+
this.nodeGroups.splice(op.index + replaceCount, deleteCount);
|
|
1334
1173
|
}
|
|
1335
1174
|
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1175
|
+
// Add extra at the end.
|
|
1176
|
+
else {
|
|
1177
|
+
let newItems = op.items.slice(replaceCount);
|
|
1178
|
+
|
|
1179
|
+
let insertBefore = this.nodeGroups[op.index + replaceCount]?.startNode || this.nodeMarker;
|
|
1180
|
+
for (let i = 0; i < newItems.length; i++) { // We use nodeMarker if the subequent (or all) nodeGroups have been removed.
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
// Try to find exact match
|
|
1184
|
+
let template = this.mapCallback(newItems[i]);
|
|
1185
|
+
let ng = this.getNodeGroup(template, true); // Removes from nodeGroupsAttached and adds to nodeGroupsRendered()
|
|
1186
|
+
if (!ng) // Find a close match or create a new node group
|
|
1187
|
+
ng = this.getNodeGroup(template, false); // adds back to nodeGroupsRendered()
|
|
1188
|
+
|
|
1189
|
+
this.nodeGroups.push(ng);
|
|
1190
|
+
|
|
1191
|
+
// Splice in the new nodes.
|
|
1192
|
+
for (let node of ng.getNodes())
|
|
1193
|
+
insertBefore.parentNode.insertBefore(node, insertBefore);
|
|
1194
|
+
}
|
|
1340
1195
|
}
|
|
1341
1196
|
|
|
1197
|
+
//#IFDEV
|
|
1198
|
+
assert(this.nodeGroups.length === op.array.length);
|
|
1199
|
+
//#ENDIF
|
|
1200
|
+
|
|
1342
1201
|
// TODO: update or invalidate the nodes cache?
|
|
1343
1202
|
this.nodesCache = null;
|
|
1344
1203
|
}
|
|
1345
1204
|
|
|
1205
|
+
/**
|
|
1206
|
+
* Recursively traverse expr.
|
|
1207
|
+
* If a value is a function, evaluate it.
|
|
1208
|
+
* If a value is an array, recurse on each item.
|
|
1209
|
+
* If it's a primitive, convert it to a Template.
|
|
1210
|
+
* Otherwise pass the item (which is now either a Template or a Node) to callback.
|
|
1211
|
+
* @param expr
|
|
1212
|
+
* @param callback {function(Node|Template)}
|
|
1213
|
+
*
|
|
1214
|
+
* TODO: have applyExactNodes() use this function. */
|
|
1215
|
+
exprToTemplates(expr, callback) {
|
|
1216
|
+
if (Array.isArray(expr))
|
|
1217
|
+
for (let subExpr of expr)
|
|
1218
|
+
this.exprToTemplates(subExpr, callback);
|
|
1219
|
+
|
|
1220
|
+
else if (typeof expr === 'function') {
|
|
1221
|
+
// TODO: One ExprPath can have multiple expr functions.
|
|
1222
|
+
// But if using it as a watch, it should only have one at the top level.
|
|
1223
|
+
// So maybe this is ok.
|
|
1224
|
+
Globals$1.currentExprPath = this; // Used by watch()
|
|
1225
|
+
|
|
1226
|
+
this.watchFunction = expr; // TODO: Only do this if it's a top level function.
|
|
1227
|
+
expr = expr(); // As expr accesses watched variables, watch() uses Globals.currentExprPath to mark where those watched variables are being used.
|
|
1228
|
+
Globals$1.currentExprPath = null;
|
|
1229
|
+
|
|
1230
|
+
this.exprToTemplates(expr, callback);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// String/Number/Date/Boolean
|
|
1234
|
+
else if (!(expr instanceof Template) && !(expr instanceof Node)){
|
|
1235
|
+
// Convert expression to a string.
|
|
1236
|
+
if (expr === undefined || expr === false || expr === null) // Util.isFalsy() inlined
|
|
1237
|
+
expr = '';
|
|
1238
|
+
else if (typeof expr !== 'string')
|
|
1239
|
+
expr += '';
|
|
1240
|
+
|
|
1241
|
+
// Get the same Template for the same string each time.
|
|
1242
|
+
// let template = Globals.stringTemplates[expr];
|
|
1243
|
+
// if (!template) {
|
|
1244
|
+
let template = new Template([expr], []);
|
|
1245
|
+
// Globals.stringTemplates[expr] = template;
|
|
1246
|
+
//}
|
|
1247
|
+
|
|
1248
|
+
// Recurse.
|
|
1249
|
+
this.exprToTemplates(template, callback);
|
|
1250
|
+
}
|
|
1251
|
+
else
|
|
1252
|
+
callback(expr);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1346
1255
|
|
|
1347
1256
|
/**
|
|
1348
|
-
*
|
|
1257
|
+
* Try to apply Nodes that are an exact match, by finding existing nodes from the last render
|
|
1258
|
+
* that have the same value as created by the expr.
|
|
1259
|
+
* This is called from ExprPath.applyNodes().
|
|
1260
|
+
*
|
|
1349
1261
|
* @param expr {Template|Node|Array|function|*}
|
|
1350
|
-
* @param newNodes {(Node|Template)[]}
|
|
1351
|
-
* @param secondPass {
|
|
1352
|
-
|
|
1262
|
+
* @param newNodes {(Node|Template)[]} An inout parameter; we add the nodes here as we go.
|
|
1263
|
+
* @param secondPass {[int, int][]} Locations within newNodes for ExprPath.applyNodes() to evaluate later,
|
|
1264
|
+
* when it tries to find partial matches. */
|
|
1265
|
+
applyExactNodes(expr, newNodes, secondPass) {
|
|
1353
1266
|
|
|
1354
1267
|
if (expr instanceof Template) {
|
|
1355
|
-
|
|
1356
1268
|
let ng = this.getNodeGroup(expr, true);
|
|
1357
1269
|
if (ng) {
|
|
1358
1270
|
|
|
@@ -1370,7 +1282,7 @@ class ExprPath {
|
|
|
1370
1282
|
}
|
|
1371
1283
|
}
|
|
1372
1284
|
|
|
1373
|
-
// Node created by an expression.
|
|
1285
|
+
// Node(s) created by an expression.
|
|
1374
1286
|
else if (expr instanceof Node) {
|
|
1375
1287
|
|
|
1376
1288
|
// DocumentFragment created by an expression.
|
|
@@ -1383,53 +1295,14 @@ class ExprPath {
|
|
|
1383
1295
|
// Arrays and functions.
|
|
1384
1296
|
// I tried iterating over the result of a generator function to avoid this recursion and simplify the code,
|
|
1385
1297
|
// but that consistently made the js-framework-benchmarks a few percentage points slower.
|
|
1386
|
-
else
|
|
1387
|
-
|
|
1388
|
-
this.
|
|
1389
|
-
|
|
1390
|
-
else if (typeof expr === 'function') {
|
|
1391
|
-
// TODO: One ExprPath can have multiple expr functions.
|
|
1392
|
-
// But if using it as a watch, it should only have one at the top level.
|
|
1393
|
-
// So maybe this is ok.
|
|
1394
|
-
Globals.currentExprPath = [this, expr]; // Used by watch3()
|
|
1395
|
-
this.watchFunction = expr; // TODO: Only do this if it's a top level function.
|
|
1396
|
-
let result = expr();
|
|
1397
|
-
Globals.currentExprPath = null;
|
|
1398
|
-
|
|
1399
|
-
this.applyExact(result, newNodes, secondPass);
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
// Text
|
|
1403
|
-
else {
|
|
1404
|
-
// Convert falsy values (but not 0) to empty string.
|
|
1405
|
-
// Convert numbers to string so they compare the same.
|
|
1406
|
-
let text = (expr === undefined || expr === false || expr === null) ? '' : (expr + '');
|
|
1407
|
-
|
|
1408
|
-
// Fast path for updating the text of a single text node.
|
|
1409
|
-
let first = this.nodeBefore.nextSibling;
|
|
1410
|
-
if (first.nodeType === 3 && first.nextSibling === this.nodeMarker && !newNodes.includes(first)) {
|
|
1411
|
-
if (first.textContent !== text)
|
|
1412
|
-
first.textContent = text;
|
|
1413
|
-
|
|
1414
|
-
newNodes.push(first);
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
else {
|
|
1418
|
-
// TODO: Optimize this into a Set or Map or something?
|
|
1419
|
-
if (!this.existingTextNodes)
|
|
1420
|
-
this.existingTextNodes = this.getNodes().filter(n => n.nodeType === 3);
|
|
1421
|
-
|
|
1422
|
-
let idx = this.existingTextNodes.findIndex(n => n.textContent === text);
|
|
1423
|
-
if (idx !== -1)
|
|
1424
|
-
newNodes.push(...this.existingTextNodes.splice(idx, 1));
|
|
1425
|
-
else
|
|
1426
|
-
newNodes.push(this.nodeMarker.ownerDocument.createTextNode(text));
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1298
|
+
else
|
|
1299
|
+
this.exprToTemplates(expr, template => {
|
|
1300
|
+
this.applyExactNodes(template, newNodes, secondPass);
|
|
1301
|
+
});
|
|
1429
1302
|
}
|
|
1430
1303
|
|
|
1431
1304
|
applyMultipleAttribs(node, expr) {
|
|
1432
|
-
/*#IFDEV*/assert(this.type ===
|
|
1305
|
+
/*#IFDEV*/assert(this.type === ExprPathType.AttribMultiple);/*#ENDIF*/
|
|
1433
1306
|
|
|
1434
1307
|
if (Array.isArray(expr))
|
|
1435
1308
|
expr = expr.flat().join(' '); // flat and join so we can accept arrays of arrays of strings.
|
|
@@ -1438,6 +1311,13 @@ class ExprPath {
|
|
|
1438
1311
|
let oldNames = this.attrNames;
|
|
1439
1312
|
this.attrNames = new Set();
|
|
1440
1313
|
if (expr) {
|
|
1314
|
+
if (typeof expr === 'function') {
|
|
1315
|
+
Globals$1.currentExprPath = this; // Used by watch()
|
|
1316
|
+
this.watchFunction = expr; // used by renderWatched()
|
|
1317
|
+
expr = expr();
|
|
1318
|
+
Globals$1.currentExprPath = null;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1441
1321
|
let attrs = (expr +'') // Split string into multiple attributes.
|
|
1442
1322
|
.split(/([\w-]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s]+))/g)
|
|
1443
1323
|
.map(text => text.trim())
|
|
@@ -1468,52 +1348,57 @@ class ExprPath {
|
|
|
1468
1348
|
* @param root */
|
|
1469
1349
|
applyEventAttrib(node, expr, root) {
|
|
1470
1350
|
/*#IFDEV*/
|
|
1471
|
-
assert(this.type ===
|
|
1351
|
+
assert(this.type === ExprPathType.Event/* || this.type === PathType.Component*/);
|
|
1472
1352
|
assert(root instanceof HTMLElement);
|
|
1473
1353
|
/*#ENDIF*/
|
|
1474
1354
|
|
|
1475
1355
|
let eventName = this.attrName.slice(2); // remove "on-" prefix.
|
|
1476
1356
|
let func;
|
|
1477
|
-
|
|
1478
|
-
// Convert array to function.
|
|
1479
1357
|
let args = [];
|
|
1480
|
-
if (Array.isArray(expr)) {
|
|
1481
|
-
|
|
1482
|
-
// oninput=${[this.doSomething, 'meow']}
|
|
1483
|
-
if (typeof expr[0] === 'function') {
|
|
1484
|
-
func = expr[0];
|
|
1485
|
-
args = expr.slice(1);
|
|
1486
|
-
}
|
|
1487
1358
|
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
// root.render(); // TODO: This causes infinite recursion.
|
|
1494
|
-
}
|
|
1359
|
+
// Convert array to function.
|
|
1360
|
+
// oninput=${[this.doSomething, 'meow']}
|
|
1361
|
+
if (Array.isArray(expr) && typeof expr[0] === 'function') {
|
|
1362
|
+
func = expr[0];
|
|
1363
|
+
args = expr.slice(1);
|
|
1495
1364
|
}
|
|
1496
|
-
else
|
|
1365
|
+
else if (typeof expr === 'function')
|
|
1497
1366
|
func = expr;
|
|
1367
|
+
else
|
|
1368
|
+
throw new Error(`Invalid event binding: <${node.tagName.toLowerCase()} ${this.attrName}=\${${JSON.stringify(expr)}}>`);
|
|
1498
1369
|
|
|
1499
1370
|
this.bindEvent(node, root, eventName, eventName, func, args);
|
|
1500
1371
|
}
|
|
1501
1372
|
|
|
1502
1373
|
|
|
1374
|
+
/**
|
|
1375
|
+
* Call function when eventName is triggerd on node.
|
|
1376
|
+
* @param node {HTMLElement}
|
|
1377
|
+
* @param root {HTMLElement}
|
|
1378
|
+
* @param key {string}
|
|
1379
|
+
* @param eventName {string}
|
|
1380
|
+
* @param func {function}
|
|
1381
|
+
* @param args {array}
|
|
1382
|
+
* @param capture {boolean} */
|
|
1503
1383
|
bindEvent(node, root, key, eventName, func, args, capture=false) {
|
|
1504
|
-
let nodeEvents = Globals.nodeEvents.get(node);
|
|
1384
|
+
let nodeEvents = Globals$1.nodeEvents.get(node);
|
|
1505
1385
|
if (!nodeEvents) {
|
|
1506
1386
|
nodeEvents = {[key]: new Array(3)};
|
|
1507
|
-
Globals.nodeEvents.set(node, nodeEvents);
|
|
1387
|
+
Globals$1.nodeEvents.set(node, nodeEvents);
|
|
1508
1388
|
}
|
|
1509
1389
|
let nodeEvent = nodeEvents[key];
|
|
1510
1390
|
if (!nodeEvent)
|
|
1511
1391
|
nodeEvents[key] = nodeEvent = new Array(3);
|
|
1512
1392
|
|
|
1393
|
+
if (typeof func !== 'function')
|
|
1394
|
+
throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${${func}}> because it's not a function.`);
|
|
1513
1395
|
|
|
1514
1396
|
// If function has changed, remove and rebind the event.
|
|
1515
1397
|
if (nodeEvent[0] !== func) {
|
|
1516
1398
|
|
|
1399
|
+
// TODO: We should be removing event listeners when calling getNodeGroup(),
|
|
1400
|
+
// when we get the node from the list of nodeGroupsAttached/nodeGroupsDetached,
|
|
1401
|
+
// instead of only when we rebind an event.
|
|
1517
1402
|
let [existing, existingBound, _] = nodeEvent;
|
|
1518
1403
|
if (existing)
|
|
1519
1404
|
node.removeEventListener(eventName, existingBound, capture);
|
|
@@ -1543,68 +1428,144 @@ class ExprPath {
|
|
|
1543
1428
|
nodeEvents[key][2] = args;
|
|
1544
1429
|
}
|
|
1545
1430
|
|
|
1546
|
-
|
|
1547
|
-
|
|
1431
|
+
/**
|
|
1432
|
+
* Handle values, including two-way binding.
|
|
1433
|
+
* @param node
|
|
1434
|
+
* @param exprs */
|
|
1435
|
+
// TODO: node is always this.nodeMarker?
|
|
1436
|
+
applyValueAttrib(node, exprs) {
|
|
1437
|
+
let expr = exprs[0];
|
|
1548
1438
|
|
|
1549
|
-
//
|
|
1550
|
-
|
|
1551
|
-
|
|
1439
|
+
// Two-way binding between attributes
|
|
1440
|
+
// Passing a path to the value attribute.
|
|
1441
|
+
// Copies the attribute to the property when the input event fires.
|
|
1442
|
+
// value=${[this, 'value]'}
|
|
1443
|
+
// checked=${[this, 'isAgree']}
|
|
1444
|
+
// This same logic is in NodeGroup.instantiateComponent() for components.
|
|
1445
|
+
if (Util.isPath(expr)) {
|
|
1446
|
+
let [obj, path] = [expr[0], expr.slice(1)];
|
|
1552
1447
|
|
|
1553
|
-
|
|
1554
|
-
|
|
1448
|
+
if (!obj)
|
|
1449
|
+
throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${[${expr.map(item => item ? `'${item}'` : item+'').join(', ')}]}>.`);
|
|
1450
|
+
|
|
1451
|
+
let value = delve(obj, path);
|
|
1452
|
+
|
|
1453
|
+
// Special case to allow setting select-multiple value from an array
|
|
1454
|
+
if (this.attrName === 'value' && node.type === 'select-multiple' && Array.isArray(value)) {
|
|
1455
|
+
// Set the .selected property on the options having a value within value.
|
|
1456
|
+
let strValues = value.map(v => v + '');
|
|
1457
|
+
for (let option of node.options)
|
|
1458
|
+
option.selected = strValues.includes(option.value);
|
|
1459
|
+
}
|
|
1460
|
+
else {
|
|
1461
|
+
// TODO: should we remove isFalsy, since these are always props?
|
|
1462
|
+
let strValue = Util.isFalsy(value) ? '' : value;
|
|
1555
1463
|
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
// TODO: We need to remove any old listeners, like in bindEventAttribute
|
|
1464
|
+
// If we don't have this condition, when we call render(), the browser will scroll to the currently
|
|
1465
|
+
// selected item in a <select> and mess up manually scrolling to a different value.
|
|
1466
|
+
if (strValue !== node[this.attrName])
|
|
1467
|
+
node[this.attrName] = strValue;
|
|
1468
|
+
}
|
|
1562
1469
|
|
|
1470
|
+
// TODO: We need to remove any old listeners, like in bindEventAttribute.
|
|
1471
|
+
// Does bindEvent() now handle that?
|
|
1563
1472
|
let func = () => {
|
|
1564
|
-
|
|
1473
|
+
let value = (this.attrName === 'value')
|
|
1474
|
+
? Util.getInputValue(node)
|
|
1475
|
+
: node[this.attrName];
|
|
1476
|
+
delve(obj, path, value);
|
|
1565
1477
|
};
|
|
1566
1478
|
|
|
1567
1479
|
// We use capture so we update the values before other events added by the user.
|
|
1568
|
-
|
|
1480
|
+
// TODO: Bind to scroll events also?
|
|
1481
|
+
// What about resize events and width/height?
|
|
1482
|
+
this.bindEvent(node, path[0], this.attrName, 'input', func, [], true);
|
|
1569
1483
|
}
|
|
1570
1484
|
|
|
1571
1485
|
// Regular attribute
|
|
1572
1486
|
else {
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
//
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1487
|
+
// TODO: Cache this on ExprPath.isProp when Shell creates the props. Have ExprPath.clone() copy .isProp
|
|
1488
|
+
// Or make it a new PathType.
|
|
1489
|
+
//if (this.attrName === 'disabled')
|
|
1490
|
+
// debugger;
|
|
1491
|
+
|
|
1492
|
+
// hasOwnProperty() checks only the object, not the parents
|
|
1493
|
+
// this.attrName in node checks the node and the parents.
|
|
1494
|
+
// This version checks the html element it extends from, to see if has a setter set:
|
|
1495
|
+
// Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), this.attrName)?.set
|
|
1496
|
+
//let isProp = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), this.attrName)?.set;
|
|
1497
|
+
let isProp = this.isHtmlProperty;
|
|
1498
|
+
if (isProp === undefined)
|
|
1499
|
+
isProp = this.isHtmlProperty = Util.isHtmlProp(node, this.attrName);
|
|
1500
|
+
|
|
1501
|
+
// Values to toggle an attribute
|
|
1502
|
+
let multiple = this.attrValue;
|
|
1503
|
+
if (!multiple) {
|
|
1504
|
+
Globals$1.currentExprPath = this; // Used by watch()
|
|
1505
|
+
if (typeof expr === 'function') {
|
|
1506
|
+
if (this.type === 4) { // Don't evaluate functions before passing them to components
|
|
1507
|
+
return
|
|
1584
1508
|
}
|
|
1509
|
+
this.watchFunction = expr; // The function that gets the expression, used for renderWatched()
|
|
1510
|
+
expr = expr();
|
|
1585
1511
|
}
|
|
1586
|
-
|
|
1512
|
+
else
|
|
1513
|
+
expr = Util.makePrimitive(expr);
|
|
1514
|
+
Globals$1.currentExprPath = null;
|
|
1515
|
+
}
|
|
1516
|
+
if (!multiple && (expr === undefined || expr === false || expr === null)) { // Util.isFalsy() inlined.
|
|
1517
|
+
if (isProp)
|
|
1518
|
+
node[this.attrName] = false;
|
|
1519
|
+
node.removeAttribute(this.attrName);
|
|
1520
|
+
}
|
|
1521
|
+
else if (!multiple && expr === true) {
|
|
1522
|
+
if (isProp)
|
|
1523
|
+
node[this.attrName] = true;
|
|
1524
|
+
node.setAttribute(this.attrName, '');
|
|
1587
1525
|
}
|
|
1588
|
-
else
|
|
1589
|
-
value.unshift(expr);
|
|
1590
1526
|
|
|
1591
|
-
|
|
1527
|
+
// A non-toggled attribute
|
|
1528
|
+
else {
|
|
1592
1529
|
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1530
|
+
// If it's a series of expressions among strings, join them together.
|
|
1531
|
+
let joinedValue;
|
|
1532
|
+
if (multiple) {
|
|
1533
|
+
let value = [];
|
|
1534
|
+
for (let i = 0; i < this.attrValue.length; i++) {
|
|
1535
|
+
value.push(this.attrValue[i]);
|
|
1536
|
+
if (i < this.attrValue.length - 1) {
|
|
1537
|
+
Globals$1.currentExprPath = this; // Used by watch()
|
|
1538
|
+
let val = Util.makePrimitive(exprs[i]);
|
|
1539
|
+
Globals$1.currentExprPath = null;
|
|
1540
|
+
if (!Util.isFalsy(val))
|
|
1541
|
+
value.push(val);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
joinedValue = value.join('');
|
|
1545
|
+
}
|
|
1599
1546
|
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1547
|
+
// If the attribute is one expression with no strings:
|
|
1548
|
+
else
|
|
1549
|
+
joinedValue = expr;
|
|
1550
|
+
|
|
1551
|
+
// Only update attributes if the value has changed.
|
|
1552
|
+
// This is needed for setting input.value, .checked, option.selected, etc.
|
|
1553
|
+
|
|
1554
|
+
let oldVal = isProp
|
|
1555
|
+
? node[this.attrName]
|
|
1556
|
+
: node.getAttribute(this.attrName);
|
|
1557
|
+
if (oldVal !== joinedValue) {
|
|
1558
|
+
|
|
1559
|
+
// <textarea value=${expr}></textarea>
|
|
1560
|
+
// Without this branch we have no way to set the value of a textarea,
|
|
1561
|
+
// since we also prohibit expressions that are a child of textarea.
|
|
1562
|
+
if (isProp)
|
|
1563
|
+
node[this.attrName] = joinedValue;
|
|
1564
|
+
// TODO: Putting an 'else' here would be more performant
|
|
1565
|
+
node.setAttribute(this.attrName, joinedValue);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1605
1568
|
}
|
|
1606
|
-
|
|
1607
|
-
return exprIndex;
|
|
1608
1569
|
}
|
|
1609
1570
|
|
|
1610
1571
|
|
|
@@ -1620,7 +1581,8 @@ class ExprPath {
|
|
|
1620
1581
|
let nodeMarker, nodeBefore;
|
|
1621
1582
|
let root = newRoot;
|
|
1622
1583
|
let path = pathOffset ? this.nodeMarkerPath.slice(0, -pathOffset) : this.nodeMarkerPath;
|
|
1623
|
-
|
|
1584
|
+
let length = path.length-1;
|
|
1585
|
+
for (let i=length; i>0; i--) // Resolve the path.
|
|
1624
1586
|
root = root.childNodes[path[i]];
|
|
1625
1587
|
let childNodes = root.childNodes;
|
|
1626
1588
|
|
|
@@ -1665,7 +1627,7 @@ class ExprPath {
|
|
|
1665
1627
|
|
|
1666
1628
|
/**
|
|
1667
1629
|
* Attempt to remove all of this ExprPath's nodes from the DOM, if it can be done using a special fast method.
|
|
1668
|
-
* @returns {boolean} Returns false if Nodes
|
|
1630
|
+
* @returns {boolean} Returns false if Nodes weren't removed, and they should instead be removed manually. */
|
|
1669
1631
|
fastClear() {
|
|
1670
1632
|
let parent = this.nodeBefore.parentNode;
|
|
1671
1633
|
if (this.nodeBefore === parent.firstChild && this.nodeMarker === parent.lastChild) {
|
|
@@ -1701,6 +1663,10 @@ class ExprPath {
|
|
|
1701
1663
|
// result2.push(...ng.getNodes())
|
|
1702
1664
|
// return result2;
|
|
1703
1665
|
|
|
1666
|
+
if (this.type === ExprPathType.AttribValue || this.type === ExprPathType.AttribMultiple || this.type === ExprPathType.ComponentAttribValue) {
|
|
1667
|
+
return [this.nodeMarker];
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1704
1670
|
|
|
1705
1671
|
let result;
|
|
1706
1672
|
|
|
@@ -1727,7 +1693,8 @@ class ExprPath {
|
|
|
1727
1693
|
return result;
|
|
1728
1694
|
}
|
|
1729
1695
|
|
|
1730
|
-
|
|
1696
|
+
/** @return {HTMLElement|ParentNode} */
|
|
1697
|
+
getParentNode() {
|
|
1731
1698
|
return this.nodeMarker.parentNode
|
|
1732
1699
|
}
|
|
1733
1700
|
|
|
@@ -1744,28 +1711,39 @@ class ExprPath {
|
|
|
1744
1711
|
* or createa new NodeGroup from the template.
|
|
1745
1712
|
* @return {NodeGroup} */
|
|
1746
1713
|
getNodeGroup(template, exact=true) {
|
|
1747
|
-
//if (exact && this.nodeGroupsFree.isEmpty())
|
|
1748
|
-
// return null;
|
|
1749
1714
|
|
|
1750
1715
|
let result;
|
|
1716
|
+
let collection = this.nodeGroupsAttachedAvailable;
|
|
1751
1717
|
|
|
1752
1718
|
// TODO: Would it be faster to maintain a separate list of detached nodegroups?
|
|
1753
1719
|
if (exact) { // [below] parentElement will be null if the parent is a DocumentFragment
|
|
1754
|
-
result =
|
|
1720
|
+
result = collection.deleteAny(template.getExactKey());
|
|
1721
|
+
if (!result) { // try searching detached
|
|
1722
|
+
collection = this.nodeGroupsDetachedAvailable;
|
|
1723
|
+
result = collection.deleteAny(template.getExactKey());
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1755
1726
|
if (result) // also delete the matching close key.
|
|
1756
|
-
|
|
1757
|
-
else
|
|
1727
|
+
collection.deleteSpecific(template.getCloseKey(), result);
|
|
1728
|
+
else {
|
|
1758
1729
|
return null;
|
|
1730
|
+
}
|
|
1759
1731
|
}
|
|
1760
1732
|
|
|
1761
1733
|
// Find a close match.
|
|
1762
1734
|
// This is a match that has matching html, but different expressions applied.
|
|
1763
1735
|
// We can then apply the expressions to make it an exact match.
|
|
1764
|
-
|
|
1765
|
-
|
|
1736
|
+
// 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.
|
|
1737
|
+
else if (template.exprs.length) {
|
|
1738
|
+
result = collection.deleteAny(template.getCloseKey());
|
|
1739
|
+
if (!result) { // try searching detached
|
|
1740
|
+
collection = this.nodeGroupsDetachedAvailable;
|
|
1741
|
+
result = collection.deleteAny(template.getCloseKey());
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1766
1744
|
if (result) {
|
|
1767
1745
|
/*#IFDEV*/assert(result.exactKey);/*#ENDIF*/
|
|
1768
|
-
|
|
1746
|
+
collection.deleteSpecific(result.exactKey, result);
|
|
1769
1747
|
|
|
1770
1748
|
// Update this close match with the new expression values.
|
|
1771
1749
|
result.applyExprs(template.exprs);
|
|
@@ -1777,64 +1755,70 @@ class ExprPath {
|
|
|
1777
1755
|
result = new NodeGroup(template, this);
|
|
1778
1756
|
|
|
1779
1757
|
// old:
|
|
1780
|
-
this.
|
|
1781
|
-
|
|
1782
|
-
// new:
|
|
1783
|
-
// let ngiu = this.nodeGroupsInUse;
|
|
1784
|
-
// ngiu.add(result.exactKey, result);
|
|
1785
|
-
// ngiu.add(result.closeKey, result);
|
|
1758
|
+
this.nodeGroupsRendered.push(result);
|
|
1786
1759
|
|
|
1787
1760
|
/*#IFDEV*/assert(result.parentPath);/*#ENDIF*/
|
|
1788
1761
|
return result;
|
|
1789
1762
|
}
|
|
1790
1763
|
|
|
1764
|
+
isComponent() {
|
|
1765
|
+
// Events won't have type===Component.
|
|
1766
|
+
// TODO: Have a special flag for components instead of it being on the type?
|
|
1767
|
+
return this.type === ExprPathType.ComponentAttribValue || (this.attrName && this.nodeMarker.tagName && this.nodeMarker.tagName.includes('-'));
|
|
1768
|
+
}
|
|
1791
1769
|
|
|
1792
1770
|
/**
|
|
1771
|
+
* TODO: Rename this to nodeGroupsInUse, nodeGroupsAvialableAttached and nodeGroupsAvailableDetached?
|
|
1772
|
+
* Nodes that have been used during the current render().
|
|
1793
1773
|
* Used with getNodeGroup() and freeNodeGroups().
|
|
1794
1774
|
* TODO: Use an array of WeakRef so the gc can collect them?
|
|
1795
1775
|
* TODO: Put items back in nodeGroupsInUse after applyExpr() is called, not before.
|
|
1796
1776
|
* @type {NodeGroup[]} */
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
/** @type {MultiValueMap<key:string, value:NodeGroup>} */
|
|
1800
|
-
//nodeGroupsInUse = new MultiValueMap();
|
|
1777
|
+
nodeGroupsRendered = [];
|
|
1801
1778
|
|
|
1802
1779
|
/**
|
|
1780
|
+
* Nodes that were added to the web component during the last render(), but are available to be used again.
|
|
1803
1781
|
* Used with getNodeGroup() and freeNodeGroups().
|
|
1804
1782
|
* Each NodeGroup is here twice, once under an exact key, and once under the close key.
|
|
1805
1783
|
* @type {MultiValueMap<key:string, value:NodeGroup>} */
|
|
1806
|
-
|
|
1784
|
+
nodeGroupsAttachedAvailable = new MultiValueMap();
|
|
1807
1785
|
|
|
1808
|
-
|
|
1786
|
+
/**
|
|
1787
|
+
* Nodes that were not added to the web component during the last render(), and available to be used again.
|
|
1788
|
+
* @type {MultiValueMap} */
|
|
1789
|
+
nodeGroupsDetachedAvailable = new MultiValueMap();
|
|
1809
1790
|
|
|
1810
1791
|
|
|
1811
1792
|
/**
|
|
1812
|
-
* Move everything from this.
|
|
1793
|
+
* Move everything from this.nodeGroupsRendered to this.nodeGroupsAttached and nodeGroupsDetached.
|
|
1794
|
+
* Called at the beginning of applyNodes() so it can have NodeGroups to use.
|
|
1813
1795
|
* TODO: this could run as needed in getNodeGroup? */
|
|
1814
1796
|
freeNodeGroups() {
|
|
1815
|
-
//
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1797
|
+
// Add nodes that weren't used during render() to nodeGroupsDetached
|
|
1798
|
+
let previouslyAttached = this.nodeGroupsAttachedAvailable.data;
|
|
1799
|
+
let detached = this.nodeGroupsDetachedAvailable.data;
|
|
1800
|
+
for (let key in previouslyAttached) {
|
|
1801
|
+
let set = detached[key];
|
|
1802
|
+
if (!set)
|
|
1803
|
+
detached[key] = previouslyAttached[key];
|
|
1804
|
+
else
|
|
1805
|
+
for (let ng of previouslyAttached[key])
|
|
1806
|
+
set.add(ng);
|
|
1807
|
+
}
|
|
1819
1808
|
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1809
|
+
// Add nodes that were used during render() to nodeGroupsRendered.
|
|
1810
|
+
this.nodeGroupsAttachedAvailable = new MultiValueMap();
|
|
1811
|
+
let nga = this.nodeGroupsAttachedAvailable;
|
|
1812
|
+
for (let ng of this.nodeGroupsRendered) {
|
|
1813
|
+
nga.add(ng.exactKey, ng);
|
|
1814
|
+
nga.add(ng.closeKey, ng);
|
|
1824
1815
|
}
|
|
1825
|
-
this.nodeGroupsInUse = [];
|
|
1826
1816
|
|
|
1827
|
-
|
|
1828
|
-
// for (let key in this.nodeGroupsFree.data)
|
|
1829
|
-
// for (let item of this.nodeGroupsFree.data[key])
|
|
1830
|
-
// this.nodeGroupsInUse.add(key, item);
|
|
1831
|
-
//
|
|
1832
|
-
// this.nodeGroupsFree = this.nodeGroupsInUse;
|
|
1833
|
-
// this.nodeGroupsInUse = new MultiValueMap();
|
|
1817
|
+
this.nodeGroupsRendered = [];
|
|
1834
1818
|
}
|
|
1835
1819
|
|
|
1836
1820
|
//#IFDEV
|
|
1837
|
-
|
|
1821
|
+
|
|
1838
1822
|
get debug() {
|
|
1839
1823
|
return [
|
|
1840
1824
|
`parentNode: ${this.nodeBefore.parentNode?.tagName?.toLowerCase()}`,
|
|
@@ -1847,7 +1831,7 @@ class ExprPath {
|
|
|
1847
1831
|
}), 1).flat()
|
|
1848
1832
|
]
|
|
1849
1833
|
}
|
|
1850
|
-
|
|
1834
|
+
|
|
1851
1835
|
get debugNodes() {
|
|
1852
1836
|
// Clear nodesCache so that getNodes() manually gets the nodes.
|
|
1853
1837
|
let nc = this.nodesCache;
|
|
@@ -1856,13 +1840,13 @@ class ExprPath {
|
|
|
1856
1840
|
this.nodesCache = nc;
|
|
1857
1841
|
return result;
|
|
1858
1842
|
}
|
|
1859
|
-
|
|
1843
|
+
|
|
1860
1844
|
verify() {
|
|
1861
1845
|
if (!window.verify)
|
|
1862
1846
|
return;
|
|
1863
1847
|
|
|
1864
|
-
assert(this.type!==
|
|
1865
|
-
assert(this.type!==
|
|
1848
|
+
assert(this.type!==ExprPathType.Content || this.nodeBefore);
|
|
1849
|
+
assert(this.type!==ExprPathType.Content || this.nodeBefore.parentNode);
|
|
1866
1850
|
|
|
1867
1851
|
// Need either nodeMarker or parentNode
|
|
1868
1852
|
assert(this.nodeMarker);
|
|
@@ -1871,10 +1855,10 @@ class ExprPath {
|
|
|
1871
1855
|
assert(!this.nodeMarker || this.nodeMarker.parentNode);
|
|
1872
1856
|
|
|
1873
1857
|
// nodeBefore and nodeMarker must have same parent.
|
|
1874
|
-
assert(this.type!==
|
|
1858
|
+
assert(this.type!==ExprPathType.Content || this.nodeBefore.parentNode === this.nodeMarker.parentNode);
|
|
1875
1859
|
|
|
1876
1860
|
assert(this.nodeBefore !== this.nodeMarker);
|
|
1877
|
-
assert(this.type!==
|
|
1861
|
+
assert(this.type!==ExprPathType.Content|| !this.nodeBefore.parentNode || this.nodeBefore.compareDocumentPosition(this.nodeMarker) === Node.DOCUMENT_POSITION_FOLLOWING);
|
|
1878
1862
|
|
|
1879
1863
|
// Detect cyclic parent and grandparent references.
|
|
1880
1864
|
assert(this.parentNg?.parentPath !== this);
|
|
@@ -1887,43 +1871,27 @@ class ExprPath {
|
|
|
1887
1871
|
// Make sure the nodesCache matches the nodes.
|
|
1888
1872
|
this.checkNodesCache();
|
|
1889
1873
|
}
|
|
1890
|
-
|
|
1874
|
+
|
|
1891
1875
|
checkNodesCache() {
|
|
1892
1876
|
return;
|
|
1893
1877
|
}
|
|
1894
1878
|
//#ENDIF
|
|
1895
1879
|
}
|
|
1896
1880
|
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
/**
|
|
1900
|
-
*
|
|
1901
|
-
* @param root
|
|
1902
|
-
* @param path {string[]}
|
|
1903
|
-
* @param node {HTMLElement}
|
|
1904
|
-
*/
|
|
1905
|
-
function setValue(root, path, node) {
|
|
1906
|
-
let val = node.value;
|
|
1907
|
-
if (node.type === 'number')
|
|
1908
|
-
val = parseFloat(val);
|
|
1909
|
-
|
|
1910
|
-
delve(root, path, val);
|
|
1911
|
-
}
|
|
1912
|
-
|
|
1913
1881
|
/** @enum {int} */
|
|
1914
|
-
const
|
|
1882
|
+
const ExprPathType = {
|
|
1915
1883
|
/** Child of a node */
|
|
1916
1884
|
Content: 1,
|
|
1917
|
-
|
|
1885
|
+
|
|
1918
1886
|
/** One or more whole attributes */
|
|
1919
|
-
|
|
1920
|
-
|
|
1887
|
+
AttribMultiple: 2,
|
|
1888
|
+
|
|
1921
1889
|
/** Value of an attribute. */
|
|
1922
|
-
|
|
1923
|
-
|
|
1890
|
+
AttribValue: 3,
|
|
1891
|
+
|
|
1924
1892
|
/** Value of an attribute being passed to a component. */
|
|
1925
|
-
|
|
1926
|
-
|
|
1893
|
+
ComponentAttribValue: 4,
|
|
1894
|
+
|
|
1927
1895
|
/** Expressions inside Html comments. */
|
|
1928
1896
|
Comment: 5,
|
|
1929
1897
|
|
|
@@ -1949,118 +1917,179 @@ function getNodePath(node) {
|
|
|
1949
1917
|
* Note that the path is backward, with the outermost element at the end.
|
|
1950
1918
|
* @param root {HTMLElement|Document|DocumentFragment|ParentNode}
|
|
1951
1919
|
* @param path {int[]}
|
|
1952
|
-
* @returns {Node|HTMLElement} */
|
|
1920
|
+
* @returns {Node|HTMLElement|HTMLStyleElement} */
|
|
1953
1921
|
function resolveNodePath(root, path) {
|
|
1954
1922
|
for (let i=path.length-1; i>=0; i--)
|
|
1955
1923
|
root = root.childNodes[path[i]];
|
|
1956
1924
|
return root;
|
|
1957
1925
|
}
|
|
1958
1926
|
|
|
1927
|
+
class HtmlParser {
|
|
1928
|
+
constructor() {
|
|
1929
|
+
this.defaultState = {
|
|
1930
|
+
context: HtmlParser.Text, // possible values: 'TEXT', 'TAG', 'ATTRIBUTE'
|
|
1931
|
+
quote: null, // possible values: null, '"', "'"
|
|
1932
|
+
buffer: '',
|
|
1933
|
+
lastChar: null
|
|
1934
|
+
};
|
|
1935
|
+
this.state = {...this.defaultState};
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
reset() {
|
|
1939
|
+
this.state = {...this.defaultState};
|
|
1940
|
+
return this.state.context;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
/**
|
|
1944
|
+
* Parse the next chunk of html, starting with the same context we left off with from the previous chunk.
|
|
1945
|
+
* @param html {string}
|
|
1946
|
+
* @param onContextChange {?function(html:string, index:int, oldContext:string, newContext:string)}
|
|
1947
|
+
* Called every time the context changes, and again at the last context.
|
|
1948
|
+
* @return {('Attribute','Text','Tag')} The context at the end of html. */
|
|
1949
|
+
parse(html, onContextChange=null) {
|
|
1950
|
+
if (html === null)
|
|
1951
|
+
return this.reset();
|
|
1952
|
+
|
|
1953
|
+
for (let i = 0; i < html.length; i++) {
|
|
1954
|
+
const char = html[i];
|
|
1955
|
+
switch (this.state.context) {
|
|
1956
|
+
case HtmlParser.Text:
|
|
1957
|
+
if (char === '<' && html[i + 1].match(/[/a-z!]/i)) { // Start of a tag or comment.
|
|
1958
|
+
onContextChange?.(html, i, this.state.context, HtmlParser.Tag);
|
|
1959
|
+
this.state.context = HtmlParser.Tag;
|
|
1960
|
+
this.state.buffer = '';
|
|
1961
|
+
}
|
|
1962
|
+
break;
|
|
1963
|
+
case HtmlParser.Tag:
|
|
1964
|
+
if (char === '>') {
|
|
1965
|
+
onContextChange?.(html, i+1, this.state.context, HtmlParser.Text);
|
|
1966
|
+
this.state.context = HtmlParser.Text;
|
|
1967
|
+
this.state.quote = null;
|
|
1968
|
+
this.state.buffer = '';
|
|
1969
|
+
}
|
|
1970
|
+
else if (char === ' ' && !this.state.buffer) {
|
|
1971
|
+
// No attribute name is present. Skipping the space.
|
|
1972
|
+
continue;
|
|
1973
|
+
}
|
|
1974
|
+
else if (char === ' ' || char === '/' || char === '?') {
|
|
1975
|
+
this.state.buffer = ''; // Reset the buffer when a delimiter or potential self-closing sign is found.
|
|
1976
|
+
}
|
|
1977
|
+
else if (char === '"' || char === "'" || char === '=') {
|
|
1978
|
+
onContextChange?.(html, i, this.state.context, HtmlParser.Attribute);
|
|
1979
|
+
this.state.context = HtmlParser.Attribute;
|
|
1980
|
+
this.state.quote = char === '=' ? null : char;
|
|
1981
|
+
this.state.buffer = '';
|
|
1982
|
+
}
|
|
1983
|
+
else
|
|
1984
|
+
this.state.buffer += char;
|
|
1985
|
+
break;
|
|
1986
|
+
case HtmlParser.Attribute:
|
|
1987
|
+
// Start an attribute quote.
|
|
1988
|
+
if (!this.state.quote && !this.state.buffer.length && (char === '"' || char === "'")) {
|
|
1989
|
+
this.state.quote = char;
|
|
1990
|
+
}
|
|
1991
|
+
else if (char === this.state.quote || (!this.state.quote && this.state.buffer.length)) {
|
|
1992
|
+
onContextChange?.(html, i, this.state.context, HtmlParser.Tag);
|
|
1993
|
+
this.state.context = HtmlParser.Tag;
|
|
1994
|
+
this.state.quote = null;
|
|
1995
|
+
this.state.buffer = '';
|
|
1996
|
+
}
|
|
1997
|
+
else if (!this.state.quote && char === '>') {
|
|
1998
|
+
onContextChange?.(html, i+1, this.state.context, HtmlParser.Text);
|
|
1999
|
+
this.state.context = HtmlParser.Text;
|
|
2000
|
+
this.state.quote = null;
|
|
2001
|
+
this.state.buffer = '';
|
|
2002
|
+
}
|
|
2003
|
+
else if (char !== ' ')
|
|
2004
|
+
this.state.buffer += char;
|
|
2005
|
+
|
|
2006
|
+
break;
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
onContextChange?.(html, html.length, this.state.context, null);
|
|
2010
|
+
return this.state.context;
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
HtmlParser.Attribute = 'Attribute';
|
|
2015
|
+
HtmlParser.Text = 'Text';
|
|
2016
|
+
HtmlParser.Tag = 'Tag';
|
|
2017
|
+
|
|
1959
2018
|
/**
|
|
1960
2019
|
* A Shell is created from a tagged template expression instantiated as Nodes,
|
|
1961
2020
|
* but without any expressions filled in.
|
|
1962
2021
|
* Only one Shell is created for all the items in a loop.
|
|
1963
2022
|
*
|
|
1964
2023
|
* When a NodeGroup is created from a Template's html strings,
|
|
1965
|
-
* the NodeGroup then clones the Shell's
|
|
2024
|
+
* the NodeGroup then clones the Shell's fragment to be its nodes. */
|
|
1966
2025
|
class Shell {
|
|
1967
2026
|
|
|
1968
2027
|
/**
|
|
1969
|
-
* @type {DocumentFragment} DOM parent of the shell nodes. */
|
|
2028
|
+
* @type {DocumentFragment|Text} DOM parent of the shell's nodes. */
|
|
1970
2029
|
fragment;
|
|
1971
2030
|
|
|
1972
2031
|
/** @type {ExprPath[]} Paths to where expressions should go. */
|
|
1973
2032
|
paths = [];
|
|
1974
2033
|
|
|
1975
|
-
//
|
|
1976
|
-
events = [];
|
|
2034
|
+
// Elements with events. Not yet used.
|
|
2035
|
+
// events = [];
|
|
1977
2036
|
|
|
1978
2037
|
/** @type {int[][]} Array of paths */
|
|
1979
2038
|
ids = [];
|
|
2039
|
+
|
|
2040
|
+
/** @type {int[][]} Array of paths */
|
|
1980
2041
|
scripts = [];
|
|
2042
|
+
|
|
2043
|
+
/** @type {int[][]} Array of paths */
|
|
1981
2044
|
styles = [];
|
|
1982
2045
|
|
|
2046
|
+
/** @type {int[][]} Array of paths. Used by activateEmbeds() to quickly find components. */
|
|
1983
2047
|
staticComponents = [];
|
|
1984
2048
|
|
|
2049
|
+
/** @type {{path:int[], attribs:Record<string, string>}[]} */
|
|
2050
|
+
//componentAttribs = [];
|
|
2051
|
+
|
|
1985
2052
|
|
|
1986
2053
|
|
|
1987
2054
|
/**
|
|
1988
2055
|
* Create the nodes but without filling in the expressions.
|
|
1989
2056
|
* This is useful because the expression-less nodes created by a template can be cached.
|
|
1990
|
-
* @param html {string[]} */
|
|
2057
|
+
* @param html {string[]} Html strings, split on places where an expression exists. */
|
|
1991
2058
|
constructor(html=null) {
|
|
1992
2059
|
if (!html)
|
|
1993
2060
|
return;
|
|
1994
2061
|
|
|
1995
2062
|
//#IFDEV
|
|
1996
|
-
this.
|
|
2063
|
+
this._html = html.join('');
|
|
1997
2064
|
//#ENDIF
|
|
1998
2065
|
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
let buffer = [];
|
|
2004
|
-
let commentPlaceholder = `<!--!✨!-->`;
|
|
2005
|
-
let componentNames = {};
|
|
2006
|
-
|
|
2007
|
-
htmlContext(null); // Reset the context.
|
|
2008
|
-
for (let i=0; i<html.length; i++) {
|
|
2009
|
-
let lastHtml = html[i];
|
|
2010
|
-
let context = htmlContext(lastHtml);
|
|
2011
|
-
|
|
2012
|
-
// Swap out Embedded Solarite Components with ${} attributes.
|
|
2013
|
-
// Later, NodeGroup.render() will search for these and replace them with the real components.
|
|
2014
|
-
// Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
|
|
2015
|
-
if (context === htmlContext.Attribute) {
|
|
2016
|
-
|
|
2017
|
-
let lastIndex, lastMatch;
|
|
2018
|
-
lastHtml.replace(/<[a-z][a-z0-9]*-[a-z0-9-]+/ig, (match, index) => {
|
|
2019
|
-
lastIndex = index+1; // +1 for after opening <
|
|
2020
|
-
lastMatch = match.slice(1);
|
|
2021
|
-
});
|
|
2022
|
-
|
|
2023
|
-
if (lastMatch) {
|
|
2024
|
-
let newTagName = lastMatch + '-solarite-placeholder';
|
|
2025
|
-
lastHtml = lastHtml.slice(0, lastIndex) + newTagName + lastHtml.slice(lastIndex + lastMatch.length);
|
|
2026
|
-
componentNames[lastMatch] = newTagName;
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
buffer.push(lastHtml);
|
|
2031
|
-
//console.log(lastHtml, context)
|
|
2032
|
-
if (i < html.length-1)
|
|
2033
|
-
if (context === htmlContext.Text)
|
|
2034
|
-
buffer.push(commentPlaceholder); // Comment Placeholder. because we can't put text in between <tr> tags for example.
|
|
2035
|
-
else
|
|
2036
|
-
buffer.push(String.fromCharCode(placeholder+i));
|
|
2066
|
+
if (html.length === 1 && !html[0].match(/[<&]/)) {
|
|
2067
|
+
this.fragment = document.createTextNode(html[0]);
|
|
2068
|
+
return;
|
|
2037
2069
|
}
|
|
2038
2070
|
|
|
2039
|
-
// 2. Create elements from html with placeholders.
|
|
2040
|
-
let template = document.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
|
|
2041
|
-
let joinedHtml = buffer.join('');
|
|
2042
2071
|
|
|
2043
|
-
//
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
template.content.append(document.createTextNode(''));
|
|
2072
|
+
// 1. Add placeholders
|
|
2073
|
+
let joinedHtml = Shell.addPlaceholders(html);
|
|
2074
|
+
|
|
2075
|
+
let template = document.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
|
|
2076
|
+
if (joinedHtml)
|
|
2077
|
+
template.innerHTML = joinedHtml;
|
|
2078
|
+
else // Create one text node, so shell isn't empty and NodeGroups created from it have something to point the startNode and endNode at.
|
|
2079
|
+
template.content.append(document.createTextNode(''));
|
|
2052
2080
|
this.fragment = template.content;
|
|
2053
2081
|
|
|
2054
|
-
//
|
|
2082
|
+
// 2. Find placeholders
|
|
2055
2083
|
let node;
|
|
2056
2084
|
let toRemove = [];
|
|
2057
|
-
|
|
2085
|
+
let placeholdersUsed = 0;
|
|
2086
|
+
const walker = document.createTreeWalker(this.fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
|
|
2058
2087
|
while (node = walker.nextNode()) {
|
|
2059
2088
|
|
|
2060
2089
|
// Remove previous after each iteration, so paths will still be calculated correctly.
|
|
2061
2090
|
toRemove.map(el => el.remove());
|
|
2062
2091
|
toRemove = [];
|
|
2063
|
-
|
|
2092
|
+
|
|
2064
2093
|
// Replace attributes
|
|
2065
2094
|
if (node.nodeType === 1) {
|
|
2066
2095
|
for (let attr of [...node.attributes]) { // Copy the attributes array b/c we remove attributes as we go.
|
|
@@ -2068,7 +2097,8 @@ class Shell {
|
|
|
2068
2097
|
// Whole attribute
|
|
2069
2098
|
let matches = attr.name.match(/^[\ue000-\uf8ff]$/);
|
|
2070
2099
|
if (matches) {
|
|
2071
|
-
this.paths.push(new ExprPath(null, node,
|
|
2100
|
+
this.paths.push(new ExprPath(null, node, ExprPathType.AttribMultiple));
|
|
2101
|
+
placeholdersUsed ++;
|
|
2072
2102
|
node.removeAttribute(matches[0]);
|
|
2073
2103
|
}
|
|
2074
2104
|
|
|
@@ -2077,16 +2107,17 @@ class Shell {
|
|
|
2077
2107
|
let parts = attr.value.split(/[\ue000-\uf8ff]/g);
|
|
2078
2108
|
if (parts.length > 1) {
|
|
2079
2109
|
let nonEmptyParts = (parts.length === 2 && !parts[0].length && !parts[1].length) ? null : parts;
|
|
2080
|
-
let type = isEvent(attr.name) ?
|
|
2110
|
+
let type = Util.isEvent(attr.name) ? ExprPathType.Event : ExprPathType.AttribValue;
|
|
2081
2111
|
|
|
2082
2112
|
this.paths.push(new ExprPath(null, node, type, attr.name, nonEmptyParts));
|
|
2113
|
+
placeholdersUsed += parts.length - 1;
|
|
2083
2114
|
node.setAttribute(attr.name, parts.join(''));
|
|
2084
2115
|
}
|
|
2085
2116
|
}
|
|
2086
2117
|
}
|
|
2087
2118
|
}
|
|
2088
2119
|
// Replace comment placeholders
|
|
2089
|
-
else if (node.nodeType ===
|
|
2120
|
+
else if (node.nodeType === 8 && node.nodeValue === '!✨!') {
|
|
2090
2121
|
|
|
2091
2122
|
// Get or create nodeBefore.
|
|
2092
2123
|
let nodeBefore = node.previousSibling; // Can be the same as another Path's nodeMarker.
|
|
@@ -2111,12 +2142,14 @@ class Shell {
|
|
|
2111
2142
|
}
|
|
2112
2143
|
/*#IFDEV*/assert(nodeMarker);/*#ENDIF*/
|
|
2113
2144
|
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
let path = new ExprPath(nodeBefore, nodeMarker, PathType.Content);
|
|
2117
|
-
|
|
2145
|
+
let path = new ExprPath(nodeBefore, nodeMarker, ExprPathType.Content);
|
|
2118
2146
|
this.paths.push(path);
|
|
2147
|
+
placeholdersUsed ++;
|
|
2119
2148
|
}
|
|
2149
|
+
|
|
2150
|
+
else if (node.nodeType === 3 && node.parentNode?.tagName === 'TEXTAREA' && node.textContent.includes('<!--!✨!-->'))
|
|
2151
|
+
throw new Error(`Textarea can't have expressions inside them. Use <textarea value="\${...}"> instead.`);
|
|
2152
|
+
|
|
2120
2153
|
|
|
2121
2154
|
|
|
2122
2155
|
// Sometimes users will comment out a block of html code that has expressions.
|
|
@@ -2127,8 +2160,9 @@ class Shell {
|
|
|
2127
2160
|
let parts = node.textContent.split(/[\ue000-\uf8ff]/g);
|
|
2128
2161
|
for (let i=0; i<parts.length-1; i++) {
|
|
2129
2162
|
let path = new ExprPath(node.previousSibling, node);
|
|
2130
|
-
path.type =
|
|
2163
|
+
path.type = ExprPathType.Comment;
|
|
2131
2164
|
this.paths.push(path);
|
|
2165
|
+
placeholdersUsed ++;
|
|
2132
2166
|
}
|
|
2133
2167
|
}
|
|
2134
2168
|
|
|
@@ -2146,8 +2180,9 @@ class Shell {
|
|
|
2146
2180
|
}
|
|
2147
2181
|
|
|
2148
2182
|
for (let i=0, node; node=placeholders[i]; i++) {
|
|
2149
|
-
let path = new ExprPath(node.previousSibling, node,
|
|
2183
|
+
let path = new ExprPath(node.previousSibling, node, ExprPathType.Content);
|
|
2150
2184
|
this.paths.push(path);
|
|
2185
|
+
placeholdersUsed ++;
|
|
2151
2186
|
|
|
2152
2187
|
/*#IFDEV*/path.verify();/*#ENDIF*/
|
|
2153
2188
|
}
|
|
@@ -2159,17 +2194,17 @@ class Shell {
|
|
|
2159
2194
|
}
|
|
2160
2195
|
toRemove.map(el => el.remove());
|
|
2161
2196
|
|
|
2197
|
+
// Less than or equal because there can be one path to multiple expressions
|
|
2198
|
+
// if those expressions are in the same attribute value.
|
|
2199
|
+
if (placeholdersUsed !== html.length-1)
|
|
2200
|
+
throw new Error(`Could not parse expressions in template. Check for duplicate attributes or malformed html: ${html.join('${...}')}`);
|
|
2201
|
+
|
|
2162
2202
|
// Handle solarite-placeholder's.
|
|
2163
|
-
// Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
|
|
2164
|
-
//if (componentNames.size)
|
|
2165
|
-
// this.components = [...this.fragment.querySelectorAll([...componentNames].join(','))]
|
|
2166
2203
|
|
|
2167
|
-
// Rename "is" attributes so the Web Components don't instantiate until we have the values of their PathExpr arguments.
|
|
2204
|
+
// 3. Rename "is" attributes so the Web Components don't instantiate until we have the values of their PathExpr arguments.
|
|
2168
2205
|
// that happens in NodeGroup.applyComponentExprs()
|
|
2169
|
-
for (let el of this.fragment.querySelectorAll('[is]'))
|
|
2206
|
+
for (let el of this.fragment.querySelectorAll('[is]'))
|
|
2170
2207
|
el.setAttribute('_is', el.getAttribute('is'));
|
|
2171
|
-
// this.components.push(el);
|
|
2172
|
-
}
|
|
2173
2208
|
|
|
2174
2209
|
for (let path of this.paths) {
|
|
2175
2210
|
if (path.nodeBefore)
|
|
@@ -2177,16 +2212,64 @@ class Shell {
|
|
|
2177
2212
|
path.nodeMarkerPath = getNodePath(path.nodeMarker);
|
|
2178
2213
|
|
|
2179
2214
|
// Cache so we don't have to calculate this later inside NodeGroup.applyExprs()
|
|
2180
|
-
if (path.type ===
|
|
2215
|
+
if (path.type === ExprPathType.AttribValue && path.nodeMarker.nodeType === 1 &&
|
|
2181
2216
|
(path.nodeMarker.tagName.includes('-') || path.nodeMarker.hasAttribute('is'))) {
|
|
2182
|
-
path.type =
|
|
2217
|
+
path.type = ExprPathType.ComponentAttribValue;
|
|
2183
2218
|
}
|
|
2184
2219
|
}
|
|
2185
2220
|
|
|
2186
2221
|
this.findEmbeds();
|
|
2187
2222
|
|
|
2188
2223
|
/*#IFDEV*/this.verify();/*#ENDIF*/
|
|
2189
|
-
}
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
/**
|
|
2227
|
+
* 1. Add a Unicode placeholder char for where expressions go within attributes.
|
|
2228
|
+
* 2. Add a comment placeholder for where expressions are children of other nodes.
|
|
2229
|
+
* 3. Append -solarite-placeholder to the tag names of custom components so that we can wait to instantiate them later.
|
|
2230
|
+
* @param htmlChunks {string[]}
|
|
2231
|
+
* @returns {string} */
|
|
2232
|
+
static addPlaceholders(htmlChunks) {
|
|
2233
|
+
let tokens = [];
|
|
2234
|
+
|
|
2235
|
+
function addToken(token, context) {
|
|
2236
|
+
|
|
2237
|
+
if (context === HtmlParser.Tag) {
|
|
2238
|
+
// Find Solarite Components tags and append -solarite-placeholder to their tag names
|
|
2239
|
+
// and give them a solarite-placeholder attribute so we can easily find them later.
|
|
2240
|
+
// This way we can gather their constructor arguments and their children before we call their constructor.
|
|
2241
|
+
// Later, NodeGroup.instantiateComponent() will replace them with the real components.
|
|
2242
|
+
// Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
|
|
2243
|
+
token = token.replace(/^<\/?[a-z][a-z0-9]*-[a-z0-9-]+/i, match => match + '-solarite-placeholder solarite-placeholder');
|
|
2244
|
+
}
|
|
2245
|
+
tokens.push(token);
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
let htmlParser = new HtmlParser(); // Reset the context.
|
|
2249
|
+
for (let i = 0; i < htmlChunks.length; i++) {
|
|
2250
|
+
let lastHtml = htmlChunks[i];
|
|
2251
|
+
|
|
2252
|
+
// Append -solarite-placholder to web component tags, so we can pass args to them when they're instantiated.
|
|
2253
|
+
let lastIndex = 0;
|
|
2254
|
+
let context = htmlParser.parse(lastHtml, (html, index, oldContext, newContext) => {
|
|
2255
|
+
if (lastIndex !== index) {
|
|
2256
|
+
let token = html.slice(lastIndex, index);
|
|
2257
|
+
addToken(token, oldContext);
|
|
2258
|
+
}
|
|
2259
|
+
lastIndex = index;
|
|
2260
|
+
});
|
|
2261
|
+
|
|
2262
|
+
// Insert placeholders
|
|
2263
|
+
if (i < htmlChunks.length - 1) {
|
|
2264
|
+
if (context === HtmlParser.Text)
|
|
2265
|
+
tokens.push(commentPlaceholder); // Comment Placeholder. because we can't put text in between <tr> tags for example.
|
|
2266
|
+
else
|
|
2267
|
+
tokens.push(String.fromCharCode(attribPlaceholder + i));
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
return tokens.join('');
|
|
2272
|
+
}
|
|
2190
2273
|
|
|
2191
2274
|
/**
|
|
2192
2275
|
* We find the path to every embed here once in the Shell, instead of every time a NodeGroup is instantiated.
|
|
@@ -2198,36 +2281,30 @@ class Shell {
|
|
|
2198
2281
|
* this.staticComponents */
|
|
2199
2282
|
findEmbeds() {
|
|
2200
2283
|
this.scripts = Array.prototype.map.call(this.fragment.querySelectorAll('scripts'), el => getNodePath(el));
|
|
2284
|
+
|
|
2285
|
+
// TODO: only find styles that have ExprPaths in them?
|
|
2201
2286
|
this.styles = Array.prototype.map.call(this.fragment.querySelectorAll('style'), el => getNodePath(el));
|
|
2202
2287
|
|
|
2203
2288
|
let idEls = this.fragment.querySelectorAll('[id],[data-id]');
|
|
2204
|
-
|
|
2205
2289
|
|
|
2206
2290
|
// Check for valid id names.
|
|
2207
2291
|
for (let el of idEls) {
|
|
2208
2292
|
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
2209
|
-
if (div.hasOwnProperty(id))
|
|
2293
|
+
if (Globals$1.div.hasOwnProperty(id))
|
|
2210
2294
|
throw new Error(`<${el.tagName.toLowerCase()} id="${id}"> can't override existing HTMLElement id property.`)
|
|
2211
2295
|
}
|
|
2212
2296
|
|
|
2213
|
-
|
|
2214
2297
|
this.ids = Array.prototype.map.call(idEls, el => getNodePath(el));
|
|
2215
2298
|
|
|
2216
|
-
// Events (not yet used)
|
|
2217
2299
|
for (let el of this.fragment.querySelectorAll('*')) {
|
|
2218
|
-
for (let attrib of el.attributes)
|
|
2219
|
-
if (isEvent(attrib.name))
|
|
2220
|
-
this.events.push([attrib.name, getNodePath(el)]);
|
|
2221
|
-
|
|
2222
2300
|
if (el.tagName.includes('-') || el.hasAttribute('_is'))
|
|
2223
2301
|
|
|
2224
|
-
// Dynamic components have attributes with expression values.
|
|
2302
|
+
// Dynamic components are components that have attributes with expression values.
|
|
2225
2303
|
// They are created from applyExprs()
|
|
2226
2304
|
// But static components are created in a separate path inside the NodeGroup constructor.
|
|
2227
2305
|
if (!this.paths.find(path => path.nodeMarker === el))
|
|
2228
2306
|
this.staticComponents.push(getNodePath(el));
|
|
2229
2307
|
}
|
|
2230
|
-
|
|
2231
2308
|
}
|
|
2232
2309
|
|
|
2233
2310
|
/**
|
|
@@ -2235,10 +2312,10 @@ class Shell {
|
|
|
2235
2312
|
* @param htmlStrings {string[]} Typically comes from a Template.
|
|
2236
2313
|
* @returns {Shell} */
|
|
2237
2314
|
static get(htmlStrings) {
|
|
2238
|
-
let result = Globals.shells.get(htmlStrings);
|
|
2315
|
+
let result = Globals$1.shells.get(htmlStrings);
|
|
2239
2316
|
if (!result) {
|
|
2240
2317
|
result = new Shell(htmlStrings);
|
|
2241
|
-
Globals.shells.set(htmlStrings, result); // cache
|
|
2318
|
+
Globals$1.shells.set(htmlStrings, result); // cache
|
|
2242
2319
|
}
|
|
2243
2320
|
|
|
2244
2321
|
/*#IFDEV*/result.verify();/*#ENDIF*/
|
|
@@ -2254,7 +2331,14 @@ class Shell {
|
|
|
2254
2331
|
}
|
|
2255
2332
|
}
|
|
2256
2333
|
//#ENDIF
|
|
2257
|
-
}
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
|
|
2337
|
+
const commentPlaceholder = `<!--!✨!-->`;
|
|
2338
|
+
|
|
2339
|
+
|
|
2340
|
+
// We increment the placeholder char as we go because nodes can't have the same attribute more than once.
|
|
2341
|
+
const attribPlaceholder = 0xe000; // https://en.wikipedia.org/wiki/Private_Use_Areas 6400.
|
|
2258
2342
|
|
|
2259
2343
|
/** @typedef {boolean|string|number|function|Object|Array|Date|Node|Template} Expr */
|
|
2260
2344
|
|
|
@@ -2280,7 +2364,8 @@ class NodeGroup {
|
|
|
2280
2364
|
startNode;
|
|
2281
2365
|
|
|
2282
2366
|
/** @type {Node|HTMLElement} A node that never changes that this NodeGroup should always insert its nodes before.
|
|
2283
|
-
* An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position
|
|
2367
|
+
* An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.
|
|
2368
|
+
* TODO: But sometimes startNode and endNode point to the same node. Document htis inconsistency. */
|
|
2284
2369
|
endNode;
|
|
2285
2370
|
|
|
2286
2371
|
/** @type {ExprPath[]} */
|
|
@@ -2298,11 +2383,11 @@ class NodeGroup {
|
|
|
2298
2383
|
nodesCache;
|
|
2299
2384
|
|
|
2300
2385
|
/**
|
|
2386
|
+
* A map between <style> Elements and their text content.
|
|
2387
|
+
* This lets NodeGroup.updateStyles() see when the style text has changed.
|
|
2301
2388
|
* @type {?Map<HTMLStyleElement, string>} */
|
|
2302
2389
|
styles;
|
|
2303
2390
|
|
|
2304
|
-
currentComponentProps = {};
|
|
2305
|
-
|
|
2306
2391
|
|
|
2307
2392
|
/**
|
|
2308
2393
|
* Create an "instantiated" NodeGroup from a Template and add it to an element.
|
|
@@ -2310,14 +2395,26 @@ class NodeGroup {
|
|
|
2310
2395
|
* @param parentPath {?ExprPath} */
|
|
2311
2396
|
constructor(template, parentPath=null) {
|
|
2312
2397
|
if (!(this instanceof RootNodeGroup)) {
|
|
2398
|
+
|
|
2313
2399
|
let [fragment, shell] = this.init(template, parentPath);
|
|
2314
2400
|
|
|
2315
|
-
|
|
2401
|
+
if (fragment && template.exprs.length) {
|
|
2402
|
+
this.updatePaths(fragment, shell.paths);
|
|
2316
2403
|
|
|
2317
|
-
|
|
2404
|
+
// Static web components can sometimes have children created via expressions.
|
|
2405
|
+
// But calling applyExprs() will mess up the shell's path to them.
|
|
2406
|
+
// So we find them first, then call activateStaticComponents() after their children have been created.
|
|
2407
|
+
let staticComponents = this.findStaticComponents(fragment, shell);
|
|
2318
2408
|
|
|
2319
|
-
|
|
2320
|
-
|
|
2409
|
+
this.activateEmbeds(fragment, shell);
|
|
2410
|
+
|
|
2411
|
+
// Apply exprs
|
|
2412
|
+
this.applyExprs(template.exprs);
|
|
2413
|
+
|
|
2414
|
+
this.instantiateStaticComponents(staticComponents);
|
|
2415
|
+
}
|
|
2416
|
+
else if (shell)
|
|
2417
|
+
this.activateEmbeds(fragment, shell);
|
|
2321
2418
|
}
|
|
2322
2419
|
}
|
|
2323
2420
|
|
|
@@ -2345,68 +2442,119 @@ class NodeGroup {
|
|
|
2345
2442
|
template.nodeGroup = this;
|
|
2346
2443
|
|
|
2347
2444
|
// Get a cached version of the parsed and instantiated html, and ExprPaths.
|
|
2348
|
-
let shell = Shell.get(template.html);
|
|
2349
|
-
let fragment = shell.fragment.cloneNode(true);
|
|
2350
2445
|
|
|
2351
|
-
|
|
2352
|
-
this.
|
|
2353
|
-
|
|
2446
|
+
// If it's just a text node, skip a bunch of unnecessary steps.
|
|
2447
|
+
if (!(this instanceof RootNodeGroup) && !template.exprs.length && !template.html[0].includes('<')) {
|
|
2448
|
+
//let doc = this.rootNg.startNode?.ownerDocument || document;
|
|
2449
|
+
let textNode = document.createTextNode(template.html[0]);
|
|
2354
2450
|
|
|
2355
|
-
|
|
2451
|
+
this.startNode = this.endNode = textNode;
|
|
2452
|
+
return [];
|
|
2453
|
+
}
|
|
2454
|
+
else {
|
|
2455
|
+
let shell = Shell.get(template.html);
|
|
2456
|
+
let fragment = shell.fragment.cloneNode(true);
|
|
2457
|
+
|
|
2458
|
+
if (fragment instanceof DocumentFragment) {
|
|
2459
|
+
let childNodes = fragment.childNodes;
|
|
2460
|
+
this.startNode = childNodes[0];
|
|
2461
|
+
this.endNode = childNodes[childNodes.length - 1];
|
|
2462
|
+
}
|
|
2463
|
+
else {
|
|
2464
|
+
this.startNode = this.endNode = fragment;
|
|
2465
|
+
}
|
|
2466
|
+
return [fragment, shell];
|
|
2467
|
+
}
|
|
2356
2468
|
}
|
|
2357
2469
|
|
|
2358
2470
|
/**
|
|
2359
2471
|
* Use the paths to insert the given expressions.
|
|
2360
2472
|
* Dispatches expression handling to other functions depending on the path type.
|
|
2361
2473
|
* @param exprs {(*|*[]|function|Template)[]}
|
|
2362
|
-
* @param paths {?ExprPath[]} Optional. */
|
|
2474
|
+
* @param paths {?ExprPath[]} Optional. Only used for testing. Normally uses this.paths. */
|
|
2363
2475
|
applyExprs(exprs, paths=null) {
|
|
2364
2476
|
paths = paths || this.paths;
|
|
2365
2477
|
|
|
2366
|
-
/*#IFDEV*/
|
|
2478
|
+
/*#IFDEV*/
|
|
2479
|
+
this.verify();/*#ENDIF*/
|
|
2480
|
+
|
|
2481
|
+
// Things to consider:
|
|
2482
|
+
// 1. One path may use multipe expressions. E.g. <div class="${1} ${2}">
|
|
2483
|
+
// 2. One component may need to use multiple attribute paths to be instantiated.
|
|
2484
|
+
// 3. We apply them in reverse order so that a <select> box has its children created from an expression
|
|
2485
|
+
// before its instantiated and its value attribute is set via an expression.
|
|
2486
|
+
|
|
2487
|
+
let exprIndex = exprs.length - 1; // Update exprs at paths.
|
|
2488
|
+
let lastComponentPathIndex;
|
|
2489
|
+
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.
|
|
2490
|
+
for (let i = paths.length - 1, path; path = paths[i]; i--) {
|
|
2491
|
+
let prevPath = paths[i - 1];
|
|
2492
|
+
let nextPath = paths[i + 1];
|
|
2493
|
+
|
|
2494
|
+
// Get the expressions associated with this path.
|
|
2495
|
+
if (path.attrValue?.length > 2) {
|
|
2496
|
+
let startIndex = (exprIndex - (path.attrValue.length - 1)) + 1;
|
|
2497
|
+
pathExprs[i] = exprs.slice(startIndex, exprIndex + 1); // probably doesn't allocate if the JS vm implements copy on write.
|
|
2498
|
+
exprIndex -= pathExprs[i].length;
|
|
2499
|
+
} else {
|
|
2500
|
+
pathExprs[i] = [exprs[exprIndex]];
|
|
2501
|
+
exprIndex--;
|
|
2502
|
+
}
|
|
2367
2503
|
|
|
2368
|
-
|
|
2369
|
-
|
|
2504
|
+
// TODO: Need to end and restart this block when going from one component to the next?
|
|
2505
|
+
// Think of having two adjacent components.
|
|
2506
|
+
// But the dynamicAttribsAdjacet test already passes.
|
|
2370
2507
|
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2508
|
+
// If expr is an attribute in a component:
|
|
2509
|
+
// 1. Instantiate it if it hasn't already been, sending all expr's to its constructor.
|
|
2510
|
+
// 2. Otherwise send them to its render function.
|
|
2511
|
+
// Components with no expressions as attributes are instead activated in activateEmbeds().
|
|
2512
|
+
if (path.nodeMarker !== this.rootNg.root && path.isComponent()) {
|
|
2375
2513
|
|
|
2376
|
-
|
|
2514
|
+
if (!nextPath || !nextPath.isComponent() || nextPath.nodeMarker !== path.nodeMarker)
|
|
2515
|
+
lastComponentPathIndex = i;
|
|
2516
|
+
let isFirstComponentPath = !prevPath || !prevPath.isComponent() || prevPath.nodeMarker !== path.nodeMarker;
|
|
2377
2517
|
|
|
2378
|
-
|
|
2379
|
-
if (lastNode && lastNode !== this.rootNg.root && lastNode !== path.nodeMarker && Object.keys(this.currentComponentProps).length) {
|
|
2380
|
-
this.applyComponentExprs(lastNode, this.currentComponentProps);
|
|
2381
|
-
this.currentComponentProps = {};
|
|
2382
|
-
}
|
|
2518
|
+
if (isFirstComponentPath) {
|
|
2383
2519
|
|
|
2384
|
-
|
|
2520
|
+
let componentProps = {};
|
|
2521
|
+
for (let j=i; j<=lastComponentPathIndex; j++) {
|
|
2522
|
+
let attrName = paths[j].attrName; // Util.dashesToCamel(paths[j].attrName);
|
|
2523
|
+
componentProps[attrName] = pathExprs[j].length > 1 ? pathExprs[j].join('') : pathExprs[j][0];
|
|
2524
|
+
}
|
|
2385
2525
|
|
|
2386
|
-
|
|
2526
|
+
this.applyComponentExprs(path.nodeMarker, componentProps);
|
|
2387
2527
|
|
|
2528
|
+
// Set attributes on component.
|
|
2529
|
+
for (let j=i; j<=lastComponentPathIndex; j++)
|
|
2530
|
+
paths[j].apply(pathExprs[j]);
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2388
2533
|
|
|
2389
|
-
|
|
2390
|
-
|
|
2534
|
+
// Else apply it normally
|
|
2535
|
+
else
|
|
2536
|
+
path.apply(pathExprs[i]);
|
|
2391
2537
|
|
|
2392
2538
|
|
|
2393
|
-
//
|
|
2394
|
-
if (lastNode && lastNode !== this.rootNg.root && Object.keys(this.currentComponentProps).length) {
|
|
2395
|
-
this.applyComponentExprs(lastNode, this.currentComponentProps);
|
|
2396
|
-
this.currentComponentProps = {};
|
|
2397
|
-
}
|
|
2539
|
+
} // end for(path of this.paths)
|
|
2398
2540
|
|
|
2541
|
+
|
|
2542
|
+
// TODO: Only do this if we have ExprPaths within styles?
|
|
2399
2543
|
this.updateStyles();
|
|
2400
2544
|
|
|
2545
|
+
|
|
2546
|
+
|
|
2401
2547
|
// Invalidate the nodes cache because we just changed it.
|
|
2402
2548
|
this.nodesCache = null;
|
|
2403
2549
|
|
|
2404
2550
|
// If there's leftover expressions, there's probably an issue with the Shell that created this NodeGroup,
|
|
2405
2551
|
// and the number of paths not matching.
|
|
2406
|
-
/*#IFDEV*/
|
|
2552
|
+
/*#IFDEV*/
|
|
2553
|
+
assert(exprIndex === -1);/*#ENDIF*/
|
|
2407
2554
|
|
|
2408
2555
|
|
|
2409
|
-
/*#IFDEV*/
|
|
2556
|
+
/*#IFDEV*/
|
|
2557
|
+
this.verify();/*#ENDIF*/
|
|
2410
2558
|
}
|
|
2411
2559
|
|
|
2412
2560
|
/**
|
|
@@ -2420,24 +2568,30 @@ class NodeGroup {
|
|
|
2420
2568
|
// then we could re-use the hash and logic from NodeManager?
|
|
2421
2569
|
let newHash = getObjectHash(props);
|
|
2422
2570
|
|
|
2423
|
-
let isPreHtmlElement = el.
|
|
2571
|
+
let isPreHtmlElement = el.hasAttribute('solarite-placeholder');
|
|
2424
2572
|
let isPreIsElement = el.hasAttribute('_is');
|
|
2425
2573
|
|
|
2426
2574
|
|
|
2427
2575
|
// Instantiate a placeholder.
|
|
2428
2576
|
if (isPreHtmlElement || isPreIsElement)
|
|
2429
|
-
el = this.
|
|
2577
|
+
el = this.instantiateComponent(el, isPreHtmlElement, props);
|
|
2430
2578
|
|
|
2431
2579
|
// Call render() with the same params that would've been passed to the constructor.
|
|
2580
|
+
// We do this even if the arguments haven't changed, so we can let the child component
|
|
2581
|
+
// compare the arguments and then decide for itself whether it wants to re-render.
|
|
2432
2582
|
else if (el.render) {
|
|
2433
|
-
let oldHash = Globals.
|
|
2434
|
-
if (oldHash !== newHash)
|
|
2435
|
-
|
|
2583
|
+
//let oldHash = Globals.componentArgsHash.get(el);
|
|
2584
|
+
//if (oldHash !== newHash) { // Only if not changed.
|
|
2585
|
+
let args = {};
|
|
2586
|
+
for (let name in props || {})
|
|
2587
|
+
args[Util.dashesToCamel(name)] = props[name];
|
|
2588
|
+
el.render(args); // Pass new values of props to render so it can decide how it wants to respond.
|
|
2589
|
+
//}
|
|
2436
2590
|
}
|
|
2437
2591
|
|
|
2438
|
-
Globals.
|
|
2592
|
+
Globals$1.componentArgsHash.set(el, newHash);
|
|
2439
2593
|
}
|
|
2440
|
-
|
|
2594
|
+
|
|
2441
2595
|
/**
|
|
2442
2596
|
* We swap the placeholder element for the real element so we can pass its dynamic attributes
|
|
2443
2597
|
* to its constructor.
|
|
@@ -2445,78 +2599,54 @@ class NodeGroup {
|
|
|
2445
2599
|
* The logic of this function is complex and could use cleaning up.
|
|
2446
2600
|
*
|
|
2447
2601
|
* @param el
|
|
2448
|
-
* @param isPreHtmlElement
|
|
2602
|
+
* @param isPreHtmlElement {?boolean} True if the element's tag name ends with -solarite-placeholder
|
|
2449
2603
|
* @param props {Object} Attributes with dynamic values.
|
|
2450
2604
|
* @return {HTMLElement} */
|
|
2451
|
-
|
|
2605
|
+
instantiateComponent(el, isPreHtmlElement=undefined, props=undefined) {
|
|
2452
2606
|
if (isPreHtmlElement === undefined)
|
|
2453
2607
|
isPreHtmlElement = !el.hasAttribute('_is');
|
|
2454
|
-
|
|
2608
|
+
|
|
2455
2609
|
let tagName = (isPreHtmlElement
|
|
2456
|
-
? el.tagName.
|
|
2457
|
-
? el.tagName.slice(0, -21)
|
|
2458
|
-
: el.tagName
|
|
2610
|
+
? el.tagName.slice(0, -21) // Remove -SOLARITE-PLACEHOLDER
|
|
2459
2611
|
: el.getAttribute('is')).toLowerCase();
|
|
2460
2612
|
|
|
2461
|
-
|
|
2462
|
-
|
|
2613
|
+
|
|
2614
|
+
// Throw if custom element isn't defined.
|
|
2615
|
+
let Constructor = customElements.get(tagName);
|
|
2616
|
+
if (!Constructor)
|
|
2617
|
+
throw new Error(`The custom tag name ${tagName} is not registered.`)
|
|
2618
|
+
|
|
2619
|
+
let args = {};
|
|
2620
|
+
for (let name in props || {})
|
|
2621
|
+
args[Util.dashesToCamel(name)] = props[name];
|
|
2622
|
+
|
|
2463
2623
|
// Pass other attribs to constructor, since otherwise they're not yet set on the element,
|
|
2464
2624
|
// and the constructor would otherwise have no way to see them.
|
|
2465
2625
|
if (el.attributes.length) {
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2626
|
+
for (let attrib of el.attributes) {
|
|
2627
|
+
let attribName = Util.dashesToCamel(attrib.name);
|
|
2628
|
+
if (!args.hasOwnProperty(attribName) && attribName !== 'solarite-placeholder')
|
|
2629
|
+
args[attribName] = attrib.value;
|
|
2630
|
+
}
|
|
2471
2631
|
}
|
|
2472
|
-
|
|
2473
|
-
// Create CustomElement and
|
|
2474
|
-
let Constructor = customElements.get(tagName);
|
|
2475
|
-
if (!Constructor)
|
|
2476
|
-
throw new Error(`The custom tag name ${tagName} is not registered.`)
|
|
2477
2632
|
|
|
2478
|
-
//
|
|
2479
|
-
//
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
// can add them as children before the rest of the constructor code executes.
|
|
2483
|
-
let ch = [... el.childNodes];
|
|
2484
|
-
Globals.pendingChildren.push(ch); // pop() is called in Solarite constructor.
|
|
2485
|
-
let newEl = new Constructor(props, ch);
|
|
2633
|
+
// Create the web component.
|
|
2634
|
+
// Get the children that aren't Solarite's comment placeholders.
|
|
2635
|
+
let ch = [...el.childNodes].filter(node => node.nodeType !== Node.COMMENT_NODE || !node.nodeValue.startsWith('ExprPath'));
|
|
2636
|
+
let newEl = new Constructor(args, ch);
|
|
2486
2637
|
|
|
2487
2638
|
if (!isPreHtmlElement)
|
|
2488
2639
|
newEl.setAttribute('is', el.getAttribute('is').toLowerCase());
|
|
2640
|
+
|
|
2641
|
+
// Replace the placeholder tag with the instantiated web component.
|
|
2489
2642
|
el.replaceWith(newEl);
|
|
2490
2643
|
|
|
2491
|
-
// Set children / slot children
|
|
2492
|
-
// TODO: Match named slots.
|
|
2493
|
-
// TODO: This only appends to slot if render() is called in the constructor.
|
|
2494
|
-
//let slot = newEl.querySelector('slot') || newEl;
|
|
2495
|
-
//slot.append(...el.childNodes);
|
|
2496
|
-
|
|
2497
|
-
// Copy over event attributes.
|
|
2498
|
-
for (let propName in props) {
|
|
2499
|
-
let val = props[propName];
|
|
2500
|
-
if (propName.startsWith('on') && typeof val === 'function')
|
|
2501
|
-
newEl.addEventListener(propName.slice(2), e => val(e, newEl));
|
|
2502
|
-
|
|
2503
|
-
// Bind array based event attributes on value.
|
|
2504
|
-
// This same logic is in ExprPath.applyValueAttrib() for non-components.
|
|
2505
|
-
if ((propName === 'value' || propName === 'data-value') && Util.isPath(val)) {
|
|
2506
|
-
let [obj, path] = [val[0], val.slice(1)];
|
|
2507
|
-
newEl.value = delve(obj, path);
|
|
2508
|
-
newEl.addEventListener('input', e => {
|
|
2509
|
-
delve(obj, path, Util.getInputValue(newEl));
|
|
2510
|
-
}, true); // We use capture so we update the values before other events added by the user.
|
|
2511
|
-
}
|
|
2512
|
-
}
|
|
2513
|
-
|
|
2514
2644
|
// If an id pointed at the placeholder, update it to point to the new element.
|
|
2515
2645
|
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
2516
2646
|
if (id)
|
|
2517
2647
|
delve(this.getRootNode(), id.split(/\./g), newEl);
|
|
2518
|
-
|
|
2519
|
-
|
|
2648
|
+
|
|
2649
|
+
|
|
2520
2650
|
// Update paths to use replaced element.
|
|
2521
2651
|
for (let path of this.paths) {
|
|
2522
2652
|
if (path.nodeMarker === el)
|
|
@@ -2528,31 +2658,31 @@ class NodeGroup {
|
|
|
2528
2658
|
this.startNode = newEl;
|
|
2529
2659
|
if (this.endNode === el)
|
|
2530
2660
|
this.endNode = newEl;
|
|
2531
|
-
|
|
2532
|
-
|
|
2661
|
+
|
|
2662
|
+
|
|
2533
2663
|
// applyComponentExprs() is called because we're rendering.
|
|
2534
2664
|
// So we want to render the sub-component also.
|
|
2535
2665
|
if (newEl.renderFirstTime)
|
|
2536
2666
|
newEl.renderFirstTime();
|
|
2537
|
-
|
|
2667
|
+
|
|
2538
2668
|
// Copy attributes over.
|
|
2539
2669
|
for (let attrib of el.attributes)
|
|
2540
|
-
if (attrib.name !== '_is')
|
|
2670
|
+
if (attrib.name !== '_is' && attrib.name !== 'solarite-placeholder')
|
|
2541
2671
|
newEl.setAttribute(attrib.name, attrib.value);
|
|
2542
2672
|
|
|
2543
2673
|
// Set dynamic attributes if they are primitive types.
|
|
2544
|
-
for (let name in
|
|
2545
|
-
let val =
|
|
2674
|
+
for (let name in props) {
|
|
2675
|
+
let val = props[name];
|
|
2546
2676
|
if (typeof val === 'boolean') {
|
|
2547
2677
|
if (val !== false && val !== undefined && val !== null)
|
|
2548
2678
|
newEl.setAttribute(name, '');
|
|
2549
2679
|
}
|
|
2550
2680
|
|
|
2551
|
-
// If type
|
|
2681
|
+
// If type is a non-boolean primitive, set the attribute value.
|
|
2552
2682
|
else if (['number', 'bigint', 'string'].includes(typeof val))
|
|
2553
2683
|
newEl.setAttribute(name, val);
|
|
2554
2684
|
}
|
|
2555
|
-
|
|
2685
|
+
|
|
2556
2686
|
return newEl;
|
|
2557
2687
|
}
|
|
2558
2688
|
|
|
@@ -2598,8 +2728,7 @@ class NodeGroup {
|
|
|
2598
2728
|
|
|
2599
2729
|
/**
|
|
2600
2730
|
* Requires the nodeCache to be present. */
|
|
2601
|
-
|
|
2602
|
-
/*#IFDEV*/assert(!this.startNode.parentNode);/*#ENDIF*/
|
|
2731
|
+
removeAndSaveOrphans() {
|
|
2603
2732
|
/*#IFDEV*/assert(this.nodesCache);/*#ENDIF*/
|
|
2604
2733
|
let fragment = document.createDocumentFragment();
|
|
2605
2734
|
for (let node of this.getNodes())
|
|
@@ -2609,8 +2738,9 @@ class NodeGroup {
|
|
|
2609
2738
|
|
|
2610
2739
|
updatePaths(fragment, paths, offset) {
|
|
2611
2740
|
// Update paths to point to the fragment.
|
|
2612
|
-
|
|
2613
|
-
|
|
2741
|
+
let pathLength = paths.length;
|
|
2742
|
+
this.paths.length = pathLength;
|
|
2743
|
+
for (let i=0; i<pathLength; i++) {
|
|
2614
2744
|
let path = paths[i].clone(fragment, offset);
|
|
2615
2745
|
path.parentNg = this;
|
|
2616
2746
|
this.paths[i] = path;
|
|
@@ -2632,23 +2762,23 @@ class NodeGroup {
|
|
|
2632
2762
|
* An interleaved array of sets of nodes and top-level ExprPaths
|
|
2633
2763
|
* @type {(Node|HTMLElement|ExprPath)[]} */
|
|
2634
2764
|
get nodes() { throw new Error('')};
|
|
2635
|
-
|
|
2765
|
+
|
|
2636
2766
|
get debug() {
|
|
2637
2767
|
return [
|
|
2638
2768
|
`parentNode: ${this.parentNode?.tagName?.toLowerCase()}`,
|
|
2639
2769
|
'nodes:',
|
|
2640
2770
|
...setIndent(this.getNodes().map(item => {
|
|
2641
2771
|
if (item instanceof Node) {
|
|
2642
|
-
|
|
2772
|
+
|
|
2643
2773
|
let tree = nodeToArrayTree(item, nextNode => {
|
|
2644
|
-
|
|
2645
|
-
let path = this.paths.find(path=>path.type ===
|
|
2774
|
+
|
|
2775
|
+
let path = this.paths.find(path=>path.type === ExprPathType.Content && path.getNodes().includes(nextNode));
|
|
2646
2776
|
if (path)
|
|
2647
2777
|
return [`Path.nodes:`]
|
|
2648
|
-
|
|
2778
|
+
|
|
2649
2779
|
return [];
|
|
2650
2780
|
});
|
|
2651
|
-
|
|
2781
|
+
|
|
2652
2782
|
// TODO: How to indend nodes belonging to a path vs those that just occur after the path?
|
|
2653
2783
|
return flattenAndIndent(tree)
|
|
2654
2784
|
}
|
|
@@ -2659,10 +2789,10 @@ class NodeGroup {
|
|
|
2659
2789
|
}
|
|
2660
2790
|
|
|
2661
2791
|
get debugNodes() { return this.getNodes() }
|
|
2662
|
-
|
|
2663
|
-
|
|
2792
|
+
|
|
2793
|
+
|
|
2664
2794
|
get debugNodesHtml() { return this.getNodes().map(n => n.outerHTML || n.textContent) }
|
|
2665
|
-
|
|
2795
|
+
|
|
2666
2796
|
verify() {
|
|
2667
2797
|
if (!window.verify)
|
|
2668
2798
|
return;
|
|
@@ -2677,7 +2807,7 @@ class NodeGroup {
|
|
|
2677
2807
|
|
|
2678
2808
|
// if (this.parentPath)
|
|
2679
2809
|
// assert(this.parentPath.nodeGroups.includes(this));
|
|
2680
|
-
|
|
2810
|
+
|
|
2681
2811
|
for (let path of this.paths) {
|
|
2682
2812
|
assert(path.parentNg === this);
|
|
2683
2813
|
|
|
@@ -2693,61 +2823,70 @@ class NodeGroup {
|
|
|
2693
2823
|
}
|
|
2694
2824
|
//#ENDIF
|
|
2695
2825
|
|
|
2826
|
+
findStaticComponents(root, shell, pathOffset=0) {
|
|
2827
|
+
let result = [];
|
|
2696
2828
|
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
activateEmbeds(root, shell, pathOffset=0) {
|
|
2702
|
-
|
|
2703
|
-
// static components. These are WebComponents not created by an expression.
|
|
2704
|
-
// Must happen before ids.
|
|
2829
|
+
// static components. These are WebComponents that do not have any constructor arguments that are expressions.
|
|
2830
|
+
// Those are instead created by applyExpr() which calls applyComponentExprs() which calls instantiateComponent().
|
|
2831
|
+
// Maybe someday these two paths will be merged?
|
|
2832
|
+
// Must happen before ids because instantiateComponent will replace the element.
|
|
2705
2833
|
for (let path of shell.staticComponents) {
|
|
2706
2834
|
if (pathOffset)
|
|
2707
2835
|
path = path.slice(0, -pathOffset);
|
|
2708
2836
|
let el = resolveNodePath(root, path);
|
|
2709
2837
|
|
|
2710
2838
|
// Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
|
|
2839
|
+
// Recreating it is necessary so we can pass the constructor args to it.
|
|
2711
2840
|
if (root !== el/* && !isReplaceEl(root, el)*/) // TODO: is isReplaceEl necessary?
|
|
2712
|
-
|
|
2841
|
+
result.push(el);
|
|
2713
2842
|
}
|
|
2843
|
+
return result;
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
instantiateStaticComponents(staticComponents) {
|
|
2847
|
+
for (let el of staticComponents)
|
|
2848
|
+
this.instantiateComponent(el);
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
/**
|
|
2852
|
+
* @param root {HTMLElement|DocumentFragment}
|
|
2853
|
+
* @param shell {Shell}
|
|
2854
|
+
* @param pathOffset {int} */
|
|
2855
|
+
activateEmbeds(root, shell, pathOffset=0) {
|
|
2714
2856
|
|
|
2715
2857
|
let rootEl = this.rootNg.root;
|
|
2716
2858
|
if (rootEl) {
|
|
2859
|
+
let options = this.rootNg.options;
|
|
2717
2860
|
|
|
2718
2861
|
// ids
|
|
2719
|
-
if (
|
|
2862
|
+
if (options?.ids !== false) {
|
|
2720
2863
|
for (let path of shell.ids) {
|
|
2721
2864
|
if (pathOffset)
|
|
2722
2865
|
path = path.slice(0, -pathOffset);
|
|
2723
2866
|
let el = resolveNodePath(root, path);
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
// Don't allow overwriting existing class properties if they already have a non-Node value.
|
|
2728
|
-
if (rootEl[id] && !(rootEl[id] instanceof Node))
|
|
2729
|
-
throw new Error(`${rootEl.constructor.name}.${id} already has a value. ` +
|
|
2730
|
-
`Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
|
|
2731
|
-
|
|
2732
|
-
delve(rootEl, id.split(/\./g), el);
|
|
2733
|
-
}
|
|
2867
|
+
Util.bindId(rootEl, el);
|
|
2868
|
+
}
|
|
2734
2869
|
}
|
|
2735
2870
|
|
|
2736
2871
|
// styles
|
|
2737
|
-
if (
|
|
2872
|
+
if (options?.styles !== false) {
|
|
2738
2873
|
if (shell.styles.length)
|
|
2739
2874
|
this.styles = new Map();
|
|
2740
2875
|
for (let path of shell.styles) {
|
|
2741
2876
|
if (pathOffset)
|
|
2742
2877
|
path = path.slice(0, -pathOffset);
|
|
2878
|
+
|
|
2879
|
+
/** @type {HTMLStyleElement} */
|
|
2743
2880
|
let style = resolveNodePath(root, path);
|
|
2744
|
-
|
|
2745
|
-
|
|
2881
|
+
if (rootEl.nodeType === 1) {
|
|
2882
|
+
Util.bindStyles(style, rootEl);
|
|
2883
|
+
this.styles.set(style, style.textContent);
|
|
2884
|
+
}
|
|
2746
2885
|
}
|
|
2747
2886
|
|
|
2748
2887
|
}
|
|
2749
2888
|
// scripts
|
|
2750
|
-
if (
|
|
2889
|
+
if (options?.scripts !== false) {
|
|
2751
2890
|
for (let path of shell.scripts) {
|
|
2752
2891
|
if (pathOffset)
|
|
2753
2892
|
path = path.slice(0, -pathOffset);
|
|
@@ -2768,20 +2907,8 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2768
2907
|
root;
|
|
2769
2908
|
|
|
2770
2909
|
/**
|
|
2771
|
-
*
|
|
2772
|
-
*
|
|
2773
|
-
* @type {Object<field:string, Set<ExprPath>>} */
|
|
2774
|
-
watchedExprPaths = {};
|
|
2775
|
-
|
|
2776
|
-
/**
|
|
2777
|
-
* Map from arrays where .map is called and their callback functions.
|
|
2778
|
-
* TODO: One array might be called with two different map functions in different places!
|
|
2779
|
-
* @type {Map<Array, function>} */
|
|
2780
|
-
mapCallbacks = new Map();
|
|
2781
|
-
|
|
2782
|
-
/**
|
|
2783
|
-
*
|
|
2784
|
-
* @type {Map<ExprPath, boolean|Array>} */
|
|
2910
|
+
* When we call renerWatched() we re-render these expressions, then clear this to a new Map()
|
|
2911
|
+
* @type {Map<ExprPath, ValueOp|WholeArrayOp|ArraySpliceOp[]>} */
|
|
2785
2912
|
exprsToRender = new Map();
|
|
2786
2913
|
|
|
2787
2914
|
/**
|
|
@@ -2798,82 +2925,102 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2798
2925
|
this.rootNg = this;
|
|
2799
2926
|
let [fragment, shell] = this.init(template);
|
|
2800
2927
|
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
if (el.childNodes.length) {
|
|
2810
|
-
slotFragment = document.createDocumentFragment();
|
|
2811
|
-
slotFragment.append(...el.childNodes);
|
|
2928
|
+
if (fragment instanceof Text) {
|
|
2929
|
+
|
|
2930
|
+
if (el) {
|
|
2931
|
+
this.startNode = el;
|
|
2932
|
+
this.endNode = el;
|
|
2933
|
+
if (fragment.nodeValue.length)
|
|
2934
|
+
el.append(fragment);
|
|
2935
|
+
this.root = el;
|
|
2812
2936
|
}
|
|
2937
|
+
Globals$1.nodeGroups.set(this.root, this);
|
|
2938
|
+
}
|
|
2939
|
+
else {
|
|
2813
2940
|
|
|
2814
|
-
|
|
2941
|
+
// If adding NodeGroup to an element.
|
|
2942
|
+
let offset = 0;
|
|
2943
|
+
let root = fragment; // TODO: Rename so it's not confused with this.root.
|
|
2944
|
+
if (el) {
|
|
2945
|
+
Globals$1.nodeGroups.set(el, this);
|
|
2946
|
+
|
|
2947
|
+
// Save slot children
|
|
2948
|
+
let slotChildren;
|
|
2949
|
+
if (el.childNodes.length) {
|
|
2950
|
+
slotChildren = document.createDocumentFragment();
|
|
2951
|
+
slotChildren.append(...el.childNodes);
|
|
2952
|
+
}
|
|
2815
2953
|
|
|
2816
|
-
|
|
2817
|
-
if (isReplaceEl(fragment, el)) {
|
|
2818
|
-
el.append(...fragment.children[0].childNodes);
|
|
2954
|
+
this.root = el;
|
|
2819
2955
|
|
|
2820
|
-
//
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
el.setAttribute(attrib.name, attrib.value);
|
|
2956
|
+
// If el should replace the root node of the fragment.
|
|
2957
|
+
if (isReplaceEl(fragment, el)) {
|
|
2958
|
+
el.append(...fragment.children[0].childNodes);
|
|
2824
2959
|
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2960
|
+
// Copy attributes
|
|
2961
|
+
for (let attrib of fragment.children[0].attributes)
|
|
2962
|
+
if (!el.hasAttribute(attrib.name) && attrib.name !== 'solarite-placeholder')
|
|
2963
|
+
el.setAttribute(attrib.name, attrib.value);
|
|
2964
|
+
|
|
2965
|
+
// Go one level deeper into all of shell's paths.
|
|
2966
|
+
offset = 1;
|
|
2967
|
+
} else {
|
|
2968
|
+
let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
|
|
2969
|
+
if (!isEmpty)
|
|
2970
|
+
el.append(...fragment.childNodes);
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
// Setup children
|
|
2974
|
+
if (slotChildren) {
|
|
2975
|
+
|
|
2976
|
+
// Named slots
|
|
2977
|
+
for (let slot of el.querySelectorAll('slot[name]')) {
|
|
2978
|
+
let name = slot.getAttribute('name');
|
|
2979
|
+
if (name) {
|
|
2980
|
+
let slotChildren2 = slotChildren.querySelectorAll(`[slot='${name}']`);
|
|
2981
|
+
slot.append(...slotChildren2);
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
// Unnamed slots
|
|
2986
|
+
let unamedSlot = el.querySelector('slot:not([name])');
|
|
2987
|
+
if (unamedSlot)
|
|
2988
|
+
unamedSlot.append(slotChildren);
|
|
2833
2989
|
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
let name = slot.getAttribute('name');
|
|
2838
|
-
if (name) {
|
|
2839
|
-
let slotChildren = slotFragment.querySelectorAll(`[slot='${name}']`);
|
|
2840
|
-
slot.append(...slotChildren);
|
|
2841
|
-
}
|
|
2990
|
+
// No slots
|
|
2991
|
+
else
|
|
2992
|
+
el.append(slotChildren);
|
|
2842
2993
|
}
|
|
2843
|
-
let unamedSlot = el.querySelector('slot:not([name])');
|
|
2844
|
-
if (unamedSlot)
|
|
2845
|
-
unamedSlot.append(slotFragment);
|
|
2846
|
-
else
|
|
2847
|
-
el.append(slotFragment);
|
|
2848
|
-
}
|
|
2849
2994
|
|
|
2850
|
-
|
|
2995
|
+
root = el;
|
|
2851
2996
|
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
|
|
2997
|
+
this.startNode = el;
|
|
2998
|
+
this.endNode = el;
|
|
2999
|
+
} else {
|
|
3000
|
+
let singleEl = getSingleEl(fragment);
|
|
3001
|
+
this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
|
|
2858
3002
|
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
3003
|
+
Globals$1.nodeGroups.set(this.root, this);
|
|
3004
|
+
if (singleEl) {
|
|
3005
|
+
root = singleEl;
|
|
3006
|
+
offset = 1;
|
|
3007
|
+
}
|
|
2863
3008
|
}
|
|
2864
|
-
}
|
|
2865
3009
|
|
|
2866
|
-
|
|
3010
|
+
this.updatePaths(root, shell.paths, offset);
|
|
2867
3011
|
|
|
2868
|
-
|
|
3012
|
+
// Static web components can sometimes have children created via expressions.
|
|
3013
|
+
// But calling applyExprs() will mess up the shell's path to them.
|
|
3014
|
+
// So we find them first, then call activateStaticComponents() after their children have been created.
|
|
3015
|
+
let staticComponents = this.findStaticComponents(root, shell, offset);
|
|
2869
3016
|
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
3017
|
+
this.activateEmbeds(root, shell, offset);
|
|
3018
|
+
|
|
3019
|
+
// Apply exprs
|
|
3020
|
+
this.applyExprs(template.exprs);
|
|
2873
3021
|
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
this.mapCallbacks = new Map();
|
|
3022
|
+
this.instantiateStaticComponents(staticComponents);
|
|
3023
|
+
}
|
|
2877
3024
|
}
|
|
2878
3025
|
}
|
|
2879
3026
|
|
|
@@ -2895,8 +3042,8 @@ function getSingleEl(fragment) {
|
|
|
2895
3042
|
* @param el {HTMLElement}
|
|
2896
3043
|
* @returns {boolean} */
|
|
2897
3044
|
function isReplaceEl(fragment, el) {
|
|
2898
|
-
return
|
|
2899
|
-
&&
|
|
3045
|
+
return fragment.children.length===1
|
|
3046
|
+
&& el.tagName.includes('-') // TODO: Check for solarite-placeholder attribute instead?
|
|
2900
3047
|
&& fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') === el.tagName;
|
|
2901
3048
|
}
|
|
2902
3049
|
|
|
@@ -2915,19 +3062,9 @@ class Template {
|
|
|
2915
3062
|
/** @type {Array} Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
|
|
2916
3063
|
hashedFields;
|
|
2917
3064
|
|
|
2918
|
-
/**
|
|
2919
|
-
* @deprecated
|
|
2920
|
-
* @type {ExprPath} Used with forEach() from watch.js
|
|
2921
|
-
* Set in ExprPath.apply() */
|
|
2922
|
-
parentPath;
|
|
2923
|
-
|
|
2924
3065
|
/** @type {NodeGroup} */
|
|
2925
3066
|
nodeGroup;
|
|
2926
3067
|
|
|
2927
|
-
/**
|
|
2928
|
-
* @type {string[][]} */
|
|
2929
|
-
paths = [];
|
|
2930
|
-
|
|
2931
3068
|
/**
|
|
2932
3069
|
*
|
|
2933
3070
|
* @param htmlStrings {string[]}
|
|
@@ -2978,17 +3115,21 @@ class Template {
|
|
|
2978
3115
|
if (standalone) {
|
|
2979
3116
|
ng = new RootNodeGroup(this, null, options);
|
|
2980
3117
|
el = ng.getRootNode();
|
|
2981
|
-
|
|
3118
|
+
Globals$1.nodeGroups.set(el, ng); // Why was this commented out?
|
|
2982
3119
|
firstTime = true;
|
|
2983
3120
|
}
|
|
2984
3121
|
else {
|
|
2985
|
-
ng = Globals.nodeGroups.get(el);
|
|
3122
|
+
ng = Globals$1.nodeGroups.get(el);
|
|
2986
3123
|
if (!ng) {
|
|
2987
3124
|
ng = new RootNodeGroup(this, el, options);
|
|
2988
|
-
|
|
3125
|
+
Globals$1.nodeGroups.set(el, ng); // Why was this commented out?
|
|
2989
3126
|
firstTime = true;
|
|
2990
3127
|
}
|
|
2991
|
-
|
|
3128
|
+
|
|
3129
|
+
// This can happen if we try manually rendering one template to a NodeGroup that was created expecting a different template.
|
|
3130
|
+
// These don't always have the same length, for example if one attribute has multiple expressions.
|
|
3131
|
+
if (ng.paths.length === 0 && this.exprs.length || ng.paths.length > this.exprs.length)
|
|
3132
|
+
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.`); }
|
|
2992
3133
|
|
|
2993
3134
|
// Creating the root nodegroup also renders it.
|
|
2994
3135
|
// If we didn't just create it, we need to render it.
|
|
@@ -2996,23 +3137,32 @@ class Template {
|
|
|
2996
3137
|
if (this.html?.length === 1 && !this.html[0])
|
|
2997
3138
|
el.innerHTML = ''; // Fast path for empty component.
|
|
2998
3139
|
else {
|
|
2999
|
-
ng.clearRenderWatched();
|
|
3000
3140
|
ng.applyExprs(this.exprs);
|
|
3001
3141
|
}
|
|
3002
3142
|
}
|
|
3003
3143
|
|
|
3144
|
+
ng.exprsToRender = new Map();
|
|
3004
3145
|
return el;
|
|
3005
3146
|
}
|
|
3006
3147
|
|
|
3007
3148
|
getExactKey() {
|
|
3008
|
-
if (!this.exactKey)
|
|
3009
|
-
|
|
3149
|
+
if (!this.exactKey) {
|
|
3150
|
+
if (this.exprs.length)
|
|
3151
|
+
this.exactKey = getObjectHash(this);// calls this.toJSON().
|
|
3152
|
+
else // Don't hash plain html.
|
|
3153
|
+
this.exactKey = this.html[0];
|
|
3154
|
+
}
|
|
3010
3155
|
return this.exactKey;
|
|
3011
3156
|
}
|
|
3012
3157
|
|
|
3013
3158
|
getCloseKey() {
|
|
3014
|
-
|
|
3015
|
-
|
|
3159
|
+
//console.log(this.exprs.length)
|
|
3160
|
+
if (!this.closeKey) {
|
|
3161
|
+
if (this.exprs.length)
|
|
3162
|
+
this.closeKey = /*'@' + */this.toJSON()[0];
|
|
3163
|
+
else
|
|
3164
|
+
this.closeKey = this.html[0];
|
|
3165
|
+
}
|
|
3016
3166
|
// Use the joined html when debugging? But it breaks some tests.
|
|
3017
3167
|
//return '@'+this.html.join('|')
|
|
3018
3168
|
|
|
@@ -3035,8 +3185,8 @@ class Template {
|
|
|
3035
3185
|
|
|
3036
3186
|
/**
|
|
3037
3187
|
* Convert strings to HTMLNodes.
|
|
3038
|
-
* Using
|
|
3039
|
-
* Using
|
|
3188
|
+
* Using h`...` as a tag will always create a Template.
|
|
3189
|
+
* Using h() as a function() will always create a DOM element.
|
|
3040
3190
|
*
|
|
3041
3191
|
* Features beyond what standard js tagged template strings do:
|
|
3042
3192
|
* 1. r`` sub-expressions
|
|
@@ -3046,24 +3196,27 @@ class Template {
|
|
|
3046
3196
|
* 5. TODO: list more
|
|
3047
3197
|
*
|
|
3048
3198
|
* Currently supported:
|
|
3049
|
-
* 1.
|
|
3050
|
-
* 2.
|
|
3199
|
+
* 1. h(el, options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
|
|
3200
|
+
* 2. h(el, template, ?options) // Render the Template created by #1 to element.
|
|
3051
3201
|
*
|
|
3052
|
-
* 3.
|
|
3202
|
+
* 3. h`<b>Hello</b> ${'World'}!` // Create Template that can later be used to create nodes.
|
|
3053
3203
|
*
|
|
3054
|
-
* 4.
|
|
3055
|
-
* 5.
|
|
3056
|
-
* 6.
|
|
3057
|
-
* 7.
|
|
3204
|
+
* 4. h('Hello'); // Create single text node.
|
|
3205
|
+
* 5. h('<b>Hello</b>'); // Create single HTMLElement
|
|
3206
|
+
* 6. h('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
|
|
3207
|
+
* 7. h()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which
|
|
3058
3208
|
* // includes properly handling nested components and r`` sub-expressions.
|
|
3059
|
-
* 8.
|
|
3060
|
-
*
|
|
3061
|
-
* 9. r({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
|
|
3209
|
+
* 8. h(template) // Render Template created by #1.
|
|
3062
3210
|
*
|
|
3211
|
+
* 9. h({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
|
|
3212
|
+
* 10. h(string, object, ...) // JSX TODO
|
|
3063
3213
|
* @param htmlStrings {?HTMLElement|string|string[]|function():Template|{render:function()}}
|
|
3064
3214
|
* @param exprs {*[]|string|Template|Object}
|
|
3065
3215
|
* @return {Node|HTMLElement|Template} */
|
|
3066
|
-
function
|
|
3216
|
+
function h(htmlStrings=undefined, ...exprs) {
|
|
3217
|
+
|
|
3218
|
+
if (htmlStrings === undefined && !exprs.length && arguments.length)
|
|
3219
|
+
throw new Error('h() cannot be called with undefined.');
|
|
3067
3220
|
|
|
3068
3221
|
// TODO: Make this a more flat if/else and call other functions for the logic.
|
|
3069
3222
|
if (htmlStrings instanceof Node) {
|
|
@@ -3078,7 +3231,7 @@ function r(htmlStrings=undefined, ...exprs) {
|
|
|
3078
3231
|
|
|
3079
3232
|
// Return a tagged template function that applies the tagged themplate to parent.
|
|
3080
3233
|
let taggedTemplate = (htmlStrings, ...exprs) => {
|
|
3081
|
-
Globals.rendered.add(parent);
|
|
3234
|
+
Globals$1.rendered.add(parent);
|
|
3082
3235
|
let template = new Template(htmlStrings, exprs);
|
|
3083
3236
|
return template.render(parent, options);
|
|
3084
3237
|
};
|
|
@@ -3086,7 +3239,7 @@ function r(htmlStrings=undefined, ...exprs) {
|
|
|
3086
3239
|
}
|
|
3087
3240
|
|
|
3088
3241
|
// 2. Render template created by #4 to element.
|
|
3089
|
-
else
|
|
3242
|
+
else { // instanceof Template
|
|
3090
3243
|
let options = exprs[1];
|
|
3091
3244
|
template.render(parent, options);
|
|
3092
3245
|
|
|
@@ -3097,16 +3250,6 @@ function r(htmlStrings=undefined, ...exprs) {
|
|
|
3097
3250
|
parent.append(this.rootNg.getParentNode());
|
|
3098
3251
|
}
|
|
3099
3252
|
}
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
// null for expr[0], remove whole element.
|
|
3104
|
-
// This path never happens?
|
|
3105
|
-
else {
|
|
3106
|
-
throw new Error('unsupported');
|
|
3107
|
-
//let ngm = NodeGroupManager.get(parent);
|
|
3108
|
-
//ngm.render(null, exprs[1])
|
|
3109
|
-
}
|
|
3110
3253
|
}
|
|
3111
3254
|
|
|
3112
3255
|
// 3. Path if used as a template tag.
|
|
@@ -3115,6 +3258,22 @@ function r(htmlStrings=undefined, ...exprs) {
|
|
|
3115
3258
|
}
|
|
3116
3259
|
|
|
3117
3260
|
else if (typeof htmlStrings === 'string' || htmlStrings instanceof String) {
|
|
3261
|
+
// 10. JSX
|
|
3262
|
+
if (typeof exprs[0] === 'object') {
|
|
3263
|
+
exprs[0] || {};
|
|
3264
|
+
exprs.slice(1);
|
|
3265
|
+
|
|
3266
|
+
let templateHtmlStrings = [];
|
|
3267
|
+
let templateExprs = [];
|
|
3268
|
+
|
|
3269
|
+
// TODO How to know which children are static html and which are expression placeholders?
|
|
3270
|
+
// Perhaps we have to treat every text child as a string?
|
|
3271
|
+
|
|
3272
|
+
assert(templateHtmlStrings.length === templateExprs.length+1);
|
|
3273
|
+
return new Template(templateHtmlStrings, templateExprs);
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
|
|
3118
3277
|
// If it starts with a string, trim both ends.
|
|
3119
3278
|
// TODO: Also trim if it ends with whitespace?
|
|
3120
3279
|
if (htmlStrings.match(/^\s^</))
|
|
@@ -3138,7 +3297,7 @@ function r(htmlStrings=undefined, ...exprs) {
|
|
|
3138
3297
|
else if (htmlStrings === undefined) {
|
|
3139
3298
|
return (htmlStrings, ...exprs) => {
|
|
3140
3299
|
//Globals.rendered.add(parent)
|
|
3141
|
-
let template =
|
|
3300
|
+
let template = h(htmlStrings, ...exprs);
|
|
3142
3301
|
return template.render();
|
|
3143
3302
|
}
|
|
3144
3303
|
}
|
|
@@ -3150,46 +3309,193 @@ function r(htmlStrings=undefined, ...exprs) {
|
|
|
3150
3309
|
|
|
3151
3310
|
|
|
3152
3311
|
// 9. Create dynamic element with render() function.
|
|
3312
|
+
// TODO: This path doesn't handle embeds like data-id="..."
|
|
3153
3313
|
else if (typeof htmlStrings === 'object') {
|
|
3154
3314
|
let obj = htmlStrings;
|
|
3155
3315
|
|
|
3316
|
+
if (obj.constructor.name !== 'Object')
|
|
3317
|
+
throw new Error(`Solarate Web Component class ${obj.constructor?.name} must extend HTMLElement.`);
|
|
3318
|
+
|
|
3319
|
+
|
|
3156
3320
|
// Special rebound render path, called by normal path.
|
|
3157
|
-
|
|
3321
|
+
// Intercepts the main r`...` function call inside render().
|
|
3322
|
+
if (Globals$1.objToEl.has(obj)) {
|
|
3158
3323
|
return function(...args) {
|
|
3159
|
-
let template =
|
|
3324
|
+
let template = h(...args);
|
|
3160
3325
|
let el = template.render();
|
|
3161
|
-
Globals.objToEl.set(obj, el);
|
|
3326
|
+
Globals$1.objToEl.set(obj, el);
|
|
3162
3327
|
}.bind(obj);
|
|
3163
3328
|
}
|
|
3164
3329
|
|
|
3165
3330
|
// Normal path
|
|
3166
3331
|
else {
|
|
3167
|
-
Globals.objToEl.set(obj, null);
|
|
3168
|
-
obj
|
|
3169
|
-
let el = Globals.objToEl.get(obj);
|
|
3170
|
-
Globals.objToEl.delete(obj);
|
|
3332
|
+
Globals$1.objToEl.set(obj, null);
|
|
3333
|
+
obj[renderF](); // Calls the Special rebound render path above, when the render function calls r(this)
|
|
3334
|
+
let el = Globals$1.objToEl.get(obj);
|
|
3335
|
+
Globals$1.objToEl.delete(obj);
|
|
3171
3336
|
|
|
3172
3337
|
for (let name in obj)
|
|
3173
3338
|
if (typeof obj[name] === 'function')
|
|
3174
|
-
el[name] = obj[name].bind(el);
|
|
3339
|
+
el[name] = obj[name].bind(el); // Make the "this" of functions be el.
|
|
3340
|
+
// TODO: But this doesn't work for passing an object with functions as a constructor arg via an attribute:
|
|
3341
|
+
// <my-element arg=${{myFunc() { return this }}}
|
|
3175
3342
|
else
|
|
3176
3343
|
el[name] = obj[name];
|
|
3177
3344
|
|
|
3345
|
+
// Bind id's
|
|
3346
|
+
// This doesn't work for id's referenced by attributes.
|
|
3347
|
+
// for (let idEl of el.querySelectorAll('[id],[data-id]')) {
|
|
3348
|
+
// Util.bindId(el, idEl);
|
|
3349
|
+
// Util.bindId(obj, idEl);
|
|
3350
|
+
// }
|
|
3351
|
+
// TODO: Bind styles
|
|
3352
|
+
|
|
3178
3353
|
return el;
|
|
3179
3354
|
}
|
|
3180
3355
|
}
|
|
3181
3356
|
|
|
3182
3357
|
else
|
|
3183
3358
|
throw new Error('Unsupported arguments.')
|
|
3184
|
-
}
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
// Trick to prevent minifier from renaming this function.
|
|
3362
|
+
let renderF = 'render';
|
|
3185
3363
|
|
|
3186
|
-
|
|
3364
|
+
/**
|
|
3365
|
+
* There are three ways to create an instance of a Solarite Component:
|
|
3366
|
+
* 1. new ComponentName(); // direct class instantiation
|
|
3367
|
+
* 2. this.html = r`<div><component-name></component-name></div>; // as a child of another RedComponent.
|
|
3368
|
+
* 3. <body><component-name></component-name></body> // in the Document html.
|
|
3369
|
+
*
|
|
3370
|
+
* When created via #3, Solarite has no way to pass attributes as arguments to the constructor. So to make
|
|
3371
|
+
* sure we get the correct value via all three paths, we write our constructors according to the following
|
|
3372
|
+
* example. Note that constructor args are embedded in an object, and must be all lower-case because
|
|
3373
|
+
* Browsers make all html attribute names lowercase.
|
|
3374
|
+
*
|
|
3375
|
+
* @example
|
|
3376
|
+
* constructor({name, userid=1}={}) {
|
|
3377
|
+
* super();
|
|
3378
|
+
*
|
|
3379
|
+
* // Get value from "name" attriute if persent, otherwise from name constructor arg.
|
|
3380
|
+
* this.name = getArg(this, 'name', name);
|
|
3381
|
+
*
|
|
3382
|
+
* // Optionally convert the value to an integer.
|
|
3383
|
+
* this.userId = getArg(this, 'userid', userid, ArgType.Int);
|
|
3384
|
+
* }
|
|
3385
|
+
*
|
|
3386
|
+
* @param el {HTMLElement}
|
|
3387
|
+
* @param attributeName {string} Attribute name. Not case-sensitive.
|
|
3388
|
+
* @param defaultValue {*} Default value to use if attribute doesn't exist.
|
|
3389
|
+
* @param type {ArgType|function|Class|*[]}
|
|
3390
|
+
* If an array, use the value if it's in the array, otherwise return undefined.
|
|
3391
|
+
* If it's a function, pass the value to the function and return the result.
|
|
3392
|
+
* @param fallback {*} If the defaultValue is undefiend and type can't be parsed as the given type, use this value.
|
|
3393
|
+
* TODO: Should this be merged with the defaultValue argument?
|
|
3394
|
+
* @return {*} Undefined if attribute isn't set. */
|
|
3395
|
+
function getArg(el, attributeName, defaultValue=undefined, type=ArgType.String, fallback=undefined) {
|
|
3396
|
+
let val = defaultValue;
|
|
3397
|
+
let attrVal = el.getAttribute(attributeName) || el.getAttribute(Util.camelToDashes(attributeName));
|
|
3398
|
+
if (attrVal !== null) // If attribute doesn't exist.
|
|
3399
|
+
val = attrVal;
|
|
3400
|
+
|
|
3401
|
+
if (Array.isArray(type))
|
|
3402
|
+
return type.includes(val) ? val : fallback;
|
|
3403
|
+
|
|
3404
|
+
if (typeof type === 'function') {
|
|
3405
|
+
return type.constructor
|
|
3406
|
+
? new type(val) // arg type is custom Class
|
|
3407
|
+
: type(val); // arg type is custom function
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
// If bool, it's true as long as it exists and its value isn't falsey.
|
|
3411
|
+
if (type===ArgType.Bool) {
|
|
3412
|
+
let lAttrVal = typeof val === 'string' ? val.toLowerCase() : val;
|
|
3413
|
+
if (['false', '0', false, 0, null, undefined, NaN].includes(lAttrVal))
|
|
3414
|
+
return false;
|
|
3415
|
+
if (['true', true].includes(lAttrVal) || parseFloat(lAttrVal) !== 0)
|
|
3416
|
+
return true;
|
|
3417
|
+
return fallback;
|
|
3418
|
+
}
|
|
3419
|
+
|
|
3420
|
+
// Attribute doesn't exist
|
|
3421
|
+
let result;
|
|
3422
|
+
switch (type) {
|
|
3423
|
+
case ArgType.Int:
|
|
3424
|
+
result = parseInt(val);
|
|
3425
|
+
return isNaN(result) ? fallback : result;
|
|
3426
|
+
case ArgType.Float:
|
|
3427
|
+
result = parseFloat(val);
|
|
3428
|
+
return isNaN(result) ? fallback : result;
|
|
3429
|
+
case ArgType.String:
|
|
3430
|
+
return [undefined, null, false].includes(val) ? '' : val+'';
|
|
3431
|
+
case ArgType.Json:
|
|
3432
|
+
case ArgType.Eval:
|
|
3433
|
+
if (typeof val === 'string' && val.length)
|
|
3434
|
+
try {
|
|
3435
|
+
if (type === ArgType.Json)
|
|
3436
|
+
return JSON.parse(val);
|
|
3437
|
+
else
|
|
3438
|
+
return eval(`(${val})`);
|
|
3439
|
+
} catch (e) {
|
|
3440
|
+
return val;
|
|
3441
|
+
}
|
|
3442
|
+
else return val;
|
|
3443
|
+
|
|
3444
|
+
// type not provided
|
|
3445
|
+
default:
|
|
3446
|
+
return val;
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
|
|
3450
|
+
|
|
3451
|
+
/**
|
|
3452
|
+
* Experimental. Set multiple arguments/attributes all at once.
|
|
3453
|
+
* @param el {HTMLElement}
|
|
3454
|
+
* @param args {Record<string, any>}
|
|
3455
|
+
* @param types {Record<string, ArgType|function|Class>}
|
|
3456
|
+
*
|
|
3457
|
+
* @example
|
|
3458
|
+
* constructor({user, path}={}) {
|
|
3459
|
+
* setArgs(this, arguments[0], {user: User, path: ArgType.String});
|
|
3460
|
+
* }
|
|
3461
|
+
*/
|
|
3462
|
+
function setArgs(el, args, types) {
|
|
3463
|
+
for (let name in args)
|
|
3464
|
+
this[name] = getArg(el, name, args[name], types[name] || ArgType.String);
|
|
3465
|
+
}
|
|
3466
|
+
|
|
3467
|
+
|
|
3468
|
+
/**
|
|
3469
|
+
* @enum */
|
|
3470
|
+
var ArgType = {
|
|
3471
|
+
|
|
3472
|
+
/**
|
|
3473
|
+
* false, 0, null, undefined, '0', and 'false' (case-insensitive) become false.
|
|
3474
|
+
* Anything else, including empty string becomes true.
|
|
3475
|
+
* Empty string is true because attributes with no value should be evaulated as true. */
|
|
3476
|
+
Bool: 'Bool',
|
|
3477
|
+
|
|
3478
|
+
Int: 'Int',
|
|
3479
|
+
Float: 'Float',
|
|
3480
|
+
String: 'String',
|
|
3187
3481
|
|
|
3482
|
+
/** @deprecated for Json */
|
|
3483
|
+
JSON: 'Json',
|
|
3188
3484
|
|
|
3485
|
+
/**
|
|
3486
|
+
* Parse the string value as JSON.
|
|
3487
|
+
* If it's not parsable, return the value as a string. */
|
|
3488
|
+
Json: 'Json',
|
|
3189
3489
|
|
|
3490
|
+
/**
|
|
3491
|
+
* Evaluate the string as JavaScript using the eval() function.
|
|
3492
|
+
* If it can't be evaluated, return the original string. */
|
|
3493
|
+
Eval: 'Eval'
|
|
3494
|
+
};
|
|
3495
|
+
|
|
3190
3496
|
function defineClass(Class, tagName, extendsTag) {
|
|
3191
|
-
if (!customElements
|
|
3192
|
-
tagName = tagName || camelToDashes(Class.name);
|
|
3497
|
+
if (!customElements[getName](Class)) { // If not previously defined.
|
|
3498
|
+
tagName = tagName || Util.camelToDashes(Class.name);
|
|
3193
3499
|
if (!tagName.includes('-'))
|
|
3194
3500
|
tagName += '-element';
|
|
3195
3501
|
|
|
@@ -3197,21 +3503,17 @@ function defineClass(Class, tagName, extendsTag) {
|
|
|
3197
3503
|
if (extendsTag)
|
|
3198
3504
|
options = {extends: extendsTag};
|
|
3199
3505
|
|
|
3200
|
-
customElements
|
|
3506
|
+
customElements[define](tagName, Class, options);
|
|
3201
3507
|
}
|
|
3202
3508
|
}
|
|
3203
3509
|
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
3510
|
/**
|
|
3209
3511
|
* Create a version of the Solarite class that extends from the given tag name.
|
|
3210
3512
|
* Reasons to inherit from this instead of HTMLElement. None of these are all that useful.
|
|
3211
3513
|
* 1. customElements.define() is called automatically when you create the first instance.
|
|
3212
3514
|
* 2. Calls render() when added to the DOM, if it hasn't been called already.
|
|
3213
|
-
* 3. Child elements are added before constructor is called. But they're also passed to the constructor.
|
|
3214
|
-
* 4. We can use this.html = r`...` to set html.
|
|
3515
|
+
* 3. Child elements are added before constructor is called. But they're also passed to the constructor. (deprecated?)
|
|
3516
|
+
* 4. We can use this.html = r`...` to set html. (deprecated)
|
|
3215
3517
|
* 5. We have the onConnect, onFirstConnect, and onDisconnect methods.
|
|
3216
3518
|
* Can't figure out how to have these work standalone though, and still be synchronous.
|
|
3217
3519
|
* 6. Can we extend from other element types like TR?
|
|
@@ -3230,10 +3532,10 @@ function createSolarite(extendsTag=null) {
|
|
|
3230
3532
|
if (extendsTag && !extendsTag.includes('-')) {
|
|
3231
3533
|
extendsTag = extendsTag.toLowerCase();
|
|
3232
3534
|
|
|
3233
|
-
BaseClass = Globals.elementClasses[extendsTag];
|
|
3535
|
+
BaseClass = Globals$1.elementClasses[extendsTag];
|
|
3234
3536
|
if (!BaseClass) { // TODO: Use Cache
|
|
3235
3537
|
BaseClass = document.createElement(extendsTag).constructor;
|
|
3236
|
-
Globals.elementClasses[extendsTag] = BaseClass;
|
|
3538
|
+
Globals$1.elementClasses[extendsTag] = BaseClass;
|
|
3237
3539
|
}
|
|
3238
3540
|
}
|
|
3239
3541
|
|
|
@@ -3259,10 +3561,10 @@ function createSolarite(extendsTag=null) {
|
|
|
3259
3561
|
* TODO: Make these standalone functions.
|
|
3260
3562
|
* Callbacks.
|
|
3261
3563
|
* Use onConnect.push(() => ...); to add new callbacks. */
|
|
3262
|
-
onConnect
|
|
3564
|
+
onConnect;
|
|
3263
3565
|
|
|
3264
|
-
onFirstConnect
|
|
3265
|
-
onDisconnect
|
|
3566
|
+
onFirstConnect;
|
|
3567
|
+
onDisconnect;
|
|
3266
3568
|
|
|
3267
3569
|
/**
|
|
3268
3570
|
* @param options {RenderOptions} */
|
|
@@ -3274,27 +3576,28 @@ function createSolarite(extendsTag=null) {
|
|
|
3274
3576
|
this.render();
|
|
3275
3577
|
|
|
3276
3578
|
else if (options.render===false)
|
|
3277
|
-
Globals.rendered.add(this); // Don't render on connectedCallback()
|
|
3579
|
+
Globals$1.rendered.add(this); // Don't render on connectedCallback()
|
|
3278
3580
|
|
|
3279
|
-
// Add children before constructor code executes.
|
|
3280
|
-
//
|
|
3581
|
+
// Add slot children before constructor code executes.
|
|
3582
|
+
// This breaks the styleStaticNested test.
|
|
3583
|
+
// PendingChildren is setup in NodeGroup.instantiateComponent()
|
|
3281
3584
|
// TODO: Match named slots.
|
|
3282
|
-
let ch = Globals.pendingChildren.pop();
|
|
3283
|
-
if (ch)
|
|
3284
|
-
|
|
3585
|
+
//let ch = Globals.pendingChildren.pop();
|
|
3586
|
+
//if (ch) // TODO: how could there be a slot before render is called?
|
|
3587
|
+
// (this.querySelector('slot') || this).append(...ch);
|
|
3285
3588
|
|
|
3286
|
-
/** @deprecated
|
|
3589
|
+
/** @deprecated
|
|
3287
3590
|
Object.defineProperty(this, 'html', {
|
|
3288
3591
|
set(html) {
|
|
3289
3592
|
Globals.rendered.add(this);
|
|
3290
3593
|
if (typeof html === 'string') {
|
|
3291
|
-
console.warn("Assigning to this.html without the r template prefix.")
|
|
3594
|
+
console.warn("Assigning to this.html without the r template prefix.")
|
|
3292
3595
|
this.innerHTML = html;
|
|
3293
3596
|
}
|
|
3294
3597
|
else
|
|
3295
3598
|
this.modifications = r(this, html, options);
|
|
3296
3599
|
}
|
|
3297
|
-
})
|
|
3600
|
+
})*/
|
|
3298
3601
|
|
|
3299
3602
|
/*
|
|
3300
3603
|
let pthis = new Proxy(this, {
|
|
@@ -3309,7 +3612,7 @@ function createSolarite(extendsTag=null) {
|
|
|
3309
3612
|
/**
|
|
3310
3613
|
* Call render() only if it hasn't already been called. */
|
|
3311
3614
|
renderFirstTime() {
|
|
3312
|
-
if (!Globals.rendered.has(this) && this.render)
|
|
3615
|
+
if (!Globals$1.rendered.has(this) && this.render)
|
|
3313
3616
|
this.render();
|
|
3314
3617
|
}
|
|
3315
3618
|
|
|
@@ -3317,279 +3620,30 @@ function createSolarite(extendsTag=null) {
|
|
|
3317
3620
|
* Called automatically by the browser. */
|
|
3318
3621
|
connectedCallback() {
|
|
3319
3622
|
this.renderFirstTime();
|
|
3320
|
-
if (!Globals.connected.has(this)) {
|
|
3321
|
-
Globals.connected.add(this);
|
|
3322
|
-
this.onFirstConnect
|
|
3623
|
+
if (!Globals$1.connected.has(this)) {
|
|
3624
|
+
Globals$1.connected.add(this);
|
|
3625
|
+
if (this.onFirstConnect)
|
|
3626
|
+
this.onFirstConnect();
|
|
3323
3627
|
}
|
|
3324
|
-
this.onConnect
|
|
3628
|
+
if (this.onConnect)
|
|
3629
|
+
this.onConnect();
|
|
3325
3630
|
}
|
|
3326
3631
|
|
|
3327
3632
|
disconnectedCallback() {
|
|
3328
|
-
this.onDisconnect
|
|
3633
|
+
if (this.onDisconnect)
|
|
3634
|
+
this.onDisconnect();
|
|
3329
3635
|
}
|
|
3330
3636
|
|
|
3331
3637
|
|
|
3332
3638
|
static define(tagName=null) {
|
|
3333
3639
|
defineClass(this, tagName, extendsTag);
|
|
3334
3640
|
}
|
|
3335
|
-
|
|
3336
|
-
//#IFDEV
|
|
3337
|
-
|
|
3338
|
-
/** @deprecated */
|
|
3339
|
-
renderWatched() {
|
|
3340
|
-
let ngm = NodeGroupManager.get(this);
|
|
3341
|
-
|
|
3342
|
-
let nodeGroupUpdates = [];
|
|
3343
|
-
|
|
3344
|
-
for (let change of ngm.changes) {
|
|
3345
|
-
if (change.action === 'set') {
|
|
3346
|
-
for (let transformerInfo of change.transformerInfo) {
|
|
3347
|
-
|
|
3348
|
-
let oldHash = transformerInfo.hash;
|
|
3349
|
-
|
|
3350
|
-
let newObj = delve(watchSet(transformerInfo.path[0]), transformerInfo.path.slice(1));
|
|
3351
|
-
let newTemplate = transformerInfo.transformer(newObj);
|
|
3352
|
-
let newHash = getObjectHash(newTemplate);
|
|
3353
|
-
let ngs = [...ngm.nodeGroupsAvailable.data[oldHash]];
|
|
3354
|
-
for (let ng of ngs) {
|
|
3355
|
-
nodeGroupUpdates.push([ng, oldHash, newHash, newTemplate.exprs, transformerInfo]);
|
|
3356
|
-
}
|
|
3357
|
-
}
|
|
3358
|
-
}
|
|
3359
|
-
|
|
3360
|
-
else if (change.action === 'delete') {
|
|
3361
|
-
for (let hash of change.value) {
|
|
3362
|
-
let ngs = [...ngm.nodeGroupsAvailable.getAll(hash)]; // deletes from nodeGroupsAvailable.
|
|
3363
|
-
|
|
3364
|
-
for (let ng of ngs) {
|
|
3365
|
-
if (ng.parentPath)
|
|
3366
|
-
ng.parentPath.clearNodesCache();
|
|
3367
|
-
|
|
3368
|
-
for (let node of ng.getNodes())
|
|
3369
|
-
node.remove();
|
|
3370
|
-
|
|
3371
|
-
// TODO: Update ancestor NodeGroup exactKeys
|
|
3372
|
-
}
|
|
3373
|
-
}
|
|
3374
|
-
}
|
|
3375
|
-
else if (change.action === 'insert') {
|
|
3376
|
-
|
|
3377
|
-
let beforeNg = change.beforeTemplate ? ngm.getNodeGroup(change.beforeTemplate, true) : null;
|
|
3378
|
-
let arrayPath = [change.root, ...change.path];
|
|
3379
|
-
|
|
3380
|
-
// Get anchor so we can use it to get the parent
|
|
3381
|
-
// TODO: Should this be watchGet(change.root) ?
|
|
3382
|
-
for (let loopInfo of ngm.getLoopInfo([change.root, ...change.path.slice(0, -1)])) {
|
|
3383
|
-
|
|
3384
|
-
// Change.extra is aTemplate telling us where to insert before.
|
|
3385
|
-
let beforeNode = beforeNg?.startNode || loopInfo.template.parentPath.nodeMarker;
|
|
3386
|
-
|
|
3387
|
-
// Loop over every item added to the array.
|
|
3388
|
-
let i = 0; // TODO: How to get real insert index.
|
|
3389
|
-
for (let obj of change.value) {
|
|
3390
|
-
|
|
3391
|
-
// Same logic as forEach() function.
|
|
3392
|
-
|
|
3393
|
-
let callback = loopInfo.itemTransformer;
|
|
3394
|
-
let path = [...arrayPath.slice(0, -1), (arrayPath.at(-1) * 1 + i) + ''];
|
|
3395
|
-
|
|
3396
|
-
// Shortened logic found in watchGet(), but not any faster?
|
|
3397
|
-
// the watchSet() is what makes this slower!
|
|
3398
|
-
// let obj = delve(watchSet(path[0]), path.slice(1));
|
|
3399
|
-
// let template = callback(obj);
|
|
3400
|
-
// let serializedPath = serializePath(path);
|
|
3401
|
-
// pathToTransformer.add(serializedPath, new TransformerInfo(path, callback, template)); // Uses a Set() to ensure no duplicates.
|
|
3402
|
-
|
|
3403
|
-
let template = watchGet(path, callback);
|
|
3404
|
-
i++;
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
//let template = loopInfo.itemTransformer(obj); // What if it takes more than one obj argument?
|
|
3408
|
-
|
|
3409
|
-
// Create new NodeGroup
|
|
3410
|
-
let ng = ngm.getNodeGroup(template, false, true);
|
|
3411
|
-
ng.parentPath = beforeNg?.parentPath || loopInfo.template.parentPath;
|
|
3412
|
-
|
|
3413
|
-
for (let node of ng.getNodes())
|
|
3414
|
-
beforeNode.parentNode.insertBefore(node, beforeNode);
|
|
3415
|
-
|
|
3416
|
-
if (ng.parentPath) // This check is needed for the forEachSpliceInsert test, but why?
|
|
3417
|
-
ng.parentPath.clearNodesCache();
|
|
3418
|
-
}
|
|
3419
|
-
|
|
3420
|
-
// TODO: Update ancestor NodeGroup exactKeys
|
|
3421
|
-
}
|
|
3422
|
-
}
|
|
3423
|
-
}
|
|
3424
|
-
|
|
3425
|
-
// Update them all at once, that way we can reassign the same value twice.
|
|
3426
|
-
for (let [ng, oldHash, newHash, exprs, ti] of nodeGroupUpdates) {
|
|
3427
|
-
ng.applyExprs(exprs);
|
|
3428
|
-
ngm.nodeGroupsAvailable.data[oldHash].delete(ng);
|
|
3429
|
-
ng.exactKey = ti.hash = newHash;
|
|
3430
|
-
ngm.nodeGroupsAvailable.add(ng.exactKey, ng); // Add back to Map with new key.
|
|
3431
|
-
}
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
ngm.changes = [];
|
|
3435
|
-
|
|
3436
|
-
return []; // TODO
|
|
3437
|
-
}
|
|
3438
|
-
|
|
3439
|
-
/**
|
|
3440
|
-
* @deprecated Use the getArg() function instead. */
|
|
3441
|
-
getArg(name, val=null, type=ArgType.String) {
|
|
3442
|
-
throw new Error('deprecated');
|
|
3443
|
-
}
|
|
3444
|
-
//#ENDIF
|
|
3445
3641
|
}
|
|
3446
|
-
}
|
|
3447
|
-
|
|
3448
|
-
/**
|
|
3449
|
-
* Trying to be able to automatically watch primitive values.
|
|
3450
|
-
* TODO:
|
|
3451
|
-
* 1. Have get() return Proxies for nested updates.
|
|
3452
|
-
* 2. Override .map() for loops to capture changes.
|
|
3453
|
-
*/
|
|
3454
|
-
|
|
3455
|
-
let unusedArg = Symbol('unusedArg');
|
|
3456
|
-
|
|
3457
|
-
/**
|
|
3458
|
-
* Custom map function triggers the get() Proxy.
|
|
3459
|
-
* @param array {Array}
|
|
3460
|
-
* @param callback {function}
|
|
3461
|
-
* @returns {*[]} */
|
|
3462
|
-
function map(array, callback) {
|
|
3463
|
-
let result = [];
|
|
3464
|
-
for (let i=0; i<array.length; i++)
|
|
3465
|
-
result.push(callback(array[i], i, array));
|
|
3466
|
-
return result;
|
|
3467
|
-
}
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
/**
|
|
3471
|
-
*
|
|
3472
|
-
* @param root {HTMLElement}
|
|
3473
|
-
* @param field {string}
|
|
3474
|
-
* @param value {string|Symbol} */
|
|
3475
|
-
function watch3(root, field, value=unusedArg) {
|
|
3476
|
-
// Store internal value used by get/set.
|
|
3477
|
-
if (value !== unusedArg)
|
|
3478
|
-
root[field] = value;
|
|
3479
|
-
else
|
|
3480
|
-
value = root[field];
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
// use a single object for both defineProperty and new Proxy's handler.
|
|
3484
|
-
const handler = {
|
|
3485
|
-
get(obj, prop, receiver) {
|
|
3486
|
-
|
|
3487
|
-
let result = (obj === receiver && field === prop)
|
|
3488
|
-
? value // top-level value.
|
|
3489
|
-
: Reflect.get(obj, prop, receiver); // avoid infinite recursion.
|
|
3490
|
-
|
|
3491
|
-
if (prop === 'map')
|
|
3492
|
-
|
|
3493
|
-
// Double function so the ExprPath calls it as a function,
|
|
3494
|
-
// instead of it being evaluated immediately when the Templat eis created.
|
|
3495
|
-
return (callback) => () => {
|
|
3496
|
-
let rootNg = Globals.nodeGroups.get(root);
|
|
3497
|
-
rootNg.mapCallbacks.set(obj, callback);
|
|
3498
|
-
return map(new Proxy(obj, handler), callback);
|
|
3499
|
-
}
|
|
3500
|
-
|
|
3501
|
-
// Track which ExprPath is using this variable.
|
|
3502
|
-
if (Globals.currentExprPath) {
|
|
3503
|
-
let [exprPath, exprFunction] = Globals.currentExprPath; // Set in ExprPath.applyExact()
|
|
3504
|
-
|
|
3505
|
-
let rootNg = Globals.nodeGroups.get(root);
|
|
3506
|
-
|
|
3507
|
-
// Init for field.
|
|
3508
|
-
rootNg.watchedExprPaths[field] = rootNg.watchedExprPaths[field] || new Set();
|
|
3509
|
-
rootNg.watchedExprPaths[field].add(exprPath);
|
|
3510
|
-
}
|
|
3511
|
-
|
|
3512
|
-
if (result && typeof result === 'object')
|
|
3513
|
-
return new Proxy(result, handler);
|
|
3514
|
-
|
|
3515
|
-
return result;
|
|
3516
|
-
},
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
// TODO: Will fail for attribute w/ a value having multiple ExprPaths.
|
|
3520
|
-
// TODO: This won't update a component's expressions.
|
|
3521
|
-
set(obj, prop, val, receiver) {
|
|
3522
|
-
|
|
3523
|
-
// 1. Set the value.
|
|
3524
|
-
if (obj === receiver && field === prop)
|
|
3525
|
-
value = val; // top-level value.
|
|
3526
|
-
else // avoid infinite recursion.
|
|
3527
|
-
Reflect.set(obj, prop, val, receiver);
|
|
3528
|
-
|
|
3529
|
-
// 2. Add to the list of ExprPaths to re-render.
|
|
3530
|
-
let rootNg = Globals.nodeGroups.get(root);
|
|
3531
|
-
for (let exprPath of rootNg.watchedExprPaths[field]) {
|
|
3532
|
-
|
|
3533
|
-
// Update a single NodeGroup created by array.map()
|
|
3534
|
-
if (Array.isArray(obj) && parseInt(prop) == prop) {
|
|
3535
|
-
let exprsToRender = rootNg.exprsToRender.get(exprPath);
|
|
3536
|
-
|
|
3537
|
-
// If we're not re-rendering the whole thing.
|
|
3538
|
-
if (exprsToRender !== true)
|
|
3539
|
-
Util$1.mapAdd(rootNg.exprsToRender, exprPath, [obj, prop, val]);
|
|
3540
|
-
}
|
|
3541
|
-
|
|
3542
|
-
// Reapply the whole expression.
|
|
3543
|
-
else
|
|
3544
|
-
rootNg.exprsToRender.set(exprPath, true);
|
|
3545
|
-
}
|
|
3546
|
-
return true;
|
|
3547
|
-
}
|
|
3548
|
-
};
|
|
3549
|
-
|
|
3550
|
-
Object.defineProperty(root, field, {
|
|
3551
|
-
get: () => handler.get(root, field, root),
|
|
3552
|
-
set: (val) => handler.set(root, field, val, root)
|
|
3553
|
-
});
|
|
3554
3642
|
}
|
|
3555
3643
|
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
* @returns {*[]} */
|
|
3560
|
-
function renderWatched(root) {
|
|
3561
|
-
let rootNg = Globals.nodeGroups.get(root);
|
|
3562
|
-
let modified = [];
|
|
3563
|
-
|
|
3564
|
-
for (let [exprPath, params] of rootNg.exprsToRender) {
|
|
3565
|
-
|
|
3566
|
-
// Reapply the whole expression.
|
|
3567
|
-
if (params === true) {
|
|
3568
|
-
exprPath.apply(exprPath.watchFunction);
|
|
3569
|
-
|
|
3570
|
-
// TODO: freeNodeGroups() could be skipped if applyExprs() never marked them as in-use.
|
|
3571
|
-
exprPath.freeNodeGroups();
|
|
3572
|
-
|
|
3573
|
-
modified.push(...exprPath.getNodes());
|
|
3574
|
-
}
|
|
3575
|
-
|
|
3576
|
-
// Update a single NodeGroup created by array.map()
|
|
3577
|
-
else {
|
|
3578
|
-
for (let row of params) {
|
|
3579
|
-
let [obj, prop, value] = row;
|
|
3580
|
-
let callback = rootNg.mapCallbacks.get(obj);
|
|
3581
|
-
let template = callback(value);
|
|
3582
|
-
exprPath.applyLoopItemUpdate(prop, template);
|
|
3583
|
-
|
|
3584
|
-
modified.push(...exprPath.nodeGroups[prop].getNodes());
|
|
3585
|
-
}
|
|
3586
|
-
}
|
|
3587
|
-
}
|
|
3588
|
-
|
|
3589
|
-
rootNg.exprsToRender = new Map(); // clear
|
|
3590
|
-
|
|
3591
|
-
return modified;
|
|
3592
|
-
}
|
|
3644
|
+
// Trick to prevent minifier from renaming this method.
|
|
3645
|
+
let define = 'define';
|
|
3646
|
+
let getName = 'getName';
|
|
3593
3647
|
|
|
3594
3648
|
/**
|
|
3595
3649
|
* Solarite JavasCript UI library.
|
|
@@ -3600,12 +3654,13 @@ function renderWatched(root) {
|
|
|
3600
3654
|
/**
|
|
3601
3655
|
* TODO: The Proxy and the multiple base classes mess up 'instanceof Solarite'
|
|
3602
3656
|
* @type {Node|Class<HTMLElement>|function(tagName:string):Node|Class<HTMLElement>} */
|
|
3603
|
-
|
|
3657
|
+
const Solarite = new Proxy(createSolarite(), {
|
|
3604
3658
|
apply(self, _, args) {
|
|
3605
3659
|
return createSolarite(...args)
|
|
3606
3660
|
}
|
|
3607
3661
|
});
|
|
3608
|
-
|
|
3609
|
-
// unfinished
|
|
3662
|
+
|
|
3663
|
+
//export {default as watch, renderWatched} from './watch.js'; // unfinished
|
|
3610
3664
|
|
|
3611
|
-
export
|
|
3665
|
+
export default h;
|
|
3666
|
+
export { ArgType, Globals$1 as Globals, Solarite, Util as SolariteUtil, Template, delve, getArg, h, h as r, setArgs };
|