slideit 0.1.0

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