@webtides/element-js 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +147 -90
  2. package/README.md +3 -3
  3. package/docs/README.md +70 -89
  4. package/docs/concepts/classes.md +37 -66
  5. package/docs/concepts/hierarchy.md +29 -31
  6. package/docs/concepts/lifecycle.md +9 -23
  7. package/docs/concepts/reactivity.md +1 -3
  8. package/docs/features/computed-properties.md +13 -16
  9. package/docs/features/dependency-injection-context-protocol.md +75 -87
  10. package/docs/features/global-styles.md +1 -1
  11. package/docs/features/methods.md +14 -15
  12. package/docs/features/properties.md +21 -33
  13. package/docs/features/refs.md +23 -31
  14. package/docs/features/renderless.md +31 -37
  15. package/docs/features/store.md +63 -78
  16. package/docs/features/templates.md +4 -1
  17. package/docs/features/watch.md +28 -35
  18. package/docs/getting-started/upgrade-to-v1.md +16 -0
  19. package/docs/guide/guide.md +10 -22
  20. package/index.js +29 -2
  21. package/package.json +62 -62
  22. package/src/BaseElement.js +632 -631
  23. package/src/StyledElement.js +104 -104
  24. package/src/TemplateElement.js +53 -53
  25. package/src/dom-parts/AttributePart.js +116 -116
  26. package/src/dom-parts/ChildNodePart.js +288 -288
  27. package/src/dom-parts/NodePart.js +27 -27
  28. package/src/dom-parts/Part.js +6 -6
  29. package/src/dom-parts/TemplatePart.js +119 -119
  30. package/src/dom-parts/TemplateResult.js +270 -270
  31. package/src/dom-parts/directives.js +105 -48
  32. package/src/dom-parts/html.js +1 -1
  33. package/src/dom-parts/render.js +6 -6
  34. package/src/util/AttributeParser.js +47 -47
  35. package/src/util/DOMHelper.js +190 -190
  36. package/src/util/Directive.js +31 -31
  37. package/src/util/GlobalStylesStore.js +63 -53
  38. package/src/util/SerializeStateHelper.js +56 -56
  39. package/src/util/Store.js +175 -174
  40. package/src/util/crypto.js +13 -0
  41. package/src/util/defineElement.js +5 -5
  42. package/src/util/toString.js +5 -5
@@ -4,67 +4,77 @@ import { Store } from './Store.js';
4
4
  * @property {CSSStyleSheet[]} cssStyleSheets
5
5
  */
6
6
  class GlobalStylesStore extends Store {
7
- /** @type {Map<Node, CSSStyleSheet>} */
8
- globalStyleSheetsCache = new WeakMap();
7
+ /** @type {Map<Node, CSSStyleSheet>} */
8
+ globalStyleSheetsCache = new WeakMap();
9
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
- }
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
29
 
30
- getGlobalStyleSheets(selector) {
31
- /** @type {CSSStyleSheet[]}*/
32
- const cssStyleSheets = [];
30
+ getGlobalStyleSheets(selector) {
31
+ /** @type {CSSStyleSheet[]}*/
32
+ const cssStyleSheets = [];
33
33
 
34
- if (selector === false) return cssStyleSheets;
34
+ if (selector === false) return cssStyleSheets;
35
35
 
36
- if (typeof selector === 'string') {
37
- selector = [selector];
38
- }
36
+ if (typeof selector === 'string') {
37
+ selector = [selector];
38
+ }
39
39
 
40
- if (selector === true || selector.includes('document')) {
41
- cssStyleSheets.push(...globalThis.document?.adoptedStyleSheets);
42
- }
40
+ if (selector === true || selector.includes('document')) {
41
+ cssStyleSheets.push(...globalThis.document?.adoptedStyleSheets);
42
+ }
43
43
 
44
- Array.from(globalThis.document?.styleSheets).map((styleSheet) => {
45
- if (Array.isArray(selector) && !selector.some((cssSelector) => styleSheet.ownerNode.matches(cssSelector))) {
46
- return;
47
- }
44
+ Array.from(globalThis.document?.styleSheets).map((styleSheet) => {
45
+ if (Array.isArray(selector) && !selector.some((cssSelector) => styleSheet.ownerNode.matches(cssSelector))) {
46
+ return;
47
+ }
48
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
- if (styleSheet.ownerNode.tagName === 'STYLE') {
54
- cssStyleSheet.replaceSync(styleSheet.ownerNode.textContent);
55
- } else if (styleSheet.ownerNode.tagName === 'LINK') {
56
- try {
57
- Array.from(styleSheet?.cssRules ?? []).map((rule) => cssStyleSheet.insertRule(rule.cssText));
58
- } catch (e) {
59
- console.error('GlobalStylesStore: cannot read cssRules. Maybe add crossorigin="anonymous" to your style link?', e);
60
- }
61
- }
62
- }
63
- cssStyleSheets.push(cssStyleSheet);
64
- });
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
+ if (styleSheet.ownerNode.tagName === 'STYLE') {
53
+ cssStyleSheet = new CSSStyleSheet({ media: styleSheet.media, disabled: styleSheet.disabled });
54
+ cssStyleSheet.replaceSync(styleSheet.ownerNode.textContent);
55
+ } else if (styleSheet.ownerNode.tagName === 'LINK') {
56
+ cssStyleSheet = new CSSStyleSheet({
57
+ baseURL: styleSheet.href,
58
+ media: styleSheet.media,
59
+ disabled: styleSheet.disabled,
60
+ });
61
+ try {
62
+ Array.from(styleSheet?.cssRules ?? []).map((rule, index) =>
63
+ cssStyleSheet.insertRule(rule.cssText, index),
64
+ );
65
+ } catch (e) {
66
+ console.error(
67
+ 'GlobalStylesStore: cannot read cssRules. Maybe add crossorigin="anonymous" to your style link?',
68
+ e,
69
+ );
70
+ }
71
+ }
72
+ }
73
+ cssStyleSheets.push(cssStyleSheet);
74
+ });
65
75
 
66
- return cssStyleSheets;
67
- }
76
+ return cssStyleSheets;
77
+ }
68
78
  }
69
79
 
70
80
  export const globalStylesStore = new GlobalStylesStore();
@@ -19,16 +19,16 @@ let globalElementJsState;
19
19
  * Initializes the global script element that holds the state for all `Serializable` objects.
20
20
  */
21
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
- }
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
32
  }
33
33
 
34
34
  /**
@@ -36,23 +36,23 @@ function initGlobalStateObject() {
36
36
  * @param {Serializable} serializableObject
37
37
  */
38
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
- });
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
56
  }
57
57
 
58
58
  /**
@@ -61,31 +61,31 @@ export function serializeState(serializableObject) {
61
61
  * @param {{[string: any]: *}} [serializedState]
62
62
  */
63
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]);
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
89
  }
90
90
 
91
91
  /**
@@ -94,10 +94,10 @@ export function deserializeState(serializableObject, serializedState) {
94
94
  * @return {boolean}
95
95
  */
96
96
  export function hasSerializedState(key) {
97
- if (!globalThis.elementJsConfig?.serializeState) return false;
97
+ if (!globalThis.elementJsConfig?.serializeState) return false;
98
98
 
99
- initGlobalStateObject();
99
+ initGlobalStateObject();
100
100
 
101
- const serializedState = JSON.parse(globalElementJsState.textContent);
102
- return serializedState.hasOwnProperty(key);
101
+ const serializedState = JSON.parse(globalElementJsState.textContent);
102
+ return serializedState.hasOwnProperty(key);
103
103
  }
package/src/util/Store.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { deepEquals, isObjectLike } from './AttributeParser.js';
2
2
  import {
3
- hasSerializedState,
4
- serializeState,
5
- deserializeState,
6
- serializableObjectsCache,
3
+ hasSerializedState,
4
+ serializeState,
5
+ deserializeState,
6
+ serializableObjectsCache,
7
7
  } from './SerializeStateHelper.js';
8
8
  import { BaseElement } from '../BaseElement.js';
9
+ import { randomUUID } from './crypto.js';
9
10
 
10
11
  /**
11
12
  * Options object for the Store
@@ -17,174 +18,174 @@ import { BaseElement } from '../BaseElement.js';
17
18
  * @implements {Serializable}
18
19
  */
19
20
  export class Store {
20
- _serializationKey;
21
- _observer = new Set([]);
22
- _singlePropertyMode = false;
23
- _state = {};
24
-
25
- /**
26
- * @param {* | object} [value]
27
- * @param {StoreOptions} [options]
28
- */
29
- constructor(value, options) {
30
- if (options?.key && serializableObjectsCache.has(options?.key)) {
31
- return serializableObjectsCache.get(options?.key);
32
- }
33
-
34
- this._singlePropertyMode = arguments.length > 0 && !isObjectLike(value);
35
-
36
- if (options?.key) {
37
- this._serializationKey = options?.key;
38
- } else {
39
- this._serializationKey = globalThis.crypto.randomUUID();
40
- }
41
-
42
- if (options?.serializedState) {
43
- deserializeState(this, options.serializedState);
44
- } else if (hasSerializedState(this._serializationKey)) {
45
- deserializeState(this);
46
- }
47
-
48
- // wrap primitives with a generic "value" field to generate getter and setter
49
- const specificValues = this._singlePropertyMode ? { value } : value;
50
- const properties = { ...(!this._singlePropertyMode && this.properties()), ...specificValues };
51
-
52
- const serializationKeys = Object.keys(this.serializeState());
53
- Object.entries(properties).map(([key, value]) => {
54
- this._state[key] =
55
- hasSerializedState(this._serializationKey) && serializationKeys.includes(key) ? this[key] : value;
56
-
57
- if (value instanceof Store) {
58
- value.subscribe(this);
59
- }
60
-
61
- Object.defineProperty(this, key, {
62
- get: () => {
63
- return this._state[key];
64
- },
65
- set: (newValue) => {
66
- const oldValue = this._state[key];
67
- this._state[key] = newValue;
68
- if (value instanceof Store) {
69
- value.subscribe(this);
70
- }
71
-
72
- serializeState(this);
73
-
74
- if (!deepEquals(newValue, oldValue)) {
75
- const watch = this.watch();
76
- if (watch.hasOwnProperty(key) && typeof watch[key] === 'function') {
77
- watch[key](newValue, oldValue);
78
- }
79
- this.requestUpdate();
80
- }
81
- },
82
- });
83
- });
84
-
85
- serializableObjectsCache.set(this._serializationKey, this);
86
- }
87
-
88
- /**
89
- * Properties to be defined and assigned on the store. Can be overridden in extended classes.
90
- * Properties defined here will also trigger an update cycle when changed.
91
- * @return {{}}
92
- */
93
- properties() {
94
- return {};
95
- }
96
-
97
- /**
98
- * When a property as the key was changed in the store, the callback function defined as value
99
- * will be called with `newValue` and `oldValue`
100
- * @return {{}}
101
- */
102
- watch() {
103
- return {};
104
- }
105
-
106
- /**
107
- * Overrides `valueOf` to automatically return .value
108
- * @returns {* | {}}
109
- */
110
- valueOf() {
111
- return this._singlePropertyMode ? this._state.value : this._state;
112
- }
113
-
114
- /**
115
- * Overrides `toString` to automatically return .value
116
- * @returns {string}
117
- */
118
- toString() {
119
- return `${this._singlePropertyMode ? this._state.value : this._state}`;
120
- }
121
-
122
- /**
123
- * Add an observer
124
- * @param {BaseElement|Function|Store} observer
125
- */
126
- subscribe(observer) {
127
- this._observer.add(observer);
128
- }
129
-
130
- /**
131
- * Remove an observer
132
- * @param {BaseElement | Function} observer
133
- */
134
- unsubscribe(observer) {
135
- this._observer.delete(observer);
136
- }
137
-
138
- /**
139
- * Request and batch an asynchronous update cycle
140
- */
141
- requestUpdate() {
142
- this._observer.forEach(async (observer) => {
143
- if (observer instanceof BaseElement || observer instanceof Store) {
144
- if (observer?._options?.autoUpdate !== false) {
145
- await observer.requestUpdate();
146
- }
147
- // check if store is watched by an observer
148
- if (Object.keys(observer.watch() ?? {}).length > 0) {
149
- // observer actually has watched properties
150
- Object.keys(observer.watch() ?? {}).forEach((key) => {
151
- if (observer._state[key] === this) {
152
- // observer is actually watching store changes provide new and old values
153
- if (observer instanceof Store) {
154
- // observer is a foreign store / call watcher
155
- observer.watch?.()[key](this, this);
156
- } else {
157
- observer.callPropertyWatcher(key, this, this);
158
- observer.notifyPropertyChange(key, this);
159
- }
160
- }
161
- });
162
- }
163
- } else if (typeof observer === 'function') {
164
- observer();
165
- } else {
166
- console.error('Store: observer type is not supported.', observer);
167
- }
168
- });
169
- }
170
-
171
- /**
172
- * This method will be used to serialize this stores state.
173
- * By default, it will generate an object with all properties from this.properties().
174
- * You can override this and return a different object with more/less/different properties.
175
- * @return {{[string: any]: *}}
176
- */
177
- serializeState() {
178
- const keys = Object.keys(this.properties());
179
- return Object.fromEntries(keys.map((key) => [key, this[key]]));
180
- }
181
-
182
- /**
183
- * This method will be used to restore properties on the store from a previously serialized state.
184
- * You can override this and handle the deserialization differently if the default is not enough.
185
- * @param {{[string: any]: *}} serializedState
186
- */
187
- restoreState(serializedState) {
188
- Object.assign(this, serializedState);
189
- }
21
+ _serializationKey;
22
+ _observer = new Set([]);
23
+ _singlePropertyMode = false;
24
+ _state = {};
25
+
26
+ /**
27
+ * @param {* | object} [value]
28
+ * @param {StoreOptions} [options]
29
+ */
30
+ constructor(value, options) {
31
+ if (options?.key && serializableObjectsCache.has(options?.key)) {
32
+ return serializableObjectsCache.get(options?.key);
33
+ }
34
+
35
+ this._singlePropertyMode = arguments.length > 0 && !isObjectLike(value);
36
+
37
+ if (options?.key) {
38
+ this._serializationKey = options?.key;
39
+ } else {
40
+ this._serializationKey = randomUUID();
41
+ }
42
+
43
+ if (options?.serializedState) {
44
+ deserializeState(this, options.serializedState);
45
+ } else if (hasSerializedState(this._serializationKey)) {
46
+ deserializeState(this);
47
+ }
48
+
49
+ // wrap primitives with a generic "value" field to generate getter and setter
50
+ const specificValues = this._singlePropertyMode ? { value } : value;
51
+ const properties = { ...(!this._singlePropertyMode && this.properties()), ...specificValues };
52
+
53
+ const serializationKeys = Object.keys(this.serializeState());
54
+ Object.entries(properties).map(([key, value]) => {
55
+ this._state[key] =
56
+ hasSerializedState(this._serializationKey) && serializationKeys.includes(key) ? this[key] : value;
57
+
58
+ if (value instanceof Store) {
59
+ value.subscribe(this);
60
+ }
61
+
62
+ Object.defineProperty(this, key, {
63
+ get: () => {
64
+ return this._state[key];
65
+ },
66
+ set: (newValue) => {
67
+ const oldValue = this._state[key];
68
+ this._state[key] = newValue;
69
+ if (value instanceof Store) {
70
+ value.subscribe(this);
71
+ }
72
+
73
+ serializeState(this);
74
+
75
+ if (!deepEquals(newValue, oldValue)) {
76
+ const watch = this.watch();
77
+ if (watch.hasOwnProperty(key) && typeof watch[key] === 'function') {
78
+ watch[key](newValue, oldValue);
79
+ }
80
+ this.requestUpdate();
81
+ }
82
+ },
83
+ });
84
+ });
85
+
86
+ serializableObjectsCache.set(this._serializationKey, this);
87
+ }
88
+
89
+ /**
90
+ * Properties to be defined and assigned on the store. Can be overridden in extended classes.
91
+ * Properties defined here will also trigger an update cycle when changed.
92
+ * @return {{}}
93
+ */
94
+ properties() {
95
+ return {};
96
+ }
97
+
98
+ /**
99
+ * When a property as the key was changed in the store, the callback function defined as value
100
+ * will be called with `newValue` and `oldValue`
101
+ * @return {{}}
102
+ */
103
+ watch() {
104
+ return {};
105
+ }
106
+
107
+ /**
108
+ * Overrides `valueOf` to automatically return .value
109
+ * @returns {* | {}}
110
+ */
111
+ valueOf() {
112
+ return this._singlePropertyMode ? this._state.value : this._state;
113
+ }
114
+
115
+ /**
116
+ * Overrides `toString` to automatically return .value
117
+ * @returns {string}
118
+ */
119
+ toString() {
120
+ return `${this._singlePropertyMode ? this._state.value : this._state}`;
121
+ }
122
+
123
+ /**
124
+ * Add an observer
125
+ * @param {BaseElement|Function|Store} observer
126
+ */
127
+ subscribe(observer) {
128
+ this._observer.add(observer);
129
+ }
130
+
131
+ /**
132
+ * Remove an observer
133
+ * @param {BaseElement | Function} observer
134
+ */
135
+ unsubscribe(observer) {
136
+ this._observer.delete(observer);
137
+ }
138
+
139
+ /**
140
+ * Request and batch an asynchronous update cycle
141
+ */
142
+ requestUpdate() {
143
+ this._observer.forEach(async (observer) => {
144
+ if (observer instanceof BaseElement || observer instanceof Store) {
145
+ if (observer?._options?.autoUpdate !== false) {
146
+ await observer.requestUpdate();
147
+ }
148
+ // check if store is watched by an observer
149
+ if (Object.keys(observer.watch() ?? {}).length > 0) {
150
+ // observer actually has watched properties
151
+ Object.keys(observer.watch() ?? {}).forEach((key) => {
152
+ if (observer._state[key] === this) {
153
+ // observer is actually watching store changes provide new and old values
154
+ if (observer instanceof Store) {
155
+ // observer is a foreign store / call watcher
156
+ observer.watch?.()[key](this, this);
157
+ } else {
158
+ observer.callPropertyWatcher(key, this, this);
159
+ observer.notifyPropertyChange(key, this);
160
+ }
161
+ }
162
+ });
163
+ }
164
+ } else if (typeof observer === 'function') {
165
+ observer();
166
+ } else {
167
+ console.error('Store: observer type is not supported.', observer);
168
+ }
169
+ });
170
+ }
171
+
172
+ /**
173
+ * This method will be used to serialize this stores state.
174
+ * By default, it will generate an object with all properties from this.properties().
175
+ * You can override this and return a different object with more/less/different properties.
176
+ * @return {{[string: any]: *}}
177
+ */
178
+ serializeState() {
179
+ const keys = Object.keys(this.properties());
180
+ return Object.fromEntries(keys.map((key) => [key, this[key]]));
181
+ }
182
+
183
+ /**
184
+ * This method will be used to restore properties on the store from a previously serialized state.
185
+ * You can override this and handle the deserialization differently if the default is not enough.
186
+ * @param {{[string: any]: *}} serializedState
187
+ */
188
+ restoreState(serializedState) {
189
+ Object.assign(this, serializedState);
190
+ }
190
191
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Helper function to use the crypto.randomUUID() as it will fail in none secure hosts (e.g. localhost)
3
+ * @return {`${string}-${string}-${string}-${string}-${string}`|*}
4
+ */
5
+ export function randomUUID() {
6
+ if (!('randomUUID' in globalThis.crypto)) {
7
+ // https://stackoverflow.com/a/2117523/2800218
8
+ return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
9
+ (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16),
10
+ );
11
+ }
12
+ return globalThis.crypto.randomUUID();
13
+ }