slippery 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +15 -0
  2. data/.travis.yml +27 -0
  3. data/Gemfile +10 -0
  4. data/Gemfile.devtools +59 -0
  5. data/Gemfile.lock +220 -0
  6. data/LICENSE +9 -0
  7. data/README.md +122 -0
  8. data/Rakefile +2 -0
  9. data/assets/impress.js/css/impress-demo.css +703 -0
  10. data/assets/impress.js/js/impress.js +800 -0
  11. data/assets/reveal.js/css/print/paper.css +176 -0
  12. data/assets/reveal.js/css/print/pdf.css +190 -0
  13. data/assets/reveal.js/css/reveal.css +1616 -0
  14. data/assets/reveal.js/css/reveal.min.css +7 -0
  15. data/assets/reveal.js/css/theme/README.md +23 -0
  16. data/assets/reveal.js/css/theme/beige.css +142 -0
  17. data/assets/reveal.js/css/theme/default.css +142 -0
  18. data/assets/reveal.js/css/theme/moon.css +142 -0
  19. data/assets/reveal.js/css/theme/night.css +130 -0
  20. data/assets/reveal.js/css/theme/serif.css +132 -0
  21. data/assets/reveal.js/css/theme/simple.css +132 -0
  22. data/assets/reveal.js/css/theme/sky.css +139 -0
  23. data/assets/reveal.js/css/theme/solarized.css +142 -0
  24. data/assets/reveal.js/css/theme/source/beige.scss +50 -0
  25. data/assets/reveal.js/css/theme/source/default.scss +42 -0
  26. data/assets/reveal.js/css/theme/source/moon.scss +68 -0
  27. data/assets/reveal.js/css/theme/source/night.scss +35 -0
  28. data/assets/reveal.js/css/theme/source/serif.scss +35 -0
  29. data/assets/reveal.js/css/theme/source/simple.scss +38 -0
  30. data/assets/reveal.js/css/theme/source/sky.scss +46 -0
  31. data/assets/reveal.js/css/theme/source/solarized.scss +74 -0
  32. data/assets/reveal.js/css/theme/template/mixins.scss +29 -0
  33. data/assets/reveal.js/css/theme/template/settings.scss +33 -0
  34. data/assets/reveal.js/css/theme/template/theme.scss +163 -0
  35. data/assets/reveal.js/js/head.min.js +8 -0
  36. data/assets/reveal.js/js/reveal.js +2577 -0
  37. data/assets/reveal.js/js/reveal.min.js +8 -0
  38. data/assets/reveal.js/lib/css/zenburn.css +115 -0
  39. data/assets/reveal.js/lib/font/league_gothic-webfont.eot +0 -0
  40. data/assets/reveal.js/lib/font/league_gothic-webfont.svg +230 -0
  41. data/assets/reveal.js/lib/font/league_gothic-webfont.ttf +0 -0
  42. data/assets/reveal.js/lib/font/league_gothic-webfont.woff +0 -0
  43. data/assets/reveal.js/lib/font/league_gothic_license +2 -0
  44. data/assets/reveal.js/lib/js/classList.js +2 -0
  45. data/assets/reveal.js/lib/js/head.min.js +8 -0
  46. data/assets/reveal.js/lib/js/html5shiv.js +7 -0
  47. data/assets/reveal.js/plugin/highlight/highlight.js +31 -0
  48. data/assets/reveal.js/plugin/leap/leap.js +154 -0
  49. data/assets/reveal.js/plugin/markdown/example.html +97 -0
  50. data/assets/reveal.js/plugin/markdown/example.md +29 -0
  51. data/assets/reveal.js/plugin/markdown/markdown.js +190 -0
  52. data/assets/reveal.js/plugin/markdown/marked.js +37 -0
  53. data/assets/reveal.js/plugin/multiplex/client.js +13 -0
  54. data/assets/reveal.js/plugin/multiplex/index.js +56 -0
  55. data/assets/reveal.js/plugin/multiplex/master.js +50 -0
  56. data/assets/reveal.js/plugin/notes/notes.html +253 -0
  57. data/assets/reveal.js/plugin/notes/notes.js +100 -0
  58. data/assets/reveal.js/plugin/notes-server/client.js +57 -0
  59. data/assets/reveal.js/plugin/notes-server/index.js +59 -0
  60. data/assets/reveal.js/plugin/notes-server/notes.html +142 -0
  61. data/assets/reveal.js/plugin/postmessage/example.html +39 -0
  62. data/assets/reveal.js/plugin/postmessage/postmessage.js +42 -0
  63. data/assets/reveal.js/plugin/print-pdf/print-pdf.js +44 -0
  64. data/assets/reveal.js/plugin/remotes/remotes.js +39 -0
  65. data/assets/reveal.js/plugin/search/search.js +196 -0
  66. data/assets/reveal.js/plugin/zoom-js/zoom.js +256 -0
  67. data/config/flay.yml +3 -0
  68. data/config/flog.yml +2 -0
  69. data/config/mutant.yml +3 -0
  70. data/config/reek.yml +108 -0
  71. data/config/rubocop.yml +71 -0
  72. data/config/yardstick.yml +2 -0
  73. data/lib/slippery/converter.rb +130 -0
  74. data/lib/slippery/document.rb +20 -0
  75. data/lib/slippery/presentation.rb +36 -0
  76. data/lib/slippery/processor_helpers.rb +43 -0
  77. data/lib/slippery/processors/add_google_font.rb +27 -0
  78. data/lib/slippery/processors/graphviz_dot.rb +46 -0
  79. data/lib/slippery/processors/hr_to_sections.rb +36 -0
  80. data/lib/slippery/processors/impress_js/add_impress_js.rb +30 -0
  81. data/lib/slippery/processors/impress_js/auto_offsets.rb +25 -0
  82. data/lib/slippery/processors/reveal_js/add_reveal_js.rb +78 -0
  83. data/lib/slippery/processors/self_contained.rb +62 -0
  84. data/lib/slippery/version.rb +3 -0
  85. data/lib/slippery.rb +42 -0
  86. data/slippery.gemspec +25 -0
  87. data/spec/fixtures/blockquotes.md +6 -0
  88. data/spec/fixtures/code_blocks.md +9 -0
  89. data/spec/fixtures/definition_lists.md +2 -0
  90. data/spec/fixtures/header_and_paragraph.md +2 -0
  91. data/spec/fixtures/headers.md +13 -0
  92. data/spec/fixtures/ordered_list.md +3 -0
  93. data/spec/fixtures/unordered_list.md +3 -0
  94. data/spec/slippery/converter_spec.rb +67 -0
  95. data/spec/slippery_spec.rb +0 -0
  96. data/spec/spec_helper.rb +20 -0
  97. metadata +208 -0
@@ -0,0 +1,2577 @@
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 centring 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
+ // Turns fragments on and off globally
60
+ fragments: true,
61
+
62
+ // Number of milliseconds between automatically proceeding to the
63
+ // next slide, disabled when set to 0, this value can be overwritten
64
+ // by using a data-autoslide attribute on your slides
65
+ autoSlide: 0,
66
+
67
+ // Enable slide navigation via mouse wheel
68
+ mouseWheel: false,
69
+
70
+ // Apply a 3D roll to links on hover
71
+ rollingLinks: true,
72
+
73
+ // Opens links in an iframe preview overlay
74
+ previewLinks: false,
75
+
76
+ // Theme (see /css/theme)
77
+ theme: null,
78
+
79
+ // Transition style
80
+ transition: 'default', // default/cube/page/concave/zoom/linear/fade/none
81
+
82
+ // Transition speed
83
+ transitionSpeed: 'default', // default/fast/slow
84
+
85
+ // Transition style for full page slide backgrounds
86
+ backgroundTransition: 'default', // default/linear
87
+
88
+ // Script dependencies to load
89
+ dependencies: []
90
+ },
91
+
92
+ // The current auto-slide duration
93
+ autoSlide = 0,
94
+
95
+ // The horizontal and vertical index of the currently active slide
96
+ indexh = 0,
97
+ indexv = 0,
98
+
99
+ // The previous and current slide HTML elements
100
+ previousSlide,
101
+ currentSlide,
102
+
103
+ // Slides may hold a data-state attribute which we pick up and apply
104
+ // as a class to the body. This list contains the combined state of
105
+ // all current slides.
106
+ state = [],
107
+
108
+ // The current scale of the presentation (see width/height config)
109
+ scale = 1,
110
+
111
+ // Cached references to DOM elements
112
+ dom = {},
113
+
114
+ // Detect support for CSS 3D transforms
115
+ supports3DTransforms = 'WebkitPerspective' in document.body.style ||
116
+ 'MozPerspective' in document.body.style ||
117
+ 'msPerspective' in document.body.style ||
118
+ 'OPerspective' in document.body.style ||
119
+ 'perspective' in document.body.style,
120
+
121
+ // Detect support for CSS 2D transforms
122
+ supports2DTransforms = 'WebkitTransform' in document.body.style ||
123
+ 'MozTransform' in document.body.style ||
124
+ 'msTransform' in document.body.style ||
125
+ 'OTransform' in document.body.style ||
126
+ 'transform' in document.body.style,
127
+
128
+ // Throttles mouse wheel navigation
129
+ lastMouseWheelStep = 0,
130
+
131
+ // An interval used to automatically move on to the next slide
132
+ autoSlideTimeout = 0,
133
+
134
+ // Delays updates to the URL due to a Chrome thumbnailer bug
135
+ writeURLTimeout = 0,
136
+
137
+ // A delay used to activate the overview mode
138
+ activateOverviewTimeout = 0,
139
+
140
+ // A delay used to deactivate the overview mode
141
+ deactivateOverviewTimeout = 0,
142
+
143
+ // Flags if the interaction event listeners are bound
144
+ eventsAreBound = false,
145
+
146
+ // Holds information about the currently ongoing touch input
147
+ touch = {
148
+ startX: 0,
149
+ startY: 0,
150
+ startSpan: 0,
151
+ startCount: 0,
152
+ handled: false,
153
+ threshold: 80
154
+ };
155
+
156
+ /**
157
+ * Starts up the presentation if the client is capable.
158
+ */
159
+ function initialize( options ) {
160
+
161
+ if( !supports2DTransforms && !supports3DTransforms ) {
162
+ document.body.setAttribute( 'class', 'no-transforms' );
163
+
164
+ // If the browser doesn't support core features we won't be
165
+ // using JavaScript to control the presentation
166
+ return;
167
+ }
168
+
169
+ // Force a layout when the whole page, incl fonts, has loaded
170
+ window.addEventListener( 'load', layout, false );
171
+
172
+ // Copy options over to our config object
173
+ extend( config, options );
174
+
175
+ // Hide the address bar in mobile browsers
176
+ hideAddressBar();
177
+
178
+ // Loads the dependencies and continues to #start() once done
179
+ load();
180
+
181
+ }
182
+
183
+ /**
184
+ * Finds and stores references to DOM elements which are
185
+ * required by the presentation. If a required element is
186
+ * not found, it is created.
187
+ */
188
+ function setupDOM() {
189
+
190
+ // Cache references to key DOM elements
191
+ dom.theme = document.querySelector( '#theme' );
192
+ dom.wrapper = document.querySelector( '.reveal' );
193
+ dom.slides = document.querySelector( '.reveal .slides' );
194
+
195
+ // Background element
196
+ if( !document.querySelector( '.reveal .backgrounds' ) ) {
197
+ dom.background = document.createElement( 'div' );
198
+ dom.background.classList.add( 'backgrounds' );
199
+ dom.wrapper.appendChild( dom.background );
200
+ }
201
+
202
+ // Progress bar
203
+ if( !dom.wrapper.querySelector( '.progress' ) ) {
204
+ var progressElement = document.createElement( 'div' );
205
+ progressElement.classList.add( 'progress' );
206
+ progressElement.innerHTML = '<span></span>';
207
+ dom.wrapper.appendChild( progressElement );
208
+ }
209
+
210
+ // Arrow controls
211
+ if( !dom.wrapper.querySelector( '.controls' ) ) {
212
+ var controlsElement = document.createElement( 'aside' );
213
+ controlsElement.classList.add( 'controls' );
214
+ controlsElement.innerHTML = '<div class="navigate-left"></div>' +
215
+ '<div class="navigate-right"></div>' +
216
+ '<div class="navigate-up"></div>' +
217
+ '<div class="navigate-down"></div>';
218
+ dom.wrapper.appendChild( controlsElement );
219
+ }
220
+
221
+ // State background element [DEPRECATED]
222
+ if( !dom.wrapper.querySelector( '.state-background' ) ) {
223
+ var stateBackgroundElement = document.createElement( 'div' );
224
+ stateBackgroundElement.classList.add( 'state-background' );
225
+ dom.wrapper.appendChild( stateBackgroundElement );
226
+ }
227
+
228
+ // Overlay graphic which is displayed during the paused mode
229
+ if( !dom.wrapper.querySelector( '.pause-overlay' ) ) {
230
+ var pausedElement = document.createElement( 'div' );
231
+ pausedElement.classList.add( 'pause-overlay' );
232
+ dom.wrapper.appendChild( pausedElement );
233
+ }
234
+
235
+ // Cache references to elements
236
+ dom.progress = document.querySelector( '.reveal .progress' );
237
+ dom.progressbar = document.querySelector( '.reveal .progress span' );
238
+
239
+ if ( config.controls ) {
240
+ dom.controls = document.querySelector( '.reveal .controls' );
241
+
242
+ // There can be multiple instances of controls throughout the page
243
+ dom.controlsLeft = toArray( document.querySelectorAll( '.navigate-left' ) );
244
+ dom.controlsRight = toArray( document.querySelectorAll( '.navigate-right' ) );
245
+ dom.controlsUp = toArray( document.querySelectorAll( '.navigate-up' ) );
246
+ dom.controlsDown = toArray( document.querySelectorAll( '.navigate-down' ) );
247
+ dom.controlsPrev = toArray( document.querySelectorAll( '.navigate-prev' ) );
248
+ dom.controlsNext = toArray( document.querySelectorAll( '.navigate-next' ) );
249
+ }
250
+
251
+ }
252
+
253
+ /**
254
+ * Creates the slide background elements and appends them
255
+ * to the background container. One element is created per
256
+ * slide no matter if the given slide has visible background.
257
+ */
258
+ function createBackgrounds() {
259
+
260
+ if( isPrintingPDF() ) {
261
+ document.body.classList.add( 'print-pdf' );
262
+ }
263
+
264
+ // Clear prior backgrounds
265
+ dom.background.innerHTML = '';
266
+ dom.background.classList.add( 'no-transition' );
267
+
268
+ // Helper method for creating a background element for the
269
+ // given slide
270
+ function _createBackground( slide, container ) {
271
+
272
+ var data = {
273
+ background: slide.getAttribute( 'data-background' ),
274
+ backgroundSize: slide.getAttribute( 'data-background-size' ),
275
+ backgroundImage: slide.getAttribute( 'data-background-image' ),
276
+ backgroundColor: slide.getAttribute( 'data-background-color' ),
277
+ backgroundRepeat: slide.getAttribute( 'data-background-repeat' ),
278
+ backgroundPosition: slide.getAttribute( 'data-background-position' ),
279
+ backgroundTransition: slide.getAttribute( 'data-background-transition' )
280
+ };
281
+
282
+ var element = document.createElement( 'div' );
283
+ element.className = 'slide-background';
284
+
285
+ if( data.background ) {
286
+ // Auto-wrap image urls in url(...)
287
+ if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(png|jpg|jpeg|gif|bmp)$/gi.test( data.background ) ) {
288
+ element.style.backgroundImage = 'url('+ data.background +')';
289
+ }
290
+ else {
291
+ element.style.background = data.background;
292
+ }
293
+ }
294
+
295
+ // Additional and optional background properties
296
+ if( data.backgroundSize ) element.style.backgroundSize = data.backgroundSize;
297
+ if( data.backgroundImage ) element.style.backgroundImage = 'url("' + data.backgroundImage + '")';
298
+ if( data.backgroundColor ) element.style.backgroundColor = data.backgroundColor;
299
+ if( data.backgroundRepeat ) element.style.backgroundRepeat = data.backgroundRepeat;
300
+ if( data.backgroundPosition ) element.style.backgroundPosition = data.backgroundPosition;
301
+ if( data.backgroundTransition ) element.setAttribute( 'data-background-transition', data.backgroundTransition );
302
+
303
+ container.appendChild( element );
304
+
305
+ return element;
306
+
307
+ }
308
+
309
+ // Iterate over all horizontal slides
310
+ toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( slideh ) {
311
+
312
+ var backgroundStack;
313
+
314
+ if( isPrintingPDF() ) {
315
+ backgroundStack = _createBackground( slideh, slideh );
316
+ }
317
+ else {
318
+ backgroundStack = _createBackground( slideh, dom.background );
319
+ }
320
+
321
+ // Iterate over all vertical slides
322
+ toArray( slideh.querySelectorAll( 'section' ) ).forEach( function( slidev ) {
323
+
324
+ if( isPrintingPDF() ) {
325
+ _createBackground( slidev, slidev );
326
+ }
327
+ else {
328
+ _createBackground( slidev, backgroundStack );
329
+ }
330
+
331
+ } );
332
+
333
+ } );
334
+
335
+ }
336
+
337
+ /**
338
+ * Hides the address bar if we're on a mobile device.
339
+ */
340
+ function hideAddressBar() {
341
+
342
+ if( /iphone|ipod|android/gi.test( navigator.userAgent ) && !/crios/gi.test( navigator.userAgent ) ) {
343
+ // Events that should trigger the address bar to hide
344
+ window.addEventListener( 'load', removeAddressBar, false );
345
+ window.addEventListener( 'orientationchange', removeAddressBar, false );
346
+ }
347
+
348
+ }
349
+
350
+ /**
351
+ * Loads the dependencies of reveal.js. Dependencies are
352
+ * defined via the configuration option 'dependencies'
353
+ * and will be loaded prior to starting/binding reveal.js.
354
+ * Some dependencies may have an 'async' flag, if so they
355
+ * will load after reveal.js has been started up.
356
+ */
357
+ function load() {
358
+
359
+ var scripts = [],
360
+ scriptsAsync = [];
361
+
362
+ for( var i = 0, len = config.dependencies.length; i < len; i++ ) {
363
+ var s = config.dependencies[i];
364
+
365
+ // Load if there's no condition or the condition is truthy
366
+ if( !s.condition || s.condition() ) {
367
+ if( s.async ) {
368
+ scriptsAsync.push( s.src );
369
+ }
370
+ else {
371
+ scripts.push( s.src );
372
+ }
373
+
374
+ // Extension may contain callback functions
375
+ if( typeof s.callback === 'function' ) {
376
+ head.ready( s.src.match( /([\w\d_\-]*)\.?js$|[^\\\/]*$/i )[0], s.callback );
377
+ }
378
+ }
379
+ }
380
+
381
+ // Called once synchronous scripts finish loading
382
+ function proceed() {
383
+ if( scriptsAsync.length ) {
384
+ // Load asynchronous scripts
385
+ head.js.apply( null, scriptsAsync );
386
+ }
387
+
388
+ start();
389
+ }
390
+
391
+ if( scripts.length ) {
392
+ head.ready( proceed );
393
+
394
+ // Load synchronous scripts
395
+ head.js.apply( null, scripts );
396
+ }
397
+ else {
398
+ proceed();
399
+ }
400
+
401
+ }
402
+
403
+ /**
404
+ * Starts up reveal.js by binding input events and navigating
405
+ * to the current URL deeplink if there is one.
406
+ */
407
+ function start() {
408
+
409
+ // Make sure we've got all the DOM elements we need
410
+ setupDOM();
411
+
412
+ // Updates the presentation to match the current configuration values
413
+ configure();
414
+
415
+ // Read the initial hash
416
+ readURL();
417
+
418
+ // Notify listeners that the presentation is ready but use a 1ms
419
+ // timeout to ensure it's not fired synchronously after #initialize()
420
+ setTimeout( function() {
421
+ dispatchEvent( 'ready', {
422
+ 'indexh': indexh,
423
+ 'indexv': indexv,
424
+ 'currentSlide': currentSlide
425
+ } );
426
+ }, 1 );
427
+
428
+ }
429
+
430
+ /**
431
+ * Applies the configuration settings from the config
432
+ * object. May be called multiple times.
433
+ */
434
+ function configure( options ) {
435
+
436
+ dom.wrapper.classList.remove( config.transition );
437
+
438
+ // New config options may be passed when this method
439
+ // is invoked through the API after initialization
440
+ if( typeof options === 'object' ) extend( config, options );
441
+
442
+ // Force linear transition based on browser capabilities
443
+ if( supports3DTransforms === false ) config.transition = 'linear';
444
+
445
+ dom.wrapper.classList.add( config.transition );
446
+
447
+ dom.wrapper.setAttribute( 'data-transition-speed', config.transitionSpeed );
448
+ dom.wrapper.setAttribute( 'data-background-transition', config.backgroundTransition );
449
+
450
+ if( dom.controls ) {
451
+ dom.controls.style.display = ( config.controls && dom.controls ) ? 'block' : 'none';
452
+ }
453
+
454
+ if( dom.progress ) {
455
+ dom.progress.style.display = ( config.progress && dom.progress ) ? 'block' : 'none';
456
+ }
457
+
458
+ if( config.rtl ) {
459
+ dom.wrapper.classList.add( 'rtl' );
460
+ }
461
+ else {
462
+ dom.wrapper.classList.remove( 'rtl' );
463
+ }
464
+
465
+ if( config.center ) {
466
+ dom.wrapper.classList.add( 'center' );
467
+ }
468
+ else {
469
+ dom.wrapper.classList.remove( 'center' );
470
+ }
471
+
472
+ if( config.mouseWheel ) {
473
+ document.addEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF
474
+ document.addEventListener( 'mousewheel', onDocumentMouseScroll, false );
475
+ }
476
+ else {
477
+ document.removeEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF
478
+ document.removeEventListener( 'mousewheel', onDocumentMouseScroll, false );
479
+ }
480
+
481
+ // Rolling 3D links
482
+ if( config.rollingLinks ) {
483
+ enableRollingLinks();
484
+ }
485
+ else {
486
+ disableRollingLinks();
487
+ }
488
+
489
+ // Iframe link previews
490
+ if( config.previewLinks ) {
491
+ enablePreviewLinks();
492
+ }
493
+ else {
494
+ disablePreviewLinks();
495
+ enablePreviewLinks( '[data-preview-link]' );
496
+ }
497
+
498
+ // Load the theme in the config, if it's not already loaded
499
+ if( config.theme && dom.theme ) {
500
+ var themeURL = dom.theme.getAttribute( 'href' );
501
+ var themeFinder = /[^\/]*?(?=\.css)/;
502
+ var themeName = themeURL.match(themeFinder)[0];
503
+
504
+ if( config.theme !== themeName ) {
505
+ themeURL = themeURL.replace(themeFinder, config.theme);
506
+ dom.theme.setAttribute( 'href', themeURL );
507
+ }
508
+ }
509
+
510
+ sync();
511
+
512
+ }
513
+
514
+ /**
515
+ * Binds all event listeners.
516
+ */
517
+ function addEventListeners() {
518
+
519
+ eventsAreBound = true;
520
+
521
+ window.addEventListener( 'hashchange', onWindowHashChange, false );
522
+ window.addEventListener( 'resize', onWindowResize, false );
523
+
524
+ if( config.touch ) {
525
+ dom.wrapper.addEventListener( 'touchstart', onTouchStart, false );
526
+ dom.wrapper.addEventListener( 'touchmove', onTouchMove, false );
527
+ dom.wrapper.addEventListener( 'touchend', onTouchEnd, false );
528
+
529
+ // Support pointer-style touch interaction as well
530
+ if( window.navigator.msPointerEnabled ) {
531
+ dom.wrapper.addEventListener( 'MSPointerDown', onPointerDown, false );
532
+ dom.wrapper.addEventListener( 'MSPointerMove', onPointerMove, false );
533
+ dom.wrapper.addEventListener( 'MSPointerUp', onPointerUp, false );
534
+ }
535
+ }
536
+
537
+ if( config.keyboard ) {
538
+ document.addEventListener( 'keydown', onDocumentKeyDown, false );
539
+ }
540
+
541
+ if ( config.progress && dom.progress ) {
542
+ dom.progress.addEventListener( 'click', onProgressClicked, false );
543
+ }
544
+
545
+ if ( config.controls && dom.controls ) {
546
+ [ 'touchstart', 'click' ].forEach( function( eventName ) {
547
+ dom.controlsLeft.forEach( function( el ) { el.addEventListener( eventName, onNavigateLeftClicked, false ); } );
548
+ dom.controlsRight.forEach( function( el ) { el.addEventListener( eventName, onNavigateRightClicked, false ); } );
549
+ dom.controlsUp.forEach( function( el ) { el.addEventListener( eventName, onNavigateUpClicked, false ); } );
550
+ dom.controlsDown.forEach( function( el ) { el.addEventListener( eventName, onNavigateDownClicked, false ); } );
551
+ dom.controlsPrev.forEach( function( el ) { el.addEventListener( eventName, onNavigatePrevClicked, false ); } );
552
+ dom.controlsNext.forEach( function( el ) { el.addEventListener( eventName, onNavigateNextClicked, false ); } );
553
+ } );
554
+ }
555
+
556
+ }
557
+
558
+ /**
559
+ * Unbinds all event listeners.
560
+ */
561
+ function removeEventListeners() {
562
+
563
+ eventsAreBound = false;
564
+
565
+ document.removeEventListener( 'keydown', onDocumentKeyDown, false );
566
+ window.removeEventListener( 'hashchange', onWindowHashChange, false );
567
+ window.removeEventListener( 'resize', onWindowResize, false );
568
+
569
+ dom.wrapper.removeEventListener( 'touchstart', onTouchStart, false );
570
+ dom.wrapper.removeEventListener( 'touchmove', onTouchMove, false );
571
+ dom.wrapper.removeEventListener( 'touchend', onTouchEnd, false );
572
+
573
+ if( window.navigator.msPointerEnabled ) {
574
+ dom.wrapper.removeEventListener( 'MSPointerDown', onPointerDown, false );
575
+ dom.wrapper.removeEventListener( 'MSPointerMove', onPointerMove, false );
576
+ dom.wrapper.removeEventListener( 'MSPointerUp', onPointerUp, false );
577
+ }
578
+
579
+ if ( config.progress && dom.progress ) {
580
+ dom.progress.removeEventListener( 'click', onProgressClicked, false );
581
+ }
582
+
583
+ if ( config.controls && dom.controls ) {
584
+ [ 'touchstart', 'click' ].forEach( function( eventName ) {
585
+ dom.controlsLeft.forEach( function( el ) { el.removeEventListener( eventName, onNavigateLeftClicked, false ); } );
586
+ dom.controlsRight.forEach( function( el ) { el.removeEventListener( eventName, onNavigateRightClicked, false ); } );
587
+ dom.controlsUp.forEach( function( el ) { el.removeEventListener( eventName, onNavigateUpClicked, false ); } );
588
+ dom.controlsDown.forEach( function( el ) { el.removeEventListener( eventName, onNavigateDownClicked, false ); } );
589
+ dom.controlsPrev.forEach( function( el ) { el.removeEventListener( eventName, onNavigatePrevClicked, false ); } );
590
+ dom.controlsNext.forEach( function( el ) { el.removeEventListener( eventName, onNavigateNextClicked, false ); } );
591
+ } );
592
+ }
593
+
594
+ }
595
+
596
+ /**
597
+ * Extend object a with the properties of object b.
598
+ * If there's a conflict, object b takes precedence.
599
+ */
600
+ function extend( a, b ) {
601
+
602
+ for( var i in b ) {
603
+ a[ i ] = b[ i ];
604
+ }
605
+
606
+ }
607
+
608
+ /**
609
+ * Converts the target object to an array.
610
+ */
611
+ function toArray( o ) {
612
+
613
+ return Array.prototype.slice.call( o );
614
+
615
+ }
616
+
617
+ /**
618
+ * Measures the distance in pixels between point a
619
+ * and point b.
620
+ *
621
+ * @param {Object} a point with x/y properties
622
+ * @param {Object} b point with x/y properties
623
+ */
624
+ function distanceBetween( a, b ) {
625
+
626
+ var dx = a.x - b.x,
627
+ dy = a.y - b.y;
628
+
629
+ return Math.sqrt( dx*dx + dy*dy );
630
+
631
+ }
632
+
633
+ /**
634
+ * Retrieves the height of the given element by looking
635
+ * at the position and height of its immediate children.
636
+ */
637
+ function getAbsoluteHeight( element ) {
638
+
639
+ var height = 0;
640
+
641
+ if( element ) {
642
+ var absoluteChildren = 0;
643
+
644
+ toArray( element.childNodes ).forEach( function( child ) {
645
+
646
+ if( typeof child.offsetTop === 'number' && child.style ) {
647
+ // Count # of abs children
648
+ if( child.style.position === 'absolute' ) {
649
+ absoluteChildren += 1;
650
+ }
651
+
652
+ height = Math.max( height, child.offsetTop + child.offsetHeight );
653
+ }
654
+
655
+ } );
656
+
657
+ // If there are no absolute children, use offsetHeight
658
+ if( absoluteChildren === 0 ) {
659
+ height = element.offsetHeight;
660
+ }
661
+
662
+ }
663
+
664
+ return height;
665
+
666
+ }
667
+
668
+ /**
669
+ * Checks if this instance is being used to print a PDF.
670
+ */
671
+ function isPrintingPDF() {
672
+
673
+ return ( /print-pdf/gi ).test( window.location.search );
674
+
675
+ }
676
+
677
+ /**
678
+ * Causes the address bar to hide on mobile devices,
679
+ * more vertical space ftw.
680
+ */
681
+ function removeAddressBar() {
682
+
683
+ if( window.orientation === 0 ) {
684
+ document.documentElement.style.overflow = 'scroll';
685
+ document.body.style.height = '120%';
686
+ }
687
+ else {
688
+ document.documentElement.style.overflow = '';
689
+ document.body.style.height = '100%';
690
+ }
691
+
692
+ setTimeout( function() {
693
+ window.scrollTo( 0, 1 );
694
+ }, 10 );
695
+
696
+ }
697
+
698
+ /**
699
+ * Dispatches an event of the specified type from the
700
+ * reveal DOM element.
701
+ */
702
+ function dispatchEvent( type, properties ) {
703
+
704
+ var event = document.createEvent( "HTMLEvents", 1, 2 );
705
+ event.initEvent( type, true, true );
706
+ extend( event, properties );
707
+ dom.wrapper.dispatchEvent( event );
708
+
709
+ }
710
+
711
+ /**
712
+ * Wrap all links in 3D goodness.
713
+ */
714
+ function enableRollingLinks() {
715
+
716
+ if( supports3DTransforms && !( 'msPerspective' in document.body.style ) ) {
717
+ var anchors = document.querySelectorAll( SLIDES_SELECTOR + ' a:not(.image)' );
718
+
719
+ for( var i = 0, len = anchors.length; i < len; i++ ) {
720
+ var anchor = anchors[i];
721
+
722
+ if( anchor.textContent && !anchor.querySelector( '*' ) && ( !anchor.className || !anchor.classList.contains( anchor, 'roll' ) ) ) {
723
+ var span = document.createElement('span');
724
+ span.setAttribute('data-title', anchor.text);
725
+ span.innerHTML = anchor.innerHTML;
726
+
727
+ anchor.classList.add( 'roll' );
728
+ anchor.innerHTML = '';
729
+ anchor.appendChild(span);
730
+ }
731
+ }
732
+ }
733
+
734
+ }
735
+
736
+ /**
737
+ * Unwrap all 3D links.
738
+ */
739
+ function disableRollingLinks() {
740
+
741
+ var anchors = document.querySelectorAll( SLIDES_SELECTOR + ' a.roll' );
742
+
743
+ for( var i = 0, len = anchors.length; i < len; i++ ) {
744
+ var anchor = anchors[i];
745
+ var span = anchor.querySelector( 'span' );
746
+
747
+ if( span ) {
748
+ anchor.classList.remove( 'roll' );
749
+ anchor.innerHTML = span.innerHTML;
750
+ }
751
+ }
752
+
753
+ }
754
+
755
+ /**
756
+ * Bind preview frame links.
757
+ */
758
+ function enablePreviewLinks( selector ) {
759
+
760
+ var anchors = toArray( document.querySelectorAll( selector ? selector : 'a' ) );
761
+
762
+ anchors.forEach( function( element ) {
763
+ if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) {
764
+ element.addEventListener( 'click', onPreviewLinkClicked, false );
765
+ }
766
+ } );
767
+
768
+ }
769
+
770
+ /**
771
+ * Unbind preview frame links.
772
+ */
773
+ function disablePreviewLinks() {
774
+
775
+ var anchors = toArray( document.querySelectorAll( 'a' ) );
776
+
777
+ anchors.forEach( function( element ) {
778
+ if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) {
779
+ element.removeEventListener( 'click', onPreviewLinkClicked, false );
780
+ }
781
+ } );
782
+
783
+ }
784
+
785
+ /**
786
+ * Opens a preview window for the target URL.
787
+ */
788
+ function openPreview( url ) {
789
+
790
+ closePreview();
791
+
792
+ dom.preview = document.createElement( 'div' );
793
+ dom.preview.classList.add( 'preview-link-overlay' );
794
+ dom.wrapper.appendChild( dom.preview );
795
+
796
+ dom.preview.innerHTML = [
797
+ '<header>',
798
+ '<a class="close" href="#"><span class="icon"></span></a>',
799
+ '<a class="external" href="'+ url +'" target="_blank"><span class="icon"></span></a>',
800
+ '</header>',
801
+ '<div class="spinner"></div>',
802
+ '<div class="viewport">',
803
+ '<iframe src="'+ url +'"></iframe>',
804
+ '</div>'
805
+ ].join('');
806
+
807
+ dom.preview.querySelector( 'iframe' ).addEventListener( 'load', function( event ) {
808
+ dom.preview.classList.add( 'loaded' );
809
+ }, false );
810
+
811
+ dom.preview.querySelector( '.close' ).addEventListener( 'click', function( event ) {
812
+ closePreview();
813
+ event.preventDefault();
814
+ }, false );
815
+
816
+ dom.preview.querySelector( '.external' ).addEventListener( 'click', function( event ) {
817
+ closePreview();
818
+ }, false );
819
+
820
+ setTimeout( function() {
821
+ dom.preview.classList.add( 'visible' );
822
+ }, 1 );
823
+
824
+ }
825
+
826
+ /**
827
+ * Closes the iframe preview window.
828
+ */
829
+ function closePreview() {
830
+
831
+ if( dom.preview ) {
832
+ dom.preview.setAttribute( 'src', '' );
833
+ dom.preview.parentNode.removeChild( dom.preview );
834
+ dom.preview = null;
835
+ }
836
+
837
+ }
838
+
839
+ /**
840
+ * Return a sorted fragments list, ordered by an increasing
841
+ * "data-fragment-index" attribute.
842
+ *
843
+ * Fragments will be revealed in the order that they are returned by
844
+ * this function, so you can use the index attributes to control the
845
+ * order of fragment appearance.
846
+ *
847
+ * To maintain a sensible default fragment order, fragments are presumed
848
+ * to be passed in document order. This function adds a "fragment-index"
849
+ * attribute to each node if such an attribute is not already present,
850
+ * and sets that attribute to an integer value which is the position of
851
+ * the fragment within the fragments list.
852
+ */
853
+ function sortFragments( fragments ) {
854
+
855
+ var a = toArray( fragments );
856
+
857
+ a.forEach( function( el, idx ) {
858
+ if( !el.hasAttribute( 'data-fragment-index' ) ) {
859
+ el.setAttribute( 'data-fragment-index', idx );
860
+ }
861
+ } );
862
+
863
+ a.sort( function( l, r ) {
864
+ return l.getAttribute( 'data-fragment-index' ) - r.getAttribute( 'data-fragment-index');
865
+ } );
866
+
867
+ return a;
868
+
869
+ }
870
+
871
+ /**
872
+ * Applies JavaScript-controlled layout rules to the
873
+ * presentation.
874
+ */
875
+ function layout() {
876
+
877
+ if( dom.wrapper && !isPrintingPDF() ) {
878
+
879
+ // Available space to scale within
880
+ var availableWidth = dom.wrapper.offsetWidth,
881
+ availableHeight = dom.wrapper.offsetHeight;
882
+
883
+ // Reduce available space by margin
884
+ availableWidth -= ( availableHeight * config.margin );
885
+ availableHeight -= ( availableHeight * config.margin );
886
+
887
+ // Dimensions of the content
888
+ var slideWidth = config.width,
889
+ slideHeight = config.height;
890
+
891
+ // Slide width may be a percentage of available width
892
+ if( typeof slideWidth === 'string' && /%$/.test( slideWidth ) ) {
893
+ slideWidth = parseInt( slideWidth, 10 ) / 100 * availableWidth;
894
+ }
895
+
896
+ // Slide height may be a percentage of available height
897
+ if( typeof slideHeight === 'string' && /%$/.test( slideHeight ) ) {
898
+ slideHeight = parseInt( slideHeight, 10 ) / 100 * availableHeight;
899
+ }
900
+
901
+ dom.slides.style.width = slideWidth + 'px';
902
+ dom.slides.style.height = slideHeight + 'px';
903
+
904
+ // Determine scale of content to fit within available space
905
+ scale = Math.min( availableWidth / slideWidth, availableHeight / slideHeight );
906
+
907
+ // Respect max/min scale settings
908
+ scale = Math.max( scale, config.minScale );
909
+ scale = Math.min( scale, config.maxScale );
910
+
911
+ // Prefer applying scale via zoom since Chrome blurs scaled content
912
+ // with nested transforms
913
+ if( typeof dom.slides.style.zoom !== 'undefined' && !navigator.userAgent.match( /(iphone|ipod|ipad|android)/gi ) ) {
914
+ dom.slides.style.zoom = scale;
915
+ }
916
+ // Apply scale transform as a fallback
917
+ else {
918
+ var transform = 'translate(-50%, -50%) scale('+ scale +') translate(50%, 50%)';
919
+
920
+ dom.slides.style.WebkitTransform = transform;
921
+ dom.slides.style.MozTransform = transform;
922
+ dom.slides.style.msTransform = transform;
923
+ dom.slides.style.OTransform = transform;
924
+ dom.slides.style.transform = transform;
925
+ }
926
+
927
+ // Select all slides, vertical and horizontal
928
+ var slides = toArray( document.querySelectorAll( SLIDES_SELECTOR ) );
929
+
930
+ for( var i = 0, len = slides.length; i < len; i++ ) {
931
+ var slide = slides[ i ];
932
+
933
+ // Don't bother updating invisible slides
934
+ if( slide.style.display === 'none' ) {
935
+ continue;
936
+ }
937
+
938
+ if( config.center ) {
939
+ // Vertical stacks are not centred since their section
940
+ // children will be
941
+ if( slide.classList.contains( 'stack' ) ) {
942
+ slide.style.top = 0;
943
+ }
944
+ else {
945
+ slide.style.top = Math.max( - ( getAbsoluteHeight( slide ) / 2 ) - 20, -slideHeight / 2 ) + 'px';
946
+ }
947
+ }
948
+ else {
949
+ slide.style.top = '';
950
+ }
951
+
952
+ }
953
+
954
+ updateProgress();
955
+
956
+ }
957
+
958
+ }
959
+
960
+ /**
961
+ * Stores the vertical index of a stack so that the same
962
+ * vertical slide can be selected when navigating to and
963
+ * from the stack.
964
+ *
965
+ * @param {HTMLElement} stack The vertical stack element
966
+ * @param {int} v Index to memorize
967
+ */
968
+ function setPreviousVerticalIndex( stack, v ) {
969
+
970
+ if( typeof stack === 'object' && typeof stack.setAttribute === 'function' ) {
971
+ stack.setAttribute( 'data-previous-indexv', v || 0 );
972
+ }
973
+
974
+ }
975
+
976
+ /**
977
+ * Retrieves the vertical index which was stored using
978
+ * #setPreviousVerticalIndex() or 0 if no previous index
979
+ * exists.
980
+ *
981
+ * @param {HTMLElement} stack The vertical stack element
982
+ */
983
+ function getPreviousVerticalIndex( stack ) {
984
+
985
+ if( typeof stack === 'object' && typeof stack.setAttribute === 'function' && stack.classList.contains( 'stack' ) ) {
986
+ // Prefer manually defined start-indexv
987
+ var attributeName = stack.hasAttribute( 'data-start-indexv' ) ? 'data-start-indexv' : 'data-previous-indexv';
988
+
989
+ return parseInt( stack.getAttribute( attributeName ) || 0, 10 );
990
+ }
991
+
992
+ return 0;
993
+
994
+ }
995
+
996
+ /**
997
+ * Displays the overview of slides (quick nav) by
998
+ * scaling down and arranging all slide elements.
999
+ *
1000
+ * Experimental feature, might be dropped if perf
1001
+ * can't be improved.
1002
+ */
1003
+ function activateOverview() {
1004
+
1005
+ // Only proceed if enabled in config
1006
+ if( config.overview ) {
1007
+
1008
+ // Don't auto-slide while in overview mode
1009
+ cancelAutoSlide();
1010
+
1011
+ var wasActive = dom.wrapper.classList.contains( 'overview' );
1012
+
1013
+ dom.wrapper.classList.add( 'overview' );
1014
+ dom.wrapper.classList.remove( 'exit-overview' );
1015
+
1016
+ clearTimeout( activateOverviewTimeout );
1017
+ clearTimeout( deactivateOverviewTimeout );
1018
+
1019
+ // Not the pretties solution, but need to let the overview
1020
+ // class apply first so that slides are measured accurately
1021
+ // before we can position them
1022
+ activateOverviewTimeout = setTimeout( function(){
1023
+
1024
+ var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR );
1025
+
1026
+ for( var i = 0, len1 = horizontalSlides.length; i < len1; i++ ) {
1027
+ var hslide = horizontalSlides[i],
1028
+ hoffset = config.rtl ? -105 : 105,
1029
+ htransform = 'translateZ(-2500px) translate(' + ( ( i - indexh ) * hoffset ) + '%, 0%)';
1030
+
1031
+ hslide.setAttribute( 'data-index-h', i );
1032
+ hslide.style.display = 'block';
1033
+ hslide.style.WebkitTransform = htransform;
1034
+ hslide.style.MozTransform = htransform;
1035
+ hslide.style.msTransform = htransform;
1036
+ hslide.style.OTransform = htransform;
1037
+ hslide.style.transform = htransform;
1038
+
1039
+ if( hslide.classList.contains( 'stack' ) ) {
1040
+
1041
+ var verticalSlides = hslide.querySelectorAll( 'section' );
1042
+
1043
+ for( var j = 0, len2 = verticalSlides.length; j < len2; j++ ) {
1044
+ var verticalIndex = i === indexh ? indexv : getPreviousVerticalIndex( hslide );
1045
+
1046
+ var vslide = verticalSlides[j],
1047
+ vtransform = 'translate(0%, ' + ( ( j - verticalIndex ) * 105 ) + '%)';
1048
+
1049
+ vslide.setAttribute( 'data-index-h', i );
1050
+ vslide.setAttribute( 'data-index-v', j );
1051
+ vslide.style.display = 'block';
1052
+ vslide.style.WebkitTransform = vtransform;
1053
+ vslide.style.MozTransform = vtransform;
1054
+ vslide.style.msTransform = vtransform;
1055
+ vslide.style.OTransform = vtransform;
1056
+ vslide.style.transform = vtransform;
1057
+
1058
+ // Navigate to this slide on click
1059
+ vslide.addEventListener( 'click', onOverviewSlideClicked, true );
1060
+ }
1061
+
1062
+ }
1063
+ else {
1064
+
1065
+ // Navigate to this slide on click
1066
+ hslide.addEventListener( 'click', onOverviewSlideClicked, true );
1067
+
1068
+ }
1069
+ }
1070
+
1071
+ layout();
1072
+
1073
+ if( !wasActive ) {
1074
+ // Notify observers of the overview showing
1075
+ dispatchEvent( 'overviewshown', {
1076
+ 'indexh': indexh,
1077
+ 'indexv': indexv,
1078
+ 'currentSlide': currentSlide
1079
+ } );
1080
+ }
1081
+
1082
+ }, 10 );
1083
+
1084
+ }
1085
+
1086
+ }
1087
+
1088
+ /**
1089
+ * Exits the slide overview and enters the currently
1090
+ * active slide.
1091
+ */
1092
+ function deactivateOverview() {
1093
+
1094
+ // Only proceed if enabled in config
1095
+ if( config.overview ) {
1096
+
1097
+ clearTimeout( activateOverviewTimeout );
1098
+ clearTimeout( deactivateOverviewTimeout );
1099
+
1100
+ dom.wrapper.classList.remove( 'overview' );
1101
+
1102
+ // Temporarily add a class so that transitions can do different things
1103
+ // depending on whether they are exiting/entering overview, or just
1104
+ // moving from slide to slide
1105
+ dom.wrapper.classList.add( 'exit-overview' );
1106
+
1107
+ deactivateOverviewTimeout = setTimeout( function () {
1108
+ dom.wrapper.classList.remove( 'exit-overview' );
1109
+ }, 10);
1110
+
1111
+ // Select all slides
1112
+ var slides = toArray( document.querySelectorAll( SLIDES_SELECTOR ) );
1113
+
1114
+ for( var i = 0, len = slides.length; i < len; i++ ) {
1115
+ var element = slides[i];
1116
+
1117
+ element.style.display = '';
1118
+
1119
+ // Resets all transforms to use the external styles
1120
+ element.style.WebkitTransform = '';
1121
+ element.style.MozTransform = '';
1122
+ element.style.msTransform = '';
1123
+ element.style.OTransform = '';
1124
+ element.style.transform = '';
1125
+
1126
+ element.removeEventListener( 'click', onOverviewSlideClicked, true );
1127
+ }
1128
+
1129
+ slide( indexh, indexv );
1130
+
1131
+ cueAutoSlide();
1132
+
1133
+ // Notify observers of the overview hiding
1134
+ dispatchEvent( 'overviewhidden', {
1135
+ 'indexh': indexh,
1136
+ 'indexv': indexv,
1137
+ 'currentSlide': currentSlide
1138
+ } );
1139
+
1140
+ }
1141
+ }
1142
+
1143
+ /**
1144
+ * Toggles the slide overview mode on and off.
1145
+ *
1146
+ * @param {Boolean} override Optional flag which overrides the
1147
+ * toggle logic and forcibly sets the desired state. True means
1148
+ * overview is open, false means it's closed.
1149
+ */
1150
+ function toggleOverview( override ) {
1151
+
1152
+ if( typeof override === 'boolean' ) {
1153
+ override ? activateOverview() : deactivateOverview();
1154
+ }
1155
+ else {
1156
+ isOverview() ? deactivateOverview() : activateOverview();
1157
+ }
1158
+
1159
+ }
1160
+
1161
+ /**
1162
+ * Checks if the overview is currently active.
1163
+ *
1164
+ * @return {Boolean} true if the overview is active,
1165
+ * false otherwise
1166
+ */
1167
+ function isOverview() {
1168
+
1169
+ return dom.wrapper.classList.contains( 'overview' );
1170
+
1171
+ }
1172
+
1173
+ /**
1174
+ * Checks if the current or specified slide is vertical
1175
+ * (nested within another slide).
1176
+ *
1177
+ * @param {HTMLElement} slide [optional] The slide to check
1178
+ * orientation of
1179
+ */
1180
+ function isVerticalSlide( slide ) {
1181
+
1182
+ // Prefer slide argument, otherwise use current slide
1183
+ slide = slide ? slide : currentSlide;
1184
+
1185
+ return slide && !!slide.parentNode.nodeName.match( /section/i );
1186
+
1187
+ }
1188
+
1189
+ /**
1190
+ * Handling the fullscreen functionality via the fullscreen API
1191
+ *
1192
+ * @see http://fullscreen.spec.whatwg.org/
1193
+ * @see https://developer.mozilla.org/en-US/docs/DOM/Using_fullscreen_mode
1194
+ */
1195
+ function enterFullscreen() {
1196
+
1197
+ var element = document.body;
1198
+
1199
+ // Check which implementation is available
1200
+ var requestMethod = element.requestFullScreen ||
1201
+ element.webkitRequestFullscreen ||
1202
+ element.webkitRequestFullScreen ||
1203
+ element.mozRequestFullScreen ||
1204
+ element.msRequestFullScreen;
1205
+
1206
+ if( requestMethod ) {
1207
+ requestMethod.apply( element );
1208
+ }
1209
+
1210
+ }
1211
+
1212
+ /**
1213
+ * Enters the paused mode which fades everything on screen to
1214
+ * black.
1215
+ */
1216
+ function pause() {
1217
+
1218
+ var wasPaused = dom.wrapper.classList.contains( 'paused' );
1219
+
1220
+ cancelAutoSlide();
1221
+ dom.wrapper.classList.add( 'paused' );
1222
+
1223
+ if( wasPaused === false ) {
1224
+ dispatchEvent( 'paused' );
1225
+ }
1226
+
1227
+ }
1228
+
1229
+ /**
1230
+ * Exits from the paused mode.
1231
+ */
1232
+ function resume() {
1233
+
1234
+ var wasPaused = dom.wrapper.classList.contains( 'paused' );
1235
+ dom.wrapper.classList.remove( 'paused' );
1236
+
1237
+ cueAutoSlide();
1238
+
1239
+ if( wasPaused ) {
1240
+ dispatchEvent( 'resumed' );
1241
+ }
1242
+
1243
+ }
1244
+
1245
+ /**
1246
+ * Toggles the paused mode on and off.
1247
+ */
1248
+ function togglePause() {
1249
+
1250
+ if( isPaused() ) {
1251
+ resume();
1252
+ }
1253
+ else {
1254
+ pause();
1255
+ }
1256
+
1257
+ }
1258
+
1259
+ /**
1260
+ * Checks if we are currently in the paused mode.
1261
+ */
1262
+ function isPaused() {
1263
+
1264
+ return dom.wrapper.classList.contains( 'paused' );
1265
+
1266
+ }
1267
+
1268
+ /**
1269
+ * Steps from the current point in the presentation to the
1270
+ * slide which matches the specified horizontal and vertical
1271
+ * indices.
1272
+ *
1273
+ * @param {int} h Horizontal index of the target slide
1274
+ * @param {int} v Vertical index of the target slide
1275
+ * @param {int} f Optional index of a fragment within the
1276
+ * target slide to activate
1277
+ * @param {int} o Optional origin for use in multimaster environments
1278
+ */
1279
+ function slide( h, v, f, o ) {
1280
+
1281
+ // Remember where we were at before
1282
+ previousSlide = currentSlide;
1283
+
1284
+ // Query all horizontal slides in the deck
1285
+ var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR );
1286
+
1287
+ // If no vertical index is specified and the upcoming slide is a
1288
+ // stack, resume at its previous vertical index
1289
+ if( v === undefined ) {
1290
+ v = getPreviousVerticalIndex( horizontalSlides[ h ] );
1291
+ }
1292
+
1293
+ // If we were on a vertical stack, remember what vertical index
1294
+ // it was on so we can resume at the same position when returning
1295
+ if( previousSlide && previousSlide.parentNode && previousSlide.parentNode.classList.contains( 'stack' ) ) {
1296
+ setPreviousVerticalIndex( previousSlide.parentNode, indexv );
1297
+ }
1298
+
1299
+ // Remember the state before this slide
1300
+ var stateBefore = state.concat();
1301
+
1302
+ // Reset the state array
1303
+ state.length = 0;
1304
+
1305
+ var indexhBefore = indexh,
1306
+ indexvBefore = indexv;
1307
+
1308
+ // Activate and transition to the new slide
1309
+ indexh = updateSlides( HORIZONTAL_SLIDES_SELECTOR, h === undefined ? indexh : h );
1310
+ indexv = updateSlides( VERTICAL_SLIDES_SELECTOR, v === undefined ? indexv : v );
1311
+
1312
+ layout();
1313
+
1314
+ // Apply the new state
1315
+ stateLoop: for( var i = 0, len = state.length; i < len; i++ ) {
1316
+ // Check if this state existed on the previous slide. If it
1317
+ // did, we will avoid adding it repeatedly
1318
+ for( var j = 0; j < stateBefore.length; j++ ) {
1319
+ if( stateBefore[j] === state[i] ) {
1320
+ stateBefore.splice( j, 1 );
1321
+ continue stateLoop;
1322
+ }
1323
+ }
1324
+
1325
+ document.documentElement.classList.add( state[i] );
1326
+
1327
+ // Dispatch custom event matching the state's name
1328
+ dispatchEvent( state[i] );
1329
+ }
1330
+
1331
+ // Clean up the remains of the previous state
1332
+ while( stateBefore.length ) {
1333
+ document.documentElement.classList.remove( stateBefore.pop() );
1334
+ }
1335
+
1336
+ // If the overview is active, re-activate it to update positions
1337
+ if( isOverview() ) {
1338
+ activateOverview();
1339
+ }
1340
+
1341
+ // Update the URL hash after a delay since updating it mid-transition
1342
+ // is likely to cause visual lag
1343
+ writeURL( 1500 );
1344
+
1345
+ // Find the current horizontal slide and any possible vertical slides
1346
+ // within it
1347
+ var currentHorizontalSlide = horizontalSlides[ indexh ],
1348
+ currentVerticalSlides = currentHorizontalSlide.querySelectorAll( 'section' );
1349
+
1350
+ // Store references to the previous and current slides
1351
+ currentSlide = currentVerticalSlides[ indexv ] || currentHorizontalSlide;
1352
+
1353
+
1354
+ // Show fragment, if specified
1355
+ if( typeof f !== 'undefined' ) {
1356
+ var fragments = sortFragments( currentSlide.querySelectorAll( '.fragment' ) );
1357
+
1358
+ toArray( fragments ).forEach( function( fragment, indexf ) {
1359
+ if( indexf < f ) {
1360
+ fragment.classList.add( 'visible' );
1361
+ }
1362
+ else {
1363
+ fragment.classList.remove( 'visible' );
1364
+ }
1365
+ } );
1366
+ }
1367
+
1368
+ // Dispatch an event if the slide changed
1369
+ var slideChanged = ( indexh !== indexhBefore || indexv !== indexvBefore );
1370
+ if( slideChanged ) {
1371
+ dispatchEvent( 'slidechanged', {
1372
+ 'indexh': indexh,
1373
+ 'indexv': indexv,
1374
+ 'previousSlide': previousSlide,
1375
+ 'currentSlide': currentSlide,
1376
+ 'origin': o
1377
+ } );
1378
+ }
1379
+ else {
1380
+ // Ensure that the previous slide is never the same as the current
1381
+ previousSlide = null;
1382
+ }
1383
+
1384
+ // Solves an edge case where the previous slide maintains the
1385
+ // 'present' class when navigating between adjacent vertical
1386
+ // stacks
1387
+ if( previousSlide ) {
1388
+ previousSlide.classList.remove( 'present' );
1389
+
1390
+ // Reset all slides upon navigate to home
1391
+ // Issue: #285
1392
+ if ( document.querySelector( HOME_SLIDE_SELECTOR ).classList.contains( 'present' ) ) {
1393
+ // Launch async task
1394
+ setTimeout( function () {
1395
+ var slides = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.stack') ), i;
1396
+ for( i in slides ) {
1397
+ if( slides[i] ) {
1398
+ // Reset stack
1399
+ setPreviousVerticalIndex( slides[i], 0 );
1400
+ }
1401
+ }
1402
+ }, 0 );
1403
+ }
1404
+ }
1405
+
1406
+ // Handle embedded content
1407
+ if( slideChanged ) {
1408
+ stopEmbeddedContent( previousSlide );
1409
+ startEmbeddedContent( currentSlide );
1410
+ }
1411
+
1412
+ updateControls();
1413
+ updateProgress();
1414
+ updateBackground();
1415
+
1416
+ }
1417
+
1418
+ /**
1419
+ * Syncs the presentation with the current DOM. Useful
1420
+ * when new slides or control elements are added or when
1421
+ * the configuration has changed.
1422
+ */
1423
+ function sync() {
1424
+
1425
+ // Subscribe to input
1426
+ removeEventListeners();
1427
+ addEventListeners();
1428
+
1429
+ // Force a layout to make sure the current config is accounted for
1430
+ layout();
1431
+
1432
+ // Reflect the current autoSlide value
1433
+ autoSlide = config.autoSlide;
1434
+
1435
+ // Start auto-sliding if it's enabled
1436
+ cueAutoSlide();
1437
+
1438
+ // Re-create the slide backgrounds
1439
+ createBackgrounds();
1440
+
1441
+ updateControls();
1442
+ updateProgress();
1443
+ updateBackground();
1444
+
1445
+ }
1446
+
1447
+ /**
1448
+ * Updates one dimension of slides by showing the slide
1449
+ * with the specified index.
1450
+ *
1451
+ * @param {String} selector A CSS selector that will fetch
1452
+ * the group of slides we are working with
1453
+ * @param {Number} index The index of the slide that should be
1454
+ * shown
1455
+ *
1456
+ * @return {Number} The index of the slide that is now shown,
1457
+ * might differ from the passed in index if it was out of
1458
+ * bounds.
1459
+ */
1460
+ function updateSlides( selector, index ) {
1461
+
1462
+ // Select all slides and convert the NodeList result to
1463
+ // an array
1464
+ var slides = toArray( document.querySelectorAll( selector ) ),
1465
+ slidesLength = slides.length;
1466
+
1467
+ if( slidesLength ) {
1468
+
1469
+ // Should the index loop?
1470
+ if( config.loop ) {
1471
+ index %= slidesLength;
1472
+
1473
+ if( index < 0 ) {
1474
+ index = slidesLength + index;
1475
+ }
1476
+ }
1477
+
1478
+ // Enforce max and minimum index bounds
1479
+ index = Math.max( Math.min( index, slidesLength - 1 ), 0 );
1480
+
1481
+ for( var i = 0; i < slidesLength; i++ ) {
1482
+ var element = slides[i];
1483
+
1484
+ // Optimization; hide all slides that are three or more steps
1485
+ // away from the present slide
1486
+ if( isOverview() === false ) {
1487
+ // The distance loops so that it measures 1 between the first
1488
+ // and last slides
1489
+ var distance = Math.abs( ( index - i ) % ( slidesLength - 3 ) ) || 0;
1490
+
1491
+ element.style.display = distance > 3 ? 'none' : 'block';
1492
+ }
1493
+
1494
+ var reverse = config.rtl && !isVerticalSlide( element );
1495
+
1496
+ element.classList.remove( 'past' );
1497
+ element.classList.remove( 'present' );
1498
+ element.classList.remove( 'future' );
1499
+
1500
+ // http://www.w3.org/html/wg/drafts/html/master/editing.html#the-hidden-attribute
1501
+ element.setAttribute( 'hidden', '' );
1502
+
1503
+ if( i < index ) {
1504
+ // Any element previous to index is given the 'past' class
1505
+ element.classList.add( reverse ? 'future' : 'past' );
1506
+ }
1507
+ else if( i > index ) {
1508
+ // Any element subsequent to index is given the 'future' class
1509
+ element.classList.add( reverse ? 'past' : 'future' );
1510
+ }
1511
+
1512
+ // If this element contains vertical slides
1513
+ if( element.querySelector( 'section' ) ) {
1514
+ element.classList.add( 'stack' );
1515
+ }
1516
+ }
1517
+
1518
+ // Mark the current slide as present
1519
+ slides[index].classList.add( 'present' );
1520
+ slides[index].removeAttribute( 'hidden' );
1521
+
1522
+ // If this slide has a state associated with it, add it
1523
+ // onto the current state of the deck
1524
+ var slideState = slides[index].getAttribute( 'data-state' );
1525
+ if( slideState ) {
1526
+ state = state.concat( slideState.split( ' ' ) );
1527
+ }
1528
+
1529
+ // If this slide has a data-autoslide attribtue associated use this as
1530
+ // autoSlide value otherwise use the global configured time
1531
+ var slideAutoSlide = slides[index].getAttribute( 'data-autoslide' );
1532
+ if( slideAutoSlide ) {
1533
+ autoSlide = parseInt( slideAutoSlide, 10 );
1534
+ }
1535
+ else {
1536
+ autoSlide = config.autoSlide;
1537
+ }
1538
+
1539
+ }
1540
+ else {
1541
+ // Since there are no slides we can't be anywhere beyond the
1542
+ // zeroth index
1543
+ index = 0;
1544
+ }
1545
+
1546
+ return index;
1547
+
1548
+ }
1549
+
1550
+ /**
1551
+ * Updates the progress bar to reflect the current slide.
1552
+ */
1553
+ function updateProgress() {
1554
+
1555
+ // Update progress if enabled
1556
+ if( config.progress && dom.progress ) {
1557
+
1558
+ var horizontalSlides = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
1559
+
1560
+ // The number of past and total slides
1561
+ var totalCount = document.querySelectorAll( SLIDES_SELECTOR + ':not(.stack)' ).length;
1562
+ var pastCount = 0;
1563
+
1564
+ // Step through all slides and count the past ones
1565
+ mainLoop: for( var i = 0; i < horizontalSlides.length; i++ ) {
1566
+
1567
+ var horizontalSlide = horizontalSlides[i];
1568
+ var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) );
1569
+
1570
+ for( var j = 0; j < verticalSlides.length; j++ ) {
1571
+
1572
+ // Stop as soon as we arrive at the present
1573
+ if( verticalSlides[j].classList.contains( 'present' ) ) {
1574
+ break mainLoop;
1575
+ }
1576
+
1577
+ pastCount++;
1578
+
1579
+ }
1580
+
1581
+ // Stop as soon as we arrive at the present
1582
+ if( horizontalSlide.classList.contains( 'present' ) ) {
1583
+ break;
1584
+ }
1585
+
1586
+ // Don't count the wrapping section for vertical slides
1587
+ if( horizontalSlide.classList.contains( 'stack' ) === false ) {
1588
+ pastCount++;
1589
+ }
1590
+
1591
+ }
1592
+
1593
+ dom.progressbar.style.width = ( pastCount / ( totalCount - 1 ) ) * window.innerWidth + 'px';
1594
+
1595
+ }
1596
+
1597
+ }
1598
+
1599
+ /**
1600
+ * Updates the state of all control/navigation arrows.
1601
+ */
1602
+ function updateControls() {
1603
+
1604
+ if ( config.controls && dom.controls ) {
1605
+
1606
+ var routes = availableRoutes();
1607
+ var fragments = availableFragments();
1608
+
1609
+ // Remove the 'enabled' class from all directions
1610
+ dom.controlsLeft.concat( dom.controlsRight )
1611
+ .concat( dom.controlsUp )
1612
+ .concat( dom.controlsDown )
1613
+ .concat( dom.controlsPrev )
1614
+ .concat( dom.controlsNext ).forEach( function( node ) {
1615
+ node.classList.remove( 'enabled' );
1616
+ node.classList.remove( 'fragmented' );
1617
+ } );
1618
+
1619
+ // Add the 'enabled' class to the available routes
1620
+ if( routes.left ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'enabled' ); } );
1621
+ if( routes.right ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'enabled' ); } );
1622
+ if( routes.up ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'enabled' ); } );
1623
+ if( routes.down ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'enabled' ); } );
1624
+
1625
+ // Prev/next buttons
1626
+ if( routes.left || routes.up ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'enabled' ); } );
1627
+ if( routes.right || routes.down ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'enabled' ); } );
1628
+
1629
+ // Highlight fragment directions
1630
+ if( currentSlide ) {
1631
+
1632
+ // Always apply fragment decorator to prev/next buttons
1633
+ if( fragments.prev ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
1634
+ if( fragments.next ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
1635
+
1636
+ // Apply fragment decorators to directional buttons based on
1637
+ // what slide axis they are in
1638
+ if( isVerticalSlide( currentSlide ) ) {
1639
+ if( fragments.prev ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
1640
+ if( fragments.next ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
1641
+ }
1642
+ else {
1643
+ if( fragments.prev ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
1644
+ if( fragments.next ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
1645
+ }
1646
+ }
1647
+
1648
+ }
1649
+
1650
+ }
1651
+
1652
+ /**
1653
+ * Updates the background elements to reflect the current
1654
+ * slide.
1655
+ */
1656
+ function updateBackground() {
1657
+
1658
+ // Update the classes of all backgrounds to match the
1659
+ // states of their slides (past/present/future)
1660
+ toArray( dom.background.childNodes ).forEach( function( backgroundh, h ) {
1661
+
1662
+ // Reverse past/future classes when in RTL mode
1663
+ var horizontalPast = config.rtl ? 'future' : 'past',
1664
+ horizontalFuture = config.rtl ? 'past' : 'future';
1665
+
1666
+ backgroundh.className = 'slide-background ' + ( h < indexh ? horizontalPast : h > indexh ? horizontalFuture : 'present' );
1667
+
1668
+ toArray( backgroundh.childNodes ).forEach( function( backgroundv, v ) {
1669
+
1670
+ backgroundv.className = 'slide-background ' + ( v < indexv ? 'past' : v > indexv ? 'future' : 'present' );
1671
+
1672
+ } );
1673
+
1674
+ } );
1675
+
1676
+ // Allow the first background to apply without transition
1677
+ setTimeout( function() {
1678
+ dom.background.classList.remove( 'no-transition' );
1679
+ }, 1 );
1680
+
1681
+ }
1682
+
1683
+ /**
1684
+ * Determine what available routes there are for navigation.
1685
+ *
1686
+ * @return {Object} containing four booleans: left/right/up/down
1687
+ */
1688
+ function availableRoutes() {
1689
+
1690
+ var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ),
1691
+ verticalSlides = document.querySelectorAll( VERTICAL_SLIDES_SELECTOR );
1692
+
1693
+ var routes = {
1694
+ left: indexh > 0 || config.loop,
1695
+ right: indexh < horizontalSlides.length - 1 || config.loop,
1696
+ up: indexv > 0,
1697
+ down: indexv < verticalSlides.length - 1
1698
+ };
1699
+
1700
+ // reverse horizontal controls for rtl
1701
+ if( config.rtl ) {
1702
+ var left = routes.left;
1703
+ routes.left = routes.right;
1704
+ routes.right = left;
1705
+ }
1706
+
1707
+ return routes;
1708
+
1709
+ }
1710
+
1711
+ /**
1712
+ * Returns an object describing the available fragment
1713
+ * directions.
1714
+ *
1715
+ * @return {Object} two boolean properties: prev/next
1716
+ */
1717
+ function availableFragments() {
1718
+
1719
+ if( currentSlide && config.fragments ) {
1720
+ var fragments = currentSlide.querySelectorAll( '.fragment' );
1721
+ var hiddenFragments = currentSlide.querySelectorAll( '.fragment:not(.visible)' );
1722
+
1723
+ return {
1724
+ prev: fragments.length - hiddenFragments.length > 0,
1725
+ next: !!hiddenFragments.length
1726
+ };
1727
+ }
1728
+ else {
1729
+ return { prev: false, next: false };
1730
+ }
1731
+
1732
+ }
1733
+
1734
+ /**
1735
+ * Start playback of any embedded content inside of
1736
+ * the targeted slide.
1737
+ */
1738
+ function startEmbeddedContent( slide ) {
1739
+
1740
+ if( slide ) {
1741
+ // HTML5 media elements
1742
+ toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
1743
+ if( el.hasAttribute( 'data-autoplay' ) ) {
1744
+ el.play();
1745
+ }
1746
+ } );
1747
+
1748
+ // YouTube embeds
1749
+ toArray( slide.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) {
1750
+ if( el.hasAttribute( 'data-autoplay' ) ) {
1751
+ el.contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}', '*');
1752
+ }
1753
+ });
1754
+ }
1755
+
1756
+ }
1757
+
1758
+ /**
1759
+ * Stop playback of any embedded content inside of
1760
+ * the targeted slide.
1761
+ */
1762
+ function stopEmbeddedContent( slide ) {
1763
+
1764
+ if( slide ) {
1765
+ // HTML5 media elements
1766
+ toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
1767
+ if( !el.hasAttribute( 'data-ignore' ) ) {
1768
+ el.pause();
1769
+ }
1770
+ } );
1771
+
1772
+ // YouTube embeds
1773
+ toArray( slide.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) {
1774
+ if( !el.hasAttribute( 'data-ignore' ) && typeof el.contentWindow.postMessage === 'function' ) {
1775
+ el.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*');
1776
+ }
1777
+ });
1778
+ }
1779
+
1780
+ }
1781
+
1782
+ /**
1783
+ * Reads the current URL (hash) and navigates accordingly.
1784
+ */
1785
+ function readURL() {
1786
+
1787
+ var hash = window.location.hash;
1788
+
1789
+ // Attempt to parse the hash as either an index or name
1790
+ var bits = hash.slice( 2 ).split( '/' ),
1791
+ name = hash.replace( /#|\//gi, '' );
1792
+
1793
+ // If the first bit is invalid and there is a name we can
1794
+ // assume that this is a named link
1795
+ if( isNaN( parseInt( bits[0], 10 ) ) && name.length ) {
1796
+ // Find the slide with the specified name
1797
+ var element = document.querySelector( '#' + name );
1798
+
1799
+ if( element ) {
1800
+ // Find the position of the named slide and navigate to it
1801
+ var indices = Reveal.getIndices( element );
1802
+ slide( indices.h, indices.v );
1803
+ }
1804
+ // If the slide doesn't exist, navigate to the current slide
1805
+ else {
1806
+ slide( indexh, indexv );
1807
+ }
1808
+ }
1809
+ else {
1810
+ // Read the index components of the hash
1811
+ var h = parseInt( bits[0], 10 ) || 0,
1812
+ v = parseInt( bits[1], 10 ) || 0;
1813
+
1814
+ slide( h, v );
1815
+ }
1816
+
1817
+ }
1818
+
1819
+ /**
1820
+ * Updates the page URL (hash) to reflect the current
1821
+ * state.
1822
+ *
1823
+ * @param {Number} delay The time in ms to wait before
1824
+ * writing the hash
1825
+ */
1826
+ function writeURL( delay ) {
1827
+
1828
+ if( config.history ) {
1829
+
1830
+ // Make sure there's never more than one timeout running
1831
+ clearTimeout( writeURLTimeout );
1832
+
1833
+ // If a delay is specified, timeout this call
1834
+ if( typeof delay === 'number' ) {
1835
+ writeURLTimeout = setTimeout( writeURL, delay );
1836
+ }
1837
+ else {
1838
+ var url = '/';
1839
+
1840
+ // If the current slide has an ID, use that as a named link
1841
+ if( currentSlide && typeof currentSlide.getAttribute( 'id' ) === 'string' ) {
1842
+ url = '/' + currentSlide.getAttribute( 'id' );
1843
+ }
1844
+ // Otherwise use the /h/v index
1845
+ else {
1846
+ if( indexh > 0 || indexv > 0 ) url += indexh;
1847
+ if( indexv > 0 ) url += '/' + indexv;
1848
+ }
1849
+
1850
+ window.location.hash = url;
1851
+ }
1852
+ }
1853
+
1854
+ }
1855
+
1856
+ /**
1857
+ * Retrieves the h/v location of the current, or specified,
1858
+ * slide.
1859
+ *
1860
+ * @param {HTMLElement} slide If specified, the returned
1861
+ * index will be for this slide rather than the currently
1862
+ * active one
1863
+ *
1864
+ * @return {Object} { h: <int>, v: <int>, f: <int> }
1865
+ */
1866
+ function getIndices( slide ) {
1867
+
1868
+ // By default, return the current indices
1869
+ var h = indexh,
1870
+ v = indexv,
1871
+ f;
1872
+
1873
+ // If a slide is specified, return the indices of that slide
1874
+ if( slide ) {
1875
+ var isVertical = isVerticalSlide( slide );
1876
+ var slideh = isVertical ? slide.parentNode : slide;
1877
+
1878
+ // Select all horizontal slides
1879
+ var horizontalSlides = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
1880
+
1881
+ // Now that we know which the horizontal slide is, get its index
1882
+ h = Math.max( horizontalSlides.indexOf( slideh ), 0 );
1883
+
1884
+ // If this is a vertical slide, grab the vertical index
1885
+ if( isVertical ) {
1886
+ v = Math.max( toArray( slide.parentNode.querySelectorAll( 'section' ) ).indexOf( slide ), 0 );
1887
+ }
1888
+ }
1889
+
1890
+ if( !slide && currentSlide ) {
1891
+ var visibleFragments = currentSlide.querySelectorAll( '.fragment.visible' );
1892
+ if( visibleFragments.length ) {
1893
+ f = visibleFragments.length;
1894
+ }
1895
+ }
1896
+
1897
+ return { h: h, v: v, f: f };
1898
+
1899
+ }
1900
+
1901
+ /**
1902
+ * Navigate to the next slide fragment.
1903
+ *
1904
+ * @return {Boolean} true if there was a next fragment,
1905
+ * false otherwise
1906
+ */
1907
+ function nextFragment() {
1908
+
1909
+ if( currentSlide && config.fragments ) {
1910
+ var fragments = sortFragments( currentSlide.querySelectorAll( '.fragment:not(.visible)' ) );
1911
+
1912
+ if( fragments.length ) {
1913
+ // Find the index of the next fragment
1914
+ var index = fragments[0].getAttribute( 'data-fragment-index' );
1915
+
1916
+ // Find all fragments with the same index
1917
+ fragments = currentSlide.querySelectorAll( '.fragment[data-fragment-index="'+ index +'"]' );
1918
+
1919
+ toArray( fragments ).forEach( function( element ) {
1920
+ element.classList.add( 'visible' );
1921
+ } );
1922
+
1923
+ // Notify subscribers of the change
1924
+ dispatchEvent( 'fragmentshown', { fragment: fragments[0], fragments: fragments } );
1925
+
1926
+ updateControls();
1927
+ return true;
1928
+ }
1929
+ }
1930
+
1931
+ return false;
1932
+
1933
+ }
1934
+
1935
+ /**
1936
+ * Navigate to the previous slide fragment.
1937
+ *
1938
+ * @return {Boolean} true if there was a previous fragment,
1939
+ * false otherwise
1940
+ */
1941
+ function previousFragment() {
1942
+
1943
+ if( currentSlide && config.fragments ) {
1944
+ var fragments = sortFragments( currentSlide.querySelectorAll( '.fragment.visible' ) );
1945
+
1946
+ if( fragments.length ) {
1947
+ // Find the index of the previous fragment
1948
+ var index = fragments[ fragments.length - 1 ].getAttribute( 'data-fragment-index' );
1949
+
1950
+ // Find all fragments with the same index
1951
+ fragments = currentSlide.querySelectorAll( '.fragment[data-fragment-index="'+ index +'"]' );
1952
+
1953
+ toArray( fragments ).forEach( function( f ) {
1954
+ f.classList.remove( 'visible' );
1955
+ } );
1956
+
1957
+ // Notify subscribers of the change
1958
+ dispatchEvent( 'fragmenthidden', { fragment: fragments[0], fragments: fragments } );
1959
+
1960
+ updateControls();
1961
+ return true;
1962
+ }
1963
+ }
1964
+
1965
+ return false;
1966
+
1967
+ }
1968
+
1969
+ /**
1970
+ * Cues a new automated slide if enabled in the config.
1971
+ */
1972
+ function cueAutoSlide() {
1973
+
1974
+ clearTimeout( autoSlideTimeout );
1975
+
1976
+ // Cue the next auto-slide if enabled
1977
+ if( autoSlide && !isPaused() && !isOverview() ) {
1978
+ autoSlideTimeout = setTimeout( navigateNext, autoSlide );
1979
+ }
1980
+
1981
+ }
1982
+
1983
+ /**
1984
+ * Cancels any ongoing request to auto-slide.
1985
+ */
1986
+ function cancelAutoSlide() {
1987
+
1988
+ clearTimeout( autoSlideTimeout );
1989
+
1990
+ }
1991
+
1992
+ function navigateLeft() {
1993
+
1994
+ // Reverse for RTL
1995
+ if( config.rtl ) {
1996
+ if( ( isOverview() || nextFragment() === false ) && availableRoutes().left ) {
1997
+ slide( indexh + 1 );
1998
+ }
1999
+ }
2000
+ // Normal navigation
2001
+ else if( ( isOverview() || previousFragment() === false ) && availableRoutes().left ) {
2002
+ slide( indexh - 1 );
2003
+ }
2004
+
2005
+ }
2006
+
2007
+ function navigateRight() {
2008
+
2009
+ // Reverse for RTL
2010
+ if( config.rtl ) {
2011
+ if( ( isOverview() || previousFragment() === false ) && availableRoutes().right ) {
2012
+ slide( indexh - 1 );
2013
+ }
2014
+ }
2015
+ // Normal navigation
2016
+ else if( ( isOverview() || nextFragment() === false ) && availableRoutes().right ) {
2017
+ slide( indexh + 1 );
2018
+ }
2019
+
2020
+ }
2021
+
2022
+ function navigateUp() {
2023
+
2024
+ // Prioritize hiding fragments
2025
+ if( ( isOverview() || previousFragment() === false ) && availableRoutes().up ) {
2026
+ slide( indexh, indexv - 1 );
2027
+ }
2028
+
2029
+ }
2030
+
2031
+ function navigateDown() {
2032
+
2033
+ // Prioritize revealing fragments
2034
+ if( ( isOverview() || nextFragment() === false ) && availableRoutes().down ) {
2035
+ slide( indexh, indexv + 1 );
2036
+ }
2037
+
2038
+ }
2039
+
2040
+ /**
2041
+ * Navigates backwards, prioritized in the following order:
2042
+ * 1) Previous fragment
2043
+ * 2) Previous vertical slide
2044
+ * 3) Previous horizontal slide
2045
+ */
2046
+ function navigatePrev() {
2047
+
2048
+ // Prioritize revealing fragments
2049
+ if( previousFragment() === false ) {
2050
+ if( availableRoutes().up ) {
2051
+ navigateUp();
2052
+ }
2053
+ else {
2054
+ // Fetch the previous horizontal slide, if there is one
2055
+ var previousSlide = document.querySelector( HORIZONTAL_SLIDES_SELECTOR + '.past:nth-child(' + indexh + ')' );
2056
+
2057
+ if( previousSlide ) {
2058
+ var v = ( previousSlide.querySelectorAll( 'section' ).length - 1 ) || undefined;
2059
+ var h = indexh - 1;
2060
+ slide( h, v );
2061
+ }
2062
+ }
2063
+ }
2064
+
2065
+ }
2066
+
2067
+ /**
2068
+ * Same as #navigatePrev() but navigates forwards.
2069
+ */
2070
+ function navigateNext() {
2071
+
2072
+ // Prioritize revealing fragments
2073
+ if( nextFragment() === false ) {
2074
+ availableRoutes().down ? navigateDown() : navigateRight();
2075
+ }
2076
+
2077
+ // If auto-sliding is enabled we need to cue up
2078
+ // another timeout
2079
+ cueAutoSlide();
2080
+
2081
+ }
2082
+
2083
+
2084
+ // --------------------------------------------------------------------//
2085
+ // ----------------------------- EVENTS -------------------------------//
2086
+ // --------------------------------------------------------------------//
2087
+
2088
+
2089
+ /**
2090
+ * Handler for the document level 'keydown' event.
2091
+ *
2092
+ * @param {Object} event
2093
+ */
2094
+ function onDocumentKeyDown( event ) {
2095
+
2096
+ // Check if there's a focused element that could be using
2097
+ // the keyboard
2098
+ var activeElement = document.activeElement;
2099
+ var hasFocus = !!( document.activeElement && ( document.activeElement.type || document.activeElement.href || document.activeElement.contentEditable !== 'inherit' ) );
2100
+
2101
+ // Disregard the event if there's a focused element or a
2102
+ // keyboard modifier key is present
2103
+ if( hasFocus || (event.shiftKey && event.keyCode !== 32) || event.altKey || event.ctrlKey || event.metaKey ) return;
2104
+
2105
+ // While paused only allow "unpausing" keyboard events (b and .)
2106
+ if( isPaused() && [66,190,191].indexOf( event.keyCode ) === -1 ) {
2107
+ return false;
2108
+ }
2109
+
2110
+ var triggered = false;
2111
+
2112
+ // 1. User defined key bindings
2113
+ if( typeof config.keyboard === 'object' ) {
2114
+
2115
+ for( var key in config.keyboard ) {
2116
+
2117
+ // Check if this binding matches the pressed key
2118
+ if( parseInt( key, 10 ) === event.keyCode ) {
2119
+
2120
+ var value = config.keyboard[ key ];
2121
+
2122
+ // Calback function
2123
+ if( typeof value === 'function' ) {
2124
+ value.apply( null, [ event ] );
2125
+ }
2126
+ // String shortcuts to reveal.js API
2127
+ else if( typeof value === 'string' && typeof Reveal[ value ] === 'function' ) {
2128
+ Reveal[ value ].call();
2129
+ }
2130
+
2131
+ triggered = true;
2132
+
2133
+ }
2134
+
2135
+ }
2136
+
2137
+ }
2138
+
2139
+ // 2. System defined key bindings
2140
+ if( triggered === false ) {
2141
+
2142
+ // Assume true and try to prove false
2143
+ triggered = true;
2144
+
2145
+ switch( event.keyCode ) {
2146
+ // p, page up
2147
+ case 80: case 33: navigatePrev(); break;
2148
+ // n, page down
2149
+ case 78: case 34: navigateNext(); break;
2150
+ // h, left
2151
+ case 72: case 37: navigateLeft(); break;
2152
+ // l, right
2153
+ case 76: case 39: navigateRight(); break;
2154
+ // k, up
2155
+ case 75: case 38: navigateUp(); break;
2156
+ // j, down
2157
+ case 74: case 40: navigateDown(); break;
2158
+ // home
2159
+ case 36: slide( 0 ); break;
2160
+ // end
2161
+ case 35: slide( Number.MAX_VALUE ); break;
2162
+ // space
2163
+ case 32: isOverview() ? deactivateOverview() : event.shiftKey ? navigatePrev() : navigateNext(); break;
2164
+ // return
2165
+ case 13: isOverview() ? deactivateOverview() : triggered = false; break;
2166
+ // b, period, Logitech presenter tools "black screen" button
2167
+ case 66: case 190: case 191: togglePause(); break;
2168
+ // f
2169
+ case 70: enterFullscreen(); break;
2170
+ default:
2171
+ triggered = false;
2172
+ }
2173
+
2174
+ }
2175
+
2176
+ // If the input resulted in a triggered action we should prevent
2177
+ // the browsers default behavior
2178
+ if( triggered ) {
2179
+ event.preventDefault();
2180
+ }
2181
+ else if ( event.keyCode === 27 && supports3DTransforms ) {
2182
+ toggleOverview();
2183
+
2184
+ event.preventDefault();
2185
+ }
2186
+
2187
+ // If auto-sliding is enabled we need to cue up
2188
+ // another timeout
2189
+ cueAutoSlide();
2190
+
2191
+ }
2192
+
2193
+ /**
2194
+ * Handler for the 'touchstart' event, enables support for
2195
+ * swipe and pinch gestures.
2196
+ */
2197
+ function onTouchStart( event ) {
2198
+
2199
+ touch.startX = event.touches[0].clientX;
2200
+ touch.startY = event.touches[0].clientY;
2201
+ touch.startCount = event.touches.length;
2202
+
2203
+ // If there's two touches we need to memorize the distance
2204
+ // between those two points to detect pinching
2205
+ if( event.touches.length === 2 && config.overview ) {
2206
+ touch.startSpan = distanceBetween( {
2207
+ x: event.touches[1].clientX,
2208
+ y: event.touches[1].clientY
2209
+ }, {
2210
+ x: touch.startX,
2211
+ y: touch.startY
2212
+ } );
2213
+ }
2214
+
2215
+ }
2216
+
2217
+ /**
2218
+ * Handler for the 'touchmove' event.
2219
+ */
2220
+ function onTouchMove( event ) {
2221
+
2222
+ // Each touch should only trigger one action
2223
+ if( !touch.handled ) {
2224
+ var currentX = event.touches[0].clientX;
2225
+ var currentY = event.touches[0].clientY;
2226
+
2227
+ // If the touch started off with two points and still has
2228
+ // two active touches; test for the pinch gesture
2229
+ if( event.touches.length === 2 && touch.startCount === 2 && config.overview ) {
2230
+
2231
+ // The current distance in pixels between the two touch points
2232
+ var currentSpan = distanceBetween( {
2233
+ x: event.touches[1].clientX,
2234
+ y: event.touches[1].clientY
2235
+ }, {
2236
+ x: touch.startX,
2237
+ y: touch.startY
2238
+ } );
2239
+
2240
+ // If the span is larger than the desire amount we've got
2241
+ // ourselves a pinch
2242
+ if( Math.abs( touch.startSpan - currentSpan ) > touch.threshold ) {
2243
+ touch.handled = true;
2244
+
2245
+ if( currentSpan < touch.startSpan ) {
2246
+ activateOverview();
2247
+ }
2248
+ else {
2249
+ deactivateOverview();
2250
+ }
2251
+ }
2252
+
2253
+ event.preventDefault();
2254
+
2255
+ }
2256
+ // There was only one touch point, look for a swipe
2257
+ else if( event.touches.length === 1 && touch.startCount !== 2 ) {
2258
+
2259
+ var deltaX = currentX - touch.startX,
2260
+ deltaY = currentY - touch.startY;
2261
+
2262
+ if( deltaX > touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
2263
+ touch.handled = true;
2264
+ navigateLeft();
2265
+ }
2266
+ else if( deltaX < -touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
2267
+ touch.handled = true;
2268
+ navigateRight();
2269
+ }
2270
+ else if( deltaY > touch.threshold ) {
2271
+ touch.handled = true;
2272
+ navigateUp();
2273
+ }
2274
+ else if( deltaY < -touch.threshold ) {
2275
+ touch.handled = true;
2276
+ navigateDown();
2277
+ }
2278
+
2279
+ event.preventDefault();
2280
+
2281
+ }
2282
+ }
2283
+ // There's a bug with swiping on some Android devices unless
2284
+ // the default action is always prevented
2285
+ else if( navigator.userAgent.match( /android/gi ) ) {
2286
+ event.preventDefault();
2287
+ }
2288
+
2289
+ }
2290
+
2291
+ /**
2292
+ * Handler for the 'touchend' event.
2293
+ */
2294
+ function onTouchEnd( event ) {
2295
+
2296
+ touch.handled = false;
2297
+
2298
+ }
2299
+
2300
+ /**
2301
+ * Convert pointer down to touch start.
2302
+ */
2303
+ function onPointerDown( event ) {
2304
+
2305
+ if( event.pointerType === event.MSPOINTER_TYPE_TOUCH ) {
2306
+ event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
2307
+ onTouchStart( event );
2308
+ }
2309
+
2310
+ }
2311
+
2312
+ /**
2313
+ * Convert pointer move to touch move.
2314
+ */
2315
+ function onPointerMove( event ) {
2316
+
2317
+ if( event.pointerType === event.MSPOINTER_TYPE_TOUCH ) {
2318
+ event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
2319
+ onTouchMove( event );
2320
+ }
2321
+
2322
+ }
2323
+
2324
+ /**
2325
+ * Convert pointer up to touch end.
2326
+ */
2327
+ function onPointerUp( event ) {
2328
+
2329
+ if( event.pointerType === event.MSPOINTER_TYPE_TOUCH ) {
2330
+ event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
2331
+ onTouchEnd( event );
2332
+ }
2333
+
2334
+ }
2335
+
2336
+ /**
2337
+ * Handles mouse wheel scrolling, throttled to avoid skipping
2338
+ * multiple slides.
2339
+ */
2340
+ function onDocumentMouseScroll( event ) {
2341
+
2342
+ if( Date.now() - lastMouseWheelStep > 600 ) {
2343
+
2344
+ lastMouseWheelStep = Date.now();
2345
+
2346
+ var delta = event.detail || -event.wheelDelta;
2347
+ if( delta > 0 ) {
2348
+ navigateNext();
2349
+ }
2350
+ else {
2351
+ navigatePrev();
2352
+ }
2353
+
2354
+ }
2355
+
2356
+ }
2357
+
2358
+ /**
2359
+ * Clicking on the progress bar results in a navigation to the
2360
+ * closest approximate horizontal slide using this equation:
2361
+ *
2362
+ * ( clickX / presentationWidth ) * numberOfSlides
2363
+ */
2364
+ function onProgressClicked( event ) {
2365
+
2366
+ event.preventDefault();
2367
+
2368
+ var slidesTotal = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).length;
2369
+ var slideIndex = Math.floor( ( event.clientX / dom.wrapper.offsetWidth ) * slidesTotal );
2370
+
2371
+ slide( slideIndex );
2372
+
2373
+ }
2374
+
2375
+ /**
2376
+ * Event handler for navigation control buttons.
2377
+ */
2378
+ function onNavigateLeftClicked( event ) { event.preventDefault(); navigateLeft(); }
2379
+ function onNavigateRightClicked( event ) { event.preventDefault(); navigateRight(); }
2380
+ function onNavigateUpClicked( event ) { event.preventDefault(); navigateUp(); }
2381
+ function onNavigateDownClicked( event ) { event.preventDefault(); navigateDown(); }
2382
+ function onNavigatePrevClicked( event ) { event.preventDefault(); navigatePrev(); }
2383
+ function onNavigateNextClicked( event ) { event.preventDefault(); navigateNext(); }
2384
+
2385
+ /**
2386
+ * Handler for the window level 'hashchange' event.
2387
+ */
2388
+ function onWindowHashChange( event ) {
2389
+
2390
+ readURL();
2391
+
2392
+ }
2393
+
2394
+ /**
2395
+ * Handler for the window level 'resize' event.
2396
+ */
2397
+ function onWindowResize( event ) {
2398
+
2399
+ layout();
2400
+
2401
+ }
2402
+
2403
+ /**
2404
+ * Invoked when a slide is and we're in the overview.
2405
+ */
2406
+ function onOverviewSlideClicked( event ) {
2407
+
2408
+ // TODO There's a bug here where the event listeners are not
2409
+ // removed after deactivating the overview.
2410
+ if( eventsAreBound && isOverview() ) {
2411
+ event.preventDefault();
2412
+
2413
+ var element = event.target;
2414
+
2415
+ while( element && !element.nodeName.match( /section/gi ) ) {
2416
+ element = element.parentNode;
2417
+ }
2418
+
2419
+ if( element && !element.classList.contains( 'disabled' ) ) {
2420
+
2421
+ deactivateOverview();
2422
+
2423
+ if( element.nodeName.match( /section/gi ) ) {
2424
+ var h = parseInt( element.getAttribute( 'data-index-h' ), 10 ),
2425
+ v = parseInt( element.getAttribute( 'data-index-v' ), 10 );
2426
+
2427
+ slide( h, v );
2428
+ }
2429
+
2430
+ }
2431
+ }
2432
+
2433
+ }
2434
+
2435
+ /**
2436
+ * Handles clicks on links that are set to preview in the
2437
+ * iframe overlay.
2438
+ */
2439
+ function onPreviewLinkClicked( event ) {
2440
+
2441
+ var url = event.target.getAttribute( 'href' );
2442
+ if( url ) {
2443
+ openPreview( url );
2444
+ event.preventDefault();
2445
+ }
2446
+
2447
+ }
2448
+
2449
+
2450
+ // --------------------------------------------------------------------//
2451
+ // ------------------------------- API --------------------------------//
2452
+ // --------------------------------------------------------------------//
2453
+
2454
+
2455
+ return {
2456
+ initialize: initialize,
2457
+ configure: configure,
2458
+ sync: sync,
2459
+
2460
+ // Navigation methods
2461
+ slide: slide,
2462
+ left: navigateLeft,
2463
+ right: navigateRight,
2464
+ up: navigateUp,
2465
+ down: navigateDown,
2466
+ prev: navigatePrev,
2467
+ next: navigateNext,
2468
+ prevFragment: previousFragment,
2469
+ nextFragment: nextFragment,
2470
+
2471
+ // Deprecated aliases
2472
+ navigateTo: slide,
2473
+ navigateLeft: navigateLeft,
2474
+ navigateRight: navigateRight,
2475
+ navigateUp: navigateUp,
2476
+ navigateDown: navigateDown,
2477
+ navigatePrev: navigatePrev,
2478
+ navigateNext: navigateNext,
2479
+
2480
+ // Forces an update in slide layout
2481
+ layout: layout,
2482
+
2483
+ // Returns an object with the available routes as booleans (left/right/top/bottom)
2484
+ availableRoutes: availableRoutes,
2485
+
2486
+ // Returns an object with the available fragments as booleans (prev/next)
2487
+ availableFragments: availableFragments,
2488
+
2489
+ // Toggles the overview mode on/off
2490
+ toggleOverview: toggleOverview,
2491
+
2492
+ // Toggles the "black screen" mode on/off
2493
+ togglePause: togglePause,
2494
+
2495
+ // State checks
2496
+ isOverview: isOverview,
2497
+ isPaused: isPaused,
2498
+
2499
+ // Adds or removes all internal event listeners (such as keyboard)
2500
+ addEventListeners: addEventListeners,
2501
+ removeEventListeners: removeEventListeners,
2502
+
2503
+ // Returns the indices of the current, or specified, slide
2504
+ getIndices: getIndices,
2505
+
2506
+ // Returns the slide at the specified index, y is optional
2507
+ getSlide: function( x, y ) {
2508
+ var horizontalSlide = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR )[ x ];
2509
+ var verticalSlides = horizontalSlide && horizontalSlide.querySelectorAll( 'section' );
2510
+
2511
+ if( typeof y !== 'undefined' ) {
2512
+ return verticalSlides ? verticalSlides[ y ] : undefined;
2513
+ }
2514
+
2515
+ return horizontalSlide;
2516
+ },
2517
+
2518
+ // Returns the previous slide element, may be null
2519
+ getPreviousSlide: function() {
2520
+ return previousSlide;
2521
+ },
2522
+
2523
+ // Returns the current slide element
2524
+ getCurrentSlide: function() {
2525
+ return currentSlide;
2526
+ },
2527
+
2528
+ // Returns the current scale of the presentation content
2529
+ getScale: function() {
2530
+ return scale;
2531
+ },
2532
+
2533
+ // Returns the current configuration object
2534
+ getConfig: function() {
2535
+ return config;
2536
+ },
2537
+
2538
+ // Helper method, retrieves query string as a key/value hash
2539
+ getQueryHash: function() {
2540
+ var query = {};
2541
+
2542
+ location.search.replace( /[A-Z0-9]+?=(\w*)/gi, function(a) {
2543
+ query[ a.split( '=' ).shift() ] = a.split( '=' ).pop();
2544
+ } );
2545
+
2546
+ return query;
2547
+ },
2548
+
2549
+ // Returns true if we're currently on the first slide
2550
+ isFirstSlide: function() {
2551
+ return document.querySelector( SLIDES_SELECTOR + '.past' ) == null ? true : false;
2552
+ },
2553
+
2554
+ // Returns true if we're currently on the last slide
2555
+ isLastSlide: function() {
2556
+ if( currentSlide && currentSlide.classList.contains( '.stack' ) ) {
2557
+ return currentSlide.querySelector( SLIDES_SELECTOR + '.future' ) == null ? true : false;
2558
+ }
2559
+ else {
2560
+ return document.querySelector( SLIDES_SELECTOR + '.future' ) == null ? true : false;
2561
+ }
2562
+ },
2563
+
2564
+ // Forward event binding to the reveal DOM element
2565
+ addEventListener: function( type, listener, useCapture ) {
2566
+ if( 'addEventListener' in window ) {
2567
+ ( dom.wrapper || document.querySelector( '.reveal' ) ).addEventListener( type, listener, useCapture );
2568
+ }
2569
+ },
2570
+ removeEventListener: function( type, listener, useCapture ) {
2571
+ if( 'addEventListener' in window ) {
2572
+ ( dom.wrapper || document.querySelector( '.reveal' ) ).removeEventListener( type, listener, useCapture );
2573
+ }
2574
+ }
2575
+ };
2576
+
2577
+ })();