From 9b958f0e00168892fd0e25c7fc560437ad45131e Mon Sep 17 00:00:00 2001 From: Henrik Ingo Date: Thu, 5 Oct 2017 14:57:59 +0300 Subject: [PATCH 1/3] 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. --- build.js | 3 + js/impress.js | 306 +++++++++++++++++++++++++-- src/impress.js | 65 +++++- src/lib/README.md | 105 +++++++++ src/lib/gc.js | 229 ++++++++++++++++++++ src/plugins/navigation/navigation.js | 9 +- src/plugins/resize/resize.js | 2 +- 7 files changed, 693 insertions(+), 26 deletions(-) create mode 100644 src/lib/README.md create mode 100644 src/lib/gc.js diff --git a/build.js b/build.js index 34cf484..aad050a 100644 --- a/build.js +++ b/build.js @@ -2,6 +2,9 @@ var buildify = require('buildify'); buildify() .load('src/impress.js') + // Libraries from src/lib + .concat(['src/lib/gc.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..cae1809 100644 --- a/js/impress.js +++ b/js/impress.js @@ -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 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 ); diff --git a/src/impress.js b/src/impress.js index c9b8390..cfc3642 100644 --- a/src/impress.js +++ b/src/impress.js @@ -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! diff --git a/src/lib/README.md b/src/lib/README.md new file mode 100644 index 0000000..7d54d8c --- /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 libararyFunction1 = function () { + /* ... */ + }; + + var libararyFunction2 = 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..316e3c2 --- /dev/null +++ b/src/lib/gc.js @@ -0,0 +1,229 @@ +/** + * 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 ); diff --git a/src/plugins/navigation/navigation.js b/src/plugins/navigation/navigation.js index 08d6b04..0db7b00 100644 --- a/src/plugins/navigation/navigation.js +++ b/src/plugins/navigation/navigation.js @@ -39,6 +39,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 @@ -87,14 +88,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 +124,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 +150,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 diff --git a/src/plugins/resize/resize.js b/src/plugins/resize/resize.js index a1ec5a4..636d5da 100644 --- a/src/plugins/resize/resize.js +++ b/src/plugins/resize/resize.js @@ -36,7 +36,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 ); From f3d193d63698d1a68fd03f69058749a0902943ad Mon Sep 17 00:00:00 2001 From: Henrik Ingo Date: Thu, 5 Oct 2017 17:57:16 +0300 Subject: [PATCH 2/3] Move helper functions from core to src/lib/util.js This facilitates them being used from plugins as well as core impress.js. --- build.js | 1 + js/impress.js | 226 +++++++++++++++------------ src/impress.js | 101 ++++-------- src/lib/util.js | 97 ++++++++++++ src/plugins/navigation/navigation.js | 12 +- src/plugins/resize/resize.js | 15 +- 6 files changed, 260 insertions(+), 192 deletions(-) create mode 100644 src/lib/util.js diff --git a/build.js b/build.js index aad050a..b359316 100644 --- a/build.js +++ b/build.js @@ -4,6 +4,7 @@ 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']) diff --git a/js/impress.js b/js/impress.js index cae1809..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 ) { @@ -236,7 +188,7 @@ } // The gc library depends on being initialized before we do any changes to DOM. - var lib = initLibraries( rootId ); + lib = initLibraries( rootId ); body.classList.remove( "impress-not-supported" ); body.classList.add( "impress-supported" ); @@ -260,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; @@ -281,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; } }; @@ -291,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; } }; @@ -302,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 }; @@ -337,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"; @@ -347,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 ) }; @@ -360,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 ); @@ -393,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 @@ -405,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. @@ -416,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; }; @@ -478,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, @@ -650,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" ); @@ -946,6 +899,104 @@ } )( 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 * @@ -973,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 ) { @@ -988,6 +1033,7 @@ // 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 @@ -1114,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 ); @@ -1142,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 - api.lib.gc.addEventListener( window, "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 cfc3642..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 ) { @@ -236,7 +188,7 @@ } // The gc library depends on being initialized before we do any changes to DOM. - var lib = initLibraries( rootId ); + lib = initLibraries( rootId ); body.classList.remove( "impress-not-supported" ); body.classList.add( "impress-supported" ); @@ -260,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; @@ -281,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; } }; @@ -291,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; } }; @@ -302,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 }; @@ -337,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"; @@ -347,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 ) }; @@ -360,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 ); @@ -393,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 @@ -405,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. @@ -416,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; }; @@ -478,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, @@ -650,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" ); 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 0db7b00..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 ) { @@ -40,6 +34,7 @@ // 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 @@ -166,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 636d5da..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 - api.lib.gc.addEventListener( window, "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 ); From 8c12757b96f699488de7bc1e257f8365dd3471ae Mon Sep 17 00:00:00 2001 From: Henrik Ingo Date: Mon, 9 Oct 2017 09:30:11 +0300 Subject: [PATCH 3/3] Documentation fixes based on code review. --- DOCUMENTATION.md | 25 +++++++++++++++++++++++++ src/lib/README.md | 4 ++-- src/lib/gc.js | 19 ++++++++++++++----- 3 files changed, 41 insertions(+), 7 deletions(-) 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/src/lib/README.md b/src/lib/README.md index 7d54d8c..88e77cc 100644 --- a/src/lib/README.md +++ b/src/lib/README.md @@ -83,11 +83,11 @@ Putting all of the above together, a skeleton library file will look like: var instanceVar = {}; // LIBRARY FUNCTIONS - var libararyFunction1 = function () { + var libraryFunction1 = function () { /* ... */ }; - var libararyFunction2 = function () { + var libraryFunction2 = function () { /* ... */ }; diff --git a/src/lib/gc.js b/src/lib/gc.js index 316e3c2..3147d7f 100644 --- a/src/lib/gc.js +++ b/src/lib/gc.js @@ -31,34 +31,43 @@ recordStartingState( rootId ); // LIBRARY FUNCTIONS - // Below are definitions of the library functions we return at the end + // 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 ); }; - // Convenience wrapper that combines DOM appendChild with gc.pushElement + // `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 } ); }; - // Convenience wrapper that combines DOM addEventListener with gc.pushEventListener + // `addEventListener` 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. + // `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