seq-theme-two-up
Version:
A two column layout, one for showing content, the other a featured image
1,534 lines (1,204 loc) • 129 kB
JavaScript
/*
* Sequence.js
*
* The responsive CSS animation framework for creating unique sliders,
* presentations, banners, and other step-based applications.
*
* @link https://github.com/IanLunn/Sequence
* @author IanLunn
* @version 2.0.0
* @license http://sequencejs.com/licenses/
* @copyright Ian Lunn Design Limited 2015
*/
function defineSequence(imagesLoaded, Hammer) {
'use strict';
var instances = [],
instance = 0;
/**
* Sequence function
*
* @param {Object} element - the element Sequence is bound to
* @param {Object} options - this instance's options
* @returns {Object} self - Properties and methods available to this instance
*/
var Sequence = (function (element, options) {
var instanceId = element.getAttribute("data-seq-enabled");
// Prevent multiple instances on the same element. Return the object instead
if (instanceId !== null) {
return instances[instanceId];
}
// The element now has Sequence attached to it
element.setAttribute("data-seq-enabled", instance);
instance++;
/* --- PRIVATE VARIABLES/FUNCTIONS --- */
// Default Sequence settings
var defaults = {
/* --- General --- */
// The first step to show
startingStepId: 1,
// Should the starting step animate in to begin with?
startingStepAnimatesIn: false,
// When the last step is reached, should Sequence cycle back to the start?
cycle: true,
// How long to wait between the current phase animating out, and the next
// animating in.
phaseThreshold: true,
// Should animations be reversed when navigating backwards?
reverseWhenNavigatingBackwards: false,
// Should the active step be given a higher z-index?
moveActiveStepToTop: true,
/* --- Canvas Animation --- */
// Should the canvas automatically animate between steps?
animateCanvas: true,
// Time it should take to animate between steps
animateCanvasDuration: 250,
/* --- autoPlay --- */
// Cause Sequence to automatically navigate between steps
// Specify a number in milliseconds or true (for a default of 5000ms) to
// define the period Sequence should wait between each step before
// navigating to the next step
autoPlay: false,
// How long to wait between each step before navigation occurs again
autoPlayInterval: 5000,
// Amount of time to wait until autoPlay starts again after being stopped
autoPlayDelay: null,
// Direction of navigation when autoPlay is enabled
autoPlayDirection: 1,
// Use an autoPlay button? You can also specify a CSS selector to
// change what element acts as the button. If true, the element uses the
// class of "seq-autoplay"
autoPlayButton: true,
// Pause autoPlay when the Sequence element is hovered over
autoPlayPauseOnHover: true,
/* --- Navigation Skipping --- */
// Allow the user to navigate between steps even if they haven't
// finished animating
navigationSkip: true,
// How long to wait before the user is allowed to skip to another step
navigationSkipThreshold: 250,
// Fade a step when it has been skipped
fadeStepWhenSkipped: true,
// How long the fade should take
fadeStepTime: 500,
// When a step is skipped, the next step will immediately animate-in
// regardless of the phaseThreshold option
ignorePhaseThresholdWhenSkipped: false,
// Don't allow the user to go to a previous step when the current one is
// still active
preventReverseSkipping: false,
/* --- Next/Prev Button --- */
// Use next and previous buttons? You can also specify a CSS selector to
// change what element acts as the button. If true, the element uses
// classes of "seq-next" and "seq-prev"
nextButton: true,
prevButton: true,
/* --- Pagination --- */
// Use pagination? You can also specify a CSS selector to
// change what element acts as pagination. If true, the element uses the
// class of "seq-pagination"
pagination: true,
/* --- Preloader --- */
// You can also specify a CSS selector to
// change what element acts as the preloader. If true, the element uses
// the class of "seq-preloader"
preloader: false,
// Preload all images from specific steps
preloadTheseSteps: [1],
// Preload specified images
preloadTheseImages: [
/**
* Example usage
* "images/catEatingSalad.jpg",
* "images/grandmaDressedAsBatman.png"
*/
],
// Hide Sequence's steps until it has preloaded
hideStepsUntilPreloaded: false,
// (Debugging only) Prevent the preloader from hiding so you can
// test it's styles
pausePreloader: false,
/* --- Keyboard --- */
// Can the user navigate between steps by pressing keyboard buttons?
keyNavigation: false,
// When numeric keys 1 - 9 are pressed, Sequence will navigate to the
// corresponding step
numericKeysGoToSteps: false,
// Events to run when the user presses the left/right keys
keyEvents: {
left: function(sequence) {sequence.prev();},
right: function(sequence) {sequence.next();}
},
/* --- Touch Swipe --- */
// Can the user navigate between steps by swiping on a touch enabled device?
swipeNavigation: true,
// Events to run when the user swipes in a particular direction
swipeEvents: {
left: function(sequence) {sequence.next();},
right: function(sequence) {sequence.prev();},
up: undefined,
down: undefined
},
// Options to supply the third-party Hammer library See: http://hammerjs.github.io/recognizer-swipe/
swipeHammerOptions: {},
/* --- hashTags --- */
// Should the URL update to include a hashTag that relates to the current
// step being shown?
hashTags: false,
// Get the hashTag from an ID or data-seq-hashtag attribute?
hashDataAttribute: false,
// Should the hash change on the first step?
hashChangesOnFirstStep: false,
/* --- Fallback Theme --- */
// Settings to use when the browser doesn't support CSS transitions
fallback: {
// The speed to transition between steps
speed: 500
}
};
// Default value for autoPlay in milliseconds
var autoPlayDefault = 5000;
// See sequence.animation.domDelay() for an explanation of this
var domThreshold = 50;
// Throttle the window resize event
// see self.manageEvents.add.resizeThrottle()
var resizeThreshold = 100;
// Convert browser fixes to CSS strings
var prefixTranslations = {
animation: {
"WebkitAnimation": "-webkit-",
"animation": ""
}
};
/**
*
* This version of Modernizr is for use with Sequence.js and is included
* internally to prevent conflicts with other Modernizr builds.
*
* Modernizr 2.8.3 (Custom Build) | MIT & BSD
* Build: http://modernizr.com/download/#-cssanimations-csstransforms-csstransitions-svg-touch-prefixed-teststyles-testprop-testallprops-prefixes-domprefixes
*/
/* jshint ignore:start */
var Modernizr=function(a,b,c){function z(a){i.cssText=a}function A(a,b){return z(l.join(a+";")+(b||""))}function B(a,b){return typeof a===b}function C(a,b){return!!~(""+a).indexOf(b)}function D(a,b){for(var d in a){var e=a[d];if(!C(e,"-")&&i[e]!==c)return b=="pfx"?e:!0}return!1}function E(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:B(f,"function")?f.bind(d||b):f}return!1}function F(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+n.join(d+" ")+d).split(" ");return B(b,"string")||B(b,"undefined")?D(e,b):(e=(a+" "+o.join(d+" ")+d).split(" "),E(e,b,c))}var d="2.8.3",e={},f=b.documentElement,g="modernizr",h=b.createElement(g),i=h.style,j,k={}.toString,l=" -webkit- -moz- -o- -ms- ".split(" "),m="Webkit Moz O ms",n=m.split(" "),o=m.toLowerCase().split(" "),p={svg:"http://www.w3.org/2000/svg"},q={},r={},s={},t=[],u=t.slice,v,w=function(a,c,d,e){var h,i,j,k,l=b.createElement("div"),m=b.body,n=m||b.createElement("body");if(parseInt(d,10))while(d--)j=b.createElement("div"),j.id=e?e[d]:g+(d+1),l.appendChild(j);return h=["­",'<style id="s',g,'">',a,"</style>"].join(""),l.id=g,(m?l:n).innerHTML+=h,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=f.style.overflow,f.style.overflow="hidden",f.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),f.style.overflow=k),!!i},x={}.hasOwnProperty,y;!B(x,"undefined")&&!B(x.call,"undefined")?y=function(a,b){return x.call(a,b)}:y=function(a,b){return b in a&&B(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=u.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(u.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(u.call(arguments)))};return e}),q.touch=function(){var c;return"ontouchstart"in a||a.DocumentTouch&&b instanceof DocumentTouch?c=!0:w(["@media (",l.join("touch-enabled),("),g,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(a){c=a.offsetTop===9}),c},q.cssanimations=function(){return F("animationName")},q.csstransforms=function(){return!!F("transform")},q.csstransitions=function(){return F("transition")},q.svg=function(){return!!b.createElementNS&&!!b.createElementNS(p.svg,"svg").createSVGRect};for(var G in q)y(q,G)&&(v=G.toLowerCase(),e[v]=q[G](),t.push((e[v]?"":"no-")+v));return e.addTest=function(a,b){if(typeof a=="object")for(var d in a)y(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof enableClasses!="undefined"&&enableClasses&&(f.className+=" "+(b?"":"no-")+a),e[a]=b}return e},z(""),h=j=null,e._version=d,e._prefixes=l,e._domPrefixes=o,e._cssomPrefixes=n,e.testProp=function(a){return D([a])},e.testAllProps=F,e.testStyles=w,e.prefixed=function(a,b,c){return b?F(a,b,c):F(a,"pfx")},e}(window,window.document);
/* jshint ignore:end */
// Add indexOf() support to arrays for Internet Explorer 8
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function (searchElement, fromIndex) {
if ( this === undefined || this === null ) {
throw new TypeError( '"this" is null or not defined' );
}
// Hack to convert object.length to a UInt32
var length = this.length >>> 0;
fromIndex = +fromIndex || 0;
if (Math.abs(fromIndex) === Infinity) {
fromIndex = 0;
}
if (fromIndex < 0) {
fromIndex += length;
if (fromIndex < 0) {
fromIndex = 0;
}
}
for (;fromIndex < length; fromIndex++) {
if (this[fromIndex] === searchElement) {
return fromIndex;
}
}
return -1;
};
}
/**
* Determine the prefix to use for the pageVisibility API
*/
var hidden,
visibilityChange;
if (typeof document.hidden !== "undefined") {
// Opera 12.10 and Firefox 18 and later support
hidden = "hidden";
visibilityChange = "visibilitychange";
} else if (typeof document.mozHidden !== "undefined") {
hidden = "mozHidden";
visibilityChange = "mozvisibilitychange";
} else if (typeof document.msHidden !== "undefined") {
hidden = "msHidden";
visibilityChange = "msvisibilitychange";
} else if (typeof document.webkitHidden !== "undefined") {
hidden = "webkitHidden";
visibilityChange = "webkitvisibilitychange";
}
/**
* Is an object an array?
*
* @param {Object} object - The object we want to test
* @api private
*/
function isArray(object) {
if (Object.prototype.toString.call(object) === '[object Array]') {
return true;
}else {
return false;
}
}
/**
* Extend object a with the properties of object b.
* If there's a conflict, object b takes precedence.
*
* @param {Object} a - The first object to merge
* @param {Object} b - The second object to merge (takes precedence)
* @api private
*/
function extend(a, b) {
for (var i in b) {
a[i] = b[i];
}
return a;
}
/**
* Get the values of an element's CSS property
*
* @param {HTMLObject} element - The element to get the value from
* @param {String} property - The CSS property to get the value of
* @returns {String} value - The value from the element's CSS property
*/
function getStyle(element, property) {
var value;
// IE
if (element.currentStyle) {
value = element.currentStyle[property];
}
else if (document.defaultView && document.defaultView.getComputedStyle) {
value = document.defaultView.getComputedStyle(element, "")[property];
}
return value;
}
/**
* Cross Browser helper for addEventListener
*
* @param {HTMLObject} element - The element to attach the event to
* @param {String} eventName - The name of the event; "click" for example
* @param {Function} handler - The function to execute when the event occurs
* @returns {Function} handler - Returns the handler so it can be removed
*/
function addEvent(element, eventName, handler) {
if (element.addEventListener) {
element.addEventListener(eventName, handler, false);
return handler;
}
else if (element.attachEvent) {
// Allows IE to return this keyword
var handlerr = function() {
handler.call(element);
};
element.attachEvent("on" + eventName, handlerr);
return handlerr;
}
}
/**
* Cross Browser helper for removeEventListener
*
* @param {HTMLObject} element - The element to attach the event to
* @param {String} eventName - The name of the event; "click" for example
* @param {Function} handler - The function to execute when the event occurs
*/
function removeEvent(element, eventName, handler) {
if (element.addEventListener) {
element.removeEventListener(eventName, handler, false);
}
else if (element.detachEvent) {
element.detachEvent("on" + eventName, handler);
}
}
/**
* Converts a time value taken from a CSS property, such as "0.5s"
* and converts it to a number in milliseconds, such as 500
*
* @param {String} time - the time in a string
* @returns {Number} convertedTime - the time as a number
* @api private
*/
function convertTimeToMs(time) {
var convertedTime,
fraction;
// Deal with milliseconds and seconds
if (time.indexOf("ms") > -1) {
fraction = 1;
}else {
fraction = 1000;
}
if (time == "0s") {
convertedTime = 0;
}else {
convertedTime = parseFloat(time.replace("s", "")) * fraction;
}
return convertedTime;
}
/**
* Does an element have a particular class?
*
* @param {HTMLElement} element - The element to check
* @param {String} name - The name of the class to check for
* @returns {Boolean}
* @api private
*/
function hasClass(element, name) {
if (element === undefined) {
return;
}
return new RegExp('(\\s|^)' + name + '(\\s|$)').test(element.className);
}
/**
* Add a class to an element
*
* @param {Object} elements - The element(s) to add a class to
* @param {String} name - The class to add
* @api private
*/
function addClass(elements, name) {
var element,
elementsLength,
i;
// If only one element is defined, turn it into a nodelist so it'll pass
// through the for loop
if (isArray(elements) === false) {
elementsLength = 1;
elements = [elements];
}
elementsLength = elements.length;
for (i = 0; i < elementsLength; i++) {
element = elements[i];
if (hasClass(element, name) === false) {
element.className += (element.className ? ' ': '') + name;
}
}
}
/**
* Remove a class from an element
*
* @param {Object} elements - The element to remove a class from
* @param {String} name - The class to remove
* @api private
*/
function removeClass(elements, name) {
var element,
elementsLength,
i;
// If only one element is defined, turn it into a nodelist so it'll pass
// through the for loop
if (isArray(elements) === false) {
elementsLength = 1;
elements = [elements];
}
else {
elementsLength = elements.length;
}
for (i = 0; i < elementsLength; i++) {
element = elements[i];
if (hasClass(element, name) === true) {
element.className = element.className.replace(new RegExp('(\\s|^)' + name + '(\\s|$)'),' ').replace(/^\s+|\s+$/g, '');
}
}
}
/**
* Determine if the cursor is inside the boundaries of an
* element.
*
* @param {Object} element - The element to test
* @param {Object} cursor - The event holding cursor properties
*/
function insideElement(element, cursor) {
// Get the elements boundaries
var rect = element.getBoundingClientRect(),
inside = false;
// Return true if inside the boundaries of the Sequence element
if (cursor.clientX >= rect.left && cursor.clientX <= rect.right && cursor.clientY >= rect.top && cursor.clientY <= rect.bottom) {
inside = true;
}
return inside;
}
/**
* Determine if an element has a specified parent, and if so, return the
* index number for the element.
*
* The index is taken from the top level elements witint a pagination
* element. This function will iterate through each parent until it
* reaches the top level, then get all top level elements and determine
* the index of the chosen top level.
*
* @param {Object} parents - The parent element(s) that the child should be
* within
* @param {Object} target - The child element to test if it has the parent
* @param {Object} previousTarget - The element that was previously checked
* to determine if it was top level
* @api private
*/
function hasParent(parent, target, previousTarget) {
if (target.nodeName === "BODY") {
return false;
}
// We're at the pagination parent
if (parent === target) {
if (previousTarget !== undefined) {
// Get the top level element clicked and all top level elements
var topLevel = previousTarget;
var allTopLevel = parent.getElementsByTagName(topLevel.nodeName);
// Count the number of top level elements
var i = allTopLevel.length;
// Which top level element was clicked?
while (i--) {
if (topLevel === allTopLevel[i]) {
// One-base the index and return it
return i + 1;
}
}
}
}
// Not yet at the pagination parent element, iterate again
else {
previousTarget = target;
return hasParent(parent, target.parentNode, previousTarget);
}
}
/**
* Determine the Hammer direction required based on the swipe directions
* being used
* hammerjs.github.io/api/#directions
*
* @param {Object} swipeEvents - An object holding each swipe direction and
* the handler to execute
* @api private
*/
function getHammerDirection(swipeEvents) {
var swipeDirections = 0,
hammerDirection = Hammer.DIRECTION_NONE;
if (swipeEvents.left !== undefined || swipeEvents.right !== undefined) {
swipeDirections += 1;
}
if (swipeEvents.up !== undefined || swipeEvents.down !== undefined) {
swipeDirections += 2;
}
if (swipeDirections === 1) {
hammerDirection = Hammer.DIRECTION_HORIZONTAL;
} else if (swipeDirections === 2) {
hammerDirection = Hammer.DIRECTION_VERTICAL;
} else if (swipeDirections === 3) {
hammerDirection = Hammer.DIRECTION_ALL;
}
return hammerDirection;
}
/**
* Add classes to the Sequence container that allow for styling based on
* feature support.
*
* @param {HTMLObject} $el - The element to add the classes to
* @param {Object} Modernizr - Sequence's instance of Modernizr
* @api private
*/
function addFeatureSupportClasses($el, Modernizr) {
// TODO: Add support for all features used by Sequence and only manipulate
// the DOM once. Currently this function just adds a class for the touch
// feature. Probably best just to copy how Modernizr does it to implement
// full list of features
var prefix = "seq-",
support = "no-touch";
if (Modernizr.touch === true) {
support = "touch";
}
addClass($el, prefix + support);
}
/* --- PUBLIC PROPERTIES/METHODS --- */
// Expose some of Sequence's private properties
var self = {
modernizr: Modernizr
};
/**
* Manage UI elements such as nextButton, prevButton, and pagination
*/
self.ui = {
// Default UI elements
defaultElements: {
"nextButton": "seq-next",
"prevButton": "seq-prev",
"autoPlayButton": "seq-autoplay",
"pagination": "seq-pagination",
"preloader": "seq-preloader"
},
/**
* Get an UI element
*
* @param {String} type - The type of UI element (nextButton for example)
* @returns {Boolean | HTMLElement} option - True if using the default
* element, else an HTMLElement
*/
getElements: function(type, option) {
var element,
elements,
elementsLength,
relatedElements = [],
rel,
i;
// Get the element being used
if (option === true) {
// Default elements
elements = document.querySelectorAll("." + this.defaultElements[type]);
} else {
// Custom elements
elements = document.querySelectorAll(option);
}
elementsLength = elements.length;
// Does the element control this instance of Sequence? We're looking
// for either a global element or one with a rel attribute the same
// as this instances ID
for (i = 0; i < elementsLength; i++) {
element = elements[i];
rel = element.getAttribute("rel");
if (rel === null || rel === self.$container.getAttribute("id")) {
relatedElements.push(element);
}
}
return relatedElements;
},
/**
* Fade an element in using transitions if they're supported, else use JS
*
* @param {HTMLElement} element - The element to show
* @param {Number} duration - The duration to show the element over
*/
show: function(element, duration) {
if (self.propertySupport.transitions === true) {
element.style[Modernizr.prefixed("transitionDuration")] = duration + "ms";
element.style[Modernizr.prefixed("transitionProperty")] = "opacity, " + Modernizr.prefixed("transform");
element.style.opacity = 1;
}
else {
self.animationFallback.animate(element, "opacity", "", 0, 1, duration);
}
},
/**
* Fade an element out using transitions if they're supported, else use JS
*
* @param {HTMLElement} element - The element to hide
* @param {Number} duration - The duration to hide the element over
* @param {Function} callback - Function to execute when the element is
* hidden
*/
hide: function(element, duration, callback) {
if (self.propertySupport.transitions === true) {
element.style[Modernizr.prefixed("transitionDuration")] = duration + "ms";
element.style[Modernizr.prefixed("transitionProperty")] = "opacity, " + Modernizr.prefixed("transform");
element.style.opacity = 0;
}
else {
self.animationFallback.animate(element, "opacity", "", 1, 0, duration);
}
if (callback !== undefined) {
self.hideTimer = setTimeout(function() {
callback();
}, duration);
}
}
};
/**
* Methods relating to autoPlay
*/
self.autoPlay = {
/**
* Initiate autoPlay
*/
init: function() {
self.isAutoPlayPaused = false;
self.isAutoPlaying = false;
},
/**
* Determine the delay that should be applied before starting autoPlay. A
* custom delay should take precedence. If delay is true then the delay
* should use options.autoPlayDelay where specified. If not
* specified, use the same time as defined in options.autoPlayInterval
*
* @param {Boolean/Number} delay - Whether a delay should be applied before
* starting autoPlay (true = same amount as options.autoPlayInterval,
* false = no interval, number = custom interval period). Applied to
* autoPlay.start()
* @param {Number} startDelay - The delay applied via options.autoPlayDelay
* @param {Number} autoPlayInterval - The delay applied via options.autoPlayInterval
*/
getDelay: function(delay, startDelay, autoPlayInterval) {
switch (delay) {
case true:
delay = (startDelay === null) ? autoPlayInterval: startDelay;
break;
case false:
case undefined:
delay = 0;
break;
}
return delay;
},
/**
* Start autoPlay
*
* @param {Boolean/Number} delay - Whether a delay should be applied before
* starting autoPlay (true = same amount as options.autoPlayInterval,
* false = no delay, number = custom delay period)
* @param {Boolean} continuing - If autoPlay is continuing from a
* previous cycle, the started() callback won't be triggered
* @returns false - When autoPlay is already active and can't be started
* again
*/
start: function(delay, continuing) {
// Only start once
if (self.isAutoPlaying === true || self.isReady === false) {
return false;
}
var options = self.options;
// Which delay should we use?
delay = this.getDelay(delay, options.autoPlayDelay, options.autoPlayInterval);
// Callback (only to be triggered when autoPlay is continuing from a
// previous cycle)
if (continuing === undefined) {
self.started(self);
}
addClass(self.$container, "seq-autoplaying");
addClass(self.$autoPlay, "seq-autoplaying");
// autoPlay is now enabled and active
options.autoPlay = true;
self.isAutoPlaying = true;
// Only start a new autoPlay timer if Sequence isn't already animating.
// If it is, a new one will be started at the end of the animation.
if (self.isAnimating === false) {
// Choose the direction and start autoPlay
self.autoPlayTimer = setTimeout(function() {
if (options.autoPlayDirection === 1) {
self.next();
}else {
self.prev();
}
}, delay);
}
return true;
},
/**
* Stop autoPlay
*/
stop: function() {
if (self.options.autoPlay === true && self.isAutoPlaying === true) {
self.options.autoPlay = false;
self.isAutoPlaying = false;
clearTimeout(self.autoPlayTimer);
removeClass(self.$container, "seq-autoplaying");
removeClass(self.$autoPlay, "seq-autoplaying");
// Callback
self.stopped(self);
} else {
return false;
}
return true;
},
/**
* Unpause autoPlay
*
* autoPlay.pause() and autoPlay.unpause() are used internally to
* temporarily stop autoPlay when hovered over.
*/
unpause: function() {
if (self.isAutoPlayPaused === true) {
self.isAutoPlayPaused = false;
this.start(true);
} else {
return false;
}
return true;
},
/**
* Pause autoPlay
*
* autoPlay.pause() and autoPlay.unpause() are used internally to
* temporarily stop autoPlay when hovered over.
*/
pause: function() {
if (self.options.autoPlay === true) {
self.isAutoPlayPaused = true;
this.stop();
} else {
return false;
}
return true;
}
};
/**
* Controls Sequence's canvas
*/
self.canvas = {
/**
* Setup the canvas, screen, and steps ready to be animated
*/
init: function(id) {
if (self.$screen !== undefined) {
self.$screen.style.height = "100%";
self.$screen.style.width = "100%";
}
// Determine the position of each step and the transform properties
// required for the canvas so it can move to each step
self.canvas.getTransformProperties();
},
/**
* Get Sequence's steps
*
* @param {HTMLElement} canvas - The canvas element
* @returns {Array} steps - The elements that make up Sequence's steps
* @api private
*/
getSteps: function(canvas) {
var steps = [],
stepId,
step,
stepElements = canvas.children,
stepsLength = stepElements.length,
i;
// Where we'll save info about the animation
self.stepProperties = {};
// Get the steps that have a parent with a class of "seq-canvas"
for (i = 0; i < stepsLength; i++) {
step = stepElements[i];
stepId = i + 1;
steps.push(step);
// Add each step to the animation map, where we'll save its transform
// properties
self.stepProperties[stepId] = {};
self.stepProperties[stepId].isActive = false;
}
return steps;
},
/**
* Determine the position of each step and the transform properties
* required for the canvas so it can move to each step
*
*/
getTransformProperties: function() {
var i,
step,
stepId,
canvasTransform;
for (i = 0; i < self.noOfSteps; i++) {
step = self.$steps[i];
stepId = i + 1;
canvasTransform = {
"seqX": 0,
"seqY": 0,
"seqZ": 0
};
// Invert the steps offsetLeft and offsetTop to the canvas will always
// move to show the step
canvasTransform.seqX += step.offsetLeft * -1;
canvasTransform.seqY += step.offsetTop * -1;
self.stepProperties[stepId].canvasTransform = canvasTransform;
}
},
/**
* Move the canvas to show a specific step
*
* @param {Number} id - The ID of the step to move to
* @param {Boolean} animate - Should the canvas animate or snap?
*/
move: function(id, animate) {
if (self.options.animateCanvas === true) {
// Get the canvas element and step element to animate to
var duration = 0,
transforms;
// Should the canvas animate?
if (animate === true && self.firstRun === false) {
duration = self.options.animateCanvasDuration;
}
// Animate the canvas using CSS transitions
if (self.isFallbackMode === false) {
transforms = self.stepProperties[id].canvasTransform;
// Apply the transform CSS to the canvas
self.$canvas.style[Modernizr.prefixed("transitionDuration")] = duration + "ms";
self.$canvas.style[Modernizr.prefixed("transform")] = "translateX(" + transforms.seqX + "px) " + "translateY(" + transforms.seqY + "px) " + "translateZ(" + transforms.seqZ + "px) ";
}
return true;
}
return false;
},
/**
* Remove the no-JS "seq-in" class from a step
*
* @param {Object} self - Properties and methods available to this instance
* @api private
*/
removeNoJsClass: function() {
if (self.isFallbackMode === true) {
return;
}
// Look for the step with the "seq-in" class and remove the class
for (var i = 0; i < self.$steps.length; i++) {
var element = self.$steps[i];
if (hasClass(element, "seq-in") === true) {
var step = i + 1;
self.animation.resetInheritedSpeed(step);
removeClass(element, "seq-in");
}
}
}
};
/**
* Controls Sequence's step animations
*/
self.animation = {
/**
* Get the properties of a phase
*
* @param {Number} stepId - The ID of the step
* @returns {Object} ID and element of the step the phase belongs to,
* array of all child elements that belong to step, array of animated
* elements, timings object containing maxDuration, maxDelay, and
* maxLength
*/
getPhaseProperties: function(stepId) {
var stepElement = self.$steps[stepId - 1],
stepAnimatedChildren = stepElement.querySelectorAll("*[data-seq]"),
stepChildren = stepElement.querySelectorAll("*"),
stepChildrenLength = stepChildren.length,
el,
i,
watchedDurations = [],
watchedDelays = [],
watchedLengths = [],
durations = [],
delays = [],
lengths = [],
duration,
delay;
// Get the animation length of each element (duration + delay) and save
// for comparisson
for (i = 0; i < stepChildrenLength; i++) {
el = stepChildren[i];
duration = convertTimeToMs(getStyle(el, Modernizr.prefixed("transitionDuration")));
delay = convertTimeToMs(getStyle(el, Modernizr.prefixed("transitionDelay")));
// Save this elements animation length for all elements
durations.push(duration);
delays.push(delay);
lengths.push(duration + delay);
// Also save animation lengths but only for watched elements
if (el.getAttribute("data-seq") !== null) {
watchedDurations.push(duration);
watchedDelays.push(delay);
watchedLengths.push(duration + delay);
}
}
// Which were the longest durations and delays?
var maxDuration = Math.max.apply(Math, durations),
maxDelay = Math.max.apply(Math, delays),
maxTotal = Math.max.apply(Math, lengths);
// Which were the longest watched durations and delays?
var watchedMaxDuration = Math.max.apply(Math, watchedDurations),
watchedMaxDelay = Math.max.apply(Math, watchedDelays),
watchedMaxTotal = Math.max.apply(Math, watchedLengths);
return {
stepId: stepId,
stepElement: stepElement,
children: stepChildren,
animatedChildren: stepAnimatedChildren,
watchedTimings: {
maxDuration: watchedMaxDuration,
maxDelay: watchedMaxDelay,
maxTotal: watchedMaxTotal
},
timings: {
maxDuration: maxDuration,
maxDelay: maxDelay,
maxTotal: maxTotal
}
};
},
/**
* How long before the next phase should start?
* Ignore the phaseThreshold (on first run for example)
*/
getPhaseThreshold: function(ignorePhaseThreshold, phaseThresholdOption, isAnimating, currentPhaseDuration) {
var phaseThresholdTime = 0;
// Ignore the phaseThreshold if the developer wishes for this to happen
// if Sequence.js is animating
if (isAnimating === true && self.options.ignorePhaseThresholdWhenSkipped === true) {
ignorePhaseThreshold = true;
}
if (ignorePhaseThreshold === undefined) {
if (phaseThresholdOption === true) {
// The phaseThreshold should be the length of the current phase
// so the next starts immediately after
phaseThresholdTime = currentPhaseDuration;
} else if (phaseThresholdOption !== false) {
// Use the developer defined phaseThreshold
phaseThresholdTime = phaseThresholdOption;
}
}
return phaseThresholdTime;
},
/**
* Do we need to add a delay to account for one phase finishing
* before another?
*
* @param {Number} currentPhaseTotal - Amount of time in milliseconds the
* current phase will animate for
* @param {Number} nextPhaseTotal - Amount of time in milliseconds the
* next phase will animate for
* @param {Boolean} ignorePhaseThresholdWhenSkippedOption - if true,
* don't use a reversePhaseDelay
* @param {Boolean} isAnimating - Whether Sequence is animating
* @returns {Object} - Contains times that should delay the next or
* current phase accordingly
*/
getReversePhaseDelay: function(currentPhaseTotal, nextPhaseTotal, phaseThresholdOption, ignorePhaseThresholdWhenSkippedOption, isAnimating) {
var phaseDifference = 0,
current = 0,
next = 0;
// Only use a reversePhaseDelay if the phaseThreshold option is true or
// a custom time, and Sequence is not animating with the
// ignorePhaseThreshold option on
if (phaseThresholdOption !== true && (ignorePhaseThresholdWhenSkippedOption === false || isAnimating === false)) {
phaseDifference = currentPhaseTotal - nextPhaseTotal;
if (phaseDifference > 0) {
next = phaseDifference;
} else if (phaseDifference < 0) {
current = Math.abs(phaseDifference);
}
}
return {
next: next,
current: current
};
},
/**
* If the moveActiveStepToTop option is being used, move the next step
* to the top and the current step to the bottom via z-index
*
* @param {HTMLElement} currentElement - The current step to be moved off
* the top
* @param {HTMLElement} nextElement - The next step to be moved to the top
*/
moveActiveStepToTop: function(currentElement, nextElement) {
if (self.options.moveActiveStepToTop === true) {
var prevStepElement = self.$steps[self.prevStepId - 1],
lastStepId = self.noOfSteps - 1;
prevStepElement.style.zIndex = 1;
currentElement.style.zIndex = lastStepId;
nextElement.style.zIndex = self.noOfSteps;
}
return null;
},
/**
* If the navigationSkipThreshold option is being used, prevent the use
* of goTo() during the threshold period
*
* @param {Number} id - The ID of the step Sequence is trying to go to
* @param {HTMLObject} nextStepElement - The element for the next step
*/
manageNavigationSkip: function(id, nextStepElement) {
if (self.isFallbackMode === true) {
return;
}
var i,
stepProperties,
stepElement,
stepId,
phaseSkipped;
// Show the next step again
self.ui.show(nextStepElement, 0);
if (self.options.navigationSkip === true) {
// Start the navigation skip threshold
self.navigationSkipThresholdActive = true;
// Are there steps currently animating that need to be faded out?
if (self.phasesAnimating !== 0) {
// If a step is waiting to animate in based on the phaseThreshold,
// cancel it
clearTimeout(self.phaseThresholdTimer);
clearTimeout(self.nextPhaseStartedTimer);
// Fade a step if the user navigates to another prior to its
// animation finishing
if (self.options.fadeStepWhenSkipped === true) {
// Fade all elements that are animating
// (not including the current one)
for (i = 1; i <= self.noOfSteps; i++) {
stepProperties = self.stepProperties[i];
// Deal with the steps that were skipped whilst still animating
if (stepProperties.isActive === true && i !== id) {
stepElement = self.$steps[i - 1];
stepId = i;
phaseSkipped = {};
phaseSkipped.id = stepId;
phaseSkipped.element = stepElement;
// Save the ID of the skipped step so we can deal with it when it has faded out
self.phasesSkipped.push(phaseSkipped);
// Deal with the skipped step
self.animation.stepSkipped(stepElement);
}
}
}
}
// Start the navigationSkipThreshold timer to prevent being able to
// navigate too quickly
self.navigationSkipThresholdTimer = setTimeout(function() {
self.navigationSkipThresholdActive = false;
}, self.options.navigationSkipThreshold);
}
},
/**
* Deal with a step when it has been skipped
*
* @param {HTMLElement} stepElement - The step element that was skipped
*/
stepSkipped: function(stepElement) {
// TODO: Add resetWhenStepSkipped option -
// https://github.com/IanLunn/Sequence/issues/257
// Reset the skipped steps current and next phase ended timers and trigger
// them as soon as the steps have faded out
// Fade the step out
self.ui.hide(stepElement, self.options.fadeStepTime, function() {
});
},
/**
* Change a step's class. Example: go from step1 to step2
*
* @param {Number} id - The ID of the step to change
*/
changeStep: function(id) {
// Get the step to add
var stepToAdd = "seq-step" + id;
// Add the new step and remove the previous
if (self.currentStepId !== undefined) {
var stepToRemove = "seq-step" + self.currentStepId;
addClass(self.$container, stepToAdd);
removeClass(self.$container, stepToRemove);
}else {
addClass(self.$container, stepToAdd);
}
},
/**
* Apply the reversed properties to all animatable elements within a phase
*
* @param {Object} phaseProperties - Properties relating to the active phases
* (seq-in and seq-out)
* @param {Number} phaseDelay - A delay that is added when one phase
* animates longer than the other
* @param {Number} phaseThresholdTime - The amount of time in milliseconds
* before the next step should start animating in
* @param {Boolean} ignorePhaseThreshold - if true, ignore the threshold
* between phases (breaks reversal of animations but may be used when
* skipping navigation etc)
* @returns {Number} maxWatchedTotal - The new total length
* (duration + delay) for watched elements when reversed
*/
reverseProperties: function(phaseProperties, phaseDelay, phaseThresholdTime, ignorePhaseThreshold) {
var animation = this,
phaseElements = phaseProperties.children,
noOfPhaseElements = phaseElements.length,
stepDurations = phaseProperties.timings,
el,
i,
timingFunction,
timingFunctionReversed,
duration,
delay,
total,
maxTotal,
maxWatchedTotal,
totals = [],
watchedTotals = [];
for (i = 0; i < noOfPhaseElements; i++) {
el = phaseElements[i];
// Get each element's duration and delay
duration = convertTimeToMs(getStyle(el, Modernizr.prefixed("transitionDuration")));
delay = convertTimeToMs(getStyle(el, Modernizr.prefixed("transitionDelay")));
// Save the total
total = duration + delay;
// Delay elements in relation to the length of other elements in the
// phase eg. If one element A transitions for 1s and element B 2s
// element A should be delayed by 1s (element B length - element A length).
delay = stepDurations.maxTotal - total;
// Delay this phase's elements so they animate in relation to the
// other phase
if (ignorePhaseThreshold !== true) {
delay += phaseDelay;
}
// Update the total with the new delay
total = duration + delay;
// Save the total of the reversed animation
totals.push(total);
// Save the total of the reversed animation for watched elements only
if (el.getAttribute("data-seq") !== null) {
watchedTotals.push(total);
}
// Get the timing-function and reverse it
timingFunction = getStyle(el, Modernizr.prefixed("transitionTimingFunction"));
timingFunctionReversed = animation.reverseTimingFunction(timingFunction);
// Apply the reversed transition properties to each element
el.style[Modernizr.prefixed("transition")] = duration + "ms " + delay + "ms " + timingFunctionReversed;
}
// Get the longest total and delay of the reversed animations
maxTotal = Math.max.apply(Math, totals);
// Get the longest total and delay of the reversed animations (for
// watched elements only)
maxWatchedTotal = Math.max.apply(Math, watchedTotals);
// Remove the reversed transition properties from each element once it
// has finished animating; allowing for the inherited styles to take
// effect again.
setTimeout(function() {
animation.domDelay(function() {
for (i = 0; i < noOfPhaseElements; i++) {
el = phaseElements[i];
el.style[Modernizr.prefixed("transition")] = "";
}
});
}, maxTotal + phaseThresholdTime);
return maxWatchedTotal;
},
/**
* Go forward to the next step
*
* @param {Number} id - The ID of the next step
* @param {HTMLObject} currentStepElement - The element for the current step
* @param {HTMLObject} nextStepElement - The element for the next step
* @param {Boolean} ignorePhaseThreshold - if true, ignore the
* transitionThreshold setting and immediately go to the specified step
* @param {Boolean} hashTagNav - If navigation is triggered by the hashTag
*/
forward: function(id, currentStepElement, nextStepElement, ignorePhaseThreshold, hashTagNav) {
var animation = this,
currentPhaseProperties,
currentPhaseTotal,
phaseThresholdTime;
if (self.firstRun === false) {
// Callback
animation.currentPhaseStarted(self.currentStepId);
}
// Snap the step to the "animate-start" phase
removeClass(nextStepElement, "seq-out");
animation.domDelay(function() {
// Make the current step transition to "seq-out"
currentPhaseProperties = animation.startAnimateOut(self.currentStepId, currentStepElement, 1);
// Total duration of the current phase (duration + delay)
currentPhaseTotal = currentPhaseProperties.watchedTimings.maxTotal;
// How long before the next phase should start?
phaseThresholdTime = animation.getPhaseThreshold(ignorePhaseThreshold, self.options.phaseThreshold, self.isAnimating, currentPhaseTotal);
// Sequence is now animating
self.isAnimating = true;
// Make the next step transition to "seq-in"
animation.startAnimateIn(id, currentPhaseTotal, nextStepElement, phaseThresholdTime, hashTagNav);
});
},
/**
* Navigate in reverse to the next step
*
* @param {Number} id - The ID of the next step
* @param {HTMLObject} currentStepElement - The element for the current step
* @param {HTMLObject} nextStepElement - The element for the next step
* @param {Boolean} ignorePhaseThreshold - if true, ignore the
* transitionThreshold setting and immediately go to the specified step
* @param {Boolean} hashTagNav - If navigation is triggered by the hashTag
*/
reverse: function(id, currentStepElement, nextStepElement, ignorePhaseThreshold, hashTagNav) {
var animation = this,
currentId,
reversePhaseDelay,
phaseThresholdTime = 0,
currentPhaseProperties,
nextPhaseProperties,
currentPhaseTotal,