1
0
mirror of https://github.com/hakimel/reveal.js.git synced 2025-07-31 20:00:28 +02:00

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

This commit is contained in:
Hakim El Hattab
2025-01-31 16:10:15 +01:00
parent 0524ae855d
commit 1b2c39a86e
9 changed files with 538 additions and 226 deletions

View File

@@ -1432,6 +1432,11 @@ $controlsArrowAngleActive: 36deg;
$overlayHeaderHeight: 40px;
$overlayHeaderPadding: 5px;
.reveal [data-preview-image],
.reveal [data-preview-video] {
cursor: zoom-in;
}
.reveal > .overlay {
position: absolute;
top: 0;
@@ -1455,9 +1460,8 @@ $overlayHeaderPadding: 5px;
z-index: 10;
background-image: url(data:image/gif;base64,R0lGODlhIAAgAPMAAJmZmf%2F%2F%2F6%2Bvr8nJybW1tcDAwOjo6Nvb26ioqKOjo7Ozs%2FLy8vz8%2FAAAAAAAAAAAACH%2FC05FVFNDQVBFMi4wAwEAAAAh%2FhpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh%2BQQJCgAAACwAAAAAIAAgAAAE5xDISWlhperN52JLhSSdRgwVo1ICQZRUsiwHpTJT4iowNS8vyW2icCF6k8HMMBkCEDskxTBDAZwuAkkqIfxIQyhBQBFvAQSDITM5VDW6XNE4KagNh6Bgwe60smQUB3d4Rz1ZBApnFASDd0hihh12BkE9kjAJVlycXIg7CQIFA6SlnJ87paqbSKiKoqusnbMdmDC2tXQlkUhziYtyWTxIfy6BE8WJt5YJvpJivxNaGmLHT0VnOgSYf0dZXS7APdpB309RnHOG5gDqXGLDaC457D1zZ%2FV%2FnmOM82XiHRLYKhKP1oZmADdEAAAh%2BQQJCgAAACwAAAAAIAAgAAAE6hDISWlZpOrNp1lGNRSdRpDUolIGw5RUYhhHukqFu8DsrEyqnWThGvAmhVlteBvojpTDDBUEIFwMFBRAmBkSgOrBFZogCASwBDEY%2FCZSg7GSE0gSCjQBMVG023xWBhklAnoEdhQEfyNqMIcKjhRsjEdnezB%2BA4k8gTwJhFuiW4dokXiloUepBAp5qaKpp6%2BHo7aWW54wl7obvEe0kRuoplCGepwSx2jJvqHEmGt6whJpGpfJCHmOoNHKaHx61WiSR92E4lbFoq%2BB6QDtuetcaBPnW6%2BO7wDHpIiK9SaVK5GgV543tzjgGcghAgAh%2BQQJCgAAACwAAAAAIAAgAAAE7hDISSkxpOrN5zFHNWRdhSiVoVLHspRUMoyUakyEe8PTPCATW9A14E0UvuAKMNAZKYUZCiBMuBakSQKG8G2FzUWox2AUtAQFcBKlVQoLgQReZhQlCIJesQXI5B0CBnUMOxMCenoCfTCEWBsJColTMANldx15BGs8B5wlCZ9Po6OJkwmRpnqkqnuSrayqfKmqpLajoiW5HJq7FL1Gr2mMMcKUMIiJgIemy7xZtJsTmsM4xHiKv5KMCXqfyUCJEonXPN2rAOIAmsfB3uPoAK%2B%2BG%2Bw48edZPK%2BM6hLJpQg484enXIdQFSS1u6UhksENEQAAIfkECQoAAAAsAAAAACAAIAAABOcQyEmpGKLqzWcZRVUQnZYg1aBSh2GUVEIQ2aQOE%2BG%2BcD4ntpWkZQj1JIiZIogDFFyHI0UxQwFugMSOFIPJftfVAEoZLBbcLEFhlQiqGp1Vd140AUklUN3eCA51C1EWMzMCezCBBmkxVIVHBWd3HHl9JQOIJSdSnJ0TDKChCwUJjoWMPaGqDKannasMo6WnM562R5YluZRwur0wpgqZE7NKUm%2BFNRPIhjBJxKZteWuIBMN4zRMIVIhffcgojwCF117i4nlLnY5ztRLsnOk%2BaV%2BoJY7V7m76PdkS4trKcdg0Zc0tTcKkRAAAIfkECQoAAAAsAAAAACAAIAAABO4QyEkpKqjqzScpRaVkXZWQEximw1BSCUEIlDohrft6cpKCk5xid5MNJTaAIkekKGQkWyKHkvhKsR7ARmitkAYDYRIbUQRQjWBwJRzChi9CRlBcY1UN4g0%2FVNB0AlcvcAYHRyZPdEQFYV8ccwR5HWxEJ02YmRMLnJ1xCYp0Y5idpQuhopmmC2KgojKasUQDk5BNAwwMOh2RtRq5uQuPZKGIJQIGwAwGf6I0JXMpC8C7kXWDBINFMxS4DKMAWVWAGYsAdNqW5uaRxkSKJOZKaU3tPOBZ4DuK2LATgJhkPJMgTwKCdFjyPHEnKxFCDhEAACH5BAkKAAAALAAAAAAgACAAAATzEMhJaVKp6s2nIkolIJ2WkBShpkVRWqqQrhLSEu9MZJKK9y1ZrqYK9WiClmvoUaF8gIQSNeF1Er4MNFn4SRSDARWroAIETg1iVwuHjYB1kYc1mwruwXKC9gmsJXliGxc%2BXiUCby9ydh1sOSdMkpMTBpaXBzsfhoc5l58Gm5yToAaZhaOUqjkDgCWNHAULCwOLaTmzswadEqggQwgHuQsHIoZCHQMMQgQGubVEcxOPFAcMDAYUA85eWARmfSRQCdcMe0zeP1AAygwLlJtPNAAL19DARdPzBOWSm1brJBi45soRAWQAAkrQIykShQ9wVhHCwCQCACH5BAkKAAAALAAAAAAgACAAAATrEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq%2BE71SRQeyqUToLA7VxF0JDyIQh%2FMVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiRMDjI0Fd30%2FiI2UA5GSS5UDj2l6NoqgOgN4gksEBgYFf0FDqKgHnyZ9OX8HrgYHdHpcHQULXAS2qKpENRg7eAMLC7kTBaixUYFkKAzWAAnLC7FLVxLWDBLKCwaKTULgEwbLA4hJtOkSBNqITT3xEgfLpBtzE%2FjiuL04RGEBgwWhShRgQExHBAAh%2BQQJCgAAACwAAAAAIAAgAAAE7xDISWlSqerNpyJKhWRdlSAVoVLCWk6JKlAqAavhO9UkUHsqlE6CwO1cRdCQ8iEIfzFVTzLdRAmZX3I2SfZiCqGk5dTESJeaOAlClzsJsqwiJwiqnFrb2nS9kmIcgEsjQydLiIlHehhpejaIjzh9eomSjZR%2BipslWIRLAgMDOR2DOqKogTB9pCUJBagDBXR6XB0EBkIIsaRsGGMMAxoDBgYHTKJiUYEGDAzHC9EACcUGkIgFzgwZ0QsSBcXHiQvOwgDdEwfFs0sDzt4S6BK4xYjkDOzn0unFeBzOBijIm1Dgmg5YFQwsCMjp1oJ8LyIAACH5BAkKAAAALAAAAAAgACAAAATwEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq%2BE71SRQeyqUToLA7VxF0JDyIQh%2FMVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiUd6GGl6NoiPOH16iZKNlH6KmyWFOggHhEEvAwwMA0N9GBsEC6amhnVcEwavDAazGwIDaH1ipaYLBUTCGgQDA8NdHz0FpqgTBwsLqAbWAAnIA4FWKdMLGdYGEgraigbT0OITBcg5QwPT4xLrROZL6AuQAPUS7bxLpoWidY0JtxLHKhwwMJBTHgPKdEQAACH5BAkKAAAALAAAAAAgACAAAATrEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq%2BE71SRQeyqUToLA7VxF0JDyIQh%2FMVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiUd6GAULDJCRiXo1CpGXDJOUjY%2BYip9DhToJA4RBLwMLCwVDfRgbBAaqqoZ1XBMHswsHtxtFaH1iqaoGNgAIxRpbFAgfPQSqpbgGBqUD1wBXeCYp1AYZ19JJOYgH1KwA4UBvQwXUBxPqVD9L3sbp2BNk2xvvFPJd%2BMFCN6HAAIKgNggY0KtEBAAh%2BQQJCgAAACwAAAAAIAAgAAAE6BDISWlSqerNpyJKhWRdlSAVoVLCWk6JKlAqAavhO9UkUHsqlE6CwO1cRdCQ8iEIfzFVTzLdRAmZX3I2SfYIDMaAFdTESJeaEDAIMxYFqrOUaNW4E4ObYcCXaiBVEgULe0NJaxxtYksjh2NLkZISgDgJhHthkpU4mW6blRiYmZOlh4JWkDqILwUGBnE6TYEbCgevr0N1gH4At7gHiRpFaLNrrq8HNgAJA70AWxQIH1%2BvsYMDAzZQPC9VCNkDWUhGkuE5PxJNwiUK4UfLzOlD4WvzAHaoG9nxPi5d%2BjYUqfAhhykOFwJWiAAAIfkECQoAAAAsAAAAACAAIAAABPAQyElpUqnqzaciSoVkXVUMFaFSwlpOCcMYlErAavhOMnNLNo8KsZsMZItJEIDIFSkLGQoQTNhIsFehRww2CQLKF0tYGKYSg%2BygsZIuNqJksKgbfgIGepNo2cIUB3V1B3IvNiBYNQaDSTtfhhx0CwVPI0UJe0%2Bbm4g5VgcGoqOcnjmjqDSdnhgEoamcsZuXO1aWQy8KAwOAuTYYGwi7w5h%2BKr0SJ8MFihpNbx%2B4Erq7BYBuzsdiH1jCAzoSfl0rVirNbRXlBBlLX%2BBP0XJLAPGzTkAuAOqb0WT5AH7OcdCm5B8TgRwSRKIHQtaLCwg1RAAAOwAAAAAAAAAAAA%3D%3D);
visibility: visible;
opacity: 0.6;
transition: all 0.3s ease;
visibility: hidden;
opacity: 0;
}
.reveal .overlay-header {
@@ -1471,20 +1475,27 @@ $overlayHeaderPadding: 5px;
box-sizing: border-box;
align-items: center;
justify-content: flex-end;
gap: 6px;
}
.reveal .overlay-header a {
.reveal .overlay-header .overlay-button {
all: unset;
display: flex;
align-items: center;
justify-content: center;
width: $overlayHeaderHeight;
height: $overlayHeaderHeight;
min-width: $overlayHeaderHeight;
min-height: $overlayHeaderHeight;
padding: 0 10px;
opacity: 0.6;
opacity: 1;
border-radius: 6px;
font-size: 18px;
gap: 8px;
cursor: pointer;
box-sizing: border-box;
}
.reveal .overlay-header a:hover {
.reveal .overlay-header .overlay-button:hover {
opacity: 1;
background-color: rgba( 255, 255, 255, 0.15 );
}
.reveal .overlay-header .icon {
display: inline-block;
@@ -1504,14 +1515,16 @@ $overlayHeaderPadding: 5px;
.reveal .overlay-viewport {
position: absolute;
display: flex;
display: grid;
place-items: center;
top: $overlayHeaderHeight + $overlayHeaderPadding*2;
right: 10px;
bottom: 10px;
left: 10px;
border-radius: 10px;
right: 6px;
bottom: 6px;
left: 6px;
border-radius: 6px;
overflow: hidden;
background-color: rgba(20, 20, 20, 0.8);
color: #fff;
}
// Preview overlay
@@ -1527,12 +1540,38 @@ $overlayHeaderPadding: 5px;
transition: all 0.3s ease;
}
.reveal .overlay-preview.loaded iframe {
.reveal .overlay-preview[data-state="loaded"] iframe {
opacity: 1;
visibility: visible;
}
.reveal .overlay-preview.loaded .overlay-viewport-inner {
.reveal .overlay-preview img,
.reveal .overlay-preview video {
position: absolute;
max-width: 100%;
max-height: 100%;
width: 100%;
height: 100%;
margin: 0;
object-fit: none;
}
.reveal .overlay-preview[data-object-fit="none"] img,
.reveal .overlay-preview[data-object-fit="none"] video {
object-fit: none;
}
.reveal .overlay-preview[data-object-fit="contain"] img,
.reveal .overlay-preview[data-object-fit="contain"] video {
object-fit: contain;
}
.reveal .overlay-preview[data-object-fit="cover"] img,
.reveal .overlay-preview[data-object-fit="cover"] video {
object-fit: cover;
}
.reveal .overlay-preview[data-state="loaded"] .overlay-viewport-inner {
position: absolute;
z-index: -1;
left: 0;
@@ -1541,55 +1580,57 @@ $overlayHeaderPadding: 5px;
text-align: center;
letter-spacing: normal;
}
.reveal .overlay-preview .overlay-error {
font-size: 18px;
color: orange;
}
.reveal .overlay-preview .x-frame-error {
opacity: 0;
transition: opacity 0.3s ease 0.3s;
}
.reveal .overlay-preview.loaded .x-frame-error {
.reveal .overlay-preview[data-state="loaded"] .x-frame-error {
opacity: 1;
}
.reveal .overlay-preview.loaded .overlay-spinner {
opacity: 0;
visibility: hidden;
transform: scale(0.2);
.reveal .overlay-preview[data-state="loading"] .overlay-spinner {
opacity: 0.6;
visibility: visible;
}
// Help overlay
.reveal .overlay-help .overlay-viewport {
overflow: auto;
color: #fff;
display: grid;
place-items: center;
}
.reveal .overlay-help .overlay-viewport-inner {
width: 600px;
.reveal .overlay-help-content {
width: 100%;
max-width: 600px;
margin: auto;
text-align: center;
letter-spacing: normal;
}
.reveal .overlay-help .overlay-viewport-inner .title {
.reveal .overlay-help-content .title {
font-size: 20px;
margin-top: 0;
}
.reveal .overlay-help .overlay-viewport-inner table {
.reveal .overlay-help-content table {
border: 1px solid #fff;
border-collapse: collapse;
font-size: 16px;
}
.reveal .overlay-help .overlay-viewport-inner table th,
.reveal .overlay-help .overlay-viewport-inner table td {
.reveal .overlay-help-content table th,
.reveal .overlay-help-content table td {
width: 200px;
padding: 14px;
border: 1px solid #fff;
vertical-align: middle;
}
.reveal .overlay-help .overlay-viewport-inner table th {
.reveal .overlay-help-content table th {
padding-top: 20px;
padding-bottom: 20px;
}

2
dist/reveal.css vendored

File diff suppressed because one or more lines are too long

2
dist/reveal.esm.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
dist/reveal.js vendored

File diff suppressed because one or more lines are too long

2
dist/reveal.js.map vendored

File diff suppressed because one or more lines are too long

View File

@@ -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
js/controllers/overlay.js Normal file
View File

@@ -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 = [];
}
}

View File

@@ -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,