/** \file
 *
 * Home-grown, or cannibalised from the web, generic useful javascript functions
 *
 * TODO: Put these in an "octal" object/namespace so we can have
 * octal.Foo() instead of octalFoo(). Will need to add "legacy" aliases for
 * the functions for back-compat, e.g.
 *     var octalAddEventListener = octal.addEventListener
 *
 * Contents:
 * CLASS_MANIPULATION
 * WINDOW_PROPERTIES
 */

/** IE major version number, or 0
 *
 * Because IE is so goddamn broken in so many indescribable ways, there are just
 * some places where you need to browser sniff specifically for it to figure out
 * what to do.
 *
 * If running under IE, should be set to the major version number.
 * If not running under IE, should be 0. In which case, assume standards
 * compliance.
 */
var octalIE = (!document
        || !document.all
        || !navigator
        || !navigator.appVersion
        || !navigator.appVersion.indexOf) ? 0
    : (navigator.appVersion.indexOf("MSIE 6.") != -1) ? 6
    : (navigator.appVersion.indexOf("MSIE 7.") != -1) ? 7
    : 0;

if (!Array.prototype.indexOf)
{
    Array.prototype.indexOf = function(searchElement, fromIndex)
    {
        var l = this.length, i = 0;
        if (fromIndex) {
            i = fromIndex;
            if (i < 0) {
                i += l;
                if (i < 0) {
                    i = 0;
                }
            }
        }

        while (i < l) {
            if (this[i] === searchElement) {
                return i;
            }
            i++;
        }

        return -1;
    }
}


if (!Array.prototype.lastIndexOf)
{
    Array.prototype.lastIndexOf = function(searchElement, fromIndex)
    {
        var i = this.length;
        if (!fromIndex) {
            fromIndex = 0;
        }
        else if (fromIndex < 1) {
            fromIndex += i;
            if (fromIndex < 0) fromIndex = 0;
        }

        while (i-- > fromIndex) {
            if (this[i] === searchElement) {
                return i;
            }
        }

        return -1;
    }
}


/** Add an event handler to an event.
 *
 * \param elem Element to add handler to
 * \param evt Event to add handler for
 * \param fn Function to run.
 *
 * e.g. addEventListener(window, "load", myonload);
 */
function
octalAddEventListener(elem, evt, fn)
{
    if (!elem) {
        alert('No element to add ' + evt + ' handler to!');
        return;
    }

    if (elem.addEventListener) {
        elem.addEventListener(evt, fn, false);
    }
    else if (elem.attachEvent) {
        elem.attachEvent('on' + evt, fn);
    }
    else {
        alert('Could not add listener!');
    }
}


/** Remove an event handler from an event.
 *
 * \param elem Element to remove handler from
 * \param evt Event to remove handler from
 * \param fn Function to not run anymore.
 */
function
octalRemoveEventListener(elem, evt, fn)
{
    if (elem.removeEventListener) {
        elem.removeEventListener(evt, fn, false);
    }
    else if (elem.detachEvent) {
        elem.detachEvent('on' + evt, fn);
    }
}


function
octalRemoveAllChildren(element)
{
	if (!element) {
		return;
	}
	while (element.firstChild) {
		element.removeChild(element.firstChild);
	}
}


/** Get the caret position/selection for an element
 *
 * \param elem Element to get selection for.
 *
 * Returns an object with "begin", "end" and "length" set.
 *
 * If there is no "selection" but there is a caret position is set,
 * then "begin" and "end" will be equal and set to the caret position
 * and "length" will be 0.
 *
 * If there is no caret position at all, or if it cannot be determined,
 * then null will be returned.
 *
 * Code/inspiration/documentation from:
 *
 * http://www.webmasterworld.com/forum91/1138.htm
 * http://parentnode.org/javascript/working-with-the-cursor-position/
 * http://the-stickman.com/web-development/javascript/finding-selection-cursor-position-in-a-textarea-in-internet-explorer/
 */
function
octalGetSelection(elem)
{
    if (!elem) {
        return null;
    }

    if (elem.selectionStart !== undefined
            && elem.selectionEnd !== undefined)
    {
        /* Gecko */
        var pos = {};
        pos.begin = elem.selectionStart;
        pos.end = elem.selectionEnd;
        pos.length = pos.end - pos.begin;
        return pos;
    }

    if (elem.ownerDocument.selection) {
        /* IE */
        var currentElem = elem.ownerDocument.activeElement;

        elem.focus();
        var range = elem.ownerDocument.selection.createRange();
        if (range.parentElement() != elem) {
            currentElem.focus();
            return null;
        }

        // We'll use this as a 'dummy'
        var stored_range = range.duplicate();

        // Select all text
        stored_range.moveToElementText(elem);
        // Now move 'dummy' end point to end point of original range
        stored_range.setEndPoint('EndToEnd', range);

        // Now we can calculate start and end points
        var pos = {};
        pos.begin = stored_range.text.length - range.text.length;
        pos.end = pos.begin + range.text.length;
        pos.length = range.text.length;

        currentElem.focus();

        return pos;
    }

    return null;
}


/** Set the focus/selection for an element. (if possible)
 *
 * \param elem Element to set focus to and selection for.
 * \param start Start position (optional)
 * \param end End position (optional)
 *
 * Code/inspiration/documentation from:
 *
 * http://parentnode.org/javascript/working-with-the-cursor-position/
 * http://www.webreference.com/js/column12/trmethods.html
 * http://msdn.microsoft.com/en-us/library/ms535872(VS.85).aspx
 */
function
octalSetSelection(elem, start, end)
{
    if (!elem
            || !elem.focus)
    {
        return;
    }

    elem.focus();

    if (!start) {
        /* Set to integer 0 if undefined or null */
        start = 0;
    }
    if (!end) {
        end = start;
    }

    if (elem.setSelectionRange) {
        /* Gecko */
        elem.setSelectionRange(start, end);
        return;
    }

    if (elem.createTextRange) { 
        /* IE */
        var range = elem.createTextRange(); 
        /* Reset text range to start of element */
        while (range.moveStart('character', -100) != 0) {
            /* do nothing. */
        }
        while (range.moveEnd('character', -100) != 0) {
            /* do nothing */
        }
        /* And then set its proper position */
        range.moveStart('character', start); 
        range.moveEnd('character', end - start);
        range.select(); 
        return;
    }

    return;
}


/** Find the source of an event
 *
 * \param o Object the event was called on.
 * \param e The event object.
 *
 * o is the "this" in the event handler.
 * e is either the parameter passed to the hander (W3C) or window.event (IE)
 *
 * Typical usage:
 *
 * function eventHandler(e) {
 *   e = e ? e : window.event;
 *   src = octalEventSrc(this, e);
 *   ...
 * }
 *
 * Code/inspiration/documentation from:
 *   http://www.quirksmode.org/js/events_properties.html
 */
function
octalEventSrc(o, e)
{
    if (e.target) {
        /* W3C compatible - use event target */
        return e.target;
    }
    if (e.srcElement) {
        /* IE  */
        return e.srcElement;
    }
    if (o && o.tagName) {
        /* W3C - should work off the "this" parameter */
        if (o.nodeType == 3/*Node.TEXT_NODE*/) {
            // defeat Safari bug
            return o.parentNode;
        }
        return o;
    }
    alert('Unable to get event target!');
    return null;
}


/** Cancel an event.
 *
 * \param e Event to cancel.
 *
 * \returns false You can cancel an event with "return octalEventCancel(e)"
 */
function
octalEventCancel(e)
{
    if (e.preventDefault) {
        e.preventDefault();
    }
    if (e.returnValue) {
        e.returnValue = false;
    }
    return false;
}


/** Key objects that can be returned by octalEventGetKey()
 *
 * These key objects DO NOT necessarily map to characters. For instance, there
 * are no lowercase keys, as there are no separate lowercase keys on keyboards.
 * 
 * You cannot assume that because you are told the user pressed the physical
 * [2] key that they actually typed a 2. They may have typed in another
 * character that is also mapped on the [2] key (e.g. ["] on UK keyboards),
 * but depends on the modifier keys they pressed (e.g. [shift]) and the
 * keyboard mapping. But you have no way of knowing the keyboard mapping as
 * you're only in a browser, and therefore no way of interpreting the state
 * of the modifier keys. All you really know is that they pressed a key which
 * is capable of creating the character [2] in at least one circumstance. See
 * the discussion on the 'shift' member below for more discussion.
 *
 * If you want to know what charcter was input, you need the to use
 * octalEventGetInput() on the keypress/textInput event, not octalEventGetKey()
 * on the keyup/keydown event.
 *
 * Members are:
 *      Name
 *          A unique, recognisable name for the key. This is what you should
 *          use to identify the key.
 *
 *          These names are mostly the same as the W3C DOM-3 Event keynames,
 *          except that ASCII character keys have readable names. Whitespace and
 *          editing keys are given readable names like 'Space', 'Tab', 'Delete',
 *          'Backspace', etc.... (Note that the W3C defines 'Enter' for the
 *          Enter key though - weird!) All other ASCII keys' names are the 1
 *          character string consisting of that character. This is so that
 *
 *              "if (key.Name == 'A')"
 *
 *          is readable, unlike
 *
 *              "if (key.Name == 'U+0041')"
 *
 *      W3C
 *          The W3C DOM-3 Event keyname for this key. See
 *          http://www.w3.org/TR/DOM-Level-3-Events/keyset.html
 *
 *      str
 *          The string representation for this key. Probably not very useful.
 *
 *      uni
 *          The unicode code points that can be produced by this key. This
 *          member is used for looking up keys based off an event 'charCode' or
 *          'which' member, or an event 'keyCode' member when the keyCode
 *          represents an ascii/unicode character and not a keyCode.
 *
 *      keycode
 *          The "keycodes" that can be produced by this key. See
 *          http://unixpapa.com/js/key.html.
 *          All the keycodes for a key are listed, except for a few old Opera
 *          keycodes where these conflict with IE or Mozilla keycodes for other
 *          keys. This member is used for looking up keys based off of event
 *          'keyCode' members when the keyCode means a keyCode and not an
 *          ascii/unicode character.
 *
 *      shift (optional)
 *          The key that should actually be used if the shift key is depressed.
 *
 *          On a UK keyboard, if you type in a quote ["] Gecko returns keycode
 *          222 which maps to the [']/["] key. Only on a UK keyboard the quote
 *          character is entered by pressing [shift]+[2]. It seems that Gecko
 *          assumes a US keyboard layout (probably to prevent breaking existing
 *          apps which assume US keyboards and shift states) and generates
 *          US-layout-style keycodes for characters that the user has input.
 *
 *          So, when we recieve a 222 for ['] when the shift key is down, we
 *          actually need to return the ["] key as that's the key the user
 *          pressed.
 *
 *          Aaaagggh!!!!!!
 *
 * Note that objects for keys that produce unicode characters above 127 are not
 * defined. These can be auto-generated by octalPhysicalKeyForChar()
 *
 * Further reading:
 *
 *      http://unixpapa.com/js/key.html.
 *
 *      That has much better description of how it all fits together than I can
 *      put in here, and I couldn't have written this without it. Thanks Jan.
 *
 * TODO: Add some kind of "type" member?
 * Possible values:
 *      Meta (Alt, Ctrl, Shift, CapsLock, NumLock, etc...)
 *          - Changes meaning of other keys
 *      Function (F1-F24, Esc, "Play", "Back", "Home", "End", etc...)
 *          - Perform some non-editing function
 *      Editing (Backspace, Delete (Cut, Paste?))
 *          - Alters text, but not character production
 *      Character
 *          - Any key which produces unicode character(s) that are added to text
 */
var octalPhysicalKeys = [
    { Name: 'Accept',           W3C: 'Accept',          str: '',        uni: [],        keycode: []     }, // The Accept (Commit) key.
    { Name: 'Again',            W3C: 'Again',           str: '',        uni: [],        keycode: []     }, // The Again key.
    { Name: 'AllCandidates',    W3C: 'AllCandidates',   str: '',        uni: [],        keycode: []     }, // The All Candidates key.
    { Name: 'Alphanumeric',     W3C: 'Alphanumeric',    str: '',        uni: [],        keycode: []     }, // The Alphanumeric key.
    { Name: 'Alt',              W3C: 'Alt',             str: '',        uni: [],        keycode: [18]   }, // The Alt (Menu) key.
    { Name: 'AltGraph',         W3C: 'AltGraph',        str: '',        uni: [],        keycode: []     }, // The Alt-Graph key.
    { Name: 'Apps',             W3C: 'Apps',            str: '',        uni: [],        keycode: []     }, // The Application key.
    { Name: 'Attn',             W3C: 'Attn',            str: '',        uni: [],        keycode: []     }, // The ATTN key.
    { Name: 'BrowserBack',      W3C: 'BrowserBack',     str: '',        uni: [],        keycode: []     }, // The Browser Back key.
    { Name: 'BrowserFavorites', W3C: 'BrowserFavorites',str: '',        uni: [],        keycode: []     }, // The Browser Favorites key.
    { Name: 'BrowserForward',   W3C: 'BrowserForward',  str: '',        uni: [],        keycode: []     }, // The Browser Forward key.
    { Name: 'BrowserHome',      W3C: 'BrowserHome',     str: '',        uni: [],        keycode: []     }, // The Browser Home key.
    { Name: 'BrowserRefresh',   W3C: 'BrowserRefresh',  str: '',        uni: [],        keycode: []     }, // The Browser Refresh key.
    { Name: 'BrowserSearch',    W3C: 'BrowserSearch',   str: '',        uni: [],        keycode: []     }, // The Browser Search key.
    { Name: 'BrowserStop',      W3C: 'BrowserStop',     str: '',        uni: [],        keycode: []     }, // The Browser Stop key.
    { Name: 'CapsLock',         W3C: 'CapsLock',        str: '',        uni: [],        keycode: [20]   }, // The Caps Lock (Capital) key.
    { Name: 'Clear',            W3C: 'Clear',           str: '',        uni: [],        keycode: []     }, // The Clear key.
    { Name: 'CodeInput',        W3C: 'CodeInput',       str: '',        uni: [],        keycode: []     }, // The Code Input key.
    { Name: 'Compose',          W3C: 'Compose',         str: '',        uni: [],        keycode: []     }, // The Compose key.
    { Name: 'Control',          W3C: 'Control',         str: '',        uni: [],        keycode: [17]   }, // The Control (Ctrl) key.
    { Name: 'Crsel',            W3C: 'Crsel',           str: '',        uni: [],        keycode: []     }, // The Crsel key.
    { Name: 'Convert',          W3C: 'Convert',         str: '',        uni: [],        keycode: []     }, // The Convert key.
    { Name: 'Copy',             W3C: 'Copy',            str: '',        uni: [],        keycode: []     }, // The Copy key.
    { Name: 'Cut',              W3C: 'Cut',             str: '',        uni: [],        keycode: []     }, // The Cut key.
    { Name: 'Down',             W3C: 'Down',            str: '',        uni: [],        keycode: [40]   }, // The Down Arrow key.
    { Name: 'End',              W3C: 'End',             str: '',        uni: [],        keycode: [35]   }, // The End key.
    { Name: 'Enter',            W3C: 'Enter',           str: '\n',      uni: [13],      keycode: [13]   }, // The Enter key.  Note: This key identifier is also used for the Return (Macintosh numpad) key.
    { Name: 'EraseEof',         W3C: 'EraseEof',        str: '',        uni: [],        keycode: []     }, // The Erase EOF key.
    { Name: 'Execute',          W3C: 'Execute',         str: '',        uni: [],        keycode: []     }, // The Execute key.
    { Name: 'Exsel',            W3C: 'Exsel',           str: '',        uni: [],        keycode: []     }, // The Exsel key.
    { Name: 'F1',               W3C: 'F1',              str: '',        uni: [],        keycode: [112]  }, // The F1 key.
    { Name: 'F2',               W3C: 'F2',              str: '',        uni: [],        keycode: [113]  }, // The F2 key.
    { Name: 'F3',               W3C: 'F3',              str: '',        uni: [],        keycode: [114]  }, // The F3 key.
    { Name: 'F4',               W3C: 'F4',              str: '',        uni: [],        keycode: [115]  }, // The F4 key.
    { Name: 'F5',               W3C: 'F5',              str: '',        uni: [],        keycode: [116]  }, // The F5 key.
    { Name: 'F6',               W3C: 'F6',              str: '',        uni: [],        keycode: [117]  }, // The F6 key.
    { Name: 'F7',               W3C: 'F7',              str: '',        uni: [],        keycode: [118]  }, // The F7 key.
    { Name: 'F8',               W3C: 'F8',              str: '',        uni: [],        keycode: [119]  }, // The F8 key.
    { Name: 'F9',               W3C: 'F9',              str: '',        uni: [],        keycode: [120]  }, // The F9 key.
    { Name: 'F10',              W3C: 'F10',             str: '',        uni: [],        keycode: [121]  }, // The F10 key.
    { Name: 'F11',              W3C: 'F11',             str: '',        uni: [],        keycode: [122]  }, // The F11 key.
    { Name: 'F12',              W3C: 'F12',             str: '',        uni: [],        keycode: [123]  }, // The F12 key.
    { Name: 'F13',              W3C: 'F13',             str: '',        uni: [],        keycode: []     }, // The F13 key.
    { Name: 'F14',              W3C: 'F14',             str: '',        uni: [],        keycode: []     }, // The F14 key.
    { Name: 'F15',              W3C: 'F15',             str: '',        uni: [],        keycode: []     }, // The F15 key.
    { Name: 'F16',              W3C: 'F16',             str: '',        uni: [],        keycode: []     }, // The F16 key.
    { Name: 'F17',              W3C: 'F17',             str: '',        uni: [],        keycode: []     }, // The F17 key.
    { Name: 'F18',              W3C: 'F18',             str: '',        uni: [],        keycode: []     }, // The F18 key.
    { Name: 'F19',              W3C: 'F19',             str: '',        uni: [],        keycode: []     }, // The F19 key.
    { Name: 'F20',              W3C: 'F20',             str: '',        uni: [],        keycode: []     }, // The F20 key.
    { Name: 'F21',              W3C: 'F21',             str: '',        uni: [],        keycode: []     }, // The F21 key.
    { Name: 'F22',              W3C: 'F22',             str: '',        uni: [],        keycode: []     }, // The F22 key.
    { Name: 'F23',              W3C: 'F23',             str: '',        uni: [],        keycode: []     }, // The F23 key.
    { Name: 'F24',              W3C: 'F24',             str: '',        uni: [],        keycode: []     }, // The F24 key.
    { Name: 'FinalMode',        W3C: 'FinalMode',       str: '',        uni: [],        keycode: []     }, // The Final Mode (Final) key used on some asian keyboards.
    { Name: 'Find',             W3C: 'Find',            str: '',        uni: [],        keycode: []     }, // The Find key.
    { Name: 'FullWidth',        W3C: 'FullWidth',       str: '',        uni: [],        keycode: []     }, // The Full-Width Characters key.
    { Name: 'HalfWidth',        W3C: 'HalfWidth',       str: '',        uni: [],        keycode: []     }, // The Half-Width Characters key.
    { Name: 'HangulMode',       W3C: 'HangulMode',      str: '',        uni: [],        keycode: []     }, // The Hangul (Korean characters) Mode key.
    { Name: 'HanjaMode',        W3C: 'HanjaMode',       str: '',        uni: [],        keycode: []     }, // The Hanja (Korean characters) Mode key.
    { Name: 'Help',             W3C: 'Help',            str: '',        uni: [],        keycode: []     }, // The Help key.
    { Name: 'Hiragana',         W3C: 'Hiragana',        str: '',        uni: [],        keycode: []     }, // The Hiragana (Japanese Kana characters) key.
    { Name: 'Home',             W3C: 'Home',            str: '',        uni: [],        keycode: [36]   }, // The Home key.
    { Name: 'Insert',           W3C: 'Insert',          str: '',        uni: [],        keycode: [45]   }, // The Insert (Ins) key.
    { Name: 'JapaneseHiragana', W3C: 'JapaneseHiragana',str: '',        uni: [],        keycode: []     }, // The Japanese-Hiragana key.
    { Name: 'JapaneseKatakana', W3C: 'JapaneseKatakana',str: '',        uni: [],        keycode: []     }, // The Japanese-Katakana key.
    { Name: 'JapaneseRomaji',   W3C: 'JapaneseRomaji',  str: '',        uni: [],        keycode: []     }, // The Japanese-Romaji key.
    { Name: 'JunjaMode',        W3C: 'JunjaMode',       str: '',        uni: [],        keycode: []     }, // The Junja Mode key.
    { Name: 'KanaMode',         W3C: 'KanaMode',        str: '',        uni: [],        keycode: []     }, // The Kana Mode (Kana Lock) key.
    { Name: 'KanjiMode',        W3C: 'KanjiMode',       str: '',        uni: [],        keycode: []     }, // The Kanji (Japanese name for ideographic characters of Chinese origin) Mode key.
    { Name: 'Katakana',         W3C: 'Katakana',        str: '',        uni: [],        keycode: []     }, // The Katakana (Japanese Kana characters) key.
    { Name: 'LaunchApplication1', W3C: 'LaunchApplication1', str: '',   uni: [],        keycode: []                                             }, // The Start Application One key.
    { Name: 'LaunchApplication2', W3C: 'LaunchApplication2', str: '',   uni: [],        keycode: []                                             }, // The Start Application Two key.
    { Name: 'LaunchMail',       W3C: 'LaunchMail',      str: '',        uni: [],        keycode: []     }, // The Start Mail key.
    { Name: 'Left',             W3C: 'Left',            str: '',        uni: [],        keycode: [37]   }, // The Left Arrow key.
    { Name: 'Meta',             W3C: 'Meta',            str: '',        uni: [],        keycode: []     }, // The Meta key.
    { Name: 'MediaNextTrack',   W3C: 'MediaNextTrack',  str: '',        uni: [],        keycode: []     }, // The Media Next Track key.
    { Name: 'MediaPlayPause',   W3C: 'MediaPlayPause',  str: '',        uni: [],        keycode: []     }, // The Media Play Pause key.
    { Name: 'MediaPreviousTrack', W3C: 'MediaPreviousTrack', str: '',   uni: [],        keycode: []                                             }, // The Media Previous Track key.
    { Name: 'MediaStop',        W3C: 'MediaStop',       str: '',        uni: [],        keycode: []     }, // The Media Stok key.
    { Name: 'ModeChange',       W3C: 'ModeChange',      str: '',        uni: [],        keycode: []     }, // The Mode Change key.
    { Name: 'Nonconvert',       W3C: 'Nonconvert',      str: '',        uni: [],        keycode: []     }, // The Nonconvert (Don't Convert) key.
    { Name: 'NumLock',          W3C: 'NumLock',         str: '',        uni: [],        keycode: [144]  }, // The Num Lock key.
    { Name: 'PageDown',         W3C: 'PageDown',        str: '',        uni: [],        keycode: [34]   }, // The Page Down (Next) key.
    { Name: 'PageUp',           W3C: 'PageUp',          str: '',        uni: [],        keycode: [33]   }, // The Page Up key.
    { Name: 'Paste',            W3C: 'Paste',           str: '',        uni: [],        keycode: []     }, // The Paste key.
    { Name: 'Pause',            W3C: 'Pause',           str: '',        uni: [],        keycode: []     }, // The Pause key.
    { Name: 'Play',             W3C: 'Play',            str: '',        uni: [],        keycode: []     }, // The Play key.
    { Name: 'PreviousCandidate', W3C: 'PreviousCandidate', str: '',     uni: [],        keycode: []                                               }, // The Previous Candidate function key.
    { Name: 'PrintScreen',      W3C: 'PrintScreen',     str: '',        uni: [],        keycode: []     }, // The Print Screen (PrintScrn, SnapShot) key.
    { Name: 'Process',          W3C: 'Process',         str: '',        uni: [],        keycode: []     }, // The Process key.
    { Name: 'Props',            W3C: 'Props',           str: '',        uni: [],        keycode: []     }, // The Props key.
    { Name: 'Right',            W3C: 'Right',           str: '',        uni: [],        keycode: [39]   }, // The Right Arrow key.
    { Name: 'RomanCharacters',  W3C: 'RomanCharacters', str: '',        uni: [],        keycode: []     }, // The Roman Characters function key.
    { Name: 'Scroll',           W3C: 'Scroll',          str: '',        uni: [],        keycode: []     }, // The Scroll Lock key.
    { Name: 'Select',           W3C: 'Select',          str: '',        uni: [],        keycode: []     }, // The Select key.
    { Name: 'SelectMedia',      W3C: 'SelectMedia',     str: '',        uni: [],        keycode: []     }, // The Select Media key.
    { Name: 'Shift',            W3C: 'Shift',           str: '',        uni: [],        keycode: [16]   }, // The Shift key.
    { Name: 'Stop',             W3C: 'Stop',            str: '',        uni: [],        keycode: []     }, // The Stop key.
    { Name: 'Up',               W3C: 'Up',              str: '',        uni: [],        keycode: [38]   }, // The Up Arrow key.
    { Name: 'Undo',             W3C: 'Undo',            str: '',        uni: [],        keycode: []     }, // The Undo key.
    { Name: 'VolumeDown',       W3C: 'VolumeDown',      str: '',        uni: [],        keycode: []     }, // The Volume Down key.
    { Name: 'VolumeMute',       W3C: 'VolumeMute',      str: '',        uni: [],        keycode: []     }, // The Volume Mute key.
    { Name: 'VolumeUp',         W3C: 'VolumeUp',        str: '',        uni: [],        keycode: []     }, // The Volume Up key.
    { Name: 'Win',              W3C: 'Win',             str: '',        uni: [],        keycode: []     }, // The Windows Logo key.
    { Name: 'Zoom',             W3C: 'Zoom',            str: '',        uni: [],        keycode: []     }, // The Zoom key.
    { Name: 'Backspace',        W3C: 'U+0008',          str: '\b',      uni: [8],       keycode: [8]    }, // The Backspace (Back) key.
    { Name: 'Tab',              W3C: 'U+0009',          str: '\t',      uni: [9],       keycode: [9]    }, // The Horizontal Tabulation (Tab) key.
    { Name: 'Cancel',           W3C: 'U+0018',          str: '',        uni: [],        keycode: []     }, // The Cancel key.
    { Name: 'Escape',           W3C: 'U+001B',          str: '\u001b',  uni: [27],      keycode: [27]   }, // The Escape (Esc) key.
    { Name: 'Space',            W3C: 'U+0020',          str: ' ',       uni: [32],      keycode: [32]   }, // The Space (Spacebar) key.
    { Name: '!',                W3C: 'U+0021',          str: '!',       uni: [33],      keycode: []     }, // The Exclamation Mark (Factorial, Bang) key (!).
    { Name: '"',                W3C: 'U+0022',          str: '"',       uni: [34],      keycode: []     }, // The Quotation Mark (Quote Double) key (").
    { Name: '#',                W3C: 'U+0023',          str: '#',       uni: [35],      keycode: []     }, // The Number Sign (Pound Sign, Hash, Crosshatch, Octothorpe) key (#).
    { Name: '$',                W3C: 'U+0024',          str: '$',       uni: [36],      keycode: []     }, // The Dollar Sign (milreis, escudo) key ($).
    { Name: '%',                W3C: 'U+0025',          str: '%',       uni: [37],      keycode: []     }, // The Percent Sign
    { Name: '&',                W3C: 'U+0026',          str: '&',       uni: [38],      keycode: []     }, // The Ampersand key (&).
    { Name: '\'',               W3C: 'U+0027',          str: '\'',      uni: [39],      keycode: [222],         shift: '"'      }, // The Apostrophe (Apostrophe-Quote, APL Quote) key (').
    { Name: '(',                W3C: 'U+0028',          str: '(',       uni: [40],      keycode: []                             }, // The Left Parenthesis (Opening Parenthesis) key (().
    { Name: ')',                W3C: 'U+0029',          str: ')',       uni: [41],      keycode: []                             }, // The Right Parenthesis (Closing Parenthesis) key ()).
    { Name: '*',                W3C: 'U+002A',          str: '*',       uni: [42],      keycode: []                             }, // The Asterix (Star) key (*).
    { Name: '+',                W3C: 'U+002B',          str: '+',       uni: [43],      keycode: []                             }, // The Plus Sign (Plus) key (+).
    { Name: ',',                W3C: 'U+002C',          str: ',',       uni: [44],      keycode: [188, 44],     shift: '<'      }, // The Comma (decimal separator) sign key (,).
    { Name: '-',                W3C: 'U+002D',          str: '-',       uni: [45],      keycode: [189, 109],    shift: '_'      }, // The Hyphen-minus (hyphen or minus sign) key (-).
    { Name: '.',                W3C: 'U+002E',          str: '.',       uni: [46],      keycode: [190],         shift: '>'      }, // The Full Stop (period, dot, decimal point) key (.).
    { Name: '/',                W3C: 'U+002F',          str: '/',       uni: [47],      keycode: [191, 47],     shift: '?'      }, // The Solidus (slash, virgule, shilling) key (/).
    { Name: '0',                W3C: 'U+0030',          str: '0',       uni: [48],      keycode: [48],          shift: ')'      }, // The Digit Zero key (0).
    { Name: '1',                W3C: 'U+0031',          str: '1',       uni: [49],      keycode: [49],          shift: '!'      }, // The Digit One key (1).
    { Name: '2',                W3C: 'U+0032',          str: '2',       uni: [50],      keycode: [50],          shift: '@'      }, // The Digit Two key (2).
    { Name: '3',                W3C: 'U+0033',          str: '3',       uni: [51],      keycode: [51],          shift: '#'      }, // The Digit Three key (3).
    { Name: '4',                W3C: 'U+0034',          str: '4',       uni: [52],      keycode: [52],          shift: '$'      }, // The Digit Four key (4).
    { Name: '5',                W3C: 'U+0035',          str: '5',       uni: [53],      keycode: [53],          shift: '%'      }, // The Digit Five key (5).
    { Name: '6',                W3C: 'U+0036',          str: '6',       uni: [54],      keycode: [54],          shift: '^'      }, // The Digit Six key (6).
    { Name: '7',                W3C: 'U+0037',          str: '7',       uni: [55],      keycode: [55],          shift: '&'      }, // The Digit Seven key (7).
    { Name: '8',                W3C: 'U+0038',          str: '8',       uni: [56],      keycode: [56],          shift: '*'      }, // The Digit Eight key (8).
    { Name: '9',                W3C: 'U+0039',          str: '9',       uni: [57],      keycode: [57],          shift: '('      }, // The Digit Nine key (9).
    { Name: ':',                W3C: 'U+003A',          str: ':',       uni: [58],      keycode: []                             }, // The Colon key (:).
    { Name: ';',                W3C: 'U+003B',          str: ';',       uni: [59],      keycode: [186, 59],     shift: ':'      }, // The Semicolon key (;).
    { Name: '<',                W3C: 'U+003C',          str: '<',       uni: [60],      keycode: []                             }, // The Less-Than Sign key (<).
    { Name: '=',                W3C: 'U+003D',          str: '=',       uni: [61],      keycode: [187, 61],     shift: '+'      }, // The Equals Sign key (=).
    { Name: '>',                W3C: 'U+003E',          str: '>',       uni: [62],      keycode: []     }, // The Greater-Than Sign key (>).
    { Name: '?',                W3C: 'U+003F',          str: '?',       uni: [63],      keycode: []     }, // The Question Mark key (?).
    { Name: '@',                W3C: 'U+0040',          str: '@',       uni: [64],      keycode: []     }, // The Commercial At (@) key.
    { Name: 'A',                W3C: 'U+0041',          str: 'A',       uni: [65, 97],  keycode: [65]   }, // The Latin Capital Letter A key (A).
    { Name: 'B',                W3C: 'U+0042',          str: 'B',       uni: [66, 98],  keycode: [66]   }, // The Latin Capital Letter B key (B).
    { Name: 'C',                W3C: 'U+0043',          str: 'C',       uni: [67, 99],  keycode: [67]   }, // The Latin Capital Letter C key (C).
    { Name: 'D',                W3C: 'U+0044',          str: 'D',       uni: [68, 100], keycode: [68]   }, // The Latin Capital Letter D key (D).
    { Name: 'E',                W3C: 'U+0045',          str: 'E',       uni: [69, 101], keycode: [69]   }, // The Latin Capital Letter E key (E).
    { Name: 'F',                W3C: 'U+0046',          str: 'F',       uni: [70, 102], keycode: [70]   }, // The Latin Capital Letter F key (F).
    { Name: 'G',                W3C: 'U+0047',          str: 'G',       uni: [71, 103], keycode: [71]   }, // The Latin Capital Letter G key (G).
    { Name: 'H',                W3C: 'U+0048',          str: 'H',       uni: [72, 104], keycode: [72]   }, // The Latin Capital Letter H key (H).
    { Name: 'I',                W3C: 'U+0049',          str: 'I',       uni: [73, 105], keycode: [73]   }, // The Latin Capital Letter I key (I).
    { Name: 'J',                W3C: 'U+004A',          str: 'J',       uni: [74, 106], keycode: [74]   }, // The Latin Capital Letter J key (J).
    { Name: 'K',                W3C: 'U+004B',          str: 'K',       uni: [75, 107], keycode: [75]   }, // The Latin Capital Letter K key (K).
    { Name: 'L',                W3C: 'U+004C',          str: 'L',       uni: [76, 108], keycode: [76]   }, // The Latin Capital Letter L key (L).
    { Name: 'M',                W3C: 'U+004D',          str: 'M',       uni: [77, 109], keycode: [77]   }, // The Latin Capital Letter M key (M).
    { Name: 'N',                W3C: 'U+004E',          str: 'N',       uni: [78, 110], keycode: [78]   }, // The Latin Capital Letter N key (N).
    { Name: 'O',                W3C: 'U+004F',          str: 'O',       uni: [79, 111], keycode: [79]   }, // The Latin Capital Letter O key (O).
    { Name: 'P',                W3C: 'U+0050',          str: 'P',       uni: [80, 112], keycode: [80]   }, // The Latin Capital Letter P key (P).
    { Name: 'Q',                W3C: 'U+0051',          str: 'Q',       uni: [81, 113], keycode: [81]   }, // The Latin Capital Letter Q key (Q).
    { Name: 'R',                W3C: 'U+0052',          str: 'R',       uni: [82, 114], keycode: [82]   }, // The Latin Capital Letter R key (R).
    { Name: 'S',                W3C: 'U+0053',          str: 'S',       uni: [83, 115], keycode: [83]   }, // The Latin Capital Letter S key (S).
    { Name: 'T',                W3C: 'U+0054',          str: 'T',       uni: [84, 116], keycode: [84]   }, // The Latin Capital Letter T key (T).
    { Name: 'U',                W3C: 'U+0055',          str: 'U',       uni: [85, 117], keycode: [85]   }, // The Latin Capital Letter U key (U).
    { Name: 'V',                W3C: 'U+0056',          str: 'V',       uni: [86, 118], keycode: [86]   }, // The Latin Capital Letter V key (V).
    { Name: 'W',                W3C: 'U+0057',          str: 'W',       uni: [87, 119], keycode: [87]   }, // The Latin Capital Letter W key (W).
    { Name: 'X',                W3C: 'U+0058',          str: 'X',       uni: [88, 120], keycode: [88]   }, // The Latin Capital Letter X key (X).
    { Name: 'Y',                W3C: 'U+0059',          str: 'Y',       uni: [89, 121], keycode: [89]   }, // The Latin Capital Letter Y key (Y).
    { Name: 'Z',                W3C: 'U+005A',          str: 'Z',       uni: [90, 122], keycode: [90]   }, // The Latin Capital Letter Z key (Z).
    { Name: '[',                W3C: 'U+005B',          str: '[',       uni: [91],      keycode: [219, 91],     shift: '{'      },    // The Left Square Bracket (Opening Square Bracket) key ([).
    { Name: '\\',               W3C: 'U+005C',          str: '\\',      uni: [92],      keycode: [220, 92],     shift: '|'      },    // The Reverse Solidus (Backslash) key (\).
    { Name: ']',                W3C: 'U+005D',          str: ']',       uni: [93],      keycode: [221, 93],     shift: '}'      },    // The Right Square Bracket (Closing Square Bracket) key (]).
    { Name: '^',                W3C: 'U+005E',          str: '^',       uni: [94],      keycode: []                             }, // The Circumflex Accent key (^).
    { Name: '_',                W3C: 'U+005F',          str: '_',       uni: [95],      keycode: []                             }, // The Low Sign (Spacing Underscore, Underscore) key (_).
    { Name: '`',                W3C: 'U+0060',          str: '`',       uni: [96],      keycode: [192, 96],     shift: '~'      }, // The Grave Accent (Back Quote) key (`).
    { Name: '{',                W3C: 'U+007B',          str: '{',       uni: [123],     keycode: []                             }, // The Left Curly Bracket (Opening Curly Bracket, Opening Brace, Brace Left) key ({).
    { Name: '|',                W3C: 'U+007C',          str: '|',       uni: [124],     keycode: []                             }, // The Vertical Line (Vertical Bar, Pipe) key (|).
    { Name: '}',                W3C: 'U+007D',          str: '}',       uni: [125],     keycode: []                             }, // The Right Curly Bracket (Closing Curly Bracket, Closing Brace, Brace Right) key (}).
    { Name: '~',                W3C: 'U+007E',          str: '~',       uni: [126],     keycode: []                             }, // The Tilde Key.
    { Name: 'Delete',           W3C: 'U+007F',          str: '\u007f',  uni: [127],     keycode: [46]                           }  // The Delete (Del) Key.
];

var octalPhysicalKeysByName = null;
var octalPhysicalKeysByW3C = null;
var octalPhysicalKeysByUni = null;
var octalPhysicalKeysByKeycode = null;

/* Build the associative arrays to look up physical key objects
 *
 * TODO: Check there are no duplicates for Names, W3C, uni or keycode.
 */
function
octalPhysicalKeysBuildArrays()
{
    if (octalPhysicalKeysByName) {
        return;
    }

    octalPhysicalKeysByName = {};
    octalPhysicalKeysByW3C = {};
    octalPhysicalKeysByUni = {};
    octalPhysicalKeysByKeycode = {};

    for (var i = 0; i < octalPhysicalKeys.length; ++i) {
        var key = octalPhysicalKeys[i];
        octalPhysicalKeysByName[key.Name] = key;
        octalPhysicalKeysByW3C[key.W3C] = key;
        for (var j = 0; j < key.uni.length; ++j) {
            octalPhysicalKeysByUni[key.uni[j]] = key;
        }
        for (var j = 0; j < key.keycode.length; ++j) {
            octalPhysicalKeysByKeycode[key.keycode[j]] = key;
        }
    }

    return;
}

/** Get the physical key for an ascii/unicode character
 *
 * \param ch Numerical character code to get key for.
 */
function
octalPhysicalKeyForChar(ch)
{
    if (ch < 128) {
        octalPhysicalKeysBuildArrays()
        return octalPhysicalKeysByUni[ch];
    }

    return {
        Name: 'U+' + ch.toString(16),
        W3C: 'U+' + ch.toString(16),
        str: String.fromCharCode(ch),
        uni: [ch],
        keycode: []
    };
}


/** Returns an object representing the physical "key" provided by an event.
 *
 * \param e The event that the key comes from.
 *
 * The event \p e should be a 'keydown', 'keyup', 'keypress' or 'textInput'
 * event.
 *
 * If an object is returned, it will be one of, or compatible with, the members
 * of the octalPhysicalKeys array. See the docuentation for that for more info.
 *
 * \see #octalPhysicalKeys
 */
function
octalEventGetKey(e)
{
    octalPhysicalKeysBuildArrays()

    var keys = octalPhysicalKeys;
    var key;

    switch (e.type) {
    case 'keydown':
    case 'keyup':
        if (e.keyIdentifier) {
            /* Standards-based */
            if ((key = octalPhysicalKeysByW3C[e.keyIdentifier]) != undefined) {
                return key;
            }
            if (e.keyIdentifier.substring(0, 2) == 'U+') {
                /* Cannot find key, but it looks like a unicode character */
                return octalPhysicalKeyForChar(
                        parseInt(e.keyIdentifier.substring(2), 16));
            }

            /*
             * Semi-standards based, for Konqueror.
             * This only runs if keyIdentifier exists, but is not in the
             * standard list and does not look like a standard char.
             */
            if ((key = octalPhysicalKeyForChar(e.keyIdentifier.charCodeAt(0)))
                    != undefined)
            {
                return key;
            }

            /* Cannot find key. Return something */
            return {
                Name: e.keyIdentifier,
                W3C: e.keyIdentifer,
                str: '',
                uni: [],
                keycode: []
            };
        }

        if (!e.which) {
            /* IE */
            if ((key = octalPhysicalKeysByKeycode[e.keyCode]) != undefined) {
                return key;
            }
        }
        else {
            /*
             * Non-IE, Non-standard. Assume Mozilla as that's all we really
             * care about for now.
             */
            if ((key = octalPhysicalKeysByKeycode[e.which]) != undefined) {
                if (key.shift && e.shiftKey) {
                    return octalPhysicalKeysByName[key.shift];
                }
                return key;
            }
        }
        break;

    case 'keypress':
    case 'textInput':
        if (e.which === undefined) {
            /* IE all */
            if ((key = octalPhysicalKeyForChar(e.keyCode)) != undefined) {
                return key;
            }
        }
        else if (e.which === 0) {
            /* Other special */
            if ((key = octalPhysicalKeysByKeycode[e.keyCode]) != undefined) {
                return key;
            }
        }
        else if (e.which > 0) {
            /* Other normal key */
            if ((key = octalPhysicalKeyForChar(e.which)) != undefined) {
                return key;
            }
        }
        break;

    default:
        alert('Unexpected event type ' + e.type);
    }

    return null;
}


/** Returns the string that a user has input from pressing a key.
 *
 * \param e The event that the key comes from.
 *
 * The event \p e should be a 'keypress' or 'textInput' event.
 */
function
octalEventGetInput(e)
{
    function isPrint(c) {
        return (c >= 9 && c <= 13)
            || (c >= 32 && c <= 126)
            || (c >= 128);
    }

    switch (e.type) {
    case 'keypress':
    case 'textInput':
        if (e.which == null) {
            // IE
            return isPrint(e.keyCode) ? String.fromCharCode(e.keyCode) : '';
        }
        else if (e.which > 0) {
            // All others.
            return isPrint(e.which) ? String.fromCharCode(e.which) : '';
        }
        break;

    default:
        alert('Unexpected event type ' + e.type);
    }

    return '';
}


/** Do an easy XPath execute.
 *
 * \param elem Element to execute xpath from.
 * \param path Path to find.
 *
 * \returns Array of elements matching the path, empty array if no elements
 * match, or null on error.
 */
function
octalEasyXPath(elem, path)
{
    if (!elem) {
        alert('Null element provided');
        return null;
    }

    var doc;
    if (elem.ownerDocument) {
        doc = elem.ownerDocument;
    }
    else if (elem.nodeType == 9/*Node.DOCUMENT_NODE*/) {
        /*
         * IE6 doesn't define Node or any of the symbolic constants that it's
         * required to for DOM Level 1! LEVEL 1 FOR FSCK'S SAKE! 1997! It
         * doesn't even work if you do (Node && elem.nodeType == Node.DOC...)
         * because it's so crap. Crap crap crap crap crap. The quicker IE
         * dies, the better off the web will be.
         */
        doc = elem;
    }
    else {
        alert('Element does not have a document! (' + elem.nodeType + ')');
        return null;
    }

    var evalfn = doc.evaluate;
    if (!evalfn && document && document.evaluate) {
        evalfn = document.evaluate;
    }
    if (!evalfn) {
        alert('No xpath evaluate() function!');
        return null;
    }

    var elems = [];

    var snapshot = evalfn.call(doc, path, elem,
            null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    for (var j = 0; j < snapshot.snapshotLength; ++j) {
        elems.push(snapshot.snapshotItem(j));
    }

    return elems;
}


/** Register form elements to be enabled when a radio/checkbox is checked.
 *
 * \param sourceElem The radio/checkbox to monitor, or its id.
 * \param targetElemIds Array of ids of elements to enable/disable.
 * \param disableIds Array of ids of elements to disable/enable.
 * \param options Options configuring type of enablement.
 *             hide Boolean. If true then hide targets instead of disabling.
 *
 * A handler is added to source element's "change" event which, when fired,
 * goes through the list of target elements and enables or disables them
 * if the source element is checked or unchecked respectively. It also
 * sets the target elements initial enabled state based on the initial
 * state of the radio/checkbox.
 */
function
octalRegisterCheckEnable(sourceElem, targetElemIds, disableIds, options)
{
    /** Enable or disable a single target,
     *
     * \param target Element to enable/disable
     * \param enable Whether to enable or disable it.
     * \param options Options for enabling/disabling.
     */
    function
    doEnableTarget(target, enable, options)
    {
        if (!target.x_octal) {
            target.x_octal = {};
        }

        if (options.hide) {
            if (enable) {
                target.style.display = target.x_octal.display;
            }
            else {
                target.x_octal.display = target.style.display;
                target.style.display = 'none';
            }
        }
        else {
            target.disabled = !enable;
        }
    }

    /** Enable or disable various elements, based on one element.
     *
     * \param elem Element used to determine which other elements to enable/disable.
     *
     * The elements identified by \p elem's x_octal.checkEnable and
     * x_octal.checkDisable arrays are enabled (or disabled respectively)
     * if \p elem is checked.
     */
    function
    doEnableElem(elem)
    {
        if (!elem
                || !elem.x_octal
                || !elem.x_octal.checkEnable
                || !elem.x_octal.checkDisable
                || !elem.x_octal.enableDisableOptions)
        {
            return;
        }

        var options = elem.x_octal.enableDisableOptions;

        for (var i = 0; i < elem.x_octal.checkEnable.length; ++i) {
            doEnableTarget(elem.x_octal.checkEnable[i], elem.checked, options);
        }
        for (var i = 0; i < elem.x_octal.checkDisable.length; ++i) {
            doEnableTarget(elem.x_octal.checkDisable[i], !elem.checked, options);
        }

        if (elem.checked
                && elem.tagName
                && elem.getAttribute('type')
                && elem.tagName.toUpperCase() == 'INPUT'
                && elem.getAttribute('type').toUpperCase() == 'RADIO')
        {
            /* 
             * Radio buttons suck. They do not emit onChange events if they
             * are changed by being unselected because a different radio
             * button has been selected. Therefore, if we have changed by
             * being selected, then we need to find all the other radio
             * buttons from the same form with the same name, and call their
             * 'doEnableElem' as their onChange deselect.
             */
            var name = elem.getAttribute('name');
            var elems = octalEasyXPath(elem,
                    './ancestor::form/descendant::input[@name="' + name + '"]');
            for (var i = 0; i < elems.length; ++i) {
                if (elems[i] == elem) {
                    /* Don't enable ourselves again */
                    continue;
                }
                if (elems[i].checked) {
                    /*
                     * Do not call if multiple buttons are selected.
                     * That way lies infinite loops!
                     */
                    alert('Multiple buttons from radio ' + name
                            + ' with values ' + elem.value
                            + ' and ' + elems[i].value + ' selected');
                    continue;
                }
                doEnableElem(elems[i]);
            }
        }
    }

    /** Enable or disable various elements, event handler version
     *
     * Just calls doEnableElem() on the event source.
     */
    function
    doEnableEvt(e)
    {
        e = e ? e : window.event;
        doEnableElem(octalEventSrc(this, e));
    }

    var elem;
    if (!sourceElem) {
        alert('octalRegisterCheckEnable: No sourceElem!');
        return;
    }
    else if (sourceElem.nodeType == 1/*ELEMENT_NODE*/) {
        elem = sourceElem;
    }
    else if (typeof(sourceElem) == 'string') {
        elem = document.getElementById(sourceElem);
    }
    else {
        alert('octalRegisterCheckEnable: invalid sourceElem!');
        return;
    }
    if (!elem) {
        alert('Source element ' + sourceElem + ' not found!');
        return;
    }

    if (!elem.x_octal) {
        elem.x_octal = {};
    }
    if (!elem.x_octal.checkEnable) {
        elem.x_octal.checkEnable = [];
    }
    if (!elem.x_octal.checkDisable) {
        elem.x_octal.checkDisable = [];
    }
    elem.x_octal.enableDisableOptions = options ? options : {};

    if (!elem.x_octal.checkEnable.push) {
        alert('Cannot push to checkEnable for elem ' + sourceElem);
        return;
    }
    for (var i = 0; i < targetElemIds.length; ++i) {
        var targetElem = elem.ownerDocument.getElementById(targetElemIds[i]);
        if (!targetElem) {
            alert('Target element ' + targetElemIds[i] + ' not found for '
                    + sourceElem);
            continue;
        }
        elem.x_octal.checkEnable.push(targetElem);
    }
    if (disableIds) {
        for (var i = 0; i < disableIds.length; ++i) {
            var targetElem = elem.ownerDocument.getElementById(disableIds[i]);
            if (!targetElem) {
                alert('Target element ' + disableIds[i] + ' not found for '
                        + sourceElem);
                continue;
            }
            elem.x_octal.checkDisable.push(targetElem);
        }
    }

    octalAddEventListener(elem, "change", doEnableEvt);

    doEnableElem(elem);

    return;
}


/* Synchronous XML HTTP Request.
 *
 * Yes, I know synchronous takes time and introduces latency, but there are
 * some cases where you *need* to wait for the function to return;
 */
function
octalXmlHttpSync(url)
{
    var request = window.XMLHttpRequest
        ? new XMLHttpRequest()
        : new ActiveXObject("MSXML2.XMLHTTP.3.0");
    request.open("GET", url, false);
    request.send(null);
    return request;
}


function
octalXmlHttpASync(url, callback)
{
    var request = window.XMLHttpRequest
        ? new XMLHttpRequest()
        : new ActiveXObject("MSXML2.XMLHTTP.3.0");
    request.open("GET", url, true);
    request.onreadystatechange = function () {
        callback(request);
    }
    request.send(null);
    return request;
}


function
octalJSArrayFromXMLNodeList(nodelist)
{
    var arr = [];
    if (!nodelist || !nodelist.length) {
        return arr;
    }
    for (var i = 0; i < nodelist.length; ++i) {
        arr.push(nodelist.item(i));
    }
    return arr;
}


/* CLASS_MANIPULATION */

/* Provides list of classes for element */
function
octalClass_getlist(element)
{
    if (element.className) return element.className.split(/\s+/);
    return [];
}


/* Sets element's class list */
function
octalClass_setlist(element, classes)
{
    element.className = classes.join(' ');
}


/* Is element of this class */
function
octalClass_inlist(element, classname)
{
    var classes = octalClass_getlist(element);
    return classes.indexOf(classname) > -1;
}


/* Add class to element */
function
octalClass_add(element, classname)
{
    var classes = octalClass_getlist(element);
    if (classes.indexOf(classname) == -1) {
        classes[classes.length] = classname;
    }
    octalClass_setlist(element, classes);
}


/* Remove class from element */
function
octalClass_remove(element, classname)
{
    var classes = octalClass_getlist(element), index;
    if ((index = classes.indexOf(classname)) > -1) {
        delete classes[index];
    }
    octalClass_setlist(element, classes);
}


/* Replace a class for an element */
function
octalClass_replace(element, oldclass, newclass)
{
    var classes = octalClass_getlist(element), index;
    if ((index = classes.indexOf(oldclass)) > -1
            && classes.indexOf(newclass) == -1)
    {
        classes[index] = newclass;
    }
    octalClass_setlist(element, classes);
}

/* WINDOW_PROPERTIES */

/** \brief Gets X coord of mouse for the given event
 *
 * \param e The event
 */
function octal_mouse_x(e) {
    var mousex = 0;
    if (!e) var e = window.event;
    if (e.pageX) {
        mousex = e.pageX;
    }
    else if (e.clientX) {
        mousex = e.clientX + document.body.scrollLeft
                + document.documentElement.scrollLeft;
    }
    return mousex;
}

/** \brief Gets Y coord of mouse for the given event
 *
 * \param e The event
 */
function octal_mouse_y(e) {
    var mousey = 0;
    if (!e) var e = window.event;
    if (e.pageY) {
        mousey = e.pageY;
    }
    else if (e.clientY) {
        mousey = e.clientY + document.body.scrollTop
                + document.documentElement.scrollTop;
    }
    return mousey;
}

/** \brief Provides width of the inner window (actual page real estate)
 */
function octal_window_x() {
    if (self.innerWidth) {
        return self.innerWidth;
    }
    else if (document.documentElement && document.documentElement.clientWidth) {
        return document.documentElement.clientWidth;
    }
    else if (document.body) {
        return document.body.clientWidth;
    }
}

/** \brief Provides height of the inner window (actual page real estate)
 */
function octal_window_y() {
    if (self.innerHeight) {
        return self.innerHeight;
    }
    else if (document.documentElement
        && document.documentElement.clientHeight)
    {
        return document.documentElement.clientHeight;
    }
    else if (document.body) {
        return document.body.clientHeight;
    }
}

/** \brief Determines how far down the page the scroll bar has moved
 */
function octal_window_scrolltop() {
	if (window.innerHeight) {
		return window.pageYOffset;
	}
	else if (document.documentElement && document.documentElement.scrollTop) {
		return document.documentElement.scrollTop;
	}
	else if (document.body) {
		return document.body.scrollTop;
	}

	return 0;
}

/** \brief Determines how far across the page the scroll bar has moved
 */
function octal_window_scrollleft() {
    if (window.innerWidth) {
        return window.pageXOffset;
    }
    else if (document.documentElement && document.documentElement.scrollLeft) {
        return document.documentElement.scrollLeft;
    }
    else if (document.body) {
        return document.body.scrollLeft;
    }

    return 0;
}


/** Get the text content of a DOM node and all its descendants.
 */
function
octalDomText(node)
{
    var text = '';
    if (!node || !node.nodeType) {
        return text;
    }
    switch (node.nodeType) {
    case 1:  /*ELEMENT_NODE*/
        for (var i = 0; i < node.childNodes.length; ++i) {
            text += octalDomText(node.childNodes.item(i));
        }
        break;

    case 3:  /*TEXT_NODE*/
    case 4:  /*CDATA_SECTION_NODE*/
        text += node.data;
        break;

    case 9:  /*DOCUMENT_NODE*/
        text = octalDomText(node.documentElement);
        break;

    case 2:  /*ATTRIBUTE_NODE*/
    case 5:  /*ENTITY_REFERENCE_NODE*/
    case 6:  /*ENTITY_NODE*/
    case 7:  /*PROCESSING_INSTRUCTION_NODE*/
    case 8:  /*COMMENT_NODE*/
    case 10: /*DOCUMENT_TYPE_NODE*/
    case 11: /*DOCUMENT_FRAGMENT_NODE*/
    case 12: /*NOTATION_NODE*/
        break;
    }

    return text;
}


/** Add a hover "menu" to an element.
 *
 * \param source Source DOM element to add menu to.
 * \param menu DOM element to popup on hover.
 * \param options Object of named options that can be passed. May contain:
 *      overlap Overlap, in pixels, of the submenu with its parent. (Default: 0)
 *      vertical Pop the menu up vertically. (Default: false = horizontal)
 *      expandmargin Increase the margin of the parent item when the child pops.
 *          (Default: false)
 *      expand Event to exapand on. 'hover' or 'click' (Default: 'hover')
 *      initialexpanded Should the item be expanded straight away (Default: false)
 *      onShow Function to call after menu is shown. (No default function)
 *      onHide Function to call after menu is hidden. (No default function)
 *
 * The menu doesn't really have to be a menu, it can be any DOM node.
 *
 * The source node should be a CSS containing block. This means it should have
 * its "position" property set to "relative" or "absolute" (or "float"?)
 *
 * The onShow and onHide functions are passed parameters "source" and "menu".
 */
function
octalHovermenuAdd(source, menu, options)
{
    var elemstohide = [];
    var timeoutid = null;
    var delay = 100;


    /** Hide flyout menu. */
    function
    elementHide(source)
    {
        var hovermenu = source.x_octal.hovermenu;

        // Alter margins if necessary
        if (hovermenu.options.expandmargin) {
            if (hovermenu.options.vertical) {
                source.style.marginBottom
                    = (parseInt(source.style.marginBottom)
                            - parseInt(hovermenu.menu.offsetHeight)) + 'px';
            }
            else {
                source.style.marginRight
                    = (parseInt(source.style.marginRight)
                            - parseInt(hovermenu.menu.offsetWidth)) + 'px';
            }
        }

        // Hide the elements
        hovermenu.menu.style.display = "none";

        octalClass_remove(source, 'expanded');

        if (hovermenu.options.onHide) {
            hovermenu.options.onHide(source, hovermenu.menu);
        }
    }


    /* Show flyout menu */
    function
    elementShow(source)
    {
        var hovermenu = source.x_octal.hovermenu;

        var l = 0;
        var t = 0;
        if (hovermenu.options.vertical) {
            t += parseInt(source.offsetHeight) - hovermenu.options.overlap;
        }
        else {
            l += parseInt(source.offsetWidth) - hovermenu.options.overlap;
        }

        // TODO: Alter this if it would put the submenu outside the
        // page bounds?
        hovermenu.menu.style.left = l + "px";
        hovermenu.menu.style.top = t + "px";
        hovermenu.menu.style.zIndex = "100";

        // Show the element.
        hovermenu.menu.style.display = "block";

        // Alter margins if necessary
        if (hovermenu.options.expandmargin) {
            if (hovermenu.options.vertical) {
                source.style.marginBottom
                    = (parseInt(source.style.marginBottom)
                            + parseInt(hovermenu.menu.offsetHeight)) + 'px';
            }
            else {
                source.style.marginRight
                    = (parseInt(source.style.marginRight)
                            + parseInt(hovermenu.menu.offsetWidth)) + 'px';
            }
        }

        octalClass_add(source, 'expanded');

        if (hovermenu.options.onShow) {
            hovermenu.options.onShow(source, hovermenu.menu);
        }
    }


    function
    elementIsShown(source)
    {
        var hovermenu = source.x_octal.hovermenu;

        return hovermenu.menu.style.display != "none";
    }


    function
    elementHideTimeout()
    {
        // Hide elements.
        for (var i = 0; i < elemstohide.length; ++i) {
            if (elemstohide[i]) {
                elementHide(elemstohide[i]);
            }
        }
        // Clear list of elements to hide and timeout id.
        elemstohide = [];
        timeoutid = null;
    }


    /** Event handler for the "mouseover" event of the source item
     *
     * Used for hover menus.
     */
    function
    onMouseover(e)
    {
        e = e ? e : window.event;
        var source = octalEventSrc(this, e);
        while (source && !source.x_octal) {
            source = source.parentNode;
        }
        if (!source) {
            return;
        }

        elementShow(source);

        // Make sure we're not due to hide this hovermenu.
        for (var i = 0; i < elemstohide.length; ++i) {
            if (elemstohide[i] == source) {
                elemstohide[i] = null;
            }
        }

        // And set the timeout to hide any hovermenus that need it.
        if (timeoutid) {
            clearTimeout(timeoutid);
        }
        timeoutid = window.setTimeout(elementHideTimeout, delay);
    }

    /** Event handler for the "mouseout" event of the source item
     *
     * Used for hover menus.
     */
    function
    onMouseout(e)
    {
        e = e ? e : window.event;
        var source = octalEventSrc(this, e);
        while (source && !source.x_octal) {
            source = source.parentNode;
        }
        if (!source) {
            return;
        }

        // Set this element as due to be hidden.
        elemstohide.push(source);

        // Set the timeout to hide hovermenus that need it (e.g. this one).
        if (timeoutid) {
            clearTimeout(timeoutid);
        }
        timeoutid = window.setTimeout(elementHideTimeout, delay);
    }

    /** Event handler for the "click" event of the source item.
     *
     * Used for click-to-show/hide menus.
     */
    function onClick(e)
    {
        e = e ? e : window.event;
        var source = octalEventSrc(this, e);
        if (!source.x_octal) {
            return;
        }

        if (elementIsShown(source)) {
            elementHide(source);
        }
        else {
            elementShow(source);
        }
    }


    if (!source || !menu) {
        return;
    }

    if (!source.style.marginBottom) {
        source.style.marginBottom = '0px';
    }
    if (!source.style.marginRight) {
        source.style.marginRight = '0px';
    }

    menu.style.display = "none";
    menu.style.position = "absolute";
    source.appendChild(menu);

    if (!source.x_octal) {
        source.x_octal = {};
    }
    if (!source.x_octal.hovermenu) {
        source.x_octal.hovermenu = {};
    }
    var hovermenu = source.x_octal.hovermenu;
    hovermenu.menu = menu;
    hovermenu.options = options ? options : {}

    if (!hovermenu.options.overlap) {
        /* Required as adding/subtracting undefined/null is a problem */
        hovermenu.options.overlap = 0;
    }

    if (hovermenu.options.expand == 'click') {
        octalAddEventListener(source, "click", onClick);
    }
    else {
        octalAddEventListener(source, "mouseover", onMouseover);
        octalAddEventListener(source, "mouseout", onMouseout);
    }

    if (hovermenu.options.initialexpanded) {
        elementShow(source);
    }
}


/** Create a hovermenu DOM node from an array of XML menu items.
 *
 * \param doc Document to create hover menu DOM node in.
 * \param xmlmenuitems Array of XML nodes describing the menu.
 */
function octalHovermenuFromMenuitems(doc, menuitems)
{
    var hovermenu = doc.createElement('div');
    octalClass_add(hovermenu, 'flyoutmenu');

    for (var i = 0; i < menuitems.length; ++i) {
        var menuitem = menuitems[i];

        var hoveritem = hovermenu.appendChild(doc.createElement('div'));
        octalClass_add(hoveritem, 'flyoutitem');

        var link = hoveritem.appendChild(doc.createElement('a'));
        link.setAttribute('href', menuitem.getAttribute('href'));
        if (menuitem.getAttribute('target')) {
            link.setAttribute('target', menuitem.getAttribute('target'));
        }
        link.appendChild(
                doc.createTextNode(menuitem.getAttribute('title')));
    }

    return hovermenu;
}


/** Find an XML menu sub item by name.
 *
 * \param menuitem XML menu item
 * \param name Name of sub menu item to find.
 */
function
octalHovermenuFind(menuitem, name)
{
    if (!menuitem
            || !menuitem.childNodes)
    {
        return null;
    }

    for (var i = 0; i < menuitem.childNodes.length; ++i) {
        var subitem = menuitem.childNodes.item(i);
        if (subitem
                /* && subitem.getAttribute -- IE breaks on this. WTF? */
                && subitem.nodeType == 1/*ELEMENT_NODE*/
                && subitem.getAttribute('title') == name)
        {
            return subitem;
        }
    }

    return null;
}


/** Get an array of XML menu sub items from an XML menu item.
 */
function
octalHovermenuSubitems(menuitem)
{
    if (!menuitem
            || !menuitem.childNodes)
    {
        return null;
    }

    var subitems = [];
    for (var i = 0; i < menuitem.childNodes.length; ++i) {
        var subitem = menuitem.childNodes.item(i);
        if (subitem.tagName
                && subitem.tagName.toLowerCase() == 'menu')
        {
            subitems.push(subitem);
        }
    }
    return subitems;
}


function
octalStringWordCount(str)
{
    var words = 0;
    var inword = false;

    for (var i = 0; i < str.length; ++i) {
        switch (str.charCodeAt(i)) {
        case 0x0008:    // Backspace
        case 0x0009:    // Horizontal tab
        case 0x000A:    // Line feed
        case 0x000B:    // Vertical tab
        case 0x000C:    // Form feed
        case 0x000D:    // Carriage return
        case 0x0020:    // Space
        case 0x007F:    // DEL
        case 0x0085:    // NEL (control character next line)
        case 0x00A0:    // NBSP (NO-BREAK SPACE)
        case 0x1680:    // OGHAM SPACE MARK
        case 0x180E:    // MONGOLIAN VOWEL SEPARATOR
        case 0x2000:    // EN QUAD
        case 0x2001:    // EM QUAD
        case 0x2002:    // EN SPACE
        case 0x2003:    // EM SPACE
        case 0x2004:    // THREE-PER-EM SPACE
        case 0x2005:    // FOUR-PER-EM SPACE
        case 0x2006:    // SIX-PER-EM SPACE
        case 0x2007:    // FIGURE SPACE
        case 0x2008:    // PUNCTUATION SPACE
        case 0x2009:    // THIN SPACE
        case 0x200A:    // HAIR SPACE
        case 0x200B:    // Zero Width Space
        case 0x200C:    // Zero Width Non Joiner
        case 0x200D:    // Zero Width Joiner
        case 0x2028:    // LS (LINE SEPARATOR)
        case 0x2029:    // PS (PARAGRAPH SEPARATOR)
        case 0x202F:    // NNBSP (NARROW NO-BREAK SPACE)
        case 0x205F:    // MMSP (MEDIUM MATHEMATICAL SPACE)
        case 0x2060:    // Word Joiner
        case 0x3000:    // IDEOGRAPHIC SPACE
        case 0xFEFF:    // Zero Width No-Break Space
            inword = false;
            break;

        default:
            if (!inword) {
                inword = true;
                ++words;
            }
            break;
        }
    }

    return words;
}


/** Add a word counter to a web page.
 *
 * \param source Element to get word count of
 * \param dest Element to write word count into.
 * \param options Various options for word counts.
 *             pretext Text to put before word count.
 *             posttext Text to put after word count.
 */
function
octalAddWordCounter(source, dest, options)
{
    function
    setWordCount(source)
    {
        var wordcounter = source.x_octal.wordcounter;
        if (!wordcounter) {
            return;
        }

        var str = '';
        if (wordcounter.options.pretext) {
            str += wordcounter.options.pretext;
        }
        str += octalStringWordCount(source.value);
        if (wordcounter.options.posttext) {
            str += wordcounter.options.posttext;
        }

        while (wordcounter.dest.firstChild) {
            wordcounter.dest.removeChild(wordcounter.dest.firstChild);
        }
        wordcounter.dest.appendChild(source.ownerDocument.createTextNode(str));
    }

    var wordcountDelay = 500;
    var wordcountTimer = null;
    var wordcountSource = source;

    function
    wordcountTimeout() {
        wordcountTimer = null;
        setWordCount(wordcountSource);
    }

    function
    onKeyUp(e) {
        e = e ? e : window.event;
        var source = octalEventSrc(this, e);
        if (!source) {
            return;
        }
        if (wordcountTimer) {
            clearTimeout(wordcountTimer);
        }
        wordcountTimer = window.setTimeout(wordcountTimeout, wordcountDelay);
    }


    if (!source || !dest) {
        return;
    }

    if (!source.x_octal) {
        source.x_octal = {};
    }
    if (!source.x_octal.wordcounter) {
        source.x_octal.wordcounter = {};
    }
    var wordcounter = source.x_octal.wordcounter;
    wordcounter.dest = dest;
    wordcounter.options = options ? options : {}

    octalAddEventListener(source, "keyup", onKeyUp);

    setWordCount(source);
}


/** Format a string with {0}-style substitutions
 *
 * \param f Format string
 *
 * Other parameters are used to substitute into {0}, {1}, etc...
 * Parameters can be used multiple times.
 */
function
octalFormat(f)
{
    var res = '';

    // Position of start of segment of normal text
    var a;
    // Position of { of {nnn} segment, which is also one-past-the-last character
    // of the normal text
    var b;
    // Position of } or first invalid char of {nnn} segment, which is also
    // one before the next run of normal text.
    var c;

    a = 0;
    while ((b = f.indexOf('{', a)) != -1) {
        // Have a and b. Append the run of normal text to "res"
        res += f.substring(a, b);

        // Get c. Also get number n from {n}, and ch = f[c]
        var ch = 0;
        var n = 0;
        for (c = b + 1; c < f.length; ++c) {
            ch = f.charAt(c);
            if (ch < '0' || ch > '9') {
                break;
            }
            n *= 10;
            n += parseInt(ch);
        }

        // Got all that. Now append whatever is required to res, and set "a" to the
        // start of the next run of normal text.
        if (c >= f.length) {
            // Unterminated format specifier 
            res += '{';
            a = b + 1;
        }
        else if (ch == '{' && c == b + 1) {
            // Double "{{"
            res += '{';
            a = c + 1;
        }
        else if (ch != '}') {
            // Unexpected character in format specifier
            res += '{';
            a = b + 1;
        }
        else if (n + 1 >= arguments.length) {
            // Format specifier greater than number of parameters args.
            res += '{';
            a = b + 1;
        }
        else {
            res += arguments[n + 1];
            a = c + 1;
        }
    }

    // Append the final part of f to res.
    if (a < f.length) {
        res += f.substring(a, f.length);
    }

    return res;
}


/** I hate IE. */
function
octalFormInputElementCreate(doc, attrs)
{
    var input;

    if (octalIE == 6 || octalIE == 7) {
        if (attrs['name']) {
            input = doc.createElement('<input name=\'' + attrs['name'] + '\'>');
        }
        else {
            input = doc.createElement('input');
        }
        for (var attr in attrs) {
            switch (attr) {
            case 'name':
                // Ignore.
                break;

            case 'class':
                octalClass_add(attrs[attr]);
                break;

            case 'checked':
                if (attrs[attr] == 'checked') {
                    input.checked = true;
                }
                break;

            case 'readonly':
                if (attrs[attr] == 'readonly') {
                    input.readOnly = true;
                }
                break;

            default:
                input[attr] = attrs[attr];
                break;
            }
        }
        return input;
    }

    /** Standards compliant version */
    input = doc.createElement('input');
    for (var attr in attrs) {
        if (attrs[attr]) {
            input.setAttribute(attr, attrs[attr]);
        }
    }
    return input;
}


/** Returns the values for an individual form element
 *
 * Returns an array of values, or null if there are none.
 *
 * An array is required to correctly return the set of values for a
 * multi-select box.
 */
function
octalFormElementValues(elem)
{
    if (!elem || !elem.type) {
        return null;
    }

    var values = [];
    switch (elem.type) {
    case 'button':
    case 'image':
    case 'submit':
    case 'reset':
        /* Can't really do anything for these */
        break;

    case 'file':
        /* Don't know what to do */
        break;

    case 'hidden':
    case 'text':
    case 'password':
    case 'textarea':
        values.push(elem.value);
        break;

    case 'checkbox':
        if (elem.checked) {
            values.push(elem.value);
        }
        break;

    case 'radio':
        for (var i = 0; i < elem.length; ++i) {
            var item = elem.item(i);
            if (item.checked) {
                values.push(item.value);
            }
        }
        break;

    case 'select-one':
    case 'select-multiple':
        for (var i = 0; i < elem.length; ++i) {
            var item = elem.item(i);
            if (item.selected) {
                values.push(item.value);
            }
        }
        break;

    default:
        alert('Unexpected form element type ' + elem.type);
        break;
    }

    if (!values.length) {
        return null;
    }

    return values;
}


/** Returns the values for all the elements in a form */
function
octalFormValues(form)
{
    if (!form) {
        return null;
    }

    var values = {};
    for (var i = 0; i < form.length; ++i) {
        var elem = form.elements[i];
        var name = elem.name;
        var val = octalFormElementValues(elem);

        if (name && val) {
            values[name] = val;
        }
    }
    return values;
}


/** Set the values of a form element.
 *
 * \param elem Element to set values of
 * \param values Array of values to set.
 */
function
octalFormElementValuesSet(elem, values)
{
    if (!elem || !elem.type) {
        return null;
    }

    switch (elem.type) {
    case 'button':
    case 'image':
    case 'submit':
    case 'reset':
        /* Can't really do anything for these */
        break;

    case 'file':
        /* Don't know what to do */
        break;

    case 'hidden':
    case 'text':
    case 'password':
    case 'textarea':
        elem.value = '';
        for (var i = 0; i < (values ? values.length : 0); ++i) {
            elem.value += values[i];
        }
        break;

    case 'checkbox':
        elem.checked = false;
        for (var i = 0; i < (values ? values.length : 0); ++i) {
            if (elem.value == values[i]) {
                elem.checked = true;
            }
        }
        break;

    case 'radio':
        for (var i = 0; i < elem.length; ++i) {
            var item = elem.item(i);
            item.checked = false;
            for (var j = 0; j < (values ? values.length : 0); ++j) {
                if (item.value == values[j]) {
                    item.checked = true;
                }
            }
        }
        break;

    case 'select-one':
    case 'select-multiple':
        for (var i = 0; i < elem.length; ++i) {
            var item = elem.item(i);
            item.selected = false;
            for (var j = 0; j < (values ? values.length : 0); ++j) {
                if (item.value == values[j]) {
                    item.selected = true;
                }
            }
        }
        break;

    default:
        alert('Unexpected form element type ' + elem.type);
        break;
    }

    return;
}

