Source:rellax-wrapper.directive.js

(function () {
    'use strict';
    // Wrapper for rellax.js
    angular
        .module('mohistory')
        .directive('rellaxWrapper', rellaxWrapper);
    rellaxWrapper.$inject = ['$timeout', '$rootScope', '$location'];
    /**
     * Wrapper for Rellax.js, parallax library, by Moe Amaya (@moeamaya). Watches $location
     * for url changes, deletes any previous Rellax objects, then recreates them for the
     * new page. Also, watches the viewport width, removing Rellax when the page is below 1160.
     * Rellax will only return when URL is changed, i.e. on route change.
     * @memberof mohistory
     * @name rellaxWrapper
     * @ngdoc directive
     * @param {Object} $timeout AngularJS wrapper for window.timeout
     * @param {Object} $rootScope AngularJS Global scope object
     * @param {Object} $location AngularJS service that wraps window.location
     */
    function rellaxWrapper($timeout, $rootScope, $location) {
        var directive = {
            link: rellaxWrapperLink,
            restrict: 'AE',
        };
        return directive;
        /**
         * Link function responsible for all DOM manipulation.
         * @function rellaxWrapperLink
         * @memberof rellaxWrapper
         * @param {Object} scope AngularJS directive scope 
         * @param {Object} element JQuery element on which the directive is activated 
         * @param {Object} attrs attributes of the element on which the directive is activated 
         */
        function rellaxWrapperLink(scope, element, attrs) {
            // Booleans used for the async Rellax setup
            var rellaxActive = false;
            var settingUpRellax = false;
            // URL from $location.absURL
            var curUrl = '';
            // JQuery object used to reduce direct DOM queries
            var bodyElem = $('body');
            // Store the current instances of Rellax
            var relClassRellax = null;
            var bgShapeRellax = null;
            // Watch for any changes in the URL
            scope.$watch(function () {
                return $location.absUrl();
            }, function (value) {
                // If the URL has changed, we're not currently setting up rellax,
                // rellax has already been setup at least once, and the viewport is
                // greater than 1160
                if (value !== curUrl && !settingUpRellax && rellaxActive && $rootScope.viewport.width > 1160) {
                    cleanUpRellax();
                    setUpRellax();
                    curUrl = value;
                }
                // For the most part, this sets up Rellax on first load
                if (!settingUpRellax && !rellaxActive && $rootScope.viewport.width > 1160) {
                    setUpRellax();
                }
            });
            // Watch the viewport width, if it ever goes below 1160, turn off
            // Rellax.
            scope.$watch(function () {
                return $rootScope.viewport.width;
            }, function (value) {
                if (value <= 1160) {
                    cleanUpRellax();
                }
            });
            // Clean up, Rellax objects if/when the directive is removed.
            scope.$on('$destroy', function () {
                cleanUpRellax();
            });
            /**
             * Setup Rellax objects for the current page. Parallax is
             * applied to elements with the class of 'rellax' or 'bg-shape'.
             * In order to prevent memory leaks, i.e. unreachable assigned
             * memory, only create Rellax objects when the associated objects
             * are null (the cleanup function sets these objects to null when
             * finished). Timeout is used to allow the HTML to generate
             * before trying to select elements.
             * @function setUpRellax
             * @memberof rellaxWrapperLink
             */
            function setUpRellax() {
                rellaxActive = true;
                settingUpRellax = true;
                $timeout(function () {
                    var relClassElems = bodyElem.find('.rellax');
                    var bgShapeElems = bodyElem.find('.bg-shape');
                    if (relClassRellax === undefined || relClassRellax === null) {
                        relClassRellax = new Rellax('.rellax', {
                            speed: -2,
                            center: true,
                            round: true
                        });
                    }
                    if (bgShapeRellax === undefined || bgShapeRellax === null) {
                        bgShapeRellax = new Rellax('.bg-shape', {
                            speed: -2,
                            center: true,
                            round: true
                        });
                    }
                    settingUpRellax = false;
                }, 2000);
            }
            /**
             * Cleanup Rellax by destroying all linking variables and ensuring all
             * style attributes are removed from the effected elements. The biggest
             * concern, is to eliminate the possibility of memory leaks.
             * @function cleanUpRellax
             * @memberof rellaxWrapperLink
             */
            function cleanUpRellax() {
                // Since, settingUpRellax is async, we need to make sure we don't try
                // to cleanup Rellax while we're waiting to set it up.
                if (!settingUpRellax) {
                    var relClassElems = bodyElem.find('.rellax');
                    var bgShapeElems = bodyElem.find('.bg-shape');
                    if (relClassRellax !== undefined && relClassRellax !== null) {
                        if (relClassRellax.destroy) {
                            relClassRellax.destroy();
                        }
                        relClassRellax = null;
                    }
                    if (bgShapeRellax !== undefined && bgShapeRellax !== null) {
                        if (bgShapeRellax.destroy) {
                            bgShapeRellax.destroy();
                        }
                        bgShapeRellax = null;
                    }
                    if (relClassElems.length > 0) {
                        relClassElems.removeAttr('style');
                    }
                    if (bgShapeElems.length > 0) {
                        bgShapeElems.removeAttr('style');
                    }
                    rellaxActive = false;
                }
            }
        }
    }
    // ------------------------------------------
    // Rellax.js - v1.0.0
    // Buttery smooth parallax library
    // Copyright (c) 2016 Moe Amaya (@moeamaya)
    // MIT license
    //
    // Thanks to Paraxify.js and Jaime Cabllero
    // for parallax concepts
    // GitHub page: https://github.com/proustibat/demo-rellax
    //
    // Modified by Sierra Gregg to work with 
    // AngularJS directive. Major changes included
    // removing declaration of global Rellax object
    // and unlinking listeners in the destroy method.
    // Original comments were left in for future reference.
    // ------------------------------------------
    var Rellax = function (el, options) {
        var self = Object.create(Rellax.prototype);
        var posY = 0; // set it to -1 so the animate function gets called at least once
        var screenY = 0;
        var posX = 0;
        var screenX = 0;
        var blocks = [];
        var pause = false;
        // check what requestAnimationFrame to use, and if
        // it's not supported, use the onscroll event
        var loop = window.requestAnimationFrame ||
            window.webkitRequestAnimationFrame ||
            window.mozRequestAnimationFrame ||
            window.msRequestAnimationFrame ||
            window.oRequestAnimationFrame ||
            function (callback) {
                setTimeout(callback, 1000 / 60);
            };
        // check which transform property to use
        var transformProp = window.transformProp || (function () {
            var testEl = document.createElement('div');
            if (testEl.style.transform == null) {
                var vendors = ['Webkit', 'Moz', 'ms'];
                for (var vendor in vendors) {
                    if (testEl.style[vendors[vendor] + 'Transform'] !== undefined) {
                        return vendors[vendor] + 'Transform';
                    }
                }
            }
            return 'transform';
        })();
        // limit the given number in the range [min, max]
        var clamp = function (num, min, max) {
            return (num <= min) ? min : ((num >= max) ? max : num);
        };
        // Default Settings
        self.options = {
            speed: -2,
            center: false,
            round: true,
            vertical: true,
            horizontal: false,
            callback: function () {},
        };
        // User defined options (might have more in the future)
        if (options) {
            Object.keys(options).forEach(function (key) {
                self.options[key] = options[key];
            });
        }
        // If some clown tries to crank speed, limit them to +-10
        self.options.speed = clamp(self.options.speed, -10, 10);
        // By default, rellax class
        if (!el) {
            el = '.rellax';
        }
        var elements = document.querySelectorAll(el);
        // Now query selector
        if (elements.length > 0) {
            self.elems = elements;
        }
        // The elements don't exist
        else {
            console.log("The elements you're trying to select don't exist.");
        }
        // Let's kick this script off
        // Build array for cached element values
        // Bind scroll and resize to animate method
        var init = function () {
            screenY = window.innerHeight;
            screenX = window.innerWidth;
            setPosition();
            // Get and cache initial position of all elements
            for (var i = 0; i < self.elems.length; i++) {
                var block = createBlock(self.elems[i]);
                blocks.push(block);
            }
            // @Edit: Removed anonymous function wrapper for animate.
            window.addEventListener('resize', animate);
            // Start the loop
            update();
            // The loop does nothing if the scrollPosition did not change
            // so call animate to make sure every element has their transforms
            animate();
        };
        // We want to cache the parallax blocks'
        // values: base, top, height, speed
        // el: is dom object, return: el cache values
        var createBlock = function (el) {
            var dataPercentage = el.getAttribute('data-rellax-percentage');
            var dataSpeed = el.getAttribute('data-rellax-speed');
            var dataZindex = el.getAttribute('data-rellax-zindex') || 0;
            // initializing at scrollY = 0 (top of browser), scrollX = 0 (left of browser)
            // ensures elements are positioned based on HTML layout.
            //
            // If the element has the percentage attribute, the posY and posX needs to be
            // the current scroll position's value, so that the elements are still positioned based on HTML layout
            var posY = self.options.vertical ? (dataPercentage || self.options.center ? (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop) : 0) : 0;
            var posX = self.options.horizontal ? (dataPercentage || self.options.center ? (window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft) : 0) : 0;
            var blockTop = posY + el.getBoundingClientRect().top;
            var blockHeight = el.clientHeight || el.offsetHeight || el.scrollHeight;
            var blockLeft = posX + el.getBoundingClientRect().left;
            var blockWidth = el.clientWidth || el.offsetWidth || el.scrollWidth;
            // apparently parallax equation everyone uses
            var percentageY = dataPercentage ? dataPercentage : (posY - blockTop + screenY) / (blockHeight + screenY);
            var percentageX = dataPercentage ? dataPercentage : (posX - blockLeft + screenX) / (blockWidth + screenX);
            if (self.options.center) {
                percentageX = 0.5;
                percentageY = 0.5;
            }
            // Optional individual block speed as data attr, otherwise global speed
            // Check if has percentage attr, and limit speed to 5, else limit it to 10
            var speed = dataSpeed ? clamp(dataSpeed, -10, 10) : self.options.speed;
            if (dataPercentage || self.options.center) {
                speed = clamp(dataSpeed || self.options.speed, -5, 5);
            }
            var bases = updatePosition(percentageX, percentageY, speed);
            // ~~Store non-translate3d transforms~~
            // Store inline styles and extract transforms
            var style = el.style.cssText;
            var transform = '';
            // Check if there's an inline styled transform
            if (style.indexOf('transform') >= 0) {
                // Get the index of the transform
                var index = style.indexOf('transform');
                // Trim the style to the transform point and get the following semi-colon index
                var trimmedStyle = style.slice(index);
                var delimiter = trimmedStyle.indexOf(';');
                // Remove "transform" string and save the attribute
                if (delimiter) {
                    transform = " " + trimmedStyle.slice(11, delimiter).replace(/\s/g, '');
                } else {
                    transform = " " + trimmedStyle.slice(11).replace(/\s/g, '');
                }
            }
            return {
                baseX: bases.x,
                baseY: bases.y,
                top: blockTop,
                left: blockLeft,
                height: blockHeight,
                width: blockWidth,
                speed: speed,
                style: style,
                transform: transform,
                zindex: dataZindex
            };
        };
        // set scroll position (posY, posX)
        // side effect method is not ideal, but okay for now
        // returns true if the scroll changed, false if nothing happened
        var setPosition = function () {
            var oldY = posY;
            var oldX = posX;
            if (window.pageYOffset !== undefined) {
                posY = window.pageYOffset;
            } else {
                posY = (document.documentElement || document.body.parentNode || document.body).scrollTop;
            }
            if (window.pageXOffset !== undefined) {
                posX = window.pageXOffset;
            } else {
                posX = (document.documentElement || document.body.parentNode || document.body).scrollLeft;
            }
            if (oldY != posY && self.options.vertical) {
                // scroll changed, return true
                return true;
            }
            if (oldX != posX && self.options.horizontal) {
                // scroll changed, return true
                return true;
            }
            // scroll did not change
            return false;
        };
        // Ahh a pure function, gets new transform value
        // based on scrollPosition and speed
        // Allow for decimal pixel values
        var updatePosition = function (percentageX, percentageY, speed) {
            var result = {};
            var valueX = (speed * (100 * (1 - percentageX)));
            var valueY = (speed * (100 * (1 - percentageY)));
            result.x = self.options.round ? Math.round(valueX) : Math.round(valueX * 100) / 100;
            result.y = self.options.round ? Math.round(valueY) : Math.round(valueY * 100) / 100;
            return result;
        };
        //
        var update = function () {
            if (setPosition() && pause === false) {
                animate();
            }
            // loop again
            if (loop) {
                loop(update);
            }
        };
        // Transform3d on parallax element
        var animate = function () {
            for (var i = 0; i < self.elems.length; i++) {
                var percentageY = ((posY - blocks[i].top + screenY) / (blocks[i].height + screenY));
                var percentageX = ((posX - blocks[i].left + screenX) / (blocks[i].width + screenX));
                // Subtracting initialize value, so element stays in same spot as HTML
                var positions = updatePosition(percentageX, percentageY, blocks[i].speed); // - blocks[i].baseX;
                var positionY = positions.y - blocks[i].baseY;
                var positionX = positions.x - blocks[i].baseX;
                var zindex = blocks[i].zindex;
                // Move that element
                // (Set the new translation and append initial inline transforms.)
                var translate = 'translate3d(' + (self.options.horizontal ? positionX : '0') + 'px,' + (self.options.vertical ? positionY : '0') + 'px,' + zindex + 'px) ' + blocks[i].transform;
                self.elems[i].style[transformProp] = translate;
            }
            self.options.callback(positions);
        };
        self.destroy = function () {
            if (self.elems && self.elems.length) {
                for (var i = 0; i < self.elems.length; i++) {
                    self.elems[i].style.cssText = blocks[i].style;
                }
                window.removeEventListener('resize', animate);
                pause = true;
            }
        };
        if (self.elems && self.elems.length > 0) {
            init();
            return self;
        }
    };
})();