Merge pull request #282 from callmephilip/feature/plugin/remotes/none-on-mobile
[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                     var span = document.createElement('span');
496                     span.setAttribute('data-title', node.text);
497                     span.innerHTML = node.innerHTML;
498
499                                         node.classList.add( 'roll' );
500                     node.innerHTML = '';
501                     node.appendChild(span);
502                                 }
503                         }
504                 }
505         }
506
507         /**
508          * Applies JavaScript-controlled layout rules to the
509          * presentation.
510          */
511         function layout() {
512
513                 if( config.center ) {
514
515                         // Select all slides, vertical and horizontal
516                         var slides = toArray( document.querySelectorAll( SLIDES_SELECTOR ) );
517
518                         // Determine the minimum top offset for slides
519                         var minTop = -dom.wrapper.offsetHeight / 2;
520
521                         for( var i = 0, len = slides.length; i < len; i++ ) {
522                                 var slide = slides[ i ];
523
524                                 // Don't bother update invisible slides
525                                 if( slide.style.display === 'none' ) {
526                                         continue;
527                                 }
528
529                                 // Vertical stacks are not centered since their section 
530                                 // children will be
531                                 if( slide.classList.contains( 'stack' ) ) {
532                                         slide.style.top = 0;
533                                 }
534                                 else {
535                                         slide.style.top = Math.max( - ( slide.offsetHeight / 2 ) - 20, minTop ) + 'px';
536                                 }
537                         }
538
539                 }
540
541         }
542
543         /**
544          * Stores the vertical index of a stack so that the same 
545          * vertical slide can be selected when navigating to and 
546          * from the stack.
547          * 
548          * @param {HTMLElement} stack The vertical stack element
549          * @param {int} v Index to memorize
550          */
551         function setPreviousVerticalIndex( stack, v ) {
552                 if( stack ) {
553                         stack.setAttribute( 'data-previous-indexv', v || 0 );
554                 }
555         }
556
557         /**
558          * Retrieves the vertical index which was stored using 
559          * #setPreviousVerticalIndex() or 0 if no previous index
560          * exists.
561          *
562          * @param {HTMLElement} stack The vertical stack element
563          */
564         function getPreviousVerticalIndex( stack ) {
565                 if( stack && stack.classList.contains( 'stack' ) ) {
566                         return parseInt( stack.getAttribute( 'data-previous-indexv' ) || 0, 10 );
567                 }
568
569                 return 0;
570         }
571
572         /**
573          * Displays the overview of slides (quick nav) by
574          * scaling down and arranging all slide elements.
575          *
576          * Experimental feature, might be dropped if perf
577          * can't be improved.
578          */
579         function activateOverview() {
580
581                 // Only proceed if enabled in config
582                 if( config.overview ) {
583
584                         dom.wrapper.classList.add( 'overview' );
585
586                         clearTimeout( activateOverviewTimeout );
587
588                         // Not the pretties solution, but need to let the overview 
589                         // class apply first so that slides are measured accurately 
590                         // before we can positon them
591                         activateOverviewTimeout = setTimeout( function(){
592
593                                 var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR );
594
595                                 for( var i = 0, len1 = horizontalSlides.length; i < len1; i++ ) {
596                                         var hslide = horizontalSlides[i],
597                                                 htransform = 'translateZ(-2500px) translate(' + ( ( i - indexh ) * 105 ) + '%, 0%)';
598
599                                         hslide.setAttribute( 'data-index-h', i );
600                                         hslide.style.display = 'block';
601                                         hslide.style.WebkitTransform = htransform;
602                                         hslide.style.MozTransform = htransform;
603                                         hslide.style.msTransform = htransform;
604                                         hslide.style.OTransform = htransform;
605                                         hslide.style.transform = htransform;
606
607                                         if( hslide.classList.contains( 'stack' ) ) {
608
609                                                 var verticalSlides = hslide.querySelectorAll( 'section' );
610
611                                                 for( var j = 0, len2 = verticalSlides.length; j < len2; j++ ) {
612                                                         var verticalIndex = i === indexh ? indexv : getPreviousVerticalIndex( hslide );
613
614                                                         var vslide = verticalSlides[j],
615                                                                 vtransform = 'translate(0%, ' + ( ( j - verticalIndex ) * 105 ) + '%)';
616
617                                                         vslide.setAttribute( 'data-index-h', i );
618                                                         vslide.setAttribute( 'data-index-v', j );
619                                                         vslide.style.display = 'block';
620                                                         vslide.style.WebkitTransform = vtransform;
621                                                         vslide.style.MozTransform = vtransform;
622                                                         vslide.style.msTransform = vtransform;
623                                                         vslide.style.OTransform = vtransform;
624                                                         vslide.style.transform = vtransform;
625
626                                                         // Navigate to this slide on click
627                                                         vslide.addEventListener( 'click', onOverviewSlideClicked, true );
628                                                 }
629
630                                         }
631                                         else {
632
633                                                 // Navigate to this slide on click
634                                                 hslide.addEventListener( 'click', onOverviewSlideClicked, true );
635
636                                         }
637                                 }
638
639                                 layout();
640
641                         }, 10 );
642
643                 }
644
645         }
646
647         /**
648          * Exits the slide overview and enters the currently
649          * active slide.
650          */
651         function deactivateOverview() {
652
653                 // Only proceed if enabled in config
654                 if( config.overview ) {
655
656                         clearTimeout( activateOverviewTimeout );
657
658                         dom.wrapper.classList.remove( 'overview' );
659
660                         // Select all slides
661                         var slides = toArray( document.querySelectorAll( SLIDES_SELECTOR ) );
662
663                         for( var i = 0, len = slides.length; i < len; i++ ) {
664                                 var element = slides[i];
665
666                                 element.style.display = '';
667
668                                 // Resets all transforms to use the external styles
669                                 element.style.WebkitTransform = '';
670                                 element.style.MozTransform = '';
671                                 element.style.msTransform = '';
672                                 element.style.OTransform = '';
673                                 element.style.transform = '';
674
675                                 element.removeEventListener( 'click', onOverviewSlideClicked, true );
676                         }
677
678                         slide( indexh, indexv );
679
680                 }
681         }
682
683         /**
684          * Toggles the slide overview mode on and off.
685          *
686          * @param {Boolean} override Optional flag which overrides the
687          * toggle logic and forcibly sets the desired state. True means
688          * overview is open, false means it's closed.
689          */
690         function toggleOverview( override ) {
691                 if( typeof override === 'boolean' ) {
692                         override ? activateOverview() : deactivateOverview();
693                 }
694                 else {
695                         isOverviewActive() ? deactivateOverview() : activateOverview();
696                 }
697         }
698
699         /**
700          * Checks if the overview is currently active.
701          *
702          * @return {Boolean} true if the overview is active,
703          * false otherwise
704          */
705         function isOverviewActive() {
706                 return dom.wrapper.classList.contains( 'overview' );
707         }
708
709         /**
710          * Handling the fullscreen functionality via the fullscreen API
711          *
712          * @see http://fullscreen.spec.whatwg.org/
713          * @see https://developer.mozilla.org/en-US/docs/DOM/Using_fullscreen_mode
714          */
715         function enterFullscreen() {
716                 var element = document.body;
717
718                 // Check which implementation is available
719                 var requestMethod = element.requestFullScreen ||
720                                                         element.webkitRequestFullScreen ||
721                                                         element.mozRequestFullScreen ||
722                                                         element.msRequestFullScreen;
723
724                 if( requestMethod ) {
725                         requestMethod.apply( element );
726                 }
727         }
728
729         /**
730          * Enters the paused mode which fades everything on screen to
731          * black.
732          */
733         function pause() {
734                 dom.wrapper.classList.add( 'paused' );
735         }
736
737         /**
738          * Exits from the paused mode.
739          */
740         function resume() {
741                 dom.wrapper.classList.remove( 'paused' );
742         }
743
744         /**
745          * Toggles the paused mode on and off.
746          */
747         function togglePause() {
748                 if( isPaused() ) {
749                         resume();
750                 }
751                 else {
752                         pause();
753                 }
754         }
755
756         /**
757          * Checks if we are currently in the paused mode.
758          */
759         function isPaused() {
760                 return dom.wrapper.classList.contains( 'paused' );
761         }
762
763         /**
764          * Steps from the current point in the presentation to the
765          * slide which matches the specified horizontal and vertical
766          * indices.
767          *
768          * @param {int} h Horizontal index of the target slide
769          * @param {int} v Vertical index of the target slide
770          * @param {int} f Optional index of a fragment within the 
771          * target slide to activate
772          */
773         function slide( h, v, f ) {
774                 // Remember where we were at before
775                 previousSlide = currentSlide;
776
777                 // Query all horizontal slides in the deck
778                 var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR );
779
780                 // If no vertical index is specified and the upcoming slide is a 
781                 // stack, resume at its previous vertical index
782                 if( v === undefined ) {
783                         v = getPreviousVerticalIndex( horizontalSlides[ h ] );
784                 }
785
786                 // If we were on a vertical stack, remember what vertical index 
787                 // it was on so we can resume at the same position when returning
788                 if( previousSlide && previousSlide.parentNode && previousSlide.parentNode.classList.contains( 'stack' ) ) {
789                         setPreviousVerticalIndex( previousSlide.parentNode, indexv );
790                 }
791
792                 // Remember the state before this slide
793                 var stateBefore = state.concat();
794
795                 // Reset the state array
796                 state.length = 0;
797
798                 var indexhBefore = indexh,
799                         indexvBefore = indexv;
800
801                 // Activate and transition to the new slide
802                 indexh = updateSlides( HORIZONTAL_SLIDES_SELECTOR, h === undefined ? indexh : h );
803                 indexv = updateSlides( VERTICAL_SLIDES_SELECTOR, v === undefined ? indexv : v );
804
805                 layout();
806
807                 // Apply the new state
808                 stateLoop: for( var i = 0, len = state.length; i < len; i++ ) {
809                         // Check if this state existed on the previous slide. If it
810                         // did, we will avoid adding it repeatedly
811                         for( var j = 0; j < stateBefore.length; j++ ) {
812                                 if( stateBefore[j] === state[i] ) {
813                                         stateBefore.splice( j, 1 );
814                                         continue stateLoop;
815                                 }
816                         }
817
818                         document.documentElement.classList.add( state[i] );
819
820                         // Dispatch custom event matching the state's name
821                         dispatchEvent( state[i] );
822                 }
823
824                 // Clean up the remaints of the previous state
825                 while( stateBefore.length ) {
826                         document.documentElement.classList.remove( stateBefore.pop() );
827                 }
828
829                 // If the overview is active, re-activate it to update positions
830                 if( isOverviewActive() ) {
831                         activateOverview();
832                 }
833
834                 // Update the URL hash after a delay since updating it mid-transition
835                 // is likely to cause visual lag
836                 writeURL( 1500 );
837
838                 // Find the current horizontal slide and any possible vertical slides
839                 // within it
840                 var currentHorizontalSlide = horizontalSlides[ indexh ],
841                         currentVerticalSlides = currentHorizontalSlide.querySelectorAll( 'section' );
842
843                 // Store references to the previous and current slides
844                 currentSlide = currentVerticalSlides[ indexv ] || currentHorizontalSlide;
845
846
847                 // Show fragment, if specified
848                 if( ( indexh !== indexhBefore || indexv !== indexvBefore ) && f ) {
849                         var fragments = currentSlide.querySelectorAll( '.fragment' );
850
851                         toArray( fragments ).forEach( function( fragment, indexf ) {
852                                 if( indexf < f ) {
853                                         fragment.classList.add( 'visible' );
854                                 }
855                                 else {
856                                         fragment.classList.remove( 'visible' );
857                                 }
858                         } );
859                 }
860
861                 // Dispatch an event if the slide changed
862                 if( indexh !== indexhBefore || indexv !== indexvBefore ) {
863                         dispatchEvent( 'slidechanged', {
864                                 'indexh': indexh,
865                                 'indexv': indexv,
866                                 'previousSlide': previousSlide,
867                                 'currentSlide': currentSlide
868                         } );
869                 }
870                 else {
871                         // Ensure that the previous slide is never the same as the current
872                         previousSlide = null;
873                 }
874
875                 // Solves an edge case where the previous slide maintains the
876                 // 'present' class when navigating between adjacent vertical
877                 // stacks
878                 if( previousSlide ) {
879                         previousSlide.classList.remove( 'present' );
880                 }
881
882                 updateControls();
883                 updateProgress();
884         }
885
886         /**
887          * Updates one dimension of slides by showing the slide
888          * with the specified index.
889          *
890          * @param {String} selector A CSS selector that will fetch
891          * the group of slides we are working with
892          * @param {Number} index The index of the slide that should be
893          * shown
894          *
895          * @return {Number} The index of the slide that is now shown,
896          * might differ from the passed in index if it was out of
897          * bounds.
898          */
899         function updateSlides( selector, index ) {
900                 // Select all slides and convert the NodeList result to
901                 // an array
902                 var slides = toArray( document.querySelectorAll( selector ) ),
903                         slidesLength = slides.length;
904
905                 if( slidesLength ) {
906
907                         // Should the index loop?
908                         if( config.loop ) {
909                                 index %= slidesLength;
910
911                                 if( index < 0 ) {
912                                         index = slidesLength + index;
913                                 }
914                         }
915
916                         // Enforce max and minimum index bounds
917                         index = Math.max( Math.min( index, slidesLength - 1 ), 0 );
918
919                         for( var i = 0; i < slidesLength; i++ ) {
920                                 var element = slides[i];
921
922                                 // Optimization; hide all slides that are three or more steps
923                                 // away from the present slide
924                                 if( isOverviewActive() === false ) {
925                                         // The distance loops so that it measures 1 between the first
926                                         // and last slides
927                                         var distance = Math.abs( ( index - i ) % ( slidesLength - 3 ) ) || 0;
928
929                                         element.style.display = distance > 3 ? 'none' : 'block';
930                                 }
931
932                                 slides[i].classList.remove( 'past' );
933                                 slides[i].classList.remove( 'present' );
934                                 slides[i].classList.remove( 'future' );
935
936                                 if( i < index ) {
937                                         // Any element previous to index is given the 'past' class
938                                         slides[i].classList.add( 'past' );
939                                 }
940                                 else if( i > index ) {
941                                         // Any element subsequent to index is given the 'future' class
942                                         slides[i].classList.add( 'future' );
943                                 }
944
945                                 // If this element contains vertical slides
946                                 if( element.querySelector( 'section' ) ) {
947                                         slides[i].classList.add( 'stack' );
948                                 }
949                         }
950
951                         // Mark the current slide as present
952                         slides[index].classList.add( 'present' );
953
954                         // If this slide has a state associated with it, add it
955                         // onto the current state of the deck
956                         var slideState = slides[index].getAttribute( 'data-state' );
957                         if( slideState ) {
958                                 state = state.concat( slideState.split( ' ' ) );
959                         }
960
961                         // If this slide has a data-autoslide attribtue associated use this as
962                         // autoSlide value otherwise use the global configured time
963                         var slideAutoSlide = slides[index].getAttribute( 'data-autoslide' );
964                         if( slideAutoSlide ) {
965                                 autoSlide = parseInt( slideAutoSlide, 10 );
966                         } 
967                         else {
968                                 autoSlide = config.autoSlide;
969                         }
970
971                 }
972                 else {
973                         // Since there are no slides we can't be anywhere beyond the
974                         // zeroth index
975                         index = 0;
976                 }
977
978                 return index;
979
980         }
981
982         /**
983          * Updates the progress bar to reflect the current slide.
984          */
985         function updateProgress() {
986                 // Update progress if enabled
987                 if( config.progress && dom.progress ) {
988
989                         var horizontalSlides = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
990
991                         // The number of past and total slides
992                         var totalCount = document.querySelectorAll( SLIDES_SELECTOR + ':not(.stack)' ).length;
993                         var pastCount = 0;
994
995                         // Step through all slides and count the past ones
996                         mainLoop: for( var i = 0; i < horizontalSlides.length; i++ ) {
997
998                                 var horizontalSlide = horizontalSlides[i];
999                                 var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) );
1000
1001                                 for( var j = 0; j < verticalSlides.length; j++ ) {
1002
1003                                         // Stop as soon as we arrive at the present
1004                                         if( verticalSlides[j].classList.contains( 'present' ) ) {
1005                                                 break mainLoop;
1006                                         }
1007
1008                                         pastCount++;
1009
1010                                 }
1011
1012                                 // Stop as soon as we arrive at the present
1013                                 if( horizontalSlide.classList.contains( 'present' ) ) {
1014                                         break;
1015                                 }
1016
1017                                 // Don't count the wrapping section for vertical slides
1018                                 if( horizontalSlide.classList.contains( 'stack' ) === false ) {
1019                                         pastCount++;
1020                                 }
1021
1022                         }
1023
1024                         dom.progressbar.style.width = ( pastCount / ( totalCount - 1 ) ) * window.innerWidth + 'px';
1025
1026                 }
1027         }
1028
1029         /**
1030          * Updates the state of all control/navigation arrows.
1031          */
1032         function updateControls() {
1033                 if ( config.controls && dom.controls ) {
1034
1035                         var routes = availableRoutes();
1036
1037                         // Remove the 'enabled' class from all directions
1038                         dom.controlsLeft.concat( dom.controlsRight )
1039                                                         .concat( dom.controlsUp )
1040                                                         .concat( dom.controlsDown )
1041                                                         .concat( dom.controlsPrev )
1042                                                         .concat( dom.controlsNext ).forEach( function( node ) {
1043                                 node.classList.remove( 'enabled' );
1044                         } );
1045
1046                         // Add the 'enabled' class to the available routes
1047                         if( routes.left ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'enabled' );     } );
1048                         if( routes.right ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'enabled' ); } );
1049                         if( routes.up ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'enabled' ); } );
1050                         if( routes.down ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'enabled' ); } );
1051
1052                         // Prev/next buttons
1053                         if( routes.left || routes.up ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'enabled' ); } );
1054                         if( routes.right || routes.down ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'enabled' ); } );
1055
1056                 }
1057         }
1058
1059         /**
1060          * Determine what available routes there are for navigation.
1061          *
1062          * @return {Object} containing four booleans: left/right/up/down
1063          */
1064         function availableRoutes() {
1065                 var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ),
1066                         verticalSlides = document.querySelectorAll( VERTICAL_SLIDES_SELECTOR );
1067
1068                 return {
1069                         left: indexh > 0,
1070                         right: indexh < horizontalSlides.length - 1,
1071                         up: indexv > 0,
1072                         down: indexv < verticalSlides.length - 1
1073                 };
1074         }
1075
1076         /**
1077          * Reads the current URL (hash) and navigates accordingly.
1078          */
1079         function readURL() {
1080                 var hash = window.location.hash;
1081
1082                 // Attempt to parse the hash as either an index or name
1083                 var bits = hash.slice( 2 ).split( '/' ),
1084                         name = hash.replace( /#|\//gi, '' );
1085
1086                 // If the first bit is invalid and there is a name we can
1087                 // assume that this is a named link
1088                 if( isNaN( parseInt( bits[0], 10 ) ) && name.length ) {
1089                         // Find the slide with the specified name
1090                         var element = document.querySelector( '#' + name );
1091
1092                         if( element ) {
1093                                 // Find the position of the named slide and navigate to it
1094                                 var indices = Reveal.getIndices( element );
1095                                 slide( indices.h, indices.v );
1096                         }
1097                         // If the slide doesn't exist, navigate to the current slide
1098                         else {
1099                                 slide( indexh, indexv );
1100                         }
1101                 }
1102                 else {
1103                         // Read the index components of the hash
1104                         var h = parseInt( bits[0], 10 ) || 0,
1105                                 v = parseInt( bits[1], 10 ) || 0;
1106
1107                         slide( h, v );
1108                 }
1109         }
1110
1111         /**
1112          * Updates the page URL (hash) to reflect the current
1113          * state.
1114          *
1115          * @param {Number} delay The time in ms to wait before 
1116          * writing the hash
1117          */
1118         function writeURL( delay ) {
1119                 if( config.history ) {
1120
1121                         // Make sure there's never more than one timeout running
1122                         clearTimeout( writeURLTimeout );
1123
1124                         // If a delay is specified, timeout this call
1125                         if( typeof delay === 'number' ) {
1126                                 writeURLTimeout = setTimeout( writeURL, delay );
1127                         }
1128                         else {
1129                                 var url = '/';
1130
1131                                 // If the current slide has an ID, use that as a named link
1132                                 if( currentSlide && typeof currentSlide.getAttribute( 'id' ) === 'string' ) {
1133                                         url = '/' + currentSlide.getAttribute( 'id' );
1134                                 }
1135                                 // Otherwise use the /h/v index
1136                                 else {
1137                                         if( indexh > 0 || indexv > 0 ) url += indexh;
1138                                         if( indexv > 0 ) url += '/' + indexv;
1139                                 }
1140
1141                                 window.location.hash = url;
1142                         }
1143                 }
1144         }
1145
1146         /**
1147          * Retrieves the h/v location of the current, or specified,
1148          * slide.
1149          *
1150          * @param {HTMLElement} slide If specified, the returned
1151          * index will be for this slide rather than the currently
1152          * active one
1153          *
1154          * @return {Object} { h: <int>, v: <int> }
1155          */
1156         function getIndices( slide ) {
1157                 // By default, return the current indices
1158                 var h = indexh,
1159                         v = indexv;
1160
1161                 // If a slide is specified, return the indices of that slide
1162                 if( slide ) {
1163                         var isVertical = !!slide.parentNode.nodeName.match( /section/gi );
1164                         var slideh = isVertical ? slide.parentNode : slide;
1165
1166                         // Select all horizontal slides
1167                         var horizontalSlides = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
1168
1169                         // Now that we know which the horizontal slide is, get its index
1170                         h = Math.max( horizontalSlides.indexOf( slideh ), 0 );
1171
1172                         // If this is a vertical slide, grab the vertical index
1173                         if( isVertical ) {
1174                                 v = Math.max( toArray( slide.parentNode.querySelectorAll( 'section' ) ).indexOf( slide ), 0 );
1175                         }
1176                 }
1177
1178                 return { h: h, v: v };
1179         }
1180
1181         /**
1182          * Navigate to the next slide fragment.
1183          *
1184          * @return {Boolean} true if there was a next fragment,
1185          * false otherwise
1186          */
1187         function nextFragment() {
1188                 // Vertical slides:
1189                 if( document.querySelector( VERTICAL_SLIDES_SELECTOR + '.present' ) ) {
1190                         var verticalFragments = document.querySelectorAll( VERTICAL_SLIDES_SELECTOR + '.present .fragment:not(.visible)' );
1191                         if( verticalFragments.length ) {
1192                                 verticalFragments[0].classList.add( 'visible' );
1193
1194                                 // Notify subscribers of the change
1195                                 dispatchEvent( 'fragmentshown', { fragment: verticalFragments[0] } );
1196                                 return true;
1197                         }
1198                 }
1199                 // Horizontal slides:
1200                 else {
1201                         var horizontalFragments = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.present .fragment:not(.visible)' );
1202                         if( horizontalFragments.length ) {
1203                                 horizontalFragments[0].classList.add( 'visible' );
1204
1205                                 // Notify subscribers of the change
1206                                 dispatchEvent( 'fragmentshown', { fragment: horizontalFragments[0] } );
1207                                 return true;
1208                         }
1209                 }
1210
1211                 return false;
1212         }
1213
1214         /**
1215          * Navigate to the previous slide fragment.
1216          *
1217          * @return {Boolean} true if there was a previous fragment,
1218          * false otherwise
1219          */
1220         function previousFragment() {
1221                 // Vertical slides:
1222                 if( document.querySelector( VERTICAL_SLIDES_SELECTOR + '.present' ) ) {
1223                         var verticalFragments = document.querySelectorAll( VERTICAL_SLIDES_SELECTOR + '.present .fragment.visible' );
1224                         if( verticalFragments.length ) {
1225                                 verticalFragments[ verticalFragments.length - 1 ].classList.remove( 'visible' );
1226
1227                                 // Notify subscribers of the change
1228                                 dispatchEvent( 'fragmenthidden', { fragment: verticalFragments[ verticalFragments.length - 1 ] } );
1229                                 return true;
1230                         }
1231                 }
1232                 // Horizontal slides:
1233                 else {
1234                         var horizontalFragments = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.present .fragment.visible' );
1235                         if( horizontalFragments.length ) {
1236                                 horizontalFragments[ horizontalFragments.length - 1 ].classList.remove( 'visible' );
1237
1238                                 // Notify subscribers of the change
1239                                 dispatchEvent( 'fragmenthidden', { fragment: horizontalFragments[ horizontalFragments.length - 1 ] } );
1240                                 return true;
1241                         }
1242                 }
1243
1244                 return false;
1245         }
1246
1247         /**
1248          * Cues a new automated slide if enabled in the config.
1249          */
1250         function cueAutoSlide() {
1251                 clearTimeout( autoSlideTimeout );
1252
1253                 // Cue the next auto-slide if enabled
1254                 if( autoSlide ) {
1255                         autoSlideTimeout = setTimeout( navigateNext, autoSlide );
1256                 }
1257         }
1258
1259         function navigateLeft() {
1260                 // Prioritize hiding fragments
1261                 if( availableRoutes().left && isOverviewActive() || previousFragment() === false ) {
1262                         slide( indexh - 1 );
1263                 }
1264         }
1265
1266         function navigateRight() {
1267                 // Prioritize revealing fragments
1268                 if( availableRoutes().right && isOverviewActive() || nextFragment() === false ) {
1269                         slide( indexh + 1 );
1270                 }
1271         }
1272
1273         function navigateUp() {
1274                 // Prioritize hiding fragments
1275                 if( availableRoutes().up && isOverviewActive() || previousFragment() === false ) {
1276                         slide( indexh, indexv - 1 );
1277                 }
1278         }
1279
1280         function navigateDown() {
1281                 // Prioritize revealing fragments
1282                 if( availableRoutes().down && isOverviewActive() || nextFragment() === false ) {
1283                         slide( indexh, indexv + 1 );
1284                 }
1285         }
1286
1287         /**
1288          * Navigates backwards, prioritized in the following order:
1289          * 1) Previous fragment
1290          * 2) Previous vertical slide
1291          * 3) Previous horizontal slide
1292          */
1293         function navigatePrev() {
1294                 // Prioritize revealing fragments
1295                 if( previousFragment() === false ) {
1296                         if( availableRoutes().up ) {
1297                                 navigateUp();
1298                         }
1299                         else {
1300                                 // Fetch the previous horizontal slide, if there is one
1301                                 var previousSlide = document.querySelector( HORIZONTAL_SLIDES_SELECTOR + '.past:nth-child(' + indexh + ')' );
1302
1303                                 if( previousSlide ) {
1304                                         indexv = ( previousSlide.querySelectorAll( 'section' ).length + 1 ) || undefined;
1305                                         indexh --;
1306                                         slide();
1307                                 }
1308                         }
1309                 }
1310         }
1311
1312         /**
1313          * Same as #navigatePrev() but navigates forwards.
1314          */
1315         function navigateNext() {
1316                 // Prioritize revealing fragments
1317                 if( nextFragment() === false ) {
1318                         availableRoutes().down ? navigateDown() : navigateRight();
1319                 }
1320
1321                 // If auto-sliding is enabled we need to cue up
1322                 // another timeout
1323                 cueAutoSlide();
1324         }
1325
1326
1327         // --------------------------------------------------------------------//
1328         // ----------------------------- EVENTS -------------------------------//
1329         // --------------------------------------------------------------------//
1330
1331
1332         /**
1333          * Handler for the document level 'keydown' event.
1334          *
1335          * @param {Object} event
1336          */
1337         function onDocumentKeyDown( event ) {
1338                 // Check if there's a focused element that could be using 
1339                 // the keyboard
1340                 var activeElement = document.activeElement;
1341                 var hasFocus = !!( document.activeElement && ( document.activeElement.type || document.activeElement.href || document.activeElement.contentEditable !== 'inherit' ) );
1342
1343                 // Disregard the event if there's a focused element or a 
1344                 // keyboard modifier key is present
1345                 if ( hasFocus || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey ) return;
1346
1347                 var triggered = true;
1348
1349                 switch( event.keyCode ) {
1350                         // p, page up
1351                         case 80: case 33: navigatePrev(); break;
1352                         // n, page down
1353                         case 78: case 34: navigateNext(); break;
1354                         // h, left
1355                         case 72: case 37: navigateLeft(); break;
1356                         // l, right
1357                         case 76: case 39: navigateRight(); break;
1358                         // k, up
1359                         case 75: case 38: navigateUp(); break;
1360                         // j, down
1361                         case 74: case 40: navigateDown(); break;
1362                         // home
1363                         case 36: slide( 0 ); break;
1364                         // end
1365                         case 35: slide( Number.MAX_VALUE ); break;
1366                         // space
1367                         case 32: isOverviewActive() ? deactivateOverview() : navigateNext(); break;
1368                         // return
1369                         case 13: isOverviewActive() ? deactivateOverview() : triggered = false; break;
1370                         // b, period
1371                         case 66: case 190: togglePause(); break;
1372                         // f
1373                         case 70: enterFullscreen(); break;
1374                         default:
1375                                 triggered = false;
1376                 }
1377
1378                 // If the input resulted in a triggered action we should prevent
1379                 // the browsers default behavior
1380                 if( triggered ) {
1381                         event.preventDefault();
1382                 }
1383                 else if ( event.keyCode === 27 && supports3DTransforms ) {
1384                         toggleOverview();
1385
1386                         event.preventDefault();
1387                 }
1388
1389                 // If auto-sliding is enabled we need to cue up
1390                 // another timeout
1391                 cueAutoSlide();
1392
1393         }
1394
1395         /**
1396          * Handler for the document level 'touchstart' event,
1397          * enables support for swipe and pinch gestures.
1398          */
1399         function onDocumentTouchStart( event ) {
1400                 touch.startX = event.touches[0].clientX;
1401                 touch.startY = event.touches[0].clientY;
1402                 touch.startCount = event.touches.length;
1403
1404                 // If there's two touches we need to memorize the distance
1405                 // between those two points to detect pinching
1406                 if( event.touches.length === 2 && config.overview ) {
1407                         touch.startSpan = distanceBetween( {
1408                                 x: event.touches[1].clientX,
1409                                 y: event.touches[1].clientY
1410                         }, {
1411                                 x: touch.startX,
1412                                 y: touch.startY
1413                         } );
1414                 }
1415         }
1416
1417         /**
1418          * Handler for the document level 'touchmove' event.
1419          */
1420         function onDocumentTouchMove( event ) {
1421                 // Each touch should only trigger one action
1422                 if( !touch.handled ) {
1423                         var currentX = event.touches[0].clientX;
1424                         var currentY = event.touches[0].clientY;
1425
1426                         // If the touch started off with two points and still has
1427                         // two active touches; test for the pinch gesture
1428                         if( event.touches.length === 2 && touch.startCount === 2 && config.overview ) {
1429
1430                                 // The current distance in pixels between the two touch points
1431                                 var currentSpan = distanceBetween( {
1432                                         x: event.touches[1].clientX,
1433                                         y: event.touches[1].clientY
1434                                 }, {
1435                                         x: touch.startX,
1436                                         y: touch.startY
1437                                 } );
1438
1439                                 // If the span is larger than the desire amount we've got
1440                                 // ourselves a pinch
1441                                 if( Math.abs( touch.startSpan - currentSpan ) > touch.threshold ) {
1442                                         touch.handled = true;
1443
1444                                         if( currentSpan < touch.startSpan ) {
1445                                                 activateOverview();
1446                                         }
1447                                         else {
1448                                                 deactivateOverview();
1449                                         }
1450                                 }
1451
1452                                 event.preventDefault();
1453
1454                         }
1455                         // There was only one touch point, look for a swipe
1456                         else if( event.touches.length === 1 && touch.startCount !== 2 ) {
1457
1458                                 var deltaX = currentX - touch.startX,
1459                                         deltaY = currentY - touch.startY;
1460
1461                                 if( deltaX > touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
1462                                         touch.handled = true;
1463                                         navigateLeft();
1464                                 }
1465                                 else if( deltaX < -touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
1466                                         touch.handled = true;
1467                                         navigateRight();
1468                                 }
1469                                 else if( deltaY > touch.threshold ) {
1470                                         touch.handled = true;
1471                                         navigateUp();
1472                                 }
1473                                 else if( deltaY < -touch.threshold ) {
1474                                         touch.handled = true;
1475                                         navigateDown();
1476                                 }
1477
1478                                 event.preventDefault();
1479
1480                         }
1481                 }
1482                 // There's a bug with swiping on some Android devices unless
1483                 // the default action is always prevented
1484                 else if( navigator.userAgent.match( /android/gi ) ) {
1485                         event.preventDefault();
1486                 }
1487         }
1488
1489         /**
1490          * Handler for the document level 'touchend' event.
1491          */
1492         function onDocumentTouchEnd( event ) {
1493                 touch.handled = false;
1494         }
1495
1496         /**
1497          * Handles mouse wheel scrolling, throttled to avoid skipping
1498          * multiple slides.
1499          */
1500         function onDocumentMouseScroll( event ){
1501                 clearTimeout( mouseWheelTimeout );
1502
1503                 mouseWheelTimeout = setTimeout( function() {
1504                         var delta = event.detail || -event.wheelDelta;
1505                         if( delta > 0 ) {
1506                                 navigateNext();
1507                         }
1508                         else {
1509                                 navigatePrev();
1510                         }
1511                 }, 100 );
1512         }
1513
1514         /**
1515          * Clicking on the progress bar results in a navigation to the
1516          * closest approximate horizontal slide using this equation:
1517          *
1518          * ( clickX / presentationWidth ) * numberOfSlides
1519          */
1520         function onProgressClick( event ) {
1521                 var slidesTotal = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).length;
1522                 var slideIndex = Math.floor( ( event.clientX / dom.wrapper.offsetWidth ) * slidesTotal );
1523
1524                 slide( slideIndex );
1525         }
1526
1527         /**
1528          * Handler for the window level 'hashchange' event.
1529          */
1530         function onWindowHashChange( event ) {
1531                 readURL();
1532         }
1533
1534         /**
1535          * Handler for the window level 'resize' event.
1536          */
1537         function onWindowResize( event ) {
1538                 layout();
1539         }
1540
1541         /**
1542          * Invoked when a slide is and we're in the overview.
1543          */
1544         function onOverviewSlideClicked( event ) {
1545                 // TODO There's a bug here where the event listeners are not
1546                 // removed after deactivating the overview.
1547                 if( isOverviewActive() ) {
1548                         event.preventDefault();
1549
1550                         deactivateOverview();
1551
1552                         var element = event.target;
1553
1554                         while( element && !element.nodeName.match( /section/gi ) ) {
1555                                 element = element.parentNode;
1556                         }
1557
1558                         if( element.nodeName.match( /section/gi ) ) {
1559                                 var h = parseInt( element.getAttribute( 'data-index-h' ), 10 ),
1560                                         v = parseInt( element.getAttribute( 'data-index-v' ), 10 );
1561
1562                                 slide( h, v );
1563                         }
1564                 }
1565         }
1566
1567
1568         // --------------------------------------------------------------------//
1569         // ------------------------------- API --------------------------------//
1570         // --------------------------------------------------------------------//
1571
1572
1573         return {
1574                 initialize: initialize,
1575
1576                 // Navigation methods
1577                 slide: slide,
1578                 left: navigateLeft,
1579                 right: navigateRight,
1580                 up: navigateUp,
1581                 down: navigateDown,
1582                 prev: navigatePrev,
1583                 next: navigateNext,
1584                 prevFragment: previousFragment,
1585                 nextFragment: nextFragment,
1586
1587                 // Deprecated aliases
1588                 navigateTo: slide,
1589                 navigateLeft: navigateLeft,
1590                 navigateRight: navigateRight,
1591                 navigateUp: navigateUp,
1592                 navigateDown: navigateDown,
1593                 navigatePrev: navigatePrev,
1594                 navigateNext: navigateNext,
1595
1596                 // Toggles the overview mode on/off
1597                 toggleOverview: toggleOverview,
1598
1599                 // Adds or removes all internal event listeners (such as keyboard)
1600                 addEventListeners: addEventListeners,
1601                 removeEventListeners: removeEventListeners,
1602
1603                 // Returns the indices of the current, or specified, slide
1604                 getIndices: getIndices,
1605
1606                 // Returns the previous slide element, may be null
1607                 getPreviousSlide: function() {
1608                         return previousSlide;
1609                 },
1610
1611                 // Returns the current slide element
1612                 getCurrentSlide: function() {
1613                         return currentSlide;
1614                 },
1615
1616                 // Helper method, retrieves query string as a key/value hash
1617                 getQueryHash: function() {
1618                         var query = {};
1619
1620                         location.search.replace( /[A-Z0-9]+?=(\w*)/gi, function(a) {
1621                                 query[ a.split( '=' ).shift() ] = a.split( '=' ).pop();
1622                         } );
1623
1624                         return query;
1625                 },
1626
1627                 // Forward event binding to the reveal DOM element
1628                 addEventListener: function( type, listener, useCapture ) {
1629                         if( 'addEventListener' in window ) {
1630                                 ( dom.wrapper || document.querySelector( '.reveal' ) ).addEventListener( type, listener, useCapture );
1631                         }
1632                 },
1633                 removeEventListener: function( type, listener, useCapture ) {
1634                         if( 'addEventListener' in window ) {
1635                                 ( dom.wrapper || document.querySelector( '.reveal' ) ).removeEventListener( type, listener, useCapture );
1636                         }
1637                 }
1638         };
1639
1640 })();