/** * 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.11 * @url https://github.com/gromo/jquery.scrollbar/ * */ ; (function (root, factory) { 'use strict'; if (typeof define === 'function' && define.amd) { define(['jquery'], factory); } else if (typeof exports !== 'undefined') { module.exports = factory(require('jquery')); } else { factory(jQuery); } }(this, function ($) { 'use strict'; // init flags & variables var debug = false; var browser = { data: { index: 0, name: 'scrollbar' }, firefox: /firefox/i.test(navigator.userAgent), macosx: /mac/i.test(navigator.platform), msedge: /edge\/\d+/i.test(navigator.userAgent), msie: /(msie|trident)/i.test(navigator.userAgent), mobile: /android|webos|iphone|ipad|ipod|blackberry/i.test(navigator.userAgent), overlay: null, scroll: null, scrolls: [], webkit: /webkit/i.test(navigator.userAgent) && !/edge\/\d+/i.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: true, // 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) isRtl: false, // is RTL 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, onFallback: null, // callback function if scrollbar is not initialized 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').addBack().off(this.namespace); this.scrolly.scroll.removeClass('scroll-scrolly_visible').find('div').addBack().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, cssOptions = {}; 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 ) { if ($.isFunction(o.onFallback)) { o.onFallback.apply(this, [c]); } 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 (o.isRtl) { w.addClass('scroll--rtl'); } if (c.is('textarea')) { this.containerWrapper = cw = $('
').insertBefore(c).append(c); w.addClass('scroll-textarea'); } cssOptions = { "height": "auto", "margin-bottom": browser.scroll.height * -1 + 'px', "max-height": "" }; cssOptions[o.isRtl ? 'margin-left' : 'margin-right'] = browser.scroll.width * -1 + 'px'; cw.addClass('scroll-content').css(cssOptions); c.on('scroll' + namespace, function (event) { var scrollLeft = c.scrollLeft(); var scrollTop = c.scrollTop(); if (o.isRtl) { // webkit 0:100 // ie/edge 100:0 // firefox -100:0 switch (true) { case browser.firefox: scrollLeft = Math.abs(scrollLeft); case browser.msedge || browser.msie: scrollLeft = c[0].scrollWidth - c[0].clientWidth - scrollLeft; break; } } if ($.isFunction(o.onScroll)) { o.onScroll.call(S, { maxScroll: s.y.maxScrollOffset, scroll: scrollTop, size: s.y.size, visible: s.y.visible }, { maxScroll: s.x.maxScrollOffset, scroll: scrollLeft, size: s.x.size, visible: s.x.visible }); } s.x.isVisible && s.x.scroll.bar.css('left', scrollLeft * s.x.kx + 'px'); s.y.isVisible && s.y.scroll.bar.css('top', 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 { cssOptions = { "height": "auto", "margin-bottom": browser.scroll.height * -1 + 'px', "max-height": "" }; cssOptions[o.isRtl ? 'margin-left' : 'margin-right'] = browser.scroll.width * -1 + 'px'; cw.css(cssOptions); } // 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; // fix new mozilla if (!delta) { if (d === 'x' && !!event.originalEvent.deltaX) { delta = event.originalEvent.deltaX * 40; } else if (d === 'y' && !!event.originalEvent.deltaY) { delta = event.originalEvent.deltaY * 40; } } 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; if (o.isRtl) { switch(true){ case browser.firefox: scrollToValue = scrollForward > 0 ? 0: data.maxScrollValue * -1; break; case browser.msie || browser.msedge: break; } } } else { scrollForward = (data.eventOffset > (data.scrollbarOffset + data.scrollbarSize) ? 1 : (data.eventOffset < data.scrollbarOffset ? -1 : 0)); if(d === 'x' && o.isRtl && (browser.msie || browser.msedge)) scrollForward = scrollForward * -1; 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); if (d === 'x' && o.isRtl && (browser.msie || browser.msedge)) diff = diff * -1; 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', ['jQueryScrollbar', '$parse', 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); } }));