-
Notifications
You must be signed in to change notification settings - Fork 16
/
html-include-element.js
149 lines (136 loc) · 4.09 KB
/
html-include-element.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
const LINK_LOAD_SUPPORTED = 'onload' in HTMLLinkElement.prototype;
/**
* Firefox may throw an error when accessing a not-yet-loaded cssRules property.
* @param {HTMLLinkElement}
* @return {boolean}
*/
function isLinkAlreadyLoaded(link) {
try {
return !!(link.sheet && link.sheet.cssRules);
} catch (error) {
if (error.name === 'InvalidAccessError' || error.name === 'SecurityError')
return false;
else
throw error;
}
}
/**
* Resolves when a `<link>` element has loaded its resource.
* Gracefully degrades for browsers that don't support the `load` event on links.
* in which case, it immediately resolves, causing a FOUC, but displaying content.
* resolves immediately if the stylesheet has already been loaded.
* @param {HTMLLinkElement} link
* @return {Promise<StyleSheet>}
*/
async function linkLoaded(link) {
return new Promise((resolve, reject) => {
if (!LINK_LOAD_SUPPORTED) resolve();
else if (isLinkAlreadyLoaded(link)) resolve(link.sheet);
else {
link.addEventListener('load', () => resolve(link.sheet), { once: true });
link.addEventListener('error', reject, { once: true });
}
});
}
/**
* Embeds HTML into a document.
*
* The HTML is fetched from the URL contained in the `src` attribute, using the
* fetch() API. A 'load' event is fired when the HTML is updated.
*
* The request is made using CORS by default. This can be chaned with the `mode`
* attribute.
*
* By default, the HTML is embedded into a shadow root. If the `no-shadow`
* attribute is present, the HTML will be embedded into the child content.
*
*/
export class HTMLIncludeElement extends HTMLElement {
static get observedAttributes() {
return ['src', 'mode', 'no-shadow'];
}
/**
* The URL to fetch an HTML document from.
*
* Setting this property causes a fetch the HTML from the URL.
*/
get src() {
return this.getAttribute('src');
}
set src(value) {
this.setAttribute('src', value);
}
/**
* The fetch mode to use: "cors", "no-cors", or "same-origin".
* See the fetch() documents for more information.
*
* Setting this property does not re-fetch the HTML.
*/
get mode() {
return this.getAttribute('mode');
}
set mode(value) {
this.setAttribute('mode', value);
}
/**
* If true, replaces the innerHTML of this element with the text response
* fetch. Setting this property does not re-fetch the HTML.
*/
get noShadow() {
return this.hasAttribute('no-shadow');
}
set noShadow(value) {
if (!!value) {
this.setAttribute('no-shadow', '');
} else {
this.removeAttribute('no-shadow');
}
}
constructor() {
super();
this.attachShadow({mode: 'open', delegatesFocus: this.hasAttribute('delegates-focus')});
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
}
</style>
`;
}
async attributeChangedCallback(name, oldValue, newValue) {
if (name === 'src') {
let text = '';
try {
const mode = this.mode || 'cors';
const response = await fetch(newValue, {mode});
if (!response.ok) {
throw new Error(`html-include fetch failed: ${response.statusText}`);
}
text = await response.text();
if (this.src !== newValue) {
// the src attribute was changed before we got the response, so bail
return;
}
} catch(e) {
console.error(e);
}
// Don't destroy the light DOM if we're using shadow DOM, so that slotted content is respected
if (this.noShadow) this.innerHTML = text;
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
}
</style>
${this.noShadow ? '<slot></slot>' : text}
`;
// If we're not using shadow DOM, then the consuming root
// is responsible to load its own resources
if (!this.noShadow) {
await Promise.all([...this.shadowRoot.querySelectorAll('link')].map(linkLoaded));
}
this.dispatchEvent(new Event('load'));
}
}
}
customElements.define('html-include', HTMLIncludeElement);