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