fix issue with auto-advance (closes #273)
[reveal.js.git] / js / reveal.js
1 /*!
2  * reveal.js
3  * http://lab.hakim.se/reveal-js
4  * MIT licensed
5  *
6  * Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se
7  */
8 var Reveal = (function(){
9
10         'use strict';
11
12         var SLIDES_SELECTOR = '.reveal .slides section',
13                 HORIZONTAL_SLIDES_SELECTOR = '.reveal .slides>section',
14                 VERTICAL_SLIDES_SELECTOR = '.reveal .slides>section.present>section',
15
16                 // Configurations defaults, can be overridden at initialization time
17                 config = {
18                         // Display controls in the bottom right corner
19                         controls: true,
20
21                         // Display a presentation progress bar
22                         progress: true,
23
24                         // Push each slide change to the browser history
25                         history: false,
26
27                         // Enable keyboard shortcuts for navigation
28                         keyboard: true,
29
30                         // Enable the slide overview mode
31                         overview: true,
32
33                         // Vertical centering of slides
34                         center: true,
35
36                         // Loop the presentation
37                         loop: false,
38
39                         // Change the presentation direction to be RTL
40                         rtl: false,
41
42                         // Number of milliseconds between automatically proceeding to the
43                         // next slide, disabled when set to 0, this value can be overwritten
44                         // by using a data-autoslide attribute on your slides
45                         autoSlide: 0,
46
47                         // Enable slide navigation via mouse wheel
48                         mouseWheel: false,
49
50                         // Apply a 3D roll to links on hover
51                         rollingLinks: true,
52
53                         // Transition style (see /css/theme)
54                         theme: null,
55
56                         // Transition style
57                         transition: 'default', // default/cube/page/concave/zoom/linear/none
58
59                         // Script dependencies to load
60                         dependencies: []
61                 },
62
63                 // Stores if the next slide should be shown automatically
64                 // after n milliseconds
65                 autoSlide = config.autoSlide,
66
67                 // The horizontal and verical index of the currently active slide
68                 indexh = 0,
69                 indexv = 0,
70
71                 // The previous and current slide HTML elements
72                 previousSlide,
73                 currentSlide,
74
75                 // Slides may hold a data-state attribute which we pick up and apply
76                 // as a class to the body. This list contains the combined state of
77                 // all current slides.
78                 state = [],
79
80                 // Cached references to DOM elements
81                 dom = {},
82
83                 // Detect support for CSS 3D transforms
84                 supports3DTransforms =  'WebkitPerspective' in document.body.style ||
85                                                                 'MozPerspective' in document.body.style ||
86                                                                 'msPerspective' in document.body.style ||
87                                                                 'OPerspective' in document.body.style ||
88                                                                 'perspective' in document.body.style,
89
90                 supports2DTransforms =  'WebkitTransform' in document.body.style ||
91                                                                 'MozTransform' in document.body.style ||
92                                                                 'msTransform' in document.body.style ||
93                                                                 'OTransform' in document.body.style ||
94                                                                 'transform' in document.body.style,
95
96                 // Throttles mouse wheel navigation
97                 mouseWheelTimeout = 0,
98
99                 // An interval used to automatically move on to the next slide
100                 autoSlideTimeout = 0,
101
102                 // Delays updates to the URL due to a Chrome thumbnailer bug
103                 writeURLTimeout = 0,
104
105                 // A delay used to ativate the overview mode
106                 activateOverviewTimeout = 0,
107
108                 // Holds information about the currently ongoing touch input
109                 touch = {
110                         startX: 0,
111                         startY: 0,
112                         startSpan: 0,
113                         startCount: 0,
114                         handled: false,
115                         threshold: 80
116                 };
117
118         /**
119          * Starts up the presentation if the client is capable.
120          */
121         function initialize( options ) {
122                 if( ( !supports2DTransforms && !supports3DTransforms ) ) {
123                         document.body.setAttribute( 'class', 'no-transforms' );
124
125                         // If the browser doesn't support core features we won't be
126                         // using JavaScript to control the presentation
127                         return;
128                 }
129
130                 // Force a layout when the whole page, incl fonts, has loaded
131                 window.addEventListener( 'load', layout, false );
132
133                 // Copy options over to our config object
134                 extend( config, options );
135
136                 // Hide the address bar in mobile browsers
137                 hideAddressBar();
138
139                 // Loads the dependencies and continues to #start() once done
140                 load();
141
142         }
143
144         /**
145          * Finds and stores references to DOM elements which are
146          * required by the presentation. If a required element is
147          * not found, it is created.
148          */
149         function setupDOM() {
150                 // Cache references to key DOM elements
151                 dom.theme = document.querySelector( '#theme' );
152                 dom.wrapper = document.querySelector( '.reveal' );
153                 dom.slides = document.querySelector( '.reveal .slides' );
154
155                 // Progress bar
156                 if( !dom.wrapper.querySelector( '.progress' ) && config.progress ) {
157                         var progressElement = document.createElement( 'div' );
158                         progressElement.classList.add( 'progress' );
159                         progressElement.innerHTML = '<span></span>';
160                         dom.wrapper.appendChild( progressElement );
161                 }
162
163                 // Arrow controls
164                 if( !dom.wrapper.querySelector( '.controls' ) && config.controls ) {
165                         var controlsElement = document.createElement( 'aside' );
166                         controlsElement.classList.add( 'controls' );
167                         controlsElement.innerHTML = '<div class="navigate-left"></div>' +
168                                                                                 '<div class="navigate-right"></div>' +
169                                                                                 '<div class="navigate-up"></div>' +
170                                                                                 '<div class="navigate-down"></div>';
171                         dom.wrapper.appendChild( controlsElement );
172                 }
173
174                 // Presentation background element
175                 if( !dom.wrapper.querySelector( '.state-background' ) ) {
176                         var backgroundElement = document.createElement( 'div' );
177                         backgroundElement.classList.add( 'state-background' );
178                         dom.wrapper.appendChild( backgroundElement );
179                 }
180
181                 // Overlay graphic which is displayed during the paused mode
182                 if( !dom.wrapper.querySelector( '.pause-overlay' ) ) {
183                         var pausedElement = document.createElement( 'div' );
184                         pausedElement.classList.add( 'pause-overlay' );
185                         dom.wrapper.appendChild( pausedElement );
186                 }
187
188                 // Cache references to elements
189                 dom.progress = document.querySelector( '.reveal .progress' );
190                 dom.progressbar = document.querySelector( '.reveal .progress span' );
191
192                 if ( config.controls ) {
193                         dom.controls = document.querySelector( '.reveal .controls' );
194
195                         // There can be multiple instances of controls throughout the page
196                         dom.controlsLeft = toArray( document.querySelectorAll( '.navigate-left' ) );
197                         dom.controlsRight = toArray( document.querySelectorAll( '.navigate-right' ) );
198                         dom.controlsUp = toArray( document.querySelectorAll( '.navigate-up' ) );
199                         dom.controlsDown = toArray( document.querySelectorAll( '.navigate-down' ) );
200                         dom.controlsPrev = toArray( document.querySelectorAll( '.navigate-prev' ) );
201                         dom.controlsNext = toArray( document.querySelectorAll( '.navigate-next' ) );
202                 }
203         }
204
205         /**
206          * Hides the address bar if we're on a mobile device.
207          */
208         function hideAddressBar() {
209                 if( navigator.userAgent.match( /(iphone|ipod)/i ) ) {
210                         // Give the page some scrollable overflow
211                         document.documentElement.style.overflow = 'scroll';
212                         document.body.style.height = '120%';
213
214                         // Events that should trigger the address bar to hide
215                         window.addEventListener( 'load', removeAddressBar, false );
216                         window.addEventListener( 'orientationchange', removeAddressBar, false );
217                 }
218         }
219
220         /**
221          * Loads the dependencies of reveal.js. Dependencies are
222          * defined via the configuration option 'dependencies'
223          * and will be loaded prior to starting/binding reveal.js.
224          * Some dependencies may have an 'async' flag, if so they
225          * will load after reveal.js has been started up.
226          */
227         function load() {
228                 var scripts = [],
229                         scriptsAsync = [];
230
231                 for( var i = 0, len = config.dependencies.length; i < len; i++ ) {
232                         var s = config.dependencies[i];
233
234                         // Load if there's no condition or the condition is truthy
235                         if( !s.condition || s.condition() ) {
236                                 if( s.async ) {
237                                         scriptsAsync.push( s.src );
238                                 }
239                                 else {
240                                         scripts.push( s.src );
241                                 }
242
243                                 // Extension may contain callback functions
244                                 if( typeof s.callback === 'function' ) {
245                                         head.ready( s.src.match( /([\w\d_\-]*)\.?js$|[^\\\/]*$/i )[0], s.callback );
246                                 }
247                         }
248                 }
249
250                 // Called once synchronous scritps finish loading
251                 function proceed() {
252                         if( scriptsAsync.length ) {
253                                 // Load asynchronous scripts
254                                 head.js.apply( null, scriptsAsync );
255                         }
256
257                         start();
258                 }
259
260                 if( scripts.length ) {
261                         head.ready( proceed );
262
263                         // Load synchronous scripts
264                         head.js.apply( null, scripts );
265                 }
266                 else {
267                         proceed();
268                 }
269         }
270
271         /**
272          * Starts up reveal.js by binding input events and navigating
273          * to the current URL deeplink if there is one.
274          */
275         function start() {
276                 // Make sure we've got all the DOM elements we need
277                 setupDOM();
278
279                 // Subscribe to input
280                 addEventListeners();
281
282                 // Updates the presentation to match the current configuration values
283                 configure();
284
285                 // Force an initial layout, will thereafter be invoked as the window
286                 // is resized
287                 layout();
288
289                 // Read the initial hash
290                 readURL();
291
292                 // Start auto-sliding if it's enabled
293                 cueAutoSlide();
294
295                 // Notify listeners that the presentation is ready but use a 1ms
296                 // timeout to ensure it's not fired synchronously after #initialize()
297                 setTimeout( function() {
298                         dispatchEvent( 'ready', {
299                                 'indexh': indexh,
300                                 'indexv': indexv,
301                                 'currentSlide': currentSlide
302                         } );
303                 }, 1 );
304         }
305
306         /**
307          * Applies the configuration settings from the config object.
308          */
309         function configure() {
310                 if( supports3DTransforms === false ) {
311                         config.transition = 'linear';
312                 }
313
314                 if( config.controls && dom.controls ) {
315                         dom.controls.style.display = 'block';
316                 }
317
318                 if( config.progress && dom.progress ) {
319                         dom.progress.style.display = 'block';
320                 }
321
322                 if( config.transition !== 'default' ) {
323                         dom.wrapper.classList.add( config.transition );
324                 }
325
326                 if( config.rtl ) {
327                         dom.wrapper.classList.add( 'rtl' );
328                 }
329
330                 if( config.center ) {
331                         dom.wrapper.classList.add( 'center' );
332                 }
333
334                 if( config.mouseWheel ) {
335                         document.addEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF
336                         document.addEventListener( 'mousewheel', onDocumentMouseScroll, false );
337                 }
338
339                 // 3D links
340                 if( config.rollingLinks ) {
341                         linkify();
342                 }
343
344                 // Load the theme in the config, if it's not already loaded
345                 if( config.theme && dom.theme ) {
346                         var themeURL = dom.theme.getAttribute( 'href' );
347                         var themeFinder = /[^\/]*?(?=\.css)/;
348                         var themeName = themeURL.match(themeFinder)[0];
349
350                         if(  config.theme !== themeName ) {
351                                 themeURL = themeURL.replace(themeFinder, config.theme);
352                                 dom.theme.setAttribute( 'href', themeURL );
353                         }
354                 }
355         }
356
357         /**
358          * Binds all event listeners.
359          */
360         function addEventListeners() {
361                 document.addEventListener( 'touchstart', onDocumentTouchStart, false );
362                 document.addEventListener( 'touchmove', onDocumentTouchMove, false );
363                 document.addEventListener( 'touchend', onDocumentTouchEnd, false );
364                 window.addEventListener( 'hashchange', onWindowHashChange, false );
365                 window.addEventListener( 'resize', onWindowResize, false );
366
367                 if( config.keyboard ) {
368                         document.addEventListener( 'keydown', onDocumentKeyDown, false );
369                 }
370
371                 if ( config.progress && dom.progress ) {
372                         dom.progress.addEventListener( 'click', preventAndForward( onProgressClick ), false );
373                 }
374
375                 if ( config.controls && dom.controls ) {
376                         var actionEvent = 'ontouchstart' in window ? 'touchstart' : 'click';
377                         dom.controlsLeft.forEach( function( el ) { el.addEventListener( actionEvent, preventAndForward( navigateLeft ), false ); } );
378                         dom.controlsRight.forEach( function( el ) { el.addEventListener( actionEvent, preventAndForward( navigateRight ), false ); } );
379                         dom.controlsUp.forEach( function( el ) { el.addEventListener( actionEvent, preventAndForward( navigateUp ), false ); } );
380                         dom.controlsDown.forEach( function( el ) { el.addEventListener( actionEvent, preventAndForward( navigateDown ), false ); } );
381                         dom.controlsPrev.forEach( function( el ) { el.addEventListener( actionEvent, preventAndForward( navigatePrev ), false ); } );
382                         dom.controlsNext.forEach( function( el ) { el.addEventListener( actionEvent, preventAndForward( navigateNext ), false ); } );
383                 }
384         }
385
386         /**
387          * Unbinds all event listeners.
388          */
389         function removeEventListeners() {
390                 document.removeEventListener( 'keydown', onDocumentKeyDown, false );
391                 document.removeEventListener( 'touchstart', onDocumentTouchStart, false );
392                 document.removeEventListener( 'touchmove', onDocumentTouchMove, false );
393                 document.removeEventListener( 'touchend', onDocumentTouchEnd, false );
394                 window.removeEventListener( 'hashchange', onWindowHashChange, false );
395                 window.removeEventListener( 'resize', onWindowResize, false );
396
397                 if ( config.progress && dom.progress ) {
398                         dom.progress.removeEventListener( 'click', preventAndForward( onProgressClick ), false );
399                 }
400
401                 if ( config.controls && dom.controls ) {
402                         var actionEvent = 'ontouchstart' in window ? 'touchstart' : 'click';
403                         dom.controlsLeft.forEach( function( el ) { el.removeEventListener( actionEvent, preventAndForward( navigateLeft ), false ); } );
404                         dom.controlsRight.forEach( function( el ) { el.removeEventListener( actionEvent, preventAndForward( navigateRight ), false ); } );
405                         dom.controlsUp.forEach( function( el ) { el.removeEventListener( actionEvent, preventAndForward( navigateUp ), false ); } );
406                         dom.controlsDown.forEach( function( el ) { el.removeEventListener( actionEvent, preventAndForward( navigateDown ), false ); } );
407                         dom.controlsPrev.forEach( function( el ) { el.removeEventListener( actionEvent, preventAndForward( navigatePrev ), false ); } );
408                         dom.controlsNext.forEach( function( el ) { el.removeEventListener( actionEvent, preventAndForward( navigateNext ), false ); } );
409                 }
410         }
411
412         /**
413          * Extend object a with the properties of object b.
414          * If there's a conflict, object b takes precedence.
415          */
416         function extend( a, b ) {
417                 for( var i in b ) {
418                         a[ i ] = b[ i ];
419                 }
420         }
421
422         /**
423          * Converts the target object to an array.
424          */
425         function toArray( o ) {
426                 return Array.prototype.slice.call( o );
427         }
428
429         function each( targets, method, args ) {
430                 targets.forEach( function( el ) {
431                         el[method].apply( el, args );
432                 } );
433         }
434
435         /**
436          * Measures the distance in pixels between point a
437          * and point b.
438          *
439          * @param {Object} a point with x/y properties
440          * @param {Object} b point with x/y properties
441          */
442         function distanceBetween( a, b ) {
443                 var dx = a.x - b.x,
444                         dy = a.y - b.y;
445
446                 return Math.sqrt( dx*dx + dy*dy );
447         }
448
449         /**
450          * Prevents an events defaults behavior calls the
451          * specified delegate.
452          *
453          * @param {Function} delegate The method to call
454          * after the wrapper has been executed
455          */
456         function preventAndForward( delegate ) {
457                 return function( event ) {
458                         event.preventDefault();
459                         delegate.call( null, event );
460                 };
461         }
462
463         /**
464          * Causes the address bar to hide on mobile devices,
465          * more vertical space ftw.
466          */
467         function removeAddressBar() {
468                 setTimeout( function() {
469                         window.scrollTo( 0, 1 );
470                 }, 0 );
471         }
472
473         /**
474          * Dispatches an event of the specified type from the
475          * reveal DOM element.
476          */
477         function dispatchEvent( type, properties ) {
478                 var event = document.createEvent( "HTMLEvents", 1, 2 );
479                 event.initEvent( type, true, true );
480                 extend( event, properties );
481                 dom.wrapper.dispatchEvent( event );
482         }
483
484         /**
485          * Wrap all links in 3D goodness.
486          */
487         function linkify() {
488                 if( supports3DTransforms && !( 'msPerspective' in document.body.style ) ) {
489                         var nodes = document.querySelectorAll( SLIDES_SELECTOR + ' a:not(.image)' );
490
491                         for( var i = 0, len = nodes.length; i < len; i++ ) {
492                                 var node = nodes[i];
493
494                                 if( node.textContent && !node.querySelector( 'img' ) && ( !node.className || !node.classList.contains( node, 'roll' ) ) ) {
495                                         node.classList.add( 'roll' );
496                                         node.innerHTML = '<span data-title="'+ node.text +'">' + node.innerHTML + '</span>';
497                                 }
498                         }
499                 }
500         }
501
502         /**
503          * Applies JavaScript-controlled layout rules to the
504          * presentation.
505          */
506         function layout() {
507
508                 if( config.center ) {
509
510                         // Select all slides, vertical and horizontal
511                         var slides = toArray( document.querySelectorAll( SLIDES_SELECTOR ) );
512
513                         // Determine the minimum top offset for slides
514                         var minTop = -dom.wrapper.offsetHeight / 2;
515
516                         for( var i = 0, len = slides.length; i < len; i++ ) {
517                                 var slide = slides[ i ];
518
519                                 // Don't bother update invisible slides
520                                 if( slide.style.display === 'none' ) {
521                                         continue;
522                                 }
523
524                                 // Vertical stacks are not centered since their section 
525                                 // children will be
526                                 if( slide.classList.contains( 'stack' ) ) {
527                                         slide.style.top = 0;
528                                 }
529                                 else {
530                                         slide.style.top = Math.max( - ( slide.offsetHeight / 2 ) - 20, minTop ) + 'px';
531                                 }
532                         }
533
534                 }
535
536         }
537
538         /**
539          * Stores the vertical index of a stack so that the same 
540          * vertical slide can be selected when navigating to and 
541          * from the stack.
542          * 
543          * @param {HTMLElement} stack The vertical stack element
544          * @param {int} v Index to memorize
545          */
546         function setPreviousVerticalIndex( stack, v ) {
547                 if( stack ) {
548                         stack.setAttribute( 'data-previous-indexv', v || 0 );
549                 }
550         }
551
552         /**
553          * Retrieves the vertical index which was stored using 
554          * #setPreviousVerticalIndex() or 0 if no previous index
555          * exists.
556          *
557          * @param {HTMLElement} stack The vertical stack element
558          */
559         function getPreviousVerticalIndex( stack ) {
560                 if( stack && stack.classList.contains( 'stack' ) ) {
561                         return parseInt( stack.getAttribute( 'data-previous-indexv' ) || 0, 10 );
562                 }
563
564                 return 0;
565         }
566
567         /**
568          * Displays the overview of slides (quick nav) by
569          * scaling down and arranging all slide elements.
570          *
571          * Experimental feature, might be dropped if perf
572          * can't be improved.
573          */
574         function activateOverview() {
575
576                 // Only proceed if enabled in config
577                 if( config.overview ) {
578
579                         dom.wrapper.classList.add( 'overview' );
580
581                         clearTimeout( activateOverviewTimeout );
582
583                         // Not the pretties solution, but need to let the overview 
584                         // class apply first so that slides are measured accurately 
585                         // before we can positon them
586                         activateOverviewTimeout = setTimeout( function(){
587
588                                 var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR );
589
590                                 for( var i = 0, len1 = horizontalSlides.length; i < len1; i++ ) {
591                                         var hslide = horizontalSlides[i],
592                                                 htransform = 'translateZ(-2500px) translate(' + ( ( i - indexh ) * 105 ) + '%, 0%)';
593
594                                         hslide.setAttribute( 'data-index-h', i );
595                                         hslide.style.display = 'block';
596                                         hslide.style.WebkitTransform = htransform;
597                                         hslide.style.MozTransform = htransform;
598                                         hslide.style.msTransform = htransform;
599                                         hslide.style.OTransform = htransform;
600                                         hslide.style.transform = htransform;
601
602                                         if( hslide.classList.contains( 'stack' ) ) {
603
604                                                 var verticalSlides = hslide.querySelectorAll( 'section' );
605
606                                                 for( var j = 0, len2 = verticalSlides.length; j < len2; j++ ) {
607                                                         var verticalIndex = i === indexh ? indexv : getPreviousVerticalIndex( hslide );
608
609                                                         var vslide = verticalSlides[j],
610                                                                 vtransform = 'translate(0%, ' + ( ( j - verticalIndex ) * 105 ) + '%)';
611
612                                                         vslide.setAttribute( 'data-index-h', i );
613                                                         vslide.setAttribute( 'data-index-v', j );
614                                                         vslide.style.display = 'block';
615                                                         vslide.style.WebkitTransform = vtransform;
616                                                         vslide.style.MozTransform = vtransform;
617                                                         vslide.style.msTransform = vtransform;
618                                                         vslide.style.OTransform = vtransform;
619                                                         vslide.style.transform = vtransform;
620
621                                                         // Navigate to this slide on click
622                                                         vslide.addEventListener( 'click', onOverviewSlideClicked, true );
623                                                 }
624
625                                         }
626                                         else {
627
628                                                 // Navigate to this slide on click
629                                                 hslide.addEventListener( 'click', onOverviewSlideClicked, true );
630
631                                         }
632                                 }
633
634                                 layout();
635
636                         }, 10 );
637
638                 }
639
640         }
641
642         /**
643          * Exits the slide overview and enters the currently
644          * active slide.
645          */
646         function deactivateOverview() {
647
648                 // Only proceed if enabled in config
649                 if( config.overview ) {
650
651                         clearTimeout( activateOverviewTimeout );
652
653                         dom.wrapper.classList.remove( 'overview' );
654
655                         // Select all slides
656                         var slides = toArray( document.querySelectorAll( SLIDES_SELECTOR ) );
657
658                         for( var i = 0, len = slides.length; i < len; i++ ) {
659                                 var element = slides[i];
660
661                                 element.style.display = '';
662
663                                 // Resets all transforms to use the external styles
664                                 element.style.WebkitTransform = '';
665                                 element.style.MozTransform = '';
666                                 element.style.msTransform = '';
667                                 element.style.OTransform = '';
668                                 element.style.transform = '';
669
670                                 element.removeEventListener( 'click', onOverviewSlideClicked, true );
671                         }
672
673                         slide( indexh, indexv );
674
675                 }
676         }
677
678         /**
679          * Toggles the slide overview mode on and off.
680          *
681          * @param {Boolean} override Optional flag which overrides the
682          * toggle logic and forcibly sets the desired state. True means
683          * overview is open, false means it's closed.
684          */
685         function toggleOverview( override ) {
686                 if( typeof override === 'boolean' ) {
687                         override ? activateOverview() : deactivateOverview();
688                 }
689                 else {
690                         isOverviewActive() ? deactivateOverview() : activateOverview();
691                 }
692         }
693
694         /**
695          * Checks if the overview is currently active.
696          *
697          * @return {Boolean} true if the overview is active,
698          * false otherwise
699          */
700         function isOverviewActive() {
701                 return dom.wrapper.classList.contains( 'overview' );
702         }
703
704         /**
705          * Handling the fullscreen functionality via the fullscreen API
706          *
707          * @see http://fullscreen.spec.whatwg.org/
708          * @see https://developer.mozilla.org/en-US/docs/DOM/Using_fullscreen_mode
709          */
710         function enterFullscreen() {
711                 var element = document.body;
712
713                 // Check which implementation is available
714                 var requestMethod = element.requestFullScreen ||
715                                                         element.webkitRequestFullScreen ||
716                                                         element.mozRequestFullScreen ||
717                                                         element.msRequestFullScreen;
718
719                 if( requestMethod ) {
720                         requestMethod.apply( element );
721                 }
722         }
723
724         /**
725          * Enters the paused mode which fades everything on screen to
726          * black.
727          */
728         function pause() {
729                 dom.wrapper.classList.add( 'paused' );
730         }
731
732         /**
733          * Exits from the paused mode.
734          */
735         function resume() {
736                 dom.wrapper.classList.remove( 'paused' );
737         }
738
739         /**
740          * Toggles the paused mode on and off.
741          */
742         function togglePause() {
743                 if( isPaused() ) {
744                         resume();
745                 }
746                 else {
747                         pause();
748                 }
749         }
750
751         /**
752          * Checks if we are currently in the paused mode.
753          */
754         function isPaused() {
755                 return dom.wrapper.classList.contains( 'paused' );
756         }
757
758         /**
759          * Steps from the current point in the presentation to the
760          * slide which matches the specified horizontal and vertical
761          * indices.
762          *
763          * @param {int} h Horizontal index of the target slide
764          * @param {int} v Vertical index of the target slide
765          * @param {int} f Optional index of a fragment within the 
766          * target slide to activate
767          */
768         function slide( h, v, f ) {
769                 // Remember where we were at before
770                 previousSlide = currentSlide;
771
772                 // Query all horizontal slides in the deck
773                 var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR );
774
775                 // If no vertical index is specified and the upcoming slide is a 
776                 // stack, resume at its previous vertical index
777                 if( v === undefined ) {
778                         v = getPreviousVerticalIndex( horizontalSlides[ h ] );
779                 }
780
781                 // If we were on a vertical stack, remember what vertical index 
782                 // it was on so we can resume at the same position when returning
783                 if( previousSlide && previousSlide.parentNode && previousSlide.parentNode.classList.contains( 'stack' ) ) {
784                         setPreviousVerticalIndex( previousSlide.parentNode, indexv );
785                 }
786
787                 // Remember the state before this slide
788                 var stateBefore = state.concat();
789
790                 // Reset the state array
791                 state.length = 0;
792
793                 var indexhBefore = indexh,
794                         indexvBefore = indexv;
795
796                 // Activate and transition to the new slide
797                 indexh = updateSlides( HORIZONTAL_SLIDES_SELECTOR, h === undefined ? indexh : h );
798                 indexv = updateSlides( VERTICAL_SLIDES_SELECTOR, v === undefined ? indexv : v );
799
800                 layout();
801
802                 // Apply the new state
803                 stateLoop: for( var i = 0, len = state.length; i < len; i++ ) {
804                         // Check if this state existed on the previous slide. If it
805                         // did, we will avoid adding it repeatedly
806                         for( var j = 0; j < stateBefore.length; j++ ) {
807                                 if( stateBefore[j] === state[i] ) {
808                                         stateBefore.splice( j, 1 );
809                                         continue stateLoop;
810                                 }
811                         }
812
813                         document.documentElement.classList.add( state[i] );
814
815                         // Dispatch custom event matching the state's name
816                         dispatchEvent( state[i] );
817                 }
818
819                 // Clean up the remaints of the previous state
820                 while( stateBefore.length ) {
821                         document.documentElement.classList.remove( stateBefore.pop() );
822                 }
823
824                 // If the overview is active, re-activate it to update positions
825                 if( isOverviewActive() ) {
826                         activateOverview();
827                 }
828
829                 // Update the URL hash after a delay since updating it mid-transition
830                 // is likely to cause visual lag
831                 writeURL( 1500 );
832
833                 // Find the current horizontal slide and any possible vertical slides
834                 // within it
835                 var currentHorizontalSlide = horizontalSlides[ indexh ],
836                         currentVerticalSlides = currentHorizontalSlide.querySelectorAll( 'section' );
837
838                 // Store references to the previous and current slides
839                 currentSlide = currentVerticalSlides[ indexv ] || currentHorizontalSlide;
840
841
842                 // Show fragment, if specified
843                 if( ( indexh !== indexhBefore || indexv !== indexvBefore ) && f ) {
844                         var fragments = currentSlide.querySelectorAll( '.fragment' );
845
846                         toArray( fragments ).forEach( function( fragment, indexf ) {
847                                 if( indexf < f ) {
848                                         fragment.classList.add( 'visible' );
849                                 }
850                                 else {
851                                         fragment.classList.remove( 'visible' );
852                                 }
853                         } );
854                 }
855
856                 // Dispatch an event if the slide changed
857                 if( indexh !== indexhBefore || indexv !== indexvBefore ) {
858                         dispatchEvent( 'slidechanged', {
859                                 'indexh': indexh,
860                                 'indexv': indexv,
861                                 'previousSlide': previousSlide,
862                                 'currentSlide': currentSlide
863                         } );
864                 }
865                 else {
866                         // Ensure that the previous slide is never the same as the current
867                         previousSlide = null;
868                 }
869
870                 // Solves an edge case where the previous slide maintains the
871                 // 'present' class when navigating between adjacent vertical
872                 // stacks
873                 if( previousSlide ) {
874                         previousSlide.classList.remove( 'present' );
875                 }
876
877                 updateControls();
878                 updateProgress();
879         }
880
881         /**
882          * Updates one dimension of slides by showing the slide
883          * with the specified index.
884          *
885          * @param {String} selector A CSS selector that will fetch
886          * the group of slides we are working with
887          * @param {Number} index The index of the slide that should be
888          * shown
889          *
890          * @return {Number} The index of the slide that is now shown,
891          * might differ from the passed in index if it was out of
892          * bounds.
893          */
894         function updateSlides( selector, index ) {
895                 // Select all slides and convert the NodeList result to
896                 // an array
897                 var slides = toArray( document.querySelectorAll( selector ) ),
898                         slidesLength = slides.length;
899
900                 if( slidesLength ) {
901
902                         // Should the index loop?
903                         if( config.loop ) {
904                                 index %= slidesLength;
905
906                                 if( index < 0 ) {
907                                         index = slidesLength + index;
908                                 }
909                         }
910
911                         // Enforce max and minimum index bounds
912                         index = Math.max( Math.min( index, slidesLength - 1 ), 0 );
913
914                         for( var i = 0; i < slidesLength; i++ ) {
915                                 var element = slides[i];
916
917                                 // Optimization; hide all slides that are three or more steps
918                                 // away from the present slide
919                                 if( isOverviewActive() === false ) {
920                                         // The distance loops so that it measures 1 between the first
921                                         // and last slides
922                                         var distance = Math.abs( ( index - i ) % ( slidesLength - 3 ) ) || 0;
923
924                                         element.style.display = distance > 3 ? 'none' : 'block';
925                                 }
926
927                                 slides[i].classList.remove( 'past' );
928                                 slides[i].classList.remove( 'present' );
929                                 slides[i].classList.remove( 'future' );
930
931                                 if( i < index ) {
932                                         // Any element previous to index is given the 'past' class
933                                         slides[i].classList.add( 'past' );
934                                 }
935                                 else if( i > index ) {
936                                         // Any element subsequent to index is given the 'future' class
937                                         slides[i].classList.add( 'future' );
938                                 }
939
940                                 // If this element contains vertical slides
941                                 if( element.querySelector( 'section' ) ) {
942                                         slides[i].classList.add( 'stack' );
943                                 }
944                         }
945
946                         // Mark the current slide as present
947                         slides[index].classList.add( 'present' );
948
949                         // If this slide has a state associated with it, add it
950                         // onto the current state of the deck
951                         var slideState = slides[index].getAttribute( 'data-state' );
952                         if( slideState ) {
953                                 state = state.concat( slideState.split( ' ' ) );
954                         }
955
956                         // If this slide has a data-autoslide attribtue associated use this as
957                         // autoSlide value otherwise use the global configured time
958                         var slideAutoSlide = slides[index].getAttribute( 'data-autoslide' );
959                         if( slideAutoSlide ) {
960                                 autoSlide = parseInt( slideAutoSlide, 10 );
961                         } 
962                         else {
963                                 autoSlide = config.autoSlide;
964                         }
965
966                 }
967                 else {
968                         // Since there are no slides we can't be anywhere beyond the
969                         // zeroth index
970                         index = 0;
971                 }
972
973                 return index;
974
975         }
976
977         /**
978          * Updates the progress bar to reflect the current slide.
979          */
980         function updateProgress() {
981                 // Update progress if enabled
982                 if( config.progress && dom.progress ) {
983
984                         var horizontalSlides = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
985
986                         // The number of past and total slides
987                         var totalCount = document.querySelectorAll( SLIDES_SELECTOR + ':not(.stack)' ).length;
988                         var pastCount = 0;
989
990                         // Step through all slides and count the past ones
991                         mainLoop: for( var i = 0; i < horizontalSlides.length; i++ ) {
992
993                                 var horizontalSlide = horizontalSlides[i];
994                                 var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) );
995
996                                 for( var j = 0; j < verticalSlides.length; j++ ) {
997
998                                         // Stop as soon as we arrive at the present
999                                         if( verticalSlides[j].classList.contains( 'present' ) ) {
1000                                                 break mainLoop;
1001                                         }
1002
1003                                         pastCount++;
1004
1005                                 }
1006
1007                                 // Stop as soon as we arrive at the present
1008                                 if( horizontalSlide.classList.contains( 'present' ) ) {
1009                                         break;
1010                                 }
1011
1012                                 // Don't count the wrapping section for vertical slides
1013                                 if( horizontalSlide.classList.contains( 'stack' ) === false ) {
1014                                         pastCount++;
1015                                 }
1016
1017                         }
1018
1019                         dom.progressbar.style.width = ( pastCount / ( totalCount - 1 ) ) * window.innerWidth + 'px';
1020
1021                 }
1022         }
1023
1024         /**
1025          * Updates the state of all control/navigation arrows.
1026          */
1027         function updateControls() {
1028                 if ( config.controls && dom.controls ) {
1029
1030                         var routes = availableRoutes();
1031
1032                         // Remove the 'enabled' class from all directions
1033                         dom.controlsLeft.concat( dom.controlsRight )
1034                                                         .concat( dom.controlsUp )
1035                                                         .concat( dom.controlsDown )
1036                                                         .concat( dom.controlsPrev )
1037                                                         .concat( dom.controlsNext ).forEach( function( node ) {
1038                                 node.classList.remove( 'enabled' );
1039                         } );
1040
1041                         // Add the 'enabled' class to the available routes
1042                         if( routes.left ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'enabled' );     } );
1043                         if( routes.right ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'enabled' ); } );
1044                         if( routes.up ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'enabled' ); } );
1045                         if( routes.down ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'enabled' ); } );
1046
1047                         // Prev/next buttons
1048                         if( routes.left || routes.up ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'enabled' ); } );
1049                         if( routes.right || routes.down ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'enabled' ); } );
1050
1051                 }
1052         }
1053
1054         /**
1055          * Determine what available routes there are for navigation.
1056          *
1057          * @return {Object} containing four booleans: left/right/up/down
1058          */
1059         function availableRoutes() {
1060                 var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ),
1061                         verticalSlides = document.querySelectorAll( VERTICAL_SLIDES_SELECTOR );
1062
1063                 return {
1064                         left: indexh > 0,
1065                         right: indexh < horizontalSlides.length - 1,
1066                         up: indexv > 0,
1067                         down: indexv < verticalSlides.length - 1
1068                 };
1069         }
1070
1071         /**
1072          * Reads the current URL (hash) and navigates accordingly.
1073          */
1074         function readURL() {
1075                 var hash = window.location.hash;
1076
1077                 // Attempt to parse the hash as either an index or name
1078                 var bits = hash.slice( 2 ).split( '/' ),
1079                         name = hash.replace( /#|\//gi, '' );
1080
1081                 // If the first bit is invalid and there is a name we can
1082                 // assume that this is a named link
1083                 if( isNaN( parseInt( bits[0], 10 ) ) && name.length ) {
1084                         // Find the slide with the specified name
1085                         var element = document.querySelector( '#' + name );
1086
1087                         if( element ) {
1088                                 // Find the position of the named slide and navigate to it
1089                                 var indices = Reveal.getIndices( element );
1090                                 slide( indices.h, indices.v );
1091                         }
1092                         // If the slide doesn't exist, navigate to the current slide
1093                         else {
1094                                 slide( indexh, indexv );
1095                         }
1096                 }
1097                 else {
1098                         // Read the index components of the hash
1099                         var h = parseInt( bits[0], 10 ) || 0,
1100                                 v = parseInt( bits[1], 10 ) || 0;
1101
1102                         slide( h, v );
1103                 }
1104         }
1105
1106         /**
1107          * Updates the page URL (hash) to reflect the current
1108          * state.
1109          *
1110          * @param {Number} delay The time in ms to wait before 
1111          * writing the hash
1112          */
1113         function writeURL( delay ) {
1114                 if( config.history ) {
1115
1116                         // Make sure there's never more than one timeout running
1117                         clearTimeout( writeURLTimeout );
1118
1119                         // If a delay is specified, timeout this call
1120                         if( typeof delay === 'number' ) {
1121                                 writeURLTimeout = setTimeout( writeURL, delay );
1122                         }
1123                         else {
1124                                 var url = '/';
1125
1126                                 // If the current slide has an ID, use that as a named link
1127                                 if( currentSlide && typeof currentSlide.getAttribute( 'id' ) === 'string' ) {
1128                                         url = '/' + currentSlide.getAttribute( 'id' );
1129                                 }
1130                                 // Otherwise use the /h/v index
1131                                 else {
1132                                         if( indexh > 0 || indexv > 0 ) url += indexh;
1133                                         if( indexv > 0 ) url += '/' + indexv;
1134                                 }
1135
1136                                 window.location.hash = url;
1137                         }
1138                 }
1139         }
1140
1141         /**
1142          * Retrieves the h/v location of the current, or specified,
1143          * slide.
1144          *
1145          * @param {HTMLElement} slide If specified, the returned
1146          * index will be for this slide rather than the currently
1147          * active one
1148          *
1149          * @return {Object} { h: <int>, v: <int> }
1150          */
1151         function getIndices( slide ) {
1152                 // By default, return the current indices
1153                 var h = indexh,
1154                         v = indexv;
1155
1156                 // If a slide is specified, return the indices of that slide
1157                 if( slide ) {
1158                         var isVertical = !!slide.parentNode.nodeName.match( /section/gi );
1159                         var slideh = isVertical ? slide.parentNode : slide;
1160
1161                         // Select all horizontal slides
1162                         var horizontalSlides = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
1163
1164                         // Now that we know which the horizontal slide is, get its index
1165                         h = Math.max( horizontalSlides.indexOf( slideh ), 0 );
1166
1167                         // If this is a vertical slide, grab the vertical index
1168                         if( isVertical ) {
1169                                 v = Math.max( toArray( slide.parentNode.querySelectorAll( 'section' ) ).indexOf( slide ), 0 );
1170                         }
1171                 }
1172
1173                 return { h: h, v: v };
1174         }
1175
1176         /**
1177          * Navigate to the next slide fragment.
1178          *
1179          * @return {Boolean} true if there was a next fragment,
1180          * false otherwise
1181          */
1182         function nextFragment() {
1183                 // Vertical slides:
1184                 if( document.querySelector( VERTICAL_SLIDES_SELECTOR + '.present' ) ) {
1185                         var verticalFragments = document.querySelectorAll( VERTICAL_SLIDES_SELECTOR + '.present .fragment:not(.visible)' );
1186                         if( verticalFragments.length ) {
1187                                 verticalFragments[0].classList.add( 'visible' );
1188
1189                                 // Notify subscribers of the change
1190                                 dispatchEvent( 'fragmentshown', { fragment: verticalFragments[0] } );
1191                                 return true;
1192                         }
1193                 }
1194                 // Horizontal slides:
1195                 else {
1196                         var horizontalFragments = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.present .fragment:not(.visible)' );
1197                         if( horizontalFragments.length ) {
1198                                 horizontalFragments[0].classList.add( 'visible' );
1199
1200                                 // Notify subscribers of the change
1201                                 dispatchEvent( 'fragmentshown', { fragment: horizontalFragments[0] } );
1202                                 return true;
1203                         }
1204                 }
1205
1206                 return false;
1207         }
1208
1209         /**
1210          * Navigate to the previous slide fragment.
1211          *
1212          * @return {Boolean} true if there was a previous fragment,
1213          * false otherwise
1214          */
1215         function previousFragment() {
1216                 // Vertical slides:
1217                 if( document.querySelector( VERTICAL_SLIDES_SELECTOR + '.present' ) ) {
1218                         var verticalFragments = document.querySelectorAll( VERTICAL_SLIDES_SELECTOR + '.present .fragment.visible' );
1219                         if( verticalFragments.length ) {
1220                                 verticalFragments[ verticalFragments.length - 1 ].classList.remove( 'visible' );
1221
1222                                 // Notify subscribers of the change
1223                                 dispatchEvent( 'fragmenthidden', { fragment: verticalFragments[ verticalFragments.length - 1 ] } );
1224                                 return true;
1225                         }
1226                 }
1227                 // Horizontal slides:
1228                 else {
1229                         var horizontalFragments = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.present .fragment.visible' );
1230                         if( horizontalFragments.length ) {
1231                                 horizontalFragments[ horizontalFragments.length - 1 ].classList.remove( 'visible' );
1232
1233                                 // Notify subscribers of the change
1234                                 dispatchEvent( 'fragmenthidden', { fragment: horizontalFragments[ horizontalFragments.length - 1 ] } );
1235                                 return true;
1236                         }
1237                 }
1238
1239                 return false;
1240         }
1241
1242         /**
1243          * Cues a new automated slide if enabled in the config.
1244          */
1245         function cueAutoSlide() {
1246                 clearTimeout( autoSlideTimeout );
1247
1248                 // Cue the next auto-slide if enabled
1249                 if( autoSlide ) {
1250                         autoSlideTimeout = setTimeout( navigateNext, autoSlide );
1251                 }
1252         }
1253
1254         function navigateLeft() {
1255                 // Prioritize hiding fragments
1256                 if( availableRoutes().left && isOverviewActive() || previousFragment() === false ) {
1257                         slide( indexh - 1 );
1258                 }
1259         }
1260
1261         function navigateRight() {
1262                 // Prioritize revealing fragments
1263                 if( availableRoutes().right && isOverviewActive() || nextFragment() === false ) {
1264                         slide( indexh + 1 );
1265                 }
1266         }
1267
1268         function navigateUp() {
1269                 // Prioritize hiding fragments
1270                 if( availableRoutes().up && isOverviewActive() || previousFragment() === false ) {
1271                         slide( indexh, indexv - 1 );
1272                 }
1273         }
1274
1275         function navigateDown() {
1276                 // Prioritize revealing fragments
1277                 if( availableRoutes().down && isOverviewActive() || nextFragment() === false ) {
1278                         slide( indexh, indexv + 1 );
1279                 }
1280         }
1281
1282         /**
1283          * Navigates backwards, prioritized in the following order:
1284          * 1) Previous fragment
1285          * 2) Previous vertical slide
1286          * 3) Previous horizontal slide
1287          */
1288         function navigatePrev() {
1289                 // Prioritize revealing fragments
1290                 if( previousFragment() === false ) {
1291                         if( availableRoutes().up ) {
1292                                 navigateUp();
1293                         }
1294                         else {
1295                                 // Fetch the previous horizontal slide, if there is one
1296                                 var previousSlide = document.querySelector( HORIZONTAL_SLIDES_SELECTOR + '.past:nth-child(' + indexh + ')' );
1297
1298                                 if( previousSlide ) {
1299                                         indexv = ( previousSlide.querySelectorAll( 'section' ).length + 1 ) || undefined;
1300                                         indexh --;
1301                                         slide();
1302                                 }
1303                         }
1304                 }
1305         }
1306
1307         /**
1308          * Same as #navigatePrev() but navigates forwards.
1309          */
1310         function navigateNext() {
1311                 // Prioritize revealing fragments
1312                 if( nextFragment() === false ) {
1313                         availableRoutes().down ? navigateDown() : navigateRight();
1314                 }
1315
1316                 // If auto-sliding is enabled we need to cue up
1317                 // another timeout
1318                 cueAutoSlide();
1319         }
1320
1321
1322         // --------------------------------------------------------------------//
1323         // ----------------------------- EVENTS -------------------------------//
1324         // --------------------------------------------------------------------//
1325
1326
1327         /**
1328          * Handler for the document level 'keydown' event.
1329          *
1330          * @param {Object} event
1331          */
1332         function onDocumentKeyDown( event ) {
1333                 // Check if there's a focused element that could be using 
1334                 // the keyboard
1335                 var activeElement = document.activeElement;
1336                 var hasFocus = !!( document.activeElement && ( document.activeElement.type || document.activeElement.href || document.activeElement.contentEditable !== 'inherit' ) );
1337
1338                 // Disregard the event if there's a focused element or a 
1339                 // keyboard modifier key is present
1340                 if ( hasFocus || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey ) return;
1341
1342                 var triggered = true;
1343
1344                 switch( event.keyCode ) {
1345                         // p, page up
1346                         case 80: case 33: navigatePrev(); break;
1347                         // n, page down
1348                         case 78: case 34: navigateNext(); break;
1349                         // h, left
1350                         case 72: case 37: navigateLeft(); break;
1351                         // l, right
1352                         case 76: case 39: navigateRight(); break;
1353                         // k, up
1354                         case 75: case 38: navigateUp(); break;
1355                         // j, down
1356                         case 74: case 40: navigateDown(); break;
1357                         // home
1358                         case 36: slide( 0 ); break;
1359                         // end
1360                         case 35: slide( Number.MAX_VALUE ); break;
1361                         // space
1362                         case 32: isOverviewActive() ? deactivateOverview() : navigateNext(); break;
1363                         // return
1364                         case 13: isOverviewActive() ? deactivateOverview() : triggered = false; break;
1365                         // b, period
1366                         case 66: case 190: togglePause(); break;
1367                         // f
1368                         case 70: enterFullscreen(); break;
1369                         default:
1370                                 triggered = false;
1371                 }
1372
1373                 // If the input resulted in a triggered action we should prevent
1374                 // the browsers default behavior
1375                 if( triggered ) {
1376                         event.preventDefault();
1377                 }
1378                 else if ( event.keyCode === 27 && supports3DTransforms ) {
1379                         toggleOverview();
1380
1381                         event.preventDefault();
1382                 }
1383
1384                 // If auto-sliding is enabled we need to cue up
1385                 // another timeout
1386                 cueAutoSlide();
1387
1388         }
1389
1390         /**
1391          * Handler for the document level 'touchstart' event,
1392          * enables support for swipe and pinch gestures.
1393          */
1394         function onDocumentTouchStart( event ) {
1395                 touch.startX = event.touches[0].clientX;
1396                 touch.startY = event.touches[0].clientY;
1397                 touch.startCount = event.touches.length;
1398
1399                 // If there's two touches we need to memorize the distance
1400                 // between those two points to detect pinching
1401                 if( event.touches.length === 2 && config.overview ) {
1402                         touch.startSpan = distanceBetween( {
1403                                 x: event.touches[1].clientX,
1404                                 y: event.touches[1].clientY
1405                         }, {
1406                                 x: touch.startX,
1407                                 y: touch.startY
1408                         } );
1409                 }
1410         }
1411
1412         /**
1413          * Handler for the document level 'touchmove' event.
1414          */
1415         function onDocumentTouchMove( event ) {
1416                 // Each touch should only trigger one action
1417                 if( !touch.handled ) {
1418                         var currentX = event.touches[0].clientX;
1419                         var currentY = event.touches[0].clientY;
1420
1421                         // If the touch started off with two points and still has
1422                         // two active touches; test for the pinch gesture
1423                         if( event.touches.length === 2 && touch.startCount === 2 && config.overview ) {
1424
1425                                 // The current distance in pixels between the two touch points
1426                                 var currentSpan = distanceBetween( {
1427                                         x: event.touches[1].clientX,
1428                                         y: event.touches[1].clientY
1429                                 }, {
1430                                         x: touch.startX,
1431                                         y: touch.startY
1432                                 } );
1433
1434                                 // If the span is larger than the desire amount we've got
1435                                 // ourselves a pinch
1436                                 if( Math.abs( touch.startSpan - currentSpan ) > touch.threshold ) {
1437                                         touch.handled = true;
1438
1439                                         if( currentSpan < touch.startSpan ) {
1440                                                 activateOverview();
1441                                         }
1442                                         else {
1443                                                 deactivateOverview();
1444                                         }
1445                                 }
1446
1447                                 event.preventDefault();
1448
1449                         }
1450                         // There was only one touch point, look for a swipe
1451                         else if( event.touches.length === 1 && touch.startCount !== 2 ) {
1452
1453                                 var deltaX = currentX - touch.startX,
1454                                         deltaY = currentY - touch.startY;
1455
1456                                 if( deltaX > touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
1457                                         touch.handled = true;
1458                                         navigateLeft();
1459                                 }
1460                                 else if( deltaX < -touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
1461                                         touch.handled = true;
1462                                         navigateRight();
1463                                 }
1464                                 else if( deltaY > touch.threshold ) {
1465                                         touch.handled = true;
1466                                         navigateUp();
1467                                 }
1468                                 else if( deltaY < -touch.threshold ) {
1469                                         touch.handled = true;
1470                                         navigateDown();
1471                                 }
1472
1473                                 event.preventDefault();
1474
1475                         }
1476                 }
1477                 // There's a bug with swiping on some Android devices unless
1478                 // the default action is always prevented
1479                 else if( navigator.userAgent.match( /android/gi ) ) {
1480                         event.preventDefault();
1481                 }
1482         }
1483
1484         /**
1485          * Handler for the document level 'touchend' event.
1486          */
1487         function onDocumentTouchEnd( event ) {
1488                 touch.handled = false;
1489         }
1490
1491         /**
1492          * Handles mouse wheel scrolling, throttled to avoid skipping
1493          * multiple slides.
1494          */
1495         function onDocumentMouseScroll( event ){
1496                 clearTimeout( mouseWheelTimeout );
1497
1498                 mouseWheelTimeout = setTimeout( function() {
1499                         var delta = event.detail || -event.wheelDelta;
1500                         if( delta > 0 ) {
1501                                 navigateNext();
1502                         }
1503                         else {
1504                                 navigatePrev();
1505                         }
1506                 }, 100 );
1507         }
1508
1509         /**
1510          * Clicking on the progress bar results in a navigation to the
1511          * closest approximate horizontal slide using this equation:
1512          *
1513          * ( clickX / presentationWidth ) * numberOfSlides
1514          */
1515         function onProgressClick( event ) {
1516                 var slidesTotal = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).length;
1517                 var slideIndex = Math.floor( ( event.clientX / dom.wrapper.offsetWidth ) * slidesTotal );
1518
1519                 slide( slideIndex );
1520         }
1521
1522         /**
1523          * Handler for the window level 'hashchange' event.
1524          */
1525         function onWindowHashChange( event ) {
1526                 readURL();
1527         }
1528
1529         /**
1530          * Handler for the window level 'resize' event.
1531          */
1532         function onWindowResize( event ) {
1533                 layout();
1534         }
1535
1536         /**
1537          * Invoked when a slide is and we're in the overview.
1538          */
1539         function onOverviewSlideClicked( event ) {
1540                 // TODO There's a bug here where the event listeners are not
1541                 // removed after deactivating the overview.
1542                 if( isOverviewActive() ) {
1543                         event.preventDefault();
1544
1545                         deactivateOverview();
1546
1547                         var element = event.target;
1548
1549                         while( element && !element.nodeName.match( /section/gi ) ) {
1550                                 element = element.parentNode;
1551                         }
1552
1553                         if( element.nodeName.match( /section/gi ) ) {
1554                                 var h = parseInt( element.getAttribute( 'data-index-h' ), 10 ),
1555                                         v = parseInt( element.getAttribute( 'data-index-v' ), 10 );
1556
1557                                 slide( h, v );
1558                         }
1559                 }
1560         }
1561
1562
1563         // --------------------------------------------------------------------//
1564         // ------------------------------- API --------------------------------//
1565         // --------------------------------------------------------------------//
1566
1567
1568         return {
1569                 initialize: initialize,
1570
1571                 // Navigation methods
1572                 slide: slide,
1573                 left: navigateLeft,
1574                 right: navigateRight,
1575                 up: navigateUp,
1576                 down: navigateDown,
1577                 prev: navigatePrev,
1578                 next: navigateNext,
1579                 prevFragment: previousFragment,
1580                 nextFragment: nextFragment,
1581
1582                 // Deprecated aliases
1583                 navigateTo: slide,
1584                 navigateLeft: navigateLeft,
1585                 navigateRight: navigateRight,
1586                 navigateUp: navigateUp,
1587                 navigateDown: navigateDown,
1588                 navigatePrev: navigatePrev,
1589                 navigateNext: navigateNext,
1590
1591                 // Toggles the overview mode on/off
1592                 toggleOverview: toggleOverview,
1593
1594                 // Adds or removes all internal event listeners (such as keyboard)
1595                 addEventListeners: addEventListeners,
1596                 removeEventListeners: removeEventListeners,
1597
1598                 // Returns the indices of the current, or specified, slide
1599                 getIndices: getIndices,
1600
1601                 // Returns the previous slide element, may be null
1602                 getPreviousSlide: function() {
1603                         return previousSlide;
1604                 },
1605
1606                 // Returns the current slide element
1607                 getCurrentSlide: function() {
1608                         return currentSlide;
1609                 },
1610
1611                 // Helper method, retrieves query string as a key/value hash
1612                 getQueryHash: function() {
1613                         var query = {};
1614
1615                         location.search.replace( /[A-Z0-9]+?=(\w*)/gi, function(a) {
1616                                 query[ a.split( '=' ).shift() ] = a.split( '=' ).pop();
1617                         } );
1618
1619                         return query;
1620                 },
1621
1622                 // Forward event binding to the reveal DOM element
1623                 addEventListener: function( type, listener, useCapture ) {
1624                         if( 'addEventListener' in window ) {
1625                                 ( dom.wrapper || document.querySelector( '.reveal' ) ).addEventListener( type, listener, useCapture );
1626                         }
1627                 },
1628                 removeEventListener: function( type, listener, useCapture ) {
1629                         if( 'addEventListener' in window ) {
1630                                 ( dom.wrapper || document.querySelector( '.reveal' ) ).removeEventListener( type, listener, useCapture );
1631                         }
1632                 }
1633         };
1634
1635 })();