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