@webtides/element-js 1.1.7 → 1.2.0

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.7",
3
+ "version": "1.2.0",
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
  }
@@ -2,211 +2,204 @@ import { COMMENT_NODE, ELEMENT_NODE } from '../util/DOMHelper.js';
2
2
  import { Part } from './Part.js';
3
3
  import { TemplateResult } from './TemplateResult.js';
4
4
  import { TemplatePart } from './TemplatePart.js';
5
+ import { PartMarkers } from './PartMarkers.js';
5
6
 
6
7
  /**
7
- * @param {Element} commentNode
8
- * @param {Element[]} nodes
8
+ * @param {Comment} commentNode
9
+ * @param {Node[]} nodes
9
10
  */
10
- const insertNodesBetweenComments = (commentNode, nodes) => {
11
+ export const insertNodesBetweenComments = (commentNode, nodes) => {
11
12
  for (const node of nodes) {
12
13
  commentNode.parentNode.insertBefore(node, commentNode);
13
14
  }
14
15
  };
15
16
 
16
17
  /**
17
- * @param {Element} commentNode
18
+ * @param {Comment} commentNode
19
+ * @param {Node[]} nodes
18
20
  */
19
- const removeNodesBetweenComments = (commentNode) => {
20
- while (commentNode.previousSibling) {
21
- if (
22
- commentNode.previousSibling.nodeType === COMMENT_NODE &&
23
- commentNode.previousSibling.data === commentNode.data.replace('/', '')
24
- ) {
25
- break;
26
- }
27
- commentNode.parentNode.removeChild(commentNode.previousSibling);
21
+ export const replaceNodesBetweenComments = (commentNode, nodes) => {
22
+ removeNodesBetweenComments(commentNode);
23
+ for (const node of nodes) {
24
+ commentNode.parentNode.insertBefore(node, commentNode);
28
25
  }
29
26
  };
30
27
 
31
28
  /**
32
- * @param {Node} parentNode
33
- * @param {Node[]} domChildNodes
34
- * @param {Node[]} templateChildNodes
35
- * @param {Node} anchorNode
29
+ * @param {Comment} commentNode
30
+ */
31
+ export const removeNodesBetweenComments = (commentNode) => {
32
+ const nodes = getNodesBetweenComments(commentNode);
33
+ for (const node of nodes) {
34
+ commentNode.parentNode.removeChild(node);
35
+ }
36
+ };
37
+
38
+ /**
39
+ * @param {Comment} commentNode
40
+ * @param {boolean} includeComments
36
41
  * @return {Node[]}
37
42
  */
38
- const diffChildNodes = function (parentNode, domChildNodes, templateChildNodes, anchorNode) {
39
- // TODO: when diffing arrays there is sometimes two lists of TemplateParts instead of lists of childNodes
40
- const offset = domChildNodes.includes(anchorNode) ? 1 : 0;
41
-
42
- // release first level references to prevent cache mutation
43
- const clonedDomChildNodes = [...domChildNodes];
44
- const clonedTemplateChildNodes = [...templateChildNodes];
45
-
46
- // We always want to have the two arrays (domChildNodes and cloneTemplateChildNodes) to have the same length.
47
- // We also don't ever want to replace/diff the last node of the domChildNodes as it will always be the
48
- // closing comment marker (/template-part) aka the anchorNode. So we push it to the end by inserting undefined items.
49
- if (clonedTemplateChildNodes.length > clonedDomChildNodes.length) {
50
- const nodesToFill = clonedTemplateChildNodes.length - clonedDomChildNodes.length;
51
- for (let i = 0; i < nodesToFill; i++) {
52
- clonedDomChildNodes.splice(clonedDomChildNodes.length - offset, 0, undefined);
43
+ export const getNodesBetweenComments = (commentNode, includeComments = false) => {
44
+ const nodes = [];
45
+ let currentNode = includeComments ? commentNode : commentNode.previousSibling;
46
+ let nestingLevel = 0;
47
+
48
+ while (currentNode) {
49
+ if (includeComments) nodes.unshift(currentNode);
50
+
51
+ if (currentNode.nodeType === COMMENT_NODE && currentNode.data === commentNode.data) {
52
+ nestingLevel++;
53
53
  }
54
- }
55
54
 
56
- if (clonedDomChildNodes.length > clonedTemplateChildNodes.length) {
57
- const nodesToFill = clonedDomChildNodes.length - clonedTemplateChildNodes.length;
58
- for (let i = 0; i < nodesToFill; i++) {
59
- clonedTemplateChildNodes.splice(clonedTemplateChildNodes.length - offset, 0, undefined);
55
+ if (currentNode.nodeType === COMMENT_NODE && currentNode.data === commentNode.data.replace('/', '')) {
56
+ if (nestingLevel === 0) {
57
+ break;
58
+ }
59
+ nestingLevel--;
60
60
  }
61
+
62
+ if (!includeComments) nodes.unshift(currentNode);
63
+ currentNode = currentNode.previousSibling;
61
64
  }
65
+ return nodes;
66
+ };
62
67
 
63
- const length = clonedDomChildNodes.length;
68
+ /**
69
+ * @param {Node[]} newChildNodes
70
+ * @param {Comment} anchorNode
71
+ */
72
+ const diffChildNodes = function (newChildNodes, anchorNode) {
73
+ const parentNode = anchorNode.parentNode;
74
+ const oldChildNodes = getNodesBetweenComments(anchorNode);
75
+
76
+ const length = Math.max(oldChildNodes.length, newChildNodes.length);
64
77
  // Diff each node in the child node lists
65
78
  for (let index = 0; index < length; index++) {
66
- let domChildNode = clonedDomChildNodes[index];
67
- let templateChildNode = clonedTemplateChildNodes[index];
79
+ let oldChildNode = oldChildNodes[index];
80
+ let newChildNode = newChildNodes[index];
68
81
 
69
- // If the DOM node doesn't exist, append/copy the template node
70
- if (domChildNode === undefined) {
71
- if (typeof templateChildNode === 'object' && 'ELEMENT_NODE' in templateChildNode) {
72
- parentNode.insertBefore(templateChildNode, anchorNode);
73
- } else {
74
- // TODO: this might not be performant?! Can we maybe handle this in processDomNode?!
75
- // TODO: I think that we need to make ChildNodeParts from all primitive values as well
76
- parentNode.insertBefore(document.createTextNode(templateChildNode), anchorNode);
77
- }
82
+ if (newChildNode && !newChildNode.nodeType) {
83
+ throw new Error('ChildNodePart: newChildNode is not a node');
84
+ }
85
+
86
+ // If the DOM node doesn't exist, append/copy the new node
87
+ if (oldChildNode === undefined) {
88
+ parentNode.insertBefore(newChildNode, anchorNode);
78
89
  continue;
79
90
  }
80
91
 
81
- if (templateChildNode === undefined) {
82
- // If the template node doesn't exist, remove the node in the DOM
83
- if ('ELEMENT_NODE' in domChildNode) {
84
- // remove dom node
85
- parentNode.removeChild(domChildNode);
86
- } else if (Array.isArray(domChildNode) || domChildNode instanceof TemplatePart) {
87
- // remove childNodes
88
- const childNodes = Array.isArray(domChildNode) ? domChildNode : domChildNode.childNodes;
89
- for (const childNode of childNodes) {
90
- childNode.remove();
91
- }
92
- } else {
93
- console.log('This should not happen... what do we do about this node?!', { domChildNode });
94
- }
92
+ // If the new node doesn't exist, remove the node in the DOM
93
+ if (newChildNode === undefined) {
94
+ parentNode.removeChild(oldChildNode);
95
95
  continue;
96
96
  }
97
97
 
98
- // If DOM node is equal to the template node, don't do anything in the DOM
99
- if (
100
- domChildNode === templateChildNode ||
101
- (domChildNode.nodeType && templateChildNode.nodeType && domChildNode.isEqualNode(templateChildNode))
102
- ) {
103
- // but we need to assign the current DOM node into the templateChildNodes
104
- // as they will be returned here at the end and become the domNodes for the next diffing.
105
- const childIndex = templateChildNodes.indexOf(templateChildNode);
106
- templateChildNodes[childIndex] = domChildNode;
98
+ // If DOM node is equal to the new node, don't do anything in the DOM
99
+ if (oldChildNode === newChildNode || oldChildNode.isEqualNode(newChildNode)) {
107
100
  continue;
108
101
  }
109
102
 
110
103
  // Everything else is somehow different and can be replaced
111
- if (parentNode.contains(domChildNode)) {
112
- parentNode.replaceChild(templateChildNode, domChildNode);
113
- } else {
114
- console.warn('This should not happen', {
115
- parentNode,
116
- 'parentNode.contains(domChildNode)': parentNode.contains(domChildNode),
117
- templateChildNode,
118
- domChildNode,
119
- });
120
- }
104
+ parentNode.replaceChild(newChildNode, oldChildNode);
121
105
  }
122
- return templateChildNodes;
106
+ };
107
+
108
+ function getType(value) {
109
+ if (value instanceof TemplatePart) return 'templatePart';
110
+ if (value === null || value === undefined) return 'empty';
111
+ if (Array.isArray(value)) return value.length === 0 ? 'empty' : 'array';
112
+ if (value?.nodeType) return 'node';
113
+ return 'text';
114
+ }
115
+
116
+ // TODO: handle funcitons?! Should be handled in parseArray!!!
117
+ const unwrapArray = (nodes) => {
118
+ // we have to unwrap any complex objects inside the array so that we can diff arrays of childNodes
119
+ return nodes.flatMap((value) => {
120
+ if (value instanceof TemplatePart) {
121
+ return value.childNodes;
122
+ }
123
+ if (Array.isArray(value)) {
124
+ return unwrapArray(value);
125
+ }
126
+ if (typeof value === 'boolean' || typeof value === 'number' || typeof value === 'string') {
127
+ return document.createTextNode(value);
128
+ }
129
+ return value;
130
+ });
123
131
  };
124
132
 
125
133
  /**
126
- * @param {Node} comment
134
+ * @param {Comment} comment
127
135
  * @param {TemplatePart | any[] | any} initialValue
136
+ * @param {boolean} serverSideRendered
128
137
  * @return {(function(*): void)|*}
129
138
  */
130
- export const processNodePart = (comment, initialValue) => {
131
- let nodes = initialValue instanceof TemplatePart ? initialValue.childNodes : [];
132
- let oldValue = initialValue instanceof TemplatePart ? initialValue.strings : initialValue;
139
+ export const processNodePart = (comment, initialValue, serverSideRendered = false) => {
140
+ let oldValueType = 'empty';
141
+ let oldValue = undefined;
133
142
  // this is for string values to be inserted into the DOM. A cached TextNode will be used so that we don't have to constantly create new DOM nodes.
134
- let cachedTextNode = undefined;
143
+ // TODO: when server side rendered, we need to get the text node from the DOM no?!
144
+ let cachedTextNode = globalThis.document?.createTextNode('');
145
+
146
+ if (serverSideRendered) {
147
+ oldValueType = getType(initialValue);
148
+ oldValue = initialValue instanceof TemplatePart ? initialValue.strings : initialValue;
149
+ }
135
150
 
136
151
  const processNodeValue = (newValue) => {
137
- switch (typeof newValue) {
138
- // primitives are handled as text content
139
- case 'string':
140
- case 'number':
141
- case 'boolean':
142
- if (oldValue !== newValue) {
143
- oldValue = newValue;
144
- if (!cachedTextNode) cachedTextNode = globalThis.document?.createTextNode('');
145
- cachedTextNode.data = newValue;
146
-
147
- if (comment.previousSibling?.data === comment.data.replace('/', '')) {
148
- // the part is empty - we haven't rendered it yet
149
- comment.parentNode.insertBefore(cachedTextNode, comment);
150
- } else {
151
- // the part was already rendered (either server side or on the client)
152
- comment.previousSibling.data = newValue;
153
- }
154
-
155
- nodes = [cachedTextNode];
156
- }
157
- break;
158
- // null (= typeof "object") and undefined are used to clean up previous content
159
- case 'object':
160
- case 'undefined':
161
- if (newValue === null || newValue === undefined) {
162
- if (oldValue !== newValue) {
163
- oldValue = newValue;
164
- removeNodesBetweenComments(comment);
165
- nodes = [];
166
- }
167
- break;
168
- }
169
- if (Array.isArray(newValue)) {
170
- if (newValue.length === 0) {
171
- removeNodesBetweenComments(comment);
172
- nodes = [];
173
- } else {
174
- // TODO: this will always diff the arrays although they might not have changed...
175
- const unwrapArray = (nodes) => {
176
- // we have to unwrap any complex objects inside the array so that we can diff arrays of childNodes
177
- return nodes.flatMap((value) => {
178
- if (value instanceof TemplatePart) {
179
- return value.childNodes;
180
- }
181
- if (Array.isArray(value)) {
182
- return unwrapArray(value);
183
- }
184
- return value;
185
- });
186
- };
187
- nodes = diffChildNodes(comment.parentNode, nodes, unwrapArray(newValue), comment);
188
- }
189
- oldValue = newValue;
190
- break;
191
- }
152
+ let newValueType = getType(newValue);
192
153
 
193
- if (newValue instanceof TemplatePart && oldValue !== newValue.strings) {
194
- // if the new value is a node or a fragment, and it's different from the live node, then it's diffed.
195
- // static strings have changed in tpl part
196
- oldValue = newValue.strings;
197
- nodes = diffChildNodes(comment.parentNode, nodes, newValue.childNodes, comment);
198
- } else if (oldValue !== newValue && 'ELEMENT_NODE' in newValue) {
199
- // DOM Node changed, needs diffing
200
- oldValue = newValue;
201
- nodes = diffChildNodes(comment.parentNode, nodes, [newValue], comment);
202
- }
203
- break;
204
- case 'function':
205
- processNodeValue(newValue(comment));
206
- break;
154
+ if (oldValueType === newValueType && oldValue === newValue) return;
155
+
156
+ if (oldValueType !== newValueType) {
157
+ if (newValueType === 'empty') {
158
+ removeNodesBetweenComments(comment);
159
+ } else {
160
+ const getNodesForType = (value, type) => {
161
+ if (type === 'templatePart') return value.childNodes;
162
+ if (type === 'array') return unwrapArray(value);
163
+ if (type === 'node') return [value];
164
+
165
+ // Every else falls back to rendering a text node
166
+ cachedTextNode.data = value;
167
+ return [cachedTextNode];
168
+ };
169
+ replaceNodesBetweenComments(comment, getNodesForType(newValue, newValueType));
170
+ }
171
+
172
+ oldValueType = newValueType;
173
+ oldValue = newValue;
174
+ return;
175
+ }
176
+
177
+ if (newValueType === 'text') {
178
+ cachedTextNode.data = newValue;
179
+
180
+ oldValue = newValue;
181
+ return;
182
+ }
183
+
184
+ if (newValueType === 'array') {
185
+ diffChildNodes(unwrapArray(newValue), comment);
186
+
187
+ oldValue = newValue;
188
+ return;
189
+ }
190
+
191
+ if (newValueType === 'node') {
192
+ diffChildNodes([newValue], comment);
193
+
194
+ oldValue = newValue;
207
195
  }
208
196
  };
209
197
 
198
+ if (!serverSideRendered) {
199
+ // initial rendering
200
+ processNodeValue(initialValue);
201
+ }
202
+
210
203
  return processNodeValue;
211
204
  };
212
205
 
@@ -214,69 +207,35 @@ export const processNodePart = (comment, initialValue) => {
214
207
  * For updating a text node, a sequence of child nodes or an array of `any` values
215
208
  */
216
209
  export class ChildNodePart extends Part {
217
- // TODO: this is only for array values
218
- /** @type {Part[]} */
219
- parts = [];
210
+ /** @type {PartMarkers|undefined} */
211
+ markers;
220
212
 
221
- // TODO: this is only for TemplatePart values
222
- templatePart = undefined;
213
+ /** @type {TemplatePart|undefined} */
214
+ templatePart;
223
215
 
224
- // TODO: maybe we can store a generic vale|parsedValue that can be either parts[] | templatePart | any
225
-
226
- /** @type {Node[]} */
227
- childNodes = [];
216
+ /** @type {Part[]} */
217
+ arrayParts = [];
228
218
 
229
219
  /**
230
220
  * @param {Node} startNode - the start comment marker node
231
221
  * @param {any | any[]} value
232
222
  */
233
223
  constructor(startNode, value) {
234
- if (startNode && startNode?.nodeType !== COMMENT_NODE) {
235
- throw new Error('ChildNodePart: startNode is not a comment node');
224
+ if (!startNode || startNode?.nodeType !== COMMENT_NODE) {
225
+ throw new Error(
226
+ startNode ? 'ChildNodePart: startNode is undefined' : 'ChildNodePart: startNode is not a comment node',
227
+ );
236
228
  }
237
229
 
238
230
  super();
239
231
 
240
- let endNode = undefined;
241
- let serverSideRendered = false;
242
- if (startNode) {
243
- startNode.__part = this; // add Part to comment node for debugging in the browser
244
-
245
- const placeholder = startNode.data;
246
- const childNodes = [startNode];
247
- let childNode = startNode.nextSibling;
248
- while (childNode && childNode.data !== `/${placeholder}`) {
249
- childNodes.push(childNode);
250
- childNode = childNode.nextSibling;
251
- }
252
-
253
- endNode = childNode;
254
- childNodes.push(endNode);
255
-
256
- // if not SSRed, childNodes will only ever have two comment nodes, the start and the end marker
257
- if (childNodes.length > 2) {
258
- serverSideRendered = true;
259
- }
260
-
261
- this.childNodes = childNodes;
262
- }
232
+ startNode.__part = this; // add Part to comment node for debugging in the browser
233
+ this.markers = PartMarkers.createFromStartNode(startNode);
263
234
 
264
235
  // value can become array | TemplatePart | any
265
236
  const initialValue = this.parseValue(value);
266
237
 
267
- if (endNode) {
268
- this.processor = processNodePart(endNode, serverSideRendered ? initialValue : undefined);
269
- }
270
-
271
- if (!serverSideRendered) {
272
- if (value instanceof TemplateResult || Array.isArray(value)) {
273
- this.updateParts(value);
274
- }
275
-
276
- if (!(value instanceof TemplateResult)) {
277
- this.processor?.(initialValue);
278
- }
279
- }
238
+ this.processor = processNodePart(this.markers?.endNode, initialValue, this.markers?.serverSideRendered);
280
239
  }
281
240
 
282
241
  /**
@@ -290,9 +249,7 @@ export class ChildNodePart extends Part {
290
249
  this.updateParts(value);
291
250
  }
292
251
 
293
- if (!(value instanceof TemplateResult)) {
294
- this.processor?.(parsedValue);
295
- }
252
+ this.processor?.(parsedValue);
296
253
  }
297
254
 
298
255
  /**
@@ -304,38 +261,45 @@ export class ChildNodePart extends Part {
304
261
  }
305
262
  if (Array.isArray(value)) {
306
263
  for (let index = 0; index < value.length; index++) {
307
- this.parts[index]?.update(value[index]);
264
+ this.arrayParts[index]?.update(value[index]);
308
265
  }
309
266
  }
310
267
  }
311
268
 
312
269
  /**
313
270
  * @param {TemplateResult | any[] | any} value
314
- * @return {TemplatePart | any[] | any}
271
+ * @return {TemplatePart | Node | string | Array<TemplatePart | Node | string>}
315
272
  */
316
273
  parseValue(value) {
317
274
  if (value instanceof TemplateResult) {
318
275
  if (!this.templatePart) {
319
- const startNode = Array.from(this.childNodes)
320
- .filter((node) => node.nodeType === COMMENT_NODE)
321
- .find((node) => node.data === 'template-part');
322
- this.templatePart = new TemplatePart(startNode, value);
323
- const serverSideRendered = startNode !== undefined;
324
- if (!serverSideRendered) {
325
- // TODO: we should already have the "endNode" from the constructor...
326
- const endNode = Array.from(this.childNodes)
327
- .filter((node) => node.nodeType === COMMENT_NODE)
328
- .find((node) => node.data.includes('/dom-part-'));
276
+ this.templatePart = new TemplatePart(this.markers?.findNestedStartNode('template-part'), value);
277
+ if (!this.markers?.serverSideRendered) {
329
278
  // INFO: this is similar to TemplateResult#237
330
- insertNodesBetweenComments(endNode, this.templatePart.childNodes);
279
+ replaceNodesBetweenComments(this.markers?.endNode, this.templatePart.childNodes);
331
280
  }
281
+ } else if (this.templatePart.strings !== value.strings) {
282
+ // this means we have a different TemplateResult here with maybe different parts also
283
+ this.templatePart = new TemplatePart(undefined, value);
284
+ // INFO: this is similar to TemplateResult#237
285
+ replaceNodesBetweenComments(this.markers?.endNode, this.templatePart.childNodes);
332
286
  }
333
287
  return this.templatePart;
334
288
  }
335
289
  if (Array.isArray(value)) {
336
290
  return this.parseArray(value);
337
291
  }
338
- return value;
292
+ if (value?.nodeType) {
293
+ return value;
294
+ }
295
+ if (typeof value === 'function') {
296
+ return this.parseValue(value());
297
+ }
298
+ if (value === null || value === undefined) {
299
+ return value;
300
+ }
301
+ // Everything else will be rendered as TextNode
302
+ return value.toString();
339
303
  }
340
304
 
341
305
  /**
@@ -348,28 +312,30 @@ export class ChildNodePart extends Part {
348
312
  for (let index = 0; index < values.length; index++) {
349
313
  let value = values[index];
350
314
 
315
+ // TODO: it would be better to use this.parseValue() here!!!
316
+
351
317
  if (value instanceof TemplateResult) {
352
318
  // TODO this is probably wrong. Should only be taken from parts cache if it has the same type (eg. template vs string, mixed types arrays)
353
- let templatePart = this.parts[index];
319
+ let templatePart = this.arrayParts[index];
354
320
  if (!templatePart) {
355
- const templatePartCommentNodes = this.childNodes?.filter(
321
+ const templatePartCommentNodes = this.markers?.childNodes?.filter(
356
322
  (node) => node && node.nodeType === COMMENT_NODE && node.data === 'template-part',
357
323
  );
358
324
  const startNode = templatePartCommentNodes[index];
359
325
  templatePart = new TemplatePart(startNode, value);
360
- this.parts[index] = templatePart;
326
+ this.arrayParts[index] = templatePart;
361
327
  }
362
328
  parsedValues[index] = templatePart;
363
329
  } else if (Array.isArray(value)) {
364
- let childNodePart = this.parts[index];
330
+ let childNodePart = this.arrayParts[index];
365
331
  if (!childNodePart) {
366
332
  // TODO: this seems not correct :( (maybe Template vs DOM Parts !?)
367
- const templatePartCommentNodes = this.childNodes?.filter(
333
+ const templatePartCommentNodes = this.markers?.childNodes?.filter(
368
334
  (node) => node && node.nodeType === COMMENT_NODE && node.data === 'template-part',
369
335
  );
370
336
  const startNode = templatePartCommentNodes[index];
371
337
  childNodePart = new ChildNodePart(startNode, value);
372
- this.parts[index] = childNodePart;
338
+ this.arrayParts[index] = childNodePart;
373
339
  }
374
340
  parsedValues[index] = childNodePart;
375
341
  } else {
@@ -0,0 +1,62 @@
1
+ import { COMMENT_NODE } from '../util/DOMHelper.js';
2
+
3
+ export class PartMarkers {
4
+ /** @type {Comment} */
5
+ startNode;
6
+
7
+ /** @type {Comment} */
8
+ endNode;
9
+
10
+ /** @type {Node[]} */
11
+ childNodes = [];
12
+
13
+ /** @type {boolean} */
14
+ serverSideRendered = false;
15
+
16
+ /**
17
+ * @param {Comment} start
18
+ * @param {Comment} end
19
+ * @param {Node[]} childNodes
20
+ * @return {void}
21
+ */
22
+ constructor(start, end, childNodes) {
23
+ this.startNode = start;
24
+ this.endNode = end;
25
+ this.childNodes = childNodes;
26
+
27
+ // if not SSRed, childNodes will only ever have two comment nodes, the start and the end marker
28
+ if (childNodes.length > 2) {
29
+ this.serverSideRendered = true;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * @param {string} partType The type of the node to search for. Defaults to 'template-part'.
35
+ * @return {Node|undefined} The first comment node matching the specified type, or undefined if no match is found.
36
+ */
37
+ findNestedStartNode(partType = 'template-part') {
38
+ return this.childNodes.filter((node) => node.nodeType === COMMENT_NODE).find((node) => node.data === partType);
39
+ }
40
+
41
+ /**
42
+ * Creates a new instance of PartMarkers from the given start node.
43
+ * @param {Comment} startNode - The starting comment node that marks the beginning of the section.
44
+ * @return {PartMarkers} A new instance of the PartMarkers class encapsulating the start and end nodes.
45
+ */
46
+ static createFromStartNode(startNode) {
47
+ let endNode;
48
+
49
+ const placeholder = startNode.data;
50
+ const childNodes = [startNode];
51
+ let childNode = startNode.nextSibling;
52
+ while (childNode && childNode.data !== `/${placeholder}`) {
53
+ childNodes.push(childNode);
54
+ childNode = childNode.nextSibling;
55
+ }
56
+
57
+ endNode = childNode;
58
+ childNodes.push(endNode);
59
+
60
+ return new PartMarkers(startNode, endNode, childNodes);
61
+ }
62
+ }
@@ -2,17 +2,21 @@ import { COMMENT_NODE, convertStringToTemplate } from '../util/DOMHelper.js';
2
2
  import { Part } from './Part.js';
3
3
  import { TemplateResult } from './TemplateResult.js';
4
4
  import { AttributePart } from './AttributePart.js';
5
- import { ChildNodePart, processNodePart } from './ChildNodePart.js';
5
+ import { ChildNodePart, getNodesBetweenComments, replaceNodesBetweenComments } from './ChildNodePart.js';
6
6
  import { NodePart } from './NodePart.js';
7
7
  import { RawTextNodePart } from './RawTextNodePart.js';
8
+ import { PartMarkers } from './PartMarkers.js';
8
9
 
9
- /** @type {Map<TemplateStringsArray, Part[]>} */
10
+ /** @type {WeakMap<TemplateStringsArray, Part[]>} */
10
11
  const partsCache = new WeakMap();
11
12
 
12
- /** @type {Map<TemplateStringsArray, DocumentFragment>} */
13
+ /** @type {WeakMap<TemplateStringsArray, DocumentFragment>} */
13
14
  const fragmentsCache = new WeakMap();
14
15
 
15
16
  export class TemplatePart extends Part {
17
+ /** @type {PartMarkers|undefined} */
18
+ markers;
19
+
16
20
  /** @type {Part[]} */
17
21
  parts = [];
18
22
 
@@ -21,9 +25,10 @@ export class TemplatePart extends Part {
21
25
 
22
26
  /** @type {Node[]} */
23
27
  childNodes = [];
28
+ // TODO: there is childNodes here and also in this.markers now...
24
29
 
25
30
  /**
26
- * @param {Node} startNode - the start comment marker node
31
+ * @param {Node|undefined} startNode - the start comment marker node
27
32
  * @param {TemplateResult} value
28
33
  */
29
34
  constructor(startNode, value) {
@@ -33,53 +38,37 @@ export class TemplatePart extends Part {
33
38
 
34
39
  super();
35
40
 
36
- let endNode = undefined;
37
- let serverSideRendered = false;
38
41
  if (startNode) {
39
42
  startNode.__part = this; // add Part to comment node for debugging in the browser
40
-
41
- const placeholder = startNode.data;
42
- const childNodes = [startNode];
43
- let childNode = startNode.nextSibling;
44
- while (childNode && childNode.data !== `/${placeholder}`) {
45
- childNodes.push(childNode);
46
- childNode = childNode.nextSibling;
47
- }
48
-
49
- endNode = childNode;
50
- childNodes.push(endNode);
51
-
52
- // if not SSRed, childNodes will only ever have two comment nodes, the start and the end marker
53
- if (childNodes.length > 2) {
54
- serverSideRendered = true;
55
- }
56
-
57
- this.childNodes = childNodes;
43
+ this.markers = PartMarkers.createFromStartNode(startNode);
44
+ this.childNodes = this.markers.childNodes;
58
45
  }
59
46
 
60
47
  this.parseValue(value);
61
48
 
62
- if (!serverSideRendered) {
49
+ if (!this.markers?.serverSideRendered) {
63
50
  // TODO: this is causing double diffing because of the this.parseValue() before, ChildNode parts will be constructed + processed
51
+ // TODO: I think that maybe ChildNode Parts work, but Other Parts like Attributes are not set initially otherwise...
64
52
  this.updateParts(value.values);
65
53
  }
66
54
 
67
- endNode = this.childNodes[this.childNodes.length - 1];
68
-
69
- if (endNode) {
70
- this.processor = processNodePart(endNode, this);
71
- }
55
+ // TODO: this is somewhat strange that we are creating this twice...
56
+ this.markers = PartMarkers.createFromStartNode(this.childNodes[0]);
72
57
  }
73
58
 
74
59
  /**
75
- * @param {TemplateResult} value
60
+ * @param {TemplateResult} templateResult
76
61
  */
77
- update(value) {
78
- const shouldUpdateDOM = this.parseValue(value);
62
+ update(templateResult) {
63
+ const shouldUpdateDOM = this.strings !== templateResult.strings;
79
64
  if (shouldUpdateDOM) {
80
- this.processor?.(this);
65
+ this.parseValue(templateResult);
66
+ // TODO: this is a strange way of replacing the childnodes...
67
+ const endNode = this.childNodes[this.childNodes.length - 1];
68
+ // INFO: this is similar to TemplateResult#237
69
+ replaceNodesBetweenComments(this.markers?.endNode, getNodesBetweenComments(endNode));
81
70
  }
82
- this.updateParts(value.values);
71
+ this.updateParts(templateResult.values);
83
72
  }
84
73
 
85
74
  /**
@@ -93,58 +82,44 @@ export class TemplatePart extends Part {
93
82
 
94
83
  /**
95
84
  * @param {TemplateResult} templateResult
96
- * @return {boolean} whether the part needs to be updated in the DOM
97
85
  */
98
86
  parseValue(templateResult) {
99
- if (this.strings !== templateResult.strings) {
100
- let fragment = fragmentsCache.get(templateResult.strings);
101
- if (!fragment) {
102
- fragment = this.parseFragment(templateResult);
103
- fragmentsCache.set(templateResult.strings, fragment);
104
- }
105
- const importedFragment = globalThis.document?.importNode(fragment, true);
106
- this.childNodes = importedFragment.childNodes;
107
-
108
- let parts = partsCache.get(templateResult.strings);
109
- if (!parts) {
110
- parts = templateResult.parseParts(this.childNodes);
111
- partsCache.set(templateResult.strings, parts);
112
- }
113
-
114
- this.parts = parts
115
- .map((part) => {
116
- // We currently need the path because the fragment will be cloned via importNode and therefore the node will be a different one
117
- part.node = part.path.reduceRight(({ childNodes }, i) => childNodes[i], this);
118
- return part;
119
- })
120
- .map((part, index) => {
121
- if (part.type === 'node') {
122
- return new ChildNodePart(part.node, templateResult.values[index]);
123
- }
124
- if (part.type === 'attribute') {
125
- return new AttributePart(part.node, part.name, part.initialValue);
126
- }
127
- if (part.type === 'raw-text-node') {
128
- return new RawTextNodePart(part.node, part.initialValue);
129
- }
130
- if (part.type === 'directive') {
131
- return new NodePart(part.node, templateResult.values[index]);
132
- }
133
- throw `cannot map part: ${part}`;
134
- });
135
- this.strings = templateResult.strings;
136
- this.childNodes = [...this.childNodes];
137
- return true;
87
+ let fragment = fragmentsCache.get(templateResult.strings);
88
+ if (!fragment) {
89
+ fragment = convertStringToTemplate(templateResult.templateString);
90
+ fragmentsCache.set(templateResult.strings, fragment);
138
91
  }
139
- return false;
140
- }
92
+ const importedFragment = globalThis.document?.importNode(fragment, true);
93
+ this.childNodes = importedFragment.childNodes;
141
94
 
142
- /**
143
- * @param {TemplateResult} templateResult
144
- * @return {DocumentFragment}
145
- */
146
- parseFragment(templateResult) {
147
- const templateString = templateResult.templateString;
148
- return convertStringToTemplate(templateString);
95
+ let parts = partsCache.get(templateResult.strings);
96
+ if (!parts) {
97
+ parts = templateResult.parseParts(this.childNodes);
98
+ partsCache.set(templateResult.strings, parts);
99
+ }
100
+
101
+ this.parts = parts
102
+ .map((part) => {
103
+ // We currently need the path because the fragment will be cloned via importNode and therefore the node will be a different one
104
+ part.node = part.path.reduceRight(({ childNodes }, i) => childNodes[i], this);
105
+ return part;
106
+ })
107
+ .map((part, index) => {
108
+ if (part.type === 'node') {
109
+ return new ChildNodePart(part.node, templateResult.values[index]);
110
+ }
111
+ if (part.type === 'attribute') {
112
+ return new AttributePart(part.node, part.name, part.initialValue);
113
+ }
114
+ if (part.type === 'raw-text-node') {
115
+ return new RawTextNodePart(part.node, 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
+ });
122
+ this.strings = templateResult.strings;
123
+ this.childNodes = [...this.childNodes];
149
124
  }
150
125
  }
@@ -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;