diff options
author | David Banham | 2013-03-08 10:29:54 +1100 |
---|---|---|
committer | David Banham | 2013-03-08 10:29:54 +1100 |
commit | 61c229a4f969a0720085c2710f8a7cfd8a12b4a6 (patch) | |
tree | 637eeedce43c63a6aa004d566944ad791b984e7d /js | |
parent | 26d5febd7f24223900aa50d24d35e610c7c8a334 (diff) | |
parent | 7081f901da553206f746c40c4a0c1b5773697cf2 (diff) |
Merge branch 'master' of https://github.com/hakimel/reveal.js
Conflicts:
README.md
index.html
js/reveal.js
package.json
plugin/speakernotes/client.js
Diffstat (limited to 'js')
-rw-r--r-- | js/reveal.js | 1979 | ||||
-rw-r--r-- | js/reveal.min.js | 8 |
2 files changed, 1483 insertions, 504 deletions
diff --git a/js/reveal.js b/js/reveal.js index d820943..2fa74df 100644 --- a/js/reveal.js +++ b/js/reveal.js @@ -1,19 +1,34 @@ /*! - * reveal.js 1.5 r11 + * reveal.js * http://lab.hakim.se/reveal-js * MIT licensed - * - * Copyright (C) 2012 Hakim El Hattab, http://hakim.se + * + * Copyright (C) 2013 Hakim El Hattab, http://hakim.se */ var Reveal = (function(){ - - var HORIZONTAL_SLIDES_SELECTOR = '.reveal .slides>section', - VERTICAL_SLIDES_SELECTOR = '.reveal .slides>section.present>section', - IS_TOUCH_DEVICE = !!( 'ontouchstart' in window ), + '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', - // Configurations defaults, can be overridden at initialization time + // Configurations defaults, can be overridden at initialization time config = { + + // The "normal" size of the presentation, aspect ratio will be preserved + // when the presentation is scaled to fit different resolutions + width: 960, + height: 700, + + // Factor of the display size that should remain empty around the content + margin: 0.1, + + // Bounds for smallest/largest possible scale to apply to content + minScale: 0.2, + maxScale: 1.0, + // Display controls in the bottom right corner controls: true, @@ -26,27 +41,47 @@ var Reveal = (function(){ // Enable keyboard shortcuts for navigation keyboard: true, + // Enable the slide overview mode + overview: true, + + // Vertical centring of slides + center: true, + + // Enables touch navigation on devices with touch input + touch: true, + // Loop the presentation loop: false, - // Number of milliseconds between automatically proceeding to the - // next slide, disabled when set to 0 + // Change the presentation direction to be RTL + rtl: false, + + // Number of milliseconds between automatically proceeding to the + // next slide, disabled when set to 0, this value can be overwritten + // by using a data-autoslide attribute on your slides autoSlide: 0, // Enable slide navigation via mouse wheel - mouseWheel: true, + mouseWheel: false, // Apply a 3D roll to links on hover rollingLinks: true, - // UI style - theme: 'default', // default/neon/beige + // Theme (see /css/theme) + theme: null, // Transition style - transition: 'default' // default/cube/page/concave/linear(2d) + transition: 'default', // default/cube/page/concave/zoom/linear/fade/none + + // Script dependencies to load + dependencies: [] }, - // The horizontal and verical index of the currently active slide + // Stores if the next slide should be shown automatically + // after n milliseconds + autoSlide = config.autoSlide, + + // The horizontal and vertical index of the currently active slide indexh = 0, indexv = 0, @@ -54,30 +89,31 @@ var Reveal = (function(){ previousSlide, currentSlide, - // 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 + // 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 = [], + // The current scale of the presentation (see width/height config) + scale = 1, + // Cached references to DOM elements dom = {}, // Detect support for CSS 3D transforms supports3DTransforms = '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 || - 'MozTransform' in document.body.style || - 'msTransform' in document.body.style || - 'OTransform' in document.body.style || - 'transform' in document.body.style, - - // Detect support for elem.classList - supportsClassList = !!document.body.classList; - + 'MozPerspective' in document.body.style || + 'msPerspective' in document.body.style || + 'OPerspective' in document.body.style || + 'perspective' in document.body.style, + + // Detect support for CSS 2D transforms + supports2DTransforms = '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, + // Throttles mouse wheel navigation mouseWheelTimeout = 0, @@ -87,6 +123,15 @@ var Reveal = (function(){ // Delays updates to the URL due to a Chrome thumbnailer bug writeURLTimeout = 0, + // A delay used to activate the overview mode + activateOverviewTimeout = 0, + + // A delay used to deactivate the overview mode + deactivateOverviewTimeout = 0, + + // Flags if the interaction event listeners are bound + eventsAreBound = false, + // Holds information about the currently ongoing touch input touch = { startX: 0, @@ -94,41 +139,176 @@ var Reveal = (function(){ startSpan: 0, startCount: 0, handled: false, - threshold: 40 + threshold: 80 }; - - + /** - * Starts up the slideshow by applying configuration - * options and binding various events. + * Starts up the presentation if the client is capable. */ function initialize( options ) { - - if( ( !supports2DTransforms && !supports3DTransforms ) || !supportsClassList ) { + + if( !supports2DTransforms && !supports3DTransforms ) { document.body.setAttribute( 'class', 'no-transforms' ); - // If the browser doesn't support core features we won't be + // If the browser doesn't support core features we won't be // using JavaScript to control the presentation return; } - // Cache references to DOM elements + // Force a layout when the whole page, incl fonts, has loaded + window.addEventListener( 'load', layout, false ); + + // Copy options over to our config object + extend( config, options ); + + // Hide the address bar in mobile browsers + hideAddressBar(); + + // Loads the dependencies and continues to #start() once done + load(); + + } + + /** + * 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() { + + // Cache references to key DOM elements + dom.theme = document.querySelector( '#theme' ); dom.wrapper = document.querySelector( '.reveal' ); + dom.slides = document.querySelector( '.reveal .slides' ); + + // Progress bar + if( !dom.wrapper.querySelector( '.progress' ) && config.progress ) { + var progressElement = document.createElement( 'div' ); + progressElement.classList.add( 'progress' ); + progressElement.innerHTML = '<span></span>'; + dom.wrapper.appendChild( progressElement ); + } + + // Arrow controls + if( !dom.wrapper.querySelector( '.controls' ) && config.controls ) { + var controlsElement = document.createElement( 'aside' ); + controlsElement.classList.add( 'controls' ); + controlsElement.innerHTML = '<div class="navigate-left"></div>' + + '<div class="navigate-right"></div>' + + '<div class="navigate-up"></div>' + + '<div class="navigate-down"></div>'; + dom.wrapper.appendChild( controlsElement ); + } + + // Presentation background element + if( !dom.wrapper.querySelector( '.state-background' ) ) { + var backgroundElement = document.createElement( 'div' ); + backgroundElement.classList.add( 'state-background' ); + dom.wrapper.appendChild( backgroundElement ); + } + + // Overlay graphic which is displayed during the paused mode + if( !dom.wrapper.querySelector( '.pause-overlay' ) ) { + var pausedElement = document.createElement( 'div' ); + pausedElement.classList.add( 'pause-overlay' ); + dom.wrapper.appendChild( pausedElement ); + } + + // Cache references to elements dom.progress = document.querySelector( '.reveal .progress' ); dom.progressbar = document.querySelector( '.reveal .progress span' ); - + if ( config.controls ) { dom.controls = document.querySelector( '.reveal .controls' ); - dom.controlsLeft = document.querySelector( '.reveal .controls .left' ); - dom.controlsRight = document.querySelector( '.reveal .controls .right' ); - dom.controlsUp = document.querySelector( '.reveal .controls .up' ); - dom.controlsDown = document.querySelector( '.reveal .controls .down' ); + + // There can be multiple instances of controls throughout the page + dom.controlsLeft = toArray( document.querySelectorAll( '.navigate-left' ) ); + dom.controlsRight = toArray( document.querySelectorAll( '.navigate-right' ) ); + dom.controlsUp = toArray( document.querySelectorAll( '.navigate-up' ) ); + dom.controlsDown = toArray( document.querySelectorAll( '.navigate-down' ) ); + dom.controlsPrev = toArray( document.querySelectorAll( '.navigate-prev' ) ); + dom.controlsNext = toArray( document.querySelectorAll( '.navigate-next' ) ); } - addEventListeners(); + } - // Copy options over to our config object - extend( config, options ); + /** + * Hides the address bar if we're on a mobile device. + */ + function hideAddressBar() { + + if( /iphone|ipod|android/gi.test( navigator.userAgent ) && !/crios/gi.test( navigator.userAgent ) ) { + // Events that should trigger the address bar to hide + window.addEventListener( 'load', removeAddressBar, false ); + window.addEventListener( 'orientationchange', removeAddressBar, false ); + } + + } + + /** + * 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 = []; + + 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() ) { + if( s.async ) { + scriptsAsync.push( s.src ); + } + 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 ); + } + } + } + + // 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 ); + + // Load synchronous scripts + head.js.apply( null, scripts ); + } + else { + proceed(); + } + + } + + /** + * Starts up reveal.js by binding input events and navigating + * to the current URL deeplink if there is one. + */ + function start() { + + // Make sure we've got all the DOM elements we need + setupDOM(); + + // Subscribe to input + addEventListeners(); // Updates the presentation to match the current configuration values configure(); @@ -139,450 +319,840 @@ var Reveal = (function(){ // Start auto-sliding if it's enabled cueAutoSlide(); - // Set up hiding of the browser address bar - if( navigator.userAgent.match( /(iphone|ipod|android)/i ) ) { - // Give the page some scrollable overflow - document.documentElement.style.overflow = 'scroll'; - document.body.style.height = '120%'; + // Notify listeners that the presentation is ready but use a 1ms + // timeout to ensure it's not fired synchronously after #initialize() + setTimeout( function() { + dispatchEvent( 'ready', { + 'indexh': indexh, + 'indexv': indexv, + 'currentSlide': currentSlide + } ); + }, 1 ); - // Events that should trigger the address bar to hide - window.addEventListener( 'load', removeAddressBar, false ); - window.addEventListener( 'orientationchange', removeAddressBar, false ); - } - } - function configure() { - if( supports3DTransforms === false ) { - // Fall back on the 2D transform theme 'linear' - config.transition = 'linear'; - } + /** + * Applies the configuration settings from the config object. + */ + function configure( options ) { + + 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 ); - if( config.controls && dom.controls ) { - dom.controls.style.display = 'block'; + // Force linear transition based on browser capabilities + if( supports3DTransforms === false ) config.transition = 'linear'; + + dom.wrapper.classList.add( config.transition ); + + if( dom.controls ) { + dom.controls.style.display = ( config.controls && dom.controls ) ? 'block' : 'none'; } - if( config.progress && dom.progress ) { - dom.progress.style.display = 'block'; + if( dom.progress ) { + dom.progress.style.display = ( config.progress && dom.progress ) ? 'block' : 'none'; } - if( config.transition !== 'default' ) { - dom.wrapper.classList.add( config.transition ); + if( config.rtl ) { + dom.wrapper.classList.add( 'rtl' ); + } + else { + dom.wrapper.classList.remove( 'rtl' ); } - if( config.theme !== 'default' ) { - document.documentElement.classList.add( 'theme-' + config.theme ); + if( config.center ) { + dom.wrapper.classList.add( 'center' ); + } + else { + dom.wrapper.classList.remove( 'center' ); } if( config.mouseWheel ) { document.addEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF document.addEventListener( 'mousewheel', onDocumentMouseScroll, false ); } + else { + document.removeEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF + document.removeEventListener( 'mousewheel', onDocumentMouseScroll, false ); + } + // 3D links if( config.rollingLinks ) { - // Add some 3D magic to our anchors - linkify(); + enable3DLinks(); + } + else { + disable3DLinks(); } + + // 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]; + + if( config.theme !== themeName ) { + themeURL = themeURL.replace(themeFinder, config.theme); + dom.theme.setAttribute( 'href', themeURL ); + } + } + + // Force a layout to make sure the current config is accounted for + layout(); + } + /** + * Binds all event listeners. + */ function addEventListeners() { - document.addEventListener( 'touchstart', onDocumentTouchStart, false ); - document.addEventListener( 'touchmove', onDocumentTouchMove, false ); - document.addEventListener( 'touchend', onDocumentTouchEnd, false ); + + eventsAreBound = true; + window.addEventListener( 'hashchange', onWindowHashChange, false ); + window.addEventListener( 'resize', onWindowResize, false ); + + if( config.touch ) { + document.addEventListener( 'touchstart', onDocumentTouchStart, false ); + document.addEventListener( 'touchmove', onDocumentTouchMove, false ); + document.addEventListener( 'touchend', onDocumentTouchEnd, false ); + } if( config.keyboard ) { document.addEventListener( 'keydown', onDocumentKeyDown, false ); } + if ( config.progress && dom.progress ) { + dom.progress.addEventListener( 'click', onProgressClicked, false ); + } + if ( config.controls && dom.controls ) { - dom.controlsLeft.addEventListener( 'click', preventAndForward( navigateLeft ), false ); - dom.controlsRight.addEventListener( 'click', preventAndForward( navigateRight ), false ); - dom.controlsUp.addEventListener( 'click', preventAndForward( navigateUp ), false ); - dom.controlsDown.addEventListener( 'click', preventAndForward( navigateDown ), false ); + var actionEvent = 'ontouchstart' in window && window.ontouchstart != null ? 'touchstart' : 'click'; + dom.controlsLeft.forEach( function( el ) { el.addEventListener( actionEvent, onNavigateLeftClicked, false ); } ); + dom.controlsRight.forEach( function( el ) { el.addEventListener( actionEvent, onNavigateRightClicked, false ); } ); + dom.controlsUp.forEach( function( el ) { el.addEventListener( actionEvent, onNavigateUpClicked, false ); } ); + dom.controlsDown.forEach( function( el ) { el.addEventListener( actionEvent, onNavigateDownClicked, false ); } ); + dom.controlsPrev.forEach( function( el ) { el.addEventListener( actionEvent, onNavigatePrevClicked, false ); } ); + dom.controlsNext.forEach( function( el ) { el.addEventListener( actionEvent, onNavigateNextClicked, false ); } ); } + } + /** + * Unbinds all event listeners. + */ function removeEventListeners() { + + eventsAreBound = false; + document.removeEventListener( 'keydown', onDocumentKeyDown, false ); - document.removeEventListener( 'touchstart', onDocumentTouchStart, false ); - document.removeEventListener( 'touchmove', onDocumentTouchMove, false ); - document.removeEventListener( 'touchend', onDocumentTouchEnd, false ); window.removeEventListener( 'hashchange', onWindowHashChange, false ); - + window.removeEventListener( 'resize', onWindowResize, false ); + + if( config.touch ) { + document.removeEventListener( 'touchstart', onDocumentTouchStart, false ); + document.removeEventListener( 'touchmove', onDocumentTouchMove, false ); + document.removeEventListener( 'touchend', onDocumentTouchEnd, false ); + } + + if ( config.progress && dom.progress ) { + dom.progress.removeEventListener( 'click', onProgressClicked, false ); + } + if ( config.controls && dom.controls ) { - dom.controlsLeft.removeEventListener( 'click', preventAndForward( navigateLeft ), false ); - dom.controlsRight.removeEventListener( 'click', preventAndForward( navigateRight ), false ); - dom.controlsUp.removeEventListener( 'click', preventAndForward( navigateUp ), false ); - dom.controlsDown.removeEventListener( 'click', preventAndForward( navigateDown ), false ); + var actionEvent = 'ontouchstart' in window && window.ontouchstart != null ? 'touchstart' : 'click'; + dom.controlsLeft.forEach( function( el ) { el.removeEventListener( actionEvent, onNavigateLeftClicked, false ); } ); + dom.controlsRight.forEach( function( el ) { el.removeEventListener( actionEvent, onNavigateRightClicked, false ); } ); + dom.controlsUp.forEach( function( el ) { el.removeEventListener( actionEvent, onNavigateUpClicked, false ); } ); + dom.controlsDown.forEach( function( el ) { el.removeEventListener( actionEvent, onNavigateDownClicked, false ); } ); + dom.controlsPrev.forEach( function( el ) { el.removeEventListener( actionEvent, onNavigatePrevClicked, false ); } ); + dom.controlsNext.forEach( function( el ) { el.removeEventListener( actionEvent, onNavigateNextClicked, false ); } ); } + } /** - * Extend object a with the properties of object b. + * Extend object a with the properties of object b. * If there's a conflict, object b takes precedence. */ function extend( a, b ) { + for( var i in b ) { a[ i ] = b[ i ]; } + + } + + /** + * Converts the target object to an array. + */ + function toArray( o ) { + + return Array.prototype.slice.call( o ); + } /** * Measures the distance in pixels between point a - * and point b. - * + * and point b. + * * @param {Object} a point with x/y properties * @param {Object} b point with x/y properties */ function distanceBetween( a, b ) { + var dx = a.x - b.x, dy = a.y - b.y; return Math.sqrt( dx*dx + dy*dy ); - } - /** - * Prevents an events defaults behavior calls the - * specified delegate. - * - * @param {Function} delegate The method to call - * after the wrapper has been executed - */ - function preventAndForward( delegate ) { - return function( event ) { - event.preventDefault(); - delegate.call(); - } } /** - * Causes the address bar to hide on mobile devices, + * Causes the address bar to hide on mobile devices, * more vertical space ftw. */ function removeAddressBar() { - setTimeout( function() { - window.scrollTo( 0, 1 ); - }, 0 ); - } - - /** - * Handler for the document level 'keydown' event. - * - * @param {Object} event - */ - function onDocumentKeyDown( event ) { - // FFT: Use document.querySelector( ':focus' ) === null - // instead of checking contentEditable? - - // Disregard the event if the target is editable or a - // modifier is present - if ( event.target.contentEditable != 'inherit' || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey ) return; - - var triggered = false; - - switch( event.keyCode ) { - // p, page up - case 80: case 33: navigatePrev(); triggered = true; break; - // n, page down - case 78: case 34: navigateNext(); triggered = true; break; - // h, left - case 72: case 37: navigateLeft(); triggered = true; break; - // l, right - case 76: case 39: navigateRight(); triggered = true; break; - // k, up - case 75: case 38: navigateUp(); triggered = true; break; - // j, down - case 74: case 40: navigateDown(); triggered = true; break; - // home - case 36: navigateTo( 0 ); triggered = true; break; - // end - case 35: navigateTo( Number.MAX_VALUE ); triggered = true; break; - // space - case 32: overviewIsActive() ? deactivateOverview() : navigateNext(); triggered = true; break; - // return - case 13: if( overviewIsActive() ) { deactivateOverview(); triggered = true; } break; - } - // If the input resulted in a triggered action we should prevent - // the browsers default behavior - if( triggered ) { - event.preventDefault(); + if( window.orientation === 0 ) { + document.documentElement.style.overflow = 'scroll'; + document.body.style.height = '120%'; } - else if ( event.keyCode === 27 && supports3DTransforms ) { - toggleOverview(); - - event.preventDefault(); + else { + document.documentElement.style.overflow = ''; + document.body.style.height = '100%'; } - // If auto-sliding is enabled we need to cue up - // another timeout - cueAutoSlide(); + setTimeout( function() { + window.scrollTo( 0, 1 ); + }, 10 ); } /** - * Handler for the document level 'touchstart' event, - * enables support for swipe and pinch gestures. + * Dispatches an event of the specified type from the + * reveal DOM element. */ - function onDocumentTouchStart( event ) { - touch.startX = event.touches[0].clientX; - touch.startY = event.touches[0].clientY; - touch.startCount = event.touches.length; + function dispatchEvent( type, properties ) { + + var event = document.createEvent( "HTMLEvents", 1, 2 ); + event.initEvent( type, true, true ); + extend( event, properties ); + dom.wrapper.dispatchEvent( event ); - // If there's two touches we need to memorize the distance - // between those two points to detect pinching - if( event.touches.length === 2 ) { - touch.startSpan = distanceBetween( { - x: event.touches[1].clientX, - y: event.touches[1].clientY - }, { - x: touch.startX, - y: touch.startY - } ); - } } - + /** - * Handler for the document level 'touchmove' event. + * Wrap all links in 3D goodness. */ - function onDocumentTouchMove( event ) { - // Each touch should only trigger one action - if( !touch.handled ) { - var currentX = event.touches[0].clientX; - var currentY = event.touches[0].clientY; + function enable3DLinks() { - // If the touch started off with two points and still has - // two active touches; test for the pinch gesture - if( event.touches.length === 2 && touch.startCount === 2 ) { + if( supports3DTransforms && !( 'msPerspective' in document.body.style ) ) { + var anchors = document.querySelectorAll( SLIDES_SELECTOR + ' a:not(.image)' ); - // The current distance in pixels between the two touch points - var currentSpan = distanceBetween( { - x: event.touches[1].clientX, - y: event.touches[1].clientY - }, { - x: touch.startX, - y: touch.startY - } ); + for( var i = 0, len = anchors.length; i < len; i++ ) { + var anchor = anchors[i]; - // If the span is larger than the desire amount we've got - // ourselves a pinch - if( Math.abs( touch.startSpan - currentSpan ) > touch.threshold ) { - touch.handled = true; + if( anchor.textContent && !anchor.querySelector( '*' ) && ( !anchor.className || !anchor.classList.contains( anchor, 'roll' ) ) ) { + var span = document.createElement('span'); + span.setAttribute('data-title', anchor.text); + span.innerHTML = anchor.innerHTML; - if( currentSpan < touch.startSpan ) { - activateOverview(); - } - else { - deactivateOverview(); - } + anchor.classList.add( 'roll' ); + anchor.innerHTML = ''; + anchor.appendChild(span); } - } - // There was only one touch point, look for a swipe - else if( event.touches.length === 1 ) { - var deltaX = currentX - touch.startX, - deltaY = currentY - touch.startY; + } - if( deltaX > touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) { - touch.handled = true; - navigateLeft(); - } - else if( deltaX < -touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) { - touch.handled = true; - navigateRight(); - } - else if( deltaY > touch.threshold ) { - touch.handled = true; - navigateUp(); - } - else if( deltaY < -touch.threshold ) { - touch.handled = true; - navigateDown(); - } - } + } - event.preventDefault(); + /** + * Unwrap all 3D links. + */ + function disable3DLinks() { + + var anchors = document.querySelectorAll( SLIDES_SELECTOR + ' a.roll' ); + + for( var i = 0, len = anchors.length; i < len; i++ ) { + var anchor = anchors[i]; + var span = anchor.querySelector( 'span' ); + + if( span ) { + anchor.classList.remove( 'roll' ); + anchor.innerHTML = span.innerHTML; + } } + } /** - * Handler for the document level 'touchend' event. + * 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 onDocumentTouchEnd( event ) { - touch.handled = false; + 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 + } /** - * Handles mouse wheel scrolling, throttled to avoid - * skipping multiple slides. + * Applies JavaScript-controlled layout rules to the + * presentation. */ - function onDocumentMouseScroll( event ){ - clearTimeout( mouseWheelTimeout ); + function layout() { - mouseWheelTimeout = setTimeout( function() { - var delta = event.detail || -event.wheelDelta; - if( delta > 0 ) { - navigateNext(); + if( dom.wrapper ) { + + // Available space to scale within + var availableWidth = dom.wrapper.offsetWidth, + availableHeight = dom.wrapper.offsetHeight; + + // Reduce available space by margin + availableWidth -= ( availableHeight * config.margin ); + availableHeight -= ( availableHeight * config.margin ); + + // Dimensions of the content + var slideWidth = config.width, + slideHeight = config.height; + + // Slide width may be a percentage of available width + if( typeof slideWidth === 'string' && /%$/.test( slideWidth ) ) { + slideWidth = parseInt( slideWidth, 10 ) / 100 * availableWidth; + } + + // Slide height may be a percentage of available height + if( typeof slideHeight === 'string' && /%$/.test( slideHeight ) ) { + slideHeight = parseInt( slideHeight, 10 ) / 100 * availableHeight; + } + + dom.slides.style.width = slideWidth + 'px'; + dom.slides.style.height = slideHeight + 'px'; + + // Determine scale of content to fit within available space + scale = Math.min( availableWidth / slideWidth, availableHeight / slideHeight ); + + // Respect max/min scale settings + scale = Math.max( scale, config.minScale ); + scale = Math.min( scale, config.maxScale ); + + // Prefer applying scale via zoom since Chrome blurs scaled content + // with nested transforms + if( typeof dom.slides.style.zoom !== 'undefined' && !navigator.userAgent.match( /(iphone|ipod|ipad|android)/gi ) ) { + dom.slides.style.zoom = scale; } + // Apply scale transform as a fallback else { - navigatePrev(); + var transform = 'translate(-50%, -50%) scale('+ scale +') translate(50%, 50%)'; + + dom.slides.style.WebkitTransform = transform; + dom.slides.style.MozTransform = transform; + dom.slides.style.msTransform = transform; + dom.slides.style.OTransform = transform; + dom.slides.style.transform = transform; } - }, 100 ); + + // Select all slides, vertical and horizontal + var slides = toArray( document.querySelectorAll( SLIDES_SELECTOR ) ); + + for( var i = 0, len = slides.length; i < len; i++ ) { + var slide = slides[ i ]; + + // Don't bother updating invisible slides + if( slide.style.display === 'none' ) { + continue; + } + + if( config.center ) { + // Vertical stacks are not centred since their section + // children will be + if( slide.classList.contains( 'stack' ) ) { + slide.style.top = 0; + } + else { + slide.style.top = Math.max( - ( slide.offsetHeight / 2 ) - 20, -slideHeight / 2 ) + 'px'; + } + } + else { + slide.style.top = ''; + } + + } + + } + } - + /** - * Handler for the window level 'hashchange' event. - * - * @param {Object} event + * Stores the vertical index of a stack so that the same + * vertical slide can be selected when navigating to and + * from the stack. + * + * @param {HTMLElement} stack The vertical stack element + * @param {int} v Index to memorize */ - function onWindowHashChange( event ) { - readURL(); + function setPreviousVerticalIndex( stack, v ) { + + if( stack ) { + stack.setAttribute( 'data-previous-indexv', v || 0 ); + } + } /** - * Wrap all links in 3D goodness. + * Retrieves the vertical index which was stored using + * #setPreviousVerticalIndex() or 0 if no previous index + * exists. + * + * @param {HTMLElement} stack The vertical stack element */ - function linkify() { - if( supports3DTransforms && !( 'msPerspective' in document.body.style ) ) { - var nodes = document.querySelectorAll( '.reveal .slides section a:not(.image)' ); - - for( var i = 0, len = nodes.length; i < len; i++ ) { - var node = nodes[i]; - - if( node.textContent && !node.querySelector( 'img' ) && ( !node.className || !node.classList.contains( node, 'roll' ) ) ) { - node.classList.add( 'roll' ); - node.innerHTML = '<span data-title="'+ node.text +'">' + node.innerHTML + '</span>'; - } - }; - } + function getPreviousVerticalIndex( stack ) { + + if( stack && stack.classList.contains( 'stack' ) ) { + return parseInt( stack.getAttribute( 'data-previous-indexv' ) || 0, 10 ); + } + + return 0; + } /** - * Displays the overview of slides (quick nav) by + * Displays the overview of slides (quick nav) by * scaling down and arranging all slide elements. - * - * Experimental feature, might be dropped if perf + * + * Experimental feature, might be dropped if perf * can't be improved. */ function activateOverview() { - - dom.wrapper.classList.add( 'overview' ); - - var horizontalSlides = Array.prototype.slice.call( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ); - - for( var i = 0, len1 = horizontalSlides.length; i < len1; i++ ) { - var hslide = horizontalSlides[i], - htransform = 'translateZ(-2500px) translate(' + ( ( i - indexh ) * 105 ) + '%, 0%)'; - - hslide.setAttribute( 'data-index-h', i ); - hslide.style.display = 'block'; - hslide.style.WebkitTransform = htransform; - hslide.style.MozTransform = htransform; - hslide.style.msTransform = htransform; - hslide.style.OTransform = htransform; - hslide.style.transform = htransform; - - if( !hslide.classList.contains( 'stack' ) ) { - // Navigate to this slide on click - hslide.addEventListener( 'click', onOverviewSlideClicked, true ); - } - - var verticalSlides = Array.prototype.slice.call( hslide.querySelectorAll( 'section' ) ); - - for( var j = 0, len2 = verticalSlides.length; j < len2; j++ ) { - var vslide = verticalSlides[j], - vtransform = 'translate(0%, ' + ( ( j - indexv ) * 105 ) + '%)'; - - vslide.setAttribute( 'data-index-h', i ); - vslide.setAttribute( 'data-index-v', j ); - vslide.style.display = 'block'; - vslide.style.WebkitTransform = vtransform; - vslide.style.MozTransform = vtransform; - vslide.style.msTransform = vtransform; - vslide.style.OTransform = vtransform; - vslide.style.transform = vtransform; - - // Navigate to this slide on click - vslide.addEventListener( 'click', onOverviewSlideClicked, true ); - } - + + // Only proceed if enabled in config + if( config.overview ) { + + // Don't auto-slide while in overview mode + cancelAutoSlide(); + + var wasActive = dom.wrapper.classList.contains( 'overview' ); + + dom.wrapper.classList.add( 'overview' ); + dom.wrapper.classList.remove( 'exit-overview' ); + + clearTimeout( activateOverviewTimeout ); + clearTimeout( deactivateOverviewTimeout ); + + // Not the pretties solution, but need to let the overview + // class apply first so that slides are measured accurately + // before we can position them + activateOverviewTimeout = setTimeout( function(){ + + var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ); + + for( var i = 0, len1 = horizontalSlides.length; i < len1; i++ ) { + var hslide = horizontalSlides[i], + htransform = 'translateZ(-2500px) translate(' + ( ( i - indexh ) * 105 ) + '%, 0%)'; + + hslide.setAttribute( 'data-index-h', i ); + hslide.style.display = 'block'; + hslide.style.WebkitTransform = htransform; + hslide.style.MozTransform = htransform; + hslide.style.msTransform = htransform; + hslide.style.OTransform = htransform; + hslide.style.transform = htransform; + + if( hslide.classList.contains( 'stack' ) ) { + + var verticalSlides = hslide.querySelectorAll( 'section' ); + + for( var j = 0, len2 = verticalSlides.length; j < len2; j++ ) { + var verticalIndex = i === indexh ? indexv : getPreviousVerticalIndex( hslide ); + + var vslide = verticalSlides[j], + vtransform = 'translate(0%, ' + ( ( j - verticalIndex ) * 105 ) + '%)'; + + vslide.setAttribute( 'data-index-h', i ); + vslide.setAttribute( 'data-index-v', j ); + vslide.style.display = 'block'; + vslide.style.WebkitTransform = vtransform; + vslide.style.MozTransform = vtransform; + vslide.style.msTransform = vtransform; + vslide.style.OTransform = vtransform; + vslide.style.transform = vtransform; + + // Navigate to this slide on click + vslide.addEventListener( 'click', onOverviewSlideClicked, true ); + } + + } + else { + + // Navigate to this slide on click + hslide.addEventListener( 'click', onOverviewSlideClicked, true ); + + } + } + + layout(); + + if( !wasActive ) { + // Notify observers of the overview showing + dispatchEvent( 'overviewshown', { + 'indexh': indexh, + 'indexv': indexv, + 'currentSlide': currentSlide + } ); + } + + }, 10 ); + } + } - + /** * Exits the slide overview and enters the currently * active slide. */ function deactivateOverview() { - dom.wrapper.classList.remove( 'overview' ); - var slides = Array.prototype.slice.call( document.querySelectorAll( '.reveal .slides section' ) ); + // Only proceed if enabled in config + if( config.overview ) { + + clearTimeout( activateOverviewTimeout ); + clearTimeout( deactivateOverviewTimeout ); + + dom.wrapper.classList.remove( 'overview' ); + + // Temporarily add a class so that transitions can do different things + // depending on whether they are exiting/entering overview, or just + // moving from slide to slide + dom.wrapper.classList.add( 'exit-overview' ); + + deactivateOverviewTimeout = setTimeout( function () { + dom.wrapper.classList.remove( 'exit-overview' ); + }, 10); + + // Select all slides + var slides = toArray( document.querySelectorAll( SLIDES_SELECTOR ) ); - for( var i = 0, len = slides.length; i < len; i++ ) { - var element = slides[i]; + for( var i = 0, len = slides.length; i < len; i++ ) { + var element = slides[i]; - // Resets all transforms to use the external styles - element.style.WebkitTransform = ''; - element.style.MozTransform = ''; - element.style.msTransform = ''; - element.style.OTransform = ''; - element.style.transform = ''; + element.style.display = ''; - element.removeEventListener( 'click', onOverviewSlideClicked ); + // Resets all transforms to use the external styles + element.style.WebkitTransform = ''; + element.style.MozTransform = ''; + element.style.msTransform = ''; + element.style.OTransform = ''; + element.style.transform = ''; + + element.removeEventListener( 'click', onOverviewSlideClicked, true ); + } + + slide( indexh, indexv ); + + cueAutoSlide(); + + // Notify observers of the overview hiding + dispatchEvent( 'overviewhidden', { + 'indexh': indexh, + 'indexv': indexv, + 'currentSlide': currentSlide + } ); + + } + } + + /** + * Toggles the slide overview mode on and off. + * + * @param {Boolean} override Optional flag which overrides the + * toggle logic and forcibly sets the desired state. True means + * overview is open, false means it's closed. + */ + function toggleOverview( override ) { + + if( typeof override === 'boolean' ) { + override ? activateOverview() : deactivateOverview(); + } + else { + isOverview() ? deactivateOverview() : activateOverview(); } - slide(); } /** * Checks if the overview is currently active. - * + * * @return {Boolean} true if the overview is active, * false otherwise */ - function overviewIsActive() { + function isOverview() { + return dom.wrapper.classList.contains( 'overview' ); + } /** - * Invoked when a slide is and we're in the overview. + * Handling the fullscreen functionality via the fullscreen API + * + * @see http://fullscreen.spec.whatwg.org/ + * @see https://developer.mozilla.org/en-US/docs/DOM/Using_fullscreen_mode */ - function onOverviewSlideClicked( event ) { - // TODO There's a bug here where the event listeners are not - // removed after deactivating the overview. - if( overviewIsActive() ) { - event.preventDefault(); + function enterFullscreen() { + + var element = document.body; + + // Check which implementation is available + var requestMethod = element.requestFullScreen || + element.webkitRequestFullScreen || + element.mozRequestFullScreen || + element.msRequestFullScreen; + + if( requestMethod ) { + requestMethod.apply( element ); + } + + } + + /** + * Enters the paused mode which fades everything on screen to + * black. + */ + function pause() { + + var wasPaused = dom.wrapper.classList.contains( 'paused' ); - deactivateOverview(); + cancelAutoSlide(); + dom.wrapper.classList.add( 'paused' ); - indexh = this.getAttribute( 'data-index-h' ); - indexv = this.getAttribute( 'data-index-v' ); + if( wasPaused === false ) { + dispatchEvent( 'paused' ); + } + + } + + /** + * Exits from the paused mode. + */ + function resume() { + + var wasPaused = dom.wrapper.classList.contains( 'paused' ); + + cueAutoSlide(); + dom.wrapper.classList.remove( 'paused' ); + + if( wasPaused ) { + dispatchEvent( 'resumed' ); + } + + } + + /** + * Toggles the paused mode on and off. + */ + function togglePause() { - slide(); + if( isPaused() ) { + resume(); + } + else { + pause(); } + + } + + /** + * Checks if we are currently in the paused mode. + */ + function isPaused() { + + return dom.wrapper.classList.contains( 'paused' ); + + } + + /** + * Steps from the current point in the presentation to the + * slide which matches the specified horizontal and vertical + * indices. + * + * @param {int} h Horizontal index of the target slide + * @param {int} v Vertical index of the target slide + * @param {int} f Optional index of a fragment within the + * target slide to activate + */ + function slide( h, v, f ) { + + // Remember where we were at before + previousSlide = currentSlide; + + // Query all horizontal slides in the deck + var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ); + + // If no vertical index is specified and the upcoming slide is a + // stack, resume at its previous vertical index + if( v === undefined ) { + v = getPreviousVerticalIndex( horizontalSlides[ h ] ); + } + + // If we were on a vertical stack, remember what vertical index + // it was on so we can resume at the same position when returning + if( previousSlide && previousSlide.parentNode && previousSlide.parentNode.classList.contains( 'stack' ) ) { + setPreviousVerticalIndex( previousSlide.parentNode, indexv ); + } + + // Remember the state before this slide + var stateBefore = state.concat(); + + // Reset the state array + state.length = 0; + + var indexhBefore = indexh, + indexvBefore = indexv; + + // Activate and transition to the new slide + indexh = updateSlides( HORIZONTAL_SLIDES_SELECTOR, h === undefined ? indexh : h ); + indexv = updateSlides( VERTICAL_SLIDES_SELECTOR, v === undefined ? indexv : v ); + + layout(); + + // Apply the new state + stateLoop: for( var i = 0, len = state.length; i < len; i++ ) { + // Check if this state existed on the previous slide. If it + // did, we will avoid adding it repeatedly + for( var j = 0; j < stateBefore.length; j++ ) { + if( stateBefore[j] === state[i] ) { + stateBefore.splice( j, 1 ); + continue stateLoop; + } + } + + document.documentElement.classList.add( state[i] ); + + // Dispatch custom event matching the state's name + dispatchEvent( state[i] ); + } + + // Clean up the remains of the previous state + while( stateBefore.length ) { + document.documentElement.classList.remove( stateBefore.pop() ); + } + + // If the overview is active, re-activate it to update positions + if( isOverview() ) { + activateOverview(); + } + + // Update the URL hash after a delay since updating it mid-transition + // is likely to cause visual lag + writeURL( 1500 ); + + // Find the current horizontal slide and any possible vertical slides + // within it + var currentHorizontalSlide = horizontalSlides[ indexh ], + 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' ); + } + } ); + } + + // Dispatch an event if the slide changed + if( indexh !== indexhBefore || indexv !== indexvBefore ) { + dispatchEvent( 'slidechanged', { + 'indexh': indexh, + 'indexv': indexv, + 'previousSlide': previousSlide, + 'currentSlide': currentSlide + } ); + } + else { + // Ensure that the previous slide is never the same as the current + previousSlide = null; + } + + // Solves an edge case where the previous slide maintains the + // 'present' class when navigating between adjacent vertical + // stacks + if( previousSlide ) { + previousSlide.classList.remove( 'present' ); + + // Reset all slides upon navigate to home + // Issue: #285 + if ( document.querySelector( HOME_SLIDE_SELECTOR ).classList.contains( 'present' ) ) { + // Launch async task + setTimeout( function () { + var slides = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.stack') ), i; + for( i in slides ) { + if( slides[i] ) { + // Reset stack + setPreviousVerticalIndex( slides[i], 0 ); + } + } + }, 0 ); + } + } + + updateControls(); + updateProgress(); + } /** * 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 * @param {Number} index The index of the slide that should be * shown - * + * * @return {Number} The index of the slide that is now shown, - * might differ from the passed in index if it was out of + * might differ from the passed in index if it was out of * bounds. */ function updateSlides( selector, index ) { - + // Select all slides and convert the NodeList result to // an array - var slides = Array.prototype.slice.call( document.querySelectorAll( selector ) ), + var slides = toArray( document.querySelectorAll( selector ) ), slidesLength = slides.length; - + if( slidesLength ) { // Should the index loop? @@ -593,21 +1163,21 @@ var Reveal = (function(){ index = slidesLength + index; } } - + // Enforce max and minimum index bounds index = Math.max( Math.min( index, slidesLength - 1 ), 0 ); - + for( var i = 0; i < slidesLength; i++ ) { - var slide = slides[i]; + var element = slides[i]; - // Optimization; hide all slides that are three or more steps + // Optimization; hide all slides that are three or more steps // away from the present slide - if( overviewIsActive() === false ) { + if( isOverview() === false ) { // The distance loops so that it measures 1 between the first // and last slides var distance = Math.abs( ( index - i ) % ( slidesLength - 3 ) ) || 0; - slide.style.display = distance > 3 ? 'none' : 'block'; + element.style.display = distance > 3 ? 'none' : 'block'; } slides[i].classList.remove( 'past' ); @@ -624,7 +1194,7 @@ var Reveal = (function(){ } // If this element contains vertical slides - if( slide.querySelector( 'section' ) ) { + if( element.querySelector( 'section' ) ) { slides[i].classList.add( 'stack' ); } } @@ -638,138 +1208,118 @@ var Reveal = (function(){ if( slideState ) { state = state.concat( slideState.split( ' ' ) ); } + + // If this slide has a data-autoslide attribtue 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; + } + } else { - // Since there are no slides we can't be anywhere beyond the + // Since there are no slides we can't be anywhere beyond the // zeroth index index = 0; } - + return index; - + } - + /** - * Updates the visual slides to represent the currently - * set indices. + * Updates the progress bar to reflect the current slide. */ - function slide( h, v, origin ) { - // Remember where we were at before - previousSlide = currentSlide; + function updateProgress() { - // Remember the state before this slide - var stateBefore = state.concat(); + // Update progress if enabled + if( config.progress && dom.progress ) { - // Reset the state array - state.length = 0; + var horizontalSlides = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ); - var indexhBefore = indexh, - indexvBefore = indexv; + // The number of past and total slides + var totalCount = document.querySelectorAll( SLIDES_SELECTOR + ':not(.stack)' ).length; + var pastCount = 0; - // Activate and transition to the new slide - indexh = updateSlides( HORIZONTAL_SLIDES_SELECTOR, h === undefined ? indexh : h ); - indexv = updateSlides( VERTICAL_SLIDES_SELECTOR, v === undefined ? indexv : v ); - - // Apply the new state - stateLoop: for( var i = 0, len = state.length; i < len; i++ ) { - // Check if this state existed on the previous slide. If it - // did, we will avoid adding it repeatedly. - for( var j = 0; j < stateBefore.length; j++ ) { - if( stateBefore[j] === state[i] ) { - stateBefore.splice( j, 1 ); - continue stateLoop; - } - } + // Step through all slides and count the past ones + mainLoop: for( var i = 0; i < horizontalSlides.length; i++ ) { - document.documentElement.classList.add( state[i] ); + var horizontalSlide = horizontalSlides[i]; + var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) ); - // Dispatch custom event matching the state's name - dispatchEvent( state[i] ); - } + for( var j = 0; j < verticalSlides.length; j++ ) { - // Clean up the remaints of the previous state - while( stateBefore.length ) { - document.documentElement.classList.remove( stateBefore.pop() ); - } + // Stop as soon as we arrive at the present + if( verticalSlides[j].classList.contains( 'present' ) ) { + break mainLoop; + } - // Update progress if enabled - if( config.progress && dom.progress ) { - dom.progressbar.style.width = ( indexh / ( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ).length - 1 ) ) * window.innerWidth + 'px'; - } + pastCount++; - // Close the overview if it's active - if( overviewIsActive() ) { - activateOverview(); - } + } - updateControls(); - - clearTimeout( writeURLTimeout ); - writeURLTimeout = setTimeout( writeURL, 1500 ); + // Stop as soon as we arrive at the present + if( horizontalSlide.classList.contains( 'present' ) ) { + break; + } - // Query all horizontal slides in the deck - var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ); + // Don't count the wrapping section for vertical slides + if( horizontalSlide.classList.contains( 'stack' ) === false ) { + pastCount++; + } - // Find the current horizontal slide and any possible vertical slides - // within it - var currentHorizontalSlide = horizontalSlides[ indexh ], - currentVerticalSlides = currentHorizontalSlide.querySelectorAll( 'section' ); + } - // Store references to the previous and current slides - currentSlide = currentVerticalSlides[ indexv ] || currentHorizontalSlide; + dom.progressbar.style.width = ( pastCount / ( totalCount - 1 ) ) * window.innerWidth + 'px'; - // Dispatch an event if the slide changed - if( indexh !== indexhBefore || indexv !== indexvBefore ) { - dispatchEvent( 'slidechanged', { - 'origin': origin, - 'indexh': indexh, - 'indexv': indexv, - 'previousSlide': previousSlide, - 'currentSlide': currentSlide - } ); - } - else { - // Ensure that the previous slide is never the same as the current - previousSlide = null; } - // Solves an edge case where the previous slide maintains the - // 'present' class when navigating between adjacent vertical - // stacks - if( previousSlide ) { - previousSlide.classList.remove( 'present' ); - } } /** - * Updates the state and link pointers of the controls. + * Updates the state of all control/navigation arrows. */ function updateControls() { - if ( !config.controls || !dom.controls ) { - return; - } - - var routes = availableRoutes(); - // Remove the 'enabled' class from all directions - [ dom.controlsLeft, dom.controlsRight, dom.controlsUp, dom.controlsDown ].forEach( function( node ) { - node.classList.remove( 'enabled' ); - } ) + if ( config.controls && dom.controls ) { + + var routes = availableRoutes(); + + // Remove the 'enabled' class from all directions + dom.controlsLeft.concat( dom.controlsRight ) + .concat( dom.controlsUp ) + .concat( dom.controlsDown ) + .concat( dom.controlsPrev ) + .concat( dom.controlsNext ).forEach( function( node ) { + node.classList.remove( 'enabled' ); + } ); + + // Add the 'enabled' class to the available routes + if( routes.left ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'enabled' ); } ); + if( routes.right ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'enabled' ); } ); + if( routes.up ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'enabled' ); } ); + if( routes.down ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'enabled' ); } ); + + // Prev/next buttons + if( routes.left || routes.up ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'enabled' ); } ); + if( routes.right || routes.down ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'enabled' ); } ); + + } - if( routes.left ) dom.controlsLeft.classList.add( 'enabled' ); - if( routes.right ) dom.controlsRight.classList.add( 'enabled' ); - if( routes.up ) dom.controlsUp.classList.add( 'enabled' ); - if( routes.down ) dom.controlsDown.classList.add( 'enabled' ); } /** * Determine what available routes there are for navigation. - * + * * @return {Object} containing four booleans: left/right/up/down */ function availableRoutes() { - var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ); - var verticalSlides = document.querySelectorAll( VERTICAL_SLIDES_SELECTOR ); + + var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ), + verticalSlides = document.querySelectorAll( VERTICAL_SLIDES_SELECTOR ); return { left: indexh > 0, @@ -777,60 +1327,132 @@ var Reveal = (function(){ up: indexv > 0, down: indexv < verticalSlides.length - 1 }; + } - + /** * Reads the current URL (hash) and navigates accordingly. */ function readURL() { - // Break the hash down to separate components - var bits = window.location.hash.slice(2).split('/'); - // Read the index components of the hash - var h = parseInt( bits[0] ) || 0 ; - var v = parseInt( bits[1] ) || 0 ; + var hash = window.location.hash; + + // Attempt to parse the hash as either an index or name + var bits = hash.slice( 2 ).split( '/' ), + name = hash.replace( /#|\//gi, '' ); + + // If the first bit is invalid and there is a name we can + // assume that this is a named link + if( isNaN( parseInt( bits[0], 10 ) ) && name.length ) { + // Find the slide with the specified name + var element = document.querySelector( '#' + name ); + + if( element ) { + // Find the position of the named slide and navigate to it + var indices = Reveal.getIndices( element ); + slide( indices.h, indices.v ); + } + // If the slide doesn't exist, navigate to the current slide + else { + slide( indexh, indexv ); + } + } + else { + // Read the index components of the hash + var h = parseInt( bits[0], 10 ) || 0, + v = parseInt( bits[1], 10 ) || 0; + + slide( h, v ); + } - navigateTo( h, v ); } - + /** * Updates the page URL (hash) to reflect the current - * state. + * state. + * + * @param {Number} delay The time in ms to wait before + * writing the hash */ - function writeURL() { + function writeURL( delay ) { + if( config.history ) { - var url = '/'; - - // Only include the minimum possible number of components in - // the URL - if( indexh > 0 || indexv > 0 ) url += indexh; - if( indexv > 0 ) url += '/' + indexv; - - window.location.hash = url; + + // Make sure there's never more than one timeout running + clearTimeout( writeURLTimeout ); + + // If a delay is specified, timeout this call + if( typeof delay === 'number' ) { + writeURLTimeout = setTimeout( writeURL, delay ); + } + else { + var url = '/'; + + // If the current slide has an ID, use that as a named link + if( currentSlide && typeof currentSlide.getAttribute( 'id' ) === 'string' ) { + url = '/' + currentSlide.getAttribute( 'id' ); + } + // Otherwise use the /h/v index + else { + if( indexh > 0 || indexv > 0 ) url += indexh; + if( indexv > 0 ) url += '/' + indexv; + } + + window.location.hash = url; + } } + } /** - * Dispatches an event of the specified type from the - * reveal DOM element. + * Retrieves the h/v location of the current, or specified, + * slide. + * + * @param {HTMLElement} slide If specified, the returned + * index will be for this slide rather than the currently + * active one + * + * @return {Object} { h: <int>, v: <int> } */ - function dispatchEvent( type, properties ) { - var event = document.createEvent( "HTMLEvents", 1, 2 ); - event.initEvent( type, true, true ); - extend( event, properties ); - dom.wrapper.dispatchEvent( event ); + function getIndices( slide ) { + + // By default, return the current indices + var h = indexh, + v = indexv; + + // If a slide is specified, return the indices of that slide + if( slide ) { + var isVertical = !!slide.parentNode.nodeName.match( /section/gi ); + var slideh = isVertical ? slide.parentNode : slide; + + // Select all horizontal slides + var horizontalSlides = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ); + + // Now that we know which the horizontal slide is, get its index + h = Math.max( horizontalSlides.indexOf( slideh ), 0 ); + + // If this is a vertical slide, grab the vertical index + if( isVertical ) { + v = Math.max( toArray( slide.parentNode.querySelectorAll( 'section' ) ).indexOf( slide ), 0 ); + } + } + + return { h: h, v: v }; + } /** * Navigate to the next slide fragment. - * + * * @return {Boolean} true if there was a next fragment, * false otherwise */ function nextFragment() { + // Vertical slides: if( document.querySelector( VERTICAL_SLIDES_SELECTOR + '.present' ) ) { - var verticalFragments = document.querySelectorAll( VERTICAL_SLIDES_SELECTOR + '.present .fragment:not(.visible)' ); + var verticalFragments = sortFragments( document.querySelectorAll( VERTICAL_SLIDES_SELECTOR + '.present .fragment:not(.visible)' ) ); + if( verticalFragments.length ) { verticalFragments[0].classList.add( 'visible' ); @@ -841,7 +1463,8 @@ var Reveal = (function(){ } // Horizontal slides: else { - var horizontalFragments = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.present .fragment:not(.visible)' ); + var horizontalFragments = sortFragments( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.present .fragment:not(.visible)' ) ); + if( horizontalFragments.length ) { horizontalFragments[0].classList.add( 'visible' ); @@ -852,18 +1475,21 @@ var Reveal = (function(){ } return false; + } /** * Navigate to the previous slide fragment. - * + * * @return {Boolean} true if there was a previous fragment, * false otherwise */ function previousFragment() { + // Vertical slides: if( document.querySelector( VERTICAL_SLIDES_SELECTOR + '.present' ) ) { - var verticalFragments = document.querySelectorAll( VERTICAL_SLIDES_SELECTOR + '.present .fragment.visible' ); + var verticalFragments = sortFragments( document.querySelectorAll( VERTICAL_SLIDES_SELECTOR + '.present .fragment.visible' ) ); + if( verticalFragments.length ) { verticalFragments[ verticalFragments.length - 1 ].classList.remove( 'visible' ); @@ -874,7 +1500,8 @@ var Reveal = (function(){ } // Horizontal slides: else { - var horizontalFragments = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.present .fragment.visible' ); + var horizontalFragments = sortFragments( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.present .fragment.visible' ) ); + if( horizontalFragments.length ) { horizontalFragments[ horizontalFragments.length - 1 ].classList.remove( 'visible' ); @@ -883,52 +1510,68 @@ var Reveal = (function(){ return true; } } - + return false; + } + /** + * Cues a new automated slide if enabled in the config. + */ function cueAutoSlide() { + clearTimeout( autoSlideTimeout ); // Cue the next auto-slide if enabled - if( config.autoSlide ) { - autoSlideTimeout = setTimeout( navigateNext, config.autoSlide ); + if( autoSlide && !isPaused() && !isOverview() ) { + autoSlideTimeout = setTimeout( navigateNext, autoSlide ); } + } - + /** - * Triggers a navigation to the specified indices. - * - * @param {Number} h The horizontal index of the slide to show - * @param {Number} v The vertical index of the slide to show + * Cancels any ongoing request to auto-slide. */ - function navigateTo( h, v, origin ) { - slide( h, v, origin ); + function cancelAutoSlide() { + + clearTimeout( autoSlideTimeout ); + } - + function navigateLeft() { + // Prioritize hiding fragments - if( overviewIsActive() || previousFragment() === false ) { - slide( indexh - 1, 0 ); + if( availableRoutes().left && isOverview() || previousFragment() === false ) { + slide( indexh - 1 ); } + } + function navigateRight() { + // Prioritize revealing fragments - if( overviewIsActive() || nextFragment() === false ) { - slide( indexh + 1, 0 ); + if( availableRoutes().right && isOverview() || nextFragment() === false ) { + slide( indexh + 1 ); } + } + function navigateUp() { + // Prioritize hiding fragments - if( overviewIsActive() || previousFragment() === false ) { + if( availableRoutes().up && isOverview() || previousFragment() === false ) { slide( indexh, indexv - 1 ); } + } + function navigateDown() { + // Prioritize revealing fragments - if( overviewIsActive() || nextFragment() === false ) { + if( availableRoutes().down && isOverview() || nextFragment() === false ) { slide( indexh, indexv + 1 ); } + } /** @@ -938,6 +1581,7 @@ var Reveal = (function(){ * 3) Previous horizontal slide */ function navigatePrev() { + // Prioritize revealing fragments if( previousFragment() === false ) { if( availableRoutes().up ) { @@ -945,75 +1589,388 @@ var Reveal = (function(){ } else { // Fetch the previous horizontal slide, if there is one - var previousSlide = document.querySelector( '.reveal .slides>section.past:nth-child(' + indexh + ')' ); + var previousSlide = document.querySelector( HORIZONTAL_SLIDES_SELECTOR + '.past:nth-child(' + indexh + ')' ); if( previousSlide ) { - indexv = ( previousSlide.querySelectorAll('section').length + 1 ) || 0; + indexv = ( previousSlide.querySelectorAll( 'section' ).length + 1 ) || undefined; indexh --; slide(); } } } + } /** * Same as #navigatePrev() but navigates forwards. */ function navigateNext() { + // Prioritize revealing fragments if( nextFragment() === false ) { availableRoutes().down ? navigateDown() : navigateRight(); } - // If auto-sliding is enabled we need to cue up + // If auto-sliding is enabled we need to cue up // another timeout cueAutoSlide(); + } + + // --------------------------------------------------------------------// + // ----------------------------- EVENTS -------------------------------// + // --------------------------------------------------------------------// + + /** - * Toggles the slide overview mode on and off. + * Handler for the document level 'keydown' event. + * + * @param {Object} event */ - function toggleOverview() { - if( overviewIsActive() ) { - deactivateOverview(); + function onDocumentKeyDown( 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' ) ); + + // Disregard the event if there's a focused element or a + // keyboard modifier key is present + if( hasFocus || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey ) return; + + var triggered = true; + + // while paused only allow "unpausing" keyboard events (b and .) + if( isPaused() && [66,190,191].indexOf( event.keyCode ) === -1 ) { + return false; } - else { - activateOverview(); + + switch( event.keyCode ) { + // p, page up + case 80: case 33: navigatePrev(); break; + // n, page down + case 78: case 34: navigateNext(); break; + // h, left + case 72: case 37: navigateLeft(); break; + // l, right + case 76: case 39: navigateRight(); break; + // k, up + case 75: case 38: navigateUp(); break; + // j, down + case 74: case 40: navigateDown(); break; + // home + case 36: slide( 0 ); break; + // end + case 35: slide( Number.MAX_VALUE ); break; + // space + case 32: isOverview() ? deactivateOverview() : navigateNext(); break; + // return + case 13: isOverview() ? deactivateOverview() : triggered = false; break; + // b, period, Logitech presenter tools "black screen" button + case 66: case 190: case 191: togglePause(); break; + // f + case 70: enterFullscreen(); break; + default: + triggered = false; + } + + // If the input resulted in a triggered action we should prevent + // the browsers default behavior + if( triggered ) { + event.preventDefault(); + } + else if ( event.keyCode === 27 && supports3DTransforms ) { + toggleOverview(); + + event.preventDefault(); + } + + // If auto-sliding is enabled we need to cue up + // another timeout + cueAutoSlide(); + + } + + /** + * Handler for the document level 'touchstart' event, + * enables support for swipe and pinch gestures. + */ + function onDocumentTouchStart( event ) { + + touch.startX = event.touches[0].clientX; + touch.startY = event.touches[0].clientY; + touch.startCount = event.touches.length; + + // If there's two touches we need to memorize the distance + // between those two points to detect pinching + if( event.touches.length === 2 && config.overview ) { + touch.startSpan = distanceBetween( { + x: event.touches[1].clientX, + y: event.touches[1].clientY + }, { + x: touch.startX, + y: touch.startY + } ); + } + + } + + /** + * Handler for the document level 'touchmove' event. + */ + function onDocumentTouchMove( event ) { + + // Each touch should only trigger one action + if( !touch.handled ) { + var currentX = event.touches[0].clientX; + var currentY = event.touches[0].clientY; + + // If the touch started off with two points and still has + // two active touches; test for the pinch gesture + if( event.touches.length === 2 && touch.startCount === 2 && config.overview ) { + + // The current distance in pixels between the two touch points + var currentSpan = distanceBetween( { + x: event.touches[1].clientX, + y: event.touches[1].clientY + }, { + x: touch.startX, + y: touch.startY + } ); + + // If the span is larger than the desire amount we've got + // ourselves a pinch + if( Math.abs( touch.startSpan - currentSpan ) > touch.threshold ) { + touch.handled = true; + + if( currentSpan < touch.startSpan ) { + activateOverview(); + } + else { + deactivateOverview(); + } + } + + event.preventDefault(); + + } + // There was only one touch point, look for a swipe + else if( event.touches.length === 1 && touch.startCount !== 2 ) { + + var deltaX = currentX - touch.startX, + deltaY = currentY - touch.startY; + + if( deltaX > touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) { + touch.handled = true; + navigateLeft(); + } + else if( deltaX < -touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) { + touch.handled = true; + navigateRight(); + } + else if( deltaY > touch.threshold ) { + touch.handled = true; + navigateUp(); + } + else if( deltaY < -touch.threshold ) { + touch.handled = true; + navigateDown(); + } + + event.preventDefault(); + + } + } + // There's a bug with swiping on some Android devices unless + // the default action is always prevented + else if( navigator.userAgent.match( /android/gi ) ) { + event.preventDefault(); + } + + } + + /** + * Handler for the document level 'touchend' event. + */ + function onDocumentTouchEnd( event ) { + + touch.handled = false; + + } + + /** + * Handles mouse wheel scrolling, throttled to avoid skipping + * multiple slides. + */ + function onDocumentMouseScroll( event ) { + + clearTimeout( mouseWheelTimeout ); + + mouseWheelTimeout = setTimeout( function() { + var delta = event.detail || -event.wheelDelta; + if( delta > 0 ) { + navigateNext(); + } + else { + navigatePrev(); + } + }, 100 ); + + } + + /** + * Clicking on the progress bar results in a navigation to the + * closest approximate horizontal slide using this equation: + * + * ( clickX / presentationWidth ) * numberOfSlides + */ + function onProgressClicked( event ) { + + event.preventDefault(); + + var slidesTotal = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).length; + var slideIndex = Math.floor( ( event.clientX / dom.wrapper.offsetWidth ) * slidesTotal ); + + slide( slideIndex ); + + } + + /** + * 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(); } + + /** + * Handler for the window level 'hashchange' event. + */ + function onWindowHashChange( event ) { + + readURL(); + + } + + /** + * Handler for the window level 'resize' event. + */ + function onWindowResize( event ) { + + layout(); + + } + + /** + * 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 + // removed after deactivating the overview. + if( eventsAreBound && isOverview() ) { + event.preventDefault(); + + var element = event.target; + + while( element && !element.nodeName.match( /section/gi ) ) { + element = element.parentNode; + } + + if( element && !element.classList.contains( 'disabled' ) ) { + + deactivateOverview(); + + if( element.nodeName.match( /section/gi ) ) { + var h = parseInt( element.getAttribute( 'data-index-h' ), 10 ), + v = parseInt( element.getAttribute( 'data-index-v' ), 10 ); + + slide( h, v ); + } + + } } + } - - // Expose some methods publicly + + + // --------------------------------------------------------------------// + // ------------------------------- API --------------------------------// + // --------------------------------------------------------------------// + + return { initialize: initialize, - navigateTo: navigateTo, + configure: configure, + + // Navigation methods + slide: slide, + left: navigateLeft, + right: navigateRight, + up: navigateUp, + down: navigateDown, + prev: navigatePrev, + next: navigateNext, + prevFragment: previousFragment, + nextFragment: nextFragment, + + // Deprecated aliases + navigateTo: slide, navigateLeft: navigateLeft, navigateRight: navigateRight, navigateUp: navigateUp, navigateDown: navigateDown, navigatePrev: navigatePrev, navigateNext: navigateNext, + + // Forces an update in slide layout + layout: layout, + + // Toggles the overview mode on/off toggleOverview: toggleOverview, + // Toggles the "black screen" mode on/off + togglePause: togglePause, + + // State checks + isOverview: isOverview, + isPaused: isPaused, + // Adds or removes all internal event listeners (such as keyboard) addEventListeners: addEventListeners, removeEventListeners: removeEventListeners, - // Returns the indices of the current slide - getIndices: function() { - return { - h: indexh, - v: indexv - }; + // Returns the indices of the current, or specified, slide + getIndices: getIndices, + + // Returns the slide at the specified index, y is optional + getSlide: function( x, y ) { + var horizontalSlide = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR )[ x ]; + var verticalSlides = horizontalSlide && horizontalSlide.querySelectorAll( 'section' ); + + if( typeof y !== 'undefined' ) { + return verticalSlides ? verticalSlides[ y ] : undefined; + } + + return horizontalSlide; }, // Returns the previous slide element, may be null getPreviousSlide: function() { - return previousSlide + return previousSlide; }, // Returns the current slide element getCurrentSlide: function() { - return currentSlide + return currentSlide; + }, + + // Returns the current scale of the presentation content + getScale: function() { + return scale; }, // Helper method, retrieves query string as a key/value hash @@ -1027,6 +1984,21 @@ var Reveal = (function(){ return query; }, + // Returns true if we're currently on the first slide + isFirstSlide: function() { + return document.querySelector( SLIDES_SELECTOR + '.past' ) == null ? true : false; + }, + + // Returns true if we're currently on the last slide + isLastSlide: function() { + if( currentSlide && currentSlide.classList.contains( '.stack' ) ) { + return currentSlide.querySelector( SLIDES_SELECTOR + '.future' ) == null ? true : false; + } + else { + return document.querySelector( SLIDES_SELECTOR + '.future' ) == null ? true : false; + } + }, + // Forward event binding to the reveal DOM element addEventListener: function( type, listener, useCapture ) { if( 'addEventListener' in window ) { @@ -1039,6 +2011,5 @@ var Reveal = (function(){ } } }; - -})(); +})(); diff --git a/js/reveal.min.js b/js/reveal.min.js new file mode 100644 index 0000000..98019d6 --- /dev/null +++ b/js/reveal.min.js @@ -0,0 +1,8 @@ +/*! + * reveal.js 2.3.0 (2013-02-28, 17:03) + * http://lab.hakim.se/reveal-js + * MIT licensed + * + * Copyright (C) 2013 Hakim El Hattab, http://hakim.se + */ +var Reveal=function(){"use strict";function e(e){return bt||wt?(window.addEventListener("load",h,!1),c(ft,e),n(),r(),void 0):(document.body.setAttribute("class","no-transforms"),void 0)}function t(){if(Lt.theme=document.querySelector("#theme"),Lt.wrapper=document.querySelector(".reveal"),Lt.slides=document.querySelector(".reveal .slides"),!Lt.wrapper.querySelector(".progress")&&ft.progress){var e=document.createElement("div");e.classList.add("progress"),e.innerHTML="<span></span>",Lt.wrapper.appendChild(e)}if(!Lt.wrapper.querySelector(".controls")&&ft.controls){var t=document.createElement("aside");t.classList.add("controls"),t.innerHTML='<div class="navigate-left"></div><div class="navigate-right"></div><div class="navigate-up"></div><div class="navigate-down"></div>',Lt.wrapper.appendChild(t)}if(!Lt.wrapper.querySelector(".state-background")){var n=document.createElement("div");n.classList.add("state-background"),Lt.wrapper.appendChild(n)}if(!Lt.wrapper.querySelector(".pause-overlay")){var r=document.createElement("div");r.classList.add("pause-overlay"),Lt.wrapper.appendChild(r)}Lt.progress=document.querySelector(".reveal .progress"),Lt.progressbar=document.querySelector(".reveal .progress span"),ft.controls&&(Lt.controls=document.querySelector(".reveal .controls"),Lt.controlsLeft=l(document.querySelectorAll(".navigate-left")),Lt.controlsRight=l(document.querySelectorAll(".navigate-right")),Lt.controlsUp=l(document.querySelectorAll(".navigate-up")),Lt.controlsDown=l(document.querySelectorAll(".navigate-down")),Lt.controlsPrev=l(document.querySelectorAll(".navigate-prev")),Lt.controlsNext=l(document.querySelectorAll(".navigate-next")))}function n(){/iphone|ipod|android/gi.test(navigator.userAgent)&&!/crios/gi.test(navigator.userAgent)&&(window.addEventListener("load",u,!1),window.addEventListener("orientationchange",u,!1))}function r(){function e(){n.length&&head.js.apply(null,n),o()}for(var t=[],n=[],r=0,s=ft.dependencies.length;s>r;r++){var a=ft.dependencies[r];(!a.condition||a.condition())&&(a.async?n.push(a.src):t.push(a.src),"function"==typeof a.callback&&head.ready(a.src.match(/([\w\d_\-]*)\.?js$|[^\\\/]*$/i)[0],a.callback))}t.length?(head.ready(e),head.js.apply(null,t)):e()}function o(){t(),a(),s(),P(),R(),setTimeout(function(){v("ready",{indexh:pt,indexv:ht,currentSlide:ct})},1)}function s(e){if(Lt.wrapper.classList.remove(ft.transition),"object"==typeof e&&c(ft,e),wt===!1&&(ft.transition="linear"),Lt.wrapper.classList.add(ft.transition),Lt.controls&&(Lt.controls.style.display=ft.controls&&Lt.controls?"block":"none"),Lt.progress&&(Lt.progress.style.display=ft.progress&&Lt.progress?"block":"none"),ft.rtl?Lt.wrapper.classList.add("rtl"):Lt.wrapper.classList.remove("rtl"),ft.center?Lt.wrapper.classList.add("center"):Lt.wrapper.classList.remove("center"),ft.mouseWheel?(document.addEventListener("DOMMouseScroll",V,!1),document.addEventListener("mousewheel",V,!1)):(document.removeEventListener("DOMMouseScroll",V,!1),document.removeEventListener("mousewheel",V,!1)),ft.rollingLinks?f():m(),ft.theme&&Lt.theme){var t=Lt.theme.getAttribute("href"),n=/[^\/]*?(?=\.css)/,r=t.match(n)[0];ft.theme!==r&&(t=t.replace(n,ft.theme),Lt.theme.setAttribute("href",t))}h()}function a(){if(xt=!0,window.addEventListener("hashchange",ot,!1),window.addEventListener("resize",st,!1),ft.touch&&(document.addEventListener("touchstart",Z,!1),document.addEventListener("touchmove",_,!1),document.addEventListener("touchend",Q,!1)),ft.keyboard&&document.addEventListener("keydown",$,!1),ft.progress&&Lt.progress&&Lt.progress.addEventListener("click",B,!1),ft.controls&&Lt.controls){var e="ontouchstart"in window&&null!=window.ontouchstart?"touchstart":"click";Lt.controlsLeft.forEach(function(t){t.addEventListener(e,G,!1)}),Lt.controlsRight.forEach(function(t){t.addEventListener(e,J,!1)}),Lt.controlsUp.forEach(function(t){t.addEventListener(e,et,!1)}),Lt.controlsDown.forEach(function(t){t.addEventListener(e,tt,!1)}),Lt.controlsPrev.forEach(function(t){t.addEventListener(e,nt,!1)}),Lt.controlsNext.forEach(function(t){t.addEventListener(e,rt,!1)})}}function i(){if(xt=!1,document.removeEventListener("keydown",$,!1),window.removeEventListener("hashchange",ot,!1),window.removeEventListener("resize",st,!1),ft.touch&&(document.removeEventListener("touchstart",Z,!1),document.removeEventListener("touchmove",_,!1),document.removeEventListener("touchend",Q,!1)),ft.progress&&Lt.progress&&Lt.progress.removeEventListener("click",B,!1),ft.controls&&Lt.controls){var e="ontouchstart"in window&&null!=window.ontouchstart?"touchstart":"click";Lt.controlsLeft.forEach(function(t){t.removeEventListener(e,G,!1)}),Lt.controlsRight.forEach(function(t){t.removeEventListener(e,J,!1)}),Lt.controlsUp.forEach(function(t){t.removeEventListener(e,et,!1)}),Lt.controlsDown.forEach(function(t){t.removeEventListener(e,tt,!1)}),Lt.controlsPrev.forEach(function(t){t.removeEventListener(e,nt,!1)}),Lt.controlsNext.forEach(function(t){t.removeEventListener(e,rt,!1)})}}function c(e,t){for(var n in t)e[n]=t[n]}function l(e){return Array.prototype.slice.call(e)}function d(e,t){var n=e.x-t.x,r=e.y-t.y;return Math.sqrt(n*n+r*r)}function u(){0===window.orientation?(document.documentElement.style.overflow="scroll",document.body.style.height="120%"):(document.documentElement.style.overflow="",document.body.style.height="100%"),setTimeout(function(){window.scrollTo(0,1)},10)}function v(e,t){var n=document.createEvent("HTMLEvents",1,2);n.initEvent(e,!0,!0),c(n,t),Lt.wrapper.dispatchEvent(n)}function f(){if(wt&&!("msPerspective"in document.body.style))for(var e=document.querySelectorAll(lt+" a:not(.image)"),t=0,n=e.length;n>t;t++){var r=e[t];if(!(!r.textContent||r.querySelector("*")||r.className&&r.classList.contains(r,"roll"))){var o=document.createElement("span");o.setAttribute("data-title",r.text),o.innerHTML=r.innerHTML,r.classList.add("roll"),r.innerHTML="",r.appendChild(o)}}}function m(){for(var e=document.querySelectorAll(lt+" a.roll"),t=0,n=e.length;n>t;t++){var r=e[t],o=r.querySelector("span");o&&(r.classList.remove("roll"),r.innerHTML=o.innerHTML)}}function p(e){var t=l(e);return t.forEach(function(e,t){e.hasAttribute("data-fragment-index")||e.setAttribute("data-fragment-index",t)}),t.sort(function(e,t){return e.getAttribute("data-fragment-index")-t.getAttribute("data-fragment-index")}),t}function h(){if(Lt.wrapper){var e=Lt.wrapper.offsetWidth,t=Lt.wrapper.offsetHeight;e-=t*ft.margin,t-=t*ft.margin;var n=ft.width,r=ft.height;if("string"==typeof n&&/%$/.test(n)&&(n=parseInt(n,10)/100*e),"string"==typeof r&&/%$/.test(r)&&(r=parseInt(r,10)/100*t),Lt.slides.style.width=n+"px",Lt.slides.style.height=r+"px",yt=Math.min(e/n,t/r),yt=Math.max(yt,ft.minScale),yt=Math.min(yt,ft.maxScale),void 0===Lt.slides.style.zoom||navigator.userAgent.match(/(iphone|ipod|ipad|android)/gi)){var o="translate(-50%, -50%) scale("+yt+") translate(50%, 50%)";Lt.slides.style.WebkitTransform=o,Lt.slides.style.MozTransform=o,Lt.slides.style.msTransform=o,Lt.slides.style.OTransform=o,Lt.slides.style.transform=o}else Lt.slides.style.zoom=yt;for(var s=l(document.querySelectorAll(lt)),a=0,i=s.length;i>a;a++){var c=s[a];"none"!==c.style.display&&(c.style.top=ft.center?c.classList.contains("stack")?0:Math.max(-(c.offsetHeight/2)-20,-r/2)+"px":"")}}}function g(e,t){e&&e.setAttribute("data-previous-indexv",t||0)}function y(e){return e&&e.classList.contains("stack")?parseInt(e.getAttribute("data-previous-indexv")||0,10):0}function L(){if(ft.overview){W();var e=Lt.wrapper.classList.contains("overview");Lt.wrapper.classList.add("overview"),Lt.wrapper.classList.remove("exit-overview"),clearTimeout(At),clearTimeout(kt),At=setTimeout(function(){for(var t=document.querySelectorAll(dt),n=0,r=t.length;r>n;n++){var o=t[n],s="translateZ(-2500px) translate("+105*(n-pt)+"%, 0%)";if(o.setAttribute("data-index-h",n),o.style.display="block",o.style.WebkitTransform=s,o.style.MozTransform=s,o.style.msTransform=s,o.style.OTransform=s,o.style.transform=s,o.classList.contains("stack"))for(var a=o.querySelectorAll("section"),i=0,c=a.length;c>i;i++){var l=n===pt?ht:y(o),d=a[i],u="translate(0%, "+105*(i-l)+"%)";d.setAttribute("data-index-h",n),d.setAttribute("data-index-v",i),d.style.display="block",d.style.WebkitTransform=u,d.style.MozTransform=u,d.style.msTransform=u,d.style.OTransform=u,d.style.transform=u,d.addEventListener("click",at,!0)}else o.addEventListener("click",at,!0)}h(),e||v("overviewshown",{indexh:pt,indexv:ht,currentSlide:ct})},10)}}function w(){if(ft.overview){clearTimeout(At),clearTimeout(kt),Lt.wrapper.classList.remove("overview"),Lt.wrapper.classList.add("exit-overview"),kt=setTimeout(function(){Lt.wrapper.classList.remove("exit-overview")},10);for(var e=l(document.querySelectorAll(lt)),t=0,n=e.length;n>t;t++){var r=e[t];r.style.display="",r.style.WebkitTransform="",r.style.MozTransform="",r.style.msTransform="",r.style.OTransform="",r.style.transform="",r.removeEventListener("click",at,!0)}T(pt,ht),R(),v("overviewhidden",{indexh:pt,indexv:ht,currentSlide:ct})}}function b(e){"boolean"==typeof e?e?L():w():E()?w():L()}function E(){return Lt.wrapper.classList.contains("overview")}function S(){var e=document.body,t=e.requestFullScreen||e.webkitRequestFullScreen||e.mozRequestFullScreen||e.msRequestFullScreen;t&&t.apply(e)}function q(){var e=Lt.wrapper.classList.contains("paused");W(),Lt.wrapper.classList.add("paused"),e===!1&&v("paused")}function A(){var e=Lt.wrapper.classList.contains("paused");R(),Lt.wrapper.classList.remove("paused"),e&&v("resumed")}function k(){x()?A():q()}function x(){return Lt.wrapper.classList.contains("paused")}function T(e,t,n){it=ct;var r=document.querySelectorAll(dt);void 0===t&&(t=y(r[e])),it&&it.parentNode&&it.parentNode.classList.contains("stack")&&g(it.parentNode,ht);var o=gt.concat();gt.length=0;var s=pt,a=ht;pt=M(dt,void 0===e?pt:e),ht=M(ut,void 0===t?ht:t),h();e:for(var i=0,c=gt.length;c>i;i++){for(var d=0;o.length>d;d++)if(o[d]===gt[i]){o.splice(d,1);continue e}document.documentElement.classList.add(gt[i]),v(gt[i])}for(;o.length;)document.documentElement.classList.remove(o.pop());E()&&L(),O(1500);var u=r[pt],f=u.querySelectorAll("section");if(ct=f[ht]||u,n!==void 0){var m=p(ct.querySelectorAll(".fragment"));l(m).forEach(function(e,t){n>t?e.classList.add("visible"):e.classList.remove("visible")})}pt!==s||ht!==a?v("slidechanged",{indexh:pt,indexv:ht,previousSlide:it,currentSlide:ct}):it=null,it&&(it.classList.remove("present"),document.querySelector(vt).classList.contains("present")&&setTimeout(function(){var e,t=l(document.querySelectorAll(dt+".stack"));for(e in t)t[e]&&g(t[e],0)},0)),N(),D()}function M(e,t){var n=l(document.querySelectorAll(e)),r=n.length;if(r){ft.loop&&(t%=r,0>t&&(t=r+t)),t=Math.max(Math.min(t,r-1),0);for(var o=0;r>o;o++){var s=n[o];if(E()===!1){var a=Math.abs((t-o)%(r-3))||0;s.style.display=a>3?"none":"block"}n[o].classList.remove("past"),n[o].classList.remove("present"),n[o].classList.remove("future"),t>o?n[o].classList.add("past"):o>t&&n[o].classList.add("future"),s.querySelector("section")&&n[o].classList.add("stack")}n[t].classList.add("present");var i=n[t].getAttribute("data-state");i&&(gt=gt.concat(i.split(" ")));var c=n[t].getAttribute("data-autoslide");mt=c?parseInt(c,10):ft.autoSlide}else t=0;return t}function D(){if(ft.progress&&Lt.progress){var e=l(document.querySelectorAll(dt)),t=document.querySelectorAll(lt+":not(.stack)").length,n=0;e:for(var r=0;e.length>r;r++){for(var o=e[r],s=l(o.querySelectorAll("section")),a=0;s.length>a;a++){if(s[a].classList.contains("present"))break e;n++}if(o.classList.contains("present"))break;o.classList.contains("stack")===!1&&n++}Lt.progressbar.style.width=n/(t-1)*window.innerWidth+"px"}}function N(){if(ft.controls&&Lt.controls){var e=C();Lt.controlsLeft.concat(Lt.controlsRight).concat(Lt.controlsUp).concat(Lt.controlsDown).concat(Lt.controlsPrev).concat(Lt.controlsNext).forEach(function(e){e.classList.remove("enabled")}),e.left&&Lt.controlsLeft.forEach(function(e){e.classList.add("enabled")}),e.right&&Lt.controlsRight.forEach(function(e){e.classList.add("enabled")}),e.up&&Lt.controlsUp.forEach(function(e){e.classList.add("enabled")}),e.down&&Lt.controlsDown.forEach(function(e){e.classList.add("enabled")}),(e.left||e.up)&&Lt.controlsPrev.forEach(function(e){e.classList.add("enabled")}),(e.right||e.down)&&Lt.controlsNext.forEach(function(e){e.classList.add("enabled")})}}function C(){var e=document.querySelectorAll(dt),t=document.querySelectorAll(ut);return{left:pt>0,right:e.length-1>pt,up:ht>0,down:t.length-1>ht}}function P(){var e=window.location.hash,t=e.slice(2).split("/"),n=e.replace(/#|\//gi,"");if(isNaN(parseInt(t[0],10))&&n.length){var r=document.querySelector("#"+n);if(r){var o=Reveal.getIndices(r);T(o.h,o.v)}else T(pt,ht)}else{var s=parseInt(t[0],10)||0,a=parseInt(t[1],10)||0;T(s,a)}}function O(e){if(ft.history)if(clearTimeout(qt),"number"==typeof e)qt=setTimeout(O,e);else{var t="/";ct&&"string"==typeof ct.getAttribute("id")?t="/"+ct.getAttribute("id"):((pt>0||ht>0)&&(t+=pt),ht>0&&(t+="/"+ht)),window.location.hash=t}}function z(e){var t=pt,n=ht;if(e){var r=!!e.parentNode.nodeName.match(/section/gi),o=r?e.parentNode:e,s=l(document.querySelectorAll(dt));t=Math.max(s.indexOf(o),0),r&&(n=Math.max(l(e.parentNode.querySelectorAll("section")).indexOf(e),0))}return{h:t,v:n}}function H(){if(document.querySelector(ut+".present")){var e=p(document.querySelectorAll(ut+".present .fragment:not(.visible)"));if(e.length)return e[0].classList.add("visible"),v("fragmentshown",{fragment:e[0]}),!0}else{var t=p(document.querySelectorAll(dt+".present .fragment:not(.visible)"));if(t.length)return t[0].classList.add("visible"),v("fragmentshown",{fragment:t[0]}),!0}return!1}function I(){if(document.querySelector(ut+".present")){var e=p(document.querySelectorAll(ut+".present .fragment.visible"));if(e.length)return e[e.length-1].classList.remove("visible"),v("fragmenthidden",{fragment:e[e.length-1]}),!0}else{var t=p(document.querySelectorAll(dt+".present .fragment.visible"));if(t.length)return t[t.length-1].classList.remove("visible"),v("fragmenthidden",{fragment:t[t.length-1]}),!0}return!1}function R(){clearTimeout(St),!mt||x()||E()||(St=setTimeout(K,mt))}function W(){clearTimeout(St)}function X(){(C().left&&E()||I()===!1)&&T(pt-1)}function Y(){(C().right&&E()||H()===!1)&&T(pt+1)}function F(){(C().up&&E()||I()===!1)&&T(pt,ht-1)}function U(){(C().down&&E()||H()===!1)&&T(pt,ht+1)}function j(){if(I()===!1)if(C().up)F();else{var e=document.querySelector(dt+".past:nth-child("+pt+")");e&&(ht=e.querySelectorAll("section").length+1||void 0,pt--,T())}}function K(){H()===!1&&(C().down?U():Y()),R()}function $(e){document.activeElement;var t=!(!document.activeElement||!document.activeElement.type&&!document.activeElement.href&&"inherit"===document.activeElement.contentEditable);if(!(t||e.shiftKey||e.altKey||e.ctrlKey||e.metaKey)){var n=!0;if(x()&&-1===[66,190,191].indexOf(e.keyCode))return!1;switch(e.keyCode){case 80:case 33:j();break;case 78:case 34:K();break;case 72:case 37:X();break;case 76:case 39:Y();break;case 75:case 38:F();break;case 74:case 40:U();break;case 36:T(0);break;case 35:T(Number.MAX_VALUE);break;case 32:E()?w():K();break;case 13:E()?w():n=!1;break;case 66:case 190:case 191:k();break;case 70:S();break;default:n=!1}n?e.preventDefault():27===e.keyCode&&wt&&(b(),e.preventDefault()),R()}}function Z(e){Tt.startX=e.touches[0].clientX,Tt.startY=e.touches[0].clientY,Tt.startCount=e.touches.length,2===e.touches.length&&ft.overview&&(Tt.startSpan=d({x:e.touches[1].clientX,y:e.touches[1].clientY},{x:Tt.startX,y:Tt.startY}))}function _(e){if(Tt.handled)navigator.userAgent.match(/android/gi)&&e.preventDefault();else{var t=e.touches[0].clientX,n=e.touches[0].clientY;if(2===e.touches.length&&2===Tt.startCount&&ft.overview){var r=d({x:e.touches[1].clientX,y:e.touches[1].clientY},{x:Tt.startX,y:Tt.startY});Math.abs(Tt.startSpan-r)>Tt.threshold&&(Tt.handled=!0,Tt.startSpan>r?L():w()),e.preventDefault()}else if(1===e.touches.length&&2!==Tt.startCount){var o=t-Tt.startX,s=n-Tt.startY;o>Tt.threshold&&Math.abs(o)>Math.abs(s)?(Tt.handled=!0,X()):-Tt.threshold>o&&Math.abs(o)>Math.abs(s)?(Tt.handled=!0,Y()):s>Tt.threshold?(Tt.handled=!0,F()):-Tt.threshold>s&&(Tt.handled=!0,U()),e.preventDefault()}}}function Q(){Tt.handled=!1}function V(e){clearTimeout(Et),Et=setTimeout(function(){var t=e.detail||-e.wheelDelta;t>0?K():j()},100)}function B(e){e.preventDefault();var t=l(document.querySelectorAll(dt)).length,n=Math.floor(e.clientX/Lt.wrapper.offsetWidth*t);T(n)}function G(e){e.preventDefault(),X()}function J(e){e.preventDefault(),Y()}function et(e){e.preventDefault(),F()}function tt(e){e.preventDefault(),U()}function nt(e){e.preventDefault(),j()}function rt(e){e.preventDefault(),K()}function ot(){P()}function st(){h()}function at(e){if(xt&&E()){e.preventDefault();for(var t=e.target;t&&!t.nodeName.match(/section/gi);)t=t.parentNode;if(t&&!t.classList.contains("disabled")&&(w(),t.nodeName.match(/section/gi))){var n=parseInt(t.getAttribute("data-index-h"),10),r=parseInt(t.getAttribute("data-index-v"),10);T(n,r)}}}var it,ct,lt=".reveal .slides section",dt=".reveal .slides>section",ut=".reveal .slides>section.present>section",vt=".reveal .slides>section:first-child",ft={width:960,height:700,margin:.1,minScale:.2,maxScale:1,controls:!0,progress:!0,history:!1,keyboard:!0,overview:!0,center:!0,touch:!0,loop:!1,rtl:!1,autoSlide:0,mouseWheel:!1,rollingLinks:!0,theme:null,transition:"default",dependencies:[]},mt=ft.autoSlide,pt=0,ht=0,gt=[],yt=1,Lt={},wt="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,bt="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,Et=0,St=0,qt=0,At=0,kt=0,xt=!1,Tt={startX:0,startY:0,startSpan:0,startCount:0,handled:!1,threshold:80};return{initialize:e,configure:s,slide:T,left:X,right:Y,up:F,down:U,prev:j,next:K,prevFragment:I,nextFragment:H,navigateTo:T,navigateLeft:X,navigateRight:Y,navigateUp:F,navigateDown:U,navigatePrev:j,navigateNext:K,layout:h,toggleOverview:b,togglePause:k,isOverview:E,isPaused:x,addEventListeners:a,removeEventListeners:i,getIndices:z,getSlide:function(e,t){var n=document.querySelectorAll(dt)[e],r=n&&n.querySelectorAll("section");return t!==void 0?r?r[t]:void 0:n},getPreviousSlide:function(){return it},getCurrentSlide:function(){return ct},getScale:function(){return yt},getQueryHash:function(){var e={};return location.search.replace(/[A-Z0-9]+?=(\w*)/gi,function(t){e[t.split("=").shift()]=t.split("=").pop()}),e},isFirstSlide:function(){return null==document.querySelector(lt+".past")?!0:!1},isLastSlide:function(){return ct&&ct.classList.contains(".stack")?null==ct.querySelector(lt+".future")?!0:!1:null==document.querySelector(lt+".future")?!0:!1},addEventListener:function(e,t,n){"addEventListener"in window&&(Lt.wrapper||document.querySelector(".reveal")).addEventListener(e,t,n)},removeEventListener:function(e,t,n){"addEventListener"in window&&(Lt.wrapper||document.querySelector(".reveal")).removeEventListener(e,t,n)}}}();
\ No newline at end of file |