@webtides/element-js 0.7.4 → 1.0.0-alpha.10
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/README.md +2 -2
- package/docs/README.md +174 -0
- package/docs/concepts/classes.md +167 -0
- package/docs/concepts/hierarchy.md +61 -0
- package/docs/concepts/lifecycle.md +45 -0
- package/docs/concepts/reactivity.md +14 -0
- package/docs/features/computed-properties.md +28 -0
- package/docs/features/dependency-injection-context-protocol.md +132 -0
- package/docs/features/events.md +155 -0
- package/docs/features/global-styles.md +131 -0
- package/docs/features/methods.md +34 -0
- package/docs/features/properties.md +56 -0
- package/docs/features/refs.md +47 -0
- package/docs/features/renderless.md +70 -0
- package/docs/features/store.md +143 -0
- package/docs/features/styles.md +169 -0
- package/docs/features/templates.md +308 -0
- package/docs/features/watch.md +54 -0
- package/docs/getting-started/upgrade-to-v1.md +243 -0
- package/docs/guide/guide.md +112 -0
- package/index.js +9 -2
- package/package.json +19 -27
- package/src/BaseElement.js +176 -86
- package/src/StyledElement.js +66 -55
- package/src/TemplateElement.js +34 -8
- package/src/dom-parts/AttributePart.js +167 -0
- package/src/dom-parts/ChildNodePart.js +329 -0
- package/src/dom-parts/NodePart.js +38 -0
- package/src/dom-parts/Part.js +12 -0
- package/src/dom-parts/TemplatePart.js +134 -0
- package/src/dom-parts/TemplateResult.js +342 -0
- package/src/dom-parts/directives.js +111 -0
- package/src/dom-parts/html.js +12 -0
- package/src/dom-parts/render.js +10 -0
- package/src/util/AttributeParser.js +62 -29
- package/src/util/DOMHelper.js +253 -5
- package/src/util/Directive.js +42 -0
- package/src/util/GlobalStylesStore.js +66 -0
- package/src/util/SerializeStateHelper.js +103 -0
- package/src/util/Store.js +92 -9
- package/src/util/defineElement.js +2 -2
- package/src/util/toString.js +2 -2
- package/CHANGELOG.md +0 -154
- package/dist/index.js +0 -1673
- package/dist/index.js.map +0 -7
- package/src/renderer/vanilla/TemplateElement.js +0 -50
- package/src/renderer/vanilla/index.js +0 -18
- package/src/renderer/vanilla/util/directives.js +0 -78
- package/src/renderer/vanilla/util/html.js +0 -65
- package/src/renderer/vanilla/util/render.js +0 -149
- package/src/util/i18n.js +0 -20
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { COMMENT_NODE, getNodePath } from '../util/DOMHelper.js';
|
|
2
|
+
import { encodeAttribute, isObjectLike } from '../util/AttributeParser.js';
|
|
3
|
+
import { TemplatePart } from './TemplatePart.js';
|
|
4
|
+
|
|
5
|
+
const voidElements = /^(?:area|base|br|col|embed|hr|img|input|link|meta|source|track|wbr)$/i;
|
|
6
|
+
const elements = /<([a-z]+[a-z0-9:._-]*)([^>]*?)(\/?)>/g;
|
|
7
|
+
// TODO: v this will not match any values with escaped quotes like onClick='console.log("\'test")'
|
|
8
|
+
const attributes = /([^\s]*)=((?:")[^"]*(?:")|(?:')[^']*(?:')|[^\s\/>]*)|([^\s\/>]*)/g;
|
|
9
|
+
const partPositions = /[\x01\x02]/g;
|
|
10
|
+
// \x01 Node.ELEMENT_NODE
|
|
11
|
+
// \x02 Node.ATTRIBUTE_NODE
|
|
12
|
+
// \x03 COMMENT.ATTRIBUTE_TOKEN
|
|
13
|
+
// \x04 Node.ATTRIBUTE_TOKEN
|
|
14
|
+
|
|
15
|
+
const interpolation = new RegExp(`(<!--dom-part-(\\d+)--><!--/dom-part-(\\d+)-->|(\\S[-\\w]+)="\x04(")?|\x04)`, 'g');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Given a template, find part positions as both nodes and attributes and
|
|
19
|
+
* return a string with placeholders as either comment nodes or named attributes.
|
|
20
|
+
* @param {TemplateStringsArray | string[]} templateStrings a template literal tag array
|
|
21
|
+
* @param {string} attributePlaceholders replace placeholders inside attribute values with this value
|
|
22
|
+
* @returns {string} X/HTML with prefixed comments or attributes
|
|
23
|
+
*/
|
|
24
|
+
export const createTemplateString = (templateStrings, attributePlaceholders = '') => {
|
|
25
|
+
// TODO: make it easier to identify attribute and node parts for SSR and leave the comments at those positions to be replaced in toString()
|
|
26
|
+
let partIndex = 0;
|
|
27
|
+
// join all interpolations (for values) with a special placeholder and remove any whitespace
|
|
28
|
+
let template = templateStrings.join('\x01').trim();
|
|
29
|
+
// find (match) all elements to identify their attributes
|
|
30
|
+
template = template.replace(elements, (_, name, attributesString, trailingSlash) => {
|
|
31
|
+
let elementTagWithAttributes = name + attributesString.replaceAll('\x01', attributePlaceholders).trimEnd();
|
|
32
|
+
if (trailingSlash.length) elementTagWithAttributes += voidElements.test(name) ? ' /' : '></' + name;
|
|
33
|
+
// collect all attribute parts so that we can place matching comment nodes
|
|
34
|
+
const attributeParts = attributesString.replace(attributes, (attribute, name, valueWithQuotes, directive) => {
|
|
35
|
+
if (directive && directive === '\x01') {
|
|
36
|
+
return `<!--\x02$-->`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// remove quotes from attribute value to normalize the value
|
|
40
|
+
const value =
|
|
41
|
+
valueWithQuotes?.startsWith('"') || valueWithQuotes?.startsWith("'")
|
|
42
|
+
? valueWithQuotes.slice(1, -1)
|
|
43
|
+
: valueWithQuotes;
|
|
44
|
+
const partsCount = (attribute.match(/\x01/g) || []).length;
|
|
45
|
+
const parts = [];
|
|
46
|
+
for (let index = 0; index < partsCount; index++) {
|
|
47
|
+
parts.push(`<!--\x02:${name}=${value.replaceAll('\x01', '\x03')}-->`);
|
|
48
|
+
}
|
|
49
|
+
return parts.join('');
|
|
50
|
+
});
|
|
51
|
+
return `
|
|
52
|
+
${attributesString.includes('\x01') ? attributeParts : ''}
|
|
53
|
+
<${elementTagWithAttributes}>
|
|
54
|
+
`;
|
|
55
|
+
});
|
|
56
|
+
// replace interpolation placeholders with our indexed markers
|
|
57
|
+
template = template.replace(partPositions, (partPosition) => {
|
|
58
|
+
if (partPosition === '\x01') {
|
|
59
|
+
return `<!--dom-part-${partIndex}--><!--/dom-part-${partIndex++}-->`;
|
|
60
|
+
}
|
|
61
|
+
if (partPosition === '\x02') {
|
|
62
|
+
return `dom-part-${partIndex++}`;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
return `
|
|
66
|
+
<!--template-part-->
|
|
67
|
+
${template}
|
|
68
|
+
<!--/template-part-->
|
|
69
|
+
`;
|
|
70
|
+
// TODO: ^ this is actually important because of the whitespace
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {String} name
|
|
75
|
+
* @param {any} value
|
|
76
|
+
* @param {Boolean} isSingleValue
|
|
77
|
+
* @return {String}
|
|
78
|
+
*/
|
|
79
|
+
const attribute = (name, value, isSingleValue = true) => {
|
|
80
|
+
return ` ${name}="${encodeAttribute(isObjectLike(value) ? JSON.stringify(value) : value)}${
|
|
81
|
+
isSingleValue ? '"' : ''
|
|
82
|
+
}`;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {any} value
|
|
87
|
+
* @return {String}
|
|
88
|
+
*/
|
|
89
|
+
const getValue = (value) => {
|
|
90
|
+
switch (typeof value) {
|
|
91
|
+
case 'string':
|
|
92
|
+
return encodeAttribute(value);
|
|
93
|
+
case 'boolean':
|
|
94
|
+
case 'number':
|
|
95
|
+
return String(value);
|
|
96
|
+
case 'object':
|
|
97
|
+
switch (true) {
|
|
98
|
+
case value === null:
|
|
99
|
+
return '';
|
|
100
|
+
case Array.isArray(value):
|
|
101
|
+
return value.map(getValue).join('');
|
|
102
|
+
case value instanceof TemplateResult:
|
|
103
|
+
return value.toString();
|
|
104
|
+
case value.__unsafeHTML:
|
|
105
|
+
return value.string;
|
|
106
|
+
default:
|
|
107
|
+
console.log('Cannot getValue for', value);
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
case 'function':
|
|
111
|
+
return getValue(value());
|
|
112
|
+
}
|
|
113
|
+
return value == null ? '' : encodeAttribute(String(value));
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** @type {Map<TemplateStringsArray, *[]>} */
|
|
117
|
+
const parsedUpdates = new WeakMap();
|
|
118
|
+
|
|
119
|
+
/** @type {Map<Element, TemplatePart>} */
|
|
120
|
+
const templateParts = new WeakMap();
|
|
121
|
+
|
|
122
|
+
export class TemplateResult {
|
|
123
|
+
/**
|
|
124
|
+
* @param {TemplateStringsArray} strings
|
|
125
|
+
* @param {any[]} values
|
|
126
|
+
*/
|
|
127
|
+
constructor(strings, ...values) {
|
|
128
|
+
this.strings = strings;
|
|
129
|
+
this.values = values;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @param {Element} domNode
|
|
134
|
+
*/
|
|
135
|
+
renderInto(domNode) {
|
|
136
|
+
let serverSideRendered = false;
|
|
137
|
+
let templatePart = templateParts.get(domNode);
|
|
138
|
+
if (!templatePart) {
|
|
139
|
+
const startNode = Array.from(domNode.childNodes)
|
|
140
|
+
.filter((node) => node.nodeType === COMMENT_NODE)
|
|
141
|
+
.find((node) => node.data === 'template-part');
|
|
142
|
+
|
|
143
|
+
serverSideRendered = startNode !== undefined;
|
|
144
|
+
|
|
145
|
+
templatePart = new TemplatePart(startNode, this);
|
|
146
|
+
templateParts.set(domNode, templatePart);
|
|
147
|
+
|
|
148
|
+
if (!serverSideRendered) {
|
|
149
|
+
domNode.replaceChildren(...templatePart.childNodes);
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
templatePart.update(this);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {String[]} strings
|
|
158
|
+
* @return {(() => String)[]}
|
|
159
|
+
*/
|
|
160
|
+
parse(strings) {
|
|
161
|
+
const html = createTemplateString(strings, '\x04');
|
|
162
|
+
const parts = [];
|
|
163
|
+
let i = 0;
|
|
164
|
+
let match = null;
|
|
165
|
+
while ((match = interpolation.exec(html))) {
|
|
166
|
+
const pre = html.slice(i, match.index);
|
|
167
|
+
i = match.index + match[0].length;
|
|
168
|
+
if (match[2]) {
|
|
169
|
+
// ChildNodePart
|
|
170
|
+
const index = match[2];
|
|
171
|
+
parts.push((value) => `${pre}<!--dom-part-${index}-->${getValue(value)}<!--/dom-part-${index}-->`);
|
|
172
|
+
} else if (match[4]) {
|
|
173
|
+
// AttributePart with single interpolation or the first interpolation right after the attribute=
|
|
174
|
+
const isSingleValue = match[5] !== undefined;
|
|
175
|
+
let name = match[4];
|
|
176
|
+
switch (true) {
|
|
177
|
+
case name[0] === '?':
|
|
178
|
+
const booleanName = name.slice(1).toLowerCase();
|
|
179
|
+
parts.push((value) => {
|
|
180
|
+
if (!value) return '';
|
|
181
|
+
return `${pre} ${booleanName}=""`;
|
|
182
|
+
});
|
|
183
|
+
break;
|
|
184
|
+
case name[0] === '.':
|
|
185
|
+
const lower = name.slice(1).toLowerCase();
|
|
186
|
+
parts.push((value) => {
|
|
187
|
+
let result = pre;
|
|
188
|
+
// null, undefined, and false are not shown at all
|
|
189
|
+
if (value === null || value === undefined || value === '') {
|
|
190
|
+
result += attribute(lower, '');
|
|
191
|
+
} else {
|
|
192
|
+
// in all other cases, just escape it in quotes
|
|
193
|
+
result += attribute(lower, value, isSingleValue);
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
});
|
|
197
|
+
break;
|
|
198
|
+
case name[0] === '@':
|
|
199
|
+
name = 'on' + name.slice(1);
|
|
200
|
+
case name[0] === 'o' && name[1] === 'n':
|
|
201
|
+
parts.push((value) => {
|
|
202
|
+
return pre;
|
|
203
|
+
});
|
|
204
|
+
break;
|
|
205
|
+
default:
|
|
206
|
+
parts.push((value) => {
|
|
207
|
+
let result = pre;
|
|
208
|
+
if (value != null) {
|
|
209
|
+
result += attribute(name, value, isSingleValue);
|
|
210
|
+
}
|
|
211
|
+
return result;
|
|
212
|
+
});
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
// AttributePart in the middle of an attribute value or NodePart
|
|
217
|
+
parts.push((value) => {
|
|
218
|
+
let result = pre;
|
|
219
|
+
// TODO: we currently cannot distinguish between NodeParts and AttributeParts before
|
|
220
|
+
if (isObjectLike(value) && value.directiveClass) {
|
|
221
|
+
// NodePart
|
|
222
|
+
const directive = new value.directiveClass();
|
|
223
|
+
result += directive.stringify(...value.values);
|
|
224
|
+
} else if (value != null) {
|
|
225
|
+
result += encodeAttribute(isObjectLike(value) ? JSON.stringify(value) : value);
|
|
226
|
+
}
|
|
227
|
+
return result;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// We couldn't correctly parse parts from the template
|
|
233
|
+
if (parts.length !== strings.length - 1) {
|
|
234
|
+
throw {
|
|
235
|
+
name: 'ParseTemplateError',
|
|
236
|
+
message: 'Could not parse parts from template correctly. Parts length has not the expected length.',
|
|
237
|
+
strings,
|
|
238
|
+
templateString: html,
|
|
239
|
+
parts,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (parts.length) {
|
|
244
|
+
const last = parts[parts.length - 1];
|
|
245
|
+
const chunk = html.slice(i);
|
|
246
|
+
parts[parts.length - 1] = (value) => last(value) + chunk;
|
|
247
|
+
} else {
|
|
248
|
+
parts.push(() => html);
|
|
249
|
+
}
|
|
250
|
+
return parts;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @param {Node[]} childNodes
|
|
255
|
+
* @return {object[]}
|
|
256
|
+
*/
|
|
257
|
+
parseParts(childNodes) {
|
|
258
|
+
// we always create a template fragment so that we can start at the root for traversing the node path
|
|
259
|
+
const range = globalThis.document?.createRange();
|
|
260
|
+
range.setStartBefore(childNodes[0]);
|
|
261
|
+
range.setEndAfter(childNodes[childNodes.length - 1]);
|
|
262
|
+
const template = range.cloneContents();
|
|
263
|
+
|
|
264
|
+
const treeWalker = globalThis.document?.createTreeWalker(template, 128);
|
|
265
|
+
let node = treeWalker.nextNode(); //to skip the root template-part
|
|
266
|
+
const parts = [];
|
|
267
|
+
|
|
268
|
+
let nestedLevel = 0;
|
|
269
|
+
// search for parts through numbered comment nodes with placeholders
|
|
270
|
+
while ((node = treeWalker.nextNode())) {
|
|
271
|
+
if (/^template-part$/.test(node.data)) {
|
|
272
|
+
nestedLevel++;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (/^\/template-part$/.test(node.data)) {
|
|
276
|
+
if (nestedLevel > 0) {
|
|
277
|
+
nestedLevel--;
|
|
278
|
+
}
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (nestedLevel > 0) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (/^dom-part-\d+$/.test(node.data)) {
|
|
285
|
+
parts.push({ type: 'node', path: getNodePath(node) });
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (/^dom-part-\d+:/.test(node.data)) {
|
|
289
|
+
const [_, ...attribute] = node.data.split(':');
|
|
290
|
+
const [name, ...initialValue] = attribute.join(':').split('=');
|
|
291
|
+
parts.push({
|
|
292
|
+
type: 'attribute',
|
|
293
|
+
path: getNodePath(node),
|
|
294
|
+
name: name,
|
|
295
|
+
initialValue: initialValue.join('='),
|
|
296
|
+
});
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (/^dom-part-\d+\$/.test(node.data)) {
|
|
300
|
+
parts.push({ type: 'directive', path: getNodePath(node) });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// We couldn't correctly parse parts from the template
|
|
305
|
+
if (parts.length !== this.strings.length - 1) {
|
|
306
|
+
throw {
|
|
307
|
+
name: 'ParseTemplateError',
|
|
308
|
+
message:
|
|
309
|
+
'Could not parse parts from template correctly. Parts length has not the expected length. -> ' +
|
|
310
|
+
JSON.stringify({
|
|
311
|
+
strings: this.strings,
|
|
312
|
+
templateString: this.templateString,
|
|
313
|
+
childNodes,
|
|
314
|
+
template,
|
|
315
|
+
parts,
|
|
316
|
+
}),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return parts;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* find interpolations in the given template for nodes and attributes and
|
|
325
|
+
* return a string with placeholders as either comment nodes or named attributes.
|
|
326
|
+
* @returns {string} template with tagged placeholders for values
|
|
327
|
+
*/
|
|
328
|
+
get templateString() {
|
|
329
|
+
return createTemplateString(this.strings);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
toString() {
|
|
333
|
+
let updates = parsedUpdates.get(this.strings);
|
|
334
|
+
|
|
335
|
+
if (!updates) {
|
|
336
|
+
updates = this.parse(this.strings);
|
|
337
|
+
parsedUpdates.set(this.strings, updates);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return this.values.length ? this.values.map((value, index) => updates[index](value)).join('') : updates[0]();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { camelToDash, decodeAttribute, encodeAttribute } from '../util/AttributeParser.js';
|
|
2
|
+
import { TemplateResult } from './TemplateResult.js';
|
|
3
|
+
import { convertStringToHTML, convertStringToTemplate } from '../util/DOMHelper.js';
|
|
4
|
+
import { defineDirective, Directive } from '../util/Directive.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Maps a list of classes to an element from an object.
|
|
8
|
+
* The keys will become the classes when the value is truthy.
|
|
9
|
+
* @param {Object.<string, string | boolean | number>} map
|
|
10
|
+
* @returns {string} string of classes joined together
|
|
11
|
+
*/
|
|
12
|
+
const classMap = (map) => {
|
|
13
|
+
const classes = [];
|
|
14
|
+
for (const [key, value] of Object.entries(map)) {
|
|
15
|
+
if (value) classes.push(key);
|
|
16
|
+
}
|
|
17
|
+
return classes.join(' ');
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Maps a list of styles to an element from an object.
|
|
22
|
+
* The keys will become the properties and the values will become the values.
|
|
23
|
+
* @param {Object.<string, string | undefined | null>} map
|
|
24
|
+
* @returns {string} string of styles joined together
|
|
25
|
+
*/
|
|
26
|
+
const styleMap = (map) => {
|
|
27
|
+
const styles = [];
|
|
28
|
+
for (const [key, value] of Object.entries(map)) {
|
|
29
|
+
if (value) styles.push(`${key}:${value};`);
|
|
30
|
+
}
|
|
31
|
+
return styles.join(' ');
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Renders one of two template parts based on a condition.
|
|
36
|
+
* @param {boolean} condition
|
|
37
|
+
* @param {TemplateResult | string} trueCase
|
|
38
|
+
* @param {TemplateResult | string} [falseCase]
|
|
39
|
+
* @returns {TemplateResult | string}
|
|
40
|
+
*/
|
|
41
|
+
const when = (condition, trueCase, falseCase) => {
|
|
42
|
+
return condition ? trueCase : falseCase;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Chooses and evaluates a template part from a map of cases based on matching the given value to a case.
|
|
47
|
+
* @param {string} value
|
|
48
|
+
* @param {Object.<string, TemplateResult | string>} cases
|
|
49
|
+
* @param {TemplateResult | string} [defaultCase]
|
|
50
|
+
* @returns {TemplateResult | string}
|
|
51
|
+
*/
|
|
52
|
+
const choose = (value, cases, defaultCase) => {
|
|
53
|
+
return cases[value] || defaultCase;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Renders a given string as HTML instead of text or as unescaped string in SSR mode
|
|
58
|
+
* @param {string} string
|
|
59
|
+
* @returns {(function(): Node) | { __unsafeHTML: true, string: string }}
|
|
60
|
+
*/
|
|
61
|
+
const unsafeHTML = (string) => {
|
|
62
|
+
if (!globalThis.DOMParser) {
|
|
63
|
+
return { __unsafeHTML: true, string };
|
|
64
|
+
}
|
|
65
|
+
const node = convertStringToHTML(string).firstChild;
|
|
66
|
+
const importedNode = globalThis.document?.importNode(node, true);
|
|
67
|
+
return () => importedNode;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Renders multiple attributes as key="value" pairs from a map of attributes
|
|
72
|
+
*/
|
|
73
|
+
export class SpreadAttributesDirective extends Directive {
|
|
74
|
+
/**
|
|
75
|
+
* @param {Object.<string, any>} attributes
|
|
76
|
+
*/
|
|
77
|
+
update(attributes) {
|
|
78
|
+
// TODO: what if the attribute was already there..?!
|
|
79
|
+
// TODO: what if the attribute was there from a previous render/update, but not anymore?!
|
|
80
|
+
for (const name of Object.keys(attributes)) {
|
|
81
|
+
const value = attributes[name];
|
|
82
|
+
this.node.setAttribute(
|
|
83
|
+
camelToDash(name),
|
|
84
|
+
encodeAttribute(typeof value !== 'string' ? JSON.stringify(value) : value),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {Object.<string, any>} attributes
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
93
|
+
stringify(attributes) {
|
|
94
|
+
return Object.keys(attributes)
|
|
95
|
+
.map((name) => {
|
|
96
|
+
let value = attributes[name];
|
|
97
|
+
return `${camelToDash(name)}='${encodeAttribute(
|
|
98
|
+
typeof value !== 'string' ? JSON.stringify(value) : value,
|
|
99
|
+
)}'`;
|
|
100
|
+
})
|
|
101
|
+
.join(' ');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Renders multiple attributes as key="value" pairs from a map of attributes
|
|
107
|
+
* @param {Object.<string, any>} attributes
|
|
108
|
+
*/
|
|
109
|
+
const spreadAttributes = defineDirective(SpreadAttributesDirective);
|
|
110
|
+
|
|
111
|
+
export { classMap, styleMap, when, choose, unsafeHTML, spreadAttributes };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { TemplateResult } from './TemplateResult.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {TemplateStringsArray} strings
|
|
5
|
+
* @param {any[]} values
|
|
6
|
+
* @return {TemplateResult}
|
|
7
|
+
*/
|
|
8
|
+
const html = function (strings, ...values) {
|
|
9
|
+
return new TemplateResult(strings, ...values);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export { html };
|
|
@@ -1,23 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests if a value is of type `object`
|
|
3
|
+
* @param {any} value
|
|
4
|
+
* @returns {boolean}
|
|
5
|
+
*/
|
|
1
6
|
export function isObjectLike(value) {
|
|
2
7
|
return typeof value == 'object' && value !== null;
|
|
3
8
|
}
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Tests if a value can be parsed as JSON
|
|
12
|
+
* @param {any} value
|
|
13
|
+
* @returns {boolean}
|
|
14
|
+
*/
|
|
15
|
+
export function isJSON(value) {
|
|
6
16
|
try {
|
|
7
|
-
return JSON.parse(
|
|
17
|
+
return JSON.parse(value) && !!value;
|
|
8
18
|
} catch (e) {
|
|
9
19
|
return false;
|
|
10
20
|
}
|
|
11
21
|
}
|
|
12
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Tests if a value is `boolean`
|
|
25
|
+
* @param {any} value
|
|
26
|
+
* @returns {boolean}
|
|
27
|
+
*/
|
|
13
28
|
export function isBoolean(value) {
|
|
14
29
|
return value === 'true' || value === 'false';
|
|
15
30
|
}
|
|
16
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Parses a value to `boolean`
|
|
34
|
+
* @param {any} value
|
|
35
|
+
* @returns {boolean}
|
|
36
|
+
*/
|
|
17
37
|
export function parseBoolean(value) {
|
|
18
38
|
return value === 'true';
|
|
19
39
|
}
|
|
20
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Tests if a value is of type `string`
|
|
43
|
+
* @param {any} value
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
21
46
|
export function isString(value) {
|
|
22
47
|
return (
|
|
23
48
|
typeof value === 'string' ||
|
|
@@ -25,26 +50,40 @@ export function isString(value) {
|
|
|
25
50
|
);
|
|
26
51
|
}
|
|
27
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Tests if a value is of type `number`
|
|
55
|
+
* @param {any} value
|
|
56
|
+
* @returns {boolean}
|
|
57
|
+
*/
|
|
28
58
|
export function isNumber(value) {
|
|
29
59
|
return new RegExp('^-?(0|0\\.\\d+|[1-9]\\d*(\\.\\d+)?)$').test(value);
|
|
30
60
|
}
|
|
31
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Tests if a value is of type `NaN`
|
|
64
|
+
* @param {any} value
|
|
65
|
+
* @returns {boolean}
|
|
66
|
+
*/
|
|
32
67
|
export function isNaN(value) {
|
|
33
68
|
return Number.isNaN(value);
|
|
34
69
|
}
|
|
35
70
|
|
|
36
71
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* @param
|
|
40
|
-
* @param valueB
|
|
72
|
+
* Compare two values (deep). It compares primitive or complex values with JSON stringify.
|
|
73
|
+
* @param {any} valueA
|
|
74
|
+
* @param {any} valueB
|
|
41
75
|
* @returns {boolean}
|
|
42
76
|
*/
|
|
43
|
-
|
|
44
77
|
export function deepEquals(valueA, valueB) {
|
|
45
78
|
return JSON.stringify(valueA) === JSON.stringify(valueB);
|
|
46
79
|
}
|
|
47
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Parses an attribute value that comes in as a `string` to its corresponding type
|
|
83
|
+
* @param {string} value
|
|
84
|
+
* @param {PropertyOptions} options
|
|
85
|
+
* @returns {string | number | boolean | object | array}
|
|
86
|
+
*/
|
|
48
87
|
export function parseAttribute(value, options = {}) {
|
|
49
88
|
if (options.parse === false || !isString(value)) {
|
|
50
89
|
// no-op
|
|
@@ -67,7 +106,8 @@ export function parseAttribute(value, options = {}) {
|
|
|
67
106
|
|
|
68
107
|
/**
|
|
69
108
|
* Replaces dashed-expression (i.e. some-value) to a camel-cased expression (i.e. someValue)
|
|
70
|
-
* @
|
|
109
|
+
* @param {string} string
|
|
110
|
+
* @returns {string}
|
|
71
111
|
*/
|
|
72
112
|
export function dashToCamel(string) {
|
|
73
113
|
if (string.indexOf('-') === -1) return string;
|
|
@@ -77,44 +117,37 @@ export function dashToCamel(string) {
|
|
|
77
117
|
|
|
78
118
|
/**
|
|
79
119
|
* Replaces camel-cased expression (i.e. someValue) to a dashed-expression (i.e. some-value)
|
|
80
|
-
* @
|
|
120
|
+
* @param {string} string
|
|
121
|
+
* @returns {string}
|
|
81
122
|
*/
|
|
82
123
|
export function camelToDash(string) {
|
|
83
124
|
return string.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
84
125
|
}
|
|
85
126
|
|
|
86
127
|
/**
|
|
87
|
-
* Decodes an attribute.
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* @param attribute
|
|
128
|
+
* Decodes an attribute.
|
|
129
|
+
* @param {string} attribute
|
|
91
130
|
* @returns {string}
|
|
92
131
|
*/
|
|
93
132
|
export function decodeAttribute(attribute) {
|
|
94
133
|
return `${attribute}`
|
|
95
|
-
.replace(
|
|
96
|
-
.replace(
|
|
97
|
-
.replace(
|
|
98
|
-
.replace(
|
|
134
|
+
.replace(/</g, '<')
|
|
135
|
+
.replace(/>/g, '>')
|
|
136
|
+
.replace(/"/g, '"')
|
|
137
|
+
.replace(/'/g, "'");
|
|
99
138
|
}
|
|
100
139
|
|
|
101
140
|
/**
|
|
102
|
-
* Encodes an attribute.
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* This does not use the standard way to encode an attribute, as the dom parser
|
|
106
|
-
* that renders the html would understand and automatically decode it. Which results
|
|
107
|
-
* in non sanitized html output.
|
|
108
|
-
*
|
|
109
|
-
* @param attribute
|
|
141
|
+
* Encodes an attribute.
|
|
142
|
+
* @param {string} attribute
|
|
110
143
|
* @returns {string}
|
|
111
144
|
*/
|
|
112
145
|
export function encodeAttribute(attribute) {
|
|
113
146
|
return `${attribute}`
|
|
114
|
-
.replace(/'/g, '
|
|
115
|
-
.replace(/"/g, '
|
|
116
|
-
.replace(/</g, '
|
|
117
|
-
.replace(/>/g, '
|
|
147
|
+
.replace(/'/g, ''')
|
|
148
|
+
.replace(/"/g, '"')
|
|
149
|
+
.replace(/</g, '<')
|
|
150
|
+
.replace(/>/g, '>')
|
|
118
151
|
.replace(/\r\n/g, '\n')
|
|
119
152
|
.replace(/[\r\n]/g, '\n');
|
|
120
153
|
}
|