123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479 |
- import { extend, queryAll, closest, getMimeTypeFromFile } from '../utils/util.js'
- import { isMobile } from '../utils/device.js'
- import fitty from 'fitty';
- /**
- * Handles loading, unloading and playback of slide
- * content such as images, videos and iframes.
- */
- export default class SlideContent {
- constructor( Reveal ) {
- this.Reveal = Reveal;
- this.startEmbeddedIframe = this.startEmbeddedIframe.bind( this );
- }
- /**
- * Should the given element be preloaded?
- * Decides based on local element attributes and global config.
- *
- * @param {HTMLElement} element
- */
- shouldPreload( element ) {
- // Prefer an explicit global preload setting
- let preload = this.Reveal.getConfig().preloadIframes;
- // If no global setting is available, fall back on the element's
- // own preload setting
- if( typeof preload !== 'boolean' ) {
- preload = element.hasAttribute( 'data-preload' );
- }
- return preload;
- }
- /**
- * Called when the given slide is within the configured view
- * distance. Shows the slide element and loads any content
- * that is set to load lazily (data-src).
- *
- * @param {HTMLElement} slide Slide to show
- */
- load( slide, options = {} ) {
- // Show the slide element
- slide.style.display = this.Reveal.getConfig().display;
- // Media elements with data-src attributes
- queryAll( slide, 'img[data-src], video[data-src], audio[data-src], iframe[data-src]' ).forEach( element => {
- if( element.tagName !== 'IFRAME' || this.shouldPreload( element ) ) {
- element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
- element.setAttribute( 'data-lazy-loaded', '' );
- element.removeAttribute( 'data-src' );
- }
- } );
- // Media elements with <source> children
- queryAll( slide, 'video, audio' ).forEach( media => {
- let sources = 0;
- queryAll( media, 'source[data-src]' ).forEach( source => {
- source.setAttribute( 'src', source.getAttribute( 'data-src' ) );
- source.removeAttribute( 'data-src' );
- source.setAttribute( 'data-lazy-loaded', '' );
- sources += 1;
- } );
- // Enable inline video playback in mobile Safari
- if( isMobile && media.tagName === 'VIDEO' ) {
- media.setAttribute( 'playsinline', '' );
- }
- // If we rewrote sources for this video/audio element, we need
- // to manually tell it to load from its new origin
- if( sources > 0 ) {
- media.load();
- }
- } );
- // Show the corresponding background element
- let background = slide.slideBackgroundElement;
- if( background ) {
- background.style.display = 'block';
- let backgroundContent = slide.slideBackgroundContentElement;
- let backgroundIframe = slide.getAttribute( 'data-background-iframe' );
- // If the background contains media, load it
- if( background.hasAttribute( 'data-loaded' ) === false ) {
- background.setAttribute( 'data-loaded', 'true' );
- let backgroundImage = slide.getAttribute( 'data-background-image' ),
- backgroundVideo = slide.getAttribute( 'data-background-video' ),
- backgroundVideoLoop = slide.hasAttribute( 'data-background-video-loop' ),
- backgroundVideoMuted = slide.hasAttribute( 'data-background-video-muted' );
- // Images
- if( backgroundImage ) {
- // base64
- if( /^data:/.test( backgroundImage.trim() ) ) {
- backgroundContent.style.backgroundImage = `url(${backgroundImage.trim()})`;
- }
- // URL(s)
- else {
- backgroundContent.style.backgroundImage = backgroundImage.split( ',' ).map( background => {
- return `url(${encodeURI(background.trim())})`;
- }).join( ',' );
- }
- }
- // Videos
- else if ( backgroundVideo && !this.Reveal.isSpeakerNotes() ) {
- let video = document.createElement( 'video' );
- if( backgroundVideoLoop ) {
- video.setAttribute( 'loop', '' );
- }
- if( backgroundVideoMuted ) {
- video.muted = true;
- }
- // Enable inline playback in mobile Safari
- //
- // Mute is required for video to play when using
- // swipe gestures to navigate since they don't
- // count as direct user actions :'(
- if( isMobile ) {
- video.muted = true;
- video.setAttribute( 'playsinline', '' );
- }
- // Support comma separated lists of video sources
- backgroundVideo.split( ',' ).forEach( source => {
- let type = getMimeTypeFromFile( source );
- if( type ) {
- video.innerHTML += `<source src="${source}" type="${type}">`;
- }
- else {
- video.innerHTML += `<source src="${source}">`;
- }
- } );
- backgroundContent.appendChild( video );
- }
- // Iframes
- else if( backgroundIframe && options.excludeIframes !== true ) {
- let iframe = document.createElement( 'iframe' );
- iframe.setAttribute( 'allowfullscreen', '' );
- iframe.setAttribute( 'mozallowfullscreen', '' );
- iframe.setAttribute( 'webkitallowfullscreen', '' );
- iframe.setAttribute( 'allow', 'autoplay' );
- iframe.setAttribute( 'data-src', backgroundIframe );
- iframe.style.width = '100%';
- iframe.style.height = '100%';
- iframe.style.maxHeight = '100%';
- iframe.style.maxWidth = '100%';
- backgroundContent.appendChild( iframe );
- }
- }
- // Start loading preloadable iframes
- let backgroundIframeElement = backgroundContent.querySelector( 'iframe[data-src]' );
- if( backgroundIframeElement ) {
- // Check if this iframe is eligible to be preloaded
- if( this.shouldPreload( background ) && !/autoplay=(1|true|yes)/gi.test( backgroundIframe ) ) {
- if( backgroundIframeElement.getAttribute( 'src' ) !== backgroundIframe ) {
- backgroundIframeElement.setAttribute( 'src', backgroundIframe );
- }
- }
- }
- }
- this.layout( slide );
- }
- /**
- * Applies JS-dependent layout helpers for the given slide,
- * if there are any.
- */
- layout( slide ) {
- // Autosize text with the r-fit-text class based on the
- // size of its container. This needs to happen after the
- // slide is visible in order to measure the text.
- Array.from( slide.querySelectorAll( '.r-fit-text' ) ).forEach( element => {
- fitty( element, {
- minSize: 24,
- maxSize: this.Reveal.getConfig().height * 0.8,
- observeMutations: false,
- observeWindow: false
- } );
- } );
- }
- /**
- * Unloads and hides the given slide. This is called when the
- * slide is moved outside of the configured view distance.
- *
- * @param {HTMLElement} slide
- */
- unload( slide ) {
- // Hide the slide element
- slide.style.display = 'none';
- // Hide the corresponding background element
- let background = this.Reveal.getSlideBackground( slide );
- if( background ) {
- background.style.display = 'none';
- // Unload any background iframes
- queryAll( background, 'iframe[src]' ).forEach( element => {
- element.removeAttribute( 'src' );
- } );
- }
- // Reset lazy-loaded media elements with src attributes
- queryAll( slide, 'video[data-lazy-loaded][src], audio[data-lazy-loaded][src], iframe[data-lazy-loaded][src]' ).forEach( element => {
- element.setAttribute( 'data-src', element.getAttribute( 'src' ) );
- element.removeAttribute( 'src' );
- } );
- // Reset lazy-loaded media elements with <source> children
- queryAll( slide, 'video[data-lazy-loaded] source[src], audio source[src]' ).forEach( source => {
- source.setAttribute( 'data-src', source.getAttribute( 'src' ) );
- source.removeAttribute( 'src' );
- } );
- }
- /**
- * Enforces origin-specific format rules for embedded media.
- */
- formatEmbeddedContent() {
- let _appendParamToIframeSource = ( sourceAttribute, sourceURL, param ) => {
- queryAll( this.Reveal.getSlidesElement(), 'iframe['+ sourceAttribute +'*="'+ sourceURL +'"]' ).forEach( el => {
- let src = el.getAttribute( sourceAttribute );
- if( src && src.indexOf( param ) === -1 ) {
- el.setAttribute( sourceAttribute, src + ( !/\?/.test( src ) ? '?' : '&' ) + param );
- }
- });
- };
- // YouTube frames must include "?enablejsapi=1"
- _appendParamToIframeSource( 'src', 'youtube.com/embed/', 'enablejsapi=1' );
- _appendParamToIframeSource( 'data-src', 'youtube.com/embed/', 'enablejsapi=1' );
- // Vimeo frames must include "?api=1"
- _appendParamToIframeSource( 'src', 'player.vimeo.com/', 'api=1' );
- _appendParamToIframeSource( 'data-src', 'player.vimeo.com/', 'api=1' );
- }
- /**
- * Start playback of any embedded content inside of
- * the given element.
- *
- * @param {HTMLElement} element
- */
- startEmbeddedContent( element ) {
- if( element && !this.Reveal.isSpeakerNotes() ) {
- // Restart GIFs
- queryAll( element, 'img[src$=".gif"]' ).forEach( el => {
- // Setting the same unchanged source like this was confirmed
- // to work in Chrome, FF & Safari
- el.setAttribute( 'src', el.getAttribute( 'src' ) );
- } );
- // HTML5 media elements
- queryAll( element, 'video, audio' ).forEach( el => {
- if( closest( el, '.fragment' ) && !closest( el, '.fragment.visible' ) ) {
- return;
- }
- // Prefer an explicit global autoplay setting
- let autoplay = this.Reveal.getConfig().autoPlayMedia;
- // If no global setting is available, fall back on the element's
- // own autoplay setting
- if( typeof autoplay !== 'boolean' ) {
- autoplay = el.hasAttribute( 'data-autoplay' ) || !!closest( el, '.slide-background' );
- }
- if( autoplay && typeof el.play === 'function' ) {
- // If the media is ready, start playback
- if( el.readyState > 1 ) {
- this.startEmbeddedMedia( { target: el } );
- }
- // Mobile devices never fire a loaded event so instead
- // of waiting, we initiate playback
- else if( isMobile ) {
- let promise = el.play();
- // If autoplay does not work, ensure that the controls are visible so
- // that the viewer can start the media on their own
- if( promise && typeof promise.catch === 'function' && el.controls === false ) {
- promise.catch( () => {
- el.controls = true;
- // Once the video does start playing, hide the controls again
- el.addEventListener( 'play', () => {
- el.controls = false;
- } );
- } );
- }
- }
- // If the media isn't loaded, wait before playing
- else {
- el.removeEventListener( 'loadeddata', this.startEmbeddedMedia ); // remove first to avoid dupes
- el.addEventListener( 'loadeddata', this.startEmbeddedMedia );
- }
- }
- } );
- // Normal iframes
- queryAll( element, 'iframe[src]' ).forEach( el => {
- if( closest( el, '.fragment' ) && !closest( el, '.fragment.visible' ) ) {
- return;
- }
- this.startEmbeddedIframe( { target: el } );
- } );
- // Lazy loading iframes
- queryAll( element, 'iframe[data-src]' ).forEach( el => {
- if( closest( el, '.fragment' ) && !closest( el, '.fragment.visible' ) ) {
- return;
- }
- if( el.getAttribute( 'src' ) !== el.getAttribute( 'data-src' ) ) {
- el.removeEventListener( 'load', this.startEmbeddedIframe ); // remove first to avoid dupes
- el.addEventListener( 'load', this.startEmbeddedIframe );
- el.setAttribute( 'src', el.getAttribute( 'data-src' ) );
- }
- } );
- }
- }
- /**
- * Starts playing an embedded video/audio element after
- * it has finished loading.
- *
- * @param {object} event
- */
- startEmbeddedMedia( event ) {
- let isAttachedToDOM = !!closest( event.target, 'html' ),
- isVisible = !!closest( event.target, '.present' );
- if( isAttachedToDOM && isVisible ) {
- event.target.currentTime = 0;
- event.target.play();
- }
- event.target.removeEventListener( 'loadeddata', this.startEmbeddedMedia );
- }
- /**
- * "Starts" the content of an embedded iframe using the
- * postMessage API.
- *
- * @param {object} event
- */
- startEmbeddedIframe( event ) {
- let iframe = event.target;
- if( iframe && iframe.contentWindow ) {
- let isAttachedToDOM = !!closest( event.target, 'html' ),
- isVisible = !!closest( event.target, '.present' );
- if( isAttachedToDOM && isVisible ) {
- // Prefer an explicit global autoplay setting
- let autoplay = this.Reveal.getConfig().autoPlayMedia;
- // If no global setting is available, fall back on the element's
- // own autoplay setting
- if( typeof autoplay !== 'boolean' ) {
- autoplay = iframe.hasAttribute( 'data-autoplay' ) || !!closest( iframe, '.slide-background' );
- }
- // YouTube postMessage API
- if( /youtube\.com\/embed\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) {
- iframe.contentWindow.postMessage( '{"event":"command","func":"playVideo","args":""}', '*' );
- }
- // Vimeo postMessage API
- else if( /player\.vimeo\.com\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) {
- iframe.contentWindow.postMessage( '{"method":"play"}', '*' );
- }
- // Generic postMessage API
- else {
- iframe.contentWindow.postMessage( 'slide:start', '*' );
- }
- }
- }
- }
- /**
- * Stop playback of any embedded content inside of
- * the targeted slide.
- *
- * @param {HTMLElement} element
- */
- stopEmbeddedContent( element, options = {} ) {
- options = extend( {
- // Defaults
- unloadIframes: true
- }, options );
- if( element && element.parentNode ) {
- // HTML5 media elements
- queryAll( element, 'video, audio' ).forEach( el => {
- if( !el.hasAttribute( 'data-ignore' ) && typeof el.pause === 'function' ) {
- el.setAttribute('data-paused-by-reveal', '');
- el.pause();
- }
- } );
- // Generic postMessage API for non-lazy loaded iframes
- queryAll( element, 'iframe' ).forEach( el => {
- if( el.contentWindow ) el.contentWindow.postMessage( 'slide:stop', '*' );
- el.removeEventListener( 'load', this.startEmbeddedIframe );
- });
- // YouTube postMessage API
- queryAll( element, 'iframe[src*="youtube.com/embed/"]' ).forEach( el => {
- if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) {
- el.contentWindow.postMessage( '{"event":"command","func":"pauseVideo","args":""}', '*' );
- }
- });
- // Vimeo postMessage API
- queryAll( element, 'iframe[src*="player.vimeo.com/"]' ).forEach( el => {
- if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) {
- el.contentWindow.postMessage( '{"method":"pause"}', '*' );
- }
- });
- if( options.unloadIframes === true ) {
- // Unload lazy-loaded iframes
- queryAll( element, 'iframe[data-src]' ).forEach( el => {
- // Only removing the src doesn't actually unload the frame
- // in all browsers (Firefox) so we set it to blank first
- el.setAttribute( 'src', 'about:blank' );
- el.removeAttribute( 'src' );
- } );
- }
- }
- }
- }
|