/**
 * Custom scrollbar.
 * Version: 2.1
 *
 * The use of this script is strictly forbidden without prior written
 * consent. You must obtain permission before copying, modifying, 
 * redistributing, deriving or selling any part of this program.
 *
 * Copyright © 2006 HYPERZOID
 * All Rights Reserved
 */

// The scrollbar component ID's.
var DOWN_ARROW_ID = "downArrow";
var CONTENT_ID    = "scrollbarContent";
var SLIDER_ID     = "slider";
var TRACK_ID      = "track";
var UP_ARROW_ID   = "upArrow";
var VIEW_PORT_ID  = "viewPort";

// The height of the slider (only needed if slider height is set to fixed).
var FIXED_SLIDER_HEIGHT = 13;

// Number of pixels to vertically shift the content for each arrow scroll event.
var SCROLL_UP   = 8;
var SCROLL_DOWN = -12;

// Number of pixels to vertically shift the content for each track jump event.
var JUMP = 25;

// The number of pixels to pad the slider (to prevent the track and slider from overlapping).
var PADDING = 4;

// Number of milliseconds to wait before triggering a scroll event.
var WAIT_DURATION = 30;

// The scrollbar component objects;
var content;
var downArrow;
var slider;
var track;
var upArrow;
var viewPort;

// The scrollbar component heights.
var contentHeight;
var sliderHeight;
var trackHeight;
var viewPortHeight;

// Whether the height of the slider is fixed or relative to content length.
var isFixedSlider = true;

// The cursor's offset when the slider gets selected.
// It is relative to the top of the screen.
var cursorOffset;

// Whether the slider is currently being dragged.
var isDragged;

// The slider's top offset when it gets selected.
// It is relative to the top of the track.
var sliderOffset;

// Timer for triggering scroll events.
var timer;


/**
 * Add event listeners for an arrow or track.
 * @param object The arrow or track object to add listeners for.
 * @param amount The number of pixels to vertically shift
 *               the content when the handler event fires.
 */
function addListeners(obj, amount) {
    // Add OnMouseDown event listeners.
    if (obj.id == UP_ARROW_ID || obj.id == DOWN_ARROW_ID) {
        obj.onmousedown = 
            function() {
                timer = setInterval("arrowScroll(" + amount + ")", WAIT_DURATION);
            };
    } else {
        obj.onmousedown = selectTrack; 
    }

    // Add OnMouseUp and OnMouseOut event listeners.    
    obj.onmouseup =
        function() {
            if (timer != null) {
                clearInterval(timer);
                timer = null;
            }
        };
    obj.onmouseout = obj.onmouseup;
}


/**
 * Event handler for the up and down arrows.
 * @param amount The number of pixels to scroll vertically.
 */
function arrowScroll(amount) {
    // Update the content position.
    setContentPosition(amount + content.offsetTop);
   
    // Update the slider position.
    var ratio = Math.abs(content.offsetTop) / (contentHeight - viewPortHeight);
    setSliderPosition(ratio * (trackHeight - sliderHeight));
}


/**
 * Gets the position of an element.
 * @param element The element to get the position for.
 * @return The top offset of the element, relative to the view port.
 */
function getElementPosition(element) {
    var offsetTop = 0;
    var item = element;
    while (item != null) {
        offsetTop += eval('item.offsetTop');
        item = eval('item.offsetParent');
    }
    return offsetTop;
}


/**
 * Gets the browser-specific event object.
 * @param event The event that was triggered.
 * @return The event object for this particular browser.
 */
function getEvent(event) {
    if (window.event) { // IE event.
        return window.event;
    }
    return event;
}

/**
 * Gets the element that triggered the event.
 * @param event The event that was triggered.
 * @return The object that triggered the event.
 */
function getEventElement(event) {
    if (window.event) { // IE event.
        return event.srcElement;
    }
    return event.target;
}


/**
 * Initializes the scrollbar.
 */
function initialize() {
    // Get the scrollbar component objects.
    content   = document.getElementById(CONTENT_ID);
    downArrow = document.getElementById(DOWN_ARROW_ID);
    slider    = document.getElementById(SLIDER_ID);
    track     = document.getElementById(TRACK_ID);
    upArrow   = document.getElementById(UP_ARROW_ID);
    viewPort  = document.getElementById(VIEW_PORT_ID);
    
    // Get the scrollbar component heights.
    contentHeight  = content.clientHeight;
    trackHeight    = track.clientHeight;
    viewPortHeight = viewPort.clientHeight;

    // Don't enable components if the content is not long enough to require a scrollbar.
    if (viewPortHeight >= contentHeight) {
        return;
    }

    // Compute the size of the slider relative to the content height.
    if (!isFixedSlider) {
        slider.style.height = (viewPortHeight * trackHeight) / contentHeight + "px";
        sliderHeight = parseInt(slider.style.height);
    } else {
        slider.style.height = FIXED_SLIDER_HEIGHT + "px";
        sliderHeight = FIXED_SLIDER_HEIGHT;
    }
    slider.style.top = 0 + "px";

    // Add action listeners to scrollbar components.
    addListeners(upArrow, SCROLL_UP);
    addListeners(downArrow, SCROLL_DOWN);
    addListeners(track, JUMP);
    isDragged = false;    
    slider.onmouseup = function() { isDragged = false; };
    slider.onmousedown = selectSlider;
}


/**
 * Jumps to an element in the content.
 * @param id The ID of the element to position at the top of the view port.
 */
function jump(id) {
    var top = -1 * document.getElementById(id).offsetTop;

    // Update the content position.
    setContentPosition(top);
   
    // Update the slider position.
    var ratio = Math.abs(content.offsetTop) / (contentHeight - viewPortHeight);
    setSliderPosition(ratio * (trackHeight - sliderHeight));
}


/**
 * OnMouseDown event handler for the slider.
 * @param event The OnMouseDown event that triggered this function.
 */
function selectSlider(event) {
    // Make sure the event originated from the slider.
    event = getEvent(event);
    if (getEventElement(event).id != SLIDER_ID) {
        return;
    }

    // Prepare to drag the slider.
    isDragged = true;
    sliderOffset = parseInt(slider.style.top);
    cursorOffset = event.clientY;
    if (event.preventDefault) {
        event.preventDefault();
    }
    document.onmousemove = sliderScroll;
}


/**
 * OnMouseDown event handler for the track.
 * @param event The OnMouseDown event that triggered this function.
 */
function selectTrack(event) {
    // Make sure the event originated from the track.
    event = getEvent(event);
    if (getEventElement(event).id != TRACK_ID) {
        return;
    }

    var top = getElementPosition(slider); // The current slider position.

    // Figure out whether to jump up or down.
    var jump;
    if (event.clientY > top + sliderHeight) { // Jump down.
        jump = JUMP;
    } else if (event.clientY < top) { // Jump up.
        jump = -1 * JUMP;
    }

    timer = setInterval("trackJump(" + jump + ", " + event.clientY + ")", WAIT_DURATION);
}


/**
 * Sets the content position.
 * @param top The top offset of the content, relative to the view port.
 */
function setContentPosition(top) {
    // Don't let content's edge scroll past that of the view port.
    if (top > 0) {
        top = 0;
    } else if (top < viewPortHeight - contentHeight) {
        top = viewPortHeight - contentHeight;
    }
    content.style.top = top + "px";
}


/**
 * Sets the slider position.
 * The content position is updated accordingly.
 * @param top The top offset of the slider relative to the track.
 */
function setSliderPosition(top) {
    // Don't let the slider move beyond the track.
    if (top < 0) {
        top = 0;
    } else if (top > trackHeight - sliderHeight - PADDING) {
        top = trackHeight - sliderHeight - PADDING;
    }
    slider.style.top = top + "px";  
}


/**
 * OnMouseMove event handler for the slider.
 * @param event The OnMouseMove event that triggered this function.
 */
function sliderScroll(event) {
    // Ignore OnMouseMove events if the slider is not selected.
    if (isDragged == false) {
        return;
    }

    // The new slider position.
    var top = sliderOffset + getEvent(event).clientY - cursorOffset;

    // Update the content position.
    setContentPosition((viewPortHeight - contentHeight) * 
                       (top / (trackHeight - sliderHeight)));

    // Update the slider position.
    setSliderPosition(top);
    return false;
}


/**
 * Timer event handler for the track.
 * @param amount The number of pixels to scroll vertically.
 * @param y The cursor's y-coordinate on the track.
 */
function trackJump(amount, y) {
    // Don't let the slider jump past the cursor.
    var position = getElementPosition(slider); // The current slider position.
    if ((amount > 0 && position + sliderHeight + amount >= y) ||
        (amount < 0 && position + amount <= y)) {
        clearInterval(timer);
        timer = null;
    }

    // The new slider position.
    var top = parseInt(slider.style.top) + amount;

    // Update the content position.
    setContentPosition((viewPortHeight - contentHeight) * 
                       (top / (trackHeight - sliderHeight)));

    // Update the slider.
    setSliderPosition(top);
}

