Add a framework for libraries, and a first library gc

- Libraries are under src/lib/
- Added to build.js as usual, before plugins.
- See src/lib/README.md for details

gc library implements a "garbage collector" library, which allows
both the core and plugins to store elements and listeners to a list,
and when impress().lib.gc.teardown() is called, to have all of them
removed from the DOM. It also allows plugins to register their own
callback functions, which are called at teardown.

Commentary:

This work is based on copying the src/lib/gc.js from impressionist. While it was
useful, it turns out on the impress.js side there was much more a need to reset
attributes rather than delete elements. For now, this means lots of plugins do this
via their own lib.gc.addCallback() functions. Probably it would be nicer to add
some generic lib.gc.resetAttributes() functionality for this particular case.
I'll return to this in a future patch.

extras/ are not supported for impress().tear(). What can I say, they're extras.
Maybe in the future I'll support them, for now I can live without.
This commit is contained in:
Henrik Ingo
2017-10-05 14:57:59 +03:00
parent 9198ca854b
commit 9b958f0e00
7 changed files with 693 additions and 26 deletions

View File

@@ -222,7 +222,9 @@
init: empty,
goto: empty,
prev: empty,
next: empty
next: empty,
tear: empty,
lib: {}
};
}
@@ -233,6 +235,12 @@
return roots[ "impress-root-" + rootId ];
}
// The gc library depends on being initialized before we do any changes to DOM.
var lib = initLibraries( rootId );
body.classList.remove( "impress-not-supported" );
body.classList.add( "impress-supported" );
// Data of all presentation steps
var stepsData = {};
@@ -576,6 +584,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 +606,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 +627,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 +638,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
@@ -649,7 +666,9 @@
init: init,
goto: goto,
next: next,
prev: prev
prev: prev,
tear: tear,
lib: lib
} );
};
@@ -657,6 +676,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 +716,236 @@
// 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 <meta name="viewport"> 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 );
/**
* Navigation events plugin
*
@@ -708,6 +987,7 @@
// 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;
// Supported keys are:
// [space] - quite common in presentation software to move forward
@@ -756,14 +1036,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 +1072,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 +1098,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
@@ -880,7 +1160,7 @@
var api = event.detail.api;
// Rescale presentation when window is resized
window.addEventListener( "resize", throttle( function() {
api.lib.gc.addEventListener( window, "resize", throttle( function() {
// Force going to active step again, to trigger rescaling
api.goto( document.querySelector( ".step.active" ), 500 );