Team:Lund/scripts/ScrollTrigger

/**

* 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); }; }));