slide_hero 0.0.10 → 0.0.11

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/Gemfile +1 -1
  4. data/README.md +2 -2
  5. data/lib/slide_hero/blockquote.rb +10 -0
  6. data/lib/slide_hero/pluggable.rb +4 -4
  7. data/lib/slide_hero/plugins.rb +0 -1
  8. data/lib/slide_hero/presentation.rb +3 -3
  9. data/lib/slide_hero/slide.rb +2 -2
  10. data/lib/slide_hero/version.rb +1 -1
  11. data/lib/slide_hero/views/blockquote.html.erb +3 -0
  12. data/lib/slide_hero.rb +1 -0
  13. data/test/slide_hero/blockquote_spec.rb +13 -0
  14. data/test/slide_hero/plugins_spec.rb +0 -1
  15. data/test/slide_hero/presentation_spec.rb +2 -1
  16. data/test/slide_hero/slide_spec.rb +14 -3
  17. data/test/slide_hero_spec.rb +1 -0
  18. data/vendor/reveal.js/.gitignore +6 -1
  19. data/vendor/reveal.js/.travis.yml +1 -1
  20. data/vendor/reveal.js/CONTRIBUTING.md +4 -0
  21. data/vendor/reveal.js/Gruntfile.js +26 -24
  22. data/vendor/reveal.js/LICENSE +1 -1
  23. data/vendor/reveal.js/README.md +215 -124
  24. data/vendor/reveal.js/bower.json +27 -0
  25. data/vendor/reveal.js/css/print/paper.css +1 -1
  26. data/vendor/reveal.js/css/print/pdf.css +30 -27
  27. data/vendor/reveal.js/css/reveal.css +381 -191
  28. data/vendor/reveal.js/css/reveal.scss +259 -164
  29. data/vendor/reveal.js/css/theme/README.md +2 -6
  30. data/vendor/reveal.js/css/theme/beige.css +75 -49
  31. data/vendor/reveal.js/css/theme/black.css +64 -38
  32. data/vendor/reveal.js/css/theme/blood.css +75 -56
  33. data/vendor/reveal.js/css/theme/league.css +69 -43
  34. data/vendor/reveal.js/css/theme/moon.css +69 -43
  35. data/vendor/reveal.js/css/theme/night.css +64 -38
  36. data/vendor/reveal.js/css/theme/serif.css +66 -40
  37. data/vendor/reveal.js/css/theme/simple.css +63 -37
  38. data/vendor/reveal.js/css/theme/sky.css +69 -43
  39. data/vendor/reveal.js/css/theme/solarized.css +69 -43
  40. data/vendor/reveal.js/css/theme/source/black.scss +1 -1
  41. data/vendor/reveal.js/css/theme/source/blood.scss +3 -15
  42. data/vendor/reveal.js/css/theme/source/white.scss +1 -1
  43. data/vendor/reveal.js/css/theme/template/theme.scss +30 -23
  44. data/vendor/reveal.js/css/theme/white.css +69 -43
  45. data/vendor/reveal.js/demo.html +410 -0
  46. data/vendor/reveal.js/index.html +13 -371
  47. data/vendor/reveal.js/js/reveal.js +643 -175
  48. data/vendor/reveal.js/lib/css/zenburn.css +41 -78
  49. data/vendor/reveal.js/lib/js/head.min.js +9 -8
  50. data/vendor/reveal.js/package.json +20 -24
  51. data/vendor/reveal.js/plugin/highlight/highlight.js +4 -3
  52. data/vendor/reveal.js/plugin/markdown/example.html +1 -1
  53. data/vendor/reveal.js/plugin/markdown/markdown.js +19 -7
  54. data/vendor/reveal.js/plugin/markdown/marked.js +2 -33
  55. data/vendor/reveal.js/plugin/math/math.js +5 -2
  56. data/vendor/reveal.js/plugin/multiplex/client.js +1 -1
  57. data/vendor/reveal.js/plugin/multiplex/index.js +24 -16
  58. data/vendor/reveal.js/plugin/multiplex/master.js +22 -42
  59. data/vendor/reveal.js/plugin/multiplex/package.json +19 -0
  60. data/vendor/reveal.js/plugin/notes/notes.html +11 -3
  61. data/vendor/reveal.js/plugin/notes/notes.js +19 -5
  62. data/vendor/reveal.js/plugin/notes-server/client.js +6 -1
  63. data/vendor/reveal.js/plugin/notes-server/index.js +17 -14
  64. data/vendor/reveal.js/plugin/notes-server/notes.html +17 -6
  65. data/vendor/reveal.js/plugin/print-pdf/print-pdf.js +1 -1
  66. data/vendor/reveal.js/plugin/zoom-js/zoom.js +1 -1
  67. data/vendor/reveal.js/test/examples/slide-backgrounds.html +1 -1
  68. data/vendor/reveal.js/test/examples/slide-transitions.html +101 -0
  69. data/vendor/reveal.js/test/test-markdown-element-attributes.html +3 -3
  70. data/vendor/reveal.js/test/test-markdown-element-attributes.js +1 -1
  71. data/vendor/reveal.js/test/test.html +5 -1
  72. data/vendor/reveal.js/test/test.js +26 -1
  73. metadata +11 -5
  74. data/vendor/reveal.js/plugin/leap/leap.js +0 -159
  75. data/vendor/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) 2016 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.3.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
@@ -78,6 +82,9 @@
78
82
  // Change the presentation direction to be RTL
79
83
  rtl: false,
80
84
 
85
+ // Randomizes the order of slides each time the presentation loads
86
+ shuffle: false,
87
+
81
88
  // Turns fragments on and off globally
82
89
  fragments: true,
83
90
 
@@ -92,6 +99,9 @@
92
99
  // Flags if it should be possible to pause the presentation (blackout)
93
100
  pause: true,
94
101
 
102
+ // Flags if speaker notes should be visible to all viewers
103
+ showNotes: false,
104
+
95
105
  // Number of milliseconds between automatically proceeding to the
96
106
  // next slide, disabled when set to 0, this value can be overwritten
97
107
  // by using a data-autoslide attribute on your slides
@@ -100,6 +110,9 @@
100
110
  // Stop auto-sliding after user input
101
111
  autoSlideStoppable: true,
102
112
 
113
+ // Use this method for navigation when auto-sliding (defaults to navigateNext)
114
+ autoSlideMethod: null,
115
+
103
116
  // Enable slide navigation via mouse wheel
104
117
  mouseWheel: false,
105
118
 
@@ -136,6 +149,10 @@
136
149
  // Parallax background size
137
150
  parallaxBackgroundSize: '', // CSS syntax, e.g. "3000px 2000px"
138
151
 
152
+ // Amount of pixels to move the parallax background per slide step
153
+ parallaxBackgroundHorizontal: null,
154
+ parallaxBackgroundVertical: null,
155
+
139
156
  // Number of slides away from the current that are visible
140
157
  viewDistance: 3,
141
158
 
@@ -147,6 +164,13 @@
147
164
  // Flags if reveal.js is loaded (has dispatched the 'ready' event)
148
165
  loaded = false,
149
166
 
167
+ // Flags if the overview mode is currently active
168
+ overview = false,
169
+
170
+ // Holds the dimensions of our overview slides, including margins
171
+ overviewSlideWidth = null,
172
+ overviewSlideHeight = null,
173
+
150
174
  // The horizontal and vertical index of the currently active slide
151
175
  indexh,
152
176
  indexv,
@@ -165,6 +189,10 @@
165
189
  // The current scale of the presentation (see width/height config)
166
190
  scale = 1,
167
191
 
192
+ // CSS transform that is currently applied to the slides container,
193
+ // split into two groups
194
+ slidesTransform = { layout: '', overview: '' },
195
+
168
196
  // Cached references to DOM elements
169
197
  dom = {},
170
198
 
@@ -174,6 +202,9 @@
174
202
  // Client is a mobile device, see #checkCapabilities()
175
203
  isMobileDevice,
176
204
 
205
+ // Client is a desktop Chrome, see #checkCapabilities()
206
+ isChrome,
207
+
177
208
  // Throttles mouse wheel navigation
178
209
  lastMouseWheelStep = 0,
179
210
 
@@ -227,14 +258,18 @@
227
258
  if( !features.transforms2d && !features.transforms3d ) {
228
259
  document.body.setAttribute( 'class', 'no-transforms' );
229
260
 
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' );
261
+ // Since JS won't be running any further, we load all lazy
262
+ // loading elements upfront
263
+ var images = toArray( document.getElementsByTagName( 'img' ) ),
264
+ iframes = toArray( document.getElementsByTagName( 'iframe' ) );
265
+
266
+ var lazyLoadable = images.concat( iframes );
267
+
268
+ for( var i = 0, len = lazyLoadable.length; i < len; i++ ) {
269
+ var element = lazyLoadable[i];
270
+ if( element.getAttribute( 'data-src' ) ) {
271
+ element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
272
+ element.removeAttribute( 'data-src' );
238
273
  }
239
274
  }
240
275
 
@@ -274,26 +309,37 @@
274
309
  */
275
310
  function checkCapabilities() {
276
311
 
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;
312
+ isMobileDevice = /(iphone|ipod|ipad|android)/gi.test( UA );
313
+ isChrome = /chrome/i.test( UA ) && !/edge/i.test( UA );
314
+
315
+ var testElement = document.createElement( 'div' );
282
316
 
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;
317
+ features.transforms3d = 'WebkitPerspective' in testElement.style ||
318
+ 'MozPerspective' in testElement.style ||
319
+ 'msPerspective' in testElement.style ||
320
+ 'OPerspective' in testElement.style ||
321
+ 'perspective' in testElement.style;
322
+
323
+ features.transforms2d = 'WebkitTransform' in testElement.style ||
324
+ 'MozTransform' in testElement.style ||
325
+ 'msTransform' in testElement.style ||
326
+ 'OTransform' in testElement.style ||
327
+ 'transform' in testElement.style;
288
328
 
289
329
  features.requestAnimationFrameMethod = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame;
290
330
  features.requestAnimationFrame = typeof features.requestAnimationFrameMethod === 'function';
291
331
 
292
332
  features.canvas = !!document.createElement( 'canvas' ).getContext;
293
333
 
294
- features.touch = !!( 'ontouchstart' in window );
334
+ // Transitions in the overview are disabled in desktop and
335
+ // Safari due to lag
336
+ features.overviewTransitions = !/Version\/[\d\.]+.*Safari/.test( UA );
295
337
 
296
- isMobileDevice = navigator.userAgent.match( /(iphone|ipod|ipad|android)/gi );
338
+ // Flags if we should use zoom instead of transform to scale
339
+ // up slides. Zoom produces crisper results but has a lot of
340
+ // xbrowser quirks so we only use it in whitelsited browsers.
341
+ features.zoom = 'zoom' in testElement.style && !isMobileDevice &&
342
+ ( isChrome || /Version\/[\d\.]+.*Safari/.test( UA ) );
297
343
 
298
344
  }
299
345
 
@@ -373,6 +419,9 @@
373
419
  // Listen to messages posted to this window
374
420
  setupPostMessage();
375
421
 
422
+ // Prevent the slides from being scrolled out of view
423
+ setupScrollPrevention();
424
+
376
425
  // Resets all vertical slides so that only the first is visible
377
426
  resetVerticalSlides();
378
427
 
@@ -435,14 +484,18 @@
435
484
 
436
485
  // Arrow controls
437
486
  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>' );
487
+ '<button class="navigate-left" aria-label="previous slide"></button>' +
488
+ '<button class="navigate-right" aria-label="next slide"></button>' +
489
+ '<button class="navigate-up" aria-label="above slide"></button>' +
490
+ '<button class="navigate-down" aria-label="below slide"></button>' );
442
491
 
443
492
  // Slide number
444
493
  dom.slideNumber = createSingletonNode( dom.wrapper, 'div', 'slide-number', '' );
445
494
 
495
+ // Element containing notes that are visible to the audience
496
+ dom.speakerNotes = createSingletonNode( dom.wrapper, 'div', 'speaker-notes', null );
497
+ dom.speakerNotes.setAttribute( 'data-prevent-swipe', '' );
498
+
446
499
  // Overlay graphic which is displayed during the paused mode
447
500
  createSingletonNode( dom.wrapper, 'div', 'pause-overlay', null );
448
501
 
@@ -513,6 +566,19 @@
513
566
  document.body.style.width = pageWidth + 'px';
514
567
  document.body.style.height = pageHeight + 'px';
515
568
 
569
+ // Add each slide's index as attributes on itself, we need these
570
+ // indices to generate slide numbers below
571
+ toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( hslide, h ) {
572
+ hslide.setAttribute( 'data-index-h', h );
573
+
574
+ if( hslide.classList.contains( 'stack' ) ) {
575
+ toArray( hslide.querySelectorAll( 'section' ) ).forEach( function( vslide, v ) {
576
+ vslide.setAttribute( 'data-index-h', h );
577
+ vslide.setAttribute( 'data-index-v', v );
578
+ } );
579
+ }
580
+ } );
581
+
516
582
  // Slide and slide background layout
517
583
  toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
518
584
 
@@ -545,6 +611,34 @@
545
611
  background.style.top = -top + 'px';
546
612
  background.style.left = -left + 'px';
547
613
  }
614
+
615
+ // Inject notes if `showNotes` is enabled
616
+ if( config.showNotes ) {
617
+ var notes = getSlideNotes( slide );
618
+ if( notes ) {
619
+ var notesSpacing = 8;
620
+ var notesElement = document.createElement( 'div' );
621
+ notesElement.classList.add( 'speaker-notes' );
622
+ notesElement.classList.add( 'speaker-notes-pdf' );
623
+ notesElement.innerHTML = notes;
624
+ notesElement.style.left = ( notesSpacing - left ) + 'px';
625
+ notesElement.style.bottom = ( notesSpacing - top ) + 'px';
626
+ notesElement.style.width = ( pageWidth - notesSpacing*2 ) + 'px';
627
+ slide.appendChild( notesElement );
628
+ }
629
+ }
630
+
631
+ // Inject slide numbers if `slideNumbers` are enabled
632
+ if( config.slideNumber ) {
633
+ var slideNumberH = parseInt( slide.getAttribute( 'data-index-h' ), 10 ) + 1,
634
+ slideNumberV = parseInt( slide.getAttribute( 'data-index-v' ), 10 ) + 1;
635
+
636
+ var numberElement = document.createElement( 'div' );
637
+ numberElement.classList.add( 'slide-number' );
638
+ numberElement.classList.add( 'slide-number-pdf' );
639
+ numberElement.innerHTML = formatSlideNumber( slideNumberH, '.', slideNumberV );
640
+ background.appendChild( numberElement );
641
+ }
548
642
  }
549
643
 
550
644
  } );
@@ -556,6 +650,26 @@
556
650
 
557
651
  }
558
652
 
653
+ /**
654
+ * This is an unfortunate necessity. Some actions – such as
655
+ * an input field being focused in an iframe or using the
656
+ * keyboard to expand text selection beyond the bounds of
657
+ * a slide – can trigger our content to be pushed out of view.
658
+ * This scrolling can not be prevented by hiding overflow in
659
+ * CSS (we already do) so we have to resort to repeatedly
660
+ * checking if the slides have been offset :(
661
+ */
662
+ function setupScrollPrevention() {
663
+
664
+ setInterval( function() {
665
+ if( dom.wrapper.scrollTop !== 0 || dom.wrapper.scrollLeft !== 0 ) {
666
+ dom.wrapper.scrollTop = 0;
667
+ dom.wrapper.scrollLeft = 0;
668
+ }
669
+ }, 1000 );
670
+
671
+ }
672
+
559
673
  /**
560
674
  * Creates an HTML element and returns a reference to it.
561
675
  * If the element already exists the existing instance will
@@ -757,7 +871,7 @@
757
871
  var data = event.data;
758
872
 
759
873
  // Make sure we're dealing with JSON
760
- if( data.charAt( 0 ) === '{' && data.charAt( data.length - 1 ) === '}' ) {
874
+ if( typeof data === 'string' && data.charAt( 0 ) === '{' && data.charAt( data.length - 1 ) === '}' ) {
761
875
  data = JSON.parse( data );
762
876
 
763
877
  // Check if the requested method can be found
@@ -794,6 +908,11 @@
794
908
 
795
909
  dom.controls.style.display = config.controls ? 'block' : 'none';
796
910
  dom.progress.style.display = config.progress ? 'block' : 'none';
911
+ dom.slideNumber.style.display = config.slideNumber && !isPrintingPDF() ? 'block' : 'none';
912
+
913
+ if( config.shuffle ) {
914
+ shuffle();
915
+ }
797
916
 
798
917
  if( config.rtl ) {
799
918
  dom.wrapper.classList.add( 'rtl' );
@@ -814,6 +933,13 @@
814
933
  resume();
815
934
  }
816
935
 
936
+ if( config.showNotes ) {
937
+ dom.speakerNotes.classList.add( 'visible' );
938
+ }
939
+ else {
940
+ dom.speakerNotes.classList.remove( 'visible' );
941
+ }
942
+
817
943
  if( config.mouseWheel ) {
818
944
  document.addEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF
819
945
  document.addEventListener( 'mousewheel', onDocumentMouseScroll, false );
@@ -931,7 +1057,7 @@
931
1057
 
932
1058
  // Only support touch for Android, fixes double navigations in
933
1059
  // stock browser
934
- if( navigator.userAgent.match( /android/gi ) ) {
1060
+ if( UA.match( /android/gi ) ) {
935
1061
  pointerEvents = [ 'touchstart' ];
936
1062
  }
937
1063
 
@@ -1051,11 +1177,31 @@
1051
1177
  element.style.WebkitTransform = transform;
1052
1178
  element.style.MozTransform = transform;
1053
1179
  element.style.msTransform = transform;
1054
- element.style.OTransform = transform;
1055
1180
  element.style.transform = transform;
1056
1181
 
1057
1182
  }
1058
1183
 
1184
+ /**
1185
+ * Applies CSS transforms to the slides container. The container
1186
+ * is transformed from two separate sources: layout and the overview
1187
+ * mode.
1188
+ */
1189
+ function transformSlides( transforms ) {
1190
+
1191
+ // Pick up new transforms from arguments
1192
+ if( typeof transforms.layout === 'string' ) slidesTransform.layout = transforms.layout;
1193
+ if( typeof transforms.overview === 'string' ) slidesTransform.overview = transforms.overview;
1194
+
1195
+ // Apply the transforms to the slides container
1196
+ if( slidesTransform.layout ) {
1197
+ transformElement( dom.slides, slidesTransform.layout + ' ' + slidesTransform.overview );
1198
+ }
1199
+ else {
1200
+ transformElement( dom.slides, slidesTransform.overview );
1201
+ }
1202
+
1203
+ }
1204
+
1059
1205
  /**
1060
1206
  * Injects the given CSS styles into the DOM.
1061
1207
  */
@@ -1074,7 +1220,7 @@
1074
1220
  }
1075
1221
 
1076
1222
  /**
1077
- * Measures the distance in pixels between point a and point b.
1223
+ * Converts various color input formats to an {r:0,g:0,b:0} object.
1078
1224
  *
1079
1225
  * @param {String} color The string representation of a color,
1080
1226
  * the following formats are supported:
@@ -1465,20 +1611,28 @@
1465
1611
  dom.slides.style.top = '';
1466
1612
  dom.slides.style.bottom = '';
1467
1613
  dom.slides.style.right = '';
1468
- transformElement( dom.slides, '' );
1614
+ transformSlides( { layout: '' } );
1469
1615
  }
1470
1616
  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' ) {
1617
+ // Prefer zoom for scaling up so that content remains crisp.
1618
+ // Don't use zoom to scale down since that can lead to shifts
1619
+ // in text layout/line breaks.
1620
+ if( scale > 1 && features.zoom ) {
1473
1621
  dom.slides.style.zoom = scale;
1622
+ dom.slides.style.left = '';
1623
+ dom.slides.style.top = '';
1624
+ dom.slides.style.bottom = '';
1625
+ dom.slides.style.right = '';
1626
+ transformSlides( { layout: '' } );
1474
1627
  }
1475
1628
  // Apply scale transform as a fallback
1476
1629
  else {
1630
+ dom.slides.style.zoom = '';
1477
1631
  dom.slides.style.left = '50%';
1478
1632
  dom.slides.style.top = '50%';
1479
1633
  dom.slides.style.bottom = 'auto';
1480
1634
  dom.slides.style.right = 'auto';
1481
- transformElement( dom.slides, 'translate(-50%, -50%) scale('+ scale +')' );
1635
+ transformSlides( { layout: 'translate(-50%, -50%) scale('+ scale +')' } );
1482
1636
  }
1483
1637
  }
1484
1638
 
@@ -1566,7 +1720,7 @@
1566
1720
  };
1567
1721
 
1568
1722
  // Reduce available space by margin
1569
- size.presentationWidth -= ( size.presentationHeight * config.margin );
1723
+ size.presentationWidth -= ( size.presentationWidth * config.margin );
1570
1724
  size.presentationHeight -= ( size.presentationHeight * config.margin );
1571
1725
 
1572
1726
  // Slide width may be a percentage of available width
@@ -1620,81 +1774,114 @@
1620
1774
  }
1621
1775
 
1622
1776
  /**
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.
1777
+ * Displays the overview of slides (quick nav) by scaling
1778
+ * down and arranging all slide elements.
1628
1779
  */
1629
1780
  function activateOverview() {
1630
1781
 
1631
1782
  // Only proceed if enabled in config
1632
- if( config.overview ) {
1633
-
1634
- // Don't auto-slide while in overview mode
1635
- cancelAutoSlide();
1783
+ if( config.overview && !isOverview() ) {
1636
1784
 
1637
- var wasActive = dom.wrapper.classList.contains( 'overview' );
1638
-
1639
- // Vary the depth of the overview based on screen size
1640
- var depth = window.innerWidth < 400 ? 1000 : 2500;
1785
+ overview = true;
1641
1786
 
1642
1787
  dom.wrapper.classList.add( 'overview' );
1643
1788
  dom.wrapper.classList.remove( 'overview-deactivating' );
1644
1789
 
1645
- var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR );
1646
-
1647
- for( var i = 0, len1 = horizontalSlides.length; i < len1; i++ ) {
1648
- var hslide = horizontalSlides[i],
1649
- hoffset = config.rtl ? -105 : 105;
1790
+ if( features.overviewTransitions ) {
1791
+ setTimeout( function() {
1792
+ dom.wrapper.classList.add( 'overview-animated' );
1793
+ }, 1 );
1794
+ }
1650
1795
 
1651
- hslide.setAttribute( 'data-index-h', i );
1796
+ // Don't auto-slide while in overview mode
1797
+ cancelAutoSlide();
1652
1798
 
1653
- // Apply CSS transform
1654
- transformElement( hslide, 'translateZ(-'+ depth +'px) translate(' + ( ( i - indexh ) * hoffset ) + '%, 0%)' );
1799
+ // Move the backgrounds element into the slide container to
1800
+ // that the same scaling is applied
1801
+ dom.slides.appendChild( dom.background );
1655
1802
 
1656
- if( hslide.classList.contains( 'stack' ) ) {
1803
+ // Clicking on an overview slide navigates to it
1804
+ toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
1805
+ if( !slide.classList.contains( 'stack' ) ) {
1806
+ slide.addEventListener( 'click', onOverviewSlideClicked, true );
1807
+ }
1808
+ } );
1657
1809
 
1658
- var verticalSlides = hslide.querySelectorAll( 'section' );
1810
+ // Calculate slide sizes
1811
+ var margin = 70;
1812
+ var slideSize = getComputedSlideSize();
1813
+ overviewSlideWidth = slideSize.width + margin;
1814
+ overviewSlideHeight = slideSize.height + margin;
1659
1815
 
1660
- for( var j = 0, len2 = verticalSlides.length; j < len2; j++ ) {
1661
- var verticalIndex = i === indexh ? indexv : getPreviousVerticalIndex( hslide );
1816
+ // Reverse in RTL mode
1817
+ if( config.rtl ) {
1818
+ overviewSlideWidth = -overviewSlideWidth;
1819
+ }
1662
1820
 
1663
- var vslide = verticalSlides[j];
1821
+ updateSlidesVisibility();
1822
+ layoutOverview();
1823
+ updateOverview();
1664
1824
 
1665
- vslide.setAttribute( 'data-index-h', i );
1666
- vslide.setAttribute( 'data-index-v', j );
1825
+ layout();
1667
1826
 
1668
- // Apply CSS transform
1669
- transformElement( vslide, 'translate(0%, ' + ( ( j - verticalIndex ) * 105 ) + '%)' );
1827
+ // Notify observers of the overview showing
1828
+ dispatchEvent( 'overviewshown', {
1829
+ 'indexh': indexh,
1830
+ 'indexv': indexv,
1831
+ 'currentSlide': currentSlide
1832
+ } );
1670
1833
 
1671
- // Navigate to this slide on click
1672
- vslide.addEventListener( 'click', onOverviewSlideClicked, true );
1673
- }
1834
+ }
1674
1835
 
1675
- }
1676
- else {
1836
+ }
1677
1837
 
1678
- // Navigate to this slide on click
1679
- hslide.addEventListener( 'click', onOverviewSlideClicked, true );
1838
+ /**
1839
+ * Uses CSS transforms to position all slides in a grid for
1840
+ * display inside of the overview mode.
1841
+ */
1842
+ function layoutOverview() {
1680
1843
 
1681
- }
1682
- }
1844
+ // Layout slides
1845
+ toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( hslide, h ) {
1846
+ hslide.setAttribute( 'data-index-h', h );
1847
+ transformElement( hslide, 'translate3d(' + ( h * overviewSlideWidth ) + 'px, 0, 0)' );
1683
1848
 
1684
- updateSlidesVisibility();
1849
+ if( hslide.classList.contains( 'stack' ) ) {
1685
1850
 
1686
- layout();
1851
+ toArray( hslide.querySelectorAll( 'section' ) ).forEach( function( vslide, v ) {
1852
+ vslide.setAttribute( 'data-index-h', h );
1853
+ vslide.setAttribute( 'data-index-v', v );
1687
1854
 
1688
- if( !wasActive ) {
1689
- // Notify observers of the overview showing
1690
- dispatchEvent( 'overviewshown', {
1691
- 'indexh': indexh,
1692
- 'indexv': indexv,
1693
- 'currentSlide': currentSlide
1855
+ transformElement( vslide, 'translate3d(0, ' + ( v * overviewSlideHeight ) + 'px, 0)' );
1694
1856
  } );
1857
+
1695
1858
  }
1859
+ } );
1696
1860
 
1697
- }
1861
+ // Layout slide backgrounds
1862
+ toArray( dom.background.childNodes ).forEach( function( hbackground, h ) {
1863
+ transformElement( hbackground, 'translate3d(' + ( h * overviewSlideWidth ) + 'px, 0, 0)' );
1864
+
1865
+ toArray( hbackground.querySelectorAll( '.slide-background' ) ).forEach( function( vbackground, v ) {
1866
+ transformElement( vbackground, 'translate3d(0, ' + ( v * overviewSlideHeight ) + 'px, 0)' );
1867
+ } );
1868
+ } );
1869
+
1870
+ }
1871
+
1872
+ /**
1873
+ * Moves the overview viewport to the current slides.
1874
+ * Called each time the current slide changes.
1875
+ */
1876
+ function updateOverview() {
1877
+
1878
+ transformSlides( {
1879
+ overview: [
1880
+ 'translateX('+ ( -indexh * overviewSlideWidth ) +'px)',
1881
+ 'translateY('+ ( -indexv * overviewSlideHeight ) +'px)',
1882
+ 'translateZ('+ ( window.innerWidth < 400 ? -1000 : -2500 ) +'px)'
1883
+ ].join( ' ' )
1884
+ } );
1698
1885
 
1699
1886
  }
1700
1887
 
@@ -1707,7 +1894,10 @@
1707
1894
  // Only proceed if enabled in config
1708
1895
  if( config.overview ) {
1709
1896
 
1897
+ overview = false;
1898
+
1710
1899
  dom.wrapper.classList.remove( 'overview' );
1900
+ dom.wrapper.classList.remove( 'overview-animated' );
1711
1901
 
1712
1902
  // Temporarily add a class so that transitions can do different things
1713
1903
  // depending on whether they are exiting/entering overview, or just
@@ -1718,16 +1908,27 @@
1718
1908
  dom.wrapper.classList.remove( 'overview-deactivating' );
1719
1909
  }, 1 );
1720
1910
 
1721
- // Select all slides
1911
+ // Move the background element back out
1912
+ dom.wrapper.appendChild( dom.background );
1913
+
1914
+ // Clean up changes made to slides
1722
1915
  toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
1723
- // Resets all transforms to use the external styles
1724
1916
  transformElement( slide, '' );
1725
1917
 
1726
1918
  slide.removeEventListener( 'click', onOverviewSlideClicked, true );
1727
1919
  } );
1728
1920
 
1921
+ // Clean up changes made to backgrounds
1922
+ toArray( dom.background.querySelectorAll( '.slide-background' ) ).forEach( function( background ) {
1923
+ transformElement( background, '' );
1924
+ } );
1925
+
1926
+ transformSlides( { overview: '' } );
1927
+
1729
1928
  slide( indexh, indexv );
1730
1929
 
1930
+ layout();
1931
+
1731
1932
  cueAutoSlide();
1732
1933
 
1733
1934
  // Notify observers of the overview hiding
@@ -1766,7 +1967,7 @@
1766
1967
  */
1767
1968
  function isOverview() {
1768
1969
 
1769
- return dom.wrapper.classList.contains( 'overview' );
1970
+ return overview;
1770
1971
 
1771
1972
  }
1772
1973
 
@@ -1916,7 +2117,7 @@
1916
2117
 
1917
2118
  // If no vertical index is specified and the upcoming slide is a
1918
2119
  // stack, resume at its previous vertical index
1919
- if( v === undefined ) {
2120
+ if( v === undefined && !isOverview() ) {
1920
2121
  v = getPreviousVerticalIndex( horizontalSlides[ h ] );
1921
2122
  }
1922
2123
 
@@ -1966,9 +2167,9 @@
1966
2167
  document.documentElement.classList.remove( stateBefore.pop() );
1967
2168
  }
1968
2169
 
1969
- // If the overview is active, re-activate it to update positions
2170
+ // Update the overview if it's currently active
1970
2171
  if( isOverview() ) {
1971
- activateOverview();
2172
+ updateOverview();
1972
2173
  }
1973
2174
 
1974
2175
  // Find the current horizontal slide and any possible vertical slides
@@ -2037,6 +2238,7 @@
2037
2238
  updateBackground();
2038
2239
  updateParallax();
2039
2240
  updateSlideNumber();
2241
+ updateNotes();
2040
2242
 
2041
2243
  // Update the URL hash
2042
2244
  writeURL();
@@ -2078,8 +2280,14 @@
2078
2280
  updateBackground( true );
2079
2281
  updateSlideNumber();
2080
2282
  updateSlidesVisibility();
2283
+ updateNotes();
2081
2284
 
2082
2285
  formatEmbeddedContent();
2286
+ startEmbeddedContent( currentSlide );
2287
+
2288
+ if( isOverview() ) {
2289
+ layoutOverview();
2290
+ }
2083
2291
 
2084
2292
  }
2085
2293
 
@@ -2130,6 +2338,23 @@
2130
2338
 
2131
2339
  }
2132
2340
 
2341
+ /**
2342
+ * Randomly shuffles all slides in the deck.
2343
+ */
2344
+ function shuffle() {
2345
+
2346
+ var slides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
2347
+
2348
+ slides.forEach( function( slide ) {
2349
+
2350
+ // Insert this slide next to another random slide. This may
2351
+ // cause the slide to insert before itself but that's fine.
2352
+ dom.slides.insertBefore( slide, slides[ Math.floor( Math.random() * slides.length ) ] );
2353
+
2354
+ } );
2355
+
2356
+ }
2357
+
2133
2358
  /**
2134
2359
  * Updates one dimension of slides by showing the slide
2135
2360
  * with the specified index.
@@ -2269,7 +2494,7 @@
2269
2494
  viewDistance = isOverview() ? 6 : 2;
2270
2495
  }
2271
2496
 
2272
- // Limit view distance on weaker devices
2497
+ // All slides need to be visible when exporting to PDF
2273
2498
  if( isPrintingPDF() ) {
2274
2499
  viewDistance = Number.MAX_VALUE;
2275
2500
  }
@@ -2280,8 +2505,14 @@
2280
2505
  var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) ),
2281
2506
  verticalSlidesLength = verticalSlides.length;
2282
2507
 
2283
- // Loops so that it measures 1 between the first and last slides
2284
- distanceX = Math.abs( ( ( indexh || 0 ) - x ) % ( horizontalSlidesLength - viewDistance ) ) || 0;
2508
+ // Determine how far away this slide is from the present
2509
+ distanceX = Math.abs( ( indexh || 0 ) - x ) || 0;
2510
+
2511
+ // If the presentation is looped, distance should measure
2512
+ // 1 between the first and last slides
2513
+ if( config.loop ) {
2514
+ distanceX = Math.abs( ( ( indexh || 0 ) - x ) % ( horizontalSlidesLength - viewDistance ) ) || 0;
2515
+ }
2285
2516
 
2286
2517
  // Show the horizontal slide if it's within the view distance
2287
2518
  if( distanceX < viewDistance ) {
@@ -2315,6 +2546,22 @@
2315
2546
 
2316
2547
  }
2317
2548
 
2549
+ /**
2550
+ * Pick up notes from the current slide and display tham
2551
+ * to the viewer.
2552
+ *
2553
+ * @see `showNotes` config value
2554
+ */
2555
+ function updateNotes() {
2556
+
2557
+ if( config.showNotes && dom.speakerNotes && currentSlide && !isPrintingPDF() ) {
2558
+
2559
+ dom.speakerNotes.innerHTML = getSlideNotes() || '';
2560
+
2561
+ }
2562
+
2563
+ }
2564
+
2318
2565
  /**
2319
2566
  * Updates the progress bar to reflect the current slide.
2320
2567
  */
@@ -2331,19 +2578,60 @@
2331
2578
 
2332
2579
  /**
2333
2580
  * Updates the slide number div to reflect the current slide.
2581
+ *
2582
+ * The following slide number formats are available:
2583
+ * "h.v": horizontal . vertical slide number (default)
2584
+ * "h/v": horizontal / vertical slide number
2585
+ * "c": flattened slide number
2586
+ * "c/t": flattened slide number / total slides
2334
2587
  */
2335
2588
  function updateSlideNumber() {
2336
2589
 
2337
2590
  // Update slide number if enabled
2338
- if( config.slideNumber && dom.slideNumber) {
2591
+ if( config.slideNumber && dom.slideNumber ) {
2592
+
2593
+ var value = [];
2594
+ var format = 'h.v';
2339
2595
 
2340
- // Display the number of the page using 'indexh - indexv' format
2341
- var indexString = indexh;
2342
- if( indexv > 0 ) {
2343
- indexString += ' - ' + indexv;
2596
+ // Check if a custom number format is available
2597
+ if( typeof config.slideNumber === 'string' ) {
2598
+ format = config.slideNumber;
2344
2599
  }
2345
2600
 
2346
- dom.slideNumber.innerHTML = indexString;
2601
+ switch( format ) {
2602
+ case 'c':
2603
+ value.push( getSlidePastCount() + 1 );
2604
+ break;
2605
+ case 'c/t':
2606
+ value.push( getSlidePastCount() + 1, '/', getTotalSlides() );
2607
+ break;
2608
+ case 'h/v':
2609
+ value.push( indexh + 1 );
2610
+ if( isVerticalSlide() ) value.push( '/', indexv + 1 );
2611
+ break;
2612
+ default:
2613
+ value.push( indexh + 1 );
2614
+ if( isVerticalSlide() ) value.push( '.', indexv + 1 );
2615
+ }
2616
+
2617
+ dom.slideNumber.innerHTML = formatSlideNumber( value[0], value[1], value[2] );
2618
+ }
2619
+
2620
+ }
2621
+
2622
+ /**
2623
+ * Applies HTML formatting to a slide number before it's
2624
+ * written to the DOM.
2625
+ */
2626
+ function formatSlideNumber( a, delimiter, b ) {
2627
+
2628
+ if( typeof b === 'number' && !isNaN( b ) ) {
2629
+ return '<span class="slide-number-a">'+ a +'</span>' +
2630
+ '<span class="slide-number-delimiter">'+ delimiter +'</span>' +
2631
+ '<span class="slide-number-b">'+ b +'</span>';
2632
+ }
2633
+ else {
2634
+ return '<span class="slide-number-a">'+ a +'</span>';
2347
2635
  }
2348
2636
 
2349
2637
  }
@@ -2472,8 +2760,29 @@
2472
2760
  // Start video playback
2473
2761
  var currentVideo = currentBackground.querySelector( 'video' );
2474
2762
  if( currentVideo ) {
2475
- currentVideo.currentTime = 0;
2476
- currentVideo.play();
2763
+
2764
+ var startVideo = function() {
2765
+ currentVideo.currentTime = 0;
2766
+ currentVideo.play();
2767
+ currentVideo.removeEventListener( 'loadeddata', startVideo );
2768
+ };
2769
+
2770
+ if( currentVideo.readyState > 1 ) {
2771
+ startVideo();
2772
+ }
2773
+ else {
2774
+ currentVideo.addEventListener( 'loadeddata', startVideo );
2775
+ }
2776
+
2777
+ }
2778
+
2779
+ var backgroundImageURL = currentBackground.style.backgroundImage || '';
2780
+
2781
+ // Restart GIFs (doesn't work in Firefox)
2782
+ if( /\.gif/i.test( backgroundImageURL ) ) {
2783
+ currentBackground.style.backgroundImage = '';
2784
+ window.getComputedStyle( currentBackground ).opacity;
2785
+ currentBackground.style.backgroundImage = backgroundImageURL;
2477
2786
  }
2478
2787
 
2479
2788
  // Don't transition between identical backgrounds. This
@@ -2530,15 +2839,35 @@
2530
2839
  backgroundHeight = parseInt( backgroundSize[1], 10 );
2531
2840
  }
2532
2841
 
2533
- var slideWidth = dom.background.offsetWidth;
2534
- var horizontalSlideCount = horizontalSlides.length;
2535
- var horizontalOffset = -( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 ) * indexh;
2842
+ var slideWidth = dom.background.offsetWidth,
2843
+ horizontalSlideCount = horizontalSlides.length,
2844
+ horizontalOffsetMultiplier,
2845
+ horizontalOffset;
2846
+
2847
+ if( typeof config.parallaxBackgroundHorizontal === 'number' ) {
2848
+ horizontalOffsetMultiplier = config.parallaxBackgroundHorizontal;
2849
+ }
2850
+ else {
2851
+ horizontalOffsetMultiplier = horizontalSlideCount > 1 ? ( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 ) : 0;
2852
+ }
2853
+
2854
+ horizontalOffset = horizontalOffsetMultiplier * indexh * -1;
2855
+
2856
+ var slideHeight = dom.background.offsetHeight,
2857
+ verticalSlideCount = verticalSlides.length,
2858
+ verticalOffsetMultiplier,
2859
+ verticalOffset;
2860
+
2861
+ if( typeof config.parallaxBackgroundVertical === 'number' ) {
2862
+ verticalOffsetMultiplier = config.parallaxBackgroundVertical;
2863
+ }
2864
+ else {
2865
+ verticalOffsetMultiplier = ( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 );
2866
+ }
2536
2867
 
2537
- var slideHeight = dom.background.offsetHeight;
2538
- var verticalSlideCount = verticalSlides.length;
2539
- var verticalOffset = verticalSlideCount > 1 ? -( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 ) * indexv : 0;
2868
+ verticalOffset = verticalSlideCount > 0 ? verticalOffsetMultiplier * indexv * 1 : 0;
2540
2869
 
2541
- dom.background.style.backgroundPosition = horizontalOffset + 'px ' + verticalOffset + 'px';
2870
+ dom.background.style.backgroundPosition = horizontalOffset + 'px ' + -verticalOffset + 'px';
2542
2871
 
2543
2872
  }
2544
2873
 
@@ -2555,7 +2884,7 @@
2555
2884
  slide.style.display = 'block';
2556
2885
 
2557
2886
  // 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 ) {
2887
+ toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src]' ) ).forEach( function( element ) {
2559
2888
  element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
2560
2889
  element.removeAttribute( 'data-src' );
2561
2890
  } );
@@ -2590,6 +2919,8 @@
2590
2919
 
2591
2920
  var backgroundImage = slide.getAttribute( 'data-background-image' ),
2592
2921
  backgroundVideo = slide.getAttribute( 'data-background-video' ),
2922
+ backgroundVideoLoop = slide.hasAttribute( 'data-background-video-loop' ),
2923
+ backgroundVideoMuted = slide.hasAttribute( 'data-background-video-muted' ),
2593
2924
  backgroundIframe = slide.getAttribute( 'data-background-iframe' );
2594
2925
 
2595
2926
  // Images
@@ -2600,6 +2931,14 @@
2600
2931
  else if ( backgroundVideo && !isSpeakerNotes() ) {
2601
2932
  var video = document.createElement( 'video' );
2602
2933
 
2934
+ if( backgroundVideoLoop ) {
2935
+ video.setAttribute( 'loop', '' );
2936
+ }
2937
+
2938
+ if( backgroundVideoMuted ) {
2939
+ video.muted = true;
2940
+ }
2941
+
2603
2942
  // Support comma separated lists of video sources
2604
2943
  backgroundVideo.split( ',' ).forEach( function( source ) {
2605
2944
  video.innerHTML += '<source src="'+ source +'">';
@@ -2608,7 +2947,7 @@
2608
2947
  background.appendChild( video );
2609
2948
  }
2610
2949
  // Iframes
2611
- else if ( backgroundIframe ) {
2950
+ else if( backgroundIframe ) {
2612
2951
  var iframe = document.createElement( 'iframe' );
2613
2952
  iframe.setAttribute( 'src', backgroundIframe );
2614
2953
  iframe.style.width = '100%';
@@ -2697,21 +3036,22 @@
2697
3036
  */
2698
3037
  function formatEmbeddedContent() {
2699
3038
 
3039
+ var _appendParamToIframeSource = function( sourceAttribute, sourceURL, param ) {
3040
+ toArray( dom.slides.querySelectorAll( 'iframe['+ sourceAttribute +'*="'+ sourceURL +'"]' ) ).forEach( function( el ) {
3041
+ var src = el.getAttribute( sourceAttribute );
3042
+ if( src && src.indexOf( param ) === -1 ) {
3043
+ el.setAttribute( sourceAttribute, src + ( !/\?/.test( src ) ? '?' : '&' ) + param );
3044
+ }
3045
+ });
3046
+ };
3047
+
2700
3048
  // 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
- });
3049
+ _appendParamToIframeSource( 'src', 'youtube.com/embed/', 'enablejsapi=1' );
3050
+ _appendParamToIframeSource( 'data-src', 'youtube.com/embed/', 'enablejsapi=1' );
2707
3051
 
2708
3052
  // 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
- });
3053
+ _appendParamToIframeSource( 'src', 'player.vimeo.com/', 'api=1' );
3054
+ _appendParamToIframeSource( 'data-src', 'player.vimeo.com/', 'api=1' );
2715
3055
 
2716
3056
  }
2717
3057
 
@@ -2722,31 +3062,56 @@
2722
3062
  function startEmbeddedContent( slide ) {
2723
3063
 
2724
3064
  if( slide && !isSpeakerNotes() ) {
3065
+ // Restart GIFs
3066
+ toArray( slide.querySelectorAll( 'img[src$=".gif"]' ) ).forEach( function( el ) {
3067
+ // Setting the same unchanged source like this was confirmed
3068
+ // to work in Chrome, FF & Safari
3069
+ el.setAttribute( 'src', el.getAttribute( 'src' ) );
3070
+ } );
3071
+
2725
3072
  // HTML5 media elements
2726
3073
  toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
2727
- if( el.hasAttribute( 'data-autoplay' ) ) {
3074
+ if( el.hasAttribute( 'data-autoplay' ) && typeof el.play === 'function' ) {
2728
3075
  el.play();
2729
3076
  }
2730
3077
  } );
2731
3078
 
2732
- // iframe embeds
2733
- toArray( slide.querySelectorAll( 'iframe' ) ).forEach( function( el ) {
2734
- el.contentWindow.postMessage( 'slide:start', '*' );
2735
- });
3079
+ // Normal iframes
3080
+ toArray( slide.querySelectorAll( 'iframe[src]' ) ).forEach( function( el ) {
3081
+ startEmbeddedIframe( { target: el } );
3082
+ } );
2736
3083
 
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":""}', '*' );
3084
+ // Lazy loading iframes
3085
+ toArray( slide.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) {
3086
+ if( el.getAttribute( 'src' ) !== el.getAttribute( 'data-src' ) ) {
3087
+ el.removeEventListener( 'load', startEmbeddedIframe ); // remove first to avoid dupes
3088
+ el.addEventListener( 'load', startEmbeddedIframe );
3089
+ el.setAttribute( 'src', el.getAttribute( 'data-src' ) );
2741
3090
  }
2742
- });
3091
+ } );
3092
+ }
2743
3093
 
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"}', '*' );
2748
- }
2749
- });
3094
+ }
3095
+
3096
+ /**
3097
+ * "Starts" the content of an embedded iframe using the
3098
+ * postmessage API.
3099
+ */
3100
+ function startEmbeddedIframe( event ) {
3101
+
3102
+ var iframe = event.target;
3103
+
3104
+ // YouTube postMessage API
3105
+ if( /youtube\.com\/embed\//.test( iframe.getAttribute( 'src' ) ) && iframe.hasAttribute( 'data-autoplay' ) ) {
3106
+ iframe.contentWindow.postMessage( '{"event":"command","func":"playVideo","args":""}', '*' );
3107
+ }
3108
+ // Vimeo postMessage API
3109
+ else if( /player\.vimeo\.com\//.test( iframe.getAttribute( 'src' ) ) && iframe.hasAttribute( 'data-autoplay' ) ) {
3110
+ iframe.contentWindow.postMessage( '{"method":"play"}', '*' );
3111
+ }
3112
+ // Generic postMessage API
3113
+ else {
3114
+ iframe.contentWindow.postMessage( 'slide:start', '*' );
2750
3115
  }
2751
3116
 
2752
3117
  }
@@ -2760,43 +3125,51 @@
2760
3125
  if( slide && slide.parentNode ) {
2761
3126
  // HTML5 media elements
2762
3127
  toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
2763
- if( !el.hasAttribute( 'data-ignore' ) ) {
3128
+ if( !el.hasAttribute( 'data-ignore' ) && typeof el.pause === 'function' ) {
2764
3129
  el.pause();
2765
3130
  }
2766
3131
  } );
2767
3132
 
2768
- // iframe embeds
3133
+ // Generic postMessage API for non-lazy loaded iframes
2769
3134
  toArray( slide.querySelectorAll( 'iframe' ) ).forEach( function( el ) {
2770
3135
  el.contentWindow.postMessage( 'slide:stop', '*' );
3136
+ el.removeEventListener( 'load', startEmbeddedIframe );
2771
3137
  });
2772
3138
 
2773
- // YouTube embeds
3139
+ // YouTube postMessage API
2774
3140
  toArray( slide.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) {
2775
3141
  if( !el.hasAttribute( 'data-ignore' ) && typeof el.contentWindow.postMessage === 'function' ) {
2776
3142
  el.contentWindow.postMessage( '{"event":"command","func":"pauseVideo","args":""}', '*' );
2777
3143
  }
2778
3144
  });
2779
3145
 
2780
- // Vimeo embeds
3146
+ // Vimeo postMessage API
2781
3147
  toArray( slide.querySelectorAll( 'iframe[src*="player.vimeo.com/"]' ) ).forEach( function( el ) {
2782
3148
  if( !el.hasAttribute( 'data-ignore' ) && typeof el.contentWindow.postMessage === 'function' ) {
2783
3149
  el.contentWindow.postMessage( '{"method":"pause"}', '*' );
2784
3150
  }
2785
3151
  });
3152
+
3153
+ // Lazy loading iframes
3154
+ toArray( slide.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) {
3155
+ // Only removing the src doesn't actually unload the frame
3156
+ // in all browsers (Firefox) so we set it to blank first
3157
+ el.setAttribute( 'src', 'about:blank' );
3158
+ el.removeAttribute( 'src' );
3159
+ } );
2786
3160
  }
2787
3161
 
2788
3162
  }
2789
3163
 
2790
3164
  /**
2791
- * Returns a value ranging from 0-1 that represents
2792
- * how far into the presentation we have navigated.
3165
+ * Returns the number of past slides. This can be used as a global
3166
+ * flattened index for slides.
2793
3167
  */
2794
- function getProgress() {
3168
+ function getSlidePastCount() {
2795
3169
 
2796
3170
  var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
2797
3171
 
2798
- // The number of past and total slides
2799
- var totalCount = getTotalSlides();
3172
+ // The number of past slides
2800
3173
  var pastCount = 0;
2801
3174
 
2802
3175
  // Step through all slides and count the past ones
@@ -2828,6 +3201,20 @@
2828
3201
 
2829
3202
  }
2830
3203
 
3204
+ return pastCount;
3205
+
3206
+ }
3207
+
3208
+ /**
3209
+ * Returns a value ranging from 0-1 that represents
3210
+ * how far into the presentation we have navigated.
3211
+ */
3212
+ function getProgress() {
3213
+
3214
+ // The number of past and total slides
3215
+ var totalCount = getTotalSlides();
3216
+ var pastCount = getSlidePastCount();
3217
+
2831
3218
  if( currentSlide ) {
2832
3219
 
2833
3220
  var allFragments = currentSlide.querySelectorAll( '.fragment' );
@@ -2880,7 +3267,7 @@
2880
3267
  // Ensure the named link is a valid HTML ID attribute
2881
3268
  if( /^[a-zA-Z][\w:.-]*$/.test( name ) ) {
2882
3269
  // Find the slide with the specified ID
2883
- element = document.querySelector( '#' + name );
3270
+ element = document.getElementById( name );
2884
3271
  }
2885
3272
 
2886
3273
  if( element ) {
@@ -2929,7 +3316,6 @@
2929
3316
  // Attempt to create a named link based on the slide's ID
2930
3317
  var id = currentSlide.getAttribute( 'id' );
2931
3318
  if( id ) {
2932
- id = id.toLowerCase();
2933
3319
  id = id.replace( /[^a-zA-Z0-9\-\_\:\.]/g, '' );
2934
3320
  }
2935
3321
 
@@ -3061,6 +3447,32 @@
3061
3447
 
3062
3448
  }
3063
3449
 
3450
+ /**
3451
+ * Retrieves the speaker notes from a slide. Notes can be
3452
+ * defined in two ways:
3453
+ * 1. As a data-notes attribute on the slide <section>
3454
+ * 2. As an <aside class="notes"> inside of the slide
3455
+ */
3456
+ function getSlideNotes( slide ) {
3457
+
3458
+ // Default to the current slide
3459
+ slide = slide || currentSlide;
3460
+
3461
+ // Notes can be specified via the data-notes attribute...
3462
+ if( slide.hasAttribute( 'data-notes' ) ) {
3463
+ return slide.getAttribute( 'data-notes' );
3464
+ }
3465
+
3466
+ // ... or using an <aside class="notes"> element
3467
+ var notesElement = slide.querySelector( 'aside.notes' );
3468
+ if( notesElement ) {
3469
+ return notesElement.innerHTML;
3470
+ }
3471
+
3472
+ return null;
3473
+
3474
+ }
3475
+
3064
3476
  /**
3065
3477
  * Retrieves the current state of the presentation as
3066
3478
  * an object. This state can then be restored at any
@@ -3312,14 +3724,17 @@
3312
3724
 
3313
3725
  // If there are media elements with data-autoplay,
3314
3726
  // 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;
3727
+ // length of that media. Not applicable if the slide
3728
+ // is divided up into fragments.
3729
+ if( currentSlide.querySelectorAll( '.fragment' ).length === 0 ) {
3730
+ toArray( currentSlide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
3731
+ if( el.hasAttribute( 'data-autoplay' ) ) {
3732
+ if( autoSlide && el.duration * 1000 > autoSlide ) {
3733
+ autoSlide = ( el.duration * 1000 ) + 1000;
3734
+ }
3320
3735
  }
3321
- }
3322
- } );
3736
+ } );
3737
+ }
3323
3738
 
3324
3739
  // Cue the next auto-slide if:
3325
3740
  // - There is an autoSlide value
@@ -3328,7 +3743,10 @@
3328
3743
  // - The overview isn't active
3329
3744
  // - The presentation isn't over
3330
3745
  if( autoSlide && !autoSlidePaused && !isPaused() && !isOverview() && ( !Reveal.isLastSlide() || availableFragments().next || config.loop === true ) ) {
3331
- autoSlideTimeout = setTimeout( navigateNext, autoSlide );
3746
+ autoSlideTimeout = setTimeout( function() {
3747
+ typeof config.autoSlideMethod === 'function' ? config.autoSlideMethod() : navigateNext();
3748
+ cueAutoSlide();
3749
+ }, autoSlide );
3332
3750
  autoSlideStartTime = Date.now();
3333
3751
  }
3334
3752
 
@@ -3474,9 +3892,20 @@
3474
3892
  }
3475
3893
  }
3476
3894
 
3477
- // If auto-sliding is enabled we need to cue up
3478
- // another timeout
3479
- cueAutoSlide();
3895
+ }
3896
+
3897
+ /**
3898
+ * Checks if the target element prevents the triggering of
3899
+ * swipe navigation.
3900
+ */
3901
+ function isSwipePrevented( target ) {
3902
+
3903
+ while( target && typeof target.hasAttribute === 'function' ) {
3904
+ if( target.hasAttribute( 'data-prevent-swipe' ) ) return true;
3905
+ target = target.parentNode;
3906
+ }
3907
+
3908
+ return false;
3480
3909
 
3481
3910
  }
3482
3911
 
@@ -3539,8 +3968,20 @@
3539
3968
  // keyboard modifier key is present
3540
3969
  if( activeElementIsCE || activeElementIsInput || (event.shiftKey && event.keyCode !== 32) || event.altKey || event.ctrlKey || event.metaKey ) return;
3541
3970
 
3542
- // While paused only allow "unpausing" keyboard events (b and .)
3543
- if( isPaused() && [66,190,191].indexOf( event.keyCode ) === -1 ) {
3971
+ // While paused only allow resume keyboard events; 'b', '.''
3972
+ var resumeKeyCodes = [66,190,191];
3973
+ var key;
3974
+
3975
+ // Custom key bindings for togglePause should be able to resume
3976
+ if( typeof config.keyboard === 'object' ) {
3977
+ for( key in config.keyboard ) {
3978
+ if( config.keyboard[key] === 'togglePause' ) {
3979
+ resumeKeyCodes.push( parseInt( key, 10 ) );
3980
+ }
3981
+ }
3982
+ }
3983
+
3984
+ if( isPaused() && resumeKeyCodes.indexOf( event.keyCode ) === -1 ) {
3544
3985
  return false;
3545
3986
  }
3546
3987
 
@@ -3549,7 +3990,7 @@
3549
3990
  // 1. User defined key bindings
3550
3991
  if( typeof config.keyboard === 'object' ) {
3551
3992
 
3552
- for( var key in config.keyboard ) {
3993
+ for( key in config.keyboard ) {
3553
3994
 
3554
3995
  // Check if this binding matches the pressed key
3555
3996
  if( parseInt( key, 10 ) === event.keyCode ) {
@@ -3641,6 +4082,8 @@
3641
4082
  */
3642
4083
  function onTouchStart( event ) {
3643
4084
 
4085
+ if( isSwipePrevented( event.target ) ) return true;
4086
+
3644
4087
  touch.startX = event.touches[0].clientX;
3645
4088
  touch.startY = event.touches[0].clientY;
3646
4089
  touch.startCount = event.touches.length;
@@ -3664,6 +4107,8 @@
3664
4107
  */
3665
4108
  function onTouchMove( event ) {
3666
4109
 
4110
+ if( isSwipePrevented( event.target ) ) return true;
4111
+
3667
4112
  // Each touch should only trigger one action
3668
4113
  if( !touch.captured ) {
3669
4114
  onUserInput( event );
@@ -3740,7 +4185,7 @@
3740
4185
  }
3741
4186
  // There's a bug with swiping on some Android devices unless
3742
4187
  // the default action is always prevented
3743
- else if( navigator.userAgent.match( /android/gi ) ) {
4188
+ else if( UA.match( /android/gi ) ) {
3744
4189
  event.preventDefault();
3745
4190
  }
3746
4191
 
@@ -3828,6 +4273,10 @@
3828
4273
  var slidesTotal = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).length;
3829
4274
  var slideIndex = Math.floor( ( event.clientX / dom.wrapper.offsetWidth ) * slidesTotal );
3830
4275
 
4276
+ if( config.rtl ) {
4277
+ slideIndex = slidesTotal - slideIndex;
4278
+ }
4279
+
3831
4280
  slide( slideIndex );
3832
4281
 
3833
4282
  }
@@ -3872,7 +4321,10 @@
3872
4321
  // If, after clicking a link or similar and we're coming back,
3873
4322
  // focus the document.body to ensure we can use keyboard shortcuts
3874
4323
  if( isHidden === false && document.activeElement !== document.body ) {
3875
- document.activeElement.blur();
4324
+ // Not all elements support .blur() - SVGs among them.
4325
+ if( typeof document.activeElement.blur === 'function' ) {
4326
+ document.activeElement.blur();
4327
+ }
3876
4328
  document.body.focus();
3877
4329
  }
3878
4330
 
@@ -3966,8 +4418,9 @@
3966
4418
  function Playback( container, progressCheck ) {
3967
4419
 
3968
4420
  // Cosmetics
3969
- this.diameter = 50;
3970
- this.thickness = 3;
4421
+ this.diameter = 100;
4422
+ this.diameter2 = this.diameter/2;
4423
+ this.thickness = 6;
3971
4424
 
3972
4425
  // Flags if we are currently playing
3973
4426
  this.playing = false;
@@ -3985,6 +4438,8 @@
3985
4438
  this.canvas.className = 'playback';
3986
4439
  this.canvas.width = this.diameter;
3987
4440
  this.canvas.height = this.diameter;
4441
+ this.canvas.style.width = this.diameter2 + 'px';
4442
+ this.canvas.style.height = this.diameter2 + 'px';
3988
4443
  this.context = this.canvas.getContext( '2d' );
3989
4444
 
3990
4445
  this.container.appendChild( this.canvas );
@@ -4035,10 +4490,10 @@
4035
4490
  Playback.prototype.render = function() {
4036
4491
 
4037
4492
  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;
4493
+ radius = ( this.diameter2 ) - this.thickness,
4494
+ x = this.diameter2,
4495
+ y = this.diameter2,
4496
+ iconSize = 28;
4042
4497
 
4043
4498
  // Ease towards 1
4044
4499
  this.progressOffset += ( 1 - this.progressOffset ) * 0.1;
@@ -4051,7 +4506,7 @@
4051
4506
 
4052
4507
  // Solid background color
4053
4508
  this.context.beginPath();
4054
- this.context.arc( x, y, radius + 2, 0, Math.PI * 2, false );
4509
+ this.context.arc( x, y, radius + 4, 0, Math.PI * 2, false );
4055
4510
  this.context.fillStyle = 'rgba( 0, 0, 0, 0.4 )';
4056
4511
  this.context.fill();
4057
4512
 
@@ -4076,14 +4531,14 @@
4076
4531
  // Draw play/pause icons
4077
4532
  if( this.playing ) {
4078
4533
  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 );
4534
+ this.context.fillRect( 0, 0, iconSize / 2 - 4, iconSize );
4535
+ this.context.fillRect( iconSize / 2 + 4, 0, iconSize / 2 - 4, iconSize );
4081
4536
  }
4082
4537
  else {
4083
4538
  this.context.beginPath();
4084
- this.context.translate( 2, 0 );
4539
+ this.context.translate( 4, 0 );
4085
4540
  this.context.moveTo( 0, 0 );
4086
- this.context.lineTo( iconSize - 2, iconSize / 2 );
4541
+ this.context.lineTo( iconSize - 4, iconSize / 2 );
4087
4542
  this.context.lineTo( 0, iconSize );
4088
4543
  this.context.fillStyle = '#fff';
4089
4544
  this.context.fill();
@@ -4118,6 +4573,8 @@
4118
4573
 
4119
4574
 
4120
4575
  Reveal = {
4576
+ VERSION: VERSION,
4577
+
4121
4578
  initialize: initialize,
4122
4579
  configure: configure,
4123
4580
  sync: sync,
@@ -4148,6 +4605,9 @@
4148
4605
  // Forces an update in slide layout
4149
4606
  layout: layout,
4150
4607
 
4608
+ // Randomizes the order of slides
4609
+ shuffle: shuffle,
4610
+
4151
4611
  // Returns an object with the available routes as booleans (left/right/top/bottom)
4152
4612
  availableRoutes: availableRoutes,
4153
4613
 
@@ -4190,6 +4650,9 @@
4190
4650
  // Returns the slide background element at the specified index
4191
4651
  getSlideBackground: getSlideBackground,
4192
4652
 
4653
+ // Returns the speaker notes string for a slide, or null
4654
+ getSlideNotes: getSlideNotes,
4655
+
4193
4656
  // Returns the previous slide element, may be null
4194
4657
  getPreviousSlide: function() {
4195
4658
  return previousSlide;
@@ -4268,6 +4731,11 @@
4268
4731
  // Programatically triggers a keyboard event
4269
4732
  triggerKey: function( keyCode ) {
4270
4733
  onDocumentKeyDown( { keyCode: keyCode } );
4734
+ },
4735
+
4736
+ // Registers a new shortcut to include in the help overlay
4737
+ registerKeyboardShortcut: function( key, value ) {
4738
+ keyboardShortcuts[key] = value;
4271
4739
  }
4272
4740
  };
4273
4741