1+ // lazyframe.ts
2+
3+ import './scss/lazyframe.scss' ;
4+
5+ // --- Type Definitions ---
6+
7+ type Vendor = 'youtube' | 'youtube_nocookie' | 'vimeo' ;
8+
9+ // Options the user can pass during initialization
10+ interface LazyframeOptions {
11+ vendor ?: Vendor ;
12+ id ?: string ;
13+ src ?: string ;
14+ thumbnail ?: string ;
15+ title ?: string ;
16+ lazyload ?: boolean ;
17+ autoplay ?: boolean ;
18+ initinview ?: boolean ;
19+ loadThumbnail ?: boolean ;
20+ showPlayButton ?: boolean ;
21+ onLoad ?: ( instance : LazyframeInstance ) => void ;
22+ onAppend ?: ( iframe : HTMLIFrameElement ) => void ;
23+ onThumbnailLoad ?: ( imgUrl : string ) => void ;
24+ }
25+
26+ // Fully resolved settings for an instance, merging defaults and data-attributes
27+ interface LazyframeSettings extends LazyframeOptions {
28+ initialized : boolean ;
29+ originalSrc ?: string ;
30+ query ?: string | null ;
31+ }
32+
33+ // The internal representation of a single lazyframe instance
34+ interface LazyframeInstance {
35+ el : HTMLElement ;
36+ settings : LazyframeSettings ;
37+ iframe ?: DocumentFragment ;
38+ }
39+
40+ // --- Library Code ---
41+
42+ const Lazyframe = ( ) => {
43+ let settings : LazyframeOptions ;
44+ const elements : LazyframeInstance [ ] = [ ] ;
45+
46+ const defaults : LazyframeSettings = {
47+ initialized : false ,
48+ lazyload : true ,
49+ autoplay : true ,
50+ loadThumbnail : true ,
51+ initinview : false ,
52+ showPlayButton : true ,
53+ onLoad : ( ) => { } ,
54+ onAppend : ( ) => { } ,
55+ onThumbnailLoad : ( ) => { }
56+ } ;
57+
58+ const constants = {
59+ regex : {
60+ youtube_nocookie : / (?: y o u t u b e - n o c o o k i e \. c o m \/ \S * (?: (?: \/ e (?: m b e d ) ) ? \/ | w a t c h \? (?: \S * ?& ? v \= ) ) ) ( [ a - z A - Z 0 - 9 _ - ] { 6 , 11 } ) / ,
61+ youtube : / (?: y o u t u b e \. c o m \/ \S * (?: (?: \/ e (?: m b e d ) ) ? \/ | w a t c h \? (?: \S * ?& ? v \= ) ) | y o u t u \. b e \/ ) ( [ a - z A - Z 0 - 9 _ - ] { 6 , 11 } ) / ,
62+ vimeo : / v i m e o \. c o m \/ (?: v i d e o \/ ) ? ( [ 0 - 9 ] * ) (?: \? | ) / ,
63+ } ,
64+ condition : {
65+ youtube : ( m : RegExpMatchArray | null ) : string | false => ( m && m [ 1 ] . length === 11 ? m [ 1 ] : false ) ,
66+ youtube_nocookie : ( m : RegExpMatchArray | null ) : string | false => ( m && m [ 1 ] . length === 11 ? m [ 1 ] : false ) ,
67+ vimeo : ( m : RegExpMatchArray | null ) : string | false =>
68+ ( m && ( m [ 1 ] . length === 10 || m [ 1 ] . length === 9 || m [ 1 ] . length === 8 ) ) ? m [ 1 ] : false ,
69+ } ,
70+ src : {
71+ youtube : ( s : LazyframeSettings ) : string =>
72+ `https://www.youtube.com/embed/${ s . id } /?autoplay=${ s . autoplay ? "1" : "0" } &${ s . query || '' } ` ,
73+ youtube_nocookie : ( s : LazyframeSettings ) : string =>
74+ `https://www.youtube-nocookie.com/embed/${ s . id } /?autoplay=${ s . autoplay ? "1" : "0" } &${ s . query || '' } ` ,
75+ vimeo : ( s : LazyframeSettings ) : string =>
76+ `https://player.vimeo.com/video/${ s . id } /?autoplay=${ s . autoplay ? "1" : "0" } &${ s . query || '' } ` ,
77+ } ,
78+ endpoint : ( s : LazyframeSettings ) : string => {
79+ if ( s . vendor === 'youtube' ) {
80+ return `https://noembed.com/embed?url=https://www.youtube.com/watch?v=${ s . id } ` ;
81+ }
82+ return `https://noembed.com/embed?url=${ s . src } ` ;
83+ } ,
84+ response : {
85+ title : ( r : { title : string } ) : string => r . title ,
86+ thumbnail : ( r : { thumbnail_url : string } ) : string => r . thumbnail_url ,
87+ } ,
88+ } ;
89+
90+ function init ( selector : string | HTMLElement | NodeListOf < HTMLElement > , userOptions ?: LazyframeOptions ) : void {
91+ settings = { ...defaults , ...userOptions } ;
92+
93+ const els = typeof selector === 'string' ? document . querySelectorAll < HTMLElement > ( selector ) : selector ;
94+
95+ if ( els instanceof HTMLElement ) {
96+ loop ( els ) ;
97+ } else {
98+ els . forEach ( loop ) ;
99+ }
100+
101+ if ( settings . lazyload ) {
102+ setObservers ( ) ;
103+ }
104+ }
105+
106+ function loop ( el : HTMLElement ) : void {
107+ if ( ! ( el instanceof HTMLElement ) || el . classList . contains ( 'lazyframe--loaded' ) ) return ;
108+
109+ const instance : LazyframeInstance = {
110+ el : el ,
111+ settings : setup ( el ) ,
112+ } ;
113+
114+ instance . el . addEventListener ( 'click' , ( ) => {
115+ if ( instance . iframe ) {
116+ instance . el . appendChild ( instance . iframe ) ;
117+ }
118+ instance . el . classList . add ( 'lazyframe--activated' ) ;
119+
120+ const iframe = el . querySelector < HTMLIFrameElement > ( 'iframe' ) ;
121+ if ( iframe && instance . settings . onAppend ) {
122+ instance . settings . onAppend ( iframe ) ;
123+ }
124+ } ) ;
125+
126+ if ( settings . lazyload ) {
127+ build ( instance ) ;
128+ } else {
129+ api ( instance ) ;
130+ }
131+ }
132+
133+ function setup ( el : HTMLElement ) : LazyframeSettings {
134+ const dataAttributes : { [ key : string ] : any } = Array . from ( el . attributes )
135+ . filter ( attr => attr . value !== '' )
136+ . reduce ( ( obj , curr ) => {
137+ const name = curr . name . startsWith ( 'data-' ) ? curr . name . substring ( 5 ) : curr . name ;
138+ obj [ name ] = curr . value ;
139+ return obj ;
140+ } , { } as { [ key : string ] : any } ) ;
141+
142+ const options : LazyframeSettings = {
143+ ...settings ,
144+ ...dataAttributes ,
145+ initialized : false , // Ensure this is reset per-instance
146+ originalSrc : dataAttributes . src ,
147+ query : getQuery ( dataAttributes . src )
148+ } ;
149+
150+ // Coerce boolean data-attributes from string to boolean
151+ [ 'lazyload' , 'autoplay' , 'initinview' , 'loadThumbnail' , 'showPlayButton' ] . forEach ( option => {
152+ if ( options [ option as keyof LazyframeOptions ] === 'false' ) {
153+ ( options as any ) [ option ] = false ;
154+ }
155+ } ) ;
156+
157+ if ( options . vendor && options . src ) {
158+ const match = options . src . match ( constants . regex [ options . vendor ] ) ;
159+ const condition = constants . condition [ options . vendor ] ;
160+ const id = condition ( match ) ;
161+ if ( id ) {
162+ options . id = id ;
163+ }
164+ }
165+
166+ return options ;
167+ }
168+
169+ function getQuery ( src : string ) : string | null {
170+ if ( ! src ) return null ;
171+ const query = src . split ( '?' ) ;
172+ return query [ 1 ] ? query [ 1 ] : null ;
173+ }
174+
175+ function useApi ( settings : LazyframeSettings ) : boolean {
176+ if ( ! settings . vendor ) return false ;
177+ return ! settings . title || ! settings . thumbnail ;
178+ }
179+
180+ function api ( instance : LazyframeInstance ) : void {
181+ if ( useApi ( instance . settings ) ) {
182+ send ( instance , ( err , data ) => {
183+ if ( err || ! data ) return ;
184+
185+ const response = data [ 0 ] ;
186+ const _instance = data [ 1 ] ;
187+
188+ if ( ! _instance . settings . title ) {
189+ _instance . settings . title = constants . response . title ( response ) ;
190+ }
191+ if ( ! _instance . settings . thumbnail ) {
192+ const url = constants . response . thumbnail ( response ) ;
193+ _instance . settings . thumbnail = url ;
194+ if ( _instance . settings . onThumbnailLoad ) {
195+ _instance . settings . onThumbnailLoad ( url ) ;
196+ }
197+ }
198+ build ( _instance , true ) ;
199+ } ) ;
200+ } else {
201+ build ( instance , true ) ;
202+ }
203+ }
204+
205+ function send ( instance : LazyframeInstance , cb : ( err : boolean | null , data ?: [ any , LazyframeInstance ] ) => void ) : void {
206+ const endpoint = constants . endpoint ( instance . settings ) ;
207+ const request = new XMLHttpRequest ( ) ;
208+
209+ request . open ( 'GET' , endpoint , true ) ;
210+
211+ request . onload = function ( ) {
212+ if ( request . status >= 200 && request . status < 400 ) {
213+ const data = JSON . parse ( request . responseText ) ;
214+ cb ( null , [ data , instance ] ) ;
215+ } else {
216+ cb ( true ) ;
217+ }
218+ } ;
219+
220+ request . onerror = function ( ) { cb ( true ) ; } ;
221+ request . send ( ) ;
222+ }
223+
224+ function setPlayBtn ( btnTxt : string = 'Play' ) : HTMLButtonElement {
225+ const playButton = document . createElement ( 'button' ) ;
226+ playButton . type = 'button' ;
227+ playButton . classList . add ( 'lf-play-btn' ) ;
228+ playButton . innerHTML = `<span class="visually-hidden">${ btnTxt } </span>` ;
229+ return playButton ;
230+ }
231+
232+ function setObservers ( ) : void {
233+ const initElement = ( instance : LazyframeInstance ) => {
234+ if ( instance . settings . initialized ) return ;
235+
236+ instance . settings . initialized = true ;
237+ instance . el . classList . add ( 'lazyframe--loaded' ) ;
238+ if ( instance . settings . showPlayButton ) {
239+ instance . el . appendChild ( setPlayBtn ( ) ) ;
240+ }
241+ api ( instance ) ;
242+
243+ if ( instance . settings . initinview ) {
244+ instance . el . click ( ) ;
245+ }
246+
247+ if ( instance . settings . onLoad ) {
248+ instance . settings . onLoad ( instance ) ;
249+ }
250+ }
251+
252+ if ( 'IntersectionObserver' in window ) {
253+ const lazyframeObserver = new IntersectionObserver ( ( entries ) => {
254+ entries . forEach ( ( entry ) => {
255+ if ( entry . isIntersecting ) {
256+ const instance = elements . find ( element => element . el === entry . target ) ;
257+ if ( instance ) {
258+ initElement ( instance ) ;
259+ lazyframeObserver . unobserve ( entry . target ) ;
260+ }
261+ }
262+ } ) ;
263+ } ) ;
264+
265+ elements . forEach ( ( instance ) => {
266+ lazyframeObserver . observe ( instance . el ) ;
267+ } ) ;
268+ } else {
269+ elements . forEach ( initElement ) ;
270+ }
271+ }
272+
273+ function build ( instance : LazyframeInstance , loadImage ?: boolean ) : void {
274+ instance . iframe = getIframe ( instance . settings ) ;
275+
276+ if ( instance . settings . thumbnail && loadImage && instance . settings . loadThumbnail ) {
277+ instance . el . style . backgroundImage = `url(${ instance . settings . thumbnail } )` ;
278+ }
279+
280+ if ( instance . settings . title && instance . el . children . length === 0 ) {
281+ const titleNode = document . createElement ( 'span' ) ;
282+ titleNode . className = 'lazyframe__title' ;
283+ titleNode . textContent = instance . settings . title ;
284+ instance . el . appendChild ( titleNode ) ;
285+ }
286+
287+ if ( ! settings . lazyload ) {
288+ instance . el . classList . add ( 'lazyframe--loaded' ) ;
289+ if ( instance . settings . onLoad ) {
290+ instance . settings . onLoad ( instance ) ;
291+ }
292+ }
293+
294+ if ( ! instance . settings . initialized ) {
295+ elements . push ( instance ) ;
296+ }
297+ }
298+
299+ function getIframe ( settings : LazyframeSettings ) : DocumentFragment {
300+ const docfrag = document . createDocumentFragment ( ) ;
301+ const iframeNode = document . createElement ( 'iframe' ) ;
302+
303+ if ( settings . vendor && constants . src [ settings . vendor ] ) {
304+ settings . src = constants . src [ settings . vendor ] ( settings ) ;
305+ }
306+
307+ iframeNode . setAttribute ( 'id' , `lazyframe-${ settings . id } ` ) ;
308+ iframeNode . setAttribute ( 'src' , settings . src || '' ) ;
309+ iframeNode . setAttribute ( 'frameborder' , '0' ) ;
310+ iframeNode . setAttribute ( 'allowfullscreen' , '' ) ;
311+
312+ if ( settings . autoplay ) {
313+ iframeNode . allow = 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture' ;
314+ }
315+
316+ docfrag . appendChild ( iframeNode ) ;
317+ return docfrag ;
318+ }
319+ return init ;
320+ }
321+
322+ const lazyframe = Lazyframe ( ) ;
323+
324+ export default lazyframe ;
0 commit comments