diff options
Diffstat (limited to 'js/reveal.js')
-rw-r--r-- | js/reveal.js | 676 |
1 files changed, 506 insertions, 170 deletions
diff --git a/js/reveal.js b/js/reveal.js index c371371..103fa82 100644 --- a/js/reveal.js +++ b/js/reveal.js @@ -3,7 +3,7 @@ * http://revealjs.com * MIT licensed * - * Copyright (C) 2017 Hakim El Hattab, http://hakim.se + * Copyright (C) 2018 Hakim El Hattab, http://hakim.se */ (function( root, factory ) { if( typeof define === 'function' && define.amd ) { @@ -69,6 +69,10 @@ // Display the page number of the current slide slideNumber: false, + // Use 1 based indexing for # links to match slide number (default is zero + // based) + hashOneBasedIndex: false, + // Determine which displays to show the slide number on showSlideNumber: 'all', @@ -84,6 +88,10 @@ // Enable the slide overview mode overview: true, + // Disables the default reveal.js slide layout so that you can use + // custom CSS layout + disableLayout: false, + // Vertical centering of slides center: true, @@ -102,6 +110,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, @@ -135,6 +147,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, @@ -173,6 +190,12 @@ // Parallax background size parallaxBackgroundSize: '', // CSS syntax, e.g. "3000px 2000px" + // Parallax background repeat + parallaxBackgroundRepeat: '', // repeat/repeat-x/repeat-y/no-repeat/initial/inherit + + // Parallax background position + parallaxBackgroundPosition: '', // CSS syntax, e.g. "top left" + // Amount of pixels to move the parallax background per slide step parallaxBackgroundHorizontal: null, parallaxBackgroundVertical: null, @@ -181,6 +204,9 @@ // to PDF, unlimited by default pdfMaxPagesPerSlide: Number.POSITIVE_INFINITY, + // Prints each fragment on a separate slide + pdfSeparateFragments: true, + // 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 @@ -291,7 +317,10 @@ 'B , .': 'Pause', 'F': 'Fullscreen', 'ESC, O': 'Slide overview' - }; + }, + + // Holds custom key code mappings + registeredKeyBindings = {}; /** * Starts up the presentation if the client is capable. @@ -393,13 +422,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 = [], @@ -417,7 +446,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 ); @@ -564,7 +593,8 @@ dom.speakerNotes.setAttribute( 'tabindex', '0' ); // Overlay graphic which is displayed during the paused mode - createSingletonNode( dom.wrapper, 'div', 'pause-overlay', null ); + dom.pauseOverlay = createSingletonNode( dom.wrapper, 'div', 'pause-overlay', '<button class="resume-button">Resume presentation</button>' ); + dom.resumeButton = dom.pauseOverlay.querySelector( '.resume-button' ); dom.wrapper.setAttribute( 'role', 'application' ); @@ -761,13 +791,58 @@ numberElement.innerHTML = formatSlideNumber( slideNumberH, '.', slideNumberV ); page.appendChild( numberElement ); } - } - } ); + // Copy page and show fragments one after another + if( config.pdfSeparateFragments ) { + + // Each fragment 'group' is an array containing one or more + // fragments. Multiple fragments that appear at the same time + // are part of the same group. + var fragmentGroups = sortFragments( page.querySelectorAll( '.fragment' ), true ); + + var previousFragmentStep; + var previousPage; + + fragmentGroups.forEach( function( fragments ) { + + // Remove 'current-fragment' from the previous group + if( previousFragmentStep ) { + previousFragmentStep.forEach( function( fragment ) { + fragment.classList.remove( 'current-fragment' ); + } ); + } + + // Show the fragments for the current index + fragments.forEach( function( fragment ) { + fragment.classList.add( 'visible', 'current-fragment' ); + } ); + + // Create a separate page for the current fragment state + var clonedPage = page.cloneNode( true ); + page.parentNode.insertBefore( clonedPage, ( previousPage || page ).nextSibling ); + + previousFragmentStep = fragments; + previousPage = clonedPage; + + } ); + + // Reset the first/original page so that all fragments are hidden + fragmentGroups.forEach( function( fragments ) { + fragments.forEach( function( fragment ) { + fragment.classList.remove( 'visible', 'current-fragment' ); + } ); + } ); + + } + // Show all fragments + else { + toArray( page.querySelectorAll( '.fragment:not(.fade-out)' ) ).forEach( function( fragment ) { + fragment.classList.add( 'visible' ); + } ); + } + + } - // Show all fragments - toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ' .fragment' ) ).forEach( function( fragment ) { - fragment.classList.add( 'visible' ); } ); // Notify subscribers that the PDF layout is good to go @@ -867,6 +942,8 @@ dom.background.style.backgroundImage = 'url("' + config.parallaxBackgroundImage + '")'; dom.background.style.backgroundSize = config.parallaxBackgroundSize; + dom.background.style.backgroundRepeat = config.parallaxBackgroundRepeat; + dom.background.style.backgroundPosition = config.parallaxBackgroundPosition; // Make sure the below properties are set on the element - these properties are // needed for proper transitions to be set on the element via CSS. To remove @@ -896,6 +973,57 @@ */ function createBackground( slide, container ) { + + // Main slide background element + var element = document.createElement( 'div' ); + element.className = 'slide-background ' + slide.className.replace( /present|past|future/, '' ); + + // Inner background element that wraps images/videos/iframes + var contentElement = document.createElement( 'div' ); + contentElement.className = 'slide-background-content'; + + element.appendChild( contentElement ); + container.appendChild( element ); + + slide.slideBackgroundElement = element; + slide.slideBackgroundContentElement = contentElement; + + // Syncs the background to reflect all current background settings + syncBackground( slide ); + + return element; + + } + + /** + * Renders all of the visual properties of a slide background + * based on the various background attributes. + * + * @param {HTMLElement} slide + */ + function syncBackground( slide ) { + + var element = slide.slideBackgroundElement, + contentElement = slide.slideBackgroundContentElement; + + // Reset the prior background state in case this is not the + // initial sync + slide.classList.remove( 'has-dark-background' ); + slide.classList.remove( 'has-light-background' ); + + element.removeAttribute( 'data-loaded' ); + element.removeAttribute( 'data-background-hash' ); + element.removeAttribute( 'data-background-size' ); + element.removeAttribute( 'data-background-transition' ); + element.style.backgroundColor = ''; + + contentElement.style.backgroundSize = ''; + contentElement.style.backgroundRepeat = ''; + contentElement.style.backgroundPosition = ''; + contentElement.style.backgroundImage = ''; + contentElement.style.opacity = ''; + contentElement.innerHTML = ''; + var data = { background: slide.getAttribute( 'data-background' ), backgroundSize: slide.getAttribute( 'data-background-size' ), @@ -905,17 +1033,13 @@ backgroundColor: slide.getAttribute( 'data-background-color' ), backgroundRepeat: slide.getAttribute( 'data-background-repeat' ), backgroundPosition: slide.getAttribute( 'data-background-position' ), - backgroundTransition: slide.getAttribute( 'data-background-transition' ) + backgroundTransition: slide.getAttribute( 'data-background-transition' ), + backgroundOpacity: slide.getAttribute( 'data-background-opacity' ) }; - var element = document.createElement( 'div' ); - - // Carry over custom classes from the slide to the background - element.className = 'slide-background ' + slide.className.replace( /present|past|future/, '' ); - 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 { @@ -935,24 +1059,20 @@ data.backgroundColor + data.backgroundRepeat + data.backgroundPosition + - data.backgroundTransition ); + data.backgroundTransition + + data.backgroundOpacity ); } // Additional and optional background properties - if( data.backgroundSize ) element.style.backgroundSize = data.backgroundSize; if( data.backgroundSize ) element.setAttribute( 'data-background-size', data.backgroundSize ); if( data.backgroundColor ) element.style.backgroundColor = data.backgroundColor; - if( data.backgroundRepeat ) element.style.backgroundRepeat = data.backgroundRepeat; - if( data.backgroundPosition ) element.style.backgroundPosition = data.backgroundPosition; if( data.backgroundTransition ) element.setAttribute( 'data-background-transition', data.backgroundTransition ); - container.appendChild( element ); - - // If backgrounds are being recreated, clear old classes - slide.classList.remove( 'has-dark-background' ); - slide.classList.remove( 'has-light-background' ); - - slide.slideBackgroundElement = element; + // Background image options are set on the content wrapper + if( data.backgroundSize ) contentElement.style.backgroundSize = data.backgroundSize; + if( data.backgroundRepeat ) contentElement.style.backgroundRepeat = data.backgroundRepeat; + if( data.backgroundPosition ) contentElement.style.backgroundPosition = data.backgroundPosition; + if( data.backgroundOpacity ) contentElement.style.opacity = data.backgroundOpacity; // If this slide has a background color, add a class that // signals if it is light or dark. If the slide has no background @@ -974,8 +1094,6 @@ } } - return element; - } /** @@ -1152,13 +1270,8 @@ window.addEventListener( 'resize', onWindowResize, false ); if( config.touch ) { - dom.wrapper.addEventListener( 'touchstart', onTouchStart, false ); - dom.wrapper.addEventListener( 'touchmove', onTouchMove, false ); - dom.wrapper.addEventListener( 'touchend', onTouchEnd, false ); - - // Support pointer-style touch interaction as well - if( window.navigator.pointerEnabled ) { - // IE 11 uses un-prefixed version of pointer events + if( 'onpointerdown' in window ) { + // Use W3C pointer events dom.wrapper.addEventListener( 'pointerdown', onPointerDown, false ); dom.wrapper.addEventListener( 'pointermove', onPointerMove, false ); dom.wrapper.addEventListener( 'pointerup', onPointerUp, false ); @@ -1169,6 +1282,12 @@ dom.wrapper.addEventListener( 'MSPointerMove', onPointerMove, false ); dom.wrapper.addEventListener( 'MSPointerUp', onPointerUp, false ); } + else { + // Fall back to touch events + dom.wrapper.addEventListener( 'touchstart', onTouchStart, false ); + dom.wrapper.addEventListener( 'touchmove', onTouchMove, false ); + dom.wrapper.addEventListener( 'touchend', onTouchEnd, false ); + } } if( config.keyboard ) { @@ -1180,6 +1299,8 @@ dom.progress.addEventListener( 'click', onProgressClicked, false ); } + dom.resumeButton.addEventListener( 'click', resume, false ); + if( config.focusBodyOnPageVisibilityChange ) { var visibilityChange; @@ -1231,22 +1352,19 @@ window.removeEventListener( 'hashchange', onWindowHashChange, false ); window.removeEventListener( 'resize', onWindowResize, false ); + dom.wrapper.removeEventListener( 'pointerdown', onPointerDown, false ); + dom.wrapper.removeEventListener( 'pointermove', onPointerMove, false ); + dom.wrapper.removeEventListener( 'pointerup', onPointerUp, false ); + + dom.wrapper.removeEventListener( 'MSPointerDown', onPointerDown, false ); + dom.wrapper.removeEventListener( 'MSPointerMove', onPointerMove, false ); + dom.wrapper.removeEventListener( 'MSPointerUp', onPointerUp, false ); + dom.wrapper.removeEventListener( 'touchstart', onTouchStart, false ); dom.wrapper.removeEventListener( 'touchmove', onTouchMove, false ); dom.wrapper.removeEventListener( 'touchend', onTouchEnd, false ); - // IE11 - if( window.navigator.pointerEnabled ) { - dom.wrapper.removeEventListener( 'pointerdown', onPointerDown, false ); - dom.wrapper.removeEventListener( 'pointermove', onPointerMove, false ); - dom.wrapper.removeEventListener( 'pointerup', onPointerUp, false ); - } - // IE10 - else if( window.navigator.msPointerEnabled ) { - dom.wrapper.removeEventListener( 'MSPointerDown', onPointerDown, false ); - dom.wrapper.removeEventListener( 'MSPointerMove', onPointerMove, false ); - dom.wrapper.removeEventListener( 'MSPointerUp', onPointerUp, false ); - } + dom.resumeButton.removeEventListener( 'click', resume, false ); if ( config.progress && dom.progress ) { dom.progress.removeEventListener( 'click', onProgressClicked, false ); @@ -1264,6 +1382,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. * @@ -1541,6 +1691,15 @@ } /** + * Check if this instance is being used to print a PDF with fragments. + */ + function isPrintingPDFFragments() { + + return ( /print-pdf-fragments/gi ).test( window.location.search ); + + } + + /** * Hides the address bar if we're on a mobile device. */ function hideAddressBar() { @@ -1750,6 +1909,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 = [ @@ -1794,76 +1960,80 @@ if( dom.wrapper && !isPrintingPDF() ) { - var size = getComputedSlideSize(); + if( !config.disableLayout ) { - // Layout the contents of the slides - layoutSlideContents( config.width, config.height ); + var size = getComputedSlideSize(); - dom.slides.style.width = size.width + 'px'; - dom.slides.style.height = size.height + 'px'; + // Layout the contents of the slides + layoutSlideContents( config.width, config.height ); - // Determine scale of content to fit within available space - scale = Math.min( size.presentationWidth / size.width, size.presentationHeight / size.height ); + dom.slides.style.width = size.width + 'px'; + dom.slides.style.height = size.height + 'px'; - // Respect max/min scale settings - scale = Math.max( scale, config.minScale ); - scale = Math.min( scale, config.maxScale ); + // Determine scale of content to fit within available space + scale = Math.min( size.presentationWidth / size.width, size.presentationHeight / size.height ); - // Don't apply any scaling styles if scale is 1 - if( scale === 1 ) { - dom.slides.style.zoom = ''; - dom.slides.style.left = ''; - dom.slides.style.top = ''; - dom.slides.style.bottom = ''; - dom.slides.style.right = ''; - transformSlides( { layout: '' } ); - } - else { - // Prefer zoom for scaling up so that content remains crisp. - // Don't use zoom to scale down since that can lead to shifts - // in text layout/line breaks. - if( scale > 1 && features.zoom ) { - dom.slides.style.zoom = scale; + // Respect max/min scale settings + scale = Math.max( scale, config.minScale ); + scale = Math.min( scale, config.maxScale ); + + // Don't apply any scaling styles if scale is 1 + if( scale === 1 ) { + dom.slides.style.zoom = ''; dom.slides.style.left = ''; dom.slides.style.top = ''; dom.slides.style.bottom = ''; dom.slides.style.right = ''; transformSlides( { layout: '' } ); } - // Apply scale transform as a fallback else { - dom.slides.style.zoom = ''; - dom.slides.style.left = '50%'; - dom.slides.style.top = '50%'; - dom.slides.style.bottom = 'auto'; - dom.slides.style.right = 'auto'; - transformSlides( { layout: 'translate(-50%, -50%) scale('+ scale +')' } ); + // Prefer zoom for scaling up so that content remains crisp. + // Don't use zoom to scale down since that can lead to shifts + // in text layout/line breaks. + if( scale > 1 && features.zoom ) { + dom.slides.style.zoom = scale; + dom.slides.style.left = ''; + dom.slides.style.top = ''; + dom.slides.style.bottom = ''; + dom.slides.style.right = ''; + transformSlides( { layout: '' } ); + } + // Apply scale transform as a fallback + else { + dom.slides.style.zoom = ''; + dom.slides.style.left = '50%'; + dom.slides.style.top = '50%'; + dom.slides.style.bottom = 'auto'; + dom.slides.style.right = 'auto'; + transformSlides( { layout: 'translate(-50%, -50%) scale('+ scale +')' } ); + } } - } - // Select all slides, vertical and horizontal - var slides = toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ); + // Select all slides, vertical and horizontal + var slides = toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ); - for( var i = 0, len = slides.length; i < len; i++ ) { - var slide = slides[ i ]; + for( var i = 0, len = slides.length; i < len; i++ ) { + var slide = slides[ i ]; - // Don't bother updating invisible slides - if( slide.style.display === 'none' ) { - continue; - } + // Don't bother updating invisible slides + if( slide.style.display === 'none' ) { + continue; + } - if( config.center || slide.classList.contains( 'center' ) ) { - // Vertical stacks are not centred since their section - // children will be - if( slide.classList.contains( 'stack' ) ) { - slide.style.top = 0; + if( config.center || slide.classList.contains( 'center' ) ) { + // Vertical stacks are not centred since their section + // children will be + if( slide.classList.contains( 'stack' ) ) { + slide.style.top = 0; + } + else { + slide.style.top = Math.max( ( size.height - slide.scrollHeight ) / 2, 0 ) + 'px'; + } } else { - slide.style.top = Math.max( ( size.height - slide.scrollHeight ) / 2, 0 ) + 'px'; + slide.style.top = ''; } - } - else { - slide.style.top = ''; + } } @@ -2190,6 +2360,41 @@ } /** + * Return a hash URL that will resolve to the current slide location. + */ + function locationHash() { + + var url = '/'; + + // Attempt to create a named link based on the slide's ID + var id = currentSlide ? currentSlide.getAttribute( 'id' ) : null; + if( id ) { + id = encodeURIComponent( id ); + } + + 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 { + var hashIndexBase = config.hashOneBasedIndex ? 1 : 0; + if( indexh > 0 || indexv > 0 || indexf !== undefined ) url += indexh + hashIndexBase; + if( indexv > 0 || indexf !== undefined ) url += '/' + (indexv + hashIndexBase ); + if( indexf !== undefined ) url += '/' + indexf; + } + + return url; + + } + + /** * Checks if the current or specified slide is vertical * (nested within another slide). * @@ -2413,16 +2618,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; } @@ -2430,7 +2626,7 @@ // Solves an edge case where the previous slide maintains the // 'present' class when navigating between adjacent vertical // stacks - if( previousSlide ) { + if( previousSlide && previousSlide !== currentSlide ) { previousSlide.classList.remove( 'present' ); previousSlide.setAttribute( 'aria-hidden', 'true' ); @@ -2450,6 +2646,16 @@ } } + if( slideChanged ) { + dispatchEvent( 'slidechanged', { + 'indexh': indexh, + 'indexv': indexv, + 'previousSlide': previousSlide, + 'currentSlide': currentSlide, + 'origin': o + } ); + } + // Handle embedded content if( slideChanged || !previousSlide ) { stopEmbeddedContent( previousSlide ); @@ -2526,6 +2732,41 @@ } /** + * Updates reveal.js to keep in sync with new slide attributes. For + * example, if you add a new `data-background-image` you can call + * this to have reveal.js render the new background image. + * + * Similar to #sync() but more efficient when you only need to + * refresh a specific slide. + * + * @param {HTMLElement} slide + */ + function syncSlide( slide ) { + + syncBackground( slide ); + syncFragments( slide ); + + updateBackground(); + updateNotes(); + + loadSlide( slide ); + + } + + /** + * Formats the fragments on the given slide so that they have + * valid indices. Call this if fragments are changed in the DOM + * after reveal.js has already initialized. + * + * @param {HTMLElement} slide + */ + function syncFragments( slide ) { + + sortFragments( slide.querySelectorAll( '.fragment' ) ); + + } + + /** * Resets all vertical slides so that only the first * is visible. */ @@ -2853,6 +3094,7 @@ } + /** * Updates the slide number div to reflect the current slide. * @@ -2875,6 +3117,12 @@ format = config.slideNumber; } + // If there are ONLY vertical slides in this deck, always use + // a flattened slide number + if( !/c/.test( format ) && dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ).length === 1 ) { + format = 'c'; + } + switch( format ) { case 'c': value.push( getSlidePastCount() + 1 ); @@ -2907,13 +3155,18 @@ */ function formatSlideNumber( a, delimiter, b ) { + var url = '#' + locationHash(); if( typeof b === 'number' && !isNaN( b ) ) { - return '<span class="slide-number-a">'+ a +'</span>' + + return '<a href="' + url + '">' + + '<span class="slide-number-a">'+ a +'</span>' + '<span class="slide-number-delimiter">'+ delimiter +'</span>' + - '<span class="slide-number-b">'+ b +'</span>'; + '<span class="slide-number-b">'+ b +'</span>' + + '</a>'; } else { - return '<span class="slide-number-a">'+ a +'</span>'; + return '<a href="' + url + '">' + + '<span class="slide-number-a">'+ a +'</span>' + + '</a>'; } } @@ -3064,13 +3317,18 @@ startEmbeddedContent( currentBackground ); - var backgroundImageURL = currentBackground.style.backgroundImage || ''; + var currentBackgroundContent = currentBackground.querySelector( '.slide-background-content' ); + if( currentBackgroundContent ) { + + var backgroundImageURL = currentBackgroundContent.style.backgroundImage || ''; + + // Restart GIFs (doesn't work in Firefox) + if( /\.gif/i.test( backgroundImageURL ) ) { + currentBackgroundContent.style.backgroundImage = ''; + window.getComputedStyle( currentBackgroundContent ).opacity; + currentBackgroundContent.style.backgroundImage = backgroundImageURL; + } - // Restart GIFs (doesn't work in Firefox) - if( /\.gif/i.test( backgroundImageURL ) ) { - currentBackground.style.backgroundImage = ''; - window.getComputedStyle( currentBackground ).opacity; - currentBackground.style.backgroundImage = backgroundImageURL; } // Don't transition between identical backgrounds. This @@ -3202,11 +3460,12 @@ // Show the corresponding background element - var indices = getIndices( slide ); - var background = getSlideBackground( indices.h, indices.v ); + var background = slide.slideBackgroundElement; if( background ) { background.style.display = 'block'; + var backgroundContent = slide.slideBackgroundContentElement; + // If the background contains media, load it if( background.hasAttribute( 'data-loaded' ) === false ) { background.setAttribute( 'data-loaded', 'true' ); @@ -3219,7 +3478,7 @@ // Images if( backgroundImage ) { - background.style.backgroundImage = 'url('+ backgroundImage +')'; + backgroundContent.style.backgroundImage = 'url('+ encodeURI( backgroundImage ) +')'; } // Videos else if ( backgroundVideo && !isSpeakerNotes() ) { @@ -3247,7 +3506,7 @@ video.innerHTML += '<source src="'+ source +'">'; } ); - background.appendChild( video ); + backgroundContent.appendChild( video ); } // Iframes else if( backgroundIframe && options.excludeIframes !== true ) { @@ -3270,7 +3529,7 @@ iframe.style.maxHeight = '100%'; iframe.style.maxWidth = '100%'; - background.appendChild( iframe ); + backgroundContent.appendChild( iframe ); } } @@ -3290,8 +3549,7 @@ 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'; } @@ -3321,13 +3579,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; @@ -3426,9 +3698,16 @@ if( autoplay && typeof el.play === 'function' ) { + // If the media is ready, start playback if( el.readyState > 1 ) { startEmbeddedMedia( { target: el } ); } + // Mobile devices never fire a loaded event so instead + // of waiting, we initiate playback + else if( isMobileDevice ) { + el.play(); + } + // If the media isn't loaded, wait before playing else { el.removeEventListener( 'loadeddata', startEmbeddedMedia ); // remove first to avoid dupes el.addEventListener( 'loadeddata', startEmbeddedMedia ); @@ -3693,12 +3972,15 @@ var element; // Ensure the named link is a valid HTML ID attribute - if( /^[a-zA-Z][\w:.-]*$/.test( name ) ) { - // Find the slide with the specified ID - element = document.getElementById( name ); + try { + element = document.getElementById( decodeURIComponent( name ) ); } + catch ( error ) { } + + // Ensure that we're not already on a slide with the same name + var isSameNameAsCurrentSlide = currentSlide ? currentSlide.getAttribute( 'id' ) === name : false; - if( element ) { + if( element && !isSameNameAsCurrentSlide ) { // Find the position of the named slide and navigate to it var indices = Reveal.getIndices( element ); slide( indices.h, indices.v ); @@ -3709,12 +3991,22 @@ } } else { + var hashIndexBase = config.hashOneBasedIndex ? 1 : 0; + // Read the index components of the hash - var h = parseInt( bits[0], 10 ) || 0, - v = parseInt( bits[1], 10 ) || 0; + var h = ( parseInt( bits[0], 10 ) - hashIndexBase ) || 0, + v = ( parseInt( bits[1], 10 ) - hashIndexBase ) || 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 ); } } @@ -3739,25 +4031,7 @@ writeURLTimeout = setTimeout( writeURL, delay ); } else if( currentSlide ) { - var url = '/'; - - // Attempt to create a named link based on the slide's ID - var id = currentSlide.getAttribute( 'id' ); - if( id ) { - 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 ) { - url = '/' + id; - } - // Otherwise use the /h/v index - else { - if( indexh > 0 || indexv > 0 ) url += indexh; - if( indexv > 0 ) url += '/' + indexv; - } - - window.location.hash = url; + window.location.hash = locationHash(); } } @@ -3860,13 +4134,14 @@ * 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 ) { - var slide = getSlide( x, y ); + var slide = typeof x === 'number' ? getSlide( x, y ) : x; if( slide ) { return slide.slideBackgroundElement; } @@ -3965,9 +4240,11 @@ * the fragment within the fragments list. * * @param {object[]|*} fragments + * @param {boolean} grouped If true the returned array will contain + * nested arrays for all fragments with the same index * @return {object[]} sorted Sorted array of fragments */ - function sortFragments( fragments ) { + function sortFragments( fragments, grouped ) { fragments = toArray( fragments ); @@ -4010,7 +4287,7 @@ index ++; } ); - return sorted; + return grouped === true ? ordered : sorted; } @@ -4091,6 +4368,9 @@ updateControls(); updateProgress(); + if( config.fragmentInURL ) { + writeURL(); + } return !!( fragmentsShown.length || fragmentsHidden.length ); @@ -4330,7 +4610,17 @@ // Prioritize revealing fragments if( nextFragment() === false ) { - if( availableRoutes().down ) { + + var routes = availableRoutes(); + + // When looping is enabled `routes.down` is always available + // so we need a separate check for when we've reached the + // end of a stack and should move horizontally + if( routes.down && routes.right && config.loop && Reveal.isLastVerticalSlide( currentSlide ) ) { + routes.down = false; + } + + if( routes.down ) { navigateDown(); } else if( config.rtl ) { @@ -4400,7 +4690,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; } @@ -4465,7 +4755,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 @@ -5059,7 +5373,10 @@ initialize: initialize, configure: configure, + sync: sync, + syncSlide: syncSlide, + syncFragments: syncFragments, // Navigation methods slide: slide, @@ -5196,7 +5513,7 @@ // Returns true if we're currently on the last slide isLastSlide: function() { if( currentSlide ) { - // Does this slide has next a sibling? + // Does this slide have a next sibling? if( currentSlide.nextElementSibling ) return false; // If it's vertical, does its parent have a next sibling? @@ -5208,6 +5525,19 @@ return false; }, + // Returns true if we're on the last slide in the current + // vertical stack + isLastVerticalSlide: function() { + if( currentSlide && isVerticalSlide( currentSlide ) ) { + // Does this slide have a next sibling? + if( currentSlide.nextElementSibling ) return false; + + return true; + } + + return false; + }, + // Checks if reveal.js has been loaded and is ready for use isReady: function() { return loaded; @@ -5225,6 +5555,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 } ); |