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.
This commit is contained in:
Henrik Ingo
2017-10-11 18:41:02 +03:00
parent 8902a0a709
commit 82ff7cbde0
8 changed files with 767 additions and 43 deletions

View File

@@ -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!

View File

@@ -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:
<!-- When leaving this step, go directly to "step-5" -->
<div class="step" data-goto="step-5">
<!-- When leaving this step with next(), go directly to "step-5", instead of the next step.
If moving backwards to previous step - e.g. prev() instead of next() - then go to "step-1". -->
<div class="step" data-goto-next="step-5" data-goto-prev="step-1">
<!-- data-goto-key-list and data-goto-next-list allow you to build advanced non-linear navigation. -->
<div class="step" data-goto-key-list="ArrowUp ArrowDown ArrowRight ArrowLeft" data-goto-next-list="step-4 step-3 step-2 step-5">
Author
------
Copyright 2016 Henrik Ingo (@henrikingo)
Released under the MIT license.

171
src/plugins/goto/goto.js Normal file
View File

@@ -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:
*
* <!-- When leaving this step, go directly to "step-5" -->
* <div class="step" data-goto="step-5">
*
* <!-- When leaving this step with next(), go directly to "step-5", instead of next step.
* If moving backwards to previous step - e.g. prev() instead of next() -
* then go to "step-1". -->
* <div class="step" data-goto-next="step-5" data-goto-prev="step-1">
*
* <!-- data-goto-key-list and data-goto-next-list allow you to build advanced non-linear
* navigation. -->
* <div class="step"
* data-goto-key-list="ArrowUp ArrowDown ArrowRight ArrowLeft"
* data-goto-next-list="step-4 step-3 step-2 step-5">
*
* 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 );

89
src/plugins/rel/README.md Normal file
View File

@@ -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:
<!-- Position step 1000 px to the right and 500 px up from the previous step. -->
<div class="step" data-rel-x="1000" data-rel-y="500">
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:
<div class="step" data-rel-x="1.5w" data-rel-y="1.5h">
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:
<div class="step" data-x="100" data-y="100" data-z="100"></div>
<div class="step" data-x="100" data-y="100"></div>
<div class="step" data-x="100" data-y="100"></div>
To get the same rendering now, you need to add an explicit `data-z="0"` to the second step:
<div class="step" data-x="100" data-y="100" data-z="100"></div>
<div class="step" data-x="100" data-y="100" data-z="0"></div>
<div class="step" data-x="100" data-y="100"></div>
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

167
src/plugins/rel/rel.js Normal file
View File

@@ -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:
*
* <!-- Position step 1000 px to the right and 500 px up from the previous step. -->
* <div class="step" data-rel-x="1000" data-rel-y="500">
*
* 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:
*
* <div class="step" data-rel-x="1.5w" data-rel-y="1.5h">
*
* 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 );

View File

@@ -0,0 +1,21 @@
Stop Plugin
===========
Example:
<!-- Stop at this slide.
(For example, when used on the last slide, this prevents the
presentation from wrapping back to the beginning.) -->
<div class="step stop">
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.

35
src/plugins/stop/stop.js Normal file
View File

@@ -0,0 +1,35 @@
/**
* Stop Plugin
*
* Example:
*
* <!-- Stop at this slide.
* (For example, when used on the last slide, this prevents the
* presentation from wrapping back to the beginning.) -->
* <div class="step stop">
*
* 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 );