Scrolling Text Nodes And Ranges Into View In A Cross-Browser Manner
Scrolling an HTML element into view can easily be achieved using the scrollIntoView() function. But what about text nodes, ranges and selections?
var o = document.getElementById('foo'); o.scrollIntoView();
The scrollIntoView() function is required by the W3C CSSOM View specification, and is widely supported on all modern browsers. However, the W3C specification says nothing about the scrolling of non-elements, such as text nodes.
var div = document.createElement('DIV'); div.innerHtml = 'foo'; var o = div.firstChild; // o is now a TextNode alert(o.nodeValue); // displays "foo" o.scrollIntoView(); // fails, because TextNodes do not support it
If we want to scroll the content of the div into view, we can call div.scrollIntoView(), which should work just fine. However, if we make this call and the div’s content is longer than the window’s height, the window scroll will align the top of the div with the top of the window, but will cause the bottom of the div to be hidden.
It seems that there is no easy way to scroll a TextNode into view. There is no way of finding out the actual position of a TextNode (no offsetTop/offsetLeft properties, and no getBoundingClientRect() method), so we have no way of knowing if it is visible to the user or not.
Solution
I have played with this a bit and come up with a relatively simple method for achieving reasonable results. It doesn’t always work, but in most cases in does do the job:
function scrollIntoView(t) { if (typeof(t) != 'object') return; // if t is not an element node, we need to skip back until we find the // previous element with which we can call scrollIntoView() o = t; while (o && o.nodeType != 1) o = o.previousSibling; t = o || t.parentNode; if (t) t.scrollIntoView(); }
The nodeType property tells us whether the supplied node is an element node (nodeType=1) or a different type of node (typically a text node, with nodeType=3). In the latter case, we try to move backwards through the document hierarchy in order to find the closest element node. When we find this element, we can use it to call scrollIntoView().
Scrolling The Selection Into View
The Selection object is composed of a collection of Range objects. A Range object may span over several sibling nodes. To be able to scroll the selection into view, we need to get hold of one of its ranges (typically the first one) and collapse it to its starting point, so as to achieve a single node. We then use the same technique to scroll this node into view:
function scrollIntoView(t) { if (typeof(t) != 'object') return; if (t.getRangeAt) { // we have a Selection object if (t.rangeCount == 0) return; t = t.getRangeAt(0); } if (t.cloneRange) { // we have a Range object var r = t.cloneRange(); // do not modify the source range r.collapse(true); // collapse to start var t = r.startContainer; // if start is an element, then startOffset is the child number // in which the range starts if (t.nodeType == 1) t = t.childNodes[r.startOffset]; } // if t is not an element node, then we need to skip back until we find the // previous element with which we can call scrollIntoView() o = t; while (o && o.nodeType != 1) o = o.previousSibling; t = o || t.parentNode; if (t) t.scrollIntoView(); }
You can call it with a Selection object, a Range object, an Element or a TextNode, and it will usually do the job and scroll the given item into view:
scrollIntoView(window.getSelection()); // Selection scrollIntoView(window.getSelection().getRangeAt(0)); // Range scrollIntoView(document.getElementById('foo')); // Element scrollIntoView(document.getElementsByTagName('SPAN')[0].firstChild); // TextNode
As I mentioned earlier, this usually works – but not always. I have found that it doesn’t work correctly when the TextNode is very long and not split up by any elements (even not BRs). However, this is a relatively rare case. If you use even some formatting in your content, you will probably do just fine.
You are welcome to download the scrollIntoView.js file with the complete implementation. Note that it includes one feature not mentioned here — the handling of BR elements. If the selection is a BR element, then in some browsers, the scrollIntoView() implementation listed above will scroll past the BR . I have added special handling for such cases in the final function in this file.
A more reliable solution exists, although more complex. In IE<9 selection ranges support scrollIntoView:
document.selection.createRange().scrollIntoView();
Other browsers support range.getBoundingClientRect method. It returns top and bottom position of the current selection in pixels relative to the top left corner of the window:
window.getSelection().getRangeAt(0).getBoundingClientRect().top
Then you can set scrollTop to scroll to that position. But it gets more complex if you have several nested scrolled elements.
This is indeed a viable alternative. As John Resig says, getBoundingClientRect is awesome. However, for the purpose of scrolling a selection into view, the scrollIntoView method is preferable in my opinion, as it takes care of all the nested scrolled elements.
As far as I know (and as far as quirksmode says), scrollIntoView is fully supported by all browsers, including some very old ones.
Excellent snippet! Thanks a lot!
You can handle the long TextNode (such as a pre, in my case) by calling range.getBoundingClientRect() and using the top property returned to calculate the offset you need for scrolling. (Works in Chrome, anyway.)
So an alternative method would seem to be to create an empty span using the insertNode method and then scroll this into view. One could clean up this new element by modifying the dom after use.
This has potential benefits when you have large blocks of text with no elements (I’m dealing with plain text documents loaded into a pre) – it remains to be seen if this approach works here :/ since in the text language pre’s tend to not contain subelements – yet the dom would appear to allow one to create such elements.