11const privateData = new WeakMap ( )
22
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-
263// Functional stand in for the W3 spec "queue a task" paradigm
274function task ( ) : Promise < void > {
285 return new Promise ( resolve => setTimeout ( resolve , 0 ) )
296}
307
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-
1118function isWildcard ( accept : string | null ) {
1129 return accept && ! ! accept . split ( ',' ) . find ( x => x . match ( / ^ \s * \* \/ \* / ) )
11310}
@@ -150,19 +47,19 @@ export default class IncludeFragmentElement extends HTMLElement {
15047 }
15148
15249 get data ( ) : Promise < string > {
153- return getData ( this )
50+ return this . # getData( )
15451 }
15552
15653 attributeChangedCallback ( attribute : string , oldVal : string | null ) : void {
15754 if ( attribute === 'src' ) {
15855 // Source changed after attached so replace element.
15956 if ( this . isConnected && this . loading === 'eager' ) {
160- handleData ( this )
57+ this . # handleData( )
16158 }
16259 } else if ( attribute === 'loading' ) {
16360 // Loading mode changed to Eager after attached so replace element.
16461 if ( this . isConnected && oldVal !== 'eager' && this . loading === 'eager' ) {
165- handleData ( this )
62+ this . # handleData( )
16663 }
16764 }
16865 }
@@ -181,10 +78,10 @@ export default class IncludeFragmentElement extends HTMLElement {
18178
18279 connectedCallback ( ) : void {
18380 if ( this . src && this . loading === 'eager' ) {
184- handleData ( this )
81+ this . # handleData( )
18582 }
18683 if ( this . loading === 'lazy' ) {
187- observer . observe ( this )
84+ this . # observer. observe ( this )
18885 }
18986 }
19087
@@ -204,12 +101,115 @@ export default class IncludeFragmentElement extends HTMLElement {
204101 }
205102
206103 load ( ) : Promise < string > {
207- return getData ( this )
104+ return this . # getData( )
208105 }
209106
210107 fetch ( request : RequestInfo ) : Promise < Response > {
211108 return fetch ( request )
212109 }
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+ }
213213}
214214
215215declare global {
0 commit comments