@webtides/element-js 1.1.8 → 1.2.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.
@@ -87,6 +87,35 @@ It is also possible to combine the `:host` selector with other matching selector
87
87
  }
88
88
  ```
89
89
 
90
+ #### CSS :state pseudo-class
91
+
92
+ All _element-js_ elements automatically have the "connected" state added when they are connected and rendered to the DOM. This uses the [ElementInternals.states](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/states) API, which is a standard web API.
93
+
94
+ You can target elements with specific states using the `:state()` pseudo-class in CSS:
95
+
96
+ ```css
97
+ /* Style the element when it's connected to the DOM */
98
+ :state(connected) {
99
+ opacity: 1;
100
+ transition: opacity 0.3s ease-in-out;
101
+ }
102
+
103
+ /* Style the element when it's not yet connected */
104
+ :not(:state(connected)) {
105
+ opacity: 0;
106
+ }
107
+ ```
108
+
109
+ You can combine the `:state()` pseudo-class with the `:host` selector in Shadow DOM:
110
+
111
+ ```css
112
+ :host(:state(connected)) {
113
+ border: 1px solid green;
114
+ }
115
+ ```
116
+
117
+ > Note: The `:state()` pseudo-class is part of the standard CSS Selectors Level 4 specification, but it might not be recognized by all CSS linters or editors yet.
118
+
90
119
  #### CSS custom properties
91
120
 
92
121
  Custom properties allow for reusing CSS values throughout a document. To read all about it, see [Using CSS custom properties (variables) - CSS: Cascading Style Sheets | MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webtides/element-js",
3
- "version": "1.1.8",
3
+ "version": "1.2.1",
4
4
  "module": "index.js",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -52,6 +52,9 @@ export { toString } from './util/toString.js';
52
52
  class BaseElement extends HTMLElement {
53
53
  _serializationKey;
54
54
 
55
+ /** @type {ElementInternals} */
56
+ _internals;
57
+
55
58
  /**
56
59
  * @param {BaseElementOptions} options
57
60
  */
@@ -76,6 +79,7 @@ class BaseElement extends HTMLElement {
76
79
  propertyOptions: {},
77
80
  ...options,
78
81
  };
82
+ this._internals = this.attachInternals();
79
83
  }
80
84
 
81
85
  /**
@@ -121,9 +125,11 @@ class BaseElement extends HTMLElement {
121
125
  this.registerEventsAndRefs();
122
126
 
123
127
  this.triggerHook('connected');
128
+ this._internals.states.add('connected');
124
129
  } else {
125
130
  this.requestUpdate({ notify: false }).then(() => {
126
131
  this.triggerHook('connected');
132
+ this._internals.states.add('connected');
127
133
  this.triggerHook('afterUpdate');
128
134
  });
129
135
  }
@@ -295,6 +295,9 @@ export class ChildNodePart extends Part {
295
295
  if (typeof value === 'function') {
296
296
  return this.parseValue(value());
297
297
  }
298
+ if (value === null || value === undefined) {
299
+ return value;
300
+ }
298
301
  // Everything else will be rendered as TextNode
299
302
  return value.toString();
300
303
  }
@@ -6,7 +6,7 @@ const voidElements = /^(?:area|base|br|col|embed|hr|img|input|link|meta|source|t
6
6
  const rawTextElements = /<(script|style|textarea|title)([^>]*)>(.*?)<\/\1>/i;
7
7
  const elements = /<([a-z]+[a-z0-9:._-]*)([^>]*?)(\/?)>/g;
8
8
  // TODO: v this will not match any values with escaped quotes like onClick='console.log("\'test")'
9
- const attributes = /([^\s]*)=((?:")[^"]*(?:")|(?:')[^']*(?:')|[^\s\/>]*)|([^\s\/>]*)/g;
9
+ const attributes = /([^\s=]*)=((?:")[^"]*(?:")|(?:')[^']*(?:')|[^\s\/>]*)|([^\s\/>]*)/g;
10
10
  const partPositions = /[\x01\x02]/g;
11
11
  const ssrPlaceholder = /{{dom-part\?(.*?)}}/g;
12
12
  // \x01 Node.ELEMENT_NODE
@@ -125,16 +125,20 @@ export function camelToDash(string) {
125
125
  }
126
126
 
127
127
  /**
128
- * Decodes an attribute according to {@link https://html.spec.whatwg.org/multipage/syntax.html#attributes-2}
128
+ * Decodes an attribute according to {@link https://html.spec.whatwg.org/multipage/syntax.html#attributes-2} & {@link https://html.spec.whatwg.org/multipage/parsing.html#escapingString}
129
129
  * @param {string} attribute
130
130
  * @returns {string}
131
131
  */
132
132
  export function decodeAttribute(attribute) {
133
- return `${attribute}`.replace(/&quot;/g, '"').replace(/&amp;/g, '&');
133
+ return `${attribute}`
134
+ .replace(/&lt;/g, '<')
135
+ .replace(/&gt;/g, '>')
136
+ .replace(/&quot;/g, '"')
137
+ .replace(/&amp;/g, '&');
134
138
  }
135
139
 
136
140
  /**
137
- * Encodes an attribute according to {@link https://html.spec.whatwg.org/multipage/syntax.html#attributes-2}
141
+ * Encodes an attribute according to {@link https://html.spec.whatwg.org/multipage/syntax.html#attributes-2} & {@link https://html.spec.whatwg.org/multipage/parsing.html#escapingString}
138
142
  * @param {string} attribute
139
143
  * @returns {string}
140
144
  */
@@ -142,6 +146,8 @@ export function encodeAttribute(attribute) {
142
146
  return `${attribute}`
143
147
  .replace(/&/g, '&amp;')
144
148
  .replace(/"/g, '&quot;')
149
+ .replace(/</g, '&lt;')
150
+ .replace(/>/g, '&gt;')
145
151
  .replace(/\r\n/g, '\n')
146
152
  .replace(/[\r\n]/g, '\n');
147
153
  }
@@ -4,26 +4,83 @@ import { Store } from './Store.js';
4
4
  * @property {CSSStyleSheet[]} cssStyleSheets
5
5
  */
6
6
  class GlobalStylesStore extends Store {
7
- /** @type {Map<Node, CSSStyleSheet>} */
7
+ /** @type {WeakMap<Node, CSSStyleSheet>} */
8
8
  globalStyleSheetsCache = new WeakMap();
9
9
 
10
10
  constructor() {
11
11
  super();
12
+ this.createGlobalStyleSheetsCache();
12
13
  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();
14
+ this.initMutationObserver();
15
+ }
16
+ }
17
+
18
+ initMutationObserver() {
19
+ const mutationObserver = new MutationObserver(async (mutationRecord) => {
20
+ if (!mutationRecord[0]) return;
21
+
22
+ const filteredNodes = Array.from(mutationRecord[0].addedNodes).filter(
23
+ (node) => node.tagName === 'STYLE' || node.tagName === 'LINK',
24
+ );
25
+
26
+ filteredNodes.forEach((styleNode) => {
27
+ if (styleNode.tagName === 'LINK') {
28
+ if (styleNode.sheet) {
29
+ // already loaded
30
+ this.processStylesheet(styleNode.sheet);
31
+ } else {
32
+ // wait for download
33
+ styleNode.onload = () => {
34
+ this.processStylesheet(styleNode.sheet);
35
+ };
36
+ }
37
+ } else if (styleNode.tagName === 'STYLE') {
38
+ this.processStylesheet(styleNode.sheet);
24
39
  }
25
40
  });
26
- mutationObserver.observe(globalThis.document, { subtree: true, childList: true });
41
+ });
42
+ mutationObserver.observe(globalThis.document, { subtree: true, childList: true });
43
+ }
44
+
45
+ createGlobalStyleSheetsCache() {
46
+ Array.from(globalThis.document?.styleSheets).map((styleSheet) => this.processStylesheet(styleSheet));
47
+ }
48
+
49
+ processStylesheet(styleSheet) {
50
+ let cssStyleSheet = this.globalStyleSheetsCache.get(styleSheet.ownerNode);
51
+
52
+ if (cssStyleSheet) {
53
+ return;
54
+ }
55
+
56
+ // if it does not exist yet -> build it
57
+ if (styleSheet.ownerNode.tagName === 'STYLE') {
58
+ const cssStyleSheet = new CSSStyleSheet({ media: styleSheet.media, disabled: styleSheet.disabled });
59
+ cssStyleSheet.replaceSync(styleSheet.ownerNode.textContent);
60
+
61
+ this.globalStyleSheetsCache.set(styleSheet.ownerNode, cssStyleSheet);
62
+ this.requestUpdate();
63
+ } else if (styleSheet.ownerNode.tagName === 'LINK') {
64
+ const cssStyleSheet = new CSSStyleSheet({
65
+ baseURL: styleSheet.href,
66
+ media: styleSheet.media,
67
+ disabled: styleSheet.disabled,
68
+ });
69
+ try {
70
+ Array.from(styleSheet?.cssRules ?? []).map((rule, index) =>
71
+ cssStyleSheet.insertRule(rule.cssText, index),
72
+ );
73
+
74
+ this.globalStyleSheetsCache.set(styleSheet.ownerNode, cssStyleSheet);
75
+
76
+ this.requestUpdate();
77
+ // update observer that there might be new styles to adopt
78
+ } catch (e) {
79
+ console.error(
80
+ 'GlobalStylesStore: cannot read cssRules. Maybe add crossorigin="anonymous" to your style link?',
81
+ e,
82
+ );
83
+ }
27
84
  }
28
85
  }
29
86
 
@@ -46,31 +103,10 @@ class GlobalStylesStore extends Store {
46
103
  return;
47
104
  }
48
105
 
49
- // TODO: this will always be null as we never set anything to the cache...
50
106
  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
- }
107
+ if (cssStyleSheet) {
108
+ cssStyleSheets.push(cssStyleSheet);
72
109
  }
73
- cssStyleSheets.push(cssStyleSheet);
74
110
  });
75
111
 
76
112
  return cssStyleSheets;