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