@webtides/element-js 1.0.0-alpha.2 → 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 +279 -76
- package/dist/index.js.map +4 -4
- package/index.js +8 -2
- package/package.json +1 -1
- package/src/BaseElement.js +104 -21
- package/src/StyledElement.js +35 -48
- package/src/TemplateElement.js +0 -1
- package/src/util/DOMHelper.js +20 -0
- package/src/util/GlobalStylesStore.js +70 -0
- package/src/util/SerializeStateHelper.js +103 -0
- package/src/util/Store.js +88 -13
- package/src/util/i18n.js +0 -20
package/index.js
CHANGED
|
@@ -3,8 +3,14 @@ import { StyledElement } from './src/StyledElement.js';
|
|
|
3
3
|
import { TemplateElement, html } from './src/TemplateElement.js';
|
|
4
4
|
import { toString } from './src/util/toString.js';
|
|
5
5
|
import { defineElement } from './src/util/defineElement.js';
|
|
6
|
-
import { i18n } from './src/util/i18n.js';
|
|
7
6
|
import { Store } from './src/util/Store.js';
|
|
8
7
|
import { Directive } from './src/util/Directive.js';
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Options object for element-js
|
|
11
|
+
* @typedef {Object} ElementJsConfig
|
|
12
|
+
* @property {boolean} [serializeState] - When set to true all elements and stores will serialize their state on changes to a global JSON object in the document. This is needed when rendering server side, so that elements and stores can hydrate with the correct state.
|
|
13
|
+
* @property {boolean} [observeGlobalStyles] - By default element-js will look for all style elements in the global document and apply them before any custom/element styles inside the shadow DOM. When set to true element-js will also observe the document for any changes regarding styles. This is needed if styles will get added async or late.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export { BaseElement, StyledElement, TemplateElement, Store, Directive, html, toString, defineElement };
|
package/package.json
CHANGED
package/src/BaseElement.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseAttribute, isNaN, dashToCamel, camelToDash, isObjectLike } from './util/AttributeParser.js';
|
|
2
|
-
import { getClosestParentCustomElementNode, isOfSameNodeType } from './util/DOMHelper.js';
|
|
2
|
+
import { getAllElementChildren, getClosestParentCustomElementNode, isOfSameNodeType } from './util/DOMHelper.js';
|
|
3
3
|
import { Store } from './util/Store.js';
|
|
4
|
-
|
|
4
|
+
import { serializeState, deserializeState, hasSerializedState } from './util/SerializeStateHelper.js';
|
|
5
5
|
export { defineElement } from './util/defineElement.js';
|
|
6
6
|
export { toString } from './util/toString.js';
|
|
7
7
|
|
|
@@ -39,7 +39,12 @@ export { toString } from './util/toString.js';
|
|
|
39
39
|
* @property {any} oldValue - The old value for the property
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* @implements {Serializable}
|
|
44
|
+
*/
|
|
42
45
|
class BaseElement extends HTMLElement {
|
|
46
|
+
_serializationKey;
|
|
47
|
+
|
|
43
48
|
/**
|
|
44
49
|
* @param {BaseElementOptions} options
|
|
45
50
|
*/
|
|
@@ -54,6 +59,7 @@ class BaseElement extends HTMLElement {
|
|
|
54
59
|
this._options = {
|
|
55
60
|
autoUpdate: true,
|
|
56
61
|
deferUpdate: true,
|
|
62
|
+
deferConnected: false,
|
|
57
63
|
mutationObserverOptions: {
|
|
58
64
|
attributes: true,
|
|
59
65
|
childList: true,
|
|
@@ -63,6 +69,13 @@ class BaseElement extends HTMLElement {
|
|
|
63
69
|
propertyOptions: {},
|
|
64
70
|
...options,
|
|
65
71
|
};
|
|
72
|
+
|
|
73
|
+
if (this.hasAttribute('defer-update')) {
|
|
74
|
+
this._options.deferUpdate = true;
|
|
75
|
+
}
|
|
76
|
+
if (this.hasAttribute('defer-connected')) {
|
|
77
|
+
this._options.deferConnected = true;
|
|
78
|
+
}
|
|
66
79
|
}
|
|
67
80
|
|
|
68
81
|
/**
|
|
@@ -71,6 +84,21 @@ class BaseElement extends HTMLElement {
|
|
|
71
84
|
* register observers, refs and events.
|
|
72
85
|
*/
|
|
73
86
|
connectedCallback() {
|
|
87
|
+
if (this._options.deferConnected) {
|
|
88
|
+
this._options.deferConnected = false;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (this.hasAttribute('ejs:key')) {
|
|
93
|
+
this._serializationKey = this.getAttribute('ejs:key');
|
|
94
|
+
deserializeState(this);
|
|
95
|
+
} else {
|
|
96
|
+
this._serializationKey = globalThis.crypto.randomUUID();
|
|
97
|
+
if (globalThis.elementJsConfig?.serializeState) {
|
|
98
|
+
this.setAttribute('ejs:key', this._serializationKey);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
74
102
|
// define all attributes to "this" as properties
|
|
75
103
|
this.defineAttributesAsProperties();
|
|
76
104
|
|
|
@@ -261,10 +289,6 @@ class BaseElement extends HTMLElement {
|
|
|
261
289
|
* @param {boolean} reflectAttribute
|
|
262
290
|
*/
|
|
263
291
|
defineProperty(property, value, reflectAttribute = false) {
|
|
264
|
-
if (value instanceof Store) {
|
|
265
|
-
value.subscribe(this);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
292
|
if (this._state.hasOwnProperty(property)) {
|
|
269
293
|
// property has already been defined as an attribute nothing to do here
|
|
270
294
|
return;
|
|
@@ -280,8 +304,16 @@ class BaseElement extends HTMLElement {
|
|
|
280
304
|
this.removeAttribute(camelToDash(property));
|
|
281
305
|
}
|
|
282
306
|
|
|
307
|
+
if (hasSerializedState(this._serializationKey) && Object.keys(this.serializeState())?.includes(property)) {
|
|
308
|
+
value = this[property];
|
|
309
|
+
}
|
|
310
|
+
|
|
283
311
|
this._state[property] = value;
|
|
284
312
|
|
|
313
|
+
if (this._state[property] instanceof Store) {
|
|
314
|
+
this._state[property].subscribe(this);
|
|
315
|
+
}
|
|
316
|
+
|
|
285
317
|
Object.defineProperty(this, property, {
|
|
286
318
|
get: () => {
|
|
287
319
|
return this._state[property];
|
|
@@ -292,6 +324,8 @@ class BaseElement extends HTMLElement {
|
|
|
292
324
|
if (JSON.stringify(oldValue) !== newValueString) {
|
|
293
325
|
this._state[property] = newValue;
|
|
294
326
|
|
|
327
|
+
serializeState(this);
|
|
328
|
+
|
|
295
329
|
if (newValue instanceof Store) {
|
|
296
330
|
newValue.subscribe(this);
|
|
297
331
|
}
|
|
@@ -395,8 +429,21 @@ class BaseElement extends HTMLElement {
|
|
|
395
429
|
this.requestContext(key, value);
|
|
396
430
|
});
|
|
397
431
|
// define context provider
|
|
398
|
-
|
|
432
|
+
const providedKeys = Object.keys(this.provideProperties());
|
|
433
|
+
if (providedKeys.length > 0) {
|
|
399
434
|
this.addEventListener('request-context', this.onRequestContext);
|
|
435
|
+
// check if there are already connected elements in child dom and restart requests
|
|
436
|
+
getAllElementChildren(this.getRoot(), BaseElement).forEach((customChild) => {
|
|
437
|
+
// if injectProperties?.() is defined it means that the child got connected BEFORE the parent (Runtime Issue)
|
|
438
|
+
const requestedProperties = customChild.injectProperties?.() ?? {};
|
|
439
|
+
Object.entries(requestedProperties).forEach(([key, value]) => {
|
|
440
|
+
// iterate over requested properties to find missed context requests
|
|
441
|
+
if (providedKeys.includes(key)) {
|
|
442
|
+
// owns the provided key -> replay request
|
|
443
|
+
customChild.requestContext(key, value);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
});
|
|
400
447
|
}
|
|
401
448
|
}
|
|
402
449
|
|
|
@@ -406,7 +453,7 @@ class BaseElement extends HTMLElement {
|
|
|
406
453
|
* @param {any | function} valueOrCallback
|
|
407
454
|
*/
|
|
408
455
|
requestContext(propertyName, valueOrCallback) {
|
|
409
|
-
this.dispatch('request-context', { [propertyName]: valueOrCallback }, true);
|
|
456
|
+
this.dispatch('request-context', { [propertyName]: valueOrCallback }, true, true, true);
|
|
410
457
|
}
|
|
411
458
|
|
|
412
459
|
/**
|
|
@@ -416,7 +463,6 @@ class BaseElement extends HTMLElement {
|
|
|
416
463
|
* @param {CustomEvent} event
|
|
417
464
|
*/
|
|
418
465
|
onRequestContext(event) {
|
|
419
|
-
const properties = this.properties();
|
|
420
466
|
const provideProperties = this.provideProperties();
|
|
421
467
|
|
|
422
468
|
Object.entries(event.detail ?? {}).forEach(([key, valueOrCallback]) => {
|
|
@@ -429,7 +475,9 @@ class BaseElement extends HTMLElement {
|
|
|
429
475
|
valueOrCallback(providedValue);
|
|
430
476
|
} else {
|
|
431
477
|
// assign to prop
|
|
432
|
-
|
|
478
|
+
const eventPath = event.composedPath();
|
|
479
|
+
const target = eventPath[0] || event.target;
|
|
480
|
+
target[key] = providedValue;
|
|
433
481
|
}
|
|
434
482
|
}
|
|
435
483
|
});
|
|
@@ -572,17 +620,32 @@ class BaseElement extends HTMLElement {
|
|
|
572
620
|
* Helper function for dispatching custom events
|
|
573
621
|
* @param {string} name
|
|
574
622
|
* @param {Object} data
|
|
575
|
-
* @param {
|
|
576
|
-
* @param {boolean} cancelable
|
|
577
|
-
* @param {boolean} composed
|
|
578
|
-
*/
|
|
579
|
-
dispatch(name, data,
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
623
|
+
* @param {CustomEventInit} options
|
|
624
|
+
* @param {boolean} cancelable - DEPRECATED
|
|
625
|
+
* @param {boolean} composed - DEPRECATED
|
|
626
|
+
*/
|
|
627
|
+
dispatch(name, data, options, cancelable = false, composed = false) {
|
|
628
|
+
let eventInit = {};
|
|
629
|
+
|
|
630
|
+
if (options !== undefined && typeof options === 'boolean') {
|
|
631
|
+
eventInit['bubbles'] = options;
|
|
632
|
+
eventInit['cancelable'] = cancelable;
|
|
633
|
+
eventInit['composed'] = composed;
|
|
634
|
+
eventInit['detail'] = data;
|
|
635
|
+
console.warn(
|
|
636
|
+
`[${this.localName}] Using the old parameter options for the event is deprecated and will be removed in v2.0! Please use the new options object that is in line with the "CustomEventInit" options`,
|
|
637
|
+
);
|
|
638
|
+
} else {
|
|
639
|
+
eventInit = {
|
|
640
|
+
bubbles: true,
|
|
641
|
+
cancelable: true,
|
|
642
|
+
composed: true,
|
|
643
|
+
detail: data,
|
|
644
|
+
...options,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const event = new CustomEvent(name, eventInit);
|
|
586
649
|
this.dispatchEvent(event);
|
|
587
650
|
}
|
|
588
651
|
|
|
@@ -593,6 +656,26 @@ class BaseElement extends HTMLElement {
|
|
|
593
656
|
getRoot() {
|
|
594
657
|
return this;
|
|
595
658
|
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* This method will be used to serialize this elements state.
|
|
662
|
+
* By default, it will generate an object with all properties from this.properties().
|
|
663
|
+
* You can override this and return a different object with more/less/different properties.
|
|
664
|
+
* @return {{[string: any]: *}}
|
|
665
|
+
*/
|
|
666
|
+
serializeState() {
|
|
667
|
+
const keys = Object.keys(this.properties());
|
|
668
|
+
return Object.fromEntries(keys.map((key) => [key, this[key]]));
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* This method will be used to restore properties on the element from a previously serialized state.
|
|
673
|
+
* You can override this and handle the deserialization differently if the default is not enough.
|
|
674
|
+
* @param {{[string: any]: *}} serializedState
|
|
675
|
+
*/
|
|
676
|
+
restoreState(serializedState) {
|
|
677
|
+
Object.assign(this, serializedState);
|
|
678
|
+
}
|
|
596
679
|
}
|
|
597
680
|
|
|
598
681
|
export { BaseElement, getClosestParentCustomElementNode, isOfSameNodeType };
|
package/src/StyledElement.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BaseElement } from './BaseElement.js';
|
|
2
2
|
import { supportsAdoptingStyleSheets, getShadowParentOrBody } from './util/DOMHelper.js';
|
|
3
|
-
|
|
3
|
+
import { globalStylesStore } from './util/GlobalStylesStore.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Options object for the StyledElement
|
|
@@ -8,23 +8,10 @@ export { i18n } from './util/i18n.js';
|
|
|
8
8
|
* @extends BaseElementOptions
|
|
9
9
|
* @property {boolean} [shadowRender] - When set to true the element will render the template (if provided) in the Shadow DOM and therefore encapsulate the element and styles from the rest of the document. Default is `false`
|
|
10
10
|
* @property {[]} [styles] - Via the styles option you can add multiple styles/stylesheets to your element. Default is `[]`
|
|
11
|
-
* @property {boolean} [adoptGlobalStyles] - When set to true element-js will look for
|
|
11
|
+
* @property {boolean | string | string[]} [adoptGlobalStyles] - When set to true element-js will look for all style elements in the global document with and apply them before any custom/element styles inside the shadow DOM. Default is `true`.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
class StyledElement extends BaseElement {
|
|
15
|
-
static globalStyles = null;
|
|
16
|
-
|
|
17
|
-
static updateGlobalStyles() {
|
|
18
|
-
// this is a runtime dependency so that every shadow dom can make use of global css
|
|
19
|
-
// we assume these styles to be inlined into the document
|
|
20
|
-
StyledElement.globalStyles = globalThis.document?.getElementById('globalStyles');
|
|
21
|
-
|
|
22
|
-
if (StyledElement.globalStyles && StyledElement['globalStyleSheet']) {
|
|
23
|
-
//updates already adopted global styles
|
|
24
|
-
StyledElement['globalStyleSheet'].replaceSync(StyledElement.globalStyles.textContent);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
15
|
/**
|
|
29
16
|
* @param {StyledElementOptions} options
|
|
30
17
|
*/
|
|
@@ -45,9 +32,23 @@ class StyledElement extends BaseElement {
|
|
|
45
32
|
connectedCallback() {
|
|
46
33
|
super.connectedCallback();
|
|
47
34
|
|
|
35
|
+
if (!this.constructor['elementStyleSheets']) {
|
|
36
|
+
this.constructor['elementStyleSheets'] = this._styles.map((style) => {
|
|
37
|
+
const cssStyleSheet = new CSSStyleSheet();
|
|
38
|
+
cssStyleSheet.replaceSync(style);
|
|
39
|
+
return cssStyleSheet;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
48
43
|
if (supportsAdoptingStyleSheets() && this._options.shadowRender) {
|
|
49
44
|
// adopting does only make sense in shadow dom. Fall back to append for light elements
|
|
50
45
|
this.adoptStyleSheets();
|
|
46
|
+
|
|
47
|
+
if (this._options.adoptGlobalStyles !== false) {
|
|
48
|
+
globalStylesStore.subscribe(() => {
|
|
49
|
+
this.adoptStyleSheets();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -65,13 +66,9 @@ class StyledElement extends BaseElement {
|
|
|
65
66
|
* @param {PropertyUpdateOptions} options
|
|
66
67
|
*/
|
|
67
68
|
update(options) {
|
|
69
|
+
// We cannot do this in connectedCallback() since the whole template will be overridden in update/render afterward
|
|
68
70
|
if (!supportsAdoptingStyleSheets() || this._options.shadowRender === false) {
|
|
69
|
-
|
|
70
|
-
const appendableStyles = [...this._styles];
|
|
71
|
-
if (this._options.shadowRender && this._options.adoptGlobalStyles) {
|
|
72
|
-
appendableStyles.unshift(StyledElement.globalStyles?.textContent ?? '');
|
|
73
|
-
}
|
|
74
|
-
this.appendStyleSheets(appendableStyles);
|
|
71
|
+
this.appendStyleSheets();
|
|
75
72
|
}
|
|
76
73
|
super.update(options);
|
|
77
74
|
}
|
|
@@ -80,39 +77,29 @@ class StyledElement extends BaseElement {
|
|
|
80
77
|
* Adopt stylesheets
|
|
81
78
|
*/
|
|
82
79
|
adoptStyleSheets() {
|
|
83
|
-
|
|
84
|
-
this.constructor['cssStyleSheets'] = this._styles.map((style) => {
|
|
85
|
-
const sheet = new CSSStyleSheet();
|
|
86
|
-
if (sheet && sheet.cssRules.length === 0) {
|
|
87
|
-
sheet.replaceSync(style);
|
|
88
|
-
}
|
|
89
|
-
return sheet;
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (StyledElement.globalStyles && !StyledElement['globalStyleSheet']) {
|
|
94
|
-
StyledElement['globalStyleSheet'] = new CSSStyleSheet();
|
|
95
|
-
StyledElement['globalStyleSheet'].replaceSync(StyledElement.globalStyles.textContent);
|
|
96
|
-
}
|
|
80
|
+
const adoptGlobalStyleSheets = this._options.shadowRender && this._options.adoptGlobalStyles !== false;
|
|
97
81
|
|
|
98
|
-
// adopt styles
|
|
99
|
-
// uses proposed solution for constructable stylesheets
|
|
100
|
-
// see: https://wicg.github.io/construct-stylesheets/#proposed-solution
|
|
101
82
|
this.getRoot().adoptedStyleSheets = [
|
|
102
|
-
...(
|
|
103
|
-
|
|
104
|
-
: []),
|
|
105
|
-
...this.constructor['cssStyleSheets'],
|
|
83
|
+
...(adoptGlobalStyleSheets ? globalStylesStore.getGlobalStyleSheets(this._options.adoptGlobalStyles) : []),
|
|
84
|
+
...this.constructor['elementStyleSheets'],
|
|
106
85
|
];
|
|
107
86
|
}
|
|
108
87
|
|
|
109
88
|
/**
|
|
110
89
|
* Custom polyfill for constructable stylesheets by appending styles to the end of an element
|
|
111
|
-
* @param {string[]} styles
|
|
112
90
|
*/
|
|
113
|
-
appendStyleSheets(
|
|
91
|
+
appendStyleSheets() {
|
|
114
92
|
const parentDocument = getShadowParentOrBody(this.getRoot());
|
|
115
|
-
|
|
93
|
+
|
|
94
|
+
const adoptGlobalStyleSheets =
|
|
95
|
+
this._options.shadowRender && this._options.adoptGlobalStyles !== false && parentDocument !== document.body;
|
|
96
|
+
|
|
97
|
+
const appendableStyles = [
|
|
98
|
+
...(adoptGlobalStyleSheets ? globalStylesStore.getGlobalStyleSheets(this._options.adoptGlobalStyles) : []),
|
|
99
|
+
...this.constructor['elementStyleSheets'],
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
appendableStyles.forEach((styleSheet, index) => {
|
|
116
103
|
const identifier = this.tagName + index;
|
|
117
104
|
|
|
118
105
|
// only append stylesheet if not already appended to shadowRoot or document
|
|
@@ -120,13 +107,13 @@ class StyledElement extends BaseElement {
|
|
|
120
107
|
const styleElement = globalThis.document?.createElement('style');
|
|
121
108
|
styleElement.id = identifier;
|
|
122
109
|
styleElement.style.display = 'none';
|
|
123
|
-
styleElement.textContent =
|
|
110
|
+
styleElement.textContent = Array.from(styleSheet.cssRules)
|
|
111
|
+
.map((rule) => rule.cssText)
|
|
112
|
+
.join('');
|
|
124
113
|
parentDocument.appendChild(styleElement);
|
|
125
114
|
}
|
|
126
115
|
});
|
|
127
116
|
}
|
|
128
117
|
}
|
|
129
118
|
|
|
130
|
-
StyledElement.updateGlobalStyles();
|
|
131
|
-
|
|
132
119
|
export { StyledElement };
|
package/src/TemplateElement.js
CHANGED
package/src/util/DOMHelper.js
CHANGED
|
@@ -75,6 +75,26 @@ export function getClosestParentCustomElementNode(domNode) {
|
|
|
75
75
|
return customElementParents.pop();
|
|
76
76
|
}
|
|
77
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
|
+
|
|
78
98
|
/**
|
|
79
99
|
* Checks whether two nodes are of the same node type
|
|
80
100
|
* @param {Node} nodeA
|
|
@@ -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
|
+
}
|