reveal.js/js/reveal.js in reveal-ck-0.1.8 vs reveal.js/js/reveal.js in reveal-ck-0.2.0

- old
+ new

@@ -10,11 +10,11 @@ 'use strict'; 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 = { // The "normal" size of the presentation, aspect ratio will be preserved @@ -33,20 +33,23 @@ controls: true, // 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, // Enable keyboard shortcuts for navigation keyboard: true, // 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 touch: true, @@ -66,10 +69,13 @@ // Number of milliseconds between automatically proceeding to the // next slide, disabled when set to 0, this value can be overwritten // by using a data-autoslide attribute on your slides autoSlide: 0, + // Stop auto-sliding after user input + autoSlideStoppable: true, + // Enable slide navigation via mouse wheel mouseWheel: false, // Apply a 3D roll to links on hover rollingLinks: false, @@ -78,10 +84,13 @@ hideAddressBar: true, // 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, // Transition style transition: 'default', // default/cube/page/concave/zoom/linear/fade/none @@ -90,31 +99,37 @@ transitionSpeed: 'default', // default/fast/slow // 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, // The previous and current slide HTML elements 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. state = [], @@ -122,25 +137,19 @@ scale = 1, // Cached references to DOM elements dom = {}, - // Client support for CSS 3D transforms, see #checkCapabilities() - supports3DTransforms, + // Features supported by the browser, see #checkCapabilities() + features = {}, - // Client support for CSS 2D transforms, see #checkCapabilities() - supports2DTransforms, - // Client is a mobile device, see #checkCapabilities() isMobileDevice, // 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, // A delay used to activate the overview mode activateOverviewTimeout = 0, @@ -149,10 +158,19 @@ deactivateOverviewTimeout = 0, // 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, startY: 0, startSpan: 0, @@ -166,23 +184,30 @@ */ function initialize( options ) { 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 // using JavaScript to control the presentation return; } // 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(); // Loads the dependencies and continues to #start() once done @@ -194,38 +219,68 @@ * Inspect the client to see what it's capable of, this * should only happens once per runtime. */ 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]; // Load if there's no condition or the condition is truthy if( !s.condition || s.condition() ) { @@ -234,29 +289,16 @@ } else { 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 ); - } + loadScript( s ); } } - // Called once synchronous scripts finish loading - function proceed() { - if( scriptsAsync.length ) { - // Load asynchronous scripts - head.js.apply( null, scriptsAsync ); - } - - start(); - } - if( scripts.length ) { - head.ready( proceed ); + scriptsToPreload = scripts.length; // Load synchronous scripts head.js.apply( null, scripts ); } else { @@ -272,19 +314,22 @@ function start() { // 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(); // 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() { // Enable transitions now that we're loaded dom.slides.classList.remove( 'no-transition' ); @@ -299,30 +344,10 @@ }, 1 ); } /** - * 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. */ function setupDOM() { @@ -347,10 +372,13 @@ '<div class="navigate-left"></div>' + '<div class="navigate-right"></div>' + '<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 ); // Overlay graphic which is displayed during the paused mode createSingletonNode( dom.wrapper, 'div', 'pause-overlay', null ); @@ -420,18 +448,22 @@ 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 ) || /\.(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 { element.style.background = data.background; } } + 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 + '")'; if( data.backgroundColor ) element.style.backgroundColor = data.backgroundColor; if( data.backgroundRepeat ) element.style.backgroundRepeat = data.backgroundRepeat; @@ -468,26 +500,50 @@ } ); } ); + // 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' ); + + } + } /** * Applies the configuration settings from the config * object. May be called multiple times. */ 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 // is invoked through the API after initialization 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 ); dom.wrapper.setAttribute( 'data-transition-speed', config.transitionSpeed ); dom.wrapper.setAttribute( 'data-background-transition', config.backgroundTransition ); @@ -533,10 +589,24 @@ else { disablePreviewLinks(); 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' ); var themeFinder = /[^\/]*?(?=\.css)/; var themeName = themeURL.match(themeFinder)[0]; @@ -576,14 +646,32 @@ if( config.keyboard ) { 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 ); } ); dom.controlsUp.forEach( function( el ) { el.addEventListener( eventName, onNavigateUpClicked, false ); } ); dom.controlsDown.forEach( function( el ) { el.addEventListener( eventName, onNavigateDownClicked, false ); } ); @@ -782,20 +870,10 @@ * Causes the address bar to hide on mobile devices, * more vertical space ftw. */ 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 ); } @@ -816,11 +894,11 @@ /** * Wrap all links in 3D goodness. */ 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++ ) { var anchor = anchors[i]; @@ -940,42 +1018,10 @@ } } /** - * 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. */ function layout() { @@ -1036,11 +1082,11 @@ // Don't bother updating invisible slides if( slide.style.display === 'none' ) { 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' ) ) { slide.style.top = 0; } @@ -1053,10 +1099,11 @@ } } updateProgress(); + updateParallax(); } } @@ -1469,23 +1516,13 @@ currentVerticalSlides = currentHorizontalSlide.querySelectorAll( 'section' ); // 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 var slideChanged = ( indexh !== indexhBefore || indexv !== indexvBefore ); if( slideChanged ) { @@ -1531,14 +1568,18 @@ } updateControls(); updateProgress(); updateBackground(); + updateParallax(); + updateSlideNumber(); // Update the URL hash writeURL(); + cueAutoSlide(); + } /** * Syncs the presentation with the current DOM. Useful * when new slides or control elements are added or when @@ -1560,17 +1601,66 @@ cueAutoSlide(); // 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' ) ); + + } ); + + } + + /** * Updates one dimension of slides by showing the slide * with the specified index. * * @param {String} selector A CSS selector that will fetch * the group of slides we are working with @@ -1615,20 +1705,31 @@ element.setAttribute( 'hidden', '' ); 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' ); } } // If this element contains vertical slides if( element.querySelector( 'section' ) ) { @@ -1645,22 +1746,10 @@ var slideState = slides[index].getAttribute( 'data-state' ); if( slideState ) { 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 // zeroth index index = 0; @@ -1773,10 +1862,29 @@ } } /** + * 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() { var routes = availableRoutes(); @@ -1823,41 +1931,118 @@ } } /** - * 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 ) { - // Update the classes of all backgrounds to match the + 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.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' ); }, 1 ); } /** + * 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 */ function availableRoutes() { @@ -1910,22 +2095,27 @@ * Start playback of any embedded content inside of * the targeted slide. */ function startEmbeddedContent( slide ) { - if( slide ) { + if( slide && !isSpeakerNotes() ) { // HTML5 media elements toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) { if( el.hasAttribute( 'data-autoplay' ) ) { el.play(); } } ); + // 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":""}', '*' ); } }); } } @@ -1942,21 +2132,36 @@ if( !el.hasAttribute( 'data-ignore' ) ) { el.pause(); } } ); + // 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":""}', '*' ); } }); } } /** + * 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() { var hash = window.location.hash; @@ -2066,109 +2271,271 @@ if( !slide && currentSlide ) { var hasFragments = currentSlide.querySelectorAll( '.fragment' ).length > 0; if( hasFragments ) { var visibleFragments = currentSlide.querySelectorAll( '.fragment.visible' ); - f = visibleFragments.length; + f = visibleFragments.length - 1; } } return { h: h, v: v, f: f }; } /** - * 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' ); - } ); + if( !ordered[index] ) { + ordered[index] = []; + } - // Notify subscribers of the change - dispatchEvent( 'fragmentshown', { fragment: fragments[0], fragments: fragments } ); - - 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(); - toArray( fragments ).forEach( function( f ) { - f.classList.remove( 'visible' ); + 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' ); + } + + } ); - // 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; } /** + * 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(); - // Cue the next auto-slide if enabled - if( autoSlide && !isPaused() && !isOverview() ) { - autoSlideTimeout = setTimeout( navigateNext, autoSlide ); + 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 ); + } + } } /** * Cancels any ongoing request to auto-slide. */ function cancelAutoSlide() { clearTimeout( autoSlideTimeout ); + autoSlideTimeout = -1; } + function pauseAutoSlide() { + + autoSlidePaused = true; + clearTimeout( autoSlideTimeout ); + + if( autoSlidePlayer ) { + autoSlidePlayer.setPlaying( false ); + } + + } + + function resumeAutoSlide() { + + autoSlidePaused = false; + cueAutoSlide(); + + } + function navigateLeft() { // Reverse for RTL if( config.rtl ) { if( ( isOverview() || nextFragment() === false ) && availableRoutes().left ) { @@ -2261,18 +2628,29 @@ // --------------------------------------------------------------------// // ----------------------------- 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; var hasFocus = !!( document.activeElement && ( document.activeElement.type || document.activeElement.href || document.activeElement.contentEditable !== 'inherit' ) ); @@ -2355,12 +2733,17 @@ // the browsers default behavior if( triggered ) { 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(); } // If auto-sliding is enabled we need to cue up @@ -2398,10 +2781,12 @@ */ function onTouchMove( event ) { // Each touch should only trigger one action if( !touch.captured ) { + onUserInput( event ); + var currentX = event.touches[0].clientX; var currentY = event.touches[0].clientY; // If the touch started with two points and still has // two active touches; test for the pinch gesture @@ -2551,10 +2936,12 @@ * * ( clickX / presentationWidth ) * numberOfSlides */ function onProgressClicked( event ) { + onUserInput( event ); + event.preventDefault(); var slidesTotal = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).length; var slideIndex = Math.floor( ( event.clientX / dom.wrapper.offsetWidth ) * slidesTotal ); @@ -2563,16 +2950,16 @@ } /** * 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. */ function onWindowHashChange( event ) { @@ -2589,10 +2976,28 @@ layout(); } /** + * 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 ) { // TODO There's a bug here where the event listeners are not @@ -2634,12 +3039,197 @@ event.preventDefault(); } } + /** + * 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 --------------------------------// // --------------------------------------------------------------------// return { @@ -2653,10 +3243,13 @@ right: navigateRight, up: navigateUp, down: navigateDown, prev: navigatePrev, next: navigateNext, + + // Fragment methods + navigateFragment: navigateFragment, prevFragment: previousFragment, nextFragment: nextFragment, // Deprecated aliases navigateTo: slide, @@ -2727,12 +3320,24 @@ // Helper method, retrieves query string as a key/value hash 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; }, // Returns true if we're currently on the first slide