summaryrefslogtreecommitdiffhomepage
path: root/js/reveal.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/reveal.js')
-rw-r--r--js/reveal.js870
1 files changed, 677 insertions, 193 deletions
diff --git a/js/reveal.js b/js/reveal.js
index f844456..b1a20cc 100644
--- a/js/reveal.js
+++ b/js/reveal.js
@@ -3,7 +3,7 @@
* http://lab.hakim.se/reveal-js
* MIT licensed
*
- * Copyright (C) 2014 Hakim El Hattab, http://hakim.se
+ * Copyright (C) 2015 Hakim El Hattab, http://hakim.se
*/
(function( root, factory ) {
if( typeof define === 'function' && define.amd ) {
@@ -30,7 +30,7 @@
VERTICAL_SLIDES_SELECTOR = '.slides>section.present>section',
HOME_SLIDE_SELECTOR = '.slides>section:first-of-type',
- // Configurations defaults, can be overridden at initialization time
+ // Configuration defaults, can be overridden at initialization time
config = {
// The "normal" size of the presentation, aspect ratio will be preserved
@@ -43,7 +43,7 @@
// Bounds for smallest/largest possible scale to apply to content
minScale: 0.2,
- maxScale: 1.0,
+ maxScale: 1.5,
// Display controls in the bottom right corner
controls: true,
@@ -89,6 +89,12 @@
// key is pressed
help: true,
+ // Flags if it should be possible to pause the presentation (blackout)
+ pause: true,
+
+ // Flags if speaker notes should be visible to all viewers
+ showNotes: false,
+
// Number of milliseconds between automatically proceeding to the
// next slide, disabled when set to 0, this value can be overwritten
// by using a data-autoslide attribute on your slides
@@ -118,17 +124,14 @@
// Focuses body when page changes visiblity to ensure keyboard shortcuts work
focusBodyOnPageVisibilityChange: true,
- // Theme (see /css/theme)
- theme: null,
-
// Transition style
- transition: 'default', // none/fade/slide/convex/concave/zoom
+ transition: 'slide', // none/fade/slide/convex/concave/zoom
// Transition speed
transitionSpeed: 'default', // default/fast/slow
// Transition style for full page slide backgrounds
- backgroundTransition: 'default', // none/fade/slide/convex/concave/zoom
+ backgroundTransition: 'fade', // none/fade/slide/convex/concave/zoom
// Parallax background image
parallaxBackgroundImage: '', // CSS syntax, e.g. "a.jpg"
@@ -136,6 +139,10 @@
// Parallax background size
parallaxBackgroundSize: '', // CSS syntax, e.g. "3000px 2000px"
+ // Amount of pixels to move the parallax background per slide step
+ parallaxBackgroundHorizontal: null,
+ parallaxBackgroundVertical: null,
+
// Number of slides away from the current that are visible
viewDistance: 3,
@@ -147,6 +154,9 @@
// Flags if reveal.js is loaded (has dispatched the 'ready' event)
loaded = false,
+ // Flags if the overview mode is currently active
+ overview = false,
+
// The horizontal and vertical index of the currently active slide
indexh,
indexv,
@@ -165,6 +175,10 @@
// The current scale of the presentation (see width/height config)
scale = 1,
+ // CSS transform that is currently applied to the slides container,
+ // split into two groups
+ slidesTransform = { layout: '', overview: '' },
+
// Cached references to DOM elements
dom = {},
@@ -227,14 +241,18 @@
if( !features.transforms2d && !features.transforms3d ) {
document.body.setAttribute( 'class', 'no-transforms' );
- // Since JS won't be running any further, we need to load all
- // images that were intended to lazy load now
- var images = document.getElementsByTagName( 'img' );
- for( var i = 0, len = images.length; i < len; i++ ) {
- var image = images[i];
- if( image.getAttribute( 'data-src' ) ) {
- image.setAttribute( 'src', image.getAttribute( 'data-src' ) );
- image.removeAttribute( 'data-src' );
+ // Since JS won't be running any further, we load all lazy
+ // loading elements upfront
+ var images = toArray( document.getElementsByTagName( 'img' ) ),
+ iframes = toArray( document.getElementsByTagName( 'iframe' ) );
+
+ var lazyLoadable = images.concat( iframes );
+
+ for( var i = 0, len = lazyLoadable.length; i < len; i++ ) {
+ var element = lazyLoadable[i];
+ if( element.getAttribute( 'data-src' ) ) {
+ element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
+ element.removeAttribute( 'data-src' );
}
}
@@ -291,7 +309,13 @@
features.canvas = !!document.createElement( 'canvas' ).getContext;
- isMobileDevice = navigator.userAgent.match( /(iphone|ipod|ipad|android)/gi );
+ features.touch = !!( 'ontouchstart' in window );
+
+ // Transitions in the overview are disabled in desktop and
+ // mobile Safari due to lag
+ features.overviewTransitions = !/Version\/[\d\.]+.*Safari/.test( navigator.userAgent );
+
+ isMobileDevice = /(iphone|ipod|ipad|android)/gi.test( navigator.userAgent );
}
@@ -371,6 +395,9 @@
// Listen to messages posted to this window
setupPostMessage();
+ // Prevent iframes from scrolling the slides out of view
+ setupIframeScrollPrevention();
+
// Resets all vertical slides so that only the first is visible
resetVerticalSlides();
@@ -433,16 +460,16 @@
// Arrow controls
createSingletonNode( dom.wrapper, 'aside', 'controls',
- '<div class="navigate-left"></div>' +
- '<div class="navigate-right"></div>' +
- '<div class="navigate-up"></div>' +
- '<div class="navigate-down"></div>' );
+ '<button class="navigate-left" aria-label="previous slide"></button>' +
+ '<button class="navigate-right" aria-label="next slide"></button>' +
+ '<button class="navigate-up" aria-label="above slide"></button>' +
+ '<button class="navigate-down" aria-label="below slide"></button>' );
// Slide number
dom.slideNumber = createSingletonNode( dom.wrapper, 'div', 'slide-number', '' );
- // State background element [DEPRECATED]
- createSingletonNode( dom.wrapper, 'div', 'state-background', null );
+ // Element containing notes that are visible to the audience
+ dom.speakerNotes = createSingletonNode( dom.wrapper, 'div', 'speaker-notes', null );
// Overlay graphic which is displayed during the paused mode
createSingletonNode( dom.wrapper, 'div', 'pause-overlay', null );
@@ -508,7 +535,7 @@
injectStyleSheet( '@page{size:'+ pageWidth +'px '+ pageHeight +'px; margin: 0;}' );
// Limit the size of certain elements to the dimensions of the slide
- injectStyleSheet( '.reveal img, .reveal video, .reveal iframe{max-width: '+ slideWidth +'px; max-height:'+ slideHeight +'px}' );
+ injectStyleSheet( '.reveal section>img, .reveal section>video, .reveal section>iframe{max-width: '+ slideWidth +'px; max-height:'+ slideHeight +'px}' );
document.body.classList.add( 'print-pdf' );
document.body.style.width = pageWidth + 'px';
@@ -546,6 +573,19 @@
background.style.top = -top + 'px';
background.style.left = -left + 'px';
}
+
+ // If we're configured to `showNotes`, inject them into each slide
+ if( config.showNotes ) {
+ var notes = getSlideNotes( slide );
+ if( notes ) {
+ var notesElement = document.createElement( 'div' );
+ notesElement.classList.add( 'speaker-notes' );
+ notesElement.classList.add( 'speaker-notes-pdf' );
+ notesElement.innerHTML = notes;
+ notesElement.style.bottom = ( 40 - top ) + 'px';
+ slide.appendChild( notesElement );
+ }
+ }
}
} );
@@ -558,6 +598,26 @@
}
/**
+ * This is an unfortunate necessity. Iframes can trigger the
+ * parent window to scroll, for example by focusing an input.
+ * This scrolling can not be prevented by hiding overflow in
+ * CSS so we have to resort to repeatedly checking if the
+ * browser has decided to offset our slides :(
+ */
+ function setupIframeScrollPrevention() {
+
+ if( dom.slides.querySelector( 'iframe' ) ) {
+ setInterval( function() {
+ if( dom.wrapper.scrollTop !== 0 || dom.wrapper.scrollLeft !== 0 ) {
+ dom.wrapper.scrollTop = 0;
+ dom.wrapper.scrollLeft = 0;
+ }
+ }, 500 );
+ }
+
+ }
+
+ /**
* Creates an HTML element and returns a reference to it.
* If the element already exists the existing instance will
* be returned.
@@ -667,6 +727,7 @@
backgroundSize: slide.getAttribute( 'data-background-size' ),
backgroundImage: slide.getAttribute( 'data-background-image' ),
backgroundVideo: slide.getAttribute( 'data-background-video' ),
+ backgroundIframe: slide.getAttribute( 'data-background-iframe' ),
backgroundColor: slide.getAttribute( 'data-background-color' ),
backgroundRepeat: slide.getAttribute( 'data-background-repeat' ),
backgroundPosition: slide.getAttribute( 'data-background-position' ),
@@ -691,11 +752,12 @@
// Create a hash for this combination of background settings.
// This is used to determine when two slide backgrounds are
// the same.
- if( data.background || data.backgroundColor || data.backgroundImage || data.backgroundVideo ) {
+ if( data.background || data.backgroundColor || data.backgroundImage || data.backgroundVideo || data.backgroundIframe ) {
element.setAttribute( 'data-background-hash', data.background +
data.backgroundSize +
data.backgroundImage +
data.backgroundVideo +
+ data.backgroundIframe +
data.backgroundColor +
data.backgroundRepeat +
data.backgroundPosition +
@@ -711,6 +773,30 @@
container.appendChild( element );
+ // If backgrounds are being recreated, clear old classes
+ slide.classList.remove( 'has-dark-background' );
+ slide.classList.remove( 'has-light-background' );
+
+ // If this slide has a background color, add a class that
+ // signals if it is light or dark. If the slide has no background
+ // color, no class will be set
+ var computedBackgroundColor = window.getComputedStyle( element ).backgroundColor;
+ if( computedBackgroundColor ) {
+ var rgb = colorToRgb( computedBackgroundColor );
+
+ // Ignore fully transparent backgrounds. Some browsers return
+ // rgba(0,0,0,0) when reading the computed background color of
+ // an element with no background
+ if( rgb && rgb.a !== 0 ) {
+ if( colorBrightness( computedBackgroundColor ) < 128 ) {
+ slide.classList.add( 'has-dark-background' );
+ }
+ else {
+ slide.classList.add( 'has-light-background' );
+ }
+ }
+ }
+
return element;
}
@@ -732,7 +818,7 @@
var data = event.data;
// Make sure we're dealing with JSON
- if( data.charAt( 0 ) === '{' && data.charAt( data.length - 1 ) === '}' ) {
+ if( typeof data === 'string' && data.charAt( 0 ) === '{' && data.charAt( data.length - 1 ) === '}' ) {
data = JSON.parse( data );
// Check if the requested method can be found
@@ -784,6 +870,18 @@
dom.wrapper.classList.remove( 'center' );
}
+ // Exit the paused mode if it was configured off
+ if( config.pause === false ) {
+ resume();
+ }
+
+ if( config.showNotes ) {
+ dom.speakerNotes.classList.add( 'visible' );
+ }
+ else {
+ dom.speakerNotes.classList.remove( 'visible' );
+ }
+
if( config.mouseWheel ) {
document.addEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF
document.addEventListener( 'mousewheel', onDocumentMouseScroll, false );
@@ -834,18 +932,6 @@
} );
}
- // Load the theme in the config, if it's not already loaded
- if( config.theme && dom.theme ) {
- var themeURL = dom.theme.getAttribute( 'href' );
- var themeFinder = /[^\/]*?(?=\.css)/;
- var themeName = themeURL.match(themeFinder)[0];
-
- if( config.theme !== themeName ) {
- themeURL = themeURL.replace(themeFinder, config.theme);
- dom.theme.setAttribute( 'href', themeURL );
- }
- }
-
sync();
}
@@ -907,7 +993,17 @@
}
}
- [ 'touchstart', 'click' ].forEach( function( eventName ) {
+ // Listen to both touch and click events, in case the device
+ // supports both
+ var pointerEvents = [ 'touchstart', 'click' ];
+
+ // Only support touch for Android, fixes double navigations in
+ // stock browser
+ if( navigator.userAgent.match( /android/gi ) ) {
+ pointerEvents = [ 'touchstart' ];
+ }
+
+ pointerEvents.forEach( function( eventName ) {
dom.controlsLeft.forEach( function( el ) { el.addEventListener( eventName, onNavigateLeftClicked, false ); } );
dom.controlsRight.forEach( function( el ) { el.addEventListener( eventName, onNavigateRightClicked, false ); } );
dom.controlsUp.forEach( function( el ) { el.addEventListener( eventName, onNavigateUpClicked, false ); } );
@@ -1023,12 +1119,32 @@
element.style.WebkitTransform = transform;
element.style.MozTransform = transform;
element.style.msTransform = transform;
- element.style.OTransform = transform;
element.style.transform = transform;
}
/**
+ * Applies CSS transforms to the slides container. The container
+ * is transformed from two separate sources: layout and the overview
+ * mode.
+ */
+ function transformSlides( transforms ) {
+
+ // Pick up new transforms from arguments
+ if( typeof transforms.layout === 'string' ) slidesTransform.layout = transforms.layout;
+ if( typeof transforms.overview === 'string' ) slidesTransform.overview = transforms.overview;
+
+ // Apply the transforms to the slides container
+ if( slidesTransform.layout ) {
+ transformElement( dom.slides, slidesTransform.layout + ' ' + slidesTransform.overview );
+ }
+ else {
+ transformElement( dom.slides, slidesTransform.overview );
+ }
+
+ }
+
+ /**
* Injects the given CSS styles into the DOM.
*/
function injectStyleSheet( value ) {
@@ -1046,6 +1162,77 @@
}
/**
+ * Converts various color input formats to an {r:0,g:0,b:0} object.
+ *
+ * @param {String} color The string representation of a color,
+ * the following formats are supported:
+ * - #000
+ * - #000000
+ * - rgb(0,0,0)
+ */
+ function colorToRgb( color ) {
+
+ var hex3 = color.match( /^#([0-9a-f]{3})$/i );
+ if( hex3 && hex3[1] ) {
+ hex3 = hex3[1];
+ return {
+ r: parseInt( hex3.charAt( 0 ), 16 ) * 0x11,
+ g: parseInt( hex3.charAt( 1 ), 16 ) * 0x11,
+ b: parseInt( hex3.charAt( 2 ), 16 ) * 0x11
+ };
+ }
+
+ var hex6 = color.match( /^#([0-9a-f]{6})$/i );
+ if( hex6 && hex6[1] ) {
+ hex6 = hex6[1];
+ return {
+ r: parseInt( hex6.substr( 0, 2 ), 16 ),
+ g: parseInt( hex6.substr( 2, 2 ), 16 ),
+ b: parseInt( hex6.substr( 4, 2 ), 16 )
+ };
+ }
+
+ var rgb = color.match( /^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i );
+ if( rgb ) {
+ return {
+ r: parseInt( rgb[1], 10 ),
+ g: parseInt( rgb[2], 10 ),
+ b: parseInt( rgb[3], 10 )
+ };
+ }
+
+ var rgba = color.match( /^rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\,\s*([\d]+|[\d]*.[\d]+)\s*\)$/i );
+ if( rgba ) {
+ return {
+ r: parseInt( rgba[1], 10 ),
+ g: parseInt( rgba[2], 10 ),
+ b: parseInt( rgba[3], 10 ),
+ a: parseFloat( rgba[4] )
+ };
+ }
+
+ return null;
+
+ }
+
+ /**
+ * Calculates brightness on a scale of 0-255.
+ *
+ * @param color See colorStringToRgb for supported formats.
+ */
+ function colorBrightness( color ) {
+
+ if( typeof color === 'string' ) color = colorToRgb( color );
+
+ if( color ) {
+ return ( color.r * 299 + color.g * 587 + color.b * 114 ) / 1000;
+ }
+
+ return null;
+
+ }
+
+ /**
* Retrieves the height of the given element by looking
* at the position and height of its immediate children.
*/
@@ -1359,17 +1546,36 @@
scale = Math.max( scale, config.minScale );
scale = Math.min( scale, config.maxScale );
- // Prefer zooming in desktop Chrome so that content remains crisp
- if( !isMobileDevice && /chrome/i.test( navigator.userAgent ) && typeof dom.slides.style.zoom !== 'undefined' ) {
- dom.slides.style.zoom = scale;
+ // Don't apply any scaling styles if scale is 1
+ if( scale === 1 ) {
+ dom.slides.style.zoom = '';
+ dom.slides.style.left = '';
+ dom.slides.style.top = '';
+ dom.slides.style.bottom = '';
+ dom.slides.style.right = '';
+ transformSlides( { layout: '' } );
}
- // Apply scale transform as a fallback
else {
- dom.slides.style.left = '50%';
- dom.slides.style.top = '50%';
- dom.slides.style.bottom = 'auto';
- dom.slides.style.right = 'auto';
- transformElement( dom.slides, 'translate(-50%, -50%) scale('+ scale +')' );
+ // Use zoom to scale up in desktop Chrome so that content
+ // remains crisp. We don't use zoom to scale down since that
+ // can lead to shifts in text layout/line breaks.
+ if( scale > 1 && !isMobileDevice && /chrome/i.test( navigator.userAgent ) && typeof dom.slides.style.zoom !== 'undefined' ) {
+ dom.slides.style.zoom = scale;
+ dom.slides.style.left = '';
+ dom.slides.style.top = '';
+ dom.slides.style.bottom = '';
+ dom.slides.style.right = '';
+ transformSlides( { layout: '' } );
+ }
+ // Apply scale transform as a fallback
+ else {
+ dom.slides.style.zoom = '';
+ dom.slides.style.left = '50%';
+ dom.slides.style.top = '50%';
+ dom.slides.style.bottom = 'auto';
+ dom.slides.style.right = 'auto';
+ transformSlides( { layout: 'translate(-50%, -50%) scale('+ scale +')' } );
+ }
}
// Select all slides, vertical and horizontal
@@ -1456,7 +1662,7 @@
};
// Reduce available space by margin
- size.presentationWidth -= ( size.presentationHeight * config.margin );
+ size.presentationWidth -= ( size.presentationWidth * config.margin );
size.presentationHeight -= ( size.presentationHeight * config.margin );
// Slide width may be a percentage of available width
@@ -1510,82 +1716,122 @@
}
/**
- * Displays the overview of slides (quick nav) by
- * scaling down and arranging all slide elements.
- *
- * Experimental feature, might be dropped if perf
- * can't be improved.
+ * Displays the overview of slides (quick nav) by scaling
+ * down and arranging all slide elements.
*/
function activateOverview() {
// Only proceed if enabled in config
- if( config.overview ) {
+ if( config.overview && !isOverview() ) {
+
+ overview = true;
+
+ dom.wrapper.classList.add( 'overview' );
+ dom.wrapper.classList.remove( 'overview-deactivating' );
+
+ if( features.overviewTransitions ) {
+ setTimeout( function() {
+ dom.wrapper.classList.add( 'overview-animated' );
+ }, 1 );
+ }
// Don't auto-slide while in overview mode
cancelAutoSlide();
- var wasActive = dom.wrapper.classList.contains( 'overview' );
+ // Move the backgrounds element into the slide container to
+ // that the same scaling is applied
+ dom.slides.appendChild( dom.background );
- // Vary the depth of the overview based on screen size
- var depth = window.innerWidth < 400 ? 1000 : 2500;
+ // Clicking on an overview slide navigates to it
+ toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
+ if( !slide.classList.contains( 'stack' ) ) {
+ slide.addEventListener( 'click', onOverviewSlideClicked, true );
+ }
+ } );
- dom.wrapper.classList.add( 'overview' );
- dom.wrapper.classList.remove( 'overview-deactivating' );
+ updateSlidesVisibility();
+ layoutOverview();
+ updateOverview();
- var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR );
+ layout();
- for( var i = 0, len1 = horizontalSlides.length; i < len1; i++ ) {
- var hslide = horizontalSlides[i],
- hoffset = config.rtl ? -105 : 105;
+ // Notify observers of the overview showing
+ dispatchEvent( 'overviewshown', {
+ 'indexh': indexh,
+ 'indexv': indexv,
+ 'currentSlide': currentSlide
+ } );
- hslide.setAttribute( 'data-index-h', i );
+ }
- // Apply CSS transform
- transformElement( hslide, 'translateZ(-'+ depth +'px) translate(' + ( ( i - indexh ) * hoffset ) + '%, 0%)' );
+ }
- if( hslide.classList.contains( 'stack' ) ) {
+ /**
+ * Uses CSS transforms to position all slides in a grid for
+ * display inside of the overview mode.
+ */
+ function layoutOverview() {
- var verticalSlides = hslide.querySelectorAll( 'section' );
+ var margin = 70;
+ var slideWidth = config.width + margin,
+ slideHeight = config.height + margin;
- for( var j = 0, len2 = verticalSlides.length; j < len2; j++ ) {
- var verticalIndex = i === indexh ? indexv : getPreviousVerticalIndex( hslide );
+ // Reverse in RTL mode
+ if( config.rtl ) {
+ slideWidth = -slideWidth;
+ }
- var vslide = verticalSlides[j];
+ // Layout slides
+ toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( hslide, h ) {
+ hslide.setAttribute( 'data-index-h', h );
+ transformElement( hslide, 'translate3d(' + ( h * slideWidth ) + 'px, 0, 0)' );
- vslide.setAttribute( 'data-index-h', i );
- vslide.setAttribute( 'data-index-v', j );
+ if( hslide.classList.contains( 'stack' ) ) {
- // Apply CSS transform
- transformElement( vslide, 'translate(0%, ' + ( ( j - verticalIndex ) * 105 ) + '%)' );
+ toArray( hslide.querySelectorAll( 'section' ) ).forEach( function( vslide, v ) {
+ vslide.setAttribute( 'data-index-h', h );
+ vslide.setAttribute( 'data-index-v', v );
- // Navigate to this slide on click
- vslide.addEventListener( 'click', onOverviewSlideClicked, true );
- }
+ transformElement( vslide, 'translate3d(0, ' + ( v * slideHeight ) + 'px, 0)' );
+ } );
- }
- else {
+ }
+ } );
- // Navigate to this slide on click
- hslide.addEventListener( 'click', onOverviewSlideClicked, true );
+ // Layout slide backgrounds
+ toArray( dom.background.childNodes ).forEach( function( hbackground, h ) {
+ transformElement( hbackground, 'translate3d(' + ( h * slideWidth ) + 'px, 0, 0)' );
- }
- }
+ toArray( hbackground.querySelectorAll( '.slide-background' ) ).forEach( function( vbackground, v ) {
+ transformElement( vbackground, 'translate3d(0, ' + ( v * slideHeight ) + 'px, 0)' );
+ } );
+ } );
- updateSlidesVisibility();
+ }
- layout();
+ /**
+ * Moves the overview viewport to the current slides.
+ * Called each time the current slide changes.
+ */
+ function updateOverview() {
- if( !wasActive ) {
- // Notify observers of the overview showing
- dispatchEvent( 'overviewshown', {
- 'indexh': indexh,
- 'indexv': indexv,
- 'currentSlide': currentSlide
- } );
- }
+ var margin = 70;
+ var slideWidth = config.width + margin,
+ slideHeight = config.height + margin;
+ // Reverse in RTL mode
+ if( config.rtl ) {
+ slideWidth = -slideWidth;
}
+ transformSlides( {
+ overview: [
+ 'translateX('+ ( -indexh * slideWidth ) +'px)',
+ 'translateY('+ ( -indexv * slideHeight ) +'px)',
+ 'translateZ('+ ( window.innerWidth < 400 ? -1000 : -2500 ) +'px)'
+ ].join( ' ' )
+ } );
+
}
/**
@@ -1597,7 +1843,10 @@
// Only proceed if enabled in config
if( config.overview ) {
+ overview = false;
+
dom.wrapper.classList.remove( 'overview' );
+ dom.wrapper.classList.remove( 'overview-animated' );
// Temporarily add a class so that transitions can do different things
// depending on whether they are exiting/entering overview, or just
@@ -1608,16 +1857,27 @@
dom.wrapper.classList.remove( 'overview-deactivating' );
}, 1 );
- // Select all slides
+ // Move the background element back out
+ dom.wrapper.appendChild( dom.background );
+
+ // Clean up changes made to slides
toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
- // Resets all transforms to use the external styles
transformElement( slide, '' );
slide.removeEventListener( 'click', onOverviewSlideClicked, true );
} );
+ // Clean up changes made to backgrounds
+ toArray( dom.background.querySelectorAll( '.slide-background' ) ).forEach( function( background ) {
+ transformElement( background, '' );
+ } );
+
+ transformSlides( { overview: '' } );
+
slide( indexh, indexv );
+ layout();
+
cueAutoSlide();
// Notify observers of the overview hiding
@@ -1656,7 +1916,7 @@
*/
function isOverview() {
- return dom.wrapper.classList.contains( 'overview' );
+ return overview;
}
@@ -1705,13 +1965,15 @@
*/
function pause() {
- var wasPaused = dom.wrapper.classList.contains( 'paused' );
+ if( config.pause ) {
+ var wasPaused = dom.wrapper.classList.contains( 'paused' );
- cancelAutoSlide();
- dom.wrapper.classList.add( 'paused' );
+ cancelAutoSlide();
+ dom.wrapper.classList.add( 'paused' );
- if( wasPaused === false ) {
- dispatchEvent( 'paused' );
+ if( wasPaused === false ) {
+ dispatchEvent( 'paused' );
+ }
}
}
@@ -1758,7 +2020,7 @@
/**
* Toggles the auto slide mode on and off.
*
- * @param {Boolean} override Optional flag which sets the desired state.
+ * @param {Boolean} override Optional flag which sets the desired state.
* True means autoplay starts, false means it stops.
*/
@@ -1804,7 +2066,7 @@
// If no vertical index is specified and the upcoming slide is a
// stack, resume at its previous vertical index
- if( v === undefined ) {
+ if( v === undefined && !isOverview() ) {
v = getPreviousVerticalIndex( horizontalSlides[ h ] );
}
@@ -1854,9 +2116,9 @@
document.documentElement.classList.remove( stateBefore.pop() );
}
- // If the overview is active, re-activate it to update positions
+ // Update the overview if it's currently active
if( isOverview() ) {
- activateOverview();
+ updateOverview();
}
// Find the current horizontal slide and any possible vertical slides
@@ -1925,6 +2187,7 @@
updateBackground();
updateParallax();
updateSlideNumber();
+ updateNotes();
// Update the URL hash
writeURL();
@@ -1966,8 +2229,14 @@
updateBackground( true );
updateSlideNumber();
updateSlidesVisibility();
+ updateNotes();
formatEmbeddedContent();
+ startEmbeddedContent( currentSlide );
+
+ if( isOverview() ) {
+ layoutOverview();
+ }
}
@@ -2146,7 +2415,7 @@
distanceX,
distanceY;
- if( horizontalSlidesLength ) {
+ if( horizontalSlidesLength && typeof indexh !== 'undefined' ) {
// The number of steps away from the present slide that will
// be visible
@@ -2157,7 +2426,7 @@
viewDistance = isOverview() ? 6 : 2;
}
- // Limit view distance on weaker devices
+ // All slides need to be visible when exporting to PDF
if( isPrintingPDF() ) {
viewDistance = Number.MAX_VALUE;
}
@@ -2168,8 +2437,14 @@
var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) ),
verticalSlidesLength = verticalSlides.length;
- // Loops so that it measures 1 between the first and last slides
- distanceX = Math.abs( ( indexh - x ) % ( horizontalSlidesLength - viewDistance ) ) || 0;
+ // Determine how far away this slide is from the present
+ distanceX = Math.abs( ( indexh || 0 ) - x ) || 0;
+
+ // If the presentation is looped, distance should measure
+ // 1 between the first and last slides
+ if( config.loop ) {
+ distanceX = Math.abs( ( ( indexh || 0 ) - x ) % ( horizontalSlidesLength - viewDistance ) ) || 0;
+ }
// Show the horizontal slide if it's within the view distance
if( distanceX < viewDistance ) {
@@ -2186,7 +2461,7 @@
for( var y = 0; y < verticalSlidesLength; y++ ) {
var verticalSlide = verticalSlides[y];
- distanceY = x === indexh ? Math.abs( indexv - y ) : Math.abs( y - oy );
+ distanceY = x === ( indexh || 0 ) ? Math.abs( ( indexv || 0 ) - y ) : Math.abs( y - oy );
if( distanceX + distanceY < viewDistance ) {
showSlide( verticalSlide );
@@ -2204,6 +2479,22 @@
}
/**
+ * Pick up notes from the current slide and display tham
+ * to the viewer.
+ *
+ * @see `showNotes` config value
+ */
+ function updateNotes() {
+
+ if( config.showNotes && dom.speakerNotes && currentSlide && !isPrintingPDF() ) {
+
+ dom.speakerNotes.innerHTML = getSlideNotes() || '';
+
+ }
+
+ }
+
+ /**
* Updates the progress bar to reflect the current slide.
*/
function updateProgress() {
@@ -2211,7 +2502,7 @@
// Update progress if enabled
if( config.progress && dom.progressbar ) {
- dom.progressbar.style.width = getProgress() * window.innerWidth + 'px';
+ dom.progressbar.style.width = getProgress() * dom.wrapper.offsetWidth + 'px';
}
@@ -2219,19 +2510,31 @@
/**
* Updates the slide number div to reflect the current slide.
+ *
+ * Slide number format can be defined as a string using the
+ * following variables:
+ * h: current slide's horizontal index
+ * v: current slide's vertical index
+ * c: current slide index (flattened)
+ * t: total number of slides (flattened)
*/
function updateSlideNumber() {
// Update slide number if enabled
if( config.slideNumber && dom.slideNumber) {
- // Display the number of the page using 'indexh - indexv' format
- var indexString = indexh;
- if( indexv > 0 ) {
- indexString += ' - ' + indexv;
+ // Default to only showing the current slide number
+ var format = 'c';
+
+ // Check if a custom slide number format is available
+ if( typeof config.slideNumber === 'string' ) {
+ format = config.slideNumber;
}
- dom.slideNumber.innerHTML = indexString;
+ dom.slideNumber.innerHTML = format.replace( /h/g, indexh )
+ .replace( /v/g, indexv )
+ .replace( /c/g, getSlidePastCount() + 1 )
+ .replace( /t/g, getTotalSlides() );
}
}
@@ -2359,7 +2662,19 @@
// Start video playback
var currentVideo = currentBackground.querySelector( 'video' );
- if( currentVideo ) currentVideo.play();
+ if( currentVideo ) {
+ if( currentVideo.currentTime > 0 ) currentVideo.currentTime = 0;
+ currentVideo.play();
+ }
+
+ var backgroundImageURL = currentBackground.style.backgroundImage || '';
+
+ // Restart GIFs (doesn't work in Firefox)
+ if( /\.gif/i.test( backgroundImageURL ) ) {
+ currentBackground.style.backgroundImage = '';
+ window.getComputedStyle( currentBackground ).opacity;
+ currentBackground.style.backgroundImage = backgroundImageURL;
+ }
// Don't transition between identical backgrounds. This
// prevents unwanted flicker.
@@ -2373,6 +2688,19 @@
}
+ // If there's a background brightness flag for this slide,
+ // bubble it to the .reveal container
+ if( currentSlide ) {
+ [ 'has-light-background', 'has-dark-background' ].forEach( function( classToBubble ) {
+ if( currentSlide.classList.contains( classToBubble ) ) {
+ dom.wrapper.classList.add( classToBubble );
+ }
+ else {
+ dom.wrapper.classList.remove( classToBubble );
+ }
+ } );
+ }
+
// Allow the first background to apply without transition
setTimeout( function() {
dom.background.classList.remove( 'no-transition' );
@@ -2402,15 +2730,35 @@
backgroundHeight = parseInt( backgroundSize[1], 10 );
}
- var slideWidth = dom.background.offsetWidth;
- var horizontalSlideCount = horizontalSlides.length;
- var horizontalOffset = -( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 ) * indexh;
+ var slideWidth = dom.background.offsetWidth,
+ horizontalSlideCount = horizontalSlides.length,
+ horizontalOffsetMultiplier,
+ horizontalOffset;
- var slideHeight = dom.background.offsetHeight;
- var verticalSlideCount = verticalSlides.length;
- var verticalOffset = verticalSlideCount > 1 ? -( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 ) * indexv : 0;
+ if( typeof config.parallaxBackgroundHorizontal === 'number' ) {
+ horizontalOffsetMultiplier = config.parallaxBackgroundHorizontal;
+ }
+ else {
+ horizontalOffsetMultiplier = ( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 );
+ }
- dom.background.style.backgroundPosition = horizontalOffset + 'px ' + verticalOffset + 'px';
+ horizontalOffset = horizontalOffsetMultiplier * indexh * -1;
+
+ var slideHeight = dom.background.offsetHeight,
+ verticalSlideCount = verticalSlides.length,
+ verticalOffsetMultiplier,
+ verticalOffset;
+
+ if( typeof config.parallaxBackgroundVertical === 'number' ) {
+ verticalOffsetMultiplier = config.parallaxBackgroundVertical;
+ }
+ else {
+ verticalOffsetMultiplier = ( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 );
+ }
+
+ verticalOffset = verticalSlideCount > 0 ? verticalOffsetMultiplier * indexv * 1 : 0;
+
+ dom.background.style.backgroundPosition = horizontalOffset + 'px ' + -verticalOffset + 'px';
}
@@ -2427,7 +2775,7 @@
slide.style.display = 'block';
// Media elements with data-src attributes
- toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src], iframe[data-src]' ) ).forEach( function( element ) {
+ toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src]' ) ).forEach( function( element ) {
element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
element.removeAttribute( 'data-src' );
} );
@@ -2461,16 +2809,22 @@
background.setAttribute( 'data-loaded', 'true' );
var backgroundImage = slide.getAttribute( 'data-background-image' ),
- backgroundVideo = slide.getAttribute( 'data-background-video' );
+ backgroundVideo = slide.getAttribute( 'data-background-video' ),
+ backgroundVideoLoop = slide.hasAttribute( 'data-background-video-loop' ),
+ backgroundIframe = slide.getAttribute( 'data-background-iframe' );
// Images
if( backgroundImage ) {
background.style.backgroundImage = 'url('+ backgroundImage +')';
}
// Videos
- else if ( backgroundVideo ) {
+ else if ( backgroundVideo && !isSpeakerNotes() ) {
var video = document.createElement( 'video' );
+ if( backgroundVideoLoop ) {
+ video.setAttribute( 'loop', '' );
+ }
+
// Support comma separated lists of video sources
backgroundVideo.split( ',' ).forEach( function( source ) {
video.innerHTML += '<source src="'+ source +'">';
@@ -2478,6 +2832,17 @@
background.appendChild( video );
}
+ // Iframes
+ else if( backgroundIframe ) {
+ var iframe = document.createElement( 'iframe' );
+ iframe.setAttribute( 'src', backgroundIframe );
+ iframe.style.width = '100%';
+ iframe.style.height = '100%';
+ iframe.style.maxHeight = '100%';
+ iframe.style.maxWidth = '100%';
+
+ background.appendChild( iframe );
+ }
}
}
@@ -2557,21 +2922,22 @@
*/
function formatEmbeddedContent() {
+ var _appendParamToIframeSource = function( sourceAttribute, sourceURL, param ) {
+ toArray( dom.slides.querySelectorAll( 'iframe['+ sourceAttribute +'*="'+ sourceURL +'"]' ) ).forEach( function( el ) {
+ var src = el.getAttribute( sourceAttribute );
+ if( src && src.indexOf( param ) === -1 ) {
+ el.setAttribute( sourceAttribute, src + ( !/\?/.test( src ) ? '?' : '&' ) + param );
+ }
+ });
+ };
+
// YouTube frames must include "?enablejsapi=1"
- toArray( dom.slides.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) {
- var src = el.getAttribute( 'src' );
- if( !/enablejsapi\=1/gi.test( src ) ) {
- el.setAttribute( 'src', src + ( !/\?/.test( src ) ? '?' : '&' ) + 'enablejsapi=1' );
- }
- });
+ _appendParamToIframeSource( 'src', 'youtube.com/embed/', 'enablejsapi=1' );
+ _appendParamToIframeSource( 'data-src', 'youtube.com/embed/', 'enablejsapi=1' );
// Vimeo frames must include "?api=1"
- toArray( dom.slides.querySelectorAll( 'iframe[src*="player.vimeo.com/"]' ) ).forEach( function( el ) {
- var src = el.getAttribute( 'src' );
- if( !/api\=1/gi.test( src ) ) {
- el.setAttribute( 'src', src + ( !/\?/.test( src ) ? '?' : '&' ) + 'api=1' );
- }
- });
+ _appendParamToIframeSource( 'src', 'player.vimeo.com/', 'api=1' );
+ _appendParamToIframeSource( 'data-src', 'player.vimeo.com/', 'api=1' );
}
@@ -2582,32 +2948,56 @@
function startEmbeddedContent( slide ) {
if( slide && !isSpeakerNotes() ) {
+ // Restart GIFs
+ toArray( slide.querySelectorAll( 'img[src$=".gif"]' ) ).forEach( function( el ) {
+ // Setting the same unchanged source like this was confirmed
+ // to work in Chrome, FF & Safari
+ el.setAttribute( 'src', el.getAttribute( 'src' ) );
+ } );
+
// HTML5 media elements
toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
- if( el.hasAttribute( 'data-autoplay' ) ) {
+ if( el.hasAttribute( 'data-autoplay' ) && typeof el.play === 'function' ) {
el.play();
}
} );
- // iframe embeds
- toArray( slide.querySelectorAll( 'iframe' ) ).forEach( function( el ) {
- el.contentWindow.postMessage( 'slide:start', '*' );
- });
+ // Normal iframes
+ toArray( slide.querySelectorAll( 'iframe[src]' ) ).forEach( function( el ) {
+ startEmbeddedIframe( { target: el } );
+ } );
- // YouTube embeds
- toArray( slide.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) {
- if( el.hasAttribute( 'data-autoplay' ) ) {
- el.contentWindow.postMessage( '{"event":"command","func":"playVideo","args":""}', '*' );
+ // Lazy loading iframes
+ toArray( slide.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) {
+ if( el.getAttribute( 'src' ) !== el.getAttribute( 'data-src' ) ) {
+ el.removeEventListener( 'load', startEmbeddedIframe ); // remove first to avoid dupes
+ el.addEventListener( 'load', startEmbeddedIframe );
+ el.setAttribute( 'src', el.getAttribute( 'data-src' ) );
}
- });
+ } );
+ }
- // Vimeo embeds
- toArray( slide.querySelectorAll( 'iframe[src*="player.vimeo.com/"]' ) ).forEach( function( el ) {
- if( el.hasAttribute( 'data-autoplay' ) ) {
- console.log(11);
- el.contentWindow.postMessage( '{"method":"play"}', '*' );
- }
- });
+ }
+
+ /**
+ * "Starts" the content of an embedded iframe using the
+ * postmessage API.
+ */
+ function startEmbeddedIframe( event ) {
+
+ var iframe = event.target;
+
+ // YouTube postMessage API
+ if( /youtube\.com\/embed\//.test( iframe.getAttribute( 'src' ) ) && iframe.hasAttribute( 'data-autoplay' ) ) {
+ iframe.contentWindow.postMessage( '{"event":"command","func":"playVideo","args":""}', '*' );
+ }
+ // Vimeo postMessage API
+ else if( /player\.vimeo\.com\//.test( iframe.getAttribute( 'src' ) ) && iframe.hasAttribute( 'data-autoplay' ) ) {
+ iframe.contentWindow.postMessage( '{"method":"play"}', '*' );
+ }
+ // Generic postMessage API
+ else {
+ iframe.contentWindow.postMessage( 'slide:start', '*' );
}
}
@@ -2618,46 +3008,54 @@
*/
function stopEmbeddedContent( slide ) {
- if( slide ) {
+ if( slide && slide.parentNode ) {
// HTML5 media elements
toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
- if( !el.hasAttribute( 'data-ignore' ) ) {
+ if( !el.hasAttribute( 'data-ignore' ) && typeof el.pause === 'function' ) {
el.pause();
}
} );
- // iframe embeds
+ // Generic postMessage API for non-lazy loaded iframes
toArray( slide.querySelectorAll( 'iframe' ) ).forEach( function( el ) {
el.contentWindow.postMessage( 'slide:stop', '*' );
+ el.removeEventListener( 'load', startEmbeddedIframe );
});
- // YouTube embeds
+ // YouTube postMessage API
toArray( slide.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) {
if( !el.hasAttribute( 'data-ignore' ) && typeof el.contentWindow.postMessage === 'function' ) {
el.contentWindow.postMessage( '{"event":"command","func":"pauseVideo","args":""}', '*' );
}
});
- // Vimeo embeds
+ // Vimeo postMessage API
toArray( slide.querySelectorAll( 'iframe[src*="player.vimeo.com/"]' ) ).forEach( function( el ) {
if( !el.hasAttribute( 'data-ignore' ) && typeof el.contentWindow.postMessage === 'function' ) {
el.contentWindow.postMessage( '{"method":"pause"}', '*' );
}
});
+
+ // Lazy loading iframes
+ toArray( slide.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) {
+ // Only removing the src doesn't actually unload the frame
+ // in all browsers (Firefox) so we set it to blank first
+ el.setAttribute( 'src', 'about:blank' );
+ el.removeAttribute( 'src' );
+ } );
}
}
/**
- * Returns a value ranging from 0-1 that represents
- * how far into the presentation we have navigated.
+ * Returns the number of past slides. This can be used as a global
+ * flattened index for slides.
*/
- function getProgress() {
+ function getSlidePastCount() {
var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
- // The number of past and total slides
- var totalCount = dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ':not(.stack)' ).length;
+ // The number of past slides
var pastCount = 0;
// Step through all slides and count the past ones
@@ -2689,6 +3087,20 @@
}
+ return pastCount;
+
+ }
+
+ /**
+ * Returns a value ranging from 0-1 that represents
+ * how far into the presentation we have navigated.
+ */
+ function getProgress() {
+
+ // The number of past and total slides
+ var totalCount = getTotalSlides();
+ var pastCount = getSlidePastCount();
+
if( currentSlide ) {
var allFragments = currentSlide.querySelectorAll( '.fragment' );
@@ -2741,7 +3153,7 @@
// Ensure the named link is a valid HTML ID attribute
if( /^[a-zA-Z][\w:.-]*$/.test( name ) ) {
// Find the slide with the specified ID
- element = document.querySelector( '#' + name );
+ element = document.getElementById( name );
}
if( element ) {
@@ -2790,7 +3202,6 @@
// Attempt to create a named link based on the slide's ID
var id = currentSlide.getAttribute( 'id' );
if( id ) {
- id = id.toLowerCase();
id = id.replace( /[^a-zA-Z0-9\-\_\:\.]/g, '' );
}
@@ -2850,8 +3261,13 @@
if( !slide && currentSlide ) {
var hasFragments = currentSlide.querySelectorAll( '.fragment' ).length > 0;
if( hasFragments ) {
- var visibleFragments = currentSlide.querySelectorAll( '.fragment.visible' );
- f = visibleFragments.length - 1;
+ var currentFragment = currentSlide.querySelector( '.current-fragment' );
+ if( currentFragment && currentFragment.hasAttribute( 'data-fragment-index' ) ) {
+ f = parseInt( currentFragment.getAttribute( 'data-fragment-index' ), 10 );
+ }
+ else {
+ f = currentSlide.querySelectorAll( '.fragment.visible' ).length - 1;
+ }
}
}
@@ -2918,6 +3334,32 @@
}
/**
+ * Retrieves the speaker notes from a slide. Notes can be
+ * defined in two ways:
+ * 1. As a data-notes attribute on the slide <section>
+ * 2. As an <aside class="notes"> inside of the slide
+ */
+ function getSlideNotes( slide ) {
+
+ // Default to the current slide
+ slide = slide || currentSlide;
+
+ // Notes can be specified via the data-notes attribute...
+ if( slide.hasAttribute( 'data-notes' ) ) {
+ return slide.getAttribute( 'data-notes' );
+ }
+
+ // ... or using an <aside class="notes"> element
+ var notesElement = slide.querySelector( 'aside.notes' );
+ if( notesElement ) {
+ return notesElement.innerHTML;
+ }
+
+ return null;
+
+ }
+
+ /**
* Retrieves the current state of the presentation as
* an object. This state can then be restored at any
* time.
@@ -3072,7 +3514,7 @@
element.classList.remove( 'current-fragment' );
// Announce the fragments one by one to the Screen Reader
- dom.statusDiv.innerHTML = element.textContent;
+ dom.statusDiv.textContent = element.textContent;
if( i === index ) {
element.classList.add( 'current-fragment' );
@@ -3168,14 +3610,17 @@
// If there are media elements with data-autoplay,
// automatically set the autoSlide duration to the
- // length of that media
- toArray( currentSlide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
- if( el.hasAttribute( 'data-autoplay' ) ) {
- if( autoSlide && el.duration * 1000 > autoSlide ) {
- autoSlide = ( el.duration * 1000 ) + 1000;
+ // length of that media. Not applicable if the slide
+ // is divided up into fragments.
+ if( currentSlide.querySelectorAll( '.fragment' ).length === 0 ) {
+ toArray( currentSlide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
+ if( el.hasAttribute( 'data-autoplay' ) ) {
+ if( autoSlide && el.duration * 1000 > autoSlide ) {
+ autoSlide = ( el.duration * 1000 ) + 1000;
+ }
}
- }
- } );
+ } );
+ }
// Cue the next auto-slide if:
// - There is an autoSlide value
@@ -3183,7 +3628,7 @@
// - The presentation isn't paused
// - The overview isn't active
// - The presentation isn't over
- if( autoSlide && !autoSlidePaused && !isPaused() && !isOverview() && ( !Reveal.isLastSlide() || config.loop === true ) ) {
+ if( autoSlide && !autoSlidePaused && !isPaused() && !isOverview() && ( !Reveal.isLastSlide() || availableFragments().next || config.loop === true ) ) {
autoSlideTimeout = setTimeout( navigateNext, autoSlide );
autoSlideStartTime = Date.now();
}
@@ -3293,7 +3738,14 @@
}
else {
// Fetch the previous horizontal slide, if there is one
- var previousSlide = dom.wrapper.querySelector( HORIZONTAL_SLIDES_SELECTOR + '.past:nth-child(' + indexh + ')' );
+ var previousSlide;
+
+ if( config.rtl ) {
+ previousSlide = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.future' ) ).pop();
+ }
+ else {
+ previousSlide = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.past' ) ).pop();
+ }
if( previousSlide ) {
var v = ( previousSlide.querySelectorAll( 'section' ).length - 1 ) || undefined;
@@ -3306,13 +3758,21 @@
}
/**
- * Same as #navigatePrev() but navigates forwards.
+ * The reverse of #navigatePrev().
*/
function navigateNext() {
// Prioritize revealing fragments
if( nextFragment() === false ) {
- availableRoutes().down ? navigateDown() : navigateRight();
+ if( availableRoutes().down ) {
+ navigateDown();
+ }
+ else if( config.rtl ) {
+ navigateLeft();
+ }
+ else {
+ navigateRight();
+ }
}
// If auto-sliding is enabled we need to cue up
@@ -3380,8 +3840,20 @@
// keyboard modifier key is present
if( activeElementIsCE || activeElementIsInput || (event.shiftKey && event.keyCode !== 32) || event.altKey || event.ctrlKey || event.metaKey ) return;
- // While paused only allow "unpausing" keyboard events (b and .)
- if( isPaused() && [66,190,191].indexOf( event.keyCode ) === -1 ) {
+ // While paused only allow resume keyboard events; 'b', '.''
+ var resumeKeyCodes = [66,190,191];
+ var key;
+
+ // Custom key bindings for togglePause should be able to resume
+ if( typeof config.keyboard === 'object' ) {
+ for( key in config.keyboard ) {
+ if( config.keyboard[key] === 'togglePause' ) {
+ resumeKeyCodes.push( parseInt( key, 10 ) );
+ }
+ }
+ }
+
+ if( isPaused() && resumeKeyCodes.indexOf( event.keyCode ) === -1 ) {
return false;
}
@@ -3390,7 +3862,7 @@
// 1. User defined key bindings
if( typeof config.keyboard === 'object' ) {
- for( var key in config.keyboard ) {
+ for( key in config.keyboard ) {
// Check if this binding matches the pressed key
if( parseInt( key, 10 ) === event.keyCode ) {
@@ -3673,6 +4145,10 @@
var slidesTotal = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).length;
var slideIndex = Math.floor( ( event.clientX / dom.wrapper.offsetWidth ) * slidesTotal );
+ if( config.rtl ) {
+ slideIndex = slidesTotal - slideIndex;
+ }
+
slide( slideIndex );
}
@@ -3717,7 +4193,10 @@
// If, after clicking a link or similar and we're coming back,
// focus the document.body to ensure we can use keyboard shortcuts
if( isHidden === false && document.activeElement !== document.body ) {
- document.activeElement.blur();
+ // Not all elements support .blur() - SVGs among them.
+ if( typeof document.activeElement.blur === 'function' ) {
+ document.activeElement.blur();
+ }
document.body.focus();
}
@@ -3761,10 +4240,12 @@
*/
function onPreviewLinkClicked( event ) {
- var url = event.target.getAttribute( 'href' );
- if( url ) {
- showPreview( url );
- event.preventDefault();
+ if( event.currentTarget && event.currentTarget.hasAttribute( 'href' ) ) {
+ var url = event.currentTarget.getAttribute( 'href' );
+ if( url ) {
+ showPreview( url );
+ event.preventDefault();
+ }
}
}
@@ -4042,6 +4523,9 @@
// Returns the slide background element at the specified index
getSlideBackground: getSlideBackground,
+ // Returns the speaker notes string for a slide, or null
+ getSlideNotes: getSlideNotes,
+
// Returns the previous slide element, may be null
getPreviousSlide: function() {
return previousSlide;