From 82ff7cbde05c5ccbc65ed2afc162c114c830f7de Mon Sep 17 00:00:00 2001 From: Henrik Ingo Date: Wed, 11 Oct 2017 18:41:02 +0300 Subject: [PATCH] Add a framework for synchronously executed preInit and preStepLeave plugins. This allows plugins to register to be executed at the beginning of impress().init() and impress().goto() respectively. By returning false, a plugin can also cancel the event. Also adds 3 plugins that use this: rel, goto and stop. --- js/impress.js | 161 ++++++++++++++++++++++++++++------ src/impress.js | 142 +++++++++++++++++++++++++----- src/plugins/goto/README.md | 24 ++++++ src/plugins/goto/goto.js | 171 +++++++++++++++++++++++++++++++++++++ src/plugins/rel/README.md | 89 +++++++++++++++++++ src/plugins/rel/rel.js | 167 ++++++++++++++++++++++++++++++++++++ src/plugins/stop/README.md | 21 +++++ src/plugins/stop/stop.js | 35 ++++++++ 8 files changed, 767 insertions(+), 43 deletions(-) create mode 100644 src/plugins/goto/README.md create mode 100644 src/plugins/goto/goto.js create mode 100644 src/plugins/rel/README.md create mode 100644 src/plugins/rel/rel.js create mode 100644 src/plugins/stop/README.md create mode 100644 src/plugins/stop/stop.js diff --git a/js/impress.js b/js/impress.js index 6a90cad..212b640 100644 --- a/js/impress.js +++ b/js/impress.js @@ -131,9 +131,6 @@ // We can't be sure that `classList` is supported body.className += " impress-not-supported "; - } else { - body.classList.remove( "impress-not-supported" ); - body.classList.add( "impress-supported" ); } // GLOBALS AND DEFAULTS @@ -143,6 +140,9 @@ // sure if it makes any sense in practice ;) var roots = {}; + var preInitPlugins = []; + var preStepLeavePlugins = []; + // Some default config values. var defaults = { width: 1024, @@ -238,12 +238,12 @@ } }; - // `onStepLeave` is called whenever the step element is left - // but the event is triggered only if the step is the same as - // last entered step. - var onStepLeave = function( step ) { - if ( lastEntered === step ) { - lib.util.triggerEvent( step, "impress:stepleave" ); + // `onStepLeave` is called whenever the currentStep element is left + // but the event is triggered only if the currentStep is the same as + // lastEntered step. + var onStepLeave = function( currentStep, nextStep ) { + if ( lastEntered === currentStep ) { + lib.util.triggerEvent( currentStep, "impress:stepleave", { next: nextStep } ); lastEntered = null; } }; @@ -283,9 +283,17 @@ } ); }; + // Initialize all steps. + // Read the data-* attributes, store in internal stepsData, and render with CSS. + var initAllSteps = function() { + steps = lib.util.$$( ".step", root ); + steps.forEach( initStep ); + }; + // `init` API function that initializes (and runs) the presentation. var init = function() { if ( initialized ) { return; } + execPreInitPlugins( root ); // First we set up the viewport for mobile devices. // For some reason iPad goes nuts when it is not done properly. @@ -345,8 +353,7 @@ body.classList.add( "impress-enabled" ); // Get and init steps - steps = lib.util.$$( ".step", root ); - steps.forEach( initStep ); + initAllSteps(); // Set a default initial state of the canvas currentState = { @@ -379,11 +386,23 @@ // `goto` API function that moves to step given as `el` parameter (by index, id or element). // `duration` optionally given as second parameter, is the transition duration in css. - var goto = function( el, duration ) { + // `reason` is the string "next", "prev" or "goto" (default) and will be made available to + // preStepLeave plugins. + // `origEvent` may contain event that caused the calll to goto, such as a key press event + var goto = function( el, duration, reason, origEvent ) { + reason = reason || "goto"; + origEvent = origEvent || null; - if ( !initialized || !( el = getStep( el ) ) ) { + if ( !initialized ) { + return false; + } - // Presentation not initialized or given element is not a step + // Re-execute initAllSteps for each transition. This allows to edit step attributes + // dynamically, such as change their coordinates, or even remove or add steps, and have + // that change apply when goto() is called. + initAllSteps(); + + if ( !( el = getStep( el ) ) ) { return false; } @@ -400,6 +419,31 @@ var step = stepsData[ "impress-" + el.id ]; + // If we are in fact moving to another step, start with executing the registered + // preStepLeave plugins. + if ( activeStep && activeStep !== el ) { + var event = { target: activeStep, detail: {} }; + event.detail.next = el; + event.detail.transitionDuration = duration; + event.detail.reason = reason; + if ( origEvent ) { + event.origEvent = origEvent; + } + + if ( execPreStepLeavePlugins( event ) === false ) { + + // PreStepLeave plugins are allowed to abort the transition altogether, by + // returning false. + // see stop and substep plugins for an example of doing just that + return false; + } + + // Plugins are allowed to change the detail values + el = event.detail.next; + step = stepsData[ "impress-" + el.id ]; + duration = event.detail.transitionDuration; + } + if ( activeStep ) { activeStep.classList.remove( "active" ); body.classList.remove( "impress-on-" + activeStep.id ); @@ -444,7 +488,7 @@ // Trigger leave of currently active element (if it's not the same step again) if ( activeStep && activeStep !== el ) { - onStepLeave( activeStep ); + onStepLeave( activeStep, el ); } // Now we alter transforms of `root` and `canvas` to trigger transitions. @@ -522,19 +566,21 @@ }; // `prev` API function goes to previous step (in document order) - var prev = function() { + // `event` is optional, may contain the event that caused the need to call prev() + var prev = function( origEvent ) { var prev = steps.indexOf( activeStep ) - 1; prev = prev >= 0 ? steps[ prev ] : steps[ steps.length - 1 ]; - return goto( prev ); + return goto( prev, undefined, "prev", origEvent ); }; // `next` API function goes to next step (in document order) - var next = function() { + // `event` is optional, may contain the event that caused the need to call next() + var next = function( origEvent ) { var next = steps.indexOf( activeStep ) + 1; next = next < steps.length ? steps[ next ] : steps[ 0 ]; - return goto( next ); + return goto( next, undefined, "next", origEvent ); }; // Teardown impress @@ -659,6 +705,64 @@ return lib; }; + // `addPreInitPlugin` allows plugins to register a function that should + // be run (synchronously) at the beginning of init, before + // impress().init() itself executes. + impress.addPreInitPlugin = function( plugin, weight ) { + weight = parseInt( weight ) || 10; + if ( weight <= 0 ) { + throw "addPreInitPlugin: weight must be a positive integer"; + } + + if ( preInitPlugins[ weight ] === undefined ) { + preInitPlugins[ weight ] = []; + } + preInitPlugins[ weight ].push( plugin ); + }; + + // Called at beginning of init, to execute all pre-init plugins. + var execPreInitPlugins = function( root ) { //jshint ignore:line + for ( var i = 0; i < preInitPlugins.length; i++ ) { + var thisLevel = preInitPlugins[ i ]; + if ( thisLevel !== undefined ) { + for ( var j = 0; j < thisLevel.length; j++ ) { + thisLevel[ j ]( root ); + } + } + } + }; + + // `addPreStepLeavePlugin` allows plugins to register a function that should + // be run (synchronously) at the beginning of goto() + impress.addPreStepLeavePlugin = function( plugin, weight ) { //jshint ignore:line + weight = parseInt( weight ) || 10; + if ( weight <= 0 ) { + throw "addPreStepLeavePlugin: weight must be a positive integer"; + } + + if ( preStepLeavePlugins[ weight ] === undefined ) { + preStepLeavePlugins[ weight ] = []; + } + preStepLeavePlugins[ weight ].push( plugin ); + }; + + // Called at beginning of goto(), to execute all preStepLeave plugins. + var execPreStepLeavePlugins = function( event ) { //jshint ignore:line + for ( var i = 0; i < preStepLeavePlugins.length; i++ ) { + var thisLevel = preStepLeavePlugins[ i ]; + if ( thisLevel !== undefined ) { + for ( var j = 0; j < thisLevel.length; j++ ) { + if ( thisLevel[ j ]( event ) === false ) { + + // If a plugin returns false, the stepleave event (and related transition) + // is aborted + return false; + } + } + } + } + }; + } )( document, window ); // THAT'S ALL FOLKS! @@ -702,34 +806,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 diff --git a/src/impress.js b/src/impress.js index 4788673..1901b17 100644 --- a/src/impress.js +++ b/src/impress.js @@ -131,9 +131,6 @@ // We can't be sure that `classList` is supported body.className += " impress-not-supported "; - } else { - body.classList.remove( "impress-not-supported" ); - body.classList.add( "impress-supported" ); } // GLOBALS AND DEFAULTS @@ -143,6 +140,9 @@ // sure if it makes any sense in practice ;) var roots = {}; + var preInitPlugins = []; + var preStepLeavePlugins = []; + // Some default config values. var defaults = { width: 1024, @@ -238,12 +238,12 @@ } }; - // `onStepLeave` is called whenever the step element is left - // but the event is triggered only if the step is the same as - // last entered step. - var onStepLeave = function( step ) { - if ( lastEntered === step ) { - lib.util.triggerEvent( step, "impress:stepleave" ); + // `onStepLeave` is called whenever the currentStep element is left + // but the event is triggered only if the currentStep is the same as + // lastEntered step. + var onStepLeave = function( currentStep, nextStep ) { + if ( lastEntered === currentStep ) { + lib.util.triggerEvent( currentStep, "impress:stepleave", { next: nextStep } ); lastEntered = null; } }; @@ -283,9 +283,17 @@ } ); }; + // Initialize all steps. + // Read the data-* attributes, store in internal stepsData, and render with CSS. + var initAllSteps = function() { + steps = lib.util.$$( ".step", root ); + steps.forEach( initStep ); + }; + // `init` API function that initializes (and runs) the presentation. var init = function() { if ( initialized ) { return; } + execPreInitPlugins( root ); // First we set up the viewport for mobile devices. // For some reason iPad goes nuts when it is not done properly. @@ -345,8 +353,7 @@ body.classList.add( "impress-enabled" ); // Get and init steps - steps = lib.util.$$( ".step", root ); - steps.forEach( initStep ); + initAllSteps(); // Set a default initial state of the canvas currentState = { @@ -379,11 +386,23 @@ // `goto` API function that moves to step given as `el` parameter (by index, id or element). // `duration` optionally given as second parameter, is the transition duration in css. - var goto = function( el, duration ) { + // `reason` is the string "next", "prev" or "goto" (default) and will be made available to + // preStepLeave plugins. + // `origEvent` may contain event that caused the calll to goto, such as a key press event + var goto = function( el, duration, reason, origEvent ) { + reason = reason || "goto"; + origEvent = origEvent || null; - if ( !initialized || !( el = getStep( el ) ) ) { + if ( !initialized ) { + return false; + } - // Presentation not initialized or given element is not a step + // Re-execute initAllSteps for each transition. This allows to edit step attributes + // dynamically, such as change their coordinates, or even remove or add steps, and have + // that change apply when goto() is called. + initAllSteps(); + + if ( !( el = getStep( el ) ) ) { return false; } @@ -400,6 +419,31 @@ var step = stepsData[ "impress-" + el.id ]; + // If we are in fact moving to another step, start with executing the registered + // preStepLeave plugins. + if ( activeStep && activeStep !== el ) { + var event = { target: activeStep, detail: {} }; + event.detail.next = el; + event.detail.transitionDuration = duration; + event.detail.reason = reason; + if ( origEvent ) { + event.origEvent = origEvent; + } + + if ( execPreStepLeavePlugins( event ) === false ) { + + // PreStepLeave plugins are allowed to abort the transition altogether, by + // returning false. + // see stop and substep plugins for an example of doing just that + return false; + } + + // Plugins are allowed to change the detail values + el = event.detail.next; + step = stepsData[ "impress-" + el.id ]; + duration = event.detail.transitionDuration; + } + if ( activeStep ) { activeStep.classList.remove( "active" ); body.classList.remove( "impress-on-" + activeStep.id ); @@ -444,7 +488,7 @@ // Trigger leave of currently active element (if it's not the same step again) if ( activeStep && activeStep !== el ) { - onStepLeave( activeStep ); + onStepLeave( activeStep, el ); } // Now we alter transforms of `root` and `canvas` to trigger transitions. @@ -522,19 +566,21 @@ }; // `prev` API function goes to previous step (in document order) - var prev = function() { + // `event` is optional, may contain the event that caused the need to call prev() + var prev = function( origEvent ) { var prev = steps.indexOf( activeStep ) - 1; prev = prev >= 0 ? steps[ prev ] : steps[ steps.length - 1 ]; - return goto( prev ); + return goto( prev, undefined, "prev", origEvent ); }; // `next` API function goes to next step (in document order) - var next = function() { + // `event` is optional, may contain the event that caused the need to call next() + var next = function( origEvent ) { var next = steps.indexOf( activeStep ) + 1; next = next < steps.length ? steps[ next ] : steps[ 0 ]; - return goto( next ); + return goto( next, undefined, "next", origEvent ); }; // Teardown impress @@ -659,6 +705,64 @@ return lib; }; + // `addPreInitPlugin` allows plugins to register a function that should + // be run (synchronously) at the beginning of init, before + // impress().init() itself executes. + impress.addPreInitPlugin = function( plugin, weight ) { + weight = parseInt( weight ) || 10; + if ( weight <= 0 ) { + throw "addPreInitPlugin: weight must be a positive integer"; + } + + if ( preInitPlugins[ weight ] === undefined ) { + preInitPlugins[ weight ] = []; + } + preInitPlugins[ weight ].push( plugin ); + }; + + // Called at beginning of init, to execute all pre-init plugins. + var execPreInitPlugins = function( root ) { //jshint ignore:line + for ( var i = 0; i < preInitPlugins.length; i++ ) { + var thisLevel = preInitPlugins[ i ]; + if ( thisLevel !== undefined ) { + for ( var j = 0; j < thisLevel.length; j++ ) { + thisLevel[ j ]( root ); + } + } + } + }; + + // `addPreStepLeavePlugin` allows plugins to register a function that should + // be run (synchronously) at the beginning of goto() + impress.addPreStepLeavePlugin = function( plugin, weight ) { //jshint ignore:line + weight = parseInt( weight ) || 10; + if ( weight <= 0 ) { + throw "addPreStepLeavePlugin: weight must be a positive integer"; + } + + if ( preStepLeavePlugins[ weight ] === undefined ) { + preStepLeavePlugins[ weight ] = []; + } + preStepLeavePlugins[ weight ].push( plugin ); + }; + + // Called at beginning of goto(), to execute all preStepLeave plugins. + var execPreStepLeavePlugins = function( event ) { //jshint ignore:line + for ( var i = 0; i < preStepLeavePlugins.length; i++ ) { + var thisLevel = preStepLeavePlugins[ i ]; + if ( thisLevel !== undefined ) { + for ( var j = 0; j < thisLevel.length; j++ ) { + if ( thisLevel[ j ]( event ) === false ) { + + // If a plugin returns false, the stepleave event (and related transition) + // is aborted + return false; + } + } + } + } + }; + } )( document, window ); // THAT'S ALL FOLKS! diff --git a/src/plugins/goto/README.md b/src/plugins/goto/README.md new file mode 100644 index 0000000..4800ba6 --- /dev/null +++ b/src/plugins/goto/README.md @@ -0,0 +1,24 @@ +Goto Plugin +=========== + +The goto plugin is a pre-stepleave plugin. It is executed before +`impress:stepleave` event, and will alter the destination where to transition next. + +Example: + + +
+ + +
+ + +
+ +Author +------ + +Copyright 2016 Henrik Ingo (@henrikingo) +Released under the MIT license. + diff --git a/src/plugins/goto/goto.js b/src/plugins/goto/goto.js new file mode 100644 index 0000000..21ec9f9 --- /dev/null +++ b/src/plugins/goto/goto.js @@ -0,0 +1,171 @@ +/** + * Goto Plugin + * + * The goto plugin is a pre-stepleave plugin. It is executed before impress:stepleave, + * and will alter the destination where to transition next. + * + * Example: + * + * + *
+ * + * + *
+ * + * + *
+ * + * Copyright 2016-2017 Henrik Ingo (@henrikingo) + * Released under the MIT license. + */ +/* global window, document, impress */ + +( function( document, window ) { + "use strict"; + var lib; + + var isNumber = function( numeric ) { + return !isNaN( numeric ); + }; + + var goto = function( event ) { + if ( ( !event ) || ( !event.target ) ) { + return; + } + + var data = event.target.dataset; + var steps = document.querySelectorAll( ".step" ); + + // Data-goto-key-list="" & data-goto-next-list="" ////////////////////////////////////////// + if ( data.gotoKeyList !== undefined && + data.gotoNextList !== undefined && + event.origEvent !== undefined && + event.origEvent.key !== undefined ) { + var keylist = data.gotoKeyList.split( " " ); + var nextlist = data.gotoNextList.split( " " ); + + if ( keylist.length !== nextlist.length ) { + window.console.log( + "impress goto plugin: data-goto-key-list and data-goto-next-list don't match:" + ); + window.console.log( keylist ); + window.console.log( nextlist ); + + // Don't return, allow the other categories to work despite this error + } else { + var index = keylist.indexOf( event.origEvent.key ); + if ( index >= 0 ) { + var next = nextlist[ index ]; + if ( isNumber( next ) ) { + event.detail.next = steps[ next ]; + + // If the new next element has its own transitionDuration, we're responsible + // for setting that on the event as well + event.detail.transitionDuration = lib.util.toNumber( + event.detail.next.dataset.transitionDuration, + event.detail.transitionDuration + ); + return; + } else { + var newTarget = document.getElementById( next ); + if ( newTarget && newTarget.classList.contains( "step" ) ) { + event.detail.next = newTarget; + event.detail.transitionDuration = lib.util.toNumber( + event.detail.next.dataset.transitionDuration, + event.detail.transitionDuration + ); + return; + } else { + window.console.log( "impress goto plugin: " + next + + " is not a step in this impress presentation." ); + } + } + } + } + } + + // Data-goto-next="" & data-goto-prev="" /////////////////////////////////////////////////// + + // Handle event.target data-goto-next attribute + if ( isNumber( data.gotoNext ) && event.detail.reason === "next" ) { + event.detail.next = steps[ data.gotoNext ]; + + // If the new next element has its own transitionDuration, we're responsible for setting + // that on the event as well + event.detail.transitionDuration = lib.util.toNumber( + event.detail.next.dataset.transitionDuration, event.detail.transitionDuration + ); + return; + } + if ( data.gotoNext && event.detail.reason === "next" ) { + var newTarget = document.getElementById( data.gotoNext ); // jshint ignore:line + if ( newTarget && newTarget.classList.contains( "step" ) ) { + event.detail.next = newTarget; + event.detail.transitionDuration = lib.util.toNumber( + event.detail.next.dataset.transitionDuration, + event.detail.transitionDuration + ); + return; + } else { + window.console.log( "impress goto plugin: " + data.gotoNext + + " is not a step in this impress presentation." ); + } + } + + // Handle event.target data-goto-prev attribute + if ( isNumber( data.gotoPrev ) && event.detail.reason === "prev" ) { + event.detail.next = steps[ data.gotoPrev ]; + event.detail.transitionDuration = lib.util.toNumber( + event.detail.next.dataset.transitionDuration, event.detail.transitionDuration + ); + return; + } + if ( data.gotoPrev && event.detail.reason === "prev" ) { + var newTarget = document.getElementById( data.gotoPrev ); // jshint ignore:line + if ( newTarget && newTarget.classList.contains( "step" ) ) { + event.detail.next = newTarget; + event.detail.transitionDuration = lib.util.toNumber( + event.detail.next.dataset.transitionDuration, event.detail.transitionDuration + ); + return; + } else { + window.console.log( "impress goto plugin: " + data.gotoPrev + + " is not a step in this impress presentation." ); + } + } + + // Data-goto="" /////////////////////////////////////////////////////////////////////////// + + // Handle event.target data-goto attribute + if ( isNumber( data.goto ) ) { + event.detail.next = steps[ data.goto ]; + event.detail.transitionDuration = lib.util.toNumber( + event.detail.next.dataset.transitionDuration, event.detail.transitionDuration + ); + return; + } + if ( data.goto ) { + var newTarget = document.getElementById( data.goto ); // jshint ignore:line + if ( newTarget && newTarget.classList.contains( "step" ) ) { + event.detail.next = newTarget; + event.detail.transitionDuration = lib.util.toNumber( + event.detail.next.dataset.transitionDuration, event.detail.transitionDuration + ); + return; + } else { + window.console.log( "impress goto plugin: " + data.goto + + " is not a step in this impress presentation." ); + } + } + }; + + // Register the plugin to be called in pre-stepleave phase + impress.addPreStepLeavePlugin( goto ); + +} )( document, window ); + diff --git a/src/plugins/rel/README.md b/src/plugins/rel/README.md new file mode 100644 index 0000000..d457185 --- /dev/null +++ b/src/plugins/rel/README.md @@ -0,0 +1,89 @@ +Relative Positioning Plugin +=========================== + +This plugin provides support for defining the coordinates of a step relative +to the previous step. This is often more convenient when creating presentations, +since as you add, remove or move steps, you may not need to edit the positions +as much as is the case with the absolute coordinates supported by impress.js +core. + +Example: + + +
+ +Following html attributes are supported for step elements: + + data-rel-x + data-rel-y + data-rel-z + +Non-zero values are also inherited from the previous step. This makes it easy to +create a boring presentation where each slide shifts for example 1000px down +from the previous. + +The above relative values are ignored, or set to zero, if the corresponding +absolute value (`data-x` etc...) is set. Note that this also has the effect of +resetting the inheritance functionality. + +In addition to plain numbers, which are pixel values, it is also possible to +define relative positions as a multiple of screen height and width, using +a unit of "h" and "w", respectively, appended to the number. + +Example: + +
+ + +IMPORTANT: Incompatible change +------------------------------ + +Enabling / adding this plugin has a small incompatible side effect on default values. + +Prior to this plugin, a missing data-x/y/z attribute would be assigned the default value of 0. +But when using a version of impress.js with this plugin enabled, a missing data-x/y/z attribute +will inherit the value from the previous step. (The first step will inherit the default value of 0.) + +For example, if you have an old presentation with the following 3 steps, they would be positioned +differently when using a version of impress.js that includes this plugin: + +
+
+
+ +To get the same rendering now, you need to add an explicit `data-z="0"` to the second step: + +
+
+
+ +Note that the latter code will render correctly also in old versions of impress.js. + +If you have an old presentation that doesn't use relative positioning, and for some reason you +cannot or don't want to add the explicit 0 values where needed, your last resort is to simply +remove the `rel.js` plugin completely. You can either: + +* Remove `rel.js` from [/build.js](../../../build.js) and recompile `impress.js` with: `npm build` +* Just open [/js/impress.js] in an editor and delete the `rel.js` code. +* Or, just uncomment the following single line, which is the last line of the plugin: + + impress.addPreInitPlugin( rel ); + + +About Pre-Init Plugins +---------------------- + +This plugin is a *pre-init plugin*. It is called synchronously from impress.js +core at the beginning of `impress().init()`. This allows it to process its own +data attributes first, and possibly alter the data-x, data-y and data-z attributes +that will then be processed by `impress().init()`. + +(Another name for this kind of plugin might be called a *filter plugin*, but +*pre-init plugin* is more generic, as a plugin might do whatever it wants in +the pre-init stage.) + + +Author +------ + +Henrik Ingo (@henrikingo), 2016 diff --git a/src/plugins/rel/rel.js b/src/plugins/rel/rel.js new file mode 100644 index 0000000..4a0204a --- /dev/null +++ b/src/plugins/rel/rel.js @@ -0,0 +1,167 @@ +/** + * Relative Positioning Plugin + * + * This plugin provides support for defining the coordinates of a step relative + * to the previous step. This is often more convenient when creating presentations, + * since as you add, remove or move steps, you may not need to edit the positions + * as much as is the case with the absolute coordinates supported by impress.js + * core. + * + * Example: + * + * + *
+ * + * Following html attributes are supported for step elements: + * + * data-rel-x + * data-rel-y + * data-rel-z + * + * These values are also inherited from the previous step. This makes it easy to + * create a boring presentation where each slide shifts for example 1000px down + * from the previous. + * + * In addition to plain numbers, which are pixel values, it is also possible to + * define relative positions as a multiple of screen height and width, using + * a unit of "h" and "w", respectively, appended to the number. + * + * Example: + * + *
+ * + * This plugin is a *pre-init plugin*. It is called synchronously from impress.js + * core at the beginning of `impress().init()`. This allows it to process its own + * data attributes first, and possibly alter the data-x, data-y and data-z attributes + * that will then be processed by `impress().init()`. + * + * (Another name for this kind of plugin might be called a *filter plugin*, but + * *pre-init plugin* is more generic, as a plugin might do whatever it wants in + * the pre-init stage.) + * + * Copyright 2016 Henrik Ingo (@henrikingo) + * Released under the MIT license. + */ + +/* global document, window */ + +( function( document, window ) { + "use strict"; + var lib; + + var startingState = {}; + + /** + * Extends toNumber() to correctly compute also relative-to-screen-size values 5w and 5h. + * + * Returns the computed value in pixels with w/h postfix removed. + */ + var toNumberAdvanced = function( numeric, fallback ) { + if ( typeof numeric !== "string" ) { + return lib.util.toNumber( numeric, fallback ); + } + var ratio = numeric.match( /^([+-]*[\d\.]+)([wh])$/ ); + if ( ratio == null ) { + return lib.util.toNumber( numeric, fallback ); + } else { + var value = parseFloat( ratio[ 1 ] ); + var multiplier = ratio[ 2 ] === "w" ? window.innerWidth : window.innerHeight; + return value * multiplier; + } + }; + + var computeRelativePositions = function( el, prev ) { + var data = el.dataset; + + if ( !prev ) { + + // For the first step, inherit these defaults + prev = { x:0, y:0, z:0, relative: { x:0, y:0, z:0 } }; + } + + var step = { + x: lib.util.toNumber( data.x, prev.x ), + y: lib.util.toNumber( data.y, prev.y ), + z: lib.util.toNumber( data.z, prev.z ), + relative: { + x: toNumberAdvanced( data.relX, prev.relative.x ), + y: toNumberAdvanced( data.relY, prev.relative.y ), + z: toNumberAdvanced( data.relZ, prev.relative.z ) + } + }; + + // Relative position is ignored/zero if absolute is given. + // Note that this also has the effect of resetting any inherited relative values. + if ( data.x !== undefined ) { + step.relative.x = 0; + } + if ( data.y !== undefined ) { + step.relative.y = 0; + } + if ( data.z !== undefined ) { + step.relative.z = 0; + } + + // Apply relative position to absolute position, if non-zero + // Note that at this point, the relative values contain a number value of pixels. + step.x = step.x + step.relative.x; + step.y = step.y + step.relative.y; + step.z = step.z + step.relative.z; + + return step; + }; + + var rel = function( root ) { + var steps = root.querySelectorAll( ".step" ); + var prev; + startingState[ root.id ] = []; + for ( var i = 0; i < steps.length; i++ ) { + var el = steps[ i ]; + startingState[ root.id ].push( { + el: el, + x: el.getAttribute( "data-x" ), + y: el.getAttribute( "data-y" ), + z: el.getAttribute( "data-z" ) + } ); + var step = computeRelativePositions( el, prev ); + + // Apply relative position (if non-zero) + el.setAttribute( "data-x", step.x ); + el.setAttribute( "data-y", step.y ); + el.setAttribute( "data-z", step.z ); + prev = step; + } + }; + + // Register the plugin to be called in pre-init phase + window.impress.addPreInitPlugin( rel ); + + // Register teardown callback to reset the data.x, .y, .z values. + document.addEventListener( "impress:init", function( event ) { + var root = event.target; + lib = event.detail.api.lib; + lib.gc.addCallback( function() { + var steps = startingState[ root.id ]; + var step; + while ( step = steps.pop() ) { + if ( step.x === null ) { + step.el.removeAttribute( "data-x" ); + } else { + step.el.setAttribute( "data-x", step.x ); + } + if ( step.y === null ) { + step.el.removeAttribute( "data-y" ); + } else { + step.el.setAttribute( "data-y", step.y ); + } + if ( step.z === null ) { + step.el.removeAttribute( "data-z" ); + } else { + step.el.setAttribute( "data-z", step.z ); + } + } + delete startingState[ root.id ]; + } ); + }, false ); +} )( document, window ); + diff --git a/src/plugins/stop/README.md b/src/plugins/stop/README.md new file mode 100644 index 0000000..f35a6f6 --- /dev/null +++ b/src/plugins/stop/README.md @@ -0,0 +1,21 @@ +Stop Plugin +=========== + +Example: + + +
+ +The stop plugin is a pre-stepleave plugin. It is executed before +`impress:stepleave` event. If the current slide has `class="stop"` +set, it will disable the next() command by setting the next slide to the current +slide. + +Author +------ + +Copyright 2016 Henrik Ingo (@henrikingo) +Released under the MIT license. + diff --git a/src/plugins/stop/stop.js b/src/plugins/stop/stop.js new file mode 100644 index 0000000..157a3ad --- /dev/null +++ b/src/plugins/stop/stop.js @@ -0,0 +1,35 @@ +/** + * Stop Plugin + * + * Example: + * + * + *
+ * + * Copyright 2016 Henrik Ingo (@henrikingo) + * Released under the MIT license. + */ +/* global document, window */ +( function( document, window ) { + "use strict"; + + var stop = function( event ) { + if ( ( !event ) || ( !event.target ) ) { + return; + } + + if ( event.target.classList.contains( "stop" ) ) { + if ( event.detail.reason === "next" ) { + return false; + } + } + }; + + // Register the plugin to be called in pre-stepleave phase + // The weight makes this plugin run fairly early. + window.impress.addPreStepLeavePlugin( stop, 2 ); + +} )( document, window ); +