Vendor reveal.js 5.2.1 (from 5.1.0)

Contains the upstream 5.2.x feature set, most notably the lightbox
(data-preview-image/data-preview-video opens media full-size in an
overlay), controls: "speaker-only", search API extensions and
speaker-view fixes. The package's local additions are carried over
unchanged: IDS theme (ids.css/ids.scss), extra theme fonts,
chalkboard/customcontrols/menu plugins, and the 1280x720 default
slide size (patched in js/config.js and the dist bundles).

Also fixes a pre-existing fork bug (unrelated to the upgrade): the
slidechanged Highcharts resize hook in default.html now checks that
Highcharts exists, no longer throwing in decks without highcharter.

Docs: README (Rmd + rendered md) gained a short Lightbox section,
NEWS.md notes the library bump.

Verified: examples/ids.html and examples/simple.html re-render and run
error-free through all 13/5 slides (Reveal.VERSION = 5.2.1); lightbox
opens on click and closes with Esc in a dedicated test deck.

Change-Id: I0751129dfb0c9d20b56241377905876314a26011
diff --git a/inst/reveal.js-5.2.1/js/controllers/overlay.js b/inst/reveal.js-5.2.1/js/controllers/overlay.js
new file mode 100644
index 0000000..e56321c
--- /dev/null
+++ b/inst/reveal.js-5.2.1/js/controllers/overlay.js
@@ -0,0 +1,389 @@
+/**
+ * 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.stateProps = ['previewIframe', 'previewImage', 'previewVideo', 'previewFit'];
+		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 ) {
+
+		// Ignore the incoming state if none of the preview related
+		// props have changed
+		if( this.stateProps.every( key => this.state[ key ] === state[ key ] ) ) {
+			return;
+		}
+
+		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
+				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();
+
+	}
+
+}
\ No newline at end of file