reveal-ck 3.6.0 → 3.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/files/reveal.js/CONTRIBUTING.md +4 -0
  3. data/files/reveal.js/Gruntfile.js +44 -26
  4. data/files/reveal.js/LICENSE +1 -1
  5. data/files/reveal.js/README.md +375 -161
  6. data/files/reveal.js/bower.json +27 -0
  7. data/files/reveal.js/css/print/paper.css +4 -3
  8. data/files/reveal.js/css/print/pdf.css +53 -38
  9. data/files/reveal.js/css/reveal.css +452 -206
  10. data/files/reveal.js/css/reveal.scss +328 -175
  11. data/files/reveal.js/css/theme/README.md +2 -6
  12. data/files/reveal.js/css/theme/beige.css +81 -50
  13. data/files/reveal.js/css/theme/black.css +70 -39
  14. data/files/reveal.js/css/theme/blood.css +81 -57
  15. data/files/reveal.js/css/theme/league.css +75 -44
  16. data/files/reveal.js/css/theme/moon.css +75 -44
  17. data/files/reveal.js/css/theme/night.css +70 -39
  18. data/files/reveal.js/css/theme/serif.css +72 -41
  19. data/files/reveal.js/css/theme/simple.css +72 -38
  20. data/files/reveal.js/css/theme/sky.css +75 -44
  21. data/files/reveal.js/css/theme/solarized.css +75 -44
  22. data/files/reveal.js/css/theme/source/black.scss +2 -2
  23. data/files/reveal.js/css/theme/source/blood.scss +3 -16
  24. data/files/reveal.js/css/theme/source/night.scss +0 -1
  25. data/files/reveal.js/css/theme/source/simple.scss +5 -0
  26. data/files/reveal.js/css/theme/source/white.scss +2 -2
  27. data/files/reveal.js/css/theme/template/settings.scss +1 -1
  28. data/files/reveal.js/css/theme/template/theme.scss +36 -23
  29. data/files/reveal.js/css/theme/white.css +75 -44
  30. data/files/reveal.js/demo.html +410 -0
  31. data/files/reveal.js/index.html +14 -373
  32. data/files/reveal.js/js/reveal.js +1186 -350
  33. data/files/reveal.js/lib/css/zenburn.css +41 -78
  34. data/files/reveal.js/lib/js/head.min.js +9 -8
  35. data/files/reveal.js/package.json +22 -26
  36. data/files/reveal.js/plugin/highlight/highlight.js +52 -4
  37. data/files/reveal.js/plugin/markdown/example.html +1 -1
  38. data/files/reveal.js/plugin/markdown/markdown.js +40 -21
  39. data/files/reveal.js/plugin/markdown/marked.js +2 -33
  40. data/files/reveal.js/plugin/math/math.js +5 -2
  41. data/files/reveal.js/plugin/multiplex/client.js +1 -1
  42. data/files/reveal.js/plugin/multiplex/index.js +24 -16
  43. data/files/reveal.js/plugin/multiplex/master.js +22 -42
  44. data/files/reveal.js/plugin/multiplex/package.json +19 -0
  45. data/files/reveal.js/plugin/notes-server/client.js +6 -1
  46. data/files/reveal.js/plugin/notes-server/index.js +17 -14
  47. data/files/reveal.js/plugin/notes-server/notes.html +215 -26
  48. data/files/reveal.js/plugin/notes/notes.html +372 -32
  49. data/files/reveal.js/plugin/notes/notes.js +40 -7
  50. data/files/reveal.js/plugin/print-pdf/print-pdf.js +47 -26
  51. data/files/reveal.js/plugin/zoom-js/zoom.js +12 -2
  52. data/files/reveal.js/test/examples/math.html +1 -1
  53. data/files/reveal.js/test/examples/slide-backgrounds.html +1 -1
  54. data/files/reveal.js/test/examples/slide-transitions.html +101 -0
  55. data/files/reveal.js/test/simple.md +12 -0
  56. data/files/reveal.js/test/test-markdown-element-attributes.html +3 -3
  57. data/files/reveal.js/test/test-markdown-element-attributes.js +1 -1
  58. data/files/reveal.js/test/test-markdown-external.html +36 -0
  59. data/files/reveal.js/test/test-markdown-external.js +24 -0
  60. data/files/reveal.js/test/test-markdown-options.html +41 -0
  61. data/files/reveal.js/test/test-markdown-options.js +26 -0
  62. data/files/reveal.js/test/test-markdown.html +1 -1
  63. data/files/reveal.js/test/test.html +5 -1
  64. data/files/reveal.js/test/test.js +26 -1
  65. data/lib/reveal-ck/version.rb +1 -1
  66. metadata +11 -4
  67. data/files/reveal.js/plugin/leap/leap.js +0 -159
  68. data/files/reveal.js/plugin/remotes/remotes.js +0 -39
@@ -3,7 +3,7 @@
3
3
  * http://lab.hakim.se/reveal-js
4
4
  * MIT licensed
5
5
  *
6
- * Copyright (C) 2015 Hakim El Hattab, http://hakim.se
6
+ * Copyright (C) 2017 Hakim El Hattab, http://hakim.se
7
7
  */
8
8
  (function( root, factory ) {
9
9
  if( typeof define === 'function' && define.amd ) {
@@ -25,12 +25,16 @@
25
25
 
26
26
  var Reveal;
27
27
 
28
+ // The reveal.js version
29
+ var VERSION = '3.5.0';
30
+
28
31
  var SLIDES_SELECTOR = '.slides section',
29
32
  HORIZONTAL_SLIDES_SELECTOR = '.slides>section',
30
33
  VERTICAL_SLIDES_SELECTOR = '.slides>section.present>section',
31
34
  HOME_SLIDE_SELECTOR = '.slides>section:first-of-type',
35
+ UA = navigator.userAgent,
32
36
 
33
- // Configurations defaults, can be overridden at initialization time
37
+ // Configuration defaults, can be overridden at initialization time
34
38
  config = {
35
39
 
36
40
  // The "normal" size of the presentation, aspect ratio will be preserved
@@ -39,11 +43,11 @@
39
43
  height: 700,
40
44
 
41
45
  // Factor of the display size that should remain empty around the content
42
- margin: 0.1,
46
+ margin: 0.04,
43
47
 
44
48
  // Bounds for smallest/largest possible scale to apply to content
45
49
  minScale: 0.2,
46
- maxScale: 1.5,
50
+ maxScale: 2.0,
47
51
 
48
52
  // Display controls in the bottom right corner
49
53
  controls: true,
@@ -54,6 +58,9 @@
54
58
  // Display the page number of the current slide
55
59
  slideNumber: false,
56
60
 
61
+ // Determine which displays to show the slide number on
62
+ showSlideNumber: 'all',
63
+
57
64
  // Push each slide change to the browser history
58
65
  history: false,
59
66
 
@@ -78,6 +85,9 @@
78
85
  // Change the presentation direction to be RTL
79
86
  rtl: false,
80
87
 
88
+ // Randomizes the order of slides each time the presentation loads
89
+ shuffle: false,
90
+
81
91
  // Turns fragments on and off globally
82
92
  fragments: true,
83
93
 
@@ -85,13 +95,22 @@
85
95
  // i.e. contained within a limited portion of the screen
86
96
  embedded: false,
87
97
 
88
- // Flags if we should show a help overlay when the questionmark
98
+ // Flags if we should show a help overlay when the question-mark
89
99
  // key is pressed
90
100
  help: true,
91
101
 
92
102
  // Flags if it should be possible to pause the presentation (blackout)
93
103
  pause: true,
94
104
 
105
+ // Flags if speaker notes should be visible to all viewers
106
+ showNotes: false,
107
+
108
+ // Global override for autolaying embedded media (video/audio/iframe)
109
+ // - null: Media will only autoplay if data-autoplay is present
110
+ // - true: All media will autoplay, regardless of individual setting
111
+ // - false: No media will autoplay, regardless of individual setting
112
+ autoPlayMedia: null,
113
+
95
114
  // Number of milliseconds between automatically proceeding to the
96
115
  // next slide, disabled when set to 0, this value can be overwritten
97
116
  // by using a data-autoslide attribute on your slides
@@ -100,6 +119,9 @@
100
119
  // Stop auto-sliding after user input
101
120
  autoSlideStoppable: true,
102
121
 
122
+ // Use this method for navigation when auto-sliding (defaults to navigateNext)
123
+ autoSlideMethod: null,
124
+
103
125
  // Enable slide navigation via mouse wheel
104
126
  mouseWheel: false,
105
127
 
@@ -118,7 +140,7 @@
118
140
  // Dispatches all reveal.js events to the parent window through postMessage
119
141
  postMessageEvents: false,
120
142
 
121
- // Focuses body when page changes visiblity to ensure keyboard shortcuts work
143
+ // Focuses body when page changes visibility to ensure keyboard shortcuts work
122
144
  focusBodyOnPageVisibilityChange: true,
123
145
 
124
146
  // Transition style
@@ -136,17 +158,45 @@
136
158
  // Parallax background size
137
159
  parallaxBackgroundSize: '', // CSS syntax, e.g. "3000px 2000px"
138
160
 
161
+ // Amount of pixels to move the parallax background per slide step
162
+ parallaxBackgroundHorizontal: null,
163
+ parallaxBackgroundVertical: null,
164
+
165
+ // The maximum number of pages a single slide can expand onto when printing
166
+ // to PDF, unlimited by default
167
+ pdfMaxPagesPerSlide: Number.POSITIVE_INFINITY,
168
+
169
+ // Offset used to reduce the height of content within exported PDF pages.
170
+ // This exists to account for environment differences based on how you
171
+ // print to PDF. CLI printing options, like phantomjs and wkpdf, can end
172
+ // on precisely the total height of the document whereas in-browser
173
+ // printing has to end one pixel before.
174
+ pdfPageHeightOffset: -1,
175
+
139
176
  // Number of slides away from the current that are visible
140
177
  viewDistance: 3,
141
178
 
179
+ // The display mode that will be used to show slides
180
+ display: 'block',
181
+
142
182
  // Script dependencies to load
143
183
  dependencies: []
144
184
 
145
185
  },
146
186
 
187
+ // Flags if Reveal.initialize() has been called
188
+ initialized = false,
189
+
147
190
  // Flags if reveal.js is loaded (has dispatched the 'ready' event)
148
191
  loaded = false,
149
192
 
193
+ // Flags if the overview mode is currently active
194
+ overview = false,
195
+
196
+ // Holds the dimensions of our overview slides, including margins
197
+ overviewSlideWidth = null,
198
+ overviewSlideHeight = null,
199
+
150
200
  // The horizontal and vertical index of the currently active slide
151
201
  indexh,
152
202
  indexv,
@@ -165,6 +215,10 @@
165
215
  // The current scale of the presentation (see width/height config)
166
216
  scale = 1,
167
217
 
218
+ // CSS transform that is currently applied to the slides container,
219
+ // split into two groups
220
+ slidesTransform = { layout: '', overview: '' },
221
+
168
222
  // Cached references to DOM elements
169
223
  dom = {},
170
224
 
@@ -174,6 +228,9 @@
174
228
  // Client is a mobile device, see #checkCapabilities()
175
229
  isMobileDevice,
176
230
 
231
+ // Client is a desktop Chrome, see #checkCapabilities()
232
+ isChrome,
233
+
177
234
  // Throttles mouse wheel navigation
178
235
  lastMouseWheelStep = 0,
179
236
 
@@ -222,19 +279,28 @@
222
279
  */
223
280
  function initialize( options ) {
224
281
 
282
+ // Make sure we only initialize once
283
+ if( initialized === true ) return;
284
+
285
+ initialized = true;
286
+
225
287
  checkCapabilities();
226
288
 
227
289
  if( !features.transforms2d && !features.transforms3d ) {
228
290
  document.body.setAttribute( 'class', 'no-transforms' );
229
291
 
230
- // Since JS won't be running any further, we need to load all
231
- // images that were intended to lazy load now
232
- var images = document.getElementsByTagName( 'img' );
233
- for( var i = 0, len = images.length; i < len; i++ ) {
234
- var image = images[i];
235
- if( image.getAttribute( 'data-src' ) ) {
236
- image.setAttribute( 'src', image.getAttribute( 'data-src' ) );
237
- image.removeAttribute( 'data-src' );
292
+ // Since JS won't be running any further, we load all lazy
293
+ // loading elements upfront
294
+ var images = toArray( document.getElementsByTagName( 'img' ) ),
295
+ iframes = toArray( document.getElementsByTagName( 'iframe' ) );
296
+
297
+ var lazyLoadable = images.concat( iframes );
298
+
299
+ for( var i = 0, len = lazyLoadable.length; i < len; i++ ) {
300
+ var element = lazyLoadable[i];
301
+ if( element.getAttribute( 'data-src' ) ) {
302
+ element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
303
+ element.removeAttribute( 'data-src' );
238
304
  }
239
305
  }
240
306
 
@@ -274,26 +340,37 @@
274
340
  */
275
341
  function checkCapabilities() {
276
342
 
277
- features.transforms3d = 'WebkitPerspective' in document.body.style ||
278
- 'MozPerspective' in document.body.style ||
279
- 'msPerspective' in document.body.style ||
280
- 'OPerspective' in document.body.style ||
281
- 'perspective' in document.body.style;
343
+ isMobileDevice = /(iphone|ipod|ipad|android)/gi.test( UA );
344
+ isChrome = /chrome/i.test( UA ) && !/edge/i.test( UA );
345
+
346
+ var testElement = document.createElement( 'div' );
282
347
 
283
- features.transforms2d = 'WebkitTransform' in document.body.style ||
284
- 'MozTransform' in document.body.style ||
285
- 'msTransform' in document.body.style ||
286
- 'OTransform' in document.body.style ||
287
- 'transform' in document.body.style;
348
+ features.transforms3d = 'WebkitPerspective' in testElement.style ||
349
+ 'MozPerspective' in testElement.style ||
350
+ 'msPerspective' in testElement.style ||
351
+ 'OPerspective' in testElement.style ||
352
+ 'perspective' in testElement.style;
353
+
354
+ features.transforms2d = 'WebkitTransform' in testElement.style ||
355
+ 'MozTransform' in testElement.style ||
356
+ 'msTransform' in testElement.style ||
357
+ 'OTransform' in testElement.style ||
358
+ 'transform' in testElement.style;
288
359
 
289
360
  features.requestAnimationFrameMethod = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame;
290
361
  features.requestAnimationFrame = typeof features.requestAnimationFrameMethod === 'function';
291
362
 
292
363
  features.canvas = !!document.createElement( 'canvas' ).getContext;
293
364
 
294
- features.touch = !!( 'ontouchstart' in window );
365
+ // Transitions in the overview are disabled in desktop and
366
+ // Safari due to lag
367
+ features.overviewTransitions = !/Version\/[\d\.]+.*Safari/.test( UA );
295
368
 
296
- isMobileDevice = navigator.userAgent.match( /(iphone|ipod|ipad|android)/gi );
369
+ // Flags if we should use zoom instead of transform to scale
370
+ // up slides. Zoom produces crisper results but has a lot of
371
+ // xbrowser quirks so we only use it in whitelsited browsers.
372
+ features.zoom = 'zoom' in testElement.style && !isMobileDevice &&
373
+ ( isChrome || /Version\/[\d\.]+.*Safari/.test( UA ) );
297
374
 
298
375
  }
299
376
 
@@ -373,6 +450,9 @@
373
450
  // Listen to messages posted to this window
374
451
  setupPostMessage();
375
452
 
453
+ // Prevent the slides from being scrolled out of view
454
+ setupScrollPrevention();
455
+
376
456
  // Resets all vertical slides so that only the first is visible
377
457
  resetVerticalSlides();
378
458
 
@@ -393,6 +473,8 @@
393
473
 
394
474
  loaded = true;
395
475
 
476
+ dom.wrapper.classList.add( 'ready' );
477
+
396
478
  dispatchEvent( 'ready', {
397
479
  'indexh': indexh,
398
480
  'indexv': indexv,
@@ -435,20 +517,24 @@
435
517
 
436
518
  // Arrow controls
437
519
  createSingletonNode( dom.wrapper, 'aside', 'controls',
438
- '<div class="navigate-left"></div>' +
439
- '<div class="navigate-right"></div>' +
440
- '<div class="navigate-up"></div>' +
441
- '<div class="navigate-down"></div>' );
520
+ '<button class="navigate-left" aria-label="previous slide"></button>' +
521
+ '<button class="navigate-right" aria-label="next slide"></button>' +
522
+ '<button class="navigate-up" aria-label="above slide"></button>' +
523
+ '<button class="navigate-down" aria-label="below slide"></button>' );
442
524
 
443
525
  // Slide number
444
526
  dom.slideNumber = createSingletonNode( dom.wrapper, 'div', 'slide-number', '' );
445
527
 
528
+ // Element containing notes that are visible to the audience
529
+ dom.speakerNotes = createSingletonNode( dom.wrapper, 'div', 'speaker-notes', null );
530
+ dom.speakerNotes.setAttribute( 'data-prevent-swipe', '' );
531
+ dom.speakerNotes.setAttribute( 'tabindex', '0' );
532
+
446
533
  // Overlay graphic which is displayed during the paused mode
447
534
  createSingletonNode( dom.wrapper, 'div', 'pause-overlay', null );
448
535
 
449
536
  // Cache references to elements
450
537
  dom.controls = document.querySelector( '.reveal .controls' );
451
- dom.theme = document.querySelector( '#theme' );
452
538
 
453
539
  dom.wrapper.setAttribute( 'role', 'application' );
454
540
 
@@ -467,6 +553,8 @@
467
553
  * Creates a hidden div with role aria-live to announce the
468
554
  * current slide content. Hide the div off-screen to make it
469
555
  * available only to Assistive Technologies.
556
+ *
557
+ * @return {HTMLElement}
470
558
  */
471
559
  function createStatusDiv() {
472
560
 
@@ -476,7 +564,7 @@
476
564
  statusDiv.style.position = 'absolute';
477
565
  statusDiv.style.height = '1px';
478
566
  statusDiv.style.width = '1px';
479
- statusDiv.style.overflow ='hidden';
567
+ statusDiv.style.overflow = 'hidden';
480
568
  statusDiv.style.clip = 'rect( 1px, 1px, 1px, 1px )';
481
569
  statusDiv.setAttribute( 'id', 'aria-status-div' );
482
570
  statusDiv.setAttribute( 'aria-live', 'polite' );
@@ -487,6 +575,38 @@
487
575
 
488
576
  }
489
577
 
578
+ /**
579
+ * Converts the given HTML element into a string of text
580
+ * that can be announced to a screen reader. Hidden
581
+ * elements are excluded.
582
+ */
583
+ function getStatusText( node ) {
584
+
585
+ var text = '';
586
+
587
+ // Text node
588
+ if( node.nodeType === 3 ) {
589
+ text += node.textContent;
590
+ }
591
+ // Element node
592
+ else if( node.nodeType === 1 ) {
593
+
594
+ var isAriaHidden = node.getAttribute( 'aria-hidden' );
595
+ var isDisplayHidden = window.getComputedStyle( node )['display'] === 'none';
596
+ if( isAriaHidden !== 'true' && !isDisplayHidden ) {
597
+
598
+ toArray( node.childNodes ).forEach( function( child ) {
599
+ text += getStatusText( child );
600
+ } );
601
+
602
+ }
603
+
604
+ }
605
+
606
+ return text;
607
+
608
+ }
609
+
490
610
  /**
491
611
  * Configures the presentation for printing to a static
492
612
  * PDF.
@@ -497,14 +617,14 @@
497
617
 
498
618
  // Dimensions of the PDF pages
499
619
  var pageWidth = Math.floor( slideSize.width * ( 1 + config.margin ) ),
500
- pageHeight = Math.floor( slideSize.height * ( 1 + config.margin ) );
620
+ pageHeight = Math.floor( slideSize.height * ( 1 + config.margin ) );
501
621
 
502
622
  // Dimensions of slides within the pages
503
623
  var slideWidth = slideSize.width,
504
624
  slideHeight = slideSize.height;
505
625
 
506
626
  // Let the browser know what page size we want to print
507
- injectStyleSheet( '@page{size:'+ pageWidth +'px '+ pageHeight +'px; margin: 0;}' );
627
+ injectStyleSheet( '@page{size:'+ pageWidth +'px '+ pageHeight +'px; margin: 0px;}' );
508
628
 
509
629
  // Limit the size of certain elements to the dimensions of the slide
510
630
  injectStyleSheet( '.reveal section>img, .reveal section>video, .reveal section>iframe{max-width: '+ slideWidth +'px; max-height:'+ slideHeight +'px}' );
@@ -513,6 +633,22 @@
513
633
  document.body.style.width = pageWidth + 'px';
514
634
  document.body.style.height = pageHeight + 'px';
515
635
 
636
+ // Make sure stretch elements fit on slide
637
+ layoutSlideContents( slideWidth, slideHeight );
638
+
639
+ // Add each slide's index as attributes on itself, we need these
640
+ // indices to generate slide numbers below
641
+ toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( hslide, h ) {
642
+ hslide.setAttribute( 'data-index-h', h );
643
+
644
+ if( hslide.classList.contains( 'stack' ) ) {
645
+ toArray( hslide.querySelectorAll( 'section' ) ).forEach( function( vslide, v ) {
646
+ vslide.setAttribute( 'data-index-h', h );
647
+ vslide.setAttribute( 'data-index-v', v );
648
+ } );
649
+ }
650
+ } );
651
+
516
652
  // Slide and slide background layout
517
653
  toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
518
654
 
@@ -523,27 +659,73 @@
523
659
  var left = ( pageWidth - slideWidth ) / 2,
524
660
  top = ( pageHeight - slideHeight ) / 2;
525
661
 
526
- var contentHeight = getAbsoluteHeight( slide );
662
+ var contentHeight = slide.scrollHeight;
527
663
  var numberOfPages = Math.max( Math.ceil( contentHeight / pageHeight ), 1 );
528
664
 
665
+ // Adhere to configured pages per slide limit
666
+ numberOfPages = Math.min( numberOfPages, config.pdfMaxPagesPerSlide );
667
+
529
668
  // Center slides vertically
530
669
  if( numberOfPages === 1 && config.center || slide.classList.contains( 'center' ) ) {
531
670
  top = Math.max( ( pageHeight - contentHeight ) / 2, 0 );
532
671
  }
533
672
 
673
+ // Wrap the slide in a page element and hide its overflow
674
+ // so that no page ever flows onto another
675
+ var page = document.createElement( 'div' );
676
+ page.className = 'pdf-page';
677
+ page.style.height = ( ( pageHeight + config.pdfPageHeightOffset ) * numberOfPages ) + 'px';
678
+ slide.parentNode.insertBefore( page, slide );
679
+ page.appendChild( slide );
680
+
534
681
  // Position the slide inside of the page
535
682
  slide.style.left = left + 'px';
536
683
  slide.style.top = top + 'px';
537
684
  slide.style.width = slideWidth + 'px';
538
685
 
539
- // TODO Backgrounds need to be multiplied when the slide
540
- // stretches over multiple pages
541
- var background = slide.querySelector( '.slide-background' );
542
- if( background ) {
543
- background.style.width = pageWidth + 'px';
544
- background.style.height = ( pageHeight * numberOfPages ) + 'px';
545
- background.style.top = -top + 'px';
546
- background.style.left = -left + 'px';
686
+ if( slide.slideBackgroundElement ) {
687
+ page.insertBefore( slide.slideBackgroundElement, slide );
688
+ }
689
+
690
+ // Inject notes if `showNotes` is enabled
691
+ if( config.showNotes ) {
692
+
693
+ // Are there notes for this slide?
694
+ var notes = getSlideNotes( slide );
695
+ if( notes ) {
696
+
697
+ var notesSpacing = 8;
698
+ var notesLayout = typeof config.showNotes === 'string' ? config.showNotes : 'inline';
699
+ var notesElement = document.createElement( 'div' );
700
+ notesElement.classList.add( 'speaker-notes' );
701
+ notesElement.classList.add( 'speaker-notes-pdf' );
702
+ notesElement.setAttribute( 'data-layout', notesLayout );
703
+ notesElement.innerHTML = notes;
704
+
705
+ if( notesLayout === 'separate-page' ) {
706
+ page.parentNode.insertBefore( notesElement, page.nextSibling );
707
+ }
708
+ else {
709
+ notesElement.style.left = notesSpacing + 'px';
710
+ notesElement.style.bottom = notesSpacing + 'px';
711
+ notesElement.style.width = ( pageWidth - notesSpacing*2 ) + 'px';
712
+ page.appendChild( notesElement );
713
+ }
714
+
715
+ }
716
+
717
+ }
718
+
719
+ // Inject slide numbers if `slideNumbers` are enabled
720
+ if( config.slideNumber && /all|print/i.test( config.showSlideNumber ) ) {
721
+ var slideNumberH = parseInt( slide.getAttribute( 'data-index-h' ), 10 ) + 1,
722
+ slideNumberV = parseInt( slide.getAttribute( 'data-index-v' ), 10 ) + 1;
723
+
724
+ var numberElement = document.createElement( 'div' );
725
+ numberElement.classList.add( 'slide-number' );
726
+ numberElement.classList.add( 'slide-number-pdf' );
727
+ numberElement.innerHTML = formatSlideNumber( slideNumberH, '.', slideNumberV );
728
+ page.appendChild( numberElement );
547
729
  }
548
730
  }
549
731
 
@@ -554,12 +736,42 @@
554
736
  fragment.classList.add( 'visible' );
555
737
  } );
556
738
 
739
+ // Notify subscribers that the PDF layout is good to go
740
+ dispatchEvent( 'pdf-ready' );
741
+
742
+ }
743
+
744
+ /**
745
+ * This is an unfortunate necessity. Some actions – such as
746
+ * an input field being focused in an iframe or using the
747
+ * keyboard to expand text selection beyond the bounds of
748
+ * a slide – can trigger our content to be pushed out of view.
749
+ * This scrolling can not be prevented by hiding overflow in
750
+ * CSS (we already do) so we have to resort to repeatedly
751
+ * checking if the slides have been offset :(
752
+ */
753
+ function setupScrollPrevention() {
754
+
755
+ setInterval( function() {
756
+ if( dom.wrapper.scrollTop !== 0 || dom.wrapper.scrollLeft !== 0 ) {
757
+ dom.wrapper.scrollTop = 0;
758
+ dom.wrapper.scrollLeft = 0;
759
+ }
760
+ }, 1000 );
761
+
557
762
  }
558
763
 
559
764
  /**
560
765
  * Creates an HTML element and returns a reference to it.
561
766
  * If the element already exists the existing instance will
562
767
  * be returned.
768
+ *
769
+ * @param {HTMLElement} container
770
+ * @param {string} tagname
771
+ * @param {string} classname
772
+ * @param {string} innerHTML
773
+ *
774
+ * @return {HTMLElement}
563
775
  */
564
776
  function createSingletonNode( container, tagname, classname, innerHTML ) {
565
777
 
@@ -603,24 +815,12 @@
603
815
  // Iterate over all horizontal slides
604
816
  toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( slideh ) {
605
817
 
606
- var backgroundStack;
607
-
608
- if( printMode ) {
609
- backgroundStack = createBackground( slideh, slideh );
610
- }
611
- else {
612
- backgroundStack = createBackground( slideh, dom.background );
613
- }
818
+ var backgroundStack = createBackground( slideh, dom.background );
614
819
 
615
820
  // Iterate over all vertical slides
616
821
  toArray( slideh.querySelectorAll( 'section' ) ).forEach( function( slidev ) {
617
822
 
618
- if( printMode ) {
619
- createBackground( slidev, slidev );
620
- }
621
- else {
622
- createBackground( slidev, backgroundStack );
623
- }
823
+ createBackground( slidev, backgroundStack );
624
824
 
625
825
  backgroundStack.classList.add( 'stack' );
626
826
 
@@ -658,6 +858,7 @@
658
858
  * @param {HTMLElement} slide
659
859
  * @param {HTMLElement} container The element that the background
660
860
  * should be appended to
861
+ * @return {HTMLElement} New background div
661
862
  */
662
863
  function createBackground( slide, container ) {
663
864
 
@@ -680,7 +881,7 @@
680
881
 
681
882
  if( data.background ) {
682
883
  // Auto-wrap image urls in url(...)
683
- if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp)$/gi.test( data.background ) ) {
884
+ if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp)([?#]|$)/gi.test( data.background ) ) {
684
885
  slide.setAttribute( 'data-background-image', data.background );
685
886
  }
686
887
  else {
@@ -705,6 +906,7 @@
705
906
 
706
907
  // Additional and optional background properties
707
908
  if( data.backgroundSize ) element.style.backgroundSize = data.backgroundSize;
909
+ if( data.backgroundSize ) element.setAttribute( 'data-background-size', data.backgroundSize );
708
910
  if( data.backgroundColor ) element.style.backgroundColor = data.backgroundColor;
709
911
  if( data.backgroundRepeat ) element.style.backgroundRepeat = data.backgroundRepeat;
710
912
  if( data.backgroundPosition ) element.style.backgroundPosition = data.backgroundPosition;
@@ -716,18 +918,20 @@
716
918
  slide.classList.remove( 'has-dark-background' );
717
919
  slide.classList.remove( 'has-light-background' );
718
920
 
921
+ slide.slideBackgroundElement = element;
922
+
719
923
  // If this slide has a background color, add a class that
720
924
  // signals if it is light or dark. If the slide has no background
721
925
  // color, no class will be set
722
- var computedBackgroundColor = window.getComputedStyle( element ).backgroundColor;
723
- if( computedBackgroundColor ) {
724
- var rgb = colorToRgb( computedBackgroundColor );
926
+ var computedBackgroundStyle = window.getComputedStyle( element );
927
+ if( computedBackgroundStyle && computedBackgroundStyle.backgroundColor ) {
928
+ var rgb = colorToRgb( computedBackgroundStyle.backgroundColor );
725
929
 
726
930
  // Ignore fully transparent backgrounds. Some browsers return
727
931
  // rgba(0,0,0,0) when reading the computed background color of
728
932
  // an element with no background
729
933
  if( rgb && rgb.a !== 0 ) {
730
- if( colorBrightness( computedBackgroundColor ) < 128 ) {
934
+ if( colorBrightness( computedBackgroundStyle.backgroundColor ) < 128 ) {
731
935
  slide.classList.add( 'has-dark-background' );
732
936
  }
733
937
  else {
@@ -757,7 +961,7 @@
757
961
  var data = event.data;
758
962
 
759
963
  // Make sure we're dealing with JSON
760
- if( data.charAt( 0 ) === '{' && data.charAt( data.length - 1 ) === '}' ) {
964
+ if( typeof data === 'string' && data.charAt( 0 ) === '{' && data.charAt( data.length - 1 ) === '}' ) {
761
965
  data = JSON.parse( data );
762
966
 
763
967
  // Check if the requested method can be found
@@ -773,6 +977,8 @@
773
977
  /**
774
978
  * Applies the configuration settings from the config
775
979
  * object. May be called multiple times.
980
+ *
981
+ * @param {object} options
776
982
  */
777
983
  function configure( options ) {
778
984
 
@@ -795,6 +1001,10 @@
795
1001
  dom.controls.style.display = config.controls ? 'block' : 'none';
796
1002
  dom.progress.style.display = config.progress ? 'block' : 'none';
797
1003
 
1004
+ if( config.shuffle ) {
1005
+ shuffle();
1006
+ }
1007
+
798
1008
  if( config.rtl ) {
799
1009
  dom.wrapper.classList.add( 'rtl' );
800
1010
  }
@@ -814,6 +1024,14 @@
814
1024
  resume();
815
1025
  }
816
1026
 
1027
+ if( config.showNotes ) {
1028
+ dom.speakerNotes.classList.add( 'visible' );
1029
+ dom.speakerNotes.setAttribute( 'data-layout', typeof config.showNotes === 'string' ? config.showNotes : 'inline' );
1030
+ }
1031
+ else {
1032
+ dom.speakerNotes.classList.remove( 'visible' );
1033
+ }
1034
+
817
1035
  if( config.mouseWheel ) {
818
1036
  document.addEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF
819
1037
  document.addEventListener( 'mousewheel', onDocumentMouseScroll, false );
@@ -834,10 +1052,11 @@
834
1052
  // Iframe link previews
835
1053
  if( config.previewLinks ) {
836
1054
  enablePreviewLinks();
1055
+ disablePreviewLinks( '[data-preview-link=false]' );
837
1056
  }
838
1057
  else {
839
1058
  disablePreviewLinks();
840
- enablePreviewLinks( '[data-preview-link]' );
1059
+ enablePreviewLinks( '[data-preview-link]:not([data-preview-link=false])' );
841
1060
  }
842
1061
 
843
1062
  // Remove existing auto-slide controls
@@ -864,6 +1083,19 @@
864
1083
  } );
865
1084
  }
866
1085
 
1086
+ // Slide numbers
1087
+ var slideNumberDisplay = 'none';
1088
+ if( config.slideNumber && !isPrintingPDF() ) {
1089
+ if( config.showSlideNumber === 'all' ) {
1090
+ slideNumberDisplay = 'block';
1091
+ }
1092
+ else if( config.showSlideNumber === 'speaker' && isSpeakerNotes() ) {
1093
+ slideNumberDisplay = 'block';
1094
+ }
1095
+ }
1096
+
1097
+ dom.slideNumber.style.display = slideNumberDisplay;
1098
+
867
1099
  sync();
868
1100
 
869
1101
  }
@@ -931,7 +1163,7 @@
931
1163
 
932
1164
  // Only support touch for Android, fixes double navigations in
933
1165
  // stock browser
934
- if( navigator.userAgent.match( /android/gi ) ) {
1166
+ if( UA.match( /android/gi ) ) {
935
1167
  pointerEvents = [ 'touchstart' ];
936
1168
  }
937
1169
 
@@ -993,6 +1225,9 @@
993
1225
  /**
994
1226
  * Extend object a with the properties of object b.
995
1227
  * If there's a conflict, object b takes precedence.
1228
+ *
1229
+ * @param {object} a
1230
+ * @param {object} b
996
1231
  */
997
1232
  function extend( a, b ) {
998
1233
 
@@ -1004,6 +1239,9 @@
1004
1239
 
1005
1240
  /**
1006
1241
  * Converts the target object to an array.
1242
+ *
1243
+ * @param {object} o
1244
+ * @return {object[]}
1007
1245
  */
1008
1246
  function toArray( o ) {
1009
1247
 
@@ -1013,6 +1251,9 @@
1013
1251
 
1014
1252
  /**
1015
1253
  * Utility for deserializing a value.
1254
+ *
1255
+ * @param {*} value
1256
+ * @return {*}
1016
1257
  */
1017
1258
  function deserialize( value ) {
1018
1259
 
@@ -1020,7 +1261,7 @@
1020
1261
  if( value === 'null' ) return null;
1021
1262
  else if( value === 'true' ) return true;
1022
1263
  else if( value === 'false' ) return false;
1023
- else if( value.match( /^\d+$/ ) ) return parseFloat( value );
1264
+ else if( value.match( /^[\d\.]+$/ ) ) return parseFloat( value );
1024
1265
  }
1025
1266
 
1026
1267
  return value;
@@ -1031,8 +1272,10 @@
1031
1272
  * Measures the distance in pixels between point a
1032
1273
  * and point b.
1033
1274
  *
1034
- * @param {Object} a point with x/y properties
1035
- * @param {Object} b point with x/y properties
1275
+ * @param {object} a point with x/y properties
1276
+ * @param {object} b point with x/y properties
1277
+ *
1278
+ * @return {number}
1036
1279
  */
1037
1280
  function distanceBetween( a, b ) {
1038
1281
 
@@ -1045,19 +1288,46 @@
1045
1288
 
1046
1289
  /**
1047
1290
  * Applies a CSS transform to the target element.
1291
+ *
1292
+ * @param {HTMLElement} element
1293
+ * @param {string} transform
1048
1294
  */
1049
1295
  function transformElement( element, transform ) {
1050
1296
 
1051
1297
  element.style.WebkitTransform = transform;
1052
1298
  element.style.MozTransform = transform;
1053
1299
  element.style.msTransform = transform;
1054
- element.style.OTransform = transform;
1055
1300
  element.style.transform = transform;
1056
1301
 
1057
1302
  }
1058
1303
 
1304
+ /**
1305
+ * Applies CSS transforms to the slides container. The container
1306
+ * is transformed from two separate sources: layout and the overview
1307
+ * mode.
1308
+ *
1309
+ * @param {object} transforms
1310
+ */
1311
+ function transformSlides( transforms ) {
1312
+
1313
+ // Pick up new transforms from arguments
1314
+ if( typeof transforms.layout === 'string' ) slidesTransform.layout = transforms.layout;
1315
+ if( typeof transforms.overview === 'string' ) slidesTransform.overview = transforms.overview;
1316
+
1317
+ // Apply the transforms to the slides container
1318
+ if( slidesTransform.layout ) {
1319
+ transformElement( dom.slides, slidesTransform.layout + ' ' + slidesTransform.overview );
1320
+ }
1321
+ else {
1322
+ transformElement( dom.slides, slidesTransform.overview );
1323
+ }
1324
+
1325
+ }
1326
+
1059
1327
  /**
1060
1328
  * Injects the given CSS styles into the DOM.
1329
+ *
1330
+ * @param {string} value
1061
1331
  */
1062
1332
  function injectStyleSheet( value ) {
1063
1333
 
@@ -1074,13 +1344,55 @@
1074
1344
  }
1075
1345
 
1076
1346
  /**
1077
- * Measures the distance in pixels between point a and point b.
1347
+ * Find the closest parent that matches the given
1348
+ * selector.
1349
+ *
1350
+ * @param {HTMLElement} target The child element
1351
+ * @param {String} selector The CSS selector to match
1352
+ * the parents against
1353
+ *
1354
+ * @return {HTMLElement} The matched parent or null
1355
+ * if no matching parent was found
1356
+ */
1357
+ function closestParent( target, selector ) {
1358
+
1359
+ var parent = target.parentNode;
1360
+
1361
+ while( parent ) {
1362
+
1363
+ // There's some overhead doing this each time, we don't
1364
+ // want to rewrite the element prototype but should still
1365
+ // be enough to feature detect once at startup...
1366
+ var matchesMethod = parent.matches || parent.matchesSelector || parent.msMatchesSelector;
1367
+
1368
+ // If we find a match, we're all set
1369
+ if( matchesMethod && matchesMethod.call( parent, selector ) ) {
1370
+ return parent;
1371
+ }
1372
+
1373
+ // Keep searching
1374
+ parent = parent.parentNode;
1375
+
1376
+ }
1377
+
1378
+ return null;
1379
+
1380
+ }
1381
+
1382
+ /**
1383
+ * Converts various color input formats to an {r:0,g:0,b:0} object.
1384
+ *
1385
+ * @param {string} color The string representation of a color
1386
+ * @example
1387
+ * colorToRgb('#000');
1388
+ * @example
1389
+ * colorToRgb('#000000');
1390
+ * @example
1391
+ * colorToRgb('rgb(0,0,0)');
1392
+ * @example
1393
+ * colorToRgb('rgba(0,0,0)');
1078
1394
  *
1079
- * @param {String} color The string representation of a color,
1080
- * the following formats are supported:
1081
- * - #000
1082
- * - #000000
1083
- * - rgb(0,0,0)
1395
+ * @return {{r: number, g: number, b: number, [a]: number}|null}
1084
1396
  */
1085
1397
  function colorToRgb( color ) {
1086
1398
 
@@ -1130,7 +1442,8 @@
1130
1442
  /**
1131
1443
  * Calculates brightness on a scale of 0-255.
1132
1444
  *
1133
- * @param color See colorStringToRgb for supported formats.
1445
+ * @param {string} color See colorToRgb for supported formats.
1446
+ * @see {@link colorToRgb}
1134
1447
  */
1135
1448
  function colorBrightness( color ) {
1136
1449
 
@@ -1144,46 +1457,14 @@
1144
1457
 
1145
1458
  }
1146
1459
 
1147
- /**
1148
- * Retrieves the height of the given element by looking
1149
- * at the position and height of its immediate children.
1150
- */
1151
- function getAbsoluteHeight( element ) {
1152
-
1153
- var height = 0;
1154
-
1155
- if( element ) {
1156
- var absoluteChildren = 0;
1157
-
1158
- toArray( element.childNodes ).forEach( function( child ) {
1159
-
1160
- if( typeof child.offsetTop === 'number' && child.style ) {
1161
- // Count # of abs children
1162
- if( window.getComputedStyle( child ).position === 'absolute' ) {
1163
- absoluteChildren += 1;
1164
- }
1165
-
1166
- height = Math.max( height, child.offsetTop + child.offsetHeight );
1167
- }
1168
-
1169
- } );
1170
-
1171
- // If there are no absolute children, use offsetHeight
1172
- if( absoluteChildren === 0 ) {
1173
- height = element.offsetHeight;
1174
- }
1175
-
1176
- }
1177
-
1178
- return height;
1179
-
1180
- }
1181
-
1182
1460
  /**
1183
1461
  * Returns the remaining height within the parent of the
1184
1462
  * target element.
1185
1463
  *
1186
1464
  * remaining height = [ configured parent height ] - [ current parent height ]
1465
+ *
1466
+ * @param {HTMLElement} element
1467
+ * @param {number} [height]
1187
1468
  */
1188
1469
  function getRemainingHeight( element, height ) {
1189
1470
 
@@ -1306,6 +1587,8 @@
1306
1587
 
1307
1588
  /**
1308
1589
  * Bind preview frame links.
1590
+ *
1591
+ * @param {string} [selector=a] - selector for anchors
1309
1592
  */
1310
1593
  function enablePreviewLinks( selector ) {
1311
1594
 
@@ -1322,9 +1605,9 @@
1322
1605
  /**
1323
1606
  * Unbind preview frame links.
1324
1607
  */
1325
- function disablePreviewLinks() {
1608
+ function disablePreviewLinks( selector ) {
1326
1609
 
1327
- var anchors = toArray( document.querySelectorAll( 'a' ) );
1610
+ var anchors = toArray( document.querySelectorAll( selector ? selector : 'a' ) );
1328
1611
 
1329
1612
  anchors.forEach( function( element ) {
1330
1613
  if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) {
@@ -1336,6 +1619,8 @@
1336
1619
 
1337
1620
  /**
1338
1621
  * Opens a preview window for the target URL.
1622
+ *
1623
+ * @param {string} url - url for preview iframe src
1339
1624
  */
1340
1625
  function showPreview( url ) {
1341
1626
 
@@ -1354,6 +1639,9 @@
1354
1639
  '<div class="spinner"></div>',
1355
1640
  '<div class="viewport">',
1356
1641
  '<iframe src="'+ url +'"></iframe>',
1642
+ '<small class="viewport-inner">',
1643
+ '<span class="x-frame-error">Unable to load iframe. This is likely due to the site\'s policy (x-frame-options).</span>',
1644
+ '</small>',
1357
1645
  '</div>'
1358
1646
  ].join('');
1359
1647
 
@@ -1377,7 +1665,29 @@
1377
1665
  }
1378
1666
 
1379
1667
  /**
1380
- * Opens a overlay window with help material.
1668
+ * Open or close help overlay window.
1669
+ *
1670
+ * @param {Boolean} [override] Flag which overrides the
1671
+ * toggle logic and forcibly sets the desired state. True means
1672
+ * help is open, false means it's closed.
1673
+ */
1674
+ function toggleHelp( override ){
1675
+
1676
+ if( typeof override === 'boolean' ) {
1677
+ override ? showHelp() : closeOverlay();
1678
+ }
1679
+ else {
1680
+ if( dom.overlay ) {
1681
+ closeOverlay();
1682
+ }
1683
+ else {
1684
+ showHelp();
1685
+ }
1686
+ }
1687
+ }
1688
+
1689
+ /**
1690
+ * Opens an overlay window with help material.
1381
1691
  */
1382
1692
  function showHelp() {
1383
1693
 
@@ -1443,10 +1753,8 @@
1443
1753
 
1444
1754
  var size = getComputedSlideSize();
1445
1755
 
1446
- var slidePadding = 20; // TODO Dig this out of DOM
1447
-
1448
1756
  // Layout the contents of the slides
1449
- layoutSlideContents( config.width, config.height, slidePadding );
1757
+ layoutSlideContents( config.width, config.height );
1450
1758
 
1451
1759
  dom.slides.style.width = size.width + 'px';
1452
1760
  dom.slides.style.height = size.height + 'px';
@@ -1465,20 +1773,28 @@
1465
1773
  dom.slides.style.top = '';
1466
1774
  dom.slides.style.bottom = '';
1467
1775
  dom.slides.style.right = '';
1468
- transformElement( dom.slides, '' );
1776
+ transformSlides( { layout: '' } );
1469
1777
  }
1470
1778
  else {
1471
- // Prefer zooming in desktop Chrome so that content remains crisp
1472
- if( !isMobileDevice && /chrome/i.test( navigator.userAgent ) && typeof dom.slides.style.zoom !== 'undefined' ) {
1779
+ // Prefer zoom for scaling up so that content remains crisp.
1780
+ // Don't use zoom to scale down since that can lead to shifts
1781
+ // in text layout/line breaks.
1782
+ if( scale > 1 && features.zoom ) {
1473
1783
  dom.slides.style.zoom = scale;
1784
+ dom.slides.style.left = '';
1785
+ dom.slides.style.top = '';
1786
+ dom.slides.style.bottom = '';
1787
+ dom.slides.style.right = '';
1788
+ transformSlides( { layout: '' } );
1474
1789
  }
1475
1790
  // Apply scale transform as a fallback
1476
1791
  else {
1792
+ dom.slides.style.zoom = '';
1477
1793
  dom.slides.style.left = '50%';
1478
1794
  dom.slides.style.top = '50%';
1479
1795
  dom.slides.style.bottom = 'auto';
1480
1796
  dom.slides.style.right = 'auto';
1481
- transformElement( dom.slides, 'translate(-50%, -50%) scale('+ scale +')' );
1797
+ transformSlides( { layout: 'translate(-50%, -50%) scale('+ scale +')' } );
1482
1798
  }
1483
1799
  }
1484
1800
 
@@ -1500,7 +1816,7 @@
1500
1816
  slide.style.top = 0;
1501
1817
  }
1502
1818
  else {
1503
- slide.style.top = Math.max( ( ( size.height - getAbsoluteHeight( slide ) ) / 2 ) - slidePadding, 0 ) + 'px';
1819
+ slide.style.top = Math.max( ( size.height - slide.scrollHeight ) / 2, 0 ) + 'px';
1504
1820
  }
1505
1821
  }
1506
1822
  else {
@@ -1512,6 +1828,10 @@
1512
1828
  updateProgress();
1513
1829
  updateParallax();
1514
1830
 
1831
+ if( isOverview() ) {
1832
+ updateOverview();
1833
+ }
1834
+
1515
1835
  }
1516
1836
 
1517
1837
  }
@@ -1519,8 +1839,11 @@
1519
1839
  /**
1520
1840
  * Applies layout logic to the contents of all slides in
1521
1841
  * the presentation.
1842
+ *
1843
+ * @param {string|number} width
1844
+ * @param {string|number} height
1522
1845
  */
1523
- function layoutSlideContents( width, height, padding ) {
1846
+ function layoutSlideContents( width, height ) {
1524
1847
 
1525
1848
  // Handle sizing of elements with the 'stretch' class
1526
1849
  toArray( dom.slides.querySelectorAll( 'section > .stretch' ) ).forEach( function( element ) {
@@ -1552,6 +1875,9 @@
1552
1875
  * Calculates the computed pixel size of our slides. These
1553
1876
  * values are based on the width and height configuration
1554
1877
  * options.
1878
+ *
1879
+ * @param {number} [presentationWidth=dom.wrapper.offsetWidth]
1880
+ * @param {number} [presentationHeight=dom.wrapper.offsetHeight]
1555
1881
  */
1556
1882
  function getComputedSlideSize( presentationWidth, presentationHeight ) {
1557
1883
 
@@ -1566,7 +1892,7 @@
1566
1892
  };
1567
1893
 
1568
1894
  // Reduce available space by margin
1569
- size.presentationWidth -= ( size.presentationHeight * config.margin );
1895
+ size.presentationWidth -= ( size.presentationWidth * config.margin );
1570
1896
  size.presentationHeight -= ( size.presentationHeight * config.margin );
1571
1897
 
1572
1898
  // Slide width may be a percentage of available width
@@ -1589,7 +1915,7 @@
1589
1915
  * from the stack.
1590
1916
  *
1591
1917
  * @param {HTMLElement} stack The vertical stack element
1592
- * @param {int} v Index to memorize
1918
+ * @param {string|number} [v=0] Index to memorize
1593
1919
  */
1594
1920
  function setPreviousVerticalIndex( stack, v ) {
1595
1921
 
@@ -1620,81 +1946,117 @@
1620
1946
  }
1621
1947
 
1622
1948
  /**
1623
- * Displays the overview of slides (quick nav) by
1624
- * scaling down and arranging all slide elements.
1625
- *
1626
- * Experimental feature, might be dropped if perf
1627
- * can't be improved.
1949
+ * Displays the overview of slides (quick nav) by scaling
1950
+ * down and arranging all slide elements.
1628
1951
  */
1629
1952
  function activateOverview() {
1630
1953
 
1631
1954
  // Only proceed if enabled in config
1632
- if( config.overview ) {
1955
+ if( config.overview && !isOverview() ) {
1956
+
1957
+ overview = true;
1958
+
1959
+ dom.wrapper.classList.add( 'overview' );
1960
+ dom.wrapper.classList.remove( 'overview-deactivating' );
1961
+
1962
+ if( features.overviewTransitions ) {
1963
+ setTimeout( function() {
1964
+ dom.wrapper.classList.add( 'overview-animated' );
1965
+ }, 1 );
1966
+ }
1633
1967
 
1634
1968
  // Don't auto-slide while in overview mode
1635
1969
  cancelAutoSlide();
1636
1970
 
1637
- var wasActive = dom.wrapper.classList.contains( 'overview' );
1971
+ // Move the backgrounds element into the slide container to
1972
+ // that the same scaling is applied
1973
+ dom.slides.appendChild( dom.background );
1638
1974
 
1639
- // Vary the depth of the overview based on screen size
1640
- var depth = window.innerWidth < 400 ? 1000 : 2500;
1975
+ // Clicking on an overview slide navigates to it
1976
+ toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
1977
+ if( !slide.classList.contains( 'stack' ) ) {
1978
+ slide.addEventListener( 'click', onOverviewSlideClicked, true );
1979
+ }
1980
+ } );
1641
1981
 
1642
- dom.wrapper.classList.add( 'overview' );
1643
- dom.wrapper.classList.remove( 'overview-deactivating' );
1982
+ // Calculate slide sizes
1983
+ var margin = 70;
1984
+ var slideSize = getComputedSlideSize();
1985
+ overviewSlideWidth = slideSize.width + margin;
1986
+ overviewSlideHeight = slideSize.height + margin;
1644
1987
 
1645
- var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR );
1988
+ // Reverse in RTL mode
1989
+ if( config.rtl ) {
1990
+ overviewSlideWidth = -overviewSlideWidth;
1991
+ }
1646
1992
 
1647
- for( var i = 0, len1 = horizontalSlides.length; i < len1; i++ ) {
1648
- var hslide = horizontalSlides[i],
1649
- hoffset = config.rtl ? -105 : 105;
1993
+ updateSlidesVisibility();
1994
+ layoutOverview();
1995
+ updateOverview();
1650
1996
 
1651
- hslide.setAttribute( 'data-index-h', i );
1997
+ layout();
1652
1998
 
1653
- // Apply CSS transform
1654
- transformElement( hslide, 'translateZ(-'+ depth +'px) translate(' + ( ( i - indexh ) * hoffset ) + '%, 0%)' );
1999
+ // Notify observers of the overview showing
2000
+ dispatchEvent( 'overviewshown', {
2001
+ 'indexh': indexh,
2002
+ 'indexv': indexv,
2003
+ 'currentSlide': currentSlide
2004
+ } );
1655
2005
 
1656
- if( hslide.classList.contains( 'stack' ) ) {
2006
+ }
1657
2007
 
1658
- var verticalSlides = hslide.querySelectorAll( 'section' );
2008
+ }
1659
2009
 
1660
- for( var j = 0, len2 = verticalSlides.length; j < len2; j++ ) {
1661
- var verticalIndex = i === indexh ? indexv : getPreviousVerticalIndex( hslide );
2010
+ /**
2011
+ * Uses CSS transforms to position all slides in a grid for
2012
+ * display inside of the overview mode.
2013
+ */
2014
+ function layoutOverview() {
1662
2015
 
1663
- var vslide = verticalSlides[j];
2016
+ // Layout slides
2017
+ toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( hslide, h ) {
2018
+ hslide.setAttribute( 'data-index-h', h );
2019
+ transformElement( hslide, 'translate3d(' + ( h * overviewSlideWidth ) + 'px, 0, 0)' );
1664
2020
 
1665
- vslide.setAttribute( 'data-index-h', i );
1666
- vslide.setAttribute( 'data-index-v', j );
2021
+ if( hslide.classList.contains( 'stack' ) ) {
1667
2022
 
1668
- // Apply CSS transform
1669
- transformElement( vslide, 'translate(0%, ' + ( ( j - verticalIndex ) * 105 ) + '%)' );
2023
+ toArray( hslide.querySelectorAll( 'section' ) ).forEach( function( vslide, v ) {
2024
+ vslide.setAttribute( 'data-index-h', h );
2025
+ vslide.setAttribute( 'data-index-v', v );
1670
2026
 
1671
- // Navigate to this slide on click
1672
- vslide.addEventListener( 'click', onOverviewSlideClicked, true );
1673
- }
2027
+ transformElement( vslide, 'translate3d(0, ' + ( v * overviewSlideHeight ) + 'px, 0)' );
2028
+ } );
1674
2029
 
1675
- }
1676
- else {
2030
+ }
2031
+ } );
1677
2032
 
1678
- // Navigate to this slide on click
1679
- hslide.addEventListener( 'click', onOverviewSlideClicked, true );
2033
+ // Layout slide backgrounds
2034
+ toArray( dom.background.childNodes ).forEach( function( hbackground, h ) {
2035
+ transformElement( hbackground, 'translate3d(' + ( h * overviewSlideWidth ) + 'px, 0, 0)' );
1680
2036
 
1681
- }
1682
- }
2037
+ toArray( hbackground.querySelectorAll( '.slide-background' ) ).forEach( function( vbackground, v ) {
2038
+ transformElement( vbackground, 'translate3d(0, ' + ( v * overviewSlideHeight ) + 'px, 0)' );
2039
+ } );
2040
+ } );
1683
2041
 
1684
- updateSlidesVisibility();
2042
+ }
1685
2043
 
1686
- layout();
2044
+ /**
2045
+ * Moves the overview viewport to the current slides.
2046
+ * Called each time the current slide changes.
2047
+ */
2048
+ function updateOverview() {
1687
2049
 
1688
- if( !wasActive ) {
1689
- // Notify observers of the overview showing
1690
- dispatchEvent( 'overviewshown', {
1691
- 'indexh': indexh,
1692
- 'indexv': indexv,
1693
- 'currentSlide': currentSlide
1694
- } );
1695
- }
2050
+ var vmin = Math.min( window.innerWidth, window.innerHeight );
2051
+ var scale = Math.max( vmin / 5, 150 ) / vmin;
1696
2052
 
1697
- }
2053
+ transformSlides( {
2054
+ overview: [
2055
+ 'scale('+ scale +')',
2056
+ 'translateX('+ ( -indexh * overviewSlideWidth ) +'px)',
2057
+ 'translateY('+ ( -indexv * overviewSlideHeight ) +'px)'
2058
+ ].join( ' ' )
2059
+ } );
1698
2060
 
1699
2061
  }
1700
2062
 
@@ -1707,7 +2069,10 @@
1707
2069
  // Only proceed if enabled in config
1708
2070
  if( config.overview ) {
1709
2071
 
2072
+ overview = false;
2073
+
1710
2074
  dom.wrapper.classList.remove( 'overview' );
2075
+ dom.wrapper.classList.remove( 'overview-animated' );
1711
2076
 
1712
2077
  // Temporarily add a class so that transitions can do different things
1713
2078
  // depending on whether they are exiting/entering overview, or just
@@ -1718,16 +2083,27 @@
1718
2083
  dom.wrapper.classList.remove( 'overview-deactivating' );
1719
2084
  }, 1 );
1720
2085
 
1721
- // Select all slides
2086
+ // Move the background element back out
2087
+ dom.wrapper.appendChild( dom.background );
2088
+
2089
+ // Clean up changes made to slides
1722
2090
  toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
1723
- // Resets all transforms to use the external styles
1724
2091
  transformElement( slide, '' );
1725
2092
 
1726
2093
  slide.removeEventListener( 'click', onOverviewSlideClicked, true );
1727
2094
  } );
1728
2095
 
2096
+ // Clean up changes made to backgrounds
2097
+ toArray( dom.background.querySelectorAll( '.slide-background' ) ).forEach( function( background ) {
2098
+ transformElement( background, '' );
2099
+ } );
2100
+
2101
+ transformSlides( { overview: '' } );
2102
+
1729
2103
  slide( indexh, indexv );
1730
2104
 
2105
+ layout();
2106
+
1731
2107
  cueAutoSlide();
1732
2108
 
1733
2109
  // Notify observers of the overview hiding
@@ -1743,7 +2119,7 @@
1743
2119
  /**
1744
2120
  * Toggles the slide overview mode on and off.
1745
2121
  *
1746
- * @param {Boolean} override Optional flag which overrides the
2122
+ * @param {Boolean} [override] Flag which overrides the
1747
2123
  * toggle logic and forcibly sets the desired state. True means
1748
2124
  * overview is open, false means it's closed.
1749
2125
  */
@@ -1766,7 +2142,7 @@
1766
2142
  */
1767
2143
  function isOverview() {
1768
2144
 
1769
- return dom.wrapper.classList.contains( 'overview' );
2145
+ return overview;
1770
2146
 
1771
2147
  }
1772
2148
 
@@ -1774,8 +2150,9 @@
1774
2150
  * Checks if the current or specified slide is vertical
1775
2151
  * (nested within another slide).
1776
2152
  *
1777
- * @param {HTMLElement} slide [optional] The slide to check
2153
+ * @param {HTMLElement} [slide=currentSlide] The slide to check
1778
2154
  * orientation of
2155
+ * @return {Boolean}
1779
2156
  */
1780
2157
  function isVerticalSlide( slide ) {
1781
2158
 
@@ -1794,10 +2171,10 @@
1794
2171
  */
1795
2172
  function enterFullscreen() {
1796
2173
 
1797
- var element = document.body;
2174
+ var element = document.documentElement;
1798
2175
 
1799
2176
  // Check which implementation is available
1800
- var requestMethod = element.requestFullScreen ||
2177
+ var requestMethod = element.requestFullscreen ||
1801
2178
  element.webkitRequestFullscreen ||
1802
2179
  element.webkitRequestFullScreen ||
1803
2180
  element.mozRequestFullScreen ||
@@ -1860,6 +2237,8 @@
1860
2237
 
1861
2238
  /**
1862
2239
  * Checks if we are currently in the paused mode.
2240
+ *
2241
+ * @return {Boolean}
1863
2242
  */
1864
2243
  function isPaused() {
1865
2244
 
@@ -1870,7 +2249,7 @@
1870
2249
  /**
1871
2250
  * Toggles the auto slide mode on and off.
1872
2251
  *
1873
- * @param {Boolean} override Optional flag which sets the desired state.
2252
+ * @param {Boolean} [override] Flag which sets the desired state.
1874
2253
  * True means autoplay starts, false means it stops.
1875
2254
  */
1876
2255
 
@@ -1888,6 +2267,8 @@
1888
2267
 
1889
2268
  /**
1890
2269
  * Checks if the auto slide mode is currently on.
2270
+ *
2271
+ * @return {Boolean}
1891
2272
  */
1892
2273
  function isAutoSliding() {
1893
2274
 
@@ -1900,11 +2281,11 @@
1900
2281
  * slide which matches the specified horizontal and vertical
1901
2282
  * indices.
1902
2283
  *
1903
- * @param {int} h Horizontal index of the target slide
1904
- * @param {int} v Vertical index of the target slide
1905
- * @param {int} f Optional index of a fragment within the
2284
+ * @param {number} [h=indexh] Horizontal index of the target slide
2285
+ * @param {number} [v=indexv] Vertical index of the target slide
2286
+ * @param {number} [f] Index of a fragment within the
1906
2287
  * target slide to activate
1907
- * @param {int} o Optional origin for use in multimaster environments
2288
+ * @param {number} [o] Origin for use in multimaster environments
1908
2289
  */
1909
2290
  function slide( h, v, f, o ) {
1910
2291
 
@@ -1914,9 +2295,12 @@
1914
2295
  // Query all horizontal slides in the deck
1915
2296
  var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR );
1916
2297
 
2298
+ // Abort if there are no slides
2299
+ if( horizontalSlides.length === 0 ) return;
2300
+
1917
2301
  // If no vertical index is specified and the upcoming slide is a
1918
2302
  // stack, resume at its previous vertical index
1919
- if( v === undefined ) {
2303
+ if( v === undefined && !isOverview() ) {
1920
2304
  v = getPreviousVerticalIndex( horizontalSlides[ h ] );
1921
2305
  }
1922
2306
 
@@ -1966,9 +2350,9 @@
1966
2350
  document.documentElement.classList.remove( stateBefore.pop() );
1967
2351
  }
1968
2352
 
1969
- // If the overview is active, re-activate it to update positions
2353
+ // Update the overview if it's currently active
1970
2354
  if( isOverview() ) {
1971
- activateOverview();
2355
+ updateOverview();
1972
2356
  }
1973
2357
 
1974
2358
  // Find the current horizontal slide and any possible vertical slides
@@ -2030,13 +2414,14 @@
2030
2414
  }
2031
2415
 
2032
2416
  // Announce the current slide contents, for screen readers
2033
- dom.statusDiv.textContent = currentSlide.textContent;
2417
+ dom.statusDiv.textContent = getStatusText( currentSlide );
2034
2418
 
2035
2419
  updateControls();
2036
2420
  updateProgress();
2037
2421
  updateBackground();
2038
2422
  updateParallax();
2039
2423
  updateSlideNumber();
2424
+ updateNotes();
2040
2425
 
2041
2426
  // Update the URL hash
2042
2427
  writeURL();
@@ -2075,12 +2460,25 @@
2075
2460
 
2076
2461
  updateControls();
2077
2462
  updateProgress();
2078
- updateBackground( true );
2079
2463
  updateSlideNumber();
2080
2464
  updateSlidesVisibility();
2465
+ updateBackground( true );
2466
+ updateNotes();
2081
2467
 
2082
2468
  formatEmbeddedContent();
2083
2469
 
2470
+ // Start or stop embedded content depending on global config
2471
+ if( config.autoPlayMedia === false ) {
2472
+ stopEmbeddedContent( currentSlide );
2473
+ }
2474
+ else {
2475
+ startEmbeddedContent( currentSlide );
2476
+ }
2477
+
2478
+ if( isOverview() ) {
2479
+ layoutOverview();
2480
+ }
2481
+
2084
2482
  }
2085
2483
 
2086
2484
  /**
@@ -2130,16 +2528,33 @@
2130
2528
 
2131
2529
  }
2132
2530
 
2531
+ /**
2532
+ * Randomly shuffles all slides in the deck.
2533
+ */
2534
+ function shuffle() {
2535
+
2536
+ var slides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
2537
+
2538
+ slides.forEach( function( slide ) {
2539
+
2540
+ // Insert this slide next to another random slide. This may
2541
+ // cause the slide to insert before itself but that's fine.
2542
+ dom.slides.insertBefore( slide, slides[ Math.floor( Math.random() * slides.length ) ] );
2543
+
2544
+ } );
2545
+
2546
+ }
2547
+
2133
2548
  /**
2134
2549
  * Updates one dimension of slides by showing the slide
2135
2550
  * with the specified index.
2136
2551
  *
2137
- * @param {String} selector A CSS selector that will fetch
2552
+ * @param {string} selector A CSS selector that will fetch
2138
2553
  * the group of slides we are working with
2139
- * @param {Number} index The index of the slide that should be
2554
+ * @param {number} index The index of the slide that should be
2140
2555
  * shown
2141
2556
  *
2142
- * @return {Number} The index of the slide that is now shown,
2557
+ * @return {number} The index of the slide that is now shown,
2143
2558
  * might differ from the passed in index if it was out of
2144
2559
  * bounds.
2145
2560
  */
@@ -2269,7 +2684,7 @@
2269
2684
  viewDistance = isOverview() ? 6 : 2;
2270
2685
  }
2271
2686
 
2272
- // Limit view distance on weaker devices
2687
+ // All slides need to be visible when exporting to PDF
2273
2688
  if( isPrintingPDF() ) {
2274
2689
  viewDistance = Number.MAX_VALUE;
2275
2690
  }
@@ -2280,8 +2695,14 @@
2280
2695
  var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) ),
2281
2696
  verticalSlidesLength = verticalSlides.length;
2282
2697
 
2283
- // Loops so that it measures 1 between the first and last slides
2284
- distanceX = Math.abs( ( ( indexh || 0 ) - x ) % ( horizontalSlidesLength - viewDistance ) ) || 0;
2698
+ // Determine how far away this slide is from the present
2699
+ distanceX = Math.abs( ( indexh || 0 ) - x ) || 0;
2700
+
2701
+ // If the presentation is looped, distance should measure
2702
+ // 1 between the first and last slides
2703
+ if( config.loop ) {
2704
+ distanceX = Math.abs( ( ( indexh || 0 ) - x ) % ( horizontalSlidesLength - viewDistance ) ) || 0;
2705
+ }
2285
2706
 
2286
2707
  // Show the horizontal slide if it's within the view distance
2287
2708
  if( distanceX < viewDistance ) {
@@ -2315,6 +2736,22 @@
2315
2736
 
2316
2737
  }
2317
2738
 
2739
+ /**
2740
+ * Pick up notes from the current slide and display them
2741
+ * to the viewer.
2742
+ *
2743
+ * @see {@link config.showNotes}
2744
+ */
2745
+ function updateNotes() {
2746
+
2747
+ if( config.showNotes && dom.speakerNotes && currentSlide && !isPrintingPDF() ) {
2748
+
2749
+ dom.speakerNotes.innerHTML = getSlideNotes() || '';
2750
+
2751
+ }
2752
+
2753
+ }
2754
+
2318
2755
  /**
2319
2756
  * Updates the progress bar to reflect the current slide.
2320
2757
  */
@@ -2331,19 +2768,65 @@
2331
2768
 
2332
2769
  /**
2333
2770
  * Updates the slide number div to reflect the current slide.
2771
+ *
2772
+ * The following slide number formats are available:
2773
+ * "h.v": horizontal . vertical slide number (default)
2774
+ * "h/v": horizontal / vertical slide number
2775
+ * "c": flattened slide number
2776
+ * "c/t": flattened slide number / total slides
2334
2777
  */
2335
2778
  function updateSlideNumber() {
2336
2779
 
2337
2780
  // Update slide number if enabled
2338
- if( config.slideNumber && dom.slideNumber) {
2781
+ if( config.slideNumber && dom.slideNumber ) {
2339
2782
 
2340
- // Display the number of the page using 'indexh - indexv' format
2341
- var indexString = indexh;
2342
- if( indexv > 0 ) {
2343
- indexString += ' - ' + indexv;
2783
+ var value = [];
2784
+ var format = 'h.v';
2785
+
2786
+ // Check if a custom number format is available
2787
+ if( typeof config.slideNumber === 'string' ) {
2788
+ format = config.slideNumber;
2789
+ }
2790
+
2791
+ switch( format ) {
2792
+ case 'c':
2793
+ value.push( getSlidePastCount() + 1 );
2794
+ break;
2795
+ case 'c/t':
2796
+ value.push( getSlidePastCount() + 1, '/', getTotalSlides() );
2797
+ break;
2798
+ case 'h/v':
2799
+ value.push( indexh + 1 );
2800
+ if( isVerticalSlide() ) value.push( '/', indexv + 1 );
2801
+ break;
2802
+ default:
2803
+ value.push( indexh + 1 );
2804
+ if( isVerticalSlide() ) value.push( '.', indexv + 1 );
2344
2805
  }
2345
2806
 
2346
- dom.slideNumber.innerHTML = indexString;
2807
+ dom.slideNumber.innerHTML = formatSlideNumber( value[0], value[1], value[2] );
2808
+ }
2809
+
2810
+ }
2811
+
2812
+ /**
2813
+ * Applies HTML formatting to a slide number before it's
2814
+ * written to the DOM.
2815
+ *
2816
+ * @param {number} a Current slide
2817
+ * @param {string} delimiter Character to separate slide numbers
2818
+ * @param {(number|*)} b Total slides
2819
+ * @return {string} HTML string fragment
2820
+ */
2821
+ function formatSlideNumber( a, delimiter, b ) {
2822
+
2823
+ if( typeof b === 'number' && !isNaN( b ) ) {
2824
+ return '<span class="slide-number-a">'+ a +'</span>' +
2825
+ '<span class="slide-number-delimiter">'+ delimiter +'</span>' +
2826
+ '<span class="slide-number-b">'+ b +'</span>';
2827
+ }
2828
+ else {
2829
+ return '<span class="slide-number-a">'+ a +'</span>';
2347
2830
  }
2348
2831
 
2349
2832
  }
@@ -2364,34 +2847,37 @@
2364
2847
  .concat( dom.controlsNext ).forEach( function( node ) {
2365
2848
  node.classList.remove( 'enabled' );
2366
2849
  node.classList.remove( 'fragmented' );
2850
+
2851
+ // Set 'disabled' attribute on all directions
2852
+ node.setAttribute( 'disabled', 'disabled' );
2367
2853
  } );
2368
2854
 
2369
- // Add the 'enabled' class to the available routes
2370
- if( routes.left ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'enabled' ); } );
2371
- if( routes.right ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'enabled' ); } );
2372
- if( routes.up ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'enabled' ); } );
2373
- if( routes.down ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'enabled' ); } );
2855
+ // Add the 'enabled' class to the available routes; remove 'disabled' attribute to enable buttons
2856
+ if( routes.left ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
2857
+ if( routes.right ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
2858
+ if( routes.up ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
2859
+ if( routes.down ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
2374
2860
 
2375
2861
  // Prev/next buttons
2376
- if( routes.left || routes.up ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'enabled' ); } );
2377
- if( routes.right || routes.down ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'enabled' ); } );
2862
+ if( routes.left || routes.up ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
2863
+ if( routes.right || routes.down ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
2378
2864
 
2379
2865
  // Highlight fragment directions
2380
2866
  if( currentSlide ) {
2381
2867
 
2382
2868
  // Always apply fragment decorator to prev/next buttons
2383
- if( fragments.prev ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
2384
- if( fragments.next ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
2869
+ if( fragments.prev ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
2870
+ if( fragments.next ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
2385
2871
 
2386
2872
  // Apply fragment decorators to directional buttons based on
2387
2873
  // what slide axis they are in
2388
2874
  if( isVerticalSlide( currentSlide ) ) {
2389
- if( fragments.prev ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
2390
- if( fragments.next ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
2875
+ if( fragments.prev ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
2876
+ if( fragments.next ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
2391
2877
  }
2392
2878
  else {
2393
- if( fragments.prev ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
2394
- if( fragments.next ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
2879
+ if( fragments.prev ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
2880
+ if( fragments.next ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
2395
2881
  }
2396
2882
 
2397
2883
  }
@@ -2402,7 +2888,7 @@
2402
2888
  * Updates the background elements to reflect the current
2403
2889
  * slide.
2404
2890
  *
2405
- * @param {Boolean} includeAll If true, the backgrounds of
2891
+ * @param {boolean} includeAll If true, the backgrounds of
2406
2892
  * all vertical slides (not just the present) will be updated.
2407
2893
  */
2408
2894
  function updateBackground( includeAll ) {
@@ -2459,21 +2945,25 @@
2459
2945
 
2460
2946
  } );
2461
2947
 
2462
- // Stop any currently playing video background
2948
+ // Stop content inside of previous backgrounds
2463
2949
  if( previousBackground ) {
2464
2950
 
2465
- var previousVideo = previousBackground.querySelector( 'video' );
2466
- if( previousVideo ) previousVideo.pause();
2951
+ stopEmbeddedContent( previousBackground );
2467
2952
 
2468
2953
  }
2469
2954
 
2955
+ // Start content in the current background
2470
2956
  if( currentBackground ) {
2471
2957
 
2472
- // Start video playback
2473
- var currentVideo = currentBackground.querySelector( 'video' );
2474
- if( currentVideo ) {
2475
- currentVideo.currentTime = 0;
2476
- currentVideo.play();
2958
+ startEmbeddedContent( currentBackground );
2959
+
2960
+ var backgroundImageURL = currentBackground.style.backgroundImage || '';
2961
+
2962
+ // Restart GIFs (doesn't work in Firefox)
2963
+ if( /\.gif/i.test( backgroundImageURL ) ) {
2964
+ currentBackground.style.backgroundImage = '';
2965
+ window.getComputedStyle( currentBackground ).opacity;
2966
+ currentBackground.style.backgroundImage = backgroundImageURL;
2477
2967
  }
2478
2968
 
2479
2969
  // Don't transition between identical backgrounds. This
@@ -2530,15 +3020,35 @@
2530
3020
  backgroundHeight = parseInt( backgroundSize[1], 10 );
2531
3021
  }
2532
3022
 
2533
- var slideWidth = dom.background.offsetWidth;
2534
- var horizontalSlideCount = horizontalSlides.length;
2535
- var horizontalOffset = -( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 ) * indexh;
3023
+ var slideWidth = dom.background.offsetWidth,
3024
+ horizontalSlideCount = horizontalSlides.length,
3025
+ horizontalOffsetMultiplier,
3026
+ horizontalOffset;
3027
+
3028
+ if( typeof config.parallaxBackgroundHorizontal === 'number' ) {
3029
+ horizontalOffsetMultiplier = config.parallaxBackgroundHorizontal;
3030
+ }
3031
+ else {
3032
+ horizontalOffsetMultiplier = horizontalSlideCount > 1 ? ( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 ) : 0;
3033
+ }
3034
+
3035
+ horizontalOffset = horizontalOffsetMultiplier * indexh * -1;
3036
+
3037
+ var slideHeight = dom.background.offsetHeight,
3038
+ verticalSlideCount = verticalSlides.length,
3039
+ verticalOffsetMultiplier,
3040
+ verticalOffset;
3041
+
3042
+ if( typeof config.parallaxBackgroundVertical === 'number' ) {
3043
+ verticalOffsetMultiplier = config.parallaxBackgroundVertical;
3044
+ }
3045
+ else {
3046
+ verticalOffsetMultiplier = ( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 );
3047
+ }
2536
3048
 
2537
- var slideHeight = dom.background.offsetHeight;
2538
- var verticalSlideCount = verticalSlides.length;
2539
- var verticalOffset = verticalSlideCount > 1 ? -( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 ) * indexv : 0;
3049
+ verticalOffset = verticalSlideCount > 0 ? verticalOffsetMultiplier * indexv : 0;
2540
3050
 
2541
- dom.background.style.backgroundPosition = horizontalOffset + 'px ' + verticalOffset + 'px';
3051
+ dom.background.style.backgroundPosition = horizontalOffset + 'px ' + -verticalOffset + 'px';
2542
3052
 
2543
3053
  }
2544
3054
 
@@ -2548,14 +3058,23 @@
2548
3058
  * Called when the given slide is within the configured view
2549
3059
  * distance. Shows the slide element and loads any content
2550
3060
  * that is set to load lazily (data-src).
3061
+ *
3062
+ * @param {HTMLElement} slide Slide to show
3063
+ */
3064
+ /**
3065
+ * Called when the given slide is within the configured view
3066
+ * distance. Shows the slide element and loads any content
3067
+ * that is set to load lazily (data-src).
3068
+ *
3069
+ * @param {HTMLElement} slide Slide to show
2551
3070
  */
2552
3071
  function showSlide( slide ) {
2553
3072
 
2554
3073
  // Show the slide element
2555
- slide.style.display = 'block';
3074
+ slide.style.display = config.display;
2556
3075
 
2557
3076
  // Media elements with data-src attributes
2558
- toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src], iframe[data-src]' ) ).forEach( function( element ) {
3077
+ toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src]' ) ).forEach( function( element ) {
2559
3078
  element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
2560
3079
  element.removeAttribute( 'data-src' );
2561
3080
  } );
@@ -2590,6 +3109,8 @@
2590
3109
 
2591
3110
  var backgroundImage = slide.getAttribute( 'data-background-image' ),
2592
3111
  backgroundVideo = slide.getAttribute( 'data-background-video' ),
3112
+ backgroundVideoLoop = slide.hasAttribute( 'data-background-video-loop' ),
3113
+ backgroundVideoMuted = slide.hasAttribute( 'data-background-video-muted' ),
2593
3114
  backgroundIframe = slide.getAttribute( 'data-background-iframe' );
2594
3115
 
2595
3116
  // Images
@@ -2600,6 +3121,23 @@
2600
3121
  else if ( backgroundVideo && !isSpeakerNotes() ) {
2601
3122
  var video = document.createElement( 'video' );
2602
3123
 
3124
+ if( backgroundVideoLoop ) {
3125
+ video.setAttribute( 'loop', '' );
3126
+ }
3127
+
3128
+ if( backgroundVideoMuted ) {
3129
+ video.muted = true;
3130
+ }
3131
+
3132
+ // Inline video playback works (at least in Mobile Safari) as
3133
+ // long as the video is muted and the `playsinline` attribute is
3134
+ // present
3135
+ if( isMobileDevice ) {
3136
+ video.muted = true;
3137
+ video.autoplay = true;
3138
+ video.setAttribute( 'playsinline', '' );
3139
+ }
3140
+
2603
3141
  // Support comma separated lists of video sources
2604
3142
  backgroundVideo.split( ',' ).forEach( function( source ) {
2605
3143
  video.innerHTML += '<source src="'+ source +'">';
@@ -2608,17 +3146,30 @@
2608
3146
  background.appendChild( video );
2609
3147
  }
2610
3148
  // Iframes
2611
- else if ( backgroundIframe ) {
3149
+ else if( backgroundIframe ) {
2612
3150
  var iframe = document.createElement( 'iframe' );
3151
+ iframe.setAttribute( 'allowfullscreen', '' );
3152
+ iframe.setAttribute( 'mozallowfullscreen', '' );
3153
+ iframe.setAttribute( 'webkitallowfullscreen', '' );
3154
+
3155
+ // Only load autoplaying content when the slide is shown to
3156
+ // avoid having it play in the background
3157
+ if( /autoplay=(1|true|yes)/gi.test( backgroundIframe ) ) {
3158
+ iframe.setAttribute( 'data-src', backgroundIframe );
3159
+ }
3160
+ else {
2613
3161
  iframe.setAttribute( 'src', backgroundIframe );
2614
- iframe.style.width = '100%';
2615
- iframe.style.height = '100%';
2616
- iframe.style.maxHeight = '100%';
2617
- iframe.style.maxWidth = '100%';
3162
+ }
3163
+
3164
+ iframe.style.width = '100%';
3165
+ iframe.style.height = '100%';
3166
+ iframe.style.maxHeight = '100%';
3167
+ iframe.style.maxWidth = '100%';
2618
3168
 
2619
3169
  background.appendChild( iframe );
2620
3170
  }
2621
3171
  }
3172
+
2622
3173
  }
2623
3174
 
2624
3175
  }
@@ -2626,6 +3177,8 @@
2626
3177
  /**
2627
3178
  * Called when the given slide is moved outside of the
2628
3179
  * configured view distance.
3180
+ *
3181
+ * @param {HTMLElement} slide
2629
3182
  */
2630
3183
  function hideSlide( slide ) {
2631
3184
 
@@ -2644,7 +3197,7 @@
2644
3197
  /**
2645
3198
  * Determine what available routes there are for navigation.
2646
3199
  *
2647
- * @return {Object} containing four booleans: left/right/up/down
3200
+ * @return {{left: boolean, right: boolean, up: boolean, down: boolean}}
2648
3201
  */
2649
3202
  function availableRoutes() {
2650
3203
 
@@ -2673,7 +3226,7 @@
2673
3226
  * Returns an object describing the available fragment
2674
3227
  * directions.
2675
3228
  *
2676
- * @return {Object} two boolean properties: prev/next
3229
+ * @return {{prev: boolean, next: boolean}}
2677
3230
  */
2678
3231
  function availableFragments() {
2679
3232
 
@@ -2697,56 +3250,157 @@
2697
3250
  */
2698
3251
  function formatEmbeddedContent() {
2699
3252
 
3253
+ var _appendParamToIframeSource = function( sourceAttribute, sourceURL, param ) {
3254
+ toArray( dom.slides.querySelectorAll( 'iframe['+ sourceAttribute +'*="'+ sourceURL +'"]' ) ).forEach( function( el ) {
3255
+ var src = el.getAttribute( sourceAttribute );
3256
+ if( src && src.indexOf( param ) === -1 ) {
3257
+ el.setAttribute( sourceAttribute, src + ( !/\?/.test( src ) ? '?' : '&' ) + param );
3258
+ }
3259
+ });
3260
+ };
3261
+
2700
3262
  // YouTube frames must include "?enablejsapi=1"
2701
- toArray( dom.slides.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) {
2702
- var src = el.getAttribute( 'src' );
2703
- if( !/enablejsapi\=1/gi.test( src ) ) {
2704
- el.setAttribute( 'src', src + ( !/\?/.test( src ) ? '?' : '&' ) + 'enablejsapi=1' );
2705
- }
2706
- });
3263
+ _appendParamToIframeSource( 'src', 'youtube.com/embed/', 'enablejsapi=1' );
3264
+ _appendParamToIframeSource( 'data-src', 'youtube.com/embed/', 'enablejsapi=1' );
2707
3265
 
2708
3266
  // Vimeo frames must include "?api=1"
2709
- toArray( dom.slides.querySelectorAll( 'iframe[src*="player.vimeo.com/"]' ) ).forEach( function( el ) {
2710
- var src = el.getAttribute( 'src' );
2711
- if( !/api\=1/gi.test( src ) ) {
2712
- el.setAttribute( 'src', src + ( !/\?/.test( src ) ? '?' : '&' ) + 'api=1' );
2713
- }
2714
- });
3267
+ _appendParamToIframeSource( 'src', 'player.vimeo.com/', 'api=1' );
3268
+ _appendParamToIframeSource( 'data-src', 'player.vimeo.com/', 'api=1' );
2715
3269
 
2716
3270
  }
2717
3271
 
2718
3272
  /**
2719
3273
  * Start playback of any embedded content inside of
2720
- * the targeted slide.
3274
+ * the given element.
3275
+ *
3276
+ * @param {HTMLElement} element
2721
3277
  */
2722
- function startEmbeddedContent( slide ) {
3278
+ function startEmbeddedContent( element ) {
3279
+
3280
+ if( element && !isSpeakerNotes() ) {
3281
+
3282
+ // Restart GIFs
3283
+ toArray( element.querySelectorAll( 'img[src$=".gif"]' ) ).forEach( function( el ) {
3284
+ // Setting the same unchanged source like this was confirmed
3285
+ // to work in Chrome, FF & Safari
3286
+ el.setAttribute( 'src', el.getAttribute( 'src' ) );
3287
+ } );
2723
3288
 
2724
- if( slide && !isSpeakerNotes() ) {
2725
3289
  // HTML5 media elements
2726
- toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
2727
- if( el.hasAttribute( 'data-autoplay' ) ) {
2728
- el.play();
3290
+ toArray( element.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
3291
+ if( closestParent( el, '.fragment' ) && !closestParent( el, '.fragment.visible' ) ) {
3292
+ return;
3293
+ }
3294
+
3295
+ // Prefer an explicit global autoplay setting
3296
+ var autoplay = config.autoPlayMedia;
3297
+
3298
+ // If no global setting is available, fall back on the element's
3299
+ // own autoplay setting
3300
+ if( typeof autoplay !== 'boolean' ) {
3301
+ autoplay = el.hasAttribute( 'data-autoplay' ) || !!closestParent( el, '.slide-background' );
3302
+ }
3303
+
3304
+ if( autoplay && typeof el.play === 'function' ) {
3305
+
3306
+ if( el.readyState > 1 ) {
3307
+ startEmbeddedMedia( { target: el } );
3308
+ }
3309
+ else {
3310
+ el.removeEventListener( 'loadeddata', startEmbeddedMedia ); // remove first to avoid dupes
3311
+ el.addEventListener( 'loadeddata', startEmbeddedMedia );
3312
+ }
3313
+
2729
3314
  }
2730
3315
  } );
2731
3316
 
2732
- // iframe embeds
2733
- toArray( slide.querySelectorAll( 'iframe' ) ).forEach( function( el ) {
2734
- el.contentWindow.postMessage( 'slide:start', '*' );
2735
- });
3317
+ // Normal iframes
3318
+ toArray( element.querySelectorAll( 'iframe[src]' ) ).forEach( function( el ) {
3319
+ if( closestParent( el, '.fragment' ) && !closestParent( el, '.fragment.visible' ) ) {
3320
+ return;
3321
+ }
3322
+
3323
+ startEmbeddedIframe( { target: el } );
3324
+ } );
2736
3325
 
2737
- // YouTube embeds
2738
- toArray( slide.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) {
2739
- if( el.hasAttribute( 'data-autoplay' ) ) {
2740
- el.contentWindow.postMessage( '{"event":"command","func":"playVideo","args":""}', '*' );
3326
+ // Lazy loading iframes
3327
+ toArray( element.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) {
3328
+ if( closestParent( el, '.fragment' ) && !closestParent( el, '.fragment.visible' ) ) {
3329
+ return;
2741
3330
  }
2742
- });
2743
3331
 
2744
- // Vimeo embeds
2745
- toArray( slide.querySelectorAll( 'iframe[src*="player.vimeo.com/"]' ) ).forEach( function( el ) {
2746
- if( el.hasAttribute( 'data-autoplay' ) ) {
2747
- el.contentWindow.postMessage( '{"method":"play"}', '*' );
3332
+ if( el.getAttribute( 'src' ) !== el.getAttribute( 'data-src' ) ) {
3333
+ el.removeEventListener( 'load', startEmbeddedIframe ); // remove first to avoid dupes
3334
+ el.addEventListener( 'load', startEmbeddedIframe );
3335
+ el.setAttribute( 'src', el.getAttribute( 'data-src' ) );
2748
3336
  }
2749
- });
3337
+ } );
3338
+
3339
+ }
3340
+
3341
+ }
3342
+
3343
+ /**
3344
+ * Starts playing an embedded video/audio element after
3345
+ * it has finished loading.
3346
+ *
3347
+ * @param {object} event
3348
+ */
3349
+ function startEmbeddedMedia( event ) {
3350
+
3351
+ var isAttachedToDOM = !!closestParent( event.target, 'html' ),
3352
+ isVisible = !!closestParent( event.target, '.present' );
3353
+
3354
+ if( isAttachedToDOM && isVisible ) {
3355
+ event.target.currentTime = 0;
3356
+ event.target.play();
3357
+ }
3358
+
3359
+ event.target.removeEventListener( 'loadeddata', startEmbeddedMedia );
3360
+
3361
+ }
3362
+
3363
+ /**
3364
+ * "Starts" the content of an embedded iframe using the
3365
+ * postMessage API.
3366
+ *
3367
+ * @param {object} event
3368
+ */
3369
+ function startEmbeddedIframe( event ) {
3370
+
3371
+ var iframe = event.target;
3372
+
3373
+ if( iframe && iframe.contentWindow ) {
3374
+
3375
+ var isAttachedToDOM = !!closestParent( event.target, 'html' ),
3376
+ isVisible = !!closestParent( event.target, '.present' );
3377
+
3378
+ if( isAttachedToDOM && isVisible ) {
3379
+
3380
+ // Prefer an explicit global autoplay setting
3381
+ var autoplay = config.autoPlayMedia;
3382
+
3383
+ // If no global setting is available, fall back on the element's
3384
+ // own autoplay setting
3385
+ if( typeof autoplay !== 'boolean' ) {
3386
+ autoplay = iframe.hasAttribute( 'data-autoplay' ) || !!closestParent( iframe, '.slide-background' );
3387
+ }
3388
+
3389
+ // YouTube postMessage API
3390
+ if( /youtube\.com\/embed\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) {
3391
+ iframe.contentWindow.postMessage( '{"event":"command","func":"playVideo","args":""}', '*' );
3392
+ }
3393
+ // Vimeo postMessage API
3394
+ else if( /player\.vimeo\.com\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) {
3395
+ iframe.contentWindow.postMessage( '{"method":"play"}', '*' );
3396
+ }
3397
+ // Generic postMessage API
3398
+ else {
3399
+ iframe.contentWindow.postMessage( 'slide:start', '*' );
3400
+ }
3401
+
3402
+ }
3403
+
2750
3404
  }
2751
3405
 
2752
3406
  }
@@ -2754,49 +3408,62 @@
2754
3408
  /**
2755
3409
  * Stop playback of any embedded content inside of
2756
3410
  * the targeted slide.
3411
+ *
3412
+ * @param {HTMLElement} element
2757
3413
  */
2758
- function stopEmbeddedContent( slide ) {
3414
+ function stopEmbeddedContent( element ) {
2759
3415
 
2760
- if( slide && slide.parentNode ) {
3416
+ if( element && element.parentNode ) {
2761
3417
  // HTML5 media elements
2762
- toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
2763
- if( !el.hasAttribute( 'data-ignore' ) ) {
3418
+ toArray( element.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
3419
+ if( !el.hasAttribute( 'data-ignore' ) && typeof el.pause === 'function' ) {
3420
+ el.setAttribute('data-paused-by-reveal', '');
2764
3421
  el.pause();
2765
3422
  }
2766
3423
  } );
2767
3424
 
2768
- // iframe embeds
2769
- toArray( slide.querySelectorAll( 'iframe' ) ).forEach( function( el ) {
2770
- el.contentWindow.postMessage( 'slide:stop', '*' );
3425
+ // Generic postMessage API for non-lazy loaded iframes
3426
+ toArray( element.querySelectorAll( 'iframe' ) ).forEach( function( el ) {
3427
+ if( el.contentWindow ) el.contentWindow.postMessage( 'slide:stop', '*' );
3428
+ el.removeEventListener( 'load', startEmbeddedIframe );
2771
3429
  });
2772
3430
 
2773
- // YouTube embeds
2774
- toArray( slide.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) {
2775
- if( !el.hasAttribute( 'data-ignore' ) && typeof el.contentWindow.postMessage === 'function' ) {
3431
+ // YouTube postMessage API
3432
+ toArray( element.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) {
3433
+ if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) {
2776
3434
  el.contentWindow.postMessage( '{"event":"command","func":"pauseVideo","args":""}', '*' );
2777
3435
  }
2778
3436
  });
2779
3437
 
2780
- // Vimeo embeds
2781
- toArray( slide.querySelectorAll( 'iframe[src*="player.vimeo.com/"]' ) ).forEach( function( el ) {
2782
- if( !el.hasAttribute( 'data-ignore' ) && typeof el.contentWindow.postMessage === 'function' ) {
3438
+ // Vimeo postMessage API
3439
+ toArray( element.querySelectorAll( 'iframe[src*="player.vimeo.com/"]' ) ).forEach( function( el ) {
3440
+ if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) {
2783
3441
  el.contentWindow.postMessage( '{"method":"pause"}', '*' );
2784
3442
  }
2785
3443
  });
3444
+
3445
+ // Lazy loading iframes
3446
+ toArray( element.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) {
3447
+ // Only removing the src doesn't actually unload the frame
3448
+ // in all browsers (Firefox) so we set it to blank first
3449
+ el.setAttribute( 'src', 'about:blank' );
3450
+ el.removeAttribute( 'src' );
3451
+ } );
2786
3452
  }
2787
3453
 
2788
3454
  }
2789
3455
 
2790
3456
  /**
2791
- * Returns a value ranging from 0-1 that represents
2792
- * how far into the presentation we have navigated.
3457
+ * Returns the number of past slides. This can be used as a global
3458
+ * flattened index for slides.
3459
+ *
3460
+ * @return {number} Past slide count
2793
3461
  */
2794
- function getProgress() {
3462
+ function getSlidePastCount() {
2795
3463
 
2796
3464
  var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
2797
3465
 
2798
- // The number of past and total slides
2799
- var totalCount = getTotalSlides();
3466
+ // The number of past slides
2800
3467
  var pastCount = 0;
2801
3468
 
2802
3469
  // Step through all slides and count the past ones
@@ -2828,6 +3495,22 @@
2828
3495
 
2829
3496
  }
2830
3497
 
3498
+ return pastCount;
3499
+
3500
+ }
3501
+
3502
+ /**
3503
+ * Returns a value ranging from 0-1 that represents
3504
+ * how far into the presentation we have navigated.
3505
+ *
3506
+ * @return {number}
3507
+ */
3508
+ function getProgress() {
3509
+
3510
+ // The number of past and total slides
3511
+ var totalCount = getTotalSlides();
3512
+ var pastCount = getSlidePastCount();
3513
+
2831
3514
  if( currentSlide ) {
2832
3515
 
2833
3516
  var allFragments = currentSlide.querySelectorAll( '.fragment' );
@@ -2854,6 +3537,8 @@
2854
3537
  /**
2855
3538
  * Checks if this presentation is running inside of the
2856
3539
  * speaker notes window.
3540
+ *
3541
+ * @return {boolean}
2857
3542
  */
2858
3543
  function isSpeakerNotes() {
2859
3544
 
@@ -2880,7 +3565,7 @@
2880
3565
  // Ensure the named link is a valid HTML ID attribute
2881
3566
  if( /^[a-zA-Z][\w:.-]*$/.test( name ) ) {
2882
3567
  // Find the slide with the specified ID
2883
- element = document.querySelector( '#' + name );
3568
+ element = document.getElementById( name );
2884
3569
  }
2885
3570
 
2886
3571
  if( element ) {
@@ -2909,7 +3594,7 @@
2909
3594
  * Updates the page URL (hash) to reflect the current
2910
3595
  * state.
2911
3596
  *
2912
- * @param {Number} delay The time in ms to wait before
3597
+ * @param {number} delay The time in ms to wait before
2913
3598
  * writing the hash
2914
3599
  */
2915
3600
  function writeURL( delay ) {
@@ -2929,7 +3614,6 @@
2929
3614
  // Attempt to create a named link based on the slide's ID
2930
3615
  var id = currentSlide.getAttribute( 'id' );
2931
3616
  if( id ) {
2932
- id = id.toLowerCase();
2933
3617
  id = id.replace( /[^a-zA-Z0-9\-\_\:\.]/g, '' );
2934
3618
  }
2935
3619
 
@@ -2948,16 +3632,15 @@
2948
3632
  }
2949
3633
 
2950
3634
  }
2951
-
2952
3635
  /**
2953
- * Retrieves the h/v location of the current, or specified,
2954
- * slide.
3636
+ * Retrieves the h/v location and fragment of the current,
3637
+ * or specified, slide.
2955
3638
  *
2956
- * @param {HTMLElement} slide If specified, the returned
3639
+ * @param {HTMLElement} [slide] If specified, the returned
2957
3640
  * index will be for this slide rather than the currently
2958
3641
  * active one
2959
3642
  *
2960
- * @return {Object} { h: <int>, v: <int>, f: <int> }
3643
+ * @return {{h: number, v: number, f: number}}
2961
3644
  */
2962
3645
  function getIndices( slide ) {
2963
3646
 
@@ -3003,17 +3686,30 @@
3003
3686
 
3004
3687
  }
3005
3688
 
3689
+ /**
3690
+ * Retrieves all slides in this presentation.
3691
+ */
3692
+ function getSlides() {
3693
+
3694
+ return toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ':not(.stack)' ));
3695
+
3696
+ }
3697
+
3006
3698
  /**
3007
3699
  * Retrieves the total number of slides in this presentation.
3700
+ *
3701
+ * @return {number}
3008
3702
  */
3009
3703
  function getTotalSlides() {
3010
3704
 
3011
- return dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ':not(.stack)' ).length;
3705
+ return getSlides().length;
3012
3706
 
3013
3707
  }
3014
3708
 
3015
3709
  /**
3016
3710
  * Returns the slide element matching the specified index.
3711
+ *
3712
+ * @return {HTMLElement}
3017
3713
  */
3018
3714
  function getSlide( x, y ) {
3019
3715
 
@@ -3033,6 +3729,10 @@
3033
3729
  * All slides, even the ones with no background properties
3034
3730
  * defined, have a background element so as long as the
3035
3731
  * index is valid an element will be returned.
3732
+ *
3733
+ * @param {number} x Horizontal background index
3734
+ * @param {number} y Vertical background index
3735
+ * @return {(HTMLElement[]|*)}
3036
3736
  */
3037
3737
  function getSlideBackground( x, y ) {
3038
3738
 
@@ -3041,10 +3741,7 @@
3041
3741
  if( isPrintingPDF() ) {
3042
3742
  var slide = getSlide( x, y );
3043
3743
  if( slide ) {
3044
- var background = slide.querySelector( '.slide-background' );
3045
- if( background && background.parentNode === slide ) {
3046
- return background;
3047
- }
3744
+ return slide.slideBackgroundElement;
3048
3745
  }
3049
3746
 
3050
3747
  return undefined;
@@ -3061,10 +3758,41 @@
3061
3758
 
3062
3759
  }
3063
3760
 
3761
+ /**
3762
+ * Retrieves the speaker notes from a slide. Notes can be
3763
+ * defined in two ways:
3764
+ * 1. As a data-notes attribute on the slide <section>
3765
+ * 2. As an <aside class="notes"> inside of the slide
3766
+ *
3767
+ * @param {HTMLElement} [slide=currentSlide]
3768
+ * @return {(string|null)}
3769
+ */
3770
+ function getSlideNotes( slide ) {
3771
+
3772
+ // Default to the current slide
3773
+ slide = slide || currentSlide;
3774
+
3775
+ // Notes can be specified via the data-notes attribute...
3776
+ if( slide.hasAttribute( 'data-notes' ) ) {
3777
+ return slide.getAttribute( 'data-notes' );
3778
+ }
3779
+
3780
+ // ... or using an <aside class="notes"> element
3781
+ var notesElement = slide.querySelector( 'aside.notes' );
3782
+ if( notesElement ) {
3783
+ return notesElement.innerHTML;
3784
+ }
3785
+
3786
+ return null;
3787
+
3788
+ }
3789
+
3064
3790
  /**
3065
3791
  * Retrieves the current state of the presentation as
3066
3792
  * an object. This state can then be restored at any
3067
3793
  * time.
3794
+ *
3795
+ * @return {{indexh: number, indexv: number, indexf: number, paused: boolean, overview: boolean}}
3068
3796
  */
3069
3797
  function getState() {
3070
3798
 
@@ -3083,7 +3811,8 @@
3083
3811
  /**
3084
3812
  * Restores the presentation to the given state.
3085
3813
  *
3086
- * @param {Object} state As generated by getState()
3814
+ * @param {object} state As generated by getState()
3815
+ * @see {@link getState} generates the parameter `state`
3087
3816
  */
3088
3817
  function setState( state ) {
3089
3818
 
@@ -3117,6 +3846,9 @@
3117
3846
  * attribute to each node if such an attribute is not already present,
3118
3847
  * and sets that attribute to an integer value which is the position of
3119
3848
  * the fragment within the fragments list.
3849
+ *
3850
+ * @param {object[]|*} fragments
3851
+ * @return {object[]} sorted Sorted array of fragments
3120
3852
  */
3121
3853
  function sortFragments( fragments ) {
3122
3854
 
@@ -3168,12 +3900,12 @@
3168
3900
  /**
3169
3901
  * Navigate to the specified slide fragment.
3170
3902
  *
3171
- * @param {Number} index The index of the fragment that
3903
+ * @param {?number} index The index of the fragment that
3172
3904
  * should be shown, -1 means all are invisible
3173
- * @param {Number} offset Integer offset to apply to the
3905
+ * @param {number} offset Integer offset to apply to the
3174
3906
  * fragment index
3175
3907
  *
3176
- * @return {Boolean} true if a change was made in any
3908
+ * @return {boolean} true if a change was made in any
3177
3909
  * fragments visibility as part of this call
3178
3910
  */
3179
3911
  function navigateFragment( index, offset ) {
@@ -3216,10 +3948,11 @@
3216
3948
  element.classList.remove( 'current-fragment' );
3217
3949
 
3218
3950
  // Announce the fragments one by one to the Screen Reader
3219
- dom.statusDiv.textContent = element.textContent;
3951
+ dom.statusDiv.textContent = getStatusText( element );
3220
3952
 
3221
3953
  if( i === index ) {
3222
3954
  element.classList.add( 'current-fragment' );
3955
+ startEmbeddedContent( element );
3223
3956
  }
3224
3957
  }
3225
3958
  // Hidden fragments
@@ -3229,7 +3962,6 @@
3229
3962
  element.classList.remove( 'current-fragment' );
3230
3963
  }
3231
3964
 
3232
-
3233
3965
  } );
3234
3966
 
3235
3967
  if( fragmentsHidden.length ) {
@@ -3256,7 +3988,7 @@
3256
3988
  /**
3257
3989
  * Navigate to the next slide fragment.
3258
3990
  *
3259
- * @return {Boolean} true if there was a next fragment,
3991
+ * @return {boolean} true if there was a next fragment,
3260
3992
  * false otherwise
3261
3993
  */
3262
3994
  function nextFragment() {
@@ -3268,7 +4000,7 @@
3268
4000
  /**
3269
4001
  * Navigate to the previous slide fragment.
3270
4002
  *
3271
- * @return {Boolean} true if there was a previous fragment,
4003
+ * @return {boolean} true if there was a previous fragment,
3272
4004
  * false otherwise
3273
4005
  */
3274
4006
  function previousFragment() {
@@ -3286,9 +4018,13 @@
3286
4018
 
3287
4019
  if( currentSlide ) {
3288
4020
 
3289
- var currentFragment = currentSlide.querySelector( '.current-fragment' );
4021
+ var fragment = currentSlide.querySelector( '.current-fragment' );
4022
+
4023
+ // When the slide first appears there is no "current" fragment so
4024
+ // we look for a data-autoslide timing on the first fragment
4025
+ if( !fragment ) fragment = currentSlide.querySelector( '.fragment' );
3290
4026
 
3291
- var fragmentAutoSlide = currentFragment ? currentFragment.getAttribute( 'data-autoslide' ) : null;
4027
+ var fragmentAutoSlide = fragment ? fragment.getAttribute( 'data-autoslide' ) : null;
3292
4028
  var parentAutoSlide = currentSlide.parentNode ? currentSlide.parentNode.getAttribute( 'data-autoslide' ) : null;
3293
4029
  var slideAutoSlide = currentSlide.getAttribute( 'data-autoslide' );
3294
4030
 
@@ -3312,14 +4048,18 @@
3312
4048
 
3313
4049
  // If there are media elements with data-autoplay,
3314
4050
  // automatically set the autoSlide duration to the
3315
- // length of that media
3316
- toArray( currentSlide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
3317
- if( el.hasAttribute( 'data-autoplay' ) ) {
3318
- if( autoSlide && el.duration * 1000 > autoSlide ) {
3319
- autoSlide = ( el.duration * 1000 ) + 1000;
4051
+ // length of that media. Not applicable if the slide
4052
+ // is divided up into fragments.
4053
+ // playbackRate is accounted for in the duration.
4054
+ if( currentSlide.querySelectorAll( '.fragment' ).length === 0 ) {
4055
+ toArray( currentSlide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
4056
+ if( el.hasAttribute( 'data-autoplay' ) ) {
4057
+ if( autoSlide && (el.duration * 1000 / el.playbackRate ) > autoSlide ) {
4058
+ autoSlide = ( el.duration * 1000 / el.playbackRate ) + 1000;
4059
+ }
3320
4060
  }
3321
- }
3322
- } );
4061
+ } );
4062
+ }
3323
4063
 
3324
4064
  // Cue the next auto-slide if:
3325
4065
  // - There is an autoSlide value
@@ -3328,7 +4068,10 @@
3328
4068
  // - The overview isn't active
3329
4069
  // - The presentation isn't over
3330
4070
  if( autoSlide && !autoSlidePaused && !isPaused() && !isOverview() && ( !Reveal.isLastSlide() || availableFragments().next || config.loop === true ) ) {
3331
- autoSlideTimeout = setTimeout( navigateNext, autoSlide );
4071
+ autoSlideTimeout = setTimeout( function() {
4072
+ typeof config.autoSlideMethod === 'function' ? config.autoSlideMethod() : navigateNext();
4073
+ cueAutoSlide();
4074
+ }, autoSlide );
3332
4075
  autoSlideStartTime = Date.now();
3333
4076
  }
3334
4077
 
@@ -3474,9 +4217,20 @@
3474
4217
  }
3475
4218
  }
3476
4219
 
3477
- // If auto-sliding is enabled we need to cue up
3478
- // another timeout
3479
- cueAutoSlide();
4220
+ }
4221
+
4222
+ /**
4223
+ * Checks if the target element prevents the triggering of
4224
+ * swipe navigation.
4225
+ */
4226
+ function isSwipePrevented( target ) {
4227
+
4228
+ while( target && typeof target.hasAttribute === 'function' ) {
4229
+ if( target.hasAttribute( 'data-prevent-swipe' ) ) return true;
4230
+ target = target.parentNode;
4231
+ }
4232
+
4233
+ return false;
3480
4234
 
3481
4235
  }
3482
4236
 
@@ -3488,6 +4242,8 @@
3488
4242
  /**
3489
4243
  * Called by all event handlers that are based on user
3490
4244
  * input.
4245
+ *
4246
+ * @param {object} [event]
3491
4247
  */
3492
4248
  function onUserInput( event ) {
3493
4249
 
@@ -3499,23 +4255,22 @@
3499
4255
 
3500
4256
  /**
3501
4257
  * Handler for the document level 'keypress' event.
4258
+ *
4259
+ * @param {object} event
3502
4260
  */
3503
4261
  function onDocumentKeyPress( event ) {
3504
4262
 
3505
4263
  // Check if the pressed key is question mark
3506
4264
  if( event.shiftKey && event.charCode === 63 ) {
3507
- if( dom.overlay ) {
3508
- closeOverlay();
3509
- }
3510
- else {
3511
- showHelp( true );
3512
- }
4265
+ toggleHelp();
3513
4266
  }
3514
4267
 
3515
4268
  }
3516
4269
 
3517
4270
  /**
3518
4271
  * Handler for the document level 'keydown' event.
4272
+ *
4273
+ * @param {object} event
3519
4274
  */
3520
4275
  function onDocumentKeyDown( event ) {
3521
4276
 
@@ -3534,13 +4289,26 @@
3534
4289
  // the keyboard
3535
4290
  var activeElementIsCE = document.activeElement && document.activeElement.contentEditable !== 'inherit';
3536
4291
  var activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test( document.activeElement.tagName );
4292
+ var activeElementIsNotes = document.activeElement && document.activeElement.className && /speaker-notes/i.test( document.activeElement.className);
3537
4293
 
3538
4294
  // Disregard the event if there's a focused element or a
3539
4295
  // keyboard modifier key is present
3540
- if( activeElementIsCE || activeElementIsInput || (event.shiftKey && event.keyCode !== 32) || event.altKey || event.ctrlKey || event.metaKey ) return;
4296
+ if( activeElementIsCE || activeElementIsInput || activeElementIsNotes || (event.shiftKey && event.keyCode !== 32) || event.altKey || event.ctrlKey || event.metaKey ) return;
3541
4297
 
3542
- // While paused only allow "unpausing" keyboard events (b and .)
3543
- if( isPaused() && [66,190,191].indexOf( event.keyCode ) === -1 ) {
4298
+ // While paused only allow resume keyboard events; 'b', 'v', '.'
4299
+ var resumeKeyCodes = [66,86,190,191];
4300
+ var key;
4301
+
4302
+ // Custom key bindings for togglePause should be able to resume
4303
+ if( typeof config.keyboard === 'object' ) {
4304
+ for( key in config.keyboard ) {
4305
+ if( config.keyboard[key] === 'togglePause' ) {
4306
+ resumeKeyCodes.push( parseInt( key, 10 ) );
4307
+ }
4308
+ }
4309
+ }
4310
+
4311
+ if( isPaused() && resumeKeyCodes.indexOf( event.keyCode ) === -1 ) {
3544
4312
  return false;
3545
4313
  }
3546
4314
 
@@ -3549,7 +4317,7 @@
3549
4317
  // 1. User defined key bindings
3550
4318
  if( typeof config.keyboard === 'object' ) {
3551
4319
 
3552
- for( var key in config.keyboard ) {
4320
+ for( key in config.keyboard ) {
3553
4321
 
3554
4322
  // Check if this binding matches the pressed key
3555
4323
  if( parseInt( key, 10 ) === event.keyCode ) {
@@ -3600,8 +4368,8 @@
3600
4368
  case 32: isOverview() ? deactivateOverview() : event.shiftKey ? navigatePrev() : navigateNext(); break;
3601
4369
  // return
3602
4370
  case 13: isOverview() ? deactivateOverview() : triggered = false; break;
3603
- // two-spot, semicolon, b, period, Logitech presenter tools "black screen" button
3604
- case 58: case 59: case 66: case 190: case 191: togglePause(); break;
4371
+ // two-spot, semicolon, b, v, period, Logitech presenter tools "black screen" button
4372
+ case 58: case 59: case 66: case 86: case 190: case 191: togglePause(); break;
3605
4373
  // f
3606
4374
  case 70: enterFullscreen(); break;
3607
4375
  // a
@@ -3638,9 +4406,13 @@
3638
4406
  /**
3639
4407
  * Handler for the 'touchstart' event, enables support for
3640
4408
  * swipe and pinch gestures.
4409
+ *
4410
+ * @param {object} event
3641
4411
  */
3642
4412
  function onTouchStart( event ) {
3643
4413
 
4414
+ if( isSwipePrevented( event.target ) ) return true;
4415
+
3644
4416
  touch.startX = event.touches[0].clientX;
3645
4417
  touch.startY = event.touches[0].clientY;
3646
4418
  touch.startCount = event.touches.length;
@@ -3661,9 +4433,13 @@
3661
4433
 
3662
4434
  /**
3663
4435
  * Handler for the 'touchmove' event.
4436
+ *
4437
+ * @param {object} event
3664
4438
  */
3665
4439
  function onTouchMove( event ) {
3666
4440
 
4441
+ if( isSwipePrevented( event.target ) ) return true;
4442
+
3667
4443
  // Each touch should only trigger one action
3668
4444
  if( !touch.captured ) {
3669
4445
  onUserInput( event );
@@ -3740,7 +4516,7 @@
3740
4516
  }
3741
4517
  // There's a bug with swiping on some Android devices unless
3742
4518
  // the default action is always prevented
3743
- else if( navigator.userAgent.match( /android/gi ) ) {
4519
+ else if( UA.match( /android/gi ) ) {
3744
4520
  event.preventDefault();
3745
4521
  }
3746
4522
 
@@ -3748,6 +4524,8 @@
3748
4524
 
3749
4525
  /**
3750
4526
  * Handler for the 'touchend' event.
4527
+ *
4528
+ * @param {object} event
3751
4529
  */
3752
4530
  function onTouchEnd( event ) {
3753
4531
 
@@ -3757,6 +4535,8 @@
3757
4535
 
3758
4536
  /**
3759
4537
  * Convert pointer down to touch start.
4538
+ *
4539
+ * @param {object} event
3760
4540
  */
3761
4541
  function onPointerDown( event ) {
3762
4542
 
@@ -3769,6 +4549,8 @@
3769
4549
 
3770
4550
  /**
3771
4551
  * Convert pointer move to touch move.
4552
+ *
4553
+ * @param {object} event
3772
4554
  */
3773
4555
  function onPointerMove( event ) {
3774
4556
 
@@ -3781,6 +4563,8 @@
3781
4563
 
3782
4564
  /**
3783
4565
  * Convert pointer up to touch end.
4566
+ *
4567
+ * @param {object} event
3784
4568
  */
3785
4569
  function onPointerUp( event ) {
3786
4570
 
@@ -3794,6 +4578,8 @@
3794
4578
  /**
3795
4579
  * Handles mouse wheel scrolling, throttled to avoid skipping
3796
4580
  * multiple slides.
4581
+ *
4582
+ * @param {object} event
3797
4583
  */
3798
4584
  function onDocumentMouseScroll( event ) {
3799
4585
 
@@ -3805,7 +4591,7 @@
3805
4591
  if( delta > 0 ) {
3806
4592
  navigateNext();
3807
4593
  }
3808
- else {
4594
+ else if( delta < 0 ) {
3809
4595
  navigatePrev();
3810
4596
  }
3811
4597
 
@@ -3818,6 +4604,8 @@
3818
4604
  * closest approximate horizontal slide using this equation:
3819
4605
  *
3820
4606
  * ( clickX / presentationWidth ) * numberOfSlides
4607
+ *
4608
+ * @param {object} event
3821
4609
  */
3822
4610
  function onProgressClicked( event ) {
3823
4611
 
@@ -3828,6 +4616,10 @@
3828
4616
  var slidesTotal = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).length;
3829
4617
  var slideIndex = Math.floor( ( event.clientX / dom.wrapper.offsetWidth ) * slidesTotal );
3830
4618
 
4619
+ if( config.rtl ) {
4620
+ slideIndex = slidesTotal - slideIndex;
4621
+ }
4622
+
3831
4623
  slide( slideIndex );
3832
4624
 
3833
4625
  }
@@ -3844,6 +4636,8 @@
3844
4636
 
3845
4637
  /**
3846
4638
  * Handler for the window level 'hashchange' event.
4639
+ *
4640
+ * @param {object} [event]
3847
4641
  */
3848
4642
  function onWindowHashChange( event ) {
3849
4643
 
@@ -3853,6 +4647,8 @@
3853
4647
 
3854
4648
  /**
3855
4649
  * Handler for the window level 'resize' event.
4650
+ *
4651
+ * @param {object} [event]
3856
4652
  */
3857
4653
  function onWindowResize( event ) {
3858
4654
 
@@ -3862,6 +4658,8 @@
3862
4658
 
3863
4659
  /**
3864
4660
  * Handle for the window level 'visibilitychange' event.
4661
+ *
4662
+ * @param {object} [event]
3865
4663
  */
3866
4664
  function onPageVisibilityChange( event ) {
3867
4665
 
@@ -3872,7 +4670,10 @@
3872
4670
  // If, after clicking a link or similar and we're coming back,
3873
4671
  // focus the document.body to ensure we can use keyboard shortcuts
3874
4672
  if( isHidden === false && document.activeElement !== document.body ) {
3875
- document.activeElement.blur();
4673
+ // Not all elements support .blur() - SVGs among them.
4674
+ if( typeof document.activeElement.blur === 'function' ) {
4675
+ document.activeElement.blur();
4676
+ }
3876
4677
  document.body.focus();
3877
4678
  }
3878
4679
 
@@ -3880,6 +4681,8 @@
3880
4681
 
3881
4682
  /**
3882
4683
  * Invoked when a slide is and we're in the overview.
4684
+ *
4685
+ * @param {object} event
3883
4686
  */
3884
4687
  function onOverviewSlideClicked( event ) {
3885
4688
 
@@ -3913,6 +4716,8 @@
3913
4716
  /**
3914
4717
  * Handles clicks on links that are set to preview in the
3915
4718
  * iframe overlay.
4719
+ *
4720
+ * @param {object} event
3916
4721
  */
3917
4722
  function onPreviewLinkClicked( event ) {
3918
4723
 
@@ -3928,6 +4733,8 @@
3928
4733
 
3929
4734
  /**
3930
4735
  * Handles click on the auto-sliding controls element.
4736
+ *
4737
+ * @param {object} [event]
3931
4738
  */
3932
4739
  function onAutoSlidePlayerClick( event ) {
3933
4740
 
@@ -3959,15 +4766,16 @@
3959
4766
  *
3960
4767
  * @param {HTMLElement} container The component will append
3961
4768
  * itself to this
3962
- * @param {Function} progressCheck A method which will be
4769
+ * @param {function} progressCheck A method which will be
3963
4770
  * called frequently to get the current progress on a range
3964
4771
  * of 0-1
3965
4772
  */
3966
4773
  function Playback( container, progressCheck ) {
3967
4774
 
3968
4775
  // Cosmetics
3969
- this.diameter = 50;
3970
- this.thickness = 3;
4776
+ this.diameter = 100;
4777
+ this.diameter2 = this.diameter/2;
4778
+ this.thickness = 6;
3971
4779
 
3972
4780
  // Flags if we are currently playing
3973
4781
  this.playing = false;
@@ -3985,6 +4793,8 @@
3985
4793
  this.canvas.className = 'playback';
3986
4794
  this.canvas.width = this.diameter;
3987
4795
  this.canvas.height = this.diameter;
4796
+ this.canvas.style.width = this.diameter2 + 'px';
4797
+ this.canvas.style.height = this.diameter2 + 'px';
3988
4798
  this.context = this.canvas.getContext( '2d' );
3989
4799
 
3990
4800
  this.container.appendChild( this.canvas );
@@ -3993,6 +4803,9 @@
3993
4803
 
3994
4804
  }
3995
4805
 
4806
+ /**
4807
+ * @param value
4808
+ */
3996
4809
  Playback.prototype.setPlaying = function( value ) {
3997
4810
 
3998
4811
  var wasPlaying = this.playing;
@@ -4035,10 +4848,10 @@
4035
4848
  Playback.prototype.render = function() {
4036
4849
 
4037
4850
  var progress = this.playing ? this.progress : 0,
4038
- radius = ( this.diameter / 2 ) - this.thickness,
4039
- x = this.diameter / 2,
4040
- y = this.diameter / 2,
4041
- iconSize = 14;
4851
+ radius = ( this.diameter2 ) - this.thickness,
4852
+ x = this.diameter2,
4853
+ y = this.diameter2,
4854
+ iconSize = 28;
4042
4855
 
4043
4856
  // Ease towards 1
4044
4857
  this.progressOffset += ( 1 - this.progressOffset ) * 0.1;
@@ -4051,7 +4864,7 @@
4051
4864
 
4052
4865
  // Solid background color
4053
4866
  this.context.beginPath();
4054
- this.context.arc( x, y, radius + 2, 0, Math.PI * 2, false );
4867
+ this.context.arc( x, y, radius + 4, 0, Math.PI * 2, false );
4055
4868
  this.context.fillStyle = 'rgba( 0, 0, 0, 0.4 )';
4056
4869
  this.context.fill();
4057
4870
 
@@ -4076,14 +4889,14 @@
4076
4889
  // Draw play/pause icons
4077
4890
  if( this.playing ) {
4078
4891
  this.context.fillStyle = '#fff';
4079
- this.context.fillRect( 0, 0, iconSize / 2 - 2, iconSize );
4080
- this.context.fillRect( iconSize / 2 + 2, 0, iconSize / 2 - 2, iconSize );
4892
+ this.context.fillRect( 0, 0, iconSize / 2 - 4, iconSize );
4893
+ this.context.fillRect( iconSize / 2 + 4, 0, iconSize / 2 - 4, iconSize );
4081
4894
  }
4082
4895
  else {
4083
4896
  this.context.beginPath();
4084
- this.context.translate( 2, 0 );
4897
+ this.context.translate( 4, 0 );
4085
4898
  this.context.moveTo( 0, 0 );
4086
- this.context.lineTo( iconSize - 2, iconSize / 2 );
4899
+ this.context.lineTo( iconSize - 4, iconSize / 2 );
4087
4900
  this.context.lineTo( 0, iconSize );
4088
4901
  this.context.fillStyle = '#fff';
4089
4902
  this.context.fill();
@@ -4118,6 +4931,8 @@
4118
4931
 
4119
4932
 
4120
4933
  Reveal = {
4934
+ VERSION: VERSION,
4935
+
4121
4936
  initialize: initialize,
4122
4937
  configure: configure,
4123
4938
  sync: sync,
@@ -4148,12 +4963,18 @@
4148
4963
  // Forces an update in slide layout
4149
4964
  layout: layout,
4150
4965
 
4966
+ // Randomizes the order of slides
4967
+ shuffle: shuffle,
4968
+
4151
4969
  // Returns an object with the available routes as booleans (left/right/top/bottom)
4152
4970
  availableRoutes: availableRoutes,
4153
4971
 
4154
4972
  // Returns an object with the available fragments as booleans (prev/next)
4155
4973
  availableFragments: availableFragments,
4156
4974
 
4975
+ // Toggles a help overlay with keyboard shortcuts
4976
+ toggleHelp: toggleHelp,
4977
+
4157
4978
  // Toggles the overview mode on/off
4158
4979
  toggleOverview: toggleOverview,
4159
4980
 
@@ -4176,12 +4997,19 @@
4176
4997
  getState: getState,
4177
4998
  setState: setState,
4178
4999
 
5000
+ // Presentation progress
5001
+ getSlidePastCount: getSlidePastCount,
5002
+
4179
5003
  // Presentation progress on range of 0-1
4180
5004
  getProgress: getProgress,
4181
5005
 
4182
5006
  // Returns the indices of the current, or specified, slide
4183
5007
  getIndices: getIndices,
4184
5008
 
5009
+ // Returns an Array of all slides
5010
+ getSlides: getSlides,
5011
+
5012
+ // Returns the total number of slides
4185
5013
  getTotalSlides: getTotalSlides,
4186
5014
 
4187
5015
  // Returns the slide element at the specified index
@@ -4190,6 +5018,9 @@
4190
5018
  // Returns the slide background element at the specified index
4191
5019
  getSlideBackground: getSlideBackground,
4192
5020
 
5021
+ // Returns the speaker notes string for a slide, or null
5022
+ getSlideNotes: getSlideNotes,
5023
+
4193
5024
  // Returns the previous slide element, may be null
4194
5025
  getPreviousSlide: function() {
4195
5026
  return previousSlide;
@@ -4268,6 +5099,11 @@
4268
5099
  // Programatically triggers a keyboard event
4269
5100
  triggerKey: function( keyCode ) {
4270
5101
  onDocumentKeyDown( { keyCode: keyCode } );
5102
+ },
5103
+
5104
+ // Registers a new shortcut to include in the help overlay
5105
+ registerKeyboardShortcut: function( key, value ) {
5106
+ keyboardShortcuts[key] = value;
4271
5107
  }
4272
5108
  };
4273
5109