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