tizenporta/impress.js

Mon, 07 May 2012 23:15:17 +0200

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Mon, 07 May 2012 23:15:17 +0200
changeset 17
768787695807
permissions
-rw-r--r--

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.

mercurial