// page init jQuery(function(){ initAnchors(); }); // initialize fixed blocks on scroll function initAnchors() { new SmoothScroll({ anchorLinks: 'a[href^="#"]', activeClasses: 'parent', anchorActiveClass: 'active', sectionActiveClass: 'active' }); } /*! * SmoothScroll module */ ;(function($, exports) { // private variables var page, win = $(window), activeBlock, activeWheelHandler, wheelEvents = ('onwheel' in document || document.documentMode >= 9 ? 'wheel' : 'mousewheel DOMMouseScroll'); // animation handlers function scrollTo(offset, options, callback) { // initialize variables var scrollBlock; if(document.body) { if(typeof options === 'number') { options = { duration: options }; } else { options = options || {}; } page = page || $('html, body'); scrollBlock = options.container || page; } else { return; } // treat single number as scrollTop if(typeof offset === 'number') { offset = { top: offset }; } // handle mousewheel/trackpad while animation is active if(activeBlock && activeWheelHandler) { activeBlock.off('mousewheel', activeWheelHandler); } if(options.wheelBehavior && options.wheelBehavior !== 'none') { activeWheelHandler = function(e) { if(options.wheelBehavior === 'stop') { scrollBlock.off('mousewheel', activeWheelHandler); scrollBlock.stop(); } else if(options.wheelBehavior === 'ignore') { e.preventDefault(); } }; activeBlock = scrollBlock.on('mousewheel', activeWheelHandler); } // start scrolling animation scrollBlock.stop().animate({ scrollLeft: offset.left, scrollTop: offset.top }, options.duration, function(){ if(activeWheelHandler) { scrollBlock.off('mousewheel', activeWheelHandler); } if($.isFunction(callback)) { callback(); } }); } // smooth scroll contstructor function SmoothScroll(options) { this.options = $.extend({ anchorLinks: 'a[href^="#"]', // selector or jQuery object container: null, // specify container for scrolling (default - whole page) extraOffset: null, // function or fixed number activeClasses: null, // null, "link", "parent" easing: 'swing', // easing of scrolling animMode: 'duration', // or "speed" mode animDuration: 800, // total duration for scroll (any distance) animSpeed: 1500, // pixels per second anchorActiveClass: 'anchor-active', sectionActiveClass: 'section-active', wheelBehavior: 'stop', // "stop", "ignore" or "none" useNativeAnchorScrolling: false // do not handle click in devices with native smooth scrolling }, options); this.init(); } SmoothScroll.prototype = { init: function() { this.initStructure(); this.attachEvents(); }, initStructure: function(options) { this.container = this.options.container ? $(this.options.container) : $('html,body'); this.scrollContainer = this.options.container ? this.container : win; this.anchorLinks = $(this.options.anchorLinks); }, getAnchorTarget: function(link) { // get target block from link href var targetId = $(link).attr('href'); return $(targetId.length > 1 ? targetId : 'html'); }, getTargetOffset: function(block) { // get target offset var blockOffset = block.offset().top; if(this.options.container) { blockOffset -= this.container.offset().top - this.container.prop('scrollTop'); } // handle extra offset if(typeof this.options.extraOffset === 'number') { blockOffset -= this.options.extraOffset; } else if(typeof this.options.extraOffset === 'function') { blockOffset -= this.options.extraOffset(block); } return {top: blockOffset}; }, attachEvents: function() { var self = this; // handle active classes if(this.options.activeClasses) { // cache structure this.anchorData = []; this.anchorLinks.each(function() { var link = jQuery(this), targetBlock = self.getAnchorTarget(link), anchorDataItem; $.each(self.anchorData, function(index, item) { if(item.block[0] === targetBlock[0]) { anchorDataItem = item; } }); if(anchorDataItem) { anchorDataItem.link = anchorDataItem.link.add(link); } else { self.anchorData.push({ link: link, block: targetBlock }); } }); // add additional event handlers this.resizeHandler = function() { self.recalculateOffsets(); }; this.scrollHandler = function() { self.refreshActiveClass(); }; this.recalculateOffsets(); this.scrollContainer.on('scroll', this.scrollHandler); win.on('resize', this.resizeHandler); } // handle click event this.clickHandler = function(e) { self.onClick(e); }; if(!this.options.useNativeAnchorScrolling) { this.anchorLinks.on('click', this.clickHandler); } }, recalculateOffsets: function() { var self = this; $.each(this.anchorData, function(index, data) { data.offset = self.getTargetOffset(data.block); data.height = data.block.outerHeight(); }); this.refreshActiveClass(); }, refreshActiveClass: function() { var self = this, foundFlag = false, winHeight = win.height(), containerHeight = this.container.prop('scrollHeight'), viewPortHeight = this.scrollContainer.height(), scrollTop = this.options.container ? this.container.prop('scrollTop') : win.scrollTop(); // user function instead of default handler if(this.options.customScrollHandler) { this.options.customScrollHandler.call(this, scrollTop, this.anchorData); return; } // sort anchor data by offsets this.anchorData.sort(function(a, b) { return a.offset.top - b.offset.top; }); function toggleActiveClass(anchor, block, state) { anchor.toggleClass(self.options.anchorActiveClass, state); block.toggleClass(self.options.sectionActiveClass, state); } // default active class handler $.each(this.anchorData, function(index) { var reverseIndex = self.anchorData.length - index - 1, data = self.anchorData[reverseIndex], anchorElement = (self.options.activeClasses === 'parent' ? data.link.parent() : data.link); if(scrollTop >= containerHeight - viewPortHeight) { // handle last section if(reverseIndex === self.anchorData.length - 1) { toggleActiveClass(anchorElement, data.block, true); } else { toggleActiveClass(anchorElement, data.block, false); } } else { // handle other sections if(!foundFlag && (scrollTop >= data.offset.top - 1 || reverseIndex === 0) ) { foundFlag = true; toggleActiveClass(anchorElement, data.block, true); } else { toggleActiveClass(anchorElement, data.block, false); } } }); }, calculateScrollDuration: function(offset) { var distance; if(this.options.animMode === 'speed') { distance = Math.abs(this.scrollContainer.scrollTop() - offset.top); return (distance / this.options.animSpeed) * 1000; } else { return this.options.animDuration; } }, onClick: function(e) { var targetBlock = this.getAnchorTarget(e.currentTarget), targetOffset = this.getTargetOffset(targetBlock); e.preventDefault(); scrollTo(targetOffset, { container: this.container, wheelBehavior: this.options.wheelBehavior, duration: this.calculateScrollDuration(targetOffset), }); }, destroy: function() { if(this.options.activeClasses) { win.off('resize', this.resizeHandler); this.scrollContainer.off('scroll', this.scrollHandler); } this.anchorLinks.off('click', this.clickHandler); } }; // public API $.extend(SmoothScroll, { scrollTo: function(blockOrOffset, durationOrOptions, callback) { scrollTo(blockOrOffset, durationOrOptions, callback); } }); // export module exports.SmoothScroll = SmoothScroll; }(jQuery, this));