slidemgr 1.0.1

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