1
1
const privateData = new WeakMap ( )
2
2
3
- const observer = new IntersectionObserver (
4
- entries => {
5
- for ( const entry of entries ) {
6
- if ( entry . isIntersecting ) {
7
- const { target} = entry
8
- observer . unobserve ( target )
9
- if ( ! ( target instanceof IncludeFragmentElement ) ) return
10
- if ( target . loading === 'lazy' ) {
11
- handleData ( target )
12
- }
13
- }
14
- }
15
- } ,
16
- {
17
- // Currently the threshold is set to 256px from the bottom of the viewport
18
- // with a threshold of 0.1. This means the element will not load until about
19
- // 2 keyboard-down-arrow presses away from being visible in the viewport,
20
- // giving us some time to fetch it before the contents are made visible
21
- rootMargin : '0px 0px 256px 0px' ,
22
- threshold : 0.01
23
- }
24
- )
25
-
26
3
// Functional stand in for the W3 spec "queue a task" paradigm
27
4
function task ( ) : Promise < void > {
28
5
return new Promise ( resolve => setTimeout ( resolve , 0 ) )
29
6
}
30
7
31
- async function handleData ( el : IncludeFragmentElement ) {
32
- observer . unobserve ( el )
33
- return getData ( el ) . then (
34
- function ( html : string ) {
35
- const template = document . createElement ( 'template' )
36
- // eslint-disable-next-line github/no-inner-html
37
- template . innerHTML = html
38
- const fragment = document . importNode ( template . content , true )
39
- const canceled = ! el . dispatchEvent (
40
- new CustomEvent ( 'include-fragment-replace' , { cancelable : true , detail : { fragment} } )
41
- )
42
- if ( canceled ) return
43
- el . replaceWith ( fragment )
44
- el . dispatchEvent ( new CustomEvent ( 'include-fragment-replaced' ) )
45
- } ,
46
- function ( ) {
47
- el . classList . add ( 'is-error' )
48
- }
49
- )
50
- }
51
-
52
- function getData ( el : IncludeFragmentElement ) {
53
- const src = el . src
54
- let data = privateData . get ( el )
55
- if ( data && data . src === src ) {
56
- return data . data
57
- } else {
58
- if ( src ) {
59
- data = fetchDataWithEvents ( el )
60
- } else {
61
- data = Promise . reject ( new Error ( 'missing src' ) )
62
- }
63
- privateData . set ( el , { src, data} )
64
- return data
65
- }
66
- }
67
-
68
- function fetchDataWithEvents ( el : IncludeFragmentElement ) {
69
- // We mimic the same event order as <img>, including the spec
70
- // which states events must be dispatched after "queue a task".
71
- // https://www.w3.org/TR/html52/semantics-embedded-content.html#the-img-element
72
- return task ( )
73
- . then ( ( ) => {
74
- el . dispatchEvent ( new Event ( 'loadstart' ) )
75
- return el . fetch ( el . request ( ) )
76
- } )
77
- . then ( response => {
78
- if ( response . status !== 200 ) {
79
- throw new Error ( `Failed to load resource: the server responded with a status of ${ response . status } ` )
80
- }
81
- const ct = response . headers . get ( 'Content-Type' )
82
- if ( ! isWildcard ( el . accept ) && ( ! ct || ! ct . includes ( el . accept ? el . accept : 'text/html' ) ) ) {
83
- throw new Error ( `Failed to load resource: expected ${ el . accept || 'text/html' } but was ${ ct } ` )
84
- }
85
- return response . text ( )
86
- } )
87
- . then (
88
- data => {
89
- // Dispatch `load` and `loadend` async to allow
90
- // the `load()` promise to resolve _before_ these
91
- // events are fired.
92
- task ( ) . then ( ( ) => {
93
- el . dispatchEvent ( new Event ( 'load' ) )
94
- el . dispatchEvent ( new Event ( 'loadend' ) )
95
- } )
96
- return data
97
- } ,
98
- error => {
99
- // Dispatch `error` and `loadend` async to allow
100
- // the `load()` promise to resolve _before_ these
101
- // events are fired.
102
- task ( ) . then ( ( ) => {
103
- el . dispatchEvent ( new Event ( 'error' ) )
104
- el . dispatchEvent ( new Event ( 'loadend' ) )
105
- } )
106
- throw error
107
- }
108
- )
109
- }
110
-
111
8
function isWildcard ( accept : string | null ) {
112
9
return accept && ! ! accept . split ( ',' ) . find ( x => x . match ( / ^ \s * \* \/ \* / ) )
113
10
}
@@ -150,19 +47,19 @@ export default class IncludeFragmentElement extends HTMLElement {
150
47
}
151
48
152
49
get data ( ) : Promise < string > {
153
- return getData ( this )
50
+ return this . # getData( )
154
51
}
155
52
156
53
attributeChangedCallback ( attribute : string , oldVal : string | null ) : void {
157
54
if ( attribute === 'src' ) {
158
55
// Source changed after attached so replace element.
159
56
if ( this . isConnected && this . loading === 'eager' ) {
160
- handleData ( this )
57
+ this . # handleData( )
161
58
}
162
59
} else if ( attribute === 'loading' ) {
163
60
// Loading mode changed to Eager after attached so replace element.
164
61
if ( this . isConnected && oldVal !== 'eager' && this . loading === 'eager' ) {
165
- handleData ( this )
62
+ this . # handleData( )
166
63
}
167
64
}
168
65
}
@@ -181,10 +78,10 @@ export default class IncludeFragmentElement extends HTMLElement {
181
78
182
79
connectedCallback ( ) : void {
183
80
if ( this . src && this . loading === 'eager' ) {
184
- handleData ( this )
81
+ this . # handleData( )
185
82
}
186
83
if ( this . loading === 'lazy' ) {
187
- observer . observe ( this )
84
+ this . # observer. observe ( this )
188
85
}
189
86
}
190
87
@@ -204,12 +101,115 @@ export default class IncludeFragmentElement extends HTMLElement {
204
101
}
205
102
206
103
load ( ) : Promise < string > {
207
- return getData ( this )
104
+ return this . # getData( )
208
105
}
209
106
210
107
fetch ( request : RequestInfo ) : Promise < Response > {
211
108
return fetch ( request )
212
109
}
110
+
111
+ #observer = new IntersectionObserver (
112
+ entries => {
113
+ for ( const entry of entries ) {
114
+ if ( entry . isIntersecting ) {
115
+ const { target} = entry
116
+ this . #observer. unobserve ( target )
117
+ if ( ! ( target instanceof IncludeFragmentElement ) ) return
118
+ if ( target . loading === 'lazy' ) {
119
+ this . #handleData( )
120
+ }
121
+ }
122
+ }
123
+ } ,
124
+ {
125
+ // Currently the threshold is set to 256px from the bottom of the viewport
126
+ // with a threshold of 0.1. This means the element will not load until about
127
+ // 2 keyboard-down-arrow presses away from being visible in the viewport,
128
+ // giving us some time to fetch it before the contents are made visible
129
+ rootMargin : '0px 0px 256px 0px' ,
130
+ threshold : 0.01
131
+ }
132
+ )
133
+
134
+ #handleData( ) : Promise < void > {
135
+ this . #observer. unobserve ( this )
136
+ return this . #getData( ) . then (
137
+ ( html : string ) => {
138
+ const template = document . createElement ( 'template' )
139
+ // eslint-disable-next-line github/no-inner-html
140
+ template . innerHTML = html
141
+ const fragment = document . importNode ( template . content , true )
142
+ const canceled = ! this . dispatchEvent (
143
+ new CustomEvent ( 'include-fragment-replace' , { cancelable : true , detail : { fragment} } )
144
+ )
145
+ if ( canceled ) return
146
+ this . replaceWith ( fragment )
147
+ this . dispatchEvent ( new CustomEvent ( 'include-fragment-replaced' ) )
148
+ } ,
149
+ ( ) => {
150
+ this . classList . add ( 'is-error' )
151
+ }
152
+ )
153
+ }
154
+
155
+ #getData( ) : Promise < string > {
156
+ const src = this . src
157
+ let data = privateData . get ( this )
158
+ if ( data && data . src === src ) {
159
+ return data . data
160
+ } else {
161
+ if ( src ) {
162
+ data = this . #fetchDataWithEvents( )
163
+ } else {
164
+ data = Promise . reject ( new Error ( 'missing src' ) )
165
+ }
166
+ privateData . set ( this , { src, data} )
167
+ return data
168
+ }
169
+ }
170
+
171
+ #fetchDataWithEvents( ) : Promise < string > {
172
+ // We mimic the same event order as <img>, including the spec
173
+ // which states events must be dispatched after "queue a task".
174
+ // https://www.w3.org/TR/html52/semantics-embedded-content.html#the-img-element
175
+ return task ( )
176
+ . then ( ( ) => {
177
+ this . dispatchEvent ( new Event ( 'loadstart' ) )
178
+ return this . fetch ( this . request ( ) )
179
+ } )
180
+ . then ( response => {
181
+ if ( response . status !== 200 ) {
182
+ throw new Error ( `Failed to load resource: the server responded with a status of ${ response . status } ` )
183
+ }
184
+ const ct = response . headers . get ( 'Content-Type' )
185
+ if ( ! isWildcard ( this . accept ) && ( ! ct || ! ct . includes ( this . accept ? this . accept : 'text/html' ) ) ) {
186
+ throw new Error ( `Failed to load resource: expected ${ this . accept || 'text/html' } but was ${ ct } ` )
187
+ }
188
+ return response . text ( )
189
+ } )
190
+ . then (
191
+ data => {
192
+ // Dispatch `load` and `loadend` async to allow
193
+ // the `load()` promise to resolve _before_ these
194
+ // events are fired.
195
+ task ( ) . then ( ( ) => {
196
+ this . dispatchEvent ( new Event ( 'load' ) )
197
+ this . dispatchEvent ( new Event ( 'loadend' ) )
198
+ } )
199
+ return data
200
+ } ,
201
+ error => {
202
+ // Dispatch `error` and `loadend` async to allow
203
+ // the `load()` promise to resolve _before_ these
204
+ // events are fired.
205
+ task ( ) . then ( ( ) => {
206
+ this . dispatchEvent ( new Event ( 'error' ) )
207
+ this . dispatchEvent ( new Event ( 'loadend' ) )
208
+ } )
209
+ throw error
210
+ }
211
+ )
212
+ }
213
213
}
214
214
215
215
declare global {
0 commit comments