When they're navigating through a long document, users often are confused or disoriented when they click on a link that jumps to another location in that same document.
Are they on the same page, or a different page? Should they scroll more from here? What's going on?
The answer to this problem is to scroll the user through the document to the linked location, as you can see from this demo. In this tutorial, we'll use a smattering of JavaScript to ensure that links that are internal to the document scroll users to their destinations, rather than jumping straight there and confusing site visitors.
First, we need to identify all the links in the document, and work out which of them are internal links. Getting a list of all the links is easy:
var allLinks = document.getElementsByTagName('a');
We need to walk through this list, and work out which of the links are internal. An internal link will have a hash (#) symbol in it, and it will point to the document we're currently looking at. The useful location object tells us about the URL of the current document, so try this:
for (var i=0;i<allLinks.length;i++) {
var lnk = allLinks[i];
if (
(
(lnk.href && lnk.href.indexOf('#') != -1) &&
(lnk.pathname == location.pathname)
) || (
('/'+lnk.pathname == location.pathname) ) &&
(lnk.search == location.search)
)
) {
DO SOMETHING WITH THE LINK HERE
}
}
The for loop walks through the list of links in the document; we check three things:
href property and the indexOf() function to find the location of one string in another.If the answer to each of these questions is yes, then we know the link is an internal one, and we can set it to scroll to its destination rather than simply navigate there.
|
Related Reading DHTML Utopia |
|
Now that we've identified an internal link, we want to scroll to it when it's clicked. To do this, we'll need to attach an onclick event handler to the link. In days of old, when web developers were bold, many thought that event handlers should be set on a link within the HTML:
<a href="http://www.sitepoint.com/" onclick="myEventHandler()">
This isn't really the truth; instead, you should attach an event listener to the link object. The W3C specifies a standard method to do this, as does Internet Explorer; Scott Andrew has usefully provided a function to handle both:
function addEvent(elm, evType, fn, useCapture)
// addEvent and removeEvent
// cross-browser event handling for IE5+, NS6 and Mozilla
// By Scott Andrew
{
if (elm.addEventListener){
elm.addEventListener(evType, fn, useCapture);
return true;
} else if (elm.attachEvent){
var r = elm.attachEvent("on"+evType, fn);
return r;
}
}
As we loop over the links, each matching link must have a new event handler attached to it for the "click" event. Then, when the link is clicked, it calls our smoothScroll() function instead of just navigating to the link as the browser normally would:
ss.addEvent(lnk,'click',ss.smoothScroll);
|
Of course, we have to have a smoothScroll() function, too. This is the complicated aspect, because it's all about finding an object's position on the page, and different browsers achieve this in various ways. The marvelous Andrew Clover has written a summary of the process that's used to find this position across browsers; we'll use this solution extensively here.
First, our smoothScroll() function is an event handler, so when it's called (that is, when a user clicks one of our internal links), we need to retrieve the link that was clicked. Netscape-class browsers pass an event object to each handler; Internet Explorer stores these details in the global window.event object.
if (window.event) {
target = window.event.srcElement;
} else if (e) {
target = e.target;
} else return;
This code sets the clicked link as the target in a cross-browser fashion. Well, nearly. Mozilla will sometimes pass you the text node within a link as the clicked item. We need to check whether or not the target is a text node (that is, whether its nodeType is 3), and take its parent if it is.
if (target.nodeType == 3) { target = target.parentNode; }
Just to be paranoid, we also check that what we've got is an a tag, in case we've missed something:
if (target.nodeName.toLowerCase() != 'a') return;
Now we need to find the destination: the <a name> tag that corresponds to the part after the hash in our clicked link. Links have a hash attribute that contains the # and the section that appears after it in the URL, so let's now walk through all of the links in the document and check whether their name attributes are equal to the hash part of the clicked link:
// First strip off the hash (first character)
anchor = target.hash.substr(1);
// Now loop all A tags until we find one with that name
var allLinks = document.getElementsByTagName('a');
var destinationLink = null;
for (var i=0;i<allLinks.length;i++) {
var lnk = allLinks[i];
if (lnk.name && (lnk.name == anchor)) {
destinationLink = lnk;
break;
}
}
// If we didn't find a destination, give up and let the browser do
// its thing
if (!destinationLink) return true;
|
We know what we clicked, and what that points to. Now, all we need to know is our location within the document, and what our destination is. This is where Andy Clover's notes are invaluable. First, we find the position of the destination link:
var destx = destinationLink.offsetLeft;
var desty = destinationLink.offsetTop;
var thisNode = destinationLink;
while (thisNode.offsetParent &&
(thisNode.offsetParent != document.body)) {
thisNode = thisNode.offsetParent;
destx += thisNode.offsetLeft;
desty += thisNode.offsetTop;
}
Note that we loop through offsetParents until we get to the document body, as IE requires. Next, we work out where we are currently located:
function getCurrentYPos() {
if (document.body && document.body.scrollTop)
return document.body.scrollTop;
if (document.documentElement && document.documentElement.scrollTop)
return document.documentElement.scrollTop;
if (window.pageYOffset)
return window.pageYOffset;
return 0;
}
IE5 and 5.5 store the current position in document.body.scrollTop, IE6 in document.documentElement.scrollTop, and Netscape-class browsers in window.pageYOffset. Phew!
The way we handle the scrolling is to use setInterval(); this very useful function sets up a repeating timer that fires a function of our choice. In this case, we'll have our function move the browser's position one step closer to the destination; setInterval() will call our function repeatedly and, when we reach the destination, we'll cancel the timer.
First, use clearInterval() to turn off any timers that are currently running:
clearInterval(ss.INTERVAL);
ss.INTERVAL is a variable in which we will later store the ouput of setInterval(). Next, we must work out how big each step should be:
ss_stepsize = parseInt((desty-cypos)/ss.STEPS);
ss.STEPS is defined in the script to be the number of steps we take from target to destination. Our "scroll one step" function is called ss.scrollWindow and takes three parameters:
We need to construct a call to this in a string, and pass that string to setInterval, along with the frequency with which we want the call repeated:
ss.INTERVAL =
setInterval('ss.scrollWindow('+ss_stepsize+','+desty+',"'+anchor+'")',10);
Notice how we're building up a string that's a call to ss.scrollWindow(), rather than just calling ss.scrollWindow() directly. This is one of the most confusing things about setInterval().
Once we've done that, we have to stop the browser from taking its normal course by obeying the link and jumping directly to the destination. Again, this process occurs differently in different browsers. To stop the browser handling this event normally in Internet Explorer, use the following code:
if (window.event) {
window.event.cancelBubble = true;
window.event.returnValue = false;
}
Notice the check for window.event, which ensures that we're using IE.
To do the same in Netscape-class browsers, use this code:
if (e && e.preventDefault && e.stopPropagation) {
e.preventDefault();
e.stopPropagation();
}
|
One last thing: how do we actually do the scrolling? The key function here is window.scrollTo(), to which you pass X and Y positions; the browser then scrolls the window to that position. One minor wrinkle is that you can't scroll all the way to the bottom. If the Y position you pass in is less than a window's height from the bottom of the document, the browser will scroll down only as far as it can--obviously, it can't go right down to the link if the distance to the bottom of the page is less than the height of the window.
Now, we need to check for that; the best way to do so is to see whether the positions before and after the scroll are the same:
function scrollWindow(scramount,dest,anchor) {
wascypos = ss.getCurrentYPos();
isAbove = (wascypos < dest);
window.scrollTo(0,wascypos + scramount);
iscypos = ss.getCurrentYPos();
isAboveNow = (iscypos < dest);
if ((isAbove != isAboveNow) || (wascypos == iscypos)) {
// if we've just scrolled past the destination, or
// we haven't moved from the last scroll (i.e., we're at the
// bottom of the page) then scroll exactly to the link
window.scrollTo(0,dest);
// cancel the repeating timer
clearInterval(ss_INTERVAL);
// and jump to the link directly so the URL's right
location.hash = anchor;
}
}
Note that, because we scroll in specific integral increments, this step might have taken us past our destination. Thus, we check whether we were above the link before and after the scroll; if these two locations are different, we've scrolled past the link, and as such, we've finished. If we're finished, we cancel the timer and set the page's URL (by setting a bit of the location object) so that it looks as if the browser had handled the link.
To apply this effect to a page, simply put the JavaScript code in a file called smoothscroll.js and include that file in your HTML page's HEAD as follows:
<script src="smoothscroll.js" type="text/javascript"></script>
That's all you need to do. This approach follows the principles of unobtrusive DHTML, making it easy for everyone to use. The script starts itself up when the page is loaded, by attaching itself to the page's "load" event:
ss.addEvent(window,"load",ss.fixAllLinks);
|
The complete code looks like this:
/* Smooth scrolling
Changes links that link to other parts of this page to scroll
smoothly to those links rather than jump to them directly, which
can be a little disorienting.
sil, http://www.kryogenix.org/
v1.0 2003-11-11
v1.1 2005-06-16 wrap it up in an object
*/
var ss = {
fixAllLinks: function() {
// Get a list of all links in the page
var allLinks = document.getElementsByTagName('a');
// Walk through the list
for (var i=0;i<allLinks.length;i++) {
var lnk = allLinks[i];
if ((lnk.href && lnk.href.indexOf('#') != -1) &&
( (lnk.pathname == location.pathname) ||
('/'+lnk.pathname == location.pathname) ) &&
(lnk.search == location.search)) {
// If the link is internal to the page (begins in #)
// then attach the smoothScroll function as an onclick
// event handler
ss.addEvent(lnk,'click',ss.smoothScroll);
}
}
},
smoothScroll: function(e) {
// This is an event handler; get the clicked on element,
// in a cross-browser fashion
if (window.event) {
target = window.event.srcElement;
} else if (e) {
target = e.target;
} else return;
// Make sure that the target is an element, not a text node
// within an element
if (target.nodeType == 3) {
target = target.parentNode;
}
// Paranoia; check this is an A tag
if (target.nodeName.toLowerCase() != 'a') return;
// Find the <a name> tag corresponding to this href
// First strip off the hash (first character)
anchor = target.hash.substr(1);
// Now loop all A tags until we find one with that name
var allLinks = document.getElementsByTagName('a');
var destinationLink = null;
for (var i=0;i<allLinks.length;i++) {
var lnk = allLinks[i];
if (lnk.name && (lnk.name == anchor)) {
destinationLink = lnk;
break;
}
}
// If we didn't find a destination, give up and let the browser do
// its thing
if (!destinationLink) return true;
// Find the destination's position
var destx = destinationLink.offsetLeft;
var desty = destinationLink.offsetTop;
var thisNode = destinationLink;
while (thisNode.offsetParent &&
(thisNode.offsetParent != document.body)) {
thisNode = thisNode.offsetParent;
destx += thisNode.offsetLeft;
desty += thisNode.offsetTop;
}
// Stop any current scrolling
clearInterval(ss.INTERVAL);
cypos = ss.getCurrentYPos();
ss_stepsize = parseInt((desty-cypos)/ss.STEPS);
ss.INTERVAL =
setInterval('ss.scrollWindow('+ss_stepsize+','+desty+',"'+anchor+'")',10);
// And stop the actual click happening
if (window.event) {
window.event.cancelBubble = true;
window.event.returnValue = false;
}
if (e && e.preventDefault && e.stopPropagation) {
e.preventDefault();
e.stopPropagation();
}
},
scrollWindow: function(scramount,dest,anchor) {
wascypos = ss.getCurrentYPos();
isAbove = (wascypos < dest);
window.scrollTo(0,wascypos + scramount);
iscypos = ss.getCurrentYPos();
isAboveNow = (iscypos < dest);
if ((isAbove != isAboveNow) || (wascypos == iscypos)) {
// if we've just scrolled past the destination, or
// we haven't moved from the last scroll (i.e., we're at the
// bottom of the page) then scroll exactly to the link
window.scrollTo(0,dest);
// cancel the repeating timer
clearInterval(ss.INTERVAL);
// and jump to the link directly so the URL's right
location.hash = anchor;
}
},
getCurrentYPos: function() {
if (document.body && document.body.scrollTop)
return document.body.scrollTop;
if (document.documentElement && document.documentElement.scrollTop)
return document.documentElement.scrollTop;
if (window.pageYOffset)
return window.pageYOffset;
return 0;
},
addEvent: function(elm, evType, fn, useCapture) {
// addEvent and removeEvent
// cross-browser event handling for IE5+, NS6 and Mozilla
// By Scott Andrew
if (elm.addEventListener){
elm.addEventListener(evType, fn, useCapture);
return true;
} else if (elm.attachEvent){
var r = elm.attachEvent("on"+evType, fn);
return r;
} else {
alert("Handler could not be removed");
}
}
}
ss.STEPS = 25;
ss.addEvent(window,"load",ss.fixAllLinks);
Your document's internal links will scroll to their destination, allowing your users to retain an awareness of the browser's location within the document, and how far they are from their starting point.
Stuart Langridge is an information architect for a law firm in the UK. He writes about JavaScript and DHTML, as well as pretty much anything else that catches his attention, at www.kryogenix.org.
Return to the O'Reilly Network
Copyright © 2007 O'Reilly Media, Inc.