/* ------------------------------------------------------------------------ */
/* 
 *   Get the current scroll position in this document (the way of getting
 *   this information varies by browser).  
 */
function getScrollPos()
{
    // try the BODY element first
    var base;
    if ((base = document.body)
        && (base.scrollLeft || base.scrollTop))
        return { x: base.scrollLeft, y: base.scrollTop };

    // no good; try the document element
    if ((base = document.documentElement) != null
        && (base.scrollLeft || base.scrollTop))
        return { x: base.scrollLeft, y: base.scrollTop };

    // still couldn't find the position - use zeroes
    return { x: 0, y: 0 };
}

/* ------------------------------------------------------------------------ */
/*
 *   Get the current window rectangle 
 */
function getWindowRect()
{
    var wid = 1000000, ht = 1000000;

    // This is one of those things that varies a lot by browser.
    // Most browsers support window.innerWidth/Height, but of course
    // IE has its own weird way.  What's more, IE has two different
    // ways, depending on version.  And to make matters even worse,
    // the innerWidth/Height properties aren't even consistent across
    // the browsers that support them - some include the scrollbar
    // width in the size, some don't.  We don't want the scrollbars
    // to count because they're not part of the area we can use
    // for drawing.  The solution seems to be to try all of the
    // different methods, and use the smallest non-zero, non-null,
    // non-undefined result.  Each browser seems to have at least
    // one way getting the area sans scrollers, which is the smallest
    // of the various values we'd get.

    // try innerHeight on the window (this is the most common approach,
    // used by almost everyone except IE, but often includes the area
    // covered by scrollbars)
    var x = window.innerWidth, y = window.innerHeight;
    if (typeof(x) == "number" && x > 0)
    {
        wid = x;
        ht = y;
    }

    // try clientHeight on the document (this works on most versions of
    // IE for Windows, and also gets us the sans-scrollbar sizes on some
    // others)
    x = document.documentElement.clientWidth;
    y = document.documentElement.clientHeight;
    if (typeof(x) == "number" && x > 0)
    {
        // keep the smallest so far
        wid = Math.min(wid, x);
        ht = Math.min(ht, y);
    }

    // For the width, also try the clientWidth on the body (this works on
    // IE for Mac and some IE for Win, and gives us sans-scrollbar sizes
    // for many others). However, DON'T use this for the height unless
    // we don't have any height at all so far, since it might only tell
    // us the content height, not the window height.
    x = document.body.clientWidth;
    y = document.body.clientHeight;
    if (typeof(x) == "number" && x > 0)
    {
        wid = Math.min(wid, x);
        if (ht == 1000000)
            ht = y;
    }

    // return the result
    return { x: 0, y: 0, width: wid, height: ht };
}

/* ------------------------------------------------------------------------ */
/*
 *   get a document element by ID 
 */
function eleById(id)
{
    return document.getElementById(id);
}

/*
 *   get an element position and size 
 */
function getElePos(ele)
{
    var x = ele.offsetLeft, y = ele.offsetTop;
    var w = ele.offsetWidth, h = ele.offsetHeight;
    for (var par = ele.offsetParent ; par && par != document.body ;
         par = par.offsetParent)
    {
        x += par.offsetLeft;
        y += par.offsetTop;
    }
    return { x: x, y: y, w: w, h: h };
}

/* ------------------------------------------------------------------------ */
/*
 *   Add an event handler to an object 
 */
function addEventHandler(obj, eventName, func)
{
    if (obj.addEventListener)
        obj.addEventListener(eventName, func, false);
    else if (obj.attachEvent)
    {
        obj.attachEvent("on" + eventName, func);
        
        // save it in our private list as well
        if (!obj.$eventList)
            obj.$eventList = [];
        obj.$eventList.push([eventName, func]);
    }
}

/*
 *   get the coordinates of an event relative to the target element's
 *   position
 */
function getTargetRelativeRect(e)
{
    // get the event coordinates relative to the target object *
    return getObjectRelativeRect(e, getEventTarget(e));
}

/*
 *   get the coordinates of an event relative to the given object 
 */
function getObjectRelativeRect(e, obj)
{
    // get the real event
    e = getEvent(e);

    // get the event coordinates, in document coordinates
    var evtpos = getEventCoords(e);

    // get the object rectangle
    var objpos = getElePos(obj);

    // We have the event and target element positions now, both in the
    // same coordinate system (document-relative): so calculating the
    // event location relative to the target element is just a matter
    // of calculating the difference.  Return the target-relative x
    // position, the target-relative y position, the target width,
    // the target height, and the target object.
    return { x: evtpos.x - objpos.x, y: evtpos.y - objpos.y,
             w: objpos.w, h: objpos.h,
             obj: obj };
}

/*
 *   get the event object 
 */
function getEvent(e)
{
    return e || window.event;
}

/*
 *   get the event target element 
 */
function getEventTarget(e)
{
    // get the actual event, and get the target from the event
    e = getEvent(e);
    var t = e.target || e.srcElement;

    // this weirdness is a work-around for a bug in some Safari versions
    if (t && t.nodeType == 3)
        t = t.parentNode;

    // got it
    return t;
}

/*
 *   is the given event within the given object?
 */
function isEventInObject(e, obj)
{
    // get the object-relative event coordinates
    var r = getObjectRelativeRect(e, obj);

    // if x is between 0 and the width of the object, and y is
    // between 0 and the height of the object, we're within the object
    return (r.x >= 0
            && r.x <= r.w
            && r.y >= 0
            && r.y <= r.h);
}

/*
 *   get the document-relative coordinates of an event
 */
function getEventCoords(e)
{
    // get the actual event
    e = getEvent(e);

    // Some browsers use pageX/pageY, while others user clientX/clientY.
    var x, y;
    if (typeof(e.pageX) == 'number')
    {
        // We've got pageX and pageY, so this is easy - all browsers that
        // provide pageX and pageY use them consistently to mean
        // document-relative coordinates, so they tell us exactly what
        // we want to know.
        x = e.pageX;
        y = e.pageY;
    }
    else
    {
        // We don't have pageX/pageY, so we'll have to fall back on clientX
        // and clientY...
        x = e.clientX;
        y = e.clientY;

        // ...which is trickier than it looks.  The problem is that some
        // browsers provide document-relative coordinates in these properties,
        // while others provide coordinates relative to the client window.
        // For the latter, we need to adjust for scrolling to get our
        // desired document coordinates.
        if (!(BrowserInfo.opera
              || window.ScriptEngine && ScriptEngine().indexOf('InScript') != -1
              || BrowserInfo.konqueror))
        {
            // Okay, it's a browser that gives us client-window-relative
            // coordinates, so we have to adjust for scrolling.
            var scrollPos = getScrollPos();
            x += scrollPos.x;
            y += scrollPos.y;
        }
    }

    // return the x,y coordinates
    return { x: x, y: y };
}

// Recursively visit all children of an element
function forEachChild(ele, func)
{
    // visit all direct children
    for (var chi = ele.firstChild ; chi ; chi = chi.nextSibling)
    {
        // visit this child
        func(chi);

        // visit the children's children
        forEachChild(chi, func);
    }
}


/* ------------------------------------------------------------------------ */
/*
 *   Split button with menu dropdown.  To create this kind of button, create
 *   a <SPAN> within a <form> like so:
 *   
 *   <SPAN class="splitBtn">bname|option 1|option 2|...</SPAN>
 *   
 *   The SPAN's class name is literally "splitBtn".
 *   
 *   "bname" is the "name" property to use for the button in the form.  We
 *   create an <INPUT TYPE=SUBMIT> object to represent the button, and we
 *   give it this name.
 *   
 *   The "option" values are strings to present in the menu.  The first of
 *   these will be initially assigned as the button label.  When the user
 *   clicks on the menu drop-down arrow attached to the button, we'll open a
 *   menu that allows the user to select one of the other options; selecting
 *   an option changes the button label to match.
 *   
 *   To get or set the selection, use the span's getSplitSel() and
 *   setSplitSel() methods, which work in terms of option indices (the first
 *   option has index 0).
 */

/* initialize the split buttons */
function initSplitBtns()
{
    // scan all <form> objects
    for (var f = document.forms, i = 0 ; i < f.length ; ++i)
    {
        // scan elements within the form
        forEachChild(f[i], function(c) 
        {
            // if this is a <span> of class "splitBtn", it's one of ours
            if (c.nodeName == "SPAN" && c.className == "splitBtn")
            {
                // get the path prefix to the form's "action" URL; we'll
                // use this as the root path for other resources, such as
                // our button resource
                var path = (f[i].action.match(/^(.*\/)[^/]+$/)
                            ? RegExp.$1 : "");

                // get the text of the span, and remove it - we'll replace
                // it with the button and menu arrow overlay
                var t = c.firstChild;
                c.removeChild(t);
                var label = t.data.split("|");

                // create the button (an <INPUT SUBMIT> element)
                var btn = document.createElement("INPUT");
                btn.type = "submit";
                btn.name = label[0];
                btn.value = label[1];
                c.appendChild(btn);

                // create the <IMG> for the menu dropdown arrow; we'll
                // use absolute positioning to draw this as an overlay
                // on top of the button
                var ov = document.createElement("IMG");
                ov.src = path + "splitBtnOv.png";
                ov.className = "splitBtnOv";
                ov.draggable = false;
                c.appendChild(ov);

                // create the dropdown menu itself; this is a custom menu
                // that we'll draw using a <DIV>, which is initially invisible
                var pop = document.createElement("DIV");
                pop.className = "splitBtnPopup";

                // set up the "close popup" function, which we'll install
                // on mouse handlers for the drop arrow and on the document
                // itself, so that the popup disappears if they click anywhere
                // else in the document
                var closePop = function(e) {
                    if (!isEventInObject(e, pop) && !isEventInObject(e, ov))
                        pop.style.display = "none";
                    return true;
                };

                // Set up a "make select" function; this creates a mouse
                // handler to select or deselect a menu item when the mouse
                // moves over it or out of it.  We need to create a function
                // for each menu item separately in order to fix each
                // function's binding at the current value of the menu item
                // iteration variable.
                var makeSel = function(item, sel) {
                    return function(e) {
                        item.className = "splitBtnPopupItem"
                                         + (sel ? "Sel" : "");
                        return true;
                    };
                };

                // likewise a builder for the "click" function for each item
                var makeClick = function(item) {
                    return function(e) {
                        btn.value = item.innerText || item.textContent;
                        pop.style.display = "none";
                        fixup();
                        return true;
                    };
                };

                // build the individual menu items and add them to the menu DIV
                for (var j = 1 ; j < label.length ; ++j)
                {
                    var item = document.createElement("DIV");
                    item.className = "splitBtnPopupItem";
                    item.innerHTML = label[j];
                    item.onmouseover = makeSel(item, true);
                    item.onmouseout = makeSel(item, false);
                    item.onmousedown = makeClick(item);
                    item.onmouseup = makeClick(item);
                    pop.appendChild(item);
                }

                // add the menu poup DIV to the form
                c.appendChild(pop);

                // add a mousedown function to the popup arrow to open the menu
                ov.onmousedown = function() {
                    pop.style.display = (pop.style.display == "block"
                                         ? "none" : "block");
                    for (var chi = pop.firstChild ; chi ; chi = chi.nextSibling)
                        chi.className = "splitBtnPopupItem";
                };

                // add mouse handlers to the document to close the menu when
                // a click occurs outside of the menu
                addEventHandler(document.body, "mousedown", closePop);
                addEventHandler(document.body, "mouseup", closePop);

                // Set up a "fixup" function to set the button metrics.  We
                // usually need to do this asynchronously to allow the browser
                // to finalize the layout for the new objects, so we'll
                // initially install this as running after a brief timeout.
                var fixup = function()
                {
                    // if the menu arrow doesn't look to be the right size
                    // yet, don't run the fixup yet after all - try again
                    // after another delay, since the browser must not have
                    // finished the initial layout yet
                    if (ov.offsetHeight < 5)
                    {
                        setTimeout(fixup, 50);
                        return;
                    }

                    // find the longest button caption among the menu items;
                    // we'll size the button so that the longest item fits,
                    // so that it stays the same size as the user makes
                    // different selections in the menu
                    var origVal = btn.value;
                    btn.style.width = "";
                    for (var maxwid = 0, maxi = 1, j = 1 ;
                         j < label.length ; ++j)
                    {
                        btn.value = label[j];
                        if (btn.offsetWidth > maxwid)
                        {
                            maxwid = btn.offsetWidth;
                            maxi = j;
                        }
                    }

                    // set the button to the longest label, and figure out
                    // how many spaces we have to add to the end of the label
                    // to make room for the overlay arrow; do this by adding
                    // spaces one at a time until the button width is big
                    // enough for the original button width with the longest
                    // label plus the width of the overlay arrow
                    btn.value = label[maxi];
                    var goal = maxwid + ov.offsetWidth;
                    var cnt = 0;
                    for (var sp = "" ; btn.offsetWidth < goal ;
                         sp += " ", btn.value += " ") ;

                    // fix the button width at the goal width
                    btn.style.width = goal;

                    // set the button to its original value, adding the spaces
                    btn.value = origVal.replace(/\s+$/, "") + sp;

                    // Set the overlay position: vertically center it within
                    // the button's vertical extent, and put it at the right
                    // edge of the button. 
                    var cpos = getElePos(c);
                    var bpos = getElePos(btn);
                    ov.style.left = (bpos.w - ov.offsetWidth) + "px";
                    ov.style.top = ((btn.offsetHeight - ov.offsetHeight)/2)+"px";
                    pop.style.left = "1px";
                    pop.style.top = (btn.offsetHeight - 1) + "px";
                    if (getElePos(pop).w < btn.offsetWidth - 2)
                        pop.style.width = (btn.offsetWidth - 2) + "px";
                };
                setTimeout(fixup, 1);

                // add the menu value get/set methods
                c.getSplitSel = function()
                {
                    // get the current text
                    var txt = btn.value;

                    // compare it to the labels; it must match the
                    // label text as a leading substring, but it will
                    // also have trailing spaces that we added to leave
                    // room for the drop arrow overlay
                    for (var j = 1 ; j < label.length ; ++j)
                    {
                        var lj = label[j];
                        if (txt.length > lj.length
                            && txt.substr(0, lj.length) == lj
                            && txt.substr(lj.length).match(/^\s+$/))
                        {
                            // this is the matching item - return a
                            // zero-based index value
                            return j - 1;
                        }
                    }

                    // didn't find an active selection
                    return -1;
                };
                c.setSplitSel = function(sel)
                {
                    // set the selection
                    btn.value = label[sel+1];
                    
                    // fix up the button display for the new selection
                    fixup();
                }
            }
        });
    }
}
function preventDragOv(e)
{
    var e = getEvent(e);
    var t = getEventTarget(e);
    if (t && t.nodeName == "IMG" && t.className == "splitBtnOv")
    {
        if (e.stopPropagation)
            e.stopPropagation();
        return false;
    }
    return true;
}
addEventHandler(window, "load", initSplitBtns);
addEventHandler(document, "mousedown", preventDragOv);
addEventHandler(document.body, "dragstart", preventDragOv);

/*
 *   Find the synthesized <INPUT TYPE=SUBMIT> element for a split button
 *   object, given the original <SPAN> template for the split button.  The
 *   span can be given as an element object or an ID string.
 */
function findSplitButton(span)
{
    if (typeof(span) == "string")
        span = document.getElementById(span);

    for (var btn = span.firstChild ;
         btn && btn.nodeName != "INPUT" && btn.type != "SUBMIT" ;
         btn = btn.nextSibling) ;

    return btn;
}

/* ------------------------------------------------------------------------ */
/*
 *   Browser detector.  Call BrowserInfo.init() during page load to do the
 *   sensing work, then refer to BrowserInfo.browser for the browser ID, or
 *   use the various browser-specific properties to test for the respective
 *   browsers.
 *   
 *   This object identifies the specific browser, and separately lets you
 *   determine if it's based on one of the common browser core libraries
 *   (WebKit, Gecko), and if so the core library version.  The core library
 *   identification is useful because many special-case quirks are features
 *   of the library rather than the specific browser, and will apply to any
 *   browser based on that library.  Code to work around (or exploit) such a
 *   quirk is simpler and more robust if it can just check a library version
 *   rather than enumerating the individual browsers based on the library.
 *   
 *   Credits: adapted from http://www.quirksmode.org/js/detect.html.  
 */
var BrowserInfo = {
    // browser ID string, version number (as a float), and OS platform
    browser: "Unknown",
    version: 0,
    OS: "Unknown",

    // Specific browser ID properties.  We'll set the appropriate property
    // for the detected browser to its version number, as a float.
    opera: false,
    safari: false,
    firefox: false,
    ie: false,
    konqueror: false,

    // WebKit version, if this is a WebKit-based browser
    webKit: false,

    // Gecko version, if this is a Gecko-based browser
    gecko: false,

    // initialize
    init: function()
    {
        // get the UA string
        var ua = navigator.userAgent;

        // identify the browser
        this.browser = this.searchString(this.dataBrowser);

        // identify the browser version
        this.version = this.searchVersion(ua)
                       || this.searchVersion(navigator.appVersion)
                       || 0;

        // identify the OS platform
        this.OS = this.searchString(this.dataOS) || "Unknown";

        // determine if it's WebKit-based, and note the version if so
        var idx = ua.indexOf("AppleWebKit/");
        if (idx != -1)
            this.webKit = this.parseVsnFloat(ua.substr(idx + 12));

        // detremine if it's a Gecko-based browser
        idx = ua.indexOf("Gecko/"), idx2 = ua.indexOf("rv:");
        if (idx != -1 && idx2 != -1)
            this.gecko = this.parseVsnFloat(ua.substr(idx2 + 3));

        // set properties for some specific browser types
        this.opera = this.versionIf("Opera");
        this.safari = this.versionIf("Safari");
        this.firefox = this.versionIf("Firefox");
        this.ie = this.versionIf("IE");
        this.konqueror = this.versionIf("Konqueror");

        // if we're on Opera, customize the text adjustment method
        if (this.opera)
        {
            // opera doesn't render unicode typographical spaces, so replace
            // them with ordinary spaces
            this.adjustText = function(txt) {
                return txt.replace(/\u2002/g, ' ');
            };
        }
    },

    versionIf: function(browser)
    {
        // if the ID matches our detected browser, return the version number
        return (this.browser == browser ? this.version : false);
    },

    searchString: function(data)
    {
        // search the item list
        for (var i = 0 ; i < data.length ; i++)
        {
            var dataString = data[i].src || navigator.userAgent;
            this.versionSearchString = data[i].vsnKey || data[i].id;
            if (dataString.indexOf(data[i].key) != -1)
                return data[i].id;
        }

        // not found
        return null;
    },

    searchVersion: function(dataString)
    {
        var index = dataString.indexOf(this.versionSearchString);
        if (index >= 0)
            return this.parseVsnFloat(dataString.substring(
                index + this.versionSearchString.length + 1));
        else
            return 0;
    },

    dataBrowser: [
                  { key: "Chrome", id: "Chrome" },
                  { key: "OmniWeb", vsnKey: "OmniWeb/", id: "OmniWeb" },
                  { src: navigator.vendor, key: "Apple", id: "Safari", vsnKey: "Version" },
                  { key: "Opera", id: "Opera" },
                  { src: navigator.vendor, key: "iCab", id: "iCab" },
                  { src: navigator.vendor, key: "KDE", id: "Konqueror" },
                  { key: "Firefox", id: "Firefox" },
                  { src: navigator.vendor, key: "Camino", id: "Camino" },
                  { key: "Netscape", id: "Netscape" },
                  { key: "MSIE", id: "IE", vsnKey: "MSIE" },
                  { key: "Gecko", id: "Gecko", vsnKey: "rv" },
                  { key: "Mozilla", id: "Netscape", vsnKey: "Mozilla" }
    ],

    dataOS: [
             { src: navigator.platform, key: "Win", id: "Windows" },
             { src: navigator.platform, key: "Mac", id: "Mac" },
             { src: navigator.userAgent, key: "iPhone", id: "iPhone/iPod" },
             { src: navigator.platform, key: "Linux", id: "Linux" }
    ],

    // Parse a version number as a floating point value.  This parses a
    // dotted version number string "x.y.z.w" as though it were a floating
    // point value, effectively pulling out the "x" as the integer portion
    // and the "y" as the fractional portion, ignoring any addition elements.
    // We do one extra bit of processing, though: if "y" is only one digit
    // long, we treat it as though it were "x.0y", so that we treat version
    // "1.9" as less than "1.10".  This doesn't generalize indefinitely,
    // but it covers the 
    parseVsnFloat: function(str)
    {
        return parseFloat(str.replace(
            /^(\d+\.)(\d)([^\d]|$)/, function(m, x, y, z) {
            return x + "0" + y + z;
        }));
    },

    // perform browser-specific adjustments to text from the server
    adjustText: function(txt) { return txt; }
};

/* ------------------------------------------------------------------------ */
/*
 *   TADS doc search handler 
 */
function searchGo(form, btn)
{
    // get the current selection
    if (typeof(btn) == "string")
        btn = document.getElementById(btn);
    var idx = btn.getSplitSel();

    // get the original action's path, so that we can apply the same path
    // to the new action
    var path = (form.action.match(/^(.*\/)[^/]+$/)
                ? RegExp.$1 : "");

    // set the appropriate action
    switch (idx)
    {
    case 0:
        form.action = path + "t3doc/doSearch.php";
        return true;

    case 1:
        form.action = path + "t2doc/doSearch.php";
        return true;

    default:
        return false;
    }
}

