123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382 |
- /**
- * 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.iframeTriggerSelector = null;
- this.mediaTriggerSelector = '[data-preview-image], [data-preview-video]';
- this.state = {};
- }
- update() {
- // Enable link previews globally
- if( this.Reveal.getConfig().previewLinks ) {
- this.iframeTriggerSelector = '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.iframeTriggerSelector = '[data-preview-link]:not([data-preview-link=false])';
- }
- const hasLinkPreviews = this.Reveal.getSlidesElement().querySelectorAll( this.iframeTriggerSelector ).length > 0;
- const hasMediaPreviews = this.Reveal.getSlidesElement().querySelectorAll( this.mediaTriggerSelector ).length > 0;
- // Only add the listener when there are previewable elements in the slides
- if( hasLinkPreviews || 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 lightbox that previews the target URL.
- *
- * @param {string} url - url for lightbox iframe src
- */
- previewIframe( url ) {
- this.close();
- this.state.previewIframe = url;
- this.createOverlay( 'r-overlay-preview' );
- this.dom.dataset.state = 'loading';
- this.viewport.innerHTML =
- `<header class="r-overlay-header">
- <a class="r-overlay-button r-overlay-external" href="${url}" target="_blank"><span class="icon"></span></a>
- <button class="r-overlay-button r-overlay-close"><span class="icon"></span></button>
- </header>
- <div class="r-overlay-spinner"></div>
- <div class="r-overlay-content">
- <iframe src="${url}"></iframe>
- <small class="r-overlay-content-inner">
- <span class="r-overlay-error x-frame-error">Unable to load iframe. This is likely due to the site's policy (x-frame-options).</span>
- </small>
- </div>`;
- 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: 'previewiframe', data: { url } });
- }
- /**
- * Opens a lightbox 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
- */
- previewMedia( 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 =
- `<header class="r-overlay-header">
- <button class="r-overlay-button r-overlay-close">Esc <span class="icon"></span></button>
- </header>
- <div class="r-overlay-spinner"></div>
- <div class="r-overlay-content"></div>`;
- 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 =
- `<span class="r-overlay-error">Unable to load image.</span>`
- }, false );
- // Hide image overlays when clicking outside the overlay
- this.dom.style.cursor = 'zoom-out';
- this.dom.addEventListener( 'click', ( event ) => {
- this.close();
- }, false );
- this.Reveal.dispatchEvent({ type: 'previewimage', data: { url } });
- }
- 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 =
- `<span class="r-overlay-error">Unable to load video.</span>`;
- }, false );
- this.Reveal.dispatchEvent({ type: 'previewvideo', data: { url } });
- }
- 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 );
- }
- previewImage( url, fitMode ) {
- this.previewMedia( url, 'image', fitMode );
- }
- previewVideo( url, fitMode ) {
- this.previewMedia( url, 'video', fitMode );
- }
- /**
- * 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 = '<p class="title">Keyboard Shortcuts</p>';
- let shortcuts = this.Reveal.keyboard.getShortcuts(),
- bindings = this.Reveal.keyboard.getBindings();
- html += '<table><th>KEY</th><th>ACTION</th>';
- for( let key in shortcuts ) {
- html += `<tr><td>${key}</td><td>${shortcuts[ key ]}</td></tr>`;
- }
- // Add custom key bindings that have associated descriptions
- for( let binding in bindings ) {
- if( bindings[binding].key && bindings[binding].description ) {
- html += `<tr><td>${bindings[binding].key}</td><td>${bindings[binding].description}</td></tr>`;
- }
- }
- html += '</table>';
- this.viewport.innerHTML = `
- <header class="r-overlay-header">
- <button class="r-overlay-button r-overlay-close">Esc <span class="icon"></span></button>
- </header>
- <div class="r-overlay-content">
- <div class="r-overlay-help-content">${html}</div>
- </div>
- `;
- 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.previewIframe( state.previewIframe );
- }
- else if( state.previewImage ) {
- this.previewImage( state.previewImage, state.previewFit );
- }
- else if( state.previewVideo ) {
- this.previewVideo( state.previewVideo, state.previewFit );
- }
- else {
- this.close();
- }
- }
- onSlidesClicked( event ) {
- const target = event.target;
- const linkTarget = target.closest( this.iframeTriggerSelector );
- const mediaTarget = target.closest( this.mediaTriggerSelector );
- // Was an iframe lightbox trigger 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.previewIframe( url );
- event.preventDefault();
- }
- }
- // Was a media lightbox trigger clicked?
- else if( mediaTarget ) {
- if( mediaTarget.hasAttribute( 'data-preview-image' ) ) {
- let url = mediaTarget.dataset.previewImage || mediaTarget.getAttribute( 'src' );
- if( url ) {
- this.previewImage( url, 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.previewVideo( url, mediaTarget.dataset.previewFit );
- event.preventDefault();
- }
- }
- }
- }
- destroy() {
- this.close();
- }
- }
|