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