diff options
Diffstat (limited to 'js/reveal.js')
-rw-r--r-- | js/reveal.js | 1057 |
1 files changed, 732 insertions, 325 deletions
diff --git a/js/reveal.js b/js/reveal.js index e7debb5..06b217b 100644 --- a/js/reveal.js +++ b/js/reveal.js @@ -3,7 +3,7 @@ * http://lab.hakim.se/reveal-js * MIT licensed * - * Copyright (C) 2013 Hakim El Hattab, http://hakim.se + * Copyright (C) 2014 Hakim El Hattab, http://hakim.se */ var Reveal = (function(){ @@ -35,6 +35,9 @@ var Reveal = (function(){ // Display a presentation progress bar progress: true, + // Display the page number of the current slide + slideNumber: false, + // Push each slide change to the browser history history: false, @@ -109,6 +112,7 @@ var Reveal = (function(){ // Script dependencies to load dependencies: [] + }, // Flags if reveal.js is loaded (has dispatched the 'ready' event) @@ -122,6 +126,8 @@ var Reveal = (function(){ previousSlide, currentSlide, + previousBackground, + // 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. @@ -145,12 +151,6 @@ var Reveal = (function(){ // Delays updates to the URL due to a Chrome thumbnailer bug writeURLTimeout = 0, - // A delay used to activate the overview mode - activateOverviewTimeout = 0, - - // A delay used to deactivate the overview mode - deactivateOverviewTimeout = 0, - // Flags if the interaction event listeners are bound eventsAreBound = false, @@ -236,17 +236,42 @@ var Reveal = (function(){ } - /** - * 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 = [], - scriptsAsync = []; + scriptsAsync = [], + scriptsToPreload = 0; + + // Called once synchronous scripts finish loading + function proceed() { + if( scriptsAsync.length ) { + // Load asynchronous scripts + head.js.apply( null, scriptsAsync ); + } + + start(); + } + + function loadScript( s ) { + head.ready( s.src.match( /([\w\d_\-]*)\.?js$|[^\\\/]*$/i )[0], function() { + // Extension may contain callback functions + if( typeof s.callback === 'function' ) { + s.callback.apply( this ); + } + + if( --scriptsToPreload === 0 ) { + proceed(); + } + }); + } for( var i = 0, len = config.dependencies.length; i < len; i++ ) { var s = config.dependencies[i]; @@ -260,25 +285,12 @@ var Reveal = (function(){ scripts.push( s.src ); } - // Extension may contain callback functions - if( typeof s.callback === 'function' ) { - head.ready( s.src.match( /([\w\d_\-]*)\.?js$|[^\\\/]*$/i )[0], s.callback ); - } - } - } - - // Called once synchronous scripts finish loading - function proceed() { - if( scriptsAsync.length ) { - // Load asynchronous scripts - head.js.apply( null, scriptsAsync ); + loadScript( s ); } - - start(); } if( scripts.length ) { - head.ready( proceed ); + scriptsToPreload = scripts.length; // Load synchronous scripts head.js.apply( null, scripts ); @@ -298,8 +310,8 @@ var Reveal = (function(){ // Make sure we've got all the DOM elements we need setupDOM(); - // Decorate the slide DOM elements with state classes (past/future) - setupSlides(); + // Resets all vertical slides so that only the first is visible + resetVerticalSlides(); // Updates the presentation to match the current configuration values configure(); @@ -307,6 +319,9 @@ var Reveal = (function(){ // Read the initial hash readURL(); + // Update all backgrounds + updateBackground( true ); + // Notify listeners that the presentation is ready but use a 1ms // timeout to ensure it's not fired synchronously after #initialize() setTimeout( function() { @@ -325,26 +340,6 @@ var Reveal = (function(){ } /** - * Iterates through and decorates slides DOM elements with - * appropriate classes. - */ - function setupSlides() { - - var horizontalSlides = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ); - horizontalSlides.forEach( function( horizontalSlide ) { - - var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) ); - verticalSlides.forEach( function( verticalSlide, y ) { - - if( y > 0 ) verticalSlide.classList.add( 'future' ); - - } ); - - } ); - - } - - /** * Finds and stores references to DOM elements which are * required by the presentation. If a required element is * not found, it is created. @@ -373,6 +368,9 @@ var Reveal = (function(){ '<div class="navigate-up"></div>' + '<div class="navigate-down"></div>' ); + // Slide number + dom.slideNumber = createSingletonNode( dom.wrapper, 'div', 'slide-number', '' ); + // State background element [DEPRECATED] createSingletonNode( dom.wrapper, 'div', 'state-background', null ); @@ -427,67 +425,26 @@ var Reveal = (function(){ dom.background.innerHTML = ''; dom.background.classList.add( 'no-transition' ); - // Helper method for creating a background element for the - // given slide - function _createBackground( slide, container ) { - - var data = { - background: slide.getAttribute( 'data-background' ), - backgroundSize: slide.getAttribute( 'data-background-size' ), - backgroundImage: slide.getAttribute( 'data-background-image' ), - backgroundColor: slide.getAttribute( 'data-background-color' ), - backgroundRepeat: slide.getAttribute( 'data-background-repeat' ), - backgroundPosition: slide.getAttribute( 'data-background-position' ), - backgroundTransition: slide.getAttribute( 'data-background-transition' ) - }; - - var element = document.createElement( 'div' ); - element.className = 'slide-background'; - - 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 ) ) { - element.style.backgroundImage = 'url('+ data.background +')'; - } - else { - element.style.background = data.background; - } - } - - // Additional and optional background properties - if( data.backgroundSize ) element.style.backgroundSize = data.backgroundSize; - if( data.backgroundImage ) element.style.backgroundImage = 'url("' + data.backgroundImage + '")'; - 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 ); - - return element; - - } - // Iterate over all horizontal slides toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( slideh ) { var backgroundStack; if( isPrintingPDF() ) { - backgroundStack = _createBackground( slideh, slideh ); + backgroundStack = createBackground( slideh, slideh ); } else { - backgroundStack = _createBackground( slideh, dom.background ); + backgroundStack = createBackground( slideh, dom.background ); } // Iterate over all vertical slides toArray( slideh.querySelectorAll( 'section' ) ).forEach( function( slidev ) { if( isPrintingPDF() ) { - _createBackground( slidev, slidev ); + createBackground( slidev, slidev ); } else { - _createBackground( slidev, backgroundStack ); + createBackground( slidev, backgroundStack ); } } ); @@ -519,6 +476,79 @@ var Reveal = (function(){ } /** + * Creates a background for the given slide. + * + * @param {HTMLElement} slide + * @param {HTMLElement} container The element that the background + * should be appended to + */ + function createBackground( slide, container ) { + + var data = { + background: slide.getAttribute( 'data-background' ), + backgroundSize: slide.getAttribute( 'data-background-size' ), + backgroundImage: slide.getAttribute( 'data-background-image' ), + backgroundVideo: slide.getAttribute( 'data-background-video' ), + backgroundColor: slide.getAttribute( 'data-background-color' ), + backgroundRepeat: slide.getAttribute( 'data-background-repeat' ), + backgroundPosition: slide.getAttribute( 'data-background-position' ), + backgroundTransition: slide.getAttribute( 'data-background-transition' ) + }; + + var element = document.createElement( 'div' ); + element.className = 'slide-background'; + + 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 ) ) { + element.style.backgroundImage = 'url('+ data.background +')'; + } + else { + element.style.background = data.background; + } + } + + // Create a hash for this combination of background settings. + // This is used to determine when two slide backgrounds are + // the same. + if( data.background || data.backgroundColor || data.backgroundImage || data.backgroundVideo ) { + element.setAttribute( 'data-background-hash', data.background + + data.backgroundSize + + data.backgroundImage + + data.backgroundVideo + + data.backgroundColor + + data.backgroundRepeat + + data.backgroundPosition + + data.backgroundTransition ); + } + + // Additional and optional background properties + if( data.backgroundSize ) element.style.backgroundSize = data.backgroundSize; + if( data.backgroundImage ) element.style.backgroundImage = 'url("' + data.backgroundImage + '")'; + 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 ); + + // Create video background element + if( data.backgroundVideo ) { + var video = document.createElement( 'video' ); + + // Support comma separated lists of video sources + data.backgroundVideo.split( ',' ).forEach( function( source ) { + video.innerHTML += '<source src="'+ source +'">'; + } ); + + element.appendChild( video ); + } + + container.appendChild( element ); + + return element; + + } + + /** * Applies the configuration settings from the config * object. May be called multiple times. */ @@ -583,7 +613,13 @@ var Reveal = (function(){ enablePreviewLinks( '[data-preview-link]' ); } - // Auto-slide playback controls + // Remove existing auto-slide controls + if( autoSlidePlayer ) { + autoSlidePlayer.destroy(); + autoSlidePlayer = null; + } + + // Generate auto-slide controls if needed if( numberOfSlides > 1 && config.autoSlide && config.autoSlideStoppable && features.canvas && features.requestAnimationFrame ) { autoSlidePlayer = new Playback( dom.wrapper, function() { return Math.min( Math.max( ( Date.now() - autoSlideStartTime ) / autoSlide, 0 ), 1 ); @@ -592,9 +628,13 @@ var Reveal = (function(){ autoSlidePlayer.on( 'click', onAutoSlidePlayerClick ); autoSlidePaused = false; } - else if( autoSlidePlayer ) { - autoSlidePlayer.destroy(); - autoSlidePlayer = null; + + // When fragments are turned off they should be visible + if( config.fragments === false ) { + toArray( dom.slides.querySelectorAll( '.fragment' ) ).forEach( function( element ) { + element.classList.add( 'visible' ); + element.classList.remove( 'current-fragment' ); + } ); } // Load the theme in the config, if it's not already loaded @@ -629,7 +669,14 @@ var Reveal = (function(){ dom.wrapper.addEventListener( 'touchend', onTouchEnd, false ); // Support pointer-style touch interaction as well - if( window.navigator.msPointerEnabled ) { + if( window.navigator.pointerEnabled ) { + // IE 11 uses un-prefixed version of pointer events + dom.wrapper.addEventListener( 'pointerdown', onPointerDown, false ); + dom.wrapper.addEventListener( 'pointermove', onPointerMove, false ); + dom.wrapper.addEventListener( 'pointerup', onPointerUp, false ); + } + else if( window.navigator.msPointerEnabled ) { + // IE 10 uses prefixed version of pointer events dom.wrapper.addEventListener( 'MSPointerDown', onPointerDown, false ); dom.wrapper.addEventListener( 'MSPointerMove', onPointerMove, false ); dom.wrapper.addEventListener( 'MSPointerUp', onPointerUp, false ); @@ -688,7 +735,14 @@ var Reveal = (function(){ dom.wrapper.removeEventListener( 'touchmove', onTouchMove, false ); dom.wrapper.removeEventListener( 'touchend', onTouchEnd, false ); - if( window.navigator.msPointerEnabled ) { + // 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 ); @@ -731,6 +785,22 @@ var Reveal = (function(){ } /** + * Utility for deserializing a value. + */ + function deserialize( value ) { + + if( typeof value === 'string' ) { + 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 ); + } + + return value; + + } + + /** * Measures the distance in pixels between point a * and point b. * @@ -796,40 +866,26 @@ var Reveal = (function(){ /** * Returns the remaining height within the parent of the - * target element after subtracting the height of all - * siblings. + * target element. * - * remaining height = [parent height] - [ siblings height] + * remaining height = [ configured parent height ] - [ current parent height ] */ function getRemainingHeight( element, height ) { height = height || 0; if( element ) { - var parent = element.parentNode; - var siblings = parent.childNodes; - - // Subtract the height of each sibling - toArray( siblings ).forEach( function( sibling ) { + var newHeight, oldHeight = element.style.height; - if( typeof sibling.offsetHeight === 'number' && sibling !== element ) { + // Change the .stretch element height to 0 in order find the height of all + // the other elements + element.style.height = '0px'; + newHeight = height - element.parentNode.offsetHeight; - var styles = window.getComputedStyle( sibling ), - marginTop = parseInt( styles.marginTop, 10 ), - marginBottom = parseInt( styles.marginBottom, 10 ); - - height -= sibling.offsetHeight + marginTop + marginBottom; - - } - - } ); - - var elementStyles = window.getComputedStyle( element ); - - // Subtract the margins of the target element - height -= parseInt( elementStyles.marginTop, 10 ) + - parseInt( elementStyles.marginBottom, 10 ); + // Restore the old height, just in case + element.style.height = oldHeight + 'px'; + return newHeight; } return height; @@ -889,7 +945,7 @@ var Reveal = (function(){ function enableRollingLinks() { if( features.transforms3d && !( 'msPerspective' in document.body.style ) ) { - var anchors = document.querySelectorAll( SLIDES_SELECTOR + ' a:not(.image)' ); + var anchors = document.querySelectorAll( SLIDES_SELECTOR + ' a' ); for( var i = 0, len = anchors.length; i < len; i++ ) { var anchor = anchors[i]; @@ -1012,38 +1068,6 @@ var Reveal = (function(){ } /** - * Return a sorted fragments list, ordered by an increasing - * "data-fragment-index" attribute. - * - * Fragments will be revealed in the order that they are returned by - * this function, so you can use the index attributes to control the - * order of fragment appearance. - * - * To maintain a sensible default fragment order, fragments are presumed - * to be passed in document order. This function adds a "fragment-index" - * attribute to each node if such an attribute is not already present, - * and sets that attribute to an integer value which is the position of - * the fragment within the fragments list. - */ - function sortFragments( fragments ) { - - var a = toArray( fragments ); - - a.forEach( function( el, idx ) { - if( !el.hasAttribute( 'data-fragment-index' ) ) { - el.setAttribute( 'data-fragment-index', idx ); - } - } ); - - a.sort( function( l, r ) { - return l.getAttribute( 'data-fragment-index' ) - r.getAttribute( 'data-fragment-index'); - } ); - - return a; - - } - - /** * Applies JavaScript-controlled layout rules to the * presentation. */ @@ -1087,14 +1111,17 @@ var Reveal = (function(){ scale = Math.max( scale, config.minScale ); scale = Math.min( scale, config.maxScale ); - // Prefer applying scale via zoom since Chrome blurs scaled content - // with nested transforms - if( typeof dom.slides.style.zoom !== 'undefined' && !navigator.userAgent.match( /(iphone|ipod|ipad|android)/gi ) ) { + // Prefer zooming in WebKit so that content remains crisp + if( /webkit/i.test( navigator.userAgent ) && typeof dom.slides.style.zoom !== 'undefined' ) { dom.slides.style.zoom = scale; } // Apply scale transform as a fallback else { - transformElement( dom.slides, 'translate(-50%, -50%) scale('+ scale +') translate(50%, 50%)' ); + dom.slides.style.left = '50%'; + dom.slides.style.top = '50%'; + dom.slides.style.bottom = 'auto'; + dom.slides.style.right = 'auto'; + transformElement( dom.slides, 'translate(-50%, -50%) scale('+ scale +')' ); } // Select all slides, vertical and horizontal @@ -1115,7 +1142,7 @@ var Reveal = (function(){ slide.style.top = 0; } else { - slide.style.top = Math.max( - ( getAbsoluteHeight( slide ) / 2 ) - slidePadding, -slideHeight / 2 ) + 'px'; + slide.style.top = Math.max( ( ( slideHeight - getAbsoluteHeight( slide ) ) / 2 ) - slidePadding, 0 ) + 'px'; } } else { @@ -1141,7 +1168,7 @@ var Reveal = (function(){ toArray( dom.slides.querySelectorAll( 'section > .stretch' ) ).forEach( function( element ) { // Determine how much vertical space we can use - var remainingHeight = getRemainingHeight( element, ( height - ( padding * 2 ) ) ); + var remainingHeight = getRemainingHeight( element, height ); // Consider the aspect ratio of media elements if( /(img|video)/gi.test( element.nodeName ) ) { @@ -1222,67 +1249,57 @@ var Reveal = (function(){ dom.wrapper.classList.add( 'overview' ); dom.wrapper.classList.remove( 'overview-deactivating' ); - clearTimeout( activateOverviewTimeout ); - clearTimeout( deactivateOverviewTimeout ); - - // Not the pretties solution, but need to let the overview - // class apply first so that slides are measured accurately - // before we can position them - activateOverviewTimeout = setTimeout( function() { - - var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ); + var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ); - for( var i = 0, len1 = horizontalSlides.length; i < len1; i++ ) { - var hslide = horizontalSlides[i], - hoffset = config.rtl ? -105 : 105; + for( var i = 0, len1 = horizontalSlides.length; i < len1; i++ ) { + var hslide = horizontalSlides[i], + hoffset = config.rtl ? -105 : 105; - hslide.setAttribute( 'data-index-h', i ); + hslide.setAttribute( 'data-index-h', i ); - // Apply CSS transform - transformElement( hslide, 'translateZ(-'+ depth +'px) translate(' + ( ( i - indexh ) * hoffset ) + '%, 0%)' ); + // Apply CSS transform + transformElement( hslide, 'translateZ(-'+ depth +'px) translate(' + ( ( i - indexh ) * hoffset ) + '%, 0%)' ); - if( hslide.classList.contains( 'stack' ) ) { + if( hslide.classList.contains( 'stack' ) ) { - var verticalSlides = hslide.querySelectorAll( 'section' ); + var verticalSlides = hslide.querySelectorAll( 'section' ); - for( var j = 0, len2 = verticalSlides.length; j < len2; j++ ) { - var verticalIndex = i === indexh ? indexv : getPreviousVerticalIndex( hslide ); + for( var j = 0, len2 = verticalSlides.length; j < len2; j++ ) { + var verticalIndex = i === indexh ? indexv : getPreviousVerticalIndex( hslide ); - var vslide = verticalSlides[j]; + var vslide = verticalSlides[j]; - vslide.setAttribute( 'data-index-h', i ); - vslide.setAttribute( 'data-index-v', j ); + vslide.setAttribute( 'data-index-h', i ); + vslide.setAttribute( 'data-index-v', j ); - // Apply CSS transform - transformElement( vslide, 'translate(0%, ' + ( ( j - verticalIndex ) * 105 ) + '%)' ); - - // Navigate to this slide on click - vslide.addEventListener( 'click', onOverviewSlideClicked, true ); - } - - } - else { + // Apply CSS transform + transformElement( vslide, 'translate(0%, ' + ( ( j - verticalIndex ) * 105 ) + '%)' ); // Navigate to this slide on click - hslide.addEventListener( 'click', onOverviewSlideClicked, true ); - + vslide.addEventListener( 'click', onOverviewSlideClicked, true ); } - } - updateSlidesVisibility(); + } + else { - layout(); + // Navigate to this slide on click + hslide.addEventListener( 'click', onOverviewSlideClicked, true ); - if( !wasActive ) { - // Notify observers of the overview showing - dispatchEvent( 'overviewshown', { - 'indexh': indexh, - 'indexv': indexv, - 'currentSlide': currentSlide - } ); } + } + + updateSlidesVisibility(); - }, 10 ); + layout(); + + if( !wasActive ) { + // Notify observers of the overview showing + dispatchEvent( 'overviewshown', { + 'indexh': indexh, + 'indexv': indexv, + 'currentSlide': currentSlide + } ); + } } @@ -1297,9 +1314,6 @@ var Reveal = (function(){ // Only proceed if enabled in config if( config.overview ) { - clearTimeout( activateOverviewTimeout ); - clearTimeout( deactivateOverviewTimeout ); - dom.wrapper.classList.remove( 'overview' ); // Temporarily add a class so that transitions can do different things @@ -1307,7 +1321,7 @@ var Reveal = (function(){ // moving from slide to slide dom.wrapper.classList.add( 'overview-deactivating' ); - deactivateOverviewTimeout = setTimeout( function () { + setTimeout( function () { dom.wrapper.classList.remove( 'overview-deactivating' ); }, 1 ); @@ -1394,7 +1408,7 @@ var Reveal = (function(){ element.webkitRequestFullscreen || element.webkitRequestFullScreen || element.mozRequestFullScreen || - element.msRequestFullScreen; + element.msRequestFullscreen; if( requestMethod ) { requestMethod.apply( element ); @@ -1438,13 +1452,13 @@ var Reveal = (function(){ /** * Toggles the paused mode on and off. */ - function togglePause() { + function togglePause( override ) { - if( isPaused() ) { - resume(); + if( typeof override === 'boolean' ) { + override ? pause() : resume(); } else { - pause(); + isPaused() ? resume() : pause(); } } @@ -1459,6 +1473,34 @@ var Reveal = (function(){ } /** + * Toggles the auto slide mode on and off. + * + * @param {Boolean} override Optional flag which sets the desired state. + * True means autoplay starts, false means it stops. + */ + + function toggleAutoSlide( override ) { + + if( typeof override === 'boolean' ) { + override ? resumeAutoSlide() : pauseAutoSlide(); + } + + else { + autoSlidePaused ? resumeAutoSlide() : pauseAutoSlide(); + } + + } + + /** + * Checks if the auto slide mode is currently on. + */ + function isAutoSliding() { + + return !!( autoSlide && !autoSlidePaused ); + + } + + /** * Steps from the current point in the presentation to the * slide which matches the specified horizontal and vertical * indices. @@ -1544,16 +1586,7 @@ var Reveal = (function(){ // Show fragment, if specified if( typeof f !== 'undefined' ) { - var fragments = sortFragments( currentSlide.querySelectorAll( '.fragment' ) ); - - toArray( fragments ).forEach( function( fragment, indexf ) { - if( indexf < f ) { - fragment.classList.add( 'visible' ); - } - else { - fragment.classList.remove( 'visible' ); - } - } ); + navigateFragment( f ); } // Dispatch an event if the slide changed @@ -1604,6 +1637,7 @@ var Reveal = (function(){ updateProgress(); updateBackground(); updateParallax(); + updateSlideNumber(); // Update the URL hash writeURL(); @@ -1635,9 +1669,58 @@ var Reveal = (function(){ // Re-create the slide backgrounds createBackgrounds(); + sortAllFragments(); + updateControls(); updateProgress(); - updateBackground(); + updateBackground( true ); + updateSlideNumber(); + + } + + /** + * Resets all vertical slides so that only the first + * is visible. + */ + function resetVerticalSlides() { + + var horizontalSlides = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ); + horizontalSlides.forEach( function( horizontalSlide ) { + + var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) ); + verticalSlides.forEach( function( verticalSlide, y ) { + + if( y > 0 ) { + verticalSlide.classList.remove( 'present' ); + verticalSlide.classList.remove( 'past' ); + verticalSlide.classList.add( 'future' ); + } + + } ); + + } ); + + } + + /** + * Sorts and formats all of fragments in the + * presentation. + */ + function sortAllFragments() { + + var horizontalSlides = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ); + horizontalSlides.forEach( function( horizontalSlide ) { + + var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) ); + verticalSlides.forEach( function( verticalSlide, y ) { + + sortFragments( verticalSlide.querySelectorAll( '.fragment' ) ); + + } ); + + if( verticalSlides.length === 0 ) sortFragments( horizontalSlide.querySelectorAll( '.fragment' ) ); + + } ); } @@ -1690,16 +1773,31 @@ var Reveal = (function(){ if( i < index ) { // Any element previous to index is given the 'past' class element.classList.add( reverse ? 'future' : 'past' ); + + if( config.fragments ) { + var pastFragments = toArray( element.querySelectorAll( '.fragment' ) ); + + // Show all fragments on prior slides + while( pastFragments.length ) { + var pastFragment = pastFragments.pop(); + pastFragment.classList.add( 'visible' ); + pastFragment.classList.remove( 'current-fragment' ); + } + } } else if( i > index ) { // Any element subsequent to index is given the 'future' class element.classList.add( reverse ? 'past' : 'future' ); - var fragments = toArray( element.querySelectorAll( '.fragment.visible' ) ); + if( config.fragments ) { + var futureFragments = toArray( element.querySelectorAll( '.fragment.visible' ) ); - // No fragments in future slides should be visible ahead of time - while( fragments.length ) { - fragments.pop().classList.remove( 'visible' ); + // No fragments in future slides should be visible ahead of time + while( futureFragments.length ) { + var futureFragment = futureFragments.pop(); + futureFragment.classList.remove( 'visible' ); + futureFragment.classList.remove( 'current-fragment' ); + } } } @@ -1794,43 +1892,27 @@ var Reveal = (function(){ // Update progress if enabled if( config.progress && dom.progress ) { - var horizontalSlides = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ); - - // The number of past and total slides - var totalCount = document.querySelectorAll( SLIDES_SELECTOR + ':not(.stack)' ).length; - var pastCount = 0; - - // Step through all slides and count the past ones - mainLoop: for( var i = 0; i < horizontalSlides.length; i++ ) { + dom.progressbar.style.width = getProgress() * window.innerWidth + 'px'; - var horizontalSlide = horizontalSlides[i]; - var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) ); - - for( var j = 0; j < verticalSlides.length; j++ ) { - - // Stop as soon as we arrive at the present - if( verticalSlides[j].classList.contains( 'present' ) ) { - break mainLoop; - } - - pastCount++; + } - } + } - // Stop as soon as we arrive at the present - if( horizontalSlide.classList.contains( 'present' ) ) { - break; - } + /** + * Updates the slide number div to reflect the current slide. + */ + function updateSlideNumber() { - // Don't count the wrapping section for vertical slides - if( horizontalSlide.classList.contains( 'stack' ) === false ) { - pastCount++; - } + // Update slide number if enabled + if( config.slideNumber && dom.slideNumber) { + // Display the number of the page using 'indexh - indexv' format + var indexString = indexh; + if( indexv > 0 ) { + indexString += ' - ' + indexv; } - dom.progressbar.style.width = ( pastCount / ( totalCount - 1 ) ) * window.innerWidth + 'px'; - + dom.slideNumber.innerHTML = indexString; } } @@ -1888,27 +1970,82 @@ var Reveal = (function(){ /** * Updates the background elements to reflect the current * slide. + * + * @param {Boolean} includeAll If true, the backgrounds of + * all vertical slides (not just the present) will be updated. */ - function updateBackground() { + function updateBackground( includeAll ) { + + var currentBackground = null; + + // Reverse past/future classes when in RTL mode + var horizontalPast = config.rtl ? 'future' : 'past', + horizontalFuture = config.rtl ? 'past' : 'future'; // Update the classes of all backgrounds to match the // states of their slides (past/present/future) toArray( dom.background.childNodes ).forEach( function( backgroundh, h ) { - // Reverse past/future classes when in RTL mode - var horizontalPast = config.rtl ? 'future' : 'past', - horizontalFuture = config.rtl ? 'past' : 'future'; + if( h < indexh ) { + backgroundh.className = 'slide-background ' + horizontalPast; + } + else if ( h > indexh ) { + backgroundh.className = 'slide-background ' + horizontalFuture; + } + else { + backgroundh.className = 'slide-background present'; - backgroundh.className = 'slide-background ' + ( h < indexh ? horizontalPast : h > indexh ? horizontalFuture : 'present' ); + // Store a reference to the current background element + currentBackground = backgroundh; + } - toArray( backgroundh.childNodes ).forEach( function( backgroundv, v ) { + if( includeAll || h === indexh ) { + toArray( backgroundh.querySelectorAll( 'section' ) ).forEach( function( backgroundv, v ) { + + if( v < indexv ) { + backgroundv.className = 'slide-background past'; + } + else if ( v > indexv ) { + backgroundv.className = 'slide-background future'; + } + else { + backgroundv.className = 'slide-background present'; - backgroundv.className = 'slide-background ' + ( v < indexv ? 'past' : v > indexv ? 'future' : 'present' ); + // Only if this is the present horizontal and vertical slide + if( h === indexh ) currentBackground = backgroundv; + } - } ); + } ); + } } ); + // Stop any currently playing video background + if( previousBackground ) { + + var previousVideo = previousBackground.querySelector( 'video' ); + if( previousVideo ) previousVideo.pause(); + + } + + if( currentBackground ) { + + // Start video playback + var currentVideo = currentBackground.querySelector( 'video' ); + if( currentVideo ) currentVideo.play(); + + // Don't transition between identical backgrounds. This + // prevents unwanted flicker. + var previousBackgroundHash = previousBackground ? previousBackground.getAttribute( 'data-background-hash' ) : null; + var currentBackgroundHash = currentBackground.getAttribute( 'data-background-hash' ); + if( currentBackgroundHash && currentBackgroundHash === previousBackgroundHash && currentBackground !== previousBackground ) { + dom.background.classList.add( 'no-transition' ); + } + + previousBackground = currentBackground; + + } + // Allow the first background to apply without transition setTimeout( function() { dom.background.classList.remove( 'no-transition' ); @@ -1944,7 +2081,7 @@ var Reveal = (function(){ var slideHeight = dom.background.offsetHeight; var verticalSlideCount = verticalSlides.length; - var verticalOffset = verticalSlideCount > 0 ? -( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 ) * indexv : 0; + var verticalOffset = verticalSlideCount > 1 ? -( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 ) * indexv : 0; dom.background.style.backgroundPosition = horizontalOffset + 'px ' + verticalOffset + 'px'; @@ -2062,6 +2199,70 @@ var Reveal = (function(){ } /** + * Returns a value ranging from 0-1 that represents + * how far into the presentation we have navigated. + */ + function getProgress() { + + var horizontalSlides = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ); + + // The number of past and total slides + var totalCount = document.querySelectorAll( SLIDES_SELECTOR + ':not(.stack)' ).length; + var pastCount = 0; + + // Step through all slides and count the past ones + mainLoop: for( var i = 0; i < horizontalSlides.length; i++ ) { + + var horizontalSlide = horizontalSlides[i]; + var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) ); + + for( var j = 0; j < verticalSlides.length; j++ ) { + + // Stop as soon as we arrive at the present + if( verticalSlides[j].classList.contains( 'present' ) ) { + break mainLoop; + } + + pastCount++; + + } + + // Stop as soon as we arrive at the present + if( horizontalSlide.classList.contains( 'present' ) ) { + break; + } + + // Don't count the wrapping section for vertical slides + if( horizontalSlide.classList.contains( 'stack' ) === false ) { + pastCount++; + } + + } + + if( currentSlide ) { + + var allFragments = currentSlide.querySelectorAll( '.fragment' ); + + // If there are fragments in the current slide those should be + // accounted for in the progress. + if( allFragments.length > 0 ) { + var visibleFragments = currentSlide.querySelectorAll( '.fragment.visible' ); + + // This value represents how big a portion of the slide progress + // that is made up by its fragments (0-1) + var fragmentWeight = 0.9; + + // Add fragment progress to the past slide count + pastCount += ( visibleFragments.length / allFragments.length ) * fragmentWeight; + } + + } + + return pastCount / ( totalCount - 1 ); + + } + + /** * Checks if this presentation is running inside of the * speaker notes window. */ @@ -2085,8 +2286,16 @@ var Reveal = (function(){ // If the first bit is invalid and there is a name we can // assume that this is a named link if( isNaN( parseInt( bits[0], 10 ) ) && name.length ) { - // Find the slide with the specified name - var element = document.querySelector( '#' + name ); + var element; + + try { + // Find the slide with the specified name + element = document.querySelector( '#' + name ); + } + catch( e ) { + // If the ID is an invalid selector a harmless SyntaxError + // may be thrown here. + } if( element ) { // Find the position of the named slide and navigate to it @@ -2131,9 +2340,16 @@ var Reveal = (function(){ else { var url = '/'; + // Attempt to create a named link based on the slide's ID + var id = currentSlide.getAttribute( 'id' ); + if( id ) { + id = id.toLowerCase(); + id = id.replace( /[^a-zA-Z0-9\-\_\:\.]/g, '' ); + } + // If the current slide has an ID, use that as a named link - if( currentSlide && typeof currentSlide.getAttribute( 'id' ) === 'string' ) { - url = '/' + currentSlide.getAttribute( 'id' ); + if( currentSlide && typeof id === 'string' && id.length ) { + url = '/' + id; } // Otherwise use the /h/v index else { @@ -2185,7 +2401,7 @@ var Reveal = (function(){ var hasFragments = currentSlide.querySelectorAll( '.fragment' ).length > 0; if( hasFragments ) { var visibleFragments = currentSlide.querySelectorAll( '.fragment.visible' ); - f = visibleFragments.length; + f = visibleFragments.length - 1; } } @@ -2194,67 +2410,188 @@ var Reveal = (function(){ } /** - * Navigate to the next slide fragment. + * Retrieves the total number of slides in this presentation. + */ + function getTotalSlides() { + + return document.querySelectorAll( SLIDES_SELECTOR + ':not(.stack)' ).length; + + } + + /** + * Retrieves the current state of the presentation as + * an object. This state can then be restored at any + * time. + */ + function getState() { + + var indices = getIndices(); + + return { + indexh: indices.h, + indexv: indices.v, + indexf: indices.f, + paused: isPaused(), + overview: isOverview() + }; + + } + + /** + * Restores the presentation to the given state. * - * @return {Boolean} true if there was a next fragment, - * false otherwise + * @param {Object} state As generated by getState() */ - function nextFragment() { + function setState( state ) { - if( currentSlide && config.fragments ) { - var fragments = sortFragments( currentSlide.querySelectorAll( '.fragment:not(.visible)' ) ); + if( typeof state === 'object' ) { + slide( deserialize( state.indexh ), deserialize( state.indexv ), deserialize( state.indexf ) ); + togglePause( deserialize( state.paused ) ); + toggleOverview( deserialize( state.overview ) ); + } - if( fragments.length ) { - // Find the index of the next fragment - var index = fragments[0].getAttribute( 'data-fragment-index' ); + } - // Find all fragments with the same index - fragments = currentSlide.querySelectorAll( '.fragment[data-fragment-index="'+ index +'"]' ); + /** + * Return a sorted fragments list, ordered by an increasing + * "data-fragment-index" attribute. + * + * Fragments will be revealed in the order that they are returned by + * this function, so you can use the index attributes to control the + * order of fragment appearance. + * + * To maintain a sensible default fragment order, fragments are presumed + * to be passed in document order. This function adds a "fragment-index" + * attribute to each node if such an attribute is not already present, + * and sets that attribute to an integer value which is the position of + * the fragment within the fragments list. + */ + function sortFragments( fragments ) { - toArray( fragments ).forEach( function( element ) { - element.classList.add( 'visible' ); - } ); + fragments = toArray( fragments ); - // Notify subscribers of the change - dispatchEvent( 'fragmentshown', { fragment: fragments[0], fragments: fragments } ); + var ordered = [], + unordered = [], + sorted = []; - updateControls(); - return true; + // Group ordered and unordered elements + fragments.forEach( function( fragment, i ) { + if( fragment.hasAttribute( 'data-fragment-index' ) ) { + var index = parseInt( fragment.getAttribute( 'data-fragment-index' ), 10 ); + + if( !ordered[index] ) { + ordered[index] = []; + } + + ordered[index].push( fragment ); } - } + else { + unordered.push( [ fragment ] ); + } + } ); - return false; + // Append fragments without explicit indices in their + // DOM order + ordered = ordered.concat( unordered ); + + // Manually count the index up per group to ensure there + // are no gaps + var index = 0; + + // Push all fragments in their sorted order to an array, + // this flattens the groups + ordered.forEach( function( group ) { + group.forEach( function( fragment ) { + sorted.push( fragment ); + fragment.setAttribute( 'data-fragment-index', index ); + } ); + + index ++; + } ); + + return sorted; } /** - * Navigate to the previous slide fragment. + * Navigate to the specified slide fragment. * - * @return {Boolean} true if there was a previous fragment, - * false otherwise + * @param {Number} index The index of the fragment that + * should be shown, -1 means all are invisible + * @param {Number} offset Integer offset to apply to the + * fragment index + * + * @return {Boolean} true if a change was made in any + * fragments visibility as part of this call */ - function previousFragment() { + function navigateFragment( index, offset ) { if( currentSlide && config.fragments ) { - var fragments = sortFragments( currentSlide.querySelectorAll( '.fragment.visible' ) ); + var fragments = sortFragments( currentSlide.querySelectorAll( '.fragment' ) ); if( fragments.length ) { - // Find the index of the previous fragment - var index = fragments[ fragments.length - 1 ].getAttribute( 'data-fragment-index' ); - // Find all fragments with the same index - fragments = currentSlide.querySelectorAll( '.fragment[data-fragment-index="'+ index +'"]' ); + // If no index is specified, find the current + if( typeof index !== 'number' ) { + var lastVisibleFragment = sortFragments( currentSlide.querySelectorAll( '.fragment.visible' ) ).pop(); + + if( lastVisibleFragment ) { + index = parseInt( lastVisibleFragment.getAttribute( 'data-fragment-index' ) || 0, 10 ); + } + else { + index = -1; + } + } + + // If an offset is specified, apply it to the index + if( typeof offset === 'number' ) { + index += offset; + } + + var fragmentsShown = [], + fragmentsHidden = []; + + toArray( fragments ).forEach( function( element, i ) { + + if( element.hasAttribute( 'data-fragment-index' ) ) { + i = parseInt( element.getAttribute( 'data-fragment-index' ), 10 ); + } + + // Visible fragments + if( i <= index ) { + if( !element.classList.contains( 'visible' ) ) fragmentsShown.push( element ); + element.classList.add( 'visible' ); + element.classList.remove( 'current-fragment' ); + + if( i === index ) { + element.classList.add( 'current-fragment' ); + } + } + // Hidden fragments + else { + if( element.classList.contains( 'visible' ) ) fragmentsHidden.push( element ); + element.classList.remove( 'visible' ); + element.classList.remove( 'current-fragment' ); + } + - toArray( fragments ).forEach( function( f ) { - f.classList.remove( 'visible' ); } ); - // Notify subscribers of the change - dispatchEvent( 'fragmenthidden', { fragment: fragments[0], fragments: fragments } ); + if( fragmentsHidden.length ) { + dispatchEvent( 'fragmenthidden', { fragment: fragmentsHidden[0], fragments: fragmentsHidden } ); + } + + if( fragmentsShown.length ) { + dispatchEvent( 'fragmentshown', { fragment: fragmentsShown[0], fragments: fragmentsShown } ); + } updateControls(); - return true; + updateProgress(); + + return !!( fragmentsShown.length || fragmentsHidden.length ); + } + } return false; @@ -2262,6 +2599,30 @@ var Reveal = (function(){ } /** + * Navigate to the next slide fragment. + * + * @return {Boolean} true if there was a next fragment, + * false otherwise + */ + function nextFragment() { + + return navigateFragment( null, 1 ); + + } + + /** + * Navigate to the previous slide fragment. + * + * @return {Boolean} true if there was a previous fragment, + * false otherwise + */ + function previousFragment() { + + return navigateFragment( null, -1 ); + + } + + /** * Cues a new automated slide if enabled in the config. */ function cueAutoSlide() { @@ -2270,16 +2631,41 @@ var Reveal = (function(){ if( currentSlide ) { - // If the current slide has a data-autoslide use that, - // otherwise use the config.autoSlide value + var currentFragment = currentSlide.querySelector( '.current-fragment' ); + + var fragmentAutoSlide = currentFragment ? currentFragment.getAttribute( 'data-autoslide' ) : null; + var parentAutoSlide = currentSlide.parentNode ? currentSlide.parentNode.getAttribute( 'data-autoslide' ) : null; var slideAutoSlide = currentSlide.getAttribute( 'data-autoslide' ); - if( slideAutoSlide ) { + + // Pick value in the following priority order: + // 1. Current fragment's data-autoslide + // 2. Current slide's data-autoslide + // 3. Parent slide's data-autoslide + // 4. Global autoSlide setting + if( fragmentAutoSlide ) { + autoSlide = parseInt( fragmentAutoSlide, 10 ); + } + else if( slideAutoSlide ) { autoSlide = parseInt( slideAutoSlide, 10 ); } + else if( parentAutoSlide ) { + autoSlide = parseInt( parentAutoSlide, 10 ); + } else { autoSlide = config.autoSlide; } + // If there are media elements with data-autoplay, + // automatically set the autoSlide duration to the + // length of that media + toArray( currentSlide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) { + if( el.hasAttribute( 'data-autoplay' ) ) { + if( autoSlide && el.duration * 1000 > autoSlide ) { + autoSlide = ( el.duration * 1000 ) + 1000; + } + } + } ); + // Cue the next auto-slide if: // - There is an autoSlide value // - Auto-sliding isn't paused by the user @@ -2312,6 +2698,7 @@ var Reveal = (function(){ function pauseAutoSlide() { autoSlidePaused = true; + dispatchEvent( 'autoslidepaused' ); clearTimeout( autoSlideTimeout ); if( autoSlidePlayer ) { @@ -2323,6 +2710,7 @@ var Reveal = (function(){ function resumeAutoSlide() { autoSlidePaused = false; + dispatchEvent( 'autoslideresumed' ); cueAutoSlide(); } @@ -2440,6 +2828,9 @@ var Reveal = (function(){ */ function onDocumentKeyDown( event ) { + // Remember if auto-sliding was paused so we can toggle it + var autoSlideWasPaused = autoSlidePaused; + onUserInput( event ); // Check if there's a focused element that could be using @@ -2512,10 +2903,12 @@ var Reveal = (function(){ case 32: isOverview() ? deactivateOverview() : event.shiftKey ? navigatePrev() : navigateNext(); break; // return case 13: isOverview() ? deactivateOverview() : triggered = false; break; - // b, period, Logitech presenter tools "black screen" button - case 66: case 190: case 191: togglePause(); break; + // two-spot, semicolon, b, period, Logitech presenter tools "black screen" button + case 58: case 59: case 66: case 190: case 191: togglePause(); break; // f case 70: enterFullscreen(); break; + // a + case 65: if ( config.autoSlideStoppable ) toggleAutoSlide( autoSlideWasPaused ); break; default: triggered = false; } @@ -2670,7 +3063,7 @@ var Reveal = (function(){ */ function onPointerDown( event ) { - if( event.pointerType === event.MSPOINTER_TYPE_TOUCH ) { + if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) { event.touches = [{ clientX: event.clientX, clientY: event.clientY }]; onTouchStart( event ); } @@ -2682,7 +3075,7 @@ var Reveal = (function(){ */ function onPointerMove( event ) { - if( event.pointerType === event.MSPOINTER_TYPE_TOUCH ) { + if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) { event.touches = [{ clientX: event.clientX, clientY: event.clientY }]; onTouchMove( event ); } @@ -2694,7 +3087,7 @@ var Reveal = (function(){ */ function onPointerUp( event ) { - if( event.pointerType === event.MSPOINTER_TYPE_TOUCH ) { + if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) { event.touches = [{ clientX: event.clientX, clientY: event.clientY }]; onTouchEnd( event ); } @@ -3038,6 +3431,9 @@ var Reveal = (function(){ down: navigateDown, prev: navigatePrev, next: navigateNext, + + // Fragment methods + navigateFragment: navigateFragment, prevFragment: previousFragment, nextFragment: nextFragment, @@ -3065,17 +3461,30 @@ var Reveal = (function(){ // Toggles the "black screen" mode on/off togglePause: togglePause, + // Toggles the auto slide mode on/off + toggleAutoSlide: toggleAutoSlide, + // State checks isOverview: isOverview, isPaused: isPaused, + isAutoSliding: isAutoSliding, // Adds or removes all internal event listeners (such as keyboard) addEventListeners: addEventListeners, removeEventListeners: removeEventListeners, + // Facility for persisting and restoring the presentation state + getState: getState, + setState: setState, + + // Presentation progress on range of 0-1 + getProgress: getProgress, + // Returns the indices of the current, or specified, slide getIndices: getIndices, + getTotalSlides: getTotalSlides, + // Returns the slide at the specified index, y is optional getSlide: function( x, y ) { var horizontalSlide = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR )[ x ]; @@ -3112,17 +3521,15 @@ var Reveal = (function(){ getQueryHash: function() { var query = {}; - location.search.replace( /[A-Z0-9]+?=(\w*)/gi, function(a) { + location.search.replace( /[A-Z0-9]+?=([\w\.%-]*)/gi, function(a) { query[ a.split( '=' ).shift() ] = a.split( '=' ).pop(); } ); // Basic deserialization for( var i in query ) { var value = query[ i ]; - if( value === 'null' ) query[ i ] = null; - else if( value === 'true' ) query[ i ] = true; - else if( value === 'false' ) query[ i ] = false; - else if( !isNaN( parseFloat( value ) ) ) query[ i ] = parseFloat( value ); + + query[ i ] = deserialize( unescape( value ) ); } return query; |