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