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