diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md
index 2d396b4..f2b298e 100644
--- a/DOCUMENTATION.md
+++ b/DOCUMENTATION.md
@@ -229,6 +229,31 @@ rootElement.addEventListener( "impress:init", function() {
impress().init();
```
+#### .tear()
+
+Resets the DOM to its original state, as it was before `init()` was called.
+
+This can be used to "unload" impress.js. A particular use case for this is, if you want to do
+dynamic changes to the presentation, you can do a teardown, apply changes, then call `init()`
+again. (In most cases, this will not cause flickering or other visible effects to the user,
+beyond the intended dynamic changes.)
+
+**Example:**
+
+```JavaScript
+impress().tear();
+```
+
+**Example:**
+
+```JavaScript
+var rootElement = document.getElementById( "impress" );
+rootElement.addEventListener( "impress:init", function() {
+ console.log( "Impress init" );
+});
+impress().init();
+```
+
#### .next()
Navigates to the next step of the presentation using the [`goto()` function](#impressgotostepindexstepelementidstepelement-duration).
diff --git a/build.js b/build.js
index 34cf484..b359316 100644
--- a/build.js
+++ b/build.js
@@ -2,6 +2,10 @@ var buildify = require('buildify');
buildify()
.load('src/impress.js')
+ // Libraries from src/lib
+ .concat(['src/lib/gc.js'])
+ .concat(['src/lib/util.js'])
+ // Plugins from src/plugins
.concat(['src/plugins/navigation/navigation.js',
'src/plugins/resize/resize.js'])
.save('js/impress.js');
diff --git a/js/impress.js b/js/impress.js
index af1f448..6a90cad 100644
--- a/js/impress.js
+++ b/js/impress.js
@@ -20,6 +20,7 @@
// Let me show you the cogs that make impress.js run...
( function( document, window ) {
"use strict";
+ var lib;
// HELPER FUNCTIONS
@@ -53,12 +54,6 @@
} )();
- // `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.
@@ -75,40 +70,6 @@
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) ";
@@ -130,15 +91,6 @@
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 ) {
@@ -222,7 +174,9 @@
init: empty,
goto: empty,
prev: empty,
- next: empty
+ next: empty,
+ tear: empty,
+ lib: {}
};
}
@@ -233,6 +187,12 @@
return roots[ "impress-root-" + rootId ];
}
+ // The gc library depends on being initialized before we do any changes to DOM.
+ lib = initLibraries( rootId );
+
+ body.classList.remove( "impress-not-supported" );
+ body.classList.add( "impress-supported" );
+
// Data of all presentation steps
var stepsData = {};
@@ -252,7 +212,7 @@
var windowScale = null;
// Root presentation elements
- var root = byId( rootId );
+ var root = lib.util.byId( rootId );
var canvas = document.createElement( "div" );
var initialized = false;
@@ -273,7 +233,7 @@
// last entered step.
var onStepEnter = function( step ) {
if ( lastEntered !== step ) {
- triggerEvent( step, "impress:stepenter" );
+ lib.util.triggerEvent( step, "impress:stepenter" );
lastEntered = step;
}
};
@@ -283,7 +243,7 @@
// last entered step.
var onStepLeave = function( step ) {
if ( lastEntered === step ) {
- triggerEvent( step, "impress:stepleave" );
+ lib.util.triggerEvent( step, "impress:stepleave" );
lastEntered = null;
}
};
@@ -294,16 +254,16 @@
var data = el.dataset,
step = {
translate: {
- x: toNumber( data.x ),
- y: toNumber( data.y ),
- z: toNumber( data.z )
+ x: lib.util.toNumber( data.x ),
+ y: lib.util.toNumber( data.y ),
+ z: lib.util.toNumber( data.z )
},
rotate: {
- x: toNumber( data.rotateX ),
- y: toNumber( data.rotateY ),
- z: toNumber( data.rotateZ || data.rotate )
+ x: lib.util.toNumber( data.rotateX ),
+ y: lib.util.toNumber( data.rotateY ),
+ z: lib.util.toNumber( data.rotateZ || data.rotate )
},
- scale: toNumber( data.scale, 1 ),
+ scale: lib.util.toNumber( data.scale, 1 ),
el: el
};
@@ -329,7 +289,7 @@
// 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" );
+ var meta = lib.util.$( "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";
@@ -339,12 +299,12 @@
// 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(
+ width: lib.util.toNumber( rootData.width, defaults.width ),
+ height: lib.util.toNumber( rootData.height, defaults.height ),
+ maxScale: lib.util.toNumber( rootData.maxScale, defaults.maxScale ),
+ minScale: lib.util.toNumber( rootData.minScale, defaults.minScale ),
+ perspective: lib.util.toNumber( rootData.perspective, defaults.perspective ),
+ transitionDuration: lib.util.toNumber(
rootData.transitionDuration, defaults.transitionDuration
)
};
@@ -352,7 +312,7 @@
windowScale = computeWindowScale( config );
// Wrap steps with "canvas" element
- arrayify( root.childNodes ).forEach( function( el ) {
+ lib.util.arrayify( root.childNodes ).forEach( function( el ) {
canvas.appendChild( el );
} );
root.appendChild( canvas );
@@ -385,7 +345,7 @@
body.classList.add( "impress-enabled" );
// Get and init steps
- steps = $$( ".step", root );
+ steps = lib.util.$$( ".step", root );
steps.forEach( initStep );
// Set a default initial state of the canvas
@@ -397,7 +357,8 @@
initialized = true;
- triggerEvent( root, "impress:init", { api: roots[ "impress-root-" + rootId ] } );
+ lib.util.triggerEvent( root, "impress:init",
+ { api: roots[ "impress-root-" + rootId ] } );
};
// `getStep` is a helper function that returns a step element defined by parameter.
@@ -408,7 +369,7 @@
if ( typeof step === "number" ) {
step = step < 0 ? steps[ steps.length + step ] : steps[ step ];
} else if ( typeof step === "string" ) {
- step = byId( step );
+ step = lib.util.byId( step );
}
return ( step && step.id && stepsData[ "impress-" + step.id ] ) ? step : null;
};
@@ -470,7 +431,7 @@
// with scaling down and move and rotation are delayed.
var zoomin = target.scale >= currentState.scale;
- duration = toNumber( duration, config.transitionDuration );
+ duration = lib.util.toNumber( duration, config.transitionDuration );
var delay = ( duration / 2 );
// If the same step is re-selected, force computing window scaling,
@@ -576,6 +537,15 @@
return goto( next );
};
+ // Teardown impress
+ // Resets the DOM to the state it was before impress().init() was called.
+ // (If you called impress(rootId).init() for multiple different rootId's, then you must
+ // also call tear() once for each of them.)
+ var tear = function() {
+ lib.gc.teardown();
+ delete roots[ "impress-root-" + rootId ];
+ };
+
// Adding some useful classes to step elements.
//
// All the steps that have not been shown yet are given `future` class.
@@ -589,20 +559,20 @@
// 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() {
+ lib.gc.addEventListener( root, "impress:init", function() {
// STEP CLASSES
steps.forEach( function( step ) {
step.classList.add( "future" );
} );
- root.addEventListener( "impress:stepenter", function( event ) {
+ lib.gc.addEventListener( root, "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 ) {
+ lib.gc.addEventListener( root, "impress:stepleave", function( event ) {
event.target.classList.remove( "present" );
event.target.classList.add( "past" );
}, false );
@@ -610,7 +580,7 @@
}, false );
// Adding hash change support.
- root.addEventListener( "impress:init", function() {
+ lib.gc.addEventListener( root, "impress:init", function() {
// Last hash detected
var lastHash = "";
@@ -621,11 +591,11 @@
// 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 ) {
+ lib.gc.addEventListener( root, "impress:stepenter", function( event ) {
window.location.hash = lastHash = "#/" + event.target.id;
}, false );
- window.addEventListener( "hashchange", function() {
+ lib.gc.addEventListener( window, "hashchange", function() {
// When the step is entered hash in the location is updated
// (just few lines above from here), so the hash change is
@@ -633,13 +603,13 @@
//
// To avoid this we store last entered hash and compare.
if ( window.location.hash !== lastHash ) {
- goto( getElementFromHash() );
+ goto( lib.util.getElementFromHash() );
}
}, false );
// START
// by selecting step defined in url or first step of the presentation
- goto( getElementFromHash() || steps[ 0 ], 0 );
+ goto( lib.util.getElementFromHash() || steps[ 0 ], 0 );
}, false );
body.classList.add( "impress-disabled" );
@@ -649,7 +619,9 @@
init: init,
goto: goto,
next: next,
- prev: prev
+ prev: prev,
+ tear: tear,
+ lib: lib
} );
};
@@ -657,6 +629,36 @@
// Flag that can be used in JS to check if browser have passed the support test
impress.supported = impressSupported;
+ // ADD and INIT LIBRARIES
+ // Library factories are defined in src/lib/*.js, and register themselves by calling
+ // impress.addLibraryFactory(libraryFactoryObject). They're stored here, and used to augment
+ // the API with library functions when client calls impress(rootId).
+ // See src/lib/README.md for clearer example.
+ // (Advanced usage: For different values of rootId, a different instance of the libaries are
+ // generated, in case they need to hold different state for different root elements.)
+ var libraryFactories = {};
+ impress.addLibraryFactory = function( obj ) {
+ for ( var libname in obj ) {
+ if ( obj.hasOwnProperty( libname ) ) {
+ libraryFactories[ libname ] = obj[ libname ];
+ }
+ }
+ };
+
+ // Call each library factory, and return the lib object that is added to the api.
+ var initLibraries = function( rootId ) { //jshint ignore:line
+ var lib = {};
+ for ( var libname in libraryFactories ) {
+ if ( libraryFactories.hasOwnProperty( libname ) ) {
+ if ( lib[ libname ] !== undefined ) {
+ throw "impress.js ERROR: Two libraries both tried to use libname: " + libname;
+ }
+ lib[ libname ] = libraryFactories[ libname ]( rootId );
+ }
+ }
+ return lib;
+ };
+
} )( document, window );
// THAT'S ALL FOLKS!
@@ -667,6 +669,334 @@
// 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.
+/**
+ * Garbage collection utility
+ *
+ * This library allows plugins to add elements and event listeners they add to the DOM. The user
+ * can call `impress().lib.gc.teardown()` to cause all of them to be removed from DOM, so that
+ * the document is in the state it was before calling `impress().init()`.
+ *
+ * In addition to just adding elements and event listeners to the garbage collector, plugins
+ * can also register callback functions to do arbitrary cleanup upon teardown.
+ *
+ * Henrik Ingo (c) 2016
+ * MIT License
+ */
+
+( function( document, window ) {
+ "use strict";
+ var roots = [];
+ var rootsCount = 0;
+ var startingState = { roots: [] };
+
+ var libraryFactory = function( rootId ) {
+ if ( roots[ rootId ] ) {
+ return roots[ rootId ];
+ }
+
+ // Per root global variables (instance variables?)
+ var elementList = [];
+ var eventListenerList = [];
+ var callbackList = [];
+
+ recordStartingState( rootId );
+
+ // LIBRARY FUNCTIONS
+ // Below are definitions of the library functions we return at the end
+ var pushElement = function( element ) {
+ elementList.push( element );
+ };
+
+ // Convenience wrapper that combines DOM appendChild with gc.pushElement
+ var appendChild = function( parent, element ) {
+ parent.appendChild( element );
+ pushElement( element );
+ };
+
+ var pushEventListener = function( target, type, listenerFunction ) {
+ eventListenerList.push( { target:target, type:type, listener:listenerFunction } );
+ };
+
+ // Convenience wrapper that combines DOM addEventListener with gc.pushEventListener
+ var addEventListener = function( target, type, listenerFunction ) {
+ target.addEventListener( type, listenerFunction );
+ pushEventListener( target, type, listenerFunction );
+ };
+
+ // If the above utilities are not enough, plugins can add their own callback function
+ // to do arbitrary things.
+ var addCallback = function( callback ) {
+ callbackList.push( callback );
+ };
+ addCallback( function( rootId ) { resetStartingState( rootId ); } );
+
+ var teardown = function() {
+
+ // Execute the callbacks in LIFO order
+ var i; // Needed by jshint
+ for ( i = callbackList.length - 1; i >= 0; i-- ) {
+ callbackList[ i ]( rootId );
+ }
+ callbackList = [];
+ for ( i = 0; i < elementList.length; i++ ) {
+ elementList[ i ].parentElement.removeChild( elementList[ i ] );
+ }
+ elementList = [];
+ for ( i = 0; i < eventListenerList.length; i++ ) {
+ var target = eventListenerList[ i ].target;
+ var type = eventListenerList[ i ].type;
+ var listener = eventListenerList[ i ].listener;
+ target.removeEventListener( type, listener );
+ }
+ };
+
+ var lib = {
+ pushElement: pushElement,
+ appendChild: appendChild,
+ pushEventListener: pushEventListener,
+ addEventListener: addEventListener,
+ addCallback: addCallback,
+ teardown: teardown
+ };
+ roots[ rootId ] = lib;
+ rootsCount++;
+ return lib;
+ };
+
+ // Let impress core know about the existence of this library
+ window.impress.addLibraryFactory( { gc: libraryFactory } );
+
+ // CORE INIT
+ // The library factory (gc(rootId)) is called at the beginning of impress(rootId).init()
+ // For the purposes of teardown(), we can use this as an opportunity to save the state
+ // of a few things in the DOM in their virgin state, before impress().init() did anything.
+ // Note: These could also be recorded by the code in impress.js core as these values
+ // are changed, but in an effort to not deviate too much from upstream, I'm adding
+ // them here rather than the core itself.
+ var recordStartingState = function( rootId ) {
+ startingState.roots[ rootId ] = {};
+ startingState.roots[ rootId ].steps = [];
+
+ // Record whether the steps have an id or not
+ var steps = document.getElementById( rootId ).querySelectorAll( ".step" );
+ for ( var i = 0; i < steps.length; i++ ) {
+ var el = steps[ i ];
+ startingState.roots[ rootId ].steps.push( {
+ el: el,
+ id: el.getAttribute( "id" )
+ } );
+ }
+
+ // In the rare case of multiple roots, the following is changed on first init() and
+ // reset at last tear().
+ if ( rootsCount === 0 ) {
+ startingState.body = {};
+
+ // It is customary for authors to set body.class="impress-not-supported" as a starting
+ // value, which can then be removed by impress().init(). But it is not required.
+ // Remember whether it was there or not.
+ if ( document.body.classList.contains( "impress-not-supported" ) ) {
+ startingState.body.impressNotSupported = true;
+ } else {
+ startingState.body.impressNotSupported = false;
+ }
+
+ // If there's a element, its contents will be overwritten by init
+ var metas = document.head.querySelectorAll( "meta" );
+ for ( i = 0; i < metas.length; i++ ) {
+ var m = metas[ i ];
+ if ( m.name === "viewport" ) {
+ startingState.meta = m.content;
+ }
+ }
+ }
+ };
+
+ // CORE TEARDOWN
+ var resetStartingState = function( rootId ) {
+
+ // Reset body element
+ document.body.classList.remove( "impress-enabled" );
+ document.body.classList.remove( "impress-disabled" );
+
+ var root = document.getElementById( rootId );
+ var activeId = root.querySelector( ".active" ).id;
+ document.body.classList.remove( "impress-on-" + activeId );
+
+ document.documentElement.style.height = "";
+ document.body.style.height = "";
+ document.body.style.overflow = "";
+
+ // Remove style values from the root and step elements
+ // Note: We remove the ones set by impress.js core. Otoh, we didn't preserve any original
+ // values. A more sophisticated implementation could keep track of original values and then
+ // reset those.
+ var steps = root.querySelectorAll( ".step" );
+ for ( var i = 0; i < steps.length; i++ ) {
+ steps[ i ].classList.remove( "future" );
+ steps[ i ].classList.remove( "past" );
+ steps[ i ].classList.remove( "present" );
+ steps[ i ].classList.remove( "active" );
+ steps[ i ].style.position = "";
+ steps[ i ].style.transform = "";
+ steps[ i ].style[ "transform-style" ] = "";
+ }
+ root.style.position = "";
+ root.style[ "transform-origin" ] = "";
+ root.style.transition = "";
+ root.style[ "transform-style" ] = "";
+ root.style.top = "";
+ root.style.left = "";
+ root.style.transform = "";
+
+ // Reset id of steps ("step-1" id's are auto generated)
+ steps = startingState.roots[ rootId ].steps;
+ var step;
+ while ( step = steps.pop() ) {
+ if ( step.id === null ) {
+ step.el.removeAttribute( "id" );
+ } else {
+ step.el.setAttribute( "id", step.id );
+ }
+ }
+ delete startingState.roots[ rootId ];
+
+ // Move step div elements away from canvas, then delete canvas
+ // Note: There's an implicit assumption here that the canvas div is the only child element
+ // of the root div. If there would be something else, it's gonna be lost.
+ var canvas = root.firstChild;
+ var canvasHTML = canvas.innerHTML;
+ root.innerHTML = canvasHTML;
+
+ if ( roots[ rootId ] !== undefined ) {
+ delete roots[ rootId ];
+ rootsCount--;
+ }
+ if ( rootsCount === 0 ) {
+
+ // In the rare case that more than one impress root elements were initialized, these
+ // are only reset when all are uninitialized.
+ document.body.classList.remove( "impress-supported" );
+ if ( startingState.body.impressNotSupported ) {
+ document.body.classList.add( "impress-not-supported" );
+ }
+
+ // We need to remove or reset the meta element inserted by impress.js
+ var metas = document.head.querySelectorAll( "meta" );
+ for ( i = 0; i < metas.length; i++ ) {
+ var m = metas[ i ];
+ if ( m.name === "viewport" ) {
+ if ( startingState.meta !== undefined ) {
+ m.content = startingState.meta;
+ } else {
+ m.parentElement.removeChild( m );
+ }
+ }
+ }
+ }
+
+ };
+
+} )( document, window );
+
+/**
+ * Common utility functions
+ *
+ * Copyright 2011-2012 Bartek Szopka (@bartaz)
+ * Henrik Ingo (c) 2016
+ * MIT License
+ */
+
+( function( document, window ) {
+ "use strict";
+ var roots = [];
+
+ var libraryFactory = function( rootId ) {
+ if ( roots[ rootId ] ) {
+ return roots[ rootId ];
+ }
+
+ // `$` 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 ) );
+ };
+
+ // `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 );
+ };
+
+ // `byId` returns element with given `id` - you probably have guessed that ;)
+ var byId = function( id ) {
+ return document.getElementById( id );
+ };
+
+ // `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( /^#\/?/, "" ) );
+ };
+
+ // 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 );
+ };
+ };
+
+ // `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 );
+ };
+
+ // `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 );
+ };
+
+ var lib = {
+ $: $,
+ $$: $$,
+ arrayify: arrayify,
+ byId: byId,
+ getElementFromHash: getElementFromHash,
+ throttle: throttle,
+ toNumber: toNumber,
+ triggerEvent: triggerEvent
+ };
+ roots[ rootId ] = lib;
+ return lib;
+ };
+
+ // Let impress core know about the existence of this library
+ window.impress.addLibraryFactory( { util: libraryFactory } );
+
+} )( document, window );
+
/**
* Navigation events plugin
*
@@ -694,12 +1024,6 @@
( 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 ) {
@@ -708,6 +1032,8 @@
// or anything. `impress:init` event data gives you everything you
// need to control the presentation that was just initialized.
var api = event.detail.api;
+ var gc = api.lib.gc;
+ var util = api.lib.util;
// Supported keys are:
// [space] - quite common in presentation software to move forward
@@ -756,14 +1082,14 @@
// KEYBOARD NAVIGATION HANDLERS
// Prevent default keydown action when one of supported key is pressed.
- document.addEventListener( "keydown", function( event ) {
+ gc.addEventListener( document, "keydown", function( event ) {
if ( isNavigationEvent( event ) ) {
event.preventDefault();
}
}, false );
// Trigger impress action (next or prev) on keyup.
- document.addEventListener( "keyup", function( event ) {
+ gc.addEventListener( document, "keyup", function( event ) {
if ( isNavigationEvent( event ) ) {
if ( event.shiftKey ) {
switch ( event.keyCode ) {
@@ -792,7 +1118,7 @@
}, false );
// Delegated handler for clicking on the links to presentation steps
- document.addEventListener( "click", function( event ) {
+ gc.addEventListener( document, "click", function( event ) {
// Event delegation with "bubbling"
// check if event target (or any of its parents is a link)
@@ -818,7 +1144,7 @@
}, false );
// Delegated handler for clicking on step elements
- document.addEventListener( "click", function( event ) {
+ gc.addEventListener( document, "click", function( event ) {
var target = event.target;
// Find closest step element that is not active
@@ -834,8 +1160,9 @@
}, false );
// Add a line to the help popup
- triggerEvent( document, "impress:help:add",
- { command: "Left & Right", text: "Previous & Next step", row: 1 } );
+ util.triggerEvent( document, "impress:help:add", { command: "Left & Right",
+ text: "Previous & Next step",
+ row: 1 } );
}, false );
@@ -862,25 +1189,12 @@
( 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() {
+ api.lib.gc.addEventListener( window, "resize", api.lib.util.throttle( function() {
// Force going to active step again, to trigger rescaling
api.goto( document.querySelector( ".step.active" ), 500 );
diff --git a/src/impress.js b/src/impress.js
index c9b8390..4788673 100644
--- a/src/impress.js
+++ b/src/impress.js
@@ -20,6 +20,7 @@
// Let me show you the cogs that make impress.js run...
( function( document, window ) {
"use strict";
+ var lib;
// HELPER FUNCTIONS
@@ -53,12 +54,6 @@
} )();
- // `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.
@@ -75,40 +70,6 @@
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) ";
@@ -130,15 +91,6 @@
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 ) {
@@ -222,7 +174,9 @@
init: empty,
goto: empty,
prev: empty,
- next: empty
+ next: empty,
+ tear: empty,
+ lib: {}
};
}
@@ -233,6 +187,12 @@
return roots[ "impress-root-" + rootId ];
}
+ // The gc library depends on being initialized before we do any changes to DOM.
+ lib = initLibraries( rootId );
+
+ body.classList.remove( "impress-not-supported" );
+ body.classList.add( "impress-supported" );
+
// Data of all presentation steps
var stepsData = {};
@@ -252,7 +212,7 @@
var windowScale = null;
// Root presentation elements
- var root = byId( rootId );
+ var root = lib.util.byId( rootId );
var canvas = document.createElement( "div" );
var initialized = false;
@@ -273,7 +233,7 @@
// last entered step.
var onStepEnter = function( step ) {
if ( lastEntered !== step ) {
- triggerEvent( step, "impress:stepenter" );
+ lib.util.triggerEvent( step, "impress:stepenter" );
lastEntered = step;
}
};
@@ -283,7 +243,7 @@
// last entered step.
var onStepLeave = function( step ) {
if ( lastEntered === step ) {
- triggerEvent( step, "impress:stepleave" );
+ lib.util.triggerEvent( step, "impress:stepleave" );
lastEntered = null;
}
};
@@ -294,16 +254,16 @@
var data = el.dataset,
step = {
translate: {
- x: toNumber( data.x ),
- y: toNumber( data.y ),
- z: toNumber( data.z )
+ x: lib.util.toNumber( data.x ),
+ y: lib.util.toNumber( data.y ),
+ z: lib.util.toNumber( data.z )
},
rotate: {
- x: toNumber( data.rotateX ),
- y: toNumber( data.rotateY ),
- z: toNumber( data.rotateZ || data.rotate )
+ x: lib.util.toNumber( data.rotateX ),
+ y: lib.util.toNumber( data.rotateY ),
+ z: lib.util.toNumber( data.rotateZ || data.rotate )
},
- scale: toNumber( data.scale, 1 ),
+ scale: lib.util.toNumber( data.scale, 1 ),
el: el
};
@@ -329,7 +289,7 @@
// 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" );
+ var meta = lib.util.$( "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";
@@ -339,12 +299,12 @@
// 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(
+ width: lib.util.toNumber( rootData.width, defaults.width ),
+ height: lib.util.toNumber( rootData.height, defaults.height ),
+ maxScale: lib.util.toNumber( rootData.maxScale, defaults.maxScale ),
+ minScale: lib.util.toNumber( rootData.minScale, defaults.minScale ),
+ perspective: lib.util.toNumber( rootData.perspective, defaults.perspective ),
+ transitionDuration: lib.util.toNumber(
rootData.transitionDuration, defaults.transitionDuration
)
};
@@ -352,7 +312,7 @@
windowScale = computeWindowScale( config );
// Wrap steps with "canvas" element
- arrayify( root.childNodes ).forEach( function( el ) {
+ lib.util.arrayify( root.childNodes ).forEach( function( el ) {
canvas.appendChild( el );
} );
root.appendChild( canvas );
@@ -385,7 +345,7 @@
body.classList.add( "impress-enabled" );
// Get and init steps
- steps = $$( ".step", root );
+ steps = lib.util.$$( ".step", root );
steps.forEach( initStep );
// Set a default initial state of the canvas
@@ -397,7 +357,8 @@
initialized = true;
- triggerEvent( root, "impress:init", { api: roots[ "impress-root-" + rootId ] } );
+ lib.util.triggerEvent( root, "impress:init",
+ { api: roots[ "impress-root-" + rootId ] } );
};
// `getStep` is a helper function that returns a step element defined by parameter.
@@ -408,7 +369,7 @@
if ( typeof step === "number" ) {
step = step < 0 ? steps[ steps.length + step ] : steps[ step ];
} else if ( typeof step === "string" ) {
- step = byId( step );
+ step = lib.util.byId( step );
}
return ( step && step.id && stepsData[ "impress-" + step.id ] ) ? step : null;
};
@@ -470,7 +431,7 @@
// with scaling down and move and rotation are delayed.
var zoomin = target.scale >= currentState.scale;
- duration = toNumber( duration, config.transitionDuration );
+ duration = lib.util.toNumber( duration, config.transitionDuration );
var delay = ( duration / 2 );
// If the same step is re-selected, force computing window scaling,
@@ -576,6 +537,15 @@
return goto( next );
};
+ // Teardown impress
+ // Resets the DOM to the state it was before impress().init() was called.
+ // (If you called impress(rootId).init() for multiple different rootId's, then you must
+ // also call tear() once for each of them.)
+ var tear = function() {
+ lib.gc.teardown();
+ delete roots[ "impress-root-" + rootId ];
+ };
+
// Adding some useful classes to step elements.
//
// All the steps that have not been shown yet are given `future` class.
@@ -589,20 +559,20 @@
// 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() {
+ lib.gc.addEventListener( root, "impress:init", function() {
// STEP CLASSES
steps.forEach( function( step ) {
step.classList.add( "future" );
} );
- root.addEventListener( "impress:stepenter", function( event ) {
+ lib.gc.addEventListener( root, "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 ) {
+ lib.gc.addEventListener( root, "impress:stepleave", function( event ) {
event.target.classList.remove( "present" );
event.target.classList.add( "past" );
}, false );
@@ -610,7 +580,7 @@
}, false );
// Adding hash change support.
- root.addEventListener( "impress:init", function() {
+ lib.gc.addEventListener( root, "impress:init", function() {
// Last hash detected
var lastHash = "";
@@ -621,11 +591,11 @@
// 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 ) {
+ lib.gc.addEventListener( root, "impress:stepenter", function( event ) {
window.location.hash = lastHash = "#/" + event.target.id;
}, false );
- window.addEventListener( "hashchange", function() {
+ lib.gc.addEventListener( window, "hashchange", function() {
// When the step is entered hash in the location is updated
// (just few lines above from here), so the hash change is
@@ -633,13 +603,13 @@
//
// To avoid this we store last entered hash and compare.
if ( window.location.hash !== lastHash ) {
- goto( getElementFromHash() );
+ goto( lib.util.getElementFromHash() );
}
}, false );
// START
// by selecting step defined in url or first step of the presentation
- goto( getElementFromHash() || steps[ 0 ], 0 );
+ goto( lib.util.getElementFromHash() || steps[ 0 ], 0 );
}, false );
body.classList.add( "impress-disabled" );
@@ -649,7 +619,9 @@
init: init,
goto: goto,
next: next,
- prev: prev
+ prev: prev,
+ tear: tear,
+ lib: lib
} );
};
@@ -657,6 +629,36 @@
// Flag that can be used in JS to check if browser have passed the support test
impress.supported = impressSupported;
+ // ADD and INIT LIBRARIES
+ // Library factories are defined in src/lib/*.js, and register themselves by calling
+ // impress.addLibraryFactory(libraryFactoryObject). They're stored here, and used to augment
+ // the API with library functions when client calls impress(rootId).
+ // See src/lib/README.md for clearer example.
+ // (Advanced usage: For different values of rootId, a different instance of the libaries are
+ // generated, in case they need to hold different state for different root elements.)
+ var libraryFactories = {};
+ impress.addLibraryFactory = function( obj ) {
+ for ( var libname in obj ) {
+ if ( obj.hasOwnProperty( libname ) ) {
+ libraryFactories[ libname ] = obj[ libname ];
+ }
+ }
+ };
+
+ // Call each library factory, and return the lib object that is added to the api.
+ var initLibraries = function( rootId ) { //jshint ignore:line
+ var lib = {};
+ for ( var libname in libraryFactories ) {
+ if ( libraryFactories.hasOwnProperty( libname ) ) {
+ if ( lib[ libname ] !== undefined ) {
+ throw "impress.js ERROR: Two libraries both tried to use libname: " + libname;
+ }
+ lib[ libname ] = libraryFactories[ libname ]( rootId );
+ }
+ }
+ return lib;
+ };
+
} )( document, window );
// THAT'S ALL FOLKS!
diff --git a/src/lib/README.md b/src/lib/README.md
new file mode 100644
index 0000000..88e77cc
--- /dev/null
+++ b/src/lib/README.md
@@ -0,0 +1,105 @@
+Impress.js Libraries
+====================
+
+The `src/lib/*.js` files contain library functions. The main difference to plugins is that:
+
+1. Libraries are closer to the impress.js core than plugins (arguably a subjective metric)
+2. Libraries are common utility functions used by many plugins
+3. Libraries are called synchronously, which is why the event based paradigm that plugins use to
+ communicate isn't useful.
+
+Plugins can access libraries via the API:
+
+ var api;
+ document.addEventListener( "impress:init", function(event){
+ api = event.detail.api;
+ api().lib..();
+ });
+
+...which is equivalent to:
+
+ impress().lib..();
+
+Implementing a library
+----------------------
+
+1. Create a file under `src/lib/`.
+
+2. Start with the standard boilerplate documentation, and the (function(document, window){})();
+wrapper.
+
+3. The library should implement a factory function, and make its existence known to impress.js core:
+
+ window.impress.addLibraryFactory( { libName : libraryFactory} );
+
+4. The library function should return a similar API object as core `impress()` function does:
+
+ var libraryFactory = function(rootId) {
+ /* implement library functions ... */
+
+ var lib = {
+ libFunction1: libFunction1,
+ libFunction2: libFunction2
+ }
+ return lib;
+ };
+
+5. While rarely used, impress.js actually supports multiple presentation root div elements on a
+single html page. Each of these have their own API object, identified by the root element id
+attribute:
+
+ impress("other-root-id").init();
+
+(The default rootId obviously is `"impress"`.)
+
+Libraries MUST implement this support for multiple root elements as well.
+
+- impress.js core will call the factory once for each separate root element being initialized via
+ `impress.init(rootId)`.
+- Any state that a library might hold, MUST be stored *per `rootId`*.
+- Note that as we now support also `impress(rootId).tear()`, the same root element might be
+ initialized more than once, and each of these MUST be treated as a new valid initialization.
+
+Putting all of the above together, a skeleton library file will look like:
+
+ /**
+ * Example library libName
+ *
+ * Henrik Ingo (c) 2016
+ * MIT License
+ */
+ (function ( document, window ) {
+ 'use strict';
+ // Singleton library variables
+ var roots = [];
+ var singletonVar = {};
+
+ var libraryFactory = function(rootId) {
+ if (roots["impress-root-" + rootId]) {
+ return roots["impress-root-" + rootId];
+ }
+
+ // Per root global variables (instance variables?)
+ var instanceVar = {};
+
+ // LIBRARY FUNCTIONS
+ var libraryFunction1 = function () {
+ /* ... */
+ };
+
+ var libraryFunction2 = function () {
+ /* ... */
+ };
+
+ var lib = {
+ libFunction1: libFunction1,
+ libFunction2: libFunction2
+ }
+ roots["impress-root-" + rootId] = lib;
+ return lib;
+ };
+
+ // Let impress core know about the existence of this library
+ window.impress.addLibraryFactory( { libName : libraryFactory } );
+
+ })(document, window);
diff --git a/src/lib/gc.js b/src/lib/gc.js
new file mode 100644
index 0000000..3147d7f
--- /dev/null
+++ b/src/lib/gc.js
@@ -0,0 +1,238 @@
+/**
+ * Garbage collection utility
+ *
+ * This library allows plugins to add elements and event listeners they add to the DOM. The user
+ * can call `impress().lib.gc.teardown()` to cause all of them to be removed from DOM, so that
+ * the document is in the state it was before calling `impress().init()`.
+ *
+ * In addition to just adding elements and event listeners to the garbage collector, plugins
+ * can also register callback functions to do arbitrary cleanup upon teardown.
+ *
+ * Henrik Ingo (c) 2016
+ * MIT License
+ */
+
+( function( document, window ) {
+ "use strict";
+ var roots = [];
+ var rootsCount = 0;
+ var startingState = { roots: [] };
+
+ var libraryFactory = function( rootId ) {
+ if ( roots[ rootId ] ) {
+ return roots[ rootId ];
+ }
+
+ // Per root global variables (instance variables?)
+ var elementList = [];
+ var eventListenerList = [];
+ var callbackList = [];
+
+ recordStartingState( rootId );
+
+ // LIBRARY FUNCTIONS
+ // Definitions of the library functions we return as an object at the end
+
+ // `pushElement` adds a DOM element to the gc stack
+ var pushElement = function( element ) {
+ elementList.push( element );
+ };
+
+ // `appendChild` is a convenience wrapper that combines DOM appendChild with gc.pushElement
+ var appendChild = function( parent, element ) {
+ parent.appendChild( element );
+ pushElement( element );
+ };
+
+ // `pushEventListener` adds an event listener to the gc stack
+ var pushEventListener = function( target, type, listenerFunction ) {
+ eventListenerList.push( { target:target, type:type, listener:listenerFunction } );
+ };
+
+ // `addEventListener` combines DOM addEventListener with gc.pushEventListener
+ var addEventListener = function( target, type, listenerFunction ) {
+ target.addEventListener( type, listenerFunction );
+ pushEventListener( target, type, listenerFunction );
+ };
+
+ // `addCallback` If the above utilities are not enough, plugins can add their own callback
+ // function to do arbitrary things.
+ var addCallback = function( callback ) {
+ callbackList.push( callback );
+ };
+ addCallback( function( rootId ) { resetStartingState( rootId ); } );
+
+ // `teardown` will
+ // - execute all callbacks in LIFO order
+ // - call `removeChild` on all DOM elements in LIFO order
+ // - call `removeEventListener` on all event listeners in LIFO order
+ // The goal of a teardown is to return to the same state that the DOM was before
+ // `impress().init()` was called.
+ var teardown = function() {
+
+ // Execute the callbacks in LIFO order
+ var i; // Needed by jshint
+ for ( i = callbackList.length - 1; i >= 0; i-- ) {
+ callbackList[ i ]( rootId );
+ }
+ callbackList = [];
+ for ( i = 0; i < elementList.length; i++ ) {
+ elementList[ i ].parentElement.removeChild( elementList[ i ] );
+ }
+ elementList = [];
+ for ( i = 0; i < eventListenerList.length; i++ ) {
+ var target = eventListenerList[ i ].target;
+ var type = eventListenerList[ i ].type;
+ var listener = eventListenerList[ i ].listener;
+ target.removeEventListener( type, listener );
+ }
+ };
+
+ var lib = {
+ pushElement: pushElement,
+ appendChild: appendChild,
+ pushEventListener: pushEventListener,
+ addEventListener: addEventListener,
+ addCallback: addCallback,
+ teardown: teardown
+ };
+ roots[ rootId ] = lib;
+ rootsCount++;
+ return lib;
+ };
+
+ // Let impress core know about the existence of this library
+ window.impress.addLibraryFactory( { gc: libraryFactory } );
+
+ // CORE INIT
+ // The library factory (gc(rootId)) is called at the beginning of impress(rootId).init()
+ // For the purposes of teardown(), we can use this as an opportunity to save the state
+ // of a few things in the DOM in their virgin state, before impress().init() did anything.
+ // Note: These could also be recorded by the code in impress.js core as these values
+ // are changed, but in an effort to not deviate too much from upstream, I'm adding
+ // them here rather than the core itself.
+ var recordStartingState = function( rootId ) {
+ startingState.roots[ rootId ] = {};
+ startingState.roots[ rootId ].steps = [];
+
+ // Record whether the steps have an id or not
+ var steps = document.getElementById( rootId ).querySelectorAll( ".step" );
+ for ( var i = 0; i < steps.length; i++ ) {
+ var el = steps[ i ];
+ startingState.roots[ rootId ].steps.push( {
+ el: el,
+ id: el.getAttribute( "id" )
+ } );
+ }
+
+ // In the rare case of multiple roots, the following is changed on first init() and
+ // reset at last tear().
+ if ( rootsCount === 0 ) {
+ startingState.body = {};
+
+ // It is customary for authors to set body.class="impress-not-supported" as a starting
+ // value, which can then be removed by impress().init(). But it is not required.
+ // Remember whether it was there or not.
+ if ( document.body.classList.contains( "impress-not-supported" ) ) {
+ startingState.body.impressNotSupported = true;
+ } else {
+ startingState.body.impressNotSupported = false;
+ }
+
+ // If there's a element, its contents will be overwritten by init
+ var metas = document.head.querySelectorAll( "meta" );
+ for ( i = 0; i < metas.length; i++ ) {
+ var m = metas[ i ];
+ if ( m.name === "viewport" ) {
+ startingState.meta = m.content;
+ }
+ }
+ }
+ };
+
+ // CORE TEARDOWN
+ var resetStartingState = function( rootId ) {
+
+ // Reset body element
+ document.body.classList.remove( "impress-enabled" );
+ document.body.classList.remove( "impress-disabled" );
+
+ var root = document.getElementById( rootId );
+ var activeId = root.querySelector( ".active" ).id;
+ document.body.classList.remove( "impress-on-" + activeId );
+
+ document.documentElement.style.height = "";
+ document.body.style.height = "";
+ document.body.style.overflow = "";
+
+ // Remove style values from the root and step elements
+ // Note: We remove the ones set by impress.js core. Otoh, we didn't preserve any original
+ // values. A more sophisticated implementation could keep track of original values and then
+ // reset those.
+ var steps = root.querySelectorAll( ".step" );
+ for ( var i = 0; i < steps.length; i++ ) {
+ steps[ i ].classList.remove( "future" );
+ steps[ i ].classList.remove( "past" );
+ steps[ i ].classList.remove( "present" );
+ steps[ i ].classList.remove( "active" );
+ steps[ i ].style.position = "";
+ steps[ i ].style.transform = "";
+ steps[ i ].style[ "transform-style" ] = "";
+ }
+ root.style.position = "";
+ root.style[ "transform-origin" ] = "";
+ root.style.transition = "";
+ root.style[ "transform-style" ] = "";
+ root.style.top = "";
+ root.style.left = "";
+ root.style.transform = "";
+
+ // Reset id of steps ("step-1" id's are auto generated)
+ steps = startingState.roots[ rootId ].steps;
+ var step;
+ while ( step = steps.pop() ) {
+ if ( step.id === null ) {
+ step.el.removeAttribute( "id" );
+ } else {
+ step.el.setAttribute( "id", step.id );
+ }
+ }
+ delete startingState.roots[ rootId ];
+
+ // Move step div elements away from canvas, then delete canvas
+ // Note: There's an implicit assumption here that the canvas div is the only child element
+ // of the root div. If there would be something else, it's gonna be lost.
+ var canvas = root.firstChild;
+ var canvasHTML = canvas.innerHTML;
+ root.innerHTML = canvasHTML;
+
+ if ( roots[ rootId ] !== undefined ) {
+ delete roots[ rootId ];
+ rootsCount--;
+ }
+ if ( rootsCount === 0 ) {
+
+ // In the rare case that more than one impress root elements were initialized, these
+ // are only reset when all are uninitialized.
+ document.body.classList.remove( "impress-supported" );
+ if ( startingState.body.impressNotSupported ) {
+ document.body.classList.add( "impress-not-supported" );
+ }
+
+ // We need to remove or reset the meta element inserted by impress.js
+ var metas = document.head.querySelectorAll( "meta" );
+ for ( i = 0; i < metas.length; i++ ) {
+ var m = metas[ i ];
+ if ( m.name === "viewport" ) {
+ if ( startingState.meta !== undefined ) {
+ m.content = startingState.meta;
+ } else {
+ m.parentElement.removeChild( m );
+ }
+ }
+ }
+ }
+
+ };
+
+} )( document, window );
diff --git a/src/lib/util.js b/src/lib/util.js
new file mode 100644
index 0000000..bd7dcbb
--- /dev/null
+++ b/src/lib/util.js
@@ -0,0 +1,97 @@
+/**
+ * Common utility functions
+ *
+ * Copyright 2011-2012 Bartek Szopka (@bartaz)
+ * Henrik Ingo (c) 2016
+ * MIT License
+ */
+
+( function( document, window ) {
+ "use strict";
+ var roots = [];
+
+ var libraryFactory = function( rootId ) {
+ if ( roots[ rootId ] ) {
+ return roots[ rootId ];
+ }
+
+ // `$` 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 ) );
+ };
+
+ // `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 );
+ };
+
+ // `byId` returns element with given `id` - you probably have guessed that ;)
+ var byId = function( id ) {
+ return document.getElementById( id );
+ };
+
+ // `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( /^#\/?/, "" ) );
+ };
+
+ // 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 );
+ };
+ };
+
+ // `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 );
+ };
+
+ // `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 );
+ };
+
+ var lib = {
+ $: $,
+ $$: $$,
+ arrayify: arrayify,
+ byId: byId,
+ getElementFromHash: getElementFromHash,
+ throttle: throttle,
+ toNumber: toNumber,
+ triggerEvent: triggerEvent
+ };
+ roots[ rootId ] = lib;
+ return lib;
+ };
+
+ // Let impress core know about the existence of this library
+ window.impress.addLibraryFactory( { util: libraryFactory } );
+
+} )( document, window );
diff --git a/src/plugins/navigation/navigation.js b/src/plugins/navigation/navigation.js
index 08d6b04..60f75ad 100644
--- a/src/plugins/navigation/navigation.js
+++ b/src/plugins/navigation/navigation.js
@@ -25,12 +25,6 @@
( 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 ) {
@@ -39,6 +33,8 @@
// or anything. `impress:init` event data gives you everything you
// need to control the presentation that was just initialized.
var api = event.detail.api;
+ var gc = api.lib.gc;
+ var util = api.lib.util;
// Supported keys are:
// [space] - quite common in presentation software to move forward
@@ -87,14 +83,14 @@
// KEYBOARD NAVIGATION HANDLERS
// Prevent default keydown action when one of supported key is pressed.
- document.addEventListener( "keydown", function( event ) {
+ gc.addEventListener( document, "keydown", function( event ) {
if ( isNavigationEvent( event ) ) {
event.preventDefault();
}
}, false );
// Trigger impress action (next or prev) on keyup.
- document.addEventListener( "keyup", function( event ) {
+ gc.addEventListener( document, "keyup", function( event ) {
if ( isNavigationEvent( event ) ) {
if ( event.shiftKey ) {
switch ( event.keyCode ) {
@@ -123,7 +119,7 @@
}, false );
// Delegated handler for clicking on the links to presentation steps
- document.addEventListener( "click", function( event ) {
+ gc.addEventListener( document, "click", function( event ) {
// Event delegation with "bubbling"
// check if event target (or any of its parents is a link)
@@ -149,7 +145,7 @@
}, false );
// Delegated handler for clicking on step elements
- document.addEventListener( "click", function( event ) {
+ gc.addEventListener( document, "click", function( event ) {
var target = event.target;
// Find closest step element that is not active
@@ -165,8 +161,9 @@
}, false );
// Add a line to the help popup
- triggerEvent( document, "impress:help:add",
- { command: "Left & Right", text: "Previous & Next step", row: 1 } );
+ util.triggerEvent( document, "impress:help:add", { command: "Left & Right",
+ text: "Previous & Next step",
+ row: 1 } );
}, false );
diff --git a/src/plugins/resize/resize.js b/src/plugins/resize/resize.js
index a1ec5a4..ca8efb4 100644
--- a/src/plugins/resize/resize.js
+++ b/src/plugins/resize/resize.js
@@ -18,25 +18,12 @@
( 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() {
+ api.lib.gc.addEventListener( window, "resize", api.lib.util.throttle( function() {
// Force going to active step again, to trigger rescaling
api.goto( document.querySelector( ".step.active" ), 500 );