perfect-scrollbar/src/perfect-scrollbar.js

707 lines
23 KiB
JavaScript
Raw Normal View History

2014-09-25 02:35:43 +08:00
/* Copyright (c) 2012, 2014 Hyunje Alex Jun and other contributors
2012-11-06 21:00:37 +08:00
* Licensed under the MIT License
*/
2013-09-20 17:04:29 +08:00
(function (factory) {
'use strict';
2013-09-20 17:04:29 +08:00
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['jquery'], factory);
2014-02-21 02:06:30 +08:00
} else if (typeof exports === 'object') {
// Node/CommonJS
factory(require('jquery'));
2013-09-20 17:04:29 +08:00
} else {
// Browser globals
factory(jQuery);
}
})(function ($) {
'use strict';
2013-05-18 11:41:15 +08:00
function int(x) {
if (typeof x === 'string') {
return parseInt(x, 10);
} else {
return ~~x;
}
}
2013-05-18 11:41:15 +08:00
var defaultSettings = {
wheelSpeed: 1,
wheelPropagation: false,
minScrollbarLength: null,
2014-07-10 01:16:16 +08:00
maxScrollbarLength: null,
useBothWheelAxes: false,
useKeyboard: true,
suppressScrollX: false,
suppressScrollY: false,
scrollXMarginOffset: 0,
scrollYMarginOffset: 0,
includePadding: false
2013-05-18 11:41:15 +08:00
};
var getEventClassName = (function () {
var incrementingId = 0;
return function () {
var id = incrementingId;
incrementingId += 1;
return '.perfect-scrollbar-' + id;
};
2014-10-14 05:04:16 +08:00
})();
2013-05-18 11:41:15 +08:00
$.fn.perfectScrollbar = function (suppliedSettings, option) {
2013-06-14 20:38:33 +08:00
return this.each(function () {
2014-10-14 05:04:16 +08:00
var settings = $.extend(true, {}, defaultSettings);
var $this = $(this);
2013-08-05 22:59:17 +08:00
if (typeof suppliedSettings === "object") {
// Override default settings with any supplied
$.extend(true, settings, suppliedSettings);
} else {
2014-10-14 05:04:16 +08:00
// If no setting was supplied, then the first param must be the option
option = suppliedSettings;
2013-05-18 11:41:15 +08:00
}
2013-08-05 22:59:17 +08:00
// Catch options
if (option === 'update') {
2013-08-05 22:59:17 +08:00
if ($this.data('perfect-scrollbar-update')) {
$this.data('perfect-scrollbar-update')();
}
2013-08-05 22:59:17 +08:00
return $this;
2013-05-18 11:41:15 +08:00
}
else if (option === 'destroy') {
2013-08-05 22:59:17 +08:00
if ($this.data('perfect-scrollbar-destroy')) {
$this.data('perfect-scrollbar-destroy')();
}
2013-08-05 22:59:17 +08:00
return $this;
2013-05-18 11:41:15 +08:00
}
2013-08-05 22:59:17 +08:00
if ($this.data('perfect-scrollbar')) {
// if there's already perfect-scrollbar
2013-08-05 22:59:17 +08:00
return $this.data('perfect-scrollbar');
2013-05-18 11:41:15 +08:00
}
2013-08-05 22:59:17 +08:00
// Or generate new perfectScrollbar
$this.addClass('ps-container');
2014-10-14 05:04:16 +08:00
var containerWidth;
var containerHeight;
var contentWidth;
var contentHeight;
var isRtl = $this.css('direction') === "rtl";
var eventClassName = getEventClassName();
var $scrollbarXRail = $("<div class='ps-scrollbar-x-rail'>").appendTo($this);
var $scrollbarX = $("<div class='ps-scrollbar-x'>").appendTo($scrollbarXRail);
var scrollbarXActive;
2014-10-14 05:04:16 +08:00
var scrollbarXWidth;
var scrollbarXLeft;
var scrollbarXBottom = int($scrollbarXRail.css('bottom'));
2014-10-14 05:04:16 +08:00
var isScrollbarXUsingBottom = scrollbarXBottom === scrollbarXBottom; // !isNaN
var scrollbarXTop = isScrollbarXUsingBottom ? null : int($scrollbarXRail.css('top'));
var railBorderXWidth = int($scrollbarXRail.css('borderLeftWidth')) + int($scrollbarXRail.css('borderRightWidth'));
var $scrollbarYRail = $("<div class='ps-scrollbar-y-rail'>").appendTo($this);
var $scrollbarY = $("<div class='ps-scrollbar-y'>").appendTo($scrollbarYRail);
var scrollbarYActive;
2014-10-14 05:04:16 +08:00
var scrollbarYHeight;
var scrollbarYTop;
var scrollbarYRight = int($scrollbarYRail.css('right'));
2014-10-14 05:04:16 +08:00
var isScrollbarYUsingRight = scrollbarYRight === scrollbarYRight; // !isNaN
var scrollbarYLeft = isScrollbarYUsingRight ? null : int($scrollbarYRail.css('left'));
var railBorderYWidth = int($scrollbarXRail.css('borderTopWidth')) + int($scrollbarXRail.css('borderBottomWidth'));
function updateScrollTop(currentTop, deltaY) {
2014-10-14 05:04:16 +08:00
var newTop = currentTop + deltaY;
var maxTop = containerHeight - scrollbarYHeight;
if (newTop < 0) {
scrollbarYTop = 0;
} else if (newTop > maxTop) {
scrollbarYTop = maxTop;
} else {
scrollbarYTop = newTop;
}
var scrollTop = int(scrollbarYTop * (contentHeight - containerHeight) / (containerHeight - scrollbarYHeight));
$this.scrollTop(scrollTop);
}
2013-05-18 11:41:15 +08:00
function updateScrollLeft(currentLeft, deltaX) {
2014-10-14 05:04:16 +08:00
var newLeft = currentLeft + deltaX;
var maxLeft = containerWidth - scrollbarXWidth;
if (newLeft < 0) {
scrollbarXLeft = 0;
} else if (newLeft > maxLeft) {
scrollbarXLeft = maxLeft;
} else {
scrollbarXLeft = newLeft;
}
var scrollLeft = int(scrollbarXLeft * (contentWidth - containerWidth) / (containerWidth - scrollbarXWidth));
$this.scrollLeft(scrollLeft);
}
function getThumbSize(thumbSize) {
if (settings.minScrollbarLength) {
thumbSize = Math.max(thumbSize, settings.minScrollbarLength);
}
2014-07-10 01:16:16 +08:00
if (settings.maxScrollbarLength) {
thumbSize = Math.min(thumbSize, settings.maxScrollbarLength);
}
return thumbSize;
}
function updateCss() {
var xRailOffset = {width: containerWidth, display: scrollbarXActive ? "inherit" : "none"};
if (isRtl) {
xRailOffset.left = $this.scrollLeft() + containerWidth - contentWidth;
} else {
xRailOffset.left = $this.scrollLeft();
}
if (isScrollbarXUsingBottom) {
xRailOffset.bottom = scrollbarXBottom - $this.scrollTop();
} else {
xRailOffset.top = scrollbarXTop + $this.scrollTop();
}
$scrollbarXRail.css(xRailOffset);
var railYOffset = {top: $this.scrollTop(), height: containerHeight, display: scrollbarYActive ? "inherit" : "none"};
if (isScrollbarYUsingRight) {
if (isRtl) {
railYOffset.right = contentWidth - $this.scrollLeft() - scrollbarYRight - $scrollbarY.outerWidth();
} else {
railYOffset.right = scrollbarYRight - $this.scrollLeft();
}
} else {
if (isRtl) {
railYOffset.left = $this.scrollLeft() + containerWidth * 2 - contentWidth - scrollbarYLeft - $scrollbarY.outerWidth();
} else {
railYOffset.left = scrollbarYLeft + $this.scrollLeft();
}
}
$scrollbarYRail.css(railYOffset);
$scrollbarX.css({left: scrollbarXLeft, width: scrollbarXWidth - railBorderXWidth});
$scrollbarY.css({top: scrollbarYTop, height: scrollbarYHeight - railBorderYWidth});
}
function updateGeometry() {
// Hide scrollbars not to affect scrollWidth and scrollHeight
$scrollbarXRail.hide();
$scrollbarYRail.hide();
containerWidth = settings.includePadding ? $this.innerWidth() : $this.width();
containerHeight = settings.includePadding ? $this.innerHeight() : $this.height();
contentWidth = $this.prop('scrollWidth');
contentHeight = $this.prop('scrollHeight');
2013-08-05 22:59:17 +08:00
if (!settings.suppressScrollX && containerWidth + settings.scrollXMarginOffset < contentWidth) {
scrollbarXActive = true;
scrollbarXWidth = getThumbSize(int(containerWidth * containerWidth / contentWidth));
scrollbarXLeft = int($this.scrollLeft() * (containerWidth - scrollbarXWidth) / (contentWidth - containerWidth));
} else {
scrollbarXActive = false;
scrollbarXWidth = 0;
scrollbarXLeft = 0;
$this.scrollLeft(0);
}
2013-08-05 22:59:17 +08:00
if (!settings.suppressScrollY && containerHeight + settings.scrollYMarginOffset < contentHeight) {
scrollbarYActive = true;
scrollbarYHeight = getThumbSize(int(containerHeight * containerHeight / contentHeight));
scrollbarYTop = int($this.scrollTop() * (containerHeight - scrollbarYHeight) / (contentHeight - containerHeight));
} else {
scrollbarYActive = false;
scrollbarYHeight = 0;
scrollbarYTop = 0;
$this.scrollTop(0);
}
if (scrollbarXLeft >= containerWidth - scrollbarXWidth) {
scrollbarXLeft = containerWidth - scrollbarXWidth;
}
if (scrollbarYTop >= containerHeight - scrollbarYHeight) {
scrollbarYTop = containerHeight - scrollbarYHeight;
}
updateCss();
if (scrollbarXActive) {
$this.addClass('ps-active-x');
} else {
$this.removeClass('ps-active-x');
}
if (scrollbarYActive) {
$this.addClass('ps-active-y');
} else {
$this.removeClass('ps-active-y');
}
// Show scrollbars if needed after updated
if (!settings.suppressScrollX) {
$scrollbarXRail.show();
}
if (!settings.suppressScrollY) {
$scrollbarYRail.show();
}
}
function bindMouseScrollXHandler() {
2014-10-14 05:04:16 +08:00
var currentLeft;
var currentPageX;
2013-05-18 11:41:15 +08:00
$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')) {
updateScrollLeft(currentLeft, e.pageX - currentPageX);
updateGeometry();
e.stopPropagation();
e.preventDefault();
}
});
$(document).bind('mouseup' + eventClassName, function (e) {
if ($scrollbarXRail.hasClass('in-scrolling')) {
$scrollbarXRail.removeClass('in-scrolling');
}
});
2013-08-05 22:59:17 +08:00
currentLeft =
currentPageX = null;
}
function bindMouseScrollYHandler() {
2014-10-14 05:04:16 +08:00
var currentTop;
var currentPageY;
$scrollbarY.bind('mousedown' + eventClassName, function (e) {
currentPageY = e.pageY;
currentTop = $scrollbarY.position().top;
$scrollbarYRail.addClass('in-scrolling');
e.stopPropagation();
2013-05-18 11:41:15 +08:00
e.preventDefault();
});
$(document).bind('mousemove' + eventClassName, function (e) {
if ($scrollbarYRail.hasClass('in-scrolling')) {
updateScrollTop(currentTop, e.pageY - currentPageY);
updateGeometry();
e.stopPropagation();
e.preventDefault();
}
});
2013-05-18 11:41:15 +08:00
$(document).bind('mouseup' + eventClassName, function (e) {
if ($scrollbarYRail.hasClass('in-scrolling')) {
$scrollbarYRail.removeClass('in-scrolling');
}
});
2013-08-05 22:59:17 +08:00
currentTop =
currentPageY = null;
}
2013-05-18 11:41:15 +08:00
// check if the default scrolling should be prevented.
function shouldPreventDefault(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;
}
function bindMouseWheelHandler() {
var shouldPrevent = false;
function getDeltaFromEvent(e) {
2014-10-14 05:04:16 +08:00
var deltaX = e.originalEvent.deltaX;
var deltaY = -1 * e.originalEvent.deltaY;
if (typeof deltaX === "undefined" || typeof deltaY === "undefined") {
// OS X Safari
deltaX = -1 * e.originalEvent.wheelDeltaX / 6;
deltaY = e.originalEvent.wheelDeltaY / 6;
}
if (e.originalEvent.deltaMode && e.originalEvent.deltaMode === 1) {
// Firefox in deltaMode 1: Line scrolling
deltaX *= 10;
deltaY *= 10;
}
if (deltaX !== deltaX && deltaY !== deltaY/* NaN checks */) {
// IE in some mouse drivers
deltaX = 0;
deltaY = e.originalEvent.wheelDelta;
}
return [deltaX, deltaY];
}
function mousewheelHandler(e) {
var delta = getDeltaFromEvent(e);
2014-10-14 05:04:16 +08:00
var deltaX = delta[0];
var deltaY = delta[1];
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;
}
updateGeometry();
shouldPrevent = (shouldPrevent || shouldPreventDefault(deltaX, deltaY));
if (shouldPrevent) {
e.stopPropagation();
e.preventDefault();
}
}
if (typeof window.onwheel !== "undefined") {
$this.bind('wheel' + eventClassName, mousewheelHandler);
} else if (typeof window.onmousewheel !== "undefined") {
$this.bind('mousewheel' + eventClassName, mousewheelHandler);
}
}
2013-05-18 11:41:15 +08:00
function bindKeyboardHandler() {
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 (e.isDefaultPrevented && e.isDefaultPrevented()) {
return;
}
if (!hovered) {
return;
}
var activeElement = document.activeElement;
// go deeper if element is a webcomponent
while (activeElement.shadowRoot) {
activeElement = activeElement.shadowRoot.activeElement;
}
if ($(activeElement).is(":input,[contenteditable]")) {
return;
}
2014-10-14 05:04:16 +08:00
var deltaX = 0;
var 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
if (e.ctrlKey) {
deltaY = -contentHeight;
} else {
deltaY = -containerHeight;
}
break;
case 36: // home
if (e.ctrlKey) {
deltaY = $this.scrollTop();
} else {
deltaY = containerHeight;
}
break;
default:
return;
}
$this.scrollTop($this.scrollTop() - deltaY);
$this.scrollLeft($this.scrollLeft() + deltaX);
shouldPrevent = shouldPreventDefault(deltaX, deltaY);
if (shouldPrevent) {
e.preventDefault();
}
});
}
function bindRailClickHandler() {
function stopPropagation(e) { e.stopPropagation(); }
$scrollbarY.bind('click' + eventClassName, stopPropagation);
$scrollbarYRail.bind('click' + eventClassName, function (e) {
var halfOfScrollbarLength = int(scrollbarYHeight / 2);
2014-10-14 05:04:16 +08:00
var positionTop = e.pageY - $scrollbarYRail.offset().top - halfOfScrollbarLength;
var maxPositionTop = containerHeight - scrollbarYHeight;
var 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 = int(scrollbarXWidth / 2);
2014-10-14 05:04:16 +08:00
var positionLeft = e.pageX - $scrollbarXRail.offset().left - halfOfScrollbarLength;
var maxPositionLeft = containerWidth - scrollbarXWidth;
var positionRatio = positionLeft / maxPositionLeft;
if (positionRatio < 0) {
positionRatio = 0;
} else if (positionRatio > 1) {
positionRatio = 1;
}
$this.scrollLeft((contentWidth - containerWidth) * positionRatio);
});
}
function bindTouchHandler() {
function applyTouchMove(differenceX, differenceY) {
$this.scrollTop($this.scrollTop() - differenceY);
$this.scrollLeft($this.scrollLeft() - differenceX);
2013-05-18 11:41:15 +08:00
updateGeometry();
}
2013-05-18 11:41:15 +08:00
var startOffset = {};
2014-10-14 05:04:16 +08:00
var startTime = 0;
var speed = {};
var breakingProcess = null;
var inGlobalTouch = false;
function globalTouchStart(e) {
inGlobalTouch = true;
}
function globalTouchEnd(e) {
inGlobalTouch = false;
}
2013-05-18 11:41:15 +08:00
function getTouch(e) {
if (e.originalEvent.targetTouches) {
return e.originalEvent.targetTouches[0];
} else {
// Maybe IE pointer
return e.originalEvent;
}
}
function touchStart(e) {
var touch = getTouch(e);
2013-05-18 11:41:15 +08:00
startOffset.pageX = touch.pageX;
startOffset.pageY = touch.pageY;
startTime = (new Date()).getTime();
if (breakingProcess !== null) {
2013-05-18 11:41:15 +08:00
clearInterval(breakingProcess);
}
e.stopPropagation();
}
function touchMove(e) {
if (!inGlobalTouch && e.originalEvent.targetTouches.length === 1) {
var touch = getTouch(e);
2013-05-18 11:41:15 +08:00
var currentOffset = {pageX: touch.pageX, pageY: touch.pageY};
var differenceX = currentOffset.pageX - startOffset.pageX;
var differenceY = currentOffset.pageY - startOffset.pageY;
applyTouchMove(differenceX, differenceY);
startOffset = currentOffset;
var currentTime = (new Date()).getTime();
var timeGap = currentTime - startTime;
if (timeGap > 0) {
speed.x = differenceX / timeGap;
speed.y = differenceY / timeGap;
startTime = currentTime;
}
e.preventDefault();
}
}
function touchEnd(e) {
2013-08-05 22:59:17 +08:00
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);
}
$(window).bind("touchstart" + eventClassName, globalTouchStart);
$(window).bind("touchend" + eventClassName, globalTouchEnd);
$this.bind("touchstart" + eventClassName, touchStart);
$this.bind("touchmove" + eventClassName, touchMove);
$this.bind("touchend" + eventClassName, touchEnd);
if (window.PointerEvent) {
$(window).bind("pointerdown" + eventClassName, globalTouchStart);
$(window).bind("pointerup" + eventClassName, globalTouchEnd);
$this.bind("pointerdown" + eventClassName, touchStart);
$this.bind("pointermove" + eventClassName, touchMove);
$this.bind("pointerup" + eventClassName, touchEnd);
} else if (window.MSPointerEvent) {
$(window).bind("MSPointerDown" + eventClassName, globalTouchStart);
$(window).bind("MSPointerUp" + eventClassName, globalTouchEnd);
$this.bind("MSPointerDown" + eventClassName, touchStart);
$this.bind("MSPointerMove" + eventClassName, touchMove);
$this.bind("MSPointerUp" + eventClassName, touchEnd);
}
}
function bindScrollHandler() {
$this.bind('scroll' + eventClassName, function (e) {
updateGeometry();
});
}
function destroy() {
$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);
2013-08-05 22:59:17 +08:00
$scrollbarX.remove();
$scrollbarY.remove();
$scrollbarXRail.remove();
$scrollbarYRail.remove();
2013-08-05 22:59:17 +08:00
// clean all variables
2014-04-27 18:43:21 +08:00
$scrollbarXRail =
$scrollbarYRail =
2013-08-05 22:59:17 +08:00
$scrollbarX =
$scrollbarY =
2014-04-27 18:43:21 +08:00
scrollbarXActive =
scrollbarYActive =
2013-08-05 22:59:17 +08:00
containerWidth =
containerHeight =
contentWidth =
contentHeight =
scrollbarXWidth =
scrollbarXLeft =
scrollbarXBottom =
2014-04-27 18:43:21 +08:00
isScrollbarXUsingBottom =
scrollbarXTop =
2013-08-05 22:59:17 +08:00
scrollbarYHeight =
scrollbarYTop =
2014-04-27 18:43:21 +08:00
scrollbarYRight =
isScrollbarYUsingRight =
scrollbarYLeft =
isRtl =
eventClassName = null;
}
var supportsTouch = (('ontouchstart' in window) || window.DocumentTouch && document instanceof window.DocumentTouch);
var supportsIePointer = window.navigator.msMaxTouchPoints !== null;
function initialize() {
updateGeometry();
bindScrollHandler();
bindMouseScrollXHandler();
bindMouseScrollYHandler();
bindRailClickHandler();
bindMouseWheelHandler();
if (supportsTouch || supportsIePointer) {
bindTouchHandler();
}
if (settings.useKeyboard) {
bindKeyboardHandler();
}
$this.data('perfect-scrollbar', $this);
$this.data('perfect-scrollbar-update', updateGeometry);
$this.data('perfect-scrollbar-destroy', destroy);
}
2013-05-18 11:41:15 +08:00
initialize();
2013-05-18 11:41:15 +08:00
return $this;
});
2013-05-18 11:41:15 +08:00
};
});