solarite 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Solarite-debug.js +1992 -1945
- package/dist/Solarite.js +1825 -1806
- package/dist/Solarite.min.js +2 -11
- package/package.json +11 -1
- package/src/Globals.js +14 -24
- package/src/HtmlParser.js +1 -1
- package/src/MultiValueMap.js +3 -3
- package/src/NodeGroup.js +248 -330
- package/src/Path.js +212 -0
- package/src/PathToAttribValue.js +259 -0
- package/src/PathToAttribs.js +77 -0
- package/src/PathToComment.js +8 -0
- package/src/PathToComponent.js +169 -0
- package/src/PathToEvent.js +65 -0
- package/src/PathToNodes.js +566 -0
- package/src/RootNodeGroup.js +2 -147
- package/src/Shell.js +89 -78
- package/src/Solarite.d.ts +78 -31
- package/src/Solarite.js +262 -14
- package/src/Template.js +42 -40
- package/src/Util.js +22 -38
- package/src/assert.js +5 -5
- package/src/getArg.js +26 -24
- package/src/h.js +23 -20
- package/src/hash.js +11 -9
- package/src/toEl.js +7 -8
- package/src/udomdiff.js +0 -2
- package/src/watch.js +76 -79
- package/src/ExprPath.js +0 -1117
- package/src/createSolarite.js +0 -154
package/src/Solarite.js
CHANGED
|
@@ -8,31 +8,279 @@ JavasCript UI library
|
|
|
8
8
|
https://vorticode.github.io/solarite/ */
|
|
9
9
|
import h from './h.js';
|
|
10
10
|
export default h;
|
|
11
|
-
export {default as
|
|
12
|
-
export {getArg, ArgType} from './getArg.js';
|
|
11
|
+
export {default as delve} from './delve.js';
|
|
13
12
|
export {default as Template} from './Template.js';
|
|
14
13
|
export {default as toEl} from './toEl.js';
|
|
15
|
-
|
|
14
|
+
import Template from './Template.js';
|
|
16
15
|
|
|
17
16
|
// Experimental:
|
|
18
17
|
//--------------
|
|
18
|
+
export {default as Globals} from './Globals.js';
|
|
19
|
+
export {default as SolariteUtil} from './Util.js';
|
|
20
|
+
|
|
21
|
+
// Deprecated:
|
|
22
|
+
//--------------
|
|
23
|
+
export {default as h} from './h.js'; // Named exports for h() are deprecated.
|
|
24
|
+
export {getArg, ArgType} from './getArg.js';
|
|
19
25
|
export {setArgs} from './getArg.js';
|
|
26
|
+
export {default as r} from './h.js';
|
|
27
|
+
export function t(html) {
|
|
28
|
+
return new Template([html], []);
|
|
29
|
+
}
|
|
30
|
+
//export {default as watch, renderWatched} from './watch.js'; // unfinished
|
|
31
|
+
|
|
32
|
+
// Only used for tests:
|
|
33
|
+
//--------------
|
|
34
|
+
export {default as HtmlParser} from './HtmlParser.js';
|
|
35
|
+
export {default as NodeGroup} from './NodeGroup.js';
|
|
36
|
+
export {default as Shell} from './Shell.js';
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
20
40
|
|
|
21
|
-
|
|
41
|
+
|
|
42
|
+
// Solarite Class:
|
|
43
|
+
//--------------
|
|
44
|
+
import Util from "./Util.js";
|
|
45
|
+
import Globals from "./Globals.js";
|
|
22
46
|
|
|
23
47
|
/**
|
|
24
|
-
*
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
48
|
+
* Intercept the construct call to auto-define the class before the constructor is called. */
|
|
49
|
+
let HTMLElementAutoDefine = new Proxy(HTMLElement, {
|
|
50
|
+
construct(Parent, args, Class) {
|
|
51
|
+
|
|
52
|
+
// 1. Call customElements.define() automatically.
|
|
53
|
+
Util.defineClass(Class);
|
|
54
|
+
|
|
55
|
+
// 2. This line is equivalent the to super() call to HTMLElement:
|
|
56
|
+
return Reflect.construct(Parent, args, Class);
|
|
29
57
|
}
|
|
30
58
|
});
|
|
31
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Solarite provides more features if your web component extends Solarite instead of HTMLElement.
|
|
62
|
+
*
|
|
63
|
+
* Reasons to inherit from Solarite instead of HTMLElement.
|
|
64
|
+
* 1. customElements.define() is called automatically when you create the first instance.
|
|
65
|
+
* 2. Calls render() when added to the DOM, if it hasn't been called already.
|
|
66
|
+
* 3. Populates the attribs argument to the constructor when instantiated from regular html outside a template string.
|
|
67
|
+
* It parses JSON from DOM attribute values surrouned with '${...}'
|
|
68
|
+
* 4. Shows an error if render() isn't defined.
|
|
69
|
+
*
|
|
70
|
+
* Advantages to inheriting from HTMLElement
|
|
71
|
+
* 1. We can inherit from things like HTMLTableRowElement directly.
|
|
72
|
+
* 2. There's less magic, since everyone is familiar with defining custom elements.
|
|
73
|
+
* 3. No confusion about how the class name becomes a tag name.
|
|
74
|
+
* @extends {HTMLElement} */
|
|
75
|
+
export class Solarite extends HTMLElementAutoDefine {
|
|
32
76
|
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
77
|
+
/**
|
|
78
|
+
* @param attribs {?Record<string, any>} */
|
|
79
|
+
constructor(attribs=null) {
|
|
80
|
+
super();
|
|
81
|
+
|
|
82
|
+
if (attribs) {
|
|
83
|
+
if (typeof attribs !== 'object')
|
|
84
|
+
throw new Error('First argument to custom element constructor must be an object.');
|
|
85
|
+
|
|
86
|
+
// 1. Populate attribs if it's an empty object.
|
|
87
|
+
if (attribs && !Object.keys(attribs).length) {
|
|
88
|
+
let attribs2 = Solarite.getAttribs(this);
|
|
89
|
+
for (let name in attribs2) {
|
|
90
|
+
attribs[name] = attribs2[name];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
37
93
|
|
|
38
|
-
//
|
|
94
|
+
// 2. Populate fields from attribs.
|
|
95
|
+
// This does nothing because the fields are overwritten by the child class after this super() constructor executes.
|
|
96
|
+
//for (let name in attribs || {}) {
|
|
97
|
+
// if (name in this) {
|
|
98
|
+
// const descriptor = Object.getOwnPropertyDescriptor(this, name);
|
|
99
|
+
// if (!descriptor || descriptor.writable || descriptor.set)
|
|
100
|
+
// this[name] = attribs[name];
|
|
101
|
+
// }
|
|
102
|
+
//}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 3. Wrap render function so it always provides the attribs argument.
|
|
106
|
+
// Disabled because this gives us strings for attribute values when we call render manually.
|
|
107
|
+
// Instead of values given from ${...} expressions.
|
|
108
|
+
// let originalRender = this.render;
|
|
109
|
+
// this.render = (attribs, changed=true) => {
|
|
110
|
+
// if (!attribs) // If we have to look up the attribs, we don't know if they changed or not.
|
|
111
|
+
// attribs = Solarite.getAttribs(this);
|
|
112
|
+
// originalRender.call(this, attribs, changed);
|
|
113
|
+
// }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
'render'() {
|
|
117
|
+
throw new Error('render() is not defined for ' + this.constructor.name);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Call render() only if it hasn't already been called. */
|
|
122
|
+
'renderFirstTime'() {
|
|
123
|
+
if (!Globals.rendered.has(this)) {
|
|
124
|
+
let attribs = Solarite.getAttribs(this);
|
|
125
|
+
this.render(attribs); // calls Globals.rendered.add(this); inside the call to h()'...'.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Called automatically by the browser. */
|
|
131
|
+
'connectedCallback'() { // quoted so terser doesn't remove it.
|
|
132
|
+
this.renderFirstTime();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
static 'define'(tagName=null) {
|
|
136
|
+
Util.defineClass(this, tagName);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
static 'getAttribs'(el) {
|
|
140
|
+
let result = Util.attribsToObject(el);
|
|
141
|
+
for (let name in result) {
|
|
142
|
+
let val = result[name];
|
|
143
|
+
if (val.startsWith('${') && val.endsWith('}'))
|
|
144
|
+
result[name] = JSON.parse(val.slice(2, -1));
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
// TODO: Do we want to use this to get the tag name from the render() function, instead of having the user define it?
|
|
151
|
+
/**
|
|
152
|
+
* Get the tag name for a class, as defined by the tag used in render().
|
|
153
|
+
*
|
|
154
|
+
* This will parse the JavaScript code of the render() function to find the tag name.
|
|
155
|
+
* It will itarage every character, keeping track of quotes and comments so it can
|
|
156
|
+
* skip them until it finds the tag name passed to h(this)`<tagname>` inside the render() function.
|
|
157
|
+
*
|
|
158
|
+
* */
|
|
159
|
+
/*
|
|
160
|
+
static getTagName(Class) {
|
|
161
|
+
let code = Class.prototype.render.toString();
|
|
162
|
+
let i = 0;
|
|
163
|
+
while (i < code.length) {
|
|
164
|
+
let char = code[i];
|
|
165
|
+
let next = code[i + 1];
|
|
166
|
+
|
|
167
|
+
// Skip single line comments
|
|
168
|
+
if (char === '/' && next === '/') {
|
|
169
|
+
i = code.indexOf('\n', i);
|
|
170
|
+
if (i === -1) break;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
// Skip multi-line comments
|
|
174
|
+
if (char === '/' && next === '*') {
|
|
175
|
+
i = code.indexOf('*'+'/', i + 2);
|
|
176
|
+
if (i === -1) break;
|
|
177
|
+
i += 2;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
// Skip strings and template literals
|
|
181
|
+
if (char === "'" || char === '"' || char === '`') {
|
|
182
|
+
let quote = char;
|
|
183
|
+
i++;
|
|
184
|
+
while (i < code.length) {
|
|
185
|
+
if (code[i] === '\\') i += 2;
|
|
186
|
+
else if (code[i] === quote) { i++; break; }
|
|
187
|
+
else i++;
|
|
188
|
+
}
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
// Skip regex literals (simple heuristic)
|
|
192
|
+
if (char === '/') {
|
|
193
|
+
let prev = code.slice(Math.max(0, i - 10), i).trim();
|
|
194
|
+
// If / is preceded by something that indicates an operator or start of expression
|
|
195
|
+
if (/[=(,;:[!&|?]$|return$|yield$|case$/.test(prev)) {
|
|
196
|
+
i++;
|
|
197
|
+
while (i < code.length) {
|
|
198
|
+
if (code[i] === '\\') i += 2;
|
|
199
|
+
else if (code[i] === '[') { // Skip character classes
|
|
200
|
+
i++;
|
|
201
|
+
while (i < code.length && code[i] !== ']') {
|
|
202
|
+
if (code[i] === '\\') i += 2;
|
|
203
|
+
else i++;
|
|
204
|
+
}
|
|
205
|
+
i++;
|
|
206
|
+
}
|
|
207
|
+
else if (code[i] === '/') { i++; break; }
|
|
208
|
+
else i++;
|
|
209
|
+
}
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Check for h(this)`
|
|
214
|
+
if (char === 'h' && code.slice(i, i + 8) === 'h(this)`') {
|
|
215
|
+
i += 8;
|
|
216
|
+
// We are now inside the template literal.
|
|
217
|
+
// Skip whitespace and HTML comments
|
|
218
|
+
while (i < code.length) {
|
|
219
|
+
// Skip JS template literal end (shouldn't happen before tag, but for safety)
|
|
220
|
+
if (code[i] === '`') return null;
|
|
221
|
+
|
|
222
|
+
// Skip whitespace
|
|
223
|
+
if (/\s/.test(code[i])) { i++; continue; }
|
|
224
|
+
|
|
225
|
+
// Skip HTML comments <!-- ... -->
|
|
226
|
+
if (code.slice(i, i + 4) === '<!--') {
|
|
227
|
+
i = code.indexOf('-->', i + 4);
|
|
228
|
+
if (i === -1) return null;
|
|
229
|
+
i += 3;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Find the first tag
|
|
234
|
+
if (code[i] === '<') {
|
|
235
|
+
let start = ++i;
|
|
236
|
+
while (i < code.length && /[a-zA-Z0-9-]/.test(code[i])) i++;
|
|
237
|
+
return code.slice(start, i);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// If we encounter anything else (like text before a tag),
|
|
241
|
+
// we can keep looking or return null depending on how strict we want to be.
|
|
242
|
+
// For now, let's just skip non-tag characters.
|
|
243
|
+
i++;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
i++;
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
*/
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Assign fields from `src` to `dest` if they exist in `dest` and their names are not in the `ignore` list.
|
|
256
|
+
* When a value in `src` is a string and the existing value in `dest` is a boolean, number, or Date,
|
|
257
|
+
* it will be converted to that type.
|
|
258
|
+
* This is often used in class constructors that accept an object of arguments.
|
|
259
|
+
* @param {object} dest
|
|
260
|
+
* @param {?object} src
|
|
261
|
+
* @param {string[]} [ignore=[]] */
|
|
262
|
+
export function assignFields(dest, src, ignore=[]) {
|
|
263
|
+
for (let name in src || {}) {
|
|
264
|
+
if (name in dest && !ignore.includes(name)) {
|
|
265
|
+
const descriptor = Object.getOwnPropertyDescriptor(dest, name)
|
|
266
|
+
|| Object.getOwnPropertyDescriptor(Object.getPrototypeOf(dest), name); // Also find (parent?) setters. Is this necssary?
|
|
267
|
+
if (!descriptor || descriptor.writable || descriptor.set) {
|
|
268
|
+
let srcVal = src[name];
|
|
269
|
+
let destVal = dest[name];
|
|
270
|
+
if (typeof src[name] === 'string') {
|
|
271
|
+
if (typeof destVal === 'boolean') // empty string (when an attribute is present with no value) is true
|
|
272
|
+
dest[name] = ![false, 'false', 0, '0'].includes(srcVal);
|
|
273
|
+
else if (typeof destVal === 'number')
|
|
274
|
+
dest[name] = Number(srcVal);
|
|
275
|
+
else if (destVal instanceof Date) {
|
|
276
|
+
dest[name] = new Date(srcVal); // TODO: read it as UTC 0 by default.
|
|
277
|
+
}
|
|
278
|
+
else
|
|
279
|
+
dest[name] = srcVal;
|
|
280
|
+
}
|
|
281
|
+
else
|
|
282
|
+
dest[name] = srcVal;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
package/src/Template.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import assert from "./assert.js";
|
|
2
2
|
import {getObjectHash, getObjectId} from "./hash.js";
|
|
3
3
|
import Globals from "./Globals.js";
|
|
4
4
|
import RootNodeGroup from "./RootNodeGroup.js";
|
|
5
|
-
import Util from "./Util.js";
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* The html strings and evaluated expressions from an html tagged template.
|
|
@@ -10,23 +9,27 @@ import Util from "./Util.js";
|
|
|
10
9
|
* Although the reference to the html strings is shared among templates. */
|
|
11
10
|
export default class Template {
|
|
12
11
|
|
|
13
|
-
/** @type {
|
|
14
|
-
exprs = []
|
|
12
|
+
/** @type {Expr[]} Evaulated expressions. */
|
|
13
|
+
'exprs' = []
|
|
15
14
|
|
|
16
15
|
/** @type {string[]} */
|
|
17
|
-
html = [];
|
|
16
|
+
'html' = [];
|
|
18
17
|
|
|
19
18
|
/** @type {Array} Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
|
|
20
19
|
hashedFields;
|
|
21
20
|
|
|
21
|
+
closeKey;
|
|
22
|
+
exactKey;
|
|
23
|
+
|
|
22
24
|
isText;
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
*
|
|
26
28
|
* @param htmlStrings {string[]}
|
|
27
29
|
* @param exprs {*[]} */
|
|
28
|
-
constructor(htmlStrings, exprs) {
|
|
30
|
+
constructor(htmlStrings=[''], exprs=[]) {
|
|
29
31
|
this.html = htmlStrings;
|
|
32
|
+
|
|
30
33
|
this.exprs = exprs;
|
|
31
34
|
|
|
32
35
|
//this.trace = new Error().stack.split(/\n/g)
|
|
@@ -50,51 +53,50 @@ export default class Template {
|
|
|
50
53
|
* Called by JSON.serialize when it encounters a Template.
|
|
51
54
|
* This prevents the hashed version from being too large. */
|
|
52
55
|
toJSON() {
|
|
53
|
-
if (
|
|
56
|
+
if (this.hashedFields===undefined)
|
|
54
57
|
this.hashedFields = [getObjectId(this.html), this.exprs];
|
|
55
58
|
|
|
56
59
|
return this.hashedFields
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
/**
|
|
60
|
-
* Render the main
|
|
61
|
-
* @param el {HTMLElement}
|
|
63
|
+
* Render the main (root) template.
|
|
64
|
+
* @param el {?HTMLElement} Null if we're rendering to a standalone element.
|
|
62
65
|
* @param options {RenderOptions}
|
|
63
66
|
* @return {?DocumentFragment|HTMLElement} */
|
|
64
|
-
render(el=null, options={}) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
el
|
|
74
|
-
Globals.nodeGroups.set(el, ng); // Why was this commented out?
|
|
75
|
-
firstTime = true;
|
|
67
|
+
'render'(el=null, options={}) {
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
let ng = el && Globals.rootNodeGroups.get(el);
|
|
72
|
+
if (!ng) {
|
|
73
|
+
ng = new RootNodeGroup(this, null, el, options);
|
|
74
|
+
if (!el) // null if it's a standalone elment.
|
|
75
|
+
el = ng.getRootNode();
|
|
76
|
+
Globals.rootNodeGroups.set(el, ng); // All tests still pass if this is commented out!
|
|
76
77
|
}
|
|
77
|
-
else {
|
|
78
|
-
ng = Globals.nodeGroups.get(el);
|
|
79
|
-
if (!ng) {
|
|
80
|
-
ng = new RootNodeGroup(this, el, options);
|
|
81
|
-
Globals.nodeGroups.set(el, ng); // Why was this commented out?
|
|
82
|
-
firstTime = true;
|
|
83
|
-
}
|
|
84
78
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
79
|
+
// Make sure the expresion count matches match the Path "hole" count.
|
|
80
|
+
// This can happen if we try manually rendering one template to a NodeGroup that was created expecting a different template.
|
|
81
|
+
// These don't always have the same length, for example if one attribute has multiple expressions.
|
|
82
|
+
// if (ng.paths.length === 0 && this.exprs.length || ng.paths.length > this.exprs.length)
|
|
83
|
+
// throw new Error(
|
|
84
|
+
// `Solarite Error: Parent HTMLElement ${ng.template.html.join('${...}')} and ${ng.paths.length} \${value} ` +
|
|
85
|
+
// `placeholders can't accomodate a Template with ${this.exprs.length} values.`);
|
|
89
86
|
|
|
90
87
|
// Creating the root nodegroup also renders it.
|
|
91
88
|
// If we didn't just create it, we need to render it.
|
|
92
|
-
if (!
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
89
|
+
if (this.html?.length === 1 && !this.html[0]) // An empty string.
|
|
90
|
+
el.innerHTML = ''; // Fast path for empty component.
|
|
91
|
+
else {
|
|
92
|
+
|
|
93
|
+
let oldKey = ng.exactKey;
|
|
94
|
+
let newKey = this.getExactKey();
|
|
95
|
+
ng.applyExprs(this.exprs, oldKey !== newKey);
|
|
96
|
+
ng.exactKey = newKey;
|
|
97
|
+
|
|
98
|
+
//if (firstTime)
|
|
99
|
+
// ng.instantiateStaticComponents(ng.staticComponents);
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
ng.exprsToRender = new Map();
|
|
@@ -102,7 +104,7 @@ export default class Template {
|
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
getExactKey() {
|
|
105
|
-
if (
|
|
107
|
+
if (this.exactKey===undefined) {
|
|
106
108
|
if (this.exprs.length)
|
|
107
109
|
this.exactKey = getObjectHash(this);// calls this.toJSON().
|
|
108
110
|
else // Don't hash plain html.
|
|
@@ -113,7 +115,7 @@ export default class Template {
|
|
|
113
115
|
|
|
114
116
|
getCloseKey() {
|
|
115
117
|
//console.log(this.exprs.length)
|
|
116
|
-
if (
|
|
118
|
+
if (this.closeKey===undefined) {
|
|
117
119
|
if (this.exprs.length)
|
|
118
120
|
this.closeKey = /*'@' + */this.toJSON()[0];
|
|
119
121
|
else
|
package/src/Util.js
CHANGED
|
@@ -20,6 +20,8 @@ let Util = {
|
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Convert HTMLElement attributes to an object.
|
|
23
|
+
* Converts dash (kebob-case) attribute names to camelCase.
|
|
24
|
+
* See also Solarite.getAttribs()
|
|
23
25
|
* @param el {HTMLElement}
|
|
24
26
|
* @param ignore {?string} Optionally ignore this attribute.
|
|
25
27
|
* @return {Object} */
|
|
@@ -135,43 +137,14 @@ let Util = {
|
|
|
135
137
|
return str.replace(/-([a-z])/g, g => g[1].toUpperCase());
|
|
136
138
|
},
|
|
137
139
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
*
|
|
147
|
-
* This function does not create a new array for the flattened values. Instead,
|
|
148
|
-
* it lazily yields each item as it is encountered. This can be more memory-efficient
|
|
149
|
-
* for large or deeply nested structures.
|
|
150
|
-
*
|
|
151
|
-
* @param {any} value - The value to flatten. Can be an array, object, function, or primitive.
|
|
152
|
-
* @yields {any} - The next item in the flattened structure.
|
|
153
|
-
*
|
|
154
|
-
* @example
|
|
155
|
-
* const complexArray = [
|
|
156
|
-
* 1,
|
|
157
|
-
* [2, () => 3, [4, () => [5, 6]], { a: 'object' }],
|
|
158
|
-
* () => () => 7,
|
|
159
|
-
* () => [() => 8, 9],
|
|
160
|
-
* ]; *
|
|
161
|
-
* for (const item of flatten(complexArray))
|
|
162
|
-
* console.log(item); // Outputs: 1, 2, 3, 4, 5, 6, { a: 'object' }, 7, 8, 9
|
|
163
|
-
*/
|
|
164
|
-
// *flatten(value) {
|
|
165
|
-
// if (Array.isArray(value)) {
|
|
166
|
-
// for (const item of value) {
|
|
167
|
-
// yield* Util.flatten(item); // Recursively flatten arrays
|
|
168
|
-
// }
|
|
169
|
-
// } else if (typeof value === 'function') {
|
|
170
|
-
// const result = value();
|
|
171
|
-
// yield* Util.flatten(result); // Recursively flatten the result of a function
|
|
172
|
-
// } else
|
|
173
|
-
// yield value; // Yield primitive values as is
|
|
174
|
-
// },
|
|
140
|
+
defineClass(Class, tagName) {
|
|
141
|
+
if (!customElements[getName](Class)) { // If not previously defined.
|
|
142
|
+
tagName = tagName || Util.camelToDashes(Class.name)
|
|
143
|
+
if (!tagName.includes('-')) // Browsers require that web components always have a dash in the name.
|
|
144
|
+
tagName += '-element';
|
|
145
|
+
customElements[define](tagName, Class)
|
|
146
|
+
}
|
|
147
|
+
},
|
|
175
148
|
|
|
176
149
|
/**
|
|
177
150
|
* Get the value of an input as the most appropriate JavaScript type.
|
|
@@ -261,7 +234,7 @@ let Util = {
|
|
|
261
234
|
|
|
262
235
|
/**
|
|
263
236
|
* Use an array as the value of a map, appending to it when we add.
|
|
264
|
-
* Used by watch.js.
|
|
237
|
+
* Used only by watch.js.
|
|
265
238
|
* @param map {Map|WeakMap|Object}
|
|
266
239
|
* @param key
|
|
267
240
|
* @param value */
|
|
@@ -275,6 +248,11 @@ let Util = {
|
|
|
275
248
|
result.push(value);
|
|
276
249
|
},
|
|
277
250
|
|
|
251
|
+
saveOrphans(nodes) {
|
|
252
|
+
let fragment = Globals.doc.createDocumentFragment();
|
|
253
|
+
fragment.append(...nodes);
|
|
254
|
+
},
|
|
255
|
+
|
|
278
256
|
/**
|
|
279
257
|
* Remove nodes from the beginning and end that are not:
|
|
280
258
|
* 1. Elements.
|
|
@@ -301,6 +279,12 @@ let Util = {
|
|
|
301
279
|
}
|
|
302
280
|
};
|
|
303
281
|
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
// Trick to prevent minifier from renaming these methods.
|
|
285
|
+
let define = 'define';
|
|
286
|
+
let getName = 'getName';
|
|
287
|
+
|
|
304
288
|
export default Util;
|
|
305
289
|
|
|
306
290
|
|
package/src/assert.js
CHANGED
package/src/getArg.js
CHANGED
|
@@ -2,10 +2,11 @@ import Util from "./Util.js";
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
+
* @deprecated Inherit from Solarite and pass arribs to super() instead.
|
|
5
6
|
* There are three ways to create an instance of a Solarite Component:
|
|
6
|
-
* 1. new ComponentName();
|
|
7
|
-
* 2. this
|
|
8
|
-
* 3. <body><component-name></component-name></body>
|
|
7
|
+
* 1. new ComponentName(3); // direct class instantiation
|
|
8
|
+
* 2. h(this)`<div><component-name user-id=${3}></component-name></div>; // as a child of another Component.
|
|
9
|
+
* 3. <body><component-name user-id="3"></component-name></body> // in the Document html.
|
|
9
10
|
*
|
|
10
11
|
* When created via #3, Solarite has no way to pass attributes as arguments to the constructor. So to make
|
|
11
12
|
* sure we get the correct value via all three paths, we write our constructors according to the following
|
|
@@ -13,40 +14,38 @@ import Util from "./Util.js";
|
|
|
13
14
|
* Browsers make all html attribute names lowercase.
|
|
14
15
|
*
|
|
15
16
|
* @example
|
|
16
|
-
* constructor({name,
|
|
17
|
+
* constructor({name, userId=1}={}) {
|
|
17
18
|
* super();
|
|
18
19
|
*
|
|
19
20
|
* // Get value from "name" attriute if persent, otherwise from name constructor arg.
|
|
20
21
|
* this.name = getArg(this, 'name', name);
|
|
21
22
|
*
|
|
22
23
|
* // Optionally convert the value to an integer.
|
|
23
|
-
* this.userId = getArg(this, '
|
|
24
|
+
* this.userId = getArg(this, 'user-id', userId, ArgType.Int);
|
|
24
25
|
* }
|
|
25
26
|
*
|
|
26
27
|
* @param el {HTMLElement}
|
|
27
28
|
* @param attributeName {string} Attribute name. Not case-sensitive.
|
|
28
|
-
* @param defaultValue {*} Default value to use if attribute doesn't exist.
|
|
29
|
+
* @param defaultValue {*} Default value to use if attribute doesn't exist. Typically the argument from the constructor.
|
|
29
30
|
* @param type {ArgType|function|Class|*[]}
|
|
30
31
|
* If an array, use the value if it's in the array, otherwise return undefined.
|
|
31
32
|
* If it's a function, pass the value to the function and return the result.
|
|
32
|
-
* @
|
|
33
|
-
|
|
34
|
-
* @return {*} Undefined if attribute isn't set. */
|
|
35
|
-
export function getArg(el, attributeName, defaultValue=undefined, type=ArgType.String, fallback=undefined) {
|
|
33
|
+
* @return {*} Undefined if attribute isn't set and there's no defaultValue, or if the value couldn't be parsed as the type. */
|
|
34
|
+
export function getArg(el, attributeName, defaultValue=undefined, type=ArgType.String) {
|
|
36
35
|
let val = defaultValue;
|
|
37
36
|
let attrVal = el.getAttribute(attributeName) || el.getAttribute(Util.camelToDashes(attributeName));
|
|
38
37
|
if (attrVal !== null) // If attribute doesn't exist.
|
|
39
38
|
val = attrVal;
|
|
40
|
-
|
|
39
|
+
|
|
41
40
|
if (Array.isArray(type))
|
|
42
|
-
return type.includes(val) ? val :
|
|
43
|
-
|
|
41
|
+
return type.includes(val) ? val : undefined;
|
|
42
|
+
|
|
44
43
|
if (typeof type === 'function') {
|
|
45
44
|
return type.constructor
|
|
46
45
|
? new type(val) // arg type is custom Class
|
|
47
46
|
: type(val); // arg type is custom function
|
|
48
47
|
}
|
|
49
|
-
|
|
48
|
+
|
|
50
49
|
// If bool, it's true as long as it exists and its value isn't falsey.
|
|
51
50
|
if (type===ArgType.Bool) {
|
|
52
51
|
let lAttrVal = typeof val === 'string' ? val.toLowerCase() : val;
|
|
@@ -54,20 +53,17 @@ export function getArg(el, attributeName, defaultValue=undefined, type=ArgType.S
|
|
|
54
53
|
return false;
|
|
55
54
|
if (['true', true].includes(lAttrVal) || parseFloat(lAttrVal) !== 0)
|
|
56
55
|
return true;
|
|
57
|
-
return
|
|
56
|
+
return undefined;
|
|
58
57
|
}
|
|
59
|
-
|
|
58
|
+
|
|
60
59
|
// Attribute doesn't exist
|
|
61
|
-
let result;
|
|
62
60
|
switch (type) {
|
|
63
61
|
case ArgType.Int:
|
|
64
|
-
|
|
65
|
-
return isNaN(result) ? fallback : result;
|
|
62
|
+
return parseInt(val);
|
|
66
63
|
case ArgType.Float:
|
|
67
|
-
|
|
68
|
-
return isNaN(result) ? fallback : result;
|
|
64
|
+
return parseFloat(val);
|
|
69
65
|
case ArgType.String:
|
|
70
|
-
return [undefined, null, false].includes(val) ? '' : val+'';
|
|
66
|
+
return [undefined, null, false].includes(val) ? '' : (val+'');
|
|
71
67
|
case ArgType.Json:
|
|
72
68
|
case ArgType.Eval:
|
|
73
69
|
if (typeof val === 'string' && val.length)
|
|
@@ -89,6 +85,7 @@ export function getArg(el, attributeName, defaultValue=undefined, type=ArgType.S
|
|
|
89
85
|
|
|
90
86
|
|
|
91
87
|
/**
|
|
88
|
+
* @deprecated for Solarite.getAttribs()
|
|
92
89
|
* Experimental. Set multiple arguments/attributes all at once.
|
|
93
90
|
* @param el {HTMLElement}
|
|
94
91
|
* @param args {Record<string, any>}
|
|
@@ -97,6 +94,10 @@ export function getArg(el, attributeName, defaultValue=undefined, type=ArgType.S
|
|
|
97
94
|
* @example
|
|
98
95
|
* constructor({user, path}={}) {
|
|
99
96
|
* setArgs(this, arguments[0], {user: User, path: ArgType.String});
|
|
97
|
+
*
|
|
98
|
+
* // Equivalent to:
|
|
99
|
+
* this.user = getArg(this, user, 'user', User); // or new User(user);
|
|
100
|
+
* this.path = getArg(this, path, 'path', ArgType.String);
|
|
100
101
|
* }
|
|
101
102
|
*/
|
|
102
103
|
export function setArgs(el, args, types) {
|
|
@@ -106,15 +107,16 @@ export function setArgs(el, args, types) {
|
|
|
106
107
|
|
|
107
108
|
|
|
108
109
|
/**
|
|
110
|
+
* @deprecated
|
|
109
111
|
* @enum */
|
|
110
112
|
var ArgType = {
|
|
111
|
-
|
|
113
|
+
|
|
112
114
|
/**
|
|
113
115
|
* false, 0, null, undefined, '0', and 'false' (case-insensitive) become false.
|
|
114
116
|
* Anything else, including empty string becomes true.
|
|
115
117
|
* Empty string is true because attributes with no value should be evaulated as true. */
|
|
116
118
|
Bool: 'Bool',
|
|
117
|
-
|
|
119
|
+
|
|
118
120
|
Int: 'Int',
|
|
119
121
|
Float: 'Float',
|
|
120
122
|
String: 'String',
|