Browse Source

add support image/video lightbox via data-preview-image/video, move overlay into standalone controller

Hakim El Hattab 3 months ago
parent
commit
1b2c39a86e
9 changed files with 467 additions and 191 deletions
  1. 5 0
      css/reveal.scss
  2. 0 0
      dist/reveal.css
  3. 0 0
      dist/reveal.esm.js
  4. 0 0
      dist/reveal.esm.js.map
  5. 0 0
      dist/reveal.js
  6. 0 0
      dist/reveal.js.map
  7. 102 0
      examples/preview-overlays.html
  8. 349 0
      js/controllers/overlay.js
  9. 11 191
      js/reveal.js

File diff suppressed because it is too large
+ 5 - 0
css/reveal.scss


File diff suppressed because it is too large
+ 0 - 0
dist/reveal.css


File diff suppressed because it is too large
+ 0 - 0
dist/reveal.esm.js


File diff suppressed because it is too large
+ 0 - 0
dist/reveal.esm.js.map


File diff suppressed because it is too large
+ 0 - 0
dist/reveal.js


File diff suppressed because it is too large
+ 0 - 0
dist/reveal.js.map


+ 102 - 0
examples/preview-overlays.html

@@ -0,0 +1,102 @@
+<!doctype html>
+<html lang="en">
+
+	<head>
+		<meta charset="utf-8">
+
+		<title>reveal.js - Slide Transitions</title>
+
+		<link rel="stylesheet" href="../dist/reveal.css">
+		<link rel="stylesheet" href="../dist/theme/black.css" id="theme">
+
+		<style>
+			.reveal {
+				font-size: 24px;
+			}
+			.reveal figure {
+				margin: 0 0 1rem 0;
+				text-align: left;
+			}
+			.reveal figure img,
+			.reveal figure video {
+				margin: 0.25rem 0 0 0;
+			}
+			figcaption, a {
+				font-size: 16px;
+			}
+		</style>
+	</head>
+
+	<body>
+
+		<div class="reveal">
+
+			<div class="slides">
+
+				<section>
+
+					<h2>Preview Overlays</h2>
+
+					<div class="r-hstack items-start">
+						<div class="r-vstack items-start">
+							<h5>Images</h5>
+							<figure>
+								<figcaption>Preview with default settings:</figcaption>
+								<img height="50" src="https://static.slid.es/images/alphabet/v1/a.png" data-preview-image>
+							</figure>
+							<figure>
+								<figcaption>Preview with data-object-fit="contain"</figcaption>
+								<img height="50" src="https://static.slid.es/images/alphabet/v1/a.png" data-preview-image data-object-fit="contain">
+							</figure>
+							<figure>
+								<figcaption>Preview another image (c)</figcaption>
+								<img height="50" src="https://static.slid.es/images/alphabet/v1/b.png" data-preview-image="https://static.slid.es/images/alphabet/v1/c.png">
+							</figure>
+							<a href="#" data-preview-image="https://static.slid.es/images/alphabet/v1/x.png">
+								Preview image from a link.
+							</a>
+						</div>
+
+						<div style="width: 1px; height: 30vh; margin: 0 3rem;background-color: #999;"></div>
+
+						<div class="r-vstack items-start">
+							<h5>Videos</h5>
+							<figure>
+								<figcaption>Preview video</figcaption>
+								<img height="50" src="https://static.slid.es/images/alphabet/v1/x.png" data-preview-video="https://static.slid.es/site/homepage/v1/homepage-video-editor.mp4">
+							</figure>
+							<figure>
+								<figcaption>Preview video</figcaption>
+								<video height="50" src="https://static.slid.es/site/homepage/v1/homepage-video-editor.mp4" data-preview-video></video>
+							</figure>
+							<a href="#" data-preview-video="https://static.slid.es/site/homepage/v1/homepage-video-editor.mp4">
+								Preview video from a link.
+							</a>
+						</div>
+
+						<div style="width: 1px; height: 30vh; margin: 0 3rem;background-color: #999;"></div>
+
+						<div class="r-vstack items-start">
+							<h5>Iframes</h5>
+							<a data-preview-link href="https://hakim.se">https://hakim.se | data-preview-link</a>
+							<a data-preview-link="false" href="https://hakim.se">https://hakim.se | data-preview-link=false</a>
+						</div>
+					</div>
+
+				</section>
+
+			</div>
+
+		</div>
+
+		<script src="../dist/reveal.js"></script>
+		<script>
+			Reveal.initialize({
+				previewLinks: true,
+				width: 1280,
+				height: 720
+			});
+		</script>
+
+	</body>
+</html>

+ 349 - 0
js/controllers/overlay.js

@@ -0,0 +1,349 @@
+/**
+ * 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 =
+			`<header class="overlay-header">
+				<a class="overlay-button overlay-external" href="${url}" target="_blank"><span class="icon"></span></a>
+				<button class="overlay-button overlay-close"><span class="icon"></span></button>
+			</header>
+			<div class="overlay-spinner"></div>
+			<div class="overlay-viewport">
+				<iframe src="${url}"></iframe>
+				<small class="overlay-viewport-inner">
+					<span class="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.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 =
+			`<header class="overlay-header">
+				<button class="overlay-button overlay-close">Esc <span class="icon"></span></button>
+			</header>
+			<div class="overlay-spinner"></div>
+			<div class="overlay-viewport"></div>`;
+
+		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 =
+						`<span class="overlay-error">Unable to load image.</span>`
+			}, 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 =
+					`<span class="overlay-error">Unable to load video.</span>`;
+			}, 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 = '<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.element.innerHTML = `
+				<header class="overlay-header">
+					<button class="overlay-button overlay-close">Esc <span class="icon"></span></button>
+				</header>
+				<div class="overlay-viewport">
+					<div class="overlay-help-content">${html}</div>
+				</div>
+			`;
+
+			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 = [];
+
+	}
+
+}

+ 11 - 191
js/reveal.js

@@ -13,6 +13,7 @@ import Controls from './controllers/controls.js'
 import Progress from './controllers/progress.js'
 import Pointer from './controllers/pointer.js'
 import Plugins from './controllers/plugins.js'
+import Overlay from './controllers/overlay.js'
 import Touch from './controllers/touch.js'
 import Focus from './controllers/focus.js'
 import Notes from './controllers/notes.js'
@@ -119,6 +120,7 @@ export default function( revealElement, options ) {
 		progress = new Progress( Reveal ),
 		pointer = new Pointer( Reveal ),
 		plugins = new Plugins( Reveal ),
+		overlay = new Overlay( Reveal ),
 		focus = new Focus( Reveal ),
 		touch = new Touch( Reveal ),
 		notes = new Notes( Reveal );
@@ -510,16 +512,6 @@ export default function( revealElement, options ) {
 			resume();
 		}
 
-		// Iframe link previews
-		if( config.previewLinks ) {
-			enablePreviewLinks();
-			disablePreviewLinks( '[data-preview-link=false]' );
-		}
-		else {
-			disablePreviewLinks();
-			enablePreviewLinks( '[data-preview-link]:not([data-preview-link=false])' );
-		}
-
 		// Reset all changes made by auto-animations
 		autoAnimate.reset();
 
@@ -622,11 +614,11 @@ export default function( revealElement, options ) {
 
 		removeEventListeners();
 		cancelAutoSlide();
-		disablePreviewLinks();
 
 		// Destroy controllers
 		notes.destroy();
 		focus.destroy();
+		overlay.destroy();
 		plugins.destroy();
 		pointer.destroy();
 		controls.destroy();
@@ -776,164 +768,6 @@ export default function( revealElement, options ) {
 
 	}
 
-	/**
-	 * Bind preview frame links.
-	 *
-	 * @param {string} [selector=a] - selector for anchors
-	 */
-	function enablePreviewLinks( selector = 'a' ) {
-
-		Array.from( dom.wrapper.querySelectorAll( selector ) ).forEach( element => {
-			if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) {
-				element.addEventListener( 'click', onPreviewLinkClicked, false );
-			}
-		} );
-
-	}
-
-	/**
-	 * Unbind preview frame links.
-	 */
-	function disablePreviewLinks( selector = 'a' ) {
-
-		Array.from( dom.wrapper.querySelectorAll( selector ) ).forEach( element => {
-			if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) {
-				element.removeEventListener( 'click', onPreviewLinkClicked, false );
-			}
-		} );
-
-	}
-
-	/**
-	 * Opens a preview window for the target URL.
-	 *
-	 * @param {string} url - url for preview iframe src
-	 */
-	function showPreview( url ) {
-
-		closeOverlay();
-
-		dom.overlay = document.createElement( 'div' );
-		dom.overlay.classList.add( 'overlay' );
-		dom.overlay.classList.add( 'overlay-preview' );
-		dom.wrapper.appendChild( dom.overlay );
-
-		dom.overlay.innerHTML =
-			`<header class="overlay-header">
-				<a class="overlay-external" href="${url}" target="_blank"><span class="icon"></span></a>
-				<a class="overlay-close" href="#"><span class="icon"></span></a>
-			</header>
-			<div class="overlay-spinner"></div>
-			<div class="overlay-viewport">
-				<iframe src="${url}"></iframe>
-				<small class="overlay-viewport-inner">
-					<span class="x-frame-error">Unable to load iframe. This is likely due to the site's policy (x-frame-options).</span>
-				</small>
-			</div>`;
-
-		dom.overlay.querySelector( 'iframe' ).addEventListener( 'load', event => {
-			dom.overlay.classList.add( 'loaded' );
-		}, false );
-
-		dom.overlay.querySelector( '.overlay-close' ).addEventListener( 'click', event => {
-			closeOverlay();
-			event.preventDefault();
-		}, false );
-
-		dom.overlay.querySelector( '.overlay-external' ).addEventListener( 'click', event => {
-			closeOverlay();
-		}, 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.
-	 */
-	function toggleHelp( override ){
-
-		if( typeof override === 'boolean' ) {
-			override ? showHelp() : closeOverlay();
-		}
-		else {
-			if( dom.overlay ) {
-				closeOverlay();
-			}
-			else {
-				showHelp();
-			}
-		}
-	}
-
-	/**
-	 * Opens an overlay window with help material.
-	 */
-	function showHelp() {
-
-		if( config.help ) {
-
-			closeOverlay();
-
-			dom.overlay = document.createElement( 'div' );
-			dom.overlay.classList.add( 'overlay' );
-			dom.overlay.classList.add( 'overlay-help' );
-			dom.wrapper.appendChild( dom.overlay );
-
-			let html = '<p class="title">Keyboard Shortcuts</p>';
-
-			let shortcuts = keyboard.getShortcuts(),
-				bindings = 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>';
-
-			dom.overlay.innerHTML = `
-				<header class="overlay-header">
-					<a class="overlay-close" href="#"><span class="icon"></span></a>
-				</header>
-				<div class="overlay-viewport">
-					<div class="overlay-viewport-inner">${html}</div>
-				</div>
-			`;
-
-			dom.overlay.querySelector( '.overlay-close' ).addEventListener( 'click', event => {
-				closeOverlay();
-				event.preventDefault();
-			}, false );
-
-		}
-
-	}
-
-	/**
-	 * Closes any currently open overlay.
-	 */
-	function closeOverlay() {
-
-		if( dom.overlay ) {
-			dom.overlay.parentNode.removeChild( dom.overlay );
-			dom.overlay = null;
-			return true;
-		}
-
-		return false;
-
-	}
-
 	/**
 	 * Applies JavaScript-controlled layout rules to the
 	 * presentation.
@@ -1690,6 +1524,7 @@ export default function( revealElement, options ) {
 
 		notes.update();
 		notes.updateVisibility();
+		overlay.update();
 		backgrounds.update( true );
 		slideNumber.update();
 		slideContent.formatEmbeddedContent();
@@ -2805,24 +2640,6 @@ export default function( revealElement, options ) {
 
 	}
 
-	/**
-	 * Handles clicks on links that are set to preview in the
-	 * iframe overlay.
-	 *
-	 * @param {object} event
-	 */
-	function onPreviewLinkClicked( event ) {
-
-		if( event.currentTarget && event.currentTarget.hasAttribute( 'href' ) ) {
-			let url = event.currentTarget.getAttribute( 'href' );
-			if( url ) {
-				showPreview( url );
-				event.preventDefault();
-			}
-		}
-
-	}
-
 	/**
 	 * Handles click on the auto-sliding controls element.
 	 *
@@ -2901,7 +2718,7 @@ export default function( revealElement, options ) {
 		availableFragments: fragments.availableRoutes.bind( fragments ),
 
 		// Toggles a help overlay with keyboard shortcuts
-		toggleHelp,
+		toggleHelp: overlay.toggleHelp.bind( overlay ),
 
 		// Toggles the overview mode on/off
 		toggleOverview: overview.toggle.bind( overview ),
@@ -2947,8 +2764,10 @@ export default function( revealElement, options ) {
 		stopEmbeddedContent: () => slideContent.stopEmbeddedContent( currentSlide, { unloadIframes: false } ),
 
 		// Preview management
-		showPreview,
-		hidePreview: closeOverlay,
+		showIframePreview: overlay.showIframePreview.bind( overlay ),
+		showMediaPreview: overlay.showMediaPreview.bind( overlay ),
+		showPreview: overlay.showIframePreview.bind( overlay ),
+		hidePreview: overlay.close.bind( overlay ),
 
 		// Adds or removes all internal event listeners
 		addEventListeners,
@@ -3062,13 +2881,14 @@ export default function( revealElement, options ) {
 		controls,
 		location,
 		overview,
+		keyboard,
 		fragments,
 		backgrounds,
 		slideContent,
 		slideNumber,
 
 		onUserInput,
-		closeOverlay,
+		closeOverlay: overlay.close.bind( overlay ),
 		updateSlidesVisibility,
 		layoutSlideContents,
 		transformSlides,

Some files were not shown because too many files changed in this diff