(function($, anim) { 'use strict'; let _defaults = { throttle: 100, scrollOffset: 200, // offset - 200 allows elements near bottom of page to scroll activeClass: 'active', getActiveElement: function(id) { return 'a[href="#' + id + '"]'; } }; /** * @class * */ class ScrollSpy extends Component { /** * Construct ScrollSpy instance * @constructor * @param {Element} el * @param {Object} options */ constructor(el, options) { super(ScrollSpy, el, options); this.el.M_ScrollSpy = this; /** * Options for the modal * @member Modal#options * @prop {Number} [throttle=100] - Throttle of scroll handler * @prop {Number} [scrollOffset=200] - Offset for centering element when scrolled to * @prop {String} [activeClass='active'] - Class applied to active elements * @prop {Function} [getActiveElement] - Used to find active element */ this.options = $.extend({}, ScrollSpy.defaults, options); // setup ScrollSpy._elements.push(this); ScrollSpy._count++; ScrollSpy._increment++; this.tickId = -1; this.id = ScrollSpy._increment; this._setupEventHandlers(); this._handleWindowScroll(); } static get defaults() { return _defaults; } static init(els, options) { return super.init(this, els, options); } /** * Get Instance */ static getInstance(el) { let domElem = !!el.jquery ? el[0] : el; return domElem.M_ScrollSpy; } /** * Teardown component */ destroy() { ScrollSpy._elements.splice(ScrollSpy._elements.indexOf(this), 1); ScrollSpy._elementsInView.splice(ScrollSpy._elementsInView.indexOf(this), 1); ScrollSpy._visibleElements.splice(ScrollSpy._visibleElements.indexOf(this.$el), 1); ScrollSpy._count--; this._removeEventHandlers(); $(this.options.getActiveElement(this.$el.attr('id'))).removeClass(this.options.activeClass); this.el.M_ScrollSpy = undefined; } /** * Setup Event Handlers */ _setupEventHandlers() { let throttledResize = M.throttle(this._handleWindowScroll, 200); this._handleThrottledResizeBound = throttledResize.bind(this); this._handleWindowScrollBound = this._handleWindowScroll.bind(this); if (ScrollSpy._count === 1) { window.addEventListener('scroll', this._handleWindowScrollBound); window.addEventListener('resize', this._handleThrottledResizeBound); document.body.addEventListener('click', this._handleTriggerClick); } } /** * Remove Event Handlers */ _removeEventHandlers() { if (ScrollSpy._count === 0) { window.removeEventListener('scroll', this._handleWindowScrollBound); window.removeEventListener('resize', this._handleThrottledResizeBound); document.body.removeEventListener('click', this._handleTriggerClick); } } /** * Handle Trigger Click * @param {Event} e */ _handleTriggerClick(e) { let $trigger = $(e.target); for (let i = ScrollSpy._elements.length - 1; i >= 0; i--) { let scrollspy = ScrollSpy._elements[i]; if ($trigger.is('a[href="#' + scrollspy.$el.attr('id') + '"]')) { e.preventDefault(); let offset = scrollspy.$el.offset().top + 1; anim({ targets: [document.documentElement, document.body], scrollTop: offset - scrollspy.options.scrollOffset, duration: 400, easing: 'easeOutCubic' }); break; } } } /** * Handle Window Scroll */ _handleWindowScroll() { // unique tick id ScrollSpy._ticks++; // viewport rectangle let top = M.getDocumentScrollTop(), left = M.getDocumentScrollLeft(), right = left + window.innerWidth, bottom = top + window.innerHeight; // determine which elements are in view let intersections = ScrollSpy._findElements(top, right, bottom, left); for (let i = 0; i < intersections.length; i++) { let scrollspy = intersections[i]; let lastTick = scrollspy.tickId; if (lastTick < 0) { // entered into view scrollspy._enter(); } // update tick id scrollspy.tickId = ScrollSpy._ticks; } for (let i = 0; i < ScrollSpy._elementsInView.length; i++) { let scrollspy = ScrollSpy._elementsInView[i]; let lastTick = scrollspy.tickId; if (lastTick >= 0 && lastTick !== ScrollSpy._ticks) { // exited from view scrollspy._exit(); scrollspy.tickId = -1; } } // remember elements in view for next tick ScrollSpy._elementsInView = intersections; } /** * Find elements that are within the boundary * @param {number} top * @param {number} right * @param {number} bottom * @param {number} left * @return {Array.} A collection of elements */ static _findElements(top, right, bottom, left) { let hits = []; for (let i = 0; i < ScrollSpy._elements.length; i++) { let scrollspy = ScrollSpy._elements[i]; let currTop = top + scrollspy.options.scrollOffset || 200; if (scrollspy.$el.height() > 0) { let elTop = scrollspy.$el.offset().top, elLeft = scrollspy.$el.offset().left, elRight = elLeft + scrollspy.$el.width(), elBottom = elTop + scrollspy.$el.height(); let isIntersect = !( elLeft > right || elRight < left || elTop > bottom || elBottom < currTop ); if (isIntersect) { hits.push(scrollspy); } } } return hits; } _enter() { ScrollSpy._visibleElements = ScrollSpy._visibleElements.filter(function(value) { return value.height() != 0; }); if (ScrollSpy._visibleElements[0]) { $(this.options.getActiveElement(ScrollSpy._visibleElements[0].attr('id'))).removeClass( this.options.activeClass ); if ( ScrollSpy._visibleElements[0][0].M_ScrollSpy && this.id < ScrollSpy._visibleElements[0][0].M_ScrollSpy.id ) { ScrollSpy._visibleElements.unshift(this.$el); } else { ScrollSpy._visibleElements.push(this.$el); } } else { ScrollSpy._visibleElements.push(this.$el); } $(this.options.getActiveElement(ScrollSpy._visibleElements[0].attr('id'))).addClass( this.options.activeClass ); } _exit() { ScrollSpy._visibleElements = ScrollSpy._visibleElements.filter(function(value) { return value.height() != 0; }); if (ScrollSpy._visibleElements[0]) { $(this.options.getActiveElement(ScrollSpy._visibleElements[0].attr('id'))).removeClass( this.options.activeClass ); ScrollSpy._visibleElements = ScrollSpy._visibleElements.filter((el) => { return el.attr('id') != this.$el.attr('id'); }); if (ScrollSpy._visibleElements[0]) { // Check if empty $(this.options.getActiveElement(ScrollSpy._visibleElements[0].attr('id'))).addClass( this.options.activeClass ); } } } } /** * @static * @memberof ScrollSpy * @type {Array.} */ ScrollSpy._elements = []; /** * @static * @memberof ScrollSpy * @type {Array.} */ ScrollSpy._elementsInView = []; /** * @static * @memberof ScrollSpy * @type {Array.} */ ScrollSpy._visibleElements = []; /** * @static * @memberof ScrollSpy */ ScrollSpy._count = 0; /** * @static * @memberof ScrollSpy */ ScrollSpy._increment = 0; /** * @static * @memberof ScrollSpy */ ScrollSpy._ticks = 0; M.ScrollSpy = ScrollSpy; if (M.jQueryLoaded) { M.initializeJqueryWrapper(ScrollSpy, 'scrollSpy', 'M_ScrollSpy'); } })(cash, M.anime);