| Christophe Dervieux | 8afae13 | 2021-12-06 15:16:42 +0100 | [diff] [blame] | 1 | import { extend, queryAll, closest, getMimeTypeFromFile } from '../utils/util.js' | 
| Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 2 | import { isMobile } from '../utils/device.js' | 
|  | 3 |  | 
|  | 4 | import fitty from 'fitty'; | 
|  | 5 |  | 
|  | 6 | /** | 
|  | 7 | * Handles loading, unloading and playback of slide | 
|  | 8 | * content such as images, videos and iframes. | 
|  | 9 | */ | 
|  | 10 | export default class SlideContent { | 
|  | 11 |  | 
|  | 12 | constructor( Reveal ) { | 
|  | 13 |  | 
|  | 14 | this.Reveal = Reveal; | 
|  | 15 |  | 
|  | 16 | this.startEmbeddedIframe = this.startEmbeddedIframe.bind( this ); | 
|  | 17 |  | 
|  | 18 | } | 
|  | 19 |  | 
|  | 20 | /** | 
|  | 21 | * Should the given element be preloaded? | 
|  | 22 | * Decides based on local element attributes and global config. | 
|  | 23 | * | 
|  | 24 | * @param {HTMLElement} element | 
|  | 25 | */ | 
|  | 26 | shouldPreload( element ) { | 
|  | 27 |  | 
|  | 28 | // Prefer an explicit global preload setting | 
|  | 29 | let preload = this.Reveal.getConfig().preloadIframes; | 
|  | 30 |  | 
|  | 31 | // If no global setting is available, fall back on the element's | 
|  | 32 | // own preload setting | 
|  | 33 | if( typeof preload !== 'boolean' ) { | 
|  | 34 | preload = element.hasAttribute( 'data-preload' ); | 
|  | 35 | } | 
|  | 36 |  | 
|  | 37 | return preload; | 
|  | 38 | } | 
|  | 39 |  | 
|  | 40 | /** | 
|  | 41 | * Called when the given slide is within the configured view | 
|  | 42 | * distance. Shows the slide element and loads any content | 
|  | 43 | * that is set to load lazily (data-src). | 
|  | 44 | * | 
|  | 45 | * @param {HTMLElement} slide Slide to show | 
|  | 46 | */ | 
|  | 47 | load( slide, options = {} ) { | 
|  | 48 |  | 
|  | 49 | // Show the slide element | 
|  | 50 | slide.style.display = this.Reveal.getConfig().display; | 
|  | 51 |  | 
|  | 52 | // Media elements with data-src attributes | 
|  | 53 | queryAll( slide, 'img[data-src], video[data-src], audio[data-src], iframe[data-src]' ).forEach( element => { | 
|  | 54 | if( element.tagName !== 'IFRAME' || this.shouldPreload( element ) ) { | 
|  | 55 | element.setAttribute( 'src', element.getAttribute( 'data-src' ) ); | 
|  | 56 | element.setAttribute( 'data-lazy-loaded', '' ); | 
|  | 57 | element.removeAttribute( 'data-src' ); | 
|  | 58 | } | 
|  | 59 | } ); | 
|  | 60 |  | 
|  | 61 | // Media elements with <source> children | 
|  | 62 | queryAll( slide, 'video, audio' ).forEach( media => { | 
|  | 63 | let sources = 0; | 
|  | 64 |  | 
|  | 65 | queryAll( media, 'source[data-src]' ).forEach( source => { | 
|  | 66 | source.setAttribute( 'src', source.getAttribute( 'data-src' ) ); | 
|  | 67 | source.removeAttribute( 'data-src' ); | 
|  | 68 | source.setAttribute( 'data-lazy-loaded', '' ); | 
|  | 69 | sources += 1; | 
|  | 70 | } ); | 
|  | 71 |  | 
|  | 72 | // Enable inline video playback in mobile Safari | 
|  | 73 | if( isMobile && media.tagName === 'VIDEO' ) { | 
|  | 74 | media.setAttribute( 'playsinline', '' ); | 
|  | 75 | } | 
|  | 76 |  | 
|  | 77 | // If we rewrote sources for this video/audio element, we need | 
|  | 78 | // to manually tell it to load from its new origin | 
|  | 79 | if( sources > 0 ) { | 
|  | 80 | media.load(); | 
|  | 81 | } | 
|  | 82 | } ); | 
|  | 83 |  | 
|  | 84 |  | 
|  | 85 | // Show the corresponding background element | 
|  | 86 | let background = slide.slideBackgroundElement; | 
|  | 87 | if( background ) { | 
|  | 88 | background.style.display = 'block'; | 
|  | 89 |  | 
|  | 90 | let backgroundContent = slide.slideBackgroundContentElement; | 
|  | 91 | let backgroundIframe = slide.getAttribute( 'data-background-iframe' ); | 
|  | 92 |  | 
|  | 93 | // If the background contains media, load it | 
|  | 94 | if( background.hasAttribute( 'data-loaded' ) === false ) { | 
|  | 95 | background.setAttribute( 'data-loaded', 'true' ); | 
|  | 96 |  | 
|  | 97 | let backgroundImage = slide.getAttribute( 'data-background-image' ), | 
|  | 98 | backgroundVideo = slide.getAttribute( 'data-background-video' ), | 
|  | 99 | backgroundVideoLoop = slide.hasAttribute( 'data-background-video-loop' ), | 
|  | 100 | backgroundVideoMuted = slide.hasAttribute( 'data-background-video-muted' ); | 
|  | 101 |  | 
|  | 102 | // Images | 
|  | 103 | if( backgroundImage ) { | 
| Christophe Dervieux | 8afae13 | 2021-12-06 15:16:42 +0100 | [diff] [blame] | 104 | // base64 | 
|  | 105 | if(  /^data:/.test( backgroundImage.trim() ) ) { | 
|  | 106 | backgroundContent.style.backgroundImage = `url(${backgroundImage.trim()})`; | 
|  | 107 | } | 
|  | 108 | // URL(s) | 
|  | 109 | else { | 
|  | 110 | backgroundContent.style.backgroundImage = backgroundImage.split( ',' ).map( background => { | 
|  | 111 | return `url(${encodeURI(background.trim())})`; | 
|  | 112 | }).join( ',' ); | 
|  | 113 | } | 
| Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 114 | } | 
|  | 115 | // Videos | 
|  | 116 | else if ( backgroundVideo && !this.Reveal.isSpeakerNotes() ) { | 
|  | 117 | let video = document.createElement( 'video' ); | 
|  | 118 |  | 
|  | 119 | if( backgroundVideoLoop ) { | 
|  | 120 | video.setAttribute( 'loop', '' ); | 
|  | 121 | } | 
|  | 122 |  | 
|  | 123 | if( backgroundVideoMuted ) { | 
|  | 124 | video.muted = true; | 
|  | 125 | } | 
|  | 126 |  | 
|  | 127 | // Enable inline playback in mobile Safari | 
|  | 128 | // | 
|  | 129 | // Mute is required for video to play when using | 
|  | 130 | // swipe gestures to navigate since they don't | 
|  | 131 | // count as direct user actions :'( | 
|  | 132 | if( isMobile ) { | 
|  | 133 | video.muted = true; | 
|  | 134 | video.setAttribute( 'playsinline', '' ); | 
|  | 135 | } | 
|  | 136 |  | 
|  | 137 | // Support comma separated lists of video sources | 
|  | 138 | backgroundVideo.split( ',' ).forEach( source => { | 
| Christophe Dervieux | 8afae13 | 2021-12-06 15:16:42 +0100 | [diff] [blame] | 139 | let type = getMimeTypeFromFile( source ); | 
|  | 140 | if( type ) { | 
|  | 141 | video.innerHTML += `<source src="${source}" type="${type}">`; | 
|  | 142 | } | 
|  | 143 | else { | 
|  | 144 | video.innerHTML += `<source src="${source}">`; | 
|  | 145 | } | 
| Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 146 | } ); | 
|  | 147 |  | 
|  | 148 | backgroundContent.appendChild( video ); | 
|  | 149 | } | 
|  | 150 | // Iframes | 
|  | 151 | else if( backgroundIframe && options.excludeIframes !== true ) { | 
|  | 152 | let iframe = document.createElement( 'iframe' ); | 
|  | 153 | iframe.setAttribute( 'allowfullscreen', '' ); | 
|  | 154 | iframe.setAttribute( 'mozallowfullscreen', '' ); | 
|  | 155 | iframe.setAttribute( 'webkitallowfullscreen', '' ); | 
|  | 156 | iframe.setAttribute( 'allow', 'autoplay' ); | 
|  | 157 |  | 
|  | 158 | iframe.setAttribute( 'data-src', backgroundIframe ); | 
|  | 159 |  | 
|  | 160 | iframe.style.width  = '100%'; | 
|  | 161 | iframe.style.height = '100%'; | 
|  | 162 | iframe.style.maxHeight = '100%'; | 
|  | 163 | iframe.style.maxWidth = '100%'; | 
|  | 164 |  | 
|  | 165 | backgroundContent.appendChild( iframe ); | 
|  | 166 | } | 
|  | 167 | } | 
|  | 168 |  | 
|  | 169 | // Start loading preloadable iframes | 
|  | 170 | let backgroundIframeElement = backgroundContent.querySelector( 'iframe[data-src]' ); | 
|  | 171 | if( backgroundIframeElement ) { | 
|  | 172 |  | 
|  | 173 | // Check if this iframe is eligible to be preloaded | 
|  | 174 | if( this.shouldPreload( background ) && !/autoplay=(1|true|yes)/gi.test( backgroundIframe ) ) { | 
|  | 175 | if( backgroundIframeElement.getAttribute( 'src' ) !== backgroundIframe ) { | 
|  | 176 | backgroundIframeElement.setAttribute( 'src', backgroundIframe ); | 
|  | 177 | } | 
|  | 178 | } | 
|  | 179 |  | 
|  | 180 | } | 
|  | 181 |  | 
|  | 182 | } | 
|  | 183 |  | 
|  | 184 | this.layout( slide ); | 
|  | 185 |  | 
|  | 186 | } | 
|  | 187 |  | 
|  | 188 | /** | 
|  | 189 | * Applies JS-dependent layout helpers for the given slide, | 
|  | 190 | * if there are any. | 
|  | 191 | */ | 
|  | 192 | layout( slide ) { | 
|  | 193 |  | 
|  | 194 | // Autosize text with the r-fit-text class based on the | 
|  | 195 | // size of its container. This needs to happen after the | 
|  | 196 | // slide is visible in order to measure the text. | 
|  | 197 | Array.from( slide.querySelectorAll( '.r-fit-text' ) ).forEach( element => { | 
|  | 198 | fitty( element, { | 
|  | 199 | minSize: 24, | 
|  | 200 | maxSize: this.Reveal.getConfig().height * 0.8, | 
|  | 201 | observeMutations: false, | 
|  | 202 | observeWindow: false | 
|  | 203 | } ); | 
|  | 204 | } ); | 
|  | 205 |  | 
|  | 206 | } | 
|  | 207 |  | 
|  | 208 | /** | 
|  | 209 | * Unloads and hides the given slide. This is called when the | 
|  | 210 | * slide is moved outside of the configured view distance. | 
|  | 211 | * | 
|  | 212 | * @param {HTMLElement} slide | 
|  | 213 | */ | 
|  | 214 | unload( slide ) { | 
|  | 215 |  | 
|  | 216 | // Hide the slide element | 
|  | 217 | slide.style.display = 'none'; | 
|  | 218 |  | 
|  | 219 | // Hide the corresponding background element | 
|  | 220 | let background = this.Reveal.getSlideBackground( slide ); | 
|  | 221 | if( background ) { | 
|  | 222 | background.style.display = 'none'; | 
|  | 223 |  | 
|  | 224 | // Unload any background iframes | 
|  | 225 | queryAll( background, 'iframe[src]' ).forEach( element => { | 
|  | 226 | element.removeAttribute( 'src' ); | 
|  | 227 | } ); | 
|  | 228 | } | 
|  | 229 |  | 
|  | 230 | // Reset lazy-loaded media elements with src attributes | 
|  | 231 | queryAll( slide, 'video[data-lazy-loaded][src], audio[data-lazy-loaded][src], iframe[data-lazy-loaded][src]' ).forEach( element => { | 
|  | 232 | element.setAttribute( 'data-src', element.getAttribute( 'src' ) ); | 
|  | 233 | element.removeAttribute( 'src' ); | 
|  | 234 | } ); | 
|  | 235 |  | 
|  | 236 | // Reset lazy-loaded media elements with <source> children | 
|  | 237 | queryAll( slide, 'video[data-lazy-loaded] source[src], audio source[src]' ).forEach( source => { | 
|  | 238 | source.setAttribute( 'data-src', source.getAttribute( 'src' ) ); | 
|  | 239 | source.removeAttribute( 'src' ); | 
|  | 240 | } ); | 
|  | 241 |  | 
|  | 242 | } | 
|  | 243 |  | 
|  | 244 | /** | 
|  | 245 | * Enforces origin-specific format rules for embedded media. | 
|  | 246 | */ | 
|  | 247 | formatEmbeddedContent() { | 
|  | 248 |  | 
|  | 249 | let _appendParamToIframeSource = ( sourceAttribute, sourceURL, param ) => { | 
|  | 250 | queryAll( this.Reveal.getSlidesElement(), 'iframe['+ sourceAttribute +'*="'+ sourceURL +'"]' ).forEach( el => { | 
|  | 251 | let src = el.getAttribute( sourceAttribute ); | 
|  | 252 | if( src && src.indexOf( param ) === -1 ) { | 
|  | 253 | el.setAttribute( sourceAttribute, src + ( !/\?/.test( src ) ? '?' : '&' ) + param ); | 
|  | 254 | } | 
|  | 255 | }); | 
|  | 256 | }; | 
|  | 257 |  | 
|  | 258 | // YouTube frames must include "?enablejsapi=1" | 
|  | 259 | _appendParamToIframeSource( 'src', 'youtube.com/embed/', 'enablejsapi=1' ); | 
|  | 260 | _appendParamToIframeSource( 'data-src', 'youtube.com/embed/', 'enablejsapi=1' ); | 
|  | 261 |  | 
|  | 262 | // Vimeo frames must include "?api=1" | 
|  | 263 | _appendParamToIframeSource( 'src', 'player.vimeo.com/', 'api=1' ); | 
|  | 264 | _appendParamToIframeSource( 'data-src', 'player.vimeo.com/', 'api=1' ); | 
|  | 265 |  | 
|  | 266 | } | 
|  | 267 |  | 
|  | 268 | /** | 
|  | 269 | * Start playback of any embedded content inside of | 
|  | 270 | * the given element. | 
|  | 271 | * | 
|  | 272 | * @param {HTMLElement} element | 
|  | 273 | */ | 
|  | 274 | startEmbeddedContent( element ) { | 
|  | 275 |  | 
|  | 276 | if( element && !this.Reveal.isSpeakerNotes() ) { | 
|  | 277 |  | 
|  | 278 | // Restart GIFs | 
|  | 279 | queryAll( element, 'img[src$=".gif"]' ).forEach( el => { | 
|  | 280 | // Setting the same unchanged source like this was confirmed | 
|  | 281 | // to work in Chrome, FF & Safari | 
|  | 282 | el.setAttribute( 'src', el.getAttribute( 'src' ) ); | 
|  | 283 | } ); | 
|  | 284 |  | 
|  | 285 | // HTML5 media elements | 
|  | 286 | queryAll( element, 'video, audio' ).forEach( el => { | 
|  | 287 | if( closest( el, '.fragment' ) && !closest( el, '.fragment.visible' ) ) { | 
|  | 288 | return; | 
|  | 289 | } | 
|  | 290 |  | 
|  | 291 | // Prefer an explicit global autoplay setting | 
|  | 292 | let autoplay = this.Reveal.getConfig().autoPlayMedia; | 
|  | 293 |  | 
|  | 294 | // If no global setting is available, fall back on the element's | 
|  | 295 | // own autoplay setting | 
|  | 296 | if( typeof autoplay !== 'boolean' ) { | 
|  | 297 | autoplay = el.hasAttribute( 'data-autoplay' ) || !!closest( el, '.slide-background' ); | 
|  | 298 | } | 
|  | 299 |  | 
|  | 300 | if( autoplay && typeof el.play === 'function' ) { | 
|  | 301 |  | 
|  | 302 | // If the media is ready, start playback | 
|  | 303 | if( el.readyState > 1 ) { | 
|  | 304 | this.startEmbeddedMedia( { target: el } ); | 
|  | 305 | } | 
|  | 306 | // Mobile devices never fire a loaded event so instead | 
|  | 307 | // of waiting, we initiate playback | 
|  | 308 | else if( isMobile ) { | 
|  | 309 | let promise = el.play(); | 
|  | 310 |  | 
|  | 311 | // If autoplay does not work, ensure that the controls are visible so | 
|  | 312 | // that the viewer can start the media on their own | 
|  | 313 | if( promise && typeof promise.catch === 'function' && el.controls === false ) { | 
|  | 314 | promise.catch( () => { | 
|  | 315 | el.controls = true; | 
|  | 316 |  | 
|  | 317 | // Once the video does start playing, hide the controls again | 
|  | 318 | el.addEventListener( 'play', () => { | 
|  | 319 | el.controls = false; | 
|  | 320 | } ); | 
|  | 321 | } ); | 
|  | 322 | } | 
|  | 323 | } | 
|  | 324 | // If the media isn't loaded, wait before playing | 
|  | 325 | else { | 
|  | 326 | el.removeEventListener( 'loadeddata', this.startEmbeddedMedia ); // remove first to avoid dupes | 
|  | 327 | el.addEventListener( 'loadeddata', this.startEmbeddedMedia ); | 
|  | 328 | } | 
|  | 329 |  | 
|  | 330 | } | 
|  | 331 | } ); | 
|  | 332 |  | 
|  | 333 | // Normal iframes | 
|  | 334 | queryAll( element, 'iframe[src]' ).forEach( el => { | 
|  | 335 | if( closest( el, '.fragment' ) && !closest( el, '.fragment.visible' ) ) { | 
|  | 336 | return; | 
|  | 337 | } | 
|  | 338 |  | 
|  | 339 | this.startEmbeddedIframe( { target: el } ); | 
|  | 340 | } ); | 
|  | 341 |  | 
|  | 342 | // Lazy loading iframes | 
|  | 343 | queryAll( element, 'iframe[data-src]' ).forEach( el => { | 
|  | 344 | if( closest( el, '.fragment' ) && !closest( el, '.fragment.visible' ) ) { | 
|  | 345 | return; | 
|  | 346 | } | 
|  | 347 |  | 
|  | 348 | if( el.getAttribute( 'src' ) !== el.getAttribute( 'data-src' ) ) { | 
|  | 349 | el.removeEventListener( 'load', this.startEmbeddedIframe ); // remove first to avoid dupes | 
|  | 350 | el.addEventListener( 'load', this.startEmbeddedIframe ); | 
|  | 351 | el.setAttribute( 'src', el.getAttribute( 'data-src' ) ); | 
|  | 352 | } | 
|  | 353 | } ); | 
|  | 354 |  | 
|  | 355 | } | 
|  | 356 |  | 
|  | 357 | } | 
|  | 358 |  | 
|  | 359 | /** | 
|  | 360 | * Starts playing an embedded video/audio element after | 
|  | 361 | * it has finished loading. | 
|  | 362 | * | 
|  | 363 | * @param {object} event | 
|  | 364 | */ | 
|  | 365 | startEmbeddedMedia( event ) { | 
|  | 366 |  | 
|  | 367 | let isAttachedToDOM = !!closest( event.target, 'html' ), | 
|  | 368 | isVisible  		= !!closest( event.target, '.present' ); | 
|  | 369 |  | 
|  | 370 | if( isAttachedToDOM && isVisible ) { | 
|  | 371 | event.target.currentTime = 0; | 
|  | 372 | event.target.play(); | 
|  | 373 | } | 
|  | 374 |  | 
|  | 375 | event.target.removeEventListener( 'loadeddata', this.startEmbeddedMedia ); | 
|  | 376 |  | 
|  | 377 | } | 
|  | 378 |  | 
|  | 379 | /** | 
|  | 380 | * "Starts" the content of an embedded iframe using the | 
|  | 381 | * postMessage API. | 
|  | 382 | * | 
|  | 383 | * @param {object} event | 
|  | 384 | */ | 
|  | 385 | startEmbeddedIframe( event ) { | 
|  | 386 |  | 
|  | 387 | let iframe = event.target; | 
|  | 388 |  | 
|  | 389 | if( iframe && iframe.contentWindow ) { | 
|  | 390 |  | 
|  | 391 | let isAttachedToDOM = !!closest( event.target, 'html' ), | 
|  | 392 | isVisible  		= !!closest( event.target, '.present' ); | 
|  | 393 |  | 
|  | 394 | if( isAttachedToDOM && isVisible ) { | 
|  | 395 |  | 
|  | 396 | // Prefer an explicit global autoplay setting | 
|  | 397 | let autoplay = this.Reveal.getConfig().autoPlayMedia; | 
|  | 398 |  | 
|  | 399 | // If no global setting is available, fall back on the element's | 
|  | 400 | // own autoplay setting | 
|  | 401 | if( typeof autoplay !== 'boolean' ) { | 
|  | 402 | autoplay = iframe.hasAttribute( 'data-autoplay' ) || !!closest( iframe, '.slide-background' ); | 
|  | 403 | } | 
|  | 404 |  | 
|  | 405 | // YouTube postMessage API | 
|  | 406 | if( /youtube\.com\/embed\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) { | 
|  | 407 | iframe.contentWindow.postMessage( '{"event":"command","func":"playVideo","args":""}', '*' ); | 
|  | 408 | } | 
|  | 409 | // Vimeo postMessage API | 
|  | 410 | else if( /player\.vimeo\.com\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) { | 
|  | 411 | iframe.contentWindow.postMessage( '{"method":"play"}', '*' ); | 
|  | 412 | } | 
|  | 413 | // Generic postMessage API | 
|  | 414 | else { | 
|  | 415 | iframe.contentWindow.postMessage( 'slide:start', '*' ); | 
|  | 416 | } | 
|  | 417 |  | 
|  | 418 | } | 
|  | 419 |  | 
|  | 420 | } | 
|  | 421 |  | 
|  | 422 | } | 
|  | 423 |  | 
|  | 424 | /** | 
|  | 425 | * Stop playback of any embedded content inside of | 
|  | 426 | * the targeted slide. | 
|  | 427 | * | 
|  | 428 | * @param {HTMLElement} element | 
|  | 429 | */ | 
|  | 430 | stopEmbeddedContent( element, options = {} ) { | 
|  | 431 |  | 
|  | 432 | options = extend( { | 
|  | 433 | // Defaults | 
|  | 434 | unloadIframes: true | 
|  | 435 | }, options ); | 
|  | 436 |  | 
|  | 437 | if( element && element.parentNode ) { | 
|  | 438 | // HTML5 media elements | 
|  | 439 | queryAll( element, 'video, audio' ).forEach( el => { | 
|  | 440 | if( !el.hasAttribute( 'data-ignore' ) && typeof el.pause === 'function' ) { | 
|  | 441 | el.setAttribute('data-paused-by-reveal', ''); | 
|  | 442 | el.pause(); | 
|  | 443 | } | 
|  | 444 | } ); | 
|  | 445 |  | 
|  | 446 | // Generic postMessage API for non-lazy loaded iframes | 
|  | 447 | queryAll( element, 'iframe' ).forEach( el => { | 
|  | 448 | if( el.contentWindow ) el.contentWindow.postMessage( 'slide:stop', '*' ); | 
|  | 449 | el.removeEventListener( 'load', this.startEmbeddedIframe ); | 
|  | 450 | }); | 
|  | 451 |  | 
|  | 452 | // YouTube postMessage API | 
|  | 453 | queryAll( element, 'iframe[src*="youtube.com/embed/"]' ).forEach( el => { | 
|  | 454 | if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) { | 
|  | 455 | el.contentWindow.postMessage( '{"event":"command","func":"pauseVideo","args":""}', '*' ); | 
|  | 456 | } | 
|  | 457 | }); | 
|  | 458 |  | 
|  | 459 | // Vimeo postMessage API | 
|  | 460 | queryAll( element, 'iframe[src*="player.vimeo.com/"]' ).forEach( el => { | 
|  | 461 | if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) { | 
|  | 462 | el.contentWindow.postMessage( '{"method":"pause"}', '*' ); | 
|  | 463 | } | 
|  | 464 | }); | 
|  | 465 |  | 
|  | 466 | if( options.unloadIframes === true ) { | 
|  | 467 | // Unload lazy-loaded iframes | 
|  | 468 | queryAll( element, 'iframe[data-src]' ).forEach( el => { | 
|  | 469 | // Only removing the src doesn't actually unload the frame | 
|  | 470 | // in all browsers (Firefox) so we set it to blank first | 
|  | 471 | el.setAttribute( 'src', 'about:blank' ); | 
|  | 472 | el.removeAttribute( 'src' ); | 
|  | 473 | } ); | 
|  | 474 | } | 
|  | 475 | } | 
|  | 476 |  | 
|  | 477 | } | 
|  | 478 |  | 
| Christophe Dervieux | 8afae13 | 2021-12-06 15:16:42 +0100 | [diff] [blame] | 479 | } |