reveal.js/js/reveal.js in reveal-ck-0.1.2 vs reveal.js/js/reveal.js in reveal-ck-0.1.3

- old
+ new

@@ -1,11 +1,11 @@ /*! * reveal.js * http://lab.hakim.se/reveal-js * MIT licensed * - * Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se + * Copyright (C) 2013 Hakim El Hattab, http://hakim.se */ var Reveal = (function(){ 'use strict'; @@ -14,10 +14,23 @@ VERTICAL_SLIDES_SELECTOR = '.reveal .slides>section.present>section', HOME_SLIDE_SELECTOR = '.reveal .slides>section:first-child', // Configurations defaults, can be overridden at initialization time config = { + + // The "normal" size of the presentation, aspect ratio will be preserved + // when the presentation is scaled to fit different resolutions + width: 960, + height: 700, + + // Factor of the display size that should remain empty around the content + margin: 0.1, + + // Bounds for smallest/largest possible scale to apply to content + minScale: 0.2, + maxScale: 1.0, + // Display controls in the bottom right corner controls: true, // Display a presentation progress bar progress: true, @@ -29,11 +42,11 @@ keyboard: true, // Enable the slide overview mode overview: true, - // Vertical centering of slides + // Vertical centring of slides center: true, // Enables touch navigation on devices with touch input touch: true, @@ -41,10 +54,13 @@ loop: false, // Change the presentation direction to be RTL rtl: false, + // Turns fragments on and off globally + fragments: true, + // 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 autoSlide: 0, @@ -52,25 +68,33 @@ mouseWheel: false, // Apply a 3D roll to links on hover rollingLinks: true, + // Opens links in an iframe preview overlay + previewLinks: false, + // Theme (see /css/theme) theme: null, // Transition style transition: 'default', // default/cube/page/concave/zoom/linear/fade/none + // Transition speed + transitionSpeed: 'default', // default/fast/slow + + // Transition style for full page slide backgrounds + backgroundTransition: 'default', // default/linear + // Script dependencies to load dependencies: [] }, - // Stores if the next slide should be shown automatically - // after n milliseconds - autoSlide = config.autoSlide, + // The current auto-slide duration + autoSlide = 0, - // The horizontal and verical index of the currently active slide + // The horizontal and vertical index of the currently active slide indexh = 0, indexv = 0, // The previous and current slide HTML elements previousSlide, @@ -79,10 +103,13 @@ // Slides may hold a data-state attribute which we pick up and apply // as a class to the body. This list contains the combined state of // all current slides. state = [], + // The current scale of the presentation (see width/height config) + scale = 1, + // Cached references to DOM elements dom = {}, // Detect support for CSS 3D transforms supports3DTransforms = 'WebkitPerspective' in document.body.style || @@ -97,11 +124,11 @@ 'msTransform' in document.body.style || 'OTransform' in document.body.style || 'transform' in document.body.style, // Throttles mouse wheel navigation - mouseWheelTimeout = 0, + 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 @@ -111,10 +138,13 @@ activateOverviewTimeout = 0, // A delay used to deactivate the overview mode deactivateOverviewTimeout = 0, + // Flags if the interaction event listeners are bound + eventsAreBound = false, + // Holds information about the currently ongoing touch input touch = { startX: 0, startY: 0, startSpan: 0, @@ -126,11 +156,11 @@ /** * Starts up the presentation if the client is capable. */ function initialize( options ) { - if( ( !supports2DTransforms && !supports3DTransforms ) ) { + if( !supports2DTransforms && !supports3DTransforms ) { document.body.setAttribute( 'class', 'no-transforms' ); // If the browser doesn't support core features we won't be // using JavaScript to control the presentation return; @@ -160,34 +190,41 @@ // Cache references to key DOM elements dom.theme = document.querySelector( '#theme' ); dom.wrapper = document.querySelector( '.reveal' ); dom.slides = document.querySelector( '.reveal .slides' ); + // Background element + if( !document.querySelector( '.reveal .backgrounds' ) ) { + dom.background = document.createElement( 'div' ); + dom.background.classList.add( 'backgrounds' ); + dom.wrapper.appendChild( dom.background ); + } + // Progress bar - if( !dom.wrapper.querySelector( '.progress' ) && config.progress ) { + if( !dom.wrapper.querySelector( '.progress' ) ) { var progressElement = document.createElement( 'div' ); progressElement.classList.add( 'progress' ); progressElement.innerHTML = '<span></span>'; dom.wrapper.appendChild( progressElement ); } // Arrow controls - if( !dom.wrapper.querySelector( '.controls' ) && config.controls ) { + if( !dom.wrapper.querySelector( '.controls' ) ) { var controlsElement = document.createElement( 'aside' ); controlsElement.classList.add( 'controls' ); controlsElement.innerHTML = '<div class="navigate-left"></div>' + '<div class="navigate-right"></div>' + '<div class="navigate-up"></div>' + '<div class="navigate-down"></div>'; dom.wrapper.appendChild( controlsElement ); } - // Presentation background element + // State background element [DEPRECATED] if( !dom.wrapper.querySelector( '.state-background' ) ) { - var backgroundElement = document.createElement( 'div' ); - backgroundElement.classList.add( 'state-background' ); - dom.wrapper.appendChild( backgroundElement ); + var stateBackgroundElement = document.createElement( 'div' ); + stateBackgroundElement.classList.add( 'state-background' ); + dom.wrapper.appendChild( stateBackgroundElement ); } // Overlay graphic which is displayed during the paused mode if( !dom.wrapper.querySelector( '.pause-overlay' ) ) { var pausedElement = document.createElement( 'div' ); @@ -212,19 +249,99 @@ } } /** + * Creates the slide background elements and appends them + * to the background container. One element is created per + * slide no matter if the given slide has visible background. + */ + function createBackgrounds() { + + if( isPrintingPDF() ) { + document.body.classList.add( 'print-pdf' ); + } + + // Clear prior backgrounds + dom.background.innerHTML = ''; + dom.background.classList.add( 'no-transition' ); + + // Helper method for creating a background element for the + // given slide + function _createBackground( slide, container ) { + + var data = { + background: slide.getAttribute( 'data-background' ), + backgroundSize: slide.getAttribute( 'data-background-size' ), + backgroundImage: slide.getAttribute( 'data-background-image' ), + backgroundColor: slide.getAttribute( 'data-background-color' ), + backgroundRepeat: slide.getAttribute( 'data-background-repeat' ), + backgroundPosition: slide.getAttribute( 'data-background-position' ), + backgroundTransition: slide.getAttribute( 'data-background-transition' ) + }; + + var element = document.createElement( 'div' ); + element.className = 'slide-background'; + + 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 ) ) { + element.style.backgroundImage = 'url('+ data.background +')'; + } + else { + element.style.background = data.background; + } + } + + // Additional and optional background properties + if( data.backgroundSize ) element.style.backgroundSize = data.backgroundSize; + if( data.backgroundImage ) element.style.backgroundImage = 'url("' + data.backgroundImage + '")'; + if( data.backgroundColor ) element.style.backgroundColor = data.backgroundColor; + if( data.backgroundRepeat ) element.style.backgroundRepeat = data.backgroundRepeat; + if( data.backgroundPosition ) element.style.backgroundPosition = data.backgroundPosition; + if( data.backgroundTransition ) element.setAttribute( 'data-background-transition', data.backgroundTransition ); + + container.appendChild( element ); + + return element; + + } + + // Iterate over all horizontal slides + toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( slideh ) { + + var backgroundStack; + + if( isPrintingPDF() ) { + backgroundStack = _createBackground( slideh, slideh ); + } + else { + backgroundStack = _createBackground( slideh, dom.background ); + } + + // Iterate over all vertical slides + toArray( slideh.querySelectorAll( 'section' ) ).forEach( function( slidev ) { + + if( isPrintingPDF() ) { + _createBackground( slidev, slidev ); + } + else { + _createBackground( slidev, backgroundStack ); + } + + } ); + + } ); + + } + + /** * Hides the address bar if we're on a mobile device. */ function hideAddressBar() { - if( navigator.userAgent.match( /(iphone|ipod)/i ) ) { - // Give the page some scrollable overflow - document.documentElement.style.overflow = 'scroll'; - document.body.style.height = '120%'; - + if( /iphone|ipod|android/gi.test( navigator.userAgent ) && !/crios/gi.test( navigator.userAgent ) ) { // Events that should trigger the address bar to hide window.addEventListener( 'load', removeAddressBar, false ); window.addEventListener( 'orientationchange', removeAddressBar, false ); } @@ -259,11 +376,11 @@ head.ready( s.src.match( /([\w\d_\-]*)\.?js$|[^\\\/]*$/i )[0], s.callback ); } } } - // Called once synchronous scritps finish loading + // Called once synchronous scripts finish loading function proceed() { if( scriptsAsync.length ) { // Load asynchronous scripts head.js.apply( null, scriptsAsync ); } @@ -290,26 +407,16 @@ function start() { // Make sure we've got all the DOM elements we need setupDOM(); - // Subscribe to input - addEventListeners(); - // Updates the presentation to match the current configuration values configure(); - // Force an initial layout, will thereafter be invoked as the window - // is resized - layout(); - // Read the initial hash readURL(); - // Start auto-sliding if it's enabled - cueAutoSlide(); - // Notify listeners that the presentation is ready but use a 1ms // timeout to ensure it's not fired synchronously after #initialize() setTimeout( function() { dispatchEvent( 'ready', { 'indexh': indexh, @@ -319,48 +426,77 @@ }, 1 ); } /** - * Applies the configuration settings from the config object. + * Applies the configuration settings from the config + * object. May be called multiple times. */ - function configure() { + function configure( options ) { - if( supports3DTransforms === false ) { - config.transition = 'linear'; - } + dom.wrapper.classList.remove( config.transition ); - if( config.controls && dom.controls ) { - dom.controls.style.display = 'block'; - } + // New config options may be passed when this method + // is invoked through the API after initialization + if( typeof options === 'object' ) extend( config, options ); - if( config.progress && dom.progress ) { - dom.progress.style.display = 'block'; + // Force linear transition based on browser capabilities + if( supports3DTransforms === false ) config.transition = 'linear'; + + dom.wrapper.classList.add( config.transition ); + + dom.wrapper.setAttribute( 'data-transition-speed', config.transitionSpeed ); + dom.wrapper.setAttribute( 'data-background-transition', config.backgroundTransition ); + + if( dom.controls ) { + dom.controls.style.display = ( config.controls && dom.controls ) ? 'block' : 'none'; } - if( config.transition !== 'default' ) { - dom.wrapper.classList.add( config.transition ); + if( dom.progress ) { + dom.progress.style.display = ( config.progress && dom.progress ) ? 'block' : 'none'; } if( config.rtl ) { dom.wrapper.classList.add( 'rtl' ); } + else { + dom.wrapper.classList.remove( 'rtl' ); + } if( config.center ) { dom.wrapper.classList.add( 'center' ); } + else { + dom.wrapper.classList.remove( 'center' ); + } if( config.mouseWheel ) { document.addEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF document.addEventListener( 'mousewheel', onDocumentMouseScroll, false ); } + else { + document.removeEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF + document.removeEventListener( 'mousewheel', onDocumentMouseScroll, false ); + } - // 3D links + // Rolling 3D links if( config.rollingLinks ) { - linkify(); + enableRollingLinks(); } + else { + disableRollingLinks(); + } + // Iframe link previews + if( config.previewLinks ) { + enablePreviewLinks(); + } + else { + disablePreviewLinks(); + enablePreviewLinks( '[data-preview-link]' ); + } + // 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]; @@ -369,73 +505,92 @@ themeURL = themeURL.replace(themeFinder, config.theme); dom.theme.setAttribute( 'href', themeURL ); } } + sync(); + } /** * Binds all event listeners. */ function addEventListeners() { + eventsAreBound = true; + window.addEventListener( 'hashchange', onWindowHashChange, false ); window.addEventListener( 'resize', onWindowResize, false ); if( config.touch ) { - document.addEventListener( 'touchstart', onDocumentTouchStart, false ); - document.addEventListener( 'touchmove', onDocumentTouchMove, false ); - document.addEventListener( 'touchend', onDocumentTouchEnd, false ); + dom.wrapper.addEventListener( 'touchstart', onTouchStart, false ); + dom.wrapper.addEventListener( 'touchmove', onTouchMove, false ); + dom.wrapper.addEventListener( 'touchend', onTouchEnd, false ); + + // Support pointer-style touch interaction as well + if( window.navigator.msPointerEnabled ) { + dom.wrapper.addEventListener( 'MSPointerDown', onPointerDown, false ); + dom.wrapper.addEventListener( 'MSPointerMove', onPointerMove, false ); + dom.wrapper.addEventListener( 'MSPointerUp', onPointerUp, false ); + } } if( config.keyboard ) { document.addEventListener( 'keydown', onDocumentKeyDown, false ); } if ( config.progress && dom.progress ) { - dom.progress.addEventListener( 'click', preventAndForward( onProgressClick ), false ); + dom.progress.addEventListener( 'click', onProgressClicked, false ); } if ( config.controls && dom.controls ) { - var actionEvent = 'ontouchstart' in window ? 'touchstart' : 'click'; - dom.controlsLeft.forEach( function( el ) { el.addEventListener( actionEvent, preventAndForward( navigateLeft ), false ); } ); - dom.controlsRight.forEach( function( el ) { el.addEventListener( actionEvent, preventAndForward( navigateRight ), false ); } ); - dom.controlsUp.forEach( function( el ) { el.addEventListener( actionEvent, preventAndForward( navigateUp ), false ); } ); - dom.controlsDown.forEach( function( el ) { el.addEventListener( actionEvent, preventAndForward( navigateDown ), false ); } ); - dom.controlsPrev.forEach( function( el ) { el.addEventListener( actionEvent, preventAndForward( navigatePrev ), false ); } ); - dom.controlsNext.forEach( function( el ) { el.addEventListener( actionEvent, preventAndForward( navigateNext ), 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 ); } ); + dom.controlsUp.forEach( function( el ) { el.addEventListener( eventName, onNavigateUpClicked, false ); } ); + dom.controlsDown.forEach( function( el ) { el.addEventListener( eventName, onNavigateDownClicked, false ); } ); + dom.controlsPrev.forEach( function( el ) { el.addEventListener( eventName, onNavigatePrevClicked, false ); } ); + dom.controlsNext.forEach( function( el ) { el.addEventListener( eventName, onNavigateNextClicked, false ); } ); + } ); } } /** * Unbinds all event listeners. */ function removeEventListeners() { + eventsAreBound = false; + document.removeEventListener( 'keydown', onDocumentKeyDown, false ); window.removeEventListener( 'hashchange', onWindowHashChange, false ); window.removeEventListener( 'resize', onWindowResize, false ); - if( config.touch ) { - document.removeEventListener( 'touchstart', onDocumentTouchStart, false ); - document.removeEventListener( 'touchmove', onDocumentTouchMove, false ); - document.removeEventListener( 'touchend', onDocumentTouchEnd, false ); + dom.wrapper.removeEventListener( 'touchstart', onTouchStart, false ); + dom.wrapper.removeEventListener( 'touchmove', onTouchMove, false ); + dom.wrapper.removeEventListener( 'touchend', onTouchEnd, false ); + + if( window.navigator.msPointerEnabled ) { + dom.wrapper.removeEventListener( 'MSPointerDown', onPointerDown, false ); + dom.wrapper.removeEventListener( 'MSPointerMove', onPointerMove, false ); + dom.wrapper.removeEventListener( 'MSPointerUp', onPointerUp, false ); } if ( config.progress && dom.progress ) { - dom.progress.removeEventListener( 'click', preventAndForward( onProgressClick ), false ); + dom.progress.removeEventListener( 'click', onProgressClicked, false ); } if ( config.controls && dom.controls ) { - var actionEvent = 'ontouchstart' in window ? 'touchstart' : 'click'; - dom.controlsLeft.forEach( function( el ) { el.removeEventListener( actionEvent, preventAndForward( navigateLeft ), false ); } ); - dom.controlsRight.forEach( function( el ) { el.removeEventListener( actionEvent, preventAndForward( navigateRight ), false ); } ); - dom.controlsUp.forEach( function( el ) { el.removeEventListener( actionEvent, preventAndForward( navigateUp ), false ); } ); - dom.controlsDown.forEach( function( el ) { el.removeEventListener( actionEvent, preventAndForward( navigateDown ), false ); } ); - dom.controlsPrev.forEach( function( el ) { el.removeEventListener( actionEvent, preventAndForward( navigatePrev ), false ); } ); - dom.controlsNext.forEach( function( el ) { el.removeEventListener( actionEvent, preventAndForward( navigateNext ), false ); } ); + [ 'touchstart', 'click' ].forEach( function( eventName ) { + dom.controlsLeft.forEach( function( el ) { el.removeEventListener( eventName, onNavigateLeftClicked, false ); } ); + dom.controlsRight.forEach( function( el ) { el.removeEventListener( eventName, onNavigateRightClicked, false ); } ); + dom.controlsUp.forEach( function( el ) { el.removeEventListener( eventName, onNavigateUpClicked, false ); } ); + dom.controlsDown.forEach( function( el ) { el.removeEventListener( eventName, onNavigateDownClicked, false ); } ); + dom.controlsPrev.forEach( function( el ) { el.removeEventListener( eventName, onNavigatePrevClicked, false ); } ); + dom.controlsNext.forEach( function( el ) { el.removeEventListener( eventName, onNavigateNextClicked, false ); } ); + } ); } } /** @@ -474,34 +629,71 @@ return Math.sqrt( dx*dx + dy*dy ); } /** - * Prevents an events defaults behavior calls the - * specified delegate. - * - * @param {Function} delegate The method to call - * after the wrapper has been executed + * Retrieves the height of the given element by looking + * at the position and height of its immediate children. */ - function preventAndForward( delegate ) { + function getAbsoluteHeight( element ) { - return function( event ) { - event.preventDefault(); - delegate.call( null, event ); - }; + var height = 0; + if( element ) { + var absoluteChildren = 0; + + toArray( element.childNodes ).forEach( function( child ) { + + if( typeof child.offsetTop === 'number' && child.style ) { + // Count # of abs children + if( child.style.position === 'absolute' ) { + absoluteChildren += 1; + } + + height = Math.max( height, child.offsetTop + child.offsetHeight ); + } + + } ); + + // If there are no absolute children, use offsetHeight + if( absoluteChildren === 0 ) { + height = element.offsetHeight; + } + + } + + return height; + } /** + * Checks if this instance is being used to print a PDF. + */ + function isPrintingPDF() { + + return ( /print-pdf/gi ).test( window.location.search ); + + } + + /** * Causes the address bar to hide on mobile devices, * more vertical space ftw. */ function removeAddressBar() { + if( window.orientation === 0 ) { + 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 ); - }, 0 ); + }, 10 ); } /** * Dispatches an event of the specified type from the @@ -517,64 +709,252 @@ } /** * Wrap all links in 3D goodness. */ - function linkify() { + function enableRollingLinks() { if( supports3DTransforms && !( 'msPerspective' in document.body.style ) ) { - var nodes = document.querySelectorAll( SLIDES_SELECTOR + ' a:not(.image)' ); + var anchors = document.querySelectorAll( SLIDES_SELECTOR + ' a:not(.image)' ); - for( var i = 0, len = nodes.length; i < len; i++ ) { - var node = nodes[i]; + for( var i = 0, len = anchors.length; i < len; i++ ) { + var anchor = anchors[i]; - if( node.textContent && !node.querySelector( '*' ) && ( !node.className || !node.classList.contains( node, 'roll' ) ) ) { + if( anchor.textContent && !anchor.querySelector( '*' ) && ( !anchor.className || !anchor.classList.contains( anchor, 'roll' ) ) ) { var span = document.createElement('span'); - span.setAttribute('data-title', node.text); - span.innerHTML = node.innerHTML; + span.setAttribute('data-title', anchor.text); + span.innerHTML = anchor.innerHTML; - node.classList.add( 'roll' ); - node.innerHTML = ''; - node.appendChild(span); + anchor.classList.add( 'roll' ); + anchor.innerHTML = ''; + anchor.appendChild(span); } } } } /** + * Unwrap all 3D links. + */ + function disableRollingLinks() { + + var anchors = document.querySelectorAll( SLIDES_SELECTOR + ' a.roll' ); + + for( var i = 0, len = anchors.length; i < len; i++ ) { + var anchor = anchors[i]; + var span = anchor.querySelector( 'span' ); + + if( span ) { + anchor.classList.remove( 'roll' ); + anchor.innerHTML = span.innerHTML; + } + } + + } + + /** + * Bind preview frame links. + */ + function enablePreviewLinks( selector ) { + + var anchors = toArray( document.querySelectorAll( selector ? selector : 'a' ) ); + + anchors.forEach( function( element ) { + if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) { + element.addEventListener( 'click', onPreviewLinkClicked, false ); + } + } ); + + } + + /** + * Unbind preview frame links. + */ + function disablePreviewLinks() { + + var anchors = toArray( document.querySelectorAll( 'a' ) ); + + anchors.forEach( function( element ) { + if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) { + element.removeEventListener( 'click', onPreviewLinkClicked, false ); + } + } ); + + } + + /** + * Opens a preview window for the target URL. + */ + function openPreview( url ) { + + closePreview(); + + dom.preview = document.createElement( 'div' ); + dom.preview.classList.add( 'preview-link-overlay' ); + dom.wrapper.appendChild( dom.preview ); + + dom.preview.innerHTML = [ + '<header>', + '<a class="close" href="#"><span class="icon"></span></a>', + '<a class="external" href="'+ url +'" target="_blank"><span class="icon"></span></a>', + '</header>', + '<div class="spinner"></div>', + '<div class="viewport">', + '<iframe src="'+ url +'"></iframe>', + '</div>' + ].join(''); + + dom.preview.querySelector( 'iframe' ).addEventListener( 'load', function( event ) { + dom.preview.classList.add( 'loaded' ); + }, false ); + + dom.preview.querySelector( '.close' ).addEventListener( 'click', function( event ) { + closePreview(); + event.preventDefault(); + }, false ); + + dom.preview.querySelector( '.external' ).addEventListener( 'click', function( event ) { + closePreview(); + }, false ); + + setTimeout( function() { + dom.preview.classList.add( 'visible' ); + }, 1 ); + + } + + /** + * Closes the iframe preview window. + */ + function closePreview() { + + if( dom.preview ) { + dom.preview.setAttribute( 'src', '' ); + dom.preview.parentNode.removeChild( dom.preview ); + dom.preview = null; + } + + } + + /** + * Return a sorted fragments list, ordered by an increasing + * "data-fragment-index" attribute. + * + * Fragments will be revealed in the order that they are returned by + * this function, so you can use the index attributes to control the + * order of fragment appearance. + * + * To maintain a sensible default fragment order, fragments are presumed + * to be passed in document order. This function adds a "fragment-index" + * attribute to each node if such an attribute is not already present, + * and sets that attribute to an integer value which is the position of + * the fragment within the fragments list. + */ + function sortFragments( fragments ) { + + var a = toArray( fragments ); + + a.forEach( function( el, idx ) { + if( !el.hasAttribute( 'data-fragment-index' ) ) { + el.setAttribute( 'data-fragment-index', idx ); + } + } ); + + a.sort( function( l, r ) { + return l.getAttribute( 'data-fragment-index' ) - r.getAttribute( 'data-fragment-index'); + } ); + + return a; + + } + + /** * Applies JavaScript-controlled layout rules to the * presentation. */ function layout() { - if( config.center ) { + if( dom.wrapper && !isPrintingPDF() ) { + // Available space to scale within + var availableWidth = dom.wrapper.offsetWidth, + availableHeight = dom.wrapper.offsetHeight; + + // Reduce available space by margin + availableWidth -= ( availableHeight * config.margin ); + availableHeight -= ( availableHeight * config.margin ); + + // Dimensions of the content + var slideWidth = config.width, + slideHeight = config.height; + + // Slide width may be a percentage of available width + if( typeof slideWidth === 'string' && /%$/.test( slideWidth ) ) { + slideWidth = parseInt( slideWidth, 10 ) / 100 * availableWidth; + } + + // Slide height may be a percentage of available height + if( typeof slideHeight === 'string' && /%$/.test( slideHeight ) ) { + slideHeight = parseInt( slideHeight, 10 ) / 100 * availableHeight; + } + + dom.slides.style.width = slideWidth + 'px'; + dom.slides.style.height = slideHeight + 'px'; + + // Determine scale of content to fit within available space + scale = Math.min( availableWidth / slideWidth, availableHeight / slideHeight ); + + // Respect max/min scale settings + scale = Math.max( scale, config.minScale ); + scale = Math.min( scale, config.maxScale ); + + // Prefer applying scale via zoom since Chrome blurs scaled content + // with nested transforms + if( typeof dom.slides.style.zoom !== 'undefined' && !navigator.userAgent.match( /(iphone|ipod|ipad|android)/gi ) ) { + dom.slides.style.zoom = scale; + } + // Apply scale transform as a fallback + else { + var transform = 'translate(-50%, -50%) scale('+ scale +') translate(50%, 50%)'; + + dom.slides.style.WebkitTransform = transform; + dom.slides.style.MozTransform = transform; + dom.slides.style.msTransform = transform; + dom.slides.style.OTransform = transform; + dom.slides.style.transform = transform; + } + // Select all slides, vertical and horizontal var slides = toArray( document.querySelectorAll( SLIDES_SELECTOR ) ); - // Determine the minimum top offset for slides - var minTop = -dom.wrapper.offsetHeight / 2; - for( var i = 0, len = slides.length; i < len; i++ ) { var slide = slides[ i ]; // Don't bother updating invisible slides if( slide.style.display === 'none' ) { continue; } - // Vertical stacks are not centered since their section - // children will be - if( slide.classList.contains( 'stack' ) ) { - slide.style.top = 0; + if( config.center ) { + // Vertical stacks are not centred since their section + // children will be + if( slide.classList.contains( 'stack' ) ) { + slide.style.top = 0; + } + else { + slide.style.top = Math.max( - ( getAbsoluteHeight( slide ) / 2 ) - 20, -slideHeight / 2 ) + 'px'; + } } else { - slide.style.top = Math.max( - ( slide.offsetHeight / 2 ) - 20, minTop ) + 'px'; + slide.style.top = ''; } + } + updateProgress(); + } } /** @@ -585,27 +965,30 @@ * @param {HTMLElement} stack The vertical stack element * @param {int} v Index to memorize */ function setPreviousVerticalIndex( stack, v ) { - if( stack ) { + if( typeof stack === 'object' && typeof stack.setAttribute === 'function' ) { stack.setAttribute( 'data-previous-indexv', v || 0 ); } } /** - * Retrieves the vertical index which was stored using + * Retrieves the vertical index which was stored using * #setPreviousVerticalIndex() or 0 if no previous index * exists. * * @param {HTMLElement} stack The vertical stack element */ function getPreviousVerticalIndex( stack ) { - if( stack && stack.classList.contains( 'stack' ) ) { - return parseInt( stack.getAttribute( 'data-previous-indexv' ) || 0, 10 ); + if( typeof stack === 'object' && typeof stack.setAttribute === 'function' && stack.classList.contains( 'stack' ) ) { + // Prefer manually defined start-indexv + var attributeName = stack.hasAttribute( 'data-start-indexv' ) ? 'data-start-indexv' : 'data-previous-indexv'; + + return parseInt( stack.getAttribute( attributeName ) || 0, 10 ); } return 0; } @@ -620,10 +1003,13 @@ function activateOverview() { // Only proceed if enabled in config if( config.overview ) { + // Don't auto-slide while in overview mode + cancelAutoSlide(); + var wasActive = dom.wrapper.classList.contains( 'overview' ); dom.wrapper.classList.add( 'overview' ); dom.wrapper.classList.remove( 'exit-overview' ); @@ -637,11 +1023,12 @@ var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ); for( var i = 0, len1 = horizontalSlides.length; i < len1; i++ ) { var hslide = horizontalSlides[i], - htransform = 'translateZ(-2500px) translate(' + ( ( i - indexh ) * 105 ) + '%, 0%)'; + hoffset = config.rtl ? -105 : 105, + htransform = 'translateZ(-2500px) translate(' + ( ( i - indexh ) * hoffset ) + '%, 0%)'; hslide.setAttribute( 'data-index-h', i ); hslide.style.display = 'block'; hslide.style.WebkitTransform = htransform; hslide.style.MozTransform = htransform; @@ -739,10 +1126,12 @@ element.removeEventListener( 'click', onOverviewSlideClicked, true ); } slide( indexh, indexv ); + cueAutoSlide(); + // Notify observers of the overview hiding dispatchEvent( 'overviewhidden', { 'indexh': indexh, 'indexv': indexv, 'currentSlide': currentSlide @@ -762,28 +1151,44 @@ if( typeof override === 'boolean' ) { override ? activateOverview() : deactivateOverview(); } else { - isOverviewActive() ? deactivateOverview() : activateOverview(); + isOverview() ? deactivateOverview() : activateOverview(); } } /** * Checks if the overview is currently active. * * @return {Boolean} true if the overview is active, * false otherwise */ - function isOverviewActive() { + function isOverview() { return dom.wrapper.classList.contains( 'overview' ); } /** + * Checks if the current or specified slide is vertical + * (nested within another slide). + * + * @param {HTMLElement} slide [optional] The slide to check + * orientation of + */ + function isVerticalSlide( slide ) { + + // Prefer slide argument, otherwise use current slide + slide = slide ? slide : currentSlide; + + return slide && !!slide.parentNode.nodeName.match( /section/i ); + + } + + /** * Handling the fullscreen functionality via the fullscreen API * * @see http://fullscreen.spec.whatwg.org/ * @see https://developer.mozilla.org/en-US/docs/DOM/Using_fullscreen_mode */ @@ -791,10 +1196,11 @@ var element = document.body; // Check which implementation is available var requestMethod = element.requestFullScreen || + element.webkitRequestFullscreen || element.webkitRequestFullScreen || element.mozRequestFullScreen || element.msRequestFullScreen; if( requestMethod ) { @@ -807,21 +1213,35 @@ * Enters the paused mode which fades everything on screen to * black. */ function pause() { + var wasPaused = dom.wrapper.classList.contains( 'paused' ); + + cancelAutoSlide(); dom.wrapper.classList.add( 'paused' ); + if( wasPaused === false ) { + dispatchEvent( 'paused' ); + } + } /** * Exits from the paused mode. */ function resume() { + var wasPaused = dom.wrapper.classList.contains( 'paused' ); dom.wrapper.classList.remove( 'paused' ); + cueAutoSlide(); + + if( wasPaused ) { + dispatchEvent( 'resumed' ); + } + } /** * Toggles the paused mode on and off. */ @@ -852,12 +1272,13 @@ * * @param {int} h Horizontal index of the target slide * @param {int} v Vertical index of the target slide * @param {int} f Optional index of a fragment within the * target slide to activate + * @param {int} o Optional origin for use in multimaster environments */ - function slide( h, v, f ) { + function slide( h, v, f, o ) { // Remember where we were at before previousSlide = currentSlide; // Query all horizontal slides in the deck @@ -911,11 +1332,11 @@ while( stateBefore.length ) { document.documentElement.classList.remove( stateBefore.pop() ); } // If the overview is active, re-activate it to update positions - if( isOverviewActive() ) { + if( isOverview() ) { activateOverview(); } // Update the URL hash after a delay since updating it mid-transition // is likely to cause visual lag @@ -930,11 +1351,11 @@ currentSlide = currentVerticalSlides[ indexv ] || currentHorizontalSlide; // Show fragment, if specified if( typeof f !== 'undefined' ) { - var fragments = currentSlide.querySelectorAll( '.fragment' ); + var fragments = sortFragments( currentSlide.querySelectorAll( '.fragment' ) ); toArray( fragments ).forEach( function( fragment, indexf ) { if( indexf < f ) { fragment.classList.add( 'visible' ); } @@ -943,16 +1364,18 @@ } } ); } // Dispatch an event if the slide changed - if( indexh !== indexhBefore || indexv !== indexvBefore ) { + var slideChanged = ( indexh !== indexhBefore || indexv !== indexvBefore ); + if( slideChanged ) { dispatchEvent( 'slidechanged', { 'indexh': indexh, 'indexv': indexv, 'previousSlide': previousSlide, - 'currentSlide': currentSlide + 'currentSlide': currentSlide, + 'origin': o } ); } else { // Ensure that the previous slide is never the same as the current previousSlide = null; @@ -978,16 +1401,52 @@ } }, 0 ); } } + // Handle embedded content + if( slideChanged ) { + stopEmbeddedContent( previousSlide ); + startEmbeddedContent( currentSlide ); + } + updateControls(); updateProgress(); + updateBackground(); } /** + * Syncs the presentation with the current DOM. Useful + * when new slides or control elements are added or when + * the configuration has changed. + */ + function sync() { + + // Subscribe to input + removeEventListeners(); + addEventListeners(); + + // Force a layout to make sure the current config is accounted for + layout(); + + // Reflect the current autoSlide value + autoSlide = config.autoSlide; + + // Start auto-sliding if it's enabled + cueAutoSlide(); + + // Re-create the slide backgrounds + createBackgrounds(); + + updateControls(); + updateProgress(); + updateBackground(); + + } + + /** * Updates one dimension of slides by showing the slide * with the specified index. * * @param {String} selector A CSS selector that will fetch * the group of slides we are working with @@ -1022,39 +1481,45 @@ for( var i = 0; i < slidesLength; i++ ) { var element = slides[i]; // Optimization; hide all slides that are three or more steps // away from the present slide - if( isOverviewActive() === false ) { + if( isOverview() === false ) { // The distance loops so that it measures 1 between the first // and last slides var distance = Math.abs( ( index - i ) % ( slidesLength - 3 ) ) || 0; element.style.display = distance > 3 ? 'none' : 'block'; } - slides[i].classList.remove( 'past' ); - slides[i].classList.remove( 'present' ); - slides[i].classList.remove( 'future' ); + var reverse = config.rtl && !isVerticalSlide( element ); + element.classList.remove( 'past' ); + element.classList.remove( 'present' ); + element.classList.remove( 'future' ); + + // http://www.w3.org/html/wg/drafts/html/master/editing.html#the-hidden-attribute + element.setAttribute( 'hidden', '' ); + if( i < index ) { // Any element previous to index is given the 'past' class - slides[i].classList.add( 'past' ); + element.classList.add( reverse ? 'future' : 'past' ); } else if( i > index ) { // Any element subsequent to index is given the 'future' class - slides[i].classList.add( 'future' ); + element.classList.add( reverse ? 'past' : 'future' ); } // If this element contains vertical slides if( element.querySelector( 'section' ) ) { - slides[i].classList.add( 'stack' ); + element.classList.add( 'stack' ); } } // Mark the current slide as present slides[index].classList.add( 'present' ); + slides[index].removeAttribute( 'hidden' ); // If this slide has a state associated with it, add it // onto the current state of the deck var slideState = slides[index].getAttribute( 'data-state' ); if( slideState ) { @@ -1064,11 +1529,11 @@ // If this slide has a data-autoslide attribtue 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; } } @@ -1137,18 +1602,20 @@ function updateControls() { if ( config.controls && dom.controls ) { var routes = availableRoutes(); + var fragments = availableFragments(); // Remove the 'enabled' class from all directions dom.controlsLeft.concat( dom.controlsRight ) .concat( dom.controlsUp ) .concat( dom.controlsDown ) .concat( dom.controlsPrev ) .concat( dom.controlsNext ).forEach( function( node ) { node.classList.remove( 'enabled' ); + node.classList.remove( 'fragmented' ); } ); // Add the 'enabled' class to the available routes if( routes.left ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'enabled' ); } ); if( routes.right ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'enabled' ); } ); @@ -1157,34 +1624,164 @@ // Prev/next buttons if( routes.left || routes.up ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'enabled' ); } ); if( routes.right || routes.down ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'enabled' ); } ); + // Highlight fragment directions + if( currentSlide ) { + + // Always apply fragment decorator to prev/next buttons + if( fragments.prev ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } ); + if( fragments.next ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } ); + + // Apply fragment decorators to directional buttons based on + // what slide axis they are in + if( isVerticalSlide( currentSlide ) ) { + if( fragments.prev ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } ); + if( fragments.next ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } ); + } + else { + if( fragments.prev ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } ); + if( fragments.next ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } ); + } + } + } } /** + * Updates the background elements to reflect the current + * slide. + */ + function updateBackground() { + + // Update the classes of all backgrounds to match the + // states of their slides (past/present/future) + toArray( dom.background.childNodes ).forEach( function( backgroundh, h ) { + + // Reverse past/future classes when in RTL mode + var horizontalPast = config.rtl ? 'future' : 'past', + horizontalFuture = config.rtl ? 'past' : 'future'; + + backgroundh.className = 'slide-background ' + ( h < indexh ? horizontalPast : h > indexh ? horizontalFuture : 'present' ); + + toArray( backgroundh.childNodes ).forEach( function( backgroundv, v ) { + + backgroundv.className = 'slide-background ' + ( v < indexv ? 'past' : v > indexv ? 'future' : 'present' ); + + } ); + + } ); + + // Allow the first background to apply without transition + setTimeout( function() { + dom.background.classList.remove( 'no-transition' ); + }, 1 ); + + } + + /** * Determine what available routes there are for navigation. * * @return {Object} containing four booleans: left/right/up/down */ function availableRoutes() { var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ), verticalSlides = document.querySelectorAll( VERTICAL_SLIDES_SELECTOR ); - return { - left: indexh > 0, - right: indexh < horizontalSlides.length - 1, + var routes = { + left: indexh > 0 || config.loop, + right: indexh < horizontalSlides.length - 1 || config.loop, up: indexv > 0, down: indexv < verticalSlides.length - 1 }; + // reverse horizontal controls for rtl + if( config.rtl ) { + var left = routes.left; + routes.left = routes.right; + routes.right = left; + } + + return routes; + } /** + * Returns an object describing the available fragment + * directions. + * + * @return {Object} two boolean properties: prev/next + */ + function availableFragments() { + + if( currentSlide && config.fragments ) { + var fragments = currentSlide.querySelectorAll( '.fragment' ); + var hiddenFragments = currentSlide.querySelectorAll( '.fragment:not(.visible)' ); + + return { + prev: fragments.length - hiddenFragments.length > 0, + next: !!hiddenFragments.length + }; + } + else { + return { prev: false, next: false }; + } + + } + + /** + * Start playback of any embedded content inside of + * the targeted slide. + */ + function startEmbeddedContent( slide ) { + + if( slide ) { + // HTML5 media elements + toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) { + if( el.hasAttribute( 'data-autoplay' ) ) { + el.play(); + } + } ); + + // 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":""}', '*'); + } + }); + } + + } + + /** + * Stop playback of any embedded content inside of + * the targeted slide. + */ + function stopEmbeddedContent( slide ) { + + if( slide ) { + // HTML5 media elements + toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) { + if( !el.hasAttribute( 'data-ignore' ) ) { + el.pause(); + } + } ); + + // YouTube embeds + 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":""}', '*'); + } + }); + } + + } + + /** * Reads the current URL (hash) and navigates accordingly. */ function readURL() { var hash = window.location.hash; @@ -1262,21 +1859,22 @@ * * @param {HTMLElement} slide If specified, the returned * index will be for this slide rather than the currently * active one * - * @return {Object} { h: <int>, v: <int> } + * @return {Object} { h: <int>, v: <int>, f: <int> } */ function getIndices( slide ) { // By default, return the current indices var h = indexh, - v = indexv; + v = indexv, + f; // If a slide is specified, return the indices of that slide if( slide ) { - var isVertical = !!slide.parentNode.nodeName.match( /section/gi ); + var isVertical = isVerticalSlide( slide ); var slideh = isVertical ? slide.parentNode : slide; // Select all horizontal slides var horizontalSlides = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ); @@ -1287,41 +1885,47 @@ if( isVertical ) { v = Math.max( toArray( slide.parentNode.querySelectorAll( 'section' ) ).indexOf( slide ), 0 ); } } - return { h: h, v: v }; + if( !slide && currentSlide ) { + var visibleFragments = currentSlide.querySelectorAll( '.fragment.visible' ); + if( visibleFragments.length ) { + f = visibleFragments.length; + } + } + return { h: h, v: v, f: f }; + } /** * Navigate to the next slide fragment. * * @return {Boolean} true if there was a next fragment, * false otherwise */ function nextFragment() { - // Vertical slides: - if( document.querySelector( VERTICAL_SLIDES_SELECTOR + '.present' ) ) { - var verticalFragments = document.querySelectorAll( VERTICAL_SLIDES_SELECTOR + '.present .fragment:not(.visible)' ); - if( verticalFragments.length ) { - verticalFragments[0].classList.add( 'visible' ); + if( currentSlide && config.fragments ) { + var fragments = sortFragments( currentSlide.querySelectorAll( '.fragment:not(.visible)' ) ); - // Notify subscribers of the change - dispatchEvent( 'fragmentshown', { fragment: verticalFragments[0] } ); - return true; - } - } - // Horizontal slides: - else { - var horizontalFragments = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.present .fragment:not(.visible)' ); - if( horizontalFragments.length ) { - horizontalFragments[0].classList.add( 'visible' ); + if( fragments.length ) { + // Find the index of the next fragment + var index = fragments[0].getAttribute( 'data-fragment-index' ); + // Find all fragments with the same index + fragments = currentSlide.querySelectorAll( '.fragment[data-fragment-index="'+ index +'"]' ); + + toArray( fragments ).forEach( function( element ) { + element.classList.add( 'visible' ); + } ); + // Notify subscribers of the change - dispatchEvent( 'fragmentshown', { fragment: horizontalFragments[0] } ); + dispatchEvent( 'fragmentshown', { fragment: fragments[0], fragments: fragments } ); + + updateControls(); return true; } } return false; @@ -1334,29 +1938,28 @@ * @return {Boolean} true if there was a previous fragment, * false otherwise */ function previousFragment() { - // Vertical slides: - if( document.querySelector( VERTICAL_SLIDES_SELECTOR + '.present' ) ) { - var verticalFragments = document.querySelectorAll( VERTICAL_SLIDES_SELECTOR + '.present .fragment.visible' ); - if( verticalFragments.length ) { - verticalFragments[ verticalFragments.length - 1 ].classList.remove( 'visible' ); + if( currentSlide && config.fragments ) { + var fragments = sortFragments( currentSlide.querySelectorAll( '.fragment.visible' ) ); - // Notify subscribers of the change - dispatchEvent( 'fragmenthidden', { fragment: verticalFragments[ verticalFragments.length - 1 ] } ); - return true; - } - } - // Horizontal slides: - else { - var horizontalFragments = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.present .fragment.visible' ); - if( horizontalFragments.length ) { - horizontalFragments[ horizontalFragments.length - 1 ].classList.remove( 'visible' ); + if( fragments.length ) { + // Find the index of the previous fragment + var index = fragments[ fragments.length - 1 ].getAttribute( 'data-fragment-index' ); + // Find all fragments with the same index + fragments = currentSlide.querySelectorAll( '.fragment[data-fragment-index="'+ index +'"]' ); + + toArray( fragments ).forEach( function( f ) { + f.classList.remove( 'visible' ); + } ); + // Notify subscribers of the change - dispatchEvent( 'fragmenthidden', { fragment: horizontalFragments[ horizontalFragments.length - 1 ] } ); + dispatchEvent( 'fragmenthidden', { fragment: fragments[0], fragments: fragments } ); + + updateControls(); return true; } } return false; @@ -1369,47 +1972,68 @@ function cueAutoSlide() { clearTimeout( autoSlideTimeout ); // Cue the next auto-slide if enabled - if( autoSlide ) { + if( autoSlide && !isPaused() && !isOverview() ) { autoSlideTimeout = setTimeout( navigateNext, autoSlide ); } } + /** + * Cancels any ongoing request to auto-slide. + */ + function cancelAutoSlide() { + + clearTimeout( autoSlideTimeout ); + + } + function navigateLeft() { - // Prioritize hiding fragments - if( availableRoutes().left && isOverviewActive() || previousFragment() === false ) { + // Reverse for RTL + if( config.rtl ) { + if( ( isOverview() || nextFragment() === false ) && availableRoutes().left ) { + slide( indexh + 1 ); + } + } + // Normal navigation + else if( ( isOverview() || previousFragment() === false ) && availableRoutes().left ) { slide( indexh - 1 ); } } function navigateRight() { - // Prioritize revealing fragments - if( availableRoutes().right && isOverviewActive() || nextFragment() === false ) { + // Reverse for RTL + if( config.rtl ) { + if( ( isOverview() || previousFragment() === false ) && availableRoutes().right ) { + slide( indexh - 1 ); + } + } + // Normal navigation + else if( ( isOverview() || nextFragment() === false ) && availableRoutes().right ) { slide( indexh + 1 ); } } function navigateUp() { // Prioritize hiding fragments - if( availableRoutes().up && isOverviewActive() || previousFragment() === false ) { + if( ( isOverview() || previousFragment() === false ) && availableRoutes().up ) { slide( indexh, indexv - 1 ); } } function navigateDown() { // Prioritize revealing fragments - if( availableRoutes().down && isOverviewActive() || nextFragment() === false ) { + if( ( isOverview() || nextFragment() === false ) && availableRoutes().down ) { slide( indexh, indexv + 1 ); } } @@ -1429,13 +2053,13 @@ else { // Fetch the previous horizontal slide, if there is one var previousSlide = document.querySelector( HORIZONTAL_SLIDES_SELECTOR + '.past:nth-child(' + indexh + ')' ); if( previousSlide ) { - indexv = ( previousSlide.querySelectorAll( 'section' ).length + 1 ) || undefined; - indexh --; - slide(); + var v = ( previousSlide.querySelectorAll( 'section' ).length - 1 ) || undefined; + var h = indexh - 1; + slide( h, v ); } } } } @@ -1474,43 +2098,83 @@ var activeElement = document.activeElement; var hasFocus = !!( document.activeElement && ( document.activeElement.type || document.activeElement.href || document.activeElement.contentEditable !== 'inherit' ) ); // Disregard the event if there's a focused element or a // keyboard modifier key is present - if ( hasFocus || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey ) return; + if( hasFocus || (event.shiftKey && event.keyCode !== 32) || event.altKey || event.ctrlKey || event.metaKey ) return; - var triggered = true; + // While paused only allow "unpausing" keyboard events (b and .) + if( isPaused() && [66,190,191].indexOf( event.keyCode ) === -1 ) { + return false; + } - switch( event.keyCode ) { - // p, page up - case 80: case 33: navigatePrev(); break; - // n, page down - case 78: case 34: navigateNext(); break; - // h, left - case 72: case 37: navigateLeft(); break; - // l, right - case 76: case 39: navigateRight(); break; - // k, up - case 75: case 38: navigateUp(); break; - // j, down - case 74: case 40: navigateDown(); break; - // home - case 36: slide( 0 ); break; - // end - case 35: slide( Number.MAX_VALUE ); break; - // space - case 32: isOverviewActive() ? deactivateOverview() : navigateNext(); break; - // return - case 13: isOverviewActive() ? deactivateOverview() : triggered = false; break; - // b, period, Logitech presenter tools "black screen" button - case 66: case 190: case 191: togglePause(); break; - // f - case 70: enterFullscreen(); break; - default: - triggered = false; + var triggered = false; + + // 1. User defined key bindings + if( typeof config.keyboard === 'object' ) { + + for( var key in config.keyboard ) { + + // Check if this binding matches the pressed key + if( parseInt( key, 10 ) === event.keyCode ) { + + var value = config.keyboard[ key ]; + + // Calback function + if( typeof value === 'function' ) { + value.apply( null, [ event ] ); + } + // String shortcuts to reveal.js API + else if( typeof value === 'string' && typeof Reveal[ value ] === 'function' ) { + Reveal[ value ].call(); + } + + triggered = true; + + } + + } + } + // 2. System defined key bindings + if( triggered === false ) { + + // Assume true and try to prove false + triggered = true; + + switch( event.keyCode ) { + // p, page up + case 80: case 33: navigatePrev(); break; + // n, page down + case 78: case 34: navigateNext(); break; + // h, left + case 72: case 37: navigateLeft(); break; + // l, right + case 76: case 39: navigateRight(); break; + // k, up + case 75: case 38: navigateUp(); break; + // j, down + case 74: case 40: navigateDown(); break; + // home + case 36: slide( 0 ); break; + // end + case 35: slide( Number.MAX_VALUE ); break; + // space + case 32: isOverview() ? deactivateOverview() : event.shiftKey ? navigatePrev() : navigateNext(); break; + // return + case 13: isOverview() ? deactivateOverview() : triggered = false; break; + // b, period, Logitech presenter tools "black screen" button + case 66: case 190: case 191: togglePause(); break; + // f + case 70: enterFullscreen(); break; + default: + triggered = false; + } + + } + // If the input resulted in a triggered action we should prevent // the browsers default behavior if( triggered ) { event.preventDefault(); } @@ -1525,14 +2189,14 @@ cueAutoSlide(); } /** - * Handler for the document level 'touchstart' event, - * enables support for swipe and pinch gestures. + * Handler for the 'touchstart' event, enables support for + * swipe and pinch gestures. */ - function onDocumentTouchStart( event ) { + function onTouchStart( event ) { touch.startX = event.touches[0].clientX; touch.startY = event.touches[0].clientY; touch.startCount = event.touches.length; @@ -1549,13 +2213,13 @@ } } /** - * Handler for the document level 'touchmove' event. + * Handler for the 'touchmove' event. */ - function onDocumentTouchMove( event ) { + function onTouchMove( event ) { // Each touch should only trigger one action if( !touch.handled ) { var currentX = event.touches[0].clientX; var currentY = event.touches[0].clientY; @@ -1623,54 +2287,104 @@ } } /** - * Handler for the document level 'touchend' event. + * Handler for the 'touchend' event. */ - function onDocumentTouchEnd( event ) { + function onTouchEnd( event ) { touch.handled = false; } /** + * Convert pointer down to touch start. + */ + function onPointerDown( event ) { + + if( event.pointerType === event.MSPOINTER_TYPE_TOUCH ) { + event.touches = [{ clientX: event.clientX, clientY: event.clientY }]; + onTouchStart( event ); + } + + } + + /** + * Convert pointer move to touch move. + */ + function onPointerMove( event ) { + + if( event.pointerType === event.MSPOINTER_TYPE_TOUCH ) { + event.touches = [{ clientX: event.clientX, clientY: event.clientY }]; + onTouchMove( event ); + } + + } + + /** + * Convert pointer up to touch end. + */ + function onPointerUp( event ) { + + if( event.pointerType === event.MSPOINTER_TYPE_TOUCH ) { + event.touches = [{ clientX: event.clientX, clientY: event.clientY }]; + onTouchEnd( event ); + } + + } + + /** * Handles mouse wheel scrolling, throttled to avoid skipping * multiple slides. */ function onDocumentMouseScroll( event ) { - clearTimeout( mouseWheelTimeout ); + if( Date.now() - lastMouseWheelStep > 600 ) { - mouseWheelTimeout = setTimeout( function() { + lastMouseWheelStep = Date.now(); + var delta = event.detail || -event.wheelDelta; if( delta > 0 ) { navigateNext(); } else { navigatePrev(); } - }, 100 ); + } + } /** * Clicking on the progress bar results in a navigation to the * closest approximate horizontal slide using this equation: * * ( clickX / presentationWidth ) * numberOfSlides */ - function onProgressClick( event ) { + function onProgressClicked( event ) { + event.preventDefault(); + var slidesTotal = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).length; var slideIndex = Math.floor( ( event.clientX / dom.wrapper.offsetWidth ) * slidesTotal ); slide( slideIndex ); } /** + * 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(); } + + /** * Handler for the window level 'hashchange' event. */ function onWindowHashChange( event ) { readURL(); @@ -1691,39 +2405,59 @@ */ function onOverviewSlideClicked( event ) { // TODO There's a bug here where the event listeners are not // removed after deactivating the overview. - if( isOverviewActive() ) { + if( eventsAreBound && isOverview() ) { event.preventDefault(); - deactivateOverview(); - var element = event.target; while( element && !element.nodeName.match( /section/gi ) ) { element = element.parentNode; } - if( element.nodeName.match( /section/gi ) ) { - var h = parseInt( element.getAttribute( 'data-index-h' ), 10 ), - v = parseInt( element.getAttribute( 'data-index-v' ), 10 ); + if( element && !element.classList.contains( 'disabled' ) ) { - slide( h, v ); + deactivateOverview(); + + if( element.nodeName.match( /section/gi ) ) { + var h = parseInt( element.getAttribute( 'data-index-h' ), 10 ), + v = parseInt( element.getAttribute( 'data-index-v' ), 10 ); + + slide( h, v ); + } + } } } + /** + * Handles clicks on links that are set to preview in the + * iframe overlay. + */ + function onPreviewLinkClicked( event ) { + var url = event.target.getAttribute( 'href' ); + if( url ) { + openPreview( url ); + event.preventDefault(); + } + + } + + // --------------------------------------------------------------------// // ------------------------------- API --------------------------------// // --------------------------------------------------------------------// return { initialize: initialize, + configure: configure, + sync: sync, // Navigation methods slide: slide, left: navigateLeft, right: navigateRight, @@ -1744,33 +2478,65 @@ navigateNext: navigateNext, // Forces an update in slide layout layout: layout, + // Returns an object with the available routes as booleans (left/right/top/bottom) + availableRoutes: availableRoutes, + + // Returns an object with the available fragments as booleans (prev/next) + availableFragments: availableFragments, + // Toggles the overview mode on/off toggleOverview: toggleOverview, // Toggles the "black screen" mode on/off togglePause: togglePause, + // State checks + isOverview: isOverview, + isPaused: isPaused, + // Adds or removes all internal event listeners (such as keyboard) addEventListeners: addEventListeners, removeEventListeners: removeEventListeners, // Returns the indices of the current, or specified, slide getIndices: getIndices, + // Returns the slide at the specified index, y is optional + getSlide: function( x, y ) { + var horizontalSlide = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR )[ x ]; + var verticalSlides = horizontalSlide && horizontalSlide.querySelectorAll( 'section' ); + + if( typeof y !== 'undefined' ) { + return verticalSlides ? verticalSlides[ y ] : undefined; + } + + return horizontalSlide; + }, + // Returns the previous slide element, may be null getPreviousSlide: function() { return previousSlide; }, // Returns the current slide element getCurrentSlide: function() { return currentSlide; }, + // Returns the current scale of the presentation content + getScale: function() { + return scale; + }, + + // Returns the current configuration object + getConfig: function() { + return config; + }, + // Helper method, retrieves query string as a key/value hash getQueryHash: function() { var query = {}; location.search.replace( /[A-Z0-9]+?=(\w*)/gi, function(a) { @@ -1778,10 +2544,25 @@ } ); return query; }, + // Returns true if we're currently on the first slide + isFirstSlide: function() { + return document.querySelector( SLIDES_SELECTOR + '.past' ) == null ? true : false; + }, + + // Returns true if we're currently on the last slide + isLastSlide: function() { + if( currentSlide && currentSlide.classList.contains( '.stack' ) ) { + return currentSlide.querySelector( SLIDES_SELECTOR + '.future' ) == null ? true : false; + } + else { + return document.querySelector( SLIDES_SELECTOR + '.future' ) == null ? true : false; + } + }, + // Forward event binding to the reveal DOM element addEventListener: function( type, listener, useCapture ) { if( 'addEventListener' in window ) { ( dom.wrapper || document.querySelector( '.reveal' ) ).addEventListener( type, listener, useCapture ); } @@ -1791,6 +2572,6 @@ ( dom.wrapper || document.querySelector( '.reveal' ) ).removeEventListener( type, listener, useCapture ); } } }; -})(); \ No newline at end of file +})();