blob: b5b81823568e894fa61919950f5a7f72b7a05050 [file] [log] [blame]
// 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));