UNPKG

jquery.scrollintoview

Version:

jQuery scrollintoview() plugin and :scrollable selector filter

241 lines (211 loc) 10.2 kB
/*! * 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); }; }); });