@webtides/element-js 0.3.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.
@@ -0,0 +1,434 @@
1
+ import { parseAttribute, isNaN, dashToCamel, camelToDash, isObjectLike } from './util/AttributeParser.js';
2
+ import { getClosestParentCustomElementNode, isOfSameNodeType } from './util/DOMHelper.js';
3
+
4
+ export { defineElement } from './util/defineElement';
5
+ export { toString } from './util/toString';
6
+
7
+ class BaseElement extends HTMLElement {
8
+ constructor(options = {}) {
9
+ super();
10
+ this.$refs = {};
11
+ this._state = {};
12
+ this._mutationObserver = null;
13
+ this._registeredEvents = [];
14
+ this._batchUpdate = null;
15
+ this._requestedUpdates = [];
16
+ this._options = {
17
+ autoUpdate: true,
18
+ deferUpdate: true,
19
+ mutationObserverOptions: {
20
+ attributes: true,
21
+ childList: true,
22
+ subtree: false,
23
+ ...options.mutationObserverOptions,
24
+ },
25
+ propertyOptions: {},
26
+ ...options,
27
+ };
28
+
29
+ if (options.childListUpdate !== undefined && options.childListUpdate !== null) {
30
+ this._options.mutationObserverOptions.childList = options.childListUpdate;
31
+ console.warn(
32
+ `[${this.localName}] Using the "childListUpdate" option is deprecated and will be removed before 1.0! Please use the "mutationObserverOptions" dictionary instead. See the docs for more info`,
33
+ );
34
+ }
35
+ }
36
+
37
+ connectedCallback() {
38
+ // define all attributes to "this" as properties
39
+ this.defineAttributesAsProperties();
40
+
41
+ // define all properties to "this"
42
+ this.defineProperties();
43
+
44
+ // define all computed properties to "this"
45
+ this.defineComputedProperties();
46
+
47
+ // define everything that should be observed
48
+ this.defineObserver();
49
+
50
+ if (this.hasAttribute('defer-update') || this._options.deferUpdate) {
51
+ // don't updates/render, but register refs and events
52
+ this.registerEventsAndRefs();
53
+
54
+ this.triggerHook('connected');
55
+ } else {
56
+ this.requestUpdate({ notify: false }).then(() => {
57
+ this.triggerHook('connected');
58
+ });
59
+ }
60
+ }
61
+
62
+ defineObserver() {
63
+ // see: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#Example_usage
64
+ this._mutationObserver = new MutationObserver((mutations) => {
65
+ mutations.forEach((mutation) => {
66
+ if (
67
+ mutation.type === 'attributes' &&
68
+ mutation.target === this &&
69
+ this._state.hasOwnProperty(dashToCamel(mutation.attributeName))
70
+ ) {
71
+ // update property by invoking the setter
72
+ this[dashToCamel(mutation.attributeName)] = parseAttribute(
73
+ this.getAttribute(mutation.attributeName),
74
+ );
75
+ }
76
+ if (mutation.type === 'attributes' && mutation.target !== this) {
77
+ this.requestUpdate();
78
+ }
79
+ if (mutation.type === 'childList') {
80
+ this.requestUpdate();
81
+ }
82
+ });
83
+ });
84
+
85
+ this._mutationObserver.observe(this, {
86
+ attributes: true,
87
+ childList: true,
88
+ subtree: false,
89
+ ...this._options.mutationObserverOptions,
90
+ });
91
+ }
92
+
93
+ disconnectedCallback() {
94
+ // remove events for elements in the component
95
+ this.removeEvents();
96
+
97
+ // remove observers
98
+ if (this._mutationObserver) this._mutationObserver.disconnect();
99
+
100
+ this.triggerHook('disconnected');
101
+ }
102
+
103
+ requestUpdate(options = { notify: true }) {
104
+ if (options.notify === true) {
105
+ this.triggerHook('beforeUpdate');
106
+ }
107
+
108
+ if (this._batchUpdate) {
109
+ cancelAnimationFrame(this._batchUpdate);
110
+ this._batchUpdate = false;
111
+ }
112
+
113
+ return new Promise((resolve, reject) => {
114
+ this._requestedUpdates.push({ resolve, reject });
115
+ this._batchUpdate = requestAnimationFrame(() => {
116
+ try {
117
+ this.update(options);
118
+ this._requestedUpdates.forEach(({ resolve, reject }) => resolve());
119
+ } catch (e) {
120
+ // console.error('Update error', e)
121
+ this._requestedUpdates.forEach(({ resolve, reject }) => reject(e));
122
+ }
123
+ this._requestedUpdates = [];
124
+ });
125
+ });
126
+ }
127
+
128
+ /**
129
+ * This should be called by the template component AFTER adding the template to
130
+ * the DOM.
131
+ * Here we will register the events and the refs for the element.
132
+ */
133
+ update(options = { notify: true }) {
134
+ this.registerEventsAndRefs();
135
+ if (options.notify === true) {
136
+ this.triggerHook('afterUpdate');
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Register events and refs for the component
142
+ */
143
+ registerEventsAndRefs() {
144
+ // register events for elements in the component
145
+ this.registerEvents();
146
+
147
+ // register $refs
148
+ this.registerRefs();
149
+ }
150
+
151
+ /**
152
+ * Defines all attributes assigned in the HTML for the element as properties on the element
153
+ * If the value for an attribute can be parsed to an object or array, it will do so
154
+ */
155
+ defineAttributesAsProperties() {
156
+ const ignoreAttributes = ['class', 'style'];
157
+ Array.from(this.attributes)
158
+ .filter((attribute) => {
159
+ return !ignoreAttributes.includes(attribute.name);
160
+ })
161
+ .forEach((attribute) => {
162
+ this.defineProperty(dashToCamel(attribute.name), parseAttribute(attribute.value), true);
163
+ });
164
+ }
165
+
166
+ /**
167
+ * Properties to be defined and assigned on the element. Should be overwritten in child components
168
+ * Properties defined here will also trigger a update() when changed.
169
+ * eg. { stringProperty: 'value', numberProperty: 13, booleanProperty: true } }
170
+ * @return {{}}
171
+ */
172
+ properties() {
173
+ return {};
174
+ }
175
+
176
+ /**
177
+ * Defines properties on the element based on keys from this.properties()
178
+ * Will trigger update() when a property was changed
179
+ */
180
+ defineProperties() {
181
+ Object.keys(this.properties()).forEach((prop) => {
182
+ // when mixing shadow dom elements with light dom elements and nesting custom elements
183
+ // it might occur that properties where set on an element before it was
184
+ // registered or connected. To avoid such timing issues we check
185
+ // if a value was set for that specific property on the
186
+ // prototype before assigning a default value
187
+ const value = this[prop] || this.properties()[prop];
188
+ this.defineProperty(prop, value);
189
+ });
190
+ }
191
+
192
+ /**
193
+ * Define a property on the element
194
+ * Will trigger watch() function when a property was changed
195
+ * Will trigger update() when a property was changed
196
+ */
197
+ defineProperty(property, value, reflectAttribute = false) {
198
+ if (this._state.hasOwnProperty(property)) {
199
+ // property has already been defined as an attribute nothing to do here
200
+ return;
201
+ }
202
+
203
+ // if property did not come from an attribute but has the option to reflect
204
+ if (!reflectAttribute && this._options.propertyOptions[property]?.reflect === true) {
205
+ this.reflectProperty({ property: property, newValue: value });
206
+ }
207
+
208
+ // remove attribute if reflect is set to false explicitly in options
209
+ if (this._options.propertyOptions[property]?.reflect === false) {
210
+ this.removeAttribute(camelToDash(property));
211
+ }
212
+
213
+ this._state[property] = value;
214
+
215
+ Object.defineProperty(this, property, {
216
+ get: () => {
217
+ return this._state[property];
218
+ },
219
+ set: (newValue) => {
220
+ const oldValue = this._state[property];
221
+ const newValueString = JSON.stringify(newValue);
222
+
223
+ if (JSON.stringify(oldValue) !== newValueString) {
224
+ this._state[property] = newValue;
225
+
226
+ if (reflectAttribute || this._options.propertyOptions[property]?.reflect === true) {
227
+ this.reflectProperty({ property, newValue, newValueString });
228
+ }
229
+
230
+ const informWatchedPropertiesAndDispatchChangeEvent = () => {
231
+ // notify watched properties (after update())
232
+ if (property in this.watch()) {
233
+ this.watch()[property](newValue, oldValue);
234
+ }
235
+
236
+ // dispatch change event
237
+ if (
238
+ property in this._options['propertyOptions'] &&
239
+ this._options['propertyOptions'][property]['notify'] === true
240
+ ) {
241
+ this.dispatch(`${camelToDash(property)}-changed`, newValue, true);
242
+ }
243
+ };
244
+
245
+ if (this._options.autoUpdate) {
246
+ this.requestUpdate({
247
+ notify: true,
248
+ property: property,
249
+ newValue: newValue,
250
+ newValueString: newValueString,
251
+ oldValue: oldValue,
252
+ }).finally(() => {
253
+ informWatchedPropertiesAndDispatchChangeEvent();
254
+ });
255
+ } else {
256
+ informWatchedPropertiesAndDispatchChangeEvent();
257
+ }
258
+ }
259
+
260
+ return this;
261
+ },
262
+ });
263
+ }
264
+
265
+ reflectProperty(options) {
266
+ const { property, newValue } = options;
267
+ const newValueString = options.newValueString || JSON.stringify(newValue);
268
+
269
+ if (newValue === undefined || newValue === null || isNaN(newValue)) {
270
+ // these would be reflected as strings: "undefined" || "null" || "NaN"
271
+ // which is not the desired behaviour. Therefore we reflect them as empty strings
272
+ this.setAttribute(camelToDash(property), '');
273
+ } else {
274
+ const attributeValue = isObjectLike(newValue) ? newValueString : newValue;
275
+ this.setAttribute(camelToDash(property), attributeValue);
276
+ }
277
+ }
278
+
279
+ // Deprecated
280
+ hooks() {
281
+ return {};
282
+ }
283
+
284
+ // Connected lifecycle hook
285
+ connected() {}
286
+
287
+ // BeforeUpdate lifecycle hook
288
+ beforeUpdate() {}
289
+
290
+ // AfterUpdate lifecycle hook
291
+ afterUpdate() {}
292
+
293
+ // Disconnected lifecycle hook
294
+ disconnected() {}
295
+
296
+ // Triggers a lifecycle hook based on the name
297
+ triggerHook(name) {
298
+ if (this.hooks && name in this.hooks()) {
299
+ console.warn(
300
+ `[${this.localName}] Using the hooks() map for lifecycle hooks is deprecated! Please overwrite the existing lifecycle hook functions. See the docs for more info`,
301
+ );
302
+ this.hooks()[name]();
303
+ return;
304
+ }
305
+
306
+ if (name in this) {
307
+ this[name]();
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Watched attributes & properties to be notified to the element when changed. Should be overwritten in child components
313
+ * When an attribute or property as the key was changed on the element, the callback function defined as value
314
+ * will be called with `newValue` and `oldValue`
315
+ * eg. { property: (oldValue, newValue) => { console.log('property changed!, oldValue, newValue); } }
316
+ * @return {{}}
317
+ */
318
+ watch() {
319
+ return {};
320
+ }
321
+
322
+ // Deprecated
323
+ computed() {
324
+ return {};
325
+ }
326
+
327
+ // Deprecated
328
+ defineComputedProperties() {
329
+ Object.keys(this.computed()).forEach((prop) => {
330
+ if (!this.hasOwnProperty(prop)) {
331
+ console.warn(
332
+ `[${this.localName}] Using the computed() map for computed properties is deprecated! Please use regular JS getters and return the computed value. See the docs for more info`,
333
+ );
334
+ Object.defineProperty(this, prop, {
335
+ get: () => this.computed()[prop](),
336
+ });
337
+ }
338
+ });
339
+ }
340
+
341
+ /**
342
+ * Return a map of events that should be registered on the element.
343
+ * eg. { '.link': {
344
+ * 'click': (e) => {console.log('MyElement.link - clicked', e)},
345
+ * 'mouseover': (e) => {console.log('MyElement.link - mouseover()', e)},
346
+ * }
347
+ * }
348
+ */
349
+ events() {
350
+ return {};
351
+ }
352
+
353
+ /**
354
+ * Registers events on the element based on events().
355
+ * Removes all previously registered events before adding them.
356
+ */
357
+ registerEvents() {
358
+ // remove all previously registered events to prevent duplicate triggers
359
+ this.removeEvents();
360
+
361
+ // register events
362
+ const eventDefinitions = this.events();
363
+ Object.keys(eventDefinitions).forEach((elementSelector) => {
364
+ const events = eventDefinitions[elementSelector];
365
+ const selectorWhiteList = { window, document, this: this.getRoot() };
366
+ const whiteListElement = selectorWhiteList[elementSelector];
367
+ const eventTargets = whiteListElement
368
+ ? [whiteListElement]
369
+ : this.getRoot().querySelectorAll(elementSelector);
370
+
371
+ eventTargets.forEach((eventTarget) => {
372
+ Object.keys(events).forEach((eventName) => {
373
+ const notation = events[eventName];
374
+ if (typeof notation === 'function') {
375
+ const callback = notation.bind(this);
376
+ eventTarget.addEventListener(eventName, callback);
377
+ this._registeredEvents.push({ eventTarget, eventName, callback });
378
+ } else if (typeof notation === 'object' && typeof notation.listener === 'function') {
379
+ const callback = notation.listener.bind(this);
380
+ eventTarget.addEventListener(eventName, callback, notation.options ?? {});
381
+ this._registeredEvents.push({ eventTarget, eventName, callback });
382
+ }
383
+ });
384
+ });
385
+ });
386
+ }
387
+
388
+ /**
389
+ * Removes all events from the element that where previously registered from the events() map.
390
+ */
391
+ removeEvents() {
392
+ this._registeredEvents.forEach(({ eventTarget, eventName, callback }) => {
393
+ if (eventTarget === window || eventTarget === document || this.getRoot().contains(eventTarget)) {
394
+ eventTarget.removeEventListener(eventName, callback);
395
+ }
396
+ });
397
+ this._registeredEvents = [];
398
+ }
399
+
400
+ /**
401
+ * Stores an object that references child elements & DOM nodes that have a ref attribute defined.
402
+ * References will be accessible on the element under this.$refs
403
+ */
404
+ registerRefs() {
405
+ const refsNodeList = this.getRoot().querySelectorAll('[ref]');
406
+ const refsArray = Array.from(refsNodeList);
407
+ const refsMap = {};
408
+
409
+ refsArray.forEach((refNode) => {
410
+ const refKey = refNode.getAttribute('ref');
411
+ refsMap[refKey] = refNode;
412
+ });
413
+
414
+ this.$refs = refsMap;
415
+ }
416
+
417
+ // Helper function for dispatching custom events
418
+ dispatch(name, data, bubble = false, cancelable = false, composed = false) {
419
+ const event = new CustomEvent(name, {
420
+ bubbles: bubble,
421
+ cancelable: cancelable,
422
+ composed: composed,
423
+ detail: data,
424
+ });
425
+ this.dispatchEvent(event);
426
+ }
427
+
428
+ // Get the root element
429
+ getRoot() {
430
+ return this;
431
+ }
432
+ }
433
+
434
+ export { BaseElement, getClosestParentCustomElementNode, isOfSameNodeType };
@@ -0,0 +1,110 @@
1
+ import { BaseElement } from './BaseElement';
2
+ import { supportsAdoptingStyleSheets, getShadowParentOrBody } from './util/DOMHelper';
3
+ export { i18n } from './util/i18n';
4
+
5
+ class StyledElement extends BaseElement {
6
+ static globalStyles = null;
7
+
8
+ static updateGlobalStyles() {
9
+ // this is a runtime dependency so that every shadow dom can make use of global css
10
+ // we assume these styles to be inlined into the document
11
+ StyledElement.globalStyles = document.getElementById('globalStyles');
12
+
13
+ if (StyledElement.globalStyles && StyledElement['globalStyleSheet']) {
14
+ //updates already adopted global styles
15
+ StyledElement['globalStyleSheet'].replaceSync(StyledElement.globalStyles.textContent);
16
+ }
17
+ }
18
+
19
+ constructor(options) {
20
+ super({
21
+ deferUpdate: false,
22
+ shadowRender: false,
23
+ styles: [],
24
+ adoptGlobalStyles: true,
25
+ ...options,
26
+ });
27
+ this._styles = [...this._options.styles, ...this.styles()];
28
+ }
29
+
30
+ connectedCallback() {
31
+ super.connectedCallback();
32
+
33
+ if (supportsAdoptingStyleSheets() && this._options.shadowRender) {
34
+ // adopting does only make sense in shadow dom. Fall back to append for light elements
35
+ this.adoptStyleSheets();
36
+ } else if (this._options.shadowRender && window.ShadyCSS !== undefined) {
37
+ // if shadowRoot is polyfilled we use ShadyCSS to copy scoped styles to <head>
38
+ window.ShadyCSS.ScopingShim.prepareAdoptedCssText(this._styles, this.localName);
39
+ }
40
+
41
+ // if shadowRoot is polyfilled - scope element template
42
+ if (window.ShadyCSS !== undefined) {
43
+ window.ShadyCSS.styleElement(this);
44
+ }
45
+ }
46
+
47
+ styles() {
48
+ return [];
49
+ }
50
+
51
+ update(options) {
52
+ if (!supportsAdoptingStyleSheets() || this._options.shadowRender === false) {
53
+ // append stylesheets to template if not already adopted
54
+ const appendableStyles = [...this._styles];
55
+ if (this._options.shadowRender && this._options.adoptGlobalStyles && !window.ShadyCSS) {
56
+ appendableStyles.unshift(StyledElement.globalStyles?.textContent ?? '');
57
+ }
58
+ this.appendStyleSheets(appendableStyles);
59
+ }
60
+ super.update(options);
61
+ }
62
+
63
+ adoptStyleSheets() {
64
+ if (!this.constructor['cssStyleSheets']) {
65
+ this.constructor['cssStyleSheets'] = this._styles.map((style) => {
66
+ const sheet = new CSSStyleSheet();
67
+ if (sheet && sheet.cssRules.length === 0) {
68
+ sheet.replaceSync(style);
69
+ }
70
+ return sheet;
71
+ });
72
+ }
73
+
74
+ if (StyledElement.globalStyles && !StyledElement['globalStyleSheet']) {
75
+ StyledElement['globalStyleSheet'] = new CSSStyleSheet();
76
+ StyledElement['globalStyleSheet'].replaceSync(StyledElement.globalStyles.textContent);
77
+ }
78
+
79
+ // adopt styles
80
+ // uses proposed solution for constructable stylesheets
81
+ // see: https://wicg.github.io/construct-stylesheets/#proposed-solution
82
+ this.getRoot().adoptedStyleSheets = [
83
+ ...(this._options.shadowRender && this._options.adoptGlobalStyles && StyledElement['globalStyleSheet']
84
+ ? [StyledElement['globalStyleSheet']]
85
+ : []),
86
+ ...this.constructor['cssStyleSheets'],
87
+ ];
88
+ }
89
+
90
+ // custom polyfill for constructable stylesheets by appending styles to the end of an element
91
+ appendStyleSheets(styles) {
92
+ const parentDocument = getShadowParentOrBody(this.getRoot());
93
+ styles.forEach((style, index) => {
94
+ const identifier = this.tagName + index;
95
+
96
+ // only append stylesheet if not already appended to shadowRoot or document
97
+ if (!parentDocument.querySelector(`#${identifier}`)) {
98
+ const styleElement = document.createElement('style');
99
+ styleElement.id = identifier;
100
+ styleElement.style.display = 'none';
101
+ styleElement.textContent = style;
102
+ parentDocument.appendChild(styleElement);
103
+ }
104
+ });
105
+ }
106
+ }
107
+
108
+ StyledElement.updateGlobalStyles();
109
+
110
+ export { StyledElement };
@@ -0,0 +1,51 @@
1
+ import { StyledElement } from './StyledElement';
2
+ import { html } from 'lit-html';
3
+ import { render } from 'lit-html/lib/shady-render.js';
4
+ export { i18n } from './util/i18n';
5
+
6
+ class TemplateElement extends StyledElement {
7
+ constructor(options) {
8
+ super({
9
+ deferUpdate: false,
10
+ shadowRender: false,
11
+ styles: [],
12
+ adoptGlobalStyles: true,
13
+ mutationObserverOptions: {
14
+ childList: false,
15
+ },
16
+ ...options,
17
+ });
18
+ this._template = this._options.template;
19
+
20
+ if (this._options.shadowRender) this.attachShadow({ mode: 'open' });
21
+ }
22
+
23
+ template() {
24
+ return html``;
25
+ }
26
+
27
+ update(options) {
28
+ this.renderTemplate();
29
+ super.update(options);
30
+ }
31
+
32
+ renderTemplate() {
33
+ const template = this._template || this.template();
34
+ if (typeof template === 'string') {
35
+ // just a plain string literal. no lit-html required
36
+ this.getRoot().innerHTML = `${template}`;
37
+ } else {
38
+ // render via lit-html
39
+ render(html` ${template} `, this.getRoot(), {
40
+ scopeName: this.localName,
41
+ eventContext: this,
42
+ });
43
+ }
44
+ }
45
+
46
+ getRoot() {
47
+ return this.shadowRoot !== null ? this.shadowRoot : this;
48
+ }
49
+ }
50
+
51
+ export { TemplateElement, html, render };
@@ -0,0 +1,66 @@
1
+ export function isObjectLike(value) {
2
+ return typeof value == 'object' && value !== null;
3
+ }
4
+
5
+ export function isJSON(str) {
6
+ try {
7
+ return JSON.parse(str) && !!str;
8
+ } catch (e) {
9
+ return false;
10
+ }
11
+ }
12
+
13
+ export function isBoolean(value) {
14
+ return value === 'true' || value === 'false';
15
+ }
16
+
17
+ export function parseBoolean(value) {
18
+ return value === 'true';
19
+ }
20
+
21
+ export function isString(value) {
22
+ return (
23
+ typeof value === 'string' ||
24
+ (!!value && typeof value === 'object' && Object.prototype.toString.call(value) === '[object String]')
25
+ );
26
+ }
27
+
28
+ export function isNumber(value) {
29
+ return new RegExp('^-?(0|0\\.\\d+|[1-9]\\d*(\\.\\d+)?)$').test(value);
30
+ }
31
+
32
+ export function isNaN(value) {
33
+ return Number.isNaN(value);
34
+ }
35
+
36
+ export function parseAttribute(value) {
37
+ if (!isString(value)) {
38
+ return value;
39
+ }
40
+
41
+ let parsedValue = value;
42
+
43
+ if (isJSON(value)) parsedValue = JSON.parse(value);
44
+ else if (isBoolean(value)) parsedValue = parseBoolean(value);
45
+ else if (isNumber(value)) parsedValue = parseFloat(value);
46
+
47
+ return parsedValue;
48
+ }
49
+
50
+ /**
51
+ * Replaces dashed-expression (i.e. some-value) to a camel-cased expression (i.e. someValue)
52
+ * @returns string
53
+ */
54
+ export function dashToCamel(string) {
55
+ if (string.indexOf('-') === -1) return string;
56
+
57
+ return string.replace(/-[a-z]/g, (matches) => matches[1].toUpperCase());
58
+ }
59
+
60
+ /**
61
+ * Replaces camel-cased expression (i.e. someValue) to a dashed-expression (i.e. some-value)
62
+ * @returns string
63
+ */
64
+ export function camelToDash(string) {
65
+ return string.replace(/([A-Z])/g, '-$1').toLowerCase();
66
+ }
@@ -0,0 +1,46 @@
1
+ export function getAllParentNodes(domNode) {
2
+ let node = domNode.parentElement;
3
+ let parents = [];
4
+ while (node) {
5
+ parents.unshift(node); // adds node to the beginning of parents
6
+ node = node.parentElement;
7
+ }
8
+
9
+ return parents;
10
+ }
11
+
12
+ // https://stackoverflow.com/questions/27453617/how-can-i-tell-if-an-element-is-in-a-shadow-dom
13
+ export function getShadowParentOrBody(element) {
14
+ if (element instanceof ShadowRoot) {
15
+ return element;
16
+ }
17
+
18
+ while (element.parentNode && (element = element.parentNode)) {
19
+ if (element instanceof ShadowRoot) {
20
+ return element;
21
+ }
22
+ }
23
+ return document.body;
24
+ }
25
+
26
+ // TODO: add function for getClosestParentOfNodeType('custom-element')
27
+
28
+ export function getClosestParentCustomElementNode(domNode) {
29
+ const customElementParents = getAllParentNodes(domNode).filter((node) => {
30
+ return node.localName && node.localName.indexOf('-') !== -1;
31
+ });
32
+
33
+ return customElementParents.pop();
34
+ }
35
+
36
+ export function isOfSameNodeType(nodeA, nodeB) {
37
+ if (!nodeA && !nodeA.localName) return false;
38
+ if (!nodeB && !nodeB.localName) return false;
39
+ return nodeA.localName === nodeB.localName;
40
+ }
41
+
42
+ export const supportsAdoptingStyleSheets = () =>
43
+ 'adoptedStyleSheets' in Document.prototype && 'replace' in CSSStyleSheet.prototype;
44
+
45
+ // for IE11 we are using the ShadyDOM Polyfill. With the polyfill we cannot append stylesheets to the shadowRoot
46
+ export const supportsAppendingStyleSheets = !window.ShadyDOM;