/**
* Written by Erik Terwan on 03/07/16. * * Erik Terwan - development + design * https://erikterwan.com * https://github.com/terwanerik * * MIT license. */
(function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define([], factory); } else if (typeof module === 'object' && module.exports) { // Node. Does not work with strict CommonJS, but // only CommonJS-like environments that support module.exports, // like Node. module.exports = factory(); } else { // Browser globals (root is window) root.ScrollTrigger = factory(); } }(this, function () {
'use strict';
return function(defaultOptions, bindTo, scrollIn) { /** * Trigger object, represents a single html element with the * data-scroll tag. Stores the options given in that tag. */ var Trigger = function(_defaultOptions, _element) { this.element = _element; this.defaultOptions = _defaultOptions; this.showCallback = null; this.hideCallback = null; this.visibleClass = 'visible'; this.hiddenClass = 'invisible'; this.addWidth = false; this.addHeight = false; this.once = false;
var xOffset = 0; var yOffset = 0;
this.left = function(_this){ return function(){ return _this.element.getBoundingClientRect().left; }; }(this);
this.top = function(_this){ return function(){ return _this.element.getBoundingClientRect().top; }; }(this);
this.xOffset = function(_this){ return function(goingLeft){ var offset = xOffset;
// add the full width of the element to the left position, so the // visibleClass is only added after the element is completely // in the viewport if (_this.addWidth && !goingLeft) { offset += _this.width(); } else if (goingLeft && !_this.addWidth) { offset -= _this.width(); }
return offset; }; }(this);
this.yOffset = function(_this){ return function(goingUp){ var offset = yOffset;
// add the full height of the element to the top position, so the // visibleClass is only added after the element is completely // in the viewport if (_this.addHeight && !goingUp) { offset += _this.height(); } else if (goingUp && !_this.addHeight) { offset -= _this.height(); }
return offset; }; }(this);
this.width = function(_this) { return function(){ return _this.element.offsetWidth; }; }(this);
this.height = function(_this) { return function(){ return _this.element.offsetHeight; }; }(this);
this.reset = function(_this) { return function() { _this.removeClass(_this.visibleClass); _this.removeClass(_this.hiddenClass); }; }(this);
this.addClass = function(_this){ var addClass = function(className, didAddCallback) { if (!_this.element.classList.contains(className)) { _this.element.classList.add(className); if ( typeof didAddCallback === 'function' ) { didAddCallback(); } } };
var retroAddClass = function(className, didAddCallback) { className = className.trim(); var regEx = new RegExp('(?:^|\\s)' + className + '(?:(\\s\\w)|$)', 'ig'); var oldClassName = _this.element.className; if ( !regEx.test(oldClassName) ) { _this.element.className += " " + className; if ( typeof didAddCallback === 'function' ) { didAddCallback(); } } };
return _this.element.classList ? addClass : retroAddClass; }(this);
this.removeClass = function(_this){ var removeClass = function(className, didRemoveCallback) { if (_this.element.classList.contains(className)) { _this.element.classList.remove(className); if ( typeof didRemoveCallback === 'function' ) { didRemoveCallback(); } } };
var retroRemoveClass = function(className, didRemoveCallback) { className = className.trim(); var regEx = new RegExp('(?:^|\\s)' + className + '(?:(\\s\\w)|$)', 'ig'); var oldClassName = _this.element.className; if ( regEx.test(oldClassName) ) { _this.element.className = oldClassName.replace(regEx, "$1").trim(); if ( typeof didRemoveCallback === 'function' ) { didRemoveCallback(); } } };
return _this.element.classList ? removeClass : retroRemoveClass; }(this);
this.init = function(_this){ return function(){ // set the default options var options = _this.defaultOptions; // parse the options given in the data-scroll attribute, if any var optionString = _this.element.getAttribute('data-scroll');
if (options) { if (options.toggle && options.toggle.visible) { _this.visibleClass = options.toggle.visible; }
if (options.toggle && options.toggle.hidden) { _this.hiddenClass = options.toggle.hidden; }
if (options.showCallback) { _this.showCallback = options.showCallback; }
if (options.hideCallback) { _this.hideCallback = options.hideCallback; }
if (options.centerHorizontal === true) { xOffset = _this.element.offsetWidth / 2; }
if (options.centerVertical === true) { yOffset = _this.element.offsetHeight / 2; }
if (options.offset && options.offset.x) { xOffset+= options.offset.x; }
if (options.offset && options.offset.y) { yOffset+= options.offset.y; }
if (options.addWidth) { _this.addWidth = options.addWidth; }
if (options.addHeight) { _this.addHeight = options.addHeight; }
if (options.once) { _this.once = options.once; } }
// parse the boolean options var parsedAddWidth = optionString.indexOf("addWidth") > -1; var parsedAddHeight = optionString.indexOf("addHeight") > -1; var parsedOnce = optionString.indexOf("once") > -1;
// check if the 'addHeight' was toggled via the data-scroll tag, that overrides the default settings object if (_this.addWidth === false && parsedAddWidth === true) { _this.addWidth = parsedAddWidth; }
if (_this.addHeight === false && parsedAddHeight === true) { _this.addHeight = parsedAddHeight; }
if (_this.once === false && parsedOnce === true) { _this.once = parsedOnce; }
// parse callbacks _this.showCallback = _this.element.hasAttribute('data-scroll-showCallback') ? _this.element.getAttribute('data-scroll-showCallback') : _this.showCallback; _this.hideCallback = _this.element.hasAttribute('data-scroll-hideCallback') ? _this.element.getAttribute('data-scroll-hideCallback') : _this.hideCallback;
// split the options on the toggle() parameter var classParts = optionString.split('toggle('); if (classParts.length > 1) { // the toggle() parameter was given, split it at ) to get the // content inside the parentheses, then split them on the comma var classes = classParts[1].split(')')[0].split(',');
// Check if trim exists if not, add the polyfill // courtesy of MDN if (!String.prototype.trim) { String.prototype.trim = function () { return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ); }; }
// trim and remove the dot _this.visibleClass = classes[0].trim().replace('.', ); _this.hiddenClass = classes[1].trim().replace('.', ); }
// adds the half of the offsetWidth/Height to the x/yOffset if (optionString.indexOf("centerHorizontal") > -1) { xOffset = _this.element.offsetWidth / 2; }
if (optionString.indexOf("centerVertical") > -1) { yOffset = _this.element.offsetHeight / 2; }
// split the options on the offset() parameter var offsetParts = optionString.split('offset('); if (offsetParts.length > 1) { // the offset() parameter was given, split it at ) to get the // content inside the parentheses, then split them on the comma var offsets = offsetParts[1].split(')')[0].split(',');
// remove the px unit and parse as integer xOffset += parseInt(offsets[0].replace('px', )); yOffset += parseInt(offsets[1].replace('px', )); }
// return this for chaining return _this; }; }(this); };
// the element to detect the scroll in this.scrollElement = window;
// the element to get the data-scroll elements from this.bindElement = document.body;
// the scope to call the callbacks in, defaults to window this.callScope = window;
// the Trigger objects var triggers = [];
// attached callbacks for the requestAnimationFrame loop, // this is handy for custom scroll based animation. So you // don't have multiple, unnecessary loops going. var attached = [];
// the previous scrollTop position, to determine if a user // is scrolling up or down. Set that to -1 -1 so the loop // always runs at least once var previousScroll = { left: -1, top: -1 };
// the loop method to use, preferred window.requestAnimationFrame var loop = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || window.oRequestAnimationFrame || function(callback){ setTimeout(callback, 1000 / 60); };
// if the requestAnimationFrame is looping var isLooping = false;
/**
* Initializes the scrollTrigger
*/
var init = function(_this) {
return function(defaultOptions, bindTo, scrollIn) {
// check if bindTo is not undefined or null,
// otherwise use the document.body
if (bindTo != undefined && bindTo != null) {
_this.bindElement = bindTo;
} else {
_this.bindElement = document.body;
}
// check if the scrollIn is not undefined or null, // otherwise use the window if (scrollIn != undefined && scrollIn != null) { _this.scrollElement = scrollIn; } else { _this.scrollElement = window; }
// Initially bind all elements with the data-scroll attribute _this.bind(_this.bindElement.querySelectorAll("[data-scroll]"));
// return 'this' for chaining return _this; }; }(this);
/** * Binds new HTMLElement objects to the trigger array */ this.bind = function(_this) { return function(elements) { // check if an array is given if (elements instanceof HTMLElement) { // if it's a single HTMLElement just create an array elements = [elements]; }
// get all trigger elements, e.g. all elements with // the data-scroll attribute and turn it from a NodeList // into a plain old array var newTriggers = [].slice.call(elements);
// map all the triggers to Trigger objects, and initialize them // so the options get parsed newTriggers = newTriggers.map(function (element, index) { var trigger = new Trigger(defaultOptions, element);
return trigger.init(); });
// add to the triggers array triggers = triggers.concat(newTriggers);
if (triggers.length > 0 && isLooping == false) { isLooping = true;
// start the update loop update(); } else { isLooping = false; }
// return 'this' for chaining return _this; }; }(this);
/** * Returns a trigger object from a htmlElement object (e.g. via querySelector()) */ this.triggerFor = function(_this) { return function(htmlElement){ var returnTrigger = null;
triggers.each(function(trigger, index) { if (trigger.element == htmlElement) { returnTrigger = trigger; } });
return returnTrigger; }; }(this);
/** * Removes a Trigger by its HTMLElement object, e.g via querySelector() */ this.destroy = function(_this) { return function(htmlElement) { triggers.each(function(trigger, index) { if (trigger.element == htmlElement) { triggers.splice(index, 1); } });
// return 'this' for chaining return _this; }; }(this);
/** * Removes all Trigger objects from the Trigger array */ this.destroyAll = function(_this) { return function() { triggers = [];
// return 'this' for chaining return _this; }; }(this);
/** * Resets a Trigger object, removes all added classes and then removes it from the triggers array. Like nothing * ever happened.. */ this.reset = function(_this) { return function(htmlElement) { var trigger = _this.triggerFor(htmlElement);
if (trigger != null) { trigger.reset();
var index = triggers.indexOf(trigger);
if (index > -1) { triggers.splice(index, 1); } }
// return 'this' for chaining return _this; }; }(this);
/** * Does the same as .reset() but for all triggers */ this.resetAll = function(_this) { return function() { triggers.each(function(trigger, index) { trigger.reset(); });
triggers = [];
// return 'this' for chaining return _this; }; }(this);
/** * Attaches a callback that get's called every time * the update method is called */ this.attach = function(_this) { return function(callback) { // add callback to array attached.push(callback);
if (!isLooping) { isLooping = true;
// start the update loop update(); }
// return 'this' for chaining return _this; }; }(this);
/**
* Detaches a callback
*/
this.detach = function(_this) {
return function(callback) {
// remove callback from array
var index = attached.indexOf(callback);
if (index > -1) { attached.splice(index, 1); }
return _this; }; }(this);
// store _this for use in the update function scope (strict)
var _this = this;
/**
* Gets called everytime the browser is ready for it, or when the user
* scrolls (on legacy browsers)
*/
function update() {
// FF and IE use the documentElement instead of body
var currentTop = !_this.bindElement.scrollTop ? document.documentElement.scrollTop : _this.bindElement.scrollTop;
var currentLeft = !_this.bindElement.scrollLeft ? document.documentElement.scrollLeft : _this.bindElement.scrollLeft;
// if the user scrolled if (previousScroll.left != currentLeft || previousScroll.top != currentTop) { _this.scrollDidChange(); }
if (triggers.length > 0 || attached.length > 0) { isLooping = true;
// and loop again loop(update); } else { isLooping = false; } }
this.scrollDidChange = function(_this) { return function() { var windowWidth = _this.scrollElement.innerWidth || _this.scrollElement.offsetWidth; var windowHeight = _this.scrollElement.innerHeight || _this.scrollElement.offsetHeight;
// FF and IE use the documentElement instead of body var currentTop = !_this.bindElement.scrollTop ? document.documentElement.scrollTop : _this.bindElement.scrollTop; var currentLeft = !_this.bindElement.scrollLeft ? document.documentElement.scrollLeft : _this.bindElement.scrollLeft;
var onceTriggers = [];
// loop through all triggers triggers.each(function(trigger, index){ var triggerLeft = trigger.left(); var triggerTop = trigger.top();
if (previousScroll.left > currentLeft) { // scrolling left, so we subtract the xOffset triggerLeft -= trigger.xOffset(true); } else if (previousScroll.left < currentLeft) { // scrolling right, so we add the xOffset triggerLeft += trigger.xOffset(false); }
if (previousScroll.top > currentTop) { // scrolling up, so we subtract the yOffset triggerTop -= trigger.yOffset(true); } else if (previousScroll.top < currentTop){ // scrolling down so then we add the yOffset triggerTop += trigger.yOffset(false); }
// toggle the classes if (triggerLeft < windowWidth && triggerLeft >= 0 && triggerTop < windowHeight && triggerTop >= 0) { // the element is visible trigger.addClass(trigger.visibleClass, function(){ if (trigger.showCallback) { functionCall(trigger, trigger.showCallback); } });
trigger.removeClass(trigger.hiddenClass);
if (trigger.once) { // remove trigger from triggers array onceTriggers.push(trigger); } } else { // the element is invisible trigger.addClass(trigger.hiddenClass); trigger.removeClass(trigger.visibleClass, function(){ if (trigger.hideCallback) { functionCall(trigger, trigger.hideCallback); } }); } });
// call the attached callbacks, if any attached.each(function(callback) { callback.call(_this, currentLeft, currentTop, windowWidth, windowHeight); });
// remove the triggers that are 'once' onceTriggers.each(function(trigger){ var index = triggers.indexOf(trigger);
if (index > -1) { triggers.splice(index, 1); } });
// save the current scroll position previousScroll.left = currentLeft; previousScroll.top = currentTop; }; }(this);
function functionCall(trigger, functionAsString) { var params = functionAsString.split('('); var method = params[0];
if (params.length > 1) { params = params[1].split(')')[0]; // get the value between the parentheses
// check if there are multiple attributes if (params.indexOf("', '") > -1) { params = params.split("', '"); } else if (params.indexOf("','") > -1) { params = params.split("','"); } else if (params.indexOf('", "') > -1) { params = params.split('", "'); } else if (params.indexOf('","') > -1) { params = params.split('","'); } else { // nope, just a single parameter params = [params]; } } else { params = []; }
// remove all quotes from the parameters params = params.map(function (param) { return removeQuotes(param); })
if (typeof _this.callScope[method] == "function") { // function exists in the call scope so let's try to call it. Some methods don't like to have the HTMLElement // passed as 'this', so retry without that if it fails. try { _this.callScope[method].apply(trigger.element, params); } catch (e) { // alright let's try again try { _this.callScope[method].apply(null, params); } catch (e) { // ah to bad. } } } }
// removes quotes from a string, e.g. turns 'foo' or "foo" into foo // typeof foo is string function removeQuotes(str) { str = str + ""; // force a string
if (str[0] == '"') { str = str.substr(1); }
if (str[0] == "'") { str = str.substr(1); }
if (str[str.length - 1] == '"') { str = str.substr(0, str.length - 1); }
if (str[str.length - 1] == "'") { str = str.substr(0, str.length - 1); }
return str; }
// Faster than .forEach Array.prototype.each = function(a) { var l = this.length; for(var i = 0; i < l; i++) { var e = this[i];
if (e) { a(e,i); } } };
return init(defaultOptions, bindTo, scrollIn); }; }));