/** * Handles the display of reveal.js' overlay elements used * to preview iframes, images & videos. */ export default class Overlay { constructor( Reveal ) { this.Reveal = Reveal; this.onPreviewLinkClicked = this.onPreviewLinkClicked.bind( this ); this.onPreviewMediaClicked = this.onPreviewMediaClicked.bind( this ); this.linkPreviews = []; this.mediaPreviews = []; } update() { this.removePreviewListeneres(); if( this.Reveal.getConfig().previewLinks ) { // Enable link previews globally this.enableLinkPreviews( 'a[href]:not([data-preview-link=false])' ); } else { // Enable link previews for individual elements this.enableLinkPreviews( '[data-preview-link]:not([data-preview-link=false])' ); } this.enableMediaPreviews( '[data-preview-image], [data-preview-video]' ); } /** * Bind preview frame links. * * @param {string} [selector=a] - selector for anchors */ enableLinkPreviews( selector = 'a' ) { Array.from( this.Reveal.getSlidesElement().querySelectorAll( selector ) ).forEach( element => { if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) { element.addEventListener( 'click', this.onPreviewLinkClicked, false ); this.linkPreviews.push( element ); } } ); } /** * Bind image/video preview links. * * @param {string} selector - css selector for images/videos */ enableMediaPreviews( selector ) { Array.from( this.Reveal.getSlidesElement().querySelectorAll( selector ) ).forEach( element => { element.addEventListener( 'click', this.onPreviewMediaClicked, false ); this.mediaPreviews.push( element ); } ); } removePreviewListeneres() { this.linkPreviews.forEach( element => element.removeEventListener( 'click', this.onPreviewLinkClicked, false ) ); this.mediaPreviews.forEach( element => element.removeEventListener( 'click', this.onPreviewMediaClicked, false ) ); } /** * Opens a preview window for the target URL. * * @param {string} url - url for preview iframe src */ showIframePreview( url ) { this.close(); this.element = document.createElement( 'div' ); this.element.classList.add( 'overlay' ); this.element.classList.add( 'overlay-preview' ); this.element.dataset.state = 'loading'; this.Reveal.getRevealElement().appendChild( this.element ); this.element.innerHTML = `
Unable to load iframe. This is likely due to the site's policy (x-frame-options).
`; this.element.querySelector( 'iframe' ).addEventListener( 'load', event => { this.element.dataset.state = 'loaded'; }, false ); this.element.querySelector( '.overlay-close' ).addEventListener( 'click', event => { this.close(); event.preventDefault(); }, false ); this.element.querySelector( '.overlay-external' ).addEventListener( 'click', event => { this.close(); }, false ); } /** * 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 {HTMLElement} trigger - the element that triggered * the preview */ showMediaPreview( url, mediaType, trigger ) { this.close(); this.element = document.createElement( 'div' ); this.element.classList.add( 'overlay' ); this.element.classList.add( 'overlay-preview' ); this.element.dataset.state = 'loading'; this.Reveal.getRevealElement().appendChild( this.element ); this.element.dataset.objectFit = trigger.dataset.objectFit || 'none'; this.element.innerHTML = `
`; const viewport = this.element.querySelector( '.overlay-viewport' ); if( mediaType === 'image' ) { const img = document.createElement( 'img', {} ); img.src = url; viewport.appendChild( img ); img.addEventListener( 'load', () => { this.element.dataset.state = 'loaded'; }, false ); img.addEventListener( 'error', () => { this.element.dataset.state = 'error'; viewport.innerHTML = `Unable to load image.` }, false ); // Hide image overlays when clicking outside the overlay this.element.style.cursor = 'zoom-out'; this.element.addEventListener( 'click', ( event ) => { this.close(); }, false ); } else if( mediaType === 'video' ) { const video = document.createElement( 'video' ); video.autoplay = true; video.controls = true; video.src = url; viewport.appendChild( video ); video.addEventListener( 'loadeddata', () => { this.element.dataset.state = 'loaded'; }, false ); video.addEventListener( 'error', () => { this.element.dataset.state = 'error'; viewport.innerHTML = `Unable to load video.`; }, false ); } else { throw new Error( 'Please specify a valid media type to preview' ); } this.element.querySelector( '.overlay-close' ).addEventListener( 'click', ( event ) => { this.close(); event.preventDefault(); }, false ); } /** * 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.element ) { this.close(); } else { this.showHelp(); } } } /** * Opens an overlay window with help material. */ showHelp() { if( this.Reveal.getConfig().help ) { this.close(); this.element = document.createElement( 'div' ); this.element.classList.add( 'overlay' ); this.element.classList.add( 'overlay-help' ); this.Reveal.getRevealElement().appendChild( this.element ); 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.element.innerHTML = `
${html}
`; this.element.querySelector( '.overlay-close' ).addEventListener( 'click', event => { this.close(); event.preventDefault(); }, false ); } } /** * Closes any currently open overlay. */ close() { if( this.element ) { this.element.remove(); this.element = null; return true; } return false; } /** * Handles clicks on links that are set to preview in the * iframe overlay. * * @param {object} event */ onPreviewLinkClicked( event ) { if( event.currentTarget && event.currentTarget.hasAttribute( 'href' ) ) { let url = event.currentTarget.getAttribute( 'href' ); if( url ) { this.showIframePreview( url ); event.preventDefault(); } } } /** * Handles clicks on images/videos that are set to preview * in the iframe overlay. * * @param {object} event */ onPreviewMediaClicked( event ) { const trigger = event.currentTarget; if( trigger ) { if( trigger.hasAttribute( 'data-preview-image' ) ) { let url = trigger.dataset.previewImage || event.currentTarget.getAttribute( 'src' ); if( url ) { this.showMediaPreview( url, 'image', trigger ); event.preventDefault(); } } else if( trigger.hasAttribute( 'data-preview-video' ) ) { let url = trigger.dataset.previewVideo || event.currentTarget.getAttribute( 'src' ); if( !url ) { let source = event.currentTarget.querySelector( 'source' ); if( source ) { url = source.getAttribute( 'src' ); } } if( url ) { this.showMediaPreview( url, 'video', trigger ); event.preventDefault(); } } } } destroy() { this.close(); this.linkPreviews = []; this.mediaPreviews = []; } }