/** * jquery css customizable scrollbar * * copyright 2015, 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.8 * @url https://github.com/gromo/jquery.scrollbar/ * */ ; (function (root, factory) { if (typeof define === 'function' && define.amd) { define(['jquery'], factory); } else { factory(root.jquery); } }(this, function ($) { 'use strict'; // init flags & variables var debug = false; var browser = { data: { index: 0, name: 'scrollbar' }, macosx: navigator.platform.tolowercase().indexof('mac') !== -1, mobile: /android|webos|iphone|ipad|ipod|blackberry/i.test(navigator.useragent), overlay: null, scroll: null, scrolls: [], webkit: /webkit/.test(navigator.useragent) }; browser.scrolls.add = function (instance) { this.remove(instance).push(instance); }; browser.scrolls.remove = function (instance) { while ($.inarray(instance, this) >= 0) { this.splice($.inarray(instance, this), 1); } return this; }; var defaults = { "autoscrollsize": true, // automatically calculate scrollsize "autoupdate": true, // update scrollbar if content/container size changed "debug": false, // debug mode "disablebodyscroll": false, // disable body scroll if mouse over container "duration": 200, // scroll animate duration in ms "ignoremobile": false, // ignore mobile devices "ignoreoverlay": false, // ignore browsers with overlay scrollbars (mobile, macos) "scrollstep": 30, // scroll step for scrollbar arrows "showarrows": false, // add class to show arrows "stepscrolling": true, // when scrolling to scrollbar mousedown position "scrollx": null, // horizontal scroll element "scrolly": null, // vertical scroll element "ondestroy": null, // callback function on destroy, "oninit": null, // callback function on first initialization "onscroll": null, // callback function on content scrolling "onupdate": null // callback function on init/resize (before scrollbar size calculation) }; var basescrollbar = function (container) { if (!browser.scroll) { browser.overlay = isscrolloverlayscontent(); browser.scroll = getbrowserscrollsize(); updatescrollbars(); $(window).resize(function () { var forceupdate = false; if (browser.scroll && (browser.scroll.height || browser.scroll.width)) { var scroll = getbrowserscrollsize(); if (scroll.height !== browser.scroll.height || scroll.width !== browser.scroll.width) { browser.scroll = scroll; forceupdate = true; // handle page zoom } } updatescrollbars(forceupdate); }); } this.container = container; this.namespace = '.scrollbar_' + browser.data.index++; this.options = $.extend({}, defaults, window.jqueryscrollbaroptions || {}); this.scrollto = null; this.scrollx = {}; this.scrolly = {}; container.data(browser.data.name, this); browser.scrolls.add(this); }; basescrollbar.prototype = { destroy: function () { if (!this.wrapper) { return; } this.container.removedata(browser.data.name); browser.scrolls.remove(this); // init variables var scrollleft = this.container.scrollleft(); var scrolltop = this.container.scrolltop(); this.container.insertbefore(this.wrapper).css({ "height": "", "margin": "", "max-height": "" }) .removeclass('scroll-content scroll-scrollx_visible scroll-scrolly_visible') .off(this.namespace) .scrollleft(scrollleft) .scrolltop(scrolltop); this.scrollx.scroll.removeclass('scroll-scrollx_visible').find('div').andself().off(this.namespace); this.scrolly.scroll.removeclass('scroll-scrolly_visible').find('div').andself().off(this.namespace); this.wrapper.remove(); $(document).add('body').off(this.namespace); if ($.isfunction(this.options.ondestroy)){ this.options.ondestroy.apply(this, [this.container]); } }, init: function (options) { // init variables var s = this, c = this.container, cw = this.containerwrapper || c, namespace = this.namespace, o = $.extend(this.options, options || {}), s = {x: this.scrollx, y: this.scrolly}, w = this.wrapper; var initscroll = { "scrollleft": c.scrollleft(), "scrolltop": c.scrolltop() }; // do not init if in ignorable browser if ((browser.mobile && o.ignoremobile) || (browser.overlay && o.ignoreoverlay) || (browser.macosx && !browser.webkit) // still required to ignore nonwebkit browsers on mac ) { return false; } // init scroll container if (!w) { this.wrapper = w = $('
').addclass('scroll-wrapper').addclass(c.attr('class')) .css('position', c.css('position') == 'absolute' ? 'absolute' : 'relative') .insertbefore(c).append(c); if (c.is('textarea')) { this.containerwrapper = cw = $('
').insertbefore(c).append(c); w.addclass('scroll-textarea'); } cw.addclass('scroll-content').css({ "height": "auto", "margin-bottom": browser.scroll.height * -1 + 'px', "margin-right": browser.scroll.width * -1 + 'px', "max-height": "" }); c.on('scroll' + namespace, function (event) { if ($.isfunction(o.onscroll)) { o.onscroll.call(s, { "maxscroll": s.y.maxscrolloffset, "scroll": c.scrolltop(), "size": s.y.size, "visible": s.y.visible }, { "maxscroll": s.x.maxscrolloffset, "scroll": c.scrollleft(), "size": s.x.size, "visible": s.x.visible }); } s.x.isvisible && s.x.scroll.bar.css('left', c.scrollleft() * s.x.kx + 'px'); s.y.isvisible && s.y.scroll.bar.css('top', c.scrolltop() * s.y.kx + 'px'); }); /* prevent native scrollbars to be visible on #anchor click */ w.on('scroll' + namespace, 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' + namespace, handlemousescroll); w.on('mousewheel' + namespace, handlemousescroll); if (browser.mobile) { w.on('touchstart' + namespace, 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() }; $(document).on('touchmove' + namespace, 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(); }); $(document).on('touchend' + namespace, function () { $(document).off(namespace); }); }); } } if ($.isfunction(o.oninit)){ o.oninit.apply(this, [c]); } } else { cw.css({ "height": "auto", "margin-bottom": browser.scroll.height * -1 + 'px', "margin-right": browser.scroll.width * -1 + 'px', "max-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.scroll) { scrollx.scroll = s._getscroll(o['scroll' + d]).addclass('scroll-' + d); if(o.showarrows){ scrollx.scroll.addclass('scroll-element_arrows_visible'); } 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 ((delta > 0 && scrolltovalue < maxscrollvalue) || (delta < 0 && scrolltovalue > 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.scroll .on('mozmousepixelscroll' + namespace, scrollx.mousewheel) .on('mousewheel' + namespace, scrollx.mousewheel) .on('mouseenter' + namespace, function () { scrolltovalue = c[scrolloffset](); }); // handle arrows & scroll inner mousedown event scrollx.scroll.find('.scroll-arrow, .scroll-element_track') .on('mousedown' + namespace, function (event) { if (event.which != 1) // lmb return true; scrollforward = 1; var data = { "eventoffset": event[(d === 'x') ? 'pagex' : 'pagey'], "maxscrollvalue": scrollx.size - scrollx.visible - scrollx.offset, "scrollbaroffset": scrollx.scroll.bar.offset()[(d === 'x') ? 'left' : 'top'], "scrollbarsize": scrollx.scroll.bar[(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 s._handlemousedown(scrollcallback, event); }); // handle scrollbar drag'n'drop scrollx.scroll.bar.on('mousedown' + namespace, function (event) { if (event.which != 1) // lmb return true; var eventposition = event[(d === 'x') ? 'pagex' : 'pagey']; var initoffset = c[scrolloffset](); scrollx.scroll.addclass('scroll-draggable'); $(document).on('mousemove' + namespace, function (event) { var diff = parseint((event[(d === 'x') ? 'pagex' : 'pagey'] - eventposition) / scrollx.kx, 10); c[scrolloffset](initoffset + diff); }); return s._handlemousedown(function () { scrollx.scroll.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.scroll.removeclass(scrollclass); scrolly.scroll.removeclass(scrollclass); cw.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() }); }); // update scrollbar visibility/dimensions this._updatescroll('x', this.scrollx); this._updatescroll('y', this.scrolly); 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.scroll.size[cssfullsize]() + (parseint(scrollx.scroll.size.css(cssoffset), 10) || 0); if (o.autoscrollsize) { scrollx.scrollbarsize = parseint(scrollsize * areavisible / areasize, 10); scrollx.scroll.bar.css(csssize, scrollx.scrollbarsize + 'px'); } scrollx.scrollbarsize = scrollx.scroll.bar[cssfullsize](); scrollx.kx = ((scrollsize - scrollx.scrollbarsize) / (areasize - areavisible)) || 1; scrollx.maxscrolloffset = areasize - areavisible; }); c.scrollleft(initscroll.scrollleft).scrolltop(initscroll.scrolltop).trigger('scroll'); }, /** * get scrollx/scrolly object * * @param {mixed} scroll * @returns {jquery} scroll object */ _getscroll: function (scroll) { var types = { advanced: [ '
', '
', '
', '
', '
', '
', // required! used for scrollbar size calculation ! '
', '
', // used for handling scrollbar click '
', '
', '
', '
', // required '
', '
', '
', '
', '
', '
', '
', '
' ].join(''), simple: [ '
', '
', '
', // required! used for scrollbar size calculation ! '
', // used for handling scrollbar click '
', // required '
', '
' ].join('') }; if (types[scroll]) { scroll = types[scroll]; } if (!scroll) { scroll = types['simple']; } if (typeof (scroll) == 'string') { scroll = $(scroll).appendto(this.wrapper); } else { scroll = $(scroll); } $.extend(scroll, { bar: scroll.find('.scroll-bar'), size: scroll.find('.scroll-element_size'), track: scroll.find('.scroll-element_track') }); return scroll; }, _handlemousedown: function(callback, event) { var namespace = this.namespace; $(document).on('blur' + namespace, function () { $(document).add('body').off(namespace); callback && callback(); }); $(document).on('dragstart' + namespace, function (event) { event.preventdefault(); return false; }); $(document).on('mouseup' + namespace, function () { $(document).add('body').off(namespace); callback && callback(); }); $('body').on('selectstart' + namespace, function (event) { event.preventdefault(); return false; }); event && event.preventdefault(); return false; }, _updatescroll: function (d, scrollx) { var container = this.container, containerwrapper = this.containerwrapper || container, scrollclass = 'scroll-scroll' + d + '_visible', scrolly = (d === 'x') ? this.scrolly : this.scrollx, offset = parseint(this.container.css((d === 'x') ? 'left' : 'top'), 10) || 0, wrapper = this.wrapper; 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.scroll.addclass(scrollclass); scrolly.scroll.addclass(scrollclass); containerwrapper.addclass(scrollclass); } else { scrollx.scroll.removeclass(scrollclass); scrolly.scroll.removeclass(scrollclass); containerwrapper.removeclass(scrollclass); } if (d === 'y') { if(container.is('textarea') || areasize < areavisible){ containerwrapper.css({ "height": (areavisible + browser.scroll.height) + 'px', "max-height": "none" }); } else { containerwrapper.css({ //"height": "auto", // do not reset height value: issue with height:100%! "max-height": (areavisible + browser.scroll.height) + 'px' }); } } if (scrollx.size != container.prop('scrollwidth') || scrolly.size != container.prop('scrollheight') || scrollx.visible != wrapper.width() || scrolly.visible != wrapper.height() || scrollx.offset != (parseint(container.css('left'), 10) || 0) || scrolly.offset != (parseint(container.css('top'), 10) || 0) ) { $.extend(this.scrollx, { "offset": parseint(container.css('left'), 10) || 0, "size": container.prop('scrollwidth'), "visible": wrapper.width() }); $.extend(this.scrolly, { "offset": parseint(container.css('top'), 10) || 0, "size": this.container.prop('scrollheight'), "visible": wrapper.height() }); this._updatescroll(d === 'x' ? 'y' : 'x', scrolly); } } }; var customscrollbar = basescrollbar; /* * extend jquery as plugin * * @param {mixed} command to execute * @param {mixed} arguments as array * @return {jquery} */ $.fn.scrollbar = function (command, args) { if (typeof command !== 'string') { args = command; command = 'init'; } if (typeof args === 'undefined') { args = []; } if (!$.isarray(args)) { args = [args]; } this.not('body, .scroll-wrapper').each(function () { var element = $(this), instance = element.data(browser.data.name); if (instance || command === 'init') { if (!instance) { instance = new customscrollbar(element); } if (instance[command]) { instance[command].apply(instance, args); } } }); return this; }; /** * connect default options to global object */ $.fn.scrollbar.options = defaults; /** * check if scroll content/container size is changed */ var updatescrollbars = (function () { var timer = 0, timercounter = 0; return function (force) { var i, container, options, scroll, wrapper, scrollx, scrolly; for (i = 0; i < browser.scrolls.length; i++) { scroll = browser.scrolls[i]; container = scroll.container; options = scroll.options; wrapper = scroll.wrapper; scrollx = scroll.scrollx; scrolly = scroll.scrolly; if (force || (options.autoupdate && wrapper && wrapper.is(':visible') && (container.prop('scrollwidth') != scrollx.size || container.prop('scrollheight') != scrolly.size || wrapper.width() != scrollx.visible || wrapper.height() != scrolly.visible))) { scroll.init(); if (options.debug) { window.console && console.log({ scrollheight: container.prop('scrollheight') + ':' + scroll.scrolly.size, scrollwidth: container.prop('scrollwidth') + ':' + scroll.scrollx.size, visibleheight: wrapper.height() + ':' + scroll.scrolly.visible, visiblewidth: wrapper.width() + ':' + scroll.scrollx.visible }, true); timercounter++; } } } if (debug && timercounter > 10) { window.console && console.log('scroll updates exceed 10'); updatescrollbars = function () {}; } else { cleartimeout(timer); timer = settimeout(updatescrollbars, 300); } }; })(); /* additional functions */ /** * get native browser scrollbar size (height/width) * * @param {boolean} actual size or css size, default - css size * @returns {object} with height, width */ function getbrowserscrollsize(actualsize) { if (browser.webkit && !actualsize) { return { "height": 0, "width": 0 }; } if (!browser.data.outer) { var css = { "border": "none", "box-sizing": "content-box", "height": "200px", "margin": "0", "padding": "0", "width": "200px" }; browser.data.inner = $("
").css($.extend({}, css)); browser.data.outer = $("
").css($.extend({ "left": "-1000px", "overflow": "scroll", "position": "absolute", "top": "-1000px" }, css)).append(browser.data.inner).appendto("body"); } browser.data.outer.scrollleft(1000).scrolltop(1000); return { "height": math.ceil((browser.data.outer.offset().top - browser.data.inner.offset().top) || 0), "width": math.ceil((browser.data.outer.offset().left - browser.data.inner.offset().left) || 0) }; } /** * check if native browser scrollbars overlay content * * @returns {boolean} */ function isscrolloverlayscontent() { var scrollsize = getbrowserscrollsize(true); return !(scrollsize.height || scrollsize.width); } function isverticalscroll(event) { var e = event.originalevent; if (e.axis && e.axis === e.horizontal_axis) return false; if (e.wheeldeltax) return false; return true; } /** * extend angularjs as ui directive * and expose a provider for override default config * */ if (window.angular) { (function (angular) { angular.module('jqueryscrollbar', []) .provider('jqueryscrollbar', function () { var defaultoptions = defaults; return { setoptions: function (options) { angular.extend(defaultoptions, options); }, $get: function () { return { options: angular.copy(defaultoptions) }; } }; }) .directive('jqueryscrollbar', function (jqueryscrollbar, $parse) { return { "restrict": "ac", "link": function (scope, element, attrs) { var model = $parse(attrs.jqueryscrollbar), options = model(scope); element.scrollbar(options || jqueryscrollbar.options) .on('$destroy', function () { element.scrollbar('destroy'); }); } }; }); })(window.angular); } }));