/**
* jQuery CSS Customizable Scrollbar
*
* Copyright 2014, Yuriy Khabarov
* Dual licensed under the MIT or GPL Version 2 licenses.
*
* If you found bug, please contact me via email <13real008@gmail.com>
*
* @author Yuriy Khabarov aka Gromo
* @version 0.2.2
* @url https://github.com/gromo/jquery.scrollbar/
*
* TODO:
*
*/
;
(function($, doc, win){
// Init flags & variables
var browser = {
"mobile": /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(win.navigator.userAgent),
"webkit": win.WebKitPoint ? true : false,
"log": function(data, toString){
var output = data;
if(toString && typeof data != "string"){
output = [];
$.each(data, function(i, v){
output.push('"' + i + '": ' + v);
});
output = output.join(", ");
}
if(win.console && win.console.log){
win.console.log(output);
}
},
"scroll": null
};
var debug = false;
var lmb = 1, px = "px";
var scrolls = [];
var defaults = {
"autoScrollSize": true, // automatically calculate scrollsize
"autoUpdate": true, // update scrollbar if content/container size changed
"disableBodyScroll": false, // disable body scroll if mouse over container
"duration": 200, // scroll animate duration in ms
"ignoreMobile": true, // ignore mobile devices
"scrollStep": 30, // scroll step for scrollbar arrows
"showArrows": false, // add class to show arrows
"stepScrolling": true, // when scrolling to scrollbar mousedown position
"type":"simple", // [advanced|simple] scrollbar html type
"scrollx": null, // horizontal scroll element
"scrolly": null, // vertical scroll element
"onDestroy": null, // callback function on destroy,
"onInit": null, // callback function on first initialization
"onUpdate": null // callback function on init/resize (before scrollbar size calculation)
};
var customScrollbar = function(container, options){
if(!browser.scroll){
debug && browser.log("Init jQuery Scrollbar v0.2.1");
browser.scroll = getBrowserScrollSize();
}
this.container = container;
this.options = $.extend({}, defaults);
this.scrollTo = null;
this.scrollx = {};
this.scrolly = {};
this.init(options);
};
customScrollbar.prototype = {
destroy: function(){
if(!this.wrapper){
return;
}
// init variables
var scrollLeft = this.container.scrollLeft();
var scrollTop = this.container.scrollTop();
this.container.insertBefore(this.wrapper).css({
"height":"",
"margin":""
})
.removeClass("scroll-content")
.removeClass("scroll-scrollx_visible")
.removeClass("scroll-scrolly_visible")
.off(".scrollbar")
.scrollLeft(scrollLeft)
.scrollTop(scrollTop);
this.scrollx.scrollbar.removeClass("scroll-scrollx_visible").find("div").andSelf().off(".scrollbar");
this.scrolly.scrollbar.removeClass("scroll-scrolly_visible").find("div").andSelf().off(".scrollbar");
this.wrapper.remove();
$(doc).add("body").off(".scrollbar");
if($.isFunction(this.options.onDestroy))
this.options.onDestroy.apply(this, [this.container]);
},
getScrollbar: function(d){
var scrollbar = this.options["scroll" + d];
var html = {
"advanced":
'<div class="scroll-element_corner"></div>' +
'<div class="scroll-arrow scroll-arrow_less"></div>' +
'<div class="scroll-arrow scroll-arrow_more"></div>' +
'<div class="scroll-element_outer">' +
' <div class="scroll-element_size"></div>' + // required! used for scrollbar size calculation !
' <div class="scroll-element_inner-wrapper">' +
' <div class="scroll-element_inner scroll-element_track">' + // used for handling scrollbar click
' <div class="scroll-element_inner-bottom"></div>' +
' </div>' +
' </div>' +
' <div class="scroll-bar">' +
' <div class="scroll-bar_body">' +
' <div class="scroll-bar_body-inner"></div>' +
' </div>' +
' <div class="scroll-bar_bottom"></div>' +
' <div class="scroll-bar_center"></div>' +
' </div>' +
'</div>',
"simple":
'<div class="scroll-element_outer">' +
' <div class="scroll-element_size"></div>' + // required! used for scrollbar size calculation !
' <div class="scroll-element_track"></div>' + // used for handling scrollbar click
' <div class="scroll-bar"></div>' +
'</div>'
};
var type = html[this.options.type] ? this.options.type : "advanced";
if(scrollbar){
if(typeof(scrollbar) == "string"){
scrollbar = $(scrollbar).appendTo(this.wrapper);
} else {
scrollbar = $(scrollbar);
}
} else {
scrollbar = $("<div>").addClass("scroll-element").html(html[type]).appendTo(this.wrapper);
}
if(this.options.showArrows){
scrollbar.addClass("scroll-element_arrows_visible");
}
return scrollbar.addClass("scroll-" + d);
},
init: function(options){
// init variables
var S = this;
var c = this.container;
var o = $.extend(this.options, options);
var s = {
"x": this.scrollx,
"y": this.scrolly
};
var w = this.wrapper;
var initScroll = {
"scrollLeft": c.scrollLeft(),
"scrollTop": c.scrollTop()
};
// ignore mobile
if(browser.mobile && o.ignoreMobile){
return false;
}
// init scroll container
if(!w){
this.wrapper = w = c.wrap($("<div>").css({
"position": (c.css("position") == "absolute") ? "absolute" : "relative"
}).addClass("scroll-wrapper").addClass(c.attr("class"))).parent();
c.addClass("scroll-content").css({
"height":"",
"margin-bottom": browser.scroll.height * -1 + px,
"margin-right": browser.scroll.width * -1 + px
}).on("scroll.scrollbar", function(){
s.x.isVisible && s.x.scroller.css("left", c.scrollLeft() * s.x.kx + px);
s.y.isVisible && s.y.scroller.css("top", c.scrollTop() * s.y.kx + px);
});
/* prevent native scrollbars to be visible on #anchor click */
w.on("scroll", function(){
w.scrollTop(0).scrollLeft(0);
});
if(o.disableBodyScroll){
var handleMouseScroll = function(event){
isVerticalScroll(event) ?
s.y.isVisible && s.y.mousewheel(event) :
s.x.isVisible && s.x.mousewheel(event);
};
w.on({
"MozMousePixelScroll.scrollbar": handleMouseScroll,
"mousewheel.scrollbar": handleMouseScroll
});
if(browser.mobile){
w.on("touchstart.scrollbar", function(event){
var touch = event.originalEvent.touches && event.originalEvent.touches[0] || event;
var originalTouch = {
"pageX": touch.pageX,
"pageY": touch.pageY
};
var originalScroll = {
"left": c.scrollLeft(),
"top": c.scrollTop()
};
$(doc).on({
"touchmove.scrollbar": function(event){
var touch = event.originalEvent.targetTouches && event.originalEvent.targetTouches[0] || event;
c.scrollLeft(originalScroll.left + originalTouch.pageX - touch.pageX);
c.scrollTop(originalScroll.top + originalTouch.pageY - touch.pageY);
event.preventDefault();
},
"touchend.scrollbar": function(){
$(doc).off(".scrollbar");
}
});
});
}
}
if($.isFunction(o.onInit))
o.onInit.apply(this, [c]);
} else {
c.css({
"height":""
});
}
// init scrollbars & recalculate sizes
$.each(s, function(d, scrollx){
var scrollCallback = null;
var scrollForward = 1;
var scrollOffset = (d == "x") ? "scrollLeft" : "scrollTop";
var scrollStep = o.scrollStep;
var scrollTo = function(){
var currentOffset = c[scrollOffset]();
c[scrollOffset](currentOffset + scrollStep);
if(scrollForward == 1 && (currentOffset + scrollStep) >= scrollToValue)
currentOffset = c[scrollOffset]();
if(scrollForward == -1 && (currentOffset + scrollStep) <= scrollToValue)
currentOffset = c[scrollOffset]();
if(c[scrollOffset]() == currentOffset && scrollCallback){
scrollCallback();
}
}
var scrollToValue = 0;
if(!scrollx.scrollbar){
scrollx.scrollbar = S.getScrollbar(d);
scrollx.scroller = scrollx.scrollbar.find(".scroll-bar");
scrollx.mousewheel = function(event){
if(!scrollx.isVisible || (d == 'x' && isVerticalScroll(event))){
return true;
}
if(d == 'y' && !isVerticalScroll(event)){
s.x.mousewheel(event);
return true;
}
var delta = event.originalEvent.wheelDelta * -1 || event.originalEvent.detail;
var maxScrollValue = scrollx.size - scrollx.visible - scrollx.offset;
if(!((scrollToValue <= 0 && delta < 0) || (scrollToValue >= maxScrollValue && delta > 0))){
scrollToValue = scrollToValue + delta;
if(scrollToValue < 0)
scrollToValue = 0;
if(scrollToValue > maxScrollValue)
scrollToValue = maxScrollValue;
S.scrollTo = S.scrollTo || {};
S.scrollTo[scrollOffset] = scrollToValue;
setTimeout(function(){
if(S.scrollTo){
c.stop().animate(S.scrollTo, 240, 'linear', function(){
scrollToValue = c[scrollOffset]();
});
S.scrollTo = null;
}
}, 1);
}
event.preventDefault();
return false;
};
scrollx.scrollbar.on({
"MozMousePixelScroll.scrollbar": scrollx.mousewheel,
"mousewheel.scrollbar": scrollx.mousewheel,
"mouseenter.scrollbar": function(){
scrollToValue = c[scrollOffset]();
}
});
// handle arrows & scroll inner mousedown event
scrollx.scrollbar.find(".scroll-arrow, .scroll-element_track")
.on("mousedown.scrollbar", function(event){
if(event.which != lmb)
return true;
scrollForward = 1;
var data = {
"eventOffset": event[(d == "x") ? "pageX" : "pageY"],
"maxScrollValue": scrollx.size - scrollx.visible - scrollx.offset,
"scrollbarOffset": scrollx.scroller.offset()[(d == "x") ? "left" : "top"],
"scrollbarSize": scrollx.scroller[(d == "x") ? "outerWidth" : "outerHeight"]()
};
var timeout = 0, timer = 0;
if($(this).hasClass('scroll-arrow')){
scrollForward = $(this).hasClass("scroll-arrow_more") ? 1 : -1;
scrollStep = o.scrollStep * scrollForward;
scrollToValue = scrollForward > 0 ? data.maxScrollValue : 0;
} else {
scrollForward = (data.eventOffset > (data.scrollbarOffset + data.scrollbarSize) ? 1
: (data.eventOffset < data.scrollbarOffset ? -1 : 0));
scrollStep = Math.round(scrollx.visible * 0.75) * scrollForward;
scrollToValue = (data.eventOffset - data.scrollbarOffset -
(o.stepScrolling ? (scrollForward == 1 ? data.scrollbarSize : 0)
: Math.round(data.scrollbarSize / 2)));
scrollToValue = c[scrollOffset]() + (scrollToValue / scrollx.kx);
}
S.scrollTo = S.scrollTo || {};
S.scrollTo[scrollOffset] = o.stepScrolling ? c[scrollOffset]() + scrollStep : scrollToValue;
if(o.stepScrolling){
scrollCallback = function(){
scrollToValue = c[scrollOffset]();
clearInterval(timer);
clearTimeout(timeout);
timeout = 0;
timer = 0;
};
timeout = setTimeout(function(){
timer = setInterval(scrollTo, 40);
}, o.duration + 100);
}
setTimeout(function(){
if(S.scrollTo){
c.animate(S.scrollTo, o.duration);
S.scrollTo = null;
}
}, 1);
return handleMouseDown(scrollCallback, event);
});
// handle scrollbar drag'n'drop
scrollx.scroller.on("mousedown.scrollbar", function(event){
if(event.which != lmb)
return true;
var eventPosition = event[(d == "x")? "pageX" : "pageY"];
var initOffset = c[scrollOffset]();
scrollx.scrollbar.addClass("scroll-draggable");
$(doc).on("mousemove.scrollbar", function(event){
var diff = parseInt((event[(d == "x")? "pageX" : "pageY"] - eventPosition) / scrollx.kx, 10);
c[scrollOffset](initOffset + diff);
});
return handleMouseDown(function(){
scrollx.scrollbar.removeClass("scroll-draggable");
scrollToValue = c[scrollOffset]();
}, event);
});
}
});
// remove classes & reset applied styles
$.each(s, function(d, scrollx){
var scrollClass = "scroll-scroll" + d + "_visible";
var scrolly = (d == "x") ? s.y : s.x;
scrollx.scrollbar.removeClass(scrollClass);
scrolly.scrollbar.removeClass(scrollClass);
c.removeClass(scrollClass);
});
// calculate init sizes
$.each(s, function(d, scrollx){
$.extend(scrollx, (d == "x") ? {
"offset": parseInt(c.css("left"), 10) || 0,
"size": c.prop("scrollWidth"),
"visible": w.width()
} : {
"offset": parseInt(c.css("top"), 10) || 0,
"size": c.prop("scrollHeight"),
"visible": w.height()
});
});
var updateScroll = function(d, scrollx){
var scrollClass = "scroll-scroll" + d + "_visible";
var scrolly = (d == "x") ? s.y : s.x;
var offset = parseInt(c.css((d == "x") ? "left" : "top"), 10) || 0;
var AreaSize = scrollx.size;
var AreaVisible = scrollx.visible + offset;
scrollx.isVisible = (AreaSize - AreaVisible) > 1; // bug in IE9/11 with 1px diff
if(scrollx.isVisible){
scrollx.scrollbar.addClass(scrollClass);
scrolly.scrollbar.addClass(scrollClass);
c.addClass(scrollClass);
} else {
scrollx.scrollbar.removeClass(scrollClass);
scrolly.scrollbar.removeClass(scrollClass);
c.removeClass(scrollClass);
}
if(d == "y" && (scrollx.isVisible || scrollx.size < scrollx.visible)){
c.css("height", (AreaVisible + browser.scroll.height) + px);
}
if(s.x.size != c.prop("scrollWidth")
|| s.y.size != c.prop("scrollHeight")
|| s.x.visible != w.width()
|| s.y.visible != w.height()
|| s.x.offset != (parseInt(c.css("left"), 10) || 0)
|| s.y.offset != (parseInt(c.css("top"), 10) || 0)
){
$.each(s, function(d, scrollx){
$.extend(scrollx, (d == "x") ? {
"offset": parseInt(c.css("left"), 10) || 0,
"size": c.prop("scrollWidth"),
"visible": w.width()
} : {
"offset": parseInt(c.css("top"), 10) || 0,
"size": c.prop("scrollHeight"),
"visible": w.height()
});
});
updateScroll(d == "x" ? "y" : "x", scrolly);
}
};
$.each(s, updateScroll);
if($.isFunction(o.onUpdate))
o.onUpdate.apply(this, [c]);
// calculate scroll size
$.each(s, function(d, scrollx){
var cssOffset = (d == "x") ? "left" : "top";
var cssFullSize = (d == "x") ? "outerWidth" : "outerHeight";
var cssSize = (d == "x") ? "width" : "height";
var offset = parseInt(c.css(cssOffset), 10) || 0;
var AreaSize = scrollx.size;
var AreaVisible = scrollx.visible + offset;
var scrollSize = scrollx.scrollbar.find(".scroll-element_size");
scrollSize = scrollSize[cssFullSize]() + (parseInt(scrollSize.css(cssOffset), 10) || 0);
if(o.autoScrollSize){
scrollx.scrollbarSize = parseInt(scrollSize * AreaVisible / AreaSize, 10);
scrollx.scroller.css(cssSize, scrollx.scrollbarSize + px);
}
scrollx.scrollbarSize = scrollx.scroller[cssFullSize]();
scrollx.kx = ((scrollSize - scrollx.scrollbarSize) / (AreaSize - AreaVisible)) || 1;
});
c.scrollLeft(initScroll.scrollLeft).scrollTop(initScroll.scrollTop).trigger("scroll");
}
};
/*
* Extend jQuery as plugin
*
* @param {object|string} options or command to execute
* @param {object|array} args additional arguments as array []
*/
$.fn.scrollbar = function(options, args){
var toReturn = this;
if(options === "get")
toReturn = null;
this.each(function() {
var container = $(this);
if(container.hasClass("scroll-wrapper")
|| container.get(0).nodeName == "body"){
return true;
}
var instance = container.data("scrollbar");
if(instance){
if(options === "get"){
toReturn = instance;
return false;
}
var func = (typeof options == "string" && instance[options]) ? options : "init";
instance[func].apply(instance, $.isArray(args) ? args : []);
if(options === "destroy"){
container.removeData("scrollbar");
while($.inArray(instance, scrolls) >= 0)
scrolls.splice($.inArray(instance, scrolls), 1);
}
} else {
if(typeof options != "string"){
instance = new customScrollbar(container, options);
container.data("scrollbar", instance);
scrolls.push(instance);
}
}
return true;
});
return toReturn;
};
/**
* Connect default options to global object
*/
$.fn.scrollbar.options = defaults;
/**
* Check if scroll content/container size is changed
*/
var timerCounter = 0;
var timer = setInterval(function(){
var i, c, o, s, w, x, y;
for(i=0; i<scrolls.length; i++){
s = scrolls[i];
c = s.container;
o = s.options;
w = s.wrapper;
x = s.scrollx;
y = s.scrolly;
if(o.autoUpdate && w && w.is(":visible") &&
(c.prop("scrollWidth") != x.size
|| c.prop("scrollHeight") != y.size
|| w.width() != x.visible
|| w.height() != y.visible
)){
s.init();
if(debug){
browser.log({
"scrollHeight": c.prop("scrollHeight") + ":" + s.scrolly.size,
"scrollWidth": c.prop("scrollWidth") + ":" + s.scrollx.size,
"visibleHeight": w.height() + ":" + s.scrolly.visible,
"visibleWidth": w.width() + ":" + s.scrollx.visible
}, true);
if(++timerCounter > 10){
browser.log("Scroll updates exceed 10");
clearInterval(timer);
}
}
}
}
}, 300);
/* ADDITIONAL FUNCTIONS */
function getBrowserScrollSize(){
if(browser.webkit){
return {
"height": 0,
"width": 0
};
}
var css = {
"border": "none",
"height": "100px",
"margin": "0",
"padding": "0",
"width": "100px"
};
var inner = $("<div>").css($.extend({}, css));
var outer = $("<div>").css($.extend({
"background": "#F00",
"left": "-200px",
"overflow": "scroll",
"position": "absolute",
"top": "-200px"
}, css)).append(inner).appendTo("body")
.scrollLeft(inner.width())
.scrollTop(inner.height());
var scrollSize = {
"height": (outer.offset().top - inner.offset().top) || 0,
"width": (outer.offset().left - inner.offset().left) || 0
};
outer.remove();
return scrollSize;
}
function handleMouseDown(callback, event){
$(doc).on({
"blur.scrollbar": function(){
$(doc).add('body').off('.scrollbar');
callback && callback();
},
"dragstart.scrollbar": function(event){
event.preventDefault();
return false;
},
"mouseup.scrollbar": function(){
$(doc).add('body').off('.scrollbar');
callback && callback();
}
});
$("body").on({
"selectstart.scrollbar": function(event){
event.preventDefault();
return false;
}
});
event && event.preventDefault();
return false;
}
function isVerticalScroll(event){
var e = event.originalEvent;
if (e.axis && e.axis === e.HORIZONTAL_AXIS)
return false;
if (e.wheelDeltaX)
return false;
return true;
}
})(jQuery, document, window);
|