@webtides/element-js 1.0.0-alpha.1 → 1.0.0-alpha.3
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/index.js +302 -80
- package/dist/index.js.map +4 -4
- package/index.js +9 -2
- package/package.json +1 -1
- package/src/BaseElement.js +200 -40
- package/src/StyledElement.js +65 -48
- package/src/TemplateElement.js +24 -1
- package/src/dom-parts/TemplateResult.js +2 -1
- package/src/dom-parts/directives.js +40 -15
- package/src/util/AttributeParser.js +54 -21
- package/src/util/DOMHelper.js +63 -14
- package/src/util/Directive.js +13 -0
- package/src/util/GlobalStylesStore.js +70 -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/src/util/i18n.js +0 -20
|
@@ -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.
|
|
@@ -55,30 +56,54 @@ const choose = (value, cases, defaultCase) => {
|
|
|
55
56
|
/**
|
|
56
57
|
* Renders a given string as HTML instead of text
|
|
57
58
|
* @param {string} string
|
|
58
|
-
* @returns {function(): Node}
|
|
59
|
+
* @returns {function(): Node | string}
|
|
59
60
|
*/
|
|
60
61
|
const unsafeHTML = (string) => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
if (!globalThis.DOMParser) return () => string;
|
|
63
|
+
const node = convertStringToHTML(string).firstChild;
|
|
64
|
+
const importedNode = globalThis.document?.importNode(node, true);
|
|
65
|
+
return () => importedNode;
|
|
64
66
|
};
|
|
65
67
|
|
|
66
68
|
/**
|
|
67
69
|
* Renders multiple attributes as key="value" pairs from a map of attributes
|
|
68
|
-
* @param {Object.<string, any>} attributes
|
|
69
|
-
* @returns {function(): string}
|
|
70
70
|
*/
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
export class SpreadAttributesDirective extends Directive {
|
|
72
|
+
/**
|
|
73
|
+
* @param {Object.<string, any>} attributes
|
|
74
|
+
*/
|
|
75
|
+
update(attributes) {
|
|
76
|
+
// TODO: what if the attribute was already there..?!
|
|
77
|
+
// TODO: what if the attribute was there from a previous render/update, but not anymore?!
|
|
78
|
+
for (const name of Object.keys(attributes)) {
|
|
79
|
+
const value = attributes[name];
|
|
80
|
+
this.node.setAttribute(
|
|
81
|
+
camelToDash(name),
|
|
82
|
+
encodeAttribute(typeof value !== 'string' ? JSON.stringify(value) : value),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {Object.<string, any>} attributes
|
|
89
|
+
* @returns {string}
|
|
90
|
+
*/
|
|
91
|
+
stringify(attributes) {
|
|
73
92
|
return Object.keys(attributes)
|
|
74
|
-
.map((
|
|
75
|
-
let value = attributes[
|
|
76
|
-
return `${camelToDash(
|
|
93
|
+
.map((name) => {
|
|
94
|
+
let value = attributes[name];
|
|
95
|
+
return `${camelToDash(name)}='${encodeAttribute(
|
|
77
96
|
typeof value !== 'string' ? JSON.stringify(value) : value,
|
|
78
97
|
)}'`;
|
|
79
98
|
})
|
|
80
99
|
.join(' ');
|
|
81
|
-
}
|
|
82
|
-
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Renders multiple attributes as key="value" pairs from a map of attributes
|
|
105
|
+
* @param {Object.<string, any>} attributes
|
|
106
|
+
*/
|
|
107
|
+
const spreadAttributes = defineDirective(SpreadAttributesDirective);
|
|
83
108
|
|
|
84
109
|
export { classMap, styleMap, when, choose, unsafeHTML, spreadAttributes };
|
|
@@ -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
|
/**
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Store } from './Store.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @property {CSSStyleSheet[]} cssStyleSheets
|
|
5
|
+
*/
|
|
6
|
+
class GlobalStylesStore extends Store {
|
|
7
|
+
/** @type {Map<Node, CSSStyleSheet>} */
|
|
8
|
+
globalStyleSheetsCache = new WeakMap();
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
super();
|
|
12
|
+
if (globalThis.elementJsConfig?.observeGlobalStyles) {
|
|
13
|
+
const mutationObserver = new MutationObserver((mutationRecord) => {
|
|
14
|
+
if (!mutationRecord[0]) return;
|
|
15
|
+
const filteredNodes = Array.from(mutationRecord[0].addedNodes).filter(
|
|
16
|
+
(node) => node.tagName === 'STYLE' || node.tagName === 'LINK',
|
|
17
|
+
);
|
|
18
|
+
if (filteredNodes && filteredNodes[0] && filteredNodes[0].tagName === 'LINK') {
|
|
19
|
+
filteredNodes[0].onload = () => {
|
|
20
|
+
this.requestUpdate();
|
|
21
|
+
};
|
|
22
|
+
} else if (filteredNodes && filteredNodes[0] && filteredNodes[0].tagName === 'STYLE') {
|
|
23
|
+
this.requestUpdate();
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
mutationObserver.observe(globalThis.document, { subtree: true, childList: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getGlobalStyleSheets(selector) {
|
|
31
|
+
/** @type {CSSStyleSheet[]}*/
|
|
32
|
+
const cssStyleSheets = [];
|
|
33
|
+
|
|
34
|
+
if (selector === false) return cssStyleSheets;
|
|
35
|
+
|
|
36
|
+
if (typeof selector === 'string') {
|
|
37
|
+
selector = [selector];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (selector === true || selector.includes('document')) {
|
|
41
|
+
cssStyleSheets.push(...globalThis.document?.adoptedStyleSheets);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
Array.from(globalThis.document?.styleSheets).map((styleSheet) => {
|
|
45
|
+
if (Array.isArray(selector) && !selector.some((cssSelector) => styleSheet.ownerNode.matches(cssSelector))) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// TODO: this will always be null as we never set anything to the cache...
|
|
50
|
+
let cssStyleSheet = this.globalStyleSheetsCache.get(styleSheet.ownerNode);
|
|
51
|
+
if (!cssStyleSheet) {
|
|
52
|
+
cssStyleSheet = new CSSStyleSheet();
|
|
53
|
+
let cssText = '';
|
|
54
|
+
if (styleSheet.ownerNode.tagName === 'STYLE') {
|
|
55
|
+
cssText = styleSheet.ownerNode.textContent;
|
|
56
|
+
} else if (styleSheet.ownerNode.tagName === 'LINK') {
|
|
57
|
+
cssText = Array.from(styleSheet.cssRules)
|
|
58
|
+
.map((rule) => rule.cssText)
|
|
59
|
+
.join('');
|
|
60
|
+
}
|
|
61
|
+
cssStyleSheet.replaceSync(cssText);
|
|
62
|
+
}
|
|
63
|
+
cssStyleSheets.push(cssStyleSheet);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return cssStyleSheets;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const globalStylesStore = new GlobalStylesStore();
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Store } from './Store.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} Serializable
|
|
5
|
+
* An interface that classes should implement to enable serialization and deserialization of their state.
|
|
6
|
+
* @property {string} _serializationKey - a unique key to be used for serialization.
|
|
7
|
+
* @property {function} serializeState - Function to retrieve the state for serialization.
|
|
8
|
+
* @property {function} restoreState - Function to set the state during deserialization.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// TODO: is it ok to expose this like this? Or should we wrap the cache in helper methods also?
|
|
12
|
+
/** @type {Map<string, Serializable>} */
|
|
13
|
+
export const serializableObjectsCache = new Map();
|
|
14
|
+
|
|
15
|
+
/** @type {HTMLScriptElement} */
|
|
16
|
+
let globalElementJsState;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Initializes the global script element that holds the state for all `Serializable` objects.
|
|
20
|
+
*/
|
|
21
|
+
function initGlobalStateObject() {
|
|
22
|
+
if (!globalElementJsState) {
|
|
23
|
+
globalElementJsState = Array.from(globalThis.document.scripts).find((script) => script.type === 'ejs/json');
|
|
24
|
+
if (!globalElementJsState) {
|
|
25
|
+
const script = document.createElement('script');
|
|
26
|
+
script.setAttribute('type', 'ejs/json');
|
|
27
|
+
script.textContent = '{}';
|
|
28
|
+
document.body.appendChild(script);
|
|
29
|
+
globalElementJsState = script;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Takes an object that implements the `Serializable` interface and serializes its state.
|
|
36
|
+
* @param {Serializable} serializableObject
|
|
37
|
+
*/
|
|
38
|
+
export function serializeState(serializableObject) {
|
|
39
|
+
if (!globalThis.elementJsConfig?.serializeState) return;
|
|
40
|
+
|
|
41
|
+
if (!serializableObject._serializationKey && !serializableObject.serializeState) {
|
|
42
|
+
throw new Error('serializableObject does not implement the Serializable interface');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
initGlobalStateObject();
|
|
46
|
+
|
|
47
|
+
const currentState = JSON.parse(globalElementJsState.textContent);
|
|
48
|
+
currentState[serializableObject._serializationKey] = serializableObject.serializeState();
|
|
49
|
+
globalElementJsState.textContent = JSON.stringify(currentState, (key, value) => {
|
|
50
|
+
if (value instanceof Store) {
|
|
51
|
+
return 'Store/' + value._serializationKey;
|
|
52
|
+
} else {
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Takes an object that implements the `Serializable` interface and deserializes its state and restores the object.
|
|
60
|
+
* @param {Serializable} serializableObject
|
|
61
|
+
* @param {{[string: any]: *}} [serializedState]
|
|
62
|
+
*/
|
|
63
|
+
export function deserializeState(serializableObject, serializedState) {
|
|
64
|
+
if (!globalThis.elementJsConfig?.serializeState) return;
|
|
65
|
+
|
|
66
|
+
if (!serializableObject._serializationKey && !serializableObject.restoreState) {
|
|
67
|
+
throw new Error('serializableObject does not implement the Serializable interface');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (serializedState) {
|
|
71
|
+
serializableObject.restoreState(serializedState);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
initGlobalStateObject();
|
|
76
|
+
|
|
77
|
+
const unresolvedState = JSON.parse(globalElementJsState.textContent);
|
|
78
|
+
const currentState = JSON.parse(globalElementJsState.textContent, (key, value) => {
|
|
79
|
+
if (typeof value === 'string' && value.startsWith('Store/')) {
|
|
80
|
+
const [_, storeUuid] = value.split('/');
|
|
81
|
+
const serializedState = unresolvedState[storeUuid];
|
|
82
|
+
return new Store(serializedState, { key: storeUuid, serializedState });
|
|
83
|
+
} else {
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
serializableObject.restoreState(currentState[serializableObject._serializationKey]);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Checks if a serialized state is available for a given key.
|
|
93
|
+
* @param key
|
|
94
|
+
* @return {boolean}
|
|
95
|
+
*/
|
|
96
|
+
export function hasSerializedState(key) {
|
|
97
|
+
if (!globalThis.elementJsConfig?.serializeState) return false;
|
|
98
|
+
|
|
99
|
+
initGlobalStateObject();
|
|
100
|
+
|
|
101
|
+
const serializedState = JSON.parse(globalElementJsState.textContent);
|
|
102
|
+
return serializedState.hasOwnProperty(key);
|
|
103
|
+
}
|