/*
 * HC-Sticky
 * =========
 * Version: 2.2.7
 * Author: Some Web Media
 * Author URL: https://github.com/somewebmedia
 * Plugin URL: https://github.com/somewebmedia/hc-sticky
 * Description: JavaScript library that makes any element on your page visible while you scroll
 * License: MIT
 */

'use strict';

(function( global, factory ) {
	if ( typeof module === 'object' && typeof module.exports === 'object' ) {
		if ( global.document ) {
			module.exports = factory( global );
		} else {
			throw new Error( 'HC-Sticky requires a browser to run.' );
		}
	} else if ( typeof define === 'function' && define.amd ) {
		define( 'hcSticky', [], factory( global ) );
	} else {
		factory( global );
	}
})( typeof window !== 'undefined' ? window : this, ( window ) => {
	const document = window.document;

	const DEFAULT_OPTIONS = {
		top: 0,
		bottom: 0,
		bottomEnd: 0,
		innerTop: 0,
		innerSticker: null,
		stickyClass: 'sticky',
		stickTo: null,
		followScroll: true,
		responsive: null,
		mobileFirst: false,
		onStart: null,
		onStop: null,
		onBeforeResize: null,
		onResize: null,
		resizeDebounce: 100,
		disable: false
	};

	const deprecated = (() => {
		const pluginName = 'HC Sticky';

		return ( what, instead, type ) => {
			console.warn( '%c' + pluginName + ':' + '%c ' + type + "%c '" + what + "'" + '%c is now deprecated and will be removed. Use' + "%c '" + instead + "'" + '%c instead.', 'color: #fa253b', 'color: default', 'color: #5595c6', 'color: default', 'color: #5595c6', 'color: default' );
		};
	})();

	const hcSticky = function( elem, userSettings ) {
		userSettings = userSettings || {};

		// use querySeletor if string is passed
		if ( typeof elem === 'string' ) {
			elem = document.querySelector( elem );
		}

		// check if element exist
		if ( ! elem ) {
			return false;
		}

		if ( userSettings.queries ) {
			deprecated( 'queries', 'responsive', 'option' );
		}

		if ( userSettings.queryFlow ) {
			deprecated( 'queryFlow', 'mobileFirst', 'option' );
		}

		let STICKY_OPTIONS = {};
		const Helpers      = hcSticky.Helpers;
		const elemParent   = elem.parentNode;

		// parent can't be static
		if ( Helpers.getStyle( elemParent, 'position' ) === 'static' ) {
			elemParent.style.position = 'relative';
		}

		const setOptions = ( options ) => {
			options = options || {};

			if ( Helpers.isEmptyObject( options ) && ! Helpers.isEmptyObject( STICKY_OPTIONS ) ) {
				// nothing to set
				return;
			}

			// extend options
			STICKY_OPTIONS = Object.assign( {}, DEFAULT_OPTIONS, STICKY_OPTIONS, options );
		};

		const resetOptions = ( options ) => {
			STICKY_OPTIONS = Object.assign( {}, DEFAULT_OPTIONS, options || {} );
		};

		const getOptions = ( option ) => {
			return option ? STICKY_OPTIONS[option] : Object.assign( {}, STICKY_OPTIONS );
		};

		const isDisabled = () => {
			return STICKY_OPTIONS.disable;
		};

		const applyQueries = () => {
			const mediaQueries = STICKY_OPTIONS.responsive || STICKY_OPTIONS.queries;

			if ( mediaQueries ) {
				const window_width = window.innerWidth;

				// reset settings
				resetOptions( userSettings );

				if ( STICKY_OPTIONS.mobileFirst ) {
					for ( const width in mediaQueries ) {
						if ( window_width >= width && ! Helpers.isEmptyObject( mediaQueries[width] ) ) {
							setOptions( mediaQueries[width] );
						}
					}
				} else {
					const queriesArr = [];

					// convert to array so we can reverse loop it
					for ( const b in mediaQueries ) {
						const q = {};

						q[b] = mediaQueries[b];
						queriesArr.push( q );
					}

					for ( let i = queriesArr.length - 1; i >= 0; i -- ) {
						const query      = queriesArr[i];
						const breakpoint = Object.keys( query )[0];

						if ( window_width <= breakpoint && ! Helpers.isEmptyObject( query[breakpoint] ) ) {
							setOptions( query[breakpoint] );
						}
					}
				}
			}
		};

		// our helper function for getting necessary styles
		const getStickyCss = ( el ) => {
			const cascadedStyle = Helpers.getCascadedStyle( el );
			const computedStyle = Helpers.getStyle( el );

			const css = {
				height: el.offsetHeight + 'px',
				left: cascadedStyle.left,
				right: cascadedStyle.right,
				top: cascadedStyle.top,
				bottom: cascadedStyle.bottom,
				position: computedStyle.position,
				display: computedStyle.display,
				verticalAlign: computedStyle.verticalAlign,
				boxSizing: computedStyle.boxSizing,
				marginLeft: cascadedStyle.marginLeft,
				marginRight: cascadedStyle.marginRight,
				marginTop: cascadedStyle.marginTop,
				marginBottom: cascadedStyle.marginBottom,
				paddingLeft: cascadedStyle.paddingLeft,
				paddingRight: cascadedStyle.paddingRight
			};

			if ( cascadedStyle['float'] ) {
				css['float'] = cascadedStyle['float'] || 'none';
			}

			if ( cascadedStyle.cssFloat ) {
				css['cssFloat'] = cascadedStyle.cssFloat || 'none';
			}

			// old firefox box-sizing
			if ( computedStyle.MozBoxSizing ) {
				css['MozBoxSizing'] = computedStyle.MozBoxSizing;
			}

			css['width'] =
				cascadedStyle.width !== 'auto' ? cascadedStyle.width : (css.boxSizing === 'border-box' || css.MozBoxSizing === 'border-box' ? el.offsetWidth + 'px' : computedStyle.width);

			return css;
		};

		const Sticky = {
			css: {},
			position: null, // so we don't need to check css all the time
			stick: ( args ) => {
				args = args || {};

				// check if element is already sticky
				if ( Helpers.hasClass( elem, STICKY_OPTIONS.stickyClass ) ) {
					return;
				}

				if ( Spacer.isAttached === false ) {
					Spacer.attach();
				}

				Sticky.position = 'fixed';

				// apply styles
				elem.style.position = 'fixed';
				elem.style.left     = Spacer.offsetLeft + 'px';
				elem.style.width    = Spacer.width;

				if ( typeof args.bottom === 'undefined' ) {
					elem.style.bottom = 'auto';
				} else {
					elem.style.bottom = args.bottom + 'px';
				}

				if ( typeof args.top === 'undefined' ) {
					elem.style.top = 'auto';
				} else {
					elem.style.top = args.top + 'px';
				}

				// add sticky class
				if ( elem.classList ) {
					elem.classList.add( STICKY_OPTIONS.stickyClass );
				} else {
					elem.className += ' ' + STICKY_OPTIONS.stickyClass;
				}

				// fire 'start' event
				if ( STICKY_OPTIONS.onStart ) {
					STICKY_OPTIONS.onStart.call( elem, Object.assign( {}, STICKY_OPTIONS ) );
				}
			},
			release: ( args ) => {
				args      = args || {};
				args.stop = args.stop || false;

				// check if we've already done this
				if ( args.stop !== true && Sticky.position !== 'fixed' && Sticky.position !== null && ((typeof args.top === 'undefined' && typeof args.bottom === 'undefined') || (typeof args.top !== 'undefined' && (parseInt( Helpers.getStyle( elem, 'top' ) ) || 0) === args.top) || (typeof args.bottom !== 'undefined' && (parseInt( Helpers.getStyle( elem, 'bottom' ) ) || 0) === args.bottom)) ) {
					return;
				}

				if ( args.stop === true ) {
					// remove spacer
					if ( Spacer.isAttached === true ) {
						Spacer.detach();
					}
				} else {
					// check spacer
					if ( Spacer.isAttached === false ) {
						Spacer.attach();
					}
				}

				const position = args.position || Sticky.css.position;

				// remember position
				Sticky.position = position;

				// apply styles
				elem.style.position = position;
				elem.style.left     = args.stop === true ? Sticky.css.left : Spacer.positionLeft + 'px';
				elem.style.width    = position !== 'absolute' ? Sticky.css.width : Spacer.width;

				if ( typeof args.bottom === 'undefined' ) {
					elem.style.bottom = args.stop === true ? '' : 'auto';
				} else {
					elem.style.bottom = args.bottom + 'px';
				}

				if ( typeof args.top === 'undefined' ) {
					elem.style.top = args.stop === true ? '' : 'auto';
				} else {
					elem.style.top = args.top + 'px';
				}

				// remove sticky class
				if ( elem.classList ) {
					elem.classList.remove( STICKY_OPTIONS.stickyClass );
				} else {
					elem.className =
						elem.className.replace( new RegExp( '(^|\\b)' + STICKY_OPTIONS.stickyClass.split( ' ' ).join( '|' ) + '(\\b|$)', 'gi' ), ' ' );
				}

				// fire 'stop' event
				if ( STICKY_OPTIONS.onStop ) {
					STICKY_OPTIONS.onStop.call( elem, Object.assign( {}, STICKY_OPTIONS ) );
				}
			}
		};

		const Spacer = {
			el: document.createElement( 'div' ),
			offsetLeft: null,
			positionLeft: null,
			width: null,
			isAttached: false,
			init: () => {
				Spacer.el.className = 'sticky-spacer';

				// copy styles from sticky element
				for ( const prop in Sticky.css ) {
					Spacer.el.style[prop] = Sticky.css[prop];
				}

				// just to be sure the spacer is behind everything
				Spacer.el.style['z-index'] = '-1';

				const elemStyle = Helpers.getStyle( elem );

				// get spacer offset and position
				Spacer.offsetLeft   = Helpers.offset( elem ).left - (parseInt( elemStyle.marginLeft ) || 0);
				Spacer.positionLeft = Helpers.position( elem ).left;

				// get spacer width
				Spacer.width = Helpers.getStyle( elem, 'width' );
			},
			attach: () => {
				// insert spacer to DOM
				elemParent.insertBefore( Spacer.el, elem );
				Spacer.isAttached = true;
			},
			detach: () => {
				// remove spacer from DOM
				Spacer.el         = elemParent.removeChild( Spacer.el );
				Spacer.isAttached = false;
			}
		};

		// define our private variables
		let stickTo_document;
		let container;
		let inner_sticker;

		let container_height;
		let container_offsetTop;

		let elemParent_offsetTop;

		let window_height;

		let options_top;
		let options_bottom;

		let stick_top;
		let stick_bottom;

		let top_limit;
		let bottom_limit;

		let largerSticky;
		let sticky_height;
		let sticky_offsetTop;

		let calcContainerHeight;
		let calcStickyHeight;

		const calcSticky = () => {
			// get/set element styles
			Sticky.css = getStickyCss( elem );

			// init or reinit spacer
			Spacer.init();

			// check if referring element is document
			stickTo_document =
				STICKY_OPTIONS.stickTo && (STICKY_OPTIONS.stickTo === 'document' || (STICKY_OPTIONS.stickTo.nodeType && STICKY_OPTIONS.stickTo.nodeType === 9) || (typeof STICKY_OPTIONS.stickTo === 'object' && STICKY_OPTIONS.stickTo instanceof (typeof HTMLDocument !== 'undefined' ? HTMLDocument : Document))) ? true : false;

			// select referred container
			container =
				STICKY_OPTIONS.stickTo ? stickTo_document ? document : Helpers.getElement( STICKY_OPTIONS.stickTo ) : elemParent;

			// get sticky height
			calcStickyHeight = () => {
				const height = elem.offsetHeight + (parseInt( Sticky.css.marginTop ) || 0) + (parseInt( Sticky.css.marginBottom ) || 0);
				const h_diff = (sticky_height || 0) - height;

				if ( h_diff >= - 1 && h_diff <= 1 ) {
					// sometimes element height changes by 1px when it get fixed position, so don't return new value
					return sticky_height;
				} else {
					return height;
				}
			};

			sticky_height = calcStickyHeight();

			// get container height
			calcContainerHeight = () => {
				return ! stickTo_document ? container.offsetHeight : Math.max( document.documentElement.clientHeight, document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight );
			};

			container_height = calcContainerHeight();

			container_offsetTop  = ! stickTo_document ? Helpers.offset( container ).top : 0;
			elemParent_offsetTop = ! STICKY_OPTIONS.stickTo ? container_offsetTop // parent is container
			                                                : ! stickTo_document ? Helpers.offset( elemParent ).top : 0;
			window_height        = window.innerHeight;
			sticky_offsetTop     = elem.offsetTop - (parseInt( Sticky.css.marginTop ) || 0);

			// get inner sticker element
			inner_sticker = Helpers.getElement( STICKY_OPTIONS.innerSticker );

			// top
			options_top =
				isNaN( STICKY_OPTIONS.top ) && STICKY_OPTIONS.top.indexOf( '%' ) > - 1 ? (parseFloat( STICKY_OPTIONS.top ) / 100) * window_height : STICKY_OPTIONS.top;

			// bottom
			options_bottom =
				isNaN( STICKY_OPTIONS.bottom ) && STICKY_OPTIONS.bottom.indexOf( '%' ) > - 1 ? (parseFloat( STICKY_OPTIONS.bottom ) / 100) * window_height : STICKY_OPTIONS.bottom;

			// calculate sticky breakpoints
			stick_top = inner_sticker ? inner_sticker.offsetTop : STICKY_OPTIONS.innerTop ? STICKY_OPTIONS.innerTop : 0;

			stick_bottom =
				isNaN( STICKY_OPTIONS.bottomEnd ) && STICKY_OPTIONS.bottomEnd.indexOf( '%' ) > - 1 ? (parseFloat( STICKY_OPTIONS.bottomEnd ) / 100) * window_height : STICKY_OPTIONS.bottomEnd;

			top_limit = container_offsetTop - options_top + stick_top + sticky_offsetTop;
		};

		// store scroll position so we can determine scroll direction
		let last_pos = window.pageYOffset || document.documentElement.scrollTop;
		let diff_y   = 0;
		let scroll_dir;

		const runSticky = () => {
			// always calculate sticky and container height in case of DOM change
			sticky_height    = calcStickyHeight();
			container_height = calcContainerHeight();

			bottom_limit = container_offsetTop + container_height - options_top - stick_bottom;

			// check if sticky is bigger than container
			largerSticky = sticky_height > window_height;

			const offset_top        = window.pageYOffset || document.documentElement.scrollTop;
			const sticky_top        = Helpers.offset( elem ).top;
			const sticky_window_top = sticky_top - offset_top;
			let bottom_distance;

			// get scroll direction
			scroll_dir = offset_top < last_pos ? 'up' : 'down';
			diff_y     = offset_top - last_pos;
			last_pos   = offset_top;

			if ( offset_top > top_limit ) {
				// http://geek-and-poke.com/geekandpoke/2012/7/27/simply-explained.html
				if ( bottom_limit + options_top + (largerSticky ? options_bottom : 0) - (STICKY_OPTIONS.followScroll && largerSticky ? 0 : options_top) <= offset_top + sticky_height - stick_top - ((sticky_height - stick_top > window_height - (top_limit - stick_top) && STICKY_OPTIONS.followScroll) ? (((bottom_distance =
					sticky_height - window_height - stick_top) > 0) ? bottom_distance : 0) : 0) ) { // bottom reached end
					Sticky.release( {
						position: 'absolute', //top: bottom_limit - sticky_height - top_limit + stick_top + sticky_offsetTop
						bottom: elemParent_offsetTop + elemParent.offsetHeight - bottom_limit - options_top
					} );
				} else if ( largerSticky && STICKY_OPTIONS.followScroll ) { // sticky is bigger than container and follows scroll
					if ( scroll_dir === 'down' ) { // scroll down
						if ( sticky_window_top + sticky_height + options_bottom <= window_height + .9 ) { // stick on bottom
							// fix subpixel precision with adding .9 pixels
							Sticky.stick( {
								//top: window_height - sticky_height - options_bottom
								bottom: options_bottom
							} );
						} else if ( Sticky.position === 'fixed' ) { // bottom reached window bottom
							Sticky.release( {
								position: 'absolute',
								top: sticky_top - options_top - top_limit - diff_y + stick_top
							} );
						}
					} else { // scroll up
						if ( Math.ceil( sticky_window_top + stick_top ) < 0 && Sticky.position === 'fixed' ) { // top reached window top
							Sticky.release( {
								position: 'absolute',
								top: sticky_top - options_top - top_limit + stick_top - diff_y
							} );
						} else if ( sticky_top >= offset_top + options_top - stick_top ) { // stick on top
							Sticky.stick( {
								top: options_top - stick_top
							} );
						}
					}
				} else { // stick on top
					Sticky.stick( {
						top: options_top - stick_top
					} );
				}
			} else { // starting point
				Sticky.release( {
					stop: true
				} );
			}
		};

		let scrollAttached = false;
		let resizeAttached = false;

		const disableSticky = () => {
			if ( scrollAttached ) {
				// detach sticky from scroll
				window.removeEventListener( 'scroll', runSticky, Helpers.supportsPassive );

				// sticky is not attached to scroll anymore
				scrollAttached = false;
			}
		};

		const initSticky = () => {
			// check if element or it's parents are visible
			if ( elem.offsetParent === null || Helpers.getStyle( elem, 'display' ) === 'none' ) {
				disableSticky();
				return;
			}

			// calculate stuff
			calcSticky();

			// check if sticky is bigger than reffering container
			if ( sticky_height > container_height ) {
				disableSticky();
				return;
			}

			// run
			runSticky();

			if ( ! scrollAttached ) {
				// attach sticky to scroll
				window.addEventListener( 'scroll', runSticky, Helpers.supportsPassive );

				// sticky is attached to scroll
				scrollAttached = true;
			}
		};

		const resetSticky = () => {
			// remove inline styles
			elem.style.position = '';
			elem.style.left     = '';
			elem.style.top      = '';
			elem.style.bottom   = '';
			elem.style.width    = '';

			// remove sticky class
			if ( elem.classList ) {
				elem.classList.remove( STICKY_OPTIONS.stickyClass );
			} else {
				elem.className =
					elem.className.replace( new RegExp( '(^|\\b)' + STICKY_OPTIONS.stickyClass.split( ' ' ).join( '|' ) + '(\\b|$)', 'gi' ), ' ' );
			}

			// reset sticky object data
			Sticky.css      = {};
			Sticky.position = null;

			// remove spacer
			if ( Spacer.isAttached === true ) {
				Spacer.detach();
			}
		};

		const reinitSticky = () => {
			resetSticky();
			applyQueries();

			if ( isDisabled() ) {
				disableSticky();
				return;
			}

			// restart sticky
			initSticky();
		};

		const resizeSticky = () => {
			// fire 'beforeResize' event
			if ( STICKY_OPTIONS.onBeforeResize ) {
				STICKY_OPTIONS.onBeforeResize.call( elem, Object.assign( {}, STICKY_OPTIONS ) );
			}

			// reinit sticky
			reinitSticky();

			// fire 'resize' event
			if ( STICKY_OPTIONS.onResize ) {
				STICKY_OPTIONS.onResize.call( elem, Object.assign( {}, STICKY_OPTIONS ) );
			}
		};

		const resize_cb = ! STICKY_OPTIONS.resizeDebounce ? resizeSticky : Helpers.debounce( resizeSticky, STICKY_OPTIONS.resizeDebounce );

		// Method for updating options
		const Update = ( options ) => {
			setOptions( options );

			// also update user settings
			userSettings = Object.assign( {}, userSettings, options || {} );

			reinitSticky();
		};

		const Detach = () => {
			// detach resize reinit
			if ( resizeAttached ) {
				window.removeEventListener( 'resize', resize_cb, Helpers.supportsPassive );
				resizeAttached = false;
			}

			disableSticky();
		};

		const Destroy = () => {
			Detach();
			resetSticky();
		};

		const Attach = () => {
			// attach resize reinit
			if ( ! resizeAttached ) {
				window.addEventListener( 'resize', resize_cb, Helpers.supportsPassive );
				resizeAttached = true;
			}

			applyQueries();

			if ( isDisabled() ) {
				disableSticky();
				return;
			}

			initSticky();
		};

		this.options = getOptions;
		this.refresh = reinitSticky;
		this.update  = Update;
		this.attach  = Attach;
		this.detach  = Detach;
		this.destroy = Destroy;

		// jQuery methods
		this.triggerMethod = ( method, options ) => {
			if ( typeof this[method] === 'function' ) {
				this[method]( options );
			}
		};

		this.reinit = () => {
			deprecated( 'reinit', 'refresh', 'method' );
			reinitSticky();
		};

		// init settings
		setOptions( userSettings );

		// start sticky
		Attach();

		// reinit on complete page load
		window.addEventListener( 'load', reinitSticky );
	};

	// jQuery Plugin
	if ( typeof window.jQuery !== 'undefined' ) {
		const $         = window.jQuery;
		const namespace = 'hcSticky';

		$.fn.extend( {
			hcSticky: function( args, update ) {
				// check if selected element exist
				if ( ! this.length ) {
					return this;
				}

				// we need to return options
				if ( args === 'options' ) {
					return $.data( this.get( 0 ), namespace ).options();
				}

				return this.each( function() {
					let instance = $.data( this, namespace );

					if ( instance ) {
						// already created, trigger method
						instance.triggerMethod( args, update );
					} else {
						// create new instance
						instance = new hcSticky( this, args );
						$.data( this, namespace, instance );
					}
				} );
			}
		} );
	}

	// browser global
	window.hcSticky = window.hcSticky || hcSticky;

	return hcSticky;
} );
