reveal.rb 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +5 -5
  2. data/.ruby-version +1 -1
  3. data/.travis.yml +2 -1
  4. data/Dockerfile +4 -0
  5. data/Gemfile +1 -1
  6. data/Gemfile.lock +7 -1
  7. data/README.md +1 -1
  8. data/lib/reveal/command.rb +9 -3
  9. data/lib/reveal/templates/revealjs/css/print/paper.css +4 -3
  10. data/lib/reveal/templates/revealjs/css/print/pdf.css +59 -38
  11. data/lib/reveal/templates/revealjs/css/reveal.css +654 -274
  12. data/lib/reveal/templates/revealjs/css/theme/beige.css +65 -68
  13. data/lib/reveal/templates/revealjs/css/theme/black.css +58 -61
  14. data/lib/reveal/templates/revealjs/css/theme/blood.css +64 -62
  15. data/lib/reveal/templates/revealjs/css/theme/league.css +59 -62
  16. data/lib/reveal/templates/revealjs/css/theme/moon.css +59 -62
  17. data/lib/reveal/templates/revealjs/css/theme/night.css +58 -61
  18. data/lib/reveal/templates/revealjs/css/theme/serif.css +56 -59
  19. data/lib/reveal/templates/revealjs/css/theme/simple.css +60 -60
  20. data/lib/reveal/templates/revealjs/css/theme/sky.css +59 -62
  21. data/lib/reveal/templates/revealjs/css/theme/solarized.css +59 -62
  22. data/lib/reveal/templates/revealjs/css/theme/white.css +59 -62
  23. data/lib/reveal/templates/revealjs/index.html +14 -376
  24. data/lib/reveal/templates/revealjs/js/reveal.js +1073 -342
  25. data/lib/reveal/templates/revealjs/lib/css/zenburn.css +41 -78
  26. data/lib/reveal/templates/revealjs/lib/js/head.min.js +9 -8
  27. data/lib/reveal/templates/revealjs/plugin/highlight/highlight.js +51 -4
  28. data/lib/reveal/templates/revealjs/plugin/markdown/markdown.js +38 -19
  29. data/lib/reveal/templates/revealjs/plugin/markdown/marked.js +1 -1
  30. data/lib/reveal/templates/revealjs/plugin/math/math.js +5 -2
  31. data/lib/reveal/templates/revealjs/plugin/multiplex/client.js +1 -1
  32. data/lib/reveal/templates/revealjs/plugin/multiplex/index.js +24 -16
  33. data/lib/reveal/templates/revealjs/plugin/multiplex/master.js +26 -43
  34. data/lib/reveal/templates/revealjs/plugin/multiplex/package.json +19 -0
  35. data/lib/reveal/templates/revealjs/plugin/notes/notes.html +385 -32
  36. data/lib/reveal/templates/revealjs/plugin/notes/notes.js +39 -6
  37. data/lib/reveal/templates/revealjs/plugin/notes-server/client.js +6 -1
  38. data/lib/reveal/templates/revealjs/plugin/notes-server/index.js +17 -14
  39. data/lib/reveal/templates/revealjs/plugin/notes-server/notes.html +215 -26
  40. data/lib/reveal/templates/revealjs/plugin/print-pdf/print-pdf.js +48 -27
  41. data/lib/reveal/templates/revealjs/plugin/search/search.js +41 -31
  42. data/lib/reveal/templates/revealjs/plugin/zoom-js/zoom.js +17 -23
  43. data/lib/reveal/templates/template.html +12 -41
  44. data/lib/reveal/version.rb +1 -1
  45. data/spec/lib/reveal/command_spec.rb +37 -0
  46. metadata +14 -16
  47. data/lib/reveal/templates/revealjs/lib/font/league-gothic/LICENSE +0 -2
  48. data/lib/reveal/templates/revealjs/lib/font/source-sans-pro/LICENSE +0 -45
  49. data/lib/reveal/templates/revealjs/plugin/leap/leap.js +0 -159
  50. data/lib/reveal/templates/revealjs/plugin/remotes/remotes.js +0 -39
@@ -1,9 +1,9 @@
1
1
  /*!
2
2
  * reveal.js
3
- * http://lab.hakim.se/reveal-js
3
+ * http://revealjs.com
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,10 +25,14 @@
25
25
 
26
26
  var Reveal;
27
27
 
28
+ // The reveal.js version
29
+ var VERSION = '3.6.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
37
  // Configuration defaults, can be overridden at initialization time
34
38
  config = {
@@ -39,21 +43,35 @@
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
- // Display controls in the bottom right corner
52
+ // Display presentation control arrows
49
53
  controls: true,
50
54
 
55
+ // Help the user learn the controls by providing hints, for example by
56
+ // bouncing the down arrow when they first encounter a vertical slide
57
+ controlsTutorial: true,
58
+
59
+ // Determines where controls appear, "edges" or "bottom-right"
60
+ controlsLayout: 'bottom-right',
61
+
62
+ // Visibility rule for backwards navigation arrows; "faded", "hidden"
63
+ // or "visible"
64
+ controlsBackArrows: 'faded',
65
+
51
66
  // Display a presentation progress bar
52
67
  progress: true,
53
68
 
54
69
  // Display the page number of the current slide
55
70
  slideNumber: false,
56
71
 
72
+ // Determine which displays to show the slide number on
73
+ showSlideNumber: 'all',
74
+
57
75
  // Push each slide change to the browser history
58
76
  history: false,
59
77
 
@@ -78,6 +96,9 @@
78
96
  // Change the presentation direction to be RTL
79
97
  rtl: false,
80
98
 
99
+ // Randomizes the order of slides each time the presentation loads
100
+ shuffle: false,
101
+
81
102
  // Turns fragments on and off globally
82
103
  fragments: true,
83
104
 
@@ -85,21 +106,35 @@
85
106
  // i.e. contained within a limited portion of the screen
86
107
  embedded: false,
87
108
 
88
- // Flags if we should show a help overlay when the questionmark
109
+ // Flags if we should show a help overlay when the question-mark
89
110
  // key is pressed
90
111
  help: true,
91
112
 
92
113
  // Flags if it should be possible to pause the presentation (blackout)
93
114
  pause: true,
94
115
 
95
- // Number of milliseconds between automatically proceeding to the
96
- // next slide, disabled when set to 0, this value can be overwritten
97
- // by using a data-autoslide attribute on your slides
116
+ // Flags if speaker notes should be visible to all viewers
117
+ showNotes: false,
118
+
119
+ // Global override for autolaying embedded media (video/audio/iframe)
120
+ // - null: Media will only autoplay if data-autoplay is present
121
+ // - true: All media will autoplay, regardless of individual setting
122
+ // - false: No media will autoplay, regardless of individual setting
123
+ autoPlayMedia: null,
124
+
125
+ // Controls automatic progression to the next slide
126
+ // - 0: Auto-sliding only happens if the data-autoslide HTML attribute
127
+ // is present on the current slide or fragment
128
+ // - 1+: All slides will progress automatically at the given interval
129
+ // - false: No auto-sliding, even if data-autoslide is present
98
130
  autoSlide: 0,
99
131
 
100
132
  // Stop auto-sliding after user input
101
133
  autoSlideStoppable: true,
102
134
 
135
+ // Use this method for navigation when auto-sliding (defaults to navigateNext)
136
+ autoSlideMethod: null,
137
+
103
138
  // Enable slide navigation via mouse wheel
104
139
  mouseWheel: false,
105
140
 
@@ -118,7 +153,7 @@
118
153
  // Dispatches all reveal.js events to the parent window through postMessage
119
154
  postMessageEvents: false,
120
155
 
121
- // Focuses body when page changes visiblity to ensure keyboard shortcuts work
156
+ // Focuses body when page changes visibility to ensure keyboard shortcuts work
122
157
  focusBodyOnPageVisibilityChange: true,
123
158
 
124
159
  // Transition style
@@ -140,20 +175,41 @@
140
175
  parallaxBackgroundHorizontal: null,
141
176
  parallaxBackgroundVertical: null,
142
177
 
178
+ // The maximum number of pages a single slide can expand onto when printing
179
+ // to PDF, unlimited by default
180
+ pdfMaxPagesPerSlide: Number.POSITIVE_INFINITY,
181
+
182
+ // Offset used to reduce the height of content within exported PDF pages.
183
+ // This exists to account for environment differences based on how you
184
+ // print to PDF. CLI printing options, like phantomjs and wkpdf, can end
185
+ // on precisely the total height of the document whereas in-browser
186
+ // printing has to end one pixel before.
187
+ pdfPageHeightOffset: -1,
188
+
143
189
  // Number of slides away from the current that are visible
144
190
  viewDistance: 3,
145
191
 
192
+ // The display mode that will be used to show slides
193
+ display: 'block',
194
+
146
195
  // Script dependencies to load
147
196
  dependencies: []
148
197
 
149
198
  },
150
199
 
200
+ // Flags if Reveal.initialize() has been called
201
+ initialized = false,
202
+
151
203
  // Flags if reveal.js is loaded (has dispatched the 'ready' event)
152
204
  loaded = false,
153
205
 
154
206
  // Flags if the overview mode is currently active
155
207
  overview = false,
156
208
 
209
+ // Holds the dimensions of our overview slides, including margins
210
+ overviewSlideWidth = null,
211
+ overviewSlideHeight = null,
212
+
157
213
  // The horizontal and vertical index of the currently active slide
158
214
  indexh,
159
215
  indexv,
@@ -164,6 +220,10 @@
164
220
 
165
221
  previousBackground,
166
222
 
223
+ // Remember which directions that the user has navigated towards
224
+ hasNavigatedRight = false,
225
+ hasNavigatedDown = false,
226
+
167
227
  // Slides may hold a data-state attribute which we pick up and apply
168
228
  // as a class to the body. This list contains the combined state of
169
229
  // all current slides.
@@ -185,6 +245,9 @@
185
245
  // Client is a mobile device, see #checkCapabilities()
186
246
  isMobileDevice,
187
247
 
248
+ // Client is a desktop Chrome, see #checkCapabilities()
249
+ isChrome,
250
+
188
251
  // Throttles mouse wheel navigation
189
252
  lastMouseWheelStep = 0,
190
253
 
@@ -233,6 +296,11 @@
233
296
  */
234
297
  function initialize( options ) {
235
298
 
299
+ // Make sure we only initialize once
300
+ if( initialized === true ) return;
301
+
302
+ initialized = true;
303
+
236
304
  checkCapabilities();
237
305
 
238
306
  if( !features.transforms2d && !features.transforms3d ) {
@@ -289,30 +357,37 @@
289
357
  */
290
358
  function checkCapabilities() {
291
359
 
292
- features.transforms3d = 'WebkitPerspective' in document.body.style ||
293
- 'MozPerspective' in document.body.style ||
294
- 'msPerspective' in document.body.style ||
295
- 'OPerspective' in document.body.style ||
296
- 'perspective' in document.body.style;
360
+ isMobileDevice = /(iphone|ipod|ipad|android)/gi.test( UA );
361
+ isChrome = /chrome/i.test( UA ) && !/edge/i.test( UA );
297
362
 
298
- features.transforms2d = 'WebkitTransform' in document.body.style ||
299
- 'MozTransform' in document.body.style ||
300
- 'msTransform' in document.body.style ||
301
- 'OTransform' in document.body.style ||
302
- 'transform' in document.body.style;
363
+ var testElement = document.createElement( 'div' );
364
+
365
+ features.transforms3d = 'WebkitPerspective' in testElement.style ||
366
+ 'MozPerspective' in testElement.style ||
367
+ 'msPerspective' in testElement.style ||
368
+ 'OPerspective' in testElement.style ||
369
+ 'perspective' in testElement.style;
370
+
371
+ features.transforms2d = 'WebkitTransform' in testElement.style ||
372
+ 'MozTransform' in testElement.style ||
373
+ 'msTransform' in testElement.style ||
374
+ 'OTransform' in testElement.style ||
375
+ 'transform' in testElement.style;
303
376
 
304
377
  features.requestAnimationFrameMethod = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame;
305
378
  features.requestAnimationFrame = typeof features.requestAnimationFrameMethod === 'function';
306
379
 
307
380
  features.canvas = !!document.createElement( 'canvas' ).getContext;
308
381
 
309
- features.touch = !!( 'ontouchstart' in window );
310
-
311
382
  // Transitions in the overview are disabled in desktop and
312
- // mobile Safari due to lag
313
- features.overviewTransitions = !/Version\/[\d\.]+.*Safari/.test( navigator.userAgent );
383
+ // Safari due to lag
384
+ features.overviewTransitions = !/Version\/[\d\.]+.*Safari/.test( UA );
314
385
 
315
- isMobileDevice = /(iphone|ipod|ipad|android)/gi.test( navigator.userAgent );
386
+ // Flags if we should use zoom instead of transform to scale
387
+ // up slides. Zoom produces crisper results but has a lot of
388
+ // xbrowser quirks so we only use it in whitelsited browsers.
389
+ features.zoom = 'zoom' in testElement.style && !isMobileDevice &&
390
+ ( isChrome || /Version\/[\d\.]+.*Safari/.test( UA ) );
316
391
 
317
392
  }
318
393
 
@@ -386,14 +461,16 @@
386
461
  */
387
462
  function start() {
388
463
 
464
+ loaded = true;
465
+
389
466
  // Make sure we've got all the DOM elements we need
390
467
  setupDOM();
391
468
 
392
469
  // Listen to messages posted to this window
393
470
  setupPostMessage();
394
471
 
395
- // Prevent iframes from scrolling the slides out of view
396
- setupIframeScrollPrevention();
472
+ // Prevent the slides from being scrolled out of view
473
+ setupScrollPrevention();
397
474
 
398
475
  // Resets all vertical slides so that only the first is visible
399
476
  resetVerticalSlides();
@@ -413,7 +490,7 @@
413
490
  // Enable transitions now that we're loaded
414
491
  dom.slides.classList.remove( 'no-transition' );
415
492
 
416
- loaded = true;
493
+ dom.wrapper.classList.add( 'ready' );
417
494
 
418
495
  dispatchEvent( 'ready', {
419
496
  'indexh': indexh,
@@ -448,6 +525,20 @@
448
525
  // Prevent transitions while we're loading
449
526
  dom.slides.classList.add( 'no-transition' );
450
527
 
528
+ if( isMobileDevice ) {
529
+ dom.wrapper.classList.add( 'no-hover' );
530
+ }
531
+ else {
532
+ dom.wrapper.classList.remove( 'no-hover' );
533
+ }
534
+
535
+ if( /iphone/gi.test( UA ) ) {
536
+ dom.wrapper.classList.add( 'ua-iphone' );
537
+ }
538
+ else {
539
+ dom.wrapper.classList.remove( 'ua-iphone' );
540
+ }
541
+
451
542
  // Background element
452
543
  dom.background = createSingletonNode( dom.wrapper, 'div', 'backgrounds', null );
453
544
 
@@ -456,22 +547,23 @@
456
547
  dom.progressbar = dom.progress.querySelector( 'span' );
457
548
 
458
549
  // Arrow controls
459
- createSingletonNode( dom.wrapper, 'aside', 'controls',
460
- '<div class="navigate-left"></div>' +
461
- '<div class="navigate-right"></div>' +
462
- '<div class="navigate-up"></div>' +
463
- '<div class="navigate-down"></div>' );
550
+ dom.controls = createSingletonNode( dom.wrapper, 'aside', 'controls',
551
+ '<button class="navigate-left" aria-label="previous slide"><div class="controls-arrow"></div></button>' +
552
+ '<button class="navigate-right" aria-label="next slide"><div class="controls-arrow"></div></button>' +
553
+ '<button class="navigate-up" aria-label="above slide"><div class="controls-arrow"></div></button>' +
554
+ '<button class="navigate-down" aria-label="below slide"><div class="controls-arrow"></div></button>' );
464
555
 
465
556
  // Slide number
466
557
  dom.slideNumber = createSingletonNode( dom.wrapper, 'div', 'slide-number', '' );
467
558
 
559
+ // Element containing notes that are visible to the audience
560
+ dom.speakerNotes = createSingletonNode( dom.wrapper, 'div', 'speaker-notes', null );
561
+ dom.speakerNotes.setAttribute( 'data-prevent-swipe', '' );
562
+ dom.speakerNotes.setAttribute( 'tabindex', '0' );
563
+
468
564
  // Overlay graphic which is displayed during the paused mode
469
565
  createSingletonNode( dom.wrapper, 'div', 'pause-overlay', null );
470
566
 
471
- // Cache references to elements
472
- dom.controls = document.querySelector( '.reveal .controls' );
473
- dom.theme = document.querySelector( '#theme' );
474
-
475
567
  dom.wrapper.setAttribute( 'role', 'application' );
476
568
 
477
569
  // There can be multiple instances of controls throughout the page
@@ -482,6 +574,10 @@
482
574
  dom.controlsPrev = toArray( document.querySelectorAll( '.navigate-prev' ) );
483
575
  dom.controlsNext = toArray( document.querySelectorAll( '.navigate-next' ) );
484
576
 
577
+ // The right and down arrows in the standard reveal.js controls
578
+ dom.controlsRightArrow = dom.controls.querySelector( '.navigate-right' );
579
+ dom.controlsDownArrow = dom.controls.querySelector( '.navigate-down' );
580
+
485
581
  dom.statusDiv = createStatusDiv();
486
582
  }
487
583
 
@@ -489,6 +585,8 @@
489
585
  * Creates a hidden div with role aria-live to announce the
490
586
  * current slide content. Hide the div off-screen to make it
491
587
  * available only to Assistive Technologies.
588
+ *
589
+ * @return {HTMLElement}
492
590
  */
493
591
  function createStatusDiv() {
494
592
 
@@ -498,7 +596,7 @@
498
596
  statusDiv.style.position = 'absolute';
499
597
  statusDiv.style.height = '1px';
500
598
  statusDiv.style.width = '1px';
501
- statusDiv.style.overflow ='hidden';
599
+ statusDiv.style.overflow = 'hidden';
502
600
  statusDiv.style.clip = 'rect( 1px, 1px, 1px, 1px )';
503
601
  statusDiv.setAttribute( 'id', 'aria-status-div' );
504
602
  statusDiv.setAttribute( 'aria-live', 'polite' );
@@ -509,6 +607,38 @@
509
607
 
510
608
  }
511
609
 
610
+ /**
611
+ * Converts the given HTML element into a string of text
612
+ * that can be announced to a screen reader. Hidden
613
+ * elements are excluded.
614
+ */
615
+ function getStatusText( node ) {
616
+
617
+ var text = '';
618
+
619
+ // Text node
620
+ if( node.nodeType === 3 ) {
621
+ text += node.textContent;
622
+ }
623
+ // Element node
624
+ else if( node.nodeType === 1 ) {
625
+
626
+ var isAriaHidden = node.getAttribute( 'aria-hidden' );
627
+ var isDisplayHidden = window.getComputedStyle( node )['display'] === 'none';
628
+ if( isAriaHidden !== 'true' && !isDisplayHidden ) {
629
+
630
+ toArray( node.childNodes ).forEach( function( child ) {
631
+ text += getStatusText( child );
632
+ } );
633
+
634
+ }
635
+
636
+ }
637
+
638
+ return text;
639
+
640
+ }
641
+
512
642
  /**
513
643
  * Configures the presentation for printing to a static
514
644
  * PDF.
@@ -519,14 +649,14 @@
519
649
 
520
650
  // Dimensions of the PDF pages
521
651
  var pageWidth = Math.floor( slideSize.width * ( 1 + config.margin ) ),
522
- pageHeight = Math.floor( slideSize.height * ( 1 + config.margin ) );
652
+ pageHeight = Math.floor( slideSize.height * ( 1 + config.margin ) );
523
653
 
524
654
  // Dimensions of slides within the pages
525
655
  var slideWidth = slideSize.width,
526
656
  slideHeight = slideSize.height;
527
657
 
528
658
  // Let the browser know what page size we want to print
529
- injectStyleSheet( '@page{size:'+ pageWidth +'px '+ pageHeight +'px; margin: 0;}' );
659
+ injectStyleSheet( '@page{size:'+ pageWidth +'px '+ pageHeight +'px; margin: 0px;}' );
530
660
 
531
661
  // Limit the size of certain elements to the dimensions of the slide
532
662
  injectStyleSheet( '.reveal section>img, .reveal section>video, .reveal section>iframe{max-width: '+ slideWidth +'px; max-height:'+ slideHeight +'px}' );
@@ -535,6 +665,22 @@
535
665
  document.body.style.width = pageWidth + 'px';
536
666
  document.body.style.height = pageHeight + 'px';
537
667
 
668
+ // Make sure stretch elements fit on slide
669
+ layoutSlideContents( slideWidth, slideHeight );
670
+
671
+ // Add each slide's index as attributes on itself, we need these
672
+ // indices to generate slide numbers below
673
+ toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( hslide, h ) {
674
+ hslide.setAttribute( 'data-index-h', h );
675
+
676
+ if( hslide.classList.contains( 'stack' ) ) {
677
+ toArray( hslide.querySelectorAll( 'section' ) ).forEach( function( vslide, v ) {
678
+ vslide.setAttribute( 'data-index-h', h );
679
+ vslide.setAttribute( 'data-index-v', v );
680
+ } );
681
+ }
682
+ } );
683
+
538
684
  // Slide and slide background layout
539
685
  toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
540
686
 
@@ -545,27 +691,73 @@
545
691
  var left = ( pageWidth - slideWidth ) / 2,
546
692
  top = ( pageHeight - slideHeight ) / 2;
547
693
 
548
- var contentHeight = getAbsoluteHeight( slide );
694
+ var contentHeight = slide.scrollHeight;
549
695
  var numberOfPages = Math.max( Math.ceil( contentHeight / pageHeight ), 1 );
550
696
 
697
+ // Adhere to configured pages per slide limit
698
+ numberOfPages = Math.min( numberOfPages, config.pdfMaxPagesPerSlide );
699
+
551
700
  // Center slides vertically
552
701
  if( numberOfPages === 1 && config.center || slide.classList.contains( 'center' ) ) {
553
702
  top = Math.max( ( pageHeight - contentHeight ) / 2, 0 );
554
703
  }
555
704
 
705
+ // Wrap the slide in a page element and hide its overflow
706
+ // so that no page ever flows onto another
707
+ var page = document.createElement( 'div' );
708
+ page.className = 'pdf-page';
709
+ page.style.height = ( ( pageHeight + config.pdfPageHeightOffset ) * numberOfPages ) + 'px';
710
+ slide.parentNode.insertBefore( page, slide );
711
+ page.appendChild( slide );
712
+
556
713
  // Position the slide inside of the page
557
714
  slide.style.left = left + 'px';
558
715
  slide.style.top = top + 'px';
559
716
  slide.style.width = slideWidth + 'px';
560
717
 
561
- // TODO Backgrounds need to be multiplied when the slide
562
- // stretches over multiple pages
563
- var background = slide.querySelector( '.slide-background' );
564
- if( background ) {
565
- background.style.width = pageWidth + 'px';
566
- background.style.height = ( pageHeight * numberOfPages ) + 'px';
567
- background.style.top = -top + 'px';
568
- background.style.left = -left + 'px';
718
+ if( slide.slideBackgroundElement ) {
719
+ page.insertBefore( slide.slideBackgroundElement, slide );
720
+ }
721
+
722
+ // Inject notes if `showNotes` is enabled
723
+ if( config.showNotes ) {
724
+
725
+ // Are there notes for this slide?
726
+ var notes = getSlideNotes( slide );
727
+ if( notes ) {
728
+
729
+ var notesSpacing = 8;
730
+ var notesLayout = typeof config.showNotes === 'string' ? config.showNotes : 'inline';
731
+ var notesElement = document.createElement( 'div' );
732
+ notesElement.classList.add( 'speaker-notes' );
733
+ notesElement.classList.add( 'speaker-notes-pdf' );
734
+ notesElement.setAttribute( 'data-layout', notesLayout );
735
+ notesElement.innerHTML = notes;
736
+
737
+ if( notesLayout === 'separate-page' ) {
738
+ page.parentNode.insertBefore( notesElement, page.nextSibling );
739
+ }
740
+ else {
741
+ notesElement.style.left = notesSpacing + 'px';
742
+ notesElement.style.bottom = notesSpacing + 'px';
743
+ notesElement.style.width = ( pageWidth - notesSpacing*2 ) + 'px';
744
+ page.appendChild( notesElement );
745
+ }
746
+
747
+ }
748
+
749
+ }
750
+
751
+ // Inject slide numbers if `slideNumbers` are enabled
752
+ if( config.slideNumber && /all|print/i.test( config.showSlideNumber ) ) {
753
+ var slideNumberH = parseInt( slide.getAttribute( 'data-index-h' ), 10 ) + 1,
754
+ slideNumberV = parseInt( slide.getAttribute( 'data-index-v' ), 10 ) + 1;
755
+
756
+ var numberElement = document.createElement( 'div' );
757
+ numberElement.classList.add( 'slide-number' );
758
+ numberElement.classList.add( 'slide-number-pdf' );
759
+ numberElement.innerHTML = formatSlideNumber( slideNumberH, '.', slideNumberV );
760
+ page.appendChild( numberElement );
569
761
  }
570
762
  }
571
763
 
@@ -576,25 +768,28 @@
576
768
  fragment.classList.add( 'visible' );
577
769
  } );
578
770
 
771
+ // Notify subscribers that the PDF layout is good to go
772
+ dispatchEvent( 'pdf-ready' );
773
+
579
774
  }
580
775
 
581
776
  /**
582
- * This is an unfortunate necessity. Iframes can trigger the
583
- * parent window to scroll, for example by focusing an input.
777
+ * This is an unfortunate necessity. Some actions such as
778
+ * an input field being focused in an iframe or using the
779
+ * keyboard to expand text selection beyond the bounds of
780
+ * a slide – can trigger our content to be pushed out of view.
584
781
  * This scrolling can not be prevented by hiding overflow in
585
- * CSS so we have to resort to repeatedly checking if the
586
- * browser has decided to offset our slides :(
782
+ * CSS (we already do) so we have to resort to repeatedly
783
+ * checking if the slides have been offset :(
587
784
  */
588
- function setupIframeScrollPrevention() {
785
+ function setupScrollPrevention() {
589
786
 
590
- if( dom.slides.querySelector( 'iframe' ) ) {
591
- setInterval( function() {
592
- if( dom.wrapper.scrollTop !== 0 || dom.wrapper.scrollLeft !== 0 ) {
593
- dom.wrapper.scrollTop = 0;
594
- dom.wrapper.scrollLeft = 0;
595
- }
596
- }, 500 );
597
- }
787
+ setInterval( function() {
788
+ if( dom.wrapper.scrollTop !== 0 || dom.wrapper.scrollLeft !== 0 ) {
789
+ dom.wrapper.scrollTop = 0;
790
+ dom.wrapper.scrollLeft = 0;
791
+ }
792
+ }, 1000 );
598
793
 
599
794
  }
600
795
 
@@ -602,6 +797,13 @@
602
797
  * Creates an HTML element and returns a reference to it.
603
798
  * If the element already exists the existing instance will
604
799
  * be returned.
800
+ *
801
+ * @param {HTMLElement} container
802
+ * @param {string} tagname
803
+ * @param {string} classname
804
+ * @param {string} innerHTML
805
+ *
806
+ * @return {HTMLElement}
605
807
  */
606
808
  function createSingletonNode( container, tagname, classname, innerHTML ) {
607
809
 
@@ -619,7 +821,7 @@
619
821
 
620
822
  // If no node was found, create it now
621
823
  var node = document.createElement( tagname );
622
- node.classList.add( classname );
824
+ node.className = classname;
623
825
  if( typeof innerHTML === 'string' ) {
624
826
  node.innerHTML = innerHTML;
625
827
  }
@@ -645,24 +847,12 @@
645
847
  // Iterate over all horizontal slides
646
848
  toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( slideh ) {
647
849
 
648
- var backgroundStack;
649
-
650
- if( printMode ) {
651
- backgroundStack = createBackground( slideh, slideh );
652
- }
653
- else {
654
- backgroundStack = createBackground( slideh, dom.background );
655
- }
850
+ var backgroundStack = createBackground( slideh, dom.background );
656
851
 
657
852
  // Iterate over all vertical slides
658
853
  toArray( slideh.querySelectorAll( 'section' ) ).forEach( function( slidev ) {
659
854
 
660
- if( printMode ) {
661
- createBackground( slidev, slidev );
662
- }
663
- else {
664
- createBackground( slidev, backgroundStack );
665
- }
855
+ createBackground( slidev, backgroundStack );
666
856
 
667
857
  backgroundStack.classList.add( 'stack' );
668
858
 
@@ -700,6 +890,7 @@
700
890
  * @param {HTMLElement} slide
701
891
  * @param {HTMLElement} container The element that the background
702
892
  * should be appended to
893
+ * @return {HTMLElement} New background div
703
894
  */
704
895
  function createBackground( slide, container ) {
705
896
 
@@ -722,7 +913,7 @@
722
913
 
723
914
  if( data.background ) {
724
915
  // Auto-wrap image urls in url(...)
725
- if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp)$/gi.test( data.background ) ) {
916
+ if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp)([?#]|$)/gi.test( data.background ) ) {
726
917
  slide.setAttribute( 'data-background-image', data.background );
727
918
  }
728
919
  else {
@@ -747,6 +938,7 @@
747
938
 
748
939
  // Additional and optional background properties
749
940
  if( data.backgroundSize ) element.style.backgroundSize = data.backgroundSize;
941
+ if( data.backgroundSize ) element.setAttribute( 'data-background-size', data.backgroundSize );
750
942
  if( data.backgroundColor ) element.style.backgroundColor = data.backgroundColor;
751
943
  if( data.backgroundRepeat ) element.style.backgroundRepeat = data.backgroundRepeat;
752
944
  if( data.backgroundPosition ) element.style.backgroundPosition = data.backgroundPosition;
@@ -758,18 +950,20 @@
758
950
  slide.classList.remove( 'has-dark-background' );
759
951
  slide.classList.remove( 'has-light-background' );
760
952
 
953
+ slide.slideBackgroundElement = element;
954
+
761
955
  // If this slide has a background color, add a class that
762
956
  // signals if it is light or dark. If the slide has no background
763
957
  // color, no class will be set
764
- var computedBackgroundColor = window.getComputedStyle( element ).backgroundColor;
765
- if( computedBackgroundColor ) {
766
- var rgb = colorToRgb( computedBackgroundColor );
958
+ var computedBackgroundStyle = window.getComputedStyle( element );
959
+ if( computedBackgroundStyle && computedBackgroundStyle.backgroundColor ) {
960
+ var rgb = colorToRgb( computedBackgroundStyle.backgroundColor );
767
961
 
768
962
  // Ignore fully transparent backgrounds. Some browsers return
769
963
  // rgba(0,0,0,0) when reading the computed background color of
770
964
  // an element with no background
771
965
  if( rgb && rgb.a !== 0 ) {
772
- if( colorBrightness( computedBackgroundColor ) < 128 ) {
966
+ if( colorBrightness( computedBackgroundStyle.backgroundColor ) < 128 ) {
773
967
  slide.classList.add( 'has-dark-background' );
774
968
  }
775
969
  else {
@@ -815,17 +1009,27 @@
815
1009
  /**
816
1010
  * Applies the configuration settings from the config
817
1011
  * object. May be called multiple times.
1012
+ *
1013
+ * @param {object} options
818
1014
  */
819
1015
  function configure( options ) {
820
1016
 
821
- var numberOfSlides = dom.wrapper.querySelectorAll( SLIDES_SELECTOR ).length;
822
-
823
- dom.wrapper.classList.remove( config.transition );
1017
+ var oldTransition = config.transition;
824
1018
 
825
1019
  // New config options may be passed when this method
826
1020
  // is invoked through the API after initialization
827
1021
  if( typeof options === 'object' ) extend( config, options );
828
1022
 
1023
+ // Abort if reveal.js hasn't finished loading, config
1024
+ // changes will be applied automatically once loading
1025
+ // finishes
1026
+ if( loaded === false ) return;
1027
+
1028
+ var numberOfSlides = dom.wrapper.querySelectorAll( SLIDES_SELECTOR ).length;
1029
+
1030
+ // Remove the previously configured transition class
1031
+ dom.wrapper.classList.remove( oldTransition );
1032
+
829
1033
  // Force linear transition based on browser capabilities
830
1034
  if( features.transforms3d === false ) config.transition = 'linear';
831
1035
 
@@ -837,6 +1041,13 @@
837
1041
  dom.controls.style.display = config.controls ? 'block' : 'none';
838
1042
  dom.progress.style.display = config.progress ? 'block' : 'none';
839
1043
 
1044
+ dom.controls.setAttribute( 'data-controls-layout', config.controlsLayout );
1045
+ dom.controls.setAttribute( 'data-controls-back-arrows', config.controlsBackArrows );
1046
+
1047
+ if( config.shuffle ) {
1048
+ shuffle();
1049
+ }
1050
+
840
1051
  if( config.rtl ) {
841
1052
  dom.wrapper.classList.add( 'rtl' );
842
1053
  }
@@ -856,6 +1067,10 @@
856
1067
  resume();
857
1068
  }
858
1069
 
1070
+ if( config.showNotes ) {
1071
+ dom.speakerNotes.setAttribute( 'data-layout', typeof config.showNotes === 'string' ? config.showNotes : 'inline' );
1072
+ }
1073
+
859
1074
  if( config.mouseWheel ) {
860
1075
  document.addEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF
861
1076
  document.addEventListener( 'mousewheel', onDocumentMouseScroll, false );
@@ -876,10 +1091,11 @@
876
1091
  // Iframe link previews
877
1092
  if( config.previewLinks ) {
878
1093
  enablePreviewLinks();
1094
+ disablePreviewLinks( '[data-preview-link=false]' );
879
1095
  }
880
1096
  else {
881
1097
  disablePreviewLinks();
882
- enablePreviewLinks( '[data-preview-link]' );
1098
+ enablePreviewLinks( '[data-preview-link]:not([data-preview-link=false])' );
883
1099
  }
884
1100
 
885
1101
  // Remove existing auto-slide controls
@@ -906,6 +1122,19 @@
906
1122
  } );
907
1123
  }
908
1124
 
1125
+ // Slide numbers
1126
+ var slideNumberDisplay = 'none';
1127
+ if( config.slideNumber && !isPrintingPDF() ) {
1128
+ if( config.showSlideNumber === 'all' ) {
1129
+ slideNumberDisplay = 'block';
1130
+ }
1131
+ else if( config.showSlideNumber === 'speaker' && isSpeakerNotes() ) {
1132
+ slideNumberDisplay = 'block';
1133
+ }
1134
+ }
1135
+
1136
+ dom.slideNumber.style.display = slideNumberDisplay;
1137
+
909
1138
  sync();
910
1139
 
911
1140
  }
@@ -973,7 +1202,7 @@
973
1202
 
974
1203
  // Only support touch for Android, fixes double navigations in
975
1204
  // stock browser
976
- if( navigator.userAgent.match( /android/gi ) ) {
1205
+ if( UA.match( /android/gi ) ) {
977
1206
  pointerEvents = [ 'touchstart' ];
978
1207
  }
979
1208
 
@@ -1035,6 +1264,9 @@
1035
1264
  /**
1036
1265
  * Extend object a with the properties of object b.
1037
1266
  * If there's a conflict, object b takes precedence.
1267
+ *
1268
+ * @param {object} a
1269
+ * @param {object} b
1038
1270
  */
1039
1271
  function extend( a, b ) {
1040
1272
 
@@ -1042,10 +1274,15 @@
1042
1274
  a[ i ] = b[ i ];
1043
1275
  }
1044
1276
 
1277
+ return a;
1278
+
1045
1279
  }
1046
1280
 
1047
1281
  /**
1048
1282
  * Converts the target object to an array.
1283
+ *
1284
+ * @param {object} o
1285
+ * @return {object[]}
1049
1286
  */
1050
1287
  function toArray( o ) {
1051
1288
 
@@ -1055,6 +1292,9 @@
1055
1292
 
1056
1293
  /**
1057
1294
  * Utility for deserializing a value.
1295
+ *
1296
+ * @param {*} value
1297
+ * @return {*}
1058
1298
  */
1059
1299
  function deserialize( value ) {
1060
1300
 
@@ -1062,7 +1302,7 @@
1062
1302
  if( value === 'null' ) return null;
1063
1303
  else if( value === 'true' ) return true;
1064
1304
  else if( value === 'false' ) return false;
1065
- else if( value.match( /^\d+$/ ) ) return parseFloat( value );
1305
+ else if( value.match( /^-?[\d\.]+$/ ) ) return parseFloat( value );
1066
1306
  }
1067
1307
 
1068
1308
  return value;
@@ -1073,8 +1313,10 @@
1073
1313
  * Measures the distance in pixels between point a
1074
1314
  * and point b.
1075
1315
  *
1076
- * @param {Object} a point with x/y properties
1077
- * @param {Object} b point with x/y properties
1316
+ * @param {object} a point with x/y properties
1317
+ * @param {object} b point with x/y properties
1318
+ *
1319
+ * @return {number}
1078
1320
  */
1079
1321
  function distanceBetween( a, b ) {
1080
1322
 
@@ -1087,6 +1329,9 @@
1087
1329
 
1088
1330
  /**
1089
1331
  * Applies a CSS transform to the target element.
1332
+ *
1333
+ * @param {HTMLElement} element
1334
+ * @param {string} transform
1090
1335
  */
1091
1336
  function transformElement( element, transform ) {
1092
1337
 
@@ -1101,6 +1346,8 @@
1101
1346
  * Applies CSS transforms to the slides container. The container
1102
1347
  * is transformed from two separate sources: layout and the overview
1103
1348
  * mode.
1349
+ *
1350
+ * @param {object} transforms
1104
1351
  */
1105
1352
  function transformSlides( transforms ) {
1106
1353
 
@@ -1120,6 +1367,8 @@
1120
1367
 
1121
1368
  /**
1122
1369
  * Injects the given CSS styles into the DOM.
1370
+ *
1371
+ * @param {string} value
1123
1372
  */
1124
1373
  function injectStyleSheet( value ) {
1125
1374
 
@@ -1135,14 +1384,56 @@
1135
1384
 
1136
1385
  }
1137
1386
 
1387
+ /**
1388
+ * Find the closest parent that matches the given
1389
+ * selector.
1390
+ *
1391
+ * @param {HTMLElement} target The child element
1392
+ * @param {String} selector The CSS selector to match
1393
+ * the parents against
1394
+ *
1395
+ * @return {HTMLElement} The matched parent or null
1396
+ * if no matching parent was found
1397
+ */
1398
+ function closestParent( target, selector ) {
1399
+
1400
+ var parent = target.parentNode;
1401
+
1402
+ while( parent ) {
1403
+
1404
+ // There's some overhead doing this each time, we don't
1405
+ // want to rewrite the element prototype but should still
1406
+ // be enough to feature detect once at startup...
1407
+ var matchesMethod = parent.matches || parent.matchesSelector || parent.msMatchesSelector;
1408
+
1409
+ // If we find a match, we're all set
1410
+ if( matchesMethod && matchesMethod.call( parent, selector ) ) {
1411
+ return parent;
1412
+ }
1413
+
1414
+ // Keep searching
1415
+ parent = parent.parentNode;
1416
+
1417
+ }
1418
+
1419
+ return null;
1420
+
1421
+ }
1422
+
1138
1423
  /**
1139
1424
  * Converts various color input formats to an {r:0,g:0,b:0} object.
1140
1425
  *
1141
- * @param {String} color The string representation of a color,
1142
- * the following formats are supported:
1143
- * - #000
1144
- * - #000000
1145
- * - rgb(0,0,0)
1426
+ * @param {string} color The string representation of a color
1427
+ * @example
1428
+ * colorToRgb('#000');
1429
+ * @example
1430
+ * colorToRgb('#000000');
1431
+ * @example
1432
+ * colorToRgb('rgb(0,0,0)');
1433
+ * @example
1434
+ * colorToRgb('rgba(0,0,0)');
1435
+ *
1436
+ * @return {{r: number, g: number, b: number, [a]: number}|null}
1146
1437
  */
1147
1438
  function colorToRgb( color ) {
1148
1439
 
@@ -1192,7 +1483,8 @@
1192
1483
  /**
1193
1484
  * Calculates brightness on a scale of 0-255.
1194
1485
  *
1195
- * @param color See colorStringToRgb for supported formats.
1486
+ * @param {string} color See colorToRgb for supported formats.
1487
+ * @see {@link colorToRgb}
1196
1488
  */
1197
1489
  function colorBrightness( color ) {
1198
1490
 
@@ -1206,46 +1498,14 @@
1206
1498
 
1207
1499
  }
1208
1500
 
1209
- /**
1210
- * Retrieves the height of the given element by looking
1211
- * at the position and height of its immediate children.
1212
- */
1213
- function getAbsoluteHeight( element ) {
1214
-
1215
- var height = 0;
1216
-
1217
- if( element ) {
1218
- var absoluteChildren = 0;
1219
-
1220
- toArray( element.childNodes ).forEach( function( child ) {
1221
-
1222
- if( typeof child.offsetTop === 'number' && child.style ) {
1223
- // Count # of abs children
1224
- if( window.getComputedStyle( child ).position === 'absolute' ) {
1225
- absoluteChildren += 1;
1226
- }
1227
-
1228
- height = Math.max( height, child.offsetTop + child.offsetHeight );
1229
- }
1230
-
1231
- } );
1232
-
1233
- // If there are no absolute children, use offsetHeight
1234
- if( absoluteChildren === 0 ) {
1235
- height = element.offsetHeight;
1236
- }
1237
-
1238
- }
1239
-
1240
- return height;
1241
-
1242
- }
1243
-
1244
1501
  /**
1245
1502
  * Returns the remaining height within the parent of the
1246
1503
  * target element.
1247
1504
  *
1248
1505
  * remaining height = [ configured parent height ] - [ current parent height ]
1506
+ *
1507
+ * @param {HTMLElement} element
1508
+ * @param {number} [height]
1249
1509
  */
1250
1510
  function getRemainingHeight( element, height ) {
1251
1511
 
@@ -1368,6 +1628,8 @@
1368
1628
 
1369
1629
  /**
1370
1630
  * Bind preview frame links.
1631
+ *
1632
+ * @param {string} [selector=a] - selector for anchors
1371
1633
  */
1372
1634
  function enablePreviewLinks( selector ) {
1373
1635
 
@@ -1384,9 +1646,9 @@
1384
1646
  /**
1385
1647
  * Unbind preview frame links.
1386
1648
  */
1387
- function disablePreviewLinks() {
1649
+ function disablePreviewLinks( selector ) {
1388
1650
 
1389
- var anchors = toArray( document.querySelectorAll( 'a' ) );
1651
+ var anchors = toArray( document.querySelectorAll( selector ? selector : 'a' ) );
1390
1652
 
1391
1653
  anchors.forEach( function( element ) {
1392
1654
  if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) {
@@ -1398,6 +1660,8 @@
1398
1660
 
1399
1661
  /**
1400
1662
  * Opens a preview window for the target URL.
1663
+ *
1664
+ * @param {string} url - url for preview iframe src
1401
1665
  */
1402
1666
  function showPreview( url ) {
1403
1667
 
@@ -1416,6 +1680,9 @@
1416
1680
  '<div class="spinner"></div>',
1417
1681
  '<div class="viewport">',
1418
1682
  '<iframe src="'+ url +'"></iframe>',
1683
+ '<small class="viewport-inner">',
1684
+ '<span class="x-frame-error">Unable to load iframe. This is likely due to the site\'s policy (x-frame-options).</span>',
1685
+ '</small>',
1419
1686
  '</div>'
1420
1687
  ].join('');
1421
1688
 
@@ -1439,7 +1706,29 @@
1439
1706
  }
1440
1707
 
1441
1708
  /**
1442
- * Opens a overlay window with help material.
1709
+ * Open or close help overlay window.
1710
+ *
1711
+ * @param {Boolean} [override] Flag which overrides the
1712
+ * toggle logic and forcibly sets the desired state. True means
1713
+ * help is open, false means it's closed.
1714
+ */
1715
+ function toggleHelp( override ){
1716
+
1717
+ if( typeof override === 'boolean' ) {
1718
+ override ? showHelp() : closeOverlay();
1719
+ }
1720
+ else {
1721
+ if( dom.overlay ) {
1722
+ closeOverlay();
1723
+ }
1724
+ else {
1725
+ showHelp();
1726
+ }
1727
+ }
1728
+ }
1729
+
1730
+ /**
1731
+ * Opens an overlay window with help material.
1443
1732
  */
1444
1733
  function showHelp() {
1445
1734
 
@@ -1505,10 +1794,8 @@
1505
1794
 
1506
1795
  var size = getComputedSlideSize();
1507
1796
 
1508
- var slidePadding = 20; // TODO Dig this out of DOM
1509
-
1510
1797
  // Layout the contents of the slides
1511
- layoutSlideContents( config.width, config.height, slidePadding );
1798
+ layoutSlideContents( config.width, config.height );
1512
1799
 
1513
1800
  dom.slides.style.width = size.width + 'px';
1514
1801
  dom.slides.style.height = size.height + 'px';
@@ -1530,13 +1817,20 @@
1530
1817
  transformSlides( { layout: '' } );
1531
1818
  }
1532
1819
  else {
1533
- // Prefer zooming in desktop Chrome so that content remains crisp
1534
- if( !isMobileDevice && /chrome/i.test( navigator.userAgent ) && typeof dom.slides.style.zoom !== 'undefined' ) {
1820
+ // Prefer zoom for scaling up so that content remains crisp.
1821
+ // Don't use zoom to scale down since that can lead to shifts
1822
+ // in text layout/line breaks.
1823
+ if( scale > 1 && features.zoom ) {
1535
1824
  dom.slides.style.zoom = scale;
1825
+ dom.slides.style.left = '';
1826
+ dom.slides.style.top = '';
1827
+ dom.slides.style.bottom = '';
1828
+ dom.slides.style.right = '';
1536
1829
  transformSlides( { layout: '' } );
1537
1830
  }
1538
1831
  // Apply scale transform as a fallback
1539
1832
  else {
1833
+ dom.slides.style.zoom = '';
1540
1834
  dom.slides.style.left = '50%';
1541
1835
  dom.slides.style.top = '50%';
1542
1836
  dom.slides.style.bottom = 'auto';
@@ -1563,7 +1857,7 @@
1563
1857
  slide.style.top = 0;
1564
1858
  }
1565
1859
  else {
1566
- slide.style.top = Math.max( ( ( size.height - getAbsoluteHeight( slide ) ) / 2 ) - slidePadding, 0 ) + 'px';
1860
+ slide.style.top = Math.max( ( size.height - slide.scrollHeight ) / 2, 0 ) + 'px';
1567
1861
  }
1568
1862
  }
1569
1863
  else {
@@ -1575,6 +1869,10 @@
1575
1869
  updateProgress();
1576
1870
  updateParallax();
1577
1871
 
1872
+ if( isOverview() ) {
1873
+ updateOverview();
1874
+ }
1875
+
1578
1876
  }
1579
1877
 
1580
1878
  }
@@ -1582,8 +1880,11 @@
1582
1880
  /**
1583
1881
  * Applies layout logic to the contents of all slides in
1584
1882
  * the presentation.
1883
+ *
1884
+ * @param {string|number} width
1885
+ * @param {string|number} height
1585
1886
  */
1586
- function layoutSlideContents( width, height, padding ) {
1887
+ function layoutSlideContents( width, height ) {
1587
1888
 
1588
1889
  // Handle sizing of elements with the 'stretch' class
1589
1890
  toArray( dom.slides.querySelectorAll( 'section > .stretch' ) ).forEach( function( element ) {
@@ -1615,6 +1916,9 @@
1615
1916
  * Calculates the computed pixel size of our slides. These
1616
1917
  * values are based on the width and height configuration
1617
1918
  * options.
1919
+ *
1920
+ * @param {number} [presentationWidth=dom.wrapper.offsetWidth]
1921
+ * @param {number} [presentationHeight=dom.wrapper.offsetHeight]
1618
1922
  */
1619
1923
  function getComputedSlideSize( presentationWidth, presentationHeight ) {
1620
1924
 
@@ -1652,7 +1956,7 @@
1652
1956
  * from the stack.
1653
1957
  *
1654
1958
  * @param {HTMLElement} stack The vertical stack element
1655
- * @param {int} v Index to memorize
1959
+ * @param {string|number} [v=0] Index to memorize
1656
1960
  */
1657
1961
  function setPreviousVerticalIndex( stack, v ) {
1658
1962
 
@@ -1716,6 +2020,17 @@
1716
2020
  }
1717
2021
  } );
1718
2022
 
2023
+ // Calculate slide sizes
2024
+ var margin = 70;
2025
+ var slideSize = getComputedSlideSize();
2026
+ overviewSlideWidth = slideSize.width + margin;
2027
+ overviewSlideHeight = slideSize.height + margin;
2028
+
2029
+ // Reverse in RTL mode
2030
+ if( config.rtl ) {
2031
+ overviewSlideWidth = -overviewSlideWidth;
2032
+ }
2033
+
1719
2034
  updateSlidesVisibility();
1720
2035
  layoutOverview();
1721
2036
  updateOverview();
@@ -1739,19 +2054,10 @@
1739
2054
  */
1740
2055
  function layoutOverview() {
1741
2056
 
1742
- var margin = 70;
1743
- var slideWidth = config.width + margin,
1744
- slideHeight = config.height + margin;
1745
-
1746
- // Reverse in RTL mode
1747
- if( config.rtl ) {
1748
- slideWidth = -slideWidth;
1749
- }
1750
-
1751
2057
  // Layout slides
1752
2058
  toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( hslide, h ) {
1753
2059
  hslide.setAttribute( 'data-index-h', h );
1754
- transformElement( hslide, 'translate3d(' + ( h * slideWidth ) + 'px, 0, 0)' );
2060
+ transformElement( hslide, 'translate3d(' + ( h * overviewSlideWidth ) + 'px, 0, 0)' );
1755
2061
 
1756
2062
  if( hslide.classList.contains( 'stack' ) ) {
1757
2063
 
@@ -1759,7 +2065,7 @@
1759
2065
  vslide.setAttribute( 'data-index-h', h );
1760
2066
  vslide.setAttribute( 'data-index-v', v );
1761
2067
 
1762
- transformElement( vslide, 'translate3d(0, ' + ( v * slideHeight ) + 'px, 0)' );
2068
+ transformElement( vslide, 'translate3d(0, ' + ( v * overviewSlideHeight ) + 'px, 0)' );
1763
2069
  } );
1764
2070
 
1765
2071
  }
@@ -1767,10 +2073,10 @@
1767
2073
 
1768
2074
  // Layout slide backgrounds
1769
2075
  toArray( dom.background.childNodes ).forEach( function( hbackground, h ) {
1770
- transformElement( hbackground, 'translate3d(' + ( h * slideWidth ) + 'px, 0, 0)' );
2076
+ transformElement( hbackground, 'translate3d(' + ( h * overviewSlideWidth ) + 'px, 0, 0)' );
1771
2077
 
1772
2078
  toArray( hbackground.querySelectorAll( '.slide-background' ) ).forEach( function( vbackground, v ) {
1773
- transformElement( vbackground, 'translate3d(0, ' + ( v * slideHeight ) + 'px, 0)' );
2079
+ transformElement( vbackground, 'translate3d(0, ' + ( v * overviewSlideHeight ) + 'px, 0)' );
1774
2080
  } );
1775
2081
  } );
1776
2082
 
@@ -1782,20 +2088,14 @@
1782
2088
  */
1783
2089
  function updateOverview() {
1784
2090
 
1785
- var margin = 70;
1786
- var slideWidth = config.width + margin,
1787
- slideHeight = config.height + margin;
1788
-
1789
- // Reverse in RTL mode
1790
- if( config.rtl ) {
1791
- slideWidth = -slideWidth;
1792
- }
2091
+ var vmin = Math.min( window.innerWidth, window.innerHeight );
2092
+ var scale = Math.max( vmin / 5, 150 ) / vmin;
1793
2093
 
1794
2094
  transformSlides( {
1795
2095
  overview: [
1796
- 'translateX('+ ( -indexh * slideWidth ) +'px)',
1797
- 'translateY('+ ( -indexv * slideHeight ) +'px)',
1798
- 'translateZ('+ ( window.innerWidth < 400 ? -1000 : -2500 ) +'px)'
2096
+ 'scale('+ scale +')',
2097
+ 'translateX('+ ( -indexh * overviewSlideWidth ) +'px)',
2098
+ 'translateY('+ ( -indexv * overviewSlideHeight ) +'px)'
1799
2099
  ].join( ' ' )
1800
2100
  } );
1801
2101
 
@@ -1860,7 +2160,7 @@
1860
2160
  /**
1861
2161
  * Toggles the slide overview mode on and off.
1862
2162
  *
1863
- * @param {Boolean} override Optional flag which overrides the
2163
+ * @param {Boolean} [override] Flag which overrides the
1864
2164
  * toggle logic and forcibly sets the desired state. True means
1865
2165
  * overview is open, false means it's closed.
1866
2166
  */
@@ -1891,8 +2191,9 @@
1891
2191
  * Checks if the current or specified slide is vertical
1892
2192
  * (nested within another slide).
1893
2193
  *
1894
- * @param {HTMLElement} slide [optional] The slide to check
2194
+ * @param {HTMLElement} [slide=currentSlide] The slide to check
1895
2195
  * orientation of
2196
+ * @return {Boolean}
1896
2197
  */
1897
2198
  function isVerticalSlide( slide ) {
1898
2199
 
@@ -1911,10 +2212,10 @@
1911
2212
  */
1912
2213
  function enterFullscreen() {
1913
2214
 
1914
- var element = document.body;
2215
+ var element = document.documentElement;
1915
2216
 
1916
2217
  // Check which implementation is available
1917
- var requestMethod = element.requestFullScreen ||
2218
+ var requestMethod = element.requestFullscreen ||
1918
2219
  element.webkitRequestFullscreen ||
1919
2220
  element.webkitRequestFullScreen ||
1920
2221
  element.mozRequestFullScreen ||
@@ -1977,6 +2278,8 @@
1977
2278
 
1978
2279
  /**
1979
2280
  * Checks if we are currently in the paused mode.
2281
+ *
2282
+ * @return {Boolean}
1980
2283
  */
1981
2284
  function isPaused() {
1982
2285
 
@@ -1987,7 +2290,7 @@
1987
2290
  /**
1988
2291
  * Toggles the auto slide mode on and off.
1989
2292
  *
1990
- * @param {Boolean} override Optional flag which sets the desired state.
2293
+ * @param {Boolean} [override] Flag which sets the desired state.
1991
2294
  * True means autoplay starts, false means it stops.
1992
2295
  */
1993
2296
 
@@ -2005,6 +2308,8 @@
2005
2308
 
2006
2309
  /**
2007
2310
  * Checks if the auto slide mode is currently on.
2311
+ *
2312
+ * @return {Boolean}
2008
2313
  */
2009
2314
  function isAutoSliding() {
2010
2315
 
@@ -2017,11 +2322,11 @@
2017
2322
  * slide which matches the specified horizontal and vertical
2018
2323
  * indices.
2019
2324
  *
2020
- * @param {int} h Horizontal index of the target slide
2021
- * @param {int} v Vertical index of the target slide
2022
- * @param {int} f Optional index of a fragment within the
2325
+ * @param {number} [h=indexh] Horizontal index of the target slide
2326
+ * @param {number} [v=indexv] Vertical index of the target slide
2327
+ * @param {number} [f] Index of a fragment within the
2023
2328
  * target slide to activate
2024
- * @param {int} o Optional origin for use in multimaster environments
2329
+ * @param {number} [o] Origin for use in multimaster environments
2025
2330
  */
2026
2331
  function slide( h, v, f, o ) {
2027
2332
 
@@ -2031,6 +2336,9 @@
2031
2336
  // Query all horizontal slides in the deck
2032
2337
  var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR );
2033
2338
 
2339
+ // Abort if there are no slides
2340
+ if( horizontalSlides.length === 0 ) return;
2341
+
2034
2342
  // If no vertical index is specified and the upcoming slide is a
2035
2343
  // stack, resume at its previous vertical index
2036
2344
  if( v === undefined && !isOverview() ) {
@@ -2147,13 +2455,14 @@
2147
2455
  }
2148
2456
 
2149
2457
  // Announce the current slide contents, for screen readers
2150
- dom.statusDiv.textContent = currentSlide.textContent;
2458
+ dom.statusDiv.textContent = getStatusText( currentSlide );
2151
2459
 
2152
2460
  updateControls();
2153
2461
  updateProgress();
2154
2462
  updateBackground();
2155
2463
  updateParallax();
2156
2464
  updateSlideNumber();
2465
+ updateNotes();
2157
2466
 
2158
2467
  // Update the URL hash
2159
2468
  writeURL();
@@ -2192,12 +2501,21 @@
2192
2501
 
2193
2502
  updateControls();
2194
2503
  updateProgress();
2195
- updateBackground( true );
2196
2504
  updateSlideNumber();
2197
2505
  updateSlidesVisibility();
2506
+ updateBackground( true );
2507
+ updateNotesVisibility();
2508
+ updateNotes();
2198
2509
 
2199
2510
  formatEmbeddedContent();
2200
- startEmbeddedContent( currentSlide );
2511
+
2512
+ // Start or stop embedded content depending on global config
2513
+ if( config.autoPlayMedia === false ) {
2514
+ stopEmbeddedContent( currentSlide, { unloadIframes: false } );
2515
+ }
2516
+ else {
2517
+ startEmbeddedContent( currentSlide );
2518
+ }
2201
2519
 
2202
2520
  if( isOverview() ) {
2203
2521
  layoutOverview();
@@ -2252,16 +2570,33 @@
2252
2570
 
2253
2571
  }
2254
2572
 
2573
+ /**
2574
+ * Randomly shuffles all slides in the deck.
2575
+ */
2576
+ function shuffle() {
2577
+
2578
+ var slides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
2579
+
2580
+ slides.forEach( function( slide ) {
2581
+
2582
+ // Insert this slide next to another random slide. This may
2583
+ // cause the slide to insert before itself but that's fine.
2584
+ dom.slides.insertBefore( slide, slides[ Math.floor( Math.random() * slides.length ) ] );
2585
+
2586
+ } );
2587
+
2588
+ }
2589
+
2255
2590
  /**
2256
2591
  * Updates one dimension of slides by showing the slide
2257
2592
  * with the specified index.
2258
2593
  *
2259
- * @param {String} selector A CSS selector that will fetch
2594
+ * @param {string} selector A CSS selector that will fetch
2260
2595
  * the group of slides we are working with
2261
- * @param {Number} index The index of the slide that should be
2596
+ * @param {number} index The index of the slide that should be
2262
2597
  * shown
2263
2598
  *
2264
- * @return {Number} The index of the slide that is now shown,
2599
+ * @return {number} The index of the slide that is now shown,
2265
2600
  * might differ from the passed in index if it was out of
2266
2601
  * bounds.
2267
2602
  */
@@ -2413,10 +2748,10 @@
2413
2748
 
2414
2749
  // Show the horizontal slide if it's within the view distance
2415
2750
  if( distanceX < viewDistance ) {
2416
- showSlide( horizontalSlide );
2751
+ loadSlide( horizontalSlide );
2417
2752
  }
2418
2753
  else {
2419
- hideSlide( horizontalSlide );
2754
+ unloadSlide( horizontalSlide );
2420
2755
  }
2421
2756
 
2422
2757
  if( verticalSlidesLength ) {
@@ -2429,20 +2764,79 @@
2429
2764
  distanceY = x === ( indexh || 0 ) ? Math.abs( ( indexv || 0 ) - y ) : Math.abs( y - oy );
2430
2765
 
2431
2766
  if( distanceX + distanceY < viewDistance ) {
2432
- showSlide( verticalSlide );
2767
+ loadSlide( verticalSlide );
2433
2768
  }
2434
2769
  else {
2435
- hideSlide( verticalSlide );
2770
+ unloadSlide( verticalSlide );
2436
2771
  }
2437
2772
  }
2438
2773
 
2439
2774
  }
2440
2775
  }
2441
2776
 
2777
+ // Flag if there are ANY vertical slides, anywhere in the deck
2778
+ if( dom.wrapper.querySelectorAll( '.slides>section>section' ).length ) {
2779
+ dom.wrapper.classList.add( 'has-vertical-slides' );
2780
+ }
2781
+ else {
2782
+ dom.wrapper.classList.remove( 'has-vertical-slides' );
2783
+ }
2784
+
2785
+ // Flag if there are ANY horizontal slides, anywhere in the deck
2786
+ if( dom.wrapper.querySelectorAll( '.slides>section' ).length > 1 ) {
2787
+ dom.wrapper.classList.add( 'has-horizontal-slides' );
2788
+ }
2789
+ else {
2790
+ dom.wrapper.classList.remove( 'has-horizontal-slides' );
2791
+ }
2792
+
2793
+ }
2794
+
2795
+ }
2796
+
2797
+ /**
2798
+ * Pick up notes from the current slide and display them
2799
+ * to the viewer.
2800
+ *
2801
+ * @see {@link config.showNotes}
2802
+ */
2803
+ function updateNotes() {
2804
+
2805
+ if( config.showNotes && dom.speakerNotes && currentSlide && !isPrintingPDF() ) {
2806
+
2807
+ dom.speakerNotes.innerHTML = getSlideNotes() || '<span class="notes-placeholder">No notes on this slide.</span>';
2808
+
2442
2809
  }
2443
2810
 
2444
2811
  }
2445
2812
 
2813
+ /**
2814
+ * Updates the visibility of the speaker notes sidebar that
2815
+ * is used to share annotated slides. The notes sidebar is
2816
+ * only visible if showNotes is true and there are notes on
2817
+ * one or more slides in the deck.
2818
+ */
2819
+ function updateNotesVisibility() {
2820
+
2821
+ if( config.showNotes && hasNotes() ) {
2822
+ dom.wrapper.classList.add( 'show-notes' );
2823
+ }
2824
+ else {
2825
+ dom.wrapper.classList.remove( 'show-notes' );
2826
+ }
2827
+
2828
+ }
2829
+
2830
+ /**
2831
+ * Checks if there are speaker notes for ANY slide in the
2832
+ * presentation.
2833
+ */
2834
+ function hasNotes() {
2835
+
2836
+ return dom.slides.querySelectorAll( '[data-notes], aside.notes' ).length > 0;
2837
+
2838
+ }
2839
+
2446
2840
  /**
2447
2841
  * Updates the progress bar to reflect the current slide.
2448
2842
  */
@@ -2460,30 +2854,64 @@
2460
2854
  /**
2461
2855
  * Updates the slide number div to reflect the current slide.
2462
2856
  *
2463
- * Slide number format can be defined as a string using the
2464
- * following variables:
2465
- * h: current slide's horizontal index
2466
- * v: current slide's vertical index
2467
- * c: current slide index (flattened)
2468
- * t: total number of slides (flattened)
2857
+ * The following slide number formats are available:
2858
+ * "h.v": horizontal . vertical slide number (default)
2859
+ * "h/v": horizontal / vertical slide number
2860
+ * "c": flattened slide number
2861
+ * "c/t": flattened slide number / total slides
2469
2862
  */
2470
2863
  function updateSlideNumber() {
2471
2864
 
2472
2865
  // Update slide number if enabled
2473
- if( config.slideNumber && dom.slideNumber) {
2866
+ if( config.slideNumber && dom.slideNumber ) {
2474
2867
 
2475
- // Default to only showing the current slide number
2476
- var format = 'c';
2868
+ var value = [];
2869
+ var format = 'h.v';
2477
2870
 
2478
- // Check if a custom slide number format is available
2871
+ // Check if a custom number format is available
2479
2872
  if( typeof config.slideNumber === 'string' ) {
2480
2873
  format = config.slideNumber;
2481
2874
  }
2482
2875
 
2483
- dom.slideNumber.innerHTML = format.replace( /h/g, indexh )
2484
- .replace( /v/g, indexv )
2485
- .replace( /c/g, getSlidePastCount() + 1 )
2486
- .replace( /t/g, getTotalSlides() );
2876
+ switch( format ) {
2877
+ case 'c':
2878
+ value.push( getSlidePastCount() + 1 );
2879
+ break;
2880
+ case 'c/t':
2881
+ value.push( getSlidePastCount() + 1, '/', getTotalSlides() );
2882
+ break;
2883
+ case 'h/v':
2884
+ value.push( indexh + 1 );
2885
+ if( isVerticalSlide() ) value.push( '/', indexv + 1 );
2886
+ break;
2887
+ default:
2888
+ value.push( indexh + 1 );
2889
+ if( isVerticalSlide() ) value.push( '.', indexv + 1 );
2890
+ }
2891
+
2892
+ dom.slideNumber.innerHTML = formatSlideNumber( value[0], value[1], value[2] );
2893
+ }
2894
+
2895
+ }
2896
+
2897
+ /**
2898
+ * Applies HTML formatting to a slide number before it's
2899
+ * written to the DOM.
2900
+ *
2901
+ * @param {number} a Current slide
2902
+ * @param {string} delimiter Character to separate slide numbers
2903
+ * @param {(number|*)} b Total slides
2904
+ * @return {string} HTML string fragment
2905
+ */
2906
+ function formatSlideNumber( a, delimiter, b ) {
2907
+
2908
+ if( typeof b === 'number' && !isNaN( b ) ) {
2909
+ return '<span class="slide-number-a">'+ a +'</span>' +
2910
+ '<span class="slide-number-delimiter">'+ delimiter +'</span>' +
2911
+ '<span class="slide-number-b">'+ b +'</span>';
2912
+ }
2913
+ else {
2914
+ return '<span class="slide-number-a">'+ a +'</span>';
2487
2915
  }
2488
2916
 
2489
2917
  }
@@ -2504,34 +2932,57 @@
2504
2932
  .concat( dom.controlsNext ).forEach( function( node ) {
2505
2933
  node.classList.remove( 'enabled' );
2506
2934
  node.classList.remove( 'fragmented' );
2935
+
2936
+ // Set 'disabled' attribute on all directions
2937
+ node.setAttribute( 'disabled', 'disabled' );
2507
2938
  } );
2508
2939
 
2509
- // Add the 'enabled' class to the available routes
2510
- if( routes.left ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'enabled' ); } );
2511
- if( routes.right ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'enabled' ); } );
2512
- if( routes.up ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'enabled' ); } );
2513
- if( routes.down ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'enabled' ); } );
2940
+ // Add the 'enabled' class to the available routes; remove 'disabled' attribute to enable buttons
2941
+ if( routes.left ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
2942
+ if( routes.right ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
2943
+ if( routes.up ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
2944
+ if( routes.down ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
2514
2945
 
2515
2946
  // Prev/next buttons
2516
- if( routes.left || routes.up ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'enabled' ); } );
2517
- if( routes.right || routes.down ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'enabled' ); } );
2947
+ if( routes.left || routes.up ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
2948
+ if( routes.right || routes.down ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
2518
2949
 
2519
2950
  // Highlight fragment directions
2520
2951
  if( currentSlide ) {
2521
2952
 
2522
2953
  // Always apply fragment decorator to prev/next buttons
2523
- if( fragments.prev ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
2524
- if( fragments.next ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
2954
+ if( fragments.prev ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
2955
+ if( fragments.next ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
2525
2956
 
2526
2957
  // Apply fragment decorators to directional buttons based on
2527
2958
  // what slide axis they are in
2528
2959
  if( isVerticalSlide( currentSlide ) ) {
2529
- if( fragments.prev ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
2530
- if( fragments.next ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
2960
+ if( fragments.prev ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
2961
+ if( fragments.next ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
2531
2962
  }
2532
2963
  else {
2533
- if( fragments.prev ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
2534
- if( fragments.next ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
2964
+ if( fragments.prev ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
2965
+ if( fragments.next ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
2966
+ }
2967
+
2968
+ }
2969
+
2970
+ if( config.controlsTutorial ) {
2971
+
2972
+ // Highlight control arrows with an animation to ensure
2973
+ // that the viewer knows how to navigate
2974
+ if( !hasNavigatedDown && routes.down ) {
2975
+ dom.controlsDownArrow.classList.add( 'highlight' );
2976
+ }
2977
+ else {
2978
+ dom.controlsDownArrow.classList.remove( 'highlight' );
2979
+
2980
+ if( !hasNavigatedRight && routes.right && indexv === 0 ) {
2981
+ dom.controlsRightArrow.classList.add( 'highlight' );
2982
+ }
2983
+ else {
2984
+ dom.controlsRightArrow.classList.remove( 'highlight' );
2985
+ }
2535
2986
  }
2536
2987
 
2537
2988
  }
@@ -2542,7 +2993,7 @@
2542
2993
  * Updates the background elements to reflect the current
2543
2994
  * slide.
2544
2995
  *
2545
- * @param {Boolean} includeAll If true, the backgrounds of
2996
+ * @param {boolean} includeAll If true, the backgrounds of
2546
2997
  * all vertical slides (not just the present) will be updated.
2547
2998
  */
2548
2999
  function updateBackground( includeAll ) {
@@ -2599,22 +3050,17 @@
2599
3050
 
2600
3051
  } );
2601
3052
 
2602
- // Stop any currently playing video background
3053
+ // Stop content inside of previous backgrounds
2603
3054
  if( previousBackground ) {
2604
3055
 
2605
- var previousVideo = previousBackground.querySelector( 'video' );
2606
- if( previousVideo ) previousVideo.pause();
3056
+ stopEmbeddedContent( previousBackground );
2607
3057
 
2608
3058
  }
2609
3059
 
3060
+ // Start content in the current background
2610
3061
  if( currentBackground ) {
2611
3062
 
2612
- // Start video playback
2613
- var currentVideo = currentBackground.querySelector( 'video' );
2614
- if( currentVideo ) {
2615
- currentVideo.currentTime = 0;
2616
- currentVideo.play();
2617
- }
3063
+ startEmbeddedContent( currentBackground );
2618
3064
 
2619
3065
  var backgroundImageURL = currentBackground.style.backgroundImage || '';
2620
3066
 
@@ -2688,7 +3134,7 @@
2688
3134
  horizontalOffsetMultiplier = config.parallaxBackgroundHorizontal;
2689
3135
  }
2690
3136
  else {
2691
- horizontalOffsetMultiplier = ( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 );
3137
+ horizontalOffsetMultiplier = horizontalSlideCount > 1 ? ( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 ) : 0;
2692
3138
  }
2693
3139
 
2694
3140
  horizontalOffset = horizontalOffsetMultiplier * indexh * -1;
@@ -2705,7 +3151,7 @@
2705
3151
  verticalOffsetMultiplier = ( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 );
2706
3152
  }
2707
3153
 
2708
- verticalOffset = verticalSlideCount > 0 ? verticalOffsetMultiplier * indexv * 1 : 0;
3154
+ verticalOffset = verticalSlideCount > 0 ? verticalOffsetMultiplier * indexv : 0;
2709
3155
 
2710
3156
  dom.background.style.backgroundPosition = horizontalOffset + 'px ' + -verticalOffset + 'px';
2711
3157
 
@@ -2717,15 +3163,20 @@
2717
3163
  * Called when the given slide is within the configured view
2718
3164
  * distance. Shows the slide element and loads any content
2719
3165
  * that is set to load lazily (data-src).
3166
+ *
3167
+ * @param {HTMLElement} slide Slide to show
2720
3168
  */
2721
- function showSlide( slide ) {
3169
+ function loadSlide( slide, options ) {
3170
+
3171
+ options = options || {};
2722
3172
 
2723
3173
  // Show the slide element
2724
- slide.style.display = 'block';
3174
+ slide.style.display = config.display;
2725
3175
 
2726
3176
  // Media elements with data-src attributes
2727
3177
  toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src]' ) ).forEach( function( element ) {
2728
3178
  element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
3179
+ element.setAttribute( 'data-lazy-loaded', '' );
2729
3180
  element.removeAttribute( 'data-src' );
2730
3181
  } );
2731
3182
 
@@ -2736,6 +3187,7 @@
2736
3187
  toArray( media.querySelectorAll( 'source[data-src]' ) ).forEach( function( source ) {
2737
3188
  source.setAttribute( 'src', source.getAttribute( 'data-src' ) );
2738
3189
  source.removeAttribute( 'data-src' );
3190
+ source.setAttribute( 'data-lazy-loaded', '' );
2739
3191
  sources += 1;
2740
3192
  } );
2741
3193
 
@@ -2760,6 +3212,7 @@
2760
3212
  var backgroundImage = slide.getAttribute( 'data-background-image' ),
2761
3213
  backgroundVideo = slide.getAttribute( 'data-background-video' ),
2762
3214
  backgroundVideoLoop = slide.hasAttribute( 'data-background-video-loop' ),
3215
+ backgroundVideoMuted = slide.hasAttribute( 'data-background-video-muted' ),
2763
3216
  backgroundIframe = slide.getAttribute( 'data-background-iframe' );
2764
3217
 
2765
3218
  // Images
@@ -2774,6 +3227,19 @@
2774
3227
  video.setAttribute( 'loop', '' );
2775
3228
  }
2776
3229
 
3230
+ if( backgroundVideoMuted ) {
3231
+ video.muted = true;
3232
+ }
3233
+
3234
+ // Inline video playback works (at least in Mobile Safari) as
3235
+ // long as the video is muted and the `playsinline` attribute is
3236
+ // present
3237
+ if( isMobileDevice ) {
3238
+ video.muted = true;
3239
+ video.autoplay = true;
3240
+ video.setAttribute( 'playsinline', '' );
3241
+ }
3242
+
2777
3243
  // Support comma separated lists of video sources
2778
3244
  backgroundVideo.split( ',' ).forEach( function( source ) {
2779
3245
  video.innerHTML += '<source src="'+ source +'">';
@@ -2782,26 +3248,41 @@
2782
3248
  background.appendChild( video );
2783
3249
  }
2784
3250
  // Iframes
2785
- else if( backgroundIframe ) {
3251
+ else if( backgroundIframe && options.excludeIframes !== true ) {
2786
3252
  var iframe = document.createElement( 'iframe' );
3253
+ iframe.setAttribute( 'allowfullscreen', '' );
3254
+ iframe.setAttribute( 'mozallowfullscreen', '' );
3255
+ iframe.setAttribute( 'webkitallowfullscreen', '' );
3256
+
3257
+ // Only load autoplaying content when the slide is shown to
3258
+ // avoid having it play in the background
3259
+ if( /autoplay=(1|true|yes)/gi.test( backgroundIframe ) ) {
3260
+ iframe.setAttribute( 'data-src', backgroundIframe );
3261
+ }
3262
+ else {
2787
3263
  iframe.setAttribute( 'src', backgroundIframe );
2788
- iframe.style.width = '100%';
2789
- iframe.style.height = '100%';
2790
- iframe.style.maxHeight = '100%';
2791
- iframe.style.maxWidth = '100%';
3264
+ }
3265
+
3266
+ iframe.style.width = '100%';
3267
+ iframe.style.height = '100%';
3268
+ iframe.style.maxHeight = '100%';
3269
+ iframe.style.maxWidth = '100%';
2792
3270
 
2793
3271
  background.appendChild( iframe );
2794
3272
  }
2795
3273
  }
3274
+
2796
3275
  }
2797
3276
 
2798
3277
  }
2799
3278
 
2800
3279
  /**
2801
- * Called when the given slide is moved outside of the
2802
- * configured view distance.
3280
+ * Unloads and hides the given slide. This is called when the
3281
+ * slide is moved outside of the configured view distance.
3282
+ *
3283
+ * @param {HTMLElement} slide
2803
3284
  */
2804
- function hideSlide( slide ) {
3285
+ function unloadSlide( slide ) {
2805
3286
 
2806
3287
  // Hide the slide element
2807
3288
  slide.style.display = 'none';
@@ -2813,12 +3294,24 @@
2813
3294
  background.style.display = 'none';
2814
3295
  }
2815
3296
 
3297
+ // Reset lazy-loaded media elements with src attributes
3298
+ toArray( slide.querySelectorAll( 'video[data-lazy-loaded][src], audio[data-lazy-loaded][src]' ) ).forEach( function( element ) {
3299
+ element.setAttribute( 'data-src', element.getAttribute( 'src' ) );
3300
+ element.removeAttribute( 'src' );
3301
+ } );
3302
+
3303
+ // Reset lazy-loaded media elements with <source> children
3304
+ toArray( slide.querySelectorAll( 'video[data-lazy-loaded] source[src], audio source[src]' ) ).forEach( function( source ) {
3305
+ source.setAttribute( 'data-src', source.getAttribute( 'src' ) );
3306
+ source.removeAttribute( 'src' );
3307
+ } );
3308
+
2816
3309
  }
2817
3310
 
2818
3311
  /**
2819
3312
  * Determine what available routes there are for navigation.
2820
3313
  *
2821
- * @return {Object} containing four booleans: left/right/up/down
3314
+ * @return {{left: boolean, right: boolean, up: boolean, down: boolean}}
2822
3315
  */
2823
3316
  function availableRoutes() {
2824
3317
 
@@ -2847,7 +3340,7 @@
2847
3340
  * Returns an object describing the available fragment
2848
3341
  * directions.
2849
3342
  *
2850
- * @return {Object} two boolean properties: prev/next
3343
+ * @return {{prev: boolean, next: boolean}}
2851
3344
  */
2852
3345
  function availableFragments() {
2853
3346
 
@@ -2888,65 +3381,147 @@
2888
3381
  _appendParamToIframeSource( 'src', 'player.vimeo.com/', 'api=1' );
2889
3382
  _appendParamToIframeSource( 'data-src', 'player.vimeo.com/', 'api=1' );
2890
3383
 
3384
+ // Always show media controls on mobile devices
3385
+ if( isMobileDevice ) {
3386
+ toArray( dom.slides.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
3387
+ el.controls = true;
3388
+ } );
3389
+ }
3390
+
2891
3391
  }
2892
3392
 
2893
3393
  /**
2894
3394
  * Start playback of any embedded content inside of
2895
- * the targeted slide.
3395
+ * the given element.
3396
+ *
3397
+ * @param {HTMLElement} element
2896
3398
  */
2897
- function startEmbeddedContent( slide ) {
3399
+ function startEmbeddedContent( element ) {
3400
+
3401
+ if( element && !isSpeakerNotes() ) {
2898
3402
 
2899
- if( slide && !isSpeakerNotes() ) {
2900
3403
  // Restart GIFs
2901
- toArray( slide.querySelectorAll( 'img[src$=".gif"]' ) ).forEach( function( el ) {
3404
+ toArray( element.querySelectorAll( 'img[src$=".gif"]' ) ).forEach( function( el ) {
2902
3405
  // Setting the same unchanged source like this was confirmed
2903
3406
  // to work in Chrome, FF & Safari
2904
3407
  el.setAttribute( 'src', el.getAttribute( 'src' ) );
2905
3408
  } );
2906
3409
 
2907
3410
  // HTML5 media elements
2908
- toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
2909
- if( el.hasAttribute( 'data-autoplay' ) && typeof el.play === 'function' ) {
2910
- el.play();
3411
+ toArray( element.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
3412
+ if( closestParent( el, '.fragment' ) && !closestParent( el, '.fragment.visible' ) ) {
3413
+ return;
3414
+ }
3415
+
3416
+ // Prefer an explicit global autoplay setting
3417
+ var autoplay = config.autoPlayMedia;
3418
+
3419
+ // If no global setting is available, fall back on the element's
3420
+ // own autoplay setting
3421
+ if( typeof autoplay !== 'boolean' ) {
3422
+ autoplay = el.hasAttribute( 'data-autoplay' ) || !!closestParent( el, '.slide-background' );
3423
+ }
3424
+
3425
+ if( autoplay && typeof el.play === 'function' ) {
3426
+
3427
+ if( el.readyState > 1 ) {
3428
+ startEmbeddedMedia( { target: el } );
3429
+ }
3430
+ else {
3431
+ el.removeEventListener( 'loadeddata', startEmbeddedMedia ); // remove first to avoid dupes
3432
+ el.addEventListener( 'loadeddata', startEmbeddedMedia );
3433
+ }
3434
+
2911
3435
  }
2912
3436
  } );
2913
3437
 
2914
3438
  // Normal iframes
2915
- toArray( slide.querySelectorAll( 'iframe[src]' ) ).forEach( function( el ) {
3439
+ toArray( element.querySelectorAll( 'iframe[src]' ) ).forEach( function( el ) {
3440
+ if( closestParent( el, '.fragment' ) && !closestParent( el, '.fragment.visible' ) ) {
3441
+ return;
3442
+ }
3443
+
2916
3444
  startEmbeddedIframe( { target: el } );
2917
3445
  } );
2918
3446
 
2919
3447
  // Lazy loading iframes
2920
- toArray( slide.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) {
3448
+ toArray( element.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) {
3449
+ if( closestParent( el, '.fragment' ) && !closestParent( el, '.fragment.visible' ) ) {
3450
+ return;
3451
+ }
3452
+
2921
3453
  if( el.getAttribute( 'src' ) !== el.getAttribute( 'data-src' ) ) {
2922
3454
  el.removeEventListener( 'load', startEmbeddedIframe ); // remove first to avoid dupes
2923
3455
  el.addEventListener( 'load', startEmbeddedIframe );
2924
3456
  el.setAttribute( 'src', el.getAttribute( 'data-src' ) );
2925
3457
  }
2926
3458
  } );
3459
+
2927
3460
  }
2928
3461
 
2929
3462
  }
2930
3463
 
3464
+ /**
3465
+ * Starts playing an embedded video/audio element after
3466
+ * it has finished loading.
3467
+ *
3468
+ * @param {object} event
3469
+ */
3470
+ function startEmbeddedMedia( event ) {
3471
+
3472
+ var isAttachedToDOM = !!closestParent( event.target, 'html' ),
3473
+ isVisible = !!closestParent( event.target, '.present' );
3474
+
3475
+ if( isAttachedToDOM && isVisible ) {
3476
+ event.target.currentTime = 0;
3477
+ event.target.play();
3478
+ }
3479
+
3480
+ event.target.removeEventListener( 'loadeddata', startEmbeddedMedia );
3481
+
3482
+ }
3483
+
2931
3484
  /**
2932
3485
  * "Starts" the content of an embedded iframe using the
2933
- * postmessage API.
3486
+ * postMessage API.
3487
+ *
3488
+ * @param {object} event
2934
3489
  */
2935
3490
  function startEmbeddedIframe( event ) {
2936
3491
 
2937
3492
  var iframe = event.target;
2938
3493
 
2939
- // YouTube postMessage API
2940
- if( /youtube\.com\/embed\//.test( iframe.getAttribute( 'src' ) ) && iframe.hasAttribute( 'data-autoplay' ) ) {
2941
- iframe.contentWindow.postMessage( '{"event":"command","func":"playVideo","args":""}', '*' );
2942
- }
2943
- // Vimeo postMessage API
2944
- else if( /player\.vimeo\.com\//.test( iframe.getAttribute( 'src' ) ) && iframe.hasAttribute( 'data-autoplay' ) ) {
2945
- iframe.contentWindow.postMessage( '{"method":"play"}', '*' );
2946
- }
2947
- // Generic postMessage API
2948
- else {
2949
- iframe.contentWindow.postMessage( 'slide:start', '*' );
3494
+ if( iframe && iframe.contentWindow ) {
3495
+
3496
+ var isAttachedToDOM = !!closestParent( event.target, 'html' ),
3497
+ isVisible = !!closestParent( event.target, '.present' );
3498
+
3499
+ if( isAttachedToDOM && isVisible ) {
3500
+
3501
+ // Prefer an explicit global autoplay setting
3502
+ var autoplay = config.autoPlayMedia;
3503
+
3504
+ // If no global setting is available, fall back on the element's
3505
+ // own autoplay setting
3506
+ if( typeof autoplay !== 'boolean' ) {
3507
+ autoplay = iframe.hasAttribute( 'data-autoplay' ) || !!closestParent( iframe, '.slide-background' );
3508
+ }
3509
+
3510
+ // YouTube postMessage API
3511
+ if( /youtube\.com\/embed\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) {
3512
+ iframe.contentWindow.postMessage( '{"event":"command","func":"playVideo","args":""}', '*' );
3513
+ }
3514
+ // Vimeo postMessage API
3515
+ else if( /player\.vimeo\.com\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) {
3516
+ iframe.contentWindow.postMessage( '{"method":"play"}', '*' );
3517
+ }
3518
+ // Generic postMessage API
3519
+ else {
3520
+ iframe.contentWindow.postMessage( 'slide:start', '*' );
3521
+ }
3522
+
3523
+ }
3524
+
2950
3525
  }
2951
3526
 
2952
3527
  }
@@ -2954,44 +3529,54 @@
2954
3529
  /**
2955
3530
  * Stop playback of any embedded content inside of
2956
3531
  * the targeted slide.
3532
+ *
3533
+ * @param {HTMLElement} element
2957
3534
  */
2958
- function stopEmbeddedContent( slide ) {
3535
+ function stopEmbeddedContent( element, options ) {
3536
+
3537
+ options = extend( {
3538
+ // Defaults
3539
+ unloadIframes: true
3540
+ }, options || {} );
2959
3541
 
2960
- if( slide && slide.parentNode ) {
3542
+ if( element && element.parentNode ) {
2961
3543
  // HTML5 media elements
2962
- toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
3544
+ toArray( element.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
2963
3545
  if( !el.hasAttribute( 'data-ignore' ) && typeof el.pause === 'function' ) {
3546
+ el.setAttribute('data-paused-by-reveal', '');
2964
3547
  el.pause();
2965
3548
  }
2966
3549
  } );
2967
3550
 
2968
3551
  // Generic postMessage API for non-lazy loaded iframes
2969
- toArray( slide.querySelectorAll( 'iframe' ) ).forEach( function( el ) {
2970
- el.contentWindow.postMessage( 'slide:stop', '*' );
3552
+ toArray( element.querySelectorAll( 'iframe' ) ).forEach( function( el ) {
3553
+ if( el.contentWindow ) el.contentWindow.postMessage( 'slide:stop', '*' );
2971
3554
  el.removeEventListener( 'load', startEmbeddedIframe );
2972
3555
  });
2973
3556
 
2974
3557
  // YouTube postMessage API
2975
- toArray( slide.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) {
2976
- if( !el.hasAttribute( 'data-ignore' ) && typeof el.contentWindow.postMessage === 'function' ) {
3558
+ toArray( element.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) {
3559
+ if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) {
2977
3560
  el.contentWindow.postMessage( '{"event":"command","func":"pauseVideo","args":""}', '*' );
2978
3561
  }
2979
3562
  });
2980
3563
 
2981
3564
  // Vimeo postMessage API
2982
- toArray( slide.querySelectorAll( 'iframe[src*="player.vimeo.com/"]' ) ).forEach( function( el ) {
2983
- if( !el.hasAttribute( 'data-ignore' ) && typeof el.contentWindow.postMessage === 'function' ) {
3565
+ toArray( element.querySelectorAll( 'iframe[src*="player.vimeo.com/"]' ) ).forEach( function( el ) {
3566
+ if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) {
2984
3567
  el.contentWindow.postMessage( '{"method":"pause"}', '*' );
2985
3568
  }
2986
3569
  });
2987
3570
 
2988
- // Lazy loading iframes
2989
- toArray( slide.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) {
2990
- // Only removing the src doesn't actually unload the frame
2991
- // in all browsers (Firefox) so we set it to blank first
2992
- el.setAttribute( 'src', 'about:blank' );
2993
- el.removeAttribute( 'src' );
2994
- } );
3571
+ if( options.unloadIframes === true ) {
3572
+ // Unload lazy-loaded iframes
3573
+ toArray( element.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) {
3574
+ // Only removing the src doesn't actually unload the frame
3575
+ // in all browsers (Firefox) so we set it to blank first
3576
+ el.setAttribute( 'src', 'about:blank' );
3577
+ el.removeAttribute( 'src' );
3578
+ } );
3579
+ }
2995
3580
  }
2996
3581
 
2997
3582
  }
@@ -2999,6 +3584,8 @@
2999
3584
  /**
3000
3585
  * Returns the number of past slides. This can be used as a global
3001
3586
  * flattened index for slides.
3587
+ *
3588
+ * @return {number} Past slide count
3002
3589
  */
3003
3590
  function getSlidePastCount() {
3004
3591
 
@@ -3043,6 +3630,8 @@
3043
3630
  /**
3044
3631
  * Returns a value ranging from 0-1 that represents
3045
3632
  * how far into the presentation we have navigated.
3633
+ *
3634
+ * @return {number}
3046
3635
  */
3047
3636
  function getProgress() {
3048
3637
 
@@ -3076,6 +3665,8 @@
3076
3665
  /**
3077
3666
  * Checks if this presentation is running inside of the
3078
3667
  * speaker notes window.
3668
+ *
3669
+ * @return {boolean}
3079
3670
  */
3080
3671
  function isSpeakerNotes() {
3081
3672
 
@@ -3131,7 +3722,7 @@
3131
3722
  * Updates the page URL (hash) to reflect the current
3132
3723
  * state.
3133
3724
  *
3134
- * @param {Number} delay The time in ms to wait before
3725
+ * @param {number} delay The time in ms to wait before
3135
3726
  * writing the hash
3136
3727
  */
3137
3728
  function writeURL( delay ) {
@@ -3151,7 +3742,6 @@
3151
3742
  // Attempt to create a named link based on the slide's ID
3152
3743
  var id = currentSlide.getAttribute( 'id' );
3153
3744
  if( id ) {
3154
- id = id.toLowerCase();
3155
3745
  id = id.replace( /[^a-zA-Z0-9\-\_\:\.]/g, '' );
3156
3746
  }
3157
3747
 
@@ -3170,16 +3760,15 @@
3170
3760
  }
3171
3761
 
3172
3762
  }
3173
-
3174
3763
  /**
3175
- * Retrieves the h/v location of the current, or specified,
3176
- * slide.
3764
+ * Retrieves the h/v location and fragment of the current,
3765
+ * or specified, slide.
3177
3766
  *
3178
- * @param {HTMLElement} slide If specified, the returned
3767
+ * @param {HTMLElement} [slide] If specified, the returned
3179
3768
  * index will be for this slide rather than the currently
3180
3769
  * active one
3181
3770
  *
3182
- * @return {Object} { h: <int>, v: <int>, f: <int> }
3771
+ * @return {{h: number, v: number, f: number}}
3183
3772
  */
3184
3773
  function getIndices( slide ) {
3185
3774
 
@@ -3225,17 +3814,30 @@
3225
3814
 
3226
3815
  }
3227
3816
 
3817
+ /**
3818
+ * Retrieves all slides in this presentation.
3819
+ */
3820
+ function getSlides() {
3821
+
3822
+ return toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ':not(.stack)' ));
3823
+
3824
+ }
3825
+
3228
3826
  /**
3229
3827
  * Retrieves the total number of slides in this presentation.
3828
+ *
3829
+ * @return {number}
3230
3830
  */
3231
3831
  function getTotalSlides() {
3232
3832
 
3233
- return dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ':not(.stack)' ).length;
3833
+ return getSlides().length;
3234
3834
 
3235
3835
  }
3236
3836
 
3237
3837
  /**
3238
3838
  * Returns the slide element matching the specified index.
3839
+ *
3840
+ * @return {HTMLElement}
3239
3841
  */
3240
3842
  function getSlide( x, y ) {
3241
3843
 
@@ -3255,31 +3857,48 @@
3255
3857
  * All slides, even the ones with no background properties
3256
3858
  * defined, have a background element so as long as the
3257
3859
  * index is valid an element will be returned.
3860
+ *
3861
+ * @param {number} x Horizontal background index
3862
+ * @param {number} y Vertical background index
3863
+ * @return {(HTMLElement[]|*)}
3258
3864
  */
3259
3865
  function getSlideBackground( x, y ) {
3260
3866
 
3261
- // When printing to PDF the slide backgrounds are nested
3262
- // inside of the slides
3263
- if( isPrintingPDF() ) {
3264
- var slide = getSlide( x, y );
3265
- if( slide ) {
3266
- var background = slide.querySelector( '.slide-background' );
3267
- if( background && background.parentNode === slide ) {
3268
- return background;
3269
- }
3270
- }
3271
-
3272
- return undefined;
3867
+ var slide = getSlide( x, y );
3868
+ if( slide ) {
3869
+ return slide.slideBackgroundElement;
3273
3870
  }
3274
3871
 
3275
- var horizontalBackground = dom.wrapper.querySelectorAll( '.backgrounds>.slide-background' )[ x ];
3276
- var verticalBackgrounds = horizontalBackground && horizontalBackground.querySelectorAll( '.slide-background' );
3872
+ return undefined;
3277
3873
 
3278
- if( verticalBackgrounds && verticalBackgrounds.length && typeof y === 'number' ) {
3279
- return verticalBackgrounds ? verticalBackgrounds[ y ] : undefined;
3874
+ }
3875
+
3876
+ /**
3877
+ * Retrieves the speaker notes from a slide. Notes can be
3878
+ * defined in two ways:
3879
+ * 1. As a data-notes attribute on the slide <section>
3880
+ * 2. As an <aside class="notes"> inside of the slide
3881
+ *
3882
+ * @param {HTMLElement} [slide=currentSlide]
3883
+ * @return {(string|null)}
3884
+ */
3885
+ function getSlideNotes( slide ) {
3886
+
3887
+ // Default to the current slide
3888
+ slide = slide || currentSlide;
3889
+
3890
+ // Notes can be specified via the data-notes attribute...
3891
+ if( slide.hasAttribute( 'data-notes' ) ) {
3892
+ return slide.getAttribute( 'data-notes' );
3280
3893
  }
3281
3894
 
3282
- return horizontalBackground;
3895
+ // ... or using an <aside class="notes"> element
3896
+ var notesElement = slide.querySelector( 'aside.notes' );
3897
+ if( notesElement ) {
3898
+ return notesElement.innerHTML;
3899
+ }
3900
+
3901
+ return null;
3283
3902
 
3284
3903
  }
3285
3904
 
@@ -3287,6 +3906,8 @@
3287
3906
  * Retrieves the current state of the presentation as
3288
3907
  * an object. This state can then be restored at any
3289
3908
  * time.
3909
+ *
3910
+ * @return {{indexh: number, indexv: number, indexf: number, paused: boolean, overview: boolean}}
3290
3911
  */
3291
3912
  function getState() {
3292
3913
 
@@ -3305,7 +3926,8 @@
3305
3926
  /**
3306
3927
  * Restores the presentation to the given state.
3307
3928
  *
3308
- * @param {Object} state As generated by getState()
3929
+ * @param {object} state As generated by getState()
3930
+ * @see {@link getState} generates the parameter `state`
3309
3931
  */
3310
3932
  function setState( state ) {
3311
3933
 
@@ -3339,6 +3961,9 @@
3339
3961
  * attribute to each node if such an attribute is not already present,
3340
3962
  * and sets that attribute to an integer value which is the position of
3341
3963
  * the fragment within the fragments list.
3964
+ *
3965
+ * @param {object[]|*} fragments
3966
+ * @return {object[]} sorted Sorted array of fragments
3342
3967
  */
3343
3968
  function sortFragments( fragments ) {
3344
3969
 
@@ -3390,12 +4015,12 @@
3390
4015
  /**
3391
4016
  * Navigate to the specified slide fragment.
3392
4017
  *
3393
- * @param {Number} index The index of the fragment that
4018
+ * @param {?number} index The index of the fragment that
3394
4019
  * should be shown, -1 means all are invisible
3395
- * @param {Number} offset Integer offset to apply to the
4020
+ * @param {number} offset Integer offset to apply to the
3396
4021
  * fragment index
3397
4022
  *
3398
- * @return {Boolean} true if a change was made in any
4023
+ * @return {boolean} true if a change was made in any
3399
4024
  * fragments visibility as part of this call
3400
4025
  */
3401
4026
  function navigateFragment( index, offset ) {
@@ -3438,10 +4063,11 @@
3438
4063
  element.classList.remove( 'current-fragment' );
3439
4064
 
3440
4065
  // Announce the fragments one by one to the Screen Reader
3441
- dom.statusDiv.textContent = element.textContent;
4066
+ dom.statusDiv.textContent = getStatusText( element );
3442
4067
 
3443
4068
  if( i === index ) {
3444
4069
  element.classList.add( 'current-fragment' );
4070
+ startEmbeddedContent( element );
3445
4071
  }
3446
4072
  }
3447
4073
  // Hidden fragments
@@ -3451,7 +4077,6 @@
3451
4077
  element.classList.remove( 'current-fragment' );
3452
4078
  }
3453
4079
 
3454
-
3455
4080
  } );
3456
4081
 
3457
4082
  if( fragmentsHidden.length ) {
@@ -3478,7 +4103,7 @@
3478
4103
  /**
3479
4104
  * Navigate to the next slide fragment.
3480
4105
  *
3481
- * @return {Boolean} true if there was a next fragment,
4106
+ * @return {boolean} true if there was a next fragment,
3482
4107
  * false otherwise
3483
4108
  */
3484
4109
  function nextFragment() {
@@ -3490,7 +4115,7 @@
3490
4115
  /**
3491
4116
  * Navigate to the previous slide fragment.
3492
4117
  *
3493
- * @return {Boolean} true if there was a previous fragment,
4118
+ * @return {boolean} true if there was a previous fragment,
3494
4119
  * false otherwise
3495
4120
  */
3496
4121
  function previousFragment() {
@@ -3506,11 +4131,15 @@
3506
4131
 
3507
4132
  cancelAutoSlide();
3508
4133
 
3509
- if( currentSlide ) {
4134
+ if( currentSlide && config.autoSlide !== false ) {
3510
4135
 
3511
- var currentFragment = currentSlide.querySelector( '.current-fragment' );
4136
+ var fragment = currentSlide.querySelector( '.current-fragment' );
3512
4137
 
3513
- var fragmentAutoSlide = currentFragment ? currentFragment.getAttribute( 'data-autoslide' ) : null;
4138
+ // When the slide first appears there is no "current" fragment so
4139
+ // we look for a data-autoslide timing on the first fragment
4140
+ if( !fragment ) fragment = currentSlide.querySelector( '.fragment' );
4141
+
4142
+ var fragmentAutoSlide = fragment ? fragment.getAttribute( 'data-autoslide' ) : null;
3514
4143
  var parentAutoSlide = currentSlide.parentNode ? currentSlide.parentNode.getAttribute( 'data-autoslide' ) : null;
3515
4144
  var slideAutoSlide = currentSlide.getAttribute( 'data-autoslide' );
3516
4145
 
@@ -3536,11 +4165,12 @@
3536
4165
  // automatically set the autoSlide duration to the
3537
4166
  // length of that media. Not applicable if the slide
3538
4167
  // is divided up into fragments.
4168
+ // playbackRate is accounted for in the duration.
3539
4169
  if( currentSlide.querySelectorAll( '.fragment' ).length === 0 ) {
3540
4170
  toArray( currentSlide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
3541
4171
  if( el.hasAttribute( 'data-autoplay' ) ) {
3542
- if( autoSlide && el.duration * 1000 > autoSlide ) {
3543
- autoSlide = ( el.duration * 1000 ) + 1000;
4172
+ if( autoSlide && (el.duration * 1000 / el.playbackRate ) > autoSlide ) {
4173
+ autoSlide = ( el.duration * 1000 / el.playbackRate ) + 1000;
3544
4174
  }
3545
4175
  }
3546
4176
  } );
@@ -3553,7 +4183,10 @@
3553
4183
  // - The overview isn't active
3554
4184
  // - The presentation isn't over
3555
4185
  if( autoSlide && !autoSlidePaused && !isPaused() && !isOverview() && ( !Reveal.isLastSlide() || availableFragments().next || config.loop === true ) ) {
3556
- autoSlideTimeout = setTimeout( navigateNext, autoSlide );
4186
+ autoSlideTimeout = setTimeout( function() {
4187
+ typeof config.autoSlideMethod === 'function' ? config.autoSlideMethod() : navigateNext();
4188
+ cueAutoSlide();
4189
+ }, autoSlide );
3557
4190
  autoSlideStartTime = Date.now();
3558
4191
  }
3559
4192
 
@@ -3616,6 +4249,8 @@
3616
4249
 
3617
4250
  function navigateRight() {
3618
4251
 
4252
+ hasNavigatedRight = true;
4253
+
3619
4254
  // Reverse for RTL
3620
4255
  if( config.rtl ) {
3621
4256
  if( ( isOverview() || previousFragment() === false ) && availableRoutes().right ) {
@@ -3640,6 +4275,8 @@
3640
4275
 
3641
4276
  function navigateDown() {
3642
4277
 
4278
+ hasNavigatedDown = true;
4279
+
3643
4280
  // Prioritize revealing fragments
3644
4281
  if( ( isOverview() || nextFragment() === false ) && availableRoutes().down ) {
3645
4282
  slide( indexh, indexv + 1 );
@@ -3686,6 +4323,9 @@
3686
4323
  */
3687
4324
  function navigateNext() {
3688
4325
 
4326
+ hasNavigatedRight = true;
4327
+ hasNavigatedDown = true;
4328
+
3689
4329
  // Prioritize revealing fragments
3690
4330
  if( nextFragment() === false ) {
3691
4331
  if( availableRoutes().down ) {
@@ -3699,9 +4339,20 @@
3699
4339
  }
3700
4340
  }
3701
4341
 
3702
- // If auto-sliding is enabled we need to cue up
3703
- // another timeout
3704
- cueAutoSlide();
4342
+ }
4343
+
4344
+ /**
4345
+ * Checks if the target element prevents the triggering of
4346
+ * swipe navigation.
4347
+ */
4348
+ function isSwipePrevented( target ) {
4349
+
4350
+ while( target && typeof target.hasAttribute === 'function' ) {
4351
+ if( target.hasAttribute( 'data-prevent-swipe' ) ) return true;
4352
+ target = target.parentNode;
4353
+ }
4354
+
4355
+ return false;
3705
4356
 
3706
4357
  }
3707
4358
 
@@ -3713,6 +4364,8 @@
3713
4364
  /**
3714
4365
  * Called by all event handlers that are based on user
3715
4366
  * input.
4367
+ *
4368
+ * @param {object} [event]
3716
4369
  */
3717
4370
  function onUserInput( event ) {
3718
4371
 
@@ -3724,23 +4377,22 @@
3724
4377
 
3725
4378
  /**
3726
4379
  * Handler for the document level 'keypress' event.
4380
+ *
4381
+ * @param {object} event
3727
4382
  */
3728
4383
  function onDocumentKeyPress( event ) {
3729
4384
 
3730
4385
  // Check if the pressed key is question mark
3731
4386
  if( event.shiftKey && event.charCode === 63 ) {
3732
- if( dom.overlay ) {
3733
- closeOverlay();
3734
- }
3735
- else {
3736
- showHelp( true );
3737
- }
4387
+ toggleHelp();
3738
4388
  }
3739
4389
 
3740
4390
  }
3741
4391
 
3742
4392
  /**
3743
4393
  * Handler for the document level 'keydown' event.
4394
+ *
4395
+ * @param {object} event
3744
4396
  */
3745
4397
  function onDocumentKeyDown( event ) {
3746
4398
 
@@ -3759,13 +4411,26 @@
3759
4411
  // the keyboard
3760
4412
  var activeElementIsCE = document.activeElement && document.activeElement.contentEditable !== 'inherit';
3761
4413
  var activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test( document.activeElement.tagName );
4414
+ var activeElementIsNotes = document.activeElement && document.activeElement.className && /speaker-notes/i.test( document.activeElement.className);
3762
4415
 
3763
4416
  // Disregard the event if there's a focused element or a
3764
4417
  // keyboard modifier key is present
3765
- if( activeElementIsCE || activeElementIsInput || (event.shiftKey && event.keyCode !== 32) || event.altKey || event.ctrlKey || event.metaKey ) return;
4418
+ if( activeElementIsCE || activeElementIsInput || activeElementIsNotes || (event.shiftKey && event.keyCode !== 32) || event.altKey || event.ctrlKey || event.metaKey ) return;
4419
+
4420
+ // While paused only allow resume keyboard events; 'b', 'v', '.'
4421
+ var resumeKeyCodes = [66,86,190,191];
4422
+ var key;
3766
4423
 
3767
- // While paused only allow "unpausing" keyboard events (b and .)
3768
- if( isPaused() && [66,190,191].indexOf( event.keyCode ) === -1 ) {
4424
+ // Custom key bindings for togglePause should be able to resume
4425
+ if( typeof config.keyboard === 'object' ) {
4426
+ for( key in config.keyboard ) {
4427
+ if( config.keyboard[key] === 'togglePause' ) {
4428
+ resumeKeyCodes.push( parseInt( key, 10 ) );
4429
+ }
4430
+ }
4431
+ }
4432
+
4433
+ if( isPaused() && resumeKeyCodes.indexOf( event.keyCode ) === -1 ) {
3769
4434
  return false;
3770
4435
  }
3771
4436
 
@@ -3774,7 +4439,7 @@
3774
4439
  // 1. User defined key bindings
3775
4440
  if( typeof config.keyboard === 'object' ) {
3776
4441
 
3777
- for( var key in config.keyboard ) {
4442
+ for( key in config.keyboard ) {
3778
4443
 
3779
4444
  // Check if this binding matches the pressed key
3780
4445
  if( parseInt( key, 10 ) === event.keyCode ) {
@@ -3825,8 +4490,8 @@
3825
4490
  case 32: isOverview() ? deactivateOverview() : event.shiftKey ? navigatePrev() : navigateNext(); break;
3826
4491
  // return
3827
4492
  case 13: isOverview() ? deactivateOverview() : triggered = false; break;
3828
- // two-spot, semicolon, b, period, Logitech presenter tools "black screen" button
3829
- case 58: case 59: case 66: case 190: case 191: togglePause(); break;
4493
+ // two-spot, semicolon, b, v, period, Logitech presenter tools "black screen" button
4494
+ case 58: case 59: case 66: case 86: case 190: case 191: togglePause(); break;
3830
4495
  // f
3831
4496
  case 70: enterFullscreen(); break;
3832
4497
  // a
@@ -3863,9 +4528,13 @@
3863
4528
  /**
3864
4529
  * Handler for the 'touchstart' event, enables support for
3865
4530
  * swipe and pinch gestures.
4531
+ *
4532
+ * @param {object} event
3866
4533
  */
3867
4534
  function onTouchStart( event ) {
3868
4535
 
4536
+ if( isSwipePrevented( event.target ) ) return true;
4537
+
3869
4538
  touch.startX = event.touches[0].clientX;
3870
4539
  touch.startY = event.touches[0].clientY;
3871
4540
  touch.startCount = event.touches.length;
@@ -3886,9 +4555,13 @@
3886
4555
 
3887
4556
  /**
3888
4557
  * Handler for the 'touchmove' event.
4558
+ *
4559
+ * @param {object} event
3889
4560
  */
3890
4561
  function onTouchMove( event ) {
3891
4562
 
4563
+ if( isSwipePrevented( event.target ) ) return true;
4564
+
3892
4565
  // Each touch should only trigger one action
3893
4566
  if( !touch.captured ) {
3894
4567
  onUserInput( event );
@@ -3965,7 +4638,7 @@
3965
4638
  }
3966
4639
  // There's a bug with swiping on some Android devices unless
3967
4640
  // the default action is always prevented
3968
- else if( navigator.userAgent.match( /android/gi ) ) {
4641
+ else if( UA.match( /android/gi ) ) {
3969
4642
  event.preventDefault();
3970
4643
  }
3971
4644
 
@@ -3973,6 +4646,8 @@
3973
4646
 
3974
4647
  /**
3975
4648
  * Handler for the 'touchend' event.
4649
+ *
4650
+ * @param {object} event
3976
4651
  */
3977
4652
  function onTouchEnd( event ) {
3978
4653
 
@@ -3982,6 +4657,8 @@
3982
4657
 
3983
4658
  /**
3984
4659
  * Convert pointer down to touch start.
4660
+ *
4661
+ * @param {object} event
3985
4662
  */
3986
4663
  function onPointerDown( event ) {
3987
4664
 
@@ -3994,6 +4671,8 @@
3994
4671
 
3995
4672
  /**
3996
4673
  * Convert pointer move to touch move.
4674
+ *
4675
+ * @param {object} event
3997
4676
  */
3998
4677
  function onPointerMove( event ) {
3999
4678
 
@@ -4006,6 +4685,8 @@
4006
4685
 
4007
4686
  /**
4008
4687
  * Convert pointer up to touch end.
4688
+ *
4689
+ * @param {object} event
4009
4690
  */
4010
4691
  function onPointerUp( event ) {
4011
4692
 
@@ -4019,6 +4700,8 @@
4019
4700
  /**
4020
4701
  * Handles mouse wheel scrolling, throttled to avoid skipping
4021
4702
  * multiple slides.
4703
+ *
4704
+ * @param {object} event
4022
4705
  */
4023
4706
  function onDocumentMouseScroll( event ) {
4024
4707
 
@@ -4030,7 +4713,7 @@
4030
4713
  if( delta > 0 ) {
4031
4714
  navigateNext();
4032
4715
  }
4033
- else {
4716
+ else if( delta < 0 ) {
4034
4717
  navigatePrev();
4035
4718
  }
4036
4719
 
@@ -4043,6 +4726,8 @@
4043
4726
  * closest approximate horizontal slide using this equation:
4044
4727
  *
4045
4728
  * ( clickX / presentationWidth ) * numberOfSlides
4729
+ *
4730
+ * @param {object} event
4046
4731
  */
4047
4732
  function onProgressClicked( event ) {
4048
4733
 
@@ -4073,6 +4758,8 @@
4073
4758
 
4074
4759
  /**
4075
4760
  * Handler for the window level 'hashchange' event.
4761
+ *
4762
+ * @param {object} [event]
4076
4763
  */
4077
4764
  function onWindowHashChange( event ) {
4078
4765
 
@@ -4082,6 +4769,8 @@
4082
4769
 
4083
4770
  /**
4084
4771
  * Handler for the window level 'resize' event.
4772
+ *
4773
+ * @param {object} [event]
4085
4774
  */
4086
4775
  function onWindowResize( event ) {
4087
4776
 
@@ -4091,6 +4780,8 @@
4091
4780
 
4092
4781
  /**
4093
4782
  * Handle for the window level 'visibilitychange' event.
4783
+ *
4784
+ * @param {object} [event]
4094
4785
  */
4095
4786
  function onPageVisibilityChange( event ) {
4096
4787
 
@@ -4112,6 +4803,8 @@
4112
4803
 
4113
4804
  /**
4114
4805
  * Invoked when a slide is and we're in the overview.
4806
+ *
4807
+ * @param {object} event
4115
4808
  */
4116
4809
  function onOverviewSlideClicked( event ) {
4117
4810
 
@@ -4145,6 +4838,8 @@
4145
4838
  /**
4146
4839
  * Handles clicks on links that are set to preview in the
4147
4840
  * iframe overlay.
4841
+ *
4842
+ * @param {object} event
4148
4843
  */
4149
4844
  function onPreviewLinkClicked( event ) {
4150
4845
 
@@ -4160,6 +4855,8 @@
4160
4855
 
4161
4856
  /**
4162
4857
  * Handles click on the auto-sliding controls element.
4858
+ *
4859
+ * @param {object} [event]
4163
4860
  */
4164
4861
  function onAutoSlidePlayerClick( event ) {
4165
4862
 
@@ -4191,15 +4888,16 @@
4191
4888
  *
4192
4889
  * @param {HTMLElement} container The component will append
4193
4890
  * itself to this
4194
- * @param {Function} progressCheck A method which will be
4891
+ * @param {function} progressCheck A method which will be
4195
4892
  * called frequently to get the current progress on a range
4196
4893
  * of 0-1
4197
4894
  */
4198
4895
  function Playback( container, progressCheck ) {
4199
4896
 
4200
4897
  // Cosmetics
4201
- this.diameter = 50;
4202
- this.thickness = 3;
4898
+ this.diameter = 100;
4899
+ this.diameter2 = this.diameter/2;
4900
+ this.thickness = 6;
4203
4901
 
4204
4902
  // Flags if we are currently playing
4205
4903
  this.playing = false;
@@ -4217,6 +4915,8 @@
4217
4915
  this.canvas.className = 'playback';
4218
4916
  this.canvas.width = this.diameter;
4219
4917
  this.canvas.height = this.diameter;
4918
+ this.canvas.style.width = this.diameter2 + 'px';
4919
+ this.canvas.style.height = this.diameter2 + 'px';
4220
4920
  this.context = this.canvas.getContext( '2d' );
4221
4921
 
4222
4922
  this.container.appendChild( this.canvas );
@@ -4225,6 +4925,9 @@
4225
4925
 
4226
4926
  }
4227
4927
 
4928
+ /**
4929
+ * @param value
4930
+ */
4228
4931
  Playback.prototype.setPlaying = function( value ) {
4229
4932
 
4230
4933
  var wasPlaying = this.playing;
@@ -4267,10 +4970,10 @@
4267
4970
  Playback.prototype.render = function() {
4268
4971
 
4269
4972
  var progress = this.playing ? this.progress : 0,
4270
- radius = ( this.diameter / 2 ) - this.thickness,
4271
- x = this.diameter / 2,
4272
- y = this.diameter / 2,
4273
- iconSize = 14;
4973
+ radius = ( this.diameter2 ) - this.thickness,
4974
+ x = this.diameter2,
4975
+ y = this.diameter2,
4976
+ iconSize = 28;
4274
4977
 
4275
4978
  // Ease towards 1
4276
4979
  this.progressOffset += ( 1 - this.progressOffset ) * 0.1;
@@ -4283,7 +4986,7 @@
4283
4986
 
4284
4987
  // Solid background color
4285
4988
  this.context.beginPath();
4286
- this.context.arc( x, y, radius + 2, 0, Math.PI * 2, false );
4989
+ this.context.arc( x, y, radius + 4, 0, Math.PI * 2, false );
4287
4990
  this.context.fillStyle = 'rgba( 0, 0, 0, 0.4 )';
4288
4991
  this.context.fill();
4289
4992
 
@@ -4291,7 +4994,7 @@
4291
4994
  this.context.beginPath();
4292
4995
  this.context.arc( x, y, radius, 0, Math.PI * 2, false );
4293
4996
  this.context.lineWidth = this.thickness;
4294
- this.context.strokeStyle = '#666';
4997
+ this.context.strokeStyle = 'rgba( 255, 255, 255, 0.2 )';
4295
4998
  this.context.stroke();
4296
4999
 
4297
5000
  if( this.playing ) {
@@ -4308,14 +5011,14 @@
4308
5011
  // Draw play/pause icons
4309
5012
  if( this.playing ) {
4310
5013
  this.context.fillStyle = '#fff';
4311
- this.context.fillRect( 0, 0, iconSize / 2 - 2, iconSize );
4312
- this.context.fillRect( iconSize / 2 + 2, 0, iconSize / 2 - 2, iconSize );
5014
+ this.context.fillRect( 0, 0, iconSize / 2 - 4, iconSize );
5015
+ this.context.fillRect( iconSize / 2 + 4, 0, iconSize / 2 - 4, iconSize );
4313
5016
  }
4314
5017
  else {
4315
5018
  this.context.beginPath();
4316
- this.context.translate( 2, 0 );
5019
+ this.context.translate( 4, 0 );
4317
5020
  this.context.moveTo( 0, 0 );
4318
- this.context.lineTo( iconSize - 2, iconSize / 2 );
5021
+ this.context.lineTo( iconSize - 4, iconSize / 2 );
4319
5022
  this.context.lineTo( 0, iconSize );
4320
5023
  this.context.fillStyle = '#fff';
4321
5024
  this.context.fill();
@@ -4350,6 +5053,8 @@
4350
5053
 
4351
5054
 
4352
5055
  Reveal = {
5056
+ VERSION: VERSION,
5057
+
4353
5058
  initialize: initialize,
4354
5059
  configure: configure,
4355
5060
  sync: sync,
@@ -4380,12 +5085,18 @@
4380
5085
  // Forces an update in slide layout
4381
5086
  layout: layout,
4382
5087
 
5088
+ // Randomizes the order of slides
5089
+ shuffle: shuffle,
5090
+
4383
5091
  // Returns an object with the available routes as booleans (left/right/top/bottom)
4384
5092
  availableRoutes: availableRoutes,
4385
5093
 
4386
5094
  // Returns an object with the available fragments as booleans (prev/next)
4387
5095
  availableFragments: availableFragments,
4388
5096
 
5097
+ // Toggles a help overlay with keyboard shortcuts
5098
+ toggleHelp: toggleHelp,
5099
+
4389
5100
  // Toggles the overview mode on/off
4390
5101
  toggleOverview: toggleOverview,
4391
5102
 
@@ -4399,6 +5110,11 @@
4399
5110
  isOverview: isOverview,
4400
5111
  isPaused: isPaused,
4401
5112
  isAutoSliding: isAutoSliding,
5113
+ isSpeakerNotes: isSpeakerNotes,
5114
+
5115
+ // Slide preloading
5116
+ loadSlide: loadSlide,
5117
+ unloadSlide: unloadSlide,
4402
5118
 
4403
5119
  // Adds or removes all internal event listeners (such as keyboard)
4404
5120
  addEventListeners: addEventListeners,
@@ -4408,12 +5124,19 @@
4408
5124
  getState: getState,
4409
5125
  setState: setState,
4410
5126
 
5127
+ // Presentation progress
5128
+ getSlidePastCount: getSlidePastCount,
5129
+
4411
5130
  // Presentation progress on range of 0-1
4412
5131
  getProgress: getProgress,
4413
5132
 
4414
5133
  // Returns the indices of the current, or specified, slide
4415
5134
  getIndices: getIndices,
4416
5135
 
5136
+ // Returns an Array of all slides
5137
+ getSlides: getSlides,
5138
+
5139
+ // Returns the total number of slides
4417
5140
  getTotalSlides: getTotalSlides,
4418
5141
 
4419
5142
  // Returns the slide element at the specified index
@@ -4422,6 +5145,9 @@
4422
5145
  // Returns the slide background element at the specified index
4423
5146
  getSlideBackground: getSlideBackground,
4424
5147
 
5148
+ // Returns the speaker notes string for a slide, or null
5149
+ getSlideNotes: getSlideNotes,
5150
+
4425
5151
  // Returns the previous slide element, may be null
4426
5152
  getPreviousSlide: function() {
4427
5153
  return previousSlide;
@@ -4500,6 +5226,11 @@
4500
5226
  // Programatically triggers a keyboard event
4501
5227
  triggerKey: function( keyCode ) {
4502
5228
  onDocumentKeyDown( { keyCode: keyCode } );
5229
+ },
5230
+
5231
+ // Registers a new shortcut to include in the help overlay
5232
+ registerKeyboardShortcut: function( key, value ) {
5233
+ keyboardShortcuts[key] = value;
4503
5234
  }
4504
5235
  };
4505
5236