diff options
Diffstat (limited to 'js/reveal.js')
-rw-r--r-- | js/reveal.js | 1027 |
1 files changed, 823 insertions, 204 deletions
diff --git a/js/reveal.js b/js/reveal.js index 794911c..ef4add9 100644 --- a/js/reveal.js +++ b/js/reveal.js @@ -12,7 +12,7 @@ var Reveal = (function(){ var SLIDES_SELECTOR = '.reveal .slides section', HORIZONTAL_SLIDES_SELECTOR = '.reveal .slides>section', VERTICAL_SLIDES_SELECTOR = '.reveal .slides>section.present>section', - HOME_SLIDE_SELECTOR = '.reveal .slides>section:first-child', + HOME_SLIDE_SELECTOR = '.reveal .slides>section:first-of-type', // Configurations defaults, can be overridden at initialization time config = { @@ -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, @@ -44,7 +47,7 @@ var Reveal = (function(){ // Enable the slide overview mode overview: true, - // Vertical centring of slides + // Vertical centering of slides center: true, // Enables touch navigation on devices with touch input @@ -68,6 +71,9 @@ var Reveal = (function(){ // by using a data-autoslide attribute on your slides autoSlide: 0, + // Stop auto-sliding after user input + autoSlideStoppable: true, + // Enable slide navigation via mouse wheel mouseWheel: false, @@ -80,6 +86,9 @@ var Reveal = (function(){ // Opens links in an iframe preview overlay previewLinks: false, + // Focuses body when page changes visiblity to ensure keyboard shortcuts work + focusBodyOnPageVisiblityChange: true, + // Theme (see /css/theme) theme: null, @@ -92,19 +101,23 @@ var Reveal = (function(){ // Transition style for full page slide backgrounds backgroundTransition: 'default', // default/linear/none + // Parallax background image + parallaxBackgroundImage: '', // CSS syntax, e.g. "a.jpg" + + // Parallax background size + parallaxBackgroundSize: '', // CSS syntax, e.g. "3000px 2000px" + // Number of slides away from the current that are visible viewDistance: 3, // Script dependencies to load dependencies: [] + }, // Flags if reveal.js is loaded (has dispatched the 'ready' event) loaded = false, - // The current auto-slide duration - autoSlide = 0, - // The horizontal and vertical index of the currently active slide indexh, indexv, @@ -113,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. @@ -124,11 +139,8 @@ var Reveal = (function(){ // Cached references to DOM elements dom = {}, - // Client support for CSS 3D transforms, see #checkCapabilities() - supports3DTransforms, - - // Client support for CSS 2D transforms, see #checkCapabilities() - supports2DTransforms, + // Features supported by the browser, see #checkCapabilities() + features = {}, // Client is a mobile device, see #checkCapabilities() isMobileDevice, @@ -136,9 +148,6 @@ var Reveal = (function(){ // Throttles mouse wheel navigation lastMouseWheelStep = 0, - // An interval used to automatically move on to the next slide - autoSlideTimeout = 0, - // Delays updates to the URL due to a Chrome thumbnailer bug writeURLTimeout = 0, @@ -151,6 +160,15 @@ var Reveal = (function(){ // Flags if the interaction event listeners are bound eventsAreBound = false, + // The current auto-slide duration + autoSlide = 0, + + // Auto slide properties + autoSlidePlayer, + autoSlideTimeout = 0, + autoSlideStartTime = -1, + autoSlidePaused = false, + // Holds information about the currently ongoing touch input touch = { startX: 0, @@ -168,7 +186,7 @@ var Reveal = (function(){ checkCapabilities(); - if( !supports2DTransforms && !supports3DTransforms ) { + if( !features.transforms2d && !features.transforms3d ) { document.body.setAttribute( 'class', 'no-transforms' ); // If the browser doesn't support core features we won't be @@ -179,8 +197,15 @@ var Reveal = (function(){ // Force a layout when the whole page, incl fonts, has loaded window.addEventListener( 'load', layout, false ); + var query = Reveal.getQueryHash(); + + // Do not accept new dependencies via query config to avoid + // the potential of malicious script injection + if( typeof query['dependencies'] !== 'undefined' ) delete query['dependencies']; + // Copy options over to our config object extend( config, options ); + extend( config, query ); // Hide the address bar in mobile browsers hideAddressBar(); @@ -196,33 +221,63 @@ var Reveal = (function(){ */ function checkCapabilities() { - supports3DTransforms = 'WebkitPerspective' in document.body.style || + features.transforms3d = 'WebkitPerspective' in document.body.style || 'MozPerspective' in document.body.style || 'msPerspective' in document.body.style || 'OPerspective' in document.body.style || 'perspective' in document.body.style; - supports2DTransforms = 'WebkitTransform' in document.body.style || + features.transforms2d = 'WebkitTransform' in document.body.style || 'MozTransform' in document.body.style || 'msTransform' in document.body.style || 'OTransform' in document.body.style || 'transform' in document.body.style; + features.requestAnimationFrameMethod = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame; + features.requestAnimationFrame = typeof features.requestAnimationFrameMethod === 'function'; + + features.canvas = !!document.createElement( 'canvas' ).getContext; + isMobileDevice = navigator.userAgent.match( /(iphone|ipod|android)/gi ); } - /** - * 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]; @@ -236,25 +291,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 ); @@ -274,8 +316,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(); @@ -283,6 +325,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() { @@ -301,26 +346,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. @@ -349,6 +374,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 ); @@ -422,7 +450,7 @@ var Reveal = (function(){ if( data.background ) { // Auto-wrap image urls in url(...) - if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(png|jpg|jpeg|gif|bmp)$/gi.test( data.background ) ) { + if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp)$/gi.test( data.background ) ) { element.style.backgroundImage = 'url('+ data.background +')'; } else { @@ -430,6 +458,10 @@ var Reveal = (function(){ } } + if( data.background || data.backgroundColor || data.backgroundImage ) { + element.setAttribute( 'data-background-hash', data.background + data.backgroundSize + data.backgroundImage + 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 + '")'; @@ -470,6 +502,28 @@ var Reveal = (function(){ } ); + // Add parallax background if specified + if( config.parallaxBackgroundImage ) { + + dom.background.style.backgroundImage = 'url("' + config.parallaxBackgroundImage + '")'; + dom.background.style.backgroundSize = config.parallaxBackgroundSize; + + // 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 + // annoying background slide-in effect when the presentation starts, apply + // these properties after short time delay + setTimeout( function() { + dom.wrapper.classList.add( 'has-parallax-background' ); + }, 1 ); + + } + else { + + dom.background.style.backgroundImage = ''; + dom.wrapper.classList.remove( 'has-parallax-background' ); + + } + } /** @@ -478,6 +532,8 @@ var Reveal = (function(){ */ function configure( options ) { + var numberOfSlides = document.querySelectorAll( SLIDES_SELECTOR ).length; + dom.wrapper.classList.remove( config.transition ); // New config options may be passed when this method @@ -485,7 +541,7 @@ var Reveal = (function(){ if( typeof options === 'object' ) extend( config, options ); // Force linear transition based on browser capabilities - if( supports3DTransforms === false ) config.transition = 'linear'; + if( features.transforms3d === false ) config.transition = 'linear'; dom.wrapper.classList.add( config.transition ); @@ -535,6 +591,20 @@ var Reveal = (function(){ enablePreviewLinks( '[data-preview-link]' ); } + // Auto-slide playback controls + 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 ); + } ); + + autoSlidePlayer.on( 'click', onAutoSlidePlayerClick ); + autoSlidePaused = false; + } + else if( autoSlidePlayer ) { + autoSlidePlayer.destroy(); + autoSlidePlayer = null; + } + // Load the theme in the config, if it's not already loaded if( config.theme && dom.theme ) { var themeURL = dom.theme.getAttribute( 'href' ); @@ -567,7 +637,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 ); @@ -578,10 +655,28 @@ var Reveal = (function(){ document.addEventListener( 'keydown', onDocumentKeyDown, false ); } - if ( config.progress && dom.progress ) { + if( config.progress && dom.progress ) { dom.progress.addEventListener( 'click', onProgressClicked, false ); } + if( config.focusBodyOnPageVisiblityChange ) { + var visibilityChange; + + if( 'hidden' in document ) { + visibilityChange = 'visibilitychange'; + } + else if( 'msHidden' in document ) { + visibilityChange = 'msvisibilitychange'; + } + else if( 'webkitHidden' in document ) { + visibilityChange = 'webkitvisibilitychange'; + } + + if( visibilityChange ) { + document.addEventListener( visibilityChange, onPageVisibilityChange, false ); + } + } + [ 'touchstart', 'click' ].forEach( function( eventName ) { dom.controlsLeft.forEach( function( el ) { el.addEventListener( eventName, onNavigateLeftClicked, false ); } ); dom.controlsRight.forEach( function( el ) { el.addEventListener( eventName, onNavigateRightClicked, false ); } ); @@ -608,7 +703,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 ); @@ -784,16 +886,6 @@ var Reveal = (function(){ */ function removeAddressBar() { - // Portrait and not Chrome for iOS - if( window.orientation === 0 && !/crios/gi.test( navigator.userAgent ) ) { - document.documentElement.style.overflow = 'scroll'; - document.body.style.height = '120%'; - } - else { - document.documentElement.style.overflow = ''; - document.body.style.height = '100%'; - } - setTimeout( function() { window.scrollTo( 0, 1 ); }, 10 ); @@ -818,7 +910,7 @@ var Reveal = (function(){ */ function enableRollingLinks() { - if( supports3DTransforms && !( 'msPerspective' in document.body.style ) ) { + if( features.transforms3d && !( 'msPerspective' in document.body.style ) ) { var anchors = document.querySelectorAll( SLIDES_SELECTOR + ' a:not(.image)' ); for( var i = 0, len = anchors.length; i < len; i++ ) { @@ -942,38 +1034,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. */ @@ -1038,7 +1098,7 @@ var Reveal = (function(){ continue; } - if( config.center ) { + if( config.center || slide.classList.contains( 'center' ) ) { // Vertical stacks are not centred since their section // children will be if( slide.classList.contains( 'stack' ) ) { @@ -1055,6 +1115,7 @@ var Reveal = (function(){ } updateProgress(); + updateParallax(); } @@ -1471,19 +1532,9 @@ var Reveal = (function(){ // Store references to the previous and current slides currentSlide = currentVerticalSlides[ indexv ] || currentHorizontalSlide; - // 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 @@ -1533,10 +1584,14 @@ var Reveal = (function(){ updateControls(); updateProgress(); updateBackground(); + updateParallax(); + updateSlideNumber(); // Update the URL hash writeURL(); + cueAutoSlide(); + } /** @@ -1562,9 +1617,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' ) ); + + } ); } @@ -1617,16 +1721,27 @@ var Reveal = (function(){ if( i < index ) { // Any element previous to index is given the 'past' class element.classList.add( reverse ? 'future' : 'past' ); + + 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' ) ); + 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' ); + while( futureFragments.length ) { + var futureFragment = futureFragments.pop(); + futureFragment.classList.remove( 'visible' ); + futureFragment.classList.remove( 'current-fragment' ); } } @@ -1647,18 +1762,6 @@ var Reveal = (function(){ state = state.concat( slideState.split( ' ' ) ); } - // If this slide has a data-autoslide attribute associated use this as - // autoSlide value otherwise use the global configured time - var slideAutoSlide = slides[index].getAttribute( 'data-autoslide' ); - if( slideAutoSlide ) { - autoSlide = parseInt( slideAutoSlide, 10 ); - } - else { - autoSlide = config.autoSlide; - } - - cueAutoSlide(); - } else { // Since there are no slides we can't be anywhere beyond the @@ -1775,6 +1878,25 @@ var Reveal = (function(){ } /** + * Updates the slide number div to reflect the current slide. + */ + function updateSlideNumber() { + + // 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.slideNumber.innerHTML = indexString; + } + + } + + /** * Updates the state of all control/navigation arrows. */ function updateControls() { @@ -1825,29 +1947,70 @@ var Reveal = (function(){ } /** - * Updates the background elements to reflect the current + * 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 + // 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.childNodes ).forEach( function( backgroundv, v ) { - backgroundv.className = 'slide-background ' + ( v < indexv ? 'past' : v > indexv ? 'future' : 'present' ); + if( v < indexv ) { + backgroundv.className = 'slide-background past'; + } + else if ( v > indexv ) { + backgroundv.className = 'slide-background future'; + } + else { + backgroundv.className = 'slide-background present'; - } ); + // Only if this is the present horizontal and vertical slide + if( h === indexh ) currentBackground = backgroundv; + } + + } ); + } } ); + // Don't transition between identical backgrounds. This + // prevents unwanted flicker. + if( currentBackground ) { + 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' ); @@ -1856,6 +2019,42 @@ var Reveal = (function(){ } /** + * Updates the position of the parallax background based + * on the current slide index. + */ + function updateParallax() { + + if( config.parallaxBackgroundImage ) { + + var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ), + verticalSlides = document.querySelectorAll( VERTICAL_SLIDES_SELECTOR ); + + var backgroundSize = dom.background.style.backgroundSize.split( ' ' ), + backgroundWidth, backgroundHeight; + + if( backgroundSize.length === 1 ) { + backgroundWidth = backgroundHeight = parseInt( backgroundSize[0], 10 ); + } + else { + backgroundWidth = parseInt( backgroundSize[0], 10 ); + backgroundHeight = parseInt( backgroundSize[1], 10 ); + } + + var slideWidth = dom.background.offsetWidth; + var horizontalSlideCount = horizontalSlides.length; + var horizontalOffset = -( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 ) * indexh; + + var slideHeight = dom.background.offsetHeight; + var verticalSlideCount = verticalSlides.length; + var verticalOffset = verticalSlideCount > 0 ? -( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 ) * indexv : 0; + + dom.background.style.backgroundPosition = horizontalOffset + 'px ' + verticalOffset + 'px'; + + } + + } + + /** * Determine what available routes there are for navigation. * * @return {Object} containing four booleans: left/right/up/down @@ -1912,7 +2111,7 @@ var Reveal = (function(){ */ function startEmbeddedContent( slide ) { - if( slide ) { + if( slide && !isSpeakerNotes() ) { // HTML5 media elements toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) { if( el.hasAttribute( 'data-autoplay' ) ) { @@ -1920,10 +2119,15 @@ var Reveal = (function(){ } } ); + // iframe embeds + toArray( slide.querySelectorAll( 'iframe' ) ).forEach( function( el ) { + el.contentWindow.postMessage( 'slide:start', '*' ); + }); + // YouTube embeds toArray( slide.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) { if( el.hasAttribute( 'data-autoplay' ) ) { - el.contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}', '*'); + el.contentWindow.postMessage( '{"event":"command","func":"playVideo","args":""}', '*' ); } }); } @@ -1944,10 +2148,15 @@ var Reveal = (function(){ } } ); + // iframe embeds + toArray( slide.querySelectorAll( 'iframe' ) ).forEach( function( el ) { + el.contentWindow.postMessage( 'slide:stop', '*' ); + }); + // YouTube embeds toArray( slide.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) { if( !el.hasAttribute( 'data-ignore' ) && typeof el.contentWindow.postMessage === 'function' ) { - el.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*'); + el.contentWindow.postMessage( '{"event":"command","func":"pauseVideo","args":""}', '*' ); } }); } @@ -1955,6 +2164,16 @@ var Reveal = (function(){ } /** + * Checks if this presentation is running inside of the + * speaker notes window. + */ + function isSpeakerNotes() { + + return !!window.location.search.match( /receiver/gi ); + + } + + /** * Reads the current URL (hash) and navigates accordingly. */ function readURL() { @@ -2068,7 +2287,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; } } @@ -2077,67 +2296,144 @@ var Reveal = (function(){ } /** - * Navigate to the next slide fragment. + * Return a sorted fragments list, ordered by an increasing + * "data-fragment-index" attribute. * - * @return {Boolean} true if there was a next fragment, - * false otherwise + * 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 nextFragment() { + function sortFragments( fragments ) { - if( currentSlide && config.fragments ) { - var fragments = sortFragments( currentSlide.querySelectorAll( '.fragment:not(.visible)' ) ); + fragments = toArray( fragments ); - if( fragments.length ) { - // Find the index of the next fragment - var index = fragments[0].getAttribute( 'data-fragment-index' ); + var ordered = [], + unordered = [], + sorted = []; - // Find all fragments with the same index - fragments = currentSlide.querySelectorAll( '.fragment[data-fragment-index="'+ index +'"]' ); + // 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 ); - toArray( fragments ).forEach( function( element ) { - element.classList.add( 'visible' ); - } ); - - // Notify subscribers of the change - dispatchEvent( 'fragmentshown', { fragment: fragments[0], fragments: fragments } ); + if( !ordered[index] ) { + ordered[index] = []; + } - updateControls(); - return true; + 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; + + return !!( fragmentsShown.length || fragmentsHidden.length ); + } + } return false; @@ -2145,15 +2441,81 @@ 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() { - clearTimeout( autoSlideTimeout ); + cancelAutoSlide(); + + if( currentSlide ) { + + var parentAutoSlide = currentSlide.parentNode ? currentSlide.parentNode.getAttribute( 'data-autoslide' ) : null; + var slideAutoSlide = currentSlide.getAttribute( 'data-autoslide' ); + + // Pick value in the following priority order: + // 1. Current slide's data-autoslide + // 2. Parent slide's data-autoslide + // 3. Global autoSlide setting + 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 + // - The presentation isn't paused + // - The overview isn't active + // - The presentation isn't over + if( autoSlide && !autoSlidePaused && !isPaused() && !isOverview() && ( !Reveal.isLastSlide() || config.loop === true ) ) { + autoSlideTimeout = setTimeout( navigateNext, autoSlide ); + autoSlideStartTime = Date.now(); + } + + if( autoSlidePlayer ) { + autoSlidePlayer.setPlaying( autoSlideTimeout !== -1 ); + } - // Cue the next auto-slide if enabled - if( autoSlide && !isPaused() && !isOverview() ) { - autoSlideTimeout = setTimeout( navigateNext, autoSlide ); } } @@ -2164,6 +2526,25 @@ var Reveal = (function(){ function cancelAutoSlide() { clearTimeout( autoSlideTimeout ); + autoSlideTimeout = -1; + + } + + function pauseAutoSlide() { + + autoSlidePaused = true; + clearTimeout( autoSlideTimeout ); + + if( autoSlidePlayer ) { + autoSlidePlayer.setPlaying( false ); + } + + } + + function resumeAutoSlide() { + + autoSlidePaused = false; + cueAutoSlide(); } @@ -2263,14 +2644,25 @@ var Reveal = (function(){ // ----------------------------- EVENTS -------------------------------// // --------------------------------------------------------------------// + /** + * Called by all event handlers that are based on user + * input. + */ + function onUserInput( event ) { + + if( config.autoSlideStoppable ) { + pauseAutoSlide(); + } + + } /** * Handler for the document level 'keydown' event. - * - * @param {Object} event */ function onDocumentKeyDown( event ) { + onUserInput( event ); + // Check if there's a focused element that could be using // the keyboard var activeElement = document.activeElement; @@ -2357,8 +2749,13 @@ var Reveal = (function(){ event.preventDefault(); } // ESC or O key - else if ( ( event.keyCode === 27 || event.keyCode === 79 ) && supports3DTransforms ) { - toggleOverview(); + else if ( ( event.keyCode === 27 || event.keyCode === 79 ) && features.transforms3d ) { + if( dom.preview ) { + closePreview(); + } + else { + toggleOverview(); + } event.preventDefault(); } @@ -2400,6 +2797,8 @@ var Reveal = (function(){ // Each touch should only trigger one action if( !touch.captured ) { + onUserInput( event ); + var currentX = event.touches[0].clientX; var currentY = event.touches[0].clientY; @@ -2492,7 +2891,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 ); } @@ -2504,7 +2903,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 ); } @@ -2516,7 +2915,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 ); } @@ -2553,6 +2952,8 @@ var Reveal = (function(){ */ function onProgressClicked( event ) { + onUserInput( event ); + event.preventDefault(); var slidesTotal = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).length; @@ -2565,12 +2966,12 @@ var Reveal = (function(){ /** * Event handler for navigation control buttons. */ - function onNavigateLeftClicked( event ) { event.preventDefault(); navigateLeft(); } - function onNavigateRightClicked( event ) { event.preventDefault(); navigateRight(); } - function onNavigateUpClicked( event ) { event.preventDefault(); navigateUp(); } - function onNavigateDownClicked( event ) { event.preventDefault(); navigateDown(); } - function onNavigatePrevClicked( event ) { event.preventDefault(); navigatePrev(); } - function onNavigateNextClicked( event ) { event.preventDefault(); navigateNext(); } + function onNavigateLeftClicked( event ) { event.preventDefault(); onUserInput(); navigateLeft(); } + function onNavigateRightClicked( event ) { event.preventDefault(); onUserInput(); navigateRight(); } + function onNavigateUpClicked( event ) { event.preventDefault(); onUserInput(); navigateUp(); } + function onNavigateDownClicked( event ) { event.preventDefault(); onUserInput(); navigateDown(); } + function onNavigatePrevClicked( event ) { event.preventDefault(); onUserInput(); navigatePrev(); } + function onNavigateNextClicked( event ) { event.preventDefault(); onUserInput(); navigateNext(); } /** * Handler for the window level 'hashchange' event. @@ -2591,6 +2992,24 @@ var Reveal = (function(){ } /** + * Handle for the window level 'visibilitychange' event. + */ + function onPageVisibilityChange( event ) { + + var isHidden = document.webkitHidden || + document.msHidden || + document.hidden; + + // If, after clicking a link or similar and we're coming back, + // focus the document.body to ensure we can use keyboard shortcuts + if( isHidden === false && document.activeElement !== document.body ) { + document.activeElement.blur(); + document.body.focus(); + } + + } + + /** * Invoked when a slide is and we're in the overview. */ function onOverviewSlideClicked( event ) { @@ -2636,6 +3055,191 @@ var Reveal = (function(){ } + /** + * Handles click on the auto-sliding controls element. + */ + function onAutoSlidePlayerClick( event ) { + + // Replay + if( Reveal.isLastSlide() && config.loop === false ) { + slide( 0, 0 ); + resumeAutoSlide(); + } + // Resume + else if( autoSlidePaused ) { + resumeAutoSlide(); + } + // Pause + else { + pauseAutoSlide(); + } + + } + + + // --------------------------------------------------------------------// + // ------------------------ PLAYBACK COMPONENT ------------------------// + // --------------------------------------------------------------------// + + + /** + * Constructor for the playback component, which displays + * play/pause/progress controls. + * + * @param {HTMLElement} container The component will append + * itself to this + * @param {Function} progressCheck A method which will be + * called frequently to get the current progress on a range + * of 0-1 + */ + function Playback( container, progressCheck ) { + + // Cosmetics + this.diameter = 50; + this.thickness = 3; + + // Flags if we are currently playing + this.playing = false; + + // Current progress on a 0-1 range + this.progress = 0; + + // Used to loop the animation smoothly + this.progressOffset = 1; + + this.container = container; + this.progressCheck = progressCheck; + + this.canvas = document.createElement( 'canvas' ); + this.canvas.className = 'playback'; + this.canvas.width = this.diameter; + this.canvas.height = this.diameter; + this.context = this.canvas.getContext( '2d' ); + + this.container.appendChild( this.canvas ); + + this.render(); + + } + + Playback.prototype.setPlaying = function( value ) { + + var wasPlaying = this.playing; + + this.playing = value; + + // Start repainting if we weren't already + if( !wasPlaying && this.playing ) { + this.animate(); + } + else { + this.render(); + } + + }; + + Playback.prototype.animate = function() { + + var progressBefore = this.progress; + + this.progress = this.progressCheck(); + + // When we loop, offset the progress so that it eases + // smoothly rather than immediately resetting + if( progressBefore > 0.8 && this.progress < 0.2 ) { + this.progressOffset = this.progress; + } + + this.render(); + + if( this.playing ) { + features.requestAnimationFrameMethod.call( window, this.animate.bind( this ) ); + } + + }; + + /** + * Renders the current progress and playback state. + */ + Playback.prototype.render = function() { + + var progress = this.playing ? this.progress : 0, + radius = ( this.diameter / 2 ) - this.thickness, + x = this.diameter / 2, + y = this.diameter / 2, + iconSize = 14; + + // Ease towards 1 + this.progressOffset += ( 1 - this.progressOffset ) * 0.1; + + var endAngle = ( - Math.PI / 2 ) + ( progress * ( Math.PI * 2 ) ); + var startAngle = ( - Math.PI / 2 ) + ( this.progressOffset * ( Math.PI * 2 ) ); + + this.context.save(); + this.context.clearRect( 0, 0, this.diameter, this.diameter ); + + // Solid background color + this.context.beginPath(); + this.context.arc( x, y, radius + 2, 0, Math.PI * 2, false ); + this.context.fillStyle = 'rgba( 0, 0, 0, 0.4 )'; + this.context.fill(); + + // Draw progress track + 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.stroke(); + + if( this.playing ) { + // Draw progress on top of track + this.context.beginPath(); + this.context.arc( x, y, radius, startAngle, endAngle, false ); + this.context.lineWidth = this.thickness; + this.context.strokeStyle = '#fff'; + this.context.stroke(); + } + + this.context.translate( x - ( iconSize / 2 ), y - ( iconSize / 2 ) ); + + // Draw play/pause icons + if( this.playing ) { + this.context.fillStyle = '#fff'; + this.context.fillRect( 0, 0, iconSize / 2 - 2, iconSize ); + this.context.fillRect( iconSize / 2 + 2, 0, iconSize / 2 - 2, iconSize ); + } + else { + this.context.beginPath(); + this.context.translate( 2, 0 ); + this.context.moveTo( 0, 0 ); + this.context.lineTo( iconSize - 2, iconSize / 2 ); + this.context.lineTo( 0, iconSize ); + this.context.fillStyle = '#fff'; + this.context.fill(); + } + + this.context.restore(); + + }; + + Playback.prototype.on = function( type, listener ) { + this.canvas.addEventListener( type, listener, false ); + }; + + Playback.prototype.off = function( type, listener ) { + this.canvas.removeEventListener( type, listener, false ); + }; + + Playback.prototype.destroy = function() { + + this.playing = false; + + if( this.canvas.parentNode ) { + this.container.removeChild( this.canvas ); + } + + }; + // --------------------------------------------------------------------// // ------------------------------- API --------------------------------// @@ -2655,6 +3259,9 @@ var Reveal = (function(){ down: navigateDown, prev: navigatePrev, next: navigateNext, + + // Fragment methods + navigateFragment: navigateFragment, prevFragment: previousFragment, nextFragment: nextFragment, @@ -2729,10 +3336,22 @@ 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 ]; + + query[ i ] = unescape( value ); + + if( value === 'null' ) query[ i ] = null; + else if( value === 'true' ) query[ i ] = true; + else if( value === 'false' ) query[ i ] = false; + else if( value.match( /^\d+$/ ) ) query[ i ] = parseFloat( value ); + } + return query; }, |