summaryrefslogtreecommitdiff
path: root/2024-06-18-guix-social/reveal.js/js/controllers/slidecontent.js
diff options
context:
space:
mode:
Diffstat (limited to '2024-06-18-guix-social/reveal.js/js/controllers/slidecontent.js')
-rw-r--r--2024-06-18-guix-social/reveal.js/js/controllers/slidecontent.js489
1 files changed, 489 insertions, 0 deletions
diff --git a/2024-06-18-guix-social/reveal.js/js/controllers/slidecontent.js b/2024-06-18-guix-social/reveal.js/js/controllers/slidecontent.js
new file mode 100644
index 0000000..2a24325
--- /dev/null
+++ b/2024-06-18-guix-social/reveal.js/js/controllers/slidecontent.js
@@ -0,0 +1,489 @@
+import { extend, queryAll, closest, getMimeTypeFromFile, encodeRFC3986URI } 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 ) {
+
+ if( this.Reveal.isScrollView() ) {
+ return true;
+ }
+
+ // 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 => {
+ // Decode URL(s) that are already encoded first
+ let decoded = decodeURI(background.trim());
+ return `url(${encodeRFC3986URI(decoded)})`;
+ }).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 => {
+ const sourceElement = document.createElement( 'source' );
+ sourceElement.setAttribute( 'src', source );
+
+ let type = getMimeTypeFromFile( source );
+ if( type ) {
+ sourceElement.setAttribute( 'type', type );
+ }
+
+ video.appendChild( sourceElement );
+ } );
+
+ 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 scope.
+ */
+ layout( scopeElement ) {
+
+ // 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( scopeElement.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 ) {
+ // Don't restart if media is already playing
+ if( event.target.paused || event.target.ended ) {
+ 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' );
+ } );
+ }
+ }
+
+ }
+
+}