(function(root, factory) {
  if (typeof define === 'function' && define.amd) {
    define([], factory);
  } else if (typeof exports === 'object') {
    module.exports = factory();
  } else {
    root.ScrollWatch = factory();
  }
}(this, function() {
'use strict';

// Give each instance on the page a unique ID.
var instanceId = 0;

// Store instance data privately so it can't be accessed/modified.
var instanceData = {};

var config = {
	// The default container is window, but we need the actual
	// documentElement to determine positioning.
	container: window.document.documentElement,
	watch: '[data-scroll-watch]',
	watchOnce: true,
	inViewClass: 'scroll-watch-in-view',
	ignoreClass: 'scroll-watch-ignore',
	debounce: false,
	debounceTriggerLeading: false,
	scrollDebounce: 250,
	resizeDebounce: 250,
	scrollThrottle: 250,
	resizeThrottle: 250,
	watchOffset: 0,
	infiniteScroll: false,
	infiniteOffset: 0,
	onElementInView: function(){},
	onElementOutOfView: function(){},
	onInfiniteXInView: function(){},
	onInfiniteYInView: function(){}
};

var initEvent = 'scrollwatchinit';

var extend = function(retObj) {

	var len = arguments.length;
	var i;
	var key;
	var obj;

	retObj = retObj || {};

	for (i = 1; i < len; i++) {

		obj = arguments[i];

		if (!obj) {

			continue;

		}

		for (key in obj) {

			if (obj.hasOwnProperty(key)) {

				retObj[key] = obj[key];

			}

		}
	}

	return retObj;

};

var throttle = function (fn, threshhold, scope) {

	var last;
	var deferTimer;

	threshhold = threshhold || 250;

	return function () {

		var context = scope || this;
		var now = +new Date();
		var args = arguments;

		if (last && now < last + threshhold) {

			window.clearTimeout(deferTimer);

			deferTimer = setTimeout(function () {

				last = now;

				fn.apply(context, args);

			}, threshhold);

		} else {

			last = now;

			fn.apply(context, args);

		}

	};

};

// http://underscorejs.org/#debounce
var debounce = function(func, wait, immediate) {

	var timeout;
	var args;
	var context;
	var timestamp;
	var result;

	var later = function() {

		var last = new Date().getTime() - timestamp;

		if (last < wait && last >= 0) {

			timeout = setTimeout(later, wait - last);

		} else {

			timeout = null;

			if (!immediate) {

				result = func.apply(context, args);

				if (!timeout) {

					context = args = null;

				}

			}

		}

	};

	return function() {

		var callNow = immediate && !timeout;

		context = this;
		args = arguments;
		timestamp = new Date().getTime();

		if (!timeout) {

			timeout = setTimeout(later, wait);

		}

		if (callNow) {

			result = func.apply(context, args);
			context = args = null;

		}

		return result;

	};

};

// Get the scrolling container element to watch if it's not the default window/documentElement.
var saveContainerElement = function() {

	if (!isContainerWindow.call(this)) {

		instanceData[this._id].config.container = document.querySelector(instanceData[this._id].config.container);

	}

};

// Save all elements to watch into an array.
var saveElements = function() {

	instanceData[this._id].elements = Array.prototype.slice.call(document.querySelectorAll(instanceData[this._id].config.watch + ':not(.' + instanceData[this._id].config.ignoreClass + ')'));

};

// Save the scroll position of the scrolling container so we can
// perform comparison checks.
var saveScrollPosition = function() {

	instanceData[this._id].lastScrollPosition = getScrollPosition.call(this);

};

var checkViewport = function(eventType) {

	checkElements.call(this, eventType);
	checkInfinite.call(this, eventType);

	// Chrome does not return 0,0 for scroll position when reloading a page
	// that was previously scrolled. To combat this, we will leave the scroll
	// position at the default 0,0 when a page is first loaded.
	if (eventType !== initEvent) {

		saveScrollPosition.call(this);

	}

};

// Determine if the watched elements are viewable within the
// scrolling container.
var checkElements = function(eventType) {

	// console.log('checkElements eventType: ' + eventType);

	var data = instanceData[this._id];
	var len = data.elements.length;
	var config = data.config;
	var inViewClass = config.inViewClass;
	var responseData = {
		eventType: eventType
	};
	var el;
	var i;

	for (i = 0; i < len; i++) {

		el = data.elements[i];

		// Prepare the data to pass to the callback.
		responseData.el = el;

		if (eventType === 'scroll') {

			responseData.direction = getScrolledDirection.call(this, getScrolledAxis.call(this));

		}

		if (isElementInView.call(this, el)) {

			if (!el.classList.contains(inViewClass)) {

				// Add a class hook and fire a callback for every
				// element that just came into view.

				el.classList.add(inViewClass);
				config.onElementInView.call(this, responseData);

				if (config.watchOnce) {

					// Remove this element so we don't check it again
					// next time.

					data.elements.splice(i, 1);
					len--;
					i--;

					// Flag this element with the ignore class so we
					// don't store it again if a refresh happens.

					el.classList.add(config.ignoreClass);

				}

			}

		} else {

			if (el.classList.contains(inViewClass)) {

				// Remove the class hook and fire a callback for every
				// element that just went out of view.

				el.classList.remove(inViewClass);
				config.onElementOutOfView.call(this, responseData);

			}

		}

	}

};

// Determine if the infinite scroll zone is in view. This could come into
// view by scrolling or resizing. Initial load must also be accounted
// for.
var checkInfinite = function(eventType) {

	var data = instanceData[this._id];
	var config = data.config;
	var i;
	var axis;
	var container;
	var viewableRange;
	var scrollSize;
	var callback;
	var responseData;

	if (config.infiniteScroll && !data.isInfiniteScrollPaused) {

		axis = ['x', 'y'];
		callback = ['onInfiniteXInView', 'onInfiniteYInView'];
		container = config.container;
		viewableRange = getViewableRange.call(this);
		scrollSize = [container.scrollWidth, container.scrollHeight];
		responseData = {};

		for (i = 0; i < 2; i++) {

			// If a scroll event triggered this check, verify the scroll
			// position actually changed for each axis. This stops
			// horizontal scrolls from triggering infiniteY callbacks
			// and vice versa. In other words, only trigger an infinite
			// callback if that axis was actually scrolled.

			if ((eventType === 'scroll' && hasScrollPositionChanged.call(this, axis[i]) || eventType === 'resize'|| eventType === 'refresh' || eventType === initEvent) && viewableRange[axis[i]].end + config.infiniteOffset >= scrollSize[i]) {

				// We've scrolled/resized all the way to the right/bottom.

				responseData.eventType = eventType;

				if (eventType === 'scroll') {

					responseData.direction = getScrolledDirection.call(this, axis[i]);

				}

				config[callback[i]].call(this, responseData);

			}

		}

	}

};

// Add listeners to the scrolling container for each instance.
var addListeners = function() {

	var data = instanceData[this._id];
	var scrollingElement = getScrollingElement.call(this);

	scrollingElement.addEventListener('scroll', data.scrollHandler, false);
	scrollingElement.addEventListener('resize', data.resizeHandler, false);

};

var removeListeners = function() {

	var data = instanceData[this._id];
	var scrollingElement = getScrollingElement.call(this);

	scrollingElement.removeEventListener('scroll', data.scrollHandler);
	scrollingElement.removeEventListener('resize', data.resizeHandler);

};

var getScrollingElement = function() {

	return isContainerWindow.call(this) ? window : instanceData[this._id].config.container;

};

// Get the width and height of viewport/scrolling container.
var getViewportSize = function() {

	var size = {
		w: instanceData[this._id].config.container.clientWidth,
		h: instanceData[this._id].config.container.clientHeight
	};

	return size;

};

// Get the scrollbar position of the scrolling container.
var getScrollPosition = function() {

	var pos = {};
	var container;

	if (isContainerWindow.call(this)) {

		pos.left = window.pageXOffset;
		pos.top = window.pageYOffset;


	} else {

		container = instanceData[this._id].config.container;

		pos.left = container.scrollLeft;
		pos.top = container.scrollTop;

	}
	// console.log(pos.top)
	// $('.banner_about').css('background-position-y',-pos.top)
	
	return pos;


};

// Get the pixel range currently viewable within the
// scrolling container.
var getViewableRange = function() {

	var range = {
		x: {},
		y: {}
	};
	var scrollPos = getScrollPosition.call(this);
	var viewportSize = getViewportSize.call(this);

	range.x.start = scrollPos.left;
	range.x.end =  range.x.start + viewportSize.w;
	range.x.size = range.x.end - range.x.start;

	range.y.start = scrollPos.top;
	range.y.end = range.y.start + viewportSize.h;
	range.y.size = range.y.end - range.y.start;

	return range;

};

// Get the pixel range of where this element falls within the
// scrolling container.
var getElementRange = function(el) {

	var range = {
		x: {},
		y: {}
	};
	var viewableRange = getViewableRange.call(this);
	var coords = el.getBoundingClientRect();
	var containerCoords;

	if (isContainerWindow.call(this)) {

		range.x.start = coords.left + viewableRange.x.start;
		range.x.end = coords.right + viewableRange.x.start;


		range.y.start = coords.top + viewableRange.y.start;
		range.y.end = coords.bottom + viewableRange.y.start;

	} else {

		containerCoords = instanceData[this._id].config.container.getBoundingClientRect();

		range.x.start = (coords.left - containerCoords.left) + viewableRange.x.start;
		range.x.end = range.x.start + coords.width;

		range.y.start = (coords.top - containerCoords.top) + viewableRange.y.start;
		range.y.end = range.y.start + coords.height;

	}

	range.x.size = range.x.end - range.x.start;
	range.y.size = range.y.end - range.y.start;

	return range;

};

// Determines which axis was just scrolled (x/horizontal or y/vertical).
var getScrolledAxis = function() {

	if (hasScrollPositionChanged.call(this, 'x')) {

		return 'x';

	}

	if (hasScrollPositionChanged.call(this, 'y')) {

		return 'y';

	}

};

var getScrolledDirection = function(axis) {

	var scrollDir = {x: ['right', 'left'], y: ['down', 'up']};
	var position = {x: 'left', y: 'top'};
	var lastScrollPosition = instanceData[this._id].lastScrollPosition;
	var curScrollPosition = getScrollPosition.call(this);

	return curScrollPosition[position[axis]] > lastScrollPosition[position[axis]] ? scrollDir[axis][0] : scrollDir[axis][1];

};

var hasScrollPositionChanged = function(axis) {

	var position = {x: 'left', y: 'top'};
	var lastScrollPosition = instanceData[this._id].lastScrollPosition;
	var curScrollPosition = getScrollPosition.call(this);

	return curScrollPosition[position[axis]] !== lastScrollPosition[position[axis]];

};

var isElementInView = function(el) {

	var viewableRange = getViewableRange.call(this);
	var elRange = getElementRange.call(this, el);
	var offset = instanceData[this._id].config.watchOffset;

	return isElementInVerticalView(elRange, viewableRange, offset) && isElementInHorizontalView(elRange, viewableRange, offset);

};

var isElementInVerticalView = function(elRange, viewableRange, offset) {

	return elRange.y.start < viewableRange.y.end + offset && elRange.y.end > viewableRange.y.start - offset;

};

var isElementInHorizontalView = function(elRange, viewableRange, offset) {

	return elRange.x.start < viewableRange.x.end + offset && elRange.x.end > viewableRange.x.start - offset;

};

var isContainerWindow = function() {

	return instanceData[this._id].config.container === window.document.documentElement;

};

var mergeOptions = function(opts) {

	extend(instanceData[this._id].config, config, opts);

};

var handler = function(e) {

	var eventType = e.type;

	// For scroll events, only check the viewport if something
	// has changed. Fixes issues when using gestures on a page
	// that doesn't need to scroll. An event would still fire,
	// but the position didn't change  because the
	// window/container "bounced" back into place.
	if (eventType === 'resize' || hasScrollPositionChanged.call(this, 'x') || hasScrollPositionChanged.call(this, 'y')) {

		checkViewport.call(this, eventType);

	}

};

var ScrollWatch = function(opts) {

	// Protect against missing new keyword.
	if (this instanceof ScrollWatch) {

		var data;

		Object.defineProperty(this, '_id', {value: instanceId++});

		// Keep all instance data private, except for the '_id', which will
		// be the key to get the private data for a specific instance.

		data = instanceData[this._id] = {

			config: {},
			// The elements to watch for this instance.
			elements: [],
			lastScrollPosition: {top: 0, left: 0},
			isInfiniteScrollPaused: false

		};

		mergeOptions.call(this, opts);

		// In order to remove listeners later and keep a correct reference
		// to 'this', give each instance it's own event handler.
		if (data.config.debounce) {

			data.scrollHandler = debounce(handler.bind(this), data.config.scrollDebounce, data.config.debounceTriggerLeading);
			data.resizeHandler = debounce(handler.bind(this), data.config.resizeDebounce, data.config.debounceTriggerLeading);

		} else {

			data.scrollHandler = throttle(handler.bind(this), data.config.scrollThrottle, this);
			data.resizeHandler = throttle(handler.bind(this), data.config.resizeThrottle, this);

		}

		saveContainerElement.call(this);
		addListeners.call(this);
		saveElements.call(this);
		checkViewport.call(this, initEvent);

	} else {

		return new ScrollWatch(opts);

	}

};

ScrollWatch.prototype = {

	// Should be manually called by user after loading in new content.
	refresh: function() {

		saveElements.call(this);
		checkViewport.call(this, 'refresh');

	},

	destroy: function() {

		removeListeners.call(this);
		delete instanceData[this._id];

	},

	pauseInfiniteScroll: function() {

		instanceData[this._id].isInfiniteScrollPaused = true;

	},

	resumeInfiniteScroll: function() {

		instanceData[this._id].isInfiniteScrollPaused = false;

	}

};

return ScrollWatch;
}));