Scrolling The Selection Into View

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.

This entry was posted in Javascript and tagged , . Bookmark the permalink.

5 Responses to Scrolling The Selection Into View

  1. 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.

    • Roy Sharon says:

      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.

  2. frank says:

    Excellent snippet! Thanks a lot!

  3. friv says:

    I do not know if it’s just me or if everyone else experiencing problems with your site.

  4. Gil Leon says:

    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.)

Leave a Reply

Your email address will not be published. Required fields are marked *