Smooth scroll when clicking in-page links

By Johan Sundström Last update Oct 27, 2005 — Installed 7,185 times. Daily Installs: 2, 2, 4, 2, 10, 1, 2, 7, 2, 0, 7, 4, 2, 5, 6, 3, 1, 2, 7, 0, 2, 0, 0, 4, 3, 4, 1, 7, 1, 2, 1, 6
// ==UserScript==
// @name          Smooth scroll when clicking in-page links
// @namespace     http://www.lysator.liu.se/~jhs/userscript
// @description	  When clicking a link in a page that moves focus to another part of the same page, this script will make the page scroll, rather than just snap into view. This gives a somewhat better perception of how the in-page links have moved you in the document.
// ==/UserScript==

smoothScrollInpageLinks();

// Smooth scroll between in-page links; idea originating from:
// http://www.sitepoint.com/article/scroll-smoothly-javascript
var ss_STEPS, ss_DELTA;

function smoothScrollInpageLinks( steps, delta )
{
  ss_STEPS = steps || ss_STEPS || 15;
  ss_DELTA = delta || ss_DELTA || 10;
  for( var i=0; i<document.links.length; i++ )
  {
    var link = document.links[i];
    if( link.hash && isSameDocument( link ) )
      addEvent( link, 'click', smoothScroll );
  }
}

function makeScrollTo( x, y )
{
  return function(){ window.scrollTo( x, y ); };
}

function makeGoto( anchor )
{
  return function() { location.hash = anchor; };
}

function smoothScroll( e )
{
  var target = window.event ? window.event.srcElement : e && e.target;
  if( !target ) return true;
  var anchor = target.hash.substr(1);
  if( !(target = document.getElementById( anchor ) ||
		 document.anchors.namedItem( anchor )) ) return true;
  var src = getScreenCoordinates();
  var dst = getScreenCoordinates( target );
  if( src.y == dst.y ) return true;

  for( var i=1; i<ss_STEPS; i++ )
  {
    var percent = i / ss_STEPS;
    var smooth = (1-Math.cos( Math.PI * percent )) / 2;
    var x = parseInt( src.x + (     0-src.x) * smooth );
    var y = parseInt( src.y + ( dst.y-src.y) * smooth );
    setTimeout( makeScrollTo( x, y ), ss_DELTA * i );
  }
  setTimeout( makeGoto( anchor ), ss_DELTA * ss_STEPS );

  // stop the browser handling the event as normal and opening the link
  // kills the bubble
  if( window.event )
  {
    window.event.cancelBubble = true;
    window.event.returnValue = false;
  }
  if( e && e.preventDefault && e.stopPropagation )
  {
    // hmmm, safari gets inside here, but then fails to stop the event, and so
    // a 'jump' rather than a 'scroll' takes place. I can't figure it out...
    e.preventDefault();
    e.stopPropagation();
  }
  return true;
}

function isSameDocument( a, b )
{
  if( !b )
    if( a.href == a.hash )
      return true;
    else
      b = location;
  if( b.protocol.toLowerCase() != a.protocol.toLowerCase() )
    return false;
  if( b.hostname.toLowerCase() != a.hostname.toLowerCase() )
    return false;
  if( b.search != a.search )
    return false;
  if( (b.pathname != a.pathname ) &&
      (b.pathname != '/'+a.pathname) )
    return false;
  return true;
}

function addEvent( node, eventType, callback, useCapture )
{
  if( node.addEventListener )
    return !node.addEventListener( eventType, callback, useCapture ) || true;
  if( node.attachEvent )
    return node.attachEvent( 'on'+ eventType, callback );
}

// Returns the screen coordinates for NODE in pixels from the window viewport.
// If no node is passed, returns the scroll position of the viewport itself.
function getScreenCoordinates( node )
{
  var x, y;
  if( !node ) // get current scroll position of viewport
  {
    y = (document.body && document.body.scrollTop) || // ie5(|.5)
      (document.documentElement && document.documentElement.scrollTop) ||// ie6
      window.pageYOffset || 0; // others
    x = (document.body && document.body.scrollLeft) ||
      (document.documentElement && document.documentElement.scrollLeft) ||
      window.pageXOffset || 0;
  }
  else
  {
    x = node.offsetLeft;
    y = node.offsetTop;
    while( (node = node.offsetParent) && (node != document.body) )
    {
      x += node.offsetLeft;
      y += node.offsetTop;
    }
  }
  return { x:x, y:y };
}