/* copyright (c) 2012, 2014 hyeonje alex jun and other contributors * licensed under the mit license */ (function (factory) { 'use strict'; if (typeof define === 'function' && define.amd) { // amd. register as an anonymous module. define(['jquery'], factory); } else if (typeof exports === 'object') { // node/commonjs factory(require('jquery')); } else { // browser globals factory(jquery); } }(function ($) { 'use strict'; // the default settings for the plugin var defaultsettings = { wheelspeed: 10, wheelpropagation: false, minscrollbarlength: null, usebothwheelaxes: false, usekeyboard: true, suppressscrollx: false, suppressscrolly: false, scrollxmarginoffset: 0, scrollymarginoffset: 0, includepadding: false }; var geteventclassname = (function () { var incrementingid = 0; return function () { var id = incrementingid; incrementingid += 1; return '.perfect-scrollbar-' + id; }; }()); $.fn.perfectscrollbar = function (suppliedsettings, option) { return this.each(function () { // use the default settings var settings = $.extend(true, {}, defaultsettings), $this = $(this); if (typeof suppliedsettings === "object") { // but over-ride any supplied $.extend(true, settings, suppliedsettings); } else { // if no settings were supplied, then the first param must be the option option = suppliedsettings; } // catch options if (option === 'update') { if ($this.data('perfect-scrollbar-update')) { $this.data('perfect-scrollbar-update')(); } return $this; } else if (option === 'destroy') { if ($this.data('perfect-scrollbar-destroy')) { $this.data('perfect-scrollbar-destroy')(); } return $this; } if ($this.data('perfect-scrollbar')) { // if there's already perfect-scrollbar return $this.data('perfect-scrollbar'); } // or generate new perfectscrollbar // set class to the container $this.addclass('ps-container'); var $scrollbarxrail = $("
").appendto($this), $scrollbaryrail = $("
").appendto($this), $scrollbarx = $("
").appendto($scrollbarxrail), $scrollbary = $("
").appendto($scrollbaryrail), scrollbarxactive, scrollbaryactive, containerwidth, containerheight, contentwidth, contentheight, scrollbarxwidth, scrollbarxleft, scrollbarxbottom = parseint($scrollbarxrail.css('bottom'), 10), isscrollbarxusingbottom = scrollbarxbottom === scrollbarxbottom, // !isnan scrollbarxtop = isscrollbarxusingbottom ? null : parseint($scrollbarxrail.css('top'), 10), scrollbaryheight, scrollbarytop, scrollbaryright = parseint($scrollbaryrail.css('right'), 10), isscrollbaryusingright = scrollbaryright === scrollbaryright, // !isnan scrollbaryleft = isscrollbaryusingright ? null: parseint($scrollbaryrail.css('left'), 10), isrtl = $this.css('direction') === "rtl", eventclassname = geteventclassname(); var updatecontentscrolltop = function (currenttop, deltay) { var newtop = currenttop + deltay, maxtop = containerheight - scrollbaryheight; if (newtop < 0) { scrollbarytop = 0; } else if (newtop > maxtop) { scrollbarytop = maxtop; } else { scrollbarytop = newtop; } var scrolltop = parseint(scrollbarytop * (contentheight - containerheight) / (containerheight - scrollbaryheight), 10); $this.scrolltop(scrolltop); if (isscrollbarxusingbottom) { $scrollbarxrail.css({bottom: scrollbarxbottom - scrolltop}); } else { $scrollbarxrail.css({top: scrollbarxtop + scrolltop}); } }; var updatecontentscrollleft = function (currentleft, deltax) { var newleft = currentleft + deltax, maxleft = containerwidth - scrollbarxwidth; if (newleft < 0) { scrollbarxleft = 0; } else if (newleft > maxleft) { scrollbarxleft = maxleft; } else { scrollbarxleft = newleft; } var scrollleft = parseint(scrollbarxleft * (contentwidth - containerwidth) / (containerwidth - scrollbarxwidth), 10); $this.scrollleft(scrollleft); if (isscrollbaryusingright) { $scrollbaryrail.css({right: scrollbaryright - scrollleft}); } else { $scrollbaryrail.css({left: scrollbaryleft + scrollleft}); } }; var getsettingsadjustedthumbsize = function (thumbsize) { if (settings.minscrollbarlength) { thumbsize = math.max(thumbsize, settings.minscrollbarlength); } return thumbsize; }; var updatescrollbarcss = function () { var scrollbarxstyles = {width: containerwidth, display: scrollbarxactive ? "inherit": "none"}; if (isrtl) { scrollbarxstyles.left = $this.scrollleft() + containerwidth - contentwidth; } else { scrollbarxstyles.left = $this.scrollleft(); } if (isscrollbarxusingbottom) { scrollbarxstyles.bottom = scrollbarxbottom - $this.scrolltop(); } else { scrollbarxstyles.top = scrollbarxtop + $this.scrolltop(); } $scrollbarxrail.css(scrollbarxstyles); var scrollbarystyles = {top: $this.scrolltop(), height: containerheight, display: scrollbaryactive ? "inherit": "none"}; if (isscrollbaryusingright) { if (isrtl) { scrollbarystyles.right = contentwidth - $this.scrollleft() - scrollbaryright - $scrollbary.outerwidth(); } else { scrollbarystyles.right = scrollbaryright - $this.scrollleft(); } } else { if (isrtl) { scrollbarystyles.left = $this.scrollleft() + containerwidth * 2 - contentwidth - scrollbaryleft - $scrollbary.outerwidth(); } else { scrollbarystyles.left = scrollbaryleft + $this.scrollleft(); } } $scrollbaryrail.css(scrollbarystyles); $scrollbarx.css({left: scrollbarxleft, width: scrollbarxwidth}); $scrollbary.css({top: scrollbarytop, height: scrollbaryheight}); }; var updatebarsizeandposition = function () { containerwidth = settings.includepadding ? $this.innerwidth() : $this.width(); containerheight = settings.includepadding ? $this.innerheight() : $this.height(); contentwidth = $this.prop('scrollwidth'); contentheight = $this.prop('scrollheight'); if (!settings.suppressscrollx && containerwidth + settings.scrollxmarginoffset < contentwidth) { scrollbarxactive = true; scrollbarxwidth = getsettingsadjustedthumbsize(parseint(containerwidth * containerwidth / contentwidth, 10)); scrollbarxleft = parseint($this.scrollleft() * (containerwidth - scrollbarxwidth) / (contentwidth - containerwidth), 10); } else { scrollbarxactive = false; scrollbarxwidth = 0; scrollbarxleft = 0; $this.scrollleft(0); } if (!settings.suppressscrolly && containerheight + settings.scrollymarginoffset < contentheight) { scrollbaryactive = true; scrollbaryheight = getsettingsadjustedthumbsize(parseint(containerheight * containerheight / contentheight, 10)); scrollbarytop = parseint($this.scrolltop() * (containerheight - scrollbaryheight) / (contentheight - containerheight), 10); } else { scrollbaryactive = false; scrollbaryheight = 0; scrollbarytop = 0; $this.scrolltop(0); } if (scrollbarytop >= containerheight - scrollbaryheight) { scrollbarytop = containerheight - scrollbaryheight; } if (scrollbarxleft >= containerwidth - scrollbarxwidth) { scrollbarxleft = containerwidth - scrollbarxwidth; } updatescrollbarcss(); }; var bindmousescrollxhandler = function () { var currentleft, currentpagex; $scrollbarx.bind('mousedown' + eventclassname, function (e) { currentpagex = e.pagex; currentleft = $scrollbarx.position().left; $scrollbarxrail.addclass('in-scrolling'); e.stoppropagation(); e.preventdefault(); }); $(document).bind('mousemove' + eventclassname, function (e) { if ($scrollbarxrail.hasclass('in-scrolling')) { updatecontentscrollleft(currentleft, e.pagex - currentpagex); e.stoppropagation(); e.preventdefault(); } }); $(document).bind('mouseup' + eventclassname, function (e) { if ($scrollbarxrail.hasclass('in-scrolling')) { $scrollbarxrail.removeclass('in-scrolling'); } }); currentleft = currentpagex = null; }; var bindmousescrollyhandler = function () { var currenttop, currentpagey; $scrollbary.bind('mousedown' + eventclassname, function (e) { currentpagey = e.pagey; currenttop = $scrollbary.position().top; $scrollbaryrail.addclass('in-scrolling'); e.stoppropagation(); e.preventdefault(); }); $(document).bind('mousemove' + eventclassname, function (e) { if ($scrollbaryrail.hasclass('in-scrolling')) { updatecontentscrolltop(currenttop, e.pagey - currentpagey); e.stoppropagation(); e.preventdefault(); } }); $(document).bind('mouseup' + eventclassname, function (e) { if ($scrollbaryrail.hasclass('in-scrolling')) { $scrollbaryrail.removeclass('in-scrolling'); } }); currenttop = currentpagey = null; }; // check if the default scrolling should be prevented. var shouldpreventdefault = function (deltax, deltay) { var scrolltop = $this.scrolltop(); if (deltax === 0) { if (!scrollbaryactive) { return false; } if ((scrolltop === 0 && deltay > 0) || (scrolltop >= contentheight - containerheight && deltay < 0)) { return !settings.wheelpropagation; } } var scrollleft = $this.scrollleft(); if (deltay === 0) { if (!scrollbarxactive) { return false; } if ((scrollleft === 0 && deltax < 0) || (scrollleft >= contentwidth - containerwidth && deltax > 0)) { return !settings.wheelpropagation; } } return true; }; // bind handlers var bindmousewheelhandler = function () { // fixme: backward compatibility. // after e.deltafactor applied, wheelspeed should have smaller value. // currently, there's no way to change the settings after the scrollbar initialized. // but if the way is implemented in the future, wheelspeed should be reset. settings.wheelspeed /= 10; var shouldprevent = false; $this.bind('mousewheel' + eventclassname, function (e, deprecateddelta, deprecateddeltax, deprecateddeltay) { var deltax = e.deltax * e.deltafactor || deprecateddeltax, deltay = e.deltay * e.deltafactor || deprecateddeltay; shouldprevent = false; if (!settings.usebothwheelaxes) { // deltax will only be used for horizontal scrolling and deltay will // only be used for vertical scrolling - this is the default $this.scrolltop($this.scrolltop() - (deltay * settings.wheelspeed)); $this.scrollleft($this.scrollleft() + (deltax * settings.wheelspeed)); } else if (scrollbaryactive && !scrollbarxactive) { // only vertical scrollbar is active and usebothwheelaxes option is // active, so let's scroll vertical bar using both mouse wheel axes if (deltay) { $this.scrolltop($this.scrolltop() - (deltay * settings.wheelspeed)); } else { $this.scrolltop($this.scrolltop() + (deltax * settings.wheelspeed)); } shouldprevent = true; } else if (scrollbarxactive && !scrollbaryactive) { // usebothwheelaxes and only horizontal bar is active, so use both // wheel axes for horizontal bar if (deltax) { $this.scrollleft($this.scrollleft() + (deltax * settings.wheelspeed)); } else { $this.scrollleft($this.scrollleft() - (deltay * settings.wheelspeed)); } shouldprevent = true; } // update bar position updatebarsizeandposition(); shouldprevent = (shouldprevent || shouldpreventdefault(deltax, deltay)); if (shouldprevent) { e.stoppropagation(); e.preventdefault(); } }); // fix firefox scroll problem $this.bind('mozmousepixelscroll' + eventclassname, function (e) { if (shouldprevent) { e.preventdefault(); } }); }; var bindkeyboardhandler = function () { var hovered = false; $this.bind('mouseenter' + eventclassname, function (e) { hovered = true; }); $this.bind('mouseleave' + eventclassname, function (e) { hovered = false; }); var shouldprevent = false; $(document).bind('keydown' + eventclassname, function (e) { if (!hovered || $(document.activeelement).is(":input,[contenteditable]")) { return; } var deltax = 0, deltay = 0; switch (e.which) { case 37: // left deltax = -30; break; case 38: // up deltay = 30; break; case 39: // right deltax = 30; break; case 40: // down deltay = -30; break; case 33: // page up deltay = 90; break; case 32: // space bar case 34: // page down deltay = -90; break; case 35: // end deltay = -containerheight; break; case 36: // home deltay = containerheight; break; default: return; } $this.scrolltop($this.scrolltop() - deltay); $this.scrollleft($this.scrollleft() + deltax); shouldprevent = shouldpreventdefault(deltax, deltay); if (shouldprevent) { e.preventdefault(); } }); }; var bindrailclickhandler = function () { var stoppropagation = function (e) { e.stoppropagation(); }; $scrollbary.bind('click' + eventclassname, stoppropagation); $scrollbaryrail.bind('click' + eventclassname, function (e) { var halfofscrollbarlength = parseint(scrollbaryheight / 2, 10), positiontop = e.pagey - $scrollbaryrail.offset().top - halfofscrollbarlength, maxpositiontop = containerheight - scrollbaryheight, positionratio = positiontop / maxpositiontop; if (positionratio < 0) { positionratio = 0; } else if (positionratio > 1) { positionratio = 1; } $this.scrolltop((contentheight - containerheight) * positionratio); }); $scrollbarx.bind('click' + eventclassname, stoppropagation); $scrollbarxrail.bind('click' + eventclassname, function (e) { var halfofscrollbarlength = parseint(scrollbarxwidth / 2, 10), positionleft = e.pagex - $scrollbarxrail.offset().left - halfofscrollbarlength, maxpositionleft = containerwidth - scrollbarxwidth, positionratio = positionleft / maxpositionleft; if (positionratio < 0) { positionratio = 0; } else if (positionratio > 1) { positionratio = 1; } $this.scrollleft((contentwidth - containerwidth) * positionratio); }); }; // bind mobile touch handler var bindmobiletouchhandler = function () { var applytouchmove = function (differencex, differencey) { $this.scrolltop($this.scrolltop() - differencey); $this.scrollleft($this.scrollleft() - differencex); // update bar position updatebarsizeandposition(); }; var startcoords = {}, starttime = 0, speed = {}, breakingprocess = null, inglobaltouch = false; $(window).bind("touchstart" + eventclassname, function (e) { inglobaltouch = true; }); $(window).bind("touchend" + eventclassname, function (e) { inglobaltouch = false; }); $this.bind("touchstart" + eventclassname, function (e) { var touch = e.originalevent.targettouches[0]; startcoords.pagex = touch.pagex; startcoords.pagey = touch.pagey; starttime = (new date()).gettime(); if (breakingprocess !== null) { clearinterval(breakingprocess); } e.stoppropagation(); }); $this.bind("touchmove" + eventclassname, function (e) { if (!inglobaltouch && e.originalevent.targettouches.length === 1) { var touch = e.originalevent.targettouches[0]; var currentcoords = {}; currentcoords.pagex = touch.pagex; currentcoords.pagey = touch.pagey; var differencex = currentcoords.pagex - startcoords.pagex, differencey = currentcoords.pagey - startcoords.pagey; applytouchmove(differencex, differencey); startcoords = currentcoords; var currenttime = (new date()).gettime(); var timegap = currenttime - starttime; if (timegap > 0) { speed.x = differencex / timegap; speed.y = differencey / timegap; starttime = currenttime; } e.preventdefault(); } }); $this.bind("touchend" + eventclassname, function (e) { clearinterval(breakingprocess); breakingprocess = setinterval(function () { if (math.abs(speed.x) < 0.01 && math.abs(speed.y) < 0.01) { clearinterval(breakingprocess); return; } applytouchmove(speed.x * 30, speed.y * 30); speed.x *= 0.8; speed.y *= 0.8; }, 10); }); }; var bindscrollhandler = function () { $this.bind('scroll' + eventclassname, function (e) { updatebarsizeandposition(); }); }; var destroy = function () { $this.unbind(eventclassname); $(window).unbind(eventclassname); $(document).unbind(eventclassname); $this.data('perfect-scrollbar', null); $this.data('perfect-scrollbar-update', null); $this.data('perfect-scrollbar-destroy', null); $scrollbarx.remove(); $scrollbary.remove(); $scrollbarxrail.remove(); $scrollbaryrail.remove(); // clean all variables $scrollbarxrail = $scrollbaryrail = $scrollbarx = $scrollbary = scrollbarxactive = scrollbaryactive = containerwidth = containerheight = contentwidth = contentheight = scrollbarxwidth = scrollbarxleft = scrollbarxbottom = isscrollbarxusingbottom = scrollbarxtop = scrollbaryheight = scrollbarytop = scrollbaryright = isscrollbaryusingright = scrollbaryleft = isrtl = eventclassname = null; }; var iesupport = function (version) { $this.addclass('ie').addclass('ie' + version); var bindhoverhandlers = function () { var mouseenter = function () { $(this).addclass('hover'); }; var mouseleave = function () { $(this).removeclass('hover'); }; $this.bind('mouseenter' + eventclassname, mouseenter).bind('mouseleave' + eventclassname, mouseleave); $scrollbarxrail.bind('mouseenter' + eventclassname, mouseenter).bind('mouseleave' + eventclassname, mouseleave); $scrollbaryrail.bind('mouseenter' + eventclassname, mouseenter).bind('mouseleave' + eventclassname, mouseleave); $scrollbarx.bind('mouseenter' + eventclassname, mouseenter).bind('mouseleave' + eventclassname, mouseleave); $scrollbary.bind('mouseenter' + eventclassname, mouseenter).bind('mouseleave' + eventclassname, mouseleave); }; var fixie6scrollbarposition = function () { updatescrollbarcss = function () { var scrollbarxstyles = {left: scrollbarxleft + $this.scrollleft(), width: scrollbarxwidth}; if (isscrollbarxusingbottom) { scrollbarxstyles.bottom = scrollbarxbottom; } else { scrollbarxstyles.top = scrollbarxtop; } $scrollbarx.css(scrollbarxstyles); var scrollbarystyles = {top: scrollbarytop + $this.scrolltop(), height: scrollbaryheight}; if (isscrollbaryusingright) { scrollbarystyles.right = scrollbaryright; } else { scrollbarystyles.left = scrollbaryleft; } $scrollbary.css(scrollbarystyles); $scrollbarx.hide().show(); $scrollbary.hide().show(); }; }; if (version === 6) { bindhoverhandlers(); fixie6scrollbarposition(); } }; var supportstouch = (('ontouchstart' in window) || window.documenttouch && document instanceof window.documenttouch); var initialize = function () { var iematch = navigator.useragent.tolowercase().match(/(msie) ([\w.]+)/); if (iematch && iematch[1] === 'msie') { // must be executed at first, because 'iesupport' may addclass to the container iesupport(parseint(iematch[2], 10)); } updatebarsizeandposition(); bindscrollhandler(); bindmousescrollxhandler(); bindmousescrollyhandler(); bindrailclickhandler(); if (supportstouch) { bindmobiletouchhandler(); } if ($this.mousewheel) { bindmousewheelhandler(); } if (settings.usekeyboard) { bindkeyboardhandler(); } $this.data('perfect-scrollbar', $this); $this.data('perfect-scrollbar-update', updatebarsizeandposition); $this.data('perfect-scrollbar-destroy', destroy); }; // initialize initialize(); return $this; }); }; }));