diff options
Diffstat (limited to 'js/reveal.js')
-rw-r--r-- | js/reveal.js | 480 |
1 files changed, 423 insertions, 57 deletions
diff --git a/js/reveal.js b/js/reveal.js index 794911c..1cbfa34 100644 --- a/js/reveal.js +++ b/js/reveal.js @@ -68,6 +68,9 @@ var Reveal = (function(){ // by using a data-autoslide attribute on your slides autoSlide: 0, + // Stop auto-sliding after user input + autoSlideStoppable: true, + // Enable slide navigation via mouse wheel mouseWheel: false, @@ -80,6 +83,9 @@ var Reveal = (function(){ // Opens links in an iframe preview overlay previewLinks: false, + // Focuses body when page changes visiblity to ensure keyboard shortcuts work + focusBodyOnPageVisiblityChange: true, + // Theme (see /css/theme) theme: null, @@ -92,6 +98,12 @@ var Reveal = (function(){ // Transition style for full page slide backgrounds backgroundTransition: 'default', // default/linear/none + // Parallax background image + parallaxBackgroundImage: '', // CSS syntax, e.g. "a.jpg" + + // Parallax background size + parallaxBackgroundSize: '', // CSS syntax, e.g. "3000px 2000px" + // Number of slides away from the current that are visible viewDistance: 3, @@ -102,9 +114,6 @@ var Reveal = (function(){ // Flags if reveal.js is loaded (has dispatched the 'ready' event) loaded = false, - // The current auto-slide duration - autoSlide = 0, - // The horizontal and vertical index of the currently active slide indexh, indexv, @@ -124,11 +133,8 @@ var Reveal = (function(){ // Cached references to DOM elements dom = {}, - // Client support for CSS 3D transforms, see #checkCapabilities() - supports3DTransforms, - - // Client support for CSS 2D transforms, see #checkCapabilities() - supports2DTransforms, + // Features supported by the browser, see #checkCapabilities() + features = {}, // Client is a mobile device, see #checkCapabilities() isMobileDevice, @@ -136,9 +142,6 @@ var Reveal = (function(){ // Throttles mouse wheel navigation lastMouseWheelStep = 0, - // An interval used to automatically move on to the next slide - autoSlideTimeout = 0, - // Delays updates to the URL due to a Chrome thumbnailer bug writeURLTimeout = 0, @@ -151,6 +154,15 @@ var Reveal = (function(){ // Flags if the interaction event listeners are bound eventsAreBound = false, + // The current auto-slide duration + autoSlide = 0, + + // Auto slide properties + autoSlidePlayer, + autoSlideTimeout = 0, + autoSlideStartTime = -1, + autoSlidePaused = false, + // Holds information about the currently ongoing touch input touch = { startX: 0, @@ -168,7 +180,7 @@ var Reveal = (function(){ checkCapabilities(); - if( !supports2DTransforms && !supports3DTransforms ) { + if( !features.transforms2d && !features.transforms3d ) { document.body.setAttribute( 'class', 'no-transforms' ); // If the browser doesn't support core features we won't be @@ -181,6 +193,7 @@ var Reveal = (function(){ // Copy options over to our config object extend( config, options ); + extend( config, Reveal.getQueryHash() ); // Hide the address bar in mobile browsers hideAddressBar(); @@ -196,18 +209,23 @@ var Reveal = (function(){ */ function checkCapabilities() { - supports3DTransforms = 'WebkitPerspective' in document.body.style || + features.transforms3d = 'WebkitPerspective' in document.body.style || 'MozPerspective' in document.body.style || 'msPerspective' in document.body.style || 'OPerspective' in document.body.style || 'perspective' in document.body.style; - supports2DTransforms = 'WebkitTransform' in document.body.style || + features.transforms2d = 'WebkitTransform' in document.body.style || 'MozTransform' in document.body.style || 'msTransform' in document.body.style || 'OTransform' in document.body.style || 'transform' in document.body.style; + features.requestAnimationFrameMethod = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame; + features.requestAnimationFrame = typeof features.requestAnimationFrameMethod === 'function'; + + features.canvas = !!document.createElement( 'canvas' ).getContext; + isMobileDevice = navigator.userAgent.match( /(iphone|ipod|android)/gi ); } @@ -422,7 +440,7 @@ var Reveal = (function(){ if( data.background ) { // Auto-wrap image urls in url(...) - if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(png|jpg|jpeg|gif|bmp)$/gi.test( data.background ) ) { + if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp)$/gi.test( data.background ) ) { element.style.backgroundImage = 'url('+ data.background +')'; } else { @@ -470,6 +488,28 @@ var Reveal = (function(){ } ); + // Add parallax background if specified + if( config.parallaxBackgroundImage ) { + + dom.background.style.backgroundImage = 'url("' + config.parallaxBackgroundImage + '")'; + dom.background.style.backgroundSize = config.parallaxBackgroundSize; + + // Make sure the below properties are set on the element - these properties are + // needed for proper transitions to be set on the element via CSS. To remove + // annoying background slide-in effect when the presentation starts, apply + // these properties after short time delay + setTimeout( function() { + dom.wrapper.classList.add( 'has-parallax-background' ); + }, 1 ); + + } + else { + + dom.background.style.backgroundImage = ''; + dom.wrapper.classList.remove( 'has-parallax-background' ); + + } + } /** @@ -478,6 +518,8 @@ var Reveal = (function(){ */ function configure( options ) { + var numberOfSlides = document.querySelectorAll( SLIDES_SELECTOR ).length; + dom.wrapper.classList.remove( config.transition ); // New config options may be passed when this method @@ -485,7 +527,7 @@ var Reveal = (function(){ if( typeof options === 'object' ) extend( config, options ); // Force linear transition based on browser capabilities - if( supports3DTransforms === false ) config.transition = 'linear'; + if( features.transforms3d === false ) config.transition = 'linear'; dom.wrapper.classList.add( config.transition ); @@ -535,6 +577,20 @@ var Reveal = (function(){ enablePreviewLinks( '[data-preview-link]' ); } + // Auto-slide playback controls + if( numberOfSlides > 1 && config.autoSlide && config.autoSlideStoppable && features.canvas && features.requestAnimationFrame ) { + autoSlidePlayer = new Playback( dom.wrapper, function() { + return Math.min( Math.max( ( Date.now() - autoSlideStartTime ) / autoSlide, 0 ), 1 ); + } ); + + autoSlidePlayer.on( 'click', onAutoSlidePlayerClick ); + autoSlidePaused = false; + } + else if( autoSlidePlayer ) { + autoSlidePlayer.destroy(); + autoSlidePlayer = null; + } + // Load the theme in the config, if it's not already loaded if( config.theme && dom.theme ) { var themeURL = dom.theme.getAttribute( 'href' ); @@ -578,10 +634,28 @@ var Reveal = (function(){ document.addEventListener( 'keydown', onDocumentKeyDown, false ); } - if ( config.progress && dom.progress ) { + if( config.progress && dom.progress ) { dom.progress.addEventListener( 'click', onProgressClicked, false ); } + if( config.focusBodyOnPageVisiblityChange ) { + var visibilityChange; + + if( 'hidden' in document ) { + visibilityChange = 'visibilitychange'; + } + else if( 'msHidden' in document ) { + visibilityChange = 'msvisibilitychange'; + } + else if( 'webkitHidden' in document ) { + visibilityChange = 'webkitvisibilitychange'; + } + + if( visibilityChange ) { + document.addEventListener( visibilityChange, onPageVisibilityChange, false ); + } + } + [ 'touchstart', 'click' ].forEach( function( eventName ) { dom.controlsLeft.forEach( function( el ) { el.addEventListener( eventName, onNavigateLeftClicked, false ); } ); dom.controlsRight.forEach( function( el ) { el.addEventListener( eventName, onNavigateRightClicked, false ); } ); @@ -784,16 +858,6 @@ var Reveal = (function(){ */ function removeAddressBar() { - // Portrait and not Chrome for iOS - if( window.orientation === 0 && !/crios/gi.test( navigator.userAgent ) ) { - document.documentElement.style.overflow = 'scroll'; - document.body.style.height = '120%'; - } - else { - document.documentElement.style.overflow = ''; - document.body.style.height = '100%'; - } - setTimeout( function() { window.scrollTo( 0, 1 ); }, 10 ); @@ -818,7 +882,7 @@ var Reveal = (function(){ */ function enableRollingLinks() { - if( supports3DTransforms && !( 'msPerspective' in document.body.style ) ) { + if( features.transforms3d && !( 'msPerspective' in document.body.style ) ) { var anchors = document.querySelectorAll( SLIDES_SELECTOR + ' a:not(.image)' ); for( var i = 0, len = anchors.length; i < len; i++ ) { @@ -1055,6 +1119,7 @@ var Reveal = (function(){ } updateProgress(); + updateParallax(); } @@ -1471,7 +1536,6 @@ var Reveal = (function(){ // Store references to the previous and current slides currentSlide = currentVerticalSlides[ indexv ] || currentHorizontalSlide; - // Show fragment, if specified if( typeof f !== 'undefined' ) { var fragments = sortFragments( currentSlide.querySelectorAll( '.fragment' ) ); @@ -1533,10 +1597,13 @@ var Reveal = (function(){ updateControls(); updateProgress(); updateBackground(); + updateParallax(); // Update the URL hash writeURL(); + cueAutoSlide(); + } /** @@ -1647,18 +1714,6 @@ var Reveal = (function(){ state = state.concat( slideState.split( ' ' ) ); } - // If this slide has a data-autoslide attribute associated use this as - // autoSlide value otherwise use the global configured time - var slideAutoSlide = slides[index].getAttribute( 'data-autoslide' ); - if( slideAutoSlide ) { - autoSlide = parseInt( slideAutoSlide, 10 ); - } - else { - autoSlide = config.autoSlide; - } - - cueAutoSlide(); - } else { // Since there are no slides we can't be anywhere beyond the @@ -1825,12 +1880,12 @@ var Reveal = (function(){ } /** - * Updates the background elements to reflect the current + * Updates the background elements to reflect the current * slide. */ function updateBackground() { - // Update the classes of all backgrounds to match the + // Update the classes of all backgrounds to match the // states of their slides (past/present/future) toArray( dom.background.childNodes ).forEach( function( backgroundh, h ) { @@ -1856,6 +1911,42 @@ var Reveal = (function(){ } /** + * Updates the position of the parallax background based + * on the current slide index. + */ + function updateParallax() { + + if( config.parallaxBackgroundImage ) { + + var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ), + verticalSlides = document.querySelectorAll( VERTICAL_SLIDES_SELECTOR ); + + var backgroundSize = dom.background.style.backgroundSize.split( ' ' ), + backgroundWidth, backgroundHeight; + + if( backgroundSize.length === 1 ) { + backgroundWidth = backgroundHeight = parseInt( backgroundSize[0], 10 ); + } + else { + backgroundWidth = parseInt( backgroundSize[0], 10 ); + backgroundHeight = parseInt( backgroundSize[1], 10 ); + } + + var slideWidth = dom.background.offsetWidth; + var horizontalSlideCount = horizontalSlides.length; + var horizontalOffset = -( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 ) * indexh; + + var slideHeight = dom.background.offsetHeight; + var verticalSlideCount = verticalSlides.length; + var verticalOffset = verticalSlideCount > 0 ? -( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 ) * indexv : 0; + + dom.background.style.backgroundPosition = horizontalOffset + 'px ' + verticalOffset + 'px'; + + } + + } + + /** * Determine what available routes there are for navigation. * * @return {Object} containing four booleans: left/right/up/down @@ -2149,11 +2240,35 @@ var Reveal = (function(){ */ function cueAutoSlide() { - clearTimeout( autoSlideTimeout ); + cancelAutoSlide(); + + if( currentSlide ) { + + // If the current slide has a data-autoslide use that, + // otherwise use the config.autoSlide value + var slideAutoSlide = currentSlide.getAttribute( 'data-autoslide' ); + if( slideAutoSlide ) { + autoSlide = parseInt( slideAutoSlide, 10 ); + } + else { + autoSlide = config.autoSlide; + } + + // Cue the next auto-slide if: + // - There is an autoSlide value + // - Auto-sliding isn't paused by the user + // - 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 ) ) { + autoSlideTimeout = setTimeout( navigateNext, autoSlide ); + autoSlideStartTime = Date.now(); + } + + if( autoSlidePlayer ) { + autoSlidePlayer.setPlaying( autoSlideTimeout !== -1 ); + } - // Cue the next auto-slide if enabled - if( autoSlide && !isPaused() && !isOverview() ) { - autoSlideTimeout = setTimeout( navigateNext, autoSlide ); } } @@ -2164,6 +2279,25 @@ var Reveal = (function(){ function cancelAutoSlide() { clearTimeout( autoSlideTimeout ); + autoSlideTimeout = -1; + + } + + function pauseAutoSlide() { + + autoSlidePaused = true; + clearTimeout( autoSlideTimeout ); + + if( autoSlidePlayer ) { + autoSlidePlayer.setPlaying( false ); + } + + } + + function resumeAutoSlide() { + + autoSlidePaused = false; + cueAutoSlide(); } @@ -2263,14 +2397,25 @@ var Reveal = (function(){ // ----------------------------- EVENTS -------------------------------// // --------------------------------------------------------------------// + /** + * Called by all event handlers that are based on user + * input. + */ + function onUserInput( event ) { + + if( config.autoSlideStoppable ) { + pauseAutoSlide(); + } + + } /** * Handler for the document level 'keydown' event. - * - * @param {Object} event */ function onDocumentKeyDown( event ) { + onUserInput( event ); + // Check if there's a focused element that could be using // the keyboard var activeElement = document.activeElement; @@ -2357,8 +2502,13 @@ var Reveal = (function(){ event.preventDefault(); } // ESC or O key - else if ( ( event.keyCode === 27 || event.keyCode === 79 ) && supports3DTransforms ) { - toggleOverview(); + else if ( ( event.keyCode === 27 || event.keyCode === 79 ) && features.transforms3d ) { + if( dom.preview ) { + closePreview(); + } + else { + toggleOverview(); + } event.preventDefault(); } @@ -2400,6 +2550,8 @@ var Reveal = (function(){ // Each touch should only trigger one action if( !touch.captured ) { + onUserInput( event ); + var currentX = event.touches[0].clientX; var currentY = event.touches[0].clientY; @@ -2553,6 +2705,8 @@ var Reveal = (function(){ */ function onProgressClicked( event ) { + onUserInput( event ); + event.preventDefault(); var slidesTotal = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).length; @@ -2565,12 +2719,12 @@ var Reveal = (function(){ /** * Event handler for navigation control buttons. */ - function onNavigateLeftClicked( event ) { event.preventDefault(); navigateLeft(); } - function onNavigateRightClicked( event ) { event.preventDefault(); navigateRight(); } - function onNavigateUpClicked( event ) { event.preventDefault(); navigateUp(); } - function onNavigateDownClicked( event ) { event.preventDefault(); navigateDown(); } - function onNavigatePrevClicked( event ) { event.preventDefault(); navigatePrev(); } - function onNavigateNextClicked( event ) { event.preventDefault(); navigateNext(); } + function onNavigateLeftClicked( event ) { event.preventDefault(); onUserInput(); navigateLeft(); } + function onNavigateRightClicked( event ) { event.preventDefault(); onUserInput(); navigateRight(); } + function onNavigateUpClicked( event ) { event.preventDefault(); onUserInput(); navigateUp(); } + function onNavigateDownClicked( event ) { event.preventDefault(); onUserInput(); navigateDown(); } + function onNavigatePrevClicked( event ) { event.preventDefault(); onUserInput(); navigatePrev(); } + function onNavigateNextClicked( event ) { event.preventDefault(); onUserInput(); navigateNext(); } /** * Handler for the window level 'hashchange' event. @@ -2591,6 +2745,24 @@ var Reveal = (function(){ } /** + * Handle for the window level 'visibilitychange' event. + */ + function onPageVisibilityChange( event ) { + + var isHidden = document.webkitHidden || + document.msHidden || + document.hidden; + + // 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(); + document.body.focus(); + } + + } + + /** * Invoked when a slide is and we're in the overview. */ function onOverviewSlideClicked( event ) { @@ -2636,6 +2808,191 @@ var Reveal = (function(){ } + /** + * Handles click on the auto-sliding controls element. + */ + function onAutoSlidePlayerClick( event ) { + + // Replay + if( Reveal.isLastSlide() && config.loop === false ) { + slide( 0, 0 ); + resumeAutoSlide(); + } + // Resume + else if( autoSlidePaused ) { + resumeAutoSlide(); + } + // Pause + else { + pauseAutoSlide(); + } + + } + + + // --------------------------------------------------------------------// + // ------------------------ PLAYBACK COMPONENT ------------------------// + // --------------------------------------------------------------------// + + + /** + * Constructor for the playback component, which displays + * play/pause/progress controls. + * + * @param {HTMLElement} container The component will append + * itself to this + * @param {Function} progressCheck A method which will be + * called frequently to get the current progress on a range + * of 0-1 + */ + function Playback( container, progressCheck ) { + + // Cosmetics + this.diameter = 50; + this.thickness = 3; + + // Flags if we are currently playing + this.playing = false; + + // Current progress on a 0-1 range + this.progress = 0; + + // Used to loop the animation smoothly + this.progressOffset = 1; + + this.container = container; + this.progressCheck = progressCheck; + + this.canvas = document.createElement( 'canvas' ); + this.canvas.className = 'playback'; + this.canvas.width = this.diameter; + this.canvas.height = this.diameter; + this.context = this.canvas.getContext( '2d' ); + + this.container.appendChild( this.canvas ); + + this.render(); + + } + + Playback.prototype.setPlaying = function( value ) { + + var wasPlaying = this.playing; + + this.playing = value; + + // Start repainting if we weren't already + if( !wasPlaying && this.playing ) { + this.animate(); + } + else { + this.render(); + } + + }; + + Playback.prototype.animate = function() { + + var progressBefore = this.progress; + + this.progress = this.progressCheck(); + + // When we loop, offset the progress so that it eases + // smoothly rather than immediately resetting + if( progressBefore > 0.8 && this.progress < 0.2 ) { + this.progressOffset = this.progress; + } + + this.render(); + + if( this.playing ) { + features.requestAnimationFrameMethod.call( window, this.animate.bind( this ) ); + } + + }; + + /** + * Renders the current progress and playback state. + */ + 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; + + // Ease towards 1 + this.progressOffset += ( 1 - this.progressOffset ) * 0.1; + + var endAngle = ( - Math.PI / 2 ) + ( progress * ( Math.PI * 2 ) ); + var startAngle = ( - Math.PI / 2 ) + ( this.progressOffset * ( Math.PI * 2 ) ); + + this.context.save(); + this.context.clearRect( 0, 0, this.diameter, this.diameter ); + + // Solid background color + this.context.beginPath(); + this.context.arc( x, y, radius + 2, 0, Math.PI * 2, false ); + this.context.fillStyle = 'rgba( 0, 0, 0, 0.4 )'; + this.context.fill(); + + // Draw progress track + this.context.beginPath(); + this.context.arc( x, y, radius, 0, Math.PI * 2, false ); + this.context.lineWidth = this.thickness; + this.context.strokeStyle = '#666'; + this.context.stroke(); + + if( this.playing ) { + // Draw progress on top of track + this.context.beginPath(); + this.context.arc( x, y, radius, startAngle, endAngle, false ); + this.context.lineWidth = this.thickness; + this.context.strokeStyle = '#fff'; + this.context.stroke(); + } + + this.context.translate( x - ( iconSize / 2 ), y - ( iconSize / 2 ) ); + + // 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 ); + } + else { + this.context.beginPath(); + this.context.translate( 2, 0 ); + this.context.moveTo( 0, 0 ); + this.context.lineTo( iconSize - 2, iconSize / 2 ); + this.context.lineTo( 0, iconSize ); + this.context.fillStyle = '#fff'; + this.context.fill(); + } + + this.context.restore(); + + }; + + Playback.prototype.on = function( type, listener ) { + this.canvas.addEventListener( type, listener, false ); + }; + + Playback.prototype.off = function( type, listener ) { + this.canvas.removeEventListener( type, listener, false ); + }; + + Playback.prototype.destroy = function() { + + this.playing = false; + + if( this.canvas.parentNode ) { + this.container.removeChild( this.canvas ); + } + + }; + // --------------------------------------------------------------------// // ------------------------------- API --------------------------------// @@ -2733,6 +3090,15 @@ var Reveal = (function(){ query[ a.split( '=' ).shift() ] = a.split( '=' ).pop(); } ); + // Basic deserialization + for( var i in query ) { + var value = query[ i ]; + if( value === 'null' ) query[ i ] = null; + else if( value === 'true' ) query[ i ] = true; + else if( value === 'false' ) query[ i ] = false; + else if( !isNaN( parseFloat( value ) ) ) query[ i ] = parseFloat( value ); + } + return query; }, |