solarite 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Solarite-debug.js +2149 -2090
- package/dist/Solarite.js +1895 -1821
- package/dist/Solarite.min.js +2 -2
- package/package.json +12 -2
- package/readme.md +2 -2
- package/src/Globals.js +18 -24
- package/src/HtmlParser.js +1 -1
- package/src/MultiValueMap.js +3 -3
- package/src/NodeGroup.js +238 -483
- 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 +57 -0
- package/src/PathToNodes.js +566 -0
- package/src/RootNodeGroup.js +8 -0
- package/src/Shell.js +92 -83
- package/src/Solarite.d.ts +73 -29
- package/src/Solarite.js +133 -21
- package/src/Template.js +173 -42
- package/src/Util.js +74 -52
- package/src/assert.js +5 -5
- package/src/getArg.js +26 -24
- package/src/h.js +83 -128
- package/src/hash.js +11 -9
- package/src/toEl.js +83 -0
- package/src/udomdiff.js +0 -57
- package/src/watch.js +78 -81
- package/src/ExprPath.js +0 -1109
- package/src/createSolarite.js +0 -154
package/src/Template.js
CHANGED
|
@@ -1,7 +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
|
-
import
|
|
4
|
+
import RootNodeGroup from "./RootNodeGroup.js";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The html strings and evaluated expressions from an html tagged template.
|
|
@@ -9,24 +9,24 @@ import {RootNodeGroup} from "./NodeGroup.js";
|
|
|
9
9
|
* Although the reference to the html strings is shared among templates. */
|
|
10
10
|
export default class Template {
|
|
11
11
|
|
|
12
|
-
/** @type {
|
|
13
|
-
exprs = []
|
|
12
|
+
/** @type {Expr[]} Evaulated expressions. */
|
|
13
|
+
'exprs' = []
|
|
14
14
|
|
|
15
15
|
/** @type {string[]} */
|
|
16
|
-
html = [];
|
|
16
|
+
'html' = [];
|
|
17
17
|
|
|
18
18
|
/** @type {Array} Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
|
|
19
19
|
hashedFields;
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
nodeGroup;
|
|
21
|
+
isText;
|
|
23
22
|
|
|
24
23
|
/**
|
|
25
24
|
*
|
|
26
25
|
* @param htmlStrings {string[]}
|
|
27
26
|
* @param exprs {*[]} */
|
|
28
|
-
constructor(htmlStrings, exprs) {
|
|
27
|
+
constructor(htmlStrings=[''], exprs=[]) {
|
|
29
28
|
this.html = htmlStrings;
|
|
29
|
+
|
|
30
30
|
this.exprs = exprs;
|
|
31
31
|
|
|
32
32
|
//this.trace = new Error().stack.split(/\n/g)
|
|
@@ -50,51 +50,50 @@ export default class Template {
|
|
|
50
50
|
* Called by JSON.serialize when it encounters a Template.
|
|
51
51
|
* This prevents the hashed version from being too large. */
|
|
52
52
|
toJSON() {
|
|
53
|
-
if (
|
|
53
|
+
if (this.hashedFields===undefined)
|
|
54
54
|
this.hashedFields = [getObjectId(this.html), this.exprs];
|
|
55
55
|
|
|
56
56
|
return this.hashedFields
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
* Render the main
|
|
61
|
-
* @param el {HTMLElement}
|
|
60
|
+
* Render the main (root) template.
|
|
61
|
+
* @param el {?HTMLElement} Null if we're rendering to a standalone element.
|
|
62
62
|
* @param options {RenderOptions}
|
|
63
63
|
* @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;
|
|
64
|
+
'render'(el=null, options={}) {
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
let ng = el && Globals.rootNodeGroups.get(el);
|
|
69
|
+
if (!ng) {
|
|
70
|
+
ng = new RootNodeGroup(this, null, el, options);
|
|
71
|
+
if (!el) // null if it's a standalone elment.
|
|
72
|
+
el = ng.getRootNode();
|
|
73
|
+
Globals.rootNodeGroups.set(el, ng); // All tests still pass if this is commented out!
|
|
76
74
|
}
|
|
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
75
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
76
|
+
// Make sure the expresion count matches match the Path "hole" count.
|
|
77
|
+
// This can happen if we try manually rendering one template to a NodeGroup that was created expecting a different template.
|
|
78
|
+
// These don't always have the same length, for example if one attribute has multiple expressions.
|
|
79
|
+
// if (ng.paths.length === 0 && this.exprs.length || ng.paths.length > this.exprs.length)
|
|
80
|
+
// throw new Error(
|
|
81
|
+
// `Solarite Error: Parent HTMLElement ${ng.template.html.join('${...}')} and ${ng.paths.length} \${value} ` +
|
|
82
|
+
// `placeholders can't accomodate a Template with ${this.exprs.length} values.`);
|
|
89
83
|
|
|
90
84
|
// Creating the root nodegroup also renders it.
|
|
91
85
|
// If we didn't just create it, we need to render it.
|
|
92
|
-
if (!
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
86
|
+
if (this.html?.length === 1 && !this.html[0]) // An empty string.
|
|
87
|
+
el.innerHTML = ''; // Fast path for empty component.
|
|
88
|
+
else {
|
|
89
|
+
|
|
90
|
+
let oldKey = ng.exactKey;
|
|
91
|
+
let newKey = this.getExactKey();
|
|
92
|
+
ng.applyExprs(this.exprs, oldKey !== newKey);
|
|
93
|
+
ng.exactKey = newKey;
|
|
94
|
+
|
|
95
|
+
//if (firstTime)
|
|
96
|
+
// ng.instantiateStaticComponents(ng.staticComponents);
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
ng.exprsToRender = new Map();
|
|
@@ -102,7 +101,7 @@ export default class Template {
|
|
|
102
101
|
}
|
|
103
102
|
|
|
104
103
|
getExactKey() {
|
|
105
|
-
if (
|
|
104
|
+
if (this.exactKey===undefined) {
|
|
106
105
|
if (this.exprs.length)
|
|
107
106
|
this.exactKey = getObjectHash(this);// calls this.toJSON().
|
|
108
107
|
else // Don't hash plain html.
|
|
@@ -113,7 +112,7 @@ export default class Template {
|
|
|
113
112
|
|
|
114
113
|
getCloseKey() {
|
|
115
114
|
//console.log(this.exprs.length)
|
|
116
|
-
if (
|
|
115
|
+
if (this.closeKey===undefined) {
|
|
117
116
|
if (this.exprs.length)
|
|
118
117
|
this.closeKey = /*'@' + */this.toJSON()[0];
|
|
119
118
|
else
|
|
@@ -124,6 +123,138 @@ export default class Template {
|
|
|
124
123
|
|
|
125
124
|
return this.closeKey;
|
|
126
125
|
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param tag {string}
|
|
129
|
+
* @param props {?Record<string, any>}
|
|
130
|
+
* @param children
|
|
131
|
+
* @returns {Template} */
|
|
132
|
+
static fromJsx(tag, props, children) {
|
|
133
|
+
|
|
134
|
+
// HTML void elements that must not have closing tags
|
|
135
|
+
const isVoid = selfClosingTags.has(tag.toLowerCase());
|
|
136
|
+
|
|
137
|
+
// Build htmlStrings/exprs so Shell can place placeholders in attribute values and child content.
|
|
138
|
+
let htmlStrings = [];
|
|
139
|
+
let templateExprs = [];
|
|
140
|
+
|
|
141
|
+
// Opening tag
|
|
142
|
+
let open = `<${tag}`;
|
|
143
|
+
|
|
144
|
+
// Attributes
|
|
145
|
+
if (props && typeof props === 'object') {
|
|
146
|
+
for (let name in props) {
|
|
147
|
+
let value = props[name];
|
|
148
|
+
|
|
149
|
+
// id and data-id are static in templates — never expressions
|
|
150
|
+
if (name === 'id' || name === 'data-id') {
|
|
151
|
+
// Write directly into the opening string with quotes
|
|
152
|
+
open += ` ${name}="${value}"`;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Dynamic attribute value: functions are unquoted (e.g., onclick=${fn}), others quoted
|
|
157
|
+
if (typeof value === 'function') {
|
|
158
|
+
open += ` ${name}=`;
|
|
159
|
+
htmlStrings.push(open);
|
|
160
|
+
templateExprs.push(value);
|
|
161
|
+
// reset so subsequent attributes start fresh (e.g., ' title=')
|
|
162
|
+
open = ``;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
open += ` ${name}=`;
|
|
166
|
+
htmlStrings.push(open);
|
|
167
|
+
templateExprs.push(value);
|
|
168
|
+
// reset so subsequent attributes start fresh (e.g., ' title=')
|
|
169
|
+
open = ``;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Finalize opening tag precisely to match tagged template splitting
|
|
175
|
+
if (!isVoid) {
|
|
176
|
+
const pushedAny = htmlStrings.length > 0;
|
|
177
|
+
// If nothing pushed yet (no dynamic attrs), push the entire open + '>'
|
|
178
|
+
if (!pushedAny)
|
|
179
|
+
htmlStrings.push(open + '>');
|
|
180
|
+
else {
|
|
181
|
+
// If we were in a quoted attr (open === '"'), then the string after expr is '">' ;
|
|
182
|
+
// Otherwise (function-valued attr), the string after expr is just '>'
|
|
183
|
+
htmlStrings.push(open === '"' ? '">' : '>');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (let child of children)
|
|
187
|
+
addChild(child, htmlStrings, templateExprs);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Closing tag (not for void tags)
|
|
191
|
+
if (!isVoid) {
|
|
192
|
+
// If we never emitted the '>' for the open tag (no children were added),
|
|
193
|
+
// then it was appended above before children. Now just add the closing tag to the last html segment.
|
|
194
|
+
let lastIdx = htmlStrings.length - 1;
|
|
195
|
+
htmlStrings[lastIdx] += `</${tag}>`;
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
// Void element: ensure we emitted a trailing '>' segment
|
|
199
|
+
const pushedAny = htmlStrings.length > 0;
|
|
200
|
+
if (!pushedAny)
|
|
201
|
+
htmlStrings.push(open + '>');
|
|
202
|
+
else
|
|
203
|
+
htmlStrings.push('>');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Ensure invariant
|
|
207
|
+
//assert(htmlStrings.length === templateExprs.length + 1);
|
|
208
|
+
//console.log([htmlStrings, templateExprs])
|
|
209
|
+
return new Template(htmlStrings, templateExprs);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
const selfClosingTags = new Set(['area','base','br','col','embed','hr','img','input','link','meta','param','source','track','wbr']);
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Add child Templates that were already created via h() and Template.fromJsx()
|
|
219
|
+
* @param template {Template}
|
|
220
|
+
* @param html {string[]}
|
|
221
|
+
* @param exprs {any[]} */
|
|
222
|
+
const addChild = (template, html, exprs) => {
|
|
223
|
+
|
|
224
|
+
if (Array.isArray(template)) {
|
|
225
|
+
for (let c of template)
|
|
226
|
+
addChild(c, html, exprs);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
let flatten = false;
|
|
230
|
+
if (template instanceof Template) {
|
|
231
|
+
// Heuristic to match tagged-template splitting:
|
|
232
|
+
// - Flatten if the child has expressions (so JSX can inline attribute/value placeholders like tagged literals would).
|
|
233
|
+
// - Also flatten void elements (e.g., <img>) so they inline like literals.
|
|
234
|
+
// - Otherwise, keep as a dynamic child placeholder to match cases where the tagged template used an expression child.
|
|
235
|
+
const childHasExprs = template.exprs.length > 0;
|
|
236
|
+
if (childHasExprs)
|
|
237
|
+
flatten = true;
|
|
238
|
+
else {
|
|
239
|
+
const m = (template.html[0] || '').match(/^<([a-zA-Z][\w:-]*)/);
|
|
240
|
+
const childTag = m ? m[1].toLowerCase() : '';
|
|
241
|
+
flatten = selfClosingTags.has(childTag);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (flatten) {
|
|
246
|
+
// Flatten/interleave into current segment to match tagged template splitting
|
|
247
|
+
html[html.length - 1] += template.html[0];
|
|
248
|
+
for (let i = 0; i < template.exprs.length; i++) {
|
|
249
|
+
exprs.push(template.exprs[i]);
|
|
250
|
+
html.push(template.html[i + 1] ?? '');
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
// Keep as dynamic child
|
|
254
|
+
exprs.push(template);
|
|
255
|
+
html.push('');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
127
258
|
}
|
|
128
259
|
|
|
129
260
|
|
package/src/Util.js
CHANGED
|
@@ -18,12 +18,27 @@ let Util = {
|
|
|
18
18
|
return true; // the same.
|
|
19
19
|
},
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Convert HTMLElement attributes to an object.
|
|
23
|
+
* Converts dash (kebob-case) attribute names to camelCase.
|
|
24
|
+
* See also Solarite.getAttribs()
|
|
25
|
+
* @param el {HTMLElement}
|
|
26
|
+
* @param ignore {?string} Optionally ignore this attribute.
|
|
27
|
+
* @return {Object} */
|
|
28
|
+
attribsToObject(el, ignore=null) {
|
|
29
|
+
let result = {};
|
|
30
|
+
for (let attrib of el.attributes)
|
|
31
|
+
if (attrib.name !== ignore)
|
|
32
|
+
result[Util.dashesToCamel(attrib.name)] = attrib.value;
|
|
33
|
+
return result;
|
|
34
|
+
},
|
|
35
|
+
|
|
21
36
|
bindId(root, el) {
|
|
22
37
|
let id = el.getAttribute('data-id') || el.getAttribute('id');
|
|
23
38
|
if (id) { // If something hasn't removed the id.
|
|
24
39
|
|
|
25
40
|
// Don't allow overwriting existing class properties if they already have a non-Node value.
|
|
26
|
-
if (root[id] && !(root[id]
|
|
41
|
+
if (root[id] && !(root[id]?.nodeType))
|
|
27
42
|
throw new Error(`${root.constructor.name}.${id} already has a value. ` +
|
|
28
43
|
`Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
|
|
29
44
|
|
|
@@ -32,26 +47,50 @@ let Util = {
|
|
|
32
47
|
},
|
|
33
48
|
|
|
34
49
|
/**
|
|
50
|
+
* If the style tab has a global attribute:
|
|
51
|
+
* 1. Put it in the document head as <style data-style="tag-name">...</style>
|
|
52
|
+
* 2. Replace the :host {...} CSS selector as tag-name {...}.
|
|
53
|
+
* Otherwise keep it where it is and:
|
|
54
|
+
* 1. Add data-style="1" attribute to the root element.
|
|
55
|
+
* 2. Replace the :host {...} selector in the style as tag-name[data-style='1'] {...}
|
|
35
56
|
* @param style {HTMLStyleElement}
|
|
36
57
|
* @param root {HTMLElement} */
|
|
37
58
|
bindStyles(style, root) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
|
|
60
|
+
let tagName = root.tagName.toLowerCase();
|
|
61
|
+
let styleId, attribSelector;
|
|
62
|
+
|
|
63
|
+
if (style.hasAttribute('global') || style.hasAttribute('data-global')) {
|
|
64
|
+
styleId = tagName;
|
|
65
|
+
attribSelector = '';
|
|
66
|
+
let doc = Globals.doc || root.ownerDocument || document;
|
|
67
|
+
if (!doc.head.querySelector(`style[data-style="${styleId}"]`)) {
|
|
68
|
+
doc.head.append(style)
|
|
69
|
+
style.setAttribute('data-style', styleId);
|
|
70
|
+
}
|
|
71
|
+
else // TODO: Make sure the style has no expressions.
|
|
72
|
+
style.remove(); // already in the head.
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
let styleId = root.getAttribute('data-style');
|
|
76
|
+
if (!styleId) {
|
|
77
|
+
// Keep track of one style id for each class.
|
|
78
|
+
// TODO: Put this outside the class in a map, so it doesn't conflict with static properties.
|
|
79
|
+
if (!root.constructor.styleId)
|
|
80
|
+
root.constructor.styleId = 1;
|
|
81
|
+
styleId = root.constructor.styleId++;
|
|
82
|
+
|
|
83
|
+
root.setAttribute('data-style', styleId);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
attribSelector = `[data-style="${styleId}"]`;
|
|
47
87
|
}
|
|
48
88
|
|
|
49
89
|
// Replace ":host" with "tagName[data-style=...]" in the css.
|
|
50
|
-
let tagName = root.tagName.toLowerCase();
|
|
51
90
|
for (let child of style.childNodes) {
|
|
52
91
|
if (child.nodeType === 3) {
|
|
53
92
|
let oldText = child.textContent;
|
|
54
|
-
let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, `${tagName}
|
|
93
|
+
let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, `${tagName}${attribSelector}`)
|
|
55
94
|
if (oldText !== newText)
|
|
56
95
|
child.textContent = newText;
|
|
57
96
|
}
|
|
@@ -98,48 +137,18 @@ let Util = {
|
|
|
98
137
|
return str.replace(/-([a-z])/g, g => g[1].toUpperCase());
|
|
99
138
|
},
|
|
100
139
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
* - Otherwise it yields the value as is.
|
|
110
|
-
*
|
|
111
|
-
* This function does not create a new array for the flattened values. Instead,
|
|
112
|
-
* it lazily yields each item as it is encountered. This can be more memory-efficient
|
|
113
|
-
* for large or deeply nested structures.
|
|
114
|
-
*
|
|
115
|
-
* @param {any} value - The value to flatten. Can be an array, object, function, or primitive.
|
|
116
|
-
* @yields {any} - The next item in the flattened structure.
|
|
117
|
-
*
|
|
118
|
-
* @example
|
|
119
|
-
* const complexArray = [
|
|
120
|
-
* 1,
|
|
121
|
-
* [2, () => 3, [4, () => [5, 6]], { a: 'object' }],
|
|
122
|
-
* () => () => 7,
|
|
123
|
-
* () => [() => 8, 9],
|
|
124
|
-
* ]; *
|
|
125
|
-
* for (const item of flatten(complexArray))
|
|
126
|
-
* console.log(item); // Outputs: 1, 2, 3, 4, 5, 6, { a: 'object' }, 7, 8, 9
|
|
127
|
-
*/
|
|
128
|
-
// *flatten(value) {
|
|
129
|
-
// if (Array.isArray(value)) {
|
|
130
|
-
// for (const item of value) {
|
|
131
|
-
// yield* Util.flatten(item); // Recursively flatten arrays
|
|
132
|
-
// }
|
|
133
|
-
// } else if (typeof value === 'function') {
|
|
134
|
-
// const result = value();
|
|
135
|
-
// yield* Util.flatten(result); // Recursively flatten the result of a function
|
|
136
|
-
// } else
|
|
137
|
-
// yield value; // Yield primitive values as is
|
|
138
|
-
// },
|
|
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
|
+
},
|
|
139
148
|
|
|
140
149
|
/**
|
|
141
150
|
* Get the value of an input as the most appropriate JavaScript type.
|
|
142
|
-
* @param node {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|
|
|
151
|
+
* @param node {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|HTMLElement}
|
|
143
152
|
* @return {string|string[]|number|[]|File[]|Date|boolean} */
|
|
144
153
|
getInputValue(node) {
|
|
145
154
|
// .type is a built-in DOM property
|
|
@@ -153,6 +162,8 @@ let Util = {
|
|
|
153
162
|
return node.valueAsDate; // Date Object
|
|
154
163
|
if (node.type === 'select-multiple') // <select multiple>
|
|
155
164
|
return [...node.selectedOptions].map(option => option.value); // Array of Strings
|
|
165
|
+
if (node.hasAttribute('contenteditable'))
|
|
166
|
+
return node.innerHTML;
|
|
156
167
|
|
|
157
168
|
return node.value; // String
|
|
158
169
|
},
|
|
@@ -223,7 +234,7 @@ let Util = {
|
|
|
223
234
|
|
|
224
235
|
/**
|
|
225
236
|
* Use an array as the value of a map, appending to it when we add.
|
|
226
|
-
* Used by watch.js.
|
|
237
|
+
* Used only by watch.js.
|
|
227
238
|
* @param map {Map|WeakMap|Object}
|
|
228
239
|
* @param key
|
|
229
240
|
* @param value */
|
|
@@ -237,6 +248,11 @@ let Util = {
|
|
|
237
248
|
result.push(value);
|
|
238
249
|
},
|
|
239
250
|
|
|
251
|
+
saveOrphans(nodes) {
|
|
252
|
+
let fragment = Globals.doc.createDocumentFragment();
|
|
253
|
+
fragment.append(...nodes);
|
|
254
|
+
},
|
|
255
|
+
|
|
240
256
|
/**
|
|
241
257
|
* Remove nodes from the beginning and end that are not:
|
|
242
258
|
* 1. Elements.
|
|
@@ -263,6 +279,12 @@ let Util = {
|
|
|
263
279
|
}
|
|
264
280
|
};
|
|
265
281
|
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
// Trick to prevent minifier from renaming these methods.
|
|
285
|
+
let define = 'define';
|
|
286
|
+
let getName = 'getName';
|
|
287
|
+
|
|
266
288
|
export default Util;
|
|
267
289
|
|
|
268
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',
|