/** * Handles the display of reveal.js' overlay elements used * to preview iframes, images & videos. */ export default class Overlay { constructor( Reveal ) { this.Reveal = Reveal; this.onSlidesClicked = this.onSlidesClicked.bind( this ); this.linkPreviewSelector = null; this.mediaPreviewSelector = '[data-preview-image], [data-preview-video]'; this.state = {}; } update() { // Enable link previews globally if( this.Reveal.getConfig().previewLinks ) { this.linkPreviewSelector = 'a[href]:not([data-preview-link=false]), [data-preview-link]:not(a):not([data-preview-link=false])'; } // Enable link previews for individual elements else { this.linkPreviewSelector = '[data-preview-link]:not([data-preview-link=false])'; } this.hasLinkPreviews = this.Reveal.getSlidesElement().querySelectorAll( this.linkPreviewSelector ).length > 0; this.hasMediaPreviews = this.Reveal.getSlidesElement().querySelectorAll( this.mediaPreviewSelector ).length > 0; // Only add the listener when there are previewable elements in the slides if( this.hasLinkPreviews || this.hasMediaPreviews ) { this.Reveal.getSlidesElement().addEventListener( 'click', this.onSlidesClicked, false ); } else { this.Reveal.getSlidesElement().removeEventListener( 'click', this.onSlidesClicked, false ); } } createOverlay( className ) { this.dom = document.createElement( 'div' ); this.dom.classList.add( 'r-overlay' ); this.dom.classList.add( className ); this.viewport = document.createElement( 'div' ); this.viewport.classList.add( 'r-overlay-viewport' ); this.dom.appendChild( this.viewport ); this.Reveal.getRevealElement().appendChild( this.dom ); } /** * Opens a preview window for the target URL. * * @param {string} url - url for preview iframe src */ showIframePreview( url ) { this.close(); this.state.previewIframe = url; this.createOverlay( 'r-overlay-preview' ); this.dom.dataset.state = 'loading'; this.viewport.innerHTML = `
Unable to load iframe. This is likely due to the site's policy (x-frame-options).
`; this.dom.querySelector( 'iframe' ).addEventListener( 'load', event => { this.dom.dataset.state = 'loaded'; }, false ); this.dom.querySelector( '.r-overlay-close' ).addEventListener( 'click', event => { this.close(); event.preventDefault(); }, false ); this.dom.querySelector( '.r-overlay-external' ).addEventListener( 'click', event => { this.close(); }, false ); this.Reveal.dispatchEvent({ type: 'showiframepreview', data: { url } }); } /** * Opens a preview window that provides a larger view of the * given image/video. * * @param {string} url - url to the image/video to preview * @param {image|video} mediaType * @param {string} [fitMode] - the fit mode to use for the preview */ showMediaPreview( url, mediaType, fitMode ) { if( mediaType !== 'image' && mediaType !== 'video' ) { console.warn( 'Please specify a valid media type to preview (image|video)' ); return; } this.close(); fitMode = fitMode || 'scale-down'; this.createOverlay( 'r-overlay-preview' ); this.dom.dataset.state = 'loading'; this.dom.dataset.previewFit = fitMode; this.viewport.innerHTML = `
`; const contentElement = this.dom.querySelector( '.r-overlay-content' ); if( mediaType === 'image' ) { this.state = { previewImage: url, previewFit: fitMode } const img = document.createElement( 'img', {} ); img.src = url; contentElement.appendChild( img ); img.addEventListener( 'load', () => { this.dom.dataset.state = 'loaded'; }, false ); img.addEventListener( 'error', () => { this.dom.dataset.state = 'error'; contentElement.innerHTML = `Unable to load image.` }, false ); // Hide image overlays when clicking outside the overlay this.dom.style.cursor = 'zoom-out'; this.dom.addEventListener( 'click', ( event ) => { this.close(); }, false ); } else if( mediaType === 'video' ) { this.state = { previewVideo: url, previewFit: fitMode } const video = document.createElement( 'video' ); video.autoplay = this.dom.dataset.previewAutoplay === 'false' ? false : true; video.controls = this.dom.dataset.previewControls === 'false' ? false : true; video.loop = this.dom.dataset.previewLoop === 'true' ? true : false; video.muted = this.dom.dataset.previewMuted === 'true' ? true : false; video.playsInline = true; video.src = url; contentElement.appendChild( video ); video.addEventListener( 'loadeddata', () => { this.dom.dataset.state = 'loaded'; }, false ); video.addEventListener( 'error', () => { this.dom.dataset.state = 'error'; contentElement.innerHTML = `Unable to load video.`; }, false ); } else { throw new Error( 'Please specify a valid media type to preview' ); } this.dom.querySelector( '.r-overlay-close' ).addEventListener( 'click', ( event ) => { this.close(); event.preventDefault(); }, false ); this.Reveal.dispatchEvent({ type: 'showmediapreview', data: { mediaType, url } }); } /** * Open or close help overlay window. * * @param {Boolean} [override] Flag which overrides the * toggle logic and forcibly sets the desired state. True means * help is open, false means it's closed. */ toggleHelp( override ) { if( typeof override === 'boolean' ) { override ? this.showHelp() : this.close(); } else { if( this.dom ) { this.close(); } else { this.showHelp(); } } } /** * Opens an overlay window with help material. */ showHelp() { if( this.Reveal.getConfig().help ) { this.close(); this.createOverlay( 'r-overlay-help' ); let html = '

Keyboard Shortcuts

'; let shortcuts = this.Reveal.keyboard.getShortcuts(), bindings = this.Reveal.keyboard.getBindings(); html += ''; for( let key in shortcuts ) { html += ``; } // Add custom key bindings that have associated descriptions for( let binding in bindings ) { if( bindings[binding].key && bindings[binding].description ) { html += ``; } } html += '
KEYACTION
${key}${shortcuts[ key ]}
${bindings[binding].key}${bindings[binding].description}
'; this.viewport.innerHTML = `
${html}
`; this.dom.querySelector( '.r-overlay-close' ).addEventListener( 'click', event => { this.close(); event.preventDefault(); }, false ); this.Reveal.dispatchEvent({ type: 'showhelp' }); } } isOpen() { return !!this.dom; } /** * Closes any currently open overlay. */ close() { if( this.dom ) { this.dom.remove(); this.dom = null; this.state = {}; this.Reveal.dispatchEvent({ type: 'closeoverlay' }); return true; } return false; } getState() { return this.state; } setState( state ) { if( state.previewIframe ) { this.showIframePreview( state.previewIframe ); } else if( state.previewImage ) { this.showMediaPreview( state.previewImage, 'image', state.previewFit ); } else if( state.previewVideo ) { this.showMediaPreview( state.previewVideo, 'video', state.previewFit ); } else { this.close(); } } onSlidesClicked( event ) { const target = event.target; const linkTarget = target.closest( this.linkPreviewSelector ); const mediaTarget = target.closest( this.mediaPreviewSelector ); // Was a link preview clicked? if( linkTarget ) { if( event.metaKey || event.shiftKey || event.altKey ) { // Let the browser handle meta keys naturally so users can cmd+click, cmd+shift+click, shift+click, alt+click, etc. return; } let url = linkTarget.getAttribute( 'href' ) || linkTarget.getAttribute( 'data-preview-link' ); if( url ) { this.showIframePreview( url ); event.preventDefault(); } } // Was a media preview clicked? else if( mediaTarget ) { if( mediaTarget.hasAttribute( 'data-preview-image' ) ) { let url = mediaTarget.dataset.previewImage || mediaTarget.getAttribute( 'src' ); if( url ) { this.showMediaPreview( url, 'image', mediaTarget.dataset.previewFit ); event.preventDefault(); } } else if( mediaTarget.hasAttribute( 'data-preview-video' ) ) { let url = mediaTarget.dataset.previewVideo || mediaTarget.getAttribute( 'src' ); if( !url ) { let source = mediaTarget.querySelector( 'source' ); if( source ) { url = source.getAttribute( 'src' ); } } if( url ) { this.showMediaPreview( url, 'video', mediaTarget.dataset.previewFit ); event.preventDefault(); } } } } destroy() { this.close(); } }