Mon, 07 May 2012 23:15:17 +0200
Add Enyo framework following discussion with Eva BRUCHERSEIFER.
michael@13 | 1 | /** |
michael@13 | 2 | * impress.js |
michael@13 | 3 | * |
michael@13 | 4 | * impress.js is a presentation tool based on the power of CSS3 transforms and transitions |
michael@13 | 5 | * in modern browsers and inspired by the idea behind prezi.com. |
michael@13 | 6 | * |
michael@13 | 7 | * |
michael@13 | 8 | * Copyright 2011-2012 Bartek Szopka (@bartaz) |
michael@13 | 9 | * |
michael@13 | 10 | * Released under the MIT and GPL Licenses. |
michael@13 | 11 | * |
michael@13 | 12 | * ------------------------------------------------ |
michael@13 | 13 | * author: Bartek Szopka |
michael@13 | 14 | * version: 0.5.3 |
michael@13 | 15 | * url: http://bartaz.github.com/impress.js/ |
michael@13 | 16 | * source: http://github.com/bartaz/impress.js/ |
michael@13 | 17 | */ |
michael@13 | 18 | |
michael@13 | 19 | /*jshint bitwise:true, curly:true, eqeqeq:true, forin:true, latedef:true, newcap:true, |
michael@13 | 20 | noarg:true, noempty:true, undef:true, strict:true, browser:true */ |
michael@13 | 21 | |
michael@13 | 22 | // You are one of those who like to know how thing work inside? |
michael@13 | 23 | // Let me show you the cogs that make impress.js run... |
michael@13 | 24 | (function ( document, window ) { |
michael@13 | 25 | 'use strict'; |
michael@13 | 26 | |
michael@13 | 27 | // HELPER FUNCTIONS |
michael@13 | 28 | |
michael@13 | 29 | // `pfx` is a function that takes a standard CSS property name as a parameter |
michael@13 | 30 | // and returns it's prefixed version valid for current browser it runs in. |
michael@13 | 31 | // The code is heavily inspired by Modernizr http://www.modernizr.com/ |
michael@13 | 32 | var pfx = (function () { |
michael@13 | 33 | |
michael@13 | 34 | var style = document.createElement('dummy').style, |
michael@13 | 35 | prefixes = 'Webkit Moz O ms Khtml'.split(' '), |
michael@13 | 36 | memory = {}; |
michael@13 | 37 | |
michael@13 | 38 | return function ( prop ) { |
michael@13 | 39 | if ( typeof memory[ prop ] === "undefined" ) { |
michael@13 | 40 | |
michael@13 | 41 | var ucProp = prop.charAt(0).toUpperCase() + prop.substr(1), |
michael@13 | 42 | props = (prop + ' ' + prefixes.join(ucProp + ' ') + ucProp).split(' '); |
michael@13 | 43 | |
michael@13 | 44 | memory[ prop ] = null; |
michael@13 | 45 | for ( var i in props ) { |
michael@13 | 46 | if ( style[ props[i] ] !== undefined ) { |
michael@13 | 47 | memory[ prop ] = props[i]; |
michael@13 | 48 | break; |
michael@13 | 49 | } |
michael@13 | 50 | } |
michael@13 | 51 | |
michael@13 | 52 | } |
michael@13 | 53 | |
michael@13 | 54 | return memory[ prop ]; |
michael@13 | 55 | }; |
michael@13 | 56 | |
michael@13 | 57 | })(); |
michael@13 | 58 | |
michael@13 | 59 | // `arraify` takes an array-like object and turns it into real Array |
michael@13 | 60 | // to make all the Array.prototype goodness available. |
michael@13 | 61 | var arrayify = function ( a ) { |
michael@13 | 62 | return [].slice.call( a ); |
michael@13 | 63 | }; |
michael@13 | 64 | |
michael@13 | 65 | // `css` function applies the styles given in `props` object to the element |
michael@13 | 66 | // given as `el`. It runs all property names through `pfx` function to make |
michael@13 | 67 | // sure proper prefixed version of the property is used. |
michael@13 | 68 | var css = function ( el, props ) { |
michael@13 | 69 | var key, pkey; |
michael@13 | 70 | for ( key in props ) { |
michael@13 | 71 | if ( props.hasOwnProperty(key) ) { |
michael@13 | 72 | pkey = pfx(key); |
michael@13 | 73 | if ( pkey !== null ) { |
michael@13 | 74 | el.style[pkey] = props[key]; |
michael@13 | 75 | } |
michael@13 | 76 | } |
michael@13 | 77 | } |
michael@13 | 78 | return el; |
michael@13 | 79 | }; |
michael@13 | 80 | |
michael@13 | 81 | // `toNumber` takes a value given as `numeric` parameter and tries to turn |
michael@13 | 82 | // it into a number. If it is not possible it returns 0 (or other value |
michael@13 | 83 | // given as `fallback`). |
michael@13 | 84 | var toNumber = function (numeric, fallback) { |
michael@13 | 85 | return isNaN(numeric) ? (fallback || 0) : Number(numeric); |
michael@13 | 86 | }; |
michael@13 | 87 | |
michael@13 | 88 | // `byId` returns element with given `id` - you probably have guessed that ;) |
michael@13 | 89 | var byId = function ( id ) { |
michael@13 | 90 | return document.getElementById(id); |
michael@13 | 91 | }; |
michael@13 | 92 | |
michael@13 | 93 | // `$` returns first element for given CSS `selector` in the `context` of |
michael@13 | 94 | // the given element or whole document. |
michael@13 | 95 | var $ = function ( selector, context ) { |
michael@13 | 96 | context = context || document; |
michael@13 | 97 | return context.querySelector(selector); |
michael@13 | 98 | }; |
michael@13 | 99 | |
michael@13 | 100 | // `$$` return an array of elements for given CSS `selector` in the `context` of |
michael@13 | 101 | // the given element or whole document. |
michael@13 | 102 | var $$ = function ( selector, context ) { |
michael@13 | 103 | context = context || document; |
michael@13 | 104 | return arrayify( context.querySelectorAll(selector) ); |
michael@13 | 105 | }; |
michael@13 | 106 | |
michael@13 | 107 | // `triggerEvent` builds a custom DOM event with given `eventName` and `detail` data |
michael@13 | 108 | // and triggers it on element given as `el`. |
michael@13 | 109 | var triggerEvent = function (el, eventName, detail) { |
michael@13 | 110 | var event = document.createEvent("CustomEvent"); |
michael@13 | 111 | event.initCustomEvent(eventName, true, true, detail); |
michael@13 | 112 | el.dispatchEvent(event); |
michael@13 | 113 | }; |
michael@13 | 114 | |
michael@13 | 115 | // `translate` builds a translate transform string for given data. |
michael@13 | 116 | var translate = function ( t ) { |
michael@13 | 117 | return " translate3d(" + t.x + "px," + t.y + "px," + t.z + "px) "; |
michael@13 | 118 | }; |
michael@13 | 119 | |
michael@13 | 120 | // `rotate` builds a rotate transform string for given data. |
michael@13 | 121 | // By default the rotations are in X Y Z order that can be reverted by passing `true` |
michael@13 | 122 | // as second parameter. |
michael@13 | 123 | var rotate = function ( r, revert ) { |
michael@13 | 124 | var rX = " rotateX(" + r.x + "deg) ", |
michael@13 | 125 | rY = " rotateY(" + r.y + "deg) ", |
michael@13 | 126 | rZ = " rotateZ(" + r.z + "deg) "; |
michael@13 | 127 | |
michael@13 | 128 | return revert ? rZ+rY+rX : rX+rY+rZ; |
michael@13 | 129 | }; |
michael@13 | 130 | |
michael@13 | 131 | // `scale` builds a scale transform string for given data. |
michael@13 | 132 | var scale = function ( s ) { |
michael@13 | 133 | return " scale(" + s + ") "; |
michael@13 | 134 | }; |
michael@13 | 135 | |
michael@13 | 136 | // `perspective` builds a perspective transform string for given data. |
michael@13 | 137 | var perspective = function ( p ) { |
michael@13 | 138 | return " perspective(" + p + "px) "; |
michael@13 | 139 | }; |
michael@13 | 140 | |
michael@13 | 141 | // `getElementFromHash` returns an element located by id from hash part of |
michael@13 | 142 | // window location. |
michael@13 | 143 | var getElementFromHash = function () { |
michael@13 | 144 | // get id from url # by removing `#` or `#/` from the beginning, |
michael@13 | 145 | // so both "fallback" `#slide-id` and "enhanced" `#/slide-id` will work |
michael@13 | 146 | return byId( window.location.hash.replace(/^#\/?/,"") ); |
michael@13 | 147 | }; |
michael@13 | 148 | |
michael@13 | 149 | // `computeWindowScale` counts the scale factor between window size and size |
michael@13 | 150 | // defined for the presentation in the config. |
michael@13 | 151 | var computeWindowScale = function ( config ) { |
michael@13 | 152 | var hScale = window.innerHeight / config.height, |
michael@13 | 153 | wScale = window.innerWidth / config.width, |
michael@13 | 154 | scale = hScale > wScale ? wScale : hScale; |
michael@13 | 155 | |
michael@13 | 156 | if (config.maxScale && scale > config.maxScale) { |
michael@13 | 157 | scale = config.maxScale; |
michael@13 | 158 | } |
michael@13 | 159 | |
michael@13 | 160 | if (config.minScale && scale < config.minScale) { |
michael@13 | 161 | scale = config.minScale; |
michael@13 | 162 | } |
michael@13 | 163 | |
michael@13 | 164 | return scale; |
michael@13 | 165 | }; |
michael@13 | 166 | |
michael@13 | 167 | // CHECK SUPPORT |
michael@13 | 168 | var body = document.body; |
michael@13 | 169 | |
michael@13 | 170 | var ua = navigator.userAgent.toLowerCase(); |
michael@13 | 171 | var impressSupported = |
michael@13 | 172 | // browser should support CSS 3D transtorms |
michael@13 | 173 | ( pfx("perspective") !== null ) && |
michael@13 | 174 | |
michael@13 | 175 | // and `classList` and `dataset` APIs |
michael@13 | 176 | ( body.classList ) && |
michael@13 | 177 | ( body.dataset ) && |
michael@13 | 178 | |
michael@13 | 179 | // but some mobile devices need to be blacklisted, |
michael@13 | 180 | // because their CSS 3D support or hardware is not |
michael@13 | 181 | // good enough to run impress.js properly, sorry... |
michael@13 | 182 | ( ua.search(/(iphone)|(ipod)|(android)/) === -1 ); |
michael@13 | 183 | |
michael@13 | 184 | if (!impressSupported) { |
michael@13 | 185 | // we can't be sure that `classList` is supported |
michael@13 | 186 | body.className += " impress-not-supported "; |
michael@13 | 187 | } else { |
michael@13 | 188 | body.classList.remove("impress-not-supported"); |
michael@13 | 189 | body.classList.add("impress-supported"); |
michael@13 | 190 | } |
michael@13 | 191 | |
michael@13 | 192 | // GLOBALS AND DEFAULTS |
michael@13 | 193 | |
michael@13 | 194 | // This is were the root elements of all impress.js instances will be kept. |
michael@13 | 195 | // Yes, this means you can have more than one instance on a page, but I'm not |
michael@13 | 196 | // sure if it makes any sense in practice ;) |
michael@13 | 197 | var roots = {}; |
michael@13 | 198 | |
michael@13 | 199 | // some default config values. |
michael@13 | 200 | var defaults = { |
michael@13 | 201 | width: 1024, |
michael@13 | 202 | height: 768, |
michael@13 | 203 | maxScale: 1, |
michael@13 | 204 | minScale: 0, |
michael@13 | 205 | |
michael@13 | 206 | perspective: 1000, |
michael@13 | 207 | |
michael@13 | 208 | transitionDuration: 1000 |
michael@13 | 209 | }; |
michael@13 | 210 | |
michael@13 | 211 | // it's just an empty function ... and a useless comment. |
michael@13 | 212 | var empty = function () { return false; }; |
michael@13 | 213 | |
michael@13 | 214 | // IMPRESS.JS API |
michael@13 | 215 | |
michael@13 | 216 | // And that's where interesting things will start to happen. |
michael@13 | 217 | // It's the core `impress` function that returns the impress.js API |
michael@13 | 218 | // for a presentation based on the element with given id ('impress' |
michael@13 | 219 | // by default). |
michael@13 | 220 | var impress = window.impress = function ( rootId ) { |
michael@13 | 221 | |
michael@13 | 222 | // If impress.js is not supported by the browser return a dummy API |
michael@13 | 223 | // it may not be a perfect solution but we return early and avoid |
michael@13 | 224 | // running code that may use features not implemented in the browser. |
michael@13 | 225 | if (!impressSupported) { |
michael@13 | 226 | return { |
michael@13 | 227 | init: empty, |
michael@13 | 228 | goto: empty, |
michael@13 | 229 | prev: empty, |
michael@13 | 230 | next: empty |
michael@13 | 231 | }; |
michael@13 | 232 | } |
michael@13 | 233 | |
michael@13 | 234 | rootId = rootId || "impress"; |
michael@13 | 235 | |
michael@13 | 236 | // if given root is already initialized just return the API |
michael@13 | 237 | if (roots["impress-root-" + rootId]) { |
michael@13 | 238 | return roots["impress-root-" + rootId]; |
michael@13 | 239 | } |
michael@13 | 240 | |
michael@13 | 241 | // data of all presentation steps |
michael@13 | 242 | var stepsData = {}; |
michael@13 | 243 | |
michael@13 | 244 | // element of currently active step |
michael@13 | 245 | var activeStep = null; |
michael@13 | 246 | |
michael@13 | 247 | // current state (position, rotation and scale) of the presentation |
michael@13 | 248 | var currentState = null; |
michael@13 | 249 | |
michael@13 | 250 | // array of step elements |
michael@13 | 251 | var steps = null; |
michael@13 | 252 | |
michael@13 | 253 | // configuration options |
michael@13 | 254 | var config = null; |
michael@13 | 255 | |
michael@13 | 256 | // scale factor of the browser window |
michael@13 | 257 | var windowScale = null; |
michael@13 | 258 | |
michael@13 | 259 | // root presentation elements |
michael@13 | 260 | var root = byId( rootId ); |
michael@13 | 261 | var canvas = document.createElement("div"); |
michael@13 | 262 | |
michael@13 | 263 | var initialized = false; |
michael@13 | 264 | |
michael@13 | 265 | // STEP EVENTS |
michael@13 | 266 | // |
michael@13 | 267 | // There are currently two step events triggered by impress.js |
michael@13 | 268 | // `impress:stepenter` is triggered when the step is shown on the |
michael@13 | 269 | // screen (the transition from the previous one is finished) and |
michael@13 | 270 | // `impress:stepleave` is triggered when the step is left (the |
michael@13 | 271 | // transition to next step just starts). |
michael@13 | 272 | |
michael@13 | 273 | // reference to last entered step |
michael@13 | 274 | var lastEntered = null; |
michael@13 | 275 | |
michael@13 | 276 | // `onStepEnter` is called whenever the step element is entered |
michael@13 | 277 | // but the event is triggered only if the step is different than |
michael@13 | 278 | // last entered step. |
michael@13 | 279 | var onStepEnter = function (step) { |
michael@13 | 280 | if (lastEntered !== step) { |
michael@13 | 281 | triggerEvent(step, "impress:stepenter"); |
michael@13 | 282 | lastEntered = step; |
michael@13 | 283 | } |
michael@13 | 284 | }; |
michael@13 | 285 | |
michael@13 | 286 | // `onStepLeave` is called whenever the step element is left |
michael@13 | 287 | // but the event is triggered only if the step is the same as |
michael@13 | 288 | // last entered step. |
michael@13 | 289 | var onStepLeave = function (step) { |
michael@13 | 290 | if (lastEntered === step) { |
michael@13 | 291 | triggerEvent(step, "impress:stepleave"); |
michael@13 | 292 | lastEntered = null; |
michael@13 | 293 | } |
michael@13 | 294 | }; |
michael@13 | 295 | |
michael@13 | 296 | // `initStep` initializes given step element by reading data from its |
michael@13 | 297 | // data attributes and setting correct styles. |
michael@13 | 298 | var initStep = function ( el, idx ) { |
michael@13 | 299 | var data = el.dataset, |
michael@13 | 300 | step = { |
michael@13 | 301 | translate: { |
michael@13 | 302 | x: toNumber(data.x), |
michael@13 | 303 | y: toNumber(data.y), |
michael@13 | 304 | z: toNumber(data.z) |
michael@13 | 305 | }, |
michael@13 | 306 | rotate: { |
michael@13 | 307 | x: toNumber(data.rotateX), |
michael@13 | 308 | y: toNumber(data.rotateY), |
michael@13 | 309 | z: toNumber(data.rotateZ || data.rotate) |
michael@13 | 310 | }, |
michael@13 | 311 | scale: toNumber(data.scale, 1), |
michael@13 | 312 | el: el |
michael@13 | 313 | }; |
michael@13 | 314 | |
michael@13 | 315 | if ( !el.id ) { |
michael@13 | 316 | el.id = "step-" + (idx + 1); |
michael@13 | 317 | } |
michael@13 | 318 | |
michael@13 | 319 | stepsData["impress-" + el.id] = step; |
michael@13 | 320 | |
michael@13 | 321 | css(el, { |
michael@13 | 322 | position: "absolute", |
michael@13 | 323 | transform: "translate(-50%,-50%)" + |
michael@13 | 324 | translate(step.translate) + |
michael@13 | 325 | rotate(step.rotate) + |
michael@13 | 326 | scale(step.scale), |
michael@13 | 327 | transformStyle: "preserve-3d" |
michael@13 | 328 | }); |
michael@13 | 329 | }; |
michael@13 | 330 | |
michael@13 | 331 | // `init` API function that initializes (and runs) the presentation. |
michael@13 | 332 | var init = function () { |
michael@13 | 333 | if (initialized) { return; } |
michael@13 | 334 | |
michael@13 | 335 | // First we set up the viewport for mobile devices. |
michael@13 | 336 | // For some reason iPad goes nuts when it is not done properly. |
michael@13 | 337 | var meta = $("meta[name='viewport']") || document.createElement("meta"); |
michael@13 | 338 | meta.content = "width=device-width, minimum-scale=1, maximum-scale=1, user-scalable=no"; |
michael@13 | 339 | if (meta.parentNode !== document.head) { |
michael@13 | 340 | meta.name = 'viewport'; |
michael@13 | 341 | document.head.appendChild(meta); |
michael@13 | 342 | } |
michael@13 | 343 | |
michael@13 | 344 | // initialize configuration object |
michael@13 | 345 | var rootData = root.dataset; |
michael@13 | 346 | config = { |
michael@13 | 347 | width: toNumber( rootData.width, defaults.width ), |
michael@13 | 348 | height: toNumber( rootData.height, defaults.height ), |
michael@13 | 349 | maxScale: toNumber( rootData.maxScale, defaults.maxScale ), |
michael@13 | 350 | minScale: toNumber( rootData.minScale, defaults.minScale ), |
michael@13 | 351 | perspective: toNumber( rootData.perspective, defaults.perspective ), |
michael@13 | 352 | transitionDuration: toNumber( rootData.transitionDuration, defaults.transitionDuration ) |
michael@13 | 353 | }; |
michael@13 | 354 | |
michael@13 | 355 | windowScale = computeWindowScale( config ); |
michael@13 | 356 | |
michael@13 | 357 | // wrap steps with "canvas" element |
michael@13 | 358 | arrayify( root.childNodes ).forEach(function ( el ) { |
michael@13 | 359 | canvas.appendChild( el ); |
michael@13 | 360 | }); |
michael@13 | 361 | root.appendChild(canvas); |
michael@13 | 362 | |
michael@13 | 363 | // set initial styles |
michael@13 | 364 | document.documentElement.style.height = "100%"; |
michael@13 | 365 | |
michael@13 | 366 | css(body, { |
michael@13 | 367 | height: "100%", |
michael@13 | 368 | overflow: "hidden" |
michael@13 | 369 | }); |
michael@13 | 370 | |
michael@13 | 371 | var rootStyles = { |
michael@13 | 372 | position: "absolute", |
michael@13 | 373 | transformOrigin: "top left", |
michael@13 | 374 | transition: "all 0s ease-in-out", |
michael@13 | 375 | transformStyle: "preserve-3d" |
michael@13 | 376 | }; |
michael@13 | 377 | |
michael@13 | 378 | css(root, rootStyles); |
michael@13 | 379 | css(root, { |
michael@13 | 380 | top: "50%", |
michael@13 | 381 | left: "50%", |
michael@13 | 382 | transform: perspective( config.perspective/windowScale ) + scale( windowScale ) |
michael@13 | 383 | }); |
michael@13 | 384 | css(canvas, rootStyles); |
michael@13 | 385 | |
michael@13 | 386 | body.classList.remove("impress-disabled"); |
michael@13 | 387 | body.classList.add("impress-enabled"); |
michael@13 | 388 | |
michael@13 | 389 | // get and init steps |
michael@13 | 390 | steps = $$(".step", root); |
michael@13 | 391 | steps.forEach( initStep ); |
michael@13 | 392 | |
michael@13 | 393 | // set a default initial state of the canvas |
michael@13 | 394 | currentState = { |
michael@13 | 395 | translate: { x: 0, y: 0, z: 0 }, |
michael@13 | 396 | rotate: { x: 0, y: 0, z: 0 }, |
michael@13 | 397 | scale: 1 |
michael@13 | 398 | }; |
michael@13 | 399 | |
michael@13 | 400 | initialized = true; |
michael@13 | 401 | |
michael@13 | 402 | triggerEvent(root, "impress:init", { api: roots[ "impress-root-" + rootId ] }); |
michael@13 | 403 | }; |
michael@13 | 404 | |
michael@13 | 405 | // `getStep` is a helper function that returns a step element defined by parameter. |
michael@13 | 406 | // If a number is given, step with index given by the number is returned, if a string |
michael@13 | 407 | // is given step element with such id is returned, if DOM element is given it is returned |
michael@13 | 408 | // if it is a correct step element. |
michael@13 | 409 | var getStep = function ( step ) { |
michael@13 | 410 | if (typeof step === "number") { |
michael@13 | 411 | step = step < 0 ? steps[ steps.length + step] : steps[ step ]; |
michael@13 | 412 | } else if (typeof step === "string") { |
michael@13 | 413 | step = byId(step); |
michael@13 | 414 | } |
michael@13 | 415 | return (step && step.id && stepsData["impress-" + step.id]) ? step : null; |
michael@13 | 416 | }; |
michael@13 | 417 | |
michael@13 | 418 | // used to reset timeout for `impress:stepenter` event |
michael@13 | 419 | var stepEnterTimeout = null; |
michael@13 | 420 | |
michael@13 | 421 | // `goto` API function that moves to step given with `el` parameter (by index, id or element), |
michael@13 | 422 | // with a transition `duration` optionally given as second parameter. |
michael@13 | 423 | var goto = function ( el, duration ) { |
michael@13 | 424 | |
michael@13 | 425 | if ( !initialized || !(el = getStep(el)) ) { |
michael@13 | 426 | // presentation not initialized or given element is not a step |
michael@13 | 427 | return false; |
michael@13 | 428 | } |
michael@13 | 429 | |
michael@13 | 430 | // Sometimes it's possible to trigger focus on first link with some keyboard action. |
michael@13 | 431 | // Browser in such a case tries to scroll the page to make this element visible |
michael@13 | 432 | // (even that body overflow is set to hidden) and it breaks our careful positioning. |
michael@13 | 433 | // |
michael@13 | 434 | // So, as a lousy (and lazy) workaround we will make the page scroll back to the top |
michael@13 | 435 | // whenever slide is selected |
michael@13 | 436 | // |
michael@13 | 437 | // If you are reading this and know any better way to handle it, I'll be glad to hear about it! |
michael@13 | 438 | window.scrollTo(0, 0); |
michael@13 | 439 | |
michael@13 | 440 | var step = stepsData["impress-" + el.id]; |
michael@13 | 441 | |
michael@13 | 442 | if ( activeStep ) { |
michael@13 | 443 | activeStep.classList.remove("active"); |
michael@13 | 444 | body.classList.remove("impress-on-" + activeStep.id); |
michael@13 | 445 | } |
michael@13 | 446 | el.classList.add("active"); |
michael@13 | 447 | |
michael@13 | 448 | body.classList.add("impress-on-" + el.id); |
michael@13 | 449 | |
michael@13 | 450 | // compute target state of the canvas based on given step |
michael@13 | 451 | var target = { |
michael@13 | 452 | rotate: { |
michael@13 | 453 | x: -step.rotate.x, |
michael@13 | 454 | y: -step.rotate.y, |
michael@13 | 455 | z: -step.rotate.z |
michael@13 | 456 | }, |
michael@13 | 457 | translate: { |
michael@13 | 458 | x: -step.translate.x, |
michael@13 | 459 | y: -step.translate.y, |
michael@13 | 460 | z: -step.translate.z |
michael@13 | 461 | }, |
michael@13 | 462 | scale: 1 / step.scale |
michael@13 | 463 | }; |
michael@13 | 464 | |
michael@13 | 465 | // Check if the transition is zooming in or not. |
michael@13 | 466 | // |
michael@13 | 467 | // This information is used to alter the transition style: |
michael@13 | 468 | // when we are zooming in - we start with move and rotate transition |
michael@13 | 469 | // and the scaling is delayed, but when we are zooming out we start |
michael@13 | 470 | // with scaling down and move and rotation are delayed. |
michael@13 | 471 | var zoomin = target.scale >= currentState.scale; |
michael@13 | 472 | |
michael@13 | 473 | duration = toNumber(duration, config.transitionDuration); |
michael@13 | 474 | var delay = (duration / 2); |
michael@13 | 475 | |
michael@13 | 476 | // if the same step is re-selected, force computing window scaling, |
michael@13 | 477 | // because it is likely to be caused by window resize |
michael@13 | 478 | if (el === activeStep) { |
michael@13 | 479 | windowScale = computeWindowScale(config); |
michael@13 | 480 | } |
michael@13 | 481 | |
michael@13 | 482 | var targetScale = target.scale * windowScale; |
michael@13 | 483 | |
michael@13 | 484 | // trigger leave of currently active element (if it's not the same step again) |
michael@13 | 485 | if (activeStep && activeStep !== el) { |
michael@13 | 486 | onStepLeave(activeStep); |
michael@13 | 487 | } |
michael@13 | 488 | |
michael@13 | 489 | // Now we alter transforms of `root` and `canvas` to trigger transitions. |
michael@13 | 490 | // |
michael@13 | 491 | // And here is why there are two elements: `root` and `canvas` - they are |
michael@13 | 492 | // being animated separately: |
michael@13 | 493 | // `root` is used for scaling and `canvas` for translate and rotations. |
michael@13 | 494 | // Transitions on them are triggered with different delays (to make |
michael@13 | 495 | // visually nice and 'natural' looking transitions), so we need to know |
michael@13 | 496 | // that both of them are finished. |
michael@13 | 497 | css(root, { |
michael@13 | 498 | // to keep the perspective look similar for different scales |
michael@13 | 499 | // we need to 'scale' the perspective, too |
michael@13 | 500 | transform: perspective( config.perspective / targetScale ) + scale( targetScale ), |
michael@13 | 501 | transitionDuration: duration + "ms", |
michael@13 | 502 | transitionDelay: (zoomin ? delay : 0) + "ms" |
michael@13 | 503 | }); |
michael@13 | 504 | |
michael@13 | 505 | css(canvas, { |
michael@13 | 506 | transform: rotate(target.rotate, true) + translate(target.translate), |
michael@13 | 507 | transitionDuration: duration + "ms", |
michael@13 | 508 | transitionDelay: (zoomin ? 0 : delay) + "ms" |
michael@13 | 509 | }); |
michael@13 | 510 | |
michael@13 | 511 | // Here is a tricky part... |
michael@13 | 512 | // |
michael@13 | 513 | // If there is no change in scale or no change in rotation and translation, it means there was actually |
michael@13 | 514 | // no delay - because there was no transition on `root` or `canvas` elements. |
michael@13 | 515 | // We want to trigger `impress:stepenter` event in the correct moment, so here we compare the current |
michael@13 | 516 | // and target values to check if delay should be taken into account. |
michael@13 | 517 | // |
michael@13 | 518 | // I know that this `if` statement looks scary, but it's pretty simple when you know what is going on |
michael@13 | 519 | // - it's simply comparing all the values. |
michael@13 | 520 | if ( currentState.scale === target.scale || |
michael@13 | 521 | (currentState.rotate.x === target.rotate.x && currentState.rotate.y === target.rotate.y && |
michael@13 | 522 | currentState.rotate.z === target.rotate.z && currentState.translate.x === target.translate.x && |
michael@13 | 523 | currentState.translate.y === target.translate.y && currentState.translate.z === target.translate.z) ) { |
michael@13 | 524 | delay = 0; |
michael@13 | 525 | } |
michael@13 | 526 | |
michael@13 | 527 | // store current state |
michael@13 | 528 | currentState = target; |
michael@13 | 529 | activeStep = el; |
michael@13 | 530 | |
michael@13 | 531 | // And here is where we trigger `impress:stepenter` event. |
michael@13 | 532 | // We simply set up a timeout to fire it taking transition duration (and possible delay) into account. |
michael@13 | 533 | // |
michael@13 | 534 | // I really wanted to make it in more elegant way. The `transitionend` event seemed to be the best way |
michael@13 | 535 | // to do it, but the fact that I'm using transitions on two separate elements and that the `transitionend` |
michael@13 | 536 | // event is only triggered when there was a transition (change in the values) caused some bugs and |
michael@13 | 537 | // made the code really complicated, cause I had to handle all the conditions separately. And it still |
michael@13 | 538 | // needed a `setTimeout` fallback for the situations when there is no transition at all. |
michael@13 | 539 | // So I decided that I'd rather make the code simpler than use shiny new `transitionend`. |
michael@13 | 540 | // |
michael@13 | 541 | // If you want learn something interesting and see how it was done with `transitionend` go back to |
michael@13 | 542 | // version 0.5.2 of impress.js: http://github.com/bartaz/impress.js/blob/0.5.2/js/impress.js |
michael@13 | 543 | window.clearTimeout(stepEnterTimeout); |
michael@13 | 544 | stepEnterTimeout = window.setTimeout(function() { |
michael@13 | 545 | onStepEnter(activeStep); |
michael@13 | 546 | }, duration + delay); |
michael@13 | 547 | |
michael@13 | 548 | return el; |
michael@13 | 549 | }; |
michael@13 | 550 | |
michael@13 | 551 | // `prev` API function goes to previous step (in document order) |
michael@13 | 552 | var prev = function () { |
michael@13 | 553 | var prev = steps.indexOf( activeStep ) - 1; |
michael@13 | 554 | prev = prev >= 0 ? steps[ prev ] : steps[ steps.length-1 ]; |
michael@13 | 555 | |
michael@13 | 556 | return goto(prev); |
michael@13 | 557 | }; |
michael@13 | 558 | |
michael@13 | 559 | // `next` API function goes to next step (in document order) |
michael@13 | 560 | var next = function () { |
michael@13 | 561 | var next = steps.indexOf( activeStep ) + 1; |
michael@13 | 562 | next = next < steps.length ? steps[ next ] : steps[ 0 ]; |
michael@13 | 563 | |
michael@13 | 564 | return goto(next); |
michael@13 | 565 | }; |
michael@13 | 566 | |
michael@13 | 567 | // Adding some useful classes to step elements. |
michael@13 | 568 | // |
michael@13 | 569 | // All the steps that have not been shown yet are given `future` class. |
michael@13 | 570 | // When the step is entered the `future` class is removed and the `present` |
michael@13 | 571 | // class is given. When the step is left `present` class is replaced with |
michael@13 | 572 | // `past` class. |
michael@13 | 573 | // |
michael@13 | 574 | // So every step element is always in one of three possible states: |
michael@13 | 575 | // `future`, `present` and `past`. |
michael@13 | 576 | // |
michael@13 | 577 | // There classes can be used in CSS to style different types of steps. |
michael@13 | 578 | // For example the `present` class can be used to trigger some custom |
michael@13 | 579 | // animations when step is shown. |
michael@13 | 580 | root.addEventListener("impress:init", function(){ |
michael@13 | 581 | // STEP CLASSES |
michael@13 | 582 | steps.forEach(function (step) { |
michael@13 | 583 | step.classList.add("future"); |
michael@13 | 584 | }); |
michael@13 | 585 | |
michael@13 | 586 | root.addEventListener("impress:stepenter", function (event) { |
michael@13 | 587 | event.target.classList.remove("past"); |
michael@13 | 588 | event.target.classList.remove("future"); |
michael@13 | 589 | event.target.classList.add("present"); |
michael@13 | 590 | }, false); |
michael@13 | 591 | |
michael@13 | 592 | root.addEventListener("impress:stepleave", function (event) { |
michael@13 | 593 | event.target.classList.remove("present"); |
michael@13 | 594 | event.target.classList.add("past"); |
michael@13 | 595 | }, false); |
michael@13 | 596 | |
michael@13 | 597 | }, false); |
michael@13 | 598 | |
michael@13 | 599 | // Adding hash change support. |
michael@13 | 600 | root.addEventListener("impress:init", function(){ |
michael@13 | 601 | |
michael@13 | 602 | // last hash detected |
michael@13 | 603 | var lastHash = ""; |
michael@13 | 604 | |
michael@13 | 605 | // `#/step-id` is used instead of `#step-id` to prevent default browser |
michael@13 | 606 | // scrolling to element in hash. |
michael@13 | 607 | // |
michael@13 | 608 | // And it has to be set after animation finishes, because in Chrome it |
michael@13 | 609 | // makes transtion laggy. |
michael@13 | 610 | // BUG: http://code.google.com/p/chromium/issues/detail?id=62820 |
michael@13 | 611 | root.addEventListener("impress:stepenter", function (event) { |
michael@13 | 612 | window.location.hash = lastHash = "#/" + event.target.id; |
michael@13 | 613 | }, false); |
michael@13 | 614 | |
michael@13 | 615 | window.addEventListener("hashchange", function () { |
michael@13 | 616 | // When the step is entered hash in the location is updated |
michael@13 | 617 | // (just few lines above from here), so the hash change is |
michael@13 | 618 | // triggered and we would call `goto` again on the same element. |
michael@13 | 619 | // |
michael@13 | 620 | // To avoid this we store last entered hash and compare. |
michael@13 | 621 | if (window.location.hash !== lastHash) { |
michael@13 | 622 | goto( getElementFromHash() ); |
michael@13 | 623 | } |
michael@13 | 624 | }, false); |
michael@13 | 625 | |
michael@13 | 626 | // START |
michael@13 | 627 | // by selecting step defined in url or first step of the presentation |
michael@13 | 628 | goto(getElementFromHash() || steps[0], 0); |
michael@13 | 629 | }, false); |
michael@13 | 630 | |
michael@13 | 631 | body.classList.add("impress-disabled"); |
michael@13 | 632 | |
michael@13 | 633 | // store and return API for given impress.js root element |
michael@13 | 634 | return (roots[ "impress-root-" + rootId ] = { |
michael@13 | 635 | init: init, |
michael@13 | 636 | goto: goto, |
michael@13 | 637 | next: next, |
michael@13 | 638 | prev: prev |
michael@13 | 639 | }); |
michael@13 | 640 | |
michael@13 | 641 | }; |
michael@13 | 642 | |
michael@13 | 643 | // flag that can be used in JS to check if browser have passed the support test |
michael@13 | 644 | impress.supported = impressSupported; |
michael@13 | 645 | |
michael@13 | 646 | })(document, window); |
michael@13 | 647 | |
michael@13 | 648 | // NAVIGATION EVENTS |
michael@13 | 649 | |
michael@13 | 650 | // As you can see this part is separate from the impress.js core code. |
michael@13 | 651 | // It's because these navigation actions only need what impress.js provides with |
michael@13 | 652 | // its simple API. |
michael@13 | 653 | // |
michael@13 | 654 | // In future I think about moving it to make them optional, move to separate files |
michael@13 | 655 | // and treat more like a 'plugins'. |
michael@13 | 656 | (function ( document, window ) { |
michael@13 | 657 | 'use strict'; |
michael@13 | 658 | |
michael@13 | 659 | // throttling function calls, by Remy Sharp |
michael@13 | 660 | // http://remysharp.com/2010/07/21/throttling-function-calls/ |
michael@13 | 661 | var throttle = function (fn, delay) { |
michael@13 | 662 | var timer = null; |
michael@13 | 663 | return function () { |
michael@13 | 664 | var context = this, args = arguments; |
michael@13 | 665 | clearTimeout(timer); |
michael@13 | 666 | timer = setTimeout(function () { |
michael@13 | 667 | fn.apply(context, args); |
michael@13 | 668 | }, delay); |
michael@13 | 669 | }; |
michael@13 | 670 | }; |
michael@13 | 671 | |
michael@13 | 672 | // wait for impress.js to be initialized |
michael@13 | 673 | document.addEventListener("impress:init", function (event) { |
michael@13 | 674 | // Getting API from event data. |
michael@13 | 675 | // So you don't event need to know what is the id of the root element |
michael@13 | 676 | // or anything. `impress:init` event data gives you everything you |
michael@13 | 677 | // need to control the presentation that was just initialized. |
michael@13 | 678 | var api = event.detail.api; |
michael@13 | 679 | |
michael@13 | 680 | // KEYBOARD NAVIGATION HANDLERS |
michael@13 | 681 | |
michael@13 | 682 | // Prevent default keydown action when one of supported key is pressed. |
michael@13 | 683 | document.addEventListener("keydown", function ( event ) { |
michael@13 | 684 | if ( event.keyCode === 9 || ( event.keyCode >= 32 && event.keyCode <= 34 ) || (event.keyCode >= 37 && event.keyCode <= 40) ) { |
michael@13 | 685 | event.preventDefault(); |
michael@13 | 686 | } |
michael@13 | 687 | }, false); |
michael@13 | 688 | |
michael@13 | 689 | // Trigger impress action (next or prev) on keyup. |
michael@13 | 690 | |
michael@13 | 691 | // Supported keys are: |
michael@13 | 692 | // [space] - quite common in presentation software to move forward |
michael@13 | 693 | // [up] [right] / [down] [left] - again common and natural addition, |
michael@13 | 694 | // [pgdown] / [pgup] - often triggered by remote controllers, |
michael@13 | 695 | // [tab] - this one is quite controversial, but the reason it ended up on |
michael@13 | 696 | // this list is quite an interesting story... Remember that strange part |
michael@13 | 697 | // in the impress.js code where window is scrolled to 0,0 on every presentation |
michael@13 | 698 | // step, because sometimes browser scrolls viewport because of the focused element? |
michael@13 | 699 | // Well, the [tab] key by default navigates around focusable elements, so clicking |
michael@13 | 700 | // it very often caused scrolling to focused element and breaking impress.js |
michael@13 | 701 | // positioning. I didn't want to just prevent this default action, so I used [tab] |
michael@13 | 702 | // as another way to moving to next step... And yes, I know that for the sake of |
michael@13 | 703 | // consistency I should add [shift+tab] as opposite action... |
michael@13 | 704 | document.addEventListener("keyup", function ( event ) { |
michael@13 | 705 | if ( event.keyCode === 9 || ( event.keyCode >= 32 && event.keyCode <= 34 ) || (event.keyCode >= 37 && event.keyCode <= 40) ) { |
michael@13 | 706 | switch( event.keyCode ) { |
michael@13 | 707 | case 33: // pg up |
michael@13 | 708 | case 37: // left |
michael@13 | 709 | case 38: // up |
michael@13 | 710 | api.prev(); |
michael@13 | 711 | break; |
michael@13 | 712 | case 9: // tab |
michael@13 | 713 | case 32: // space |
michael@13 | 714 | case 34: // pg down |
michael@13 | 715 | case 39: // right |
michael@13 | 716 | case 40: // down |
michael@13 | 717 | api.next(); |
michael@13 | 718 | break; |
michael@13 | 719 | } |
michael@13 | 720 | |
michael@13 | 721 | event.preventDefault(); |
michael@13 | 722 | } |
michael@13 | 723 | }, false); |
michael@13 | 724 | |
michael@13 | 725 | // delegated handler for clicking on the links to presentation steps |
michael@13 | 726 | document.addEventListener("click", function ( event ) { |
michael@13 | 727 | // event delegation with "bubbling" |
michael@13 | 728 | // check if event target (or any of its parents is a link) |
michael@13 | 729 | var target = event.target; |
michael@13 | 730 | while ( (target.tagName !== "A") && |
michael@13 | 731 | (target !== document.documentElement) ) { |
michael@13 | 732 | target = target.parentNode; |
michael@13 | 733 | } |
michael@13 | 734 | |
michael@13 | 735 | if ( target.tagName === "A" ) { |
michael@13 | 736 | var href = target.getAttribute("href"); |
michael@13 | 737 | |
michael@13 | 738 | // if it's a link to presentation step, target this step |
michael@13 | 739 | if ( href && href[0] === '#' ) { |
michael@13 | 740 | target = document.getElementById( href.slice(1) ); |
michael@13 | 741 | } |
michael@13 | 742 | } |
michael@13 | 743 | |
michael@13 | 744 | if ( api.goto(target) ) { |
michael@13 | 745 | event.stopImmediatePropagation(); |
michael@13 | 746 | event.preventDefault(); |
michael@13 | 747 | } |
michael@13 | 748 | }, false); |
michael@13 | 749 | |
michael@13 | 750 | // delegated handler for clicking on step elements |
michael@13 | 751 | document.addEventListener("click", function ( event ) { |
michael@13 | 752 | var target = event.target; |
michael@13 | 753 | // find closest step element that is not active |
michael@13 | 754 | while ( !(target.classList.contains("step") && !target.classList.contains("active")) && |
michael@13 | 755 | (target !== document.documentElement) ) { |
michael@13 | 756 | target = target.parentNode; |
michael@13 | 757 | } |
michael@13 | 758 | |
michael@13 | 759 | if ( api.goto(target) ) { |
michael@13 | 760 | event.preventDefault(); |
michael@13 | 761 | } |
michael@13 | 762 | }, false); |
michael@13 | 763 | |
michael@13 | 764 | // touch handler to detect taps on the left and right side of the screen |
michael@13 | 765 | // based on awesome work of @hakimel: https://github.com/hakimel/reveal.js |
michael@13 | 766 | document.addEventListener("touchstart", function ( event ) { |
michael@13 | 767 | if (event.touches.length === 1) { |
michael@13 | 768 | var x = event.touches[0].clientX, |
michael@13 | 769 | width = window.innerWidth * 0.3, |
michael@13 | 770 | result = null; |
michael@13 | 771 | |
michael@13 | 772 | if ( x < width ) { |
michael@13 | 773 | result = api.prev(); |
michael@13 | 774 | } else if ( x > window.innerWidth - width ) { |
michael@13 | 775 | result = api.next(); |
michael@13 | 776 | } |
michael@13 | 777 | |
michael@13 | 778 | if (result) { |
michael@13 | 779 | event.preventDefault(); |
michael@13 | 780 | } |
michael@13 | 781 | } |
michael@13 | 782 | }, false); |
michael@13 | 783 | |
michael@13 | 784 | // rescale presentation when window is resized |
michael@13 | 785 | window.addEventListener("resize", throttle(function () { |
michael@13 | 786 | // force going to active step again, to trigger rescaling |
michael@13 | 787 | api.goto( document.querySelector(".active"), 500 ); |
michael@13 | 788 | }, 250), false); |
michael@13 | 789 | |
michael@13 | 790 | }, false); |
michael@13 | 791 | |
michael@13 | 792 | })(document, window); |
michael@13 | 793 | |
michael@13 | 794 | // THAT'S ALL FOLKS! |
michael@13 | 795 | // |
michael@13 | 796 | // Thanks for reading it all. |
michael@13 | 797 | // Or thanks for scrolling down and reading the last part. |
michael@13 | 798 | // |
michael@13 | 799 | // I've learnt a lot when building impress.js and I hope this code and comments |
michael@13 | 800 | // will help somebody learn at least some part of it. |