slide_hero 0.0.10 → 0.0.11

Sign up to get free protection for your applications and to get access to all the features.
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