@webtides/element-js 1.0.0-alpha.1 → 1.0.0-alpha.11
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/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 +15 -19
- package/src/BaseElement.js +206 -42
- package/src/StyledElement.js +67 -48
- package/src/TemplateElement.js +32 -7
- package/src/dom-parts/AttributePart.js +16 -9
- package/src/dom-parts/ChildNodePart.js +8 -22
- package/src/dom-parts/NodePart.js +1 -0
- package/src/dom-parts/TemplatePart.js +19 -25
- package/src/dom-parts/TemplateResult.js +46 -35
- package/src/dom-parts/directives.js +43 -16
- package/src/dom-parts/html.js +1 -1
- package/src/dom-parts/render.js +6 -1
- package/src/util/AttributeParser.js +54 -21
- package/src/util/DOMHelper.js +63 -14
- package/src/util/Directive.js +21 -1
- package/src/util/GlobalStylesStore.js +66 -0
- package/src/util/SerializeStateHelper.js +103 -0
- package/src/util/Store.js +111 -15
- package/src/util/defineElement.js +2 -2
- package/src/util/toString.js +2 -2
- package/dist/index.js +0 -1330
- package/dist/index.js.map +0 -7
- package/src/util/i18n.js +0 -20
|
@@ -12,13 +12,13 @@ const removeChildNodes = (node) => {
|
|
|
12
12
|
const firstChild = Array.isArray(node)
|
|
13
13
|
? node[0]
|
|
14
14
|
: node instanceof TemplatePart
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
? node.childNodes[0]
|
|
16
|
+
: node.firstChild;
|
|
17
17
|
const lastChild = Array.isArray(node)
|
|
18
18
|
? node[node.length - 1]
|
|
19
19
|
: node instanceof TemplatePart
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
? node.childNodes[node.childNodes.length - 1]
|
|
21
|
+
: node.lastChild;
|
|
22
22
|
range.setStartAfter(firstChild);
|
|
23
23
|
range.setEndAfter(lastChild);
|
|
24
24
|
range.deleteContents();
|
|
@@ -75,28 +75,12 @@ const diffChildNodes = function (parentNode, domChildNodes, templateChildNodes,
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
// If DOM node is equal to the template node, don't do anything
|
|
78
|
-
// if (domChildNode.isEqualNode(templateChildNode)) {
|
|
79
78
|
if (domChildNode === templateChildNode) {
|
|
80
79
|
continue;
|
|
81
80
|
}
|
|
82
81
|
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
parentNode.replaceChild(templateChildNode, domChildNode);
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// If the element tag names are not the same, replace the DOM node with the template node
|
|
90
|
-
if (templateChildNode.nodeType === ELEMENT_NODE && templateChildNode.tagName !== domChildNode.tagName) {
|
|
91
|
-
parentNode.replaceChild(templateChildNode, domChildNode);
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// If the node is an SVG element, don't even think about diffing it, just replace it
|
|
96
|
-
if (templateChildNode.nodeType === ELEMENT_NODE && templateChildNode.tagName === 'SVG') {
|
|
97
|
-
parentNode.replaceChild(templateChildNode, domChildNode);
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
82
|
+
// Everything else is somehow different and can be replaced
|
|
83
|
+
parentNode.replaceChild(templateChildNode, domChildNode);
|
|
100
84
|
}
|
|
101
85
|
return templateChildNodes;
|
|
102
86
|
};
|
|
@@ -212,6 +196,8 @@ export class ChildNodePart extends Part {
|
|
|
212
196
|
|
|
213
197
|
let serverSideRendered = false;
|
|
214
198
|
if (startNode) {
|
|
199
|
+
startNode.__part = this; // add Part to comment node for debugging in the browser
|
|
200
|
+
|
|
215
201
|
const placeholder = startNode.data;
|
|
216
202
|
const childNodes = [startNode];
|
|
217
203
|
let childNode = startNode.nextSibling;
|
|
@@ -22,6 +22,7 @@ export class NodePart extends Part {
|
|
|
22
22
|
);
|
|
23
23
|
}
|
|
24
24
|
super();
|
|
25
|
+
node.__part = this; // add Part to comment node for debugging in the browser
|
|
25
26
|
this.node = node.nextElementSibling;
|
|
26
27
|
const { directiveClass, values } = initialValue;
|
|
27
28
|
this.directive = new directiveClass(node.nextElementSibling);
|
|
@@ -34,6 +34,8 @@ export class TemplatePart extends Part {
|
|
|
34
34
|
|
|
35
35
|
let serverSideRendered = false;
|
|
36
36
|
if (startNode) {
|
|
37
|
+
startNode.__part = this; // add Part to comment node for debugging in the browser
|
|
38
|
+
|
|
37
39
|
const placeholder = startNode.data;
|
|
38
40
|
const childNodes = [startNode];
|
|
39
41
|
let childNode = startNode.nextSibling;
|
|
@@ -99,32 +101,24 @@ export class TemplatePart extends Part {
|
|
|
99
101
|
partsCache.set(templateResult.strings, parts);
|
|
100
102
|
}
|
|
101
103
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
if (part.type === 'attribute') {
|
|
113
|
-
// If we have multiple attribute parts with the same name, it means we have multiple
|
|
114
|
-
// interpolations inside that attribute. Instead of creating a new part, we will return the same
|
|
115
|
-
// as before and let it defer the update until the last interpolation gets updated
|
|
116
|
-
if (previousAttributePart && previousAttributePart.name === part.name) {
|
|
117
|
-
previousAttributePart.addNode(node);
|
|
118
|
-
return previousAttributePart;
|
|
104
|
+
this.parts = parts
|
|
105
|
+
.map((part) => {
|
|
106
|
+
// We currently need the path because the fragment will be cloned via importNode and therefore the node will be a different one
|
|
107
|
+
part.node = part.path.reduceRight(({ childNodes }, i) => childNodes[i], this);
|
|
108
|
+
return part;
|
|
109
|
+
})
|
|
110
|
+
.map((part, index) => {
|
|
111
|
+
if (part.type === 'node') {
|
|
112
|
+
return new ChildNodePart(part.node, templateResult.values[index]);
|
|
119
113
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
114
|
+
if (part.type === 'attribute') {
|
|
115
|
+
return new AttributePart(part.node, part.name, part.initialValue);
|
|
116
|
+
}
|
|
117
|
+
if (part.type === 'directive') {
|
|
118
|
+
return new NodePart(part.node, templateResult.values[index]);
|
|
119
|
+
}
|
|
120
|
+
throw `cannot map part: ${part}`;
|
|
121
|
+
});
|
|
128
122
|
this.strings = templateResult.strings;
|
|
129
123
|
}
|
|
130
124
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { COMMENT_NODE, getNodePath } from '../util/DOMHelper';
|
|
1
|
+
import { COMMENT_NODE, getNodePath } from '../util/DOMHelper.js';
|
|
2
2
|
import { encodeAttribute, isObjectLike } from '../util/AttributeParser.js';
|
|
3
3
|
import { TemplatePart } from './TemplatePart.js';
|
|
4
4
|
|
|
@@ -12,7 +12,7 @@ const partPositions = /[\x01\x02]/g;
|
|
|
12
12
|
// \x03 COMMENT.ATTRIBUTE_TOKEN
|
|
13
13
|
// \x04 Node.ATTRIBUTE_TOKEN
|
|
14
14
|
|
|
15
|
-
const interpolation = new RegExp(`(<!--dom-part-(\\d+)--><!--/dom-part-(\\d+)-->|(\\S
|
|
15
|
+
const interpolation = new RegExp(`(<!--dom-part-(\\d+)--><!--/dom-part-(\\d+)-->|(\\S[-\\w]+)="\x04(")?|\x04)`, 'g');
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Given a template, find part positions as both nodes and attributes and
|
|
@@ -32,7 +32,7 @@ export const createTemplateString = (templateStrings, attributePlaceholders = ''
|
|
|
32
32
|
if (trailingSlash.length) elementTagWithAttributes += voidElements.test(name) ? ' /' : '></' + name;
|
|
33
33
|
// collect all attribute parts so that we can place matching comment nodes
|
|
34
34
|
const attributeParts = attributesString.replace(attributes, (attribute, name, valueWithQuotes, directive) => {
|
|
35
|
-
if (directive) {
|
|
35
|
+
if (directive && directive === '\x01') {
|
|
36
36
|
return `<!--\x02$-->`;
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -95,10 +95,16 @@ const getValue = (value) => {
|
|
|
95
95
|
return String(value);
|
|
96
96
|
case 'object':
|
|
97
97
|
switch (true) {
|
|
98
|
+
case value === null:
|
|
99
|
+
return '';
|
|
98
100
|
case Array.isArray(value):
|
|
99
101
|
return value.map(getValue).join('');
|
|
100
102
|
case value instanceof TemplateResult:
|
|
101
103
|
return value.toString();
|
|
104
|
+
case value.__unsafeHTML:
|
|
105
|
+
return value.string;
|
|
106
|
+
default:
|
|
107
|
+
console.log('Cannot getValue for', value);
|
|
102
108
|
}
|
|
103
109
|
break;
|
|
104
110
|
case 'function':
|
|
@@ -213,7 +219,8 @@ export class TemplateResult {
|
|
|
213
219
|
// TODO: we currently cannot distinguish between NodeParts and AttributeParts before
|
|
214
220
|
if (isObjectLike(value) && value.directiveClass) {
|
|
215
221
|
// NodePart
|
|
216
|
-
|
|
222
|
+
const directive = new value.directiveClass();
|
|
223
|
+
result += directive.stringify(...value.values);
|
|
217
224
|
} else if (value != null) {
|
|
218
225
|
result += encodeAttribute(isObjectLike(value) ? JSON.stringify(value) : value);
|
|
219
226
|
}
|
|
@@ -255,40 +262,44 @@ export class TemplateResult {
|
|
|
255
262
|
const template = range.cloneContents();
|
|
256
263
|
|
|
257
264
|
const treeWalker = globalThis.document?.createTreeWalker(template, 128);
|
|
258
|
-
let node = treeWalker.
|
|
265
|
+
let node = treeWalker.nextNode(); //to skip the root template-part
|
|
266
|
+
const parts = [];
|
|
259
267
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (/^\/template-part$/.test(node.data)) {
|
|
271
|
-
// end recursion
|
|
272
|
-
return parts;
|
|
273
|
-
}
|
|
274
|
-
if (/^dom-part-\d+$/.test(node.data)) {
|
|
275
|
-
parts.push((currentPart = { type: 'node', path: getNodePath(node), parts: [] }));
|
|
276
|
-
}
|
|
277
|
-
if (/^dom-part-\d+:/.test(node.data)) {
|
|
278
|
-
const [_, attribute] = node.data.split(':');
|
|
279
|
-
const [name, initialValue] = attribute.split('=');
|
|
280
|
-
currentPart = undefined;
|
|
281
|
-
parts.push({ type: 'attribute', path: getNodePath(node), name: name, initialValue });
|
|
282
|
-
}
|
|
283
|
-
if (/^dom-part-\d+\$/.test(node.data)) {
|
|
284
|
-
currentPart = undefined;
|
|
285
|
-
parts.push({ type: 'directive', path: getNodePath(node) });
|
|
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--;
|
|
286
278
|
}
|
|
279
|
+
continue;
|
|
287
280
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
+
}
|
|
292
303
|
|
|
293
304
|
// We couldn't correctly parse parts from the template
|
|
294
305
|
if (parts.length !== this.strings.length - 1) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { camelToDash, decodeAttribute, encodeAttribute } from '../util/AttributeParser.js';
|
|
2
|
-
import { TemplateResult } from './TemplateResult';
|
|
3
|
-
import { convertStringToHTML } from '../util/DOMHelper';
|
|
2
|
+
import { TemplateResult } from './TemplateResult.js';
|
|
3
|
+
import { convertStringToHTML, convertStringToTemplate } from '../util/DOMHelper.js';
|
|
4
|
+
import { defineDirective, Directive } from '../util/Directive.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Maps a list of classes to an element from an object.
|
|
@@ -53,32 +54,58 @@ const choose = (value, cases, defaultCase) => {
|
|
|
53
54
|
};
|
|
54
55
|
|
|
55
56
|
/**
|
|
56
|
-
* Renders a given string as HTML instead of text
|
|
57
|
+
* Renders a given string as HTML instead of text or as unescaped string in SSR mode
|
|
57
58
|
* @param {string} string
|
|
58
|
-
* @returns {function(): Node}
|
|
59
|
+
* @returns {(function(): Node) | { __unsafeHTML: true, string: string }}
|
|
59
60
|
*/
|
|
60
61
|
const unsafeHTML = (string) => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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;
|
|
64
68
|
};
|
|
65
69
|
|
|
66
70
|
/**
|
|
67
71
|
* Renders multiple attributes as key="value" pairs from a map of attributes
|
|
68
|
-
* @param {Object.<string, any>} attributes
|
|
69
|
-
* @returns {function(): string}
|
|
70
72
|
*/
|
|
71
|
-
|
|
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) {
|
|
73
94
|
return Object.keys(attributes)
|
|
74
|
-
.map((
|
|
75
|
-
let value = attributes[
|
|
76
|
-
return `${camelToDash(
|
|
95
|
+
.map((name) => {
|
|
96
|
+
let value = attributes[name];
|
|
97
|
+
return `${camelToDash(name)}='${encodeAttribute(
|
|
77
98
|
typeof value !== 'string' ? JSON.stringify(value) : value,
|
|
78
99
|
)}'`;
|
|
79
100
|
})
|
|
80
101
|
.join(' ');
|
|
81
|
-
}
|
|
82
|
-
}
|
|
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);
|
|
83
110
|
|
|
84
111
|
export { classMap, styleMap, when, choose, unsafeHTML, spreadAttributes };
|
package/src/dom-parts/html.js
CHANGED
package/src/dom-parts/render.js
CHANGED
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
* @param {Element} domNode
|
|
5
5
|
*/
|
|
6
6
|
const render = (template, domNode) => {
|
|
7
|
-
template
|
|
7
|
+
if (typeof template === 'string') {
|
|
8
|
+
// just a plain string (or literal)
|
|
9
|
+
domNode.innerHTML = template;
|
|
10
|
+
} else {
|
|
11
|
+
template.renderInto(domNode);
|
|
12
|
+
}
|
|
8
13
|
};
|
|
9
14
|
|
|
10
15
|
export { render };
|
|
@@ -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,17 +117,16 @@ 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) {
|
|
@@ -99,14 +138,8 @@ export function decodeAttribute(attribute) {
|
|
|
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) {
|
package/src/util/DOMHelper.js
CHANGED
|
@@ -6,10 +6,12 @@ export const ATTRIBUTE_NODE = 2;
|
|
|
6
6
|
export const TEXT_NODE = 3;
|
|
7
7
|
/* A Comment node, such as <!-- … --> */
|
|
8
8
|
export const COMMENT_NODE = 8;
|
|
9
|
+
/* A Document Fragment, a document without a parent */
|
|
10
|
+
export const DOCUMENT_FRAGMENT_NODE = 11;
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
|
-
* Gets list of (childNode) indexes for the given node
|
|
12
|
-
* @param node
|
|
13
|
+
* Gets a list of (childNode) indexes for the given node
|
|
14
|
+
* @param {Node} node
|
|
13
15
|
* @return {number[]}
|
|
14
16
|
*/
|
|
15
17
|
export const getNodePath = (node) => {
|
|
@@ -23,6 +25,11 @@ export const getNodePath = (node) => {
|
|
|
23
25
|
return path;
|
|
24
26
|
};
|
|
25
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Get a list of all parent nodes up the DOM tree
|
|
30
|
+
* @param {Node} domNode
|
|
31
|
+
* @returns {Node[]}
|
|
32
|
+
*/
|
|
26
33
|
export function getAllParentNodes(domNode) {
|
|
27
34
|
let node = domNode.parentElement;
|
|
28
35
|
let parents = [];
|
|
@@ -34,7 +41,12 @@ export function getAllParentNodes(domNode) {
|
|
|
34
41
|
return parents;
|
|
35
42
|
}
|
|
36
43
|
|
|
37
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Gets the parent/root element of a shadow root or the global document body if not in shadow root.
|
|
46
|
+
* https://stackoverflow.com/questions/27453617/how-can-i-tell-if-an-element-is-in-a-shadow-dom
|
|
47
|
+
* @param {Node} element
|
|
48
|
+
* @returns {ShadowRoot | HTMLElement}
|
|
49
|
+
*/
|
|
38
50
|
export function getShadowParentOrBody(element) {
|
|
39
51
|
if (element instanceof ShadowRoot) {
|
|
40
52
|
return element;
|
|
@@ -50,6 +62,11 @@ export function getShadowParentOrBody(element) {
|
|
|
50
62
|
|
|
51
63
|
// TODO: add function for getClosestParentOfNodeType('custom-element')
|
|
52
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Get the closest/first parent node that is a custom element
|
|
67
|
+
* @param {Node} domNode
|
|
68
|
+
* @returns {Node}
|
|
69
|
+
*/
|
|
53
70
|
export function getClosestParentCustomElementNode(domNode) {
|
|
54
71
|
const customElementParents = getAllParentNodes(domNode).filter((node) => {
|
|
55
72
|
return node.localName && node.localName.indexOf('-') !== -1;
|
|
@@ -58,19 +75,51 @@ export function getClosestParentCustomElementNode(domNode) {
|
|
|
58
75
|
return customElementParents.pop();
|
|
59
76
|
}
|
|
60
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Get all
|
|
80
|
+
* @param {Node} domNode
|
|
81
|
+
* @param {{prototype: HTMLElement, new()}} instanceOfFilter
|
|
82
|
+
* @return {Node[]}
|
|
83
|
+
*/
|
|
84
|
+
export function getAllElementChildren(domNode, instanceOfFilter = HTMLElement) {
|
|
85
|
+
const nodeIterator = document.createNodeIterator(domNode, NodeFilter.SHOW_ELEMENT, (node) =>
|
|
86
|
+
node.nodeName.includes('-') && node instanceof instanceOfFilter
|
|
87
|
+
? NodeFilter.FILTER_ACCEPT
|
|
88
|
+
: NodeFilter.FILTER_REJECT,
|
|
89
|
+
);
|
|
90
|
+
const customElements = [];
|
|
91
|
+
let currentNode;
|
|
92
|
+
while ((currentNode = nodeIterator.nextNode())) {
|
|
93
|
+
customElements.push(currentNode);
|
|
94
|
+
}
|
|
95
|
+
return customElements;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Checks whether two nodes are of the same node type
|
|
100
|
+
* @param {Node} nodeA
|
|
101
|
+
* @param {Node} nodeB
|
|
102
|
+
* @returns {boolean}
|
|
103
|
+
*/
|
|
61
104
|
export function isOfSameNodeType(nodeA, nodeB) {
|
|
62
105
|
if (!nodeA && !nodeA.localName) return false;
|
|
63
106
|
if (!nodeB && !nodeB.localName) return false;
|
|
64
107
|
return nodeA.localName === nodeB.localName;
|
|
65
108
|
}
|
|
66
109
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
export const
|
|
110
|
+
/**
|
|
111
|
+
* Checks if adopting style sheets is supported in browser
|
|
112
|
+
* @returns {boolean}
|
|
113
|
+
*/
|
|
114
|
+
export const supportsAdoptingStyleSheets = () => {
|
|
115
|
+
return 'adoptedStyleSheets' in Document.prototype && 'replace' in CSSStyleSheet.prototype;
|
|
116
|
+
};
|
|
72
117
|
|
|
73
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Better implementation of hasChildNodes check because the native check will also count for empty whitespace as child nodes... :(
|
|
120
|
+
* @param {Node} node
|
|
121
|
+
* @returns {boolean}
|
|
122
|
+
*/
|
|
74
123
|
export const hasChildNodes = (node) => {
|
|
75
124
|
const childNodesLength = node.childNodes.length;
|
|
76
125
|
|
|
@@ -86,7 +135,7 @@ export const hasChildNodes = (node) => {
|
|
|
86
135
|
};
|
|
87
136
|
|
|
88
137
|
/**
|
|
89
|
-
* Parses the given template
|
|
138
|
+
* Parses the given string as HTML template and returns a real DOM element
|
|
90
139
|
* @param {string} template
|
|
91
140
|
* @returns {HTMLElement}
|
|
92
141
|
*/
|
|
@@ -97,8 +146,8 @@ export const convertStringToHTML = (template) => {
|
|
|
97
146
|
};
|
|
98
147
|
|
|
99
148
|
/**
|
|
100
|
-
* Parses the given string and returns a DocumentFragment
|
|
101
|
-
*
|
|
149
|
+
* Parses the given string and returns a DocumentFragment which contains the DOM subtree
|
|
150
|
+
* representing the <template> element's template contents.
|
|
102
151
|
* @param {string} string
|
|
103
152
|
* @return {DocumentFragment}
|
|
104
153
|
*/
|
|
@@ -111,8 +160,8 @@ export const convertStringToTemplate = (string) => {
|
|
|
111
160
|
|
|
112
161
|
/**
|
|
113
162
|
* Diff the attributes for a live DOM element against a template DOM element
|
|
114
|
-
* @param
|
|
115
|
-
* @param
|
|
163
|
+
* @param {Element} templateElement - The new template
|
|
164
|
+
* @param {Element} domElement - The existing DOM node
|
|
116
165
|
*/
|
|
117
166
|
export const diffAttributes = function (templateElement, domElement) {
|
|
118
167
|
// remove or update previous attributes
|
package/src/util/Directive.js
CHANGED
|
@@ -2,6 +2,9 @@ export class Directive {
|
|
|
2
2
|
/** @type {Element} */
|
|
3
3
|
node = undefined;
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @param {Node} node
|
|
7
|
+
*/
|
|
5
8
|
constructor(node) {
|
|
6
9
|
this.node = node;
|
|
7
10
|
}
|
|
@@ -10,6 +13,16 @@ export class Directive {
|
|
|
10
13
|
* @abstract
|
|
11
14
|
*/
|
|
12
15
|
update() {}
|
|
16
|
+
|
|
17
|
+
// TODO: this is somewhat custom... maybe we could set the initial values in the constructor and then simply override the default toString() method?!
|
|
18
|
+
/**
|
|
19
|
+
* Implement the stringify method and return a string for rendering the directive in SSR mode
|
|
20
|
+
* @param {...any} values
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
stringify(...values) {
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
13
26
|
}
|
|
14
27
|
|
|
15
28
|
/**
|
|
@@ -17,6 +30,13 @@ export class Directive {
|
|
|
17
30
|
*/
|
|
18
31
|
export const defineDirective = (directiveClass) => {
|
|
19
32
|
return (...values) => {
|
|
20
|
-
return {
|
|
33
|
+
return {
|
|
34
|
+
directiveClass,
|
|
35
|
+
values,
|
|
36
|
+
toString: () => {
|
|
37
|
+
const directive = new directiveClass();
|
|
38
|
+
return directive.stringify(...values);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
21
41
|
};
|
|
22
42
|
};
|