@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.
- package/CHANGELOG.md +58 -0
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/dist/index.js +1483 -0
- package/dist/index.js.map +7 -0
- package/index.js +8 -0
- package/package.json +67 -0
- package/src/BaseElement.js +434 -0
- package/src/StyledElement.js +110 -0
- package/src/TemplateElement.js +51 -0
- package/src/util/AttributeParser.js +66 -0
- package/src/util/DOMHelper.js +46 -0
- package/src/util/defineElement.js +12 -0
- package/src/util/i18n.js +20 -0
- package/src/util/toString.js +13 -0
|
@@ -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;
|