solarite 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Solarite-debug.js +1671 -1492
- package/dist/Solarite.js +1536 -1259
- package/dist/Solarite.min.js +2 -2
- package/package.json +1 -1
- package/readme.md +1 -3
- package/src/solarite/ExprPath.js +426 -210
- package/src/solarite/Globals.js +77 -52
- package/src/solarite/HtmlParser.js +91 -0
- package/src/solarite/NodeGroup.js +275 -219
- package/src/solarite/Shell.js +117 -91
- package/src/solarite/Solarite.js +7 -3
- package/src/solarite/Template.js +21 -18
- package/src/solarite/Util.js +117 -179
- package/src/solarite/createSolarite.js +16 -138
- package/src/solarite/getArg.js +17 -12
- package/src/solarite/{r.js → h.js} +56 -18
- package/src/solarite/hash.js +12 -9
- package/src/solarite/watch.js +546 -0
- package/src/util/Errors.js +1 -0
- package/src/util/MultiValueMap.js +42 -15
- package/src/util/Util.js +5 -17
- package/src/util/delve.js +5 -4
- package/src/solarite/watch3.js +0 -189
- package/src/util/WeakArray.js +0 -33
package/dist/Solarite-debug.js
CHANGED
|
@@ -78,43 +78,110 @@ var Util$1 = {
|
|
|
78
78
|
},
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
|
+
* Use an array as the value of a map, appending to it when we add.
|
|
82
|
+
* Used by watch.js.
|
|
81
83
|
* @param map {Map|WeakMap|Object}
|
|
82
84
|
* @param key
|
|
83
85
|
* @param value */
|
|
84
|
-
|
|
85
|
-
let
|
|
86
|
-
let result = isMap ? map.get(key) : map[key];
|
|
86
|
+
mapArrayAdd(map, key, value) {
|
|
87
|
+
let result = map.get(key);
|
|
87
88
|
if (!result) {
|
|
88
89
|
result = [value];
|
|
89
|
-
|
|
90
|
-
map.set(key, result);
|
|
91
|
-
else
|
|
92
|
-
map[key] = result;
|
|
90
|
+
map.set(key, result);
|
|
93
91
|
}
|
|
94
92
|
else
|
|
95
93
|
result.push(value);
|
|
96
94
|
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
var Globals;
|
|
97
98
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Created with a reset() function because it's useful for testing. */
|
|
101
|
+
function reset() {
|
|
102
|
+
Globals = {
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Used by NodeGroup.applyComponentExprs() */
|
|
106
|
+
componentArgsHash: new WeakMap(),
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Store which instances of Solarite have already been added to the DOM.
|
|
110
|
+
* @type {WeakSet<HTMLElement>} */
|
|
111
|
+
connected: new WeakSet(),
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* ExprPath.applyExactNodes() sets this property when an expression is being accessed.
|
|
115
|
+
* watch() then adds the ExprPath to the list of ExprPaths that should be re-rendered when the value changes.
|
|
116
|
+
* @type {ExprPath}*/
|
|
117
|
+
currentExprPath: null,
|
|
118
|
+
|
|
119
|
+
div: document.createElement("div"),
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @type {Object<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
|
|
123
|
+
elementClasses: {},
|
|
124
|
+
|
|
125
|
+
/** @type {Object<string, boolean>} Key is tag-name.propName. Value is whether it's an attribute.*/
|
|
126
|
+
htmlProps: {},
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Used by ExprPath.applyEventAttrib()
|
|
130
|
+
* @type {WeakMap<Node, Object<eventName:string, [original:function, bound:function, args:*[]]>>} */
|
|
131
|
+
nodeEvents: new WeakMap(),
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get the RootNodeGroup for an element.
|
|
135
|
+
* @type {WeakMap<HTMLElement, RootNodeGroup>} */
|
|
136
|
+
nodeGroups: new WeakMap(),
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Used by r() path 9. */
|
|
140
|
+
objToEl: new WeakMap(),
|
|
141
|
+
|
|
142
|
+
//pendingChildren: [],
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Elements that have been rendered to by r() at least once.
|
|
147
|
+
* This is used by the Solarite class to know when to call onFirstConnect()
|
|
148
|
+
* @type {WeakSet<HTMLElement>} */
|
|
149
|
+
rendered: new WeakSet(),
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Elements that are currently rendering via the r() function.
|
|
153
|
+
* @type {WeakSet<HTMLElement>} */
|
|
154
|
+
rendering: new WeakSet(),
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Map from array of Html strings to a Shell created from them.
|
|
158
|
+
* @type {WeakMap<string[], Shell>} */
|
|
159
|
+
shells: new WeakMap(),
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* A map of individual untagged strings to their Templates.
|
|
163
|
+
* This way we don't keep creating new Templates for the same string when re-rendering.
|
|
164
|
+
* This is used by ExprPath.applyExactNodes()
|
|
165
|
+
* @type {Object<string, Template>} */
|
|
166
|
+
//stringTemplates: {},
|
|
167
|
+
|
|
168
|
+
reset,
|
|
107
169
|
|
|
108
|
-
|
|
170
|
+
count: 0
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
reset();
|
|
174
|
+
|
|
175
|
+
var Globals$1 = Globals;
|
|
109
176
|
|
|
110
177
|
/**
|
|
111
178
|
* Follow a path into an object.
|
|
112
179
|
* @param obj {object}
|
|
113
180
|
* @param path {string[]}
|
|
114
|
-
* @param createVal {*} If set, non-
|
|
181
|
+
* @param createVal {*} If set, non-existent paths will be created and value at path will be set to createVal.
|
|
115
182
|
* @return {*} The value, or undefined if it can't be reached. */
|
|
116
|
-
function delve(obj, path, createVal =
|
|
117
|
-
let isCreate = createVal !==
|
|
183
|
+
function delve(obj, path, createVal = d) {
|
|
184
|
+
let isCreate = createVal !== d;
|
|
118
185
|
|
|
119
186
|
let len = path.length;
|
|
120
187
|
if (!obj && !isCreate && len)
|
|
@@ -149,264 +216,27 @@ function delve(obj, path, createVal = delveDontCreate) {
|
|
|
149
216
|
return obj;
|
|
150
217
|
}
|
|
151
218
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* There are three ways to create an instance of a Solarite Component:
|
|
156
|
-
* 1. new ComponentName(); // direct class instantiation
|
|
157
|
-
* 2. this.html = r`<div><component-name></component-name></div>; // as a child of another RedComponent.
|
|
158
|
-
* 3. <body><component-name></component-name></body> // in the Document html.
|
|
159
|
-
*
|
|
160
|
-
* When created via #3, Solarite has no way to pass attributes as arguments to the constructor. So to make
|
|
161
|
-
* sure we get the correct value via all three paths, we write our constructors according to the following
|
|
162
|
-
* example. Note that constructor args are embedded in an object, and must be all lower-case because
|
|
163
|
-
* Browsers make all html attribute names lowercase.
|
|
164
|
-
*
|
|
165
|
-
* @example
|
|
166
|
-
* constructor({name, userid=1}={}) {
|
|
167
|
-
* super();
|
|
168
|
-
*
|
|
169
|
-
* // Get value from "name" attriute if persent, otherwise from name constructor arg.
|
|
170
|
-
* this.name = getArg(this, 'name', name);
|
|
171
|
-
*
|
|
172
|
-
* // Optionally convert the value to an integer.
|
|
173
|
-
* this.userId = getArg(this, 'userid', userid, ArgType.Int);
|
|
174
|
-
* }
|
|
175
|
-
*
|
|
176
|
-
* @param el {HTMLElement}
|
|
177
|
-
* @param name {string} Attribute name. Not case-sensitive.
|
|
178
|
-
* @param val {*} Default value to use if attribute doesn't exist.
|
|
179
|
-
* @param type {ArgType|function|*[]}
|
|
180
|
-
* If an array, use the value if it's in the array, otherwise return undefined.
|
|
181
|
-
* If it's a function, pass the value to the function and return the result.
|
|
182
|
-
* @param fallback {*} If the type can't be parsed as the given type, use this value.
|
|
183
|
-
* @return {*} Undefined if attribute isn't set. */
|
|
184
|
-
function getArg(el, name, val=undefined, type=ArgType.String, fallback=undefined) {
|
|
185
|
-
let attrVal = el.getAttribute(name);
|
|
186
|
-
if (attrVal !== null) // If attribute doesn't exist.
|
|
187
|
-
val = attrVal;
|
|
188
|
-
|
|
189
|
-
if (Array.isArray(type))
|
|
190
|
-
return type.includes(val) ? val : fallback;
|
|
191
|
-
|
|
192
|
-
if (typeof type === 'function')
|
|
193
|
-
return type(val);
|
|
194
|
-
|
|
195
|
-
// If bool, it's true as long as it exists and its value isn't falsey.
|
|
196
|
-
if (type===ArgType.Bool) {
|
|
197
|
-
let lAttrVal = typeof val === 'string' ? val.toLowerCase() : val;
|
|
198
|
-
if (['false', '0', false, 0, null, undefined, NaN].includes(lAttrVal))
|
|
199
|
-
return false;
|
|
200
|
-
if (['true', true].includes(lAttrVal) || parseFloat(lAttrVal) !== 0)
|
|
201
|
-
return true;
|
|
202
|
-
return fallback;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Attribute doesn't exist
|
|
206
|
-
let result;
|
|
207
|
-
switch (type) {
|
|
208
|
-
case ArgType.Int:
|
|
209
|
-
result = parseInt(val);
|
|
210
|
-
return isNaN(result) ? fallback : result;
|
|
211
|
-
case ArgType.Float:
|
|
212
|
-
result = parseFloat(val);
|
|
213
|
-
return isNaN(result) ? fallback : result;
|
|
214
|
-
case ArgType.String:
|
|
215
|
-
return [undefined, null, false].includes(val) ? '' : val+'';
|
|
216
|
-
case ArgType.JSON:
|
|
217
|
-
case ArgType.Eval:
|
|
218
|
-
if (typeof val === 'string' && val.length)
|
|
219
|
-
try {
|
|
220
|
-
if (type === ArgType.JSON)
|
|
221
|
-
return JSON.parse(val);
|
|
222
|
-
else
|
|
223
|
-
return eval(`(${val})`);
|
|
224
|
-
} catch (e) {
|
|
225
|
-
return val;
|
|
226
|
-
}
|
|
227
|
-
else return fallback;
|
|
228
|
-
|
|
229
|
-
// type not provided
|
|
230
|
-
default:
|
|
231
|
-
return val;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* @enum */
|
|
237
|
-
var ArgType = {
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* false, 0, null, undefined, '0', and 'false' (case-insensitive) become false.
|
|
241
|
-
* Anything else, including empty string becomes true.
|
|
242
|
-
* Empty string is true because attributes with no value should be evaulated as true. */
|
|
243
|
-
Bool: 'Bool',
|
|
244
|
-
|
|
245
|
-
Int: 'Int',
|
|
246
|
-
Float: 'Float',
|
|
247
|
-
String: 'String',
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Parse the string value as JSON.
|
|
251
|
-
* If it's not parsable, return the value as a string. */
|
|
252
|
-
JSON: 'JSON',
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Evaluate the string as JavaScript using the eval() function.
|
|
256
|
-
* If it can't be evaluated, return the original string. */
|
|
257
|
-
Eval: 'Eval'
|
|
258
|
-
};
|
|
219
|
+
// d means "don't create"
|
|
220
|
+
let d = {};
|
|
259
221
|
|
|
260
|
-
let
|
|
261
|
-
let objectIds = new WeakMap();
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* @param obj {Object|string|Node}
|
|
265
|
-
* @returns {string} */
|
|
266
|
-
function getObjectId(obj) {
|
|
267
|
-
// if (typeof obj === 'function')
|
|
268
|
-
// return obj.toString(); // This fails to detect when a function's bound variables changes.
|
|
269
|
-
|
|
270
|
-
let result = objectIds.get(obj);
|
|
271
|
-
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()
|
|
273
|
-
objectIds.set(obj, result);
|
|
274
|
-
}
|
|
275
|
-
return result;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Control how JSON.stringify() handles Nodes and Functions.
|
|
280
|
-
* Normally, we'd pass a replacer() function argument to JSON.stringify() to handle Nodes and Functions.
|
|
281
|
-
* But that makes JSON.stringify() take twice as long to run.
|
|
282
|
-
* Adding a toJSON method globally on these object prototypes doesn't incur that performance penalty. */
|
|
283
|
-
let isHashing = true;
|
|
284
|
-
function toJSON() {
|
|
285
|
-
return isHashing ? getObjectId(this) : this
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
// Node.prototype.toJSON = toJSON;
|
|
290
|
-
// Function.prototype.toJSON = toJSON;
|
|
291
|
-
// Sometimes these get unassigned by Chrome and Brave 119, as well as Firefox, seemingly randomly!
|
|
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
|
-
}
|
|
222
|
+
let Util = {
|
|
299
223
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
* This is used by NodeGroupManager to create a hash that represents the current values of a NodeGroup.
|
|
304
|
-
*
|
|
305
|
-
* Relies on the Node and Function prototypes being overridden above.
|
|
306
|
-
*
|
|
307
|
-
* Note that passing an integer may collide with the number we get from hashing an object.
|
|
308
|
-
* But we don't handle that case because we need max performance and Solarite never passes integers to this function.
|
|
309
|
-
*
|
|
310
|
-
* @param obj {*}
|
|
311
|
-
* @returns {string} */
|
|
312
|
-
function getObjectHash(obj) {
|
|
313
|
-
let result;
|
|
314
|
-
isHashing = true;
|
|
315
|
-
try {
|
|
316
|
-
result = JSON.stringify(obj);
|
|
317
|
-
}
|
|
318
|
-
catch(e) {
|
|
319
|
-
result = getObjectHashCircular(obj);
|
|
320
|
-
}
|
|
321
|
-
isHashing = false;
|
|
322
|
-
return result;
|
|
323
|
-
}
|
|
224
|
+
bindId(root, el) {
|
|
225
|
+
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
226
|
+
if (id) { // If something hasn't removed the id.
|
|
324
227
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
function getObjectHashCircular(obj) {
|
|
228
|
+
// Don't allow overwriting existing class properties if they already have a non-Node value.
|
|
229
|
+
if (root[id] && !(root[id] instanceof Node))
|
|
230
|
+
throw new Error(`${root.constructor.name}.${id} already has a value. ` +
|
|
231
|
+
`Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
|
|
330
232
|
|
|
331
|
-
|
|
332
|
-
// Slower version that handles circular references.
|
|
333
|
-
// Just adding any callback at all, even one that just returns the value, makes JSON.stringify() twice as slow.
|
|
334
|
-
const seen = new Set();
|
|
335
|
-
return JSON.stringify(obj, (key, value) => {
|
|
336
|
-
if (typeof value === 'object' && value !== null) {
|
|
337
|
-
if (seen.has(value))
|
|
338
|
-
return getObjectId(value);
|
|
339
|
-
seen.add(value);
|
|
233
|
+
delve(root, id.split(/\./g), el);
|
|
340
234
|
}
|
|
341
|
-
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
//#IFDEV
|
|
346
|
-
/*@__NO_SIDE_EFFECTS__*/
|
|
347
|
-
function assert(val) {
|
|
348
|
-
if (!val) {
|
|
349
|
-
debugger;
|
|
350
|
-
throw new Error('Assertion failed: ' + val);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
//#ENDIF
|
|
354
|
-
|
|
355
|
-
var Globals = {
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Used by NodeGroup.applyComponentExprs() */
|
|
359
|
-
componentHash: new WeakMap(),
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* Store which instances of Solarite have already been added to the DOM.
|
|
363
|
-
* @type {WeakSet<HTMLElement>} */
|
|
364
|
-
connected: new WeakSet(),
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Elements that have been rendered to by r() at least once.
|
|
368
|
-
* This is used by the Solarite class to know when to call onFirstConnect()
|
|
369
|
-
* @type {WeakSet<HTMLElement>} */
|
|
370
|
-
rendered: new WeakSet(),
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Used by watch3 to see which expressions are being accessed.
|
|
374
|
-
* @type {[]}*/
|
|
375
|
-
currentExprPath: null,
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* @type {Object<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
|
|
379
|
-
elementClasses: {},
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* Used by ExprPath.applyEventAttrib()
|
|
383
|
-
* @type {WeakMap<Node, Object<eventName:string, [original:function, bound:function, args:*[]]>>} */
|
|
384
|
-
nodeEvents: new WeakMap(),
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Get the RootNodeGroup for an element.
|
|
388
|
-
* @type {WeakMap<HTMLElement, RootNodeGroup>} */
|
|
389
|
-
nodeGroups: new WeakMap(),
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Used by r() path 9. */
|
|
393
|
-
objToEl: new WeakMap(),
|
|
394
|
-
|
|
395
|
-
pendingChildren: [],
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Elements that are currently rendering via the r() function.
|
|
399
|
-
* @type {WeakSet<HTMLElement>} */
|
|
400
|
-
rendering: new WeakSet(),
|
|
235
|
+
},
|
|
401
236
|
|
|
402
237
|
/**
|
|
403
|
-
*
|
|
404
|
-
* @
|
|
405
|
-
shells: new WeakMap()
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
let Util = {
|
|
409
|
-
|
|
238
|
+
* @param style {HTMLStyleElement}
|
|
239
|
+
* @param root {HTMLElement} */
|
|
410
240
|
bindStyles(style, root) {
|
|
411
241
|
let styleId = root.getAttribute('data-style');
|
|
412
242
|
if (!styleId) {
|
|
@@ -419,25 +249,68 @@ let Util = {
|
|
|
419
249
|
root.setAttribute('data-style', styleId);
|
|
420
250
|
}
|
|
421
251
|
|
|
252
|
+
// Replace ":host" with "tagName[data-style=...]" in the css.
|
|
422
253
|
let tagName = root.tagName.toLowerCase();
|
|
423
254
|
for (let child of style.childNodes) {
|
|
424
255
|
if (child.nodeType === 3) {
|
|
425
256
|
let oldText = child.textContent;
|
|
426
|
-
let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, tagName
|
|
257
|
+
let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, `${tagName}[data-style="${styleId}"]`);
|
|
427
258
|
if (oldText !== newText)
|
|
428
259
|
child.textContent = newText;
|
|
429
260
|
}
|
|
430
261
|
}
|
|
431
262
|
},
|
|
432
263
|
|
|
264
|
+
|
|
433
265
|
/**
|
|
434
|
-
*
|
|
266
|
+
* Convert a Proper Case name to a name with dashes.
|
|
267
|
+
* Dashes will be placed between letters and numbers.
|
|
268
|
+
* If there are multiple consecutive capital letters followed by another chracater, a dash will be placed before the last capital letter.
|
|
269
|
+
* @param str {string}
|
|
270
|
+
* @return {string}
|
|
435
271
|
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
272
|
+
* @example
|
|
273
|
+
* 'ProperName' => 'proper-name'
|
|
274
|
+
* 'HTMLElement' => 'html-element'
|
|
275
|
+
* 'BigUI' => 'big-ui'
|
|
276
|
+
* 'UIForm' => 'ui-form'
|
|
277
|
+
* 'A100' => 'a-100' */
|
|
278
|
+
camelToDashes(str) {
|
|
279
|
+
// Convert any capital letter that is preceded by a lowercase letter or number to lowercase and precede with a dash.
|
|
280
|
+
str = str.replace(/([a-z0-9])([A-Z])/g, '$1-$2');
|
|
281
|
+
|
|
282
|
+
// Convert any capital letter that is followed by a lowercase letter or number to lowercase and precede with a dash.
|
|
283
|
+
str = str.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2');
|
|
284
|
+
|
|
285
|
+
// Convert any number that is preceded by a lowercase or uppercase letter to be preceded by a dash.
|
|
286
|
+
str = str.replace(/([a-zA-Z])([0-9])/g, '$1-$2');
|
|
287
|
+
|
|
288
|
+
// Convert all the remaining capital letters to lowercase.
|
|
289
|
+
return str.toLowerCase();
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Converts a string written in kebab-case to camelCase.
|
|
294
|
+
*
|
|
295
|
+
* @param {string} str - The input string written in kebab-case.
|
|
296
|
+
* @return {string} - The resulting camelCase string.
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* dashesToCamel('example-string') // Returns 'exampleString'
|
|
300
|
+
* dashesToCamel('another-example-test') // Returns 'anotherExampleTest' */
|
|
301
|
+
dashesToCamel(str) {
|
|
302
|
+
return str.replace(/-([a-z])/g, g => g[1].toUpperCase());
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* A generator function that recursively traverses and flattens a value.
|
|
308
|
+
*
|
|
309
|
+
* - If the input is an array, it recursively traverses and flattens the array.
|
|
310
|
+
* - If the input is a function, it calls the function, replaces the function
|
|
311
|
+
* with its result, and flattens the result if necessary. It will recursively
|
|
312
|
+
* call functions that return other functions.
|
|
313
|
+
* - Otherwise it yields the value as is.
|
|
441
314
|
*
|
|
442
315
|
* This function does not create a new array for the flattened values. Instead,
|
|
443
316
|
* it lazily yields each item as it is encountered. This can be more memory-efficient
|
|
@@ -473,6 +346,7 @@ let Util = {
|
|
|
473
346
|
* @param node {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|HTMLDivElement}
|
|
474
347
|
* @return {string|string[]|number|[]|File[]|Date|boolean} */
|
|
475
348
|
getInputValue(node) {
|
|
349
|
+
// .type is a built-in DOM property
|
|
476
350
|
if (node.type === 'checkbox' || node.type === 'radio')
|
|
477
351
|
return node.checked; // Boolean
|
|
478
352
|
if (node.type === 'file')
|
|
@@ -487,48 +361,63 @@ let Util = {
|
|
|
487
361
|
return node.value; // String
|
|
488
362
|
},
|
|
489
363
|
|
|
364
|
+
/**
|
|
365
|
+
* @param el {HTMLElement}
|
|
366
|
+
* @param prop {string}
|
|
367
|
+
* @returns {boolean} */
|
|
368
|
+
isHtmlProp(el, prop) {
|
|
369
|
+
let key = el.tagName + '.' + prop;
|
|
370
|
+
let result = Globals$1.htmlProps[key];
|
|
371
|
+
if (result === undefined) { // Caching just barely makes this slightly faster.
|
|
372
|
+
let proto = Object.getPrototypeOf(el);
|
|
373
|
+
|
|
374
|
+
// Find the first HTMLElement that we inherit from (not our own classes)
|
|
375
|
+
while (proto) {
|
|
376
|
+
const ctorName = proto.constructor.name;
|
|
377
|
+
if (ctorName.startsWith('HTML') && ctorName.endsWith('Element'))
|
|
378
|
+
break
|
|
379
|
+
proto = Object.getPrototypeOf(proto);
|
|
380
|
+
}
|
|
381
|
+
Globals$1.htmlProps[key] = result = (proto
|
|
382
|
+
? !!Object.getOwnPropertyDescriptor(proto, prop)?.set
|
|
383
|
+
: false);
|
|
384
|
+
}
|
|
385
|
+
return result;
|
|
386
|
+
},
|
|
387
|
+
|
|
490
388
|
/**
|
|
491
389
|
* Is it an array and a path that can be evaluated by delve() ?
|
|
390
|
+
* We allow the first element to be null/undefined so binding can report errors.
|
|
492
391
|
* @param arr {Array|*}
|
|
493
392
|
* @returns {boolean} */
|
|
494
393
|
isPath(arr) {
|
|
495
|
-
return Array.isArray(arr) &&
|
|
394
|
+
return Array.isArray(arr) && arr.length >=2 // An array of at least two elements.
|
|
395
|
+
&& (typeof arr[0] === 'object' || arr[0] === undefined) // Where the first element is an object, null, or undefined.
|
|
396
|
+
&& !arr.slice(1).find(p => typeof p !== 'string' && typeof p !== 'number'); // Path 1..x is only numbers and strings.
|
|
397
|
+
},
|
|
398
|
+
|
|
399
|
+
isFalsy(val) {
|
|
400
|
+
return val === undefined || val === false || val === null;
|
|
401
|
+
},
|
|
402
|
+
|
|
403
|
+
isPrimitive(val) {
|
|
404
|
+
return typeof val === 'string' || typeof val === 'number'
|
|
496
405
|
},
|
|
497
406
|
|
|
498
407
|
/**
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
}
|
|
531
|
-
}
|
|
408
|
+
* If val is a function, evaluate it recursively until the result is not a function.
|
|
409
|
+
* If it's an array or an object, convert it to Json.
|
|
410
|
+
* If it's a Date, format it as Y-m-d H:i:s
|
|
411
|
+
* @param val
|
|
412
|
+
* @returns {string|number|boolean} */
|
|
413
|
+
makePrimitive(val) {
|
|
414
|
+
if (typeof val === 'function')
|
|
415
|
+
return Util.makePrimitive(val());
|
|
416
|
+
else if (val instanceof Date)
|
|
417
|
+
return val.toISOString().replace(/T/, ' ');
|
|
418
|
+
else if (Array.isArray(val) || typeof val === 'object')
|
|
419
|
+
return ''; // JSON.stringify(val);
|
|
420
|
+
return val;
|
|
532
421
|
},
|
|
533
422
|
|
|
534
423
|
/**
|
|
@@ -559,44 +448,14 @@ let Util = {
|
|
|
559
448
|
|
|
560
449
|
|
|
561
450
|
|
|
562
|
-
let
|
|
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
|
-
}
|
|
451
|
+
let isEvent = attrName => attrName.startsWith('on') && attrName in Globals$1.div;
|
|
593
452
|
|
|
594
453
|
|
|
595
454
|
|
|
596
455
|
|
|
597
456
|
|
|
598
457
|
/**
|
|
599
|
-
* Returns
|
|
458
|
+
* Returns true if they're the same.
|
|
600
459
|
* @param a
|
|
601
460
|
* @param b
|
|
602
461
|
* @returns {boolean} */
|
|
@@ -611,80 +470,7 @@ function arraySame(a, b) {
|
|
|
611
470
|
}
|
|
612
471
|
|
|
613
472
|
|
|
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
473
|
|
|
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
474
|
|
|
689
475
|
|
|
690
476
|
// For debugging only
|
|
@@ -749,131 +535,406 @@ function flattenAndIndent(inputArray, indent = "") {
|
|
|
749
535
|
}
|
|
750
536
|
//#ENDIF
|
|
751
537
|
|
|
752
|
-
|
|
538
|
+
function defineClass(Class, tagName, extendsTag) {
|
|
539
|
+
if (!customElements[getName](Class)) { // If not previously defined.
|
|
540
|
+
tagName = tagName || Util.camelToDashes(Class.name);
|
|
541
|
+
if (!tagName.includes('-'))
|
|
542
|
+
tagName += '-element';
|
|
753
543
|
|
|
754
|
-
|
|
755
|
-
|
|
544
|
+
let options = null;
|
|
545
|
+
if (extendsTag)
|
|
546
|
+
options = {extends: extendsTag};
|
|
756
547
|
|
|
757
|
-
|
|
758
|
-
add(key, value) {
|
|
759
|
-
let data = this.data;
|
|
760
|
-
let set = data[key];
|
|
761
|
-
if (!set) {
|
|
762
|
-
set = new Set();
|
|
763
|
-
data[key] = set;
|
|
764
|
-
}
|
|
765
|
-
set.add(value);
|
|
548
|
+
customElements[define](tagName, Class, options);
|
|
766
549
|
}
|
|
550
|
+
}
|
|
767
551
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
552
|
+
/**
|
|
553
|
+
* Create a version of the Solarite class that extends from the given tag name.
|
|
554
|
+
* Reasons to inherit from this instead of HTMLElement. None of these are all that useful.
|
|
555
|
+
* 1. customElements.define() is called automatically when you create the first instance.
|
|
556
|
+
* 2. Calls render() when added to the DOM, if it hasn't been called already.
|
|
557
|
+
* 3. Child elements are added before constructor is called. But they're also passed to the constructor. (deprecated?)
|
|
558
|
+
* 4. We can use this.html = r`...` to set html. (deprecated)
|
|
559
|
+
* 5. We have the onConnect, onFirstConnect, and onDisconnect methods.
|
|
560
|
+
* Can't figure out how to have these work standalone though, and still be synchronous.
|
|
561
|
+
* 6. Can we extend from other element types like TR?
|
|
562
|
+
* 7. Shows default text if render() function isn't defined.
|
|
563
|
+
*
|
|
564
|
+
* Advantages to inheriting from HTMLElement
|
|
565
|
+
* 1. Minimization won't break when it renames the Class and we call customElements.define() on the wrong name.
|
|
566
|
+
* 2. We can inherit from things like HTMLTableRowElement directly.
|
|
567
|
+
* 3. There's less magic, since everyone is familiar with defining custom elements.
|
|
568
|
+
*
|
|
569
|
+
* @param extendsTag {?string}
|
|
570
|
+
* @return {Class} */
|
|
571
|
+
function createSolarite(extendsTag=null) {
|
|
773
572
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
573
|
+
let BaseClass = HTMLElement;
|
|
574
|
+
if (extendsTag && !extendsTag.includes('-')) {
|
|
575
|
+
extendsTag = extendsTag.toLowerCase();
|
|
576
|
+
|
|
577
|
+
BaseClass = Globals$1.elementClasses[extendsTag];
|
|
578
|
+
if (!BaseClass) { // TODO: Use Cache
|
|
579
|
+
BaseClass = document.createElement(extendsTag).constructor;
|
|
580
|
+
Globals$1.elementClasses[extendsTag] = BaseClass;
|
|
581
|
+
}
|
|
780
582
|
}
|
|
781
583
|
|
|
782
584
|
/**
|
|
783
|
-
*
|
|
784
|
-
* @
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
// if (key === '["Html2",[[["Html3",["F1","A"]],["Html3",["F1","B"]]]]]')
|
|
789
|
-
// debugger;
|
|
585
|
+
* Intercept the construct call to auto-define the class before the constructor is called.
|
|
586
|
+
* @type {HTMLElement} */
|
|
587
|
+
let HTMLElementAutoDefine = new Proxy(BaseClass, {
|
|
588
|
+
construct(Parent, args, Class) {
|
|
589
|
+
defineClass(Class, null, extendsTag);
|
|
790
590
|
|
|
791
|
-
|
|
591
|
+
// This is a good place to manipulate any args before they're sent to the constructor.
|
|
592
|
+
// Such as loading them from attributes, if I could find a way to do so.
|
|
792
593
|
|
|
793
|
-
|
|
794
|
-
|
|
594
|
+
// This line is equivalent the to super() call.
|
|
595
|
+
return Reflect.construct(Parent, args, Class);
|
|
596
|
+
}
|
|
597
|
+
});
|
|
795
598
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
599
|
+
return class Solarite extends HTMLElementAutoDefine {
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* TODO: Make these standalone functions.
|
|
604
|
+
* Callbacks.
|
|
605
|
+
* Use onConnect.push(() => ...); to add new callbacks. */
|
|
606
|
+
onConnect = Util$1.callback();
|
|
607
|
+
|
|
608
|
+
onFirstConnect = Util$1.callback();
|
|
609
|
+
onDisconnect = Util$1.callback();
|
|
801
610
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
set.delete(result);
|
|
807
|
-
}
|
|
611
|
+
/**
|
|
612
|
+
* @param options {RenderOptions} */
|
|
613
|
+
constructor(options={}) {
|
|
614
|
+
super();
|
|
808
615
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
result = val;
|
|
813
|
-
}
|
|
616
|
+
// TODO: Is options.render ever used?
|
|
617
|
+
if (options.render===true)
|
|
618
|
+
this.render();
|
|
814
619
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
delete data[key];
|
|
620
|
+
else if (options.render===false)
|
|
621
|
+
Globals$1.rendered.add(this); // Don't render on connectedCallback()
|
|
818
622
|
|
|
819
|
-
|
|
820
|
-
|
|
623
|
+
// Add slot children before constructor code executes.
|
|
624
|
+
// This breaks the styleStaticNested test.
|
|
625
|
+
// PendingChildren is setup in NodeGroup.createNewComponent()
|
|
626
|
+
// TODO: Match named slots.
|
|
627
|
+
//let ch = Globals.pendingChildren.pop();
|
|
628
|
+
//if (ch) // TODO: how could there be a slot before render is called?
|
|
629
|
+
// (this.querySelector('slot') || this).append(...ch);
|
|
821
630
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
631
|
+
/** @deprecated
|
|
632
|
+
Object.defineProperty(this, 'html', {
|
|
633
|
+
set(html) {
|
|
634
|
+
Globals.rendered.add(this);
|
|
635
|
+
if (typeof html === 'string') {
|
|
636
|
+
console.warn("Assigning to this.html without the r template prefix.")
|
|
637
|
+
this.innerHTML = html;
|
|
638
|
+
}
|
|
639
|
+
else
|
|
640
|
+
this.modifications = r(this, html, options);
|
|
641
|
+
}
|
|
642
|
+
})*/
|
|
834
643
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
644
|
+
/*
|
|
645
|
+
let pthis = new Proxy(this, {
|
|
646
|
+
get(obj, prop) {
|
|
647
|
+
return Reflect.get(obj, prop)
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
this.render = this.render.bind(pthis);
|
|
651
|
+
*/
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Call render() only if it hasn't already been called. */
|
|
656
|
+
renderFirstTime() {
|
|
657
|
+
if (!Globals$1.rendered.has(this) && this.render)
|
|
658
|
+
this.render();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Called automatically by the browser. */
|
|
663
|
+
connectedCallback() {
|
|
664
|
+
this.renderFirstTime();
|
|
665
|
+
if (!Globals$1.connected.has(this)) {
|
|
666
|
+
Globals$1.connected.add(this);
|
|
667
|
+
this.onFirstConnect();
|
|
840
668
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
669
|
+
this.onConnect();
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
disconnectedCallback() {
|
|
673
|
+
this.onDisconnect();
|
|
844
674
|
}
|
|
845
675
|
|
|
846
|
-
if (set.size === 0)
|
|
847
|
-
delete data[key];
|
|
848
676
|
|
|
849
|
-
|
|
677
|
+
static define(tagName=null) {
|
|
678
|
+
defineClass(this, tagName, extendsTag);
|
|
679
|
+
}
|
|
850
680
|
}
|
|
681
|
+
}
|
|
851
682
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
683
|
+
// Trick to prevent minifier from renaming this method.
|
|
684
|
+
let define = 'define';
|
|
685
|
+
let getName = 'getName';
|
|
686
|
+
|
|
687
|
+
//#IFDEV
|
|
688
|
+
/*@__NO_SIDE_EFFECTS__*/
|
|
689
|
+
function assert(val) {
|
|
690
|
+
if (!val) {
|
|
691
|
+
debugger;
|
|
692
|
+
throw new Error('Assertion failed: ' + val);
|
|
859
693
|
}
|
|
860
|
-
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
//#ENDIF
|
|
861
697
|
|
|
698
|
+
let lastObjectId = 1>>>0; // Is a 32-bit int faster to increment than JavaScript's Number, which is a 64-bit float?
|
|
699
|
+
let objectIds = new WeakMap();
|
|
700
|
+
|
|
862
701
|
/**
|
|
863
|
-
*
|
|
864
|
-
*
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
702
|
+
* @param obj {Object|string|Node}
|
|
703
|
+
* @returns {string} */
|
|
704
|
+
function getObjectId(obj) {
|
|
705
|
+
// if (typeof obj === 'function')
|
|
706
|
+
// return obj.toString(); // This fails to detect when a function's bound variables changes.
|
|
707
|
+
|
|
708
|
+
let result = objectIds.get(obj);
|
|
709
|
+
if (!result) { // convert to string, store in result, then add 1 to lastObjectId.
|
|
710
|
+
result = '~\f' + (lastObjectId++); // We use a unique, 2-byte prefix to ensure it doesn't collide w/ strings not from getObjectId()
|
|
711
|
+
objectIds.set(obj, result);
|
|
712
|
+
}
|
|
713
|
+
return result;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Control how JSON.stringify() handles Nodes and Functions.
|
|
718
|
+
* Normally, we'd pass a replacer() function argument to JSON.stringify() to handle Nodes and Functions.
|
|
719
|
+
* But that makes JSON.stringify() take twice as long to run.
|
|
720
|
+
* Adding a toJSON method globally on these object prototypes doesn't incur that performance penalty. */
|
|
721
|
+
let isHashing = true;
|
|
722
|
+
function toJSON() {
|
|
723
|
+
return isHashing ? getObjectId(this) : this
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
// Node.prototype.toJSON = toJSON;
|
|
728
|
+
// Function.prototype.toJSON = toJSON;
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Get a string that uniquely maps to the values of the given object.
|
|
733
|
+
* If a value in obj changes, calling getObjectHash(obj) will then return a different hash.
|
|
734
|
+
* This is used by NodeGroupManager to create a hash that represents the current values of a NodeGroup.
|
|
735
|
+
*
|
|
736
|
+
* Relies on the Node and Function prototypes being overridden above.
|
|
737
|
+
*
|
|
738
|
+
* Note that passing an integer may collide with the number we get from hashing an object.
|
|
739
|
+
* But we don't handle that case because we need max performance and Solarite never passes integers to this function.
|
|
740
|
+
*
|
|
741
|
+
* @param obj {*}
|
|
742
|
+
* @returns {string} */
|
|
743
|
+
function getObjectHash(obj) {
|
|
744
|
+
|
|
745
|
+
// Sometimes these get unassigned by Chrome and Brave 119, as well as Firefox, seemingly randomly!
|
|
746
|
+
// The same tests sometimes pass, sometimes fail, even after browser and OS restarts.
|
|
747
|
+
// So we check the assignments on every run of getObjectHash()
|
|
748
|
+
if (Node.prototype.toJSON !== toJSON) {
|
|
749
|
+
Node.prototype.toJSON = toJSON;
|
|
750
|
+
if (Function.prototype.toJSON !== toJSON) // Will it only unmap one but not the other?
|
|
751
|
+
Function.prototype.toJSON = toJSON;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
let result;
|
|
755
|
+
isHashing = true;
|
|
756
|
+
try {
|
|
757
|
+
result = JSON.stringify(obj);
|
|
758
|
+
}
|
|
759
|
+
catch(e) {
|
|
760
|
+
result = getObjectHashCircular(obj);
|
|
761
|
+
}
|
|
762
|
+
isHashing = false;
|
|
763
|
+
return result;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Slower hashing method that supports.
|
|
768
|
+
* @param obj
|
|
769
|
+
* @returns {string} */
|
|
770
|
+
function getObjectHashCircular(obj) {
|
|
771
|
+
|
|
772
|
+
//console.log('circular')
|
|
773
|
+
// Slower version that handles circular references.
|
|
774
|
+
// Just adding any callback at all, even one that just returns the value, makes JSON.stringify() twice as slow.
|
|
775
|
+
const seen = new Set();
|
|
776
|
+
return JSON.stringify(obj, (key, value) => {
|
|
777
|
+
if (typeof value === 'object' && value !== null) {
|
|
778
|
+
if (seen.has(value))
|
|
779
|
+
return getObjectId(value);
|
|
780
|
+
seen.add(value);
|
|
781
|
+
}
|
|
782
|
+
return value;
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
class MultiValueMap {
|
|
787
|
+
|
|
788
|
+
/** @type {Object<string, Set>} */
|
|
789
|
+
data = {};
|
|
790
|
+
|
|
791
|
+
// Set a new value for a key
|
|
792
|
+
add(key, value) {
|
|
793
|
+
let data = this.data;
|
|
794
|
+
let set = data[key];
|
|
795
|
+
if (!set) {
|
|
796
|
+
set = new Set();
|
|
797
|
+
data[key] = set;
|
|
798
|
+
}
|
|
799
|
+
set.add(value);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
isEmpty() {
|
|
803
|
+
for (let key in this.data)
|
|
804
|
+
return true;
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Get all values for a key.
|
|
810
|
+
* @param key {string}
|
|
811
|
+
* @returns {Set|*[]} */
|
|
812
|
+
getAll(key) {
|
|
813
|
+
return this.data[key] || [];
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Remove one value from a key, and return it.
|
|
818
|
+
* @param key {string}
|
|
819
|
+
* @param val If specified, make sure we delete this specific value, if a key exists more than once.
|
|
820
|
+
* @returns {*|undefined} The deleted item. */
|
|
821
|
+
delete(key, val=undefined) {
|
|
822
|
+
let data = this.data;
|
|
823
|
+
let result;
|
|
824
|
+
let set = data[key];
|
|
825
|
+
if (!set)
|
|
826
|
+
return undefined;
|
|
827
|
+
|
|
828
|
+
// Delete any value.
|
|
829
|
+
if (val === undefined) {
|
|
830
|
+
[result] = set; // Does the same as above and seems to be about the same speed.
|
|
831
|
+
set.delete(result);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Delete a specific value.
|
|
835
|
+
else {
|
|
836
|
+
set.delete(val);
|
|
837
|
+
result = val;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (set.size === 0)
|
|
841
|
+
delete data[key];
|
|
842
|
+
|
|
843
|
+
return result;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Remove one value from a key, and return it.
|
|
848
|
+
* @param key {string}
|
|
849
|
+
* @returns {*|undefined} The deleted item. */
|
|
850
|
+
deleteAny(key) {
|
|
851
|
+
let data = this.data;
|
|
852
|
+
let result;
|
|
853
|
+
let set = data[key];
|
|
854
|
+
if (!set) // slower than pre-check.
|
|
855
|
+
return undefined;
|
|
856
|
+
|
|
857
|
+
[result] = set; // Does the same as above and seems to be about the same speed.
|
|
858
|
+
set.delete(result);
|
|
859
|
+
|
|
860
|
+
if (set.size === 0)
|
|
861
|
+
delete data[key];
|
|
862
|
+
|
|
863
|
+
return result;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
deleteSpecific(key, val) {
|
|
867
|
+
let data = this.data;
|
|
868
|
+
let result;
|
|
869
|
+
let set = data[key];
|
|
870
|
+
if (!set)
|
|
871
|
+
return undefined;
|
|
872
|
+
|
|
873
|
+
set.delete(val);
|
|
874
|
+
result = val;
|
|
875
|
+
|
|
876
|
+
if (set.size === 0)
|
|
877
|
+
delete data[key];
|
|
878
|
+
|
|
879
|
+
return result;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Try to delete an item that matches the key and the isPreferred function.
|
|
885
|
+
* if not the latter, just delete any item that matches the key.
|
|
886
|
+
* @param key {string}
|
|
887
|
+
* @returns {*|undefined} The deleted item. */
|
|
888
|
+
deletePreferred(key, parent) {
|
|
889
|
+
let result;
|
|
890
|
+
let data = this.data;
|
|
891
|
+
let set = data[key];
|
|
892
|
+
if (!set)
|
|
893
|
+
return undefined;
|
|
894
|
+
|
|
895
|
+
for (let val of set) {
|
|
896
|
+
if (val?.parentNode === parent) {
|
|
897
|
+
set.delete(val);
|
|
898
|
+
result = val;
|
|
899
|
+
break;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
if (!result) {
|
|
903
|
+
[result] = set;
|
|
904
|
+
set.delete(result);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
if (set.size === 0)
|
|
908
|
+
delete data[key];
|
|
909
|
+
|
|
910
|
+
return result;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
hasValue(val) {
|
|
914
|
+
let data = this.data;
|
|
915
|
+
let names = [];
|
|
916
|
+
for (let name in data)
|
|
917
|
+
if (data[name].has(val)) // TODO: iterate twice to pre-size array?
|
|
918
|
+
names.push(name);
|
|
919
|
+
return names;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* ISC License
|
|
925
|
+
*
|
|
926
|
+
* Copyright (c) 2020, Andrea Giammarchi, @WebReflection
|
|
927
|
+
*
|
|
928
|
+
* Permission to use, copy, modify, and/or distribute this software for any
|
|
929
|
+
* purpose with or without fee is hereby granted, provided that the above
|
|
930
|
+
* copyright notice and this permission notice appear in all copies.
|
|
931
|
+
*
|
|
932
|
+
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
933
|
+
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
934
|
+
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
935
|
+
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
936
|
+
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
|
937
|
+
* OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
877
938
|
* PERFORMANCE OF THIS SOFTWARE.
|
|
878
939
|
*/
|
|
879
940
|
|
|
@@ -1089,14 +1150,23 @@ const udomdiff = (parentNode, a, b, before) => {
|
|
|
1089
1150
|
return b;
|
|
1090
1151
|
};
|
|
1091
1152
|
|
|
1153
|
+
//import {ArraySpliceOp} from "./watch.js";
|
|
1154
|
+
//#IFDEV
|
|
1155
|
+
var exprPathId = 0;
|
|
1156
|
+
//#ENDIF
|
|
1157
|
+
|
|
1092
1158
|
/**
|
|
1093
1159
|
* Path to where an expression should be evaluated within a Shell or NodeGroup.
|
|
1094
1160
|
* Path is only valid until the expressions before it are evaluated.
|
|
1095
1161
|
* TODO: Make this based on parent and node instead of path? */
|
|
1096
1162
|
class ExprPath {
|
|
1097
1163
|
|
|
1164
|
+
//#IFDEV
|
|
1165
|
+
eid = exprPathId++;
|
|
1166
|
+
//#ENDIF
|
|
1167
|
+
|
|
1098
1168
|
/**
|
|
1099
|
-
* @type {
|
|
1169
|
+
* @type {ExprPathType} */
|
|
1100
1170
|
type;
|
|
1101
1171
|
|
|
1102
1172
|
// Used for attributes:
|
|
@@ -1112,8 +1182,6 @@ class ExprPath {
|
|
|
1112
1182
|
* @type {Set<string>} Used for type=AttribType.Multiple to remember the attributes that were added. */
|
|
1113
1183
|
attrNames;
|
|
1114
1184
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
1185
|
/**
|
|
1118
1186
|
* @type {Node} Node that occurs before this ExprPath's first Node.
|
|
1119
1187
|
* This is necessary because udomdiff() can steal nodes from another ExprPath.
|
|
@@ -1155,16 +1223,23 @@ class ExprPath {
|
|
|
1155
1223
|
nodeMarkerPath;
|
|
1156
1224
|
|
|
1157
1225
|
|
|
1158
|
-
/** @type {?function} */
|
|
1226
|
+
/** @type {?function} A function called by renderWatched() to update the value of this expression. */
|
|
1159
1227
|
watchFunction
|
|
1160
1228
|
|
|
1229
|
+
/**
|
|
1230
|
+
* @type {?function} The most recent callback passed to a .map() function in this ExprPath.
|
|
1231
|
+
* TODO: What if one ExprPath has two .map() calls? Maybe we just won't support that. */
|
|
1232
|
+
mapCallback
|
|
1233
|
+
|
|
1234
|
+
isHtmlProperty = undefined;
|
|
1235
|
+
|
|
1161
1236
|
/**
|
|
1162
1237
|
* @param nodeBefore {Node}
|
|
1163
1238
|
* @param nodeMarker {?Node}
|
|
1164
|
-
* @param type {
|
|
1239
|
+
* @param type {ExprPathType}
|
|
1165
1240
|
* @param attrName {?string}
|
|
1166
1241
|
* @param attrValue {string[]} */
|
|
1167
|
-
constructor(nodeBefore, nodeMarker, type=
|
|
1242
|
+
constructor(nodeBefore, nodeMarker, type=ExprPathType.Content, attrName=null, attrValue=null) {
|
|
1168
1243
|
|
|
1169
1244
|
// If path is a node.
|
|
1170
1245
|
this.nodeBefore = nodeBefore;
|
|
@@ -1172,7 +1247,7 @@ class ExprPath {
|
|
|
1172
1247
|
this.type = type;
|
|
1173
1248
|
this.attrName = attrName;
|
|
1174
1249
|
this.attrValue = attrValue;
|
|
1175
|
-
if (type ===
|
|
1250
|
+
if (type === ExprPathType.AttribMultiple)
|
|
1176
1251
|
this.attrNames = new Set();
|
|
1177
1252
|
}
|
|
1178
1253
|
|
|
@@ -1186,36 +1261,27 @@ class ExprPath {
|
|
|
1186
1261
|
* We should modify path.applyValueAttrib so it stores the procssed parts and then only calls
|
|
1187
1262
|
* setAttribute() once all the pieces are in place.
|
|
1188
1263
|
*
|
|
1189
|
-
* @param expr {Expr}
|
|
1190
1264
|
* @param exprs {Expr[]}
|
|
1191
|
-
* @param
|
|
1192
|
-
|
|
1193
|
-
* @returns {int} */
|
|
1194
|
-
apply(expr, exprs=null, exprIndex=0, componentExprs={}) {
|
|
1265
|
+
* @param freeNodeGroups {boolean} */
|
|
1266
|
+
apply(exprs, freeNodeGroups=true) {
|
|
1195
1267
|
switch (this.type) {
|
|
1196
1268
|
case 1: // PathType.Content:
|
|
1197
|
-
this.applyNodes(
|
|
1269
|
+
this.applyNodes(exprs[0], freeNodeGroups);
|
|
1198
1270
|
break;
|
|
1199
1271
|
case 2: // PathType.Multiple:
|
|
1200
|
-
this.applyMultipleAttribs(this.nodeMarker,
|
|
1272
|
+
this.applyMultipleAttribs(this.nodeMarker, exprs[0]);
|
|
1201
1273
|
break;
|
|
1202
1274
|
case 5: // PathType.Comment:
|
|
1203
1275
|
// Expressions inside Html comments. Deliberately empty because we won't waste time updating them.
|
|
1204
1276
|
break;
|
|
1205
1277
|
case 6: // PathType.Event:
|
|
1206
|
-
this.applyEventAttrib(this.nodeMarker,
|
|
1278
|
+
this.applyEventAttrib(this.nodeMarker, exprs[0], this.parentNg.rootNg.root);
|
|
1207
1279
|
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
|
-
}
|
|
1280
|
+
default: // TODO: Is this still used? Lots of tests fail without it.
|
|
1281
|
+
// One attribute value may have multiple expressions. Here we apply them all at once.
|
|
1282
|
+
this.applyValueAttrib(this.nodeMarker, exprs);
|
|
1215
1283
|
break;
|
|
1216
1284
|
}
|
|
1217
|
-
|
|
1218
|
-
return exprIndex;
|
|
1219
1285
|
}
|
|
1220
1286
|
|
|
1221
1287
|
/**
|
|
@@ -1223,14 +1289,16 @@ class ExprPath {
|
|
|
1223
1289
|
* Called by applyExprs()
|
|
1224
1290
|
* This function is recursive, as the functions it calls also call it.
|
|
1225
1291
|
* @param expr {Expr}
|
|
1292
|
+
* @param freeNodeGroups {boolean}
|
|
1226
1293
|
* @return {Node[]} New Nodes created. */
|
|
1227
|
-
applyNodes(expr) {
|
|
1294
|
+
applyNodes(expr, freeNodeGroups=true) {
|
|
1228
1295
|
let path = this;
|
|
1229
1296
|
|
|
1230
1297
|
// This can be done at the beginning or the end of this function.
|
|
1231
1298
|
// If at the end, we may get rendering done faster.
|
|
1232
1299
|
// But when at the beginning, it leaves all the nodes in-use so we can do a renderWatched().
|
|
1233
|
-
|
|
1300
|
+
if (freeNodeGroups)
|
|
1301
|
+
path.freeNodeGroups();
|
|
1234
1302
|
|
|
1235
1303
|
/*#IFDEV*/path.verify();/*#ENDIF*/
|
|
1236
1304
|
|
|
@@ -1240,10 +1308,10 @@ class ExprPath {
|
|
|
1240
1308
|
/*#IFDEV*/assert(!oldNodeGroups.includes(null));/*#ENDIF*/
|
|
1241
1309
|
let secondPass = []; // indices
|
|
1242
1310
|
|
|
1243
|
-
path.nodeGroups = []; // Reset before
|
|
1244
|
-
path.
|
|
1311
|
+
path.nodeGroups = []; // Reset before applyExactNodes and the code below rebuilds it.
|
|
1312
|
+
path.applyExactNodes(expr, newNodes, secondPass);
|
|
1245
1313
|
|
|
1246
|
-
this.existingTextNodes = null;
|
|
1314
|
+
//this.existingTextNodes = null;
|
|
1247
1315
|
|
|
1248
1316
|
// TODO: Create an array of old vs Nodes and NodeGroups together.
|
|
1249
1317
|
// If they're all the same, skip the next steps.
|
|
@@ -1302,7 +1370,7 @@ class ExprPath {
|
|
|
1302
1370
|
|
|
1303
1371
|
for (let ng of oldNodeGroups)
|
|
1304
1372
|
if (!ng.startNode.parentNode)
|
|
1305
|
-
ng.
|
|
1373
|
+
ng.removeAndSaveOrphans();
|
|
1306
1374
|
}
|
|
1307
1375
|
|
|
1308
1376
|
|
|
@@ -1310,67 +1378,163 @@ class ExprPath {
|
|
|
1310
1378
|
}
|
|
1311
1379
|
|
|
1312
1380
|
/**
|
|
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);
|
|
1381
|
+
* Used by watch() for inserting/removing/replacing individual loop items.
|
|
1382
|
+
* @param op {ArraySpliceOp} */
|
|
1383
|
+
applyArrayOp(op) {
|
|
1319
1384
|
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1385
|
+
// Replace NodeGroups
|
|
1386
|
+
let replaceCount = Math.min(op.deleteCount, op.items.length);
|
|
1387
|
+
let deleteCount = op.deleteCount - replaceCount;
|
|
1388
|
+
for (let i=0; i<replaceCount; i++) {
|
|
1389
|
+
let oldNg = this.nodeGroups[op.index + i]; // TODO: One expr can create multiple nodegroups.
|
|
1324
1390
|
|
|
1391
|
+
// Try to find an exact match
|
|
1392
|
+
let func = this.mapCallback || this.watchFunction;
|
|
1393
|
+
let expr = func(op.items[i]);
|
|
1325
1394
|
|
|
1395
|
+
// If the result of func isn't a template, conver it to one or more templates.
|
|
1396
|
+
this.exprToTemplates(expr, template => { // TODO: An expr can create multiple NodeGroups. I need a way to group them.
|
|
1326
1397
|
|
|
1327
|
-
|
|
1398
|
+
let ng = this.getNodeGroup(template, true); // Removes from nodeGroupsAttached and adds to nodeGroupsRendered()
|
|
1399
|
+
if (ng && ng === oldNg) ; else {
|
|
1328
1400
|
|
|
1329
|
-
|
|
1401
|
+
// Find a close match or create a new node group
|
|
1402
|
+
if (!ng)
|
|
1403
|
+
ng = this.getNodeGroup(template, false); // adds back to nodeGroupsRendered()
|
|
1404
|
+
this.nodeGroups[op.index + i] = ng; // TODO: Remove old one to nodeGroupsDetached?
|
|
1330
1405
|
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1406
|
+
// Splice in the new nodes.
|
|
1407
|
+
let insertBefore = oldNg.startNode;
|
|
1408
|
+
for (let node of ng.getNodes())
|
|
1409
|
+
insertBefore.parentNode.insertBefore(node, insertBefore);
|
|
1335
1410
|
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1411
|
+
// Remove the old nodes.
|
|
1412
|
+
if (ng !== oldNg)
|
|
1413
|
+
oldNg.removeAndSaveOrphans();
|
|
1414
|
+
}
|
|
1415
|
+
});
|
|
1340
1416
|
}
|
|
1341
1417
|
|
|
1342
|
-
//
|
|
1343
|
-
|
|
1344
|
-
|
|
1418
|
+
// Delete extra at the end.
|
|
1419
|
+
if (deleteCount > 0) {
|
|
1420
|
+
for (let i=0; i<deleteCount; i++) {
|
|
1421
|
+
let oldNg = this.nodeGroups[op.index + replaceCount + i];
|
|
1422
|
+
oldNg.removeAndSaveOrphans();
|
|
1423
|
+
}
|
|
1424
|
+
this.nodeGroups.splice(op.index + replaceCount, deleteCount);
|
|
1425
|
+
}
|
|
1345
1426
|
|
|
1427
|
+
// Add extra at the end.
|
|
1428
|
+
else {
|
|
1429
|
+
let newItems = op.items.slice(replaceCount);
|
|
1346
1430
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
* @param expr {Template|Node|Array|function|*}
|
|
1350
|
-
* @param newNodes {(Node|Template)[]}
|
|
1351
|
-
* @param secondPass {Array} Locations within newNodes to evaluate later. */
|
|
1352
|
-
applyExact(expr, newNodes, secondPass) {
|
|
1431
|
+
let insertBefore = this.nodeGroups[op.index + replaceCount]?.startNode || this.nodeMarker;
|
|
1432
|
+
for (let i = 0; i < newItems.length; i++) { // We use nodeMarker if the subequent (or all) nodeGroups have been removed.
|
|
1353
1433
|
|
|
1354
|
-
if (expr instanceof Template) {
|
|
1355
1434
|
|
|
1356
|
-
|
|
1357
|
-
|
|
1435
|
+
// Try to find exact match
|
|
1436
|
+
let template = this.mapCallback(newItems[i]);
|
|
1437
|
+
let ng = this.getNodeGroup(template, true); // Removes from nodeGroupsAttached and adds to nodeGroupsRendered()
|
|
1438
|
+
if (!ng) // Find a close match or create a new node group
|
|
1439
|
+
ng = this.getNodeGroup(template, false); // adds back to nodeGroupsRendered()
|
|
1358
1440
|
|
|
1359
|
-
// TODO: Track ranges of changed nodes and only pass those to udomdiff?
|
|
1360
|
-
// But will that break the swap benchmark?
|
|
1361
|
-
newNodes.push(...ng.getNodes());
|
|
1362
1441
|
this.nodeGroups.push(ng);
|
|
1442
|
+
|
|
1443
|
+
// Splice in the new nodes.
|
|
1444
|
+
for (let node of ng.getNodes())
|
|
1445
|
+
insertBefore.parentNode.insertBefore(node, insertBefore);
|
|
1363
1446
|
}
|
|
1447
|
+
}
|
|
1364
1448
|
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1449
|
+
//#IFDEV
|
|
1450
|
+
assert(this.nodeGroups.length === op.array.length);
|
|
1451
|
+
//#ENDIF
|
|
1452
|
+
|
|
1453
|
+
// TODO: update or invalidate the nodes cache?
|
|
1454
|
+
this.nodesCache = null;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Recursively traverse expr.
|
|
1459
|
+
* If a value is a function, evaluate it.
|
|
1460
|
+
* If a value is an array, recurse on each item.
|
|
1461
|
+
* If it's a primitive, convert it to a Template.
|
|
1462
|
+
* Otherwise pass the item (which is now either a Template or a Node) to callback.
|
|
1463
|
+
* @param expr
|
|
1464
|
+
* @param callback {function(Node|Template)}
|
|
1465
|
+
*
|
|
1466
|
+
* TODO: have applyExactNodes() use this function. */
|
|
1467
|
+
exprToTemplates(expr, callback) {
|
|
1468
|
+
if (Array.isArray(expr))
|
|
1469
|
+
for (let subExpr of expr)
|
|
1470
|
+
this.exprToTemplates(subExpr, callback);
|
|
1471
|
+
|
|
1472
|
+
else if (typeof expr === 'function') {
|
|
1473
|
+
// TODO: One ExprPath can have multiple expr functions.
|
|
1474
|
+
// But if using it as a watch, it should only have one at the top level.
|
|
1475
|
+
// So maybe this is ok.
|
|
1476
|
+
Globals$1.currentExprPath = this; // Used by watch()
|
|
1477
|
+
|
|
1478
|
+
this.watchFunction = expr; // TODO: Only do this if it's a top level function.
|
|
1479
|
+
expr = expr(); // As expr accesses watched variables, watch() uses Globals.currentExprPath to mark where those watched variables are being used.
|
|
1480
|
+
Globals$1.currentExprPath = null;
|
|
1481
|
+
|
|
1482
|
+
this.exprToTemplates(expr, callback);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// String/Number/Date/Boolean
|
|
1486
|
+
else if (!(expr instanceof Template) && !(expr instanceof Node)){
|
|
1487
|
+
// Convert expression to a string.
|
|
1488
|
+
if (expr === undefined || expr === false || expr === null) // Util.isFalsy() inlined
|
|
1489
|
+
expr = '';
|
|
1490
|
+
else if (typeof expr !== 'string')
|
|
1491
|
+
expr += '';
|
|
1492
|
+
|
|
1493
|
+
// Get the same Template for the same string each time.
|
|
1494
|
+
// let template = Globals.stringTemplates[expr];
|
|
1495
|
+
// if (!template) {
|
|
1496
|
+
let template = new Template([expr], []);
|
|
1497
|
+
// Globals.stringTemplates[expr] = template;
|
|
1498
|
+
//}
|
|
1499
|
+
|
|
1500
|
+
// Recurse.
|
|
1501
|
+
this.exprToTemplates(template, callback);
|
|
1502
|
+
}
|
|
1503
|
+
else
|
|
1504
|
+
callback(expr);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
|
|
1508
|
+
/**
|
|
1509
|
+
* Try to apply Nodes that are an exact match, by finding existing nodes from the last render
|
|
1510
|
+
* that have the same value as created by the expr.
|
|
1511
|
+
* This is called from ExprPath.applyNodes().
|
|
1512
|
+
*
|
|
1513
|
+
* @param expr {Template|Node|Array|function|*}
|
|
1514
|
+
* @param newNodes {(Node|Template)[]} An inout parameter; we add the nodes here as we go.
|
|
1515
|
+
* @param secondPass {[int, int][]} Locations within newNodes for ExprPath.applyNodes() to evaluate later,
|
|
1516
|
+
* when it tries to find partial matches. */
|
|
1517
|
+
applyExactNodes(expr, newNodes, secondPass) {
|
|
1518
|
+
|
|
1519
|
+
if (expr instanceof Template) {
|
|
1520
|
+
let ng = this.getNodeGroup(expr, true);
|
|
1521
|
+
if (ng) {
|
|
1522
|
+
|
|
1523
|
+
// TODO: Track ranges of changed nodes and only pass those to udomdiff?
|
|
1524
|
+
// But will that break the swap benchmark?
|
|
1525
|
+
newNodes.push(...ng.getNodes());
|
|
1526
|
+
this.nodeGroups.push(ng);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// If expression, mark it to be evaluated later in ExprPath.apply() to find partial match.
|
|
1530
|
+
else {
|
|
1531
|
+
secondPass.push([newNodes.length, this.nodeGroups.length]);
|
|
1368
1532
|
newNodes.push(expr);
|
|
1369
1533
|
this.nodeGroups.push(null); // placeholder
|
|
1370
1534
|
}
|
|
1371
1535
|
}
|
|
1372
1536
|
|
|
1373
|
-
// Node created by an expression.
|
|
1537
|
+
// Node(s) created by an expression.
|
|
1374
1538
|
else if (expr instanceof Node) {
|
|
1375
1539
|
|
|
1376
1540
|
// DocumentFragment created by an expression.
|
|
@@ -1383,53 +1547,54 @@ class ExprPath {
|
|
|
1383
1547
|
// Arrays and functions.
|
|
1384
1548
|
// I tried iterating over the result of a generator function to avoid this recursion and simplify the code,
|
|
1385
1549
|
// but that consistently made the js-framework-benchmarks a few percentage points slower.
|
|
1386
|
-
else
|
|
1550
|
+
else {
|
|
1551
|
+
this.exprToTemplates(expr, template => {
|
|
1552
|
+
this.applyExactNodes(template, newNodes, secondPass);
|
|
1553
|
+
});
|
|
1554
|
+
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// Old version
|
|
1558
|
+
/*else if (Array.isArray(expr))
|
|
1387
1559
|
for (let subExpr of expr)
|
|
1388
|
-
this.
|
|
1560
|
+
this.applyExactNodes(subExpr, newNodes, secondPass);
|
|
1389
1561
|
|
|
1390
1562
|
else if (typeof expr === 'function') {
|
|
1391
1563
|
// TODO: One ExprPath can have multiple expr functions.
|
|
1392
1564
|
// But if using it as a watch, it should only have one at the top level.
|
|
1393
1565
|
// So maybe this is ok.
|
|
1394
|
-
Globals.currentExprPath =
|
|
1566
|
+
Globals.currentExprPath = this; // Used by watch()
|
|
1567
|
+
|
|
1395
1568
|
this.watchFunction = expr; // TODO: Only do this if it's a top level function.
|
|
1396
|
-
let result = expr();
|
|
1569
|
+
let result = expr(); // As expr accesses watched variables, watch() uses Globals.currentExprPath to mark where those watched variables are being used.
|
|
1397
1570
|
Globals.currentExprPath = null;
|
|
1398
1571
|
|
|
1399
|
-
this.
|
|
1572
|
+
this.applyExactNodes(result, newNodes, secondPass);
|
|
1400
1573
|
}
|
|
1401
1574
|
|
|
1402
|
-
//
|
|
1575
|
+
// String
|
|
1403
1576
|
else {
|
|
1404
|
-
// Convert
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1577
|
+
// Convert expression to a string.
|
|
1578
|
+
let stringExpr = expr;
|
|
1579
|
+
if (expr === undefined || expr === false || expr === null) // Util.isFalsy()
|
|
1580
|
+
stringExpr = '';
|
|
1581
|
+
else if (typeof expr !== 'string')
|
|
1582
|
+
stringExpr = expr + '';
|
|
1583
|
+
|
|
1584
|
+
// Get the same Template for the same string each time.
|
|
1585
|
+
let template = Globals.stringTemplates[stringExpr];
|
|
1586
|
+
if (!template) {
|
|
1587
|
+
template = new Template([stringExpr], []);
|
|
1588
|
+
Globals.stringTemplates[stringExpr] = template;
|
|
1415
1589
|
}
|
|
1416
1590
|
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
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
|
-
}
|
|
1591
|
+
// Recurse.
|
|
1592
|
+
this.applyExactNodes(template, newNodes, secondPass);
|
|
1593
|
+
}*/
|
|
1429
1594
|
}
|
|
1430
1595
|
|
|
1431
1596
|
applyMultipleAttribs(node, expr) {
|
|
1432
|
-
/*#IFDEV*/assert(this.type ===
|
|
1597
|
+
/*#IFDEV*/assert(this.type === ExprPathType.AttribMultiple);/*#ENDIF*/
|
|
1433
1598
|
|
|
1434
1599
|
if (Array.isArray(expr))
|
|
1435
1600
|
expr = expr.flat().join(' '); // flat and join so we can accept arrays of arrays of strings.
|
|
@@ -1438,6 +1603,13 @@ class ExprPath {
|
|
|
1438
1603
|
let oldNames = this.attrNames;
|
|
1439
1604
|
this.attrNames = new Set();
|
|
1440
1605
|
if (expr) {
|
|
1606
|
+
if (typeof expr === 'function') {
|
|
1607
|
+
Globals$1.currentExprPath = this; // Used by watch()
|
|
1608
|
+
this.watchFunction = expr; // used by renderWatched()
|
|
1609
|
+
expr = expr();
|
|
1610
|
+
Globals$1.currentExprPath = null;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1441
1613
|
let attrs = (expr +'') // Split string into multiple attributes.
|
|
1442
1614
|
.split(/([\w-]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s]+))/g)
|
|
1443
1615
|
.map(text => text.trim())
|
|
@@ -1468,52 +1640,57 @@ class ExprPath {
|
|
|
1468
1640
|
* @param root */
|
|
1469
1641
|
applyEventAttrib(node, expr, root) {
|
|
1470
1642
|
/*#IFDEV*/
|
|
1471
|
-
assert(this.type ===
|
|
1643
|
+
assert(this.type === ExprPathType.Event/* || this.type === PathType.Component*/);
|
|
1472
1644
|
assert(root instanceof HTMLElement);
|
|
1473
1645
|
/*#ENDIF*/
|
|
1474
1646
|
|
|
1475
1647
|
let eventName = this.attrName.slice(2); // remove "on-" prefix.
|
|
1476
1648
|
let func;
|
|
1477
|
-
|
|
1478
|
-
// Convert array to function.
|
|
1479
1649
|
let args = [];
|
|
1480
|
-
if (Array.isArray(expr)) {
|
|
1481
1650
|
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
// oninput=${[this, 'value']}
|
|
1489
|
-
else {
|
|
1490
|
-
func = setValue;
|
|
1491
|
-
args = [expr[0], expr.slice(1), node];
|
|
1492
|
-
node.value = delve(expr[0], expr.slice(1));
|
|
1493
|
-
// root.render(); // TODO: This causes infinite recursion.
|
|
1494
|
-
}
|
|
1651
|
+
// Convert array to function.
|
|
1652
|
+
// oninput=${[this.doSomething, 'meow']}
|
|
1653
|
+
if (Array.isArray(expr) && typeof expr[0] === 'function') {
|
|
1654
|
+
func = expr[0];
|
|
1655
|
+
args = expr.slice(1);
|
|
1495
1656
|
}
|
|
1496
|
-
else
|
|
1657
|
+
else if (typeof expr === 'function')
|
|
1497
1658
|
func = expr;
|
|
1659
|
+
else
|
|
1660
|
+
throw new Error(`Invalid event binding: <${node.tagName.toLowerCase()} ${this.attrName}=\${${JSON.stringify(expr)}}>`);
|
|
1498
1661
|
|
|
1499
1662
|
this.bindEvent(node, root, eventName, eventName, func, args);
|
|
1500
1663
|
}
|
|
1501
1664
|
|
|
1502
1665
|
|
|
1666
|
+
/**
|
|
1667
|
+
* Call function when eventName is triggerd on node.
|
|
1668
|
+
* @param node {HTMLElement}
|
|
1669
|
+
* @param root {HTMLElement}
|
|
1670
|
+
* @param key {string}
|
|
1671
|
+
* @param eventName {string}
|
|
1672
|
+
* @param func {function}
|
|
1673
|
+
* @param args {array}
|
|
1674
|
+
* @param capture {boolean} */
|
|
1503
1675
|
bindEvent(node, root, key, eventName, func, args, capture=false) {
|
|
1504
|
-
let nodeEvents = Globals.nodeEvents.get(node);
|
|
1676
|
+
let nodeEvents = Globals$1.nodeEvents.get(node);
|
|
1505
1677
|
if (!nodeEvents) {
|
|
1506
1678
|
nodeEvents = {[key]: new Array(3)};
|
|
1507
|
-
Globals.nodeEvents.set(node, nodeEvents);
|
|
1679
|
+
Globals$1.nodeEvents.set(node, nodeEvents);
|
|
1508
1680
|
}
|
|
1509
1681
|
let nodeEvent = nodeEvents[key];
|
|
1510
1682
|
if (!nodeEvent)
|
|
1511
1683
|
nodeEvents[key] = nodeEvent = new Array(3);
|
|
1512
1684
|
|
|
1685
|
+
if (typeof func !== 'function')
|
|
1686
|
+
throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${${func}}> because it's not a function.`);
|
|
1513
1687
|
|
|
1514
1688
|
// If function has changed, remove and rebind the event.
|
|
1515
1689
|
if (nodeEvent[0] !== func) {
|
|
1516
1690
|
|
|
1691
|
+
// TODO: We should be removing event listeners when calling getNodeGroup(),
|
|
1692
|
+
// when we get the node from the list of nodeGroupsAttached/nodeGroupsDetached,
|
|
1693
|
+
// instead of only when we rebind an event.
|
|
1517
1694
|
let [existing, existingBound, _] = nodeEvent;
|
|
1518
1695
|
if (existing)
|
|
1519
1696
|
node.removeEventListener(eventName, existingBound, capture);
|
|
@@ -1543,68 +1720,144 @@ class ExprPath {
|
|
|
1543
1720
|
nodeEvents[key][2] = args;
|
|
1544
1721
|
}
|
|
1545
1722
|
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
else if (!this.attrValue && expr === true)
|
|
1554
|
-
node.setAttribute(this.attrName, '');
|
|
1723
|
+
/**
|
|
1724
|
+
* Handle values, including two-way binding.
|
|
1725
|
+
* @param node
|
|
1726
|
+
* @param exprs */
|
|
1727
|
+
// TODO: node is always this.nodeMarker?
|
|
1728
|
+
applyValueAttrib(node, exprs) {
|
|
1729
|
+
let expr = exprs[0];
|
|
1555
1730
|
|
|
1731
|
+
// Two-way binding between attributes
|
|
1556
1732
|
// Passing a path to the value attribute.
|
|
1733
|
+
// Copies the attribute to the property when the input event fires.
|
|
1734
|
+
// value=${[this, 'value]'}
|
|
1735
|
+
// checked=${[this, 'isAgree']}
|
|
1557
1736
|
// This same logic is in NodeGroup.createNewComponent() for components.
|
|
1558
|
-
|
|
1737
|
+
if (Util.isPath(expr)) {
|
|
1559
1738
|
let [obj, path] = [expr[0], expr.slice(1)];
|
|
1560
|
-
node.value = delve(obj, path);
|
|
1561
|
-
// TODO: We need to remove any old listeners, like in bindEventAttribute
|
|
1562
1739
|
|
|
1740
|
+
if (!obj)
|
|
1741
|
+
throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${[${expr.map(item => item ? `'${item}'` : item+'').join(', ')}]}>.`);
|
|
1742
|
+
|
|
1743
|
+
let value = delve(obj, path);
|
|
1744
|
+
|
|
1745
|
+
// Special case to allow setting select-multiple value from an array
|
|
1746
|
+
if (this.attrName === 'value' && node.type === 'select-multiple' && Array.isArray(value)) {
|
|
1747
|
+
// Set the .selected property on the options having a value within value.
|
|
1748
|
+
let strValues = value.map(v => v + '');
|
|
1749
|
+
for (let option of node.options)
|
|
1750
|
+
option.selected = strValues.includes(option.value);
|
|
1751
|
+
}
|
|
1752
|
+
else {
|
|
1753
|
+
// TODO: should we remove isFalsy, since these are always props?
|
|
1754
|
+
let strValue = Util.isFalsy(value) ? '' : value;
|
|
1755
|
+
|
|
1756
|
+
// If we don't have this condition, when we call render(), the browser will scroll to the currently
|
|
1757
|
+
// selected item in a <select> and mess up manually scrolling to a different value.
|
|
1758
|
+
if (strValue !== node[this.attrName])
|
|
1759
|
+
node[this.attrName] = strValue;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
// TODO: We need to remove any old listeners, like in bindEventAttribute.
|
|
1763
|
+
// Does bindEvent() now handle that?
|
|
1563
1764
|
let func = () => {
|
|
1564
|
-
|
|
1765
|
+
let value = (this.attrName === 'value')
|
|
1766
|
+
? Util.getInputValue(node)
|
|
1767
|
+
: node[this.attrName];
|
|
1768
|
+
delve(obj, path, value);
|
|
1565
1769
|
};
|
|
1566
1770
|
|
|
1567
1771
|
// We use capture so we update the values before other events added by the user.
|
|
1568
|
-
|
|
1772
|
+
// TODO: Bind to scroll events also?
|
|
1773
|
+
// What about resize events and width/height?
|
|
1774
|
+
this.bindEvent(node, path[0], this.attrName, 'input', func, [], true);
|
|
1569
1775
|
}
|
|
1570
1776
|
|
|
1571
1777
|
// Regular attribute
|
|
1572
1778
|
else {
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
//
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1779
|
+
// TODO: Cache this on ExprPath.isProp when Shell creates the props. Have ExprPath.clone() copy .isProp
|
|
1780
|
+
// Or make it a new PathType.
|
|
1781
|
+
//if (this.attrName === 'disabled')
|
|
1782
|
+
// debugger;
|
|
1783
|
+
|
|
1784
|
+
// hasOwnProperty() checks only the object, not the parents
|
|
1785
|
+
// this.attrName in node checks the node and the parents.
|
|
1786
|
+
// This version checks the html element it extends from, to see if has a setter set:
|
|
1787
|
+
// Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), this.attrName)?.set
|
|
1788
|
+
//let isProp = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), this.attrName)?.set;
|
|
1789
|
+
let isProp = this.isHtmlProperty;
|
|
1790
|
+
if (isProp === undefined)
|
|
1791
|
+
isProp = this.isHtmlProperty = Util.isHtmlProp(node, this.attrName);
|
|
1792
|
+
|
|
1793
|
+
// Values to toggle an attribute
|
|
1794
|
+
let multiple = this.attrValue;
|
|
1795
|
+
if (!multiple) {
|
|
1796
|
+
Globals$1.currentExprPath = this; // Used by watch()
|
|
1797
|
+
if (typeof expr === 'function') {
|
|
1798
|
+
if (this.type === 4) { // Don't evaluate functions before passing them to components
|
|
1799
|
+
return
|
|
1584
1800
|
}
|
|
1801
|
+
this.watchFunction = expr; // The function that gets the expression, used for renderWatched()
|
|
1802
|
+
expr = expr();
|
|
1585
1803
|
}
|
|
1586
|
-
|
|
1804
|
+
else
|
|
1805
|
+
expr = Util.makePrimitive(expr);
|
|
1806
|
+
Globals$1.currentExprPath = null;
|
|
1807
|
+
}
|
|
1808
|
+
if (!multiple && (expr === undefined || expr === false || expr === null)) { // Util.isFalsy() inlined.
|
|
1809
|
+
if (isProp)
|
|
1810
|
+
node[this.attrName] = false;
|
|
1811
|
+
node.removeAttribute(this.attrName);
|
|
1812
|
+
}
|
|
1813
|
+
else if (!multiple && expr === true) {
|
|
1814
|
+
if (isProp)
|
|
1815
|
+
node[this.attrName] = true;
|
|
1816
|
+
node.setAttribute(this.attrName, '');
|
|
1587
1817
|
}
|
|
1588
|
-
else
|
|
1589
|
-
value.unshift(expr);
|
|
1590
1818
|
|
|
1591
|
-
|
|
1819
|
+
// A non-toggled attribute
|
|
1820
|
+
else {
|
|
1592
1821
|
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1822
|
+
// If it's a series of expressions among strings, join them together.
|
|
1823
|
+
let joinedValue;
|
|
1824
|
+
if (multiple) {
|
|
1825
|
+
let value = [];
|
|
1826
|
+
for (let i = 0; i < this.attrValue.length; i++) {
|
|
1827
|
+
value.push(this.attrValue[i]);
|
|
1828
|
+
if (i < this.attrValue.length - 1) {
|
|
1829
|
+
Globals$1.currentExprPath = this; // Used by watch()
|
|
1830
|
+
let val = Util.makePrimitive(exprs[i]);
|
|
1831
|
+
Globals$1.currentExprPath = null;
|
|
1832
|
+
if (!Util.isFalsy(val))
|
|
1833
|
+
value.push(val);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
joinedValue = value.join('');
|
|
1837
|
+
}
|
|
1599
1838
|
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1839
|
+
// If the attribute is one expression with no strings:
|
|
1840
|
+
else
|
|
1841
|
+
joinedValue = expr;
|
|
1842
|
+
|
|
1843
|
+
// Only update attributes if the value has changed.
|
|
1844
|
+
// This is needed for setting input.value, .checked, option.selected, etc.
|
|
1845
|
+
|
|
1846
|
+
let oldVal = isProp
|
|
1847
|
+
? node[this.attrName]
|
|
1848
|
+
: node.getAttribute(this.attrName);
|
|
1849
|
+
if (oldVal !== joinedValue) {
|
|
1850
|
+
|
|
1851
|
+
// <textarea value=${expr}></textarea>
|
|
1852
|
+
// Without this branch we have no way to set the value of a textarea,
|
|
1853
|
+
// since we also prohibit expressions that are a child of textarea.
|
|
1854
|
+
if (isProp)
|
|
1855
|
+
node[this.attrName] = joinedValue;
|
|
1856
|
+
// TODO: Putting an 'else' here would be more performant
|
|
1857
|
+
node.setAttribute(this.attrName, joinedValue);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1605
1860
|
}
|
|
1606
|
-
|
|
1607
|
-
return exprIndex;
|
|
1608
1861
|
}
|
|
1609
1862
|
|
|
1610
1863
|
|
|
@@ -1620,7 +1873,8 @@ class ExprPath {
|
|
|
1620
1873
|
let nodeMarker, nodeBefore;
|
|
1621
1874
|
let root = newRoot;
|
|
1622
1875
|
let path = pathOffset ? this.nodeMarkerPath.slice(0, -pathOffset) : this.nodeMarkerPath;
|
|
1623
|
-
|
|
1876
|
+
let length = path.length-1;
|
|
1877
|
+
for (let i=length; i>0; i--) // Resolve the path.
|
|
1624
1878
|
root = root.childNodes[path[i]];
|
|
1625
1879
|
let childNodes = root.childNodes;
|
|
1626
1880
|
|
|
@@ -1665,7 +1919,7 @@ class ExprPath {
|
|
|
1665
1919
|
|
|
1666
1920
|
/**
|
|
1667
1921
|
* 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
|
|
1922
|
+
* @returns {boolean} Returns false if Nodes weren't removed, and they should instead be removed manually. */
|
|
1669
1923
|
fastClear() {
|
|
1670
1924
|
let parent = this.nodeBefore.parentNode;
|
|
1671
1925
|
if (this.nodeBefore === parent.firstChild && this.nodeMarker === parent.lastChild) {
|
|
@@ -1701,6 +1955,10 @@ class ExprPath {
|
|
|
1701
1955
|
// result2.push(...ng.getNodes())
|
|
1702
1956
|
// return result2;
|
|
1703
1957
|
|
|
1958
|
+
if (this.type === ExprPathType.AttribValue || this.type === ExprPathType.AttribMultiple || this.type === ExprPathType.ComponentAttribValue) {
|
|
1959
|
+
return [this.nodeMarker];
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1704
1962
|
|
|
1705
1963
|
let result;
|
|
1706
1964
|
|
|
@@ -1727,7 +1985,8 @@ class ExprPath {
|
|
|
1727
1985
|
return result;
|
|
1728
1986
|
}
|
|
1729
1987
|
|
|
1730
|
-
|
|
1988
|
+
/** @return {HTMLElement|ParentNode} */
|
|
1989
|
+
getParentNode() {
|
|
1731
1990
|
return this.nodeMarker.parentNode
|
|
1732
1991
|
}
|
|
1733
1992
|
|
|
@@ -1744,28 +2003,39 @@ class ExprPath {
|
|
|
1744
2003
|
* or createa new NodeGroup from the template.
|
|
1745
2004
|
* @return {NodeGroup} */
|
|
1746
2005
|
getNodeGroup(template, exact=true) {
|
|
1747
|
-
//if (exact && this.nodeGroupsFree.isEmpty())
|
|
1748
|
-
// return null;
|
|
1749
2006
|
|
|
1750
2007
|
let result;
|
|
2008
|
+
let collection = this.nodeGroupsAttachedAvailable;
|
|
1751
2009
|
|
|
1752
2010
|
// TODO: Would it be faster to maintain a separate list of detached nodegroups?
|
|
1753
2011
|
if (exact) { // [below] parentElement will be null if the parent is a DocumentFragment
|
|
1754
|
-
result =
|
|
2012
|
+
result = collection.deleteAny(template.getExactKey());
|
|
2013
|
+
if (!result) { // try searching detached
|
|
2014
|
+
collection = this.nodeGroupsDetachedAvailable;
|
|
2015
|
+
result = collection.deleteAny(template.getExactKey());
|
|
2016
|
+
}
|
|
2017
|
+
|
|
1755
2018
|
if (result) // also delete the matching close key.
|
|
1756
|
-
|
|
1757
|
-
else
|
|
2019
|
+
collection.deleteSpecific(template.getCloseKey(), result);
|
|
2020
|
+
else {
|
|
1758
2021
|
return null;
|
|
2022
|
+
}
|
|
1759
2023
|
}
|
|
1760
2024
|
|
|
1761
2025
|
// Find a close match.
|
|
1762
2026
|
// This is a match that has matching html, but different expressions applied.
|
|
1763
2027
|
// We can then apply the expressions to make it an exact match.
|
|
1764
|
-
|
|
1765
|
-
|
|
2028
|
+
// 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.
|
|
2029
|
+
else if (template.exprs.length) {
|
|
2030
|
+
result = collection.deleteAny(template.getCloseKey());
|
|
2031
|
+
if (!result) { // try searching detached
|
|
2032
|
+
collection = this.nodeGroupsDetachedAvailable;
|
|
2033
|
+
result = collection.deleteAny(template.getCloseKey());
|
|
2034
|
+
}
|
|
2035
|
+
|
|
1766
2036
|
if (result) {
|
|
1767
2037
|
/*#IFDEV*/assert(result.exactKey);/*#ENDIF*/
|
|
1768
|
-
|
|
2038
|
+
collection.deleteSpecific(result.exactKey, result);
|
|
1769
2039
|
|
|
1770
2040
|
// Update this close match with the new expression values.
|
|
1771
2041
|
result.applyExprs(template.exprs);
|
|
@@ -1777,64 +2047,70 @@ class ExprPath {
|
|
|
1777
2047
|
result = new NodeGroup(template, this);
|
|
1778
2048
|
|
|
1779
2049
|
// old:
|
|
1780
|
-
this.
|
|
1781
|
-
|
|
1782
|
-
// new:
|
|
1783
|
-
// let ngiu = this.nodeGroupsInUse;
|
|
1784
|
-
// ngiu.add(result.exactKey, result);
|
|
1785
|
-
// ngiu.add(result.closeKey, result);
|
|
2050
|
+
this.nodeGroupsRendered.push(result);
|
|
1786
2051
|
|
|
1787
2052
|
/*#IFDEV*/assert(result.parentPath);/*#ENDIF*/
|
|
1788
2053
|
return result;
|
|
1789
2054
|
}
|
|
1790
2055
|
|
|
2056
|
+
isComponent() {
|
|
2057
|
+
// Events won't have type===Component.
|
|
2058
|
+
// TODO: Have a special flag for components instead of it being on the type?
|
|
2059
|
+
return this.type === ExprPathType.ComponentAttribValue || (this.attrName && this.nodeMarker.tagName && this.nodeMarker.tagName.includes('-'));
|
|
2060
|
+
}
|
|
1791
2061
|
|
|
1792
2062
|
/**
|
|
2063
|
+
* TODO: Rename this to nodeGroupsInUse, nodeGroupsAvialableAttached and nodeGroupsAvailableDetached?
|
|
2064
|
+
* Nodes that have been used during the current render().
|
|
1793
2065
|
* Used with getNodeGroup() and freeNodeGroups().
|
|
1794
2066
|
* TODO: Use an array of WeakRef so the gc can collect them?
|
|
1795
2067
|
* TODO: Put items back in nodeGroupsInUse after applyExpr() is called, not before.
|
|
1796
2068
|
* @type {NodeGroup[]} */
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
/** @type {MultiValueMap<key:string, value:NodeGroup>} */
|
|
1800
|
-
//nodeGroupsInUse = new MultiValueMap();
|
|
2069
|
+
nodeGroupsRendered = [];
|
|
1801
2070
|
|
|
1802
2071
|
/**
|
|
2072
|
+
* Nodes that were added to the web component during the last render(), but are available to be used again.
|
|
1803
2073
|
* Used with getNodeGroup() and freeNodeGroups().
|
|
1804
2074
|
* Each NodeGroup is here twice, once under an exact key, and once under the close key.
|
|
1805
2075
|
* @type {MultiValueMap<key:string, value:NodeGroup>} */
|
|
1806
|
-
|
|
2076
|
+
nodeGroupsAttachedAvailable = new MultiValueMap();
|
|
1807
2077
|
|
|
1808
|
-
|
|
2078
|
+
/**
|
|
2079
|
+
* Nodes that were not added to the web component during the last render(), and available to be used again.
|
|
2080
|
+
* @type {MultiValueMap} */
|
|
2081
|
+
nodeGroupsDetachedAvailable = new MultiValueMap();
|
|
1809
2082
|
|
|
1810
2083
|
|
|
1811
2084
|
/**
|
|
1812
|
-
* Move everything from this.
|
|
2085
|
+
* Move everything from this.nodeGroupsRendered to this.nodeGroupsAttached and nodeGroupsDetached.
|
|
2086
|
+
* Called at the beginning of applyNodes() so it can have NodeGroups to use.
|
|
1813
2087
|
* TODO: this could run as needed in getNodeGroup? */
|
|
1814
2088
|
freeNodeGroups() {
|
|
1815
|
-
//
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
2089
|
+
// Add nodes that weren't used during render() to nodeGroupsDetached
|
|
2090
|
+
let previouslyAttached = this.nodeGroupsAttachedAvailable.data;
|
|
2091
|
+
let detached = this.nodeGroupsDetachedAvailable.data;
|
|
2092
|
+
for (let key in previouslyAttached) {
|
|
2093
|
+
let set = detached[key];
|
|
2094
|
+
if (!set)
|
|
2095
|
+
detached[key] = previouslyAttached[key];
|
|
2096
|
+
else
|
|
2097
|
+
for (let ng of previouslyAttached[key])
|
|
2098
|
+
set.add(ng);
|
|
2099
|
+
}
|
|
1819
2100
|
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
2101
|
+
// Add nodes that were used during render() to nodeGroupsRendered.
|
|
2102
|
+
this.nodeGroupsAttachedAvailable = new MultiValueMap();
|
|
2103
|
+
let nga = this.nodeGroupsAttachedAvailable;
|
|
2104
|
+
for (let ng of this.nodeGroupsRendered) {
|
|
2105
|
+
nga.add(ng.exactKey, ng);
|
|
2106
|
+
nga.add(ng.closeKey, ng);
|
|
1824
2107
|
}
|
|
1825
|
-
this.nodeGroupsInUse = [];
|
|
1826
2108
|
|
|
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();
|
|
2109
|
+
this.nodeGroupsRendered = [];
|
|
1834
2110
|
}
|
|
1835
2111
|
|
|
1836
2112
|
//#IFDEV
|
|
1837
|
-
|
|
2113
|
+
|
|
1838
2114
|
get debug() {
|
|
1839
2115
|
return [
|
|
1840
2116
|
`parentNode: ${this.nodeBefore.parentNode?.tagName?.toLowerCase()}`,
|
|
@@ -1847,7 +2123,7 @@ class ExprPath {
|
|
|
1847
2123
|
}), 1).flat()
|
|
1848
2124
|
]
|
|
1849
2125
|
}
|
|
1850
|
-
|
|
2126
|
+
|
|
1851
2127
|
get debugNodes() {
|
|
1852
2128
|
// Clear nodesCache so that getNodes() manually gets the nodes.
|
|
1853
2129
|
let nc = this.nodesCache;
|
|
@@ -1856,13 +2132,13 @@ class ExprPath {
|
|
|
1856
2132
|
this.nodesCache = nc;
|
|
1857
2133
|
return result;
|
|
1858
2134
|
}
|
|
1859
|
-
|
|
2135
|
+
|
|
1860
2136
|
verify() {
|
|
1861
2137
|
if (!window.verify)
|
|
1862
2138
|
return;
|
|
1863
2139
|
|
|
1864
|
-
assert(this.type!==
|
|
1865
|
-
assert(this.type!==
|
|
2140
|
+
assert(this.type!==ExprPathType.Content || this.nodeBefore);
|
|
2141
|
+
assert(this.type!==ExprPathType.Content || this.nodeBefore.parentNode);
|
|
1866
2142
|
|
|
1867
2143
|
// Need either nodeMarker or parentNode
|
|
1868
2144
|
assert(this.nodeMarker);
|
|
@@ -1871,10 +2147,10 @@ class ExprPath {
|
|
|
1871
2147
|
assert(!this.nodeMarker || this.nodeMarker.parentNode);
|
|
1872
2148
|
|
|
1873
2149
|
// nodeBefore and nodeMarker must have same parent.
|
|
1874
|
-
assert(this.type!==
|
|
2150
|
+
assert(this.type!==ExprPathType.Content || this.nodeBefore.parentNode === this.nodeMarker.parentNode);
|
|
1875
2151
|
|
|
1876
2152
|
assert(this.nodeBefore !== this.nodeMarker);
|
|
1877
|
-
assert(this.type!==
|
|
2153
|
+
assert(this.type!==ExprPathType.Content|| !this.nodeBefore.parentNode || this.nodeBefore.compareDocumentPosition(this.nodeMarker) === Node.DOCUMENT_POSITION_FOLLOWING);
|
|
1878
2154
|
|
|
1879
2155
|
// Detect cyclic parent and grandparent references.
|
|
1880
2156
|
assert(this.parentNg?.parentPath !== this);
|
|
@@ -1887,43 +2163,27 @@ class ExprPath {
|
|
|
1887
2163
|
// Make sure the nodesCache matches the nodes.
|
|
1888
2164
|
this.checkNodesCache();
|
|
1889
2165
|
}
|
|
1890
|
-
|
|
2166
|
+
|
|
1891
2167
|
checkNodesCache() {
|
|
1892
2168
|
return;
|
|
1893
2169
|
}
|
|
1894
2170
|
//#ENDIF
|
|
1895
2171
|
}
|
|
1896
2172
|
|
|
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
2173
|
/** @enum {int} */
|
|
1914
|
-
const
|
|
2174
|
+
const ExprPathType = {
|
|
1915
2175
|
/** Child of a node */
|
|
1916
2176
|
Content: 1,
|
|
1917
|
-
|
|
2177
|
+
|
|
1918
2178
|
/** One or more whole attributes */
|
|
1919
|
-
|
|
1920
|
-
|
|
2179
|
+
AttribMultiple: 2,
|
|
2180
|
+
|
|
1921
2181
|
/** Value of an attribute. */
|
|
1922
|
-
|
|
1923
|
-
|
|
2182
|
+
AttribValue: 3,
|
|
2183
|
+
|
|
1924
2184
|
/** Value of an attribute being passed to a component. */
|
|
1925
|
-
|
|
1926
|
-
|
|
2185
|
+
ComponentAttribValue: 4,
|
|
2186
|
+
|
|
1927
2187
|
/** Expressions inside Html comments. */
|
|
1928
2188
|
Comment: 5,
|
|
1929
2189
|
|
|
@@ -1949,118 +2209,179 @@ function getNodePath(node) {
|
|
|
1949
2209
|
* Note that the path is backward, with the outermost element at the end.
|
|
1950
2210
|
* @param root {HTMLElement|Document|DocumentFragment|ParentNode}
|
|
1951
2211
|
* @param path {int[]}
|
|
1952
|
-
* @returns {Node|HTMLElement} */
|
|
2212
|
+
* @returns {Node|HTMLElement|HTMLStyleElement} */
|
|
1953
2213
|
function resolveNodePath(root, path) {
|
|
1954
2214
|
for (let i=path.length-1; i>=0; i--)
|
|
1955
2215
|
root = root.childNodes[path[i]];
|
|
1956
2216
|
return root;
|
|
1957
2217
|
}
|
|
1958
2218
|
|
|
2219
|
+
class HtmlParser {
|
|
2220
|
+
constructor() {
|
|
2221
|
+
this.defaultState = {
|
|
2222
|
+
context: HtmlParser.Text, // possible values: 'TEXT', 'TAG', 'ATTRIBUTE'
|
|
2223
|
+
quote: null, // possible values: null, '"', "'"
|
|
2224
|
+
buffer: '',
|
|
2225
|
+
lastChar: null
|
|
2226
|
+
};
|
|
2227
|
+
this.state = {...this.defaultState};
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
reset() {
|
|
2231
|
+
this.state = {...this.defaultState};
|
|
2232
|
+
return this.state.context;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
/**
|
|
2236
|
+
* Parse the next chunk of html, starting with the same context we left off with from the previous chunk.
|
|
2237
|
+
* @param html {string}
|
|
2238
|
+
* @param onContextChange {?function(html:string, index:int, oldContext:string, newContext:string)}
|
|
2239
|
+
* Called every time the context changes, and again at the last context.
|
|
2240
|
+
* @return {('Attribute','Text','Tag')} The context at the end of html. */
|
|
2241
|
+
parse(html, onContextChange=null) {
|
|
2242
|
+
if (html === null)
|
|
2243
|
+
return this.reset();
|
|
2244
|
+
|
|
2245
|
+
for (let i = 0; i < html.length; i++) {
|
|
2246
|
+
const char = html[i];
|
|
2247
|
+
switch (this.state.context) {
|
|
2248
|
+
case HtmlParser.Text:
|
|
2249
|
+
if (char === '<' && html[i + 1].match(/[/a-z!]/i)) { // Start of a tag or comment.
|
|
2250
|
+
onContextChange?.(html, i, this.state.context, HtmlParser.Tag);
|
|
2251
|
+
this.state.context = HtmlParser.Tag;
|
|
2252
|
+
this.state.buffer = '';
|
|
2253
|
+
}
|
|
2254
|
+
break;
|
|
2255
|
+
case HtmlParser.Tag:
|
|
2256
|
+
if (char === '>') {
|
|
2257
|
+
onContextChange?.(html, i+1, this.state.context, HtmlParser.Text);
|
|
2258
|
+
this.state.context = HtmlParser.Text;
|
|
2259
|
+
this.state.quote = null;
|
|
2260
|
+
this.state.buffer = '';
|
|
2261
|
+
}
|
|
2262
|
+
else if (char === ' ' && !this.state.buffer) {
|
|
2263
|
+
// No attribute name is present. Skipping the space.
|
|
2264
|
+
continue;
|
|
2265
|
+
}
|
|
2266
|
+
else if (char === ' ' || char === '/' || char === '?') {
|
|
2267
|
+
this.state.buffer = ''; // Reset the buffer when a delimiter or potential self-closing sign is found.
|
|
2268
|
+
}
|
|
2269
|
+
else if (char === '"' || char === "'" || char === '=') {
|
|
2270
|
+
onContextChange?.(html, i, this.state.context, HtmlParser.Attribute);
|
|
2271
|
+
this.state.context = HtmlParser.Attribute;
|
|
2272
|
+
this.state.quote = char === '=' ? null : char;
|
|
2273
|
+
this.state.buffer = '';
|
|
2274
|
+
}
|
|
2275
|
+
else
|
|
2276
|
+
this.state.buffer += char;
|
|
2277
|
+
break;
|
|
2278
|
+
case HtmlParser.Attribute:
|
|
2279
|
+
// Start an attribute quote.
|
|
2280
|
+
if (!this.state.quote && !this.state.buffer.length && (char === '"' || char === "'")) {
|
|
2281
|
+
this.state.quote = char;
|
|
2282
|
+
}
|
|
2283
|
+
else if (char === this.state.quote || (!this.state.quote && this.state.buffer.length)) {
|
|
2284
|
+
onContextChange?.(html, i, this.state.context, HtmlParser.Tag);
|
|
2285
|
+
this.state.context = HtmlParser.Tag;
|
|
2286
|
+
this.state.quote = null;
|
|
2287
|
+
this.state.buffer = '';
|
|
2288
|
+
}
|
|
2289
|
+
else if (!this.state.quote && char === '>') {
|
|
2290
|
+
onContextChange?.(html, i+1, this.state.context, HtmlParser.Text);
|
|
2291
|
+
this.state.context = HtmlParser.Text;
|
|
2292
|
+
this.state.quote = null;
|
|
2293
|
+
this.state.buffer = '';
|
|
2294
|
+
}
|
|
2295
|
+
else if (char !== ' ')
|
|
2296
|
+
this.state.buffer += char;
|
|
2297
|
+
|
|
2298
|
+
break;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
onContextChange?.(html, html.length, this.state.context, null);
|
|
2302
|
+
return this.state.context;
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
HtmlParser.Attribute = 'Attribute';
|
|
2307
|
+
HtmlParser.Text = 'Text';
|
|
2308
|
+
HtmlParser.Tag = 'Tag';
|
|
2309
|
+
|
|
1959
2310
|
/**
|
|
1960
2311
|
* A Shell is created from a tagged template expression instantiated as Nodes,
|
|
1961
2312
|
* but without any expressions filled in.
|
|
1962
2313
|
* Only one Shell is created for all the items in a loop.
|
|
1963
2314
|
*
|
|
1964
2315
|
* When a NodeGroup is created from a Template's html strings,
|
|
1965
|
-
* the NodeGroup then clones the Shell's
|
|
2316
|
+
* the NodeGroup then clones the Shell's fragment to be its nodes. */
|
|
1966
2317
|
class Shell {
|
|
1967
2318
|
|
|
1968
2319
|
/**
|
|
1969
|
-
* @type {DocumentFragment} DOM parent of the shell nodes. */
|
|
2320
|
+
* @type {DocumentFragment|Text} DOM parent of the shell's nodes. */
|
|
1970
2321
|
fragment;
|
|
1971
2322
|
|
|
1972
2323
|
/** @type {ExprPath[]} Paths to where expressions should go. */
|
|
1973
2324
|
paths = [];
|
|
1974
2325
|
|
|
1975
|
-
//
|
|
1976
|
-
events = [];
|
|
2326
|
+
// Elements with events. Not yet used.
|
|
2327
|
+
// events = [];
|
|
1977
2328
|
|
|
1978
2329
|
/** @type {int[][]} Array of paths */
|
|
1979
2330
|
ids = [];
|
|
2331
|
+
|
|
2332
|
+
/** @type {int[][]} Array of paths */
|
|
1980
2333
|
scripts = [];
|
|
2334
|
+
|
|
2335
|
+
/** @type {int[][]} Array of paths */
|
|
1981
2336
|
styles = [];
|
|
1982
2337
|
|
|
2338
|
+
/** @type {int[][]} Array of paths. Used by activateEmbeds() to quickly find components. */
|
|
1983
2339
|
staticComponents = [];
|
|
1984
2340
|
|
|
2341
|
+
/** @type {{path:int[], attribs:Object<string, string>}[]} */
|
|
2342
|
+
//componentAttribs = [];
|
|
2343
|
+
|
|
1985
2344
|
|
|
1986
2345
|
|
|
1987
2346
|
/**
|
|
1988
2347
|
* Create the nodes but without filling in the expressions.
|
|
1989
2348
|
* This is useful because the expression-less nodes created by a template can be cached.
|
|
1990
|
-
* @param html {string[]} */
|
|
2349
|
+
* @param html {string[]} Html strings, split on places where an expression exists. */
|
|
1991
2350
|
constructor(html=null) {
|
|
1992
2351
|
if (!html)
|
|
1993
2352
|
return;
|
|
1994
2353
|
|
|
1995
2354
|
//#IFDEV
|
|
1996
|
-
this.
|
|
2355
|
+
this._html = html.join('');
|
|
1997
2356
|
//#ENDIF
|
|
1998
2357
|
|
|
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));
|
|
2358
|
+
if (html.length === 1 && !html[0].match(/[<&]/)) {
|
|
2359
|
+
this.fragment = document.createTextNode(html[0]);
|
|
2360
|
+
return;
|
|
2037
2361
|
}
|
|
2038
2362
|
|
|
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
2363
|
|
|
2043
|
-
//
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
template.content.append(document.createTextNode(''));
|
|
2364
|
+
// 1. Add placeholders
|
|
2365
|
+
let joinedHtml = Shell.addPlaceholders(html);
|
|
2366
|
+
|
|
2367
|
+
let template = document.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
|
|
2368
|
+
if (joinedHtml)
|
|
2369
|
+
template.innerHTML = joinedHtml;
|
|
2370
|
+
else // Create one text node, so shell isn't empty and NodeGroups created from it have something to point the startNode and endNode at.
|
|
2371
|
+
template.content.append(document.createTextNode(''));
|
|
2052
2372
|
this.fragment = template.content;
|
|
2053
2373
|
|
|
2054
|
-
//
|
|
2374
|
+
// 2. Find placeholders
|
|
2055
2375
|
let node;
|
|
2056
2376
|
let toRemove = [];
|
|
2057
|
-
|
|
2377
|
+
let placeholdersUsed = 0;
|
|
2378
|
+
const walker = document.createTreeWalker(this.fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
|
|
2058
2379
|
while (node = walker.nextNode()) {
|
|
2059
2380
|
|
|
2060
2381
|
// Remove previous after each iteration, so paths will still be calculated correctly.
|
|
2061
2382
|
toRemove.map(el => el.remove());
|
|
2062
2383
|
toRemove = [];
|
|
2063
|
-
|
|
2384
|
+
|
|
2064
2385
|
// Replace attributes
|
|
2065
2386
|
if (node.nodeType === 1) {
|
|
2066
2387
|
for (let attr of [...node.attributes]) { // Copy the attributes array b/c we remove attributes as we go.
|
|
@@ -2068,7 +2389,8 @@ class Shell {
|
|
|
2068
2389
|
// Whole attribute
|
|
2069
2390
|
let matches = attr.name.match(/^[\ue000-\uf8ff]$/);
|
|
2070
2391
|
if (matches) {
|
|
2071
|
-
this.paths.push(new ExprPath(null, node,
|
|
2392
|
+
this.paths.push(new ExprPath(null, node, ExprPathType.AttribMultiple));
|
|
2393
|
+
placeholdersUsed ++;
|
|
2072
2394
|
node.removeAttribute(matches[0]);
|
|
2073
2395
|
}
|
|
2074
2396
|
|
|
@@ -2077,16 +2399,17 @@ class Shell {
|
|
|
2077
2399
|
let parts = attr.value.split(/[\ue000-\uf8ff]/g);
|
|
2078
2400
|
if (parts.length > 1) {
|
|
2079
2401
|
let nonEmptyParts = (parts.length === 2 && !parts[0].length && !parts[1].length) ? null : parts;
|
|
2080
|
-
let type = isEvent(attr.name) ?
|
|
2402
|
+
let type = isEvent(attr.name) ? ExprPathType.Event : ExprPathType.AttribValue;
|
|
2081
2403
|
|
|
2082
2404
|
this.paths.push(new ExprPath(null, node, type, attr.name, nonEmptyParts));
|
|
2405
|
+
placeholdersUsed += parts.length - 1;
|
|
2083
2406
|
node.setAttribute(attr.name, parts.join(''));
|
|
2084
2407
|
}
|
|
2085
2408
|
}
|
|
2086
2409
|
}
|
|
2087
2410
|
}
|
|
2088
2411
|
// Replace comment placeholders
|
|
2089
|
-
else if (node.nodeType ===
|
|
2412
|
+
else if (node.nodeType === 8 && node.nodeValue === '!✨!') {
|
|
2090
2413
|
|
|
2091
2414
|
// Get or create nodeBefore.
|
|
2092
2415
|
let nodeBefore = node.previousSibling; // Can be the same as another Path's nodeMarker.
|
|
@@ -2111,12 +2434,14 @@ class Shell {
|
|
|
2111
2434
|
}
|
|
2112
2435
|
/*#IFDEV*/assert(nodeMarker);/*#ENDIF*/
|
|
2113
2436
|
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
let path = new ExprPath(nodeBefore, nodeMarker, PathType.Content);
|
|
2117
|
-
|
|
2437
|
+
let path = new ExprPath(nodeBefore, nodeMarker, ExprPathType.Content);
|
|
2118
2438
|
this.paths.push(path);
|
|
2439
|
+
placeholdersUsed ++;
|
|
2119
2440
|
}
|
|
2441
|
+
|
|
2442
|
+
else if (node.nodeType === 3 && node.parentNode?.tagName === 'TEXTAREA' && node.textContent.includes('<!--!✨!-->'))
|
|
2443
|
+
throw new Error(`Textarea can't have expressions inside them. Use <textarea value="\${...}"> instead.`);
|
|
2444
|
+
|
|
2120
2445
|
|
|
2121
2446
|
|
|
2122
2447
|
// Sometimes users will comment out a block of html code that has expressions.
|
|
@@ -2127,8 +2452,9 @@ class Shell {
|
|
|
2127
2452
|
let parts = node.textContent.split(/[\ue000-\uf8ff]/g);
|
|
2128
2453
|
for (let i=0; i<parts.length-1; i++) {
|
|
2129
2454
|
let path = new ExprPath(node.previousSibling, node);
|
|
2130
|
-
path.type =
|
|
2455
|
+
path.type = ExprPathType.Comment;
|
|
2131
2456
|
this.paths.push(path);
|
|
2457
|
+
placeholdersUsed ++;
|
|
2132
2458
|
}
|
|
2133
2459
|
}
|
|
2134
2460
|
|
|
@@ -2146,8 +2472,9 @@ class Shell {
|
|
|
2146
2472
|
}
|
|
2147
2473
|
|
|
2148
2474
|
for (let i=0, node; node=placeholders[i]; i++) {
|
|
2149
|
-
let path = new ExprPath(node.previousSibling, node,
|
|
2475
|
+
let path = new ExprPath(node.previousSibling, node, ExprPathType.Content);
|
|
2150
2476
|
this.paths.push(path);
|
|
2477
|
+
placeholdersUsed ++;
|
|
2151
2478
|
|
|
2152
2479
|
/*#IFDEV*/path.verify();/*#ENDIF*/
|
|
2153
2480
|
}
|
|
@@ -2159,17 +2486,17 @@ class Shell {
|
|
|
2159
2486
|
}
|
|
2160
2487
|
toRemove.map(el => el.remove());
|
|
2161
2488
|
|
|
2489
|
+
// Less than or equal because there can be one path to multiple expressions
|
|
2490
|
+
// if those expressions are in the same attribute value.
|
|
2491
|
+
if (placeholdersUsed !== html.length-1)
|
|
2492
|
+
throw new Error(`Could not parse expressions in template. Check for duplicate attributes or malformed html: ${html.join('${...}')}`);
|
|
2493
|
+
|
|
2162
2494
|
// 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
2495
|
|
|
2167
|
-
// Rename "is" attributes so the Web Components don't instantiate until we have the values of their PathExpr arguments.
|
|
2496
|
+
// 3. Rename "is" attributes so the Web Components don't instantiate until we have the values of their PathExpr arguments.
|
|
2168
2497
|
// that happens in NodeGroup.applyComponentExprs()
|
|
2169
|
-
for (let el of this.fragment.querySelectorAll('[is]'))
|
|
2498
|
+
for (let el of this.fragment.querySelectorAll('[is]'))
|
|
2170
2499
|
el.setAttribute('_is', el.getAttribute('is'));
|
|
2171
|
-
// this.components.push(el);
|
|
2172
|
-
}
|
|
2173
2500
|
|
|
2174
2501
|
for (let path of this.paths) {
|
|
2175
2502
|
if (path.nodeBefore)
|
|
@@ -2177,16 +2504,63 @@ class Shell {
|
|
|
2177
2504
|
path.nodeMarkerPath = getNodePath(path.nodeMarker);
|
|
2178
2505
|
|
|
2179
2506
|
// Cache so we don't have to calculate this later inside NodeGroup.applyExprs()
|
|
2180
|
-
if (path.type ===
|
|
2507
|
+
if (path.type === ExprPathType.AttribValue && path.nodeMarker.nodeType === 1 &&
|
|
2181
2508
|
(path.nodeMarker.tagName.includes('-') || path.nodeMarker.hasAttribute('is'))) {
|
|
2182
|
-
path.type =
|
|
2509
|
+
path.type = ExprPathType.ComponentAttribValue;
|
|
2183
2510
|
}
|
|
2184
2511
|
}
|
|
2185
2512
|
|
|
2186
2513
|
this.findEmbeds();
|
|
2187
2514
|
|
|
2188
2515
|
/*#IFDEV*/this.verify();/*#ENDIF*/
|
|
2189
|
-
}
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
/**
|
|
2519
|
+
* 1. Add a Unicode placeholder char for where expressions go within attributes.
|
|
2520
|
+
* 2. Add a comment placeholder for where expressions are children of other nodes.
|
|
2521
|
+
* 3. Append -solarite-placeholder to the tag names of custom components so that we can wait to instantiate them later.
|
|
2522
|
+
* @param htmlChunks {string[]}
|
|
2523
|
+
* @returns {string} */
|
|
2524
|
+
static addPlaceholders(htmlChunks) {
|
|
2525
|
+
let tokens = [];
|
|
2526
|
+
|
|
2527
|
+
function addToken(token, context) {
|
|
2528
|
+
|
|
2529
|
+
if (context === HtmlParser.Tag) {
|
|
2530
|
+
// Find Solarite Components tags and append -solarite-placeholder to their tag names.
|
|
2531
|
+
// This way we can gather their constructor arguments and their children before we call their constructor.
|
|
2532
|
+
// Later, NodeGroup.createNewComponent() will replace them with the real components.
|
|
2533
|
+
// Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
|
|
2534
|
+
token = token.replace(/^<\/?[a-z][a-z0-9]*-[a-z0-9-]+/i, match => match + '-solarite-placeholder');
|
|
2535
|
+
}
|
|
2536
|
+
tokens.push(token);
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
let htmlParser = new HtmlParser(); // Reset the context.
|
|
2540
|
+
for (let i = 0; i < htmlChunks.length; i++) {
|
|
2541
|
+
let lastHtml = htmlChunks[i];
|
|
2542
|
+
|
|
2543
|
+
// Append -solarite-placholder to web component tags, so we can pass args to them when they're instantiated.
|
|
2544
|
+
let lastIndex = 0;
|
|
2545
|
+
let context = htmlParser.parse(lastHtml, (html, index, oldContext, newContext) => {
|
|
2546
|
+
if (lastIndex !== index) {
|
|
2547
|
+
let token = html.slice(lastIndex, index);
|
|
2548
|
+
addToken(token, oldContext);
|
|
2549
|
+
}
|
|
2550
|
+
lastIndex = index;
|
|
2551
|
+
});
|
|
2552
|
+
|
|
2553
|
+
// Insert placeholders
|
|
2554
|
+
if (i < htmlChunks.length - 1) {
|
|
2555
|
+
if (context === HtmlParser.Text)
|
|
2556
|
+
tokens.push(commentPlaceholder); // Comment Placeholder. because we can't put text in between <tr> tags for example.
|
|
2557
|
+
else
|
|
2558
|
+
tokens.push(String.fromCharCode(attribPlaceholder + i));
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
return tokens.join('');
|
|
2563
|
+
}
|
|
2190
2564
|
|
|
2191
2565
|
/**
|
|
2192
2566
|
* We find the path to every embed here once in the Shell, instead of every time a NodeGroup is instantiated.
|
|
@@ -2198,36 +2572,30 @@ class Shell {
|
|
|
2198
2572
|
* this.staticComponents */
|
|
2199
2573
|
findEmbeds() {
|
|
2200
2574
|
this.scripts = Array.prototype.map.call(this.fragment.querySelectorAll('scripts'), el => getNodePath(el));
|
|
2575
|
+
|
|
2576
|
+
// TODO: only find styles that have ExprPaths in them?
|
|
2201
2577
|
this.styles = Array.prototype.map.call(this.fragment.querySelectorAll('style'), el => getNodePath(el));
|
|
2202
2578
|
|
|
2203
2579
|
let idEls = this.fragment.querySelectorAll('[id],[data-id]');
|
|
2204
|
-
|
|
2205
2580
|
|
|
2206
2581
|
// Check for valid id names.
|
|
2207
2582
|
for (let el of idEls) {
|
|
2208
2583
|
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
2209
|
-
if (div.hasOwnProperty(id))
|
|
2584
|
+
if (Globals$1.div.hasOwnProperty(id))
|
|
2210
2585
|
throw new Error(`<${el.tagName.toLowerCase()} id="${id}"> can't override existing HTMLElement id property.`)
|
|
2211
2586
|
}
|
|
2212
2587
|
|
|
2213
|
-
|
|
2214
2588
|
this.ids = Array.prototype.map.call(idEls, el => getNodePath(el));
|
|
2215
2589
|
|
|
2216
|
-
// Events (not yet used)
|
|
2217
2590
|
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
2591
|
if (el.tagName.includes('-') || el.hasAttribute('_is'))
|
|
2223
2592
|
|
|
2224
|
-
// Dynamic components have attributes with expression values.
|
|
2593
|
+
// Dynamic components are components that have attributes with expression values.
|
|
2225
2594
|
// They are created from applyExprs()
|
|
2226
2595
|
// But static components are created in a separate path inside the NodeGroup constructor.
|
|
2227
2596
|
if (!this.paths.find(path => path.nodeMarker === el))
|
|
2228
2597
|
this.staticComponents.push(getNodePath(el));
|
|
2229
2598
|
}
|
|
2230
|
-
|
|
2231
2599
|
}
|
|
2232
2600
|
|
|
2233
2601
|
/**
|
|
@@ -2235,10 +2603,10 @@ class Shell {
|
|
|
2235
2603
|
* @param htmlStrings {string[]} Typically comes from a Template.
|
|
2236
2604
|
* @returns {Shell} */
|
|
2237
2605
|
static get(htmlStrings) {
|
|
2238
|
-
let result = Globals.shells.get(htmlStrings);
|
|
2606
|
+
let result = Globals$1.shells.get(htmlStrings);
|
|
2239
2607
|
if (!result) {
|
|
2240
2608
|
result = new Shell(htmlStrings);
|
|
2241
|
-
Globals.shells.set(htmlStrings, result); // cache
|
|
2609
|
+
Globals$1.shells.set(htmlStrings, result); // cache
|
|
2242
2610
|
}
|
|
2243
2611
|
|
|
2244
2612
|
/*#IFDEV*/result.verify();/*#ENDIF*/
|
|
@@ -2254,7 +2622,14 @@ class Shell {
|
|
|
2254
2622
|
}
|
|
2255
2623
|
}
|
|
2256
2624
|
//#ENDIF
|
|
2257
|
-
}
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
|
|
2628
|
+
const commentPlaceholder = `<!--!✨!-->`;
|
|
2629
|
+
|
|
2630
|
+
|
|
2631
|
+
// We increment the placeholder char as we go because nodes can't have the same attribute more than once.
|
|
2632
|
+
const attribPlaceholder = 0xe000; // https://en.wikipedia.org/wiki/Private_Use_Areas 6400.
|
|
2258
2633
|
|
|
2259
2634
|
/** @typedef {boolean|string|number|function|Object|Array|Date|Node|Template} Expr */
|
|
2260
2635
|
|
|
@@ -2280,7 +2655,8 @@ class NodeGroup {
|
|
|
2280
2655
|
startNode;
|
|
2281
2656
|
|
|
2282
2657
|
/** @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
|
|
2658
|
+
* An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.
|
|
2659
|
+
* TODO: But sometimes startNode and endNode point to the same node. Document htis inconsistency. */
|
|
2284
2660
|
endNode;
|
|
2285
2661
|
|
|
2286
2662
|
/** @type {ExprPath[]} */
|
|
@@ -2298,11 +2674,11 @@ class NodeGroup {
|
|
|
2298
2674
|
nodesCache;
|
|
2299
2675
|
|
|
2300
2676
|
/**
|
|
2677
|
+
* A map between <style> Elements and their text content.
|
|
2678
|
+
* This lets NodeGroup.updateStyles() see when the style text has changed.
|
|
2301
2679
|
* @type {?Map<HTMLStyleElement, string>} */
|
|
2302
2680
|
styles;
|
|
2303
2681
|
|
|
2304
|
-
currentComponentProps = {};
|
|
2305
|
-
|
|
2306
2682
|
|
|
2307
2683
|
/**
|
|
2308
2684
|
* Create an "instantiated" NodeGroup from a Template and add it to an element.
|
|
@@ -2310,14 +2686,26 @@ class NodeGroup {
|
|
|
2310
2686
|
* @param parentPath {?ExprPath} */
|
|
2311
2687
|
constructor(template, parentPath=null) {
|
|
2312
2688
|
if (!(this instanceof RootNodeGroup)) {
|
|
2689
|
+
|
|
2313
2690
|
let [fragment, shell] = this.init(template, parentPath);
|
|
2314
2691
|
|
|
2315
|
-
|
|
2692
|
+
if (fragment && template.exprs.length) {
|
|
2693
|
+
this.updatePaths(fragment, shell.paths);
|
|
2316
2694
|
|
|
2317
|
-
|
|
2695
|
+
// Static web components can sometimes have children created via expressions.
|
|
2696
|
+
// But calling applyExprs() will mess up the shell's path to them.
|
|
2697
|
+
// So we find them first, then call activateStaticComponents() after their children have been created.
|
|
2698
|
+
let staticComponents = this.findStaticComponents(fragment, shell);
|
|
2318
2699
|
|
|
2319
|
-
|
|
2320
|
-
|
|
2700
|
+
this.activateEmbeds(fragment, shell);
|
|
2701
|
+
|
|
2702
|
+
// Apply exprs
|
|
2703
|
+
this.applyExprs(template.exprs);
|
|
2704
|
+
|
|
2705
|
+
this.activateStaticComponents(staticComponents);
|
|
2706
|
+
}
|
|
2707
|
+
else if (shell)
|
|
2708
|
+
this.activateEmbeds(fragment, shell);
|
|
2321
2709
|
}
|
|
2322
2710
|
}
|
|
2323
2711
|
|
|
@@ -2345,57 +2733,104 @@ class NodeGroup {
|
|
|
2345
2733
|
template.nodeGroup = this;
|
|
2346
2734
|
|
|
2347
2735
|
// 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
2736
|
|
|
2351
|
-
|
|
2352
|
-
this.
|
|
2353
|
-
|
|
2737
|
+
// If it's just a text node, skip a bunch of unnecessary steps.
|
|
2738
|
+
if (!(this instanceof RootNodeGroup) && !template.exprs.length && !template.html[0].includes('<')) {
|
|
2739
|
+
//let doc = this.rootNg.startNode?.ownerDocument || document;
|
|
2740
|
+
let textNode = document.createTextNode(template.html[0]);
|
|
2741
|
+
|
|
2742
|
+
this.startNode = this.endNode = textNode;
|
|
2743
|
+
return [];
|
|
2744
|
+
}
|
|
2745
|
+
else {
|
|
2746
|
+
let shell = Shell.get(template.html);
|
|
2747
|
+
let fragment = shell.fragment.cloneNode(true);
|
|
2354
2748
|
|
|
2355
|
-
|
|
2749
|
+
if (fragment instanceof DocumentFragment) {
|
|
2750
|
+
let childNodes = fragment.childNodes;
|
|
2751
|
+
this.startNode = childNodes[0];
|
|
2752
|
+
this.endNode = childNodes[childNodes.length - 1];
|
|
2753
|
+
}
|
|
2754
|
+
else {
|
|
2755
|
+
this.startNode = this.endNode = fragment;
|
|
2756
|
+
}
|
|
2757
|
+
return [fragment, shell];
|
|
2758
|
+
}
|
|
2356
2759
|
}
|
|
2357
2760
|
|
|
2358
2761
|
/**
|
|
2359
2762
|
* Use the paths to insert the given expressions.
|
|
2360
2763
|
* Dispatches expression handling to other functions depending on the path type.
|
|
2361
2764
|
* @param exprs {(*|*[]|function|Template)[]}
|
|
2362
|
-
* @param paths {?ExprPath[]} Optional. */
|
|
2765
|
+
* @param paths {?ExprPath[]} Optional. Only used for testing. Normally uses this.paths. */
|
|
2363
2766
|
applyExprs(exprs, paths=null) {
|
|
2364
2767
|
paths = paths || this.paths;
|
|
2365
2768
|
|
|
2366
|
-
/*#IFDEV*/
|
|
2769
|
+
/*#IFDEV*/
|
|
2770
|
+
this.verify();/*#ENDIF*/
|
|
2771
|
+
|
|
2772
|
+
// Things to consider:
|
|
2773
|
+
// 1. One path may use multipe expressions. E.g. <div class="${1} ${2}">
|
|
2774
|
+
// 2. One component may need to use multiple attribute paths to be instantiated.
|
|
2775
|
+
// 3. We apply them in reverse order so that a <select> box has its children created from an expression
|
|
2776
|
+
// before its instantiated and its value attribute is set via an expression.
|
|
2777
|
+
|
|
2778
|
+
let exprIndex = exprs.length - 1; // Update exprs at paths.
|
|
2779
|
+
let lastComponentPathIndex;
|
|
2780
|
+
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.
|
|
2781
|
+
for (let i = paths.length - 1, path; path = paths[i]; i--) {
|
|
2782
|
+
let prevPath = paths[i - 1];
|
|
2783
|
+
let nextPath = paths[i + 1];
|
|
2784
|
+
|
|
2785
|
+
// Get the expressions associated with this path.
|
|
2786
|
+
if (path.attrValue?.length > 2) {
|
|
2787
|
+
let startIndex = (exprIndex - (path.attrValue.length - 1)) + 1;
|
|
2788
|
+
pathExprs[i] = exprs.slice(startIndex, exprIndex + 1); // probably doesn't allocate if the JS vm implements copy on write.
|
|
2789
|
+
exprIndex -= pathExprs[i].length;
|
|
2790
|
+
} else {
|
|
2791
|
+
pathExprs[i] = [exprs[exprIndex]];
|
|
2792
|
+
exprIndex--;
|
|
2793
|
+
}
|
|
2367
2794
|
|
|
2368
|
-
|
|
2369
|
-
|
|
2795
|
+
// TODO: Need to end and restart this block when going from one component to the next?
|
|
2796
|
+
// Think of having two adjacent components.
|
|
2797
|
+
// But the dynamicAttribsAdjacet test already passes.
|
|
2370
2798
|
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2799
|
+
// If a component:
|
|
2800
|
+
// 1. Instantiate it if it hasn't already been, sending all expr's to its constructor.
|
|
2801
|
+
// 2. Otherwise send them to its render function.
|
|
2802
|
+
// Components with no expressions as attributes are instead activated in activateEmbeds().
|
|
2803
|
+
if (path.nodeMarker !== this.rootNg.root && path.isComponent()) {
|
|
2375
2804
|
|
|
2376
|
-
|
|
2805
|
+
if (!nextPath || !nextPath.isComponent() || nextPath.nodeMarker !== path.nodeMarker)
|
|
2806
|
+
lastComponentPathIndex = i;
|
|
2807
|
+
let isFirstComponentPath = !prevPath || !prevPath.isComponent() || prevPath.nodeMarker !== path.nodeMarker;
|
|
2377
2808
|
|
|
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
|
-
}
|
|
2809
|
+
if (isFirstComponentPath) {
|
|
2383
2810
|
|
|
2384
|
-
|
|
2811
|
+
let componentProps = {};
|
|
2812
|
+
for (let j=i; j<=lastComponentPathIndex; j++) {
|
|
2813
|
+
let attrName = paths[j].attrName; // Util.dashesToCamel(paths[j].attrName);
|
|
2814
|
+
componentProps[attrName] = pathExprs[j].length > 1 ? pathExprs[j].join('') : pathExprs[j][0];
|
|
2815
|
+
}
|
|
2385
2816
|
|
|
2386
|
-
|
|
2817
|
+
this.applyComponentExprs(path.nodeMarker, componentProps);
|
|
2387
2818
|
|
|
2819
|
+
// Set attributes on component.
|
|
2820
|
+
for (let j=i; j<=lastComponentPathIndex; j++)
|
|
2821
|
+
paths[j].apply(pathExprs[j]);
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2388
2824
|
|
|
2389
|
-
|
|
2390
|
-
|
|
2825
|
+
// Else apply it normally
|
|
2826
|
+
else
|
|
2827
|
+
path.apply(pathExprs[i]);
|
|
2391
2828
|
|
|
2392
2829
|
|
|
2393
|
-
//
|
|
2394
|
-
|
|
2395
|
-
this.applyComponentExprs(lastNode, this.currentComponentProps);
|
|
2396
|
-
this.currentComponentProps = {};
|
|
2397
|
-
}
|
|
2830
|
+
} // end for(path of this.paths)
|
|
2831
|
+
|
|
2398
2832
|
|
|
2833
|
+
// TODO: Only do this if we have ExprPaths within styles?
|
|
2399
2834
|
this.updateStyles();
|
|
2400
2835
|
|
|
2401
2836
|
// Invalidate the nodes cache because we just changed it.
|
|
@@ -2403,10 +2838,12 @@ class NodeGroup {
|
|
|
2403
2838
|
|
|
2404
2839
|
// If there's leftover expressions, there's probably an issue with the Shell that created this NodeGroup,
|
|
2405
2840
|
// and the number of paths not matching.
|
|
2406
|
-
/*#IFDEV*/
|
|
2841
|
+
/*#IFDEV*/
|
|
2842
|
+
assert(exprIndex === -1);/*#ENDIF*/
|
|
2407
2843
|
|
|
2408
2844
|
|
|
2409
|
-
/*#IFDEV*/
|
|
2845
|
+
/*#IFDEV*/
|
|
2846
|
+
this.verify();/*#ENDIF*/
|
|
2410
2847
|
}
|
|
2411
2848
|
|
|
2412
2849
|
/**
|
|
@@ -2430,14 +2867,18 @@ class NodeGroup {
|
|
|
2430
2867
|
|
|
2431
2868
|
// Call render() with the same params that would've been passed to the constructor.
|
|
2432
2869
|
else if (el.render) {
|
|
2433
|
-
let oldHash = Globals.
|
|
2434
|
-
if (oldHash !== newHash)
|
|
2435
|
-
|
|
2870
|
+
let oldHash = Globals$1.componentArgsHash.get(el);
|
|
2871
|
+
if (oldHash !== newHash) {
|
|
2872
|
+
let args = {};
|
|
2873
|
+
for (let name in props || {})
|
|
2874
|
+
args[Util.dashesToCamel(name)] = props[name];
|
|
2875
|
+
el.render(args); // Pass new values of props to render so it can decide how it wants to respond.
|
|
2876
|
+
}
|
|
2436
2877
|
}
|
|
2437
2878
|
|
|
2438
|
-
Globals.
|
|
2879
|
+
Globals$1.componentArgsHash.set(el, newHash);
|
|
2439
2880
|
}
|
|
2440
|
-
|
|
2881
|
+
|
|
2441
2882
|
/**
|
|
2442
2883
|
* We swap the placeholder element for the real element so we can pass its dynamic attributes
|
|
2443
2884
|
* to its constructor.
|
|
@@ -2451,72 +2892,48 @@ class NodeGroup {
|
|
|
2451
2892
|
createNewComponent(el, isPreHtmlElement=undefined, props=undefined) {
|
|
2452
2893
|
if (isPreHtmlElement === undefined)
|
|
2453
2894
|
isPreHtmlElement = !el.hasAttribute('_is');
|
|
2454
|
-
|
|
2895
|
+
|
|
2455
2896
|
let tagName = (isPreHtmlElement
|
|
2456
|
-
? el.tagName.
|
|
2457
|
-
? el.tagName.slice(0, -21)
|
|
2458
|
-
: el.tagName
|
|
2897
|
+
? el.tagName.slice(0, -21) // Remove -SOLARITE-PLACEHOLDER
|
|
2459
2898
|
: el.getAttribute('is')).toLowerCase();
|
|
2460
2899
|
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
// Pass other attribs to constructor, since otherwise they're not yet set on the element,
|
|
2464
|
-
// and the constructor would otherwise have no way to see them.
|
|
2465
|
-
if (el.attributes.length) {
|
|
2466
|
-
if (!props)
|
|
2467
|
-
props = {};
|
|
2468
|
-
for (let attrib of el.attributes)
|
|
2469
|
-
if (!props.hasOwnProperty(attrib.name))
|
|
2470
|
-
props[attrib.name] = attrib.value;
|
|
2471
|
-
}
|
|
2472
|
-
|
|
2473
|
-
// Create CustomElement and
|
|
2900
|
+
|
|
2901
|
+
// Throw if custom element isn't defined.
|
|
2474
2902
|
let Constructor = customElements.get(tagName);
|
|
2475
2903
|
if (!Constructor)
|
|
2476
2904
|
throw new Error(`The custom tag name ${tagName} is not registered.`)
|
|
2477
2905
|
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
//
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2906
|
+
let args = {};
|
|
2907
|
+
for (let name in props || {})
|
|
2908
|
+
args[Util.dashesToCamel(name)] = props[name];
|
|
2909
|
+
|
|
2910
|
+
// Pass other attribs to constructor, since otherwise they're not yet set on the element,
|
|
2911
|
+
// and the constructor would otherwise have no way to see them.
|
|
2912
|
+
if (el.attributes.length) {
|
|
2913
|
+
for (let attrib of el.attributes) {
|
|
2914
|
+
let attribName = Util.dashesToCamel(attrib.name);
|
|
2915
|
+
if (!args.hasOwnProperty(attribName))
|
|
2916
|
+
args[attribName] = attrib.value;
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
// Create the web component.
|
|
2921
|
+
// Get the children that aren't Solarite's comment placeholders.
|
|
2922
|
+
let ch = [...el.childNodes].filter(node => node.nodeType !== Node.COMMENT_NODE || !node.nodeValue.startsWith('ExprPath'));
|
|
2923
|
+
let newEl = new Constructor(args, ch);
|
|
2486
2924
|
|
|
2487
2925
|
if (!isPreHtmlElement)
|
|
2488
2926
|
newEl.setAttribute('is', el.getAttribute('is').toLowerCase());
|
|
2927
|
+
|
|
2928
|
+
// Replace the placeholder tag with the instantiated web component.
|
|
2489
2929
|
el.replaceWith(newEl);
|
|
2490
2930
|
|
|
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
2931
|
// If an id pointed at the placeholder, update it to point to the new element.
|
|
2515
2932
|
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
2516
2933
|
if (id)
|
|
2517
2934
|
delve(this.getRootNode(), id.split(/\./g), newEl);
|
|
2518
|
-
|
|
2519
|
-
|
|
2935
|
+
|
|
2936
|
+
|
|
2520
2937
|
// Update paths to use replaced element.
|
|
2521
2938
|
for (let path of this.paths) {
|
|
2522
2939
|
if (path.nodeMarker === el)
|
|
@@ -2528,31 +2945,31 @@ class NodeGroup {
|
|
|
2528
2945
|
this.startNode = newEl;
|
|
2529
2946
|
if (this.endNode === el)
|
|
2530
2947
|
this.endNode = newEl;
|
|
2531
|
-
|
|
2532
|
-
|
|
2948
|
+
|
|
2949
|
+
|
|
2533
2950
|
// applyComponentExprs() is called because we're rendering.
|
|
2534
2951
|
// So we want to render the sub-component also.
|
|
2535
2952
|
if (newEl.renderFirstTime)
|
|
2536
2953
|
newEl.renderFirstTime();
|
|
2537
|
-
|
|
2954
|
+
|
|
2538
2955
|
// Copy attributes over.
|
|
2539
2956
|
for (let attrib of el.attributes)
|
|
2540
2957
|
if (attrib.name !== '_is')
|
|
2541
2958
|
newEl.setAttribute(attrib.name, attrib.value);
|
|
2542
2959
|
|
|
2543
2960
|
// Set dynamic attributes if they are primitive types.
|
|
2544
|
-
for (let name in
|
|
2545
|
-
let val =
|
|
2961
|
+
for (let name in props) {
|
|
2962
|
+
let val = props[name];
|
|
2546
2963
|
if (typeof val === 'boolean') {
|
|
2547
2964
|
if (val !== false && val !== undefined && val !== null)
|
|
2548
2965
|
newEl.setAttribute(name, '');
|
|
2549
2966
|
}
|
|
2550
2967
|
|
|
2551
|
-
// If type
|
|
2968
|
+
// If type is a non-boolean primitive, set the attribute value.
|
|
2552
2969
|
else if (['number', 'bigint', 'string'].includes(typeof val))
|
|
2553
2970
|
newEl.setAttribute(name, val);
|
|
2554
2971
|
}
|
|
2555
|
-
|
|
2972
|
+
|
|
2556
2973
|
return newEl;
|
|
2557
2974
|
}
|
|
2558
2975
|
|
|
@@ -2598,8 +3015,7 @@ class NodeGroup {
|
|
|
2598
3015
|
|
|
2599
3016
|
/**
|
|
2600
3017
|
* Requires the nodeCache to be present. */
|
|
2601
|
-
|
|
2602
|
-
/*#IFDEV*/assert(!this.startNode.parentNode);/*#ENDIF*/
|
|
3018
|
+
removeAndSaveOrphans() {
|
|
2603
3019
|
/*#IFDEV*/assert(this.nodesCache);/*#ENDIF*/
|
|
2604
3020
|
let fragment = document.createDocumentFragment();
|
|
2605
3021
|
for (let node of this.getNodes())
|
|
@@ -2609,8 +3025,9 @@ class NodeGroup {
|
|
|
2609
3025
|
|
|
2610
3026
|
updatePaths(fragment, paths, offset) {
|
|
2611
3027
|
// Update paths to point to the fragment.
|
|
2612
|
-
|
|
2613
|
-
|
|
3028
|
+
let pathLength = paths.length;
|
|
3029
|
+
this.paths.length = pathLength;
|
|
3030
|
+
for (let i=0; i<pathLength; i++) {
|
|
2614
3031
|
let path = paths[i].clone(fragment, offset);
|
|
2615
3032
|
path.parentNg = this;
|
|
2616
3033
|
this.paths[i] = path;
|
|
@@ -2632,23 +3049,23 @@ class NodeGroup {
|
|
|
2632
3049
|
* An interleaved array of sets of nodes and top-level ExprPaths
|
|
2633
3050
|
* @type {(Node|HTMLElement|ExprPath)[]} */
|
|
2634
3051
|
get nodes() { throw new Error('')};
|
|
2635
|
-
|
|
3052
|
+
|
|
2636
3053
|
get debug() {
|
|
2637
3054
|
return [
|
|
2638
3055
|
`parentNode: ${this.parentNode?.tagName?.toLowerCase()}`,
|
|
2639
3056
|
'nodes:',
|
|
2640
3057
|
...setIndent(this.getNodes().map(item => {
|
|
2641
3058
|
if (item instanceof Node) {
|
|
2642
|
-
|
|
3059
|
+
|
|
2643
3060
|
let tree = nodeToArrayTree(item, nextNode => {
|
|
2644
|
-
|
|
2645
|
-
let path = this.paths.find(path=>path.type ===
|
|
3061
|
+
|
|
3062
|
+
let path = this.paths.find(path=>path.type === ExprPathType.Content && path.getNodes().includes(nextNode));
|
|
2646
3063
|
if (path)
|
|
2647
3064
|
return [`Path.nodes:`]
|
|
2648
|
-
|
|
3065
|
+
|
|
2649
3066
|
return [];
|
|
2650
3067
|
});
|
|
2651
|
-
|
|
3068
|
+
|
|
2652
3069
|
// TODO: How to indend nodes belonging to a path vs those that just occur after the path?
|
|
2653
3070
|
return flattenAndIndent(tree)
|
|
2654
3071
|
}
|
|
@@ -2659,10 +3076,10 @@ class NodeGroup {
|
|
|
2659
3076
|
}
|
|
2660
3077
|
|
|
2661
3078
|
get debugNodes() { return this.getNodes() }
|
|
2662
|
-
|
|
2663
|
-
|
|
3079
|
+
|
|
3080
|
+
|
|
2664
3081
|
get debugNodesHtml() { return this.getNodes().map(n => n.outerHTML || n.textContent) }
|
|
2665
|
-
|
|
3082
|
+
|
|
2666
3083
|
verify() {
|
|
2667
3084
|
if (!window.verify)
|
|
2668
3085
|
return;
|
|
@@ -2677,7 +3094,7 @@ class NodeGroup {
|
|
|
2677
3094
|
|
|
2678
3095
|
// if (this.parentPath)
|
|
2679
3096
|
// assert(this.parentPath.nodeGroups.includes(this));
|
|
2680
|
-
|
|
3097
|
+
|
|
2681
3098
|
for (let path of this.paths) {
|
|
2682
3099
|
assert(path.parentNg === this);
|
|
2683
3100
|
|
|
@@ -2693,61 +3110,70 @@ class NodeGroup {
|
|
|
2693
3110
|
}
|
|
2694
3111
|
//#ENDIF
|
|
2695
3112
|
|
|
3113
|
+
findStaticComponents(root, shell, pathOffset=0) {
|
|
3114
|
+
let result = [];
|
|
2696
3115
|
|
|
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.
|
|
3116
|
+
// static components. These are WebComponents that do not have any constructor arguments that are expressions.
|
|
3117
|
+
// Those are instead created by applyExpr() which calls applyComponentExprs() which calls createNewcomponent().
|
|
3118
|
+
// Maybe someday these two paths will be merged?
|
|
3119
|
+
// Must happen before ids because createNewComponent will replace the element.
|
|
2705
3120
|
for (let path of shell.staticComponents) {
|
|
2706
3121
|
if (pathOffset)
|
|
2707
3122
|
path = path.slice(0, -pathOffset);
|
|
2708
3123
|
let el = resolveNodePath(root, path);
|
|
2709
3124
|
|
|
2710
3125
|
// Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
|
|
3126
|
+
// Recreating it is necessary so we can pass the constructor args to it.
|
|
2711
3127
|
if (root !== el/* && !isReplaceEl(root, el)*/) // TODO: is isReplaceEl necessary?
|
|
2712
|
-
|
|
3128
|
+
result.push(el);
|
|
2713
3129
|
}
|
|
3130
|
+
return result;
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
activateStaticComponents(staticComponents) {
|
|
3134
|
+
for (let el of staticComponents)
|
|
3135
|
+
this.createNewComponent(el);
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
/**
|
|
3139
|
+
* @param root {HTMLElement}
|
|
3140
|
+
* @param shell {Shell}
|
|
3141
|
+
* @param pathOffset {int} */
|
|
3142
|
+
activateEmbeds(root, shell, pathOffset=0) {
|
|
2714
3143
|
|
|
2715
3144
|
let rootEl = this.rootNg.root;
|
|
2716
3145
|
if (rootEl) {
|
|
3146
|
+
let options = this.rootNg.options;
|
|
2717
3147
|
|
|
2718
3148
|
// ids
|
|
2719
|
-
if (
|
|
3149
|
+
if (options?.ids !== false) {
|
|
2720
3150
|
for (let path of shell.ids) {
|
|
2721
3151
|
if (pathOffset)
|
|
2722
3152
|
path = path.slice(0, -pathOffset);
|
|
2723
3153
|
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
|
-
}
|
|
3154
|
+
Util.bindId(rootEl, el);
|
|
3155
|
+
}
|
|
2734
3156
|
}
|
|
2735
3157
|
|
|
2736
3158
|
// styles
|
|
2737
|
-
if (
|
|
3159
|
+
if (options?.styles !== false) {
|
|
2738
3160
|
if (shell.styles.length)
|
|
2739
3161
|
this.styles = new Map();
|
|
2740
3162
|
for (let path of shell.styles) {
|
|
2741
3163
|
if (pathOffset)
|
|
2742
3164
|
path = path.slice(0, -pathOffset);
|
|
3165
|
+
|
|
3166
|
+
/** @type {HTMLStyleElement} */
|
|
2743
3167
|
let style = resolveNodePath(root, path);
|
|
2744
|
-
|
|
2745
|
-
|
|
3168
|
+
if (rootEl.nodeType === 1) {
|
|
3169
|
+
Util.bindStyles(style, rootEl);
|
|
3170
|
+
this.styles.set(style, style.textContent);
|
|
3171
|
+
}
|
|
2746
3172
|
}
|
|
2747
3173
|
|
|
2748
3174
|
}
|
|
2749
3175
|
// scripts
|
|
2750
|
-
if (
|
|
3176
|
+
if (options?.scripts !== false) {
|
|
2751
3177
|
for (let path of shell.scripts) {
|
|
2752
3178
|
if (pathOffset)
|
|
2753
3179
|
path = path.slice(0, -pathOffset);
|
|
@@ -2768,20 +3194,8 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2768
3194
|
root;
|
|
2769
3195
|
|
|
2770
3196
|
/**
|
|
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>} */
|
|
3197
|
+
* When we call renerWatched() we re-render these expressions, then clear this to a new Map()
|
|
3198
|
+
* @type {Map<ExprPath, ValueOp|WholeArrayOp|ArraySpliceOp[]>} */
|
|
2785
3199
|
exprsToRender = new Map();
|
|
2786
3200
|
|
|
2787
3201
|
/**
|
|
@@ -2798,82 +3212,102 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2798
3212
|
this.rootNg = this;
|
|
2799
3213
|
let [fragment, shell] = this.init(template);
|
|
2800
3214
|
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
if (el.childNodes.length) {
|
|
2810
|
-
slotFragment = document.createDocumentFragment();
|
|
2811
|
-
slotFragment.append(...el.childNodes);
|
|
3215
|
+
if (fragment instanceof Text) {
|
|
3216
|
+
|
|
3217
|
+
if (el) {
|
|
3218
|
+
this.startNode = el;
|
|
3219
|
+
this.endNode = el;
|
|
3220
|
+
if (fragment.nodeValue.length)
|
|
3221
|
+
el.append(fragment);
|
|
3222
|
+
this.root = el;
|
|
2812
3223
|
}
|
|
3224
|
+
Globals$1.nodeGroups.set(this.root, this);
|
|
3225
|
+
}
|
|
3226
|
+
else {
|
|
2813
3227
|
|
|
2814
|
-
|
|
3228
|
+
// If adding NodeGroup to an element.
|
|
3229
|
+
let offset = 0;
|
|
3230
|
+
let root = fragment; // TODO: Rename so it's not confused with this.root.
|
|
3231
|
+
if (el) {
|
|
3232
|
+
Globals$1.nodeGroups.set(el, this);
|
|
3233
|
+
|
|
3234
|
+
// Save slot children
|
|
3235
|
+
let slotChildren;
|
|
3236
|
+
if (el.childNodes.length) {
|
|
3237
|
+
slotChildren = document.createDocumentFragment();
|
|
3238
|
+
slotChildren.append(...el.childNodes);
|
|
3239
|
+
}
|
|
2815
3240
|
|
|
2816
|
-
|
|
2817
|
-
if (isReplaceEl(fragment, el)) {
|
|
2818
|
-
el.append(...fragment.children[0].childNodes);
|
|
3241
|
+
this.root = el;
|
|
2819
3242
|
|
|
2820
|
-
//
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
el.setAttribute(attrib.name, attrib.value);
|
|
3243
|
+
// If el should replace the root node of the fragment.
|
|
3244
|
+
if (isReplaceEl(fragment, el)) {
|
|
3245
|
+
el.append(...fragment.children[0].childNodes);
|
|
2824
3246
|
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
|
|
2830
|
-
if (!isEmpty)
|
|
2831
|
-
el.append(...fragment.childNodes);
|
|
2832
|
-
}
|
|
3247
|
+
// Copy attributes
|
|
3248
|
+
for (let attrib of fragment.children[0].attributes)
|
|
3249
|
+
if (!el.hasAttribute(attrib.name))
|
|
3250
|
+
el.setAttribute(attrib.name, attrib.value);
|
|
2833
3251
|
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
let
|
|
2838
|
-
if (
|
|
2839
|
-
|
|
2840
|
-
|
|
3252
|
+
// Go one level deeper into all of shell's paths.
|
|
3253
|
+
offset = 1;
|
|
3254
|
+
} else {
|
|
3255
|
+
let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
|
|
3256
|
+
if (!isEmpty)
|
|
3257
|
+
el.append(...fragment.childNodes);
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
// Setup children
|
|
3261
|
+
if (slotChildren) {
|
|
3262
|
+
|
|
3263
|
+
// Named slots
|
|
3264
|
+
for (let slot of el.querySelectorAll('slot[name]')) {
|
|
3265
|
+
let name = slot.getAttribute('name');
|
|
3266
|
+
if (name) {
|
|
3267
|
+
let slotChildren2 = slotChildren.querySelectorAll(`[slot='${name}']`);
|
|
3268
|
+
slot.append(...slotChildren2);
|
|
3269
|
+
}
|
|
2841
3270
|
}
|
|
3271
|
+
|
|
3272
|
+
// Unnamed slots
|
|
3273
|
+
let unamedSlot = el.querySelector('slot:not([name])');
|
|
3274
|
+
if (unamedSlot)
|
|
3275
|
+
unamedSlot.append(slotChildren);
|
|
3276
|
+
|
|
3277
|
+
// No slots
|
|
3278
|
+
else
|
|
3279
|
+
el.append(slotChildren);
|
|
2842
3280
|
}
|
|
2843
|
-
let unamedSlot = el.querySelector('slot:not([name])');
|
|
2844
|
-
if (unamedSlot)
|
|
2845
|
-
unamedSlot.append(slotFragment);
|
|
2846
|
-
else
|
|
2847
|
-
el.append(slotFragment);
|
|
2848
|
-
}
|
|
2849
3281
|
|
|
2850
|
-
|
|
3282
|
+
root = el;
|
|
2851
3283
|
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
|
|
3284
|
+
this.startNode = el;
|
|
3285
|
+
this.endNode = el;
|
|
3286
|
+
} else {
|
|
3287
|
+
let singleEl = getSingleEl(fragment);
|
|
3288
|
+
this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
|
|
2858
3289
|
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
3290
|
+
Globals$1.nodeGroups.set(this.root, this);
|
|
3291
|
+
if (singleEl) {
|
|
3292
|
+
root = singleEl;
|
|
3293
|
+
offset = 1;
|
|
3294
|
+
}
|
|
2863
3295
|
}
|
|
2864
|
-
}
|
|
2865
3296
|
|
|
2866
|
-
|
|
3297
|
+
this.updatePaths(root, shell.paths, offset);
|
|
2867
3298
|
|
|
2868
|
-
|
|
3299
|
+
// Static web components can sometimes have children created via expressions.
|
|
3300
|
+
// But calling applyExprs() will mess up the shell's path to them.
|
|
3301
|
+
// So we find them first, then call activateStaticComponents() after their children have been created.
|
|
3302
|
+
let staticComponents = this.findStaticComponents(root, shell, offset);
|
|
2869
3303
|
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
3304
|
+
this.activateEmbeds(root, shell, offset);
|
|
3305
|
+
|
|
3306
|
+
// Apply exprs
|
|
3307
|
+
this.applyExprs(template.exprs);
|
|
2873
3308
|
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
this.mapCallbacks = new Map();
|
|
3309
|
+
this.activateStaticComponents(staticComponents);
|
|
3310
|
+
}
|
|
2877
3311
|
}
|
|
2878
3312
|
}
|
|
2879
3313
|
|
|
@@ -2895,8 +3329,8 @@ function getSingleEl(fragment) {
|
|
|
2895
3329
|
* @param el {HTMLElement}
|
|
2896
3330
|
* @returns {boolean} */
|
|
2897
3331
|
function isReplaceEl(fragment, el) {
|
|
2898
|
-
return
|
|
2899
|
-
&&
|
|
3332
|
+
return fragment.children.length===1
|
|
3333
|
+
&& el.tagName.includes('-')
|
|
2900
3334
|
&& fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') === el.tagName;
|
|
2901
3335
|
}
|
|
2902
3336
|
|
|
@@ -2915,19 +3349,9 @@ class Template {
|
|
|
2915
3349
|
/** @type {Array} Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
|
|
2916
3350
|
hashedFields;
|
|
2917
3351
|
|
|
2918
|
-
/**
|
|
2919
|
-
* @deprecated
|
|
2920
|
-
* @type {ExprPath} Used with forEach() from watch.js
|
|
2921
|
-
* Set in ExprPath.apply() */
|
|
2922
|
-
parentPath;
|
|
2923
|
-
|
|
2924
3352
|
/** @type {NodeGroup} */
|
|
2925
3353
|
nodeGroup;
|
|
2926
3354
|
|
|
2927
|
-
/**
|
|
2928
|
-
* @type {string[][]} */
|
|
2929
|
-
paths = [];
|
|
2930
|
-
|
|
2931
3355
|
/**
|
|
2932
3356
|
*
|
|
2933
3357
|
* @param htmlStrings {string[]}
|
|
@@ -2978,17 +3402,21 @@ class Template {
|
|
|
2978
3402
|
if (standalone) {
|
|
2979
3403
|
ng = new RootNodeGroup(this, null, options);
|
|
2980
3404
|
el = ng.getRootNode();
|
|
2981
|
-
|
|
3405
|
+
Globals$1.nodeGroups.set(el, ng); // Why was this commented out?
|
|
2982
3406
|
firstTime = true;
|
|
2983
3407
|
}
|
|
2984
3408
|
else {
|
|
2985
|
-
ng = Globals.nodeGroups.get(el);
|
|
3409
|
+
ng = Globals$1.nodeGroups.get(el);
|
|
2986
3410
|
if (!ng) {
|
|
2987
3411
|
ng = new RootNodeGroup(this, el, options);
|
|
2988
|
-
|
|
3412
|
+
Globals$1.nodeGroups.set(el, ng); // Why was this commented out?
|
|
2989
3413
|
firstTime = true;
|
|
2990
3414
|
}
|
|
2991
|
-
|
|
3415
|
+
|
|
3416
|
+
// This can happen if we try manually rendering one template to a NodeGroup that was created expecting a different template.
|
|
3417
|
+
// These don't always have the same length, for example if one attribute has multiple expressions.
|
|
3418
|
+
if (ng.paths.length === 0 && this.exprs.length || ng.paths.length > this.exprs.length)
|
|
3419
|
+
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
3420
|
|
|
2993
3421
|
// Creating the root nodegroup also renders it.
|
|
2994
3422
|
// If we didn't just create it, we need to render it.
|
|
@@ -2996,23 +3424,32 @@ class Template {
|
|
|
2996
3424
|
if (this.html?.length === 1 && !this.html[0])
|
|
2997
3425
|
el.innerHTML = ''; // Fast path for empty component.
|
|
2998
3426
|
else {
|
|
2999
|
-
ng.clearRenderWatched();
|
|
3000
3427
|
ng.applyExprs(this.exprs);
|
|
3001
3428
|
}
|
|
3002
3429
|
}
|
|
3003
3430
|
|
|
3431
|
+
ng.exprsToRender = new Map();
|
|
3004
3432
|
return el;
|
|
3005
3433
|
}
|
|
3006
3434
|
|
|
3007
3435
|
getExactKey() {
|
|
3008
|
-
if (!this.exactKey)
|
|
3009
|
-
|
|
3436
|
+
if (!this.exactKey) {
|
|
3437
|
+
if (this.exprs.length)
|
|
3438
|
+
this.exactKey = getObjectHash(this);// calls this.toJSON().
|
|
3439
|
+
else // Don't hash plain html.
|
|
3440
|
+
this.exactKey = this.html[0];
|
|
3441
|
+
}
|
|
3010
3442
|
return this.exactKey;
|
|
3011
3443
|
}
|
|
3012
3444
|
|
|
3013
3445
|
getCloseKey() {
|
|
3014
|
-
|
|
3015
|
-
|
|
3446
|
+
//console.log(this.exprs.length)
|
|
3447
|
+
if (!this.closeKey) {
|
|
3448
|
+
if (this.exprs.length)
|
|
3449
|
+
this.closeKey = /*'@' + */this.toJSON()[0];
|
|
3450
|
+
else
|
|
3451
|
+
this.closeKey = this.html[0];
|
|
3452
|
+
}
|
|
3016
3453
|
// Use the joined html when debugging? But it breaks some tests.
|
|
3017
3454
|
//return '@'+this.html.join('|')
|
|
3018
3455
|
|
|
@@ -3035,8 +3472,8 @@ class Template {
|
|
|
3035
3472
|
|
|
3036
3473
|
/**
|
|
3037
3474
|
* Convert strings to HTMLNodes.
|
|
3038
|
-
* Using
|
|
3039
|
-
* Using
|
|
3475
|
+
* Using h`...` as a tag will always create a Template.
|
|
3476
|
+
* Using h() as a function() will always create a DOM element.
|
|
3040
3477
|
*
|
|
3041
3478
|
* Features beyond what standard js tagged template strings do:
|
|
3042
3479
|
* 1. r`` sub-expressions
|
|
@@ -3046,24 +3483,25 @@ class Template {
|
|
|
3046
3483
|
* 5. TODO: list more
|
|
3047
3484
|
*
|
|
3048
3485
|
* Currently supported:
|
|
3049
|
-
* 1.
|
|
3050
|
-
* 2.
|
|
3486
|
+
* 1. h(el, options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
|
|
3487
|
+
* 2. h(el, template, ?options) // Render the Template created by #1 to element.
|
|
3051
3488
|
*
|
|
3052
|
-
* 3.
|
|
3489
|
+
* 3. h`<b>Hello</b> ${'World'}!` // Create Template that can later be used to create nodes.
|
|
3053
3490
|
*
|
|
3054
|
-
* 4.
|
|
3055
|
-
* 5.
|
|
3056
|
-
* 6.
|
|
3057
|
-
* 7.
|
|
3491
|
+
* 4. h('Hello'); // Create single text node.
|
|
3492
|
+
* 5. h('<b>Hello</b>'); // Create single HTMLElement
|
|
3493
|
+
* 6. h('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
|
|
3494
|
+
* 7. h()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which
|
|
3058
3495
|
* // 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.
|
|
3496
|
+
* 8. h(template) // Render Template created by #1.
|
|
3062
3497
|
*
|
|
3498
|
+
* 9. h({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
|
|
3499
|
+
* 10. h(string, object, ...) // JSX TODO
|
|
3063
3500
|
* @param htmlStrings {?HTMLElement|string|string[]|function():Template|{render:function()}}
|
|
3064
3501
|
* @param exprs {*[]|string|Template|Object}
|
|
3065
3502
|
* @return {Node|HTMLElement|Template} */
|
|
3066
|
-
function
|
|
3503
|
+
function h(htmlStrings=undefined, ...exprs) {
|
|
3504
|
+
|
|
3067
3505
|
|
|
3068
3506
|
// TODO: Make this a more flat if/else and call other functions for the logic.
|
|
3069
3507
|
if (htmlStrings instanceof Node) {
|
|
@@ -3078,7 +3516,7 @@ function r(htmlStrings=undefined, ...exprs) {
|
|
|
3078
3516
|
|
|
3079
3517
|
// Return a tagged template function that applies the tagged themplate to parent.
|
|
3080
3518
|
let taggedTemplate = (htmlStrings, ...exprs) => {
|
|
3081
|
-
Globals.rendered.add(parent);
|
|
3519
|
+
Globals$1.rendered.add(parent);
|
|
3082
3520
|
let template = new Template(htmlStrings, exprs);
|
|
3083
3521
|
return template.render(parent, options);
|
|
3084
3522
|
};
|
|
@@ -3115,6 +3553,22 @@ function r(htmlStrings=undefined, ...exprs) {
|
|
|
3115
3553
|
}
|
|
3116
3554
|
|
|
3117
3555
|
else if (typeof htmlStrings === 'string' || htmlStrings instanceof String) {
|
|
3556
|
+
// 10. JSX
|
|
3557
|
+
if (typeof exprs[0] === 'object') {
|
|
3558
|
+
exprs[0] || {};
|
|
3559
|
+
exprs.slice(1);
|
|
3560
|
+
|
|
3561
|
+
let templateHtmlStrings = [];
|
|
3562
|
+
let templateExprs = [];
|
|
3563
|
+
|
|
3564
|
+
// TODO How to know which children are static html and which are expression placeholders?
|
|
3565
|
+
// Perhaps we have to treat every text child as a string?
|
|
3566
|
+
|
|
3567
|
+
assert(templateHtmlStrings.length === templateExprs.length+1);
|
|
3568
|
+
return new Template(templateHtmlStrings, templateExprs);
|
|
3569
|
+
}
|
|
3570
|
+
|
|
3571
|
+
|
|
3118
3572
|
// If it starts with a string, trim both ends.
|
|
3119
3573
|
// TODO: Also trim if it ends with whitespace?
|
|
3120
3574
|
if (htmlStrings.match(/^\s^</))
|
|
@@ -3138,7 +3592,7 @@ function r(htmlStrings=undefined, ...exprs) {
|
|
|
3138
3592
|
else if (htmlStrings === undefined) {
|
|
3139
3593
|
return (htmlStrings, ...exprs) => {
|
|
3140
3594
|
//Globals.rendered.add(parent)
|
|
3141
|
-
let template =
|
|
3595
|
+
let template = h(htmlStrings, ...exprs);
|
|
3142
3596
|
return template.render();
|
|
3143
3597
|
}
|
|
3144
3598
|
}
|
|
@@ -3150,446 +3604,168 @@ function r(htmlStrings=undefined, ...exprs) {
|
|
|
3150
3604
|
|
|
3151
3605
|
|
|
3152
3606
|
// 9. Create dynamic element with render() function.
|
|
3607
|
+
// TODO: This path doesn't handle embeds like data-id="..."
|
|
3153
3608
|
else if (typeof htmlStrings === 'object') {
|
|
3154
3609
|
let obj = htmlStrings;
|
|
3155
3610
|
|
|
3611
|
+
if (obj.constructor.name !== 'Object')
|
|
3612
|
+
throw new Error(`Solarate Web Component class ${obj.constructor?.name} must extend HTMLElement.`);
|
|
3613
|
+
|
|
3614
|
+
|
|
3156
3615
|
// Special rebound render path, called by normal path.
|
|
3157
|
-
|
|
3616
|
+
// Intercepts the main r`...` function call inside render().
|
|
3617
|
+
if (Globals$1.objToEl.has(obj)) {
|
|
3158
3618
|
return function(...args) {
|
|
3159
|
-
let template =
|
|
3619
|
+
let template = h(...args);
|
|
3160
3620
|
let el = template.render();
|
|
3161
|
-
Globals.objToEl.set(obj, el);
|
|
3621
|
+
Globals$1.objToEl.set(obj, el);
|
|
3162
3622
|
}.bind(obj);
|
|
3163
3623
|
}
|
|
3164
3624
|
|
|
3165
3625
|
// Normal path
|
|
3166
3626
|
else {
|
|
3167
|
-
Globals.objToEl.set(obj, null);
|
|
3168
|
-
obj
|
|
3169
|
-
let el = Globals.objToEl.get(obj);
|
|
3170
|
-
Globals.objToEl.delete(obj);
|
|
3627
|
+
Globals$1.objToEl.set(obj, null);
|
|
3628
|
+
obj[renderF](); // Calls the Special rebound render path above, when the render function calls r(this)
|
|
3629
|
+
let el = Globals$1.objToEl.get(obj);
|
|
3630
|
+
Globals$1.objToEl.delete(obj);
|
|
3171
3631
|
|
|
3172
3632
|
for (let name in obj)
|
|
3173
3633
|
if (typeof obj[name] === 'function')
|
|
3174
|
-
el[name] = obj[name].bind(el);
|
|
3634
|
+
el[name] = obj[name].bind(el); // Make the "this" of functions be el.
|
|
3635
|
+
// TODO: But this doesn't work for passing an object with functions as a constructor arg via an attribute:
|
|
3636
|
+
// <my-element arg=${{myFunc() { return this }}}
|
|
3175
3637
|
else
|
|
3176
3638
|
el[name] = obj[name];
|
|
3177
3639
|
|
|
3640
|
+
// Bind id's
|
|
3641
|
+
// This doesn't work for id's referenced by attributes.
|
|
3642
|
+
// for (let idEl of el.querySelectorAll('[id],[data-id]')) {
|
|
3643
|
+
// Util.bindId(el, idEl);
|
|
3644
|
+
// Util.bindId(obj, idEl);
|
|
3645
|
+
// }
|
|
3646
|
+
// TODO: Bind styles
|
|
3647
|
+
|
|
3178
3648
|
return el;
|
|
3179
3649
|
}
|
|
3180
3650
|
}
|
|
3181
3651
|
|
|
3182
3652
|
else
|
|
3183
3653
|
throw new Error('Unsupported arguments.')
|
|
3184
|
-
}
|
|
3185
|
-
|
|
3186
|
-
//import {watchGet, watchSet} from "./watch.js";
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
function defineClass(Class, tagName, extendsTag) {
|
|
3191
|
-
if (!customElements.getName(Class)) { // If not previously defined.
|
|
3192
|
-
tagName = tagName || camelToDashes(Class.name);
|
|
3193
|
-
if (!tagName.includes('-'))
|
|
3194
|
-
tagName += '-element';
|
|
3195
|
-
|
|
3196
|
-
let options = null;
|
|
3197
|
-
if (extendsTag)
|
|
3198
|
-
options = {extends: extendsTag};
|
|
3199
|
-
|
|
3200
|
-
customElements.define(tagName, Class, options);
|
|
3201
|
-
}
|
|
3202
3654
|
}
|
|
3203
3655
|
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3656
|
+
// Trick to prevent minifier from renaming this function.
|
|
3657
|
+
let renderF = 'render';
|
|
3658
|
+
|
|
3208
3659
|
/**
|
|
3209
|
-
*
|
|
3210
|
-
*
|
|
3211
|
-
*
|
|
3212
|
-
*
|
|
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.
|
|
3215
|
-
* 5. We have the onConnect, onFirstConnect, and onDisconnect methods.
|
|
3216
|
-
* Can't figure out how to have these work standalone though, and still be synchronous.
|
|
3217
|
-
* 6. Can we extend from other element types like TR?
|
|
3218
|
-
* 7. Shows default text if render() function isn't defined.
|
|
3660
|
+
* There are three ways to create an instance of a Solarite Component:
|
|
3661
|
+
* 1. new ComponentName(); // direct class instantiation
|
|
3662
|
+
* 2. this.html = r`<div><component-name></component-name></div>; // as a child of another RedComponent.
|
|
3663
|
+
* 3. <body><component-name></component-name></body> // in the Document html.
|
|
3219
3664
|
*
|
|
3220
|
-
*
|
|
3221
|
-
*
|
|
3222
|
-
*
|
|
3223
|
-
*
|
|
3665
|
+
* When created via #3, Solarite has no way to pass attributes as arguments to the constructor. So to make
|
|
3666
|
+
* sure we get the correct value via all three paths, we write our constructors according to the following
|
|
3667
|
+
* example. Note that constructor args are embedded in an object, and must be all lower-case because
|
|
3668
|
+
* Browsers make all html attribute names lowercase.
|
|
3224
3669
|
*
|
|
3225
|
-
* @
|
|
3226
|
-
*
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
// This line is equivalent the to super() call.
|
|
3251
|
-
return Reflect.construct(Parent, args, Class);
|
|
3252
|
-
}
|
|
3253
|
-
});
|
|
3254
|
-
|
|
3255
|
-
return class Solarite extends HTMLElementAutoDefine {
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
/**
|
|
3259
|
-
* TODO: Make these standalone functions.
|
|
3260
|
-
* Callbacks.
|
|
3261
|
-
* Use onConnect.push(() => ...); to add new callbacks. */
|
|
3262
|
-
onConnect = Util$1.callback();
|
|
3670
|
+
* @example
|
|
3671
|
+
* constructor({name, userid=1}={}) {
|
|
3672
|
+
* super();
|
|
3673
|
+
*
|
|
3674
|
+
* // Get value from "name" attriute if persent, otherwise from name constructor arg.
|
|
3675
|
+
* this.name = getArg(this, 'name', name);
|
|
3676
|
+
*
|
|
3677
|
+
* // Optionally convert the value to an integer.
|
|
3678
|
+
* this.userId = getArg(this, 'userid', userid, ArgType.Int);
|
|
3679
|
+
* }
|
|
3680
|
+
*
|
|
3681
|
+
* @param el {HTMLElement}
|
|
3682
|
+
* @param attributeName {string} Attribute name. Not case-sensitive.
|
|
3683
|
+
* @param defaultValue {*} Default value to use if attribute doesn't exist.
|
|
3684
|
+
* @param type {ArgType|function|*[]}
|
|
3685
|
+
* If an array, use the value if it's in the array, otherwise return undefined.
|
|
3686
|
+
* If it's a function, pass the value to the function and return the result.
|
|
3687
|
+
* @param fallback {*} If the defaultValue is undefiend and type can't be parsed as the given type, use this value.
|
|
3688
|
+
* TODO: Should this be merged with the defaultValue argument?
|
|
3689
|
+
* @return {*} Undefined if attribute isn't set. */
|
|
3690
|
+
function getArg(el, attributeName, defaultValue=undefined, type=ArgType.String, fallback=undefined) {
|
|
3691
|
+
let val = defaultValue;
|
|
3692
|
+
let attrVal = el.getAttribute(attributeName) || el.getAttribute(Util.camelToDashes(attributeName));
|
|
3693
|
+
if (attrVal !== null) // If attribute doesn't exist.
|
|
3694
|
+
val = attrVal;
|
|
3263
3695
|
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3696
|
+
if (Array.isArray(type))
|
|
3697
|
+
return type.includes(val) ? val : fallback;
|
|
3698
|
+
|
|
3699
|
+
if (typeof type === 'function')
|
|
3700
|
+
return type(val);
|
|
3701
|
+
|
|
3702
|
+
// If bool, it's true as long as it exists and its value isn't falsey.
|
|
3703
|
+
if (type===ArgType.Bool) {
|
|
3704
|
+
let lAttrVal = typeof val === 'string' ? val.toLowerCase() : val;
|
|
3705
|
+
if (['false', '0', false, 0, null, undefined, NaN].includes(lAttrVal))
|
|
3706
|
+
return false;
|
|
3707
|
+
if (['true', true].includes(lAttrVal) || parseFloat(lAttrVal) !== 0)
|
|
3708
|
+
return true;
|
|
3709
|
+
return fallback;
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3712
|
+
// Attribute doesn't exist
|
|
3713
|
+
let result;
|
|
3714
|
+
switch (type) {
|
|
3715
|
+
case ArgType.Int:
|
|
3716
|
+
result = parseInt(val);
|
|
3717
|
+
return isNaN(result) ? fallback : result;
|
|
3718
|
+
case ArgType.Float:
|
|
3719
|
+
result = parseFloat(val);
|
|
3720
|
+
return isNaN(result) ? fallback : result;
|
|
3721
|
+
case ArgType.String:
|
|
3722
|
+
return [undefined, null, false].includes(val) ? '' : val+'';
|
|
3723
|
+
case ArgType.Json:
|
|
3724
|
+
case ArgType.Eval:
|
|
3725
|
+
if (typeof val === 'string' && val.length)
|
|
3726
|
+
try {
|
|
3727
|
+
if (type === ArgType.Json)
|
|
3728
|
+
return JSON.parse(val);
|
|
3294
3729
|
else
|
|
3295
|
-
|
|
3296
|
-
}
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
/*
|
|
3300
|
-
let pthis = new Proxy(this, {
|
|
3301
|
-
get(obj, prop) {
|
|
3302
|
-
return Reflect.get(obj, prop)
|
|
3303
|
-
}
|
|
3304
|
-
});
|
|
3305
|
-
this.render = this.render.bind(pthis);
|
|
3306
|
-
*/
|
|
3307
|
-
}
|
|
3308
|
-
|
|
3309
|
-
/**
|
|
3310
|
-
* Call render() only if it hasn't already been called. */
|
|
3311
|
-
renderFirstTime() {
|
|
3312
|
-
if (!Globals.rendered.has(this) && this.render)
|
|
3313
|
-
this.render();
|
|
3314
|
-
}
|
|
3315
|
-
|
|
3316
|
-
/**
|
|
3317
|
-
* Called automatically by the browser. */
|
|
3318
|
-
connectedCallback() {
|
|
3319
|
-
this.renderFirstTime();
|
|
3320
|
-
if (!Globals.connected.has(this)) {
|
|
3321
|
-
Globals.connected.add(this);
|
|
3322
|
-
this.onFirstConnect();
|
|
3323
|
-
}
|
|
3324
|
-
this.onConnect();
|
|
3325
|
-
}
|
|
3326
|
-
|
|
3327
|
-
disconnectedCallback() {
|
|
3328
|
-
this.onDisconnect();
|
|
3329
|
-
}
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
static define(tagName=null) {
|
|
3333
|
-
defineClass(this, tagName, extendsTag);
|
|
3334
|
-
}
|
|
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
|
-
}
|
|
3730
|
+
return eval(`(${val})`);
|
|
3731
|
+
} catch (e) {
|
|
3732
|
+
return val;
|
|
3422
3733
|
}
|
|
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
|
-
}
|
|
3734
|
+
else return val;
|
|
3432
3735
|
|
|
3433
|
-
|
|
3434
|
-
|
|
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
|
|
3736
|
+
// type not provided
|
|
3737
|
+
default:
|
|
3738
|
+
return val;
|
|
3445
3739
|
}
|
|
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
3740
|
}
|
|
3555
3741
|
|
|
3556
3742
|
/**
|
|
3557
|
-
*
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
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);
|
|
3743
|
+
* @enum */
|
|
3744
|
+
var ArgType = {
|
|
3745
|
+
|
|
3746
|
+
/**
|
|
3747
|
+
* false, 0, null, undefined, '0', and 'false' (case-insensitive) become false.
|
|
3748
|
+
* Anything else, including empty string becomes true.
|
|
3749
|
+
* Empty string is true because attributes with no value should be evaulated as true. */
|
|
3750
|
+
Bool: 'Bool',
|
|
3751
|
+
|
|
3752
|
+
Int: 'Int',
|
|
3753
|
+
Float: 'Float',
|
|
3754
|
+
String: 'String',
|
|
3583
3755
|
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
}
|
|
3587
|
-
}
|
|
3756
|
+
/** @deprecated for Json */
|
|
3757
|
+
JSON: 'Json',
|
|
3588
3758
|
|
|
3589
|
-
|
|
3759
|
+
/**
|
|
3760
|
+
* Parse the string value as JSON.
|
|
3761
|
+
* If it's not parsable, return the value as a string. */
|
|
3762
|
+
Json: 'Json',
|
|
3590
3763
|
|
|
3591
|
-
|
|
3592
|
-
|
|
3764
|
+
/**
|
|
3765
|
+
* Evaluate the string as JavaScript using the eval() function.
|
|
3766
|
+
* If it can't be evaluated, return the original string. */
|
|
3767
|
+
Eval: 'Eval'
|
|
3768
|
+
};
|
|
3593
3769
|
|
|
3594
3770
|
/**
|
|
3595
3771
|
* Solarite JavasCript UI library.
|
|
@@ -3606,6 +3782,9 @@ let Solarite = new Proxy(createSolarite(), {
|
|
|
3606
3782
|
}
|
|
3607
3783
|
});
|
|
3608
3784
|
let getInputValue = Util.getInputValue;
|
|
3609
|
-
|
|
3785
|
+
|
|
3786
|
+
//Experimental:
|
|
3787
|
+
//export {default as watch, renderWatched} from './watch.js'; // unfinished
|
|
3610
3788
|
|
|
3611
|
-
export
|
|
3789
|
+
export default h;
|
|
3790
|
+
export { ArgType, Globals$1 as Globals, Solarite, Util as SolariteUtil, Template, delve, getArg, getInputValue, h, h as r };
|