solarite 0.2.4 → 0.3.0

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