diff options
Diffstat (limited to 'js')
-rw-r--r-- | js/reveal.js | 754 |
1 files changed, 589 insertions, 165 deletions
diff --git a/js/reveal.js b/js/reveal.js index 421fa2c..e1d80b9 100644 --- a/js/reveal.js +++ b/js/reveal.js @@ -3,7 +3,7 @@ * http://lab.hakim.se/reveal-js * MIT licensed * - * Copyright (C) 2015 Hakim El Hattab, http://hakim.se + * Copyright (C) 2016 Hakim El Hattab, http://hakim.se */ (function( root, factory ) { if( typeof define === 'function' && define.amd ) { @@ -25,12 +25,15 @@ var Reveal; + // The reveal.js version + var VERSION = '3.2.0'; + var SLIDES_SELECTOR = '.slides section', HORIZONTAL_SLIDES_SELECTOR = '.slides>section', 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 @@ -92,6 +95,9 @@ // 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 @@ -100,6 +106,9 @@ // Stop auto-sliding after user input autoSlideStoppable: true, + // Use this method for navigation when auto-sliding (defaults to navigateNext) + autoSlideMethod: null, + // Enable slide navigation via mouse wheel mouseWheel: false, @@ -136,6 +145,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 +160,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 +181,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 +247,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' ); } } @@ -293,7 +317,11 @@ features.touch = !!( 'ontouchstart' in window ); - isMobileDevice = navigator.userAgent.match( /(iphone|ipod|ipad|android)/gi ); + // 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 ); } @@ -373,6 +401,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(); @@ -435,14 +466,18 @@ // 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', '' ); + // Element containing notes that are visible to the audience + dom.speakerNotes = createSingletonNode( dom.wrapper, 'div', 'speaker-notes', null ); + dom.speakerNotes.setAttribute( 'data-prevent-swipe', '' ); + // Overlay graphic which is displayed during the paused mode createSingletonNode( dom.wrapper, 'div', 'pause-overlay', null ); @@ -513,6 +548,19 @@ document.body.style.width = pageWidth + 'px'; document.body.style.height = pageHeight + 'px'; + // Add each slide's index as attributes on itself, we need these + // indices to generate slide numbers below + toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( hslide, h ) { + hslide.setAttribute( 'data-index-h', h ); + + if( hslide.classList.contains( 'stack' ) ) { + toArray( hslide.querySelectorAll( 'section' ) ).forEach( function( vslide, v ) { + vslide.setAttribute( 'data-index-h', h ); + vslide.setAttribute( 'data-index-v', v ); + } ); + } + } ); + // Slide and slide background layout toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) { @@ -545,6 +593,34 @@ background.style.top = -top + 'px'; background.style.left = -left + 'px'; } + + // Inject notes if `showNotes` is enabled + if( config.showNotes ) { + var notes = getSlideNotes( slide ); + if( notes ) { + var notesSpacing = 8; + var notesElement = document.createElement( 'div' ); + notesElement.classList.add( 'speaker-notes' ); + notesElement.classList.add( 'speaker-notes-pdf' ); + notesElement.innerHTML = notes; + notesElement.style.left = ( notesSpacing - left ) + 'px'; + notesElement.style.bottom = ( notesSpacing - top ) + 'px'; + notesElement.style.width = ( pageWidth - notesSpacing*2 ) + 'px'; + slide.appendChild( notesElement ); + } + } + + // Inject slide numbers if `slideNumbers` are enabled + if( config.slideNumber ) { + var slideNumberH = parseInt( slide.getAttribute( 'data-index-h' ), 10 ) + 1, + slideNumberV = parseInt( slide.getAttribute( 'data-index-v' ), 10 ) + 1; + + var numberElement = document.createElement( 'div' ); + numberElement.classList.add( 'slide-number' ); + numberElement.classList.add( 'slide-number-pdf' ); + numberElement.innerHTML = formatSlideNumber( slideNumberH, '.', slideNumberV ); + background.appendChild( numberElement ); + } } } ); @@ -557,6 +633,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. @@ -757,7 +853,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 @@ -794,6 +890,7 @@ dom.controls.style.display = config.controls ? 'block' : 'none'; dom.progress.style.display = config.progress ? 'block' : 'none'; + dom.slideNumber.style.display = config.slideNumber && !isPrintingPDF() ? 'block' : 'none'; if( config.rtl ) { dom.wrapper.classList.add( 'rtl' ); @@ -814,6 +911,13 @@ 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 ); @@ -1051,12 +1155,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 ) { @@ -1074,7 +1198,7 @@ } /** - * Measures the distance in pixels between point a and point b. + * 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: @@ -1465,20 +1589,28 @@ dom.slides.style.top = ''; dom.slides.style.bottom = ''; dom.slides.style.right = ''; - transformElement( dom.slides, '' ); + transformSlides( { layout: '' } ); } else { - // Prefer zooming in desktop Chrome so that content remains crisp - if( !isMobileDevice && /chrome/i.test( navigator.userAgent ) && typeof dom.slides.style.zoom !== 'undefined' ) { + // 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'; - transformElement( dom.slides, 'translate(-50%, -50%) scale('+ scale +')' ); + transformSlides( { layout: 'translate(-50%, -50%) scale('+ scale +')' } ); } } @@ -1566,7 +1698,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 @@ -1620,82 +1752,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( ' ' ) + } ); + } /** @@ -1707,7 +1879,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 @@ -1718,16 +1893,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 @@ -1766,7 +1952,7 @@ */ function isOverview() { - return dom.wrapper.classList.contains( 'overview' ); + return overview; } @@ -1916,7 +2102,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 ] ); } @@ -1966,9 +2152,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 @@ -2037,6 +2223,7 @@ updateBackground(); updateParallax(); updateSlideNumber(); + updateNotes(); // Update the URL hash writeURL(); @@ -2078,8 +2265,14 @@ updateBackground( true ); updateSlideNumber(); updateSlidesVisibility(); + updateNotes(); formatEmbeddedContent(); + startEmbeddedContent( currentSlide ); + + if( isOverview() ) { + layoutOverview(); + } } @@ -2269,7 +2462,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; } @@ -2280,8 +2473,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 || 0 ) - 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 ) { @@ -2316,6 +2515,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() { @@ -2331,19 +2546,60 @@ /** * Updates the slide number div to reflect the current slide. + * + * The following slide number formats are available: + * "h.v": horizontal . vertical slide number (default) + * "h/v": horizontal / vertical slide number + * "c": flattened slide number + * "c/t": flattened slide number / total slides */ function updateSlideNumber() { // Update slide number if enabled - if( config.slideNumber && dom.slideNumber) { + if( config.slideNumber && dom.slideNumber ) { + + var value = []; + var format = 'h.v'; - // Display the number of the page using 'indexh - indexv' format - var indexString = indexh; - if( indexv > 0 ) { - indexString += ' - ' + indexv; + // Check if a custom number format is available + if( typeof config.slideNumber === 'string' ) { + format = config.slideNumber; } - dom.slideNumber.innerHTML = indexString; + switch( format ) { + case 'c': + value.push( getSlidePastCount() + 1 ); + break; + case 'c/t': + value.push( getSlidePastCount() + 1, '/', getTotalSlides() ); + break; + case 'h/v': + value.push( indexh + 1 ); + if( isVerticalSlide() ) value.push( '/', indexv + 1 ); + break; + default: + value.push( indexh + 1 ); + if( isVerticalSlide() ) value.push( '.', indexv + 1 ); + } + + dom.slideNumber.innerHTML = formatSlideNumber( value[0], value[1], value[2] ); + } + + } + + /** + * Applies HTML formatting to a slide number before it's + * written to the DOM. + */ + function formatSlideNumber( a, delimiter, b ) { + + if( typeof b === 'number' && !isNaN( b ) ) { + return '<span class="slide-number-a">'+ a +'</span>' + + '<span class="slide-number-delimiter">'+ delimiter +'</span>' + + '<span class="slide-number-b">'+ b +'</span>'; + } + else { + return '<span class="slide-number-a">'+ a +'</span>'; } } @@ -2472,17 +2728,29 @@ // Start video playback var currentVideo = currentBackground.querySelector( 'video' ); if( currentVideo ) { - if(currentVideo.readyState > 1){ + + var startVideo = function() { currentVideo.currentTime = 0; currentVideo.play(); + currentVideo.removeEventListener( 'loadeddata', startVideo ); + }; + + if( currentVideo.readyState > 1 ) { + startVideo(); } - else{ - currentVideo.addEventListener("loadeddata", function() { - currentVideo.currentTime = 0; - currentVideo.play(); - currentVideo.removeEventListener("loadeddata",function(){return false}); - }); + else { + currentVideo.addEventListener( 'loadeddata', startVideo ); } + + } + + 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 @@ -2539,15 +2807,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; + + if( typeof config.parallaxBackgroundHorizontal === 'number' ) { + horizontalOffsetMultiplier = config.parallaxBackgroundHorizontal; + } + else { + horizontalOffsetMultiplier = horizontalSlideCount > 1 ? ( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 ) : 0; + } + + horizontalOffset = horizontalOffsetMultiplier * indexh * -1; - var slideHeight = dom.background.offsetHeight; - var verticalSlideCount = verticalSlides.length; - var verticalOffset = verticalSlideCount > 1 ? -( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 ) * indexv : 0; + var slideHeight = dom.background.offsetHeight, + verticalSlideCount = verticalSlides.length, + verticalOffsetMultiplier, + verticalOffset; + + if( typeof config.parallaxBackgroundVertical === 'number' ) { + verticalOffsetMultiplier = config.parallaxBackgroundVertical; + } + else { + verticalOffsetMultiplier = ( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 ); + } - dom.background.style.backgroundPosition = horizontalOffset + 'px ' + verticalOffset + 'px'; + verticalOffset = verticalSlideCount > 0 ? verticalOffsetMultiplier * indexv * 1 : 0; + + dom.background.style.backgroundPosition = horizontalOffset + 'px ' + -verticalOffset + 'px'; } @@ -2564,7 +2852,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' ); } ); @@ -2599,6 +2887,8 @@ var backgroundImage = slide.getAttribute( 'data-background-image' ), backgroundVideo = slide.getAttribute( 'data-background-video' ), + backgroundVideoLoop = slide.hasAttribute( 'data-background-video-loop' ), + backgroundVideoMuted = slide.hasAttribute( 'data-background-video-muted' ), backgroundIframe = slide.getAttribute( 'data-background-iframe' ); // Images @@ -2609,6 +2899,14 @@ else if ( backgroundVideo && !isSpeakerNotes() ) { var video = document.createElement( 'video' ); + if( backgroundVideoLoop ) { + video.setAttribute( 'loop', '' ); + } + + if( backgroundVideoMuted ) { + video.muted = true; + } + // Support comma separated lists of video sources backgroundVideo.split( ',' ).forEach( function( source ) { video.innerHTML += '<source src="'+ source +'">'; @@ -2617,7 +2915,7 @@ background.appendChild( video ); } // Iframes - else if ( backgroundIframe ) { + else if( backgroundIframe ) { var iframe = document.createElement( 'iframe' ); iframe.setAttribute( 'src', backgroundIframe ); iframe.style.width = '100%'; @@ -2706,21 +3004,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' ); } @@ -2731,31 +3030,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' ) ) { - 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', '*' ); } } @@ -2769,43 +3093,51 @@ 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 = getTotalSlides(); + // The number of past slides var pastCount = 0; // Step through all slides and count the past ones @@ -2837,6 +3169,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' ); @@ -2889,7 +3235,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 ) { @@ -2938,7 +3284,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, '' ); } @@ -3071,6 +3416,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. @@ -3321,14 +3692,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 @@ -3337,7 +3711,10 @@ // - The overview isn't active // - The presentation isn't over if( autoSlide && !autoSlidePaused && !isPaused() && !isOverview() && ( !Reveal.isLastSlide() || availableFragments().next || config.loop === true ) ) { - autoSlideTimeout = setTimeout( navigateNext, autoSlide ); + autoSlideTimeout = setTimeout( function() { + typeof config.autoSlideMethod === 'function' ? config.autoSlideMethod() : navigateNext(); + cueAutoSlide(); + }, autoSlide ); autoSlideStartTime = Date.now(); } @@ -3483,9 +3860,20 @@ } } - // If auto-sliding is enabled we need to cue up - // another timeout - cueAutoSlide(); + } + + /** + * Checks if the target element prevents the triggering of + * swipe navigation. + */ + function isSwipePrevented( target ) { + + while( target && typeof target.hasAttribute === 'function' ) { + if( target.hasAttribute( 'data-prevent-swipe' ) ) return true; + target = target.parentNode; + } + + return false; } @@ -3548,8 +3936,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; } @@ -3558,7 +3958,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 ) { @@ -3650,6 +4050,8 @@ */ function onTouchStart( event ) { + if( isSwipePrevented( event.target ) ) return true; + touch.startX = event.touches[0].clientX; touch.startY = event.touches[0].clientY; touch.startCount = event.touches.length; @@ -3673,6 +4075,8 @@ */ function onTouchMove( event ) { + if( isSwipePrevented( event.target ) ) return true; + // Each touch should only trigger one action if( !touch.captured ) { onUserInput( event ); @@ -3837,6 +4241,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 ); } @@ -3881,7 +4289,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(); } @@ -3975,8 +4386,9 @@ function Playback( container, progressCheck ) { // Cosmetics - this.diameter = 50; - this.thickness = 3; + this.diameter = 100; + this.diameter2 = this.diameter/2; + this.thickness = 6; // Flags if we are currently playing this.playing = false; @@ -3994,6 +4406,8 @@ this.canvas.className = 'playback'; this.canvas.width = this.diameter; this.canvas.height = this.diameter; + this.canvas.style.width = this.diameter2 + 'px'; + this.canvas.style.height = this.diameter2 + 'px'; this.context = this.canvas.getContext( '2d' ); this.container.appendChild( this.canvas ); @@ -4044,10 +4458,10 @@ Playback.prototype.render = function() { var progress = this.playing ? this.progress : 0, - radius = ( this.diameter / 2 ) - this.thickness, - x = this.diameter / 2, - y = this.diameter / 2, - iconSize = 14; + radius = ( this.diameter2 ) - this.thickness, + x = this.diameter2, + y = this.diameter2, + iconSize = 28; // Ease towards 1 this.progressOffset += ( 1 - this.progressOffset ) * 0.1; @@ -4060,7 +4474,7 @@ // Solid background color this.context.beginPath(); - this.context.arc( x, y, radius + 2, 0, Math.PI * 2, false ); + this.context.arc( x, y, radius + 4, 0, Math.PI * 2, false ); this.context.fillStyle = 'rgba( 0, 0, 0, 0.4 )'; this.context.fill(); @@ -4085,14 +4499,14 @@ // Draw play/pause icons if( this.playing ) { this.context.fillStyle = '#fff'; - this.context.fillRect( 0, 0, iconSize / 2 - 2, iconSize ); - this.context.fillRect( iconSize / 2 + 2, 0, iconSize / 2 - 2, iconSize ); + this.context.fillRect( 0, 0, iconSize / 2 - 4, iconSize ); + this.context.fillRect( iconSize / 2 + 4, 0, iconSize / 2 - 4, iconSize ); } else { this.context.beginPath(); - this.context.translate( 2, 0 ); + this.context.translate( 4, 0 ); this.context.moveTo( 0, 0 ); - this.context.lineTo( iconSize - 2, iconSize / 2 ); + this.context.lineTo( iconSize - 4, iconSize / 2 ); this.context.lineTo( 0, iconSize ); this.context.fillStyle = '#fff'; this.context.fill(); @@ -4127,6 +4541,8 @@ Reveal = { + VERSION: VERSION, + initialize: initialize, configure: configure, sync: sync, @@ -4199,6 +4615,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; @@ -4277,6 +4696,11 @@ // Programatically triggers a keyboard event triggerKey: function( keyCode ) { onDocumentKeyDown( { keyCode: keyCode } ); + }, + + // Registers a new shortcut to include in the help overlay + registerKeyboardShortcut: function( key, value ) { + keyboardShortcuts[key] = value; } }; |