michael@13: /** michael@13: * impress.js michael@13: * michael@13: * impress.js is a presentation tool based on the power of CSS3 transforms and transitions michael@13: * in modern browsers and inspired by the idea behind prezi.com. michael@13: * michael@13: * michael@13: * Copyright 2011-2012 Bartek Szopka (@bartaz) michael@13: * michael@13: * Released under the MIT and GPL Licenses. michael@13: * michael@13: * ------------------------------------------------ michael@13: * author: Bartek Szopka michael@13: * version: 0.5.3 michael@13: * url: http://bartaz.github.com/impress.js/ michael@13: * source: http://github.com/bartaz/impress.js/ michael@13: */ michael@13: michael@13: /*jshint bitwise:true, curly:true, eqeqeq:true, forin:true, latedef:true, newcap:true, michael@13: noarg:true, noempty:true, undef:true, strict:true, browser:true */ michael@13: michael@13: // You are one of those who like to know how thing work inside? michael@13: // Let me show you the cogs that make impress.js run... michael@13: (function ( document, window ) { michael@13: 'use strict'; michael@13: michael@13: // HELPER FUNCTIONS michael@13: michael@13: // `pfx` is a function that takes a standard CSS property name as a parameter michael@13: // and returns it's prefixed version valid for current browser it runs in. michael@13: // The code is heavily inspired by Modernizr http://www.modernizr.com/ michael@13: var pfx = (function () { michael@13: michael@13: var style = document.createElement('dummy').style, michael@13: prefixes = 'Webkit Moz O ms Khtml'.split(' '), michael@13: memory = {}; michael@13: michael@13: return function ( prop ) { michael@13: if ( typeof memory[ prop ] === "undefined" ) { michael@13: michael@13: var ucProp = prop.charAt(0).toUpperCase() + prop.substr(1), michael@13: props = (prop + ' ' + prefixes.join(ucProp + ' ') + ucProp).split(' '); michael@13: michael@13: memory[ prop ] = null; michael@13: for ( var i in props ) { michael@13: if ( style[ props[i] ] !== undefined ) { michael@13: memory[ prop ] = props[i]; michael@13: break; michael@13: } michael@13: } michael@13: michael@13: } michael@13: michael@13: return memory[ prop ]; michael@13: }; michael@13: michael@13: })(); michael@13: michael@13: // `arraify` takes an array-like object and turns it into real Array michael@13: // to make all the Array.prototype goodness available. michael@13: var arrayify = function ( a ) { michael@13: return [].slice.call( a ); michael@13: }; michael@13: michael@13: // `css` function applies the styles given in `props` object to the element michael@13: // given as `el`. It runs all property names through `pfx` function to make michael@13: // sure proper prefixed version of the property is used. michael@13: var css = function ( el, props ) { michael@13: var key, pkey; michael@13: for ( key in props ) { michael@13: if ( props.hasOwnProperty(key) ) { michael@13: pkey = pfx(key); michael@13: if ( pkey !== null ) { michael@13: el.style[pkey] = props[key]; michael@13: } michael@13: } michael@13: } michael@13: return el; michael@13: }; michael@13: michael@13: // `toNumber` takes a value given as `numeric` parameter and tries to turn michael@13: // it into a number. If it is not possible it returns 0 (or other value michael@13: // given as `fallback`). michael@13: var toNumber = function (numeric, fallback) { michael@13: return isNaN(numeric) ? (fallback || 0) : Number(numeric); michael@13: }; michael@13: michael@13: // `byId` returns element with given `id` - you probably have guessed that ;) michael@13: var byId = function ( id ) { michael@13: return document.getElementById(id); michael@13: }; michael@13: michael@13: // `$` returns first element for given CSS `selector` in the `context` of michael@13: // the given element or whole document. michael@13: var $ = function ( selector, context ) { michael@13: context = context || document; michael@13: return context.querySelector(selector); michael@13: }; michael@13: michael@13: // `$$` return an array of elements for given CSS `selector` in the `context` of michael@13: // the given element or whole document. michael@13: var $$ = function ( selector, context ) { michael@13: context = context || document; michael@13: return arrayify( context.querySelectorAll(selector) ); michael@13: }; michael@13: michael@13: // `triggerEvent` builds a custom DOM event with given `eventName` and `detail` data michael@13: // and triggers it on element given as `el`. michael@13: var triggerEvent = function (el, eventName, detail) { michael@13: var event = document.createEvent("CustomEvent"); michael@13: event.initCustomEvent(eventName, true, true, detail); michael@13: el.dispatchEvent(event); michael@13: }; michael@13: michael@13: // `translate` builds a translate transform string for given data. michael@13: var translate = function ( t ) { michael@13: return " translate3d(" + t.x + "px," + t.y + "px," + t.z + "px) "; michael@13: }; michael@13: michael@13: // `rotate` builds a rotate transform string for given data. michael@13: // By default the rotations are in X Y Z order that can be reverted by passing `true` michael@13: // as second parameter. michael@13: var rotate = function ( r, revert ) { michael@13: var rX = " rotateX(" + r.x + "deg) ", michael@13: rY = " rotateY(" + r.y + "deg) ", michael@13: rZ = " rotateZ(" + r.z + "deg) "; michael@13: michael@13: return revert ? rZ+rY+rX : rX+rY+rZ; michael@13: }; michael@13: michael@13: // `scale` builds a scale transform string for given data. michael@13: var scale = function ( s ) { michael@13: return " scale(" + s + ") "; michael@13: }; michael@13: michael@13: // `perspective` builds a perspective transform string for given data. michael@13: var perspective = function ( p ) { michael@13: return " perspective(" + p + "px) "; michael@13: }; michael@13: michael@13: // `getElementFromHash` returns an element located by id from hash part of michael@13: // window location. michael@13: var getElementFromHash = function () { michael@13: // get id from url # by removing `#` or `#/` from the beginning, michael@13: // so both "fallback" `#slide-id` and "enhanced" `#/slide-id` will work michael@13: return byId( window.location.hash.replace(/^#\/?/,"") ); michael@13: }; michael@13: michael@13: // `computeWindowScale` counts the scale factor between window size and size michael@13: // defined for the presentation in the config. michael@13: var computeWindowScale = function ( config ) { michael@13: var hScale = window.innerHeight / config.height, michael@13: wScale = window.innerWidth / config.width, michael@13: scale = hScale > wScale ? wScale : hScale; michael@13: michael@13: if (config.maxScale && scale > config.maxScale) { michael@13: scale = config.maxScale; michael@13: } michael@13: michael@13: if (config.minScale && scale < config.minScale) { michael@13: scale = config.minScale; michael@13: } michael@13: michael@13: return scale; michael@13: }; michael@13: michael@13: // CHECK SUPPORT michael@13: var body = document.body; michael@13: michael@13: var ua = navigator.userAgent.toLowerCase(); michael@13: var impressSupported = michael@13: // browser should support CSS 3D transtorms michael@13: ( pfx("perspective") !== null ) && michael@13: michael@13: // and `classList` and `dataset` APIs michael@13: ( body.classList ) && michael@13: ( body.dataset ) && michael@13: michael@13: // but some mobile devices need to be blacklisted, michael@13: // because their CSS 3D support or hardware is not michael@13: // good enough to run impress.js properly, sorry... michael@13: ( ua.search(/(iphone)|(ipod)|(android)/) === -1 ); michael@13: michael@13: if (!impressSupported) { michael@13: // we can't be sure that `classList` is supported michael@13: body.className += " impress-not-supported "; michael@13: } else { michael@13: body.classList.remove("impress-not-supported"); michael@13: body.classList.add("impress-supported"); michael@13: } michael@13: michael@13: // GLOBALS AND DEFAULTS michael@13: michael@13: // This is were the root elements of all impress.js instances will be kept. michael@13: // Yes, this means you can have more than one instance on a page, but I'm not michael@13: // sure if it makes any sense in practice ;) michael@13: var roots = {}; michael@13: michael@13: // some default config values. michael@13: var defaults = { michael@13: width: 1024, michael@13: height: 768, michael@13: maxScale: 1, michael@13: minScale: 0, michael@13: michael@13: perspective: 1000, michael@13: michael@13: transitionDuration: 1000 michael@13: }; michael@13: michael@13: // it's just an empty function ... and a useless comment. michael@13: var empty = function () { return false; }; michael@13: michael@13: // IMPRESS.JS API michael@13: michael@13: // And that's where interesting things will start to happen. michael@13: // It's the core `impress` function that returns the impress.js API michael@13: // for a presentation based on the element with given id ('impress' michael@13: // by default). michael@13: var impress = window.impress = function ( rootId ) { michael@13: michael@13: // If impress.js is not supported by the browser return a dummy API michael@13: // it may not be a perfect solution but we return early and avoid michael@13: // running code that may use features not implemented in the browser. michael@13: if (!impressSupported) { michael@13: return { michael@13: init: empty, michael@13: goto: empty, michael@13: prev: empty, michael@13: next: empty michael@13: }; michael@13: } michael@13: michael@13: rootId = rootId || "impress"; michael@13: michael@13: // if given root is already initialized just return the API michael@13: if (roots["impress-root-" + rootId]) { michael@13: return roots["impress-root-" + rootId]; michael@13: } michael@13: michael@13: // data of all presentation steps michael@13: var stepsData = {}; michael@13: michael@13: // element of currently active step michael@13: var activeStep = null; michael@13: michael@13: // current state (position, rotation and scale) of the presentation michael@13: var currentState = null; michael@13: michael@13: // array of step elements michael@13: var steps = null; michael@13: michael@13: // configuration options michael@13: var config = null; michael@13: michael@13: // scale factor of the browser window michael@13: var windowScale = null; michael@13: michael@13: // root presentation elements michael@13: var root = byId( rootId ); michael@13: var canvas = document.createElement("div"); michael@13: michael@13: var initialized = false; michael@13: michael@13: // STEP EVENTS michael@13: // michael@13: // There are currently two step events triggered by impress.js michael@13: // `impress:stepenter` is triggered when the step is shown on the michael@13: // screen (the transition from the previous one is finished) and michael@13: // `impress:stepleave` is triggered when the step is left (the michael@13: // transition to next step just starts). michael@13: michael@13: // reference to last entered step michael@13: var lastEntered = null; michael@13: michael@13: // `onStepEnter` is called whenever the step element is entered michael@13: // but the event is triggered only if the step is different than michael@13: // last entered step. michael@13: var onStepEnter = function (step) { michael@13: if (lastEntered !== step) { michael@13: triggerEvent(step, "impress:stepenter"); michael@13: lastEntered = step; michael@13: } michael@13: }; michael@13: michael@13: // `onStepLeave` is called whenever the step element is left michael@13: // but the event is triggered only if the step is the same as michael@13: // last entered step. michael@13: var onStepLeave = function (step) { michael@13: if (lastEntered === step) { michael@13: triggerEvent(step, "impress:stepleave"); michael@13: lastEntered = null; michael@13: } michael@13: }; michael@13: michael@13: // `initStep` initializes given step element by reading data from its michael@13: // data attributes and setting correct styles. michael@13: var initStep = function ( el, idx ) { michael@13: var data = el.dataset, michael@13: step = { michael@13: translate: { michael@13: x: toNumber(data.x), michael@13: y: toNumber(data.y), michael@13: z: toNumber(data.z) michael@13: }, michael@13: rotate: { michael@13: x: toNumber(data.rotateX), michael@13: y: toNumber(data.rotateY), michael@13: z: toNumber(data.rotateZ || data.rotate) michael@13: }, michael@13: scale: toNumber(data.scale, 1), michael@13: el: el michael@13: }; michael@13: michael@13: if ( !el.id ) { michael@13: el.id = "step-" + (idx + 1); michael@13: } michael@13: michael@13: stepsData["impress-" + el.id] = step; michael@13: michael@13: css(el, { michael@13: position: "absolute", michael@13: transform: "translate(-50%,-50%)" + michael@13: translate(step.translate) + michael@13: rotate(step.rotate) + michael@13: scale(step.scale), michael@13: transformStyle: "preserve-3d" michael@13: }); michael@13: }; michael@13: michael@13: // `init` API function that initializes (and runs) the presentation. michael@13: var init = function () { michael@13: if (initialized) { return; } michael@13: michael@13: // First we set up the viewport for mobile devices. michael@13: // For some reason iPad goes nuts when it is not done properly. michael@13: var meta = $("meta[name='viewport']") || document.createElement("meta"); michael@13: meta.content = "width=device-width, minimum-scale=1, maximum-scale=1, user-scalable=no"; michael@13: if (meta.parentNode !== document.head) { michael@13: meta.name = 'viewport'; michael@13: document.head.appendChild(meta); michael@13: } michael@13: michael@13: // initialize configuration object michael@13: var rootData = root.dataset; michael@13: config = { michael@13: width: toNumber( rootData.width, defaults.width ), michael@13: height: toNumber( rootData.height, defaults.height ), michael@13: maxScale: toNumber( rootData.maxScale, defaults.maxScale ), michael@13: minScale: toNumber( rootData.minScale, defaults.minScale ), michael@13: perspective: toNumber( rootData.perspective, defaults.perspective ), michael@13: transitionDuration: toNumber( rootData.transitionDuration, defaults.transitionDuration ) michael@13: }; michael@13: michael@13: windowScale = computeWindowScale( config ); michael@13: michael@13: // wrap steps with "canvas" element michael@13: arrayify( root.childNodes ).forEach(function ( el ) { michael@13: canvas.appendChild( el ); michael@13: }); michael@13: root.appendChild(canvas); michael@13: michael@13: // set initial styles michael@13: document.documentElement.style.height = "100%"; michael@13: michael@13: css(body, { michael@13: height: "100%", michael@13: overflow: "hidden" michael@13: }); michael@13: michael@13: var rootStyles = { michael@13: position: "absolute", michael@13: transformOrigin: "top left", michael@13: transition: "all 0s ease-in-out", michael@13: transformStyle: "preserve-3d" michael@13: }; michael@13: michael@13: css(root, rootStyles); michael@13: css(root, { michael@13: top: "50%", michael@13: left: "50%", michael@13: transform: perspective( config.perspective/windowScale ) + scale( windowScale ) michael@13: }); michael@13: css(canvas, rootStyles); michael@13: michael@13: body.classList.remove("impress-disabled"); michael@13: body.classList.add("impress-enabled"); michael@13: michael@13: // get and init steps michael@13: steps = $$(".step", root); michael@13: steps.forEach( initStep ); michael@13: michael@13: // set a default initial state of the canvas michael@13: currentState = { michael@13: translate: { x: 0, y: 0, z: 0 }, michael@13: rotate: { x: 0, y: 0, z: 0 }, michael@13: scale: 1 michael@13: }; michael@13: michael@13: initialized = true; michael@13: michael@13: triggerEvent(root, "impress:init", { api: roots[ "impress-root-" + rootId ] }); michael@13: }; michael@13: michael@13: // `getStep` is a helper function that returns a step element defined by parameter. michael@13: // If a number is given, step with index given by the number is returned, if a string michael@13: // is given step element with such id is returned, if DOM element is given it is returned michael@13: // if it is a correct step element. michael@13: var getStep = function ( step ) { michael@13: if (typeof step === "number") { michael@13: step = step < 0 ? steps[ steps.length + step] : steps[ step ]; michael@13: } else if (typeof step === "string") { michael@13: step = byId(step); michael@13: } michael@13: return (step && step.id && stepsData["impress-" + step.id]) ? step : null; michael@13: }; michael@13: michael@13: // used to reset timeout for `impress:stepenter` event michael@13: var stepEnterTimeout = null; michael@13: michael@13: // `goto` API function that moves to step given with `el` parameter (by index, id or element), michael@13: // with a transition `duration` optionally given as second parameter. michael@13: var goto = function ( el, duration ) { michael@13: michael@13: if ( !initialized || !(el = getStep(el)) ) { michael@13: // presentation not initialized or given element is not a step michael@13: return false; michael@13: } michael@13: michael@13: // Sometimes it's possible to trigger focus on first link with some keyboard action. michael@13: // Browser in such a case tries to scroll the page to make this element visible michael@13: // (even that body overflow is set to hidden) and it breaks our careful positioning. michael@13: // michael@13: // So, as a lousy (and lazy) workaround we will make the page scroll back to the top michael@13: // whenever slide is selected michael@13: // michael@13: // If you are reading this and know any better way to handle it, I'll be glad to hear about it! michael@13: window.scrollTo(0, 0); michael@13: michael@13: var step = stepsData["impress-" + el.id]; michael@13: michael@13: if ( activeStep ) { michael@13: activeStep.classList.remove("active"); michael@13: body.classList.remove("impress-on-" + activeStep.id); michael@13: } michael@13: el.classList.add("active"); michael@13: michael@13: body.classList.add("impress-on-" + el.id); michael@13: michael@13: // compute target state of the canvas based on given step michael@13: var target = { michael@13: rotate: { michael@13: x: -step.rotate.x, michael@13: y: -step.rotate.y, michael@13: z: -step.rotate.z michael@13: }, michael@13: translate: { michael@13: x: -step.translate.x, michael@13: y: -step.translate.y, michael@13: z: -step.translate.z michael@13: }, michael@13: scale: 1 / step.scale michael@13: }; michael@13: michael@13: // Check if the transition is zooming in or not. michael@13: // michael@13: // This information is used to alter the transition style: michael@13: // when we are zooming in - we start with move and rotate transition michael@13: // and the scaling is delayed, but when we are zooming out we start michael@13: // with scaling down and move and rotation are delayed. michael@13: var zoomin = target.scale >= currentState.scale; michael@13: michael@13: duration = toNumber(duration, config.transitionDuration); michael@13: var delay = (duration / 2); michael@13: michael@13: // if the same step is re-selected, force computing window scaling, michael@13: // because it is likely to be caused by window resize michael@13: if (el === activeStep) { michael@13: windowScale = computeWindowScale(config); michael@13: } michael@13: michael@13: var targetScale = target.scale * windowScale; michael@13: michael@13: // trigger leave of currently active element (if it's not the same step again) michael@13: if (activeStep && activeStep !== el) { michael@13: onStepLeave(activeStep); michael@13: } michael@13: michael@13: // Now we alter transforms of `root` and `canvas` to trigger transitions. michael@13: // michael@13: // And here is why there are two elements: `root` and `canvas` - they are michael@13: // being animated separately: michael@13: // `root` is used for scaling and `canvas` for translate and rotations. michael@13: // Transitions on them are triggered with different delays (to make michael@13: // visually nice and 'natural' looking transitions), so we need to know michael@13: // that both of them are finished. michael@13: css(root, { michael@13: // to keep the perspective look similar for different scales michael@13: // we need to 'scale' the perspective, too michael@13: transform: perspective( config.perspective / targetScale ) + scale( targetScale ), michael@13: transitionDuration: duration + "ms", michael@13: transitionDelay: (zoomin ? delay : 0) + "ms" michael@13: }); michael@13: michael@13: css(canvas, { michael@13: transform: rotate(target.rotate, true) + translate(target.translate), michael@13: transitionDuration: duration + "ms", michael@13: transitionDelay: (zoomin ? 0 : delay) + "ms" michael@13: }); michael@13: michael@13: // Here is a tricky part... michael@13: // michael@13: // If there is no change in scale or no change in rotation and translation, it means there was actually michael@13: // no delay - because there was no transition on `root` or `canvas` elements. michael@13: // We want to trigger `impress:stepenter` event in the correct moment, so here we compare the current michael@13: // and target values to check if delay should be taken into account. michael@13: // michael@13: // I know that this `if` statement looks scary, but it's pretty simple when you know what is going on michael@13: // - it's simply comparing all the values. michael@13: if ( currentState.scale === target.scale || michael@13: (currentState.rotate.x === target.rotate.x && currentState.rotate.y === target.rotate.y && michael@13: currentState.rotate.z === target.rotate.z && currentState.translate.x === target.translate.x && michael@13: currentState.translate.y === target.translate.y && currentState.translate.z === target.translate.z) ) { michael@13: delay = 0; michael@13: } michael@13: michael@13: // store current state michael@13: currentState = target; michael@13: activeStep = el; michael@13: michael@13: // And here is where we trigger `impress:stepenter` event. michael@13: // We simply set up a timeout to fire it taking transition duration (and possible delay) into account. michael@13: // michael@13: // I really wanted to make it in more elegant way. The `transitionend` event seemed to be the best way michael@13: // to do it, but the fact that I'm using transitions on two separate elements and that the `transitionend` michael@13: // event is only triggered when there was a transition (change in the values) caused some bugs and michael@13: // made the code really complicated, cause I had to handle all the conditions separately. And it still michael@13: // needed a `setTimeout` fallback for the situations when there is no transition at all. michael@13: // So I decided that I'd rather make the code simpler than use shiny new `transitionend`. michael@13: // michael@13: // If you want learn something interesting and see how it was done with `transitionend` go back to michael@13: // version 0.5.2 of impress.js: http://github.com/bartaz/impress.js/blob/0.5.2/js/impress.js michael@13: window.clearTimeout(stepEnterTimeout); michael@13: stepEnterTimeout = window.setTimeout(function() { michael@13: onStepEnter(activeStep); michael@13: }, duration + delay); michael@13: michael@13: return el; michael@13: }; michael@13: michael@13: // `prev` API function goes to previous step (in document order) michael@13: var prev = function () { michael@13: var prev = steps.indexOf( activeStep ) - 1; michael@13: prev = prev >= 0 ? steps[ prev ] : steps[ steps.length-1 ]; michael@13: michael@13: return goto(prev); michael@13: }; michael@13: michael@13: // `next` API function goes to next step (in document order) michael@13: var next = function () { michael@13: var next = steps.indexOf( activeStep ) + 1; michael@13: next = next < steps.length ? steps[ next ] : steps[ 0 ]; michael@13: michael@13: return goto(next); michael@13: }; michael@13: michael@13: // Adding some useful classes to step elements. michael@13: // michael@13: // All the steps that have not been shown yet are given `future` class. michael@13: // When the step is entered the `future` class is removed and the `present` michael@13: // class is given. When the step is left `present` class is replaced with michael@13: // `past` class. michael@13: // michael@13: // So every step element is always in one of three possible states: michael@13: // `future`, `present` and `past`. michael@13: // michael@13: // There classes can be used in CSS to style different types of steps. michael@13: // For example the `present` class can be used to trigger some custom michael@13: // animations when step is shown. michael@13: root.addEventListener("impress:init", function(){ michael@13: // STEP CLASSES michael@13: steps.forEach(function (step) { michael@13: step.classList.add("future"); michael@13: }); michael@13: michael@13: root.addEventListener("impress:stepenter", function (event) { michael@13: event.target.classList.remove("past"); michael@13: event.target.classList.remove("future"); michael@13: event.target.classList.add("present"); michael@13: }, false); michael@13: michael@13: root.addEventListener("impress:stepleave", function (event) { michael@13: event.target.classList.remove("present"); michael@13: event.target.classList.add("past"); michael@13: }, false); michael@13: michael@13: }, false); michael@13: michael@13: // Adding hash change support. michael@13: root.addEventListener("impress:init", function(){ michael@13: michael@13: // last hash detected michael@13: var lastHash = ""; michael@13: michael@13: // `#/step-id` is used instead of `#step-id` to prevent default browser michael@13: // scrolling to element in hash. michael@13: // michael@13: // And it has to be set after animation finishes, because in Chrome it michael@13: // makes transtion laggy. michael@13: // BUG: http://code.google.com/p/chromium/issues/detail?id=62820 michael@13: root.addEventListener("impress:stepenter", function (event) { michael@13: window.location.hash = lastHash = "#/" + event.target.id; michael@13: }, false); michael@13: michael@13: window.addEventListener("hashchange", function () { michael@13: // When the step is entered hash in the location is updated michael@13: // (just few lines above from here), so the hash change is michael@13: // triggered and we would call `goto` again on the same element. michael@13: // michael@13: // To avoid this we store last entered hash and compare. michael@13: if (window.location.hash !== lastHash) { michael@13: goto( getElementFromHash() ); michael@13: } michael@13: }, false); michael@13: michael@13: // START michael@13: // by selecting step defined in url or first step of the presentation michael@13: goto(getElementFromHash() || steps[0], 0); michael@13: }, false); michael@13: michael@13: body.classList.add("impress-disabled"); michael@13: michael@13: // store and return API for given impress.js root element michael@13: return (roots[ "impress-root-" + rootId ] = { michael@13: init: init, michael@13: goto: goto, michael@13: next: next, michael@13: prev: prev michael@13: }); michael@13: michael@13: }; michael@13: michael@13: // flag that can be used in JS to check if browser have passed the support test michael@13: impress.supported = impressSupported; michael@13: michael@13: })(document, window); michael@13: michael@13: // NAVIGATION EVENTS michael@13: michael@13: // As you can see this part is separate from the impress.js core code. michael@13: // It's because these navigation actions only need what impress.js provides with michael@13: // its simple API. michael@13: // michael@13: // In future I think about moving it to make them optional, move to separate files michael@13: // and treat more like a 'plugins'. michael@13: (function ( document, window ) { michael@13: 'use strict'; michael@13: michael@13: // throttling function calls, by Remy Sharp michael@13: // http://remysharp.com/2010/07/21/throttling-function-calls/ michael@13: var throttle = function (fn, delay) { michael@13: var timer = null; michael@13: return function () { michael@13: var context = this, args = arguments; michael@13: clearTimeout(timer); michael@13: timer = setTimeout(function () { michael@13: fn.apply(context, args); michael@13: }, delay); michael@13: }; michael@13: }; michael@13: michael@13: // wait for impress.js to be initialized michael@13: document.addEventListener("impress:init", function (event) { michael@13: // Getting API from event data. michael@13: // So you don't event need to know what is the id of the root element michael@13: // or anything. `impress:init` event data gives you everything you michael@13: // need to control the presentation that was just initialized. michael@13: var api = event.detail.api; michael@13: michael@13: // KEYBOARD NAVIGATION HANDLERS michael@13: michael@13: // Prevent default keydown action when one of supported key is pressed. michael@13: document.addEventListener("keydown", function ( event ) { michael@13: if ( event.keyCode === 9 || ( event.keyCode >= 32 && event.keyCode <= 34 ) || (event.keyCode >= 37 && event.keyCode <= 40) ) { michael@13: event.preventDefault(); michael@13: } michael@13: }, false); michael@13: michael@13: // Trigger impress action (next or prev) on keyup. michael@13: michael@13: // Supported keys are: michael@13: // [space] - quite common in presentation software to move forward michael@13: // [up] [right] / [down] [left] - again common and natural addition, michael@13: // [pgdown] / [pgup] - often triggered by remote controllers, michael@13: // [tab] - this one is quite controversial, but the reason it ended up on michael@13: // this list is quite an interesting story... Remember that strange part michael@13: // in the impress.js code where window is scrolled to 0,0 on every presentation michael@13: // step, because sometimes browser scrolls viewport because of the focused element? michael@13: // Well, the [tab] key by default navigates around focusable elements, so clicking michael@13: // it very often caused scrolling to focused element and breaking impress.js michael@13: // positioning. I didn't want to just prevent this default action, so I used [tab] michael@13: // as another way to moving to next step... And yes, I know that for the sake of michael@13: // consistency I should add [shift+tab] as opposite action... michael@13: document.addEventListener("keyup", function ( event ) { michael@13: if ( event.keyCode === 9 || ( event.keyCode >= 32 && event.keyCode <= 34 ) || (event.keyCode >= 37 && event.keyCode <= 40) ) { michael@13: switch( event.keyCode ) { michael@13: case 33: // pg up michael@13: case 37: // left michael@13: case 38: // up michael@13: api.prev(); michael@13: break; michael@13: case 9: // tab michael@13: case 32: // space michael@13: case 34: // pg down michael@13: case 39: // right michael@13: case 40: // down michael@13: api.next(); michael@13: break; michael@13: } michael@13: michael@13: event.preventDefault(); michael@13: } michael@13: }, false); michael@13: michael@13: // delegated handler for clicking on the links to presentation steps michael@13: document.addEventListener("click", function ( event ) { michael@13: // event delegation with "bubbling" michael@13: // check if event target (or any of its parents is a link) michael@13: var target = event.target; michael@13: while ( (target.tagName !== "A") && michael@13: (target !== document.documentElement) ) { michael@13: target = target.parentNode; michael@13: } michael@13: michael@13: if ( target.tagName === "A" ) { michael@13: var href = target.getAttribute("href"); michael@13: michael@13: // if it's a link to presentation step, target this step michael@13: if ( href && href[0] === '#' ) { michael@13: target = document.getElementById( href.slice(1) ); michael@13: } michael@13: } michael@13: michael@13: if ( api.goto(target) ) { michael@13: event.stopImmediatePropagation(); michael@13: event.preventDefault(); michael@13: } michael@13: }, false); michael@13: michael@13: // delegated handler for clicking on step elements michael@13: document.addEventListener("click", function ( event ) { michael@13: var target = event.target; michael@13: // find closest step element that is not active michael@13: while ( !(target.classList.contains("step") && !target.classList.contains("active")) && michael@13: (target !== document.documentElement) ) { michael@13: target = target.parentNode; michael@13: } michael@13: michael@13: if ( api.goto(target) ) { michael@13: event.preventDefault(); michael@13: } michael@13: }, false); michael@13: michael@13: // touch handler to detect taps on the left and right side of the screen michael@13: // based on awesome work of @hakimel: https://github.com/hakimel/reveal.js michael@13: document.addEventListener("touchstart", function ( event ) { michael@13: if (event.touches.length === 1) { michael@13: var x = event.touches[0].clientX, michael@13: width = window.innerWidth * 0.3, michael@13: result = null; michael@13: michael@13: if ( x < width ) { michael@13: result = api.prev(); michael@13: } else if ( x > window.innerWidth - width ) { michael@13: result = api.next(); michael@13: } michael@13: michael@13: if (result) { michael@13: event.preventDefault(); michael@13: } michael@13: } michael@13: }, false); michael@13: michael@13: // rescale presentation when window is resized michael@13: window.addEventListener("resize", throttle(function () { michael@13: // force going to active step again, to trigger rescaling michael@13: api.goto( document.querySelector(".active"), 500 ); michael@13: }, 250), false); michael@13: michael@13: }, false); michael@13: michael@13: })(document, window); michael@13: michael@13: // THAT'S ALL FOLKS! michael@13: // michael@13: // Thanks for reading it all. michael@13: // Or thanks for scrolling down and reading the last part. michael@13: // michael@13: // I've learnt a lot when building impress.js and I hope this code and comments michael@13: // will help somebody learn at least some part of it.