diff options
Diffstat (limited to 'js/reveal.js')
-rw-r--r-- | js/reveal.js | 546 |
1 files changed, 424 insertions, 122 deletions
diff --git a/js/reveal.js b/js/reveal.js index 841cbb6..95d1b8c 100644 --- a/js/reveal.js +++ b/js/reveal.js @@ -1,6 +1,6 @@ /*! * reveal.js - * http://lab.hakim.se/reveal-js + * http://revealjs.com * MIT licensed * * Copyright (C) 2017 Hakim El Hattab, http://hakim.se @@ -26,7 +26,7 @@ var Reveal; // The reveal.js version - var VERSION = '3.4.1'; + var VERSION = '3.6.0'; var SLIDES_SELECTOR = '.slides section', HORIZONTAL_SLIDES_SELECTOR = '.slides>section', @@ -49,17 +49,28 @@ minScale: 0.2, maxScale: 2.0, - // Display controls in the bottom right corner + // Display presentation control arrows controls: true, + // Help the user learn the controls by providing hints, for example by + // bouncing the down arrow when they first encounter a vertical slide + controlsTutorial: true, + + // Determines where controls appear, "edges" or "bottom-right" + controlsLayout: 'bottom-right', + + // Visibility rule for backwards navigation arrows; "faded", "hidden" + // or "visible" + controlsBackArrows: 'faded', + // Display a presentation progress bar progress: true, // Display the page number of the current slide slideNumber: false, - // Determine which displays to show the slide number on - showSlideNumber: 'all', + // Determine which displays to show the slide number on + showSlideNumber: 'all', // Push each slide change to the browser history history: false, @@ -91,6 +102,10 @@ // Turns fragments on and off globally fragments: true, + // Flags whether to include the current fragment in the URL, + // so that reloading brings you to the same fragment position + fragmentInURL: false, + // Flags if the presentation is running in an embedded mode, // i.e. contained within a limited portion of the screen embedded: false, @@ -105,9 +120,17 @@ // Flags if speaker notes should be visible to all viewers showNotes: false, - // Number of milliseconds between automatically proceeding to the - // next slide, disabled when set to 0, this value can be overwritten - // by using a data-autoslide attribute on your slides + // Global override for autolaying embedded media (video/audio/iframe) + // - null: Media will only autoplay if data-autoplay is present + // - true: All media will autoplay, regardless of individual setting + // - false: No media will autoplay, regardless of individual setting + autoPlayMedia: null, + + // Controls automatic progression to the next slide + // - 0: Auto-sliding only happens if the data-autoslide HTML attribute + // is present on the current slide or fragment + // - 1+: All slides will progress automatically at the given interval + // - false: No auto-sliding, even if data-autoslide is present autoSlide: 0, // Stop auto-sliding after user input @@ -116,6 +139,11 @@ // Use this method for navigation when auto-sliding (defaults to navigateNext) autoSlideMethod: null, + // Specify the average time in seconds that you think you will spend + // presenting each slide. This is used to show a pacing timer in the + // speaker view + defaultTiming: null, + // Enable slide navigation via mouse wheel mouseWheel: false, @@ -160,6 +188,13 @@ // to PDF, unlimited by default pdfMaxPagesPerSlide: Number.POSITIVE_INFINITY, + // Offset used to reduce the height of content within exported PDF pages. + // This exists to account for environment differences based on how you + // print to PDF. CLI printing options, like phantomjs and wkpdf, can end + // on precisely the total height of the document whereas in-browser + // printing has to end one pixel before. + pdfPageHeightOffset: -1, + // Number of slides away from the current that are visible viewDistance: 3, @@ -194,6 +229,10 @@ previousBackground, + // Remember which directions that the user has navigated towards + hasNavigatedRight = false, + hasNavigatedDown = false, + // Slides may hold a data-state attribute which we pick up and apply // as a class to the body. This list contains the combined state of // all current slides. @@ -259,7 +298,10 @@ 'B , .': 'Pause', 'F': 'Fullscreen', 'ESC, O': 'Slide overview' - }; + }, + + // Holds custom key code mappings + registeredKeyBindings = {}; /** * Starts up the presentation if the client is capable. @@ -361,13 +403,13 @@ } - /** - * Loads the dependencies of reveal.js. Dependencies are - * defined via the configuration option 'dependencies' - * and will be loaded prior to starting/binding reveal.js. - * Some dependencies may have an 'async' flag, if so they - * will load after reveal.js has been started up. - */ + /** + * Loads the dependencies of reveal.js. Dependencies are + * defined via the configuration option 'dependencies' + * and will be loaded prior to starting/binding reveal.js. + * Some dependencies may have an 'async' flag, if so they + * will load after reveal.js has been started up. + */ function load() { var scripts = [], @@ -385,7 +427,7 @@ } function loadScript( s ) { - head.ready( s.src.match( /([\w\d_\-]*)\.?js$|[^\\\/]*$/i )[0], function() { + head.ready( s.src.match( /([\w\d_\-]*)\.?js(\?[\w\d.=&]*)?$|[^\\\/]*$/i )[0], function() { // Extension may contain callback functions if( typeof s.callback === 'function' ) { s.callback.apply( this ); @@ -431,6 +473,8 @@ */ function start() { + loaded = true; + // Make sure we've got all the DOM elements we need setupDOM(); @@ -458,8 +502,6 @@ // Enable transitions now that we're loaded dom.slides.classList.remove( 'no-transition' ); - loaded = true; - dom.wrapper.classList.add( 'ready' ); dispatchEvent( 'ready', { @@ -495,6 +537,20 @@ // Prevent transitions while we're loading dom.slides.classList.add( 'no-transition' ); + if( isMobileDevice ) { + dom.wrapper.classList.add( 'no-hover' ); + } + else { + dom.wrapper.classList.remove( 'no-hover' ); + } + + if( /iphone/gi.test( UA ) ) { + dom.wrapper.classList.add( 'ua-iphone' ); + } + else { + dom.wrapper.classList.remove( 'ua-iphone' ); + } + // Background element dom.background = createSingletonNode( dom.wrapper, 'div', 'backgrounds', null ); @@ -503,11 +559,11 @@ dom.progressbar = dom.progress.querySelector( 'span' ); // Arrow controls - createSingletonNode( dom.wrapper, 'aside', 'controls', - '<button class="navigate-left" aria-label="previous slide"></button>' + - '<button class="navigate-right" aria-label="next slide"></button>' + - '<button class="navigate-up" aria-label="above slide"></button>' + - '<button class="navigate-down" aria-label="below slide"></button>' ); + dom.controls = createSingletonNode( dom.wrapper, 'aside', 'controls', + '<button class="navigate-left" aria-label="previous slide"><div class="controls-arrow"></div></button>' + + '<button class="navigate-right" aria-label="next slide"><div class="controls-arrow"></div></button>' + + '<button class="navigate-up" aria-label="above slide"><div class="controls-arrow"></div></button>' + + '<button class="navigate-down" aria-label="below slide"><div class="controls-arrow"></div></button>' ); // Slide number dom.slideNumber = createSingletonNode( dom.wrapper, 'div', 'slide-number', '' ); @@ -520,9 +576,6 @@ // Overlay graphic which is displayed during the paused mode createSingletonNode( dom.wrapper, 'div', 'pause-overlay', null ); - // Cache references to elements - dom.controls = document.querySelector( '.reveal .controls' ); - dom.wrapper.setAttribute( 'role', 'application' ); // There can be multiple instances of controls throughout the page @@ -533,6 +586,10 @@ dom.controlsPrev = toArray( document.querySelectorAll( '.navigate-prev' ) ); dom.controlsNext = toArray( document.querySelectorAll( '.navigate-next' ) ); + // The right and down arrows in the standard reveal.js controls + dom.controlsRightArrow = dom.controls.querySelector( '.navigate-right' ); + dom.controlsDownArrow = dom.controls.querySelector( '.navigate-down' ); + dom.statusDiv = createStatusDiv(); } @@ -611,7 +668,7 @@ slideHeight = slideSize.height; // Let the browser know what page size we want to print - injectStyleSheet( '@page{size:'+ pageWidth +'px '+ pageHeight +'px; margin: 0 0 -1px 0;}' ); + injectStyleSheet( '@page{size:'+ pageWidth +'px '+ pageHeight +'px; margin: 0px;}' ); // Limit the size of certain elements to the dimensions of the slide injectStyleSheet( '.reveal section>img, .reveal section>video, .reveal section>iframe{max-width: '+ slideWidth +'px; max-height:'+ slideHeight +'px}' ); @@ -620,6 +677,9 @@ document.body.style.width = pageWidth + 'px'; document.body.style.height = pageHeight + 'px'; + // Make sure stretch elements fit on slide + layoutSlideContents( slideWidth, slideHeight ); + // Add each slide's index as attributes on itself, we need these // indices to generate slide numbers below toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( hslide, h ) { @@ -658,7 +718,7 @@ // so that no page ever flows onto another var page = document.createElement( 'div' ); page.className = 'pdf-page'; - page.style.height = ( pageHeight * numberOfPages ) + 'px'; + page.style.height = ( ( pageHeight + config.pdfPageHeightOffset ) * numberOfPages ) + 'px'; slide.parentNode.insertBefore( page, slide ); page.appendChild( slide ); @@ -773,7 +833,7 @@ // If no node was found, create it now var node = document.createElement( tagname ); - node.classList.add( classname ); + node.className = classname; if( typeof innerHTML === 'string' ) { node.innerHTML = innerHTML; } @@ -865,7 +925,7 @@ if( data.background ) { // Auto-wrap image urls in url(...) - if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp)([?#]|$)/gi.test( data.background ) ) { + if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp)([?#\s]|$)/gi.test( data.background ) ) { slide.setAttribute( 'data-background-image', data.background ); } else { @@ -966,14 +1026,22 @@ */ function configure( options ) { - var numberOfSlides = dom.wrapper.querySelectorAll( SLIDES_SELECTOR ).length; - - dom.wrapper.classList.remove( config.transition ); + var oldTransition = config.transition; // New config options may be passed when this method // is invoked through the API after initialization if( typeof options === 'object' ) extend( config, options ); + // Abort if reveal.js hasn't finished loading, config + // changes will be applied automatically once loading + // finishes + if( loaded === false ) return; + + var numberOfSlides = dom.wrapper.querySelectorAll( SLIDES_SELECTOR ).length; + + // Remove the previously configured transition class + dom.wrapper.classList.remove( oldTransition ); + // Force linear transition based on browser capabilities if( features.transforms3d === false ) config.transition = 'linear'; @@ -985,6 +1053,9 @@ dom.controls.style.display = config.controls ? 'block' : 'none'; dom.progress.style.display = config.progress ? 'block' : 'none'; + dom.controls.setAttribute( 'data-controls-layout', config.controlsLayout ); + dom.controls.setAttribute( 'data-controls-back-arrows', config.controlsBackArrows ); + if( config.shuffle ) { shuffle(); } @@ -1009,12 +1080,8 @@ } if( config.showNotes ) { - dom.speakerNotes.classList.add( 'visible' ); dom.speakerNotes.setAttribute( 'data-layout', typeof config.showNotes === 'string' ? config.showNotes : 'inline' ); } - else { - dom.speakerNotes.classList.remove( 'visible' ); - } if( config.mouseWheel ) { document.addEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF @@ -1207,6 +1274,38 @@ } /** + * Add a custom key binding with optional description to + * be added to the help screen. + */ + function addKeyBinding( binding, callback ) { + + if( typeof binding === 'object' && binding.keyCode ) { + registeredKeyBindings[binding.keyCode] = { + callback: callback, + key: binding.key, + description: binding.description + }; + } + else { + registeredKeyBindings[binding] = { + callback: callback, + key: null, + description: null + }; + } + + } + + /** + * Removes the specified custom key binding. + */ + function removeKeyBinding( keyCode ) { + + delete registeredKeyBindings[keyCode]; + + } + + /** * Extend object a with the properties of object b. * If there's a conflict, object b takes precedence. * @@ -1219,6 +1318,8 @@ a[ i ] = b[ i ]; } + return a; + } /** @@ -1245,7 +1346,7 @@ if( value === 'null' ) return null; else if( value === 'true' ) return true; else if( value === 'false' ) return false; - else if( value.match( /^[\d\.]+$/ ) ) return parseFloat( value ); + else if( value.match( /^-?[\d\.]+$/ ) ) return parseFloat( value ); } return value; @@ -1446,7 +1547,7 @@ * target element. * * remaining height = [ configured parent height ] - [ current parent height ] - * + * * @param {HTMLElement} element * @param {number} [height] */ @@ -1691,6 +1792,13 @@ html += '<tr><td>' + key + '</td><td>' + keyboardShortcuts[ key ] + '</td></tr>'; } + // Add custom key bindings that have associated descriptions + for( var binding in registeredKeyBindings ) { + if( registeredKeyBindings[binding].key && registeredKeyBindings[binding].description ) { + html += '<tr><td>' + registeredKeyBindings[binding].key + '</td><td>' + registeredKeyBindings[binding].description + '</td></tr>'; + } + } + html += '</table>'; dom.overlay.innerHTML = [ @@ -2354,16 +2462,7 @@ // Dispatch an event if the slide changed var slideChanged = ( indexh !== indexhBefore || indexv !== indexvBefore ); - if( slideChanged ) { - dispatchEvent( 'slidechanged', { - 'indexh': indexh, - 'indexv': indexv, - 'previousSlide': previousSlide, - 'currentSlide': currentSlide, - 'origin': o - } ); - } - else { + if (!slideChanged) { // Ensure that the previous slide is never the same as the current previousSlide = null; } @@ -2391,6 +2490,16 @@ } } + if( slideChanged ) { + dispatchEvent( 'slidechanged', { + 'indexh': indexh, + 'indexv': indexv, + 'previousSlide': previousSlide, + 'currentSlide': currentSlide, + 'origin': o + } ); + } + // Handle embedded content if( slideChanged || !previousSlide ) { stopEmbeddedContent( previousSlide ); @@ -2447,10 +2556,18 @@ updateSlideNumber(); updateSlidesVisibility(); updateBackground( true ); + updateNotesVisibility(); updateNotes(); formatEmbeddedContent(); - startEmbeddedContent( currentSlide ); + + // Start or stop embedded content depending on global config + if( config.autoPlayMedia === false ) { + stopEmbeddedContent( currentSlide, { unloadIframes: false } ); + } + else { + startEmbeddedContent( currentSlide ); + } if( isOverview() ) { layoutOverview(); @@ -2683,10 +2800,10 @@ // Show the horizontal slide if it's within the view distance if( distanceX < viewDistance ) { - showSlide( horizontalSlide ); + loadSlide( horizontalSlide ); } else { - hideSlide( horizontalSlide ); + unloadSlide( horizontalSlide ); } if( verticalSlidesLength ) { @@ -2699,16 +2816,32 @@ distanceY = x === ( indexh || 0 ) ? Math.abs( ( indexv || 0 ) - y ) : Math.abs( y - oy ); if( distanceX + distanceY < viewDistance ) { - showSlide( verticalSlide ); + loadSlide( verticalSlide ); } else { - hideSlide( verticalSlide ); + unloadSlide( verticalSlide ); } } } } + // Flag if there are ANY vertical slides, anywhere in the deck + if( dom.wrapper.querySelectorAll( '.slides>section>section' ).length ) { + dom.wrapper.classList.add( 'has-vertical-slides' ); + } + else { + dom.wrapper.classList.remove( 'has-vertical-slides' ); + } + + // Flag if there are ANY horizontal slides, anywhere in the deck + if( dom.wrapper.querySelectorAll( '.slides>section' ).length > 1 ) { + dom.wrapper.classList.add( 'has-horizontal-slides' ); + } + else { + dom.wrapper.classList.remove( 'has-horizontal-slides' ); + } + } } @@ -2723,13 +2856,40 @@ if( config.showNotes && dom.speakerNotes && currentSlide && !isPrintingPDF() ) { - dom.speakerNotes.innerHTML = getSlideNotes() || ''; + dom.speakerNotes.innerHTML = getSlideNotes() || '<span class="notes-placeholder">No notes on this slide.</span>'; + + } + + } + + /** + * Updates the visibility of the speaker notes sidebar that + * is used to share annotated slides. The notes sidebar is + * only visible if showNotes is true and there are notes on + * one or more slides in the deck. + */ + function updateNotesVisibility() { + if( config.showNotes && hasNotes() ) { + dom.wrapper.classList.add( 'show-notes' ); + } + else { + dom.wrapper.classList.remove( 'show-notes' ); } } /** + * Checks if there are speaker notes for ANY slide in the + * presentation. + */ + function hasNotes() { + + return dom.slides.querySelectorAll( '[data-notes], aside.notes' ).length > 0; + + } + + /** * Updates the progress bar to reflect the current slide. */ function updateProgress() { @@ -2859,6 +3019,26 @@ } + if( config.controlsTutorial ) { + + // Highlight control arrows with an animation to ensure + // that the viewer knows how to navigate + if( !hasNavigatedDown && routes.down ) { + dom.controlsDownArrow.classList.add( 'highlight' ); + } + else { + dom.controlsDownArrow.classList.remove( 'highlight' ); + + if( !hasNavigatedRight && routes.right && indexv === 0 ) { + dom.controlsRightArrow.classList.add( 'highlight' ); + } + else { + dom.controlsRightArrow.classList.remove( 'highlight' ); + } + } + + } + } /** @@ -3038,7 +3218,9 @@ * * @param {HTMLElement} slide Slide to show */ - function showSlide( slide ) { + function loadSlide( slide, options ) { + + options = options || {}; // Show the slide element slide.style.display = config.display; @@ -3046,6 +3228,7 @@ // Media elements with data-src attributes toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src]' ) ).forEach( function( element ) { element.setAttribute( 'src', element.getAttribute( 'data-src' ) ); + element.setAttribute( 'data-lazy-loaded', '' ); element.removeAttribute( 'data-src' ); } ); @@ -3056,6 +3239,7 @@ toArray( media.querySelectorAll( 'source[data-src]' ) ).forEach( function( source ) { source.setAttribute( 'src', source.getAttribute( 'data-src' ) ); source.removeAttribute( 'data-src' ); + source.setAttribute( 'data-lazy-loaded', '' ); sources += 1; } ); @@ -3068,8 +3252,7 @@ // Show the corresponding background element - var indices = getIndices( slide ); - var background = getSlideBackground( indices.h, indices.v ); + var background = getSlideBackground( slide ); if( background ) { background.style.display = 'block'; @@ -3085,7 +3268,7 @@ // Images if( backgroundImage ) { - background.style.backgroundImage = 'url('+ encodeURI(backgroundImage) +')'; + background.style.backgroundImage = 'url('+ encodeURI( backgroundImage ) +')'; } // Videos else if ( backgroundVideo && !isSpeakerNotes() ) { @@ -3099,6 +3282,15 @@ video.muted = true; } + // Inline video playback works (at least in Mobile Safari) as + // long as the video is muted and the `playsinline` attribute is + // present + if( isMobileDevice ) { + video.muted = true; + video.autoplay = true; + video.setAttribute( 'playsinline', '' ); + } + // Support comma separated lists of video sources backgroundVideo.split( ',' ).forEach( function( source ) { video.innerHTML += '<source src="'+ source +'">'; @@ -3107,8 +3299,11 @@ background.appendChild( video ); } // Iframes - else if( backgroundIframe ) { + else if( backgroundIframe && options.excludeIframes !== true ) { var iframe = document.createElement( 'iframe' ); + iframe.setAttribute( 'allowfullscreen', '' ); + iframe.setAttribute( 'mozallowfullscreen', '' ); + iframe.setAttribute( 'webkitallowfullscreen', '' ); // Only load autoplaying content when the slide is shown to // avoid having it play in the background @@ -3127,28 +3322,40 @@ background.appendChild( iframe ); } } + } } /** - * Called when the given slide is moved outside of the - * configured view distance. + * Unloads and hides the given slide. This is called when the + * slide is moved outside of the configured view distance. * * @param {HTMLElement} slide */ - function hideSlide( slide ) { + function unloadSlide( slide ) { // Hide the slide element slide.style.display = 'none'; // Hide the corresponding background element - var indices = getIndices( slide ); - var background = getSlideBackground( indices.h, indices.v ); + var background = getSlideBackground( slide ); if( background ) { background.style.display = 'none'; } + // Reset lazy-loaded media elements with src attributes + toArray( slide.querySelectorAll( 'video[data-lazy-loaded][src], audio[data-lazy-loaded][src]' ) ).forEach( function( element ) { + element.setAttribute( 'data-src', element.getAttribute( 'src' ) ); + element.removeAttribute( 'src' ); + } ); + + // Reset lazy-loaded media elements with <source> children + toArray( slide.querySelectorAll( 'video[data-lazy-loaded] source[src], audio source[src]' ) ).forEach( function( source ) { + source.setAttribute( 'data-src', source.getAttribute( 'src' ) ); + source.removeAttribute( 'src' ); + } ); + } /** @@ -3162,13 +3369,27 @@ verticalSlides = dom.wrapper.querySelectorAll( VERTICAL_SLIDES_SELECTOR ); var routes = { - left: indexh > 0 || config.loop, - right: indexh < horizontalSlides.length - 1 || config.loop, + left: indexh > 0, + right: indexh < horizontalSlides.length - 1, up: indexv > 0, down: indexv < verticalSlides.length - 1 }; - // reverse horizontal controls for rtl + // Looped presentations can always be navigated as long as + // there are slides available + if( config.loop ) { + if( horizontalSlides.length > 1 ) { + routes.left = true; + routes.right = true; + } + + if( verticalSlides.length > 1 ) { + routes.up = true; + routes.down = true; + } + } + + // Reverse horizontal controls for rtl if( config.rtl ) { var left = routes.left; routes.left = routes.right; @@ -3224,6 +3445,13 @@ _appendParamToIframeSource( 'src', 'player.vimeo.com/', 'api=1' ); _appendParamToIframeSource( 'data-src', 'player.vimeo.com/', 'api=1' ); + // Always show media controls on mobile devices + if( isMobileDevice ) { + toArray( dom.slides.querySelectorAll( 'video, audio' ) ).forEach( function( el ) { + el.controls = true; + } ); + } + } /** @@ -3249,14 +3477,16 @@ return; } - // Autoplay is always on for slide backgrounds - var autoplay = el.hasAttribute( 'data-autoplay' ) || - el.hasAttribute( 'data-paused-by-reveal' ) || - !!closestParent( el, '.slide-background' ); + // Prefer an explicit global autoplay setting + var autoplay = config.autoPlayMedia; - if( autoplay && typeof el.play === 'function' ) { + // If no global setting is available, fall back on the element's + // own autoplay setting + if( typeof autoplay !== 'boolean' ) { + autoplay = el.hasAttribute( 'data-autoplay' ) || !!closestParent( el, '.slide-background' ); + } - el.removeAttribute( 'data-paused-by-reveal' ); + if( autoplay && typeof el.play === 'function' ) { if( el.readyState > 1 ) { startEmbeddedMedia( { target: el } ); @@ -3264,9 +3494,6 @@ else { el.removeEventListener( 'loadeddata', startEmbeddedMedia ); // remove first to avoid dupes el.addEventListener( 'loadeddata', startEmbeddedMedia ); - - // `loadeddata` never fires unless we start playing on iPad - if( /ipad/gi.test( UA ) ) el.play(); } } @@ -3335,7 +3562,14 @@ if( isAttachedToDOM && isVisible ) { - var autoplay = iframe.hasAttribute( 'data-autoplay' ) || !!closestParent( iframe, '.slide-background' ); + // Prefer an explicit global autoplay setting + var autoplay = config.autoPlayMedia; + + // If no global setting is available, fall back on the element's + // own autoplay setting + if( typeof autoplay !== 'boolean' ) { + autoplay = iframe.hasAttribute( 'data-autoplay' ) || !!closestParent( iframe, '.slide-background' ); + } // YouTube postMessage API if( /youtube\.com\/embed\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) { @@ -3361,10 +3595,13 @@ * the targeted slide. * * @param {HTMLElement} element - * @param {boolean} autoplay Optionally override the - * autoplay setting of media elements */ - function stopEmbeddedContent( element, autoplay ) { + function stopEmbeddedContent( element, options ) { + + options = extend( { + // Defaults + unloadIframes: true + }, options || {} ); if( element && element.parentNode ) { // HTML5 media elements @@ -3377,31 +3614,33 @@ // Generic postMessage API for non-lazy loaded iframes toArray( element.querySelectorAll( 'iframe' ) ).forEach( function( el ) { - el.contentWindow.postMessage( 'slide:stop', '*' ); + if( el.contentWindow ) el.contentWindow.postMessage( 'slide:stop', '*' ); el.removeEventListener( 'load', startEmbeddedIframe ); }); // YouTube postMessage API toArray( element.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) { - if( !el.hasAttribute( 'data-ignore' ) && typeof el.contentWindow.postMessage === 'function' ) { + if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) { el.contentWindow.postMessage( '{"event":"command","func":"pauseVideo","args":""}', '*' ); } }); // Vimeo postMessage API toArray( element.querySelectorAll( 'iframe[src*="player.vimeo.com/"]' ) ).forEach( function( el ) { - if( !el.hasAttribute( 'data-ignore' ) && typeof el.contentWindow.postMessage === 'function' ) { + if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) { el.contentWindow.postMessage( '{"method":"pause"}', '*' ); } }); - // Lazy loading iframes - toArray( element.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) { - // Only removing the src doesn't actually unload the frame - // in all browsers (Firefox) so we set it to blank first - el.setAttribute( 'src', 'about:blank' ); - el.removeAttribute( 'src' ); - } ); + if( options.unloadIframes === true ) { + // Unload lazy-loaded iframes + toArray( element.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) { + // Only removing the src doesn't actually unload the frame + // in all browsers (Firefox) so we set it to blank first + el.setAttribute( 'src', 'about:blank' ); + el.removeAttribute( 'src' ); + } ); + } } } @@ -3534,10 +3773,17 @@ else { // Read the index components of the hash var h = parseInt( bits[0], 10 ) || 0, - v = parseInt( bits[1], 10 ) || 0; + v = parseInt( bits[1], 10 ) || 0, + f; + if( config.fragmentInURL ) { + f = parseInt( bits[2], 10 ); + if( isNaN( f ) ) { + f = undefined; + } + } - if( h !== indexh || v !== indexv ) { - slide( h, v ); + if( h !== indexh || v !== indexv || f !== undefined ) { + slide( h, v, f ); } } @@ -3570,14 +3816,21 @@ id = id.replace( /[^a-zA-Z0-9\-\_\:\.]/g, '' ); } - // If the current slide has an ID, use that as a named link - if( typeof id === 'string' && id.length ) { + var indexf; + if( config.fragmentInURL ) { + indexf = getIndices().f; + } + + // If the current slide has an ID, use that as a named link, + // but we don't support named links with a fragment index + if( typeof id === 'string' && id.length && indexf === undefined ) { url = '/' + id; } // Otherwise use the /h/v index else { - if( indexh > 0 || indexv > 0 ) url += indexh; - if( indexv > 0 ) url += '/' + indexv; + if( indexh > 0 || indexv > 0 || indexf !== undefined ) url += indexh; + if( indexv > 0 || indexf !== undefined ) url += '/' + indexv; + if( indexf !== undefined ) url += '/' + indexf; } window.location.hash = url; @@ -3640,13 +3893,22 @@ } /** + * Retrieves all slides in this presentation. + */ + function getSlides() { + + return toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ':not(.stack)' )); + + } + + /** * Retrieves the total number of slides in this presentation. * * @return {number} */ function getTotalSlides() { - return dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ':not(.stack)' ).length; + return getSlides().length; } @@ -3674,31 +3936,19 @@ * defined, have a background element so as long as the * index is valid an element will be returned. * - * @param {number} x Horizontal background index + * @param {mixed} x Horizontal background index OR a slide + * HTML element * @param {number} y Vertical background index * @return {(HTMLElement[]|*)} */ function getSlideBackground( x, y ) { - // When printing to PDF the slide backgrounds are nested - // inside of the slides - if( isPrintingPDF() ) { - var slide = getSlide( x, y ); - if( slide ) { - return slide.slideBackgroundElement; - } - - return undefined; - } - - var horizontalBackground = dom.wrapper.querySelectorAll( '.backgrounds>.slide-background' )[ x ]; - var verticalBackgrounds = horizontalBackground && horizontalBackground.querySelectorAll( '.slide-background' ); - - if( verticalBackgrounds && verticalBackgrounds.length && typeof y === 'number' ) { - return verticalBackgrounds ? verticalBackgrounds[ y ] : undefined; + var slide = typeof x === 'number' ? getSlide( x, y ) : x; + if( slide ) { + return slide.slideBackgroundElement; } - return horizontalBackground; + return undefined; } @@ -3918,6 +4168,9 @@ updateControls(); updateProgress(); + if( config.fragmentInURL ) { + writeURL(); + } return !!( fragmentsShown.length || fragmentsHidden.length ); @@ -3960,7 +4213,7 @@ cancelAutoSlide(); - if( currentSlide ) { + if( currentSlide && config.autoSlide !== false ) { var fragment = currentSlide.querySelector( '.current-fragment' ); @@ -4078,6 +4331,8 @@ function navigateRight() { + hasNavigatedRight = true; + // Reverse for RTL if( config.rtl ) { if( ( isOverview() || previousFragment() === false ) && availableRoutes().right ) { @@ -4102,6 +4357,8 @@ function navigateDown() { + hasNavigatedDown = true; + // Prioritize revealing fragments if( ( isOverview() || nextFragment() === false ) && availableRoutes().down ) { slide( indexh, indexv + 1 ); @@ -4148,6 +4405,9 @@ */ function navigateNext() { + hasNavigatedRight = true; + hasNavigatedDown = true; + // Prioritize revealing fragments if( nextFragment() === false ) { if( availableRoutes().down ) { @@ -4220,7 +4480,7 @@ // If there's a condition specified and it returns false, // ignore this event - if( typeof config.keyboardCondition === 'function' && config.keyboardCondition() === false ) { + if( typeof config.keyboardCondition === 'function' && config.keyboardCondition(event) === false ) { return true; } @@ -4285,7 +4545,31 @@ } - // 2. System defined key bindings + // 2. Registered custom key bindings + if( triggered === false ) { + + for( key in registeredKeyBindings ) { + + // Check if this binding matches the pressed key + if( parseInt( key, 10 ) === event.keyCode ) { + + var action = registeredKeyBindings[ key ].callback; + + // Callback function + if( typeof action === 'function' ) { + action.apply( null, [ event ] ); + } + // String shortcuts to reveal.js API + else if( typeof action === 'string' && typeof Reveal[ action ] === 'function' ) { + Reveal[ action ].call(); + } + + triggered = true; + } + } + } + + // 3. System defined key bindings if( triggered === false ) { // Assume true and try to prove false @@ -4816,7 +5100,7 @@ this.context.beginPath(); this.context.arc( x, y, radius, 0, Math.PI * 2, false ); this.context.lineWidth = this.thickness; - this.context.strokeStyle = '#666'; + this.context.strokeStyle = 'rgba( 255, 255, 255, 0.2 )'; this.context.stroke(); if( this.playing ) { @@ -4932,6 +5216,11 @@ isOverview: isOverview, isPaused: isPaused, isAutoSliding: isAutoSliding, + isSpeakerNotes: isSpeakerNotes, + + // Slide preloading + loadSlide: loadSlide, + unloadSlide: unloadSlide, // Adds or removes all internal event listeners (such as keyboard) addEventListeners: addEventListeners, @@ -4941,12 +5230,19 @@ getState: getState, setState: setState, + // Presentation progress + getSlidePastCount: getSlidePastCount, + // Presentation progress on range of 0-1 getProgress: getProgress, // Returns the indices of the current, or specified, slide getIndices: getIndices, + // Returns an Array of all slides + getSlides: getSlides, + + // Returns the total number of slides getTotalSlides: getTotalSlides, // Returns the slide element at the specified index @@ -5033,6 +5329,12 @@ } }, + // Adds a custom key binding + addKeyBinding: addKeyBinding, + + // Removes a custom key binding + removeKeyBinding: removeKeyBinding, + // Programatically triggers a keyboard event triggerKey: function( keyCode ) { onDocumentKeyDown( { keyCode: keyCode } ); |