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