From 0dc8b436502cde657dda3137c17271e8fb3e87a0 Mon Sep 17 00:00:00 2001 From: Henrik Ingo Date: Mon, 25 Sep 2017 03:01:58 +0300 Subject: [PATCH] Introduce plugin framework * Source files are under src/ * js/impress.js is now generated, but remains part of the repo (so it just works) * npm run build * build.js uses buildify node module * Break out navigation and resize plugins from core src/impress.js file --- .gitignore | 1 + build.js | 12 + js/impress.js | 251 ++++--- karma.conf.js | 4 +- package.json | 6 +- qunit_test_runner.html | 2 +- src/impress.js | 672 ++++++++++++++++++ src/plugins/README.md | 407 +++++++++++ src/plugins/navigation/navigation.js | 174 +++++ .../plugins/navigation}/navigation_tests.js | 0 src/plugins/resize/resize.js | 47 ++ 11 files changed, 1474 insertions(+), 102 deletions(-) create mode 100644 build.js create mode 100644 src/impress.js create mode 100644 src/plugins/README.md create mode 100644 src/plugins/navigation/navigation.js rename {test => src/plugins/navigation}/navigation_tests.js (100%) create mode 100644 src/plugins/resize/resize.js diff --git a/.gitignore b/.gitignore index 7a3a95d..b27ac9d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +/js/impress.min.js /node_modules /npm-debug.log diff --git a/build.js b/build.js new file mode 100644 index 0000000..34cf484 --- /dev/null +++ b/build.js @@ -0,0 +1,12 @@ +var buildify = require('buildify'); + +buildify() + .load('src/impress.js') + .concat(['src/plugins/navigation/navigation.js', + 'src/plugins/resize/resize.js']) + .save('js/impress.js'); +/* + * Disabled until uglify supports ES6: https://github.com/mishoo/UglifyJS2/issues/448 + .uglify() + .save('js/impress.min.js'); +*/ \ No newline at end of file diff --git a/js/impress.js b/js/impress.js index 25e8996..a830307 100644 --- a/js/impress.js +++ b/js/impress.js @@ -18,6 +18,7 @@ /*jshint bitwise:true, curly:true, eqeqeq:true, forin:true, latedef:true, newcap:true, noarg:true, noempty:true, undef:true, strict:true, browser:true */ +/*global window*/ // You are one of those who like to know how things work inside? // Let me show you the cogs that make impress.js run... @@ -213,7 +214,7 @@ // And that's where interesting things will start to happen. // It's the core `impress` function that returns the impress.js API - // for a presentation based on the element with given id ('impress' + // for a presentation based on the element with given id ("impress" // by default). var impress = window.impress = function( rootId ) { @@ -419,9 +420,8 @@ // Used to reset timeout for `impress:stepenter` event var stepEnterTimeout = null; - // `goto` API function that moves to step given with `el` parameter - // (by index, id or element), with a transition `duration` optionally - // given as second parameter. + // `goto` API function that moves to step given as `el` parameter (by index, id or element). + // `duration` optionally given as second parameter, is the transition duration in css. var goto = function( el, duration ) { if ( !initialized || !( el = getStep( el ) ) ) { @@ -496,16 +496,15 @@ // being animated separately: // `root` is used for scaling and `canvas` for translate and rotations. // Transitions on them are triggered with different delays (to make - // visually nice and 'natural' looking transitions), so we need to know + // visually nice and "natural" looking transitions), so we need to know // that both of them are finished. css( root, { + // To keep the perspective look similar for different scales + // we need to "scale" the perspective, too // For IE 11 support we must specify perspective independent // of transform. perspective: ( config.perspective / targetScale ) + "px", - - // To keep the perspective look similar for different scales - // we need to 'scale' the perspective, too transform: scale( targetScale ), transitionDuration: duration + "ms", transitionDelay: ( zoomin ? delay : 0 ) + "ms" @@ -526,8 +525,7 @@ // account. // // I know that this `if` statement looks scary, but it's pretty simple when you know - // what is going on - // - it's simply comparing all the values. + // what is going on - it's simply comparing all the values. if ( currentState.scale === target.scale || ( currentState.rotate.x === target.rotate.x && currentState.rotate.y === target.rotate.y && @@ -543,22 +541,20 @@ activeStep = el; // And here is where we trigger `impress:stepenter` event. - // We simply set up a timeout to fire it taking transition duration - // (and possible delay) into account. + // We simply set up a timeout to fire it taking transition duration (and possible delay) + // into account. // // I really wanted to make it in more elegant way. The `transitionend` event seemed to // be the best way to do it, but the fact that I'm using transitions on two separate // elements and that the `transitionend` event is only triggered when there was a // transition (change in the values) caused some bugs and made the code really // complicated, cause I had to handle all the conditions separately. And it still - // needed a `setTimeout` fallback for the situations when there is no transition at - // all. + // needed a `setTimeout` fallback for the situations when there is no transition at all. // So I decided that I'd rather make the code simpler than use shiny new // `transitionend`. // // If you want learn something interesting and see how it was done with `transitionend` - // go back to - // version 0.5.2 of impress.js: + // go back to version 0.5.2 of impress.js: // http://github.com/bartaz/impress.js/blob/0.5.2/js/impress.js window.clearTimeout( stepEnterTimeout ); stepEnterTimeout = window.setTimeout( function() { @@ -667,28 +663,45 @@ } )( document, window ); -// NAVIGATION EVENTS - -// As you can see this part is separate from the impress.js core code. -// It's because these navigation actions only need what impress.js provides with -// its simple API. +// THAT'S ALL FOLKS! // -// In future I think about moving it to make them optional, move to separate files -// and treat more like a 'plugins'. -( function( document, window ) { +// Thanks for reading it all. +// Or thanks for scrolling down and reading the last part. +// +// I've learnt a lot when building impress.js and I hope this code and comments +// will help somebody learn at least some part of it. + +/** + * Navigation events plugin + * + * As you can see this part is separate from the impress.js core code. + * It's because these navigation actions only need what impress.js provides with + * its simple API. + * + * This plugin is what we call an _init plugin_. It's a simple kind of + * impress.js plugin. When loaded, it starts listening to the `impress:init` + * event. That event listener initializes the plugin functionality - in this + * case we listen to some keypress and mouse events. The only dependencies on + * core impress.js functionality is the `impress:init` method, as well as using + * the public api `next(), prev(),` etc when keys are pressed. + * + * Copyright 2011-2012 Bartek Szopka (@bartaz) + * Released under the MIT license. + * ------------------------------------------------ + * author: Bartek Szopka + * version: 0.5.3 + * url: http://bartaz.github.com/impress.js/ + * source: http://github.com/bartaz/impress.js/ + * + */ +/* global document */ +( function( document ) { "use strict"; - // Throttling function calls, by Remy Sharp - // http://remysharp.com/2010/07/21/throttling-function-calls/ - var throttle = function( fn, delay ) { - var timer = null; - return function() { - var context = this, args = arguments; - clearTimeout( timer ); - timer = setTimeout( function() { - fn.apply( context, args ); - }, delay ); - }; + var triggerEvent = function( el, eventName, detail ) { + var event = document.createEvent( "CustomEvent" ); + event.initCustomEvent( eventName, true, true, detail ); + el.dispatchEvent( event ); }; // Wait for impress.js to be initialized @@ -700,19 +713,6 @@ // need to control the presentation that was just initialized. var api = event.detail.api; - // KEYBOARD NAVIGATION HANDLERS - - // Prevent default keydown action when one of supported key is pressed. - document.addEventListener( "keydown", function( event ) { - if ( event.keyCode === 9 || - ( event.keyCode >= 32 && event.keyCode <= 34 ) || - ( event.keyCode >= 37 && event.keyCode <= 40 ) ) { - event.preventDefault(); - } - }, false ); - - // Trigger impress action (next or prev) on keyup. - // Supported keys are: // [space] - quite common in presentation software to move forward // [up] [right] / [down] [left] - again common and natural addition, @@ -726,30 +726,71 @@ // positioning. I didn't want to just prevent this default action, so I used [tab] // as another way to moving to next step... And yes, I know that for the sake of // consistency I should add [shift+tab] as opposite action... - document.addEventListener( "keyup", function( event ) { + var isNavigationEvent = function( event ) { - if ( event.shiftKey || event.altKey || event.ctrlKey || event.metaKey ) { - return; + // Don't trigger navigation for example when user returns to browser window with ALT+TAB + if ( event.altKey || event.ctrlKey || event.metaKey ) { + return false; } - if ( event.keyCode === 9 || - ( event.keyCode >= 32 && event.keyCode <= 34 ) || - ( event.keyCode >= 37 && event.keyCode <= 40 ) ) { - switch ( event.keyCode ) { - case 33: // Page up - case 37: // Left - case 38: // Up - api.prev(); - break; - case 9: // Tab - case 32: // Space - case 34: // Page down - case 39: // Right - case 40: // Down - api.next(); - break; - } + // In the case of TAB, we force step navigation always, overriding the browser + // navigation between input elements, buttons and links. + if ( event.keyCode === 9 ) { + return true; + } + // With the sole exception of TAB, we also ignore keys pressed if shift is down. + if ( event.shiftKey ) { + return false; + } + + // For arrows, etc, check that event target is html or body element. This is to allow + // presentations to have, for example, forms with input elements where user can type + // text, including space, and not move to next step. + if ( event.target.nodeName !== "BODY" && event.target.nodeName !== "HTML" ) { + return false; + } + + if ( ( event.keyCode >= 32 && event.keyCode <= 34 ) || + ( event.keyCode >= 37 && event.keyCode <= 40 ) ) { + return true; + } + }; + + // KEYBOARD NAVIGATION HANDLERS + + // Prevent default keydown action when one of supported key is pressed. + document.addEventListener( "keydown", function( event ) { + if ( isNavigationEvent( event ) ) { + event.preventDefault(); + } + }, false ); + + // Trigger impress action (next or prev) on keyup. + document.addEventListener( "keyup", function( event ) { + if ( isNavigationEvent( event ) ) { + if ( event.shiftKey ) { + switch ( event.keyCode ) { + case 9: // Shift+tab + api.prev(); + break; + } + } else { + switch ( event.keyCode ) { + case 33: // Pg up + case 37: // Left + case 38: // Up + api.prev( event ); + break; + case 9: // Tab + case 32: // Space + case 34: // Pg down + case 39: // Right + case 40: // Down + api.next( event ); + break; + } + } event.preventDefault(); } }, false ); @@ -758,7 +799,7 @@ document.addEventListener( "click", function( event ) { // Event delegation with "bubbling" - // Check if event target (or any of its parents is a link) + // check if event target (or any of its parents is a link) var target = event.target; while ( ( target.tagName !== "A" ) && ( target !== document.documentElement ) ) { @@ -786,8 +827,8 @@ // Find closest step element that is not active while ( !( target.classList.contains( "step" ) && - !target.classList.contains( "active" ) ) && - ( target !== document.documentElement ) ) { + !target.classList.contains( "active" ) ) && + ( target !== document.documentElement ) ) { target = target.parentNode; } @@ -796,25 +837,51 @@ } }, false ); - // Touch handler to detect taps on the left and right side of the screen - // based on awesome work of @hakimel: https://github.com/hakimel/reveal.js - document.addEventListener( "touchstart", function( event ) { - if ( event.touches.length === 1 ) { - var x = event.touches[ 0 ].clientX, - width = window.innerWidth * 0.3, - result = null; + // Add a line to the help popup + triggerEvent( document, "impress:help:add", + { command: "Left & Right", text: "Previous & Next step", row: 1 } ); - if ( x < width ) { - result = api.prev(); - } else if ( x > window.innerWidth - width ) { - result = api.next(); - } + }, false ); - if ( result ) { - event.preventDefault(); - } - } - }, false ); +} )( document ); + + +/** + * Resize plugin + * + * Rescale the presentation after a window resize. + * + * Copyright 2011-2012 Bartek Szopka (@bartaz) + * Released under the MIT license. + * ------------------------------------------------ + * author: Bartek Szopka + * version: 0.5.3 + * url: http://bartaz.github.com/impress.js/ + * source: http://github.com/bartaz/impress.js/ + * + */ + +/* global document, window */ + +( function( document, window ) { + "use strict"; + + // Throttling function calls, by Remy Sharp + // http://remysharp.com/2010/07/21/throttling-function-calls/ + var throttle = function( fn, delay ) { + var timer = null; + return function() { + var context = this, args = arguments; + window.clearTimeout( timer ); + timer = window.setTimeout( function() { + fn.apply( context, args ); + }, delay ); + }; + }; + + // Wait for impress.js to be initialized + document.addEventListener( "impress:init", function( event ) { + var api = event.detail.api; // Rescale presentation when window is resized window.addEventListener( "resize", throttle( function() { @@ -822,15 +889,7 @@ // Force going to active step again, to trigger rescaling api.goto( document.querySelector( ".step.active" ), 500 ); }, 250 ), false ); - }, false ); } )( document, window ); -// THAT'S ALL FOLKS! -// -// Thanks for reading it all. -// Or thanks for scrolling down and reading the last part. -// -// I've learnt a lot when building impress.js and I hope this code and comments -// will help somebody learn at least some part of it. diff --git a/karma.conf.js b/karma.conf.js index 4d42e54..4251687 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -18,10 +18,8 @@ module.exports = function( config ) { // The QUnit tests "test/helpers.js", "test/core_tests.js", - "test/navigation_tests.js", + "src/plugins/navigation/navigation_tests.js", // Presentation files, for the iframe - //"test/core_tests_presentation.html" - //{pattern: "test/core_tests_presentation.html", watched: true, served: true, included: false} {pattern: "test/*.html", watched: true, served: true, included: false}, {pattern: "test/plugins/*/*.html", watched: true, served: true, included: false}, // JS files for iframe diff --git a/package.json b/package.json index 1b377eb..60be279 100644 --- a/package.json +++ b/package.json @@ -19,15 +19,17 @@ "author": "Bartek Szopka", "license": "MIT", "bugs": { - "url": "https://github.com/bartaz/impress.js/issues" + "url": "https://github.com/impress/impress.js/issues" }, "scripts": { - "lint": "jshint js/impress.js test/*.js && jscs js/impress.js test/*.js", + "build": "node build.js", + "lint": "jshint src test/*.js && jscs src test/*.js", "test": "karma start --single-run", "test:dev": "karma start", "test:sauce": "karma start karma.conf-sauce.js" }, "devDependencies": { + "buildify": "*", "chrome": "0.1.0", "firefox": "0.0.1", "jscs": "2.11.0", diff --git a/qunit_test_runner.html b/qunit_test_runner.html index 92f24d6..2087e23 100644 --- a/qunit_test_runner.html +++ b/qunit_test_runner.html @@ -20,7 +20,7 @@ - + diff --git a/src/impress.js b/src/impress.js new file mode 100644 index 0000000..fda6506 --- /dev/null +++ b/src/impress.js @@ -0,0 +1,672 @@ +/** + * impress.js + * + * impress.js is a presentation tool based on the power of CSS3 transforms and transitions + * in modern browsers and inspired by the idea behind prezi.com. + * + * + * Copyright 2011-2012 Bartek Szopka (@bartaz) + * + * Released under the MIT and GPL Licenses. + * + * ------------------------------------------------ + * author: Bartek Szopka + * version: 0.6.0 + * url: http://bartaz.github.com/impress.js/ + * source: http://github.com/bartaz/impress.js/ + */ + +/*jshint bitwise:true, curly:true, eqeqeq:true, forin:true, latedef:true, newcap:true, + noarg:true, noempty:true, undef:true, strict:true, browser:true */ +/*global window*/ + +// You are one of those who like to know how things work inside? +// Let me show you the cogs that make impress.js run... +( function( document, window ) { + "use strict"; + + // HELPER FUNCTIONS + + // `pfx` is a function that takes a standard CSS property name as a parameter + // and returns it's prefixed version valid for current browser it runs in. + // The code is heavily inspired by Modernizr http://www.modernizr.com/ + var pfx = ( function() { + + var style = document.createElement( "dummy" ).style, + prefixes = "Webkit Moz O ms Khtml".split( " " ), + memory = {}; + + return function( prop ) { + if ( typeof memory[ prop ] === "undefined" ) { + + var ucProp = prop.charAt( 0 ).toUpperCase() + prop.substr( 1 ), + props = ( prop + " " + prefixes.join( ucProp + " " ) + ucProp ).split( " " ); + + memory[ prop ] = null; + for ( var i in props ) { + if ( style[ props[ i ] ] !== undefined ) { + memory[ prop ] = props[ i ]; + break; + } + } + + } + + return memory[ prop ]; + }; + + } )(); + + // `arrayify` takes an array-like object and turns it into real Array + // to make all the Array.prototype goodness available. + var arrayify = function( a ) { + return [].slice.call( a ); + }; + + // `css` function applies the styles given in `props` object to the element + // given as `el`. It runs all property names through `pfx` function to make + // sure proper prefixed version of the property is used. + var css = function( el, props ) { + var key, pkey; + for ( key in props ) { + if ( props.hasOwnProperty( key ) ) { + pkey = pfx( key ); + if ( pkey !== null ) { + el.style[ pkey ] = props[ key ]; + } + } + } + return el; + }; + + // `toNumber` takes a value given as `numeric` parameter and tries to turn + // it into a number. If it is not possible it returns 0 (or other value + // given as `fallback`). + var toNumber = function( numeric, fallback ) { + return isNaN( numeric ) ? ( fallback || 0 ) : Number( numeric ); + }; + + // `byId` returns element with given `id` - you probably have guessed that ;) + var byId = function( id ) { + return document.getElementById( id ); + }; + + // `$` returns first element for given CSS `selector` in the `context` of + // the given element or whole document. + var $ = function( selector, context ) { + context = context || document; + return context.querySelector( selector ); + }; + + // `$$` return an array of elements for given CSS `selector` in the `context` of + // the given element or whole document. + var $$ = function( selector, context ) { + context = context || document; + return arrayify( context.querySelectorAll( selector ) ); + }; + + // `triggerEvent` builds a custom DOM event with given `eventName` and `detail` data + // and triggers it on element given as `el`. + var triggerEvent = function( el, eventName, detail ) { + var event = document.createEvent( "CustomEvent" ); + event.initCustomEvent( eventName, true, true, detail ); + el.dispatchEvent( event ); + }; + + // `translate` builds a translate transform string for given data. + var translate = function( t ) { + return " translate3d(" + t.x + "px," + t.y + "px," + t.z + "px) "; + }; + + // `rotate` builds a rotate transform string for given data. + // By default the rotations are in X Y Z order that can be reverted by passing `true` + // as second parameter. + var rotate = function( r, revert ) { + var rX = " rotateX(" + r.x + "deg) ", + rY = " rotateY(" + r.y + "deg) ", + rZ = " rotateZ(" + r.z + "deg) "; + + return revert ? rZ + rY + rX : rX + rY + rZ; + }; + + // `scale` builds a scale transform string for given data. + var scale = function( s ) { + return " scale(" + s + ") "; + }; + + // `getElementFromHash` returns an element located by id from hash part of + // window location. + var getElementFromHash = function() { + + // Get id from url # by removing `#` or `#/` from the beginning, + // so both "fallback" `#slide-id` and "enhanced" `#/slide-id` will work + return byId( window.location.hash.replace( /^#\/?/, "" ) ); + }; + + // `computeWindowScale` counts the scale factor between window size and size + // defined for the presentation in the config. + var computeWindowScale = function( config ) { + var hScale = window.innerHeight / config.height, + wScale = window.innerWidth / config.width, + scale = hScale > wScale ? wScale : hScale; + + if ( config.maxScale && scale > config.maxScale ) { + scale = config.maxScale; + } + + if ( config.minScale && scale < config.minScale ) { + scale = config.minScale; + } + + return scale; + }; + + // CHECK SUPPORT + var body = document.body; + + var ua = navigator.userAgent.toLowerCase(); + var impressSupported = + + // Browser should support CSS 3D transtorms + ( pfx( "perspective" ) !== null ) && + + // Browser should support `classList` and `dataset` APIs + ( body.classList ) && + ( body.dataset ) && + + // But some mobile devices need to be blacklisted, + // because their CSS 3D support or hardware is not + // good enough to run impress.js properly, sorry... + ( ua.search( /(iphone)|(ipod)|(android)/ ) === -1 ); + + if ( !impressSupported ) { + + // We can't be sure that `classList` is supported + body.className += " impress-not-supported "; + } else { + body.classList.remove( "impress-not-supported" ); + body.classList.add( "impress-supported" ); + } + + // GLOBALS AND DEFAULTS + + // This is where the root elements of all impress.js instances will be kept. + // Yes, this means you can have more than one instance on a page, but I'm not + // sure if it makes any sense in practice ;) + var roots = {}; + + // Some default config values. + var defaults = { + width: 1024, + height: 768, + maxScale: 1, + minScale: 0, + + perspective: 1000, + + transitionDuration: 1000 + }; + + // It's just an empty function ... and a useless comment. + var empty = function() { return false; }; + + // IMPRESS.JS API + + // And that's where interesting things will start to happen. + // It's the core `impress` function that returns the impress.js API + // for a presentation based on the element with given id ("impress" + // by default). + var impress = window.impress = function( rootId ) { + + // If impress.js is not supported by the browser return a dummy API + // it may not be a perfect solution but we return early and avoid + // running code that may use features not implemented in the browser. + if ( !impressSupported ) { + return { + init: empty, + goto: empty, + prev: empty, + next: empty + }; + } + + rootId = rootId || "impress"; + + // If given root is already initialized just return the API + if ( roots[ "impress-root-" + rootId ] ) { + return roots[ "impress-root-" + rootId ]; + } + + // Data of all presentation steps + var stepsData = {}; + + // Element of currently active step + var activeStep = null; + + // Current state (position, rotation and scale) of the presentation + var currentState = null; + + // Array of step elements + var steps = null; + + // Configuration options + var config = null; + + // Scale factor of the browser window + var windowScale = null; + + // Root presentation elements + var root = byId( rootId ); + var canvas = document.createElement( "div" ); + + var initialized = false; + + // STEP EVENTS + // + // There are currently two step events triggered by impress.js + // `impress:stepenter` is triggered when the step is shown on the + // screen (the transition from the previous one is finished) and + // `impress:stepleave` is triggered when the step is left (the + // transition to next step just starts). + + // Reference to last entered step + var lastEntered = null; + + // `onStepEnter` is called whenever the step element is entered + // but the event is triggered only if the step is different than + // last entered step. + var onStepEnter = function( step ) { + if ( lastEntered !== step ) { + triggerEvent( step, "impress:stepenter" ); + lastEntered = step; + } + }; + + // `onStepLeave` is called whenever the step element is left + // but the event is triggered only if the step is the same as + // last entered step. + var onStepLeave = function( step ) { + if ( lastEntered === step ) { + triggerEvent( step, "impress:stepleave" ); + lastEntered = null; + } + }; + + // `initStep` initializes given step element by reading data from its + // data attributes and setting correct styles. + var initStep = function( el, idx ) { + var data = el.dataset, + step = { + translate: { + x: toNumber( data.x ), + y: toNumber( data.y ), + z: toNumber( data.z ) + }, + rotate: { + x: toNumber( data.rotateX ), + y: toNumber( data.rotateY ), + z: toNumber( data.rotateZ || data.rotate ) + }, + scale: toNumber( data.scale, 1 ), + el: el + }; + + if ( !el.id ) { + el.id = "step-" + ( idx + 1 ); + } + + stepsData[ "impress-" + el.id ] = step; + + css( el, { + position: "absolute", + transform: "translate(-50%,-50%)" + + translate( step.translate ) + + rotate( step.rotate ) + + scale( step.scale ), + transformStyle: "preserve-3d" + } ); + }; + + // `init` API function that initializes (and runs) the presentation. + var init = function() { + if ( initialized ) { return; } + + // First we set up the viewport for mobile devices. + // For some reason iPad goes nuts when it is not done properly. + var meta = $( "meta[name='viewport']" ) || document.createElement( "meta" ); + meta.content = "width=device-width, minimum-scale=1, maximum-scale=1, user-scalable=no"; + if ( meta.parentNode !== document.head ) { + meta.name = "viewport"; + document.head.appendChild( meta ); + } + + // Initialize configuration object + var rootData = root.dataset; + config = { + width: toNumber( rootData.width, defaults.width ), + height: toNumber( rootData.height, defaults.height ), + maxScale: toNumber( rootData.maxScale, defaults.maxScale ), + minScale: toNumber( rootData.minScale, defaults.minScale ), + perspective: toNumber( rootData.perspective, defaults.perspective ), + transitionDuration: toNumber( + rootData.transitionDuration, defaults.transitionDuration + ) + }; + + windowScale = computeWindowScale( config ); + + // Wrap steps with "canvas" element + arrayify( root.childNodes ).forEach( function( el ) { + canvas.appendChild( el ); + } ); + root.appendChild( canvas ); + + // Set initial styles + document.documentElement.style.height = "100%"; + + css( body, { + height: "100%", + overflow: "hidden" + } ); + + var rootStyles = { + position: "absolute", + transformOrigin: "top left", + transition: "all 0s ease-in-out", + transformStyle: "preserve-3d" + }; + + css( root, rootStyles ); + css( root, { + top: "50%", + left: "50%", + perspective: ( config.perspective / windowScale ) + "px", + transform: scale( windowScale ) + } ); + css( canvas, rootStyles ); + + body.classList.remove( "impress-disabled" ); + body.classList.add( "impress-enabled" ); + + // Get and init steps + steps = $$( ".step", root ); + steps.forEach( initStep ); + + // Set a default initial state of the canvas + currentState = { + translate: { x: 0, y: 0, z: 0 }, + rotate: { x: 0, y: 0, z: 0 }, + scale: 1 + }; + + initialized = true; + + triggerEvent( root, "impress:init", { api: roots[ "impress-root-" + rootId ] } ); + }; + + // `getStep` is a helper function that returns a step element defined by parameter. + // If a number is given, step with index given by the number is returned, if a string + // is given step element with such id is returned, if DOM element is given it is returned + // if it is a correct step element. + var getStep = function( step ) { + if ( typeof step === "number" ) { + step = step < 0 ? steps[ steps.length + step ] : steps[ step ]; + } else if ( typeof step === "string" ) { + step = byId( step ); + } + return ( step && step.id && stepsData[ "impress-" + step.id ] ) ? step : null; + }; + + // Used to reset timeout for `impress:stepenter` event + var stepEnterTimeout = null; + + // `goto` API function that moves to step given as `el` parameter (by index, id or element). + // `duration` optionally given as second parameter, is the transition duration in css. + var goto = function( el, duration ) { + + if ( !initialized || !( el = getStep( el ) ) ) { + + // Presentation not initialized or given element is not a step + return false; + } + + // Sometimes it's possible to trigger focus on first link with some keyboard action. + // Browser in such a case tries to scroll the page to make this element visible + // (even that body overflow is set to hidden) and it breaks our careful positioning. + // + // So, as a lousy (and lazy) workaround we will make the page scroll back to the top + // whenever slide is selected + // + // If you are reading this and know any better way to handle it, I'll be glad to hear + // about it! + window.scrollTo( 0, 0 ); + + var step = stepsData[ "impress-" + el.id ]; + + if ( activeStep ) { + activeStep.classList.remove( "active" ); + body.classList.remove( "impress-on-" + activeStep.id ); + } + el.classList.add( "active" ); + + body.classList.add( "impress-on-" + el.id ); + + // Compute target state of the canvas based on given step + var target = { + rotate: { + x: -step.rotate.x, + y: -step.rotate.y, + z: -step.rotate.z + }, + translate: { + x: -step.translate.x, + y: -step.translate.y, + z: -step.translate.z + }, + scale: 1 / step.scale + }; + + // Check if the transition is zooming in or not. + // + // This information is used to alter the transition style: + // when we are zooming in - we start with move and rotate transition + // and the scaling is delayed, but when we are zooming out we start + // with scaling down and move and rotation are delayed. + var zoomin = target.scale >= currentState.scale; + + duration = toNumber( duration, config.transitionDuration ); + var delay = ( duration / 2 ); + + // If the same step is re-selected, force computing window scaling, + // because it is likely to be caused by window resize + if ( el === activeStep ) { + windowScale = computeWindowScale( config ); + } + + var targetScale = target.scale * windowScale; + + // Trigger leave of currently active element (if it's not the same step again) + if ( activeStep && activeStep !== el ) { + onStepLeave( activeStep ); + } + + // Now we alter transforms of `root` and `canvas` to trigger transitions. + // + // And here is why there are two elements: `root` and `canvas` - they are + // being animated separately: + // `root` is used for scaling and `canvas` for translate and rotations. + // Transitions on them are triggered with different delays (to make + // visually nice and "natural" looking transitions), so we need to know + // that both of them are finished. + css( root, { + + // To keep the perspective look similar for different scales + // we need to "scale" the perspective, too + // For IE 11 support we must specify perspective independent + // of transform. + perspective: ( config.perspective / targetScale ) + "px", + transform: scale( targetScale ), + transitionDuration: duration + "ms", + transitionDelay: ( zoomin ? delay : 0 ) + "ms" + } ); + + css( canvas, { + transform: rotate( target.rotate, true ) + translate( target.translate ), + transitionDuration: duration + "ms", + transitionDelay: ( zoomin ? 0 : delay ) + "ms" + } ); + + // Here is a tricky part... + // + // If there is no change in scale or no change in rotation and translation, it means + // there was actually no delay - because there was no transition on `root` or `canvas` + // elements. We want to trigger `impress:stepenter` event in the correct moment, so + // here we compare the current and target values to check if delay should be taken into + // account. + // + // I know that this `if` statement looks scary, but it's pretty simple when you know + // what is going on - it's simply comparing all the values. + if ( currentState.scale === target.scale || + ( currentState.rotate.x === target.rotate.x && + currentState.rotate.y === target.rotate.y && + currentState.rotate.z === target.rotate.z && + currentState.translate.x === target.translate.x && + currentState.translate.y === target.translate.y && + currentState.translate.z === target.translate.z ) ) { + delay = 0; + } + + // Store current state + currentState = target; + activeStep = el; + + // And here is where we trigger `impress:stepenter` event. + // We simply set up a timeout to fire it taking transition duration (and possible delay) + // into account. + // + // I really wanted to make it in more elegant way. The `transitionend` event seemed to + // be the best way to do it, but the fact that I'm using transitions on two separate + // elements and that the `transitionend` event is only triggered when there was a + // transition (change in the values) caused some bugs and made the code really + // complicated, cause I had to handle all the conditions separately. And it still + // needed a `setTimeout` fallback for the situations when there is no transition at all. + // So I decided that I'd rather make the code simpler than use shiny new + // `transitionend`. + // + // If you want learn something interesting and see how it was done with `transitionend` + // go back to version 0.5.2 of impress.js: + // http://github.com/bartaz/impress.js/blob/0.5.2/js/impress.js + window.clearTimeout( stepEnterTimeout ); + stepEnterTimeout = window.setTimeout( function() { + onStepEnter( activeStep ); + }, duration + delay ); + + return el; + }; + + // `prev` API function goes to previous step (in document order) + var prev = function() { + var prev = steps.indexOf( activeStep ) - 1; + prev = prev >= 0 ? steps[ prev ] : steps[ steps.length - 1 ]; + + return goto( prev ); + }; + + // `next` API function goes to next step (in document order) + var next = function() { + var next = steps.indexOf( activeStep ) + 1; + next = next < steps.length ? steps[ next ] : steps[ 0 ]; + + return goto( next ); + }; + + // Adding some useful classes to step elements. + // + // All the steps that have not been shown yet are given `future` class. + // When the step is entered the `future` class is removed and the `present` + // class is given. When the step is left `present` class is replaced with + // `past` class. + // + // So every step element is always in one of three possible states: + // `future`, `present` and `past`. + // + // There classes can be used in CSS to style different types of steps. + // For example the `present` class can be used to trigger some custom + // animations when step is shown. + root.addEventListener( "impress:init", function() { + + // STEP CLASSES + steps.forEach( function( step ) { + step.classList.add( "future" ); + } ); + + root.addEventListener( "impress:stepenter", function( event ) { + event.target.classList.remove( "past" ); + event.target.classList.remove( "future" ); + event.target.classList.add( "present" ); + }, false ); + + root.addEventListener( "impress:stepleave", function( event ) { + event.target.classList.remove( "present" ); + event.target.classList.add( "past" ); + }, false ); + + }, false ); + + // Adding hash change support. + root.addEventListener( "impress:init", function() { + + // Last hash detected + var lastHash = ""; + + // `#/step-id` is used instead of `#step-id` to prevent default browser + // scrolling to element in hash. + // + // And it has to be set after animation finishes, because in Chrome it + // makes transtion laggy. + // BUG: http://code.google.com/p/chromium/issues/detail?id=62820 + root.addEventListener( "impress:stepenter", function( event ) { + window.location.hash = lastHash = "#/" + event.target.id; + }, false ); + + window.addEventListener( "hashchange", function() { + + // When the step is entered hash in the location is updated + // (just few lines above from here), so the hash change is + // triggered and we would call `goto` again on the same element. + // + // To avoid this we store last entered hash and compare. + if ( window.location.hash !== lastHash ) { + goto( getElementFromHash() ); + } + }, false ); + + // START + // by selecting step defined in url or first step of the presentation + goto( getElementFromHash() || steps[ 0 ], 0 ); + }, false ); + + body.classList.add( "impress-disabled" ); + + // Store and return API for given impress.js root element + return ( roots[ "impress-root-" + rootId ] = { + init: init, + goto: goto, + next: next, + prev: prev + } ); + + }; + + // Flag that can be used in JS to check if browser have passed the support test + impress.supported = impressSupported; + +} )( document, window ); + +// THAT'S ALL FOLKS! +// +// Thanks for reading it all. +// Or thanks for scrolling down and reading the last part. +// +// I've learnt a lot when building impress.js and I hope this code and comments +// will help somebody learn at least some part of it. diff --git a/src/plugins/README.md b/src/plugins/README.md new file mode 100644 index 0000000..a3789ec --- /dev/null +++ b/src/plugins/README.md @@ -0,0 +1,407 @@ +Impress.js Plugins documentation +================================ + +The default set of plugins +-------------------------- + +A lot of impress.js features are and will be implemented as plugins. Each plugin +has user documentation in a README.md file in [its own directory](./). + +The plugins in this directory are called default plugins, and - unsurprisingly - +are enabled by default. However, most of them won't do anything by default, +rather require the user to invoke them somehow. For example: + +* The *navigation* plugin waits for the user to press some keys, arrows, page + down, page up, space or tab. +* The *autoplay* plugin looks for the HTML attribute `data-autoplay` to see + whether it should do its thing. +* The *toolbar* plugin looks for a `
` element to become visible. + +Extra addons +------------ + +Yet more features are available in presentations that enable +[extra addons](../../extras/). Extra addons are 3rd party plugins integrated +into impress.js to provide convenient and standardized access to them. However, +they are not activated by default, rather must be included with a ` + + + + + +### Sample CSS related to plugins and extra addons + + /* Using the substep plugin, hide bullet points at first, then show them one by one. */ + #impress .step .substep { + opacity: 0; + } + + #impress .step .substep.substep-visible { + opacity: 1; + transition: opacity 1s; + } + /* + Speaker notes allow you to write comments within the steps, that will not + be displayed as part of the presentation. However, they will be picked up + and displayed by impressConsole.js when you press P. + */ + .notes { + display: none; + } + + /* Toolbar plugin */ + .impress-enabled div#impress-toolbar { + position: fixed; + right: 1px; + bottom: 1px; + opacity: 0.6; + z-index: 10; + } + .impress-enabled div#impress-toolbar > span { + margin-right: 10px; + } + .impress-enabled div#impress-toolbar.impress-toolbar-show { + display: block; + } + .impress-enabled div#impress-toolbar.impress-toolbar-hide { + display: none; + } + /* If you disable pointer-events (like in the impress.js official demo), you need to re-enable them for the toolbar. */ + .impress-enabled #impress-toolbar { pointer-events: auto } + /* Progress bar */ + .impress-enabled .impress-progressbar { + position: absolute; + right: 318px; + bottom: 1px; + left: 118px; + border-radius: 7px; + border: 2px solid rgba(100, 100, 100, 0.2); + } + .impress-enabled .impress-progressbar DIV { + width: 0; + height: 2px; + border-radius: 5px; + background: rgba(75, 75, 75, 0.4); + transition: width 1s linear; + } + .impress-enabled .impress-progress { + position: absolute; + left: 59px; + bottom: 1px; + text-align: left; + opacity: 0.6; + } + .impress-enabled #impress-help { + background: none repeat scroll 0 0 rgba(0, 0, 0, 0.5); + color: #EEEEEE; + font-size: 80%; + position: fixed; + left: 2em; + bottom: 2em; + width: 24em; + border-radius: 1em; + padding: 1em; + text-align: center; + z-index: 100; + font-family: Verdana, Arial, Sans; + } + .impress-enabled #impress-help td { + padding-left: 1em; + padding-right: 1em; + } + + +For developers +============== + +The vision for impress.js is to provide a compact core library doing the +actual presentations, with a collection of plugins that provide additional +functionality. A default set of plugins are distributed together with the core +impress.js, and are located in this directory. They are called *default plugins* +because they are distributed and active when users use the [js/impress.js](../../js/impress.js) +in their presentations. + +Building js/impress.js +----------------------- + +The common way to use impress.js is to link to the file +[js/impress.js](../../js/impress.js). This is a simple concatenation of the +core impress.js and all plugins in this directory. If you edit or add code +under [src/](../), you can run `node build.js` to recreate the distributable +`js/impress.js` file. The build script also creates a minified file, but this +is not included in the git repository. + +### Tip: Build errors + +If your code has parse errors, the `build.js` will print a rather unhelpful +exception like + + /home/hingo/hacking/impress.js/js/impress.js + + /home/hingo/hacking/impress.js/node_modules/uglify-js/lib/parse-js.js:271 + throw new JS_Parse_Error(message, line, col, pos); + ^ + Error + at new JS_Parse_Error (/home/hingo/hacking/impress.js/node_modules/uglify-js/lib/parse-js.js:263:18) + at js_error (/home/hingo/hacking/impress.js/node_modules/uglify-js/lib/parse-js.js:271:11) + at croak (/home/hingo/hacking/impress.js/node_modules/uglify-js/lib/parse-js.js:733:9) + at token_error (/home/hingo/hacking/impress.js/node_modules/uglify-js/lib/parse-js.js:740:9) + at unexpected (/home/hingo/hacking/impress.js/node_modules/uglify-js/lib/parse-js.js:746:9) + at Object.semicolon [as 1] (/home/hingo/hacking/impress.js/node_modules/uglify-js/lib/parse-js.js:766:43) + at prog1 (/home/hingo/hacking/impress.js/node_modules/uglify-js/lib/parse-js.js:1314:21) + at simple_statement (/home/hingo/hacking/impress.js/node_modules/uglify-js/lib/parse-js.js:906:27) + at /home/hingo/hacking/impress.js/node_modules/uglify-js/lib/parse-js.js:814:19 + at block_ (/home/hingo/hacking/impress.js/node_modules/uglify-js/lib/parse-js.js:1003:20) + +You will be pleased to know, that the concatenation of the unminified file +[js/impress.js](../../js/impress.js) has already succeeded at this point. Just +open a test in your browser, and the browser will show you the line and error. + + +### Structure, naming and policy + +Each plugin is contained within its own directory. The name of the directory +is the name of the plugin. For example, imagine a plugin called *pluginA*: + + src/plugins/plugina/ + +The main javascript file should use the directory name as its root name: + + src/plugins/plugina/plugina.js + +For most plugins, a single `.js` file is enough. + +Note that the plugin name is also used as a namespace for various things. For +example, the *autoplay* plugin can be configured by setting the `data-autoplay="5"` +attribute on a `div`. + +As a general rule ids, classes and attributes within the `div#impress` root +element, may use the plugin name directly (e.g. `data-autoplay="5"`). However, +outside of the root element, you should use `impress-pluginname` (e.g. +`
`. The latter (longer) form also applies to all +events, they should be prefixed with `impress:pluginname`. + +You should use crisp and descriptive names for your plugins. But +sometimes you might optimize for a short namespace. Hence, the +[Relative Positioning Plugin](rel/rel.js) is called `rel` to keep html attributes +short. You should not overuse this idea! + +Note that for default plugins, which is all plugins in this directory, +**NO css, html or image files** are allowed. + +Default plugins must not add any global variables. + +### Testing + +The plugin directory should also include tests, which should use the *QUnit* and +*Syn* libraries under [test/](../../test). You can have as many tests as you like, +but it is suggested your first and main test file is called `plugina_tests.html` +and `plugina_tests.js` respectively. You need to add your test `.js` file into +[/qunit_test_runner.html](../../qunit_test_runner.html), and the `.js` file +should start by loading the test `.html` file into the +`iframe#presentation-iframe`. See [navigation-ui](navigation-ui) plugin for an +example. + +You are allowed to test your plugin whatever way you like, but the general +approach is for the test to load the [js/impress.js](../../js/impress.js) file +produced by build.js. This way you are testing what users will actually be +using, rather than the uncompiled source code. + +HowTo write a plugin +-------------------- + +### Encapsulation + +To avoid polluting the global namespace, plugins must encapsulate them in the +standard javascript anonymous function: + + /** + * Plugin A - An example plugin + * + * Description... + * + * Copyright 2016 Firstname Lastname, email or github handle + * Released under the MIT license. + */ + (function ( document, window ) { + + // Plugin implementation... + + })(document, window); + + +### Init plugins + +We categorize plugins into various categories, based on how and when they are +called, and what they do. + +An init plugin is the simplest kind of plugin. It simply listens for the +`impress().init()` method to send the `impress:init` event, at which point +the plugin can initialize itself and start doing whatever it does, for example +by calling methods in the public api returned by `impress()`. + +Both [Navigation](navigation/navigation.js) and [Autoplay](autoplay/autoplay.js) +are init plugins. + +To provide end user configurability in your plugin, a good idea might be to +read html attributes from the impress presentation. The +[Autoplay](autoplay/autoplay.js) plugin does exactly this, you can provide +a default value in the `div#impress` element, or in each `div.step`. + +A plugin must only use html attributes in its designated namespace, which is + + data-pluginName-*="value" + +For example, if *pluginA* offers config options `foo` and `bar`, it would look +like this: + +
+ + +### Pre-init plugins + +Some plugins need to run before even impress().init() does anything. These +are typically *filters*: they want to modify the html via DOM calls, before +impress.js core parses the presentation. We call these *pre-init plugins*. + +A pre-init plugin must be called synchronously, before `impress().init()` is +executed. Plugins can register themselves to be called in the pre-init phase +by calling: + + impress.addPreInitPlugin( plugin [, weight] ); + +The argument `plugin` must be a function. `weight` is optional and defaults to +`10`. Plugins are ordered by weight when they are executed, with lower weight +first. + +The [Relative Positioning Plugin](rel/rel.js) is an example of a pre-init plugin. + +### Pre-StepLeave plugins + +A *pre-stepleave plugin* is called synchronously from impress.js core at the +beginning of `impress().goto()`. + +To register a plugin, call + + impress.addPreStepLeavePlugin( plugin [, weight] ); + +When the plugin function is executed, it will be passed an argument +that resembles the `event` object from DOM event handlers: + +`event.target` contains the current step, which we are about to leave. + +`event.detail.next` contains the element we are about to transition to. + +`event.detail.reason` contains a string, one of "next", "prev" or "goto", +which tells you which API function was called to initiate the transition. + +`event.detail.transitionDuration` contains the transitionDuration for the +upcoming transition. + +A pre-stepleave plugin may alter the values in `event.detail` (except for +`reason`), and this can change the behavior of the upcoming transition. +For example, the `goto` plugin will set the `event.detail.next` to point to +some other element, causing the presentation to jump to that step instead. + + +### GUI plugins + +A *GUI plugin* is actually just an init plugin, but is a special category that +exposes visible widgets or effects in the presentation. For example, it might +provide clickable buttons to go to the next and previous slide. + +Note that all plugins shipped in the default set **must not** produce any visible +html elements unless the user asks for it. A recommended best practice is to let +the user add a div element, with an id equaling the plugin's namespace, in the +place where he wants to see whatever visual UI elements the plugin is providing: + +
+ +Another way to show the elements of a UI plugin might be by allowing the user +to explicitly press a key, like "H" for a help dialog. + +[Toolbar plugin](toolbar/README.md) is an example of a GUI plugin. It presents +a toolbar where other plugins can add their buttons in a centralized fashion. + +Remember that for default plugins, even GUI plugins, no html files, css files +or images are allowed. Everything must be generated from javascript. The idea +is that users can theme widgets with their own CSS. (A plugin is of course welcome +to provide example CSS that can be copypasted :-) + +Dependencies +------------ + +If *pluginB* depends on the existence of *pluginA*, and also *pluginA* must run +before *pluginB*, then *pluginB* should not listen to the `impress:init` event, +rather *pluginA* should send its own init event, which *pluginB* listens to. + +Example: + + // pluginA + document.addEventListener("impress:init", function (event) { + // plugin A does it's own initialization first... + + // Signal other plugins that plugin A is now initialized + var root = document.querySelector( "div#impress" ); + var event = document.createEvent("CustomEvent"); + event.initCustomEvent("impress:plugina:init', true, true, { "plugina" : "data..." }); + root.dispatchEvent(event); + }, false); + + // pluginB + document.addEventListener("impress:init", function (event) { + // plugin B implementation + }, false); + +A plugin should use the namespace `impress:pluginname:*` for any events it sends. + +In theory all plugins could always send an `init` and other events, but in +practice we're adding them on an as needed basis. diff --git a/src/plugins/navigation/navigation.js b/src/plugins/navigation/navigation.js new file mode 100644 index 0000000..08d6b04 --- /dev/null +++ b/src/plugins/navigation/navigation.js @@ -0,0 +1,174 @@ +/** + * Navigation events plugin + * + * As you can see this part is separate from the impress.js core code. + * It's because these navigation actions only need what impress.js provides with + * its simple API. + * + * This plugin is what we call an _init plugin_. It's a simple kind of + * impress.js plugin. When loaded, it starts listening to the `impress:init` + * event. That event listener initializes the plugin functionality - in this + * case we listen to some keypress and mouse events. The only dependencies on + * core impress.js functionality is the `impress:init` method, as well as using + * the public api `next(), prev(),` etc when keys are pressed. + * + * Copyright 2011-2012 Bartek Szopka (@bartaz) + * Released under the MIT license. + * ------------------------------------------------ + * author: Bartek Szopka + * version: 0.5.3 + * url: http://bartaz.github.com/impress.js/ + * source: http://github.com/bartaz/impress.js/ + * + */ +/* global document */ +( function( document ) { + "use strict"; + + var triggerEvent = function( el, eventName, detail ) { + var event = document.createEvent( "CustomEvent" ); + event.initCustomEvent( eventName, true, true, detail ); + el.dispatchEvent( event ); + }; + + // Wait for impress.js to be initialized + document.addEventListener( "impress:init", function( event ) { + + // Getting API from event data. + // So you don't event need to know what is the id of the root element + // or anything. `impress:init` event data gives you everything you + // need to control the presentation that was just initialized. + var api = event.detail.api; + + // Supported keys are: + // [space] - quite common in presentation software to move forward + // [up] [right] / [down] [left] - again common and natural addition, + // [pgdown] / [pgup] - often triggered by remote controllers, + // [tab] - this one is quite controversial, but the reason it ended up on + // this list is quite an interesting story... Remember that strange part + // in the impress.js code where window is scrolled to 0,0 on every presentation + // step, because sometimes browser scrolls viewport because of the focused element? + // Well, the [tab] key by default navigates around focusable elements, so clicking + // it very often caused scrolling to focused element and breaking impress.js + // positioning. I didn't want to just prevent this default action, so I used [tab] + // as another way to moving to next step... And yes, I know that for the sake of + // consistency I should add [shift+tab] as opposite action... + var isNavigationEvent = function( event ) { + + // Don't trigger navigation for example when user returns to browser window with ALT+TAB + if ( event.altKey || event.ctrlKey || event.metaKey ) { + return false; + } + + // In the case of TAB, we force step navigation always, overriding the browser + // navigation between input elements, buttons and links. + if ( event.keyCode === 9 ) { + return true; + } + + // With the sole exception of TAB, we also ignore keys pressed if shift is down. + if ( event.shiftKey ) { + return false; + } + + // For arrows, etc, check that event target is html or body element. This is to allow + // presentations to have, for example, forms with input elements where user can type + // text, including space, and not move to next step. + if ( event.target.nodeName !== "BODY" && event.target.nodeName !== "HTML" ) { + return false; + } + + if ( ( event.keyCode >= 32 && event.keyCode <= 34 ) || + ( event.keyCode >= 37 && event.keyCode <= 40 ) ) { + return true; + } + }; + + // KEYBOARD NAVIGATION HANDLERS + + // Prevent default keydown action when one of supported key is pressed. + document.addEventListener( "keydown", function( event ) { + if ( isNavigationEvent( event ) ) { + event.preventDefault(); + } + }, false ); + + // Trigger impress action (next or prev) on keyup. + document.addEventListener( "keyup", function( event ) { + if ( isNavigationEvent( event ) ) { + if ( event.shiftKey ) { + switch ( event.keyCode ) { + case 9: // Shift+tab + api.prev(); + break; + } + } else { + switch ( event.keyCode ) { + case 33: // Pg up + case 37: // Left + case 38: // Up + api.prev( event ); + break; + case 9: // Tab + case 32: // Space + case 34: // Pg down + case 39: // Right + case 40: // Down + api.next( event ); + break; + } + } + event.preventDefault(); + } + }, false ); + + // Delegated handler for clicking on the links to presentation steps + document.addEventListener( "click", function( event ) { + + // Event delegation with "bubbling" + // check if event target (or any of its parents is a link) + var target = event.target; + while ( ( target.tagName !== "A" ) && + ( target !== document.documentElement ) ) { + target = target.parentNode; + } + + if ( target.tagName === "A" ) { + var href = target.getAttribute( "href" ); + + // If it's a link to presentation step, target this step + if ( href && href[ 0 ] === "#" ) { + target = document.getElementById( href.slice( 1 ) ); + } + } + + if ( api.goto( target ) ) { + event.stopImmediatePropagation(); + event.preventDefault(); + } + }, false ); + + // Delegated handler for clicking on step elements + document.addEventListener( "click", function( event ) { + var target = event.target; + + // Find closest step element that is not active + while ( !( target.classList.contains( "step" ) && + !target.classList.contains( "active" ) ) && + ( target !== document.documentElement ) ) { + target = target.parentNode; + } + + if ( api.goto( target ) ) { + event.preventDefault(); + } + }, false ); + + // Add a line to the help popup + triggerEvent( document, "impress:help:add", + { command: "Left & Right", text: "Previous & Next step", row: 1 } ); + + }, false ); + +} )( document ); + diff --git a/test/navigation_tests.js b/src/plugins/navigation/navigation_tests.js similarity index 100% rename from test/navigation_tests.js rename to src/plugins/navigation/navigation_tests.js diff --git a/src/plugins/resize/resize.js b/src/plugins/resize/resize.js new file mode 100644 index 0000000..a1ec5a4 --- /dev/null +++ b/src/plugins/resize/resize.js @@ -0,0 +1,47 @@ +/** + * Resize plugin + * + * Rescale the presentation after a window resize. + * + * Copyright 2011-2012 Bartek Szopka (@bartaz) + * Released under the MIT license. + * ------------------------------------------------ + * author: Bartek Szopka + * version: 0.5.3 + * url: http://bartaz.github.com/impress.js/ + * source: http://github.com/bartaz/impress.js/ + * + */ + +/* global document, window */ + +( function( document, window ) { + "use strict"; + + // Throttling function calls, by Remy Sharp + // http://remysharp.com/2010/07/21/throttling-function-calls/ + var throttle = function( fn, delay ) { + var timer = null; + return function() { + var context = this, args = arguments; + window.clearTimeout( timer ); + timer = window.setTimeout( function() { + fn.apply( context, args ); + }, delay ); + }; + }; + + // Wait for impress.js to be initialized + document.addEventListener( "impress:init", function( event ) { + var api = event.detail.api; + + // Rescale presentation when window is resized + window.addEventListener( "resize", throttle( function() { + + // Force going to active step again, to trigger rescaling + api.goto( document.querySelector( ".step.active" ), 500 ); + }, 250 ), false ); + }, false ); + +} )( document, window ); +