/* * Scroller v3.1.2 - 2014-12-08 * A jQuery plugin for replacing default browser scrollbars. Part of the Formstone Library. * http://formstone.it/scroller/ * * Copyright 2014 Ben Plum; MIT Licensed */ ;(function ($, window) { "use strict"; var namespace = "scroller", $body = null, classes = { base: "scroller", content: "scroller-content", bar: "scroller-bar", track: "scroller-track", handle: "scroller-handle", isHorizontal: "scroller-horizontal", isSetup: "scroller-setup", isActive: "scroller-active" }, events = { start: "touchstart." + namespace + " mousedown." + namespace, move: "touchmove." + namespace + " mousemove." + namespace, end: "touchend." + namespace + " mouseup." + namespace }; /** * @options * @param customClass [string] <''> "Class applied to instance" * @param duration [int] <0> "Scroll animation length" * @param handleSize [int] <0> "Handle size; 0 to auto size" * @param horizontal [boolean] "Scroll horizontally" * @param trackMargin [int] <0> "Margin between track and handle edge” */ var options = { customClass: "", duration: 0, handleSize: 0, horizontal: false, trackMargin: 0 }; var pub = { /** * @method * @name defaults * @description Sets default plugin options * @param opts [object] <{}> "Options object" * @example $.scroller("defaults", opts); */ defaults: function(opts) { options = $.extend(options, opts || {}); return (typeof this === 'object') ? $(this) : true; }, /** * @method * @name destroy * @description Removes instance of plugin * @example $(".target").scroller("destroy"); */ destroy: function() { return $(this).each(function(i, el) { var data = $(el).data(namespace); if (data) { data.$scroller.removeClass( [data.customClass, classes.base, classes.isActive].join(" ") ); data.$bar.remove(); data.$content.contents().unwrap(); data.$content.off( classify(namespace) ); data.$scroller.off( classify(namespace) ) .removeData(namespace); } }); }, /** * @method * @name scroll * @description Scrolls instance of plugin to element or position * @param pos [string || int] "Target element selector or static position" * @param duration [int] "Optional scroll duration" * @example $.scroller("scroll", pos, duration); */ scroll: function(pos, dur) { return $(this).each(function(i) { var data = $(this).data(namespace), duration = dur || options.duration; if (typeof pos !== "number") { var $el = $(pos); if ($el.length > 0) { var offset = $el.position(); if (data.horizontal) { pos = offset.left + data.$content.scrollLeft(); } else { pos = offset.top + data.$content.scrollTop(); } } else { pos = data.$content.scrollTop(); } } var styles = data.horizontal ? { scrollLeft: pos } : { scrollTop: pos }; data.$content.stop().animate(styles, duration); }); }, /** * @method * @name reset * @description Resets layout on instance of plugin * @example $.scroller("reset"); */ reset: function() { return $(this).each(function(i) { var data = $(this).data(namespace); if (data) { data.$scroller.addClass(classes.isSetup); var barStyles = {}, trackStyles = {}, handleStyles = {}, handlePosition = 0, isActive = true; if (data.horizontal) { // Horizontal data.barHeight = data.$content[0].offsetHeight - data.$content[0].clientHeight; data.frameWidth = data.$content.outerWidth(); data.trackWidth = data.frameWidth - (data.trackMargin * 2); data.scrollWidth = data.$content[0].scrollWidth; data.ratio = data.trackWidth / data.scrollWidth; data.trackRatio = data.trackWidth / data.scrollWidth; data.handleWidth = (data.handleSize > 0) ? data.handleSize : data.trackWidth * data.trackRatio; data.scrollRatio = (data.scrollWidth - data.frameWidth) / (data.trackWidth - data.handleWidth); data.handleBounds = { left: 0, right: data.trackWidth - data.handleWidth }; data.$content.css({ paddingBottom: data.barHeight + data.paddingBottom }); var scrollLeft = data.$content.scrollLeft(); handlePosition = scrollLeft * data.ratio; isActive = (data.scrollWidth <= data.frameWidth); barStyles = { width: data.frameWidth }; trackStyles = { width: data.trackWidth, marginLeft: data.trackMargin, marginRight: data.trackMargin }; handleStyles = { width: data.handleWidth }; } else { // Vertical data.barWidth = data.$content[0].offsetWidth - data.$content[0].clientWidth; data.frameHeight = data.$content.outerHeight(); data.trackHeight = data.frameHeight - (data.trackMargin * 2); data.scrollHeight = data.$content[0].scrollHeight; data.ratio = data.trackHeight / data.scrollHeight; data.trackRatio = data.trackHeight / data.scrollHeight; data.handleHeight = (data.handleSize > 0) ? data.handleSize : data.trackHeight * data.trackRatio; data.scrollRatio = (data.scrollHeight - data.frameHeight) / (data.trackHeight - data.handleHeight); data.handleBounds = { top: 0, bottom: data.trackHeight - data.handleHeight }; var scrollTop = data.$content.scrollTop(); handlePosition = scrollTop * data.ratio; isActive = (data.scrollHeight <= data.frameHeight); barStyles = { height: data.frameHeight }; trackStyles = { height: data.trackHeight, marginBottom: data.trackMargin, marginTop: data.trackMargin }; handleStyles = { height: data.handleHeight }; } // Updates if (isActive) { data.$scroller.removeClass(classes.isActive); } else { data.$scroller.addClass(classes.isActive); } data.$bar.css(barStyles); data.$track.css(trackStyles); data.$handle.css(handleStyles); position(data, handlePosition); data.$scroller.removeClass(classes.isSetup); } }); } }; /** * @method private * @name init * @description Initializes plugin * @param opts [object] "Initialization options" */ function init(opts) { // Local options opts = $.extend({}, options, opts || {}); // Check for Body if ($body === null) { $body = $("body"); } // Apply to each element var $items = $(this); for (var i = 0, count = $items.length; i < count; i++) { build($items.eq(i), opts); } return $items; } /** * @method private * @name build * @description Builds each instance * @param $scroller [jQuery object] "Target jQuery object" * @param opts [object] <{}> "Options object" */ function build($scroller, opts) { if (!$scroller.hasClass(classes.base)) { // EXTEND OPTIONS opts = $.extend({}, opts, $scroller.data(namespace + "-options")); var html = ''; html += '
'; html += '
'; html += '
'; html += '
'; opts.paddingRight = parseInt($scroller.css("padding-right"), 10); opts.paddingBottom = parseInt($scroller.css("padding-bottom"), 10); $scroller.addClass( [classes.base, opts.customClass].join(" ") ) .wrapInner('
') .prepend(html); if (opts.horizontal) { $scroller.addClass(classes.isHorizontal); } var data = $.extend({ $scroller: $scroller, $content: $scroller.find( classify(classes.content) ), $bar: $scroller.find( classify(classes.bar) ), $track: $scroller.find( classify(classes.track) ), $handle: $scroller.find( classify(classes.handle) ) }, opts); data.trackMargin = parseInt(data.trackMargin, 10); data.$content.on("scroll." + namespace, data, onScroll); data.$scroller.on(events.start, classify(classes.track), data, onTrackDown) .on(events.start, classify(classes.handle), data, onHandleDown) .data(namespace, data); pub.reset.apply($scroller); $(window).one("load", function() { pub.reset.apply($scroller); }); } } /** * @method private * @name onScroll * @description Handles scroll event * @param e [object] "Event data" */ function onScroll(e) { e.preventDefault(); e.stopPropagation(); var data = e.data, handleStyles = {}; if (data.horizontal) { // Horizontal var scrollLeft = data.$content.scrollLeft(); if (scrollLeft < 0) { scrollLeft = 0; } var handleLeft = scrollLeft / data.scrollRatio; if (handleLeft > data.handleBounds.right) { handleLeft = data.handleBounds.right; } handleStyles = { left: handleLeft }; } else { // Vertical var scrollTop = data.$content.scrollTop(); if (scrollTop < 0) { scrollTop = 0; } var handleTop = scrollTop / data.scrollRatio; if (handleTop > data.handleBounds.bottom) { handleTop = data.handleBounds.bottom; } handleStyles = { top: handleTop }; } data.$handle.css(handleStyles); } /** * @method private * @name onTrackDown * @description Handles mousedown event on track * @param e [object] "Event data" */ function onTrackDown(e) { e.preventDefault(); e.stopPropagation(); var data = e.data, oe = e.originalEvent, offset = data.$track.offset(), touch = (typeof oe.targetTouches !== "undefined") ? oe.targetTouches[0] : null, pageX = (touch) ? touch.pageX : e.clientX, pageY = (touch) ? touch.pageY : e.clientY; if (data.horizontal) { // Horizontal data.mouseStart = pageX; data.handleLeft = pageX - offset.left - (data.handleWidth / 2); position(data, data.handleLeft); } else { // Vertical data.mouseStart = pageY; data.handleTop = pageY - offset.top - (data.handleHeight / 2); position(data, data.handleTop); } onStart(data); } /** * @method private * @name onHandleDown * @description Handles mousedown event on handle * @param e [object] "Event data" */ function onHandleDown(e) { e.preventDefault(); e.stopPropagation(); var data = e.data, oe = e.originalEvent, touch = (typeof oe.targetTouches !== "undefined") ? oe.targetTouches[0] : null, pageX = (touch) ? touch.pageX : e.clientX, pageY = (touch) ? touch.pageY : e.clientY; if (data.horizontal) { // Horizontal data.mouseStart = pageX; data.handleLeft = parseInt(data.$handle.css("left"), 10); } else { // Vertical data.mouseStart = pageY; data.handleTop = parseInt(data.$handle.css("top"), 10); } onStart(data); } /** * @method private * @name onStart * @description Handles touch.mouse start * @param data [object] "Instance data" */ function onStart(data) { data.$content.off( classify(namespace) ); $body.on(events.move, data, onMouseMove) .on(events.end, data, onMouseUp); } /** * @method private * @name onMouseMove * @description Handles mousemove event * @param e [object] "Event data" */ function onMouseMove(e) { e.preventDefault(); e.stopPropagation(); var data = e.data, oe = e.originalEvent, pos = 0, delta = 0, touch = (typeof oe.targetTouches !== "undefined") ? oe.targetTouches[0] : null, pageX = (touch) ? touch.pageX : e.clientX, pageY = (touch) ? touch.pageY : e.clientY; if (data.horizontal) { // Horizontal delta = data.mouseStart - pageX; pos = data.handleLeft - delta; } else { // Vertical delta = data.mouseStart - pageY; pos = data.handleTop - delta; } position(data, pos); } /** * @method private * @name onMouseUp * @description Handles mouseup event * @param e [object] "Event data" */ function onMouseUp(e) { e.preventDefault(); e.stopPropagation(); var data = e.data; data.$content.on("scroll.scroller", data, onScroll); $body.off(".scroller"); } /** * @method private * @name onTouchEnd * @description Handles mouseup event * @param e [object] "Event data" */ function onTouchEnd(e) { e.preventDefault(); e.stopPropagation(); var data = e.data; data.$content.on("scroll.scroller", data, onScroll); $body.off(".scroller"); } /** * @method private * @name position * @description Position handle based on scroll * @param data [object] "Instance data" * @param pos [int] "Scroll position" */ function position(data, pos) { var handleStyles = {}; if (data.horizontal) { // Horizontal if (pos < data.handleBounds.left) { pos = data.handleBounds.left; } if (pos > data.handleBounds.right) { pos = data.handleBounds.right; } var scrollLeft = Math.round(pos * data.scrollRatio); handleStyles = { left: pos }; data.$content.scrollLeft( scrollLeft ); } else { // Vertical if (pos < data.handleBounds.top) { pos = data.handleBounds.top; } if (pos > data.handleBounds.bottom) { pos = data.handleBounds.bottom; } var scrollTop = Math.round(pos * data.scrollRatio); handleStyles = { top: pos }; data.$content.scrollTop( scrollTop ); } data.$handle.css(handleStyles); } /** * @method private * @name classify * @description Create class selector from text * @param text [string] "Text to convert" * @return [string] "New class name" */ function classify(text) { return "." + text; } $.fn[namespace] = function(method) { if (pub[method]) { return pub[method].apply(this, Array.prototype.slice.call(arguments, 1)); } else if (typeof method === 'object' || !method) { return init.apply(this, arguments); } return this; }; $[namespace] = function(method) { if (method === "defaults") { pub.defaults.apply(this, Array.prototype.slice.call(arguments, 1)); } }; })(jQuery);