solarite 0.1.1 → 0.2.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.
Files changed (45) hide show
  1. package/benchmarks/naive/Solarite.min.js +4 -0
  2. package/benchmarks/naive/index.html +14 -0
  3. package/benchmarks/naive/main.js +339 -0
  4. package/benchmarks/naive/package-lock.json +13 -0
  5. package/benchmarks/naive/package.json +23 -0
  6. package/benchmarks/readme.md +48 -0
  7. package/build/build.bat +2 -2
  8. package/build/build.js +1 -1
  9. package/dist/Solarite-debug.js +1344 -1532
  10. package/dist/Solarite.js +1298 -1334
  11. package/dist/Solarite.min.js +3 -3
  12. package/dist/udomdiff-license.txt +18 -0
  13. package/docs/index.md +440 -130
  14. package/docs/js/Playground.js +1 -1
  15. package/docs/js/codemirror/codemirror6.js +3683 -3206
  16. package/docs/js/codemirror/themeSolarIce.js +1 -1
  17. package/docs/js/documentation.js +1 -1
  18. package/docs/js/ui/CodeEditor.js +183 -45
  19. package/docs/js/ui/FlexResizer.js +14 -3
  20. package/docs/js/util/Errors.js +11 -0
  21. package/docs/media/documentation.css +6 -3
  22. package/index.html +449 -30
  23. package/package.json +1 -1
  24. package/readme.md +1 -1
  25. package/src/solarite/ExprPath.js +349 -151
  26. package/src/solarite/Globals.js +43 -1
  27. package/src/solarite/NodeGroup.js +275 -293
  28. package/src/solarite/Shell.js +26 -28
  29. package/src/solarite/Solarite.js +12 -0
  30. package/src/solarite/Template.js +55 -128
  31. package/src/solarite/Util.js +131 -7
  32. package/src/solarite/createSolarite.js +14 -19
  33. package/src/solarite/getArg.js +1 -1
  34. package/src/solarite/hash.js +18 -35
  35. package/src/solarite/r.js +127 -127
  36. package/src/solarite/watch3.js +18 -11
  37. package/src/{solarite → unused}/NodeGroupManager.js +81 -109
  38. package/src/{solarite → unused}/watch.js +1 -1
  39. package/src/{solarite → unused}/watch2.js +1 -2
  40. package/src/{solarite → util}/MultiValueMap.js +18 -11
  41. package/src/util/WeakArray.js +33 -0
  42. package/tests/Solarite.test.js +862 -167
  43. package/tests/index.html +4 -2
  44. package/tests/run.bat +1 -1
  45. package/deno.lock +0 -176
@@ -1,23 +1,24 @@
1
1
  import {assert} from "../util/Errors.js";
2
2
  import ExprPath, {PathType, getNodePath} from "./ExprPath.js";
3
-
4
3
  import {div, htmlContext, isEvent} from "./Util.js";
4
+ import Globals from "./Globals.js";
5
5
 
6
6
  /**
7
7
  * A Shell is created from a tagged template expression instantiated as Nodes,
8
- * but without any expressions filled in. */
8
+ * but without any expressions filled in.
9
+ * Only one Shell is created for all the items in a loop.
10
+ *
11
+ * When a NodeGroup is created from a Template's html strings,
12
+ * the NodeGroup then clones the Shell's fragmentn to be its nodes. */
9
13
  export default class Shell {
10
14
 
11
15
  /**
12
- * @type {DocumentFragment} Parent of the shell nodes. */
16
+ * @type {DocumentFragment} DOM parent of the shell nodes. */
13
17
  fragment;
14
18
 
15
19
  /** @type {ExprPath[]} Paths to where expressions should go. */
16
20
  paths = [];
17
21
 
18
- /** @type {?Template} Template that created this element. */
19
- template;
20
-
21
22
  // Embeds and ids
22
23
  events = [];
23
24
 
@@ -29,6 +30,7 @@ export default class Shell {
29
30
  staticComponents = [];
30
31
 
31
32
 
33
+
32
34
  /**
33
35
  * Create the nodes but without filling in the expressions.
34
36
  * This is useful because the expression-less nodes created by a template can be cached.
@@ -122,7 +124,9 @@ export default class Shell {
122
124
  let parts = attr.value.split(/[\ue000-\uf8ff]/g);
123
125
  if (parts.length > 1) {
124
126
  let nonEmptyParts = (parts.length === 2 && !parts[0].length && !parts[1].length) ? null : parts;
125
- this.paths.push(new ExprPath(null, node, PathType.Value, attr.name, nonEmptyParts));
127
+ let type = isEvent(attr.name) ? PathType.Event : PathType.Value;
128
+
129
+ this.paths.push(new ExprPath(null, node, type, attr.name, nonEmptyParts));
126
130
  node.setAttribute(attr.name, parts.join(''));
127
131
  }
128
132
  }
@@ -134,7 +138,7 @@ export default class Shell {
134
138
  // Get or create nodeBefore.
135
139
  let nodeBefore = node.previousSibling; // Can be the same as another Path's nodeMarker.
136
140
  if (!nodeBefore) {
137
- nodeBefore = document.createComment('PathStart:'+this.paths.length);
141
+ nodeBefore = document.createComment('ExprPath:'+this.paths.length);
138
142
  node.parentNode.insertBefore(nodeBefore, node)
139
143
  }
140
144
  /*#IFDEV*/assert(nodeBefore);/*#ENDIF*/
@@ -150,16 +154,14 @@ export default class Shell {
150
154
  // Re-use existing comment placeholder.
151
155
  else {
152
156
  nodeMarker = node;
153
- nodeMarker.textContent = 'PathEnd:'+ this.paths.length;
157
+ nodeMarker.textContent = 'ExprPathEnd:'+ this.paths.length;
154
158
  }
155
159
  /*#IFDEV*/assert(nodeMarker);/*#ENDIF*/
156
160
 
157
161
 
158
162
 
159
163
  let path = new ExprPath(nodeBefore, nodeMarker, PathType.Content);
160
- //#IFDEV
161
- path.parentIndex = this.paths.length; // For debugging.
162
- //#ENDIF
164
+
163
165
  this.paths.push(path);
164
166
  }
165
167
 
@@ -173,9 +175,6 @@ export default class Shell {
173
175
  for (let i=0; i<parts.length-1; i++) {
174
176
  let path = new ExprPath(node.previousSibling, node)
175
177
  path.type = PathType.Comment;
176
- //#IFDEV
177
- path.parentIndex = i; // For debugging.
178
- //#ENDIF
179
178
  this.paths.push(path);
180
179
  }
181
180
  }
@@ -194,10 +193,7 @@ export default class Shell {
194
193
  }
195
194
 
196
195
  for (let i=0, node; node=placeholders[i]; i++) {
197
- let path = new ExprPath(node.previousSibling, node, PathType.Content)
198
- //#IFDEV
199
- path.parentIndex = i; // For debugging.
200
- //#ENDIF
196
+ let path = new ExprPath(node.previousSibling, node, PathType.Content);
201
197
  this.paths.push(path);
202
198
 
203
199
  /*#IFDEV*/path.verify();/*#ENDIF*/
@@ -228,22 +224,25 @@ export default class Shell {
228
224
  path.nodeMarkerPath = getNodePath(path.nodeMarker)
229
225
 
230
226
  // Cache so we don't have to calculate this later inside NodeGroup.applyExprs()
231
- if (path.type === PathType.Value && path.nodeMarker.nodeType === 1 &&
227
+ if (path.type === PathType.Value && path.nodeMarker.nodeType === 1 && /*path.nodeMarker !== template.content.children[0] &&*/
232
228
  (path.nodeMarker.tagName.includes('-') || path.nodeMarker.hasAttribute('is'))) {
233
229
  path.type = PathType.Component;
234
230
  }
235
231
  }
236
232
 
237
-
238
233
  this.findEmbeds();
239
234
 
240
-
241
235
  /*#IFDEV*/this.verify();/*#ENDIF*/
242
236
  } // end constructor
243
237
 
244
238
  /**
245
239
  * We find the path to every embed here once in the Shell, instead of every time a NodeGroup is instantiated.
246
- * When a Nodegroup is created, it calls NodeGroup.activateEmbeds() that uses these paths. */
240
+ * When a Nodegroup is created, it calls NodeGroup.activateEmbeds() that uses these paths.
241
+ * Populates:
242
+ * this.scripts
243
+ * this.styles
244
+ * this.ids
245
+ * this.staticComponents */
247
246
  findEmbeds() {
248
247
  this.scripts = Array.prototype.map.call(this.fragment.querySelectorAll('scripts'), el => getNodePath(el))
249
248
  this.styles = Array.prototype.map.call(this.fragment.querySelectorAll('style'), el => getNodePath(el))
@@ -255,7 +254,7 @@ export default class Shell {
255
254
  for (let el of idEls) {
256
255
  let id = el.getAttribute('data-id') || el.getAttribute('id')
257
256
  if (div.hasOwnProperty(id))
258
- throw new Error(`<${el.tagName.toLowerCase()} id="${id}"> can't override existing HTMLElement property.`)
257
+ throw new Error(`<${el.tagName.toLowerCase()} id="${id}"> can't override existing HTMLElement id property.`)
259
258
  }
260
259
 
261
260
 
@@ -280,13 +279,13 @@ export default class Shell {
280
279
 
281
280
  /**
282
281
  * Get the shell for the html strings.
283
- * @param htmlStrings {string[]}
282
+ * @param htmlStrings {string[]} Typically comes from a Template.
284
283
  * @returns {Shell} */
285
284
  static get(htmlStrings) {
286
- let result = shells.get(htmlStrings);
285
+ let result = Globals.shells.get(htmlStrings);
287
286
  if (!result) {
288
287
  result = new Shell(htmlStrings);
289
- shells.set(htmlStrings, result); // cache
288
+ Globals.shells.set(htmlStrings, result); // cache
290
289
  }
291
290
 
292
291
  /*#IFDEV*/result.verify();/*#ENDIF*/
@@ -304,4 +303,3 @@ export default class Shell {
304
303
  //#ENDIF
305
304
  }
306
305
 
307
- let shells = new WeakMap();
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Solarite JavasCript UI library.
3
+ * MIT License
4
+ * https://vorticode.github.io/solarite/
5
+ */
6
+
7
+
1
8
  import createSolarite from "./createSolarite.js";
2
9
 
3
10
  /**
@@ -15,7 +22,12 @@ export {Solarite}
15
22
  export {default as r} from './r.js';
16
23
  export {getArg, ArgType} from './getArg.js';
17
24
  export {default as Template} from './Template.js';
25
+ export {default as Globals} from './Globals.js';
18
26
 
27
+ import Util from './Util.js';
28
+ let getInputValue = Util.getInputValue;
29
+ export {getInputValue};
30
+ export {default as delve} from '../util/delve.js';
19
31
 
20
32
  //Experimental:
21
33
  //export {forEach, watchGet, watchSet} from './watch.js' // old, unfinished
@@ -1,8 +1,7 @@
1
1
  import {assert} from "../util/Errors.js";
2
- import {getObjectId} from "./hash.js";
3
- import NodeGroupManager from "./NodeGroupManager.js";
4
- import NodeGroup from "./NodeGroup.js";
5
-
2
+ import {getObjectHash, getObjectId} from "./hash.js";
3
+ import Globals from "./Globals.js";
4
+ import {RootNodeGroup} from "./NodeGroup.js";
6
5
 
7
6
  /**
8
7
  * The html strings and evaluated expressions from an html tagged template.
@@ -16,12 +15,7 @@ export default class Template {
16
15
  /** @type {string[]} */
17
16
  html = [];
18
17
 
19
- /**
20
- * If true, use this template to replace an existing element, instead of appending children to it.
21
- * @type {?boolean} */
22
- replaceMode;
23
-
24
- /** Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
18
+ /** @type {Array} Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
25
19
  hashedFields;
26
20
 
27
21
  /**
@@ -29,10 +23,10 @@ export default class Template {
29
23
  * @type {ExprPath} Used with forEach() from watch.js
30
24
  * Set in ExprPath.apply() */
31
25
  parentPath;
32
-
26
+
33
27
  /** @type {NodeGroup} */
34
28
  nodeGroup;
35
-
29
+
36
30
  /**
37
31
  * @type {string[][]} */
38
32
  paths = [];
@@ -44,7 +38,7 @@ export default class Template {
44
38
  constructor(htmlStrings, exprs) {
45
39
  this.html = htmlStrings;
46
40
  this.exprs = exprs;
47
-
41
+
48
42
  //this.trace = new Error().stack.split(/\n/g)
49
43
 
50
44
  // Multiple templates can share the same htmlStrings array.
@@ -53,7 +47,7 @@ export default class Template {
53
47
  //#IFDEV
54
48
  assert(Array.isArray(htmlStrings))
55
49
  assert(Array.isArray(exprs))
56
-
50
+
57
51
  Object.defineProperty(this, 'debug', {
58
52
  get() {
59
53
  return JSON.stringify([this.html, this.exprs]);
@@ -67,8 +61,8 @@ export default class Template {
67
61
  * This prevents the hashed version from being too large. */
68
62
  toJSON() {
69
63
  if (!this.hashedFields)
70
- this.hashedFields = [getObjectId(this.html, 'Html'), this.exprs];
71
-
64
+ this.hashedFields = [getObjectId(this.html), this.exprs];
65
+
72
66
  return this.hashedFields
73
67
  }
74
68
 
@@ -78,131 +72,64 @@ export default class Template {
78
72
  * @param options {RenderOptions}
79
73
  * @return {?DocumentFragment|HTMLElement} */
80
74
  render(el=null, options={}) {
81
-
82
75
  let ng;
83
- if (!el) {
84
- ng = new NodeGroup(this);
85
- el = ng.getParentNode();
86
- }
87
-
88
- let ngm = NodeGroupManager.get(el);
89
- if (ng)
90
- ng.manager = ngm;
91
-
92
- //#IFDEV
93
- ngm.modifications = {
94
- created: [],
95
- updated: [],
96
- moved: [],
97
- deleted: []
98
- };
99
- //#ENDIF
100
-
101
- ngm.options = options;
102
- ngm.clearSubscribers = false; // Used for deprecated watch() path?
103
- ngm.mutationWatcherEnabled = false;
104
-
105
- // Fast path for empty component.
106
- if (this.html?.length === 1 && !this.html[0]) {
107
- el.innerHTML = '';
76
+ let standalone = !el;
77
+ let firstTime = false;
78
+
79
+ // Rendering a standalone element.
80
+ // TODO: figure out when to not use RootNodeGroup
81
+ if (standalone) {
82
+ ng = new RootNodeGroup(this, null, options);
83
+ el = ng.getRootNode();
84
+ Globals.nodeGroups.set(el, ng);
85
+ firstTime = true;
108
86
  }
109
87
  else {
110
-
111
- // Find or create a NodeGroup for the template.
112
- // This updates all nodes from the template.
113
- let close;
114
- let exact = ngm.getNodeGroup(this, true);
115
- if (!exact) {
116
- close = ngm.getNodeGroup(this, false);
117
- }
118
-
119
- let firstTime = !ngm.rootNg;
120
- ngm.rootNg = exact || close;
121
-
122
- // Reparent NodeGroup
123
- // TODO: Move this to NodeGroup?
124
- let parent = ngm.rootNg.getParentNode();
125
-
126
-
127
- // If this is the first time rendering this element.
128
- if (firstTime) {
129
-
130
- // Save slot children
131
- let fragment;
132
- if (el.childNodes.length) {
133
- fragment = document.createDocumentFragment();
134
- fragment.append(...el.childNodes);
135
- }
136
-
137
- // Add rendered elements.
138
- if (parent instanceof DocumentFragment)
139
- el.append(parent);
140
- else if (parent)
141
- el.append(...parent.childNodes)
142
-
143
- // Apply slot children
144
- if (fragment) {
145
- for (let slot of el.querySelectorAll('slot[name]')) {
146
- let name = slot.getAttribute('name')
147
- if (name)
148
- slot.append(...fragment.querySelectorAll(`[slot='${name}']`))
149
- }
150
- let unamedSlot = el.querySelector('slot:not([name])')
151
- if (unamedSlot)
152
- unamedSlot.append(fragment)
153
- }
88
+ ng = Globals.nodeGroups.get(el);
89
+ if (!ng) {
90
+ ng = new RootNodeGroup(this, el, options);
91
+ Globals.nodeGroups.set(el, ng);
92
+ firstTime = true;
154
93
  }
94
+ }
155
95
 
156
- ngm.rootEl = el;
157
-
158
- // this.rootNg was rendered as childrenOnly=true
159
- // Apply attributes from a root element to the real root element.
160
- let ng = ngm.rootNg;
161
- if (ng.pseudoRoot && ng.pseudoRoot !== el) {
162
- /*#IFDEV*/assert(el)/*#ENDIF*/
163
-
164
- // Remove old attributes
165
- // for (let attrib of this.rootEl.attributes)
166
- // if (attrib.name !== 'is' && attrib.name !== 'data-style' && !ng.pseudoRoot.hasAttribute(attrib.name))
167
- // this.rootEl.removeAttribute(attrib.name)
168
-
169
- // Add/set new attributes
170
- if (firstTime)
171
- for (let attrib of ng.pseudoRoot.attributes)
172
- if (!el.hasAttribute(attrib.name))
173
- el.setAttribute(attrib.name, attrib.value);
174
-
175
- // ng.startNode = ng.endNode = this.rootEl;
176
- // ng.nodesCache = [ng.startNode]
177
- // for (let path of ng.paths) {
178
- // if (path.nodeMarker === ng.rootEl)
179
- // path.nodeMarker = this.rootEl;
180
- // path.nodesCache = null;
181
- // /*#IFDEV*/assert(path.nodeBefore !== ng.rootEl)/*#ENDIF*/
182
- // }
183
- //
184
- // ng.rootEl = this.rootEl;
185
- }
186
-
187
- /*#IFDEV*/ngm.rootNg.verify();/*#ENDIF*/
188
- ngm.reset(); // Mark all NodeGroups as available, for next render.
189
- /*#IFDEV*/ngm.rootNg.verify();/*#ENDIF*/
190
-
191
- window.ngm = ngm;
96
+ // Creating the root nodegroup also renders it.
97
+ // If we didn't just create it, we need to render it.
98
+ if (!firstTime) {
99
+ if (this.html?.length === 1 && !this.html[0])
100
+ el.innerHTML = ''; // Fast path for empty component.
101
+ else
102
+ ng.applyExprs(this.exprs);
192
103
  }
193
104
 
194
- ngm.mutationWatcherEnabled = true;
195
105
  return el;
196
- //#IFDEV
197
- //return ngm.modifications;
198
- //#ENDIF
199
106
  }
200
107
 
108
+ getExactKey() {
109
+ if (!this.exactKey)
110
+ this.exactKey = getObjectHash(this); // calls this.toJSON().
111
+ return this.exactKey;
112
+ }
201
113
 
202
114
  getCloseKey() {
203
- // Use the joined html when debugging?
115
+ if (!this.closeKey)
116
+ this.closeKey = '@'+this.toJSON()[0];
117
+ // Use the joined html when debugging? But it breaks some tests.
204
118
  //return '@'+this.html.join('|')
205
119
 
206
- return '@'+this.hashedFields[0];
120
+ return this.closeKey;
207
121
  }
208
122
  }
123
+
124
+
125
+ /**
126
+ * @typedef {Object} RenderOptions
127
+ * @property {boolean=} styles - Replace :host in style tags to scope them locally.
128
+ * @property {boolean=} scripts - Execute script tags.
129
+ * @property {boolean=} ids - Create references to elements with id or data-id attributes.
130
+ * @property {?boolean} render - Deprecated.
131
+ * Used only when options are given to a class super constructor inheriting from Solarite.
132
+ * True to call render() immediately in super constructor.
133
+ * False to automatically call render() at all.
134
+ * Undefined (default) to call render() when added to the DOM, unless already rendered.
135
+ */
@@ -21,9 +21,133 @@ let Util = {
21
21
  child.textContent = newText;
22
22
  }
23
23
  }
24
- }
24
+ },
25
+
26
+ /**
27
+ * A generator function that recursively traverses and flattens a value.
28
+ *
29
+ * - If the input is an array, it recursively traverses and flattens the array.
30
+ * - If the input is a function, it calls the function, replaces the function
31
+ * with its result, and flattens the result if necessary. It will recursively
32
+ * call functions that return other functions.
33
+ * - Otherwise it yields the value as is.
34
+ *
35
+ * This function does not create a new array for the flattened values. Instead,
36
+ * it lazily yields each item as it is encountered. This can be more memory-efficient
37
+ * for large or deeply nested structures.
38
+ *
39
+ * @param {any} value - The value to flatten. Can be an array, object, function, or primitive.
40
+ * @yields {any} - The next item in the flattened structure.
41
+ *
42
+ * @example
43
+ * const complexArray = [
44
+ * 1,
45
+ * [2, () => 3, [4, () => [5, 6]], { a: 'object' }],
46
+ * () => () => 7,
47
+ * () => [() => 8, 9],
48
+ * ]; *
49
+ * for (const item of flatten(complexArray))
50
+ * console.log(item); // Outputs: 1, 2, 3, 4, 5, 6, { a: 'object' }, 7, 8, 9
51
+ */
52
+ *flatten(value) {
53
+ if (Array.isArray(value)) {
54
+ for (const item of value) {
55
+ yield* Util.flatten(item); // Recursively flatten arrays
56
+ }
57
+ } else if (typeof value === 'function') {
58
+ const result = value();
59
+ yield* Util.flatten(result); // Recursively flatten the result of a function
60
+ } else
61
+ yield value; // Yield primitive values as is
62
+ },
63
+
64
+ /**
65
+ * Get the value of an input as the most appropriate JavaScript type.
66
+ * @param node {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|HTMLDivElement}
67
+ * @return {string|string[]|number|[]|File[]|Date|boolean} */
68
+ getInputValue(node) {
69
+ if (node.type === 'checkbox' || node.type === 'radio')
70
+ return node.checked; // Boolean
71
+ if (node.type === 'file')
72
+ return [...node.files]; // FileList
73
+ if (node.type === 'number' || node.type === 'range')
74
+ return node.valueAsNumber; // Number
75
+ if (node.type === 'date' || node.type === 'time' || node.type === 'datetime-local')
76
+ return node.valueAsDate; // Date Object
77
+ if (node.type === 'select-multiple') // <select multiple>
78
+ return [...node.selectedOptions].map(option => option.value); // Array of Strings
79
+
80
+ return node.value; // String
81
+ },
82
+
83
+ /**
84
+ * Is it an array and a path that can be evaluated by delve() ?
85
+ * @param arr {Array|*}
86
+ * @returns {boolean} */
87
+ isPath(arr) {
88
+ return Array.isArray(arr) && typeof arr[0] === 'object' && !arr.slice(1).find(p => typeof p !== 'string' && typeof p !== 'number');
89
+ },
90
+
91
+ /**
92
+ * Find NodeGroups that had their nodes removed and add those nodes to a Fragment so
93
+ * they're not lost forever and the NodeGroup's internal structure is still consistent.
94
+ * This saves all of a NodeGroup's nodes in order, so that nextChildNode still works.
95
+ * This is necessary because a NodeGroup normally only stores the first and last node.
96
+ * Called from ExprPath.apply().
97
+ * @param oldNodeGroups {NodeGroup[]}
98
+ * @param oldNodes {Node[]} */
99
+ saveOrphans(oldNodeGroups, oldNodes) {
100
+ let oldNgMap = new Map();
101
+ for (let ng of oldNodeGroups) {
102
+ oldNgMap.set(ng.startNode, ng)
103
+
104
+ // TODO: Is this necessary?
105
+ // if (ng.parentPath)
106
+ // ng.parentPath.clearNodesCache();
107
+ }
25
108
 
109
+ for (let i=0, node; node = oldNodes[i]; i++) {
110
+ let ng;
111
+ if (!node.parentNode && (ng = oldNgMap.get(node))) {
112
+ //ng.nodesCache = [];
113
+ let fragment = document.createDocumentFragment();
114
+ let endNode = ng.endNode;
115
+ while (node !== endNode) {
116
+ fragment.append(node);
117
+ //ng.nodesCache.push(node);
118
+ i++;
119
+ node = oldNodes[i];
120
+ }
121
+ fragment.append(endNode);
122
+ //ng.nodesCache.push(endNode);
123
+ }
124
+ }
125
+ },
126
+
127
+ /**
128
+ * Remove nodes from the beginning and end that are not:
129
+ * 1. Elements.
130
+ * 2. Non-whitespace text nodes.
131
+ * @param nodes {Node[]|NodeList}
132
+ * @returns {Node[]} */
133
+ trimEmptyNodes(nodes) {
134
+ const shouldTrimNode = node =>
135
+ node.nodeType !== Node.ELEMENT_NODE &&
136
+ (node.nodeType !== Node.TEXT_NODE || node.textContent.trim() === '');
137
+
138
+ // Convert nodeList to an array for easier manipulation
139
+ const result = [...nodes]
140
+
141
+ // Trim from the start
142
+ while (result.length > 0 && shouldTrimNode(result[0]))
143
+ result.shift();
144
+
145
+ // Trim from the end
146
+ while (result.length > 0 && shouldTrimNode(result[result.length - 1]))
147
+ result.pop();
26
148
 
149
+ return result;
150
+ }
27
151
  };
28
152
 
29
153
  export default Util;
@@ -72,15 +196,15 @@ export function camelToDashes(str) {
72
196
  * Returns false if they're the same. Or the first index where they differ.
73
197
  * @param a
74
198
  * @param b
75
- * @returns {int|false} */
76
- export function findArrayDiff(a, b) {
77
- if (a.length !== b.length)
78
- return -1;
199
+ * @returns {boolean} */
200
+ export function arraySame(a, b) {
79
201
  let aLength = a.length;
202
+ if (aLength !== b.length)
203
+ return false;
80
204
  for (let i=0; i<aLength; i++)
81
205
  if (a[i] !== b[i])
82
- return i;
83
- return false; // the same.
206
+ return false;
207
+ return true; // the same.
84
208
  }
85
209
 
86
210
 
@@ -3,9 +3,10 @@ import Util from "../util/Util.js";
3
3
  import delve from "../util/delve.js";
4
4
  import {getArg, ArgType} from "./getArg.js";
5
5
  import {getObjectHash} from "./hash.js";
6
- import NodeGroupManager from "./NodeGroupManager.js";
7
- import r, {rendered} from "./r.js";
6
+ //import NodeGroupManager from "./NodeGroupManager.js";
7
+ import r from "./r.js";
8
8
  import {camelToDashes} from "./Util.js";
9
+ import Globals from "./Globals.js";
9
10
 
10
11
 
11
12
  //import {watchGet, watchSet} from "./watch.js";
@@ -26,14 +27,9 @@ function defineClass(Class, tagName, extendsTag) {
26
27
  }
27
28
  }
28
29
 
29
- /**
30
- * @type {Object<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
31
- let elementClasses = {};
32
30
 
33
- /**
34
- * Store which instances of Solarite have already been added to the DOM. * @type {WeakSet<HTMLElement>}
35
- */
36
- let connected = new WeakSet();
31
+
32
+
37
33
 
38
34
  /**
39
35
  * Create a version of the Solarite class that extends from the given tag name.
@@ -45,6 +41,7 @@ let connected = new WeakSet();
45
41
  * 5. We have the onConnect, onFirstConnect, and onDisconnect methods.
46
42
  * Can't figure out how to have these work standalone though, and still be synchronous.
47
43
  * 6. Can we extend from other element types like TR?
44
+ * 7. Shows default text if render() function isn't defined.
48
45
  *
49
46
  * Advantages to inheriting from HTMLElement
50
47
  * 1. Minimization won't break when it renames the Class and we call customElements.define() on the wrong name.
@@ -59,10 +56,10 @@ export default function createSolarite(extendsTag=null) {
59
56
  if (extendsTag && !extendsTag.includes('-')) {
60
57
  extendsTag = extendsTag.toLowerCase();
61
58
 
62
- BaseClass = elementClasses[extendsTag];
59
+ BaseClass = Globals.elementClasses[extendsTag];
63
60
  if (!BaseClass) { // TODO: Use Cache
64
61
  BaseClass = document.createElement(extendsTag).constructor;
65
- elementClasses[extendsTag] = BaseClass
62
+ Globals.elementClasses[extendsTag] = BaseClass
66
63
  }
67
64
  }
68
65
 
@@ -98,26 +95,24 @@ export default function createSolarite(extendsTag=null) {
98
95
  constructor(options={}) {
99
96
  super();
100
97
 
101
-
102
-
103
98
  // TODO: Is options.render ever used?
104
99
  if (options.render===true)
105
100
  this.render();
106
101
 
107
102
  else if (options.render===false)
108
- rendered.add(this); // Don't render on connectedCallback()
103
+ Globals.rendered.add(this); // Don't render on connectedCallback()
109
104
 
110
105
  // Add children before constructor code executes.
111
106
  // PendingChildren is setup in NodeGroup.createNewComponent()
112
107
  // TODO: Match named slots.
113
- let ch = NodeGroupManager.pendingChildren.pop();
108
+ let ch = Globals.pendingChildren.pop();
114
109
  if (ch)
115
110
  (this.querySelector('slot') || this).append(...ch);
116
111
 
117
112
  /** @deprecated */
118
113
  Object.defineProperty(this, 'html', {
119
114
  set(html) {
120
- rendered.add(this);
115
+ Globals.rendered.add(this);
121
116
  if (typeof html === 'string') {
122
117
  console.warn("Assigning to this.html without the r template prefix.")
123
118
  this.innerHTML = html;
@@ -140,7 +135,7 @@ export default function createSolarite(extendsTag=null) {
140
135
  /**
141
136
  * Call render() only if it hasn't already been called. */
142
137
  renderFirstTime() {
143
- if (!rendered.has(this) && this.render)
138
+ if (!Globals.rendered.has(this) && this.render)
144
139
  this.render();
145
140
  }
146
141
 
@@ -148,8 +143,8 @@ export default function createSolarite(extendsTag=null) {
148
143
  * Called automatically by the browser. */
149
144
  connectedCallback() {
150
145
  this.renderFirstTime();
151
- if (!connected.has(this)) {
152
- connected.add(this);
146
+ if (!Globals.connected.has(this)) {
147
+ Globals.connected.add(this);
153
148
  this.onFirstConnect();
154
149
  }
155
150
  this.onConnect();
@@ -4,7 +4,7 @@
4
4
  /**
5
5
  * There are three ways to create an instance of a Solarite Component:
6
6
  * 1. new ComponentName(); // direct class instantiation
7
- * 2. this.html = r`<div><component-name></component-name></div>; // as a child of another RedComponent.
7
+ * 2. this.html = r`<div><component-name></component-name></div>; // as a child of another Component.
8
8
  * 3. <body><component-name></component-name></body> // in the Document html.
9
9
  *
10
10
  * When created via #3, Solarite has no way to pass attributes as arguments to the constructor. So to make