jquery.scrollintoview
Version:
jQuery scrollintoview() plugin and :scrollable selector filter
241 lines (211 loc) • 10.2 kB
JavaScript
/*!
* jQuery scrollintoview() plugin and :scrollable selector filter
*
* Version 1.9.4 (06 April 2016)
* Requires jQuery 1.4 or newer
*
* Copyright (c) 2011 Robert Koritnik
* Licensed under the terms of the MIT license
* http://www.opensource.org/licenses/mit-license.php
*/
!function(root, factory) {
if (typeof define === 'function' && define.amd) {
define(['jquery'], factory);
} else if (typeof exports === 'object') {
factory(require('jquery'));
} else {
factory(root.jQuery);
}
}
(this, function($) {
var converter = {
vertical: { x: false, y: true },
horizontal: { x: true, y: false },
both: { x: true, y: true },
x: { x: true, y: false },
y: { x: false, y: true }
};
var settings = {
duration: "fast",
direction: "both",
viewPadding: 0
};
var rootrx = /^(?:html)$/i;
// gets border dimensions
var borders = function(domElement, styles) {
styles = styles || (document.defaultView && document.defaultView.getComputedStyle ? document.defaultView.getComputedStyle(domElement, null) : domElement.currentStyle);
var px = document.defaultView && document.defaultView.getComputedStyle ? true : false;
var b = {
top: (parseFloat(px ? styles.borderTopWidth : $.css(domElement, "borderTopWidth")) || 0),
left: (parseFloat(px ? styles.borderLeftWidth : $.css(domElement, "borderLeftWidth")) || 0),
bottom: (parseFloat(px ? styles.borderBottomWidth : $.css(domElement, "borderBottomWidth")) || 0),
right: (parseFloat(px ? styles.borderRightWidth : $.css(domElement, "borderRightWidth")) || 0)
};
return {
top: b.top,
left: b.left,
bottom: b.bottom,
right: b.right,
vertical: b.top + b.bottom,
horizontal: b.left + b.right
};
};
var dimensions = function($element) {
var elem = $element[0],
isRoot = rootrx.test(elem.nodeName),
$elem = isRoot ? $(window) : $element;
return {
border: isRoot ? { top: 0, left: 0, bottom: 0, right: 0 } : borders(elem),
scroll: {
top: $elem.scrollTop(),
left: $elem.scrollLeft(),
maxtop: elem.scrollHeight - elem.clientHeight,
maxleft: elem.scrollWidth - elem.clientWidth
},
scrollbar: isRoot
? { right: 0, bottom: 0 }
: {
right: $elem.innerWidth() - elem.clientWidth,
bottom: $elem.innerHeight() - elem.clientHeight
},
rect: isRoot ? { top: 0, left: 0, bottom: elem.clientHeight, right: elem.clientWidth } : elem.getBoundingClientRect()
};
};
$.fn.extend({
scrollintoview: function(options) {
/// <summary>Scrolls the first element in the set into view by scrolling its closest scrollable parent.</summary>
/// <param name="options" type="Object">Additional options that can configure scrolling:
/// duration (default: "fast") - jQuery animation speed (can be a duration string or number of milliseconds)
/// direction (default: "both") - select possible scrollings ("vertical" or "y", "horizontal" or "x", "both")
/// complete (default: none) - a function to call when scrolling completes (called in context of the DOM element being scrolled)
/// </param>
/// <return type="jQuery">Returns the same jQuery set that this function was run on.</return>
options = $.extend({}, settings, options);
options.direction = converter[typeof (options.direction) === "string" && options.direction.toLowerCase()] || converter.both;
if (typeof options.viewPadding == "number") {
options.viewPadding = { x: options.viewPadding, y: options.viewPadding };
} else if (typeof options.viewPadding == "object") {
if (options.viewPadding.x == undefined) {
options.viewPadding.x = 0;
}
if (options.viewPadding.y == undefined) {
options.viewPadding.y = 0;
}
}
var dirStr = "";
if (options.direction.x === true) dirStr = "horizontal";
if (options.direction.y === true) dirStr = dirStr ? "both" : "vertical";
var el = this.eq(0);
var scroller = el.parent().closest(":scrollable(" + dirStr + ")");
// check if there's anything to scroll in the first place
if (scroller.length > 0) {
scroller = scroller.eq(0);
var dim = {
e: dimensions(el),
s: dimensions(scroller)
};
var rel = {
top: dim.e.rect.top - (dim.s.rect.top + dim.s.border.top),
bottom: dim.s.rect.bottom - dim.s.border.bottom - dim.s.scrollbar.bottom - dim.e.rect.bottom,
left: dim.e.rect.left - (dim.s.rect.left + dim.s.border.left),
right: dim.s.rect.right - dim.s.border.right - dim.s.scrollbar.right - dim.e.rect.right
};
var animProperties = {};
// vertical scroll
if (options.direction.y === true) {
if (rel.top < 0) {
animProperties.scrollTop = Math.max(0, dim.s.scroll.top + rel.top - options.viewPadding.y);
} else if (rel.top > 0 && rel.bottom < 0) {
animProperties.scrollTop = Math.min(dim.s.scroll.top + Math.min(rel.top, -rel.bottom) + options.viewPadding.y, dim.s.scroll.maxtop);
}
}
// horizontal scroll
if (options.direction.x === true) {
if (rel.left < 0) {
animProperties.scrollLeft = Math.max(0, dim.s.scroll.left + rel.left - options.viewPadding.x);
} else if (rel.left > 0 && rel.right < 0) {
animProperties.scrollLeft = Math.min(dim.s.scroll.left + Math.min(rel.left, -rel.right) + options.viewPadding.x, dim.s.scroll.maxleft);
}
}
// scroll if needed
if (!$.isEmptyObject(animProperties)) {
var scrollExpect = {},
scrollListener = scroller;
if (rootrx.test(scroller[0].nodeName)) {
scroller = $("html,body");
scrollListener = $(window);
}
function animateStep(now, tween) {
scrollExpect[tween.prop] = Math.floor(now);
};
function onscroll(event) {
$.each(scrollExpect, function(key, value) {
if (Math.floor(scrollListener[key]()) != Math.floor(value)) {
options.complete = null; // don't run complete function if the scrolling was interrupted
scroller.stop('scrollintoview');
}
});
}
scrollListener.on('scroll', onscroll);
scroller
.stop('scrollintoview')
.animate(animProperties, { duration: options.duration, step: animateStep, queue: 'scrollintoview' })
.eq(0) // we want function to be called just once (ref. "html,body")
.queue('scrollintoview', function(next) {
scrollListener.off('scroll', onscroll);
$.isFunction(options.complete) && options.complete.call(scroller[0]);
next();
})
scroller.dequeue('scrollintoview');
} else {
// when there's nothing to scroll, just call the "complete" function
$.isFunction(options.complete) && options.complete.call(scroller[0]);
}
}
// return set back
return this;
}
});
var scrollValue = {
auto: true,
scroll: true,
visible: false,
hidden: false
};
var scroll = function(element, direction) {
direction = converter[typeof (direction) === "string" && direction.toLowerCase()] || converter.both;
var styles = (document.defaultView && document.defaultView.getComputedStyle ? document.defaultView.getComputedStyle(element, null) : element.currentStyle);
var overflow = {
x: scrollValue[styles.overflowX.toLowerCase()] || false,
y: scrollValue[styles.overflowY.toLowerCase()] || false,
isRoot: rootrx.test(element.nodeName)
};
// check if completely unscrollable (exclude HTML element because it's special)
if (!overflow.x && !overflow.y && !overflow.isRoot) {
return false;
}
var size = {
height: {
scroll: element.scrollHeight,
client: element.clientHeight
},
width: {
scroll: element.scrollWidth,
client: element.clientWidth
},
// check overflow.x/y because iPad (and possibly other tablets) don't dislay scrollbars
scrollableX: function() {
return (overflow.x || overflow.isRoot) && this.width.scroll > this.width.client;
},
scrollableY: function() {
return (overflow.y || overflow.isRoot) && this.height.scroll > this.height.client;
}
};
return direction.y && size.scrollableY() || direction.x && size.scrollableX();
};
$.expr[":"].scrollable = $.expr.createPseudo(function(direction) {
return function(element) {
return scroll(element, direction);
};
});
});