reveal.rb 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +3 -0
  3. data/Gemfile.lock +15 -1
  4. data/README.md +8 -1
  5. data/Rakefile +7 -0
  6. data/lib/reveal/cli.rb +15 -97
  7. data/lib/reveal/command.rb +109 -0
  8. data/lib/reveal/templates/revealjs/css/print/paper.css +193 -167
  9. data/lib/reveal/templates/revealjs/css/print/pdf.css +20 -53
  10. data/lib/reveal/templates/revealjs/css/reveal.css +1175 -0
  11. data/lib/reveal/templates/revealjs/css/theme/beige.css +183 -60
  12. data/lib/reveal/templates/revealjs/css/theme/black.css +267 -0
  13. data/lib/reveal/templates/revealjs/css/theme/blood.css +190 -80
  14. data/lib/reveal/templates/revealjs/css/theme/league.css +273 -0
  15. data/lib/reveal/templates/revealjs/css/theme/moon.css +174 -51
  16. data/lib/reveal/templates/revealjs/css/theme/night.css +171 -42
  17. data/lib/reveal/templates/revealjs/css/theme/serif.css +187 -58
  18. data/lib/reveal/templates/revealjs/css/theme/simple.css +179 -50
  19. data/lib/reveal/templates/revealjs/css/theme/sky.css +176 -47
  20. data/lib/reveal/templates/revealjs/css/theme/solarized.css +174 -51
  21. data/lib/reveal/templates/revealjs/css/theme/white.css +267 -0
  22. data/lib/reveal/templates/revealjs/index.html +411 -0
  23. data/lib/reveal/templates/revealjs/js/reveal.js +4508 -0
  24. data/lib/reveal/templates/revealjs/lib/css/zenburn.css +74 -71
  25. data/lib/reveal/templates/revealjs/lib/font/{league_gothic_license → league-gothic/LICENSE} +0 -0
  26. data/lib/reveal/templates/revealjs/lib/font/league-gothic/league-gothic.css +10 -0
  27. data/lib/reveal/templates/revealjs/lib/font/league-gothic/league-gothic.eot +0 -0
  28. data/lib/reveal/templates/revealjs/lib/font/league-gothic/league-gothic.ttf +0 -0
  29. data/lib/reveal/templates/revealjs/lib/font/league-gothic/league-gothic.woff +0 -0
  30. data/lib/reveal/templates/revealjs/lib/font/source-sans-pro/LICENSE +45 -0
  31. data/lib/reveal/templates/revealjs/lib/font/source-sans-pro/source-sans-pro-italic.eot +0 -0
  32. data/lib/reveal/templates/revealjs/lib/font/source-sans-pro/source-sans-pro-italic.ttf +0 -0
  33. data/lib/reveal/templates/revealjs/lib/font/source-sans-pro/source-sans-pro-italic.woff +0 -0
  34. data/lib/reveal/templates/revealjs/lib/font/source-sans-pro/source-sans-pro-regular.eot +0 -0
  35. data/lib/reveal/templates/revealjs/lib/font/source-sans-pro/source-sans-pro-regular.ttf +0 -0
  36. data/lib/reveal/templates/revealjs/lib/font/source-sans-pro/source-sans-pro-regular.woff +0 -0
  37. data/lib/reveal/templates/revealjs/lib/font/source-sans-pro/source-sans-pro-semibold.eot +0 -0
  38. data/lib/reveal/templates/revealjs/lib/font/source-sans-pro/source-sans-pro-semibold.ttf +0 -0
  39. data/lib/reveal/templates/revealjs/lib/font/source-sans-pro/source-sans-pro-semibold.woff +0 -0
  40. data/lib/reveal/templates/revealjs/lib/font/source-sans-pro/source-sans-pro-semibolditalic.eot +0 -0
  41. data/lib/reveal/templates/revealjs/lib/font/source-sans-pro/source-sans-pro-semibolditalic.ttf +0 -0
  42. data/lib/reveal/templates/revealjs/lib/font/source-sans-pro/source-sans-pro-semibolditalic.woff +0 -0
  43. data/lib/reveal/templates/revealjs/lib/font/source-sans-pro/source-sans-pro.css +39 -0
  44. data/lib/reveal/templates/revealjs/lib/js/html5shiv.js +0 -0
  45. data/lib/reveal/templates/revealjs/plugin/highlight/highlight.js +2 -4
  46. data/lib/reveal/templates/revealjs/plugin/leap/leap.js +4 -2
  47. data/lib/reveal/templates/revealjs/plugin/markdown/example.html +3 -3
  48. data/lib/reveal/templates/revealjs/plugin/markdown/example.md +0 -0
  49. data/lib/reveal/templates/revealjs/plugin/markdown/markdown.js +10 -9
  50. data/lib/reveal/templates/revealjs/plugin/markdown/marked.js +2 -33
  51. data/lib/reveal/templates/revealjs/plugin/math/math.js +1 -1
  52. data/lib/reveal/templates/revealjs/plugin/multiplex/client.js +0 -0
  53. data/lib/reveal/templates/revealjs/plugin/multiplex/index.js +0 -0
  54. data/lib/reveal/templates/revealjs/plugin/multiplex/master.js +0 -0
  55. data/lib/reveal/templates/revealjs/plugin/notes/notes.html +321 -182
  56. data/lib/reveal/templates/revealjs/plugin/notes/notes.js +89 -45
  57. data/lib/reveal/templates/revealjs/plugin/notes-server/client.js +49 -46
  58. data/lib/reveal/templates/revealjs/plugin/notes-server/index.js +28 -21
  59. data/lib/reveal/templates/revealjs/plugin/notes-server/notes.html +351 -97
  60. data/lib/reveal/templates/revealjs/plugin/print-pdf/print-pdf.js +24 -20
  61. data/lib/reveal/templates/revealjs/plugin/remotes/remotes.js +0 -0
  62. data/lib/reveal/templates/revealjs/plugin/search/search.js +0 -0
  63. data/lib/reveal/templates/revealjs/plugin/zoom-js/zoom.js +78 -58
  64. data/lib/reveal/templates/template.html +17 -24
  65. data/lib/reveal/version.rb +1 -1
  66. data/lib/reveal.rb +1 -0
  67. data/reveal.rb.gemspec +3 -0
  68. data/spec/lib/reveal/cli_spec.rb +26 -0
  69. data/spec/lib/reveal/command_spec.rb +165 -0
  70. metadata +65 -28
  71. data/lib/reveal/templates/revealjs/css/reveal.min.css +0 -7
  72. data/lib/reveal/templates/revealjs/css/theme/README.md +0 -25
  73. data/lib/reveal/templates/revealjs/css/theme/default.css +0 -148
  74. data/lib/reveal/templates/revealjs/css/theme/source/beige.scss +0 -50
  75. data/lib/reveal/templates/revealjs/css/theme/source/blood.scss +0 -91
  76. data/lib/reveal/templates/revealjs/css/theme/source/default.scss +0 -42
  77. data/lib/reveal/templates/revealjs/css/theme/source/moon.scss +0 -68
  78. data/lib/reveal/templates/revealjs/css/theme/source/night.scss +0 -35
  79. data/lib/reveal/templates/revealjs/css/theme/source/serif.scss +0 -35
  80. data/lib/reveal/templates/revealjs/css/theme/source/simple.scss +0 -38
  81. data/lib/reveal/templates/revealjs/css/theme/source/sky.scss +0 -46
  82. data/lib/reveal/templates/revealjs/css/theme/source/solarized.scss +0 -74
  83. data/lib/reveal/templates/revealjs/css/theme/template/mixins.scss +0 -29
  84. data/lib/reveal/templates/revealjs/css/theme/template/settings.scss +0 -34
  85. data/lib/reveal/templates/revealjs/css/theme/template/theme.scss +0 -170
  86. data/lib/reveal/templates/revealjs/js/reveal.min.js +0 -9
  87. data/lib/reveal/templates/revealjs/lib/font/league_gothic-webfont.eot +0 -0
  88. data/lib/reveal/templates/revealjs/lib/font/league_gothic-webfont.svg +0 -230
  89. data/lib/reveal/templates/revealjs/lib/font/league_gothic-webfont.ttf +0 -0
  90. data/lib/reveal/templates/revealjs/lib/font/league_gothic-webfont.woff +0 -0
  91. data/lib/reveal/templates/revealjs/plugin/postmessage/example.html +0 -39
  92. data/lib/reveal/templates/revealjs/plugin/postmessage/postmessage.js +0 -42
@@ -0,0 +1,4508 @@
1
+ /*!
2
+ * reveal.js
3
+ * http://lab.hakim.se/reveal-js
4
+ * MIT licensed
5
+ *
6
+ * Copyright (C) 2015 Hakim El Hattab, http://hakim.se
7
+ */
8
+ (function( root, factory ) {
9
+ if( typeof define === 'function' && define.amd ) {
10
+ // AMD. Register as an anonymous module.
11
+ define( function() {
12
+ root.Reveal = factory();
13
+ return root.Reveal;
14
+ } );
15
+ } else if( typeof exports === 'object' ) {
16
+ // Node. Does not work with strict CommonJS.
17
+ module.exports = factory();
18
+ } else {
19
+ // Browser globals.
20
+ root.Reveal = factory();
21
+ }
22
+ }( this, function() {
23
+
24
+ 'use strict';
25
+
26
+ var Reveal;
27
+
28
+ var SLIDES_SELECTOR = '.slides section',
29
+ HORIZONTAL_SLIDES_SELECTOR = '.slides>section',
30
+ VERTICAL_SLIDES_SELECTOR = '.slides>section.present>section',
31
+ HOME_SLIDE_SELECTOR = '.slides>section:first-of-type',
32
+
33
+ // Configuration defaults, can be overridden at initialization time
34
+ config = {
35
+
36
+ // The "normal" size of the presentation, aspect ratio will be preserved
37
+ // when the presentation is scaled to fit different resolutions
38
+ width: 960,
39
+ height: 700,
40
+
41
+ // Factor of the display size that should remain empty around the content
42
+ margin: 0.1,
43
+
44
+ // Bounds for smallest/largest possible scale to apply to content
45
+ minScale: 0.2,
46
+ maxScale: 1.5,
47
+
48
+ // Display controls in the bottom right corner
49
+ controls: true,
50
+
51
+ // Display a presentation progress bar
52
+ progress: true,
53
+
54
+ // Display the page number of the current slide
55
+ slideNumber: false,
56
+
57
+ // Push each slide change to the browser history
58
+ history: false,
59
+
60
+ // Enable keyboard shortcuts for navigation
61
+ keyboard: true,
62
+
63
+ // Optional function that blocks keyboard events when retuning false
64
+ keyboardCondition: null,
65
+
66
+ // Enable the slide overview mode
67
+ overview: true,
68
+
69
+ // Vertical centering of slides
70
+ center: true,
71
+
72
+ // Enables touch navigation on devices with touch input
73
+ touch: true,
74
+
75
+ // Loop the presentation
76
+ loop: false,
77
+
78
+ // Change the presentation direction to be RTL
79
+ rtl: false,
80
+
81
+ // Turns fragments on and off globally
82
+ fragments: true,
83
+
84
+ // Flags if the presentation is running in an embedded mode,
85
+ // i.e. contained within a limited portion of the screen
86
+ embedded: false,
87
+
88
+ // Flags if we should show a help overlay when the questionmark
89
+ // key is pressed
90
+ help: true,
91
+
92
+ // Flags if it should be possible to pause the presentation (blackout)
93
+ pause: true,
94
+
95
+ // Number of milliseconds between automatically proceeding to the
96
+ // next slide, disabled when set to 0, this value can be overwritten
97
+ // by using a data-autoslide attribute on your slides
98
+ autoSlide: 0,
99
+
100
+ // Stop auto-sliding after user input
101
+ autoSlideStoppable: true,
102
+
103
+ // Enable slide navigation via mouse wheel
104
+ mouseWheel: false,
105
+
106
+ // Apply a 3D roll to links on hover
107
+ rollingLinks: false,
108
+
109
+ // Hides the address bar on mobile devices
110
+ hideAddressBar: true,
111
+
112
+ // Opens links in an iframe preview overlay
113
+ previewLinks: false,
114
+
115
+ // Exposes the reveal.js API through window.postMessage
116
+ postMessage: true,
117
+
118
+ // Dispatches all reveal.js events to the parent window through postMessage
119
+ postMessageEvents: false,
120
+
121
+ // Focuses body when page changes visiblity to ensure keyboard shortcuts work
122
+ focusBodyOnPageVisibilityChange: true,
123
+
124
+ // Transition style
125
+ transition: 'slide', // none/fade/slide/convex/concave/zoom
126
+
127
+ // Transition speed
128
+ transitionSpeed: 'default', // default/fast/slow
129
+
130
+ // Transition style for full page slide backgrounds
131
+ backgroundTransition: 'fade', // none/fade/slide/convex/concave/zoom
132
+
133
+ // Parallax background image
134
+ parallaxBackgroundImage: '', // CSS syntax, e.g. "a.jpg"
135
+
136
+ // Parallax background size
137
+ parallaxBackgroundSize: '', // CSS syntax, e.g. "3000px 2000px"
138
+
139
+ // Amount of pixels to move the parallax background per slide step
140
+ parallaxBackgroundHorizontal: null,
141
+ parallaxBackgroundVertical: null,
142
+
143
+ // Number of slides away from the current that are visible
144
+ viewDistance: 3,
145
+
146
+ // Script dependencies to load
147
+ dependencies: []
148
+
149
+ },
150
+
151
+ // Flags if reveal.js is loaded (has dispatched the 'ready' event)
152
+ loaded = false,
153
+
154
+ // Flags if the overview mode is currently active
155
+ overview = false,
156
+
157
+ // The horizontal and vertical index of the currently active slide
158
+ indexh,
159
+ indexv,
160
+
161
+ // The previous and current slide HTML elements
162
+ previousSlide,
163
+ currentSlide,
164
+
165
+ previousBackground,
166
+
167
+ // Slides may hold a data-state attribute which we pick up and apply
168
+ // as a class to the body. This list contains the combined state of
169
+ // all current slides.
170
+ state = [],
171
+
172
+ // The current scale of the presentation (see width/height config)
173
+ scale = 1,
174
+
175
+ // CSS transform that is currently applied to the slides container,
176
+ // split into two groups
177
+ slidesTransform = { layout: '', overview: '' },
178
+
179
+ // Cached references to DOM elements
180
+ dom = {},
181
+
182
+ // Features supported by the browser, see #checkCapabilities()
183
+ features = {},
184
+
185
+ // Client is a mobile device, see #checkCapabilities()
186
+ isMobileDevice,
187
+
188
+ // Throttles mouse wheel navigation
189
+ lastMouseWheelStep = 0,
190
+
191
+ // Delays updates to the URL due to a Chrome thumbnailer bug
192
+ writeURLTimeout = 0,
193
+
194
+ // Flags if the interaction event listeners are bound
195
+ eventsAreBound = false,
196
+
197
+ // The current auto-slide duration
198
+ autoSlide = 0,
199
+
200
+ // Auto slide properties
201
+ autoSlidePlayer,
202
+ autoSlideTimeout = 0,
203
+ autoSlideStartTime = -1,
204
+ autoSlidePaused = false,
205
+
206
+ // Holds information about the currently ongoing touch input
207
+ touch = {
208
+ startX: 0,
209
+ startY: 0,
210
+ startSpan: 0,
211
+ startCount: 0,
212
+ captured: false,
213
+ threshold: 40
214
+ },
215
+
216
+ // Holds information about the keyboard shortcuts
217
+ keyboardShortcuts = {
218
+ 'N , SPACE': 'Next slide',
219
+ 'P': 'Previous slide',
220
+ '← , H': 'Navigate left',
221
+ '→ , L': 'Navigate right',
222
+ '↑ , K': 'Navigate up',
223
+ '↓ , J': 'Navigate down',
224
+ 'Home': 'First slide',
225
+ 'End': 'Last slide',
226
+ 'B , .': 'Pause',
227
+ 'F': 'Fullscreen',
228
+ 'ESC, O': 'Slide overview'
229
+ };
230
+
231
+ /**
232
+ * Starts up the presentation if the client is capable.
233
+ */
234
+ function initialize( options ) {
235
+
236
+ checkCapabilities();
237
+
238
+ if( !features.transforms2d && !features.transforms3d ) {
239
+ document.body.setAttribute( 'class', 'no-transforms' );
240
+
241
+ // Since JS won't be running any further, we load all lazy
242
+ // loading elements upfront
243
+ var images = toArray( document.getElementsByTagName( 'img' ) ),
244
+ iframes = toArray( document.getElementsByTagName( 'iframe' ) );
245
+
246
+ var lazyLoadable = images.concat( iframes );
247
+
248
+ for( var i = 0, len = lazyLoadable.length; i < len; i++ ) {
249
+ var element = lazyLoadable[i];
250
+ if( element.getAttribute( 'data-src' ) ) {
251
+ element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
252
+ element.removeAttribute( 'data-src' );
253
+ }
254
+ }
255
+
256
+ // If the browser doesn't support core features we won't be
257
+ // using JavaScript to control the presentation
258
+ return;
259
+ }
260
+
261
+ // Cache references to key DOM elements
262
+ dom.wrapper = document.querySelector( '.reveal' );
263
+ dom.slides = document.querySelector( '.reveal .slides' );
264
+
265
+ // Force a layout when the whole page, incl fonts, has loaded
266
+ window.addEventListener( 'load', layout, false );
267
+
268
+ var query = Reveal.getQueryHash();
269
+
270
+ // Do not accept new dependencies via query config to avoid
271
+ // the potential of malicious script injection
272
+ if( typeof query['dependencies'] !== 'undefined' ) delete query['dependencies'];
273
+
274
+ // Copy options over to our config object
275
+ extend( config, options );
276
+ extend( config, query );
277
+
278
+ // Hide the address bar in mobile browsers
279
+ hideAddressBar();
280
+
281
+ // Loads the dependencies and continues to #start() once done
282
+ load();
283
+
284
+ }
285
+
286
+ /**
287
+ * Inspect the client to see what it's capable of, this
288
+ * should only happens once per runtime.
289
+ */
290
+ function checkCapabilities() {
291
+
292
+ features.transforms3d = 'WebkitPerspective' in document.body.style ||
293
+ 'MozPerspective' in document.body.style ||
294
+ 'msPerspective' in document.body.style ||
295
+ 'OPerspective' in document.body.style ||
296
+ 'perspective' in document.body.style;
297
+
298
+ features.transforms2d = 'WebkitTransform' in document.body.style ||
299
+ 'MozTransform' in document.body.style ||
300
+ 'msTransform' in document.body.style ||
301
+ 'OTransform' in document.body.style ||
302
+ 'transform' in document.body.style;
303
+
304
+ features.requestAnimationFrameMethod = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame;
305
+ features.requestAnimationFrame = typeof features.requestAnimationFrameMethod === 'function';
306
+
307
+ features.canvas = !!document.createElement( 'canvas' ).getContext;
308
+
309
+ features.touch = !!( 'ontouchstart' in window );
310
+
311
+ // Transitions in the overview are disabled in desktop and
312
+ // mobile Safari due to lag
313
+ features.overviewTransitions = !/Version\/[\d\.]+.*Safari/.test( navigator.userAgent );
314
+
315
+ isMobileDevice = /(iphone|ipod|ipad|android)/gi.test( navigator.userAgent );
316
+
317
+ }
318
+
319
+ /**
320
+ * Loads the dependencies of reveal.js. Dependencies are
321
+ * defined via the configuration option 'dependencies'
322
+ * and will be loaded prior to starting/binding reveal.js.
323
+ * Some dependencies may have an 'async' flag, if so they
324
+ * will load after reveal.js has been started up.
325
+ */
326
+ function load() {
327
+
328
+ var scripts = [],
329
+ scriptsAsync = [],
330
+ scriptsToPreload = 0;
331
+
332
+ // Called once synchronous scripts finish loading
333
+ function proceed() {
334
+ if( scriptsAsync.length ) {
335
+ // Load asynchronous scripts
336
+ head.js.apply( null, scriptsAsync );
337
+ }
338
+
339
+ start();
340
+ }
341
+
342
+ function loadScript( s ) {
343
+ head.ready( s.src.match( /([\w\d_\-]*)\.?js$|[^\\\/]*$/i )[0], function() {
344
+ // Extension may contain callback functions
345
+ if( typeof s.callback === 'function' ) {
346
+ s.callback.apply( this );
347
+ }
348
+
349
+ if( --scriptsToPreload === 0 ) {
350
+ proceed();
351
+ }
352
+ });
353
+ }
354
+
355
+ for( var i = 0, len = config.dependencies.length; i < len; i++ ) {
356
+ var s = config.dependencies[i];
357
+
358
+ // Load if there's no condition or the condition is truthy
359
+ if( !s.condition || s.condition() ) {
360
+ if( s.async ) {
361
+ scriptsAsync.push( s.src );
362
+ }
363
+ else {
364
+ scripts.push( s.src );
365
+ }
366
+
367
+ loadScript( s );
368
+ }
369
+ }
370
+
371
+ if( scripts.length ) {
372
+ scriptsToPreload = scripts.length;
373
+
374
+ // Load synchronous scripts
375
+ head.js.apply( null, scripts );
376
+ }
377
+ else {
378
+ proceed();
379
+ }
380
+
381
+ }
382
+
383
+ /**
384
+ * Starts up reveal.js by binding input events and navigating
385
+ * to the current URL deeplink if there is one.
386
+ */
387
+ function start() {
388
+
389
+ // Make sure we've got all the DOM elements we need
390
+ setupDOM();
391
+
392
+ // Listen to messages posted to this window
393
+ setupPostMessage();
394
+
395
+ // Prevent iframes from scrolling the slides out of view
396
+ setupIframeScrollPrevention();
397
+
398
+ // Resets all vertical slides so that only the first is visible
399
+ resetVerticalSlides();
400
+
401
+ // Updates the presentation to match the current configuration values
402
+ configure();
403
+
404
+ // Read the initial hash
405
+ readURL();
406
+
407
+ // Update all backgrounds
408
+ updateBackground( true );
409
+
410
+ // Notify listeners that the presentation is ready but use a 1ms
411
+ // timeout to ensure it's not fired synchronously after #initialize()
412
+ setTimeout( function() {
413
+ // Enable transitions now that we're loaded
414
+ dom.slides.classList.remove( 'no-transition' );
415
+
416
+ loaded = true;
417
+
418
+ dispatchEvent( 'ready', {
419
+ 'indexh': indexh,
420
+ 'indexv': indexv,
421
+ 'currentSlide': currentSlide
422
+ } );
423
+ }, 1 );
424
+
425
+ // Special setup and config is required when printing to PDF
426
+ if( isPrintingPDF() ) {
427
+ removeEventListeners();
428
+
429
+ // The document needs to have loaded for the PDF layout
430
+ // measurements to be accurate
431
+ if( document.readyState === 'complete' ) {
432
+ setupPDF();
433
+ }
434
+ else {
435
+ window.addEventListener( 'load', setupPDF );
436
+ }
437
+ }
438
+
439
+ }
440
+
441
+ /**
442
+ * Finds and stores references to DOM elements which are
443
+ * required by the presentation. If a required element is
444
+ * not found, it is created.
445
+ */
446
+ function setupDOM() {
447
+
448
+ // Prevent transitions while we're loading
449
+ dom.slides.classList.add( 'no-transition' );
450
+
451
+ // Background element
452
+ dom.background = createSingletonNode( dom.wrapper, 'div', 'backgrounds', null );
453
+
454
+ // Progress bar
455
+ dom.progress = createSingletonNode( dom.wrapper, 'div', 'progress', '<span></span>' );
456
+ dom.progressbar = dom.progress.querySelector( 'span' );
457
+
458
+ // Arrow controls
459
+ createSingletonNode( dom.wrapper, 'aside', 'controls',
460
+ '<div class="navigate-left"></div>' +
461
+ '<div class="navigate-right"></div>' +
462
+ '<div class="navigate-up"></div>' +
463
+ '<div class="navigate-down"></div>' );
464
+
465
+ // Slide number
466
+ dom.slideNumber = createSingletonNode( dom.wrapper, 'div', 'slide-number', '' );
467
+
468
+ // Overlay graphic which is displayed during the paused mode
469
+ createSingletonNode( dom.wrapper, 'div', 'pause-overlay', null );
470
+
471
+ // Cache references to elements
472
+ dom.controls = document.querySelector( '.reveal .controls' );
473
+ dom.theme = document.querySelector( '#theme' );
474
+
475
+ dom.wrapper.setAttribute( 'role', 'application' );
476
+
477
+ // There can be multiple instances of controls throughout the page
478
+ dom.controlsLeft = toArray( document.querySelectorAll( '.navigate-left' ) );
479
+ dom.controlsRight = toArray( document.querySelectorAll( '.navigate-right' ) );
480
+ dom.controlsUp = toArray( document.querySelectorAll( '.navigate-up' ) );
481
+ dom.controlsDown = toArray( document.querySelectorAll( '.navigate-down' ) );
482
+ dom.controlsPrev = toArray( document.querySelectorAll( '.navigate-prev' ) );
483
+ dom.controlsNext = toArray( document.querySelectorAll( '.navigate-next' ) );
484
+
485
+ dom.statusDiv = createStatusDiv();
486
+ }
487
+
488
+ /**
489
+ * Creates a hidden div with role aria-live to announce the
490
+ * current slide content. Hide the div off-screen to make it
491
+ * available only to Assistive Technologies.
492
+ */
493
+ function createStatusDiv() {
494
+
495
+ var statusDiv = document.getElementById( 'aria-status-div' );
496
+ if( !statusDiv ) {
497
+ statusDiv = document.createElement( 'div' );
498
+ statusDiv.style.position = 'absolute';
499
+ statusDiv.style.height = '1px';
500
+ statusDiv.style.width = '1px';
501
+ statusDiv.style.overflow ='hidden';
502
+ statusDiv.style.clip = 'rect( 1px, 1px, 1px, 1px )';
503
+ statusDiv.setAttribute( 'id', 'aria-status-div' );
504
+ statusDiv.setAttribute( 'aria-live', 'polite' );
505
+ statusDiv.setAttribute( 'aria-atomic','true' );
506
+ dom.wrapper.appendChild( statusDiv );
507
+ }
508
+ return statusDiv;
509
+
510
+ }
511
+
512
+ /**
513
+ * Configures the presentation for printing to a static
514
+ * PDF.
515
+ */
516
+ function setupPDF() {
517
+
518
+ var slideSize = getComputedSlideSize( window.innerWidth, window.innerHeight );
519
+
520
+ // Dimensions of the PDF pages
521
+ var pageWidth = Math.floor( slideSize.width * ( 1 + config.margin ) ),
522
+ pageHeight = Math.floor( slideSize.height * ( 1 + config.margin ) );
523
+
524
+ // Dimensions of slides within the pages
525
+ var slideWidth = slideSize.width,
526
+ slideHeight = slideSize.height;
527
+
528
+ // Let the browser know what page size we want to print
529
+ injectStyleSheet( '@page{size:'+ pageWidth +'px '+ pageHeight +'px; margin: 0;}' );
530
+
531
+ // Limit the size of certain elements to the dimensions of the slide
532
+ injectStyleSheet( '.reveal section>img, .reveal section>video, .reveal section>iframe{max-width: '+ slideWidth +'px; max-height:'+ slideHeight +'px}' );
533
+
534
+ document.body.classList.add( 'print-pdf' );
535
+ document.body.style.width = pageWidth + 'px';
536
+ document.body.style.height = pageHeight + 'px';
537
+
538
+ // Slide and slide background layout
539
+ toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
540
+
541
+ // Vertical stacks are not centred since their section
542
+ // children will be
543
+ if( slide.classList.contains( 'stack' ) === false ) {
544
+ // Center the slide inside of the page, giving the slide some margin
545
+ var left = ( pageWidth - slideWidth ) / 2,
546
+ top = ( pageHeight - slideHeight ) / 2;
547
+
548
+ var contentHeight = getAbsoluteHeight( slide );
549
+ var numberOfPages = Math.max( Math.ceil( contentHeight / pageHeight ), 1 );
550
+
551
+ // Center slides vertically
552
+ if( numberOfPages === 1 && config.center || slide.classList.contains( 'center' ) ) {
553
+ top = Math.max( ( pageHeight - contentHeight ) / 2, 0 );
554
+ }
555
+
556
+ // Position the slide inside of the page
557
+ slide.style.left = left + 'px';
558
+ slide.style.top = top + 'px';
559
+ slide.style.width = slideWidth + 'px';
560
+
561
+ // TODO Backgrounds need to be multiplied when the slide
562
+ // stretches over multiple pages
563
+ var background = slide.querySelector( '.slide-background' );
564
+ if( background ) {
565
+ background.style.width = pageWidth + 'px';
566
+ background.style.height = ( pageHeight * numberOfPages ) + 'px';
567
+ background.style.top = -top + 'px';
568
+ background.style.left = -left + 'px';
569
+ }
570
+ }
571
+
572
+ } );
573
+
574
+ // Show all fragments
575
+ toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ' .fragment' ) ).forEach( function( fragment ) {
576
+ fragment.classList.add( 'visible' );
577
+ } );
578
+
579
+ }
580
+
581
+ /**
582
+ * This is an unfortunate necessity. Iframes can trigger the
583
+ * parent window to scroll, for example by focusing an input.
584
+ * This scrolling can not be prevented by hiding overflow in
585
+ * CSS so we have to resort to repeatedly checking if the
586
+ * browser has decided to offset our slides :(
587
+ */
588
+ function setupIframeScrollPrevention() {
589
+
590
+ if( dom.slides.querySelector( 'iframe' ) ) {
591
+ setInterval( function() {
592
+ if( dom.wrapper.scrollTop !== 0 || dom.wrapper.scrollLeft !== 0 ) {
593
+ dom.wrapper.scrollTop = 0;
594
+ dom.wrapper.scrollLeft = 0;
595
+ }
596
+ }, 500 );
597
+ }
598
+
599
+ }
600
+
601
+ /**
602
+ * Creates an HTML element and returns a reference to it.
603
+ * If the element already exists the existing instance will
604
+ * be returned.
605
+ */
606
+ function createSingletonNode( container, tagname, classname, innerHTML ) {
607
+
608
+ // Find all nodes matching the description
609
+ var nodes = container.querySelectorAll( '.' + classname );
610
+
611
+ // Check all matches to find one which is a direct child of
612
+ // the specified container
613
+ for( var i = 0; i < nodes.length; i++ ) {
614
+ var testNode = nodes[i];
615
+ if( testNode.parentNode === container ) {
616
+ return testNode;
617
+ }
618
+ }
619
+
620
+ // If no node was found, create it now
621
+ var node = document.createElement( tagname );
622
+ node.classList.add( classname );
623
+ if( typeof innerHTML === 'string' ) {
624
+ node.innerHTML = innerHTML;
625
+ }
626
+ container.appendChild( node );
627
+
628
+ return node;
629
+
630
+ }
631
+
632
+ /**
633
+ * Creates the slide background elements and appends them
634
+ * to the background container. One element is created per
635
+ * slide no matter if the given slide has visible background.
636
+ */
637
+ function createBackgrounds() {
638
+
639
+ var printMode = isPrintingPDF();
640
+
641
+ // Clear prior backgrounds
642
+ dom.background.innerHTML = '';
643
+ dom.background.classList.add( 'no-transition' );
644
+
645
+ // Iterate over all horizontal slides
646
+ toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( slideh ) {
647
+
648
+ var backgroundStack;
649
+
650
+ if( printMode ) {
651
+ backgroundStack = createBackground( slideh, slideh );
652
+ }
653
+ else {
654
+ backgroundStack = createBackground( slideh, dom.background );
655
+ }
656
+
657
+ // Iterate over all vertical slides
658
+ toArray( slideh.querySelectorAll( 'section' ) ).forEach( function( slidev ) {
659
+
660
+ if( printMode ) {
661
+ createBackground( slidev, slidev );
662
+ }
663
+ else {
664
+ createBackground( slidev, backgroundStack );
665
+ }
666
+
667
+ backgroundStack.classList.add( 'stack' );
668
+
669
+ } );
670
+
671
+ } );
672
+
673
+ // Add parallax background if specified
674
+ if( config.parallaxBackgroundImage ) {
675
+
676
+ dom.background.style.backgroundImage = 'url("' + config.parallaxBackgroundImage + '")';
677
+ dom.background.style.backgroundSize = config.parallaxBackgroundSize;
678
+
679
+ // Make sure the below properties are set on the element - these properties are
680
+ // needed for proper transitions to be set on the element via CSS. To remove
681
+ // annoying background slide-in effect when the presentation starts, apply
682
+ // these properties after short time delay
683
+ setTimeout( function() {
684
+ dom.wrapper.classList.add( 'has-parallax-background' );
685
+ }, 1 );
686
+
687
+ }
688
+ else {
689
+
690
+ dom.background.style.backgroundImage = '';
691
+ dom.wrapper.classList.remove( 'has-parallax-background' );
692
+
693
+ }
694
+
695
+ }
696
+
697
+ /**
698
+ * Creates a background for the given slide.
699
+ *
700
+ * @param {HTMLElement} slide
701
+ * @param {HTMLElement} container The element that the background
702
+ * should be appended to
703
+ */
704
+ function createBackground( slide, container ) {
705
+
706
+ var data = {
707
+ background: slide.getAttribute( 'data-background' ),
708
+ backgroundSize: slide.getAttribute( 'data-background-size' ),
709
+ backgroundImage: slide.getAttribute( 'data-background-image' ),
710
+ backgroundVideo: slide.getAttribute( 'data-background-video' ),
711
+ backgroundIframe: slide.getAttribute( 'data-background-iframe' ),
712
+ backgroundColor: slide.getAttribute( 'data-background-color' ),
713
+ backgroundRepeat: slide.getAttribute( 'data-background-repeat' ),
714
+ backgroundPosition: slide.getAttribute( 'data-background-position' ),
715
+ backgroundTransition: slide.getAttribute( 'data-background-transition' )
716
+ };
717
+
718
+ var element = document.createElement( 'div' );
719
+
720
+ // Carry over custom classes from the slide to the background
721
+ element.className = 'slide-background ' + slide.className.replace( /present|past|future/, '' );
722
+
723
+ if( data.background ) {
724
+ // Auto-wrap image urls in url(...)
725
+ if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp)$/gi.test( data.background ) ) {
726
+ slide.setAttribute( 'data-background-image', data.background );
727
+ }
728
+ else {
729
+ element.style.background = data.background;
730
+ }
731
+ }
732
+
733
+ // Create a hash for this combination of background settings.
734
+ // This is used to determine when two slide backgrounds are
735
+ // the same.
736
+ if( data.background || data.backgroundColor || data.backgroundImage || data.backgroundVideo || data.backgroundIframe ) {
737
+ element.setAttribute( 'data-background-hash', data.background +
738
+ data.backgroundSize +
739
+ data.backgroundImage +
740
+ data.backgroundVideo +
741
+ data.backgroundIframe +
742
+ data.backgroundColor +
743
+ data.backgroundRepeat +
744
+ data.backgroundPosition +
745
+ data.backgroundTransition );
746
+ }
747
+
748
+ // Additional and optional background properties
749
+ if( data.backgroundSize ) element.style.backgroundSize = data.backgroundSize;
750
+ if( data.backgroundColor ) element.style.backgroundColor = data.backgroundColor;
751
+ if( data.backgroundRepeat ) element.style.backgroundRepeat = data.backgroundRepeat;
752
+ if( data.backgroundPosition ) element.style.backgroundPosition = data.backgroundPosition;
753
+ if( data.backgroundTransition ) element.setAttribute( 'data-background-transition', data.backgroundTransition );
754
+
755
+ container.appendChild( element );
756
+
757
+ // If backgrounds are being recreated, clear old classes
758
+ slide.classList.remove( 'has-dark-background' );
759
+ slide.classList.remove( 'has-light-background' );
760
+
761
+ // If this slide has a background color, add a class that
762
+ // signals if it is light or dark. If the slide has no background
763
+ // color, no class will be set
764
+ var computedBackgroundColor = window.getComputedStyle( element ).backgroundColor;
765
+ if( computedBackgroundColor ) {
766
+ var rgb = colorToRgb( computedBackgroundColor );
767
+
768
+ // Ignore fully transparent backgrounds. Some browsers return
769
+ // rgba(0,0,0,0) when reading the computed background color of
770
+ // an element with no background
771
+ if( rgb && rgb.a !== 0 ) {
772
+ if( colorBrightness( computedBackgroundColor ) < 128 ) {
773
+ slide.classList.add( 'has-dark-background' );
774
+ }
775
+ else {
776
+ slide.classList.add( 'has-light-background' );
777
+ }
778
+ }
779
+ }
780
+
781
+ return element;
782
+
783
+ }
784
+
785
+ /**
786
+ * Registers a listener to postMessage events, this makes it
787
+ * possible to call all reveal.js API methods from another
788
+ * window. For example:
789
+ *
790
+ * revealWindow.postMessage( JSON.stringify({
791
+ * method: 'slide',
792
+ * args: [ 2 ]
793
+ * }), '*' );
794
+ */
795
+ function setupPostMessage() {
796
+
797
+ if( config.postMessage ) {
798
+ window.addEventListener( 'message', function ( event ) {
799
+ var data = event.data;
800
+
801
+ // Make sure we're dealing with JSON
802
+ if( typeof data === 'string' && data.charAt( 0 ) === '{' && data.charAt( data.length - 1 ) === '}' ) {
803
+ data = JSON.parse( data );
804
+
805
+ // Check if the requested method can be found
806
+ if( data.method && typeof Reveal[data.method] === 'function' ) {
807
+ Reveal[data.method].apply( Reveal, data.args );
808
+ }
809
+ }
810
+ }, false );
811
+ }
812
+
813
+ }
814
+
815
+ /**
816
+ * Applies the configuration settings from the config
817
+ * object. May be called multiple times.
818
+ */
819
+ function configure( options ) {
820
+
821
+ var numberOfSlides = dom.wrapper.querySelectorAll( SLIDES_SELECTOR ).length;
822
+
823
+ dom.wrapper.classList.remove( config.transition );
824
+
825
+ // New config options may be passed when this method
826
+ // is invoked through the API after initialization
827
+ if( typeof options === 'object' ) extend( config, options );
828
+
829
+ // Force linear transition based on browser capabilities
830
+ if( features.transforms3d === false ) config.transition = 'linear';
831
+
832
+ dom.wrapper.classList.add( config.transition );
833
+
834
+ dom.wrapper.setAttribute( 'data-transition-speed', config.transitionSpeed );
835
+ dom.wrapper.setAttribute( 'data-background-transition', config.backgroundTransition );
836
+
837
+ dom.controls.style.display = config.controls ? 'block' : 'none';
838
+ dom.progress.style.display = config.progress ? 'block' : 'none';
839
+
840
+ if( config.rtl ) {
841
+ dom.wrapper.classList.add( 'rtl' );
842
+ }
843
+ else {
844
+ dom.wrapper.classList.remove( 'rtl' );
845
+ }
846
+
847
+ if( config.center ) {
848
+ dom.wrapper.classList.add( 'center' );
849
+ }
850
+ else {
851
+ dom.wrapper.classList.remove( 'center' );
852
+ }
853
+
854
+ // Exit the paused mode if it was configured off
855
+ if( config.pause === false ) {
856
+ resume();
857
+ }
858
+
859
+ if( config.mouseWheel ) {
860
+ document.addEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF
861
+ document.addEventListener( 'mousewheel', onDocumentMouseScroll, false );
862
+ }
863
+ else {
864
+ document.removeEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF
865
+ document.removeEventListener( 'mousewheel', onDocumentMouseScroll, false );
866
+ }
867
+
868
+ // Rolling 3D links
869
+ if( config.rollingLinks ) {
870
+ enableRollingLinks();
871
+ }
872
+ else {
873
+ disableRollingLinks();
874
+ }
875
+
876
+ // Iframe link previews
877
+ if( config.previewLinks ) {
878
+ enablePreviewLinks();
879
+ }
880
+ else {
881
+ disablePreviewLinks();
882
+ enablePreviewLinks( '[data-preview-link]' );
883
+ }
884
+
885
+ // Remove existing auto-slide controls
886
+ if( autoSlidePlayer ) {
887
+ autoSlidePlayer.destroy();
888
+ autoSlidePlayer = null;
889
+ }
890
+
891
+ // Generate auto-slide controls if needed
892
+ if( numberOfSlides > 1 && config.autoSlide && config.autoSlideStoppable && features.canvas && features.requestAnimationFrame ) {
893
+ autoSlidePlayer = new Playback( dom.wrapper, function() {
894
+ return Math.min( Math.max( ( Date.now() - autoSlideStartTime ) / autoSlide, 0 ), 1 );
895
+ } );
896
+
897
+ autoSlidePlayer.on( 'click', onAutoSlidePlayerClick );
898
+ autoSlidePaused = false;
899
+ }
900
+
901
+ // When fragments are turned off they should be visible
902
+ if( config.fragments === false ) {
903
+ toArray( dom.slides.querySelectorAll( '.fragment' ) ).forEach( function( element ) {
904
+ element.classList.add( 'visible' );
905
+ element.classList.remove( 'current-fragment' );
906
+ } );
907
+ }
908
+
909
+ sync();
910
+
911
+ }
912
+
913
+ /**
914
+ * Binds all event listeners.
915
+ */
916
+ function addEventListeners() {
917
+
918
+ eventsAreBound = true;
919
+
920
+ window.addEventListener( 'hashchange', onWindowHashChange, false );
921
+ window.addEventListener( 'resize', onWindowResize, false );
922
+
923
+ if( config.touch ) {
924
+ dom.wrapper.addEventListener( 'touchstart', onTouchStart, false );
925
+ dom.wrapper.addEventListener( 'touchmove', onTouchMove, false );
926
+ dom.wrapper.addEventListener( 'touchend', onTouchEnd, false );
927
+
928
+ // Support pointer-style touch interaction as well
929
+ if( window.navigator.pointerEnabled ) {
930
+ // IE 11 uses un-prefixed version of pointer events
931
+ dom.wrapper.addEventListener( 'pointerdown', onPointerDown, false );
932
+ dom.wrapper.addEventListener( 'pointermove', onPointerMove, false );
933
+ dom.wrapper.addEventListener( 'pointerup', onPointerUp, false );
934
+ }
935
+ else if( window.navigator.msPointerEnabled ) {
936
+ // IE 10 uses prefixed version of pointer events
937
+ dom.wrapper.addEventListener( 'MSPointerDown', onPointerDown, false );
938
+ dom.wrapper.addEventListener( 'MSPointerMove', onPointerMove, false );
939
+ dom.wrapper.addEventListener( 'MSPointerUp', onPointerUp, false );
940
+ }
941
+ }
942
+
943
+ if( config.keyboard ) {
944
+ document.addEventListener( 'keydown', onDocumentKeyDown, false );
945
+ document.addEventListener( 'keypress', onDocumentKeyPress, false );
946
+ }
947
+
948
+ if( config.progress && dom.progress ) {
949
+ dom.progress.addEventListener( 'click', onProgressClicked, false );
950
+ }
951
+
952
+ if( config.focusBodyOnPageVisibilityChange ) {
953
+ var visibilityChange;
954
+
955
+ if( 'hidden' in document ) {
956
+ visibilityChange = 'visibilitychange';
957
+ }
958
+ else if( 'msHidden' in document ) {
959
+ visibilityChange = 'msvisibilitychange';
960
+ }
961
+ else if( 'webkitHidden' in document ) {
962
+ visibilityChange = 'webkitvisibilitychange';
963
+ }
964
+
965
+ if( visibilityChange ) {
966
+ document.addEventListener( visibilityChange, onPageVisibilityChange, false );
967
+ }
968
+ }
969
+
970
+ // Listen to both touch and click events, in case the device
971
+ // supports both
972
+ var pointerEvents = [ 'touchstart', 'click' ];
973
+
974
+ // Only support touch for Android, fixes double navigations in
975
+ // stock browser
976
+ if( navigator.userAgent.match( /android/gi ) ) {
977
+ pointerEvents = [ 'touchstart' ];
978
+ }
979
+
980
+ pointerEvents.forEach( function( eventName ) {
981
+ dom.controlsLeft.forEach( function( el ) { el.addEventListener( eventName, onNavigateLeftClicked, false ); } );
982
+ dom.controlsRight.forEach( function( el ) { el.addEventListener( eventName, onNavigateRightClicked, false ); } );
983
+ dom.controlsUp.forEach( function( el ) { el.addEventListener( eventName, onNavigateUpClicked, false ); } );
984
+ dom.controlsDown.forEach( function( el ) { el.addEventListener( eventName, onNavigateDownClicked, false ); } );
985
+ dom.controlsPrev.forEach( function( el ) { el.addEventListener( eventName, onNavigatePrevClicked, false ); } );
986
+ dom.controlsNext.forEach( function( el ) { el.addEventListener( eventName, onNavigateNextClicked, false ); } );
987
+ } );
988
+
989
+ }
990
+
991
+ /**
992
+ * Unbinds all event listeners.
993
+ */
994
+ function removeEventListeners() {
995
+
996
+ eventsAreBound = false;
997
+
998
+ document.removeEventListener( 'keydown', onDocumentKeyDown, false );
999
+ document.removeEventListener( 'keypress', onDocumentKeyPress, false );
1000
+ window.removeEventListener( 'hashchange', onWindowHashChange, false );
1001
+ window.removeEventListener( 'resize', onWindowResize, false );
1002
+
1003
+ dom.wrapper.removeEventListener( 'touchstart', onTouchStart, false );
1004
+ dom.wrapper.removeEventListener( 'touchmove', onTouchMove, false );
1005
+ dom.wrapper.removeEventListener( 'touchend', onTouchEnd, false );
1006
+
1007
+ // IE11
1008
+ if( window.navigator.pointerEnabled ) {
1009
+ dom.wrapper.removeEventListener( 'pointerdown', onPointerDown, false );
1010
+ dom.wrapper.removeEventListener( 'pointermove', onPointerMove, false );
1011
+ dom.wrapper.removeEventListener( 'pointerup', onPointerUp, false );
1012
+ }
1013
+ // IE10
1014
+ else if( window.navigator.msPointerEnabled ) {
1015
+ dom.wrapper.removeEventListener( 'MSPointerDown', onPointerDown, false );
1016
+ dom.wrapper.removeEventListener( 'MSPointerMove', onPointerMove, false );
1017
+ dom.wrapper.removeEventListener( 'MSPointerUp', onPointerUp, false );
1018
+ }
1019
+
1020
+ if ( config.progress && dom.progress ) {
1021
+ dom.progress.removeEventListener( 'click', onProgressClicked, false );
1022
+ }
1023
+
1024
+ [ 'touchstart', 'click' ].forEach( function( eventName ) {
1025
+ dom.controlsLeft.forEach( function( el ) { el.removeEventListener( eventName, onNavigateLeftClicked, false ); } );
1026
+ dom.controlsRight.forEach( function( el ) { el.removeEventListener( eventName, onNavigateRightClicked, false ); } );
1027
+ dom.controlsUp.forEach( function( el ) { el.removeEventListener( eventName, onNavigateUpClicked, false ); } );
1028
+ dom.controlsDown.forEach( function( el ) { el.removeEventListener( eventName, onNavigateDownClicked, false ); } );
1029
+ dom.controlsPrev.forEach( function( el ) { el.removeEventListener( eventName, onNavigatePrevClicked, false ); } );
1030
+ dom.controlsNext.forEach( function( el ) { el.removeEventListener( eventName, onNavigateNextClicked, false ); } );
1031
+ } );
1032
+
1033
+ }
1034
+
1035
+ /**
1036
+ * Extend object a with the properties of object b.
1037
+ * If there's a conflict, object b takes precedence.
1038
+ */
1039
+ function extend( a, b ) {
1040
+
1041
+ for( var i in b ) {
1042
+ a[ i ] = b[ i ];
1043
+ }
1044
+
1045
+ }
1046
+
1047
+ /**
1048
+ * Converts the target object to an array.
1049
+ */
1050
+ function toArray( o ) {
1051
+
1052
+ return Array.prototype.slice.call( o );
1053
+
1054
+ }
1055
+
1056
+ /**
1057
+ * Utility for deserializing a value.
1058
+ */
1059
+ function deserialize( value ) {
1060
+
1061
+ if( typeof value === 'string' ) {
1062
+ if( value === 'null' ) return null;
1063
+ else if( value === 'true' ) return true;
1064
+ else if( value === 'false' ) return false;
1065
+ else if( value.match( /^\d+$/ ) ) return parseFloat( value );
1066
+ }
1067
+
1068
+ return value;
1069
+
1070
+ }
1071
+
1072
+ /**
1073
+ * Measures the distance in pixels between point a
1074
+ * and point b.
1075
+ *
1076
+ * @param {Object} a point with x/y properties
1077
+ * @param {Object} b point with x/y properties
1078
+ */
1079
+ function distanceBetween( a, b ) {
1080
+
1081
+ var dx = a.x - b.x,
1082
+ dy = a.y - b.y;
1083
+
1084
+ return Math.sqrt( dx*dx + dy*dy );
1085
+
1086
+ }
1087
+
1088
+ /**
1089
+ * Applies a CSS transform to the target element.
1090
+ */
1091
+ function transformElement( element, transform ) {
1092
+
1093
+ element.style.WebkitTransform = transform;
1094
+ element.style.MozTransform = transform;
1095
+ element.style.msTransform = transform;
1096
+ element.style.transform = transform;
1097
+
1098
+ }
1099
+
1100
+ /**
1101
+ * Applies CSS transforms to the slides container. The container
1102
+ * is transformed from two separate sources: layout and the overview
1103
+ * mode.
1104
+ */
1105
+ function transformSlides( transforms ) {
1106
+
1107
+ // Pick up new transforms from arguments
1108
+ if( typeof transforms.layout === 'string' ) slidesTransform.layout = transforms.layout;
1109
+ if( typeof transforms.overview === 'string' ) slidesTransform.overview = transforms.overview;
1110
+
1111
+ // Apply the transforms to the slides container
1112
+ if( slidesTransform.layout ) {
1113
+ transformElement( dom.slides, slidesTransform.layout + ' ' + slidesTransform.overview );
1114
+ }
1115
+ else {
1116
+ transformElement( dom.slides, slidesTransform.overview );
1117
+ }
1118
+
1119
+ }
1120
+
1121
+ /**
1122
+ * Injects the given CSS styles into the DOM.
1123
+ */
1124
+ function injectStyleSheet( value ) {
1125
+
1126
+ var tag = document.createElement( 'style' );
1127
+ tag.type = 'text/css';
1128
+ if( tag.styleSheet ) {
1129
+ tag.styleSheet.cssText = value;
1130
+ }
1131
+ else {
1132
+ tag.appendChild( document.createTextNode( value ) );
1133
+ }
1134
+ document.getElementsByTagName( 'head' )[0].appendChild( tag );
1135
+
1136
+ }
1137
+
1138
+ /**
1139
+ * Converts various color input formats to an {r:0,g:0,b:0} object.
1140
+ *
1141
+ * @param {String} color The string representation of a color,
1142
+ * the following formats are supported:
1143
+ * - #000
1144
+ * - #000000
1145
+ * - rgb(0,0,0)
1146
+ */
1147
+ function colorToRgb( color ) {
1148
+
1149
+ var hex3 = color.match( /^#([0-9a-f]{3})$/i );
1150
+ if( hex3 && hex3[1] ) {
1151
+ hex3 = hex3[1];
1152
+ return {
1153
+ r: parseInt( hex3.charAt( 0 ), 16 ) * 0x11,
1154
+ g: parseInt( hex3.charAt( 1 ), 16 ) * 0x11,
1155
+ b: parseInt( hex3.charAt( 2 ), 16 ) * 0x11
1156
+ };
1157
+ }
1158
+
1159
+ var hex6 = color.match( /^#([0-9a-f]{6})$/i );
1160
+ if( hex6 && hex6[1] ) {
1161
+ hex6 = hex6[1];
1162
+ return {
1163
+ r: parseInt( hex6.substr( 0, 2 ), 16 ),
1164
+ g: parseInt( hex6.substr( 2, 2 ), 16 ),
1165
+ b: parseInt( hex6.substr( 4, 2 ), 16 )
1166
+ };
1167
+ }
1168
+
1169
+ var rgb = color.match( /^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i );
1170
+ if( rgb ) {
1171
+ return {
1172
+ r: parseInt( rgb[1], 10 ),
1173
+ g: parseInt( rgb[2], 10 ),
1174
+ b: parseInt( rgb[3], 10 )
1175
+ };
1176
+ }
1177
+
1178
+ var rgba = color.match( /^rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\,\s*([\d]+|[\d]*.[\d]+)\s*\)$/i );
1179
+ if( rgba ) {
1180
+ return {
1181
+ r: parseInt( rgba[1], 10 ),
1182
+ g: parseInt( rgba[2], 10 ),
1183
+ b: parseInt( rgba[3], 10 ),
1184
+ a: parseFloat( rgba[4] )
1185
+ };
1186
+ }
1187
+
1188
+ return null;
1189
+
1190
+ }
1191
+
1192
+ /**
1193
+ * Calculates brightness on a scale of 0-255.
1194
+ *
1195
+ * @param color See colorStringToRgb for supported formats.
1196
+ */
1197
+ function colorBrightness( color ) {
1198
+
1199
+ if( typeof color === 'string' ) color = colorToRgb( color );
1200
+
1201
+ if( color ) {
1202
+ return ( color.r * 299 + color.g * 587 + color.b * 114 ) / 1000;
1203
+ }
1204
+
1205
+ return null;
1206
+
1207
+ }
1208
+
1209
+ /**
1210
+ * Retrieves the height of the given element by looking
1211
+ * at the position and height of its immediate children.
1212
+ */
1213
+ function getAbsoluteHeight( element ) {
1214
+
1215
+ var height = 0;
1216
+
1217
+ if( element ) {
1218
+ var absoluteChildren = 0;
1219
+
1220
+ toArray( element.childNodes ).forEach( function( child ) {
1221
+
1222
+ if( typeof child.offsetTop === 'number' && child.style ) {
1223
+ // Count # of abs children
1224
+ if( window.getComputedStyle( child ).position === 'absolute' ) {
1225
+ absoluteChildren += 1;
1226
+ }
1227
+
1228
+ height = Math.max( height, child.offsetTop + child.offsetHeight );
1229
+ }
1230
+
1231
+ } );
1232
+
1233
+ // If there are no absolute children, use offsetHeight
1234
+ if( absoluteChildren === 0 ) {
1235
+ height = element.offsetHeight;
1236
+ }
1237
+
1238
+ }
1239
+
1240
+ return height;
1241
+
1242
+ }
1243
+
1244
+ /**
1245
+ * Returns the remaining height within the parent of the
1246
+ * target element.
1247
+ *
1248
+ * remaining height = [ configured parent height ] - [ current parent height ]
1249
+ */
1250
+ function getRemainingHeight( element, height ) {
1251
+
1252
+ height = height || 0;
1253
+
1254
+ if( element ) {
1255
+ var newHeight, oldHeight = element.style.height;
1256
+
1257
+ // Change the .stretch element height to 0 in order find the height of all
1258
+ // the other elements
1259
+ element.style.height = '0px';
1260
+ newHeight = height - element.parentNode.offsetHeight;
1261
+
1262
+ // Restore the old height, just in case
1263
+ element.style.height = oldHeight + 'px';
1264
+
1265
+ return newHeight;
1266
+ }
1267
+
1268
+ return height;
1269
+
1270
+ }
1271
+
1272
+ /**
1273
+ * Checks if this instance is being used to print a PDF.
1274
+ */
1275
+ function isPrintingPDF() {
1276
+
1277
+ return ( /print-pdf/gi ).test( window.location.search );
1278
+
1279
+ }
1280
+
1281
+ /**
1282
+ * Hides the address bar if we're on a mobile device.
1283
+ */
1284
+ function hideAddressBar() {
1285
+
1286
+ if( config.hideAddressBar && isMobileDevice ) {
1287
+ // Events that should trigger the address bar to hide
1288
+ window.addEventListener( 'load', removeAddressBar, false );
1289
+ window.addEventListener( 'orientationchange', removeAddressBar, false );
1290
+ }
1291
+
1292
+ }
1293
+
1294
+ /**
1295
+ * Causes the address bar to hide on mobile devices,
1296
+ * more vertical space ftw.
1297
+ */
1298
+ function removeAddressBar() {
1299
+
1300
+ setTimeout( function() {
1301
+ window.scrollTo( 0, 1 );
1302
+ }, 10 );
1303
+
1304
+ }
1305
+
1306
+ /**
1307
+ * Dispatches an event of the specified type from the
1308
+ * reveal DOM element.
1309
+ */
1310
+ function dispatchEvent( type, args ) {
1311
+
1312
+ var event = document.createEvent( 'HTMLEvents', 1, 2 );
1313
+ event.initEvent( type, true, true );
1314
+ extend( event, args );
1315
+ dom.wrapper.dispatchEvent( event );
1316
+
1317
+ // If we're in an iframe, post each reveal.js event to the
1318
+ // parent window. Used by the notes plugin
1319
+ if( config.postMessageEvents && window.parent !== window.self ) {
1320
+ window.parent.postMessage( JSON.stringify({ namespace: 'reveal', eventName: type, state: getState() }), '*' );
1321
+ }
1322
+
1323
+ }
1324
+
1325
+ /**
1326
+ * Wrap all links in 3D goodness.
1327
+ */
1328
+ function enableRollingLinks() {
1329
+
1330
+ if( features.transforms3d && !( 'msPerspective' in document.body.style ) ) {
1331
+ var anchors = dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ' a' );
1332
+
1333
+ for( var i = 0, len = anchors.length; i < len; i++ ) {
1334
+ var anchor = anchors[i];
1335
+
1336
+ if( anchor.textContent && !anchor.querySelector( '*' ) && ( !anchor.className || !anchor.classList.contains( anchor, 'roll' ) ) ) {
1337
+ var span = document.createElement('span');
1338
+ span.setAttribute('data-title', anchor.text);
1339
+ span.innerHTML = anchor.innerHTML;
1340
+
1341
+ anchor.classList.add( 'roll' );
1342
+ anchor.innerHTML = '';
1343
+ anchor.appendChild(span);
1344
+ }
1345
+ }
1346
+ }
1347
+
1348
+ }
1349
+
1350
+ /**
1351
+ * Unwrap all 3D links.
1352
+ */
1353
+ function disableRollingLinks() {
1354
+
1355
+ var anchors = dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ' a.roll' );
1356
+
1357
+ for( var i = 0, len = anchors.length; i < len; i++ ) {
1358
+ var anchor = anchors[i];
1359
+ var span = anchor.querySelector( 'span' );
1360
+
1361
+ if( span ) {
1362
+ anchor.classList.remove( 'roll' );
1363
+ anchor.innerHTML = span.innerHTML;
1364
+ }
1365
+ }
1366
+
1367
+ }
1368
+
1369
+ /**
1370
+ * Bind preview frame links.
1371
+ */
1372
+ function enablePreviewLinks( selector ) {
1373
+
1374
+ var anchors = toArray( document.querySelectorAll( selector ? selector : 'a' ) );
1375
+
1376
+ anchors.forEach( function( element ) {
1377
+ if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) {
1378
+ element.addEventListener( 'click', onPreviewLinkClicked, false );
1379
+ }
1380
+ } );
1381
+
1382
+ }
1383
+
1384
+ /**
1385
+ * Unbind preview frame links.
1386
+ */
1387
+ function disablePreviewLinks() {
1388
+
1389
+ var anchors = toArray( document.querySelectorAll( 'a' ) );
1390
+
1391
+ anchors.forEach( function( element ) {
1392
+ if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) {
1393
+ element.removeEventListener( 'click', onPreviewLinkClicked, false );
1394
+ }
1395
+ } );
1396
+
1397
+ }
1398
+
1399
+ /**
1400
+ * Opens a preview window for the target URL.
1401
+ */
1402
+ function showPreview( url ) {
1403
+
1404
+ closeOverlay();
1405
+
1406
+ dom.overlay = document.createElement( 'div' );
1407
+ dom.overlay.classList.add( 'overlay' );
1408
+ dom.overlay.classList.add( 'overlay-preview' );
1409
+ dom.wrapper.appendChild( dom.overlay );
1410
+
1411
+ dom.overlay.innerHTML = [
1412
+ '<header>',
1413
+ '<a class="close" href="#"><span class="icon"></span></a>',
1414
+ '<a class="external" href="'+ url +'" target="_blank"><span class="icon"></span></a>',
1415
+ '</header>',
1416
+ '<div class="spinner"></div>',
1417
+ '<div class="viewport">',
1418
+ '<iframe src="'+ url +'"></iframe>',
1419
+ '</div>'
1420
+ ].join('');
1421
+
1422
+ dom.overlay.querySelector( 'iframe' ).addEventListener( 'load', function( event ) {
1423
+ dom.overlay.classList.add( 'loaded' );
1424
+ }, false );
1425
+
1426
+ dom.overlay.querySelector( '.close' ).addEventListener( 'click', function( event ) {
1427
+ closeOverlay();
1428
+ event.preventDefault();
1429
+ }, false );
1430
+
1431
+ dom.overlay.querySelector( '.external' ).addEventListener( 'click', function( event ) {
1432
+ closeOverlay();
1433
+ }, false );
1434
+
1435
+ setTimeout( function() {
1436
+ dom.overlay.classList.add( 'visible' );
1437
+ }, 1 );
1438
+
1439
+ }
1440
+
1441
+ /**
1442
+ * Opens a overlay window with help material.
1443
+ */
1444
+ function showHelp() {
1445
+
1446
+ if( config.help ) {
1447
+
1448
+ closeOverlay();
1449
+
1450
+ dom.overlay = document.createElement( 'div' );
1451
+ dom.overlay.classList.add( 'overlay' );
1452
+ dom.overlay.classList.add( 'overlay-help' );
1453
+ dom.wrapper.appendChild( dom.overlay );
1454
+
1455
+ var html = '<p class="title">Keyboard Shortcuts</p><br/>';
1456
+
1457
+ html += '<table><th>KEY</th><th>ACTION</th>';
1458
+ for( var key in keyboardShortcuts ) {
1459
+ html += '<tr><td>' + key + '</td><td>' + keyboardShortcuts[ key ] + '</td></tr>';
1460
+ }
1461
+
1462
+ html += '</table>';
1463
+
1464
+ dom.overlay.innerHTML = [
1465
+ '<header>',
1466
+ '<a class="close" href="#"><span class="icon"></span></a>',
1467
+ '</header>',
1468
+ '<div class="viewport">',
1469
+ '<div class="viewport-inner">'+ html +'</div>',
1470
+ '</div>'
1471
+ ].join('');
1472
+
1473
+ dom.overlay.querySelector( '.close' ).addEventListener( 'click', function( event ) {
1474
+ closeOverlay();
1475
+ event.preventDefault();
1476
+ }, false );
1477
+
1478
+ setTimeout( function() {
1479
+ dom.overlay.classList.add( 'visible' );
1480
+ }, 1 );
1481
+
1482
+ }
1483
+
1484
+ }
1485
+
1486
+ /**
1487
+ * Closes any currently open overlay.
1488
+ */
1489
+ function closeOverlay() {
1490
+
1491
+ if( dom.overlay ) {
1492
+ dom.overlay.parentNode.removeChild( dom.overlay );
1493
+ dom.overlay = null;
1494
+ }
1495
+
1496
+ }
1497
+
1498
+ /**
1499
+ * Applies JavaScript-controlled layout rules to the
1500
+ * presentation.
1501
+ */
1502
+ function layout() {
1503
+
1504
+ if( dom.wrapper && !isPrintingPDF() ) {
1505
+
1506
+ var size = getComputedSlideSize();
1507
+
1508
+ var slidePadding = 20; // TODO Dig this out of DOM
1509
+
1510
+ // Layout the contents of the slides
1511
+ layoutSlideContents( config.width, config.height, slidePadding );
1512
+
1513
+ dom.slides.style.width = size.width + 'px';
1514
+ dom.slides.style.height = size.height + 'px';
1515
+
1516
+ // Determine scale of content to fit within available space
1517
+ scale = Math.min( size.presentationWidth / size.width, size.presentationHeight / size.height );
1518
+
1519
+ // Respect max/min scale settings
1520
+ scale = Math.max( scale, config.minScale );
1521
+ scale = Math.min( scale, config.maxScale );
1522
+
1523
+ // Don't apply any scaling styles if scale is 1
1524
+ if( scale === 1 ) {
1525
+ dom.slides.style.zoom = '';
1526
+ dom.slides.style.left = '';
1527
+ dom.slides.style.top = '';
1528
+ dom.slides.style.bottom = '';
1529
+ dom.slides.style.right = '';
1530
+ transformSlides( { layout: '' } );
1531
+ }
1532
+ else {
1533
+ // Prefer zooming in desktop Chrome so that content remains crisp
1534
+ if( !isMobileDevice && /chrome/i.test( navigator.userAgent ) && typeof dom.slides.style.zoom !== 'undefined' ) {
1535
+ dom.slides.style.zoom = scale;
1536
+ transformSlides( { layout: '' } );
1537
+ }
1538
+ // Apply scale transform as a fallback
1539
+ else {
1540
+ dom.slides.style.left = '50%';
1541
+ dom.slides.style.top = '50%';
1542
+ dom.slides.style.bottom = 'auto';
1543
+ dom.slides.style.right = 'auto';
1544
+ transformSlides( { layout: 'translate(-50%, -50%) scale('+ scale +')' } );
1545
+ }
1546
+ }
1547
+
1548
+ // Select all slides, vertical and horizontal
1549
+ var slides = toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) );
1550
+
1551
+ for( var i = 0, len = slides.length; i < len; i++ ) {
1552
+ var slide = slides[ i ];
1553
+
1554
+ // Don't bother updating invisible slides
1555
+ if( slide.style.display === 'none' ) {
1556
+ continue;
1557
+ }
1558
+
1559
+ if( config.center || slide.classList.contains( 'center' ) ) {
1560
+ // Vertical stacks are not centred since their section
1561
+ // children will be
1562
+ if( slide.classList.contains( 'stack' ) ) {
1563
+ slide.style.top = 0;
1564
+ }
1565
+ else {
1566
+ slide.style.top = Math.max( ( ( size.height - getAbsoluteHeight( slide ) ) / 2 ) - slidePadding, 0 ) + 'px';
1567
+ }
1568
+ }
1569
+ else {
1570
+ slide.style.top = '';
1571
+ }
1572
+
1573
+ }
1574
+
1575
+ updateProgress();
1576
+ updateParallax();
1577
+
1578
+ }
1579
+
1580
+ }
1581
+
1582
+ /**
1583
+ * Applies layout logic to the contents of all slides in
1584
+ * the presentation.
1585
+ */
1586
+ function layoutSlideContents( width, height, padding ) {
1587
+
1588
+ // Handle sizing of elements with the 'stretch' class
1589
+ toArray( dom.slides.querySelectorAll( 'section > .stretch' ) ).forEach( function( element ) {
1590
+
1591
+ // Determine how much vertical space we can use
1592
+ var remainingHeight = getRemainingHeight( element, height );
1593
+
1594
+ // Consider the aspect ratio of media elements
1595
+ if( /(img|video)/gi.test( element.nodeName ) ) {
1596
+ var nw = element.naturalWidth || element.videoWidth,
1597
+ nh = element.naturalHeight || element.videoHeight;
1598
+
1599
+ var es = Math.min( width / nw, remainingHeight / nh );
1600
+
1601
+ element.style.width = ( nw * es ) + 'px';
1602
+ element.style.height = ( nh * es ) + 'px';
1603
+
1604
+ }
1605
+ else {
1606
+ element.style.width = width + 'px';
1607
+ element.style.height = remainingHeight + 'px';
1608
+ }
1609
+
1610
+ } );
1611
+
1612
+ }
1613
+
1614
+ /**
1615
+ * Calculates the computed pixel size of our slides. These
1616
+ * values are based on the width and height configuration
1617
+ * options.
1618
+ */
1619
+ function getComputedSlideSize( presentationWidth, presentationHeight ) {
1620
+
1621
+ var size = {
1622
+ // Slide size
1623
+ width: config.width,
1624
+ height: config.height,
1625
+
1626
+ // Presentation size
1627
+ presentationWidth: presentationWidth || dom.wrapper.offsetWidth,
1628
+ presentationHeight: presentationHeight || dom.wrapper.offsetHeight
1629
+ };
1630
+
1631
+ // Reduce available space by margin
1632
+ size.presentationWidth -= ( size.presentationWidth * config.margin );
1633
+ size.presentationHeight -= ( size.presentationHeight * config.margin );
1634
+
1635
+ // Slide width may be a percentage of available width
1636
+ if( typeof size.width === 'string' && /%$/.test( size.width ) ) {
1637
+ size.width = parseInt( size.width, 10 ) / 100 * size.presentationWidth;
1638
+ }
1639
+
1640
+ // Slide height may be a percentage of available height
1641
+ if( typeof size.height === 'string' && /%$/.test( size.height ) ) {
1642
+ size.height = parseInt( size.height, 10 ) / 100 * size.presentationHeight;
1643
+ }
1644
+
1645
+ return size;
1646
+
1647
+ }
1648
+
1649
+ /**
1650
+ * Stores the vertical index of a stack so that the same
1651
+ * vertical slide can be selected when navigating to and
1652
+ * from the stack.
1653
+ *
1654
+ * @param {HTMLElement} stack The vertical stack element
1655
+ * @param {int} v Index to memorize
1656
+ */
1657
+ function setPreviousVerticalIndex( stack, v ) {
1658
+
1659
+ if( typeof stack === 'object' && typeof stack.setAttribute === 'function' ) {
1660
+ stack.setAttribute( 'data-previous-indexv', v || 0 );
1661
+ }
1662
+
1663
+ }
1664
+
1665
+ /**
1666
+ * Retrieves the vertical index which was stored using
1667
+ * #setPreviousVerticalIndex() or 0 if no previous index
1668
+ * exists.
1669
+ *
1670
+ * @param {HTMLElement} stack The vertical stack element
1671
+ */
1672
+ function getPreviousVerticalIndex( stack ) {
1673
+
1674
+ if( typeof stack === 'object' && typeof stack.setAttribute === 'function' && stack.classList.contains( 'stack' ) ) {
1675
+ // Prefer manually defined start-indexv
1676
+ var attributeName = stack.hasAttribute( 'data-start-indexv' ) ? 'data-start-indexv' : 'data-previous-indexv';
1677
+
1678
+ return parseInt( stack.getAttribute( attributeName ) || 0, 10 );
1679
+ }
1680
+
1681
+ return 0;
1682
+
1683
+ }
1684
+
1685
+ /**
1686
+ * Displays the overview of slides (quick nav) by scaling
1687
+ * down and arranging all slide elements.
1688
+ */
1689
+ function activateOverview() {
1690
+
1691
+ // Only proceed if enabled in config
1692
+ if( config.overview && !isOverview() ) {
1693
+
1694
+ overview = true;
1695
+
1696
+ dom.wrapper.classList.add( 'overview' );
1697
+ dom.wrapper.classList.remove( 'overview-deactivating' );
1698
+
1699
+ if( features.overviewTransitions ) {
1700
+ setTimeout( function() {
1701
+ dom.wrapper.classList.add( 'overview-animated' );
1702
+ }, 1 );
1703
+ }
1704
+
1705
+ // Don't auto-slide while in overview mode
1706
+ cancelAutoSlide();
1707
+
1708
+ // Move the backgrounds element into the slide container to
1709
+ // that the same scaling is applied
1710
+ dom.slides.appendChild( dom.background );
1711
+
1712
+ // Clicking on an overview slide navigates to it
1713
+ toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
1714
+ if( !slide.classList.contains( 'stack' ) ) {
1715
+ slide.addEventListener( 'click', onOverviewSlideClicked, true );
1716
+ }
1717
+ } );
1718
+
1719
+ updateSlidesVisibility();
1720
+ layoutOverview();
1721
+ updateOverview();
1722
+
1723
+ layout();
1724
+
1725
+ // Notify observers of the overview showing
1726
+ dispatchEvent( 'overviewshown', {
1727
+ 'indexh': indexh,
1728
+ 'indexv': indexv,
1729
+ 'currentSlide': currentSlide
1730
+ } );
1731
+
1732
+ }
1733
+
1734
+ }
1735
+
1736
+ /**
1737
+ * Uses CSS transforms to position all slides in a grid for
1738
+ * display inside of the overview mode.
1739
+ */
1740
+ function layoutOverview() {
1741
+
1742
+ var margin = 70;
1743
+ var slideWidth = config.width + margin,
1744
+ slideHeight = config.height + margin;
1745
+
1746
+ // Reverse in RTL mode
1747
+ if( config.rtl ) {
1748
+ slideWidth = -slideWidth;
1749
+ }
1750
+
1751
+ // Layout slides
1752
+ toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( hslide, h ) {
1753
+ hslide.setAttribute( 'data-index-h', h );
1754
+ transformElement( hslide, 'translate3d(' + ( h * slideWidth ) + 'px, 0, 0)' );
1755
+
1756
+ if( hslide.classList.contains( 'stack' ) ) {
1757
+
1758
+ toArray( hslide.querySelectorAll( 'section' ) ).forEach( function( vslide, v ) {
1759
+ vslide.setAttribute( 'data-index-h', h );
1760
+ vslide.setAttribute( 'data-index-v', v );
1761
+
1762
+ transformElement( vslide, 'translate3d(0, ' + ( v * slideHeight ) + 'px, 0)' );
1763
+ } );
1764
+
1765
+ }
1766
+ } );
1767
+
1768
+ // Layout slide backgrounds
1769
+ toArray( dom.background.childNodes ).forEach( function( hbackground, h ) {
1770
+ transformElement( hbackground, 'translate3d(' + ( h * slideWidth ) + 'px, 0, 0)' );
1771
+
1772
+ toArray( hbackground.querySelectorAll( '.slide-background' ) ).forEach( function( vbackground, v ) {
1773
+ transformElement( vbackground, 'translate3d(0, ' + ( v * slideHeight ) + 'px, 0)' );
1774
+ } );
1775
+ } );
1776
+
1777
+ }
1778
+
1779
+ /**
1780
+ * Moves the overview viewport to the current slides.
1781
+ * Called each time the current slide changes.
1782
+ */
1783
+ function updateOverview() {
1784
+
1785
+ var margin = 70;
1786
+ var slideWidth = config.width + margin,
1787
+ slideHeight = config.height + margin;
1788
+
1789
+ // Reverse in RTL mode
1790
+ if( config.rtl ) {
1791
+ slideWidth = -slideWidth;
1792
+ }
1793
+
1794
+ transformSlides( {
1795
+ overview: [
1796
+ 'translateX('+ ( -indexh * slideWidth ) +'px)',
1797
+ 'translateY('+ ( -indexv * slideHeight ) +'px)',
1798
+ 'translateZ('+ ( window.innerWidth < 400 ? -1000 : -2500 ) +'px)'
1799
+ ].join( ' ' )
1800
+ } );
1801
+
1802
+ }
1803
+
1804
+ /**
1805
+ * Exits the slide overview and enters the currently
1806
+ * active slide.
1807
+ */
1808
+ function deactivateOverview() {
1809
+
1810
+ // Only proceed if enabled in config
1811
+ if( config.overview ) {
1812
+
1813
+ overview = false;
1814
+
1815
+ dom.wrapper.classList.remove( 'overview' );
1816
+ dom.wrapper.classList.remove( 'overview-animated' );
1817
+
1818
+ // Temporarily add a class so that transitions can do different things
1819
+ // depending on whether they are exiting/entering overview, or just
1820
+ // moving from slide to slide
1821
+ dom.wrapper.classList.add( 'overview-deactivating' );
1822
+
1823
+ setTimeout( function () {
1824
+ dom.wrapper.classList.remove( 'overview-deactivating' );
1825
+ }, 1 );
1826
+
1827
+ // Move the background element back out
1828
+ dom.wrapper.appendChild( dom.background );
1829
+
1830
+ // Clean up changes made to slides
1831
+ toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
1832
+ transformElement( slide, '' );
1833
+
1834
+ slide.removeEventListener( 'click', onOverviewSlideClicked, true );
1835
+ } );
1836
+
1837
+ // Clean up changes made to backgrounds
1838
+ toArray( dom.background.querySelectorAll( '.slide-background' ) ).forEach( function( background ) {
1839
+ transformElement( background, '' );
1840
+ } );
1841
+
1842
+ transformSlides( { overview: '' } );
1843
+
1844
+ slide( indexh, indexv );
1845
+
1846
+ layout();
1847
+
1848
+ cueAutoSlide();
1849
+
1850
+ // Notify observers of the overview hiding
1851
+ dispatchEvent( 'overviewhidden', {
1852
+ 'indexh': indexh,
1853
+ 'indexv': indexv,
1854
+ 'currentSlide': currentSlide
1855
+ } );
1856
+
1857
+ }
1858
+ }
1859
+
1860
+ /**
1861
+ * Toggles the slide overview mode on and off.
1862
+ *
1863
+ * @param {Boolean} override Optional flag which overrides the
1864
+ * toggle logic and forcibly sets the desired state. True means
1865
+ * overview is open, false means it's closed.
1866
+ */
1867
+ function toggleOverview( override ) {
1868
+
1869
+ if( typeof override === 'boolean' ) {
1870
+ override ? activateOverview() : deactivateOverview();
1871
+ }
1872
+ else {
1873
+ isOverview() ? deactivateOverview() : activateOverview();
1874
+ }
1875
+
1876
+ }
1877
+
1878
+ /**
1879
+ * Checks if the overview is currently active.
1880
+ *
1881
+ * @return {Boolean} true if the overview is active,
1882
+ * false otherwise
1883
+ */
1884
+ function isOverview() {
1885
+
1886
+ return overview;
1887
+
1888
+ }
1889
+
1890
+ /**
1891
+ * Checks if the current or specified slide is vertical
1892
+ * (nested within another slide).
1893
+ *
1894
+ * @param {HTMLElement} slide [optional] The slide to check
1895
+ * orientation of
1896
+ */
1897
+ function isVerticalSlide( slide ) {
1898
+
1899
+ // Prefer slide argument, otherwise use current slide
1900
+ slide = slide ? slide : currentSlide;
1901
+
1902
+ return slide && slide.parentNode && !!slide.parentNode.nodeName.match( /section/i );
1903
+
1904
+ }
1905
+
1906
+ /**
1907
+ * Handling the fullscreen functionality via the fullscreen API
1908
+ *
1909
+ * @see http://fullscreen.spec.whatwg.org/
1910
+ * @see https://developer.mozilla.org/en-US/docs/DOM/Using_fullscreen_mode
1911
+ */
1912
+ function enterFullscreen() {
1913
+
1914
+ var element = document.body;
1915
+
1916
+ // Check which implementation is available
1917
+ var requestMethod = element.requestFullScreen ||
1918
+ element.webkitRequestFullscreen ||
1919
+ element.webkitRequestFullScreen ||
1920
+ element.mozRequestFullScreen ||
1921
+ element.msRequestFullscreen;
1922
+
1923
+ if( requestMethod ) {
1924
+ requestMethod.apply( element );
1925
+ }
1926
+
1927
+ }
1928
+
1929
+ /**
1930
+ * Enters the paused mode which fades everything on screen to
1931
+ * black.
1932
+ */
1933
+ function pause() {
1934
+
1935
+ if( config.pause ) {
1936
+ var wasPaused = dom.wrapper.classList.contains( 'paused' );
1937
+
1938
+ cancelAutoSlide();
1939
+ dom.wrapper.classList.add( 'paused' );
1940
+
1941
+ if( wasPaused === false ) {
1942
+ dispatchEvent( 'paused' );
1943
+ }
1944
+ }
1945
+
1946
+ }
1947
+
1948
+ /**
1949
+ * Exits from the paused mode.
1950
+ */
1951
+ function resume() {
1952
+
1953
+ var wasPaused = dom.wrapper.classList.contains( 'paused' );
1954
+ dom.wrapper.classList.remove( 'paused' );
1955
+
1956
+ cueAutoSlide();
1957
+
1958
+ if( wasPaused ) {
1959
+ dispatchEvent( 'resumed' );
1960
+ }
1961
+
1962
+ }
1963
+
1964
+ /**
1965
+ * Toggles the paused mode on and off.
1966
+ */
1967
+ function togglePause( override ) {
1968
+
1969
+ if( typeof override === 'boolean' ) {
1970
+ override ? pause() : resume();
1971
+ }
1972
+ else {
1973
+ isPaused() ? resume() : pause();
1974
+ }
1975
+
1976
+ }
1977
+
1978
+ /**
1979
+ * Checks if we are currently in the paused mode.
1980
+ */
1981
+ function isPaused() {
1982
+
1983
+ return dom.wrapper.classList.contains( 'paused' );
1984
+
1985
+ }
1986
+
1987
+ /**
1988
+ * Toggles the auto slide mode on and off.
1989
+ *
1990
+ * @param {Boolean} override Optional flag which sets the desired state.
1991
+ * True means autoplay starts, false means it stops.
1992
+ */
1993
+
1994
+ function toggleAutoSlide( override ) {
1995
+
1996
+ if( typeof override === 'boolean' ) {
1997
+ override ? resumeAutoSlide() : pauseAutoSlide();
1998
+ }
1999
+
2000
+ else {
2001
+ autoSlidePaused ? resumeAutoSlide() : pauseAutoSlide();
2002
+ }
2003
+
2004
+ }
2005
+
2006
+ /**
2007
+ * Checks if the auto slide mode is currently on.
2008
+ */
2009
+ function isAutoSliding() {
2010
+
2011
+ return !!( autoSlide && !autoSlidePaused );
2012
+
2013
+ }
2014
+
2015
+ /**
2016
+ * Steps from the current point in the presentation to the
2017
+ * slide which matches the specified horizontal and vertical
2018
+ * indices.
2019
+ *
2020
+ * @param {int} h Horizontal index of the target slide
2021
+ * @param {int} v Vertical index of the target slide
2022
+ * @param {int} f Optional index of a fragment within the
2023
+ * target slide to activate
2024
+ * @param {int} o Optional origin for use in multimaster environments
2025
+ */
2026
+ function slide( h, v, f, o ) {
2027
+
2028
+ // Remember where we were at before
2029
+ previousSlide = currentSlide;
2030
+
2031
+ // Query all horizontal slides in the deck
2032
+ var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR );
2033
+
2034
+ // If no vertical index is specified and the upcoming slide is a
2035
+ // stack, resume at its previous vertical index
2036
+ if( v === undefined && !isOverview() ) {
2037
+ v = getPreviousVerticalIndex( horizontalSlides[ h ] );
2038
+ }
2039
+
2040
+ // If we were on a vertical stack, remember what vertical index
2041
+ // it was on so we can resume at the same position when returning
2042
+ if( previousSlide && previousSlide.parentNode && previousSlide.parentNode.classList.contains( 'stack' ) ) {
2043
+ setPreviousVerticalIndex( previousSlide.parentNode, indexv );
2044
+ }
2045
+
2046
+ // Remember the state before this slide
2047
+ var stateBefore = state.concat();
2048
+
2049
+ // Reset the state array
2050
+ state.length = 0;
2051
+
2052
+ var indexhBefore = indexh || 0,
2053
+ indexvBefore = indexv || 0;
2054
+
2055
+ // Activate and transition to the new slide
2056
+ indexh = updateSlides( HORIZONTAL_SLIDES_SELECTOR, h === undefined ? indexh : h );
2057
+ indexv = updateSlides( VERTICAL_SLIDES_SELECTOR, v === undefined ? indexv : v );
2058
+
2059
+ // Update the visibility of slides now that the indices have changed
2060
+ updateSlidesVisibility();
2061
+
2062
+ layout();
2063
+
2064
+ // Apply the new state
2065
+ stateLoop: for( var i = 0, len = state.length; i < len; i++ ) {
2066
+ // Check if this state existed on the previous slide. If it
2067
+ // did, we will avoid adding it repeatedly
2068
+ for( var j = 0; j < stateBefore.length; j++ ) {
2069
+ if( stateBefore[j] === state[i] ) {
2070
+ stateBefore.splice( j, 1 );
2071
+ continue stateLoop;
2072
+ }
2073
+ }
2074
+
2075
+ document.documentElement.classList.add( state[i] );
2076
+
2077
+ // Dispatch custom event matching the state's name
2078
+ dispatchEvent( state[i] );
2079
+ }
2080
+
2081
+ // Clean up the remains of the previous state
2082
+ while( stateBefore.length ) {
2083
+ document.documentElement.classList.remove( stateBefore.pop() );
2084
+ }
2085
+
2086
+ // Update the overview if it's currently active
2087
+ if( isOverview() ) {
2088
+ updateOverview();
2089
+ }
2090
+
2091
+ // Find the current horizontal slide and any possible vertical slides
2092
+ // within it
2093
+ var currentHorizontalSlide = horizontalSlides[ indexh ],
2094
+ currentVerticalSlides = currentHorizontalSlide.querySelectorAll( 'section' );
2095
+
2096
+ // Store references to the previous and current slides
2097
+ currentSlide = currentVerticalSlides[ indexv ] || currentHorizontalSlide;
2098
+
2099
+ // Show fragment, if specified
2100
+ if( typeof f !== 'undefined' ) {
2101
+ navigateFragment( f );
2102
+ }
2103
+
2104
+ // Dispatch an event if the slide changed
2105
+ var slideChanged = ( indexh !== indexhBefore || indexv !== indexvBefore );
2106
+ if( slideChanged ) {
2107
+ dispatchEvent( 'slidechanged', {
2108
+ 'indexh': indexh,
2109
+ 'indexv': indexv,
2110
+ 'previousSlide': previousSlide,
2111
+ 'currentSlide': currentSlide,
2112
+ 'origin': o
2113
+ } );
2114
+ }
2115
+ else {
2116
+ // Ensure that the previous slide is never the same as the current
2117
+ previousSlide = null;
2118
+ }
2119
+
2120
+ // Solves an edge case where the previous slide maintains the
2121
+ // 'present' class when navigating between adjacent vertical
2122
+ // stacks
2123
+ if( previousSlide ) {
2124
+ previousSlide.classList.remove( 'present' );
2125
+ previousSlide.setAttribute( 'aria-hidden', 'true' );
2126
+
2127
+ // Reset all slides upon navigate to home
2128
+ // Issue: #285
2129
+ if ( dom.wrapper.querySelector( HOME_SLIDE_SELECTOR ).classList.contains( 'present' ) ) {
2130
+ // Launch async task
2131
+ setTimeout( function () {
2132
+ var slides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.stack') ), i;
2133
+ for( i in slides ) {
2134
+ if( slides[i] ) {
2135
+ // Reset stack
2136
+ setPreviousVerticalIndex( slides[i], 0 );
2137
+ }
2138
+ }
2139
+ }, 0 );
2140
+ }
2141
+ }
2142
+
2143
+ // Handle embedded content
2144
+ if( slideChanged || !previousSlide ) {
2145
+ stopEmbeddedContent( previousSlide );
2146
+ startEmbeddedContent( currentSlide );
2147
+ }
2148
+
2149
+ // Announce the current slide contents, for screen readers
2150
+ dom.statusDiv.textContent = currentSlide.textContent;
2151
+
2152
+ updateControls();
2153
+ updateProgress();
2154
+ updateBackground();
2155
+ updateParallax();
2156
+ updateSlideNumber();
2157
+
2158
+ // Update the URL hash
2159
+ writeURL();
2160
+
2161
+ cueAutoSlide();
2162
+
2163
+ }
2164
+
2165
+ /**
2166
+ * Syncs the presentation with the current DOM. Useful
2167
+ * when new slides or control elements are added or when
2168
+ * the configuration has changed.
2169
+ */
2170
+ function sync() {
2171
+
2172
+ // Subscribe to input
2173
+ removeEventListeners();
2174
+ addEventListeners();
2175
+
2176
+ // Force a layout to make sure the current config is accounted for
2177
+ layout();
2178
+
2179
+ // Reflect the current autoSlide value
2180
+ autoSlide = config.autoSlide;
2181
+
2182
+ // Start auto-sliding if it's enabled
2183
+ cueAutoSlide();
2184
+
2185
+ // Re-create the slide backgrounds
2186
+ createBackgrounds();
2187
+
2188
+ // Write the current hash to the URL
2189
+ writeURL();
2190
+
2191
+ sortAllFragments();
2192
+
2193
+ updateControls();
2194
+ updateProgress();
2195
+ updateBackground( true );
2196
+ updateSlideNumber();
2197
+ updateSlidesVisibility();
2198
+
2199
+ formatEmbeddedContent();
2200
+ startEmbeddedContent( currentSlide );
2201
+
2202
+ if( isOverview() ) {
2203
+ layoutOverview();
2204
+ }
2205
+
2206
+ }
2207
+
2208
+ /**
2209
+ * Resets all vertical slides so that only the first
2210
+ * is visible.
2211
+ */
2212
+ function resetVerticalSlides() {
2213
+
2214
+ var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
2215
+ horizontalSlides.forEach( function( horizontalSlide ) {
2216
+
2217
+ var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) );
2218
+ verticalSlides.forEach( function( verticalSlide, y ) {
2219
+
2220
+ if( y > 0 ) {
2221
+ verticalSlide.classList.remove( 'present' );
2222
+ verticalSlide.classList.remove( 'past' );
2223
+ verticalSlide.classList.add( 'future' );
2224
+ verticalSlide.setAttribute( 'aria-hidden', 'true' );
2225
+ }
2226
+
2227
+ } );
2228
+
2229
+ } );
2230
+
2231
+ }
2232
+
2233
+ /**
2234
+ * Sorts and formats all of fragments in the
2235
+ * presentation.
2236
+ */
2237
+ function sortAllFragments() {
2238
+
2239
+ var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
2240
+ horizontalSlides.forEach( function( horizontalSlide ) {
2241
+
2242
+ var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) );
2243
+ verticalSlides.forEach( function( verticalSlide, y ) {
2244
+
2245
+ sortFragments( verticalSlide.querySelectorAll( '.fragment' ) );
2246
+
2247
+ } );
2248
+
2249
+ if( verticalSlides.length === 0 ) sortFragments( horizontalSlide.querySelectorAll( '.fragment' ) );
2250
+
2251
+ } );
2252
+
2253
+ }
2254
+
2255
+ /**
2256
+ * Updates one dimension of slides by showing the slide
2257
+ * with the specified index.
2258
+ *
2259
+ * @param {String} selector A CSS selector that will fetch
2260
+ * the group of slides we are working with
2261
+ * @param {Number} index The index of the slide that should be
2262
+ * shown
2263
+ *
2264
+ * @return {Number} The index of the slide that is now shown,
2265
+ * might differ from the passed in index if it was out of
2266
+ * bounds.
2267
+ */
2268
+ function updateSlides( selector, index ) {
2269
+
2270
+ // Select all slides and convert the NodeList result to
2271
+ // an array
2272
+ var slides = toArray( dom.wrapper.querySelectorAll( selector ) ),
2273
+ slidesLength = slides.length;
2274
+
2275
+ var printMode = isPrintingPDF();
2276
+
2277
+ if( slidesLength ) {
2278
+
2279
+ // Should the index loop?
2280
+ if( config.loop ) {
2281
+ index %= slidesLength;
2282
+
2283
+ if( index < 0 ) {
2284
+ index = slidesLength + index;
2285
+ }
2286
+ }
2287
+
2288
+ // Enforce max and minimum index bounds
2289
+ index = Math.max( Math.min( index, slidesLength - 1 ), 0 );
2290
+
2291
+ for( var i = 0; i < slidesLength; i++ ) {
2292
+ var element = slides[i];
2293
+
2294
+ var reverse = config.rtl && !isVerticalSlide( element );
2295
+
2296
+ element.classList.remove( 'past' );
2297
+ element.classList.remove( 'present' );
2298
+ element.classList.remove( 'future' );
2299
+
2300
+ // http://www.w3.org/html/wg/drafts/html/master/editing.html#the-hidden-attribute
2301
+ element.setAttribute( 'hidden', '' );
2302
+ element.setAttribute( 'aria-hidden', 'true' );
2303
+
2304
+ // If this element contains vertical slides
2305
+ if( element.querySelector( 'section' ) ) {
2306
+ element.classList.add( 'stack' );
2307
+ }
2308
+
2309
+ // If we're printing static slides, all slides are "present"
2310
+ if( printMode ) {
2311
+ element.classList.add( 'present' );
2312
+ continue;
2313
+ }
2314
+
2315
+ if( i < index ) {
2316
+ // Any element previous to index is given the 'past' class
2317
+ element.classList.add( reverse ? 'future' : 'past' );
2318
+
2319
+ if( config.fragments ) {
2320
+ var pastFragments = toArray( element.querySelectorAll( '.fragment' ) );
2321
+
2322
+ // Show all fragments on prior slides
2323
+ while( pastFragments.length ) {
2324
+ var pastFragment = pastFragments.pop();
2325
+ pastFragment.classList.add( 'visible' );
2326
+ pastFragment.classList.remove( 'current-fragment' );
2327
+ }
2328
+ }
2329
+ }
2330
+ else if( i > index ) {
2331
+ // Any element subsequent to index is given the 'future' class
2332
+ element.classList.add( reverse ? 'past' : 'future' );
2333
+
2334
+ if( config.fragments ) {
2335
+ var futureFragments = toArray( element.querySelectorAll( '.fragment.visible' ) );
2336
+
2337
+ // No fragments in future slides should be visible ahead of time
2338
+ while( futureFragments.length ) {
2339
+ var futureFragment = futureFragments.pop();
2340
+ futureFragment.classList.remove( 'visible' );
2341
+ futureFragment.classList.remove( 'current-fragment' );
2342
+ }
2343
+ }
2344
+ }
2345
+ }
2346
+
2347
+ // Mark the current slide as present
2348
+ slides[index].classList.add( 'present' );
2349
+ slides[index].removeAttribute( 'hidden' );
2350
+ slides[index].removeAttribute( 'aria-hidden' );
2351
+
2352
+ // If this slide has a state associated with it, add it
2353
+ // onto the current state of the deck
2354
+ var slideState = slides[index].getAttribute( 'data-state' );
2355
+ if( slideState ) {
2356
+ state = state.concat( slideState.split( ' ' ) );
2357
+ }
2358
+
2359
+ }
2360
+ else {
2361
+ // Since there are no slides we can't be anywhere beyond the
2362
+ // zeroth index
2363
+ index = 0;
2364
+ }
2365
+
2366
+ return index;
2367
+
2368
+ }
2369
+
2370
+ /**
2371
+ * Optimization method; hide all slides that are far away
2372
+ * from the present slide.
2373
+ */
2374
+ function updateSlidesVisibility() {
2375
+
2376
+ // Select all slides and convert the NodeList result to
2377
+ // an array
2378
+ var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ),
2379
+ horizontalSlidesLength = horizontalSlides.length,
2380
+ distanceX,
2381
+ distanceY;
2382
+
2383
+ if( horizontalSlidesLength && typeof indexh !== 'undefined' ) {
2384
+
2385
+ // The number of steps away from the present slide that will
2386
+ // be visible
2387
+ var viewDistance = isOverview() ? 10 : config.viewDistance;
2388
+
2389
+ // Limit view distance on weaker devices
2390
+ if( isMobileDevice ) {
2391
+ viewDistance = isOverview() ? 6 : 2;
2392
+ }
2393
+
2394
+ // All slides need to be visible when exporting to PDF
2395
+ if( isPrintingPDF() ) {
2396
+ viewDistance = Number.MAX_VALUE;
2397
+ }
2398
+
2399
+ for( var x = 0; x < horizontalSlidesLength; x++ ) {
2400
+ var horizontalSlide = horizontalSlides[x];
2401
+
2402
+ var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) ),
2403
+ verticalSlidesLength = verticalSlides.length;
2404
+
2405
+ // Determine how far away this slide is from the present
2406
+ distanceX = Math.abs( ( indexh || 0 ) - x ) || 0;
2407
+
2408
+ // If the presentation is looped, distance should measure
2409
+ // 1 between the first and last slides
2410
+ if( config.loop ) {
2411
+ distanceX = Math.abs( ( ( indexh || 0 ) - x ) % ( horizontalSlidesLength - viewDistance ) ) || 0;
2412
+ }
2413
+
2414
+ // Show the horizontal slide if it's within the view distance
2415
+ if( distanceX < viewDistance ) {
2416
+ showSlide( horizontalSlide );
2417
+ }
2418
+ else {
2419
+ hideSlide( horizontalSlide );
2420
+ }
2421
+
2422
+ if( verticalSlidesLength ) {
2423
+
2424
+ var oy = getPreviousVerticalIndex( horizontalSlide );
2425
+
2426
+ for( var y = 0; y < verticalSlidesLength; y++ ) {
2427
+ var verticalSlide = verticalSlides[y];
2428
+
2429
+ distanceY = x === ( indexh || 0 ) ? Math.abs( ( indexv || 0 ) - y ) : Math.abs( y - oy );
2430
+
2431
+ if( distanceX + distanceY < viewDistance ) {
2432
+ showSlide( verticalSlide );
2433
+ }
2434
+ else {
2435
+ hideSlide( verticalSlide );
2436
+ }
2437
+ }
2438
+
2439
+ }
2440
+ }
2441
+
2442
+ }
2443
+
2444
+ }
2445
+
2446
+ /**
2447
+ * Updates the progress bar to reflect the current slide.
2448
+ */
2449
+ function updateProgress() {
2450
+
2451
+ // Update progress if enabled
2452
+ if( config.progress && dom.progressbar ) {
2453
+
2454
+ dom.progressbar.style.width = getProgress() * dom.wrapper.offsetWidth + 'px';
2455
+
2456
+ }
2457
+
2458
+ }
2459
+
2460
+ /**
2461
+ * Updates the slide number div to reflect the current slide.
2462
+ *
2463
+ * Slide number format can be defined as a string using the
2464
+ * following variables:
2465
+ * h: current slide's horizontal index
2466
+ * v: current slide's vertical index
2467
+ * c: current slide index (flattened)
2468
+ * t: total number of slides (flattened)
2469
+ */
2470
+ function updateSlideNumber() {
2471
+
2472
+ // Update slide number if enabled
2473
+ if( config.slideNumber && dom.slideNumber) {
2474
+
2475
+ // Default to only showing the current slide number
2476
+ var format = 'c';
2477
+
2478
+ // Check if a custom slide number format is available
2479
+ if( typeof config.slideNumber === 'string' ) {
2480
+ format = config.slideNumber;
2481
+ }
2482
+
2483
+ dom.slideNumber.innerHTML = format.replace( /h/g, indexh )
2484
+ .replace( /v/g, indexv )
2485
+ .replace( /c/g, getSlidePastCount() + 1 )
2486
+ .replace( /t/g, getTotalSlides() );
2487
+ }
2488
+
2489
+ }
2490
+
2491
+ /**
2492
+ * Updates the state of all control/navigation arrows.
2493
+ */
2494
+ function updateControls() {
2495
+
2496
+ var routes = availableRoutes();
2497
+ var fragments = availableFragments();
2498
+
2499
+ // Remove the 'enabled' class from all directions
2500
+ dom.controlsLeft.concat( dom.controlsRight )
2501
+ .concat( dom.controlsUp )
2502
+ .concat( dom.controlsDown )
2503
+ .concat( dom.controlsPrev )
2504
+ .concat( dom.controlsNext ).forEach( function( node ) {
2505
+ node.classList.remove( 'enabled' );
2506
+ node.classList.remove( 'fragmented' );
2507
+ } );
2508
+
2509
+ // Add the 'enabled' class to the available routes
2510
+ if( routes.left ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'enabled' ); } );
2511
+ if( routes.right ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'enabled' ); } );
2512
+ if( routes.up ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'enabled' ); } );
2513
+ if( routes.down ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'enabled' ); } );
2514
+
2515
+ // Prev/next buttons
2516
+ if( routes.left || routes.up ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'enabled' ); } );
2517
+ if( routes.right || routes.down ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'enabled' ); } );
2518
+
2519
+ // Highlight fragment directions
2520
+ if( currentSlide ) {
2521
+
2522
+ // Always apply fragment decorator to prev/next buttons
2523
+ if( fragments.prev ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
2524
+ if( fragments.next ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
2525
+
2526
+ // Apply fragment decorators to directional buttons based on
2527
+ // what slide axis they are in
2528
+ if( isVerticalSlide( currentSlide ) ) {
2529
+ if( fragments.prev ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
2530
+ if( fragments.next ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
2531
+ }
2532
+ else {
2533
+ if( fragments.prev ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
2534
+ if( fragments.next ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
2535
+ }
2536
+
2537
+ }
2538
+
2539
+ }
2540
+
2541
+ /**
2542
+ * Updates the background elements to reflect the current
2543
+ * slide.
2544
+ *
2545
+ * @param {Boolean} includeAll If true, the backgrounds of
2546
+ * all vertical slides (not just the present) will be updated.
2547
+ */
2548
+ function updateBackground( includeAll ) {
2549
+
2550
+ var currentBackground = null;
2551
+
2552
+ // Reverse past/future classes when in RTL mode
2553
+ var horizontalPast = config.rtl ? 'future' : 'past',
2554
+ horizontalFuture = config.rtl ? 'past' : 'future';
2555
+
2556
+ // Update the classes of all backgrounds to match the
2557
+ // states of their slides (past/present/future)
2558
+ toArray( dom.background.childNodes ).forEach( function( backgroundh, h ) {
2559
+
2560
+ backgroundh.classList.remove( 'past' );
2561
+ backgroundh.classList.remove( 'present' );
2562
+ backgroundh.classList.remove( 'future' );
2563
+
2564
+ if( h < indexh ) {
2565
+ backgroundh.classList.add( horizontalPast );
2566
+ }
2567
+ else if ( h > indexh ) {
2568
+ backgroundh.classList.add( horizontalFuture );
2569
+ }
2570
+ else {
2571
+ backgroundh.classList.add( 'present' );
2572
+
2573
+ // Store a reference to the current background element
2574
+ currentBackground = backgroundh;
2575
+ }
2576
+
2577
+ if( includeAll || h === indexh ) {
2578
+ toArray( backgroundh.querySelectorAll( '.slide-background' ) ).forEach( function( backgroundv, v ) {
2579
+
2580
+ backgroundv.classList.remove( 'past' );
2581
+ backgroundv.classList.remove( 'present' );
2582
+ backgroundv.classList.remove( 'future' );
2583
+
2584
+ if( v < indexv ) {
2585
+ backgroundv.classList.add( 'past' );
2586
+ }
2587
+ else if ( v > indexv ) {
2588
+ backgroundv.classList.add( 'future' );
2589
+ }
2590
+ else {
2591
+ backgroundv.classList.add( 'present' );
2592
+
2593
+ // Only if this is the present horizontal and vertical slide
2594
+ if( h === indexh ) currentBackground = backgroundv;
2595
+ }
2596
+
2597
+ } );
2598
+ }
2599
+
2600
+ } );
2601
+
2602
+ // Stop any currently playing video background
2603
+ if( previousBackground ) {
2604
+
2605
+ var previousVideo = previousBackground.querySelector( 'video' );
2606
+ if( previousVideo ) previousVideo.pause();
2607
+
2608
+ }
2609
+
2610
+ if( currentBackground ) {
2611
+
2612
+ // Start video playback
2613
+ var currentVideo = currentBackground.querySelector( 'video' );
2614
+ if( currentVideo ) {
2615
+ currentVideo.currentTime = 0;
2616
+ currentVideo.play();
2617
+ }
2618
+
2619
+ var backgroundImageURL = currentBackground.style.backgroundImage || '';
2620
+
2621
+ // Restart GIFs (doesn't work in Firefox)
2622
+ if( /\.gif/i.test( backgroundImageURL ) ) {
2623
+ currentBackground.style.backgroundImage = '';
2624
+ window.getComputedStyle( currentBackground ).opacity;
2625
+ currentBackground.style.backgroundImage = backgroundImageURL;
2626
+ }
2627
+
2628
+ // Don't transition between identical backgrounds. This
2629
+ // prevents unwanted flicker.
2630
+ var previousBackgroundHash = previousBackground ? previousBackground.getAttribute( 'data-background-hash' ) : null;
2631
+ var currentBackgroundHash = currentBackground.getAttribute( 'data-background-hash' );
2632
+ if( currentBackgroundHash && currentBackgroundHash === previousBackgroundHash && currentBackground !== previousBackground ) {
2633
+ dom.background.classList.add( 'no-transition' );
2634
+ }
2635
+
2636
+ previousBackground = currentBackground;
2637
+
2638
+ }
2639
+
2640
+ // If there's a background brightness flag for this slide,
2641
+ // bubble it to the .reveal container
2642
+ if( currentSlide ) {
2643
+ [ 'has-light-background', 'has-dark-background' ].forEach( function( classToBubble ) {
2644
+ if( currentSlide.classList.contains( classToBubble ) ) {
2645
+ dom.wrapper.classList.add( classToBubble );
2646
+ }
2647
+ else {
2648
+ dom.wrapper.classList.remove( classToBubble );
2649
+ }
2650
+ } );
2651
+ }
2652
+
2653
+ // Allow the first background to apply without transition
2654
+ setTimeout( function() {
2655
+ dom.background.classList.remove( 'no-transition' );
2656
+ }, 1 );
2657
+
2658
+ }
2659
+
2660
+ /**
2661
+ * Updates the position of the parallax background based
2662
+ * on the current slide index.
2663
+ */
2664
+ function updateParallax() {
2665
+
2666
+ if( config.parallaxBackgroundImage ) {
2667
+
2668
+ var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ),
2669
+ verticalSlides = dom.wrapper.querySelectorAll( VERTICAL_SLIDES_SELECTOR );
2670
+
2671
+ var backgroundSize = dom.background.style.backgroundSize.split( ' ' ),
2672
+ backgroundWidth, backgroundHeight;
2673
+
2674
+ if( backgroundSize.length === 1 ) {
2675
+ backgroundWidth = backgroundHeight = parseInt( backgroundSize[0], 10 );
2676
+ }
2677
+ else {
2678
+ backgroundWidth = parseInt( backgroundSize[0], 10 );
2679
+ backgroundHeight = parseInt( backgroundSize[1], 10 );
2680
+ }
2681
+
2682
+ var slideWidth = dom.background.offsetWidth,
2683
+ horizontalSlideCount = horizontalSlides.length,
2684
+ horizontalOffsetMultiplier,
2685
+ horizontalOffset;
2686
+
2687
+ if( typeof config.parallaxBackgroundHorizontal === 'number' ) {
2688
+ horizontalOffsetMultiplier = config.parallaxBackgroundHorizontal;
2689
+ }
2690
+ else {
2691
+ horizontalOffsetMultiplier = ( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 );
2692
+ }
2693
+
2694
+ horizontalOffset = horizontalOffsetMultiplier * indexh * -1;
2695
+
2696
+ var slideHeight = dom.background.offsetHeight,
2697
+ verticalSlideCount = verticalSlides.length,
2698
+ verticalOffsetMultiplier,
2699
+ verticalOffset;
2700
+
2701
+ if( typeof config.parallaxBackgroundVertical === 'number' ) {
2702
+ verticalOffsetMultiplier = config.parallaxBackgroundVertical;
2703
+ }
2704
+ else {
2705
+ verticalOffsetMultiplier = ( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 );
2706
+ }
2707
+
2708
+ verticalOffset = verticalSlideCount > 0 ? verticalOffsetMultiplier * indexv * 1 : 0;
2709
+
2710
+ dom.background.style.backgroundPosition = horizontalOffset + 'px ' + -verticalOffset + 'px';
2711
+
2712
+ }
2713
+
2714
+ }
2715
+
2716
+ /**
2717
+ * Called when the given slide is within the configured view
2718
+ * distance. Shows the slide element and loads any content
2719
+ * that is set to load lazily (data-src).
2720
+ */
2721
+ function showSlide( slide ) {
2722
+
2723
+ // Show the slide element
2724
+ slide.style.display = 'block';
2725
+
2726
+ // Media elements with data-src attributes
2727
+ toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src]' ) ).forEach( function( element ) {
2728
+ element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
2729
+ element.removeAttribute( 'data-src' );
2730
+ } );
2731
+
2732
+ // Media elements with <source> children
2733
+ toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( media ) {
2734
+ var sources = 0;
2735
+
2736
+ toArray( media.querySelectorAll( 'source[data-src]' ) ).forEach( function( source ) {
2737
+ source.setAttribute( 'src', source.getAttribute( 'data-src' ) );
2738
+ source.removeAttribute( 'data-src' );
2739
+ sources += 1;
2740
+ } );
2741
+
2742
+ // If we rewrote sources for this video/audio element, we need
2743
+ // to manually tell it to load from its new origin
2744
+ if( sources > 0 ) {
2745
+ media.load();
2746
+ }
2747
+ } );
2748
+
2749
+
2750
+ // Show the corresponding background element
2751
+ var indices = getIndices( slide );
2752
+ var background = getSlideBackground( indices.h, indices.v );
2753
+ if( background ) {
2754
+ background.style.display = 'block';
2755
+
2756
+ // If the background contains media, load it
2757
+ if( background.hasAttribute( 'data-loaded' ) === false ) {
2758
+ background.setAttribute( 'data-loaded', 'true' );
2759
+
2760
+ var backgroundImage = slide.getAttribute( 'data-background-image' ),
2761
+ backgroundVideo = slide.getAttribute( 'data-background-video' ),
2762
+ backgroundVideoLoop = slide.hasAttribute( 'data-background-video-loop' ),
2763
+ backgroundIframe = slide.getAttribute( 'data-background-iframe' );
2764
+
2765
+ // Images
2766
+ if( backgroundImage ) {
2767
+ background.style.backgroundImage = 'url('+ backgroundImage +')';
2768
+ }
2769
+ // Videos
2770
+ else if ( backgroundVideo && !isSpeakerNotes() ) {
2771
+ var video = document.createElement( 'video' );
2772
+
2773
+ if( backgroundVideoLoop ) {
2774
+ video.setAttribute( 'loop', '' );
2775
+ }
2776
+
2777
+ // Support comma separated lists of video sources
2778
+ backgroundVideo.split( ',' ).forEach( function( source ) {
2779
+ video.innerHTML += '<source src="'+ source +'">';
2780
+ } );
2781
+
2782
+ background.appendChild( video );
2783
+ }
2784
+ // Iframes
2785
+ else if( backgroundIframe ) {
2786
+ var iframe = document.createElement( 'iframe' );
2787
+ iframe.setAttribute( 'src', backgroundIframe );
2788
+ iframe.style.width = '100%';
2789
+ iframe.style.height = '100%';
2790
+ iframe.style.maxHeight = '100%';
2791
+ iframe.style.maxWidth = '100%';
2792
+
2793
+ background.appendChild( iframe );
2794
+ }
2795
+ }
2796
+ }
2797
+
2798
+ }
2799
+
2800
+ /**
2801
+ * Called when the given slide is moved outside of the
2802
+ * configured view distance.
2803
+ */
2804
+ function hideSlide( slide ) {
2805
+
2806
+ // Hide the slide element
2807
+ slide.style.display = 'none';
2808
+
2809
+ // Hide the corresponding background element
2810
+ var indices = getIndices( slide );
2811
+ var background = getSlideBackground( indices.h, indices.v );
2812
+ if( background ) {
2813
+ background.style.display = 'none';
2814
+ }
2815
+
2816
+ }
2817
+
2818
+ /**
2819
+ * Determine what available routes there are for navigation.
2820
+ *
2821
+ * @return {Object} containing four booleans: left/right/up/down
2822
+ */
2823
+ function availableRoutes() {
2824
+
2825
+ var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ),
2826
+ verticalSlides = dom.wrapper.querySelectorAll( VERTICAL_SLIDES_SELECTOR );
2827
+
2828
+ var routes = {
2829
+ left: indexh > 0 || config.loop,
2830
+ right: indexh < horizontalSlides.length - 1 || config.loop,
2831
+ up: indexv > 0,
2832
+ down: indexv < verticalSlides.length - 1
2833
+ };
2834
+
2835
+ // reverse horizontal controls for rtl
2836
+ if( config.rtl ) {
2837
+ var left = routes.left;
2838
+ routes.left = routes.right;
2839
+ routes.right = left;
2840
+ }
2841
+
2842
+ return routes;
2843
+
2844
+ }
2845
+
2846
+ /**
2847
+ * Returns an object describing the available fragment
2848
+ * directions.
2849
+ *
2850
+ * @return {Object} two boolean properties: prev/next
2851
+ */
2852
+ function availableFragments() {
2853
+
2854
+ if( currentSlide && config.fragments ) {
2855
+ var fragments = currentSlide.querySelectorAll( '.fragment' );
2856
+ var hiddenFragments = currentSlide.querySelectorAll( '.fragment:not(.visible)' );
2857
+
2858
+ return {
2859
+ prev: fragments.length - hiddenFragments.length > 0,
2860
+ next: !!hiddenFragments.length
2861
+ };
2862
+ }
2863
+ else {
2864
+ return { prev: false, next: false };
2865
+ }
2866
+
2867
+ }
2868
+
2869
+ /**
2870
+ * Enforces origin-specific format rules for embedded media.
2871
+ */
2872
+ function formatEmbeddedContent() {
2873
+
2874
+ var _appendParamToIframeSource = function( sourceAttribute, sourceURL, param ) {
2875
+ toArray( dom.slides.querySelectorAll( 'iframe['+ sourceAttribute +'*="'+ sourceURL +'"]' ) ).forEach( function( el ) {
2876
+ var src = el.getAttribute( sourceAttribute );
2877
+ if( src && src.indexOf( param ) === -1 ) {
2878
+ el.setAttribute( sourceAttribute, src + ( !/\?/.test( src ) ? '?' : '&' ) + param );
2879
+ }
2880
+ });
2881
+ };
2882
+
2883
+ // YouTube frames must include "?enablejsapi=1"
2884
+ _appendParamToIframeSource( 'src', 'youtube.com/embed/', 'enablejsapi=1' );
2885
+ _appendParamToIframeSource( 'data-src', 'youtube.com/embed/', 'enablejsapi=1' );
2886
+
2887
+ // Vimeo frames must include "?api=1"
2888
+ _appendParamToIframeSource( 'src', 'player.vimeo.com/', 'api=1' );
2889
+ _appendParamToIframeSource( 'data-src', 'player.vimeo.com/', 'api=1' );
2890
+
2891
+ }
2892
+
2893
+ /**
2894
+ * Start playback of any embedded content inside of
2895
+ * the targeted slide.
2896
+ */
2897
+ function startEmbeddedContent( slide ) {
2898
+
2899
+ if( slide && !isSpeakerNotes() ) {
2900
+ // Restart GIFs
2901
+ toArray( slide.querySelectorAll( 'img[src$=".gif"]' ) ).forEach( function( el ) {
2902
+ // Setting the same unchanged source like this was confirmed
2903
+ // to work in Chrome, FF & Safari
2904
+ el.setAttribute( 'src', el.getAttribute( 'src' ) );
2905
+ } );
2906
+
2907
+ // HTML5 media elements
2908
+ toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
2909
+ if( el.hasAttribute( 'data-autoplay' ) && typeof el.play === 'function' ) {
2910
+ el.play();
2911
+ }
2912
+ } );
2913
+
2914
+ // Normal iframes
2915
+ toArray( slide.querySelectorAll( 'iframe[src]' ) ).forEach( function( el ) {
2916
+ startEmbeddedIframe( { target: el } );
2917
+ } );
2918
+
2919
+ // Lazy loading iframes
2920
+ toArray( slide.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) {
2921
+ if( el.getAttribute( 'src' ) !== el.getAttribute( 'data-src' ) ) {
2922
+ el.removeEventListener( 'load', startEmbeddedIframe ); // remove first to avoid dupes
2923
+ el.addEventListener( 'load', startEmbeddedIframe );
2924
+ el.setAttribute( 'src', el.getAttribute( 'data-src' ) );
2925
+ }
2926
+ } );
2927
+ }
2928
+
2929
+ }
2930
+
2931
+ /**
2932
+ * "Starts" the content of an embedded iframe using the
2933
+ * postmessage API.
2934
+ */
2935
+ function startEmbeddedIframe( event ) {
2936
+
2937
+ var iframe = event.target;
2938
+
2939
+ // YouTube postMessage API
2940
+ if( /youtube\.com\/embed\//.test( iframe.getAttribute( 'src' ) ) && iframe.hasAttribute( 'data-autoplay' ) ) {
2941
+ iframe.contentWindow.postMessage( '{"event":"command","func":"playVideo","args":""}', '*' );
2942
+ }
2943
+ // Vimeo postMessage API
2944
+ else if( /player\.vimeo\.com\//.test( iframe.getAttribute( 'src' ) ) && iframe.hasAttribute( 'data-autoplay' ) ) {
2945
+ iframe.contentWindow.postMessage( '{"method":"play"}', '*' );
2946
+ }
2947
+ // Generic postMessage API
2948
+ else {
2949
+ iframe.contentWindow.postMessage( 'slide:start', '*' );
2950
+ }
2951
+
2952
+ }
2953
+
2954
+ /**
2955
+ * Stop playback of any embedded content inside of
2956
+ * the targeted slide.
2957
+ */
2958
+ function stopEmbeddedContent( slide ) {
2959
+
2960
+ if( slide && slide.parentNode ) {
2961
+ // HTML5 media elements
2962
+ toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
2963
+ if( !el.hasAttribute( 'data-ignore' ) && typeof el.pause === 'function' ) {
2964
+ el.pause();
2965
+ }
2966
+ } );
2967
+
2968
+ // Generic postMessage API for non-lazy loaded iframes
2969
+ toArray( slide.querySelectorAll( 'iframe' ) ).forEach( function( el ) {
2970
+ el.contentWindow.postMessage( 'slide:stop', '*' );
2971
+ el.removeEventListener( 'load', startEmbeddedIframe );
2972
+ });
2973
+
2974
+ // YouTube postMessage API
2975
+ toArray( slide.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) {
2976
+ if( !el.hasAttribute( 'data-ignore' ) && typeof el.contentWindow.postMessage === 'function' ) {
2977
+ el.contentWindow.postMessage( '{"event":"command","func":"pauseVideo","args":""}', '*' );
2978
+ }
2979
+ });
2980
+
2981
+ // Vimeo postMessage API
2982
+ toArray( slide.querySelectorAll( 'iframe[src*="player.vimeo.com/"]' ) ).forEach( function( el ) {
2983
+ if( !el.hasAttribute( 'data-ignore' ) && typeof el.contentWindow.postMessage === 'function' ) {
2984
+ el.contentWindow.postMessage( '{"method":"pause"}', '*' );
2985
+ }
2986
+ });
2987
+
2988
+ // Lazy loading iframes
2989
+ toArray( slide.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) {
2990
+ // Only removing the src doesn't actually unload the frame
2991
+ // in all browsers (Firefox) so we set it to blank first
2992
+ el.setAttribute( 'src', 'about:blank' );
2993
+ el.removeAttribute( 'src' );
2994
+ } );
2995
+ }
2996
+
2997
+ }
2998
+
2999
+ /**
3000
+ * Returns the number of past slides. This can be used as a global
3001
+ * flattened index for slides.
3002
+ */
3003
+ function getSlidePastCount() {
3004
+
3005
+ var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
3006
+
3007
+ // The number of past slides
3008
+ var pastCount = 0;
3009
+
3010
+ // Step through all slides and count the past ones
3011
+ mainLoop: for( var i = 0; i < horizontalSlides.length; i++ ) {
3012
+
3013
+ var horizontalSlide = horizontalSlides[i];
3014
+ var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) );
3015
+
3016
+ for( var j = 0; j < verticalSlides.length; j++ ) {
3017
+
3018
+ // Stop as soon as we arrive at the present
3019
+ if( verticalSlides[j].classList.contains( 'present' ) ) {
3020
+ break mainLoop;
3021
+ }
3022
+
3023
+ pastCount++;
3024
+
3025
+ }
3026
+
3027
+ // Stop as soon as we arrive at the present
3028
+ if( horizontalSlide.classList.contains( 'present' ) ) {
3029
+ break;
3030
+ }
3031
+
3032
+ // Don't count the wrapping section for vertical slides
3033
+ if( horizontalSlide.classList.contains( 'stack' ) === false ) {
3034
+ pastCount++;
3035
+ }
3036
+
3037
+ }
3038
+
3039
+ return pastCount;
3040
+
3041
+ }
3042
+
3043
+ /**
3044
+ * Returns a value ranging from 0-1 that represents
3045
+ * how far into the presentation we have navigated.
3046
+ */
3047
+ function getProgress() {
3048
+
3049
+ // The number of past and total slides
3050
+ var totalCount = getTotalSlides();
3051
+ var pastCount = getSlidePastCount();
3052
+
3053
+ if( currentSlide ) {
3054
+
3055
+ var allFragments = currentSlide.querySelectorAll( '.fragment' );
3056
+
3057
+ // If there are fragments in the current slide those should be
3058
+ // accounted for in the progress.
3059
+ if( allFragments.length > 0 ) {
3060
+ var visibleFragments = currentSlide.querySelectorAll( '.fragment.visible' );
3061
+
3062
+ // This value represents how big a portion of the slide progress
3063
+ // that is made up by its fragments (0-1)
3064
+ var fragmentWeight = 0.9;
3065
+
3066
+ // Add fragment progress to the past slide count
3067
+ pastCount += ( visibleFragments.length / allFragments.length ) * fragmentWeight;
3068
+ }
3069
+
3070
+ }
3071
+
3072
+ return pastCount / ( totalCount - 1 );
3073
+
3074
+ }
3075
+
3076
+ /**
3077
+ * Checks if this presentation is running inside of the
3078
+ * speaker notes window.
3079
+ */
3080
+ function isSpeakerNotes() {
3081
+
3082
+ return !!window.location.search.match( /receiver/gi );
3083
+
3084
+ }
3085
+
3086
+ /**
3087
+ * Reads the current URL (hash) and navigates accordingly.
3088
+ */
3089
+ function readURL() {
3090
+
3091
+ var hash = window.location.hash;
3092
+
3093
+ // Attempt to parse the hash as either an index or name
3094
+ var bits = hash.slice( 2 ).split( '/' ),
3095
+ name = hash.replace( /#|\//gi, '' );
3096
+
3097
+ // If the first bit is invalid and there is a name we can
3098
+ // assume that this is a named link
3099
+ if( isNaN( parseInt( bits[0], 10 ) ) && name.length ) {
3100
+ var element;
3101
+
3102
+ // Ensure the named link is a valid HTML ID attribute
3103
+ if( /^[a-zA-Z][\w:.-]*$/.test( name ) ) {
3104
+ // Find the slide with the specified ID
3105
+ element = document.getElementById( name );
3106
+ }
3107
+
3108
+ if( element ) {
3109
+ // Find the position of the named slide and navigate to it
3110
+ var indices = Reveal.getIndices( element );
3111
+ slide( indices.h, indices.v );
3112
+ }
3113
+ // If the slide doesn't exist, navigate to the current slide
3114
+ else {
3115
+ slide( indexh || 0, indexv || 0 );
3116
+ }
3117
+ }
3118
+ else {
3119
+ // Read the index components of the hash
3120
+ var h = parseInt( bits[0], 10 ) || 0,
3121
+ v = parseInt( bits[1], 10 ) || 0;
3122
+
3123
+ if( h !== indexh || v !== indexv ) {
3124
+ slide( h, v );
3125
+ }
3126
+ }
3127
+
3128
+ }
3129
+
3130
+ /**
3131
+ * Updates the page URL (hash) to reflect the current
3132
+ * state.
3133
+ *
3134
+ * @param {Number} delay The time in ms to wait before
3135
+ * writing the hash
3136
+ */
3137
+ function writeURL( delay ) {
3138
+
3139
+ if( config.history ) {
3140
+
3141
+ // Make sure there's never more than one timeout running
3142
+ clearTimeout( writeURLTimeout );
3143
+
3144
+ // If a delay is specified, timeout this call
3145
+ if( typeof delay === 'number' ) {
3146
+ writeURLTimeout = setTimeout( writeURL, delay );
3147
+ }
3148
+ else if( currentSlide ) {
3149
+ var url = '/';
3150
+
3151
+ // Attempt to create a named link based on the slide's ID
3152
+ var id = currentSlide.getAttribute( 'id' );
3153
+ if( id ) {
3154
+ id = id.toLowerCase();
3155
+ id = id.replace( /[^a-zA-Z0-9\-\_\:\.]/g, '' );
3156
+ }
3157
+
3158
+ // If the current slide has an ID, use that as a named link
3159
+ if( typeof id === 'string' && id.length ) {
3160
+ url = '/' + id;
3161
+ }
3162
+ // Otherwise use the /h/v index
3163
+ else {
3164
+ if( indexh > 0 || indexv > 0 ) url += indexh;
3165
+ if( indexv > 0 ) url += '/' + indexv;
3166
+ }
3167
+
3168
+ window.location.hash = url;
3169
+ }
3170
+ }
3171
+
3172
+ }
3173
+
3174
+ /**
3175
+ * Retrieves the h/v location of the current, or specified,
3176
+ * slide.
3177
+ *
3178
+ * @param {HTMLElement} slide If specified, the returned
3179
+ * index will be for this slide rather than the currently
3180
+ * active one
3181
+ *
3182
+ * @return {Object} { h: <int>, v: <int>, f: <int> }
3183
+ */
3184
+ function getIndices( slide ) {
3185
+
3186
+ // By default, return the current indices
3187
+ var h = indexh,
3188
+ v = indexv,
3189
+ f;
3190
+
3191
+ // If a slide is specified, return the indices of that slide
3192
+ if( slide ) {
3193
+ var isVertical = isVerticalSlide( slide );
3194
+ var slideh = isVertical ? slide.parentNode : slide;
3195
+
3196
+ // Select all horizontal slides
3197
+ var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
3198
+
3199
+ // Now that we know which the horizontal slide is, get its index
3200
+ h = Math.max( horizontalSlides.indexOf( slideh ), 0 );
3201
+
3202
+ // Assume we're not vertical
3203
+ v = undefined;
3204
+
3205
+ // If this is a vertical slide, grab the vertical index
3206
+ if( isVertical ) {
3207
+ v = Math.max( toArray( slide.parentNode.querySelectorAll( 'section' ) ).indexOf( slide ), 0 );
3208
+ }
3209
+ }
3210
+
3211
+ if( !slide && currentSlide ) {
3212
+ var hasFragments = currentSlide.querySelectorAll( '.fragment' ).length > 0;
3213
+ if( hasFragments ) {
3214
+ var currentFragment = currentSlide.querySelector( '.current-fragment' );
3215
+ if( currentFragment && currentFragment.hasAttribute( 'data-fragment-index' ) ) {
3216
+ f = parseInt( currentFragment.getAttribute( 'data-fragment-index' ), 10 );
3217
+ }
3218
+ else {
3219
+ f = currentSlide.querySelectorAll( '.fragment.visible' ).length - 1;
3220
+ }
3221
+ }
3222
+ }
3223
+
3224
+ return { h: h, v: v, f: f };
3225
+
3226
+ }
3227
+
3228
+ /**
3229
+ * Retrieves the total number of slides in this presentation.
3230
+ */
3231
+ function getTotalSlides() {
3232
+
3233
+ return dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ':not(.stack)' ).length;
3234
+
3235
+ }
3236
+
3237
+ /**
3238
+ * Returns the slide element matching the specified index.
3239
+ */
3240
+ function getSlide( x, y ) {
3241
+
3242
+ var horizontalSlide = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR )[ x ];
3243
+ var verticalSlides = horizontalSlide && horizontalSlide.querySelectorAll( 'section' );
3244
+
3245
+ if( verticalSlides && verticalSlides.length && typeof y === 'number' ) {
3246
+ return verticalSlides ? verticalSlides[ y ] : undefined;
3247
+ }
3248
+
3249
+ return horizontalSlide;
3250
+
3251
+ }
3252
+
3253
+ /**
3254
+ * Returns the background element for the given slide.
3255
+ * All slides, even the ones with no background properties
3256
+ * defined, have a background element so as long as the
3257
+ * index is valid an element will be returned.
3258
+ */
3259
+ function getSlideBackground( x, y ) {
3260
+
3261
+ // When printing to PDF the slide backgrounds are nested
3262
+ // inside of the slides
3263
+ if( isPrintingPDF() ) {
3264
+ var slide = getSlide( x, y );
3265
+ if( slide ) {
3266
+ var background = slide.querySelector( '.slide-background' );
3267
+ if( background && background.parentNode === slide ) {
3268
+ return background;
3269
+ }
3270
+ }
3271
+
3272
+ return undefined;
3273
+ }
3274
+
3275
+ var horizontalBackground = dom.wrapper.querySelectorAll( '.backgrounds>.slide-background' )[ x ];
3276
+ var verticalBackgrounds = horizontalBackground && horizontalBackground.querySelectorAll( '.slide-background' );
3277
+
3278
+ if( verticalBackgrounds && verticalBackgrounds.length && typeof y === 'number' ) {
3279
+ return verticalBackgrounds ? verticalBackgrounds[ y ] : undefined;
3280
+ }
3281
+
3282
+ return horizontalBackground;
3283
+
3284
+ }
3285
+
3286
+ /**
3287
+ * Retrieves the current state of the presentation as
3288
+ * an object. This state can then be restored at any
3289
+ * time.
3290
+ */
3291
+ function getState() {
3292
+
3293
+ var indices = getIndices();
3294
+
3295
+ return {
3296
+ indexh: indices.h,
3297
+ indexv: indices.v,
3298
+ indexf: indices.f,
3299
+ paused: isPaused(),
3300
+ overview: isOverview()
3301
+ };
3302
+
3303
+ }
3304
+
3305
+ /**
3306
+ * Restores the presentation to the given state.
3307
+ *
3308
+ * @param {Object} state As generated by getState()
3309
+ */
3310
+ function setState( state ) {
3311
+
3312
+ if( typeof state === 'object' ) {
3313
+ slide( deserialize( state.indexh ), deserialize( state.indexv ), deserialize( state.indexf ) );
3314
+
3315
+ var pausedFlag = deserialize( state.paused ),
3316
+ overviewFlag = deserialize( state.overview );
3317
+
3318
+ if( typeof pausedFlag === 'boolean' && pausedFlag !== isPaused() ) {
3319
+ togglePause( pausedFlag );
3320
+ }
3321
+
3322
+ if( typeof overviewFlag === 'boolean' && overviewFlag !== isOverview() ) {
3323
+ toggleOverview( overviewFlag );
3324
+ }
3325
+ }
3326
+
3327
+ }
3328
+
3329
+ /**
3330
+ * Return a sorted fragments list, ordered by an increasing
3331
+ * "data-fragment-index" attribute.
3332
+ *
3333
+ * Fragments will be revealed in the order that they are returned by
3334
+ * this function, so you can use the index attributes to control the
3335
+ * order of fragment appearance.
3336
+ *
3337
+ * To maintain a sensible default fragment order, fragments are presumed
3338
+ * to be passed in document order. This function adds a "fragment-index"
3339
+ * attribute to each node if such an attribute is not already present,
3340
+ * and sets that attribute to an integer value which is the position of
3341
+ * the fragment within the fragments list.
3342
+ */
3343
+ function sortFragments( fragments ) {
3344
+
3345
+ fragments = toArray( fragments );
3346
+
3347
+ var ordered = [],
3348
+ unordered = [],
3349
+ sorted = [];
3350
+
3351
+ // Group ordered and unordered elements
3352
+ fragments.forEach( function( fragment, i ) {
3353
+ if( fragment.hasAttribute( 'data-fragment-index' ) ) {
3354
+ var index = parseInt( fragment.getAttribute( 'data-fragment-index' ), 10 );
3355
+
3356
+ if( !ordered[index] ) {
3357
+ ordered[index] = [];
3358
+ }
3359
+
3360
+ ordered[index].push( fragment );
3361
+ }
3362
+ else {
3363
+ unordered.push( [ fragment ] );
3364
+ }
3365
+ } );
3366
+
3367
+ // Append fragments without explicit indices in their
3368
+ // DOM order
3369
+ ordered = ordered.concat( unordered );
3370
+
3371
+ // Manually count the index up per group to ensure there
3372
+ // are no gaps
3373
+ var index = 0;
3374
+
3375
+ // Push all fragments in their sorted order to an array,
3376
+ // this flattens the groups
3377
+ ordered.forEach( function( group ) {
3378
+ group.forEach( function( fragment ) {
3379
+ sorted.push( fragment );
3380
+ fragment.setAttribute( 'data-fragment-index', index );
3381
+ } );
3382
+
3383
+ index ++;
3384
+ } );
3385
+
3386
+ return sorted;
3387
+
3388
+ }
3389
+
3390
+ /**
3391
+ * Navigate to the specified slide fragment.
3392
+ *
3393
+ * @param {Number} index The index of the fragment that
3394
+ * should be shown, -1 means all are invisible
3395
+ * @param {Number} offset Integer offset to apply to the
3396
+ * fragment index
3397
+ *
3398
+ * @return {Boolean} true if a change was made in any
3399
+ * fragments visibility as part of this call
3400
+ */
3401
+ function navigateFragment( index, offset ) {
3402
+
3403
+ if( currentSlide && config.fragments ) {
3404
+
3405
+ var fragments = sortFragments( currentSlide.querySelectorAll( '.fragment' ) );
3406
+ if( fragments.length ) {
3407
+
3408
+ // If no index is specified, find the current
3409
+ if( typeof index !== 'number' ) {
3410
+ var lastVisibleFragment = sortFragments( currentSlide.querySelectorAll( '.fragment.visible' ) ).pop();
3411
+
3412
+ if( lastVisibleFragment ) {
3413
+ index = parseInt( lastVisibleFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
3414
+ }
3415
+ else {
3416
+ index = -1;
3417
+ }
3418
+ }
3419
+
3420
+ // If an offset is specified, apply it to the index
3421
+ if( typeof offset === 'number' ) {
3422
+ index += offset;
3423
+ }
3424
+
3425
+ var fragmentsShown = [],
3426
+ fragmentsHidden = [];
3427
+
3428
+ toArray( fragments ).forEach( function( element, i ) {
3429
+
3430
+ if( element.hasAttribute( 'data-fragment-index' ) ) {
3431
+ i = parseInt( element.getAttribute( 'data-fragment-index' ), 10 );
3432
+ }
3433
+
3434
+ // Visible fragments
3435
+ if( i <= index ) {
3436
+ if( !element.classList.contains( 'visible' ) ) fragmentsShown.push( element );
3437
+ element.classList.add( 'visible' );
3438
+ element.classList.remove( 'current-fragment' );
3439
+
3440
+ // Announce the fragments one by one to the Screen Reader
3441
+ dom.statusDiv.textContent = element.textContent;
3442
+
3443
+ if( i === index ) {
3444
+ element.classList.add( 'current-fragment' );
3445
+ }
3446
+ }
3447
+ // Hidden fragments
3448
+ else {
3449
+ if( element.classList.contains( 'visible' ) ) fragmentsHidden.push( element );
3450
+ element.classList.remove( 'visible' );
3451
+ element.classList.remove( 'current-fragment' );
3452
+ }
3453
+
3454
+
3455
+ } );
3456
+
3457
+ if( fragmentsHidden.length ) {
3458
+ dispatchEvent( 'fragmenthidden', { fragment: fragmentsHidden[0], fragments: fragmentsHidden } );
3459
+ }
3460
+
3461
+ if( fragmentsShown.length ) {
3462
+ dispatchEvent( 'fragmentshown', { fragment: fragmentsShown[0], fragments: fragmentsShown } );
3463
+ }
3464
+
3465
+ updateControls();
3466
+ updateProgress();
3467
+
3468
+ return !!( fragmentsShown.length || fragmentsHidden.length );
3469
+
3470
+ }
3471
+
3472
+ }
3473
+
3474
+ return false;
3475
+
3476
+ }
3477
+
3478
+ /**
3479
+ * Navigate to the next slide fragment.
3480
+ *
3481
+ * @return {Boolean} true if there was a next fragment,
3482
+ * false otherwise
3483
+ */
3484
+ function nextFragment() {
3485
+
3486
+ return navigateFragment( null, 1 );
3487
+
3488
+ }
3489
+
3490
+ /**
3491
+ * Navigate to the previous slide fragment.
3492
+ *
3493
+ * @return {Boolean} true if there was a previous fragment,
3494
+ * false otherwise
3495
+ */
3496
+ function previousFragment() {
3497
+
3498
+ return navigateFragment( null, -1 );
3499
+
3500
+ }
3501
+
3502
+ /**
3503
+ * Cues a new automated slide if enabled in the config.
3504
+ */
3505
+ function cueAutoSlide() {
3506
+
3507
+ cancelAutoSlide();
3508
+
3509
+ if( currentSlide ) {
3510
+
3511
+ var currentFragment = currentSlide.querySelector( '.current-fragment' );
3512
+
3513
+ var fragmentAutoSlide = currentFragment ? currentFragment.getAttribute( 'data-autoslide' ) : null;
3514
+ var parentAutoSlide = currentSlide.parentNode ? currentSlide.parentNode.getAttribute( 'data-autoslide' ) : null;
3515
+ var slideAutoSlide = currentSlide.getAttribute( 'data-autoslide' );
3516
+
3517
+ // Pick value in the following priority order:
3518
+ // 1. Current fragment's data-autoslide
3519
+ // 2. Current slide's data-autoslide
3520
+ // 3. Parent slide's data-autoslide
3521
+ // 4. Global autoSlide setting
3522
+ if( fragmentAutoSlide ) {
3523
+ autoSlide = parseInt( fragmentAutoSlide, 10 );
3524
+ }
3525
+ else if( slideAutoSlide ) {
3526
+ autoSlide = parseInt( slideAutoSlide, 10 );
3527
+ }
3528
+ else if( parentAutoSlide ) {
3529
+ autoSlide = parseInt( parentAutoSlide, 10 );
3530
+ }
3531
+ else {
3532
+ autoSlide = config.autoSlide;
3533
+ }
3534
+
3535
+ // If there are media elements with data-autoplay,
3536
+ // automatically set the autoSlide duration to the
3537
+ // length of that media. Not applicable if the slide
3538
+ // is divided up into fragments.
3539
+ if( currentSlide.querySelectorAll( '.fragment' ).length === 0 ) {
3540
+ toArray( currentSlide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
3541
+ if( el.hasAttribute( 'data-autoplay' ) ) {
3542
+ if( autoSlide && el.duration * 1000 > autoSlide ) {
3543
+ autoSlide = ( el.duration * 1000 ) + 1000;
3544
+ }
3545
+ }
3546
+ } );
3547
+ }
3548
+
3549
+ // Cue the next auto-slide if:
3550
+ // - There is an autoSlide value
3551
+ // - Auto-sliding isn't paused by the user
3552
+ // - The presentation isn't paused
3553
+ // - The overview isn't active
3554
+ // - The presentation isn't over
3555
+ if( autoSlide && !autoSlidePaused && !isPaused() && !isOverview() && ( !Reveal.isLastSlide() || availableFragments().next || config.loop === true ) ) {
3556
+ autoSlideTimeout = setTimeout( navigateNext, autoSlide );
3557
+ autoSlideStartTime = Date.now();
3558
+ }
3559
+
3560
+ if( autoSlidePlayer ) {
3561
+ autoSlidePlayer.setPlaying( autoSlideTimeout !== -1 );
3562
+ }
3563
+
3564
+ }
3565
+
3566
+ }
3567
+
3568
+ /**
3569
+ * Cancels any ongoing request to auto-slide.
3570
+ */
3571
+ function cancelAutoSlide() {
3572
+
3573
+ clearTimeout( autoSlideTimeout );
3574
+ autoSlideTimeout = -1;
3575
+
3576
+ }
3577
+
3578
+ function pauseAutoSlide() {
3579
+
3580
+ if( autoSlide && !autoSlidePaused ) {
3581
+ autoSlidePaused = true;
3582
+ dispatchEvent( 'autoslidepaused' );
3583
+ clearTimeout( autoSlideTimeout );
3584
+
3585
+ if( autoSlidePlayer ) {
3586
+ autoSlidePlayer.setPlaying( false );
3587
+ }
3588
+ }
3589
+
3590
+ }
3591
+
3592
+ function resumeAutoSlide() {
3593
+
3594
+ if( autoSlide && autoSlidePaused ) {
3595
+ autoSlidePaused = false;
3596
+ dispatchEvent( 'autoslideresumed' );
3597
+ cueAutoSlide();
3598
+ }
3599
+
3600
+ }
3601
+
3602
+ function navigateLeft() {
3603
+
3604
+ // Reverse for RTL
3605
+ if( config.rtl ) {
3606
+ if( ( isOverview() || nextFragment() === false ) && availableRoutes().left ) {
3607
+ slide( indexh + 1 );
3608
+ }
3609
+ }
3610
+ // Normal navigation
3611
+ else if( ( isOverview() || previousFragment() === false ) && availableRoutes().left ) {
3612
+ slide( indexh - 1 );
3613
+ }
3614
+
3615
+ }
3616
+
3617
+ function navigateRight() {
3618
+
3619
+ // Reverse for RTL
3620
+ if( config.rtl ) {
3621
+ if( ( isOverview() || previousFragment() === false ) && availableRoutes().right ) {
3622
+ slide( indexh - 1 );
3623
+ }
3624
+ }
3625
+ // Normal navigation
3626
+ else if( ( isOverview() || nextFragment() === false ) && availableRoutes().right ) {
3627
+ slide( indexh + 1 );
3628
+ }
3629
+
3630
+ }
3631
+
3632
+ function navigateUp() {
3633
+
3634
+ // Prioritize hiding fragments
3635
+ if( ( isOverview() || previousFragment() === false ) && availableRoutes().up ) {
3636
+ slide( indexh, indexv - 1 );
3637
+ }
3638
+
3639
+ }
3640
+
3641
+ function navigateDown() {
3642
+
3643
+ // Prioritize revealing fragments
3644
+ if( ( isOverview() || nextFragment() === false ) && availableRoutes().down ) {
3645
+ slide( indexh, indexv + 1 );
3646
+ }
3647
+
3648
+ }
3649
+
3650
+ /**
3651
+ * Navigates backwards, prioritized in the following order:
3652
+ * 1) Previous fragment
3653
+ * 2) Previous vertical slide
3654
+ * 3) Previous horizontal slide
3655
+ */
3656
+ function navigatePrev() {
3657
+
3658
+ // Prioritize revealing fragments
3659
+ if( previousFragment() === false ) {
3660
+ if( availableRoutes().up ) {
3661
+ navigateUp();
3662
+ }
3663
+ else {
3664
+ // Fetch the previous horizontal slide, if there is one
3665
+ var previousSlide;
3666
+
3667
+ if( config.rtl ) {
3668
+ previousSlide = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.future' ) ).pop();
3669
+ }
3670
+ else {
3671
+ previousSlide = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.past' ) ).pop();
3672
+ }
3673
+
3674
+ if( previousSlide ) {
3675
+ var v = ( previousSlide.querySelectorAll( 'section' ).length - 1 ) || undefined;
3676
+ var h = indexh - 1;
3677
+ slide( h, v );
3678
+ }
3679
+ }
3680
+ }
3681
+
3682
+ }
3683
+
3684
+ /**
3685
+ * The reverse of #navigatePrev().
3686
+ */
3687
+ function navigateNext() {
3688
+
3689
+ // Prioritize revealing fragments
3690
+ if( nextFragment() === false ) {
3691
+ if( availableRoutes().down ) {
3692
+ navigateDown();
3693
+ }
3694
+ else if( config.rtl ) {
3695
+ navigateLeft();
3696
+ }
3697
+ else {
3698
+ navigateRight();
3699
+ }
3700
+ }
3701
+
3702
+ // If auto-sliding is enabled we need to cue up
3703
+ // another timeout
3704
+ cueAutoSlide();
3705
+
3706
+ }
3707
+
3708
+
3709
+ // --------------------------------------------------------------------//
3710
+ // ----------------------------- EVENTS -------------------------------//
3711
+ // --------------------------------------------------------------------//
3712
+
3713
+ /**
3714
+ * Called by all event handlers that are based on user
3715
+ * input.
3716
+ */
3717
+ function onUserInput( event ) {
3718
+
3719
+ if( config.autoSlideStoppable ) {
3720
+ pauseAutoSlide();
3721
+ }
3722
+
3723
+ }
3724
+
3725
+ /**
3726
+ * Handler for the document level 'keypress' event.
3727
+ */
3728
+ function onDocumentKeyPress( event ) {
3729
+
3730
+ // Check if the pressed key is question mark
3731
+ if( event.shiftKey && event.charCode === 63 ) {
3732
+ if( dom.overlay ) {
3733
+ closeOverlay();
3734
+ }
3735
+ else {
3736
+ showHelp( true );
3737
+ }
3738
+ }
3739
+
3740
+ }
3741
+
3742
+ /**
3743
+ * Handler for the document level 'keydown' event.
3744
+ */
3745
+ function onDocumentKeyDown( event ) {
3746
+
3747
+ // If there's a condition specified and it returns false,
3748
+ // ignore this event
3749
+ if( typeof config.keyboardCondition === 'function' && config.keyboardCondition() === false ) {
3750
+ return true;
3751
+ }
3752
+
3753
+ // Remember if auto-sliding was paused so we can toggle it
3754
+ var autoSlideWasPaused = autoSlidePaused;
3755
+
3756
+ onUserInput( event );
3757
+
3758
+ // Check if there's a focused element that could be using
3759
+ // the keyboard
3760
+ var activeElementIsCE = document.activeElement && document.activeElement.contentEditable !== 'inherit';
3761
+ var activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test( document.activeElement.tagName );
3762
+
3763
+ // Disregard the event if there's a focused element or a
3764
+ // keyboard modifier key is present
3765
+ if( activeElementIsCE || activeElementIsInput || (event.shiftKey && event.keyCode !== 32) || event.altKey || event.ctrlKey || event.metaKey ) return;
3766
+
3767
+ // While paused only allow "unpausing" keyboard events (b and .)
3768
+ if( isPaused() && [66,190,191].indexOf( event.keyCode ) === -1 ) {
3769
+ return false;
3770
+ }
3771
+
3772
+ var triggered = false;
3773
+
3774
+ // 1. User defined key bindings
3775
+ if( typeof config.keyboard === 'object' ) {
3776
+
3777
+ for( var key in config.keyboard ) {
3778
+
3779
+ // Check if this binding matches the pressed key
3780
+ if( parseInt( key, 10 ) === event.keyCode ) {
3781
+
3782
+ var value = config.keyboard[ key ];
3783
+
3784
+ // Callback function
3785
+ if( typeof value === 'function' ) {
3786
+ value.apply( null, [ event ] );
3787
+ }
3788
+ // String shortcuts to reveal.js API
3789
+ else if( typeof value === 'string' && typeof Reveal[ value ] === 'function' ) {
3790
+ Reveal[ value ].call();
3791
+ }
3792
+
3793
+ triggered = true;
3794
+
3795
+ }
3796
+
3797
+ }
3798
+
3799
+ }
3800
+
3801
+ // 2. System defined key bindings
3802
+ if( triggered === false ) {
3803
+
3804
+ // Assume true and try to prove false
3805
+ triggered = true;
3806
+
3807
+ switch( event.keyCode ) {
3808
+ // p, page up
3809
+ case 80: case 33: navigatePrev(); break;
3810
+ // n, page down
3811
+ case 78: case 34: navigateNext(); break;
3812
+ // h, left
3813
+ case 72: case 37: navigateLeft(); break;
3814
+ // l, right
3815
+ case 76: case 39: navigateRight(); break;
3816
+ // k, up
3817
+ case 75: case 38: navigateUp(); break;
3818
+ // j, down
3819
+ case 74: case 40: navigateDown(); break;
3820
+ // home
3821
+ case 36: slide( 0 ); break;
3822
+ // end
3823
+ case 35: slide( Number.MAX_VALUE ); break;
3824
+ // space
3825
+ case 32: isOverview() ? deactivateOverview() : event.shiftKey ? navigatePrev() : navigateNext(); break;
3826
+ // return
3827
+ case 13: isOverview() ? deactivateOverview() : triggered = false; break;
3828
+ // two-spot, semicolon, b, period, Logitech presenter tools "black screen" button
3829
+ case 58: case 59: case 66: case 190: case 191: togglePause(); break;
3830
+ // f
3831
+ case 70: enterFullscreen(); break;
3832
+ // a
3833
+ case 65: if ( config.autoSlideStoppable ) toggleAutoSlide( autoSlideWasPaused ); break;
3834
+ default:
3835
+ triggered = false;
3836
+ }
3837
+
3838
+ }
3839
+
3840
+ // If the input resulted in a triggered action we should prevent
3841
+ // the browsers default behavior
3842
+ if( triggered ) {
3843
+ event.preventDefault && event.preventDefault();
3844
+ }
3845
+ // ESC or O key
3846
+ else if ( ( event.keyCode === 27 || event.keyCode === 79 ) && features.transforms3d ) {
3847
+ if( dom.overlay ) {
3848
+ closeOverlay();
3849
+ }
3850
+ else {
3851
+ toggleOverview();
3852
+ }
3853
+
3854
+ event.preventDefault && event.preventDefault();
3855
+ }
3856
+
3857
+ // If auto-sliding is enabled we need to cue up
3858
+ // another timeout
3859
+ cueAutoSlide();
3860
+
3861
+ }
3862
+
3863
+ /**
3864
+ * Handler for the 'touchstart' event, enables support for
3865
+ * swipe and pinch gestures.
3866
+ */
3867
+ function onTouchStart( event ) {
3868
+
3869
+ touch.startX = event.touches[0].clientX;
3870
+ touch.startY = event.touches[0].clientY;
3871
+ touch.startCount = event.touches.length;
3872
+
3873
+ // If there's two touches we need to memorize the distance
3874
+ // between those two points to detect pinching
3875
+ if( event.touches.length === 2 && config.overview ) {
3876
+ touch.startSpan = distanceBetween( {
3877
+ x: event.touches[1].clientX,
3878
+ y: event.touches[1].clientY
3879
+ }, {
3880
+ x: touch.startX,
3881
+ y: touch.startY
3882
+ } );
3883
+ }
3884
+
3885
+ }
3886
+
3887
+ /**
3888
+ * Handler for the 'touchmove' event.
3889
+ */
3890
+ function onTouchMove( event ) {
3891
+
3892
+ // Each touch should only trigger one action
3893
+ if( !touch.captured ) {
3894
+ onUserInput( event );
3895
+
3896
+ var currentX = event.touches[0].clientX;
3897
+ var currentY = event.touches[0].clientY;
3898
+
3899
+ // If the touch started with two points and still has
3900
+ // two active touches; test for the pinch gesture
3901
+ if( event.touches.length === 2 && touch.startCount === 2 && config.overview ) {
3902
+
3903
+ // The current distance in pixels between the two touch points
3904
+ var currentSpan = distanceBetween( {
3905
+ x: event.touches[1].clientX,
3906
+ y: event.touches[1].clientY
3907
+ }, {
3908
+ x: touch.startX,
3909
+ y: touch.startY
3910
+ } );
3911
+
3912
+ // If the span is larger than the desire amount we've got
3913
+ // ourselves a pinch
3914
+ if( Math.abs( touch.startSpan - currentSpan ) > touch.threshold ) {
3915
+ touch.captured = true;
3916
+
3917
+ if( currentSpan < touch.startSpan ) {
3918
+ activateOverview();
3919
+ }
3920
+ else {
3921
+ deactivateOverview();
3922
+ }
3923
+ }
3924
+
3925
+ event.preventDefault();
3926
+
3927
+ }
3928
+ // There was only one touch point, look for a swipe
3929
+ else if( event.touches.length === 1 && touch.startCount !== 2 ) {
3930
+
3931
+ var deltaX = currentX - touch.startX,
3932
+ deltaY = currentY - touch.startY;
3933
+
3934
+ if( deltaX > touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
3935
+ touch.captured = true;
3936
+ navigateLeft();
3937
+ }
3938
+ else if( deltaX < -touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
3939
+ touch.captured = true;
3940
+ navigateRight();
3941
+ }
3942
+ else if( deltaY > touch.threshold ) {
3943
+ touch.captured = true;
3944
+ navigateUp();
3945
+ }
3946
+ else if( deltaY < -touch.threshold ) {
3947
+ touch.captured = true;
3948
+ navigateDown();
3949
+ }
3950
+
3951
+ // If we're embedded, only block touch events if they have
3952
+ // triggered an action
3953
+ if( config.embedded ) {
3954
+ if( touch.captured || isVerticalSlide( currentSlide ) ) {
3955
+ event.preventDefault();
3956
+ }
3957
+ }
3958
+ // Not embedded? Block them all to avoid needless tossing
3959
+ // around of the viewport in iOS
3960
+ else {
3961
+ event.preventDefault();
3962
+ }
3963
+
3964
+ }
3965
+ }
3966
+ // There's a bug with swiping on some Android devices unless
3967
+ // the default action is always prevented
3968
+ else if( navigator.userAgent.match( /android/gi ) ) {
3969
+ event.preventDefault();
3970
+ }
3971
+
3972
+ }
3973
+
3974
+ /**
3975
+ * Handler for the 'touchend' event.
3976
+ */
3977
+ function onTouchEnd( event ) {
3978
+
3979
+ touch.captured = false;
3980
+
3981
+ }
3982
+
3983
+ /**
3984
+ * Convert pointer down to touch start.
3985
+ */
3986
+ function onPointerDown( event ) {
3987
+
3988
+ if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
3989
+ event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
3990
+ onTouchStart( event );
3991
+ }
3992
+
3993
+ }
3994
+
3995
+ /**
3996
+ * Convert pointer move to touch move.
3997
+ */
3998
+ function onPointerMove( event ) {
3999
+
4000
+ if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
4001
+ event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
4002
+ onTouchMove( event );
4003
+ }
4004
+
4005
+ }
4006
+
4007
+ /**
4008
+ * Convert pointer up to touch end.
4009
+ */
4010
+ function onPointerUp( event ) {
4011
+
4012
+ if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
4013
+ event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
4014
+ onTouchEnd( event );
4015
+ }
4016
+
4017
+ }
4018
+
4019
+ /**
4020
+ * Handles mouse wheel scrolling, throttled to avoid skipping
4021
+ * multiple slides.
4022
+ */
4023
+ function onDocumentMouseScroll( event ) {
4024
+
4025
+ if( Date.now() - lastMouseWheelStep > 600 ) {
4026
+
4027
+ lastMouseWheelStep = Date.now();
4028
+
4029
+ var delta = event.detail || -event.wheelDelta;
4030
+ if( delta > 0 ) {
4031
+ navigateNext();
4032
+ }
4033
+ else {
4034
+ navigatePrev();
4035
+ }
4036
+
4037
+ }
4038
+
4039
+ }
4040
+
4041
+ /**
4042
+ * Clicking on the progress bar results in a navigation to the
4043
+ * closest approximate horizontal slide using this equation:
4044
+ *
4045
+ * ( clickX / presentationWidth ) * numberOfSlides
4046
+ */
4047
+ function onProgressClicked( event ) {
4048
+
4049
+ onUserInput( event );
4050
+
4051
+ event.preventDefault();
4052
+
4053
+ var slidesTotal = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).length;
4054
+ var slideIndex = Math.floor( ( event.clientX / dom.wrapper.offsetWidth ) * slidesTotal );
4055
+
4056
+ if( config.rtl ) {
4057
+ slideIndex = slidesTotal - slideIndex;
4058
+ }
4059
+
4060
+ slide( slideIndex );
4061
+
4062
+ }
4063
+
4064
+ /**
4065
+ * Event handler for navigation control buttons.
4066
+ */
4067
+ function onNavigateLeftClicked( event ) { event.preventDefault(); onUserInput(); navigateLeft(); }
4068
+ function onNavigateRightClicked( event ) { event.preventDefault(); onUserInput(); navigateRight(); }
4069
+ function onNavigateUpClicked( event ) { event.preventDefault(); onUserInput(); navigateUp(); }
4070
+ function onNavigateDownClicked( event ) { event.preventDefault(); onUserInput(); navigateDown(); }
4071
+ function onNavigatePrevClicked( event ) { event.preventDefault(); onUserInput(); navigatePrev(); }
4072
+ function onNavigateNextClicked( event ) { event.preventDefault(); onUserInput(); navigateNext(); }
4073
+
4074
+ /**
4075
+ * Handler for the window level 'hashchange' event.
4076
+ */
4077
+ function onWindowHashChange( event ) {
4078
+
4079
+ readURL();
4080
+
4081
+ }
4082
+
4083
+ /**
4084
+ * Handler for the window level 'resize' event.
4085
+ */
4086
+ function onWindowResize( event ) {
4087
+
4088
+ layout();
4089
+
4090
+ }
4091
+
4092
+ /**
4093
+ * Handle for the window level 'visibilitychange' event.
4094
+ */
4095
+ function onPageVisibilityChange( event ) {
4096
+
4097
+ var isHidden = document.webkitHidden ||
4098
+ document.msHidden ||
4099
+ document.hidden;
4100
+
4101
+ // If, after clicking a link or similar and we're coming back,
4102
+ // focus the document.body to ensure we can use keyboard shortcuts
4103
+ if( isHidden === false && document.activeElement !== document.body ) {
4104
+ // Not all elements support .blur() - SVGs among them.
4105
+ if( typeof document.activeElement.blur === 'function' ) {
4106
+ document.activeElement.blur();
4107
+ }
4108
+ document.body.focus();
4109
+ }
4110
+
4111
+ }
4112
+
4113
+ /**
4114
+ * Invoked when a slide is and we're in the overview.
4115
+ */
4116
+ function onOverviewSlideClicked( event ) {
4117
+
4118
+ // TODO There's a bug here where the event listeners are not
4119
+ // removed after deactivating the overview.
4120
+ if( eventsAreBound && isOverview() ) {
4121
+ event.preventDefault();
4122
+
4123
+ var element = event.target;
4124
+
4125
+ while( element && !element.nodeName.match( /section/gi ) ) {
4126
+ element = element.parentNode;
4127
+ }
4128
+
4129
+ if( element && !element.classList.contains( 'disabled' ) ) {
4130
+
4131
+ deactivateOverview();
4132
+
4133
+ if( element.nodeName.match( /section/gi ) ) {
4134
+ var h = parseInt( element.getAttribute( 'data-index-h' ), 10 ),
4135
+ v = parseInt( element.getAttribute( 'data-index-v' ), 10 );
4136
+
4137
+ slide( h, v );
4138
+ }
4139
+
4140
+ }
4141
+ }
4142
+
4143
+ }
4144
+
4145
+ /**
4146
+ * Handles clicks on links that are set to preview in the
4147
+ * iframe overlay.
4148
+ */
4149
+ function onPreviewLinkClicked( event ) {
4150
+
4151
+ if( event.currentTarget && event.currentTarget.hasAttribute( 'href' ) ) {
4152
+ var url = event.currentTarget.getAttribute( 'href' );
4153
+ if( url ) {
4154
+ showPreview( url );
4155
+ event.preventDefault();
4156
+ }
4157
+ }
4158
+
4159
+ }
4160
+
4161
+ /**
4162
+ * Handles click on the auto-sliding controls element.
4163
+ */
4164
+ function onAutoSlidePlayerClick( event ) {
4165
+
4166
+ // Replay
4167
+ if( Reveal.isLastSlide() && config.loop === false ) {
4168
+ slide( 0, 0 );
4169
+ resumeAutoSlide();
4170
+ }
4171
+ // Resume
4172
+ else if( autoSlidePaused ) {
4173
+ resumeAutoSlide();
4174
+ }
4175
+ // Pause
4176
+ else {
4177
+ pauseAutoSlide();
4178
+ }
4179
+
4180
+ }
4181
+
4182
+
4183
+ // --------------------------------------------------------------------//
4184
+ // ------------------------ PLAYBACK COMPONENT ------------------------//
4185
+ // --------------------------------------------------------------------//
4186
+
4187
+
4188
+ /**
4189
+ * Constructor for the playback component, which displays
4190
+ * play/pause/progress controls.
4191
+ *
4192
+ * @param {HTMLElement} container The component will append
4193
+ * itself to this
4194
+ * @param {Function} progressCheck A method which will be
4195
+ * called frequently to get the current progress on a range
4196
+ * of 0-1
4197
+ */
4198
+ function Playback( container, progressCheck ) {
4199
+
4200
+ // Cosmetics
4201
+ this.diameter = 50;
4202
+ this.thickness = 3;
4203
+
4204
+ // Flags if we are currently playing
4205
+ this.playing = false;
4206
+
4207
+ // Current progress on a 0-1 range
4208
+ this.progress = 0;
4209
+
4210
+ // Used to loop the animation smoothly
4211
+ this.progressOffset = 1;
4212
+
4213
+ this.container = container;
4214
+ this.progressCheck = progressCheck;
4215
+
4216
+ this.canvas = document.createElement( 'canvas' );
4217
+ this.canvas.className = 'playback';
4218
+ this.canvas.width = this.diameter;
4219
+ this.canvas.height = this.diameter;
4220
+ this.context = this.canvas.getContext( '2d' );
4221
+
4222
+ this.container.appendChild( this.canvas );
4223
+
4224
+ this.render();
4225
+
4226
+ }
4227
+
4228
+ Playback.prototype.setPlaying = function( value ) {
4229
+
4230
+ var wasPlaying = this.playing;
4231
+
4232
+ this.playing = value;
4233
+
4234
+ // Start repainting if we weren't already
4235
+ if( !wasPlaying && this.playing ) {
4236
+ this.animate();
4237
+ }
4238
+ else {
4239
+ this.render();
4240
+ }
4241
+
4242
+ };
4243
+
4244
+ Playback.prototype.animate = function() {
4245
+
4246
+ var progressBefore = this.progress;
4247
+
4248
+ this.progress = this.progressCheck();
4249
+
4250
+ // When we loop, offset the progress so that it eases
4251
+ // smoothly rather than immediately resetting
4252
+ if( progressBefore > 0.8 && this.progress < 0.2 ) {
4253
+ this.progressOffset = this.progress;
4254
+ }
4255
+
4256
+ this.render();
4257
+
4258
+ if( this.playing ) {
4259
+ features.requestAnimationFrameMethod.call( window, this.animate.bind( this ) );
4260
+ }
4261
+
4262
+ };
4263
+
4264
+ /**
4265
+ * Renders the current progress and playback state.
4266
+ */
4267
+ Playback.prototype.render = function() {
4268
+
4269
+ var progress = this.playing ? this.progress : 0,
4270
+ radius = ( this.diameter / 2 ) - this.thickness,
4271
+ x = this.diameter / 2,
4272
+ y = this.diameter / 2,
4273
+ iconSize = 14;
4274
+
4275
+ // Ease towards 1
4276
+ this.progressOffset += ( 1 - this.progressOffset ) * 0.1;
4277
+
4278
+ var endAngle = ( - Math.PI / 2 ) + ( progress * ( Math.PI * 2 ) );
4279
+ var startAngle = ( - Math.PI / 2 ) + ( this.progressOffset * ( Math.PI * 2 ) );
4280
+
4281
+ this.context.save();
4282
+ this.context.clearRect( 0, 0, this.diameter, this.diameter );
4283
+
4284
+ // Solid background color
4285
+ this.context.beginPath();
4286
+ this.context.arc( x, y, radius + 2, 0, Math.PI * 2, false );
4287
+ this.context.fillStyle = 'rgba( 0, 0, 0, 0.4 )';
4288
+ this.context.fill();
4289
+
4290
+ // Draw progress track
4291
+ this.context.beginPath();
4292
+ this.context.arc( x, y, radius, 0, Math.PI * 2, false );
4293
+ this.context.lineWidth = this.thickness;
4294
+ this.context.strokeStyle = '#666';
4295
+ this.context.stroke();
4296
+
4297
+ if( this.playing ) {
4298
+ // Draw progress on top of track
4299
+ this.context.beginPath();
4300
+ this.context.arc( x, y, radius, startAngle, endAngle, false );
4301
+ this.context.lineWidth = this.thickness;
4302
+ this.context.strokeStyle = '#fff';
4303
+ this.context.stroke();
4304
+ }
4305
+
4306
+ this.context.translate( x - ( iconSize / 2 ), y - ( iconSize / 2 ) );
4307
+
4308
+ // Draw play/pause icons
4309
+ if( this.playing ) {
4310
+ this.context.fillStyle = '#fff';
4311
+ this.context.fillRect( 0, 0, iconSize / 2 - 2, iconSize );
4312
+ this.context.fillRect( iconSize / 2 + 2, 0, iconSize / 2 - 2, iconSize );
4313
+ }
4314
+ else {
4315
+ this.context.beginPath();
4316
+ this.context.translate( 2, 0 );
4317
+ this.context.moveTo( 0, 0 );
4318
+ this.context.lineTo( iconSize - 2, iconSize / 2 );
4319
+ this.context.lineTo( 0, iconSize );
4320
+ this.context.fillStyle = '#fff';
4321
+ this.context.fill();
4322
+ }
4323
+
4324
+ this.context.restore();
4325
+
4326
+ };
4327
+
4328
+ Playback.prototype.on = function( type, listener ) {
4329
+ this.canvas.addEventListener( type, listener, false );
4330
+ };
4331
+
4332
+ Playback.prototype.off = function( type, listener ) {
4333
+ this.canvas.removeEventListener( type, listener, false );
4334
+ };
4335
+
4336
+ Playback.prototype.destroy = function() {
4337
+
4338
+ this.playing = false;
4339
+
4340
+ if( this.canvas.parentNode ) {
4341
+ this.container.removeChild( this.canvas );
4342
+ }
4343
+
4344
+ };
4345
+
4346
+
4347
+ // --------------------------------------------------------------------//
4348
+ // ------------------------------- API --------------------------------//
4349
+ // --------------------------------------------------------------------//
4350
+
4351
+
4352
+ Reveal = {
4353
+ initialize: initialize,
4354
+ configure: configure,
4355
+ sync: sync,
4356
+
4357
+ // Navigation methods
4358
+ slide: slide,
4359
+ left: navigateLeft,
4360
+ right: navigateRight,
4361
+ up: navigateUp,
4362
+ down: navigateDown,
4363
+ prev: navigatePrev,
4364
+ next: navigateNext,
4365
+
4366
+ // Fragment methods
4367
+ navigateFragment: navigateFragment,
4368
+ prevFragment: previousFragment,
4369
+ nextFragment: nextFragment,
4370
+
4371
+ // Deprecated aliases
4372
+ navigateTo: slide,
4373
+ navigateLeft: navigateLeft,
4374
+ navigateRight: navigateRight,
4375
+ navigateUp: navigateUp,
4376
+ navigateDown: navigateDown,
4377
+ navigatePrev: navigatePrev,
4378
+ navigateNext: navigateNext,
4379
+
4380
+ // Forces an update in slide layout
4381
+ layout: layout,
4382
+
4383
+ // Returns an object with the available routes as booleans (left/right/top/bottom)
4384
+ availableRoutes: availableRoutes,
4385
+
4386
+ // Returns an object with the available fragments as booleans (prev/next)
4387
+ availableFragments: availableFragments,
4388
+
4389
+ // Toggles the overview mode on/off
4390
+ toggleOverview: toggleOverview,
4391
+
4392
+ // Toggles the "black screen" mode on/off
4393
+ togglePause: togglePause,
4394
+
4395
+ // Toggles the auto slide mode on/off
4396
+ toggleAutoSlide: toggleAutoSlide,
4397
+
4398
+ // State checks
4399
+ isOverview: isOverview,
4400
+ isPaused: isPaused,
4401
+ isAutoSliding: isAutoSliding,
4402
+
4403
+ // Adds or removes all internal event listeners (such as keyboard)
4404
+ addEventListeners: addEventListeners,
4405
+ removeEventListeners: removeEventListeners,
4406
+
4407
+ // Facility for persisting and restoring the presentation state
4408
+ getState: getState,
4409
+ setState: setState,
4410
+
4411
+ // Presentation progress on range of 0-1
4412
+ getProgress: getProgress,
4413
+
4414
+ // Returns the indices of the current, or specified, slide
4415
+ getIndices: getIndices,
4416
+
4417
+ getTotalSlides: getTotalSlides,
4418
+
4419
+ // Returns the slide element at the specified index
4420
+ getSlide: getSlide,
4421
+
4422
+ // Returns the slide background element at the specified index
4423
+ getSlideBackground: getSlideBackground,
4424
+
4425
+ // Returns the previous slide element, may be null
4426
+ getPreviousSlide: function() {
4427
+ return previousSlide;
4428
+ },
4429
+
4430
+ // Returns the current slide element
4431
+ getCurrentSlide: function() {
4432
+ return currentSlide;
4433
+ },
4434
+
4435
+ // Returns the current scale of the presentation content
4436
+ getScale: function() {
4437
+ return scale;
4438
+ },
4439
+
4440
+ // Returns the current configuration object
4441
+ getConfig: function() {
4442
+ return config;
4443
+ },
4444
+
4445
+ // Helper method, retrieves query string as a key/value hash
4446
+ getQueryHash: function() {
4447
+ var query = {};
4448
+
4449
+ location.search.replace( /[A-Z0-9]+?=([\w\.%-]*)/gi, function(a) {
4450
+ query[ a.split( '=' ).shift() ] = a.split( '=' ).pop();
4451
+ } );
4452
+
4453
+ // Basic deserialization
4454
+ for( var i in query ) {
4455
+ var value = query[ i ];
4456
+
4457
+ query[ i ] = deserialize( unescape( value ) );
4458
+ }
4459
+
4460
+ return query;
4461
+ },
4462
+
4463
+ // Returns true if we're currently on the first slide
4464
+ isFirstSlide: function() {
4465
+ return ( indexh === 0 && indexv === 0 );
4466
+ },
4467
+
4468
+ // Returns true if we're currently on the last slide
4469
+ isLastSlide: function() {
4470
+ if( currentSlide ) {
4471
+ // Does this slide has next a sibling?
4472
+ if( currentSlide.nextElementSibling ) return false;
4473
+
4474
+ // If it's vertical, does its parent have a next sibling?
4475
+ if( isVerticalSlide( currentSlide ) && currentSlide.parentNode.nextElementSibling ) return false;
4476
+
4477
+ return true;
4478
+ }
4479
+
4480
+ return false;
4481
+ },
4482
+
4483
+ // Checks if reveal.js has been loaded and is ready for use
4484
+ isReady: function() {
4485
+ return loaded;
4486
+ },
4487
+
4488
+ // Forward event binding to the reveal DOM element
4489
+ addEventListener: function( type, listener, useCapture ) {
4490
+ if( 'addEventListener' in window ) {
4491
+ ( dom.wrapper || document.querySelector( '.reveal' ) ).addEventListener( type, listener, useCapture );
4492
+ }
4493
+ },
4494
+ removeEventListener: function( type, listener, useCapture ) {
4495
+ if( 'addEventListener' in window ) {
4496
+ ( dom.wrapper || document.querySelector( '.reveal' ) ).removeEventListener( type, listener, useCapture );
4497
+ }
4498
+ },
4499
+
4500
+ // Programatically triggers a keyboard event
4501
+ triggerKey: function( keyCode ) {
4502
+ onDocumentKeyDown( { keyCode: keyCode } );
4503
+ }
4504
+ };
4505
+
4506
+ return Reveal;
4507
+
4508
+ }));