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
+})();