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.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,256 +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
|
-
|
|
346
|
-
|
|
347
|
-
var Globals = {
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Used by NodeGroup.applyComponentExprs() */
|
|
351
|
-
componentHash: new WeakMap(),
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Store which instances of Solarite have already been added to the DOM.
|
|
355
|
-
* @type {WeakSet<HTMLElement>} */
|
|
356
|
-
connected: new WeakSet(),
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Elements that have been rendered to by r() at least once.
|
|
360
|
-
* This is used by the Solarite class to know when to call onFirstConnect()
|
|
361
|
-
* @type {WeakSet<HTMLElement>} */
|
|
362
|
-
rendered: new WeakSet(),
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Used by watch3 to see which expressions are being accessed.
|
|
366
|
-
* @type {[]}*/
|
|
367
|
-
currentExprPath: null,
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* @type {Object<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
|
|
371
|
-
elementClasses: {},
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Used by ExprPath.applyEventAttrib()
|
|
375
|
-
* @type {WeakMap<Node, Object<eventName:string, [original:function, bound:function, args:*[]]>>} */
|
|
376
|
-
nodeEvents: new WeakMap(),
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Get the RootNodeGroup for an element.
|
|
380
|
-
* @type {WeakMap<HTMLElement, RootNodeGroup>} */
|
|
381
|
-
nodeGroups: new WeakMap(),
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* Used by r() path 9. */
|
|
385
|
-
objToEl: new WeakMap(),
|
|
386
|
-
|
|
387
|
-
pendingChildren: [],
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Elements that are currently rendering via the r() function.
|
|
391
|
-
* @type {WeakSet<HTMLElement>} */
|
|
392
|
-
rendering: new WeakSet(),
|
|
235
|
+
},
|
|
393
236
|
|
|
394
237
|
/**
|
|
395
|
-
*
|
|
396
|
-
* @
|
|
397
|
-
shells: new WeakMap()
|
|
398
|
-
};
|
|
399
|
-
|
|
400
|
-
let Util = {
|
|
401
|
-
|
|
238
|
+
* @param style {HTMLStyleElement}
|
|
239
|
+
* @param root {HTMLElement} */
|
|
402
240
|
bindStyles(style, root) {
|
|
403
241
|
let styleId = root.getAttribute('data-style');
|
|
404
242
|
if (!styleId) {
|
|
@@ -411,17 +249,60 @@ let Util = {
|
|
|
411
249
|
root.setAttribute('data-style', styleId);
|
|
412
250
|
}
|
|
413
251
|
|
|
252
|
+
// Replace ":host" with "tagName[data-style=...]" in the css.
|
|
414
253
|
let tagName = root.tagName.toLowerCase();
|
|
415
254
|
for (let child of style.childNodes) {
|
|
416
255
|
if (child.nodeType === 3) {
|
|
417
256
|
let oldText = child.textContent;
|
|
418
|
-
let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, tagName
|
|
257
|
+
let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, `${tagName}[data-style="${styleId}"]`);
|
|
419
258
|
if (oldText !== newText)
|
|
420
259
|
child.textContent = newText;
|
|
421
260
|
}
|
|
422
261
|
}
|
|
423
262
|
},
|
|
424
263
|
|
|
264
|
+
|
|
265
|
+
/**
|
|
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}
|
|
271
|
+
*
|
|
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
|
+
|
|
425
306
|
/**
|
|
426
307
|
* A generator function that recursively traverses and flattens a value.
|
|
427
308
|
*
|
|
@@ -465,6 +346,7 @@ let Util = {
|
|
|
465
346
|
* @param node {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|HTMLDivElement}
|
|
466
347
|
* @return {string|string[]|number|[]|File[]|Date|boolean} */
|
|
467
348
|
getInputValue(node) {
|
|
349
|
+
// .type is a built-in DOM property
|
|
468
350
|
if (node.type === 'checkbox' || node.type === 'radio')
|
|
469
351
|
return node.checked; // Boolean
|
|
470
352
|
if (node.type === 'file')
|
|
@@ -480,47 +362,62 @@ let Util = {
|
|
|
480
362
|
},
|
|
481
363
|
|
|
482
364
|
/**
|
|
483
|
-
*
|
|
484
|
-
* @param
|
|
365
|
+
* @param el {HTMLElement}
|
|
366
|
+
* @param prop {string}
|
|
485
367
|
* @returns {boolean} */
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
saveOrphans(oldNodeGroups, oldNodes) {
|
|
499
|
-
let oldNgMap = new Map();
|
|
500
|
-
for (let ng of oldNodeGroups) {
|
|
501
|
-
oldNgMap.set(ng.startNode, ng);
|
|
502
|
-
|
|
503
|
-
// TODO: Is this necessary?
|
|
504
|
-
// if (ng.parentPath)
|
|
505
|
-
// ng.parentPath.clearNodesCache();
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
for (let i=0, node; node = oldNodes[i]; i++) {
|
|
509
|
-
let ng;
|
|
510
|
-
if (!node.parentNode && (ng = oldNgMap.get(node))) {
|
|
511
|
-
//ng.nodesCache = [];
|
|
512
|
-
let fragment = document.createDocumentFragment();
|
|
513
|
-
let endNode = ng.endNode;
|
|
514
|
-
while (node !== endNode) {
|
|
515
|
-
fragment.append(node);
|
|
516
|
-
//ng.nodesCache.push(node);
|
|
517
|
-
i++;
|
|
518
|
-
node = oldNodes[i];
|
|
519
|
-
}
|
|
520
|
-
fragment.append(endNode);
|
|
521
|
-
//ng.nodesCache.push(endNode);
|
|
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);
|
|
522
380
|
}
|
|
381
|
+
Globals$1.htmlProps[key] = result = (proto
|
|
382
|
+
? !!Object.getOwnPropertyDescriptor(proto, prop)?.set
|
|
383
|
+
: false);
|
|
523
384
|
}
|
|
385
|
+
return result;
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
/**
|
|
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.
|
|
391
|
+
* @param arr {Array|*}
|
|
392
|
+
* @returns {boolean} */
|
|
393
|
+
isPath(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'
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
/**
|
|
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;
|
|
524
421
|
},
|
|
525
422
|
|
|
526
423
|
/**
|
|
@@ -551,44 +448,14 @@ let Util = {
|
|
|
551
448
|
|
|
552
449
|
|
|
553
450
|
|
|
554
|
-
let
|
|
555
|
-
|
|
556
|
-
let isEvent = attrName => attrName.startsWith('on') && attrName in div;
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
/**
|
|
560
|
-
* Convert a Proper Case name to a name with dashes.
|
|
561
|
-
* Dashes will be placed between letters and numbers.
|
|
562
|
-
* If there are multiple consecutive capital letters followed by another chracater, a dash will be placed before the last capital letter.
|
|
563
|
-
* @param str {string}
|
|
564
|
-
* @return {string}
|
|
565
|
-
*
|
|
566
|
-
* @example
|
|
567
|
-
* 'ProperName' => 'proper-name'
|
|
568
|
-
* 'HTMLElement' => 'html-element'
|
|
569
|
-
* 'BigUI' => 'big-ui'
|
|
570
|
-
* 'UIForm' => 'ui-form'
|
|
571
|
-
* 'A100' => 'a-100' */
|
|
572
|
-
function camelToDashes(str) {
|
|
573
|
-
// Convert any capital letter that is preceded by a lowercase letter or number to lowercase and precede with a dash.
|
|
574
|
-
str = str.replace(/([a-z0-9])([A-Z])/g, '$1-$2');
|
|
575
|
-
|
|
576
|
-
// Convert any capital letter that is followed by a lowercase letter or number to lowercase and precede with a dash.
|
|
577
|
-
str = str.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2');
|
|
578
|
-
|
|
579
|
-
// Convert any number that is preceded by a lowercase or uppercase letter to be preceded by a dash.
|
|
580
|
-
str = str.replace(/([a-zA-Z])([0-9])/g, '$1-$2');
|
|
581
|
-
|
|
582
|
-
// Convert all the remaining capital letters to lowercase.
|
|
583
|
-
return str.toLowerCase();
|
|
584
|
-
}
|
|
451
|
+
let isEvent = attrName => attrName.startsWith('on') && attrName in Globals$1.div;
|
|
585
452
|
|
|
586
453
|
|
|
587
454
|
|
|
588
455
|
|
|
589
456
|
|
|
590
457
|
/**
|
|
591
|
-
* Returns
|
|
458
|
+
* Returns true if they're the same.
|
|
592
459
|
* @param a
|
|
593
460
|
* @param b
|
|
594
461
|
* @returns {boolean} */
|
|
@@ -603,84 +470,250 @@ function arraySame(a, b) {
|
|
|
603
470
|
}
|
|
604
471
|
|
|
605
472
|
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
// For debugging only
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
function defineClass(Class, tagName, extendsTag) {
|
|
480
|
+
if (!customElements[getName](Class)) { // If not previously defined.
|
|
481
|
+
tagName = tagName || Util.camelToDashes(Class.name);
|
|
482
|
+
if (!tagName.includes('-'))
|
|
483
|
+
tagName += '-element';
|
|
484
|
+
|
|
485
|
+
let options = null;
|
|
486
|
+
if (extendsTag)
|
|
487
|
+
options = {extends: extendsTag};
|
|
488
|
+
|
|
489
|
+
customElements[define](tagName, Class, options);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
606
493
|
/**
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
494
|
+
* Create a version of the Solarite class that extends from the given tag name.
|
|
495
|
+
* Reasons to inherit from this instead of HTMLElement. None of these are all that useful.
|
|
496
|
+
* 1. customElements.define() is called automatically when you create the first instance.
|
|
497
|
+
* 2. Calls render() when added to the DOM, if it hasn't been called already.
|
|
498
|
+
* 3. Child elements are added before constructor is called. But they're also passed to the constructor. (deprecated?)
|
|
499
|
+
* 4. We can use this.html = r`...` to set html. (deprecated)
|
|
500
|
+
* 5. We have the onConnect, onFirstConnect, and onDisconnect methods.
|
|
501
|
+
* Can't figure out how to have these work standalone though, and still be synchronous.
|
|
502
|
+
* 6. Can we extend from other element types like TR?
|
|
503
|
+
* 7. Shows default text if render() function isn't defined.
|
|
504
|
+
*
|
|
505
|
+
* Advantages to inheriting from HTMLElement
|
|
506
|
+
* 1. Minimization won't break when it renames the Class and we call customElements.define() on the wrong name.
|
|
507
|
+
* 2. We can inherit from things like HTMLTableRowElement directly.
|
|
508
|
+
* 3. There's less magic, since everyone is familiar with defining custom elements.
|
|
509
|
+
*
|
|
510
|
+
* @param extendsTag {?string}
|
|
511
|
+
* @return {Class} */
|
|
512
|
+
function createSolarite(extendsTag=null) {
|
|
513
|
+
|
|
514
|
+
let BaseClass = HTMLElement;
|
|
515
|
+
if (extendsTag && !extendsTag.includes('-')) {
|
|
516
|
+
extendsTag = extendsTag.toLowerCase();
|
|
517
|
+
|
|
518
|
+
BaseClass = Globals$1.elementClasses[extendsTag];
|
|
519
|
+
if (!BaseClass) { // TODO: Use Cache
|
|
520
|
+
BaseClass = document.createElement(extendsTag).constructor;
|
|
521
|
+
Globals$1.elementClasses[extendsTag] = BaseClass;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Intercept the construct call to auto-define the class before the constructor is called.
|
|
527
|
+
* @type {HTMLElement} */
|
|
528
|
+
let HTMLElementAutoDefine = new Proxy(BaseClass, {
|
|
529
|
+
construct(Parent, args, Class) {
|
|
530
|
+
defineClass(Class, null, extendsTag);
|
|
531
|
+
|
|
532
|
+
// This is a good place to manipulate any args before they're sent to the constructor.
|
|
533
|
+
// Such as loading them from attributes, if I could find a way to do so.
|
|
534
|
+
|
|
535
|
+
// This line is equivalent the to super() call.
|
|
536
|
+
return Reflect.construct(Parent, args, Class);
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
return class Solarite extends HTMLElementAutoDefine {
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* TODO: Make these standalone functions.
|
|
545
|
+
* Callbacks.
|
|
546
|
+
* Use onConnect.push(() => ...); to add new callbacks. */
|
|
547
|
+
onConnect = Util$1.callback();
|
|
548
|
+
|
|
549
|
+
onFirstConnect = Util$1.callback();
|
|
550
|
+
onDisconnect = Util$1.callback();
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* @param options {RenderOptions} */
|
|
554
|
+
constructor(options={}) {
|
|
555
|
+
super();
|
|
556
|
+
|
|
557
|
+
// TODO: Is options.render ever used?
|
|
558
|
+
if (options.render===true)
|
|
559
|
+
this.render();
|
|
560
|
+
|
|
561
|
+
else if (options.render===false)
|
|
562
|
+
Globals$1.rendered.add(this); // Don't render on connectedCallback()
|
|
563
|
+
|
|
564
|
+
// Add slot children before constructor code executes.
|
|
565
|
+
// This breaks the styleStaticNested test.
|
|
566
|
+
// PendingChildren is setup in NodeGroup.createNewComponent()
|
|
567
|
+
// TODO: Match named slots.
|
|
568
|
+
//let ch = Globals.pendingChildren.pop();
|
|
569
|
+
//if (ch) // TODO: how could there be a slot before render is called?
|
|
570
|
+
// (this.querySelector('slot') || this).append(...ch);
|
|
571
|
+
|
|
572
|
+
/** @deprecated
|
|
573
|
+
Object.defineProperty(this, 'html', {
|
|
574
|
+
set(html) {
|
|
575
|
+
Globals.rendered.add(this);
|
|
576
|
+
if (typeof html === 'string') {
|
|
577
|
+
console.warn("Assigning to this.html without the r template prefix.")
|
|
578
|
+
this.innerHTML = html;
|
|
579
|
+
}
|
|
580
|
+
else
|
|
581
|
+
this.modifications = r(this, html, options);
|
|
644
582
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
state.context = htmlContext.Tag;
|
|
652
|
-
state.quote = null;
|
|
653
|
-
state.buffer = '';
|
|
654
|
-
} else if (!state.quote && char === '>') {
|
|
655
|
-
state.context = htmlContext.Text;
|
|
656
|
-
state.quote = null;
|
|
657
|
-
state.buffer = '';
|
|
658
|
-
} else if (char !== ' ') {
|
|
659
|
-
state.buffer += char;
|
|
583
|
+
})*/
|
|
584
|
+
|
|
585
|
+
/*
|
|
586
|
+
let pthis = new Proxy(this, {
|
|
587
|
+
get(obj, prop) {
|
|
588
|
+
return Reflect.get(obj, prop)
|
|
660
589
|
}
|
|
661
|
-
|
|
590
|
+
});
|
|
591
|
+
this.render = this.render.bind(pthis);
|
|
592
|
+
*/
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Call render() only if it hasn't already been called. */
|
|
597
|
+
renderFirstTime() {
|
|
598
|
+
if (!Globals$1.rendered.has(this) && this.render)
|
|
599
|
+
this.render();
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Called automatically by the browser. */
|
|
604
|
+
connectedCallback() {
|
|
605
|
+
this.renderFirstTime();
|
|
606
|
+
if (!Globals$1.connected.has(this)) {
|
|
607
|
+
Globals$1.connected.add(this);
|
|
608
|
+
this.onFirstConnect();
|
|
609
|
+
}
|
|
610
|
+
this.onConnect();
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
disconnectedCallback() {
|
|
614
|
+
this.onDisconnect();
|
|
662
615
|
}
|
|
663
616
|
|
|
617
|
+
|
|
618
|
+
static define(tagName=null) {
|
|
619
|
+
defineClass(this, tagName, extendsTag);
|
|
620
|
+
}
|
|
664
621
|
}
|
|
665
|
-
return state.context;
|
|
666
622
|
}
|
|
667
623
|
|
|
624
|
+
// Trick to prevent minifier from renaming this method.
|
|
625
|
+
let define = 'define';
|
|
626
|
+
let getName = 'getName';
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
let lastObjectId = 1>>>0; // Is a 32-bit int faster to increment than JavaScript's Number, which is a 64-bit float?
|
|
631
|
+
let objectIds = new WeakMap();
|
|
668
632
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
633
|
+
/**
|
|
634
|
+
* @param obj {Object|string|Node}
|
|
635
|
+
* @returns {string} */
|
|
636
|
+
function getObjectId(obj) {
|
|
637
|
+
// if (typeof obj === 'function')
|
|
638
|
+
// return obj.toString(); // This fails to detect when a function's bound variables changes.
|
|
639
|
+
|
|
640
|
+
let result = objectIds.get(obj);
|
|
641
|
+
if (!result) { // convert to string, store in result, then add 1 to lastObjectId.
|
|
642
|
+
result = '~\f' + (lastObjectId++); // We use a unique, 2-byte prefix to ensure it doesn't collide w/ strings not from getObjectId()
|
|
643
|
+
objectIds.set(obj, result);
|
|
644
|
+
}
|
|
645
|
+
return result;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Control how JSON.stringify() handles Nodes and Functions.
|
|
650
|
+
* Normally, we'd pass a replacer() function argument to JSON.stringify() to handle Nodes and Functions.
|
|
651
|
+
* But that makes JSON.stringify() take twice as long to run.
|
|
652
|
+
* Adding a toJSON method globally on these object prototypes doesn't incur that performance penalty. */
|
|
653
|
+
let isHashing = true;
|
|
654
|
+
function toJSON() {
|
|
655
|
+
return isHashing ? getObjectId(this) : this
|
|
656
|
+
}
|
|
680
657
|
|
|
681
658
|
|
|
682
|
-
//
|
|
683
|
-
|
|
659
|
+
// Node.prototype.toJSON = toJSON;
|
|
660
|
+
// Function.prototype.toJSON = toJSON;
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Get a string that uniquely maps to the values of the given object.
|
|
665
|
+
* If a value in obj changes, calling getObjectHash(obj) will then return a different hash.
|
|
666
|
+
* This is used by NodeGroupManager to create a hash that represents the current values of a NodeGroup.
|
|
667
|
+
*
|
|
668
|
+
* Relies on the Node and Function prototypes being overridden above.
|
|
669
|
+
*
|
|
670
|
+
* Note that passing an integer may collide with the number we get from hashing an object.
|
|
671
|
+
* But we don't handle that case because we need max performance and Solarite never passes integers to this function.
|
|
672
|
+
*
|
|
673
|
+
* @param obj {*}
|
|
674
|
+
* @returns {string} */
|
|
675
|
+
function getObjectHash(obj) {
|
|
676
|
+
|
|
677
|
+
// Sometimes these get unassigned by Chrome and Brave 119, as well as Firefox, seemingly randomly!
|
|
678
|
+
// The same tests sometimes pass, sometimes fail, even after browser and OS restarts.
|
|
679
|
+
// So we check the assignments on every run of getObjectHash()
|
|
680
|
+
if (Node.prototype.toJSON !== toJSON) {
|
|
681
|
+
Node.prototype.toJSON = toJSON;
|
|
682
|
+
if (Function.prototype.toJSON !== toJSON) // Will it only unmap one but not the other?
|
|
683
|
+
Function.prototype.toJSON = toJSON;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
let result;
|
|
687
|
+
isHashing = true;
|
|
688
|
+
try {
|
|
689
|
+
result = JSON.stringify(obj);
|
|
690
|
+
}
|
|
691
|
+
catch(e) {
|
|
692
|
+
result = getObjectHashCircular(obj);
|
|
693
|
+
}
|
|
694
|
+
isHashing = false;
|
|
695
|
+
return result;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Slower hashing method that supports.
|
|
700
|
+
* @param obj
|
|
701
|
+
* @returns {string} */
|
|
702
|
+
function getObjectHashCircular(obj) {
|
|
703
|
+
|
|
704
|
+
//console.log('circular')
|
|
705
|
+
// Slower version that handles circular references.
|
|
706
|
+
// Just adding any callback at all, even one that just returns the value, makes JSON.stringify() twice as slow.
|
|
707
|
+
const seen = new Set();
|
|
708
|
+
return JSON.stringify(obj, (key, value) => {
|
|
709
|
+
if (typeof value === 'object' && value !== null) {
|
|
710
|
+
if (seen.has(value))
|
|
711
|
+
return getObjectId(value);
|
|
712
|
+
seen.add(value);
|
|
713
|
+
}
|
|
714
|
+
return value;
|
|
715
|
+
});
|
|
716
|
+
}
|
|
684
717
|
|
|
685
718
|
class MultiValueMap {
|
|
686
719
|
|
|
@@ -718,23 +751,14 @@ class MultiValueMap {
|
|
|
718
751
|
* @param val If specified, make sure we delete this specific value, if a key exists more than once.
|
|
719
752
|
* @returns {*|undefined} The deleted item. */
|
|
720
753
|
delete(key, val=undefined) {
|
|
721
|
-
// if (key === '["Html2",[[["Html3",["F1","A"]],["Html3",["F1","B"]]]]]')
|
|
722
|
-
// debugger;
|
|
723
|
-
|
|
724
754
|
let data = this.data;
|
|
725
|
-
|
|
726
|
-
// if (!data.hasOwnProperty(key))
|
|
727
|
-
// return undefined;
|
|
728
|
-
|
|
729
|
-
// Delete a specific value.
|
|
730
755
|
let result;
|
|
731
756
|
let set = data[key];
|
|
732
|
-
if (!set)
|
|
757
|
+
if (!set)
|
|
733
758
|
return undefined;
|
|
734
759
|
|
|
735
760
|
// Delete any value.
|
|
736
761
|
if (val === undefined) {
|
|
737
|
-
//result = set.values().next().value; // get first item from set.
|
|
738
762
|
[result] = set; // Does the same as above and seems to be about the same speed.
|
|
739
763
|
set.delete(result);
|
|
740
764
|
}
|
|
@@ -745,7 +769,6 @@ class MultiValueMap {
|
|
|
745
769
|
result = val;
|
|
746
770
|
}
|
|
747
771
|
|
|
748
|
-
// TODO: Will this make it slower?
|
|
749
772
|
if (set.size === 0)
|
|
750
773
|
delete data[key];
|
|
751
774
|
|
|
@@ -753,27 +776,64 @@ class MultiValueMap {
|
|
|
753
776
|
}
|
|
754
777
|
|
|
755
778
|
/**
|
|
756
|
-
*
|
|
757
|
-
* if not the latter, just delete any item that matches the key.
|
|
779
|
+
* Remove one value from a key, and return it.
|
|
758
780
|
* @param key {string}
|
|
759
|
-
* @param isPreferred {function}
|
|
760
781
|
* @returns {*|undefined} The deleted item. */
|
|
761
|
-
|
|
762
|
-
let result;
|
|
782
|
+
deleteAny(key) {
|
|
763
783
|
let data = this.data;
|
|
784
|
+
let result;
|
|
764
785
|
let set = data[key];
|
|
765
|
-
if (!set)
|
|
786
|
+
if (!set) // slower than pre-check.
|
|
766
787
|
return undefined;
|
|
767
788
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
789
|
+
[result] = set; // Does the same as above and seems to be about the same speed.
|
|
790
|
+
set.delete(result);
|
|
791
|
+
|
|
792
|
+
if (set.size === 0)
|
|
793
|
+
delete data[key];
|
|
794
|
+
|
|
795
|
+
return result;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
deleteSpecific(key, val) {
|
|
799
|
+
let data = this.data;
|
|
800
|
+
let result;
|
|
801
|
+
let set = data[key];
|
|
802
|
+
if (!set)
|
|
803
|
+
return undefined;
|
|
804
|
+
|
|
805
|
+
set.delete(val);
|
|
806
|
+
result = val;
|
|
807
|
+
|
|
808
|
+
if (set.size === 0)
|
|
809
|
+
delete data[key];
|
|
810
|
+
|
|
811
|
+
return result;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Try to delete an item that matches the key and the isPreferred function.
|
|
817
|
+
* if not the latter, just delete any item that matches the key.
|
|
818
|
+
* @param key {string}
|
|
819
|
+
* @returns {*|undefined} The deleted item. */
|
|
820
|
+
deletePreferred(key, parent) {
|
|
821
|
+
let result;
|
|
822
|
+
let data = this.data;
|
|
823
|
+
let set = data[key];
|
|
824
|
+
if (!set)
|
|
825
|
+
return undefined;
|
|
826
|
+
|
|
827
|
+
for (let val of set) {
|
|
828
|
+
if (val?.parentNode === parent) {
|
|
829
|
+
set.delete(val);
|
|
830
|
+
result = val;
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
if (!result) {
|
|
835
|
+
[result] = set;
|
|
836
|
+
set.delete(result);
|
|
777
837
|
}
|
|
778
838
|
|
|
779
839
|
if (set.size === 0)
|
|
@@ -982,14 +1042,19 @@ const udomdiff = (parentNode, a, b, before) => {
|
|
|
982
1042
|
return b;
|
|
983
1043
|
};
|
|
984
1044
|
|
|
1045
|
+
//import {ArraySpliceOp} from "./watch.js";
|
|
1046
|
+
|
|
1047
|
+
|
|
985
1048
|
/**
|
|
986
1049
|
* Path to where an expression should be evaluated within a Shell or NodeGroup.
|
|
987
1050
|
* Path is only valid until the expressions before it are evaluated.
|
|
988
1051
|
* TODO: Make this based on parent and node instead of path? */
|
|
989
1052
|
class ExprPath {
|
|
990
1053
|
|
|
1054
|
+
|
|
1055
|
+
|
|
991
1056
|
/**
|
|
992
|
-
* @type {
|
|
1057
|
+
* @type {ExprPathType} */
|
|
993
1058
|
type;
|
|
994
1059
|
|
|
995
1060
|
// Used for attributes:
|
|
@@ -1005,8 +1070,6 @@ class ExprPath {
|
|
|
1005
1070
|
* @type {Set<string>} Used for type=AttribType.Multiple to remember the attributes that were added. */
|
|
1006
1071
|
attrNames;
|
|
1007
1072
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
1073
|
/**
|
|
1011
1074
|
* @type {Node} Node that occurs before this ExprPath's first Node.
|
|
1012
1075
|
* This is necessary because udomdiff() can steal nodes from another ExprPath.
|
|
@@ -1048,16 +1111,23 @@ class ExprPath {
|
|
|
1048
1111
|
nodeMarkerPath;
|
|
1049
1112
|
|
|
1050
1113
|
|
|
1051
|
-
/** @type {?function} */
|
|
1114
|
+
/** @type {?function} A function called by renderWatched() to update the value of this expression. */
|
|
1052
1115
|
watchFunction
|
|
1053
1116
|
|
|
1117
|
+
/**
|
|
1118
|
+
* @type {?function} The most recent callback passed to a .map() function in this ExprPath.
|
|
1119
|
+
* TODO: What if one ExprPath has two .map() calls? Maybe we just won't support that. */
|
|
1120
|
+
mapCallback
|
|
1121
|
+
|
|
1122
|
+
isHtmlProperty = undefined;
|
|
1123
|
+
|
|
1054
1124
|
/**
|
|
1055
1125
|
* @param nodeBefore {Node}
|
|
1056
1126
|
* @param nodeMarker {?Node}
|
|
1057
|
-
* @param type {
|
|
1127
|
+
* @param type {ExprPathType}
|
|
1058
1128
|
* @param attrName {?string}
|
|
1059
1129
|
* @param attrValue {string[]} */
|
|
1060
|
-
constructor(nodeBefore, nodeMarker, type=
|
|
1130
|
+
constructor(nodeBefore, nodeMarker, type=ExprPathType.Content, attrName=null, attrValue=null) {
|
|
1061
1131
|
|
|
1062
1132
|
// If path is a node.
|
|
1063
1133
|
this.nodeBefore = nodeBefore;
|
|
@@ -1065,7 +1135,7 @@ class ExprPath {
|
|
|
1065
1135
|
this.type = type;
|
|
1066
1136
|
this.attrName = attrName;
|
|
1067
1137
|
this.attrValue = attrValue;
|
|
1068
|
-
if (type ===
|
|
1138
|
+
if (type === ExprPathType.AttribMultiple)
|
|
1069
1139
|
this.attrNames = new Set();
|
|
1070
1140
|
}
|
|
1071
1141
|
|
|
@@ -1079,36 +1149,27 @@ class ExprPath {
|
|
|
1079
1149
|
* We should modify path.applyValueAttrib so it stores the procssed parts and then only calls
|
|
1080
1150
|
* setAttribute() once all the pieces are in place.
|
|
1081
1151
|
*
|
|
1082
|
-
* @param expr {Expr}
|
|
1083
1152
|
* @param exprs {Expr[]}
|
|
1084
|
-
* @param
|
|
1085
|
-
|
|
1086
|
-
* @returns {int} */
|
|
1087
|
-
apply(expr, exprs=null, exprIndex=0, componentExprs={}) {
|
|
1153
|
+
* @param freeNodeGroups {boolean} */
|
|
1154
|
+
apply(exprs, freeNodeGroups=true) {
|
|
1088
1155
|
switch (this.type) {
|
|
1089
1156
|
case 1: // PathType.Content:
|
|
1090
|
-
this.applyNodes(
|
|
1157
|
+
this.applyNodes(exprs[0], freeNodeGroups);
|
|
1091
1158
|
break;
|
|
1092
1159
|
case 2: // PathType.Multiple:
|
|
1093
|
-
this.applyMultipleAttribs(this.nodeMarker,
|
|
1160
|
+
this.applyMultipleAttribs(this.nodeMarker, exprs[0]);
|
|
1094
1161
|
break;
|
|
1095
1162
|
case 5: // PathType.Comment:
|
|
1096
1163
|
// Expressions inside Html comments. Deliberately empty because we won't waste time updating them.
|
|
1097
1164
|
break;
|
|
1098
1165
|
case 6: // PathType.Event:
|
|
1099
|
-
this.applyEventAttrib(this.nodeMarker,
|
|
1166
|
+
this.applyEventAttrib(this.nodeMarker, exprs[0], this.parentNg.rootNg.root);
|
|
1100
1167
|
break;
|
|
1101
|
-
default:
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
else {
|
|
1105
|
-
// One attribute value may have multiple expressions. Here we apply them all at once.
|
|
1106
|
-
exprIndex = this.applyValueAttrib(this.nodeMarker, exprs || [expr], exprIndex);
|
|
1107
|
-
}
|
|
1168
|
+
default: // TODO: Is this still used? Lots of tests fail without it.
|
|
1169
|
+
// One attribute value may have multiple expressions. Here we apply them all at once.
|
|
1170
|
+
this.applyValueAttrib(this.nodeMarker, exprs);
|
|
1108
1171
|
break;
|
|
1109
1172
|
}
|
|
1110
|
-
|
|
1111
|
-
return exprIndex;
|
|
1112
1173
|
}
|
|
1113
1174
|
|
|
1114
1175
|
/**
|
|
@@ -1116,14 +1177,16 @@ class ExprPath {
|
|
|
1116
1177
|
* Called by applyExprs()
|
|
1117
1178
|
* This function is recursive, as the functions it calls also call it.
|
|
1118
1179
|
* @param expr {Expr}
|
|
1180
|
+
* @param freeNodeGroups {boolean}
|
|
1119
1181
|
* @return {Node[]} New Nodes created. */
|
|
1120
|
-
applyNodes(expr) {
|
|
1182
|
+
applyNodes(expr, freeNodeGroups=true) {
|
|
1121
1183
|
let path = this;
|
|
1122
1184
|
|
|
1123
1185
|
// This can be done at the beginning or the end of this function.
|
|
1124
1186
|
// If at the end, we may get rendering done faster.
|
|
1125
1187
|
// But when at the beginning, it leaves all the nodes in-use so we can do a renderWatched().
|
|
1126
|
-
|
|
1188
|
+
if (freeNodeGroups)
|
|
1189
|
+
path.freeNodeGroups();
|
|
1127
1190
|
|
|
1128
1191
|
|
|
1129
1192
|
|
|
@@ -1133,10 +1196,10 @@ class ExprPath {
|
|
|
1133
1196
|
|
|
1134
1197
|
let secondPass = []; // indices
|
|
1135
1198
|
|
|
1136
|
-
path.nodeGroups = []; // Reset before
|
|
1137
|
-
path.
|
|
1199
|
+
path.nodeGroups = []; // Reset before applyExactNodes and the code below rebuilds it.
|
|
1200
|
+
path.applyExactNodes(expr, newNodes, secondPass);
|
|
1138
1201
|
|
|
1139
|
-
this.existingTextNodes = null;
|
|
1202
|
+
//this.existingTextNodes = null;
|
|
1140
1203
|
|
|
1141
1204
|
// TODO: Create an array of old vs Nodes and NodeGroups together.
|
|
1142
1205
|
// If they're all the same, skip the next steps.
|
|
@@ -1195,7 +1258,7 @@ class ExprPath {
|
|
|
1195
1258
|
|
|
1196
1259
|
for (let ng of oldNodeGroups)
|
|
1197
1260
|
if (!ng.startNode.parentNode)
|
|
1198
|
-
ng.
|
|
1261
|
+
ng.removeAndSaveOrphans();
|
|
1199
1262
|
}
|
|
1200
1263
|
|
|
1201
1264
|
|
|
@@ -1203,49 +1266,143 @@ class ExprPath {
|
|
|
1203
1266
|
}
|
|
1204
1267
|
|
|
1205
1268
|
/**
|
|
1206
|
-
* Used by watch() for replacing individual loop items.
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
let oldNg = this.nodeGroups[index];
|
|
1210
|
-
this.nodeGroupsFree.add(oldNg.exactKey, oldNg);
|
|
1211
|
-
this.nodeGroupsFree.add(oldNg.closeKey, oldNg);
|
|
1269
|
+
* Used by watch() for inserting/removing/replacing individual loop items.
|
|
1270
|
+
* @param op {ArraySpliceOp} */
|
|
1271
|
+
applyArrayOp(op) {
|
|
1212
1272
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1273
|
+
// Replace NodeGroups
|
|
1274
|
+
let replaceCount = Math.min(op.deleteCount, op.items.length);
|
|
1275
|
+
let deleteCount = op.deleteCount - replaceCount;
|
|
1276
|
+
for (let i=0; i<replaceCount; i++) {
|
|
1277
|
+
let oldNg = this.nodeGroups[op.index + i]; // TODO: One expr can create multiple nodegroups.
|
|
1278
|
+
|
|
1279
|
+
// Try to find an exact match
|
|
1280
|
+
let func = this.mapCallback || this.watchFunction;
|
|
1281
|
+
let expr = func(op.items[i]);
|
|
1282
|
+
|
|
1283
|
+
// If the result of func isn't a template, conver it to one or more templates.
|
|
1284
|
+
this.exprToTemplates(expr, template => { // TODO: An expr can create multiple NodeGroups. I need a way to group them.
|
|
1217
1285
|
|
|
1286
|
+
let ng = this.getNodeGroup(template, true); // Removes from nodeGroupsAttached and adds to nodeGroupsRendered()
|
|
1287
|
+
if (ng && ng === oldNg) ; else {
|
|
1218
1288
|
|
|
1289
|
+
// Find a close match or create a new node group
|
|
1290
|
+
if (!ng)
|
|
1291
|
+
ng = this.getNodeGroup(template, false); // adds back to nodeGroupsRendered()
|
|
1292
|
+
this.nodeGroups[op.index + i] = ng; // TODO: Remove old one to nodeGroupsDetached?
|
|
1219
1293
|
|
|
1220
|
-
|
|
1294
|
+
// Splice in the new nodes.
|
|
1295
|
+
let insertBefore = oldNg.startNode;
|
|
1296
|
+
for (let node of ng.getNodes())
|
|
1297
|
+
insertBefore.parentNode.insertBefore(node, insertBefore);
|
|
1221
1298
|
|
|
1222
|
-
|
|
1299
|
+
// Remove the old nodes.
|
|
1300
|
+
if (ng !== oldNg)
|
|
1301
|
+
oldNg.removeAndSaveOrphans();
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1223
1305
|
|
|
1224
|
-
//
|
|
1225
|
-
|
|
1226
|
-
|
|
1306
|
+
// Delete extra at the end.
|
|
1307
|
+
if (deleteCount > 0) {
|
|
1308
|
+
for (let i=0; i<deleteCount; i++) {
|
|
1309
|
+
let oldNg = this.nodeGroups[op.index + replaceCount + i];
|
|
1310
|
+
oldNg.removeAndSaveOrphans();
|
|
1311
|
+
}
|
|
1312
|
+
this.nodeGroups.splice(op.index + replaceCount, deleteCount);
|
|
1227
1313
|
}
|
|
1228
1314
|
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1315
|
+
// Add extra at the end.
|
|
1316
|
+
else {
|
|
1317
|
+
let newItems = op.items.slice(replaceCount);
|
|
1318
|
+
|
|
1319
|
+
let insertBefore = this.nodeGroups[op.index + replaceCount]?.startNode || this.nodeMarker;
|
|
1320
|
+
for (let i = 0; i < newItems.length; i++) { // We use nodeMarker if the subequent (or all) nodeGroups have been removed.
|
|
1321
|
+
|
|
1322
|
+
|
|
1323
|
+
// Try to find exact match
|
|
1324
|
+
let template = this.mapCallback(newItems[i]);
|
|
1325
|
+
let ng = this.getNodeGroup(template, true); // Removes from nodeGroupsAttached and adds to nodeGroupsRendered()
|
|
1326
|
+
if (!ng) // Find a close match or create a new node group
|
|
1327
|
+
ng = this.getNodeGroup(template, false); // adds back to nodeGroupsRendered()
|
|
1328
|
+
|
|
1329
|
+
this.nodeGroups.push(ng);
|
|
1330
|
+
|
|
1331
|
+
// Splice in the new nodes.
|
|
1332
|
+
for (let node of ng.getNodes())
|
|
1333
|
+
insertBefore.parentNode.insertBefore(node, insertBefore);
|
|
1334
|
+
}
|
|
1233
1335
|
}
|
|
1234
1336
|
|
|
1337
|
+
|
|
1338
|
+
|
|
1235
1339
|
// TODO: update or invalidate the nodes cache?
|
|
1236
1340
|
this.nodesCache = null;
|
|
1237
1341
|
}
|
|
1238
1342
|
|
|
1343
|
+
/**
|
|
1344
|
+
* Recursively traverse expr.
|
|
1345
|
+
* If a value is a function, evaluate it.
|
|
1346
|
+
* If a value is an array, recurse on each item.
|
|
1347
|
+
* If it's a primitive, convert it to a Template.
|
|
1348
|
+
* Otherwise pass the item (which is now either a Template or a Node) to callback.
|
|
1349
|
+
* @param expr
|
|
1350
|
+
* @param callback {function(Node|Template)}
|
|
1351
|
+
*
|
|
1352
|
+
* TODO: have applyExactNodes() use this function. */
|
|
1353
|
+
exprToTemplates(expr, callback) {
|
|
1354
|
+
if (Array.isArray(expr))
|
|
1355
|
+
for (let subExpr of expr)
|
|
1356
|
+
this.exprToTemplates(subExpr, callback);
|
|
1357
|
+
|
|
1358
|
+
else if (typeof expr === 'function') {
|
|
1359
|
+
// TODO: One ExprPath can have multiple expr functions.
|
|
1360
|
+
// But if using it as a watch, it should only have one at the top level.
|
|
1361
|
+
// So maybe this is ok.
|
|
1362
|
+
Globals$1.currentExprPath = this; // Used by watch()
|
|
1363
|
+
|
|
1364
|
+
this.watchFunction = expr; // TODO: Only do this if it's a top level function.
|
|
1365
|
+
expr = expr(); // As expr accesses watched variables, watch() uses Globals.currentExprPath to mark where those watched variables are being used.
|
|
1366
|
+
Globals$1.currentExprPath = null;
|
|
1367
|
+
|
|
1368
|
+
this.exprToTemplates(expr, callback);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// String/Number/Date/Boolean
|
|
1372
|
+
else if (!(expr instanceof Template) && !(expr instanceof Node)){
|
|
1373
|
+
// Convert expression to a string.
|
|
1374
|
+
if (expr === undefined || expr === false || expr === null) // Util.isFalsy() inlined
|
|
1375
|
+
expr = '';
|
|
1376
|
+
else if (typeof expr !== 'string')
|
|
1377
|
+
expr += '';
|
|
1378
|
+
|
|
1379
|
+
// Get the same Template for the same string each time.
|
|
1380
|
+
// let template = Globals.stringTemplates[expr];
|
|
1381
|
+
// if (!template) {
|
|
1382
|
+
let template = new Template([expr], []);
|
|
1383
|
+
// Globals.stringTemplates[expr] = template;
|
|
1384
|
+
//}
|
|
1385
|
+
|
|
1386
|
+
// Recurse.
|
|
1387
|
+
this.exprToTemplates(template, callback);
|
|
1388
|
+
}
|
|
1389
|
+
else
|
|
1390
|
+
callback(expr);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1239
1393
|
|
|
1240
1394
|
/**
|
|
1241
|
-
*
|
|
1395
|
+
* Try to apply Nodes that are an exact match, by finding existing nodes from the last render
|
|
1396
|
+
* that have the same value as created by the expr.
|
|
1397
|
+
* This is called from ExprPath.applyNodes().
|
|
1398
|
+
*
|
|
1242
1399
|
* @param expr {Template|Node|Array|function|*}
|
|
1243
|
-
* @param newNodes {(Node|Template)[]}
|
|
1244
|
-
* @param secondPass {
|
|
1245
|
-
|
|
1400
|
+
* @param newNodes {(Node|Template)[]} An inout parameter; we add the nodes here as we go.
|
|
1401
|
+
* @param secondPass {[int, int][]} Locations within newNodes for ExprPath.applyNodes() to evaluate later,
|
|
1402
|
+
* when it tries to find partial matches. */
|
|
1403
|
+
applyExactNodes(expr, newNodes, secondPass) {
|
|
1246
1404
|
|
|
1247
1405
|
if (expr instanceof Template) {
|
|
1248
|
-
|
|
1249
1406
|
let ng = this.getNodeGroup(expr, true);
|
|
1250
1407
|
if (ng) {
|
|
1251
1408
|
|
|
@@ -1263,7 +1420,7 @@ class ExprPath {
|
|
|
1263
1420
|
}
|
|
1264
1421
|
}
|
|
1265
1422
|
|
|
1266
|
-
// Node created by an expression.
|
|
1423
|
+
// Node(s) created by an expression.
|
|
1267
1424
|
else if (expr instanceof Node) {
|
|
1268
1425
|
|
|
1269
1426
|
// DocumentFragment created by an expression.
|
|
@@ -1276,49 +1433,50 @@ class ExprPath {
|
|
|
1276
1433
|
// Arrays and functions.
|
|
1277
1434
|
// I tried iterating over the result of a generator function to avoid this recursion and simplify the code,
|
|
1278
1435
|
// but that consistently made the js-framework-benchmarks a few percentage points slower.
|
|
1279
|
-
else
|
|
1436
|
+
else {
|
|
1437
|
+
this.exprToTemplates(expr, template => {
|
|
1438
|
+
this.applyExactNodes(template, newNodes, secondPass);
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// Old version
|
|
1444
|
+
/*else if (Array.isArray(expr))
|
|
1280
1445
|
for (let subExpr of expr)
|
|
1281
|
-
this.
|
|
1446
|
+
this.applyExactNodes(subExpr, newNodes, secondPass);
|
|
1282
1447
|
|
|
1283
1448
|
else if (typeof expr === 'function') {
|
|
1284
1449
|
// TODO: One ExprPath can have multiple expr functions.
|
|
1285
1450
|
// But if using it as a watch, it should only have one at the top level.
|
|
1286
1451
|
// So maybe this is ok.
|
|
1287
|
-
Globals.currentExprPath =
|
|
1452
|
+
Globals.currentExprPath = this; // Used by watch()
|
|
1453
|
+
|
|
1288
1454
|
this.watchFunction = expr; // TODO: Only do this if it's a top level function.
|
|
1289
|
-
let result = expr();
|
|
1455
|
+
let result = expr(); // As expr accesses watched variables, watch() uses Globals.currentExprPath to mark where those watched variables are being used.
|
|
1290
1456
|
Globals.currentExprPath = null;
|
|
1291
1457
|
|
|
1292
|
-
this.
|
|
1458
|
+
this.applyExactNodes(result, newNodes, secondPass);
|
|
1293
1459
|
}
|
|
1294
1460
|
|
|
1295
|
-
//
|
|
1461
|
+
// String
|
|
1296
1462
|
else {
|
|
1297
|
-
// Convert
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1463
|
+
// Convert expression to a string.
|
|
1464
|
+
let stringExpr = expr;
|
|
1465
|
+
if (expr === undefined || expr === false || expr === null) // Util.isFalsy()
|
|
1466
|
+
stringExpr = '';
|
|
1467
|
+
else if (typeof expr !== 'string')
|
|
1468
|
+
stringExpr = expr + '';
|
|
1469
|
+
|
|
1470
|
+
// Get the same Template for the same string each time.
|
|
1471
|
+
let template = Globals.stringTemplates[stringExpr];
|
|
1472
|
+
if (!template) {
|
|
1473
|
+
template = new Template([stringExpr], []);
|
|
1474
|
+
Globals.stringTemplates[stringExpr] = template;
|
|
1308
1475
|
}
|
|
1309
1476
|
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
this.existingTextNodes = this.getNodes().filter(n => n.nodeType === 3);
|
|
1314
|
-
|
|
1315
|
-
let idx = this.existingTextNodes.findIndex(n => n.textContent === text);
|
|
1316
|
-
if (idx !== -1)
|
|
1317
|
-
newNodes.push(...this.existingTextNodes.splice(idx, 1));
|
|
1318
|
-
else
|
|
1319
|
-
newNodes.push(this.nodeMarker.ownerDocument.createTextNode(text));
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1477
|
+
// Recurse.
|
|
1478
|
+
this.applyExactNodes(template, newNodes, secondPass);
|
|
1479
|
+
}*/
|
|
1322
1480
|
}
|
|
1323
1481
|
|
|
1324
1482
|
applyMultipleAttribs(node, expr) {
|
|
@@ -1331,6 +1489,13 @@ class ExprPath {
|
|
|
1331
1489
|
let oldNames = this.attrNames;
|
|
1332
1490
|
this.attrNames = new Set();
|
|
1333
1491
|
if (expr) {
|
|
1492
|
+
if (typeof expr === 'function') {
|
|
1493
|
+
Globals$1.currentExprPath = this; // Used by watch()
|
|
1494
|
+
this.watchFunction = expr; // used by renderWatched()
|
|
1495
|
+
expr = expr();
|
|
1496
|
+
Globals$1.currentExprPath = null;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1334
1499
|
let attrs = (expr +'') // Split string into multiple attributes.
|
|
1335
1500
|
.split(/([\w-]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s]+))/g)
|
|
1336
1501
|
.map(text => text.trim())
|
|
@@ -1364,46 +1529,51 @@ class ExprPath {
|
|
|
1364
1529
|
|
|
1365
1530
|
let eventName = this.attrName.slice(2); // remove "on-" prefix.
|
|
1366
1531
|
let func;
|
|
1367
|
-
|
|
1368
|
-
// Convert array to function.
|
|
1369
1532
|
let args = [];
|
|
1370
|
-
if (Array.isArray(expr)) {
|
|
1371
|
-
|
|
1372
|
-
// oninput=${[this.doSomething, 'meow']}
|
|
1373
|
-
if (typeof expr[0] === 'function') {
|
|
1374
|
-
func = expr[0];
|
|
1375
|
-
args = expr.slice(1);
|
|
1376
|
-
}
|
|
1377
1533
|
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
// root.render(); // TODO: This causes infinite recursion.
|
|
1384
|
-
}
|
|
1534
|
+
// Convert array to function.
|
|
1535
|
+
// oninput=${[this.doSomething, 'meow']}
|
|
1536
|
+
if (Array.isArray(expr) && typeof expr[0] === 'function') {
|
|
1537
|
+
func = expr[0];
|
|
1538
|
+
args = expr.slice(1);
|
|
1385
1539
|
}
|
|
1386
|
-
else
|
|
1540
|
+
else if (typeof expr === 'function')
|
|
1387
1541
|
func = expr;
|
|
1542
|
+
else
|
|
1543
|
+
throw new Error(`Invalid event binding: <${node.tagName.toLowerCase()} ${this.attrName}=\${${JSON.stringify(expr)}}>`);
|
|
1388
1544
|
|
|
1389
1545
|
this.bindEvent(node, root, eventName, eventName, func, args);
|
|
1390
1546
|
}
|
|
1391
1547
|
|
|
1392
1548
|
|
|
1549
|
+
/**
|
|
1550
|
+
* Call function when eventName is triggerd on node.
|
|
1551
|
+
* @param node {HTMLElement}
|
|
1552
|
+
* @param root {HTMLElement}
|
|
1553
|
+
* @param key {string}
|
|
1554
|
+
* @param eventName {string}
|
|
1555
|
+
* @param func {function}
|
|
1556
|
+
* @param args {array}
|
|
1557
|
+
* @param capture {boolean} */
|
|
1393
1558
|
bindEvent(node, root, key, eventName, func, args, capture=false) {
|
|
1394
|
-
let nodeEvents = Globals.nodeEvents.get(node);
|
|
1559
|
+
let nodeEvents = Globals$1.nodeEvents.get(node);
|
|
1395
1560
|
if (!nodeEvents) {
|
|
1396
1561
|
nodeEvents = {[key]: new Array(3)};
|
|
1397
|
-
Globals.nodeEvents.set(node, nodeEvents);
|
|
1562
|
+
Globals$1.nodeEvents.set(node, nodeEvents);
|
|
1398
1563
|
}
|
|
1399
1564
|
let nodeEvent = nodeEvents[key];
|
|
1400
1565
|
if (!nodeEvent)
|
|
1401
1566
|
nodeEvents[key] = nodeEvent = new Array(3);
|
|
1402
1567
|
|
|
1568
|
+
if (typeof func !== 'function')
|
|
1569
|
+
throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${${func}}> because it's not a function.`);
|
|
1403
1570
|
|
|
1404
1571
|
// If function has changed, remove and rebind the event.
|
|
1405
1572
|
if (nodeEvent[0] !== func) {
|
|
1406
1573
|
|
|
1574
|
+
// TODO: We should be removing event listeners when calling getNodeGroup(),
|
|
1575
|
+
// when we get the node from the list of nodeGroupsAttached/nodeGroupsDetached,
|
|
1576
|
+
// instead of only when we rebind an event.
|
|
1407
1577
|
let [existing, existingBound, _] = nodeEvent;
|
|
1408
1578
|
if (existing)
|
|
1409
1579
|
node.removeEventListener(eventName, existingBound, capture);
|
|
@@ -1433,68 +1603,144 @@ class ExprPath {
|
|
|
1433
1603
|
nodeEvents[key][2] = args;
|
|
1434
1604
|
}
|
|
1435
1605
|
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
else if (!this.attrValue && expr === true)
|
|
1444
|
-
node.setAttribute(this.attrName, '');
|
|
1606
|
+
/**
|
|
1607
|
+
* Handle values, including two-way binding.
|
|
1608
|
+
* @param node
|
|
1609
|
+
* @param exprs */
|
|
1610
|
+
// TODO: node is always this.nodeMarker?
|
|
1611
|
+
applyValueAttrib(node, exprs) {
|
|
1612
|
+
let expr = exprs[0];
|
|
1445
1613
|
|
|
1614
|
+
// Two-way binding between attributes
|
|
1446
1615
|
// Passing a path to the value attribute.
|
|
1616
|
+
// Copies the attribute to the property when the input event fires.
|
|
1617
|
+
// value=${[this, 'value]'}
|
|
1618
|
+
// checked=${[this, 'isAgree']}
|
|
1447
1619
|
// This same logic is in NodeGroup.createNewComponent() for components.
|
|
1448
|
-
|
|
1620
|
+
if (Util.isPath(expr)) {
|
|
1449
1621
|
let [obj, path] = [expr[0], expr.slice(1)];
|
|
1450
|
-
node.value = delve(obj, path);
|
|
1451
|
-
// TODO: We need to remove any old listeners, like in bindEventAttribute
|
|
1452
1622
|
|
|
1623
|
+
if (!obj)
|
|
1624
|
+
throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${[${expr.map(item => item ? `'${item}'` : item+'').join(', ')}]}>.`);
|
|
1625
|
+
|
|
1626
|
+
let value = delve(obj, path);
|
|
1627
|
+
|
|
1628
|
+
// Special case to allow setting select-multiple value from an array
|
|
1629
|
+
if (this.attrName === 'value' && node.type === 'select-multiple' && Array.isArray(value)) {
|
|
1630
|
+
// Set the .selected property on the options having a value within value.
|
|
1631
|
+
let strValues = value.map(v => v + '');
|
|
1632
|
+
for (let option of node.options)
|
|
1633
|
+
option.selected = strValues.includes(option.value);
|
|
1634
|
+
}
|
|
1635
|
+
else {
|
|
1636
|
+
// TODO: should we remove isFalsy, since these are always props?
|
|
1637
|
+
let strValue = Util.isFalsy(value) ? '' : value;
|
|
1638
|
+
|
|
1639
|
+
// If we don't have this condition, when we call render(), the browser will scroll to the currently
|
|
1640
|
+
// selected item in a <select> and mess up manually scrolling to a different value.
|
|
1641
|
+
if (strValue !== node[this.attrName])
|
|
1642
|
+
node[this.attrName] = strValue;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// TODO: We need to remove any old listeners, like in bindEventAttribute.
|
|
1646
|
+
// Does bindEvent() now handle that?
|
|
1453
1647
|
let func = () => {
|
|
1454
|
-
|
|
1648
|
+
let value = (this.attrName === 'value')
|
|
1649
|
+
? Util.getInputValue(node)
|
|
1650
|
+
: node[this.attrName];
|
|
1651
|
+
delve(obj, path, value);
|
|
1455
1652
|
};
|
|
1456
1653
|
|
|
1457
1654
|
// We use capture so we update the values before other events added by the user.
|
|
1458
|
-
|
|
1655
|
+
// TODO: Bind to scroll events also?
|
|
1656
|
+
// What about resize events and width/height?
|
|
1657
|
+
this.bindEvent(node, path[0], this.attrName, 'input', func, [], true);
|
|
1459
1658
|
}
|
|
1460
1659
|
|
|
1461
1660
|
// Regular attribute
|
|
1462
1661
|
else {
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
//
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1662
|
+
// TODO: Cache this on ExprPath.isProp when Shell creates the props. Have ExprPath.clone() copy .isProp
|
|
1663
|
+
// Or make it a new PathType.
|
|
1664
|
+
//if (this.attrName === 'disabled')
|
|
1665
|
+
// debugger;
|
|
1666
|
+
|
|
1667
|
+
// hasOwnProperty() checks only the object, not the parents
|
|
1668
|
+
// this.attrName in node checks the node and the parents.
|
|
1669
|
+
// This version checks the html element it extends from, to see if has a setter set:
|
|
1670
|
+
// Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), this.attrName)?.set
|
|
1671
|
+
//let isProp = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), this.attrName)?.set;
|
|
1672
|
+
let isProp = this.isHtmlProperty;
|
|
1673
|
+
if (isProp === undefined)
|
|
1674
|
+
isProp = this.isHtmlProperty = Util.isHtmlProp(node, this.attrName);
|
|
1675
|
+
|
|
1676
|
+
// Values to toggle an attribute
|
|
1677
|
+
let multiple = this.attrValue;
|
|
1678
|
+
if (!multiple) {
|
|
1679
|
+
Globals$1.currentExprPath = this; // Used by watch()
|
|
1680
|
+
if (typeof expr === 'function') {
|
|
1681
|
+
if (this.type === 4) { // Don't evaluate functions before passing them to components
|
|
1682
|
+
return
|
|
1474
1683
|
}
|
|
1684
|
+
this.watchFunction = expr; // The function that gets the expression, used for renderWatched()
|
|
1685
|
+
expr = expr();
|
|
1475
1686
|
}
|
|
1476
|
-
|
|
1687
|
+
else
|
|
1688
|
+
expr = Util.makePrimitive(expr);
|
|
1689
|
+
Globals$1.currentExprPath = null;
|
|
1690
|
+
}
|
|
1691
|
+
if (!multiple && (expr === undefined || expr === false || expr === null)) { // Util.isFalsy() inlined.
|
|
1692
|
+
if (isProp)
|
|
1693
|
+
node[this.attrName] = false;
|
|
1694
|
+
node.removeAttribute(this.attrName);
|
|
1695
|
+
}
|
|
1696
|
+
else if (!multiple && expr === true) {
|
|
1697
|
+
if (isProp)
|
|
1698
|
+
node[this.attrName] = true;
|
|
1699
|
+
node.setAttribute(this.attrName, '');
|
|
1477
1700
|
}
|
|
1478
|
-
else
|
|
1479
|
-
value.unshift(expr);
|
|
1480
1701
|
|
|
1481
|
-
|
|
1702
|
+
// A non-toggled attribute
|
|
1703
|
+
else {
|
|
1482
1704
|
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1705
|
+
// If it's a series of expressions among strings, join them together.
|
|
1706
|
+
let joinedValue;
|
|
1707
|
+
if (multiple) {
|
|
1708
|
+
let value = [];
|
|
1709
|
+
for (let i = 0; i < this.attrValue.length; i++) {
|
|
1710
|
+
value.push(this.attrValue[i]);
|
|
1711
|
+
if (i < this.attrValue.length - 1) {
|
|
1712
|
+
Globals$1.currentExprPath = this; // Used by watch()
|
|
1713
|
+
let val = Util.makePrimitive(exprs[i]);
|
|
1714
|
+
Globals$1.currentExprPath = null;
|
|
1715
|
+
if (!Util.isFalsy(val))
|
|
1716
|
+
value.push(val);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
joinedValue = value.join('');
|
|
1720
|
+
}
|
|
1489
1721
|
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1722
|
+
// If the attribute is one expression with no strings:
|
|
1723
|
+
else
|
|
1724
|
+
joinedValue = expr;
|
|
1725
|
+
|
|
1726
|
+
// Only update attributes if the value has changed.
|
|
1727
|
+
// This is needed for setting input.value, .checked, option.selected, etc.
|
|
1728
|
+
|
|
1729
|
+
let oldVal = isProp
|
|
1730
|
+
? node[this.attrName]
|
|
1731
|
+
: node.getAttribute(this.attrName);
|
|
1732
|
+
if (oldVal !== joinedValue) {
|
|
1733
|
+
|
|
1734
|
+
// <textarea value=${expr}></textarea>
|
|
1735
|
+
// Without this branch we have no way to set the value of a textarea,
|
|
1736
|
+
// since we also prohibit expressions that are a child of textarea.
|
|
1737
|
+
if (isProp)
|
|
1738
|
+
node[this.attrName] = joinedValue;
|
|
1739
|
+
// TODO: Putting an 'else' here would be more performant
|
|
1740
|
+
node.setAttribute(this.attrName, joinedValue);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1495
1743
|
}
|
|
1496
|
-
|
|
1497
|
-
return exprIndex;
|
|
1498
1744
|
}
|
|
1499
1745
|
|
|
1500
1746
|
|
|
@@ -1510,7 +1756,8 @@ class ExprPath {
|
|
|
1510
1756
|
let nodeMarker, nodeBefore;
|
|
1511
1757
|
let root = newRoot;
|
|
1512
1758
|
let path = pathOffset ? this.nodeMarkerPath.slice(0, -pathOffset) : this.nodeMarkerPath;
|
|
1513
|
-
|
|
1759
|
+
let length = path.length-1;
|
|
1760
|
+
for (let i=length; i>0; i--) // Resolve the path.
|
|
1514
1761
|
root = root.childNodes[path[i]];
|
|
1515
1762
|
let childNodes = root.childNodes;
|
|
1516
1763
|
|
|
@@ -1550,7 +1797,7 @@ class ExprPath {
|
|
|
1550
1797
|
|
|
1551
1798
|
/**
|
|
1552
1799
|
* Attempt to remove all of this ExprPath's nodes from the DOM, if it can be done using a special fast method.
|
|
1553
|
-
* @returns {boolean} Returns false if Nodes
|
|
1800
|
+
* @returns {boolean} Returns false if Nodes weren't removed, and they should instead be removed manually. */
|
|
1554
1801
|
fastClear() {
|
|
1555
1802
|
let parent = this.nodeBefore.parentNode;
|
|
1556
1803
|
if (this.nodeBefore === parent.firstChild && this.nodeMarker === parent.lastChild) {
|
|
@@ -1586,6 +1833,10 @@ class ExprPath {
|
|
|
1586
1833
|
// result2.push(...ng.getNodes())
|
|
1587
1834
|
// return result2;
|
|
1588
1835
|
|
|
1836
|
+
if (this.type === ExprPathType.AttribValue || this.type === ExprPathType.AttribMultiple || this.type === ExprPathType.ComponentAttribValue) {
|
|
1837
|
+
return [this.nodeMarker];
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1589
1840
|
|
|
1590
1841
|
let result;
|
|
1591
1842
|
|
|
@@ -1610,7 +1861,8 @@ class ExprPath {
|
|
|
1610
1861
|
return result;
|
|
1611
1862
|
}
|
|
1612
1863
|
|
|
1613
|
-
|
|
1864
|
+
/** @return {HTMLElement|ParentNode} */
|
|
1865
|
+
getParentNode() {
|
|
1614
1866
|
return this.nodeMarker.parentNode
|
|
1615
1867
|
}
|
|
1616
1868
|
|
|
@@ -1627,28 +1879,39 @@ class ExprPath {
|
|
|
1627
1879
|
* or createa new NodeGroup from the template.
|
|
1628
1880
|
* @return {NodeGroup} */
|
|
1629
1881
|
getNodeGroup(template, exact=true) {
|
|
1630
|
-
//if (exact && this.nodeGroupsFree.isEmpty())
|
|
1631
|
-
// return null;
|
|
1632
1882
|
|
|
1633
1883
|
let result;
|
|
1884
|
+
let collection = this.nodeGroupsAttachedAvailable;
|
|
1634
1885
|
|
|
1635
1886
|
// TODO: Would it be faster to maintain a separate list of detached nodegroups?
|
|
1636
1887
|
if (exact) { // [below] parentElement will be null if the parent is a DocumentFragment
|
|
1637
|
-
result =
|
|
1888
|
+
result = collection.deleteAny(template.getExactKey());
|
|
1889
|
+
if (!result) { // try searching detached
|
|
1890
|
+
collection = this.nodeGroupsDetachedAvailable;
|
|
1891
|
+
result = collection.deleteAny(template.getExactKey());
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1638
1894
|
if (result) // also delete the matching close key.
|
|
1639
|
-
|
|
1640
|
-
else
|
|
1895
|
+
collection.deleteSpecific(template.getCloseKey(), result);
|
|
1896
|
+
else {
|
|
1641
1897
|
return null;
|
|
1898
|
+
}
|
|
1642
1899
|
}
|
|
1643
1900
|
|
|
1644
1901
|
// Find a close match.
|
|
1645
1902
|
// This is a match that has matching html, but different expressions applied.
|
|
1646
1903
|
// We can then apply the expressions to make it an exact match.
|
|
1647
|
-
|
|
1648
|
-
|
|
1904
|
+
// 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.
|
|
1905
|
+
else if (template.exprs.length) {
|
|
1906
|
+
result = collection.deleteAny(template.getCloseKey());
|
|
1907
|
+
if (!result) { // try searching detached
|
|
1908
|
+
collection = this.nodeGroupsDetachedAvailable;
|
|
1909
|
+
result = collection.deleteAny(template.getCloseKey());
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1649
1912
|
if (result) {
|
|
1650
1913
|
|
|
1651
|
-
|
|
1914
|
+
collection.deleteSpecific(result.exactKey, result);
|
|
1652
1915
|
|
|
1653
1916
|
// Update this close match with the new expression values.
|
|
1654
1917
|
result.applyExprs(template.exprs);
|
|
@@ -1660,95 +1923,85 @@ class ExprPath {
|
|
|
1660
1923
|
result = new NodeGroup(template, this);
|
|
1661
1924
|
|
|
1662
1925
|
// old:
|
|
1663
|
-
this.
|
|
1664
|
-
|
|
1665
|
-
// new:
|
|
1666
|
-
// let ngiu = this.nodeGroupsInUse;
|
|
1667
|
-
// ngiu.add(result.exactKey, result);
|
|
1668
|
-
// ngiu.add(result.closeKey, result);
|
|
1926
|
+
this.nodeGroupsRendered.push(result);
|
|
1669
1927
|
|
|
1670
1928
|
|
|
1671
1929
|
return result;
|
|
1672
1930
|
}
|
|
1673
1931
|
|
|
1932
|
+
isComponent() {
|
|
1933
|
+
// Events won't have type===Component.
|
|
1934
|
+
// TODO: Have a special flag for components instead of it being on the type?
|
|
1935
|
+
return this.type === ExprPathType.ComponentAttribValue || (this.attrName && this.nodeMarker.tagName && this.nodeMarker.tagName.includes('-'));
|
|
1936
|
+
}
|
|
1674
1937
|
|
|
1675
1938
|
/**
|
|
1939
|
+
* TODO: Rename this to nodeGroupsInUse, nodeGroupsAvialableAttached and nodeGroupsAvailableDetached?
|
|
1940
|
+
* Nodes that have been used during the current render().
|
|
1676
1941
|
* Used with getNodeGroup() and freeNodeGroups().
|
|
1677
1942
|
* TODO: Use an array of WeakRef so the gc can collect them?
|
|
1678
1943
|
* TODO: Put items back in nodeGroupsInUse after applyExpr() is called, not before.
|
|
1679
1944
|
* @type {NodeGroup[]} */
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
/** @type {MultiValueMap<key:string, value:NodeGroup>} */
|
|
1683
|
-
//nodeGroupsInUse = new MultiValueMap();
|
|
1945
|
+
nodeGroupsRendered = [];
|
|
1684
1946
|
|
|
1685
1947
|
/**
|
|
1948
|
+
* Nodes that were added to the web component during the last render(), but are available to be used again.
|
|
1686
1949
|
* Used with getNodeGroup() and freeNodeGroups().
|
|
1687
1950
|
* Each NodeGroup is here twice, once under an exact key, and once under the close key.
|
|
1688
1951
|
* @type {MultiValueMap<key:string, value:NodeGroup>} */
|
|
1689
|
-
|
|
1952
|
+
nodeGroupsAttachedAvailable = new MultiValueMap();
|
|
1690
1953
|
|
|
1691
|
-
|
|
1954
|
+
/**
|
|
1955
|
+
* Nodes that were not added to the web component during the last render(), and available to be used again.
|
|
1956
|
+
* @type {MultiValueMap} */
|
|
1957
|
+
nodeGroupsDetachedAvailable = new MultiValueMap();
|
|
1692
1958
|
|
|
1693
1959
|
|
|
1694
1960
|
/**
|
|
1695
|
-
* Move everything from this.
|
|
1961
|
+
* Move everything from this.nodeGroupsRendered to this.nodeGroupsAttached and nodeGroupsDetached.
|
|
1962
|
+
* Called at the beginning of applyNodes() so it can have NodeGroups to use.
|
|
1696
1963
|
* TODO: this could run as needed in getNodeGroup? */
|
|
1697
1964
|
freeNodeGroups() {
|
|
1698
|
-
//
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1965
|
+
// Add nodes that weren't used during render() to nodeGroupsDetached
|
|
1966
|
+
let previouslyAttached = this.nodeGroupsAttachedAvailable.data;
|
|
1967
|
+
let detached = this.nodeGroupsDetachedAvailable.data;
|
|
1968
|
+
for (let key in previouslyAttached) {
|
|
1969
|
+
let set = detached[key];
|
|
1970
|
+
if (!set)
|
|
1971
|
+
detached[key] = previouslyAttached[key];
|
|
1972
|
+
else
|
|
1973
|
+
for (let ng of previouslyAttached[key])
|
|
1974
|
+
set.add(ng);
|
|
1975
|
+
}
|
|
1702
1976
|
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1977
|
+
// Add nodes that were used during render() to nodeGroupsRendered.
|
|
1978
|
+
this.nodeGroupsAttachedAvailable = new MultiValueMap();
|
|
1979
|
+
let nga = this.nodeGroupsAttachedAvailable;
|
|
1980
|
+
for (let ng of this.nodeGroupsRendered) {
|
|
1981
|
+
nga.add(ng.exactKey, ng);
|
|
1982
|
+
nga.add(ng.closeKey, ng);
|
|
1707
1983
|
}
|
|
1708
|
-
this.nodeGroupsInUse = [];
|
|
1709
1984
|
|
|
1710
|
-
|
|
1711
|
-
// for (let key in this.nodeGroupsFree.data)
|
|
1712
|
-
// for (let item of this.nodeGroupsFree.data[key])
|
|
1713
|
-
// this.nodeGroupsInUse.add(key, item);
|
|
1714
|
-
//
|
|
1715
|
-
// this.nodeGroupsFree = this.nodeGroupsInUse;
|
|
1716
|
-
// this.nodeGroupsInUse = new MultiValueMap();
|
|
1985
|
+
this.nodeGroupsRendered = [];
|
|
1717
1986
|
}
|
|
1718
1987
|
|
|
1719
1988
|
|
|
1720
1989
|
}
|
|
1721
1990
|
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
/**
|
|
1725
|
-
*
|
|
1726
|
-
* @param root
|
|
1727
|
-
* @param path {string[]}
|
|
1728
|
-
* @param node {HTMLElement}
|
|
1729
|
-
*/
|
|
1730
|
-
function setValue(root, path, node) {
|
|
1731
|
-
let val = node.value;
|
|
1732
|
-
if (node.type === 'number')
|
|
1733
|
-
val = parseFloat(val);
|
|
1734
|
-
|
|
1735
|
-
delve(root, path, val);
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
1991
|
/** @enum {int} */
|
|
1739
|
-
const
|
|
1992
|
+
const ExprPathType = {
|
|
1740
1993
|
/** Child of a node */
|
|
1741
1994
|
Content: 1,
|
|
1742
|
-
|
|
1995
|
+
|
|
1743
1996
|
/** One or more whole attributes */
|
|
1744
|
-
|
|
1745
|
-
|
|
1997
|
+
AttribMultiple: 2,
|
|
1998
|
+
|
|
1746
1999
|
/** Value of an attribute. */
|
|
1747
|
-
|
|
1748
|
-
|
|
2000
|
+
AttribValue: 3,
|
|
2001
|
+
|
|
1749
2002
|
/** Value of an attribute being passed to a component. */
|
|
1750
|
-
|
|
1751
|
-
|
|
2003
|
+
ComponentAttribValue: 4,
|
|
2004
|
+
|
|
1752
2005
|
/** Expressions inside Html comments. */
|
|
1753
2006
|
Comment: 5,
|
|
1754
2007
|
|
|
@@ -1774,116 +2027,177 @@ function getNodePath(node) {
|
|
|
1774
2027
|
* Note that the path is backward, with the outermost element at the end.
|
|
1775
2028
|
* @param root {HTMLElement|Document|DocumentFragment|ParentNode}
|
|
1776
2029
|
* @param path {int[]}
|
|
1777
|
-
* @returns {Node|HTMLElement} */
|
|
2030
|
+
* @returns {Node|HTMLElement|HTMLStyleElement} */
|
|
1778
2031
|
function resolveNodePath(root, path) {
|
|
1779
2032
|
for (let i=path.length-1; i>=0; i--)
|
|
1780
2033
|
root = root.childNodes[path[i]];
|
|
1781
2034
|
return root;
|
|
1782
2035
|
}
|
|
1783
2036
|
|
|
2037
|
+
class HtmlParser {
|
|
2038
|
+
constructor() {
|
|
2039
|
+
this.defaultState = {
|
|
2040
|
+
context: HtmlParser.Text, // possible values: 'TEXT', 'TAG', 'ATTRIBUTE'
|
|
2041
|
+
quote: null, // possible values: null, '"', "'"
|
|
2042
|
+
buffer: '',
|
|
2043
|
+
lastChar: null
|
|
2044
|
+
};
|
|
2045
|
+
this.state = {...this.defaultState};
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
reset() {
|
|
2049
|
+
this.state = {...this.defaultState};
|
|
2050
|
+
return this.state.context;
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
/**
|
|
2054
|
+
* Parse the next chunk of html, starting with the same context we left off with from the previous chunk.
|
|
2055
|
+
* @param html {string}
|
|
2056
|
+
* @param onContextChange {?function(html:string, index:int, oldContext:string, newContext:string)}
|
|
2057
|
+
* Called every time the context changes, and again at the last context.
|
|
2058
|
+
* @return {('Attribute','Text','Tag')} The context at the end of html. */
|
|
2059
|
+
parse(html, onContextChange=null) {
|
|
2060
|
+
if (html === null)
|
|
2061
|
+
return this.reset();
|
|
2062
|
+
|
|
2063
|
+
for (let i = 0; i < html.length; i++) {
|
|
2064
|
+
const char = html[i];
|
|
2065
|
+
switch (this.state.context) {
|
|
2066
|
+
case HtmlParser.Text:
|
|
2067
|
+
if (char === '<' && html[i + 1].match(/[/a-z!]/i)) { // Start of a tag or comment.
|
|
2068
|
+
onContextChange?.(html, i, this.state.context, HtmlParser.Tag);
|
|
2069
|
+
this.state.context = HtmlParser.Tag;
|
|
2070
|
+
this.state.buffer = '';
|
|
2071
|
+
}
|
|
2072
|
+
break;
|
|
2073
|
+
case HtmlParser.Tag:
|
|
2074
|
+
if (char === '>') {
|
|
2075
|
+
onContextChange?.(html, i+1, this.state.context, HtmlParser.Text);
|
|
2076
|
+
this.state.context = HtmlParser.Text;
|
|
2077
|
+
this.state.quote = null;
|
|
2078
|
+
this.state.buffer = '';
|
|
2079
|
+
}
|
|
2080
|
+
else if (char === ' ' && !this.state.buffer) {
|
|
2081
|
+
// No attribute name is present. Skipping the space.
|
|
2082
|
+
continue;
|
|
2083
|
+
}
|
|
2084
|
+
else if (char === ' ' || char === '/' || char === '?') {
|
|
2085
|
+
this.state.buffer = ''; // Reset the buffer when a delimiter or potential self-closing sign is found.
|
|
2086
|
+
}
|
|
2087
|
+
else if (char === '"' || char === "'" || char === '=') {
|
|
2088
|
+
onContextChange?.(html, i, this.state.context, HtmlParser.Attribute);
|
|
2089
|
+
this.state.context = HtmlParser.Attribute;
|
|
2090
|
+
this.state.quote = char === '=' ? null : char;
|
|
2091
|
+
this.state.buffer = '';
|
|
2092
|
+
}
|
|
2093
|
+
else
|
|
2094
|
+
this.state.buffer += char;
|
|
2095
|
+
break;
|
|
2096
|
+
case HtmlParser.Attribute:
|
|
2097
|
+
// Start an attribute quote.
|
|
2098
|
+
if (!this.state.quote && !this.state.buffer.length && (char === '"' || char === "'")) {
|
|
2099
|
+
this.state.quote = char;
|
|
2100
|
+
}
|
|
2101
|
+
else if (char === this.state.quote || (!this.state.quote && this.state.buffer.length)) {
|
|
2102
|
+
onContextChange?.(html, i, this.state.context, HtmlParser.Tag);
|
|
2103
|
+
this.state.context = HtmlParser.Tag;
|
|
2104
|
+
this.state.quote = null;
|
|
2105
|
+
this.state.buffer = '';
|
|
2106
|
+
}
|
|
2107
|
+
else if (!this.state.quote && char === '>') {
|
|
2108
|
+
onContextChange?.(html, i+1, this.state.context, HtmlParser.Text);
|
|
2109
|
+
this.state.context = HtmlParser.Text;
|
|
2110
|
+
this.state.quote = null;
|
|
2111
|
+
this.state.buffer = '';
|
|
2112
|
+
}
|
|
2113
|
+
else if (char !== ' ')
|
|
2114
|
+
this.state.buffer += char;
|
|
2115
|
+
|
|
2116
|
+
break;
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
onContextChange?.(html, html.length, this.state.context, null);
|
|
2120
|
+
return this.state.context;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
HtmlParser.Attribute = 'Attribute';
|
|
2125
|
+
HtmlParser.Text = 'Text';
|
|
2126
|
+
HtmlParser.Tag = 'Tag';
|
|
2127
|
+
|
|
1784
2128
|
/**
|
|
1785
2129
|
* A Shell is created from a tagged template expression instantiated as Nodes,
|
|
1786
2130
|
* but without any expressions filled in.
|
|
1787
2131
|
* Only one Shell is created for all the items in a loop.
|
|
1788
2132
|
*
|
|
1789
2133
|
* When a NodeGroup is created from a Template's html strings,
|
|
1790
|
-
* the NodeGroup then clones the Shell's
|
|
2134
|
+
* the NodeGroup then clones the Shell's fragment to be its nodes. */
|
|
1791
2135
|
class Shell {
|
|
1792
2136
|
|
|
1793
2137
|
/**
|
|
1794
|
-
* @type {DocumentFragment} DOM parent of the shell nodes. */
|
|
2138
|
+
* @type {DocumentFragment|Text} DOM parent of the shell's nodes. */
|
|
1795
2139
|
fragment;
|
|
1796
2140
|
|
|
1797
2141
|
/** @type {ExprPath[]} Paths to where expressions should go. */
|
|
1798
2142
|
paths = [];
|
|
1799
2143
|
|
|
1800
|
-
//
|
|
1801
|
-
events = [];
|
|
2144
|
+
// Elements with events. Not yet used.
|
|
2145
|
+
// events = [];
|
|
1802
2146
|
|
|
1803
2147
|
/** @type {int[][]} Array of paths */
|
|
1804
2148
|
ids = [];
|
|
2149
|
+
|
|
2150
|
+
/** @type {int[][]} Array of paths */
|
|
1805
2151
|
scripts = [];
|
|
2152
|
+
|
|
2153
|
+
/** @type {int[][]} Array of paths */
|
|
1806
2154
|
styles = [];
|
|
1807
2155
|
|
|
2156
|
+
/** @type {int[][]} Array of paths. Used by activateEmbeds() to quickly find components. */
|
|
1808
2157
|
staticComponents = [];
|
|
1809
2158
|
|
|
2159
|
+
/** @type {{path:int[], attribs:Object<string, string>}[]} */
|
|
2160
|
+
//componentAttribs = [];
|
|
2161
|
+
|
|
1810
2162
|
|
|
1811
2163
|
|
|
1812
2164
|
/**
|
|
1813
2165
|
* Create the nodes but without filling in the expressions.
|
|
1814
2166
|
* This is useful because the expression-less nodes created by a template can be cached.
|
|
1815
|
-
* @param html {string[]} */
|
|
2167
|
+
* @param html {string[]} Html strings, split on places where an expression exists. */
|
|
1816
2168
|
constructor(html=null) {
|
|
1817
2169
|
if (!html)
|
|
1818
2170
|
return;
|
|
1819
2171
|
|
|
1820
2172
|
|
|
1821
2173
|
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
let buffer = [];
|
|
1827
|
-
let commentPlaceholder = `<!--!✨!-->`;
|
|
1828
|
-
let componentNames = {};
|
|
1829
|
-
|
|
1830
|
-
htmlContext(null); // Reset the context.
|
|
1831
|
-
for (let i=0; i<html.length; i++) {
|
|
1832
|
-
let lastHtml = html[i];
|
|
1833
|
-
let context = htmlContext(lastHtml);
|
|
1834
|
-
|
|
1835
|
-
// Swap out Embedded Solarite Components with ${} attributes.
|
|
1836
|
-
// Later, NodeGroup.render() will search for these and replace them with the real components.
|
|
1837
|
-
// Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
|
|
1838
|
-
if (context === htmlContext.Attribute) {
|
|
1839
|
-
|
|
1840
|
-
let lastIndex, lastMatch;
|
|
1841
|
-
lastHtml.replace(/<[a-z][a-z0-9]*-[a-z0-9-]+/ig, (match, index) => {
|
|
1842
|
-
lastIndex = index+1; // +1 for after opening <
|
|
1843
|
-
lastMatch = match.slice(1);
|
|
1844
|
-
});
|
|
1845
|
-
|
|
1846
|
-
if (lastMatch) {
|
|
1847
|
-
let newTagName = lastMatch + '-solarite-placeholder';
|
|
1848
|
-
lastHtml = lastHtml.slice(0, lastIndex) + newTagName + lastHtml.slice(lastIndex + lastMatch.length);
|
|
1849
|
-
componentNames[lastMatch] = newTagName;
|
|
1850
|
-
}
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
buffer.push(lastHtml);
|
|
1854
|
-
//console.log(lastHtml, context)
|
|
1855
|
-
if (i < html.length-1)
|
|
1856
|
-
if (context === htmlContext.Text)
|
|
1857
|
-
buffer.push(commentPlaceholder); // Comment Placeholder. because we can't put text in between <tr> tags for example.
|
|
1858
|
-
else
|
|
1859
|
-
buffer.push(String.fromCharCode(placeholder+i));
|
|
2174
|
+
if (html.length === 1 && !html[0].match(/[<&]/)) {
|
|
2175
|
+
this.fragment = document.createTextNode(html[0]);
|
|
2176
|
+
return;
|
|
1860
2177
|
}
|
|
1861
2178
|
|
|
1862
|
-
// 2. Create elements from html with placeholders.
|
|
1863
|
-
let template = document.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
|
|
1864
|
-
let joinedHtml = buffer.join('');
|
|
1865
2179
|
|
|
1866
|
-
//
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
template.content.append(document.createTextNode(''));
|
|
2180
|
+
// 1. Add placeholders
|
|
2181
|
+
let joinedHtml = Shell.addPlaceholders(html);
|
|
2182
|
+
|
|
2183
|
+
let template = document.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
|
|
2184
|
+
if (joinedHtml)
|
|
2185
|
+
template.innerHTML = joinedHtml;
|
|
2186
|
+
else // Create one text node, so shell isn't empty and NodeGroups created from it have something to point the startNode and endNode at.
|
|
2187
|
+
template.content.append(document.createTextNode(''));
|
|
1875
2188
|
this.fragment = template.content;
|
|
1876
2189
|
|
|
1877
|
-
//
|
|
2190
|
+
// 2. Find placeholders
|
|
1878
2191
|
let node;
|
|
1879
2192
|
let toRemove = [];
|
|
1880
|
-
|
|
2193
|
+
let placeholdersUsed = 0;
|
|
2194
|
+
const walker = document.createTreeWalker(this.fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
|
|
1881
2195
|
while (node = walker.nextNode()) {
|
|
1882
2196
|
|
|
1883
2197
|
// Remove previous after each iteration, so paths will still be calculated correctly.
|
|
1884
2198
|
toRemove.map(el => el.remove());
|
|
1885
2199
|
toRemove = [];
|
|
1886
|
-
|
|
2200
|
+
|
|
1887
2201
|
// Replace attributes
|
|
1888
2202
|
if (node.nodeType === 1) {
|
|
1889
2203
|
for (let attr of [...node.attributes]) { // Copy the attributes array b/c we remove attributes as we go.
|
|
@@ -1891,7 +2205,8 @@ class Shell {
|
|
|
1891
2205
|
// Whole attribute
|
|
1892
2206
|
let matches = attr.name.match(/^[\ue000-\uf8ff]$/);
|
|
1893
2207
|
if (matches) {
|
|
1894
|
-
this.paths.push(new ExprPath(null, node,
|
|
2208
|
+
this.paths.push(new ExprPath(null, node, ExprPathType.AttribMultiple));
|
|
2209
|
+
placeholdersUsed ++;
|
|
1895
2210
|
node.removeAttribute(matches[0]);
|
|
1896
2211
|
}
|
|
1897
2212
|
|
|
@@ -1900,16 +2215,17 @@ class Shell {
|
|
|
1900
2215
|
let parts = attr.value.split(/[\ue000-\uf8ff]/g);
|
|
1901
2216
|
if (parts.length > 1) {
|
|
1902
2217
|
let nonEmptyParts = (parts.length === 2 && !parts[0].length && !parts[1].length) ? null : parts;
|
|
1903
|
-
let type = isEvent(attr.name) ?
|
|
2218
|
+
let type = isEvent(attr.name) ? ExprPathType.Event : ExprPathType.AttribValue;
|
|
1904
2219
|
|
|
1905
2220
|
this.paths.push(new ExprPath(null, node, type, attr.name, nonEmptyParts));
|
|
2221
|
+
placeholdersUsed += parts.length - 1;
|
|
1906
2222
|
node.setAttribute(attr.name, parts.join(''));
|
|
1907
2223
|
}
|
|
1908
2224
|
}
|
|
1909
2225
|
}
|
|
1910
2226
|
}
|
|
1911
2227
|
// Replace comment placeholders
|
|
1912
|
-
else if (node.nodeType ===
|
|
2228
|
+
else if (node.nodeType === 8 && node.nodeValue === '!✨!') {
|
|
1913
2229
|
|
|
1914
2230
|
// Get or create nodeBefore.
|
|
1915
2231
|
let nodeBefore = node.previousSibling; // Can be the same as another Path's nodeMarker.
|
|
@@ -1934,12 +2250,14 @@ class Shell {
|
|
|
1934
2250
|
}
|
|
1935
2251
|
|
|
1936
2252
|
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
let path = new ExprPath(nodeBefore, nodeMarker, PathType.Content);
|
|
1940
|
-
|
|
2253
|
+
let path = new ExprPath(nodeBefore, nodeMarker, ExprPathType.Content);
|
|
1941
2254
|
this.paths.push(path);
|
|
2255
|
+
placeholdersUsed ++;
|
|
1942
2256
|
}
|
|
2257
|
+
|
|
2258
|
+
else if (node.nodeType === 3 && node.parentNode?.tagName === 'TEXTAREA' && node.textContent.includes('<!--!✨!-->'))
|
|
2259
|
+
throw new Error(`Textarea can't have expressions inside them. Use <textarea value="\${...}"> instead.`);
|
|
2260
|
+
|
|
1943
2261
|
|
|
1944
2262
|
|
|
1945
2263
|
// Sometimes users will comment out a block of html code that has expressions.
|
|
@@ -1950,8 +2268,9 @@ class Shell {
|
|
|
1950
2268
|
let parts = node.textContent.split(/[\ue000-\uf8ff]/g);
|
|
1951
2269
|
for (let i=0; i<parts.length-1; i++) {
|
|
1952
2270
|
let path = new ExprPath(node.previousSibling, node);
|
|
1953
|
-
path.type =
|
|
2271
|
+
path.type = ExprPathType.Comment;
|
|
1954
2272
|
this.paths.push(path);
|
|
2273
|
+
placeholdersUsed ++;
|
|
1955
2274
|
}
|
|
1956
2275
|
}
|
|
1957
2276
|
|
|
@@ -1969,8 +2288,9 @@ class Shell {
|
|
|
1969
2288
|
}
|
|
1970
2289
|
|
|
1971
2290
|
for (let i=0, node; node=placeholders[i]; i++) {
|
|
1972
|
-
let path = new ExprPath(node.previousSibling, node,
|
|
2291
|
+
let path = new ExprPath(node.previousSibling, node, ExprPathType.Content);
|
|
1973
2292
|
this.paths.push(path);
|
|
2293
|
+
placeholdersUsed ++;
|
|
1974
2294
|
|
|
1975
2295
|
|
|
1976
2296
|
}
|
|
@@ -1982,17 +2302,17 @@ class Shell {
|
|
|
1982
2302
|
}
|
|
1983
2303
|
toRemove.map(el => el.remove());
|
|
1984
2304
|
|
|
2305
|
+
// Less than or equal because there can be one path to multiple expressions
|
|
2306
|
+
// if those expressions are in the same attribute value.
|
|
2307
|
+
if (placeholdersUsed !== html.length-1)
|
|
2308
|
+
throw new Error(`Could not parse expressions in template. Check for duplicate attributes or malformed html: ${html.join('${...}')}`);
|
|
2309
|
+
|
|
1985
2310
|
// Handle solarite-placeholder's.
|
|
1986
|
-
// Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
|
|
1987
|
-
//if (componentNames.size)
|
|
1988
|
-
// this.components = [...this.fragment.querySelectorAll([...componentNames].join(','))]
|
|
1989
2311
|
|
|
1990
|
-
// Rename "is" attributes so the Web Components don't instantiate until we have the values of their PathExpr arguments.
|
|
2312
|
+
// 3. Rename "is" attributes so the Web Components don't instantiate until we have the values of their PathExpr arguments.
|
|
1991
2313
|
// that happens in NodeGroup.applyComponentExprs()
|
|
1992
|
-
for (let el of this.fragment.querySelectorAll('[is]'))
|
|
2314
|
+
for (let el of this.fragment.querySelectorAll('[is]'))
|
|
1993
2315
|
el.setAttribute('_is', el.getAttribute('is'));
|
|
1994
|
-
// this.components.push(el);
|
|
1995
|
-
}
|
|
1996
2316
|
|
|
1997
2317
|
for (let path of this.paths) {
|
|
1998
2318
|
if (path.nodeBefore)
|
|
@@ -2000,16 +2320,63 @@ class Shell {
|
|
|
2000
2320
|
path.nodeMarkerPath = getNodePath(path.nodeMarker);
|
|
2001
2321
|
|
|
2002
2322
|
// Cache so we don't have to calculate this later inside NodeGroup.applyExprs()
|
|
2003
|
-
if (path.type ===
|
|
2323
|
+
if (path.type === ExprPathType.AttribValue && path.nodeMarker.nodeType === 1 &&
|
|
2004
2324
|
(path.nodeMarker.tagName.includes('-') || path.nodeMarker.hasAttribute('is'))) {
|
|
2005
|
-
path.type =
|
|
2325
|
+
path.type = ExprPathType.ComponentAttribValue;
|
|
2006
2326
|
}
|
|
2007
2327
|
}
|
|
2008
2328
|
|
|
2009
2329
|
this.findEmbeds();
|
|
2010
2330
|
|
|
2011
2331
|
|
|
2012
|
-
}
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
/**
|
|
2335
|
+
* 1. Add a Unicode placeholder char for where expressions go within attributes.
|
|
2336
|
+
* 2. Add a comment placeholder for where expressions are children of other nodes.
|
|
2337
|
+
* 3. Append -solarite-placeholder to the tag names of custom components so that we can wait to instantiate them later.
|
|
2338
|
+
* @param htmlChunks {string[]}
|
|
2339
|
+
* @returns {string} */
|
|
2340
|
+
static addPlaceholders(htmlChunks) {
|
|
2341
|
+
let tokens = [];
|
|
2342
|
+
|
|
2343
|
+
function addToken(token, context) {
|
|
2344
|
+
|
|
2345
|
+
if (context === HtmlParser.Tag) {
|
|
2346
|
+
// Find Solarite Components tags and append -solarite-placeholder to their tag names.
|
|
2347
|
+
// This way we can gather their constructor arguments and their children before we call their constructor.
|
|
2348
|
+
// Later, NodeGroup.createNewComponent() will replace them with the real components.
|
|
2349
|
+
// Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
|
|
2350
|
+
token = token.replace(/^<\/?[a-z][a-z0-9]*-[a-z0-9-]+/i, match => match + '-solarite-placeholder');
|
|
2351
|
+
}
|
|
2352
|
+
tokens.push(token);
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
let htmlParser = new HtmlParser(); // Reset the context.
|
|
2356
|
+
for (let i = 0; i < htmlChunks.length; i++) {
|
|
2357
|
+
let lastHtml = htmlChunks[i];
|
|
2358
|
+
|
|
2359
|
+
// Append -solarite-placholder to web component tags, so we can pass args to them when they're instantiated.
|
|
2360
|
+
let lastIndex = 0;
|
|
2361
|
+
let context = htmlParser.parse(lastHtml, (html, index, oldContext, newContext) => {
|
|
2362
|
+
if (lastIndex !== index) {
|
|
2363
|
+
let token = html.slice(lastIndex, index);
|
|
2364
|
+
addToken(token, oldContext);
|
|
2365
|
+
}
|
|
2366
|
+
lastIndex = index;
|
|
2367
|
+
});
|
|
2368
|
+
|
|
2369
|
+
// Insert placeholders
|
|
2370
|
+
if (i < htmlChunks.length - 1) {
|
|
2371
|
+
if (context === HtmlParser.Text)
|
|
2372
|
+
tokens.push(commentPlaceholder); // Comment Placeholder. because we can't put text in between <tr> tags for example.
|
|
2373
|
+
else
|
|
2374
|
+
tokens.push(String.fromCharCode(attribPlaceholder + i));
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
return tokens.join('');
|
|
2379
|
+
}
|
|
2013
2380
|
|
|
2014
2381
|
/**
|
|
2015
2382
|
* We find the path to every embed here once in the Shell, instead of every time a NodeGroup is instantiated.
|
|
@@ -2021,36 +2388,30 @@ class Shell {
|
|
|
2021
2388
|
* this.staticComponents */
|
|
2022
2389
|
findEmbeds() {
|
|
2023
2390
|
this.scripts = Array.prototype.map.call(this.fragment.querySelectorAll('scripts'), el => getNodePath(el));
|
|
2391
|
+
|
|
2392
|
+
// TODO: only find styles that have ExprPaths in them?
|
|
2024
2393
|
this.styles = Array.prototype.map.call(this.fragment.querySelectorAll('style'), el => getNodePath(el));
|
|
2025
2394
|
|
|
2026
2395
|
let idEls = this.fragment.querySelectorAll('[id],[data-id]');
|
|
2027
|
-
|
|
2028
2396
|
|
|
2029
2397
|
// Check for valid id names.
|
|
2030
2398
|
for (let el of idEls) {
|
|
2031
2399
|
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
2032
|
-
if (div.hasOwnProperty(id))
|
|
2400
|
+
if (Globals$1.div.hasOwnProperty(id))
|
|
2033
2401
|
throw new Error(`<${el.tagName.toLowerCase()} id="${id}"> can't override existing HTMLElement id property.`)
|
|
2034
2402
|
}
|
|
2035
2403
|
|
|
2036
|
-
|
|
2037
2404
|
this.ids = Array.prototype.map.call(idEls, el => getNodePath(el));
|
|
2038
2405
|
|
|
2039
|
-
// Events (not yet used)
|
|
2040
2406
|
for (let el of this.fragment.querySelectorAll('*')) {
|
|
2041
|
-
for (let attrib of el.attributes)
|
|
2042
|
-
if (isEvent(attrib.name))
|
|
2043
|
-
this.events.push([attrib.name, getNodePath(el)]);
|
|
2044
|
-
|
|
2045
2407
|
if (el.tagName.includes('-') || el.hasAttribute('_is'))
|
|
2046
2408
|
|
|
2047
|
-
// Dynamic components have attributes with expression values.
|
|
2409
|
+
// Dynamic components are components that have attributes with expression values.
|
|
2048
2410
|
// They are created from applyExprs()
|
|
2049
2411
|
// But static components are created in a separate path inside the NodeGroup constructor.
|
|
2050
2412
|
if (!this.paths.find(path => path.nodeMarker === el))
|
|
2051
2413
|
this.staticComponents.push(getNodePath(el));
|
|
2052
2414
|
}
|
|
2053
|
-
|
|
2054
2415
|
}
|
|
2055
2416
|
|
|
2056
2417
|
/**
|
|
@@ -2058,10 +2419,10 @@ class Shell {
|
|
|
2058
2419
|
* @param htmlStrings {string[]} Typically comes from a Template.
|
|
2059
2420
|
* @returns {Shell} */
|
|
2060
2421
|
static get(htmlStrings) {
|
|
2061
|
-
let result = Globals.shells.get(htmlStrings);
|
|
2422
|
+
let result = Globals$1.shells.get(htmlStrings);
|
|
2062
2423
|
if (!result) {
|
|
2063
2424
|
result = new Shell(htmlStrings);
|
|
2064
|
-
Globals.shells.set(htmlStrings, result); // cache
|
|
2425
|
+
Globals$1.shells.set(htmlStrings, result); // cache
|
|
2065
2426
|
}
|
|
2066
2427
|
|
|
2067
2428
|
|
|
@@ -2069,11 +2430,18 @@ class Shell {
|
|
|
2069
2430
|
}
|
|
2070
2431
|
|
|
2071
2432
|
|
|
2072
|
-
}
|
|
2073
|
-
|
|
2074
|
-
/** @typedef {boolean|string|number|function|Object|Array|Date|Node|Template} Expr */
|
|
2433
|
+
}
|
|
2075
2434
|
|
|
2076
|
-
|
|
2435
|
+
|
|
2436
|
+
const commentPlaceholder = `<!--!✨!-->`;
|
|
2437
|
+
|
|
2438
|
+
|
|
2439
|
+
// We increment the placeholder char as we go because nodes can't have the same attribute more than once.
|
|
2440
|
+
const attribPlaceholder = 0xe000; // https://en.wikipedia.org/wiki/Private_Use_Areas 6400.
|
|
2441
|
+
|
|
2442
|
+
/** @typedef {boolean|string|number|function|Object|Array|Date|Node|Template} Expr */
|
|
2443
|
+
|
|
2444
|
+
/**
|
|
2077
2445
|
* A group of Nodes instantiated from a Shell, with Expr's filled in.
|
|
2078
2446
|
*
|
|
2079
2447
|
* The range is determined by startNode and nodeMarker.
|
|
@@ -2095,7 +2463,8 @@ class NodeGroup {
|
|
|
2095
2463
|
startNode;
|
|
2096
2464
|
|
|
2097
2465
|
/** @type {Node|HTMLElement} A node that never changes that this NodeGroup should always insert its nodes before.
|
|
2098
|
-
* An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position
|
|
2466
|
+
* An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.
|
|
2467
|
+
* TODO: But sometimes startNode and endNode point to the same node. Document htis inconsistency. */
|
|
2099
2468
|
endNode;
|
|
2100
2469
|
|
|
2101
2470
|
/** @type {ExprPath[]} */
|
|
@@ -2113,11 +2482,11 @@ class NodeGroup {
|
|
|
2113
2482
|
nodesCache;
|
|
2114
2483
|
|
|
2115
2484
|
/**
|
|
2485
|
+
* A map between <style> Elements and their text content.
|
|
2486
|
+
* This lets NodeGroup.updateStyles() see when the style text has changed.
|
|
2116
2487
|
* @type {?Map<HTMLStyleElement, string>} */
|
|
2117
2488
|
styles;
|
|
2118
2489
|
|
|
2119
|
-
currentComponentProps = {};
|
|
2120
|
-
|
|
2121
2490
|
|
|
2122
2491
|
/**
|
|
2123
2492
|
* Create an "instantiated" NodeGroup from a Template and add it to an element.
|
|
@@ -2125,14 +2494,26 @@ class NodeGroup {
|
|
|
2125
2494
|
* @param parentPath {?ExprPath} */
|
|
2126
2495
|
constructor(template, parentPath=null) {
|
|
2127
2496
|
if (!(this instanceof RootNodeGroup)) {
|
|
2497
|
+
|
|
2128
2498
|
let [fragment, shell] = this.init(template, parentPath);
|
|
2129
2499
|
|
|
2130
|
-
|
|
2500
|
+
if (fragment && template.exprs.length) {
|
|
2501
|
+
this.updatePaths(fragment, shell.paths);
|
|
2131
2502
|
|
|
2132
|
-
|
|
2503
|
+
// Static web components can sometimes have children created via expressions.
|
|
2504
|
+
// But calling applyExprs() will mess up the shell's path to them.
|
|
2505
|
+
// So we find them first, then call activateStaticComponents() after their children have been created.
|
|
2506
|
+
let staticComponents = this.findStaticComponents(fragment, shell);
|
|
2133
2507
|
|
|
2134
|
-
|
|
2135
|
-
|
|
2508
|
+
this.activateEmbeds(fragment, shell);
|
|
2509
|
+
|
|
2510
|
+
// Apply exprs
|
|
2511
|
+
this.applyExprs(template.exprs);
|
|
2512
|
+
|
|
2513
|
+
this.activateStaticComponents(staticComponents);
|
|
2514
|
+
}
|
|
2515
|
+
else if (shell)
|
|
2516
|
+
this.activateEmbeds(fragment, shell);
|
|
2136
2517
|
}
|
|
2137
2518
|
}
|
|
2138
2519
|
|
|
@@ -2160,57 +2541,103 @@ class NodeGroup {
|
|
|
2160
2541
|
template.nodeGroup = this;
|
|
2161
2542
|
|
|
2162
2543
|
// Get a cached version of the parsed and instantiated html, and ExprPaths.
|
|
2163
|
-
let shell = Shell.get(template.html);
|
|
2164
|
-
let fragment = shell.fragment.cloneNode(true);
|
|
2165
2544
|
|
|
2166
|
-
|
|
2167
|
-
this.
|
|
2168
|
-
|
|
2545
|
+
// If it's just a text node, skip a bunch of unnecessary steps.
|
|
2546
|
+
if (!(this instanceof RootNodeGroup) && !template.exprs.length && !template.html[0].includes('<')) {
|
|
2547
|
+
//let doc = this.rootNg.startNode?.ownerDocument || document;
|
|
2548
|
+
let textNode = document.createTextNode(template.html[0]);
|
|
2549
|
+
|
|
2550
|
+
this.startNode = this.endNode = textNode;
|
|
2551
|
+
return [];
|
|
2552
|
+
}
|
|
2553
|
+
else {
|
|
2554
|
+
let shell = Shell.get(template.html);
|
|
2555
|
+
let fragment = shell.fragment.cloneNode(true);
|
|
2169
2556
|
|
|
2170
|
-
|
|
2557
|
+
if (fragment instanceof DocumentFragment) {
|
|
2558
|
+
let childNodes = fragment.childNodes;
|
|
2559
|
+
this.startNode = childNodes[0];
|
|
2560
|
+
this.endNode = childNodes[childNodes.length - 1];
|
|
2561
|
+
}
|
|
2562
|
+
else {
|
|
2563
|
+
this.startNode = this.endNode = fragment;
|
|
2564
|
+
}
|
|
2565
|
+
return [fragment, shell];
|
|
2566
|
+
}
|
|
2171
2567
|
}
|
|
2172
2568
|
|
|
2173
2569
|
/**
|
|
2174
2570
|
* Use the paths to insert the given expressions.
|
|
2175
2571
|
* Dispatches expression handling to other functions depending on the path type.
|
|
2176
2572
|
* @param exprs {(*|*[]|function|Template)[]}
|
|
2177
|
-
* @param paths {?ExprPath[]} Optional. */
|
|
2573
|
+
* @param paths {?ExprPath[]} Optional. Only used for testing. Normally uses this.paths. */
|
|
2178
2574
|
applyExprs(exprs, paths=null) {
|
|
2179
2575
|
paths = paths || this.paths;
|
|
2180
2576
|
|
|
2181
2577
|
|
|
2182
2578
|
|
|
2183
|
-
//
|
|
2184
|
-
|
|
2579
|
+
// Things to consider:
|
|
2580
|
+
// 1. One path may use multipe expressions. E.g. <div class="${1} ${2}">
|
|
2581
|
+
// 2. One component may need to use multiple attribute paths to be instantiated.
|
|
2582
|
+
// 3. We apply them in reverse order so that a <select> box has its children created from an expression
|
|
2583
|
+
// before its instantiated and its value attribute is set via an expression.
|
|
2584
|
+
|
|
2585
|
+
let exprIndex = exprs.length - 1; // Update exprs at paths.
|
|
2586
|
+
let lastComponentPathIndex;
|
|
2587
|
+
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.
|
|
2588
|
+
for (let i = paths.length - 1, path; path = paths[i]; i--) {
|
|
2589
|
+
let prevPath = paths[i - 1];
|
|
2590
|
+
let nextPath = paths[i + 1];
|
|
2591
|
+
|
|
2592
|
+
// Get the expressions associated with this path.
|
|
2593
|
+
if (path.attrValue?.length > 2) {
|
|
2594
|
+
let startIndex = (exprIndex - (path.attrValue.length - 1)) + 1;
|
|
2595
|
+
pathExprs[i] = exprs.slice(startIndex, exprIndex + 1); // probably doesn't allocate if the JS vm implements copy on write.
|
|
2596
|
+
exprIndex -= pathExprs[i].length;
|
|
2597
|
+
} else {
|
|
2598
|
+
pathExprs[i] = [exprs[exprIndex]];
|
|
2599
|
+
exprIndex--;
|
|
2600
|
+
}
|
|
2185
2601
|
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
expr = exprs[exprIndex];
|
|
2602
|
+
// TODO: Need to end and restart this block when going from one component to the next?
|
|
2603
|
+
// Think of having two adjacent components.
|
|
2604
|
+
// But the dynamicAttribsAdjacet test already passes.
|
|
2190
2605
|
|
|
2191
|
-
//
|
|
2606
|
+
// If a component:
|
|
2607
|
+
// 1. Instantiate it if it hasn't already been, sending all expr's to its constructor.
|
|
2608
|
+
// 2. Otherwise send them to its render function.
|
|
2609
|
+
// Components with no expressions as attributes are instead activated in activateEmbeds().
|
|
2610
|
+
if (path.nodeMarker !== this.rootNg.root && path.isComponent()) {
|
|
2192
2611
|
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
this.currentComponentProps = {};
|
|
2197
|
-
}
|
|
2612
|
+
if (!nextPath || !nextPath.isComponent() || nextPath.nodeMarker !== path.nodeMarker)
|
|
2613
|
+
lastComponentPathIndex = i;
|
|
2614
|
+
let isFirstComponentPath = !prevPath || !prevPath.isComponent() || prevPath.nodeMarker !== path.nodeMarker;
|
|
2198
2615
|
|
|
2199
|
-
|
|
2616
|
+
if (isFirstComponentPath) {
|
|
2200
2617
|
|
|
2201
|
-
|
|
2618
|
+
let componentProps = {};
|
|
2619
|
+
for (let j=i; j<=lastComponentPathIndex; j++) {
|
|
2620
|
+
let attrName = paths[j].attrName; // Util.dashesToCamel(paths[j].attrName);
|
|
2621
|
+
componentProps[attrName] = pathExprs[j].length > 1 ? pathExprs[j].join('') : pathExprs[j][0];
|
|
2622
|
+
}
|
|
2202
2623
|
|
|
2624
|
+
this.applyComponentExprs(path.nodeMarker, componentProps);
|
|
2203
2625
|
|
|
2204
|
-
|
|
2205
|
-
|
|
2626
|
+
// Set attributes on component.
|
|
2627
|
+
for (let j=i; j<=lastComponentPathIndex; j++)
|
|
2628
|
+
paths[j].apply(pathExprs[j]);
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2206
2631
|
|
|
2632
|
+
// Else apply it normally
|
|
2633
|
+
else
|
|
2634
|
+
path.apply(pathExprs[i]);
|
|
2207
2635
|
|
|
2208
|
-
// Check again after we iterate through all paths to apply to a component.
|
|
2209
|
-
if (lastNode && lastNode !== this.rootNg.root && Object.keys(this.currentComponentProps).length) {
|
|
2210
|
-
this.applyComponentExprs(lastNode, this.currentComponentProps);
|
|
2211
|
-
this.currentComponentProps = {};
|
|
2212
|
-
}
|
|
2213
2636
|
|
|
2637
|
+
} // end for(path of this.paths)
|
|
2638
|
+
|
|
2639
|
+
|
|
2640
|
+
// TODO: Only do this if we have ExprPaths within styles?
|
|
2214
2641
|
this.updateStyles();
|
|
2215
2642
|
|
|
2216
2643
|
// Invalidate the nodes cache because we just changed it.
|
|
@@ -2245,14 +2672,18 @@ class NodeGroup {
|
|
|
2245
2672
|
|
|
2246
2673
|
// Call render() with the same params that would've been passed to the constructor.
|
|
2247
2674
|
else if (el.render) {
|
|
2248
|
-
let oldHash = Globals.
|
|
2249
|
-
if (oldHash !== newHash)
|
|
2250
|
-
|
|
2675
|
+
let oldHash = Globals$1.componentArgsHash.get(el);
|
|
2676
|
+
if (oldHash !== newHash) {
|
|
2677
|
+
let args = {};
|
|
2678
|
+
for (let name in props || {})
|
|
2679
|
+
args[Util.dashesToCamel(name)] = props[name];
|
|
2680
|
+
el.render(args); // Pass new values of props to render so it can decide how it wants to respond.
|
|
2681
|
+
}
|
|
2251
2682
|
}
|
|
2252
2683
|
|
|
2253
|
-
Globals.
|
|
2684
|
+
Globals$1.componentArgsHash.set(el, newHash);
|
|
2254
2685
|
}
|
|
2255
|
-
|
|
2686
|
+
|
|
2256
2687
|
/**
|
|
2257
2688
|
* We swap the placeholder element for the real element so we can pass its dynamic attributes
|
|
2258
2689
|
* to its constructor.
|
|
@@ -2266,72 +2697,48 @@ class NodeGroup {
|
|
|
2266
2697
|
createNewComponent(el, isPreHtmlElement=undefined, props=undefined) {
|
|
2267
2698
|
if (isPreHtmlElement === undefined)
|
|
2268
2699
|
isPreHtmlElement = !el.hasAttribute('_is');
|
|
2269
|
-
|
|
2700
|
+
|
|
2270
2701
|
let tagName = (isPreHtmlElement
|
|
2271
|
-
? el.tagName.
|
|
2272
|
-
? el.tagName.slice(0, -21)
|
|
2273
|
-
: el.tagName
|
|
2702
|
+
? el.tagName.slice(0, -21) // Remove -SOLARITE-PLACEHOLDER
|
|
2274
2703
|
: el.getAttribute('is')).toLowerCase();
|
|
2275
2704
|
|
|
2276
|
-
|
|
2277
|
-
|
|
2705
|
+
|
|
2706
|
+
// Throw if custom element isn't defined.
|
|
2707
|
+
let Constructor = customElements.get(tagName);
|
|
2708
|
+
if (!Constructor)
|
|
2709
|
+
throw new Error(`The custom tag name ${tagName} is not registered.`)
|
|
2710
|
+
|
|
2711
|
+
let args = {};
|
|
2712
|
+
for (let name in props || {})
|
|
2713
|
+
args[Util.dashesToCamel(name)] = props[name];
|
|
2714
|
+
|
|
2278
2715
|
// Pass other attribs to constructor, since otherwise they're not yet set on the element,
|
|
2279
2716
|
// and the constructor would otherwise have no way to see them.
|
|
2280
2717
|
if (el.attributes.length) {
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2718
|
+
for (let attrib of el.attributes) {
|
|
2719
|
+
let attribName = Util.dashesToCamel(attrib.name);
|
|
2720
|
+
if (!args.hasOwnProperty(attribName))
|
|
2721
|
+
args[attribName] = attrib.value;
|
|
2722
|
+
}
|
|
2286
2723
|
}
|
|
2287
|
-
|
|
2288
|
-
// Create CustomElement and
|
|
2289
|
-
let Constructor = customElements.get(tagName);
|
|
2290
|
-
if (!Constructor)
|
|
2291
|
-
throw new Error(`The custom tag name ${tagName} is not registered.`)
|
|
2292
2724
|
|
|
2293
|
-
//
|
|
2294
|
-
//
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
// can add them as children before the rest of the constructor code executes.
|
|
2298
|
-
let ch = [... el.childNodes];
|
|
2299
|
-
Globals.pendingChildren.push(ch); // pop() is called in Solarite constructor.
|
|
2300
|
-
let newEl = new Constructor(props, ch);
|
|
2725
|
+
// Create the web component.
|
|
2726
|
+
// Get the children that aren't Solarite's comment placeholders.
|
|
2727
|
+
let ch = [...el.childNodes].filter(node => node.nodeType !== Node.COMMENT_NODE || !node.nodeValue.startsWith('ExprPath'));
|
|
2728
|
+
let newEl = new Constructor(args, ch);
|
|
2301
2729
|
|
|
2302
2730
|
if (!isPreHtmlElement)
|
|
2303
2731
|
newEl.setAttribute('is', el.getAttribute('is').toLowerCase());
|
|
2732
|
+
|
|
2733
|
+
// Replace the placeholder tag with the instantiated web component.
|
|
2304
2734
|
el.replaceWith(newEl);
|
|
2305
2735
|
|
|
2306
|
-
// Set children / slot children
|
|
2307
|
-
// TODO: Match named slots.
|
|
2308
|
-
// TODO: This only appends to slot if render() is called in the constructor.
|
|
2309
|
-
//let slot = newEl.querySelector('slot') || newEl;
|
|
2310
|
-
//slot.append(...el.childNodes);
|
|
2311
|
-
|
|
2312
|
-
// Copy over event attributes.
|
|
2313
|
-
for (let propName in props) {
|
|
2314
|
-
let val = props[propName];
|
|
2315
|
-
if (propName.startsWith('on') && typeof val === 'function')
|
|
2316
|
-
newEl.addEventListener(propName.slice(2), e => val(e, newEl));
|
|
2317
|
-
|
|
2318
|
-
// Bind array based event attributes on value.
|
|
2319
|
-
// This same logic is in ExprPath.applyValueAttrib() for non-components.
|
|
2320
|
-
if ((propName === 'value' || propName === 'data-value') && Util.isPath(val)) {
|
|
2321
|
-
let [obj, path] = [val[0], val.slice(1)];
|
|
2322
|
-
newEl.value = delve(obj, path);
|
|
2323
|
-
newEl.addEventListener('input', e => {
|
|
2324
|
-
delve(obj, path, Util.getInputValue(newEl));
|
|
2325
|
-
}, true); // We use capture so we update the values before other events added by the user.
|
|
2326
|
-
}
|
|
2327
|
-
}
|
|
2328
|
-
|
|
2329
2736
|
// If an id pointed at the placeholder, update it to point to the new element.
|
|
2330
2737
|
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
2331
2738
|
if (id)
|
|
2332
2739
|
delve(this.getRootNode(), id.split(/\./g), newEl);
|
|
2333
|
-
|
|
2334
|
-
|
|
2740
|
+
|
|
2741
|
+
|
|
2335
2742
|
// Update paths to use replaced element.
|
|
2336
2743
|
for (let path of this.paths) {
|
|
2337
2744
|
if (path.nodeMarker === el)
|
|
@@ -2343,31 +2750,31 @@ class NodeGroup {
|
|
|
2343
2750
|
this.startNode = newEl;
|
|
2344
2751
|
if (this.endNode === el)
|
|
2345
2752
|
this.endNode = newEl;
|
|
2346
|
-
|
|
2347
|
-
|
|
2753
|
+
|
|
2754
|
+
|
|
2348
2755
|
// applyComponentExprs() is called because we're rendering.
|
|
2349
2756
|
// So we want to render the sub-component also.
|
|
2350
2757
|
if (newEl.renderFirstTime)
|
|
2351
2758
|
newEl.renderFirstTime();
|
|
2352
|
-
|
|
2759
|
+
|
|
2353
2760
|
// Copy attributes over.
|
|
2354
2761
|
for (let attrib of el.attributes)
|
|
2355
2762
|
if (attrib.name !== '_is')
|
|
2356
2763
|
newEl.setAttribute(attrib.name, attrib.value);
|
|
2357
2764
|
|
|
2358
2765
|
// Set dynamic attributes if they are primitive types.
|
|
2359
|
-
for (let name in
|
|
2360
|
-
let val =
|
|
2766
|
+
for (let name in props) {
|
|
2767
|
+
let val = props[name];
|
|
2361
2768
|
if (typeof val === 'boolean') {
|
|
2362
2769
|
if (val !== false && val !== undefined && val !== null)
|
|
2363
2770
|
newEl.setAttribute(name, '');
|
|
2364
2771
|
}
|
|
2365
2772
|
|
|
2366
|
-
// If type
|
|
2773
|
+
// If type is a non-boolean primitive, set the attribute value.
|
|
2367
2774
|
else if (['number', 'bigint', 'string'].includes(typeof val))
|
|
2368
2775
|
newEl.setAttribute(name, val);
|
|
2369
2776
|
}
|
|
2370
|
-
|
|
2777
|
+
|
|
2371
2778
|
return newEl;
|
|
2372
2779
|
}
|
|
2373
2780
|
|
|
@@ -2413,8 +2820,7 @@ class NodeGroup {
|
|
|
2413
2820
|
|
|
2414
2821
|
/**
|
|
2415
2822
|
* Requires the nodeCache to be present. */
|
|
2416
|
-
|
|
2417
|
-
|
|
2823
|
+
removeAndSaveOrphans() {
|
|
2418
2824
|
|
|
2419
2825
|
let fragment = document.createDocumentFragment();
|
|
2420
2826
|
for (let node of this.getNodes())
|
|
@@ -2424,8 +2830,9 @@ class NodeGroup {
|
|
|
2424
2830
|
|
|
2425
2831
|
updatePaths(fragment, paths, offset) {
|
|
2426
2832
|
// Update paths to point to the fragment.
|
|
2427
|
-
|
|
2428
|
-
|
|
2833
|
+
let pathLength = paths.length;
|
|
2834
|
+
this.paths.length = pathLength;
|
|
2835
|
+
for (let i=0; i<pathLength; i++) {
|
|
2429
2836
|
let path = paths[i].clone(fragment, offset);
|
|
2430
2837
|
path.parentNg = this;
|
|
2431
2838
|
this.paths[i] = path;
|
|
@@ -2443,61 +2850,70 @@ class NodeGroup {
|
|
|
2443
2850
|
|
|
2444
2851
|
|
|
2445
2852
|
|
|
2853
|
+
findStaticComponents(root, shell, pathOffset=0) {
|
|
2854
|
+
let result = [];
|
|
2446
2855
|
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
activateEmbeds(root, shell, pathOffset=0) {
|
|
2452
|
-
|
|
2453
|
-
// static components. These are WebComponents not created by an expression.
|
|
2454
|
-
// Must happen before ids.
|
|
2856
|
+
// static components. These are WebComponents that do not have any constructor arguments that are expressions.
|
|
2857
|
+
// Those are instead created by applyExpr() which calls applyComponentExprs() which calls createNewcomponent().
|
|
2858
|
+
// Maybe someday these two paths will be merged?
|
|
2859
|
+
// Must happen before ids because createNewComponent will replace the element.
|
|
2455
2860
|
for (let path of shell.staticComponents) {
|
|
2456
2861
|
if (pathOffset)
|
|
2457
2862
|
path = path.slice(0, -pathOffset);
|
|
2458
2863
|
let el = resolveNodePath(root, path);
|
|
2459
2864
|
|
|
2460
2865
|
// Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
|
|
2866
|
+
// Recreating it is necessary so we can pass the constructor args to it.
|
|
2461
2867
|
if (root !== el/* && !isReplaceEl(root, el)*/) // TODO: is isReplaceEl necessary?
|
|
2462
|
-
|
|
2868
|
+
result.push(el);
|
|
2463
2869
|
}
|
|
2870
|
+
return result;
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
activateStaticComponents(staticComponents) {
|
|
2874
|
+
for (let el of staticComponents)
|
|
2875
|
+
this.createNewComponent(el);
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
/**
|
|
2879
|
+
* @param root {HTMLElement}
|
|
2880
|
+
* @param shell {Shell}
|
|
2881
|
+
* @param pathOffset {int} */
|
|
2882
|
+
activateEmbeds(root, shell, pathOffset=0) {
|
|
2464
2883
|
|
|
2465
2884
|
let rootEl = this.rootNg.root;
|
|
2466
2885
|
if (rootEl) {
|
|
2886
|
+
let options = this.rootNg.options;
|
|
2467
2887
|
|
|
2468
2888
|
// ids
|
|
2469
|
-
if (
|
|
2889
|
+
if (options?.ids !== false) {
|
|
2470
2890
|
for (let path of shell.ids) {
|
|
2471
2891
|
if (pathOffset)
|
|
2472
2892
|
path = path.slice(0, -pathOffset);
|
|
2473
2893
|
let el = resolveNodePath(root, path);
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
// Don't allow overwriting existing class properties if they already have a non-Node value.
|
|
2478
|
-
if (rootEl[id] && !(rootEl[id] instanceof Node))
|
|
2479
|
-
throw new Error(`${rootEl.constructor.name}.${id} already has a value. ` +
|
|
2480
|
-
`Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
|
|
2481
|
-
|
|
2482
|
-
delve(rootEl, id.split(/\./g), el);
|
|
2483
|
-
}
|
|
2894
|
+
Util.bindId(rootEl, el);
|
|
2895
|
+
}
|
|
2484
2896
|
}
|
|
2485
2897
|
|
|
2486
2898
|
// styles
|
|
2487
|
-
if (
|
|
2899
|
+
if (options?.styles !== false) {
|
|
2488
2900
|
if (shell.styles.length)
|
|
2489
2901
|
this.styles = new Map();
|
|
2490
2902
|
for (let path of shell.styles) {
|
|
2491
2903
|
if (pathOffset)
|
|
2492
2904
|
path = path.slice(0, -pathOffset);
|
|
2905
|
+
|
|
2906
|
+
/** @type {HTMLStyleElement} */
|
|
2493
2907
|
let style = resolveNodePath(root, path);
|
|
2494
|
-
|
|
2495
|
-
|
|
2908
|
+
if (rootEl.nodeType === 1) {
|
|
2909
|
+
Util.bindStyles(style, rootEl);
|
|
2910
|
+
this.styles.set(style, style.textContent);
|
|
2911
|
+
}
|
|
2496
2912
|
}
|
|
2497
2913
|
|
|
2498
2914
|
}
|
|
2499
2915
|
// scripts
|
|
2500
|
-
if (
|
|
2916
|
+
if (options?.scripts !== false) {
|
|
2501
2917
|
for (let path of shell.scripts) {
|
|
2502
2918
|
if (pathOffset)
|
|
2503
2919
|
path = path.slice(0, -pathOffset);
|
|
@@ -2518,20 +2934,8 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2518
2934
|
root;
|
|
2519
2935
|
|
|
2520
2936
|
/**
|
|
2521
|
-
*
|
|
2522
|
-
*
|
|
2523
|
-
* @type {Object<field:string, Set<ExprPath>>} */
|
|
2524
|
-
watchedExprPaths = {};
|
|
2525
|
-
|
|
2526
|
-
/**
|
|
2527
|
-
* Map from arrays where .map is called and their callback functions.
|
|
2528
|
-
* TODO: One array might be called with two different map functions in different places!
|
|
2529
|
-
* @type {Map<Array, function>} */
|
|
2530
|
-
mapCallbacks = new Map();
|
|
2531
|
-
|
|
2532
|
-
/**
|
|
2533
|
-
*
|
|
2534
|
-
* @type {Map<ExprPath, boolean|Array>} */
|
|
2937
|
+
* When we call renerWatched() we re-render these expressions, then clear this to a new Map()
|
|
2938
|
+
* @type {Map<ExprPath, ValueOp|WholeArrayOp|ArraySpliceOp[]>} */
|
|
2535
2939
|
exprsToRender = new Map();
|
|
2536
2940
|
|
|
2537
2941
|
/**
|
|
@@ -2548,82 +2952,102 @@ class RootNodeGroup extends NodeGroup {
|
|
|
2548
2952
|
this.rootNg = this;
|
|
2549
2953
|
let [fragment, shell] = this.init(template);
|
|
2550
2954
|
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
if (el.childNodes.length) {
|
|
2560
|
-
slotFragment = document.createDocumentFragment();
|
|
2561
|
-
slotFragment.append(...el.childNodes);
|
|
2955
|
+
if (fragment instanceof Text) {
|
|
2956
|
+
|
|
2957
|
+
if (el) {
|
|
2958
|
+
this.startNode = el;
|
|
2959
|
+
this.endNode = el;
|
|
2960
|
+
if (fragment.nodeValue.length)
|
|
2961
|
+
el.append(fragment);
|
|
2962
|
+
this.root = el;
|
|
2562
2963
|
}
|
|
2964
|
+
Globals$1.nodeGroups.set(this.root, this);
|
|
2965
|
+
}
|
|
2966
|
+
else {
|
|
2563
2967
|
|
|
2564
|
-
|
|
2968
|
+
// If adding NodeGroup to an element.
|
|
2969
|
+
let offset = 0;
|
|
2970
|
+
let root = fragment; // TODO: Rename so it's not confused with this.root.
|
|
2971
|
+
if (el) {
|
|
2972
|
+
Globals$1.nodeGroups.set(el, this);
|
|
2973
|
+
|
|
2974
|
+
// Save slot children
|
|
2975
|
+
let slotChildren;
|
|
2976
|
+
if (el.childNodes.length) {
|
|
2977
|
+
slotChildren = document.createDocumentFragment();
|
|
2978
|
+
slotChildren.append(...el.childNodes);
|
|
2979
|
+
}
|
|
2565
2980
|
|
|
2566
|
-
|
|
2567
|
-
if (isReplaceEl(fragment, el)) {
|
|
2568
|
-
el.append(...fragment.children[0].childNodes);
|
|
2981
|
+
this.root = el;
|
|
2569
2982
|
|
|
2570
|
-
//
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
el.setAttribute(attrib.name, attrib.value);
|
|
2983
|
+
// If el should replace the root node of the fragment.
|
|
2984
|
+
if (isReplaceEl(fragment, el)) {
|
|
2985
|
+
el.append(...fragment.children[0].childNodes);
|
|
2574
2986
|
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2987
|
+
// Copy attributes
|
|
2988
|
+
for (let attrib of fragment.children[0].attributes)
|
|
2989
|
+
if (!el.hasAttribute(attrib.name))
|
|
2990
|
+
el.setAttribute(attrib.name, attrib.value);
|
|
2991
|
+
|
|
2992
|
+
// Go one level deeper into all of shell's paths.
|
|
2993
|
+
offset = 1;
|
|
2994
|
+
} else {
|
|
2995
|
+
let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
|
|
2996
|
+
if (!isEmpty)
|
|
2997
|
+
el.append(...fragment.childNodes);
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
// Setup children
|
|
3001
|
+
if (slotChildren) {
|
|
2583
3002
|
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
3003
|
+
// Named slots
|
|
3004
|
+
for (let slot of el.querySelectorAll('slot[name]')) {
|
|
3005
|
+
let name = slot.getAttribute('name');
|
|
3006
|
+
if (name) {
|
|
3007
|
+
let slotChildren2 = slotChildren.querySelectorAll(`[slot='${name}']`);
|
|
3008
|
+
slot.append(...slotChildren2);
|
|
3009
|
+
}
|
|
2591
3010
|
}
|
|
3011
|
+
|
|
3012
|
+
// Unnamed slots
|
|
3013
|
+
let unamedSlot = el.querySelector('slot:not([name])');
|
|
3014
|
+
if (unamedSlot)
|
|
3015
|
+
unamedSlot.append(slotChildren);
|
|
3016
|
+
|
|
3017
|
+
// No slots
|
|
3018
|
+
else
|
|
3019
|
+
el.append(slotChildren);
|
|
2592
3020
|
}
|
|
2593
|
-
let unamedSlot = el.querySelector('slot:not([name])');
|
|
2594
|
-
if (unamedSlot)
|
|
2595
|
-
unamedSlot.append(slotFragment);
|
|
2596
|
-
else
|
|
2597
|
-
el.append(slotFragment);
|
|
2598
|
-
}
|
|
2599
3021
|
|
|
2600
|
-
|
|
3022
|
+
root = el;
|
|
2601
3023
|
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
|
|
3024
|
+
this.startNode = el;
|
|
3025
|
+
this.endNode = el;
|
|
3026
|
+
} else {
|
|
3027
|
+
let singleEl = getSingleEl(fragment);
|
|
3028
|
+
this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
|
|
2608
3029
|
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
3030
|
+
Globals$1.nodeGroups.set(this.root, this);
|
|
3031
|
+
if (singleEl) {
|
|
3032
|
+
root = singleEl;
|
|
3033
|
+
offset = 1;
|
|
3034
|
+
}
|
|
2613
3035
|
}
|
|
2614
|
-
}
|
|
2615
3036
|
|
|
2616
|
-
|
|
3037
|
+
this.updatePaths(root, shell.paths, offset);
|
|
2617
3038
|
|
|
2618
|
-
|
|
3039
|
+
// Static web components can sometimes have children created via expressions.
|
|
3040
|
+
// But calling applyExprs() will mess up the shell's path to them.
|
|
3041
|
+
// So we find them first, then call activateStaticComponents() after their children have been created.
|
|
3042
|
+
let staticComponents = this.findStaticComponents(root, shell, offset);
|
|
2619
3043
|
|
|
2620
|
-
|
|
2621
|
-
this.applyExprs(template.exprs);
|
|
2622
|
-
}
|
|
3044
|
+
this.activateEmbeds(root, shell, offset);
|
|
2623
3045
|
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
3046
|
+
// Apply exprs
|
|
3047
|
+
this.applyExprs(template.exprs);
|
|
3048
|
+
|
|
3049
|
+
this.activateStaticComponents(staticComponents);
|
|
3050
|
+
}
|
|
2627
3051
|
}
|
|
2628
3052
|
}
|
|
2629
3053
|
|
|
@@ -2645,8 +3069,8 @@ function getSingleEl(fragment) {
|
|
|
2645
3069
|
* @param el {HTMLElement}
|
|
2646
3070
|
* @returns {boolean} */
|
|
2647
3071
|
function isReplaceEl(fragment, el) {
|
|
2648
|
-
return
|
|
2649
|
-
&&
|
|
3072
|
+
return fragment.children.length===1
|
|
3073
|
+
&& el.tagName.includes('-')
|
|
2650
3074
|
&& fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') === el.tagName;
|
|
2651
3075
|
}
|
|
2652
3076
|
|
|
@@ -2665,19 +3089,9 @@ class Template {
|
|
|
2665
3089
|
/** @type {Array} Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
|
|
2666
3090
|
hashedFields;
|
|
2667
3091
|
|
|
2668
|
-
/**
|
|
2669
|
-
* @deprecated
|
|
2670
|
-
* @type {ExprPath} Used with forEach() from watch.js
|
|
2671
|
-
* Set in ExprPath.apply() */
|
|
2672
|
-
parentPath;
|
|
2673
|
-
|
|
2674
3092
|
/** @type {NodeGroup} */
|
|
2675
3093
|
nodeGroup;
|
|
2676
3094
|
|
|
2677
|
-
/**
|
|
2678
|
-
* @type {string[][]} */
|
|
2679
|
-
paths = [];
|
|
2680
|
-
|
|
2681
3095
|
/**
|
|
2682
3096
|
*
|
|
2683
3097
|
* @param htmlStrings {string[]}
|
|
@@ -2719,17 +3133,21 @@ class Template {
|
|
|
2719
3133
|
if (standalone) {
|
|
2720
3134
|
ng = new RootNodeGroup(this, null, options);
|
|
2721
3135
|
el = ng.getRootNode();
|
|
2722
|
-
|
|
3136
|
+
Globals$1.nodeGroups.set(el, ng); // Why was this commented out?
|
|
2723
3137
|
firstTime = true;
|
|
2724
3138
|
}
|
|
2725
3139
|
else {
|
|
2726
|
-
ng = Globals.nodeGroups.get(el);
|
|
3140
|
+
ng = Globals$1.nodeGroups.get(el);
|
|
2727
3141
|
if (!ng) {
|
|
2728
3142
|
ng = new RootNodeGroup(this, el, options);
|
|
2729
|
-
|
|
3143
|
+
Globals$1.nodeGroups.set(el, ng); // Why was this commented out?
|
|
2730
3144
|
firstTime = true;
|
|
2731
3145
|
}
|
|
2732
|
-
|
|
3146
|
+
|
|
3147
|
+
// This can happen if we try manually rendering one template to a NodeGroup that was created expecting a different template.
|
|
3148
|
+
// These don't always have the same length, for example if one attribute has multiple expressions.
|
|
3149
|
+
if (ng.paths.length === 0 && this.exprs.length || ng.paths.length > this.exprs.length)
|
|
3150
|
+
throw new Error(`Solarite Error: Parent HTMLElement ${ng.template.html.join('${...}')} and ${ng.paths.length} \${value} placeholders can't accomodate a Template with ${this.exprs.length} values.`); }
|
|
2733
3151
|
|
|
2734
3152
|
// Creating the root nodegroup also renders it.
|
|
2735
3153
|
// If we didn't just create it, we need to render it.
|
|
@@ -2737,23 +3155,32 @@ class Template {
|
|
|
2737
3155
|
if (this.html?.length === 1 && !this.html[0])
|
|
2738
3156
|
el.innerHTML = ''; // Fast path for empty component.
|
|
2739
3157
|
else {
|
|
2740
|
-
ng.clearRenderWatched();
|
|
2741
3158
|
ng.applyExprs(this.exprs);
|
|
2742
3159
|
}
|
|
2743
3160
|
}
|
|
2744
3161
|
|
|
3162
|
+
ng.exprsToRender = new Map();
|
|
2745
3163
|
return el;
|
|
2746
3164
|
}
|
|
2747
3165
|
|
|
2748
3166
|
getExactKey() {
|
|
2749
|
-
if (!this.exactKey)
|
|
2750
|
-
|
|
3167
|
+
if (!this.exactKey) {
|
|
3168
|
+
if (this.exprs.length)
|
|
3169
|
+
this.exactKey = getObjectHash(this);// calls this.toJSON().
|
|
3170
|
+
else // Don't hash plain html.
|
|
3171
|
+
this.exactKey = this.html[0];
|
|
3172
|
+
}
|
|
2751
3173
|
return this.exactKey;
|
|
2752
3174
|
}
|
|
2753
3175
|
|
|
2754
3176
|
getCloseKey() {
|
|
2755
|
-
|
|
2756
|
-
|
|
3177
|
+
//console.log(this.exprs.length)
|
|
3178
|
+
if (!this.closeKey) {
|
|
3179
|
+
if (this.exprs.length)
|
|
3180
|
+
this.closeKey = /*'@' + */this.toJSON()[0];
|
|
3181
|
+
else
|
|
3182
|
+
this.closeKey = this.html[0];
|
|
3183
|
+
}
|
|
2757
3184
|
// Use the joined html when debugging? But it breaks some tests.
|
|
2758
3185
|
//return '@'+this.html.join('|')
|
|
2759
3186
|
|
|
@@ -2776,8 +3203,8 @@ class Template {
|
|
|
2776
3203
|
|
|
2777
3204
|
/**
|
|
2778
3205
|
* Convert strings to HTMLNodes.
|
|
2779
|
-
* Using
|
|
2780
|
-
* Using
|
|
3206
|
+
* Using h`...` as a tag will always create a Template.
|
|
3207
|
+
* Using h() as a function() will always create a DOM element.
|
|
2781
3208
|
*
|
|
2782
3209
|
* Features beyond what standard js tagged template strings do:
|
|
2783
3210
|
* 1. r`` sub-expressions
|
|
@@ -2787,24 +3214,25 @@ class Template {
|
|
|
2787
3214
|
* 5. TODO: list more
|
|
2788
3215
|
*
|
|
2789
3216
|
* Currently supported:
|
|
2790
|
-
* 1.
|
|
2791
|
-
* 2.
|
|
3217
|
+
* 1. h(el, options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
|
|
3218
|
+
* 2. h(el, template, ?options) // Render the Template created by #1 to element.
|
|
2792
3219
|
*
|
|
2793
|
-
* 3.
|
|
3220
|
+
* 3. h`<b>Hello</b> ${'World'}!` // Create Template that can later be used to create nodes.
|
|
2794
3221
|
*
|
|
2795
|
-
* 4.
|
|
2796
|
-
* 5.
|
|
2797
|
-
* 6.
|
|
2798
|
-
* 7.
|
|
3222
|
+
* 4. h('Hello'); // Create single text node.
|
|
3223
|
+
* 5. h('<b>Hello</b>'); // Create single HTMLElement
|
|
3224
|
+
* 6. h('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
|
|
3225
|
+
* 7. h()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which
|
|
2799
3226
|
* // includes properly handling nested components and r`` sub-expressions.
|
|
2800
|
-
* 8.
|
|
2801
|
-
*
|
|
2802
|
-
* 9. r({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
|
|
3227
|
+
* 8. h(template) // Render Template created by #1.
|
|
2803
3228
|
*
|
|
3229
|
+
* 9. h({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
|
|
3230
|
+
* 10. h(string, object, ...) // JSX TODO
|
|
2804
3231
|
* @param htmlStrings {?HTMLElement|string|string[]|function():Template|{render:function()}}
|
|
2805
3232
|
* @param exprs {*[]|string|Template|Object}
|
|
2806
3233
|
* @return {Node|HTMLElement|Template} */
|
|
2807
|
-
function
|
|
3234
|
+
function h(htmlStrings=undefined, ...exprs) {
|
|
3235
|
+
|
|
2808
3236
|
|
|
2809
3237
|
// TODO: Make this a more flat if/else and call other functions for the logic.
|
|
2810
3238
|
if (htmlStrings instanceof Node) {
|
|
@@ -2819,7 +3247,7 @@ function r(htmlStrings=undefined, ...exprs) {
|
|
|
2819
3247
|
|
|
2820
3248
|
// Return a tagged template function that applies the tagged themplate to parent.
|
|
2821
3249
|
let taggedTemplate = (htmlStrings, ...exprs) => {
|
|
2822
|
-
Globals.rendered.add(parent);
|
|
3250
|
+
Globals$1.rendered.add(parent);
|
|
2823
3251
|
let template = new Template(htmlStrings, exprs);
|
|
2824
3252
|
return template.render(parent, options);
|
|
2825
3253
|
};
|
|
@@ -2856,6 +3284,22 @@ function r(htmlStrings=undefined, ...exprs) {
|
|
|
2856
3284
|
}
|
|
2857
3285
|
|
|
2858
3286
|
else if (typeof htmlStrings === 'string' || htmlStrings instanceof String) {
|
|
3287
|
+
// 10. JSX
|
|
3288
|
+
if (typeof exprs[0] === 'object') {
|
|
3289
|
+
exprs[0] || {};
|
|
3290
|
+
exprs.slice(1);
|
|
3291
|
+
|
|
3292
|
+
let templateHtmlStrings = [];
|
|
3293
|
+
let templateExprs = [];
|
|
3294
|
+
|
|
3295
|
+
// TODO How to know which children are static html and which are expression placeholders?
|
|
3296
|
+
// Perhaps we have to treat every text child as a string?
|
|
3297
|
+
|
|
3298
|
+
assert(templateHtmlStrings.length === templateExprs.length+1);
|
|
3299
|
+
return new Template(templateHtmlStrings, templateExprs);
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
|
|
2859
3303
|
// If it starts with a string, trim both ends.
|
|
2860
3304
|
// TODO: Also trim if it ends with whitespace?
|
|
2861
3305
|
if (htmlStrings.match(/^\s^</))
|
|
@@ -2879,7 +3323,7 @@ function r(htmlStrings=undefined, ...exprs) {
|
|
|
2879
3323
|
else if (htmlStrings === undefined) {
|
|
2880
3324
|
return (htmlStrings, ...exprs) => {
|
|
2881
3325
|
//Globals.rendered.add(parent)
|
|
2882
|
-
let template =
|
|
3326
|
+
let template = h(htmlStrings, ...exprs);
|
|
2883
3327
|
return template.render();
|
|
2884
3328
|
}
|
|
2885
3329
|
}
|
|
@@ -2891,338 +3335,168 @@ function r(htmlStrings=undefined, ...exprs) {
|
|
|
2891
3335
|
|
|
2892
3336
|
|
|
2893
3337
|
// 9. Create dynamic element with render() function.
|
|
3338
|
+
// TODO: This path doesn't handle embeds like data-id="..."
|
|
2894
3339
|
else if (typeof htmlStrings === 'object') {
|
|
2895
3340
|
let obj = htmlStrings;
|
|
2896
3341
|
|
|
3342
|
+
if (obj.constructor.name !== 'Object')
|
|
3343
|
+
throw new Error(`Solarate Web Component class ${obj.constructor?.name} must extend HTMLElement.`);
|
|
3344
|
+
|
|
3345
|
+
|
|
2897
3346
|
// Special rebound render path, called by normal path.
|
|
2898
|
-
|
|
3347
|
+
// Intercepts the main r`...` function call inside render().
|
|
3348
|
+
if (Globals$1.objToEl.has(obj)) {
|
|
2899
3349
|
return function(...args) {
|
|
2900
|
-
let template =
|
|
3350
|
+
let template = h(...args);
|
|
2901
3351
|
let el = template.render();
|
|
2902
|
-
Globals.objToEl.set(obj, el);
|
|
3352
|
+
Globals$1.objToEl.set(obj, el);
|
|
2903
3353
|
}.bind(obj);
|
|
2904
3354
|
}
|
|
2905
3355
|
|
|
2906
3356
|
// Normal path
|
|
2907
3357
|
else {
|
|
2908
|
-
Globals.objToEl.set(obj, null);
|
|
2909
|
-
obj
|
|
2910
|
-
let el = Globals.objToEl.get(obj);
|
|
2911
|
-
Globals.objToEl.delete(obj);
|
|
3358
|
+
Globals$1.objToEl.set(obj, null);
|
|
3359
|
+
obj[renderF](); // Calls the Special rebound render path above, when the render function calls r(this)
|
|
3360
|
+
let el = Globals$1.objToEl.get(obj);
|
|
3361
|
+
Globals$1.objToEl.delete(obj);
|
|
2912
3362
|
|
|
2913
3363
|
for (let name in obj)
|
|
2914
3364
|
if (typeof obj[name] === 'function')
|
|
2915
|
-
el[name] = obj[name].bind(el);
|
|
3365
|
+
el[name] = obj[name].bind(el); // Make the "this" of functions be el.
|
|
3366
|
+
// TODO: But this doesn't work for passing an object with functions as a constructor arg via an attribute:
|
|
3367
|
+
// <my-element arg=${{myFunc() { return this }}}
|
|
2916
3368
|
else
|
|
2917
3369
|
el[name] = obj[name];
|
|
2918
3370
|
|
|
3371
|
+
// Bind id's
|
|
3372
|
+
// This doesn't work for id's referenced by attributes.
|
|
3373
|
+
// for (let idEl of el.querySelectorAll('[id],[data-id]')) {
|
|
3374
|
+
// Util.bindId(el, idEl);
|
|
3375
|
+
// Util.bindId(obj, idEl);
|
|
3376
|
+
// }
|
|
3377
|
+
// TODO: Bind styles
|
|
3378
|
+
|
|
2919
3379
|
return el;
|
|
2920
3380
|
}
|
|
2921
3381
|
}
|
|
2922
3382
|
|
|
2923
3383
|
else
|
|
2924
3384
|
throw new Error('Unsupported arguments.')
|
|
2925
|
-
}
|
|
2926
|
-
|
|
2927
|
-
//import {watchGet, watchSet} from "./watch.js";
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
function defineClass(Class, tagName, extendsTag) {
|
|
2932
|
-
if (!customElements.getName(Class)) { // If not previously defined.
|
|
2933
|
-
tagName = tagName || camelToDashes(Class.name);
|
|
2934
|
-
if (!tagName.includes('-'))
|
|
2935
|
-
tagName += '-element';
|
|
2936
|
-
|
|
2937
|
-
let options = null;
|
|
2938
|
-
if (extendsTag)
|
|
2939
|
-
options = {extends: extendsTag};
|
|
2940
|
-
|
|
2941
|
-
customElements.define(tagName, Class, options);
|
|
2942
|
-
}
|
|
2943
3385
|
}
|
|
2944
3386
|
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
3387
|
+
// Trick to prevent minifier from renaming this function.
|
|
3388
|
+
let renderF = 'render';
|
|
3389
|
+
|
|
2949
3390
|
/**
|
|
2950
|
-
*
|
|
2951
|
-
*
|
|
2952
|
-
*
|
|
2953
|
-
*
|
|
2954
|
-
* 3. Child elements are added before constructor is called. But they're also passed to the constructor.
|
|
2955
|
-
* 4. We can use this.html = r`...` to set html.
|
|
2956
|
-
* 5. We have the onConnect, onFirstConnect, and onDisconnect methods.
|
|
2957
|
-
* Can't figure out how to have these work standalone though, and still be synchronous.
|
|
2958
|
-
* 6. Can we extend from other element types like TR?
|
|
2959
|
-
* 7. Shows default text if render() function isn't defined.
|
|
3391
|
+
* There are three ways to create an instance of a Solarite Component:
|
|
3392
|
+
* 1. new ComponentName(); // direct class instantiation
|
|
3393
|
+
* 2. this.html = r`<div><component-name></component-name></div>; // as a child of another RedComponent.
|
|
3394
|
+
* 3. <body><component-name></component-name></body> // in the Document html.
|
|
2960
3395
|
*
|
|
2961
|
-
*
|
|
2962
|
-
*
|
|
2963
|
-
*
|
|
2964
|
-
*
|
|
3396
|
+
* When created via #3, Solarite has no way to pass attributes as arguments to the constructor. So to make
|
|
3397
|
+
* sure we get the correct value via all three paths, we write our constructors according to the following
|
|
3398
|
+
* example. Note that constructor args are embedded in an object, and must be all lower-case because
|
|
3399
|
+
* Browsers make all html attribute names lowercase.
|
|
2965
3400
|
*
|
|
2966
|
-
* @
|
|
2967
|
-
*
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
// This line is equivalent the to super() call.
|
|
2992
|
-
return Reflect.construct(Parent, args, Class);
|
|
2993
|
-
}
|
|
2994
|
-
});
|
|
2995
|
-
|
|
2996
|
-
return class Solarite extends HTMLElementAutoDefine {
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
/**
|
|
3000
|
-
* TODO: Make these standalone functions.
|
|
3001
|
-
* Callbacks.
|
|
3002
|
-
* Use onConnect.push(() => ...); to add new callbacks. */
|
|
3003
|
-
onConnect = Util$1.callback();
|
|
3401
|
+
* @example
|
|
3402
|
+
* constructor({name, userid=1}={}) {
|
|
3403
|
+
* super();
|
|
3404
|
+
*
|
|
3405
|
+
* // Get value from "name" attriute if persent, otherwise from name constructor arg.
|
|
3406
|
+
* this.name = getArg(this, 'name', name);
|
|
3407
|
+
*
|
|
3408
|
+
* // Optionally convert the value to an integer.
|
|
3409
|
+
* this.userId = getArg(this, 'userid', userid, ArgType.Int);
|
|
3410
|
+
* }
|
|
3411
|
+
*
|
|
3412
|
+
* @param el {HTMLElement}
|
|
3413
|
+
* @param attributeName {string} Attribute name. Not case-sensitive.
|
|
3414
|
+
* @param defaultValue {*} Default value to use if attribute doesn't exist.
|
|
3415
|
+
* @param type {ArgType|function|*[]}
|
|
3416
|
+
* If an array, use the value if it's in the array, otherwise return undefined.
|
|
3417
|
+
* If it's a function, pass the value to the function and return the result.
|
|
3418
|
+
* @param fallback {*} If the defaultValue is undefiend and type can't be parsed as the given type, use this value.
|
|
3419
|
+
* TODO: Should this be merged with the defaultValue argument?
|
|
3420
|
+
* @return {*} Undefined if attribute isn't set. */
|
|
3421
|
+
function getArg(el, attributeName, defaultValue=undefined, type=ArgType.String, fallback=undefined) {
|
|
3422
|
+
let val = defaultValue;
|
|
3423
|
+
let attrVal = el.getAttribute(attributeName) || el.getAttribute(Util.camelToDashes(attributeName));
|
|
3424
|
+
if (attrVal !== null) // If attribute doesn't exist.
|
|
3425
|
+
val = attrVal;
|
|
3004
3426
|
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3427
|
+
if (Array.isArray(type))
|
|
3428
|
+
return type.includes(val) ? val : fallback;
|
|
3429
|
+
|
|
3430
|
+
if (typeof type === 'function')
|
|
3431
|
+
return type(val);
|
|
3432
|
+
|
|
3433
|
+
// If bool, it's true as long as it exists and its value isn't falsey.
|
|
3434
|
+
if (type===ArgType.Bool) {
|
|
3435
|
+
let lAttrVal = typeof val === 'string' ? val.toLowerCase() : val;
|
|
3436
|
+
if (['false', '0', false, 0, null, undefined, NaN].includes(lAttrVal))
|
|
3437
|
+
return false;
|
|
3438
|
+
if (['true', true].includes(lAttrVal) || parseFloat(lAttrVal) !== 0)
|
|
3439
|
+
return true;
|
|
3440
|
+
return fallback;
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
// Attribute doesn't exist
|
|
3444
|
+
let result;
|
|
3445
|
+
switch (type) {
|
|
3446
|
+
case ArgType.Int:
|
|
3447
|
+
result = parseInt(val);
|
|
3448
|
+
return isNaN(result) ? fallback : result;
|
|
3449
|
+
case ArgType.Float:
|
|
3450
|
+
result = parseFloat(val);
|
|
3451
|
+
return isNaN(result) ? fallback : result;
|
|
3452
|
+
case ArgType.String:
|
|
3453
|
+
return [undefined, null, false].includes(val) ? '' : val+'';
|
|
3454
|
+
case ArgType.Json:
|
|
3455
|
+
case ArgType.Eval:
|
|
3456
|
+
if (typeof val === 'string' && val.length)
|
|
3457
|
+
try {
|
|
3458
|
+
if (type === ArgType.Json)
|
|
3459
|
+
return JSON.parse(val);
|
|
3035
3460
|
else
|
|
3036
|
-
|
|
3037
|
-
}
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
/*
|
|
3041
|
-
let pthis = new Proxy(this, {
|
|
3042
|
-
get(obj, prop) {
|
|
3043
|
-
return Reflect.get(obj, prop)
|
|
3461
|
+
return eval(`(${val})`);
|
|
3462
|
+
} catch (e) {
|
|
3463
|
+
return val;
|
|
3044
3464
|
}
|
|
3045
|
-
|
|
3046
|
-
this.render = this.render.bind(pthis);
|
|
3047
|
-
*/
|
|
3048
|
-
}
|
|
3465
|
+
else return val;
|
|
3049
3466
|
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
if (!Globals.rendered.has(this) && this.render)
|
|
3054
|
-
this.render();
|
|
3055
|
-
}
|
|
3056
|
-
|
|
3057
|
-
/**
|
|
3058
|
-
* Called automatically by the browser. */
|
|
3059
|
-
connectedCallback() {
|
|
3060
|
-
this.renderFirstTime();
|
|
3061
|
-
if (!Globals.connected.has(this)) {
|
|
3062
|
-
Globals.connected.add(this);
|
|
3063
|
-
this.onFirstConnect();
|
|
3064
|
-
}
|
|
3065
|
-
this.onConnect();
|
|
3066
|
-
}
|
|
3067
|
-
|
|
3068
|
-
disconnectedCallback() {
|
|
3069
|
-
this.onDisconnect();
|
|
3070
|
-
}
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
static define(tagName=null) {
|
|
3074
|
-
defineClass(this, tagName, extendsTag);
|
|
3075
|
-
}
|
|
3076
|
-
|
|
3077
|
-
|
|
3467
|
+
// type not provided
|
|
3468
|
+
default:
|
|
3469
|
+
return val;
|
|
3078
3470
|
}
|
|
3079
|
-
}
|
|
3080
|
-
|
|
3081
|
-
/**
|
|
3082
|
-
* Trying to be able to automatically watch primitive values.
|
|
3083
|
-
* TODO:
|
|
3084
|
-
* 1. Have get() return Proxies for nested updates.
|
|
3085
|
-
* 2. Override .map() for loops to capture changes.
|
|
3086
|
-
*/
|
|
3087
|
-
|
|
3088
|
-
let unusedArg = Symbol('unusedArg');
|
|
3089
|
-
|
|
3090
|
-
/**
|
|
3091
|
-
* Custom map function triggers the get() Proxy.
|
|
3092
|
-
* @param array {Array}
|
|
3093
|
-
* @param callback {function}
|
|
3094
|
-
* @returns {*[]} */
|
|
3095
|
-
function map(array, callback) {
|
|
3096
|
-
let result = [];
|
|
3097
|
-
for (let i=0; i<array.length; i++)
|
|
3098
|
-
result.push(callback(array[i], i, array));
|
|
3099
|
-
return result;
|
|
3100
3471
|
}
|
|
3101
3472
|
|
|
3102
|
-
|
|
3103
3473
|
/**
|
|
3104
|
-
*
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
// use a single object for both defineProperty and new Proxy's handler.
|
|
3117
|
-
const handler = {
|
|
3118
|
-
get(obj, prop, receiver) {
|
|
3119
|
-
|
|
3120
|
-
let result = (obj === receiver && field === prop)
|
|
3121
|
-
? value // top-level value.
|
|
3122
|
-
: Reflect.get(obj, prop, receiver); // avoid infinite recursion.
|
|
3123
|
-
|
|
3124
|
-
if (prop === 'map')
|
|
3125
|
-
|
|
3126
|
-
// Double function so the ExprPath calls it as a function,
|
|
3127
|
-
// instead of it being evaluated immediately when the Templat eis created.
|
|
3128
|
-
return (callback) => () => {
|
|
3129
|
-
let rootNg = Globals.nodeGroups.get(root);
|
|
3130
|
-
rootNg.mapCallbacks.set(obj, callback);
|
|
3131
|
-
return map(new Proxy(obj, handler), callback);
|
|
3132
|
-
}
|
|
3133
|
-
|
|
3134
|
-
// Track which ExprPath is using this variable.
|
|
3135
|
-
if (Globals.currentExprPath) {
|
|
3136
|
-
let [exprPath, exprFunction] = Globals.currentExprPath; // Set in ExprPath.applyExact()
|
|
3137
|
-
|
|
3138
|
-
let rootNg = Globals.nodeGroups.get(root);
|
|
3139
|
-
|
|
3140
|
-
// Init for field.
|
|
3141
|
-
rootNg.watchedExprPaths[field] = rootNg.watchedExprPaths[field] || new Set();
|
|
3142
|
-
rootNg.watchedExprPaths[field].add(exprPath);
|
|
3143
|
-
}
|
|
3144
|
-
|
|
3145
|
-
if (result && typeof result === 'object')
|
|
3146
|
-
return new Proxy(result, handler);
|
|
3147
|
-
|
|
3148
|
-
return result;
|
|
3149
|
-
},
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
// TODO: Will fail for attribute w/ a value having multiple ExprPaths.
|
|
3153
|
-
// TODO: This won't update a component's expressions.
|
|
3154
|
-
set(obj, prop, val, receiver) {
|
|
3155
|
-
|
|
3156
|
-
// 1. Set the value.
|
|
3157
|
-
if (obj === receiver && field === prop)
|
|
3158
|
-
value = val; // top-level value.
|
|
3159
|
-
else // avoid infinite recursion.
|
|
3160
|
-
Reflect.set(obj, prop, val, receiver);
|
|
3161
|
-
|
|
3162
|
-
// 2. Add to the list of ExprPaths to re-render.
|
|
3163
|
-
let rootNg = Globals.nodeGroups.get(root);
|
|
3164
|
-
for (let exprPath of rootNg.watchedExprPaths[field]) {
|
|
3165
|
-
|
|
3166
|
-
// Update a single NodeGroup created by array.map()
|
|
3167
|
-
if (Array.isArray(obj) && parseInt(prop) == prop) {
|
|
3168
|
-
let exprsToRender = rootNg.exprsToRender.get(exprPath);
|
|
3169
|
-
|
|
3170
|
-
// If we're not re-rendering the whole thing.
|
|
3171
|
-
if (exprsToRender !== true)
|
|
3172
|
-
Util$1.mapAdd(rootNg.exprsToRender, exprPath, [obj, prop, val]);
|
|
3173
|
-
}
|
|
3174
|
-
|
|
3175
|
-
// Reapply the whole expression.
|
|
3176
|
-
else
|
|
3177
|
-
rootNg.exprsToRender.set(exprPath, true);
|
|
3178
|
-
}
|
|
3179
|
-
return true;
|
|
3180
|
-
}
|
|
3181
|
-
};
|
|
3182
|
-
|
|
3183
|
-
Object.defineProperty(root, field, {
|
|
3184
|
-
get: () => handler.get(root, field, root),
|
|
3185
|
-
set: (val) => handler.set(root, field, val, root)
|
|
3186
|
-
});
|
|
3187
|
-
}
|
|
3188
|
-
|
|
3189
|
-
/**
|
|
3190
|
-
* TODO: Rename so we have watch.add() and watch.render() ?
|
|
3191
|
-
* @param root
|
|
3192
|
-
* @returns {*[]} */
|
|
3193
|
-
function renderWatched(root) {
|
|
3194
|
-
let rootNg = Globals.nodeGroups.get(root);
|
|
3195
|
-
let modified = [];
|
|
3196
|
-
|
|
3197
|
-
for (let [exprPath, params] of rootNg.exprsToRender) {
|
|
3198
|
-
|
|
3199
|
-
// Reapply the whole expression.
|
|
3200
|
-
if (params === true) {
|
|
3201
|
-
exprPath.apply(exprPath.watchFunction);
|
|
3202
|
-
|
|
3203
|
-
// TODO: freeNodeGroups() could be skipped if applyExprs() never marked them as in-use.
|
|
3204
|
-
exprPath.freeNodeGroups();
|
|
3205
|
-
|
|
3206
|
-
modified.push(...exprPath.getNodes());
|
|
3207
|
-
}
|
|
3208
|
-
|
|
3209
|
-
// Update a single NodeGroup created by array.map()
|
|
3210
|
-
else {
|
|
3211
|
-
for (let row of params) {
|
|
3212
|
-
let [obj, prop, value] = row;
|
|
3213
|
-
let callback = rootNg.mapCallbacks.get(obj);
|
|
3214
|
-
let template = callback(value);
|
|
3215
|
-
exprPath.applyLoopItemUpdate(prop, template);
|
|
3474
|
+
* @enum */
|
|
3475
|
+
var ArgType = {
|
|
3476
|
+
|
|
3477
|
+
/**
|
|
3478
|
+
* false, 0, null, undefined, '0', and 'false' (case-insensitive) become false.
|
|
3479
|
+
* Anything else, including empty string becomes true.
|
|
3480
|
+
* Empty string is true because attributes with no value should be evaulated as true. */
|
|
3481
|
+
Bool: 'Bool',
|
|
3482
|
+
|
|
3483
|
+
Int: 'Int',
|
|
3484
|
+
Float: 'Float',
|
|
3485
|
+
String: 'String',
|
|
3216
3486
|
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
}
|
|
3220
|
-
}
|
|
3487
|
+
/** @deprecated for Json */
|
|
3488
|
+
JSON: 'Json',
|
|
3221
3489
|
|
|
3222
|
-
|
|
3490
|
+
/**
|
|
3491
|
+
* Parse the string value as JSON.
|
|
3492
|
+
* If it's not parsable, return the value as a string. */
|
|
3493
|
+
Json: 'Json',
|
|
3223
3494
|
|
|
3224
|
-
|
|
3225
|
-
|
|
3495
|
+
/**
|
|
3496
|
+
* Evaluate the string as JavaScript using the eval() function.
|
|
3497
|
+
* If it can't be evaluated, return the original string. */
|
|
3498
|
+
Eval: 'Eval'
|
|
3499
|
+
};
|
|
3226
3500
|
|
|
3227
3501
|
/**
|
|
3228
3502
|
* Solarite JavasCript UI library.
|
|
@@ -3239,6 +3513,9 @@ let Solarite = new Proxy(createSolarite(), {
|
|
|
3239
3513
|
}
|
|
3240
3514
|
});
|
|
3241
3515
|
let getInputValue = Util.getInputValue;
|
|
3242
|
-
|
|
3516
|
+
|
|
3517
|
+
//Experimental:
|
|
3518
|
+
//export {default as watch, renderWatched} from './watch.js'; // unfinished
|
|
3243
3519
|
|
|
3244
|
-
export
|
|
3520
|
+
export default h;
|
|
3521
|
+
export { ArgType, Globals$1 as Globals, Solarite, Util as SolariteUtil, Template, delve, getArg, getInputValue, h, h as r };
|