reveal.rb 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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