However, I found out the plugin could easily be modified to handle multiple elements; see code below. There is still a limitation though, all elements must reside in the same scrollable element, and only the closest scrollable element will be affected. But I think that's a fair limitation, as I don’t care much of interfaces with nested scrolling panels!
/*!
* jQuery scrollIntoView() plugin and :scrollable selector filter
*
* Original code (v1.8) by Robert Koritnik, see:
* http://erraticdev.blogspot.com/2011/02/jquery-scroll-into-view-plugin-with.html
*
* This is a simple adaptation to handle multiple elements. Documentation at:
* http://regularwebmaster.blogspot.com/2011/11/scrollintoview-revised.html
*
* Version 1.8b (1 Nov 2011)
* Requires jQuery 1.4 or newer
*
* Copyright (c) 2011 Robert Koritnik
* modifications by Martin Boström
*
* Licensed under the terms of the MIT license
* http://www.opensource.org/licenses/mit-license.php
*
*
*/
(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"
};
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
};
};
// Calculates the union of two dimension objects, stores result in lhs:
var union = function (lhs, rhs) {
for(var key in lhs) {
for(var property in lhs[key]) {
if(property == 'bottom' || property == 'right')
lhs[key][property] = Math.max(lhs[key][property], rhs[key][property]);
else
lhs[key][property] = Math.min(lhs[key][property], rhs[key][property]);
}
}
}
// Gets the extreme dimensions of elements in $scope:
var dimensions = function ($scope) {
var win = $(window);
var dim = {
border: { top: Number.MAX_VALUE, left: Number.MAX_VALUE, bottom: Number.MIN_VALUE, right: Number.MIN_VALUE },
scroll: { top: Number.MAX_VALUE, left: Number.MAX_VALUE },
scrollbar: { right: Number.MIN_VALUE, bottom: Number.MIN_VALUE },
rect: { top: Number.MAX_VALUE, left: Number.MAX_VALUE, bottom: Number.MIN_VALUE, right: Number.MIN_VALUE }
};
$scope.each(function() {
var $element = $(this);
var isRoot = rootrx.test($element[0].nodeName);
union(dim, (function() {
return {
border: isRoot ? { top: 0, left: 0, bottom: 0, right: 0} : borders($element[0]),
scroll: {
top: (isRoot ? win : $element).scrollTop(),
left: (isRoot ? win : $element).scrollLeft()
},
scrollbar: {
right: isRoot ? 0 : $element.innerWidth() - $element[0].clientWidth,
bottom: isRoot ? 0 : $element.innerHeight() - $element[0].clientHeight
},
rect: (function () {
var r = $element[0].getBoundingClientRect();
return {
top: isRoot ? 0 : r.top,
left: isRoot ? 0 : r.left,
bottom: isRoot ? $element[0].clientHeight : r.bottom,
right: isRoot ? $element[0].clientWidth : r.right
};
})()
};
})())});
return dim;
};
$.fn.extend({
scrollIntoView: function (options) {
/// Scrolls the first element in the set into view by scrolling its closest scrollable parent.
/// 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)
/// /// Returns the same jQuery set that this function was run on.
options = $.extend({}, settings, options);
options.direction = converter[typeof (options.direction) === "string" && options.direction.toLowerCase()] || converter.both;
var dirStr = "";
if (options.direction.x === true) dirStr = "horizontal";
if (options.direction.y === true) dirStr = dirStr ? "both" : "vertical";
var scroller = this.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(this),
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 animOptions = {};
// vertical scroll
if (options.direction.y === true)
{
if (rel.top < 0)
{
animOptions.scrollTop = dim.s.scroll.top + rel.top;
}
else if (rel.top > 0 && rel.bottom < 0)
{
animOptions.scrollTop = dim.s.scroll.top + Math.min(rel.top, -rel.bottom);
}
}
// horizontal scroll
if (options.direction.x === true)
{
if (rel.left < 0)
{
animOptions.scrollLeft = dim.s.scroll.left + rel.left;
}
else if (rel.left > 0 && rel.right < 0)
{
animOptions.scrollLeft = dim.s.scroll.left + Math.min(rel.left, -rel.right);
}
}
// scroll if needed
if (!$.isEmptyObject(animOptions))
{
if (rootrx.test(scroller[0].nodeName))
{
scroller = $("html,body");
}
scroller
.animate(animOptions, options.duration)
.eq(0) // we want function to be called just once (ref. "html,body")
.queue(function (next) {
$.isFunction(options.complete) && options.complete.call(scroller[0]);
next();
});
}
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
};
$.extend($.expr[":"], {
scrollable: function (element, index, meta, stack) {
var direction = converter[typeof (meta[3]) === "string" && meta[3].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();
}
});
})(jQuery);
The changes from the original code are found in the dimensions() function, now capable of calculating the union of dimensions of multiple elements, and the new helper function union(). Some notes about usability. The purpose of scrollIntoView() is to allow an improved user experience by scrolling elements of interest, e.g. the "current clip" in a play list, into view if necessary. By applying a smooth scroll rather that having the page or scrolling panel just jumping up and down, the user is less likely to loose track of what is going on. When designing my foldingParagraphs() plugin (have content between headings hidden by default, and revealed when clicking corresponding headings), I found that besides scrolling the content into view; the heading itself must never leave the view as that would obstruct the reading. Additionally the (folded) heading following the content should, if possible, be revealed as well to help the user to go on reading the next section without needing to fiddle with the scrollbar. So, I throw the clicked heading, the content, and the optional heading following the content, to the tigers - sorry - the slightly modified scrollIntoView plugin, and that’s it.

Thanks for sharing! This is a very nice addition to the original plugin. I have a table that acts like an 'accordion' (a row with extra content is opened below a table row when it is selected via keyboard or mouse click) and I can now get two rows to remain in view.
ReplyDelete