Вход Регистрация
Файл: scripts/loki.js
Строк: 31882
// Loki WYSIWIG Editor 2.0.4
// Copyright (c) 2006 Carleton College

// Compiled 2010-01-12 11:39:53 
// http://loki-editor.googlecode.com/


// file TinyMCE.js
/**
 * $RCSfile: tiny_mce_src.js,v $
 * $Revision: 1.233 $
 * $Date: 2005/08/26 15:20:32 $
 *
 * @author Moxiecode. Extracts made by NF starting 2005/10/14.
 * @copyright Copyright © 2004, Moxiecode Systems AB, All rights reserved.
 */
function TinyMCE() {};

/**
 * This new function written by NF to integrate with Loki.
 */
TinyMCE.prototype.init = function(win, selectedInstance)
{
    this.contentWindow = win;
    this.selectedInstance = selectedInstance;
    this.settings = { force_p_newlines : true };
    this.isGecko = true; // because we only call any of this from Gecko
    this.blockRegExp = new RegExp("^(h[1-6]|p|div|address|pre|form|table|li|ol|ul|td|blockquote)$", "i"); // nf: added blockquote
};

TinyMCE.prototype.isBlockElement = function(node) {
        return node != null && node.nodeType == 1 && this.blockRegExp.test(node.nodeName);
};

TinyMCE.prototype.getParentBlockElement = function(node) {
    // Search up the tree for block element
    while (node) {
        if (this.blockRegExp.test(node.nodeName))
            return node;

        node = node.parentNode;
    }

    return null;
};

TinyMCE.prototype.getNodeTree = function(node, node_array, type, node_name) {
    if (typeof(type) == "undefined" || node.nodeType == type && (typeof(node_name) == "undefined" || node.nodeName == node_name))
        node_array[node_array.length] = node;

    if (node.hasChildNodes()) {
        for (var i=0; i<node.childNodes.length; i++)
            tinyMCE.getNodeTree(node.childNodes[i], node_array, type, node_name);
    }

    return node_array;
};

TinyMCE.prototype.getAbsPosition = function(node) {
    var pos = {absLeft: 0, absTop: 0};
    
    // if (node.nodeType != 1)
    //     node = node.parentNode;
    for (var n = node; n; n = n.offsetParent) {
        pos.absLeft += n.offsetLeft;
        pos.absTop += n.offsetTop;
    }
    
    return pos;
};

TinyMCE.prototype.cancelEvent = function(e) {
    if (tinyMCE.isMSIE) {
        e.returnValue = false;
        e.cancelBubble = true;
    } else
        e.preventDefault();
};


TinyMCE.prototype.handleEvent = function(e) {
    tinyMCE = this; // NF: because we don't want a global

    // Remove odd, error
    if (typeof(tinyMCE) == "undefined")
        return true;

    //tinyMCE.debug(e.type + " " + e.target.nodeName + " " + (e.relatedTarget ? e.relatedTarget.nodeName : ""));

    switch (e.type) {
        case "blur":
            if (tinyMCE.selectedInstance)
                tinyMCE.selectedInstance.execCommand('mceEndTyping');

            return;

        case "submit":
            tinyMCE.removeTinyMCEFormElements(tinyMCE.isMSIE ? window.event.srcElement : e.target);
            tinyMCE.triggerSave();
            tinyMCE.isNotDirty = true;
            return;

        case "reset":
            var formObj = tinyMCE.isMSIE ? window.event.srcElement : e.target;

            for (var i=0; i<document.forms.length; i++) {
                if (document.forms[i] == formObj)
                    window.setTimeout('tinyMCE.resetForm(' + i + ');', 10);
            }

            return;

        case "keypress":
            /* NF: irrelevant
            if (e.target.editorId) {
                tinyMCE.selectedInstance = tinyMCE.instances[e.target.editorId];
            } else {
                if (e.target.ownerDocument.editorId)
                    tinyMCE.selectedInstance = tinyMCE.instances[e.target.ownerDocument.editorId];
            }

            if (tinyMCE.selectedInstance)
                tinyMCE.selectedInstance.switchSettings();
            */

            // Insert space instead of &nbsp;
            /*            
            if (tinyMCE.isGecko && e.charCode == 32) {
                if (tinyMCE.selectedInstance._insertSpace()) {
                    // Cancel event
                    e.preventDefault();
                    return false;
                }
            }
            */

            //Util.Object.print_r(tinyMCE);
            //alert(tinyMCE.settings['force_p_newlines']);

            // Insert P element
            if (tinyMCE.isGecko && tinyMCE.settings['force_p_newlines'] && e.keyCode == 13 && !e.shiftKey) {
                // Insert P element instead of BR
                if (tinyMCE.selectedInstance._insertPara(e)) {
                    // Cancel event
                    //tinyMCE.execCommand("mceAddUndoLevel"); // NF: irrelevant
                    tinyMCE.cancelEvent(e);
                    return false;
                }
            }

            // Handle backspace
            if (tinyMCE.isGecko && tinyMCE.settings['force_p_newlines'] && (e.keyCode == 8 || e.keyCode == 46) && !e.shiftKey) {
                // Insert P element instead of BR
                if (tinyMCE.selectedInstance._handleBackSpace(e.type)) {
                    // Cancel event
                    //tinyMCE.execCommand("mceAddUndoLevel"); // NF: irrelevant
                    e.preventDefault();
                    return false;
                }
            }
/*
            // Mozilla custom key handling
            if (tinyMCE.isGecko && e.ctrlKey && tinyMCE.settings['custom_undo_redo']) {
                if (tinyMCE.settings['custom_undo_redo_keyboard_shortcuts']) {
                    if (e.charCode == 122) { // Ctrl+Z
                        tinyMCE.selectedInstance.execCommand("Undo");

                        // Cancel event
                        e.preventDefault();
                        return false;
                    }

                    if (e.charCode == 121) { // Ctrl+Y
                        tinyMCE.selectedInstance.execCommand("Redo");

                        // Cancel event
                        e.preventDefault();
                        return false;
                    }
                }

                if (e.charCode == 98) { // Ctrl+B
                    tinyMCE.selectedInstance.execCommand("Bold");

                    // Cancel event
                    e.preventDefault();
                    return false;
                }

                if (e.charCode == 105) { // Ctrl+I
                    tinyMCE.selectedInstance.execCommand("Italic");

                    // Cancel event
                    e.preventDefault();
                    return false;
                }

                if (e.charCode == 117) { // Ctrl+U
                    tinyMCE.selectedInstance.execCommand("Underline");

                    // Cancel event
                    e.preventDefault();
                    return false;
                }
            }

            // Return key pressed
            if (tinyMCE.isMSIE && tinyMCE.settings['force_br_newlines'] && e.keyCode == 13) {
                if (e.target.editorId)
                    tinyMCE.selectedInstance = tinyMCE.instances[e.target.editorId];

                if (tinyMCE.selectedInstance) {
                    var sel = tinyMCE.selectedInstance.getDoc().selection;
                    var rng = sel.createRange();

                    if (tinyMCE.getParentElement(rng.parentElement(), "li") != null)
                        return false;

                    // Cancel event
                    e.returnValue = false;
                    e.cancelBubble = true;

                    // Insert BR element
                    rng.pasteHTML("<br />");
                    rng.collapse(false);
                    rng.select();

                    tinyMCE.execCommand("mceAddUndoLevel");
                    tinyMCE.triggerNodeChange(false);
                    return false;
                }
            }

            // Backspace or delete
            if (e.keyCode == 8 || e.keyCode == 46) {
                tinyMCE.selectedElement = e.target;
                tinyMCE.linkElement = tinyMCE.getParentElement(e.target, "a");
                tinyMCE.imgElement = tinyMCE.getParentElement(e.target, "img");
                tinyMCE.triggerNodeChange(false);
            }

            return false;
        break;

        case "keyup":
        case "keydown":
            if (e.target.editorId)
                tinyMCE.selectedInstance = tinyMCE.instances[e.target.editorId];
            else
                return;

            if (tinyMCE.selectedInstance)
                tinyMCE.selectedInstance.switchSettings();

            var inst = tinyMCE.selectedInstance;

            // Handle backspace
            if (tinyMCE.isGecko && tinyMCE.settings['force_p_newlines'] && (e.keyCode == 8 || e.keyCode == 46) && !e.shiftKey) {
                // Insert P element instead of BR
                if (tinyMCE.selectedInstance._handleBackSpace(e.type)) {
                    // Cancel event
                    tinyMCE.execCommand("mceAddUndoLevel");
                    e.preventDefault();
                    return false;
                }
            }

            tinyMCE.selectedElement = null;
            tinyMCE.selectedNode = null;
            var elm = tinyMCE.selectedInstance.getFocusElement();
            tinyMCE.linkElement = tinyMCE.getParentElement(elm, "a");
            tinyMCE.imgElement = tinyMCE.getParentElement(elm, "img");
            tinyMCE.selectedElement = elm;

            // Update visualaids on tabs
            if (tinyMCE.isGecko && e.type == "keyup" && e.keyCode == 9)
                tinyMCE.handleVisualAid(tinyMCE.selectedInstance.getBody(), true, tinyMCE.settings['visual'], tinyMCE.selectedInstance);

            // Run image/link fix on Gecko if diffrent document base on paste
            if (tinyMCE.isGecko && tinyMCE.settings['document_base_url'] != "" + document.location.href && e.type == "keyup" && e.ctrlKey && e.keyCode == 86)
                tinyMCE.selectedInstance.fixBrokenURLs();

            // Fix empty elements on return/enter, check where enter occured
            if (tinyMCE.isMSIE && e.type == "keydown" && e.keyCode == 13)
                tinyMCE.enterKeyElement = tinyMCE.selectedInstance.getFocusElement();

            // Fix empty elements on return/enter
            if (tinyMCE.isMSIE && e.type == "keyup" && e.keyCode == 13) {
                var elm = tinyMCE.enterKeyElement;
                if (elm) {
                    var re = new RegExp('^HR|IMG|BR$','g'); // Skip these
                    var dre = new RegExp('^H[1-6]$','g'); // Add double on these

                    if (!elm.hasChildNodes() && !re.test(elm.nodeName)) {
                        if (dre.test(elm.nodeName))
                            elm.innerHTML = "&nbsp;&nbsp;";
                        else
                            elm.innerHTML = "&nbsp;";
                    }
                }
            }

            // Check if it's a position key
            var keys = tinyMCE.posKeyCodes;
            var posKey = false;
            for (var i=0; i<keys.length; i++) {
                if (keys[i] == e.keyCode) {
                    posKey = true;
                    break;
                }
            }

            //tinyMCE.debug(e.keyCode);

            // MSIE custom key handling
            if (tinyMCE.isMSIE && tinyMCE.settings['custom_undo_redo']) {
                var keys = new Array(8,46); // Backspace,Delete
                for (var i=0; i<keys.length; i++) {
                    if (keys[i] == e.keyCode) {
                        if (e.type == "keyup")
                            tinyMCE.triggerNodeChange(false);
                    }
                }

                if (tinyMCE.settings['custom_undo_redo_keyboard_shortcuts']) {
                    if (e.keyCode == 90 && e.ctrlKey && e.type == "keydown") { // Ctrl+Z
                        tinyMCE.selectedInstance.execCommand("Undo");
                        tinyMCE.triggerNodeChange(false);
                    }

                    if (e.keyCode == 89 && e.ctrlKey && e.type == "keydown") { // Ctrl+Y
                        tinyMCE.selectedInstance.execCommand("Redo");
                        tinyMCE.triggerNodeChange(false);
                    }

                    if ((e.keyCode == 90 || e.keyCode == 89) && e.ctrlKey) {
                        // Cancel event
                        e.returnValue = false;
                        e.cancelBubble = true;
                        return false;
                    }
                }
            }

            // Handle Undo/Redo when typing content

            // Start typing (non position key)
            if (!posKey && e.type == "keyup")
                tinyMCE.execCommand("mceStartTyping");

            // End typing (position key) or some Ctrl event
            if (e.type == "keyup" && (posKey || e.ctrlKey))
                tinyMCE.execCommand("mceEndTyping");

            if (posKey && e.type == "keyup")
                tinyMCE.triggerNodeChange(false);
        break;

        case "mousedown":
        case "mouseup":
        case "click":
        case "focus":
            if (tinyMCE.selectedInstance)
                tinyMCE.selectedInstance.switchSettings();

            // Check instance event trigged on
            var targetBody = tinyMCE.getParentElement(e.target, "body");
            for (var instanceName in tinyMCE.instances) {
                if (typeof(tinyMCE.instances[instanceName]) == 'function')
                    continue;

                var inst = tinyMCE.instances[instanceName];

                // Reset design mode if lost (on everything just in case)
                inst.autoResetDesignMode();

                if (inst.getBody() == targetBody) {
                    tinyMCE.selectedInstance = inst;
                    tinyMCE.selectedElement = e.target;
                    tinyMCE.linkElement = tinyMCE.getParentElement(tinyMCE.selectedElement, "a");
                    tinyMCE.imgElement = tinyMCE.getParentElement(tinyMCE.selectedElement, "img");
                    break;
                }
            }

            if (tinyMCE.isSafari) {
                tinyMCE.selectedInstance.lastSafariSelection = tinyMCE.selectedInstance.getBookmark();
                tinyMCE.selectedInstance.lastSafariSelectedElement = tinyMCE.selectedElement;

                var lnk = tinyMCE.getParentElement(tinyMCE.selectedElement, "a");

                // Patch the darned link
                if (lnk && e.type == "mousedown") {
                    lnk.setAttribute("mce_real_href", lnk.getAttribute("href"));
                    lnk.setAttribute("href", "javascript:void(0);");
                }

                // Patch back
                if (lnk && e.type == "click") {
                    window.setTimeout(function() {
                        lnk.setAttribute("href", lnk.getAttribute("mce_real_href"));
                        lnk.removeAttribute("mce_real_href");
                    }, 10);
                }
            }

            // Reset selected node
            if (e.type != "focus")
                tinyMCE.selectedNode = null;

            tinyMCE.triggerNodeChange(false);
            tinyMCE.execCommand("mceEndTyping");

            if (e.type == "mouseup")
                tinyMCE.execCommand("mceAddUndoLevel");

            // Just in case
            if (!tinyMCE.selectedInstance && e.target.editorId)
                tinyMCE.selectedInstance = tinyMCE.instances[e.target.editorId];

            // Run image/link fix on Gecko if diffrent document base
            if (tinyMCE.isGecko && tinyMCE.settings['document_base_url'] != "" + document.location.href)
                window.setTimeout('tinyMCE.getInstanceById("' + inst.editorId + '").fixBrokenURLs();', 10);

            return false;
*/
        break;
    } // end switch
}; // end function



// TinyMCEControl
function TinyMCEControl() {}

/**
  * This function written by NF to integrate with Loki.
  */
TinyMCEControl.prototype.init = function(win, targetElement, loki) {
    this.contentWindow = win;
    this.targetElement = targetElement;
    this.loki = loki;
};

TinyMCEControl.prototype._insertPara = function(e) {
    function isEmpty(para) {
        function isEmptyHTML(html) {
            return html.replace(new RegExp('[ trn]+', 'g'), '').toLowerCase() == "";
        }

        // Check for images
        if (para.getElementsByTagName("img").length > 0)
            return false;

        // Check for tables
        if (para.getElementsByTagName("table").length > 0)
            return false;

        // Check for HRs
        if (para.getElementsByTagName("hr").length > 0)
            return false;

        // Check all textnodes
        var nodes = tinyMCE.getNodeTree(para, new Array(), 3);
        for (var i=0; i<nodes.length; i++) {
            if (!isEmptyHTML(nodes[i].nodeValue))
                return false;
        }

        // No images, no tables, no hrs, no text content then it's empty
        return true;
    }

    // NF: added these to fit our way of doing things
    /*
    var doc = win.document;
    var sel = Util.Selection.get_selection(win);
    var rng = sel.getRangeAt(0);
    var body = doc.body;
    var rootElm = doc.documentElement;
    var blockName = "P";
    */
    var doc = this.getDoc();
    var sel = this.getSel();
    var win = this.contentWindow;
    var rng = sel.getRangeAt(0);
    var body = doc.body;
    var rootElm = doc.documentElement;
    var self = this;
    var blockName = "P";

//    tinyMCE.debug(body.innerHTML);

//    debug(e.target, sel.anchorNode.nodeName, sel.focusNode.nodeName, rng.startContainer, rng.endContainer, rng.commonAncestorContainer, sel.anchorOffset, sel.focusOffset, rng.toString());

    // Setup before range
    var rngBefore = doc.createRange();
    rngBefore.setStart(sel.anchorNode, sel.anchorOffset);
    rngBefore.collapse(true);

    // Setup after range
    var rngAfter = doc.createRange();
    rngAfter.setStart(sel.focusNode, sel.focusOffset);
    rngAfter.collapse(true);

    // Setup start/end points
    var direct = rngBefore.compareBoundaryPoints(rngBefore.START_TO_END, rngAfter) < 0;
    var startNode = direct ? sel.anchorNode : sel.focusNode;
    var startOffset = direct ? sel.anchorOffset : sel.focusOffset;
    var endNode = direct ? sel.focusNode : sel.anchorNode;
    var endOffset = direct ? sel.focusOffset : sel.anchorOffset;

    startNode = startNode.nodeName == "BODY" ? startNode.firstChild : startNode;
    endNode = endNode.nodeName == "BODY" ? endNode.firstChild : endNode;

    // tinyMCE.debug(startNode, endNode);

    // Get block elements
    var startBlock = tinyMCE.getParentBlockElement(startNode);
    var endBlock = tinyMCE.getParentBlockElement(endNode);

    mb('startBlock, endBlock', [startBlock, endBlock]);
    // NF: But then check the parentBlock of the parentBlock, to see whether
    // it's a blockquote or highlight div. If so, then make that the start/endBlock.
    /*
    var startBlock2 = tinyMCE.getParentBlockElement(startBlock.parentNode);
    var endBlock2 = tinyMCE.getParentBlockElement(endBlock.parentNode);
    if ( startBlock2 != null &&
         ( startBlock2.nodeName == 'BLOCKQUOTE' ||
           ( startBlock2.nodeName == 'DIV' && 
             Util.Element.has_class(startBlock2, 'callOut') ) ) )
    {
        mb('startBlock = startBlock2');
        startBlock = startBlock2;
    }
    if ( endBlock2 != null &&
         ( endBlock2.nodeName == 'BLOCKQUOTE' ||
           ( endBlock2.nodeName == 'DIV' && 
             Util.Element.has_class(endBlock2, 'callOut') ) ) )
    {
        mb('endBlock = endBlock2');
        endBlock = endBlock2;
    }
    */

    // Use current block name
    if (startBlock != null) {
        blockName = startBlock.nodeName;

        // Use P instead
        if (blockName == "TD" || blockName == "TABLE" || (blockName == "DIV" && new RegExp('left|right', 'gi').test(startBlock.style.cssFloat)))
        {
            blockName = "P";
        }
    }

    // NF: If we're inside pre, insert a BR instead of a new pre tag
    if ( blockName == 'PRE' )
    {
        var br_helper = (new UI.BR_Helper).init(this.loki);
        br_helper.insert_br();
        return true;
    }

    // NF: added this chunk, and changed all references below 
    // to block(Before|After)Name from blockName
    var blockBeforeName = blockName;
    var blockAfterName = blockName;
    if ( blockAfterName == "H1" || blockAfterName == "H3" || blockAfterName == "H4" || 
         blockAfterName == "H5" || blockAfterName == "H6" ||
         blockAfterName == "BLOCKQUOTE" || ( blockAfterName == "DIV" && Util.Element.has_class(startBlock, 'callOut') ) )
        var blockAfterName = 'P';

    // Within a list item (use normal behavior)
    if ((startBlock != null && startBlock.nodeName == "LI") || (endBlock != null && endBlock.nodeName == "LI"))
        return false;

    // Within a table create new paragraphs
    if ((startBlock != null && startBlock.nodeName == "TABLE") || (endBlock != null && endBlock.nodeName == "TABLE"))
        startBlock = endBlock = null;

    // Setup new paragraphs
    var paraBefore = (startBlock != null && startBlock.nodeName.toUpperCase() == blockBeforeName) ? startBlock.cloneNode(false) : doc.createElement(blockBeforeName);
    var paraAfter = (endBlock != null && endBlock.nodeName.toUpperCase() == blockAfterName) ? endBlock.cloneNode(false) : doc.createElement(blockAfterName);

    // Setup chop nodes
    //nf made these var startChop = startBlock == startBlock2 ? startNode.parentNode : startNode;
    // " var endChop = endBlock == endBlock2 ? endNode.parentNode : endNode;
    var startChop = startBlock;
    var endChop = endBlock;

    // Get startChop node
    node = startChop;
    do {
        if (node == body || node.nodeType == 9 || tinyMCE.isBlockElement(node))
            break;

        startChop = node;
    } while ((node = node.previousSibling ? node.previousSibling : node.parentNode));

    // Get endChop node
    node = endChop;
    do {
        if (node == body || node.nodeType == 9 || tinyMCE.isBlockElement(node))
            break;

        endChop = node;
    } while ((node = node.nextSibling ? node.nextSibling : node.parentNode));

    // Fix when only a image is within the TD
    if (startChop.nodeName == "TD")
        startChop = startChop.firstChild;

    if (endChop.nodeName == "TD")
        endChop = endChop.lastChild;

    // If not in a block element
    if (startBlock == null) {
        // Delete selection
        rng.deleteContents();
        sel.removeAllRanges();

        if (startChop != rootElm && endChop != rootElm) {
            // Insert paragraph before
            rngBefore = rng.cloneRange();

            if (startChop == body)
                rngBefore.setStart(startChop, 0);
            else
                rngBefore.setStartBefore(startChop);

            paraBefore.appendChild(rngBefore.cloneContents());

            // Insert paragraph after
            if (endChop.parentNode.nodeName == blockBeforeName)
                endChop = endChop.parentNode;

            rng.setEndAfter(endChop);
            if (endChop.nodeName != "#text" && endChop.nodeName != "BODY")
                rngBefore.setEndAfter(endChop);

            var contents = rng.cloneContents();
            if (contents.firstChild && (contents.firstChild.nodeName == blockBeforeName || contents.firstChild.nodeName == "BODY")) {
                var nodes = contents.firstChild.childNodes;
                for (var i=0; i<nodes.length; i++) {
                    if (nodes[i].nodeName != "BODY")
                        paraAfter.appendChild(nodes[i]);
                }
            } else
                paraAfter.appendChild(contents);

            /* NF: this is obnoxious; is it necessary? (appears not)
            // Check if it's a empty paragraph
            if (isEmpty(paraBefore))
                paraBefore.innerHTML = "&nbsp;";

            // Check if it's a empty paragraph
            if (isEmpty(paraAfter))
                paraAfter.innerHTML = "&nbsp;";
            */

            // Delete old contents
            rng.deleteContents();
            rngAfter.deleteContents();
            rngBefore.deleteContents();

            // Insert new paragraphs
            paraAfter.normalize();
            rngBefore.insertNode(paraAfter);
            paraBefore.normalize();
            rngBefore.insertNode(paraBefore);

//            tinyMCE.debug("1: ", paraBefore.innerHTML, paraAfter.innerHTML);
        } else {
            body.innerHTML = "<" + blockBeforeName + ">&nbsp;</" + blockBeforeName + "><" + blockAfterName + ">&nbsp;</" + blockAfterName + ">";
            paraAfter = body.childNodes[1];
        }

        this.selectNode(paraAfter, true, true, true, false);

        return true;
    }

    // Place first part within new paragraph
    if (startChop.nodeName == blockBeforeName)
        rngBefore.setStart(startChop, 0);
    else
        rngBefore.setStartBefore(startChop);
    rngBefore.setEnd(startNode, startOffset);
    paraBefore.appendChild(rngBefore.cloneContents());

    // Place secound part within new paragraph
    rngAfter.setEndAfter(endChop);
    rngAfter.setStart(endNode, endOffset);
    var contents = rngAfter.cloneContents();
    if (contents.firstChild && contents.firstChild.nodeName == blockBeforeName) {
        /* NF: this skips every other node
        var nodes = contents.firstChild.childNodes;
        for (var i=0; i<nodes.length; i++) {
            if (nodes[i].nodeName.toLowerCase() != "body")
                paraAfter.appendChild(nodes[i]);
        */
        var nodes = contents.firstChild.childNodes;
        while ( nodes.length > 0 )
        {
            if (nodes[0].nodeName.toLowerCase() != "body")
                paraAfter.appendChild(nodes[0]);
        }
    } else
        paraAfter.appendChild(contents);

    // Check if it's a empty paragraph
    if (isEmpty(paraBefore))
        paraBefore.innerHTML = "&nbsp;";

    // Check if it's a empty paragraph
    if (isEmpty(paraAfter))
        paraAfter.innerHTML = "&nbsp;";

    // Create a range around everything
    var rng = doc.createRange();

    if (!startChop.previousSibling && startChop.parentNode.nodeName.toUpperCase() == blockBeforeName) {
        rng.setStartBefore(startChop.parentNode);
    } else {
        if (rngBefore.startContainer.nodeName.toUpperCase() == blockBeforeName && rngBefore.startOffset == 0)
            rng.setStartBefore(rngBefore.startContainer);
        else
            rng.setStart(rngBefore.startContainer, rngBefore.startOffset);
    }

    if (!endChop.nextSibling && endChop.parentNode.nodeName.toUpperCase() == blockBeforeName)
        rng.setEndAfter(endChop.parentNode);
    else
        rng.setEnd(rngAfter.endContainer, rngAfter.endOffset);

    // Delete all contents and insert new paragraphs
    rng.deleteContents();
    rng.insertNode(paraAfter);
    rng.insertNode(paraBefore);
    // debug("2", paraBefore.innerHTML, paraAfter.innerHTML);

    // Normalize
    paraAfter.normalize();
    paraBefore.normalize();

    this.selectNode(paraAfter, true, true, true, false);

    return true;
};

TinyMCEControl.prototype._handleBackSpace = function(evt_type) {
    var doc = this.getDoc();
    var sel = this.getSel();
    if (sel == null)
        return false;

    var rng = sel.getRangeAt(0);
    var node = rng.startContainer;
    var elm = node.nodeType == 3 ? node.parentNode : node;

    if (node == null)
        return;

    // Empty node, wrap contents in paragraph
    if (elm && elm.nodeName == "") {
        var para = doc.createElement("p");

        while (elm.firstChild)
            para.appendChild(elm.firstChild);

        elm.parentNode.insertBefore(para, elm);
        elm.parentNode.removeChild(elm);

        var rng = rng.cloneRange();
        rng.setStartBefore(node.nextSibling);
        rng.setEndAfter(node.nextSibling);
        rng.extractContents();

        this.selectNode(node.nextSibling, true, true);
    }

    // Remove empty paragraphs
    var para = tinyMCE.getParentBlockElement(node);
    if (para != null && para.nodeName.toLowerCase() == 'p' && evt_type == "keypress") {
        var htm = para.innerHTML;
        var block = tinyMCE.getParentBlockElement(node);
        
        // Empty node, we do the killing!!
        if (htm == "" || htm == "&nbsp;" || block.nodeName.toLowerCase() == "li") {
            var prevElm = para.previousSibling;

            while (prevElm != null && prevElm.nodeType != 1)
                prevElm = prevElm.previousSibling;

            if (prevElm == null)
                return false;

            // Get previous elements last text node
            var nodes = tinyMCE.getNodeTree(prevElm, new Array(), 3);
            var lastTextNode = nodes.length == 0 ? null : nodes[nodes.length-1];
            
            // Select the last text node and move curstor to end
            if (lastTextNode != null)
                this.selectNode(lastTextNode, true, false, false, false);

            // Remove the empty paragrapsh
            para.parentNode.removeChild(para);

            //debug("within p element" + para.innerHTML);
            //showHTML(this.getBody().innerHTML);
            return true;
        }
    }

    // Remove BR elements
/*    while (node != null && (node = node.nextSibling) != null) {
        if (node.nodeName.toLowerCase() == 'br')
            node.parentNode.removeChild(node);
        else if (node.nodeType == 1) // Break at other element
            break;
    }*/

    //showHTML(this.getBody().innerHTML);

    return false;
};

TinyMCEControl.prototype.selectNode = function(node, collapse, select_text_node, to_start, scroll) {
    if (!node)
        return;

    if (typeof(collapse) == "undefined")
        collapse = true;

    if (typeof(select_text_node) == "undefined")
        select_text_node = false;

    if (typeof(to_start) == "undefined")
        to_start = true;
        
    if (typeof(scroll) == "undefined")
        scroll = true;

    if (tinyMCE.isMSIE) {
        var rng = this.getBody().createTextRange();

        try {
            rng.moveToElementText(node);

            if (collapse)
                rng.collapse(to_start);

            rng.select();
        } catch (e) {
            // Throws illigal agrument in MSIE some times
        }
    } else {
        var sel = this.getSel();

        if (!sel)
            return;

        if (tinyMCE.isSafari) {
            sel.realSelection.setBaseAndExtent(node, 0, node, node.innerText.length);

            if (collapse) {
                if (to_start)
                    sel.realSelection.collapseToStart();
                else
                    sel.realSelection.collapseToEnd();
            }

            if (scroll)
                this.scrollToNode(node);

            return;
        }

        var rng = this.getDoc().createRange();

        if (select_text_node) {
            // Find first textnode in tree
            var nodes = tinyMCE.getNodeTree(node, new Array(), 3);
            if (nodes.length > 0)
                rng.selectNodeContents(nodes[0]);
            else
                rng.selectNodeContents(node);
        } else
            rng.selectNode(node);

        if (collapse) {
            // Special treatment of textnode collapse
            if (!to_start && node.nodeType == 3) {
                rng.setStart(node, node.nodeValue.length);
                rng.setEnd(node, node.nodeValue.length);
            } else
                rng.collapse(to_start);
        }

        sel.removeAllRanges();
        sel.addRange(rng);
    }

    if (scroll)
        this.scrollToNode(node);

    // Set selected element
    tinyMCE.selectedElement = null;
    if (node.nodeType == 1)
        tinyMCE.selectedElement = node;
};

TinyMCEControl.prototype.scrollToNode = function(node) {
    // Scroll to node position
    var pos = tinyMCE.getAbsPosition(node);
    var doc = this.getDoc();
    var scrollX = doc.body.scrollLeft + doc.documentElement.scrollLeft;
    var scrollY = doc.body.scrollTop + doc.documentElement.scrollTop;
    var height = tinyMCE.isMSIE ? document.getElementById(this.editorId).style.pixelHeight : this.targetElement.clientHeight;

    // Only scroll if out of visible area
    if (!tinyMCE.settings['auto_resize'] && !(node.absTop > scrollY && node.absTop < (scrollY - 25 + height))) {
        this.contentWindow.scrollTo(pos.absLeft, pos.absTop - height + 25);
    }
};

TinyMCEControl.prototype.getBody = function() {
    return this.getDoc().body;
};

TinyMCEControl.prototype.getDoc = function() {
    return this.contentWindow.document;
};

TinyMCEControl.prototype.getWin = function() {
    return this.contentWindow;
};

TinyMCEControl.prototype.getSel = function() {
    if (tinyMCE.isMSIE)
        return this.getDoc().selection;

    var sel = this.contentWindow.getSelection();

    // Fake getRangeAt
    if (tinyMCE.isSafari && !sel.getRangeAt) {
        var newSel = new Object();
        var doc = this.getDoc();

        function getRangeAt(idx) {
            var rng = new Object();

            rng.startContainer = this.focusNode;
            rng.endContainer = this.anchorNode;
            rng.commonAncestorContainer = this.focusNode;
            rng.createContextualFragment = function (html) {
                // Seems to be a tag
                if (html.charAt(0) == '<') {
                    var elm = doc.createElement("div");

                    elm.innerHTML = html;

                    return elm.firstChild;
                }

                return doc.createTextNode("UNSUPPORTED, DUE TO LIMITATIONS IN SAFARI!");
            };

            rng.deleteContents = function () {
                doc.execCommand("Delete", false, "");
            };

            return rng;
        }

        // Patch selection

        newSel.focusNode = sel.baseNode;
        newSel.focusOffset = sel.baseOffset;
        newSel.anchorNode = sel.extentNode;
        newSel.anchorOffset = sel.extentOffset;
        newSel.getRangeAt = getRangeAt;
        newSel.text = "" + sel;
        newSel.realSelection = sel;

        newSel.toString = function () {return this.text;};

        return newSel;
    }

    return sel;
};



// file mb.js
/**
 * For debugging.
 */
var messagebox = function() { };
var mb = messagebox;

// file Util.js
/**
 * @class This is merely a container which holds a library of utility
 * functions and classes. The library is organized around existing
 * DOM/JS classes, if they exist. For example, functions which extend
 * or provide cross-browser functionality on DOM Nodes are located in
 * Util.Node.
 */
var Util = {
    is: function is(type, objects)
    {
        for (var i = 0; i < objects.length; i++) {
            if (typeof(objects[i]) != type)
                return false;
        }
        
        return true;
    },
    
    is_boolean: function is_boolean()
    {
        return Util.is('boolean', arguments);
    },
    
    is_function: function is_function()
    {
        return Util.is('function', arguments);
    },
    
    is_string: function is_string()
    {
        return Util.is('string', arguments);
    },
    
    is_number: function is_number()
    {
        return Util.is('number', arguments);
    },
    
    is_object: function is_object()
    {
        return Util.is('object', arguments);
    },
    
    is_valid_object: function is_non_null_object()
    {
        for (var i = 0; i < arguments.length; i++) {
            if (typeof(arguments[i]) != 'object' || arguments[i] == null)
                return false;
        }
        
        return true;
    },
    
    is_undefined: function is_undefined()
    {
        return Util.is('undefined', arguments);
    },
    
    is_null: function is_null()
    {
        for (var i = 0; i < arguments.length; i++) {
            if (arguments[i] != null)
                return false;
        }
        
        return true;
    },
    
    is_blank: function is_blank()
    {
        for (var i = 0; i < arguments.length; i++) {
            if (typeof(arguments[i]) != 'undefined' || arguments[i] != null)
                return false;
        }
        
        return true;
    },
    
    is_enumerable: function is_enumerable()
    {
        for (var i = 0; i < arguments.length; i++) {
            var o = arguments[i];
            if (!o || typeof(o.length) != 'number')
                return false;
        }
        
        return true;
    },
    
    trim: function trim_string(str)
    {
        str = str.replace(/^s+/, '');
        for (var i = str.length - 1; i >= 0; i--) {
            if (/S/.test(str.charAt(i))) {
                str = str.substring(0, i + 1);
                break;
            }
        }
        return str;
    },
    
    regexp_escape: function escape_string_for_regexp(str)
    {
        // credit: Prototype
        return String(str).replace(/([.*+?^=!:${}()|[]/\])/g, '\$1');
    }
};

// file Util.Scheduler.js
/**
 * @class Provides a more convenient interface to setTimeout and clearTimeout.
 * @author Eric Naeseth
 */
Util.Scheduler = function Scheduler()
{
    throw new Error('This is a static class; it does not make sense to call its constructor.');
}

Util.Scheduler.Error = function SchedulerError(message)
{
    Util.OOP.inherits(this, Error, message);
    this.name = 'Util.Scheduler.Error';
}

Util.Scheduler.Task = function SchedulerTask(callable)
{
    this.id = null;
    this.invoke = callable;
    
    this.runDelayed = function run_task_delayed(delay)
    {
        this.id = setTimeout(callable, delay * 1000);
    }
    
    this.runPeriodically = function run_task_periodically(interval)
    {
        var self = this;
        interval *= 1000;
        
        function standin() {
            self.invoke.apply(this, arguments);
            self.id = setTimeout(standin, interval);
        }
        
        this.id = setTimeout(standin, interval);
    }
    
    this.cancel = function cancel_task()
    {
        if (this.id === null) {
            throw new Util.Scheduler.Error('Nothing has been scheduled.');
        }
        clearTimeout(this.id);
        this.id = null;
    }
}

Util.Scheduler.delay = function sched_delay(func, delay)
{
    var task = new Util.Scheduler.Task(func);
    task.runDelayed(delay);
    return task;
}

Util.Scheduler.defer = function sched_defer(func)
{
    var task = new Util.Scheduler.Task(func);
    task.runDelayed(0.01 /* 10ms */);
    return task;
}

Util.Scheduler.runPeriodically = function sched_run_periodically(func, interval)
{
    var task = new Util.Scheduler.Task(func);
    task.runPeriodically(interval);
    return task;

// file Util.Function.js
Util.Function = {
    /**
     * Synchronizes calls to the function; i.e. prevents it from being called
     * more than once at the same time.
     * @author Eric Naeseth
     * @see http://taylor-hughes.com/?entry=112
     */
    synchronize: function synchronize(function_)
    {
        var sync = Util.Function.synchronize;
        
        if (!sync.next_id) {
            sync.next_id = 0;
            sync.wait_list = {};
            sync.next = function(k) {
                for (i in sync.wait_list) {
                    if (!k)
                        return sync.wait_list[i];
                    if (k == i)
                        k = null;
                }

                return null;
            }
        }
        
        return function() {
            var lock = {
                id: ++sync.next_id,
                enter: false
            };

            sync.wait_list[lock.id] = lock;

            lock.enter = true;
            lock.number = (new Date()).getTime();
            lock.enter = false;
            
            var context = [this, arguments];

            function attempt(start)
            {
                for (var j = start; j != null; j = sync.next(j.id)) {
                    if (j.enter ||
                        (j.number && j.number < lock.number ||
                            (j.number == lock.number && j.id < lock.id))) 
                    {
                        (function () { attempt(j); }).delay(100);
                        return;
                    }
                }

                // run with exclusive access
                function_.apply(context[0], context[1]);
                // release
                lock.number = 0;
                sync.wait_list[lock.id] = null;
            }
            
            attempt(sync.next());
        }
    },
    
    empty: function empty()
    {
        
    },
    
    constant: function constant(k)
    {
        return k;
    },
    
    optimist: function optimist()
    {
        return true;
    },
    
    pessimist: function pessimist()
    {
        return false;
    },
    
    unimplemented: function unimplemented()
    {
        throw new Error('Function not implemented!');
    }
};

var $S = Util.Function.synchronize;

Util.Function.Methods = {
    bind: function bind(function_)
    {
        if (arguments.length < 2 && arguments[0] === undefined)
            return function_;
        
        var args = Util.Array.from(arguments).slice(1), object = args.shift();
        return function binder() {
            return function_.apply(object, args.concat(Util.Array.from(arguments)));
        }
    },
    
    bind_to_event: function bind_to_event(function_)
    {
        var args = Util.Array.from(arguments), object = args.shift();
        return function event_binder(event) {
            return function_.apply(object, [event || window.event].concat(args));
        }
    },
    
    curry: function curry(function_)
    {
        if (arguments.length <= 1)
            return function_;
        
        var args = Util.Array.from(arguments).slice(1);
        
        return function currier() {
            return function_.apply(this, args.concat(Util.Array.from(arguments)));
        }
    },
    
    dynamic_curry: function dynamic_curry(function_)
    {
        if (arguments.length <= 1)
            return function_;
        
        var args = Util.Array.from(arguments).slice(1).map(function (a) {
            return (typeof(a) == 'function')
                ? a()
                : a;
        });
        
        return function dynamic_currier() {
            return function_.apply(this, args.concat(Util.Array.from(arguments)));
        }
    },
    
    methodize: function methodize(function_)
    {
        if (!function_.methodized) {
            function_.methodized = function methodized() {
                return function_.apply(null, [this].concat(Util.Array.from(arguments)));
            }
        }
        
        return function_.methodized;
    },
    
    delay: function delay(function_, delay)
    {
        return Util.Scheduler.delay(function_, delay);
    },
    
    defer: function defer(function_)
    {
        return Util.Scheduler.defer(function_);
    }
};

Util.Function.bindToEvent = Util.Function.bind_to_event;

for (var name in Util.Function.Methods) {
    Function.prototype[name] =
        Util.Function.Methods.methodize(Util.Function.Methods[name]);
    Util.Function[name] = Util.Function.Methods[name];

// file Util.Array.js
/**
 * Does nothing.
 *
 * @class Container for functions relating to arrays.
 */
Util.Array = function()
{
};

/**
 * Forms a legitimate JavaScript array from an array-like object
 * (eg NodeList objects, function argument lists).
 */
Util.Array.from = function array_from_iterable(iterable)
{
    if (!iterable)
        return [];
    if (iterable.toArray)
        return iterable.toArray();
    
    try {
        return Array.prototype.slice.call(iterable, 0);
    } catch (e) {
        // This doesn't work in Internet Explorer with iterables that are not
        // real JavaScript objects. But we still want to keep around the slice
        // version for performance on Gecko.
        
        var new_array = [];
        for (var i = 0; i < iterable.length; i++) {
            new_array.push(iterable[i]);
        }
        
        return new_array;
    }
    
};

var $A = Util.Array.from; // convenience alias

/**
 * Creates an array of integers from start up to (but not including) stop.
 */
Util.Array.range = function range(start, stop)
{
    if (arguments.length == 1) {
        stop = start;
        start = 0;
    }
    
    var ret = [];
    for (var i = start; i < stop; i++) {
        ret.push(i);
    }
    return ret;
}

var $R = Util.Array.range; // convenience alias

/**
 * Methods that are callable by two methods:
 *  - Util.Array.method_name(some_array, ...)
 *  - some_array.methodName(...)
 * Note the change in naming convention! When added to
 * Array's prototype it is changed to use the JavaScript
 * naming convention (camelCase) instead of Loki's
 * (underscore_separated).
 */
Util.Array.Methods = {
    /**
     * Executes the given function for each element in the array.
     * (Available as the "each" method of arrays.)
     * @param    array    the array over which for_each will loop
     * @param    func    the function which will be called
     * @param    thisp    optional "this" context
     * @see    http://tinyurl.com/ds8lo
     */
    for_each: function each(array, func)
    {
        var thisp = arguments[2] || null;

        if (typeof(func) != 'function')
            throw new TypeError();

        //if (typeof(array.forEach) == 'function')
        //    return array.forEach(func, thisp);

        var len = array.length;
        for (var i = 0; i < len; i++) {
            if (i in array)
                func.call(thisp, array[i], i, array);
        }
    },
    
    /**
     * Creates a new array by applying the given function to each element of
     * the given array.
     * i.e. [a, b, c, ...] -> [func(a), func(b), func(c), ...]
     * @param {array} array the array over which map will loop
     * @param {function} fund the function to apply to each element
     * @param {object} thisp optional "this" context for the function
     * @type array
     * @see http://tinyurl.com/32ww7d
     */
    map: function map(array, func)
    {
        var thisp = arguments[2] || null;

        var len = array.length;
        var ret = new Array(len);
        for (var i = 0; i < len; i++) {
            if (i in array)
                ret[i] = func.call(thisp, array[i], i, array);
        }

        return ret;
    },
    
    /**
     * @see http://tinyurl.com/yq3c9f
     */
    reduce: function reduce(array, func, initial_value)
    {
        if (typeof(func) != 'function')
            throw new TypeError();
        
        var value;
        
        array.each(function(v, i, a) {
            if (value === undefined && initial_value === undefined) {
                value = v;
            } else {
                value = func.call(null, value, v, i, a);
            }
        });
        
        return value;
    },
    
    /**
     * Returns the first item in the array for which the test function
     * returns true.
     * @param    array    the array to search
     * @param    test    the function which will be called
     * @param    thisp    optional "this" context
     */
    find: function find_in_array(array, test, thisp)
    {
        if (typeof(thisp) == 'undefined')
            thisp = null;
        if (typeof(test) != 'function')
            throw new TypeError();

        var len = array.length;

        for (var i = 0; i < len; i++) {
            if (i in array && test.call(thisp, array[i]))
                return array[i];
        }
    },
    
    /**
     * Returns all items in the array for which the test function
     * returns true.
     * @param    array    the array to search
     * @param    test    the function which will be called
     * @param    thisp    optional "this" context
     */
    find_all: function find_all_in_array(array, test, thisp)
    {
        if (typeof(thisp) == 'undefined')
            thisp = null;
        if (typeof(test) != 'function')
            throw new TypeError();

        var len = array.length;
        var results = [];

        for (var i = 0; i < len; i++) {
            if (i in array && test.call(thisp, array[i]))
                results.push(array[i]);
        }

        return results;
    },
    
    /**
     * Converts the array to a "set": an object whose keys are the original
     * array's values and whose values are all true. This allows efficient
     * membership testing of the array when it needs to be done repeatedly.
     */
    to_set: function array_to_set(array)
    {
        var s = {};
        var len = array.length;
        
        for (var i = 0; i < len; i++) {
            if (i in array)
                s[array[i]] = true;
        }
        
        return s;
    },
    
    min: function min_in_array(array, key_func)
    {
        return array.reduce(function(a, b) {
            if (key_func) {
                return (key_func(b) < key_func(a))
                    ? b
                    : a;
            } else {
                return (b < a)
                    ? b
                    : a;
            }
        });
    },
    
    max: function max_in_array(array, key_func)
    {
        return array.reduce(function(a, b) {
            if (key_func) {
                return (key_func(b) > key_func(a))
                    ? b
                    : a;
            } else {
                return (b > a)
                    ? b
                    : a;
            }
        });
    },
    
    pluck: function pluck_from_array(array, property_name)
    {
        return array.map(function(obj) {
            return obj[property_name];
        });
    },
    
    sum: function sum_of_array(array)
    {
        return array.reduce(function(a, b) {
            return a + b;
        });
    },
    
    product: function product_of_array(array)
    {
        return array.reduce(function(a, b) {
            return a * b;
        });
    },
    
    contains: function array_contains(array, item)
    {
        if (Util.is_function(array.indexOf)) {
            return -1 != array.indexOf(item);
        }
        
        return !!array.find(function(element) {
            return item == element;
        });
    },
    
    /**
     * Returns true if the function test returns true when given any element
     * in array.
     * @param {array}    array    the array to examine
     * @param {function}    test    the test to apply to the array's elements
     * @param {object}    thisp    an optional "this" context in which the test
     *                            function will be called
     * @type boolean
     */
    some: function some(array, test)
    {
        var thisp = arguments[2] || null;
        
        for (var i = 0; i < array.length; i++) {
            if (i in array) {
                if (test.call(thisp, array[i])) {
                    // Found one that works.
                    return true;
                }
            }
        }
        
        return false;
    },
    
    /**
     * Returns true if the function test returns true when executed for each
     * element in array.
     * @param {array}    array    the array to examine
     * @param {function}    test    the test to apply to the array's elements
     * @param {object}    thisp    an optional "this" context in which the test
     *                            function will be called
     * @type boolean
     */
    every: function every(array, test)
    {
        var thisp = arguments[2] || null;
        
        for (var i = 0; i < array.length; i++) {
            if (i in array) {
                if (!test.call(thisp, array[i])) {
                    // Found one that doesn't work.
                    return false;
                }
            }
        }
        
        return true;
    },
    
    /**
     * Returns all of the elements of the array that passed the given test.
     * @param {array}    array    the array to filter
     * @param {function}    test    a function that will be called for each
     *                                element in the array to determine whether
     *                                or not it should be included
     * @param {object}    thisp    an optional "this" context in which the test
     *                            function will be called
     * @type array
     */
    filter: function filter_array(array, test)
    {
        var thisp = arguments[2] || null;
        
        return array.reduce(function perform_filtration(matches, element) {
            if (test.call(thisp, element))
                matches.push(element);
            return matches;
        }, []);
    },
    
    remove: function remove_from_array(array, item)
    {
        var len = array.length;
        for (var i = 0; i < len; i++) {
            if (i in array && array[i] == item) {
                array.splice(i, 1);
                return true;
            }
        }
        
        return false;
    },
    
    remove_all: function remove_all_from_array(array, item)
    {
        var len = array.length;
        var found = false;
        
        for (var i = 0; i < len; i++) {
            if (i in array && array[i] == item) {
                found = true;
                array.splice(i, 1);
            }
        }
        
        return found;
    },
    
    append: function append_array(a, b)
    {
        // XXX: any more efficient way to do this using Array.splice?
        
        if (b.length === undefined || b.length === null) {
            throw new TypeError("Cannot append a non-iterable to an array.");
        }
        
        var len = b.length;
        for (var i = 0; i < len; i++) {
            if (i in b) {
                a.push(b[i]);
            }
        }
    }
}

for (var name in Util.Array.Methods) {
    function transform_name(name)
    {
        var new_name = '';
        parts = name.split(/_+/);
        
        new_name += parts[0];
        for (var i = 1; i < parts.length; i++) {
            new_name += parts[1].substr(0, 1).toUpperCase();
            new_name += parts[1].substr(1);
        }
        
        return new_name;
    }
    
    Util.Array[name] = Util.Array.Methods[name];
    
    var new_name;
    switch (name) {
        case 'map':
        case 'reduce':
        case 'filter':
        case 'every':
        case 'some':
            if (!Util.is_function(Array.prototype[name]))
                Array.prototype[name] = Util.Array.Methods[name].methodize();
            break;
        case 'for_each':
            Array.prototype.each = (Array.prototype.forEach ||
                    Util.Array.Methods.for_each.methodize());
            break;
        default:
            new_name = transform_name(name);
            Array.prototype[new_name] = Util.Array.Methods[name].methodize();
    }

// file Util.Node.js
/**
 * Does nothing.
 *
 * @class Container for functions relating to nodes.
 */
Util.Node = function()
{
};

// Since IE doesn't expose these constants, they are reproduced here
Util.Node.ELEMENT_NODE                   = 1;
Util.Node.ATTRIBUTE_NODE                 = 2;
Util.Node.TEXT_NODE                      = 3;
Util.Node.CDATA_SECTION_NODE             = 4;
Util.Node.ENTITY_REFERENCE_NODE          = 5;
Util.Node.ENTITY_NODE                    = 6;
Util.Node.PROCESSING_INSTRUCTION_NODE    = 7;
Util.Node.COMMENT_NODE                   = 8;
Util.Node.DOCUMENT_NODE                  = 9;
Util.Node.DOCUMENT_TYPE_NODE             = 10;
Util.Node.DOCUMENT_FRAGMENT_NODE         = 11;
Util.Node.NOTATION_NODE                  = 12;

// Constants which indicate which direction to iterate through a node
// list, e.g. in get_nearest_non_whitespace_sibling_node
Util.Node.NEXT                             = 1;
Util.Node.PREVIOUS                         = 2;

/**
 * Removes child nodes of <code>node</code> for which
 * <code>boolean_test</code> returns true.
 *
 * @param    node            the node whose child nodes are in question
 * @param    boolean_test    (optional) A function which takes a node 
 *                          as its parameter, and which returns true 
 *                          if the node should be removed, or false
 *                          otherwise. If boolean_test is not given,
 *                          all child nodes will be removed.
 */
Util.Node.remove_child_nodes = function(node, boolean_test)
{
    if ( boolean_test == null )
        boolean_test = function(node) { return true; };

    while ( node.childNodes.length > 0 )
        if ( boolean_test(node.firstChild) )
            node.removeChild(node.firstChild);
};

/**
 * Returns all children of the given node who match the given test.
 * @param {Node} node the node whose children will be traversed
 * @param {Function|String|Number} match either a boolean-test matching function,
 *        or a tag name, or a node type to be matched
 * @return {Node[]} all matching child nodes
 */
Util.Node.find_children = function find_matching_node_children(node, match) {
    var i, length, node_type;
    var children = [], child;
    
    if (!node || !node.nodeType) {
        throw new TypeError('Must provide Util.Node.find_children with a ' +
            'node to traverse.');
    }
    
    if (Util.is_string(match)) {
        match = Util.Node.curry_is_tag(match);
    } else if (Util.is_number(match)) {
        node_type = match;
        match = function is_correct_node_type(node) {
            return (node && node.nodeType == node_type);
        }
    } else if (!Util.is_function(match)) {
        throw new TypeError('Must provide Util.Node.find_children with ' +
            'something to match nodes against.');
    }
    
    for (i = 0, length = node.childNodes.length; i < length; i++) {
        child = node.childNodes[i];
        if (match(child))
            children.push(child);
    }
    
    return children;
};

/**
 * <p>Recurses through the ancestor nodes of the specified node,
 * until either (a) a node is found which meets the conditions
 * specified inthe function boolean_test, or (b) the root of the
 * document tree isreached. If (a) obtains, the found node is
 * returned; if (b)obtains, null is returned.</p>
 * 
 * <li>Example usage 1: <code>var nearest_ancestor = this._get_nearest_ancestor_element(node, function(node) { return node.tagName == 'A' });</code></li>
 * <li>Example usage 2: <pre>
 *
 *          var nearest_ancestor = this._get_nearest_ancestor_element(
 *              node,
 *              function(node, extra_args) {
 *                  return node.tagName == extra_args.ref_to_this.something
 *              },
 *              { ref_to_this : this }
 *          );
 *
 * </pre></li>
 *
 * @param    node            the starting node
 * @param    boolean_test    <p>the function to use as a test. The given function should
 *                          accept the following paramaters:</p>
 *                          <li>cur_node - the node currently being tested</li>
 *                          <li>extra_args - (optional) any extra arguments this function
 *                          might need, e.g. a reference to the calling object (deprecated:
 *                          use closures instead)</li>
 * @param    extra_args        any extra arguments the boolean function might need (deprecated:
 *                          use closures instead)
 * @return                    the nearest matching ancestor node, or null if none matches
 */
Util.Node.get_nearest_ancestor_node = function(node, boolean_test, extra_args)
{
    function terminal(node) {
        switch (node.nodeType) {
            case Util.Node.DOCUMENT_NODE:
            case Util.Node.DOCUMENT_FRAGMENT_NODE:
                return true;
            default:
                return false;
        }
    }
    
    for (var n = node.parentNode; n && !terminal(n); n = n.parentNode) {
        if (boolean_test(n, extra_args))
            return n;
    }
    
    return null;
};

/**
 * Returns true if there exists an ancestor of the given node 
 * that satisfies the given boolean_test. Paramaters same as for
 * get_nearest_ancestor_node.
 */
Util.Node.has_ancestor_node =
    function node_has_matching_ancestor(node, boolean_test, extra_args)
{
    return Util.Node.get_nearest_ancestor_node(node, boolean_test, extra_args) != null;
};

/**
 * Finds the node that is equal to or an ancestor of the given node that
 * matches the provided test.
 * @param    {Node}    node    the node to examine
 * @param    {function}    test    the test function that should return true when
 *                                passed a suitable node
 * @return {Node}    the matching node if one was found, otherwise null
 */
Util.Node.find_match_in_ancestry =
    function find_matching_node_in_ancestry(node, test)
{
    function terminal(node) {
        switch (node.nodeType) {
            case Util.Node.DOCUMENT_NODE:
            case Util.Node.DOCUMENT_FRAGMENT_NODE:
                return true;
            default:
                return false;
        }
    }
    
    for (var n = node; n && !terminal(n); n = n.parentNode) {
        if (test(n))
            return n;
    }
    
    return null;
}

/**
 * Gets the nearest ancestor of the node that is currently being displayed as
 * a block.
 * @param {Node}    node        the node to examine
 * @param {Window}    node_window    the node's window
 * @type Element
 * @see Util.Node.get_nearest_bl_ancestor_element()
 * @see Util.Element.is_block_level()
 */
Util.Node.get_enclosing_block =
    function get_enclosing_block_of_node(node, node_window)
{
    // Sanity checks.
    if (!node || !node.nodeType) {
        throw new TypeError('Must provide a node to ' + 
            'Util.Node.get_enclosing_block.');
    } else if (!Util.is_valid_object(node_window)) {
        throw new TypeError('Must provide the node's window object to ' + 
            'Util.Node.get_enclosing_block.');
    } else if (node_window.document != node.ownerDocument) {
        throw new Error('The window provided to Util.Node.get_enclosing_block' +
            ' is not actually the window in which the provided node resides.');
    }
    
    function is_block(node) {
        return (node.nodeType == Util.Node.ELEMENT_NODE &&
            Util.Element.is_block_level(window, node));
    }
    
    return Util.Node.get_nearest_ancestor_node(node, is_block);
}

/**
 * Gets the nearest ancester of node which is a block-level
 * element. (Uses get_nearest_ancestor_node.)
 *
 * @param {Node}    node        the starting node
 * @type Element
 * @see Util.Node.get_enclosing_block()
 */
Util.Node.get_nearest_bl_ancestor_element = function(node)
{
    return Util.Node.get_nearest_ancestor_node(node, Util.Node.is_block_level_element);
};

/**
 * Gets the given node's nearest ancestor which is an element whose
 * tagname matches the one given.
 *
 * @param    node            the starting node
 * @param    tag_name        the desired tag name    
 * @return                    the matching ancestor, if any
 */
Util.Node.get_nearest_ancestor_element_by_tag_name = function(node, tag_name)
{
    // Yes, I could use curry_is_tag, but I'd rather only have one closure.
    function matches_tag_name(node)
    {
        return Util.Node.is_tag(node, tag_name);
    }
    
    return Util.Node.get_nearest_ancestor_node(node, matches_tag_name);
};

/**
 * Iterates previouss through the given node's children, and returns
 * the first node which matches boolean_test.
 *
 * @param    node            the starting node
 * @param    boolean_test    the function to use as a test. The given function should
 *                          accept one paramater:
 *                          <li>cur_node - the node currently being tested</li>
 * @return                    the last matching child, or null if none matches
 */
Util.Node.get_last_child_node = function(node, boolean_test)
{
    for (var n = node.lastChild; n; n = n.previousSibling) {
        if (boolean_test(n))
            return n;
    }
    
    return null;
};

Util.Node.has_child_node = function(node, boolean_test)
{
    return Util.Node.get_last_child_node(node, boolean_test) != null;
};

/**
 * Returns true if the given node is an element node.
 * @param {Node} node node whose type will be tested
 * @returns {Boolean} true if "node" is an element node, false if otherwise
 */
Util.Node.is_element = function node_is_element(node) {
    return (node && node.nodeType == Util.Node.ELEMENT_NODE);
}

/**
 * Returns true if the given node is a text node.
 * @param {Node} node node whose type will be tested
 * @returns {Boolean} true if "node" is a text node, false if otherwise
 */
Util.Node.is_text = function node_is_text(node) {
    return (node && node.nodeType == Util.Node.TEXT_NODE);
}

/**
 * Returns true if the given node is a document node.
 * @param {Node} node node whose type will be tested
 * @returns {Boolean} true if "node" is a document node, false if otherwise
 */
Util.Node.is_document = function node_is_document(node) {
    return (node && node.nodeType == Util.Node.DOCUMENT_NODE);
}

/**
 * Returns true if the node is an element node and its node name matches the
 * tag parameter, false otherwise.
 *
 * @param    node    node on which the test will be run
 * @param    tag        tag name to look for
 * @type boolean
 */
Util.Node.is_tag = function(node, tag)
{
    return (node.nodeType == Util.Node.ELEMENT_NODE
        && node.nodeName == tag.toUpperCase());
};

/**
 * Creates a function that calls is_tag using the given tag.
 */
Util.Node.curry_is_tag = function(tag)
{
    return function(node) { return Util.Node.is_tag(node, tag); };
}

/**
 * Finds the offset of the given node within its parent.
 * @param {Node}  node  the node whose offset is desired
 * @return {Number}     the node's offset
 * @throws {Error} if the node is orphaned (i.e. it has no parent)
 */
Util.Node.get_offset = function get_node_offset_within_parent(node)
{
    var parent = node.parentNode;
    
    if (!parent) {
        throw new Error('Node ' + Util.Node.get_debug_string(node) + ' has ' +
            ' no parent.');
    }
    
    for (var i = 0; i < parent.childNodes.length; i++) {
        if (parent.childNodes[i] == node)
            return i;
    }
    
    throw new Error();
}

/**
 * Attempts to find the window that corresponds with a given node.
 * @param {Node}  node   the node whose window is desired
 * @return {Window}   the window object if it could be found, otherwise null.
 */
Util.Node.get_window = function find_window_of_node(node)
{
    var doc = (node.nodeType == Util.Node.DOCUMENT_NODE)
        ? node
        : node.ownerDocument;
    var seen;
    var stack;
    var candidate;
    
    if (!doc)
        return null;
    
    if (doc._loki__document_window) {
        return doc._loki__document_window;
    }
    
    function accept(w)
    {
        if (!w)
            return false;
        
        if (!seen.contains(w)) {
            seen.push(w);
            return true;
        }
        
        return false;
    }
    
    function get_elements(tag)
    {
        return candidate.document.getElementsByTagName(tag);
    }
    
    seen = [];
    stack = [window];
    
    accept(window);
    
    while (candidate = stack.pop()) { // assignment intentional
        try {
            if (candidate.document == doc) {
                // found it!
                doc._loki__document_window = candidate;
                return candidate;
            }

            if (candidate.parent != candidate && accept(candidate)) {
                stack.push(candidate);
            }


            ['FRAME', 'IFRAME'].map(get_elements).each(function (frames) {
                for (var i = 0; i < frames.length; i++) {
                    if (accept(frames[i].contentWindow))
                        stack.push(frames[i].contentWindow);
                }
            });
        } catch (e) {
            // Sometimes Mozilla gives security errors when trying to access
            // the documents.
        }
    }
    
    // guess it couldn't be found
    return null;
}

Util.Node.non_whitespace_regexp = /[^fnrtv ]/gi;
Util.Node.is_non_whitespace_text_node = function(node)
{
    // [^fnrtv] should be the same as S, but at least on
    // Gecko/20040206 Firefox/0.8 for Windows, S doesn't always match
    // what the explicitly specified character class matches--and what
    // S should match.

    return ( node.nodeType != Util.Node.TEXT_NODE ||
             Util.Node.non_whitespace_regexp.test(node.nodeValue) );
};

/**
 * Gets the last child node which is other than mere whitespace. (Uses
 * get_last_child_node.)
 *
 * @param    node    the node to look for
 * @return            the last non-whitespace child node
 */
Util.Node.get_last_non_whitespace_child_node = function(node)
{
    node.ownerDocument.normalizeDocument();
    return Util.Node.get_last_child_node(node, Util.Node.is_non_whitespace_text_node);
};

/**
 * Returns the given node's nearest sibling which is not a text node
 * that contains only whitespace.
 *
 * @param    node                    the node to look for
 * @param    next_or_previous        indicates which direction to look,
 *                                  either Util.Node.NEXT or
 *                                  Util.Node.PREVIOUS
 */
Util.Node.get_nearest_non_whitespace_sibling_node = function(node, next_or_previous)
{
    do
    {
        if ( next_or_previous == Util.Node.NEXT )
            node = node.nextSibling;
        else if ( next_or_previous == Util.Node.PREVIOUS )
            node = node.previousSibling;
        else
            throw("Util.get_nearest_non_whitespace_sibling_node: Argument next_or_previous must have Util.Node.NEXT or Util.Node.PREVIOUS as its value.");
    }
    while (!( node == null ||
              node.nodeType != Util.Node.TEXT_NODE ||
              Util.Node.non_whitespace_regexp.test(node.nodeValue)
           ))

    return node;
};

/**
 * Determines whether the given node is a block-level element. Tries to use the
 * element's computed style, and if that fails, falls back on what the default
 * is for the element's tag.
 *
 * @see Util.Element.is_block_level
 * @see Util.Block.is_block
 * @param    {Node}    node    the node in question
 * @return    {Boolean}    true if the node is a block-level element
 */
Util.Node.is_block_level_element = function(node)
{
    var w;
    
    if (node.nodeType != Util.Node.ELEMENT_NODE)
        return false;
    
    try {
        w = Util.Node.get_window(node);
        return Util.Element.is_block_level(w, node);
    } catch (e) {
        return Util.Block.is_block(node);
    }
};

Util.Node.is_block = Util.Node.is_block_level_element;

/**
 * Determines whether the given node, in addition to being a block-level
 * element, is also one that it we can nest inside any arbitrary block.
 * It is generally not permitted to surround the elements in the list below 
 * with most other blocks. E.g., we don't want to surround a TD with BLOCKQUOTE.
 */
Util.Node.is_nestable_block_level_element = function(node)
{
    return Util.Node.is_block_level_element(node)
        && !(/^(BODY|TBODY|THEAD|TR|TH|TD)$/i).test(node.tagName);
};

/**
 * Returns the rightmost descendent of the given node.
 */
Util.Node.get_rightmost_descendent = function(node)
{
    var rightmost = node;
    while ( rightmost.lastChild != null )
        rightmost = rightmost.lastChild;
    return rightmost;
};

Util.Node.get_leftmost_descendent = function(node)
{
    var leftmost = node;
    while ( leftmost.firstChild != null )
        leftmost = leftmost.firstChild;
    return leftmost;
};

Util.Node.is_rightmost_descendent = function(node, ref)
{
    return Util.Node.get_rightmost_descendent(ref) == node;
};

Util.Node.is_leftmost_descendent = function(node, ref)
{
    return Util.Node.get_leftmost_descendent(ref) == node;
};

/**
 * Inserts the given new node after the given reference node.
 * (Similar to W3C Node.insertBefore.)
 */
Util.Node.insert_after = function(new_node, ref_node)
{
    ref_node.parentNode.insertBefore(new_node, ref_node.nextSibling);
};

/**
 * Surrounds the given node with an element of the given tagname, 
 * and returns the new surrounding elem.
 */
Util.Node.surround_with_tag = function(node, tagname)
{
    var new_elem = node.ownerDocument.createElement(tagname);
    Util.Node.surround_with_node(node, new_elem);
    return new_elem;
};

/**
 * Surrounds the given inner node with the given outer node.
 */
Util.Node.surround_with_node = function(inner_node, outer_node)
{
    inner_node.parentNode.insertBefore(outer_node, inner_node);
    outer_node.appendChild(inner_node);
};

/**
 * Replaces given node with its children, e.g.
 * lkj <em>asdf</em> jkl becomes, after replace_with_children(em_node),
 * lkj asdf jkl
 */
Util.Node.replace_with_children = function(node)
{
    var parent = node.parentNode;

    if (!parent)
        return; // node was removed already
    
    while (node.firstChild) {
        parent.insertBefore(node.removeChild(node.firstChild), node);
    }
    
    parent.removeChild(node);
};

/**
 * Moves all children and attributes from old_node to new_node. 
 *
 * If old_node is within a DOM tree (i.e., has a non-null parentNode),
 * it is replaced in the tree with new_node. (Since new_node now has
 * all of old_node's former children, the tree is otherwise exactly as 
 * it was before.)
 *
 * If old_node is not within a DOM tree (i.e., has a null parentNode),
 * old_node's children and attrs are moved to new_node, but new_node
 * is not added to any DOM tree (nor is any error thrown).
 * 
 * E.g.,
 *   asdf <i>inside</i> jkl;    
 * becomes, after swap_node(em_elem, i_elem),
 *   asdf <em>inside</em> jkl;
 */
Util.Node.swap_node = function(new_node, old_node)
{
    for ( var i = 0; i < old_node.attributes.length; i++ )
    {
        var attr = old_node.attributes.item(i);
        new_node.setAttributeNode(attr.cloneNode(true));
    }
    while ( old_node.firstChild != null )
    {
        new_node.appendChild( old_node.removeChild(old_node.firstChild) );
    }
    if ( old_node.parentNode != null )
        old_node.parentNode.replaceChild(new_node, old_node);
};

/**
 * Returns the previous sibling of the node that matches the given test,
 * or null if there is none.
 */
Util.Node.previous_matching_sibling = function(node, boolean_test)
{    
    for (var sib = node.previousSibling; sib; sib = sib.previousSibling) {
        if (boolean_test(sib))
            return sib;
    }
    
    return null;
};

/**
 * Returns the next sibling of the node that matches the given test,
 * or null if there is none.
 */
Util.Node.next_matching_sibling = function(node, boolean_test)
{    
    for (var sib = node.nextSibling; sib; sib = sib.nextSibling) {
        if (boolean_test(sib))
            return sib;
    }
    
    return null;
};

/**
 * Returns the previous sibling of the node that is an element node,
 * or null if there is none.
 */
Util.Node.previous_element_sibling = function(node)
{
    return Util.Node.previous_matching_sibling(node, function(n) {
        return n.nodeType == Util.Node.ELEMENT_NODE;
    })
};

/**
 * Returns the next sibling of the node that is an element node,
 * or null if there is none.
 */
Util.Node.next_element_sibling = function(node)
{
    return Util.Node.next_matching_sibling(node, function(n) {
        return n.nodeType == Util.Node.ELEMENT_NODE;
    })
};

/**
 * @return {String} a string that describes the node
 */
Util.Node.get_debug_string = function get_node_debug_string(node)
{
    var str;
    
    if (!Util.is_number(node.nodeType)) {
        return '(Non-node ' + node + ')';
    }
    
    switch (node.nodeType) {
        case Util.Node.ELEMENT_NODE:
            str = '<' + node.nodeName.toLowerCase();
            
            Util.Object.enumerate(Util.Element.get_attributes(node),
                function append_attribute(name, value) {
                    str += ' ' + name + '="' + value + '"';
                }
            );
            
            str += '>';
            break;
        case Util.Node.TEXT_NODE:
            str = '"' + Util.trim(node.nodeValue.toString()) + '"';
            break;
        case Util.Node.DOCUMENT_NODE:
            str = '[Document';
            if (node.location)
                str += ' ' + node.location;
            str += ']';
            break;
        default:
            str = '[' + node.nodeName + ']';
    }
    
    return str;
}

// end file Util.Node.js


// file Util.Browser.js
Util.Browser = {
    IE:     !!(window.attachEvent && !window.opera),
    Opera:  !!window.opera,
    WebKit: (navigator.userAgent.indexOf('AppleWebKit/') > -1),
    Gecko:  (navigator.userAgent.indexOf('Gecko') > -1
        && navigator.userAgent.indexOf('KHTML') == -1),
        
    Windows: (navigator.platform.indexOf('Win') > -1),
    Mac: (navigator.platform.indexOf('Mac') > -1),
    
    get_version: function get_browser_version() {
        var pattern, match;
        
        if (Util.Browser.IE) {
            pattern = /MSIEs+([d+.]+)/;
        } else if (Util.Browser.Gecko) {
            pattern = /rv:([d+.]+)/;
        } else if (Util.Browser.WebKit) {
            if (/Safari/.test(navigator.userAgent)) {
                match = /Version/([d+.]+)/.exec(navigator.userAgent);
                if (match && match.length >= 1)
                    return match[1];
                match = /Safari/([d+.]+)/.exec(navigator.userAgent);
                if (match && match.length >= 1) {
                    if (Util.Browser._safari_versions[match[1]])
                        return Util.Browser._safari_versions[match[1]];
                }
            }
            return '';
        } else if (Util.Browser.Opera) {
            pattern = /Opera[/ ]([d+.]+)/;
        }
        
        match = pattern.exec(navigator.userAgent);
        return (match && match.length >= 1)
            ? match[1]
            : '';
    },
    
    _safari_versions: {
        '525.19': '3.1.2',
        '525.18': '3.1.1',
        '525.7': '3.1',
        '523': '3.0.4',
        '418.8': '2.0.4',
        '417.9': '2.0.3',
        '416': '2.0.2',
        '412.7': '2.0.1',
        '412': '2.0',
        '312.8': '1.3.2',
        '312.5': '1.3.1',
        '312.1': '1.3',
        '125.5.5': '1.2.4',
        '125.4': '1.2.3',
        '125.2': '1.2.2',
        '100': '1.1',
        '85.8.2': '1.0.3',
        '85.7': '1.0.2'
    }
};


// file Util.Element.js
/**
 * @class Container for functions relating to document elements.
 */
Util.Element = {
    /**
     * Set of empty elements
     * @type Object
     */
    empty: (['BR', 'AREA', 'LINK', 'IMG', 'PARAM', 'HR', 'INPUT', 'COL',
        'BASE', 'META'].toSet()),
        
    /**
     * Determines if the given node or tag name represents an empty HTML tag.
     * @param {Element|String}
     * @return {Boolean}
     */
    empty_tag: function is_empty_tag(el)
    {
        var tag = (el.nodeName || String(el)).toUpperCase();
        return (tag in Util.Element.empty);
    },
    
    /**
     * Gets an element's computed styles.
     * @param {Window}    window    the element's window
     * @param {Element}    elem    the element whose computed style is desired
     * @return {object}
     */
    get_computed_style: function get_element_computed_style(window, elem)
    {
        if (!elem || !Util.is_valid_object(window)) {
            throw new TypeError('Valid window and element objects must be ' +
                'provided to Util.Element.get_computed_style.');
        }
        
        if (!elem.nodeType || elem.nodeType != Util.Node.ELEMENT_NODE) {
            throw new TypeError('An element node must be provided to ' + 
                'Util.Element.get_computed_style');
        }
        
        if (Util.is_function(window.getComputedStyle)) {
            return window.getComputedStyle(elem, null);
        } else if (Util.is_valid_object(elem.currentStyle)) {
            return elem.currentStyle;
        } else {
            throw new Util.Unsupported_Error('getting an element's computed ' +
                'style');
        }
    },
    
    /**
     * Tests whether or not an element is at block-level.
     * Cf. Util.Node.is_block_level_element; this uses different logic.
     * @param {Window}    window    the element's window
     * @param {Element}    elem    the element whose block level status is desired
     * @return {boolean}
     */
    is_block_level: function is_block_level_element(window, elem)
    {
        var s;
        
        try {
            s = Util.Element.get_computed_style(window, elem);
            if (s.display == 'inline' || s.display == 'none')
                return false;
            // Assume that everything else ('block', 'table-cell', 'list-item',
            // etc.) is a block.
            return true;
        } catch (e) {
            var ex = new Error('Unable to get the computed style for ' +
                Util.Node.get_debug_string(elem) + '.');
            ex.cause = e;
            throw ex;
        }
    },
    
    /**
     * Returns the attributes of an element.
     * @param {Element}    elem
     * @param {Boolean} [no_translation=false] if true, attribute names that may
     * be language keywords (like "class" and "for") will not be translated
     * @return {Object}    an object whose keys are attribute names and whose
     *                    values are the corresponding values
     */
    get_attributes: function get_element_attributes(elem, no_translation)
    {
        var attrs = {};
        
        if (!elem) {
            throw new TypeError('No element provided; cannot get attributes.');
        }
        
        if (elem.nodeType != Util.Node.ELEMENT_NODE) {
            return attrs;
        } else if (elem.hasAttributes && !elem.hasAttributes()) {
            return attrs;
        }
        
        var names = Util.Element._get_attribute_names(elem);
        var i, name, v, length = names.length;
        for (i = 0; i < length; i++) {
            name = names[i];
            v = elem.getAttribute(name);
            try {
                v = v.toString();
            } catch (e) {
                // Why not just test for toString? Because IE will throw an
                // exception.
            }
            
            switch (name) {
                case 'class':
                case 'className':
                    attrs[(no_translation) ? 'class' : 'className'] = v;
                    break;
                case 'for':
                case 'htmlFor':
                    attrs[(no_translation) ? 'for' : 'htmlFor'] = v;
                    break;
                case 'style':
                    attrs.style = elem.style.cssText;
                    break;
                default:
                    attrs[name] = v;
            }
        }
        
        return attrs;
    },
    
    /**
     * Tests if the element is "basically empty".
     * An element is basically empty if:
     *    - It contains no image, horizontal rule, or table elements, and
     *    - It contains no non-whitespace (spaces, tabs, or line breaks) text.
     * @param {Element}    elem    the element whose emptiness will be tested
     * @return {boolean}    true if the element is basically empty, false if not
     *
     * Logic from TinyMCE.
     */
    is_basically_empty: function element_is_basically_empty(elem)
    {
        if (!elem || elem.nodeType != Util.Node.ELEMENT_NODE) {
            throw new TypeError('Must provide an element node to ' +
                'Util.Element.is_basically_empty(); instead got ' +
                Util.Node.get_debug_string(elem));
        }
        
        var doc = elem.ownerDocument;
        var non_whitespace = /[^ trn]/;
        var acceptable_tags;
        
        if (doc.createTreeWalker && NodeFilter) {
            // Browser supports DOM Level 2 Traversal; use it in the hope that
            // it will be faster than the other branch which uses string
            // manipulations.
            
            // This map must stay in sync with the pattern in the next branch.
            acceptable_tags = {IMG: true, HR: true, TABLE: true};
            
            var filter = {
                acceptNode: function accept_node_for_emptiness_check(node) {
                    switch (node.nodeType) {
                        case Util.Node.TEXT_NODE:
                            // Allow text nodes through if they have
                            // non-whitespace characters so that the code below
                            // can safely return false whenever it receives a
                            // text node.
                            return (non_whitespace.test(node.nodeValue))
                                ? NodeFilter.FILTER_ACCEPT
                                : NodeFilter.FILTER_REJECT
                        case Util.Node.ELEMENT_NODE:
                            // Similarly, allow elements through only if they're
                            // one of the acceptable tags so that the code below
                            // will know what to do instantly. But, skip a non-
                            // acceptable element instead of rejecting it
                            // outright so that any of its descendant text nodes
                            // can be processed.
                            return (node.tagName in acceptable_tags)
                                ? NodeFilter.FILTER_ACCEPT
                                : NodeFilter.FILTER_SKIP;
                        default:
                            // No other types should be making it through
                            // because of our choice of whatToShow below, but
                            // be defensive anyway.
                            return NodeFilter.FILTER_SKIP;
                    }
                }
            };
            
            var walker = doc.createTreeWalker(elem,
                NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, filter, false);
            
            // Because of our filtering above, if we get any next node back
            // (the next node can be any node below our root, which is the
            // element being tested), we know that the element is not empty.
            // If we get nothing back, that means that the tree walker went
            // through all of the ancestors without finding a node that our
            // filter accepted, and thus the element is empty.
            return !walker.nextNode();
        } else {
            // No traversal support. Look at the element's inner HTML.
            
            // This pattern must be kept in sync with the map in the previous
            // branch.
            acceptable_tags = /^<(img|hr|table)$/ig;
            
            var html = elem.innerHTML;
            
            // Preserve our acceptable tags from being eliminated on the next
            // replacement.
            html = html.replace(acceptable_tags, 'k');
            
            // Remove all non-preserved tags.
            html = html.replace(/<[^>]+>/g, '');
            
            // Check to see if what's remaining contains any non-whitespace
            // characters; if it does, then the element is non-empty.
            return !non_whitespace.test(html);
        }
    },
    
    /**
     * Adds a class to an element.
     * @param {Element}    elem    the element to which the class will be added
     * @param {string}    class_name    the name of the class to add
     * @return {void}
     */
    add_class: function add_class_to_element(elem, class_name)
    {
        var classes = Util.Element.get_class_array(elem);
        classes.push(class_name);
        Util.Element.set_class_array(elem, classes);
    },
    
    /**
     * Removes a class from an element.
     * @param {Element}    elem    the element from which the class will be removed
     * @param {string}    class_name    the name of the class to remove
     * @return {void}
     */
    remove_class: function remove_class_from_element(elem, class_name)
    {
        var classes = Util.Element.get_class_array(elem);

        for (var i = 0; i < classes.length; i++) {
            if (classes[i] == class_name)
                classes.splice(i, 1);
        }

        Util.Element.set_class_array(elem, classes);
    },
    
    /**
     * Checks if an element has a particular class.
     * @param {Element}    elem    the element to check
     * @param {string}    class_name    the name of the class to check for
     * @return true if the element has the class, false otherwise
     * @return {boolean}
     */
    has_class: function element_has_class(elem, class_name)
    {
        return Util.Element.get_class_array(elem).contains(class_name);
    },
    
    /**
     * Checks if an element has all of the given classes.
     * @param {Element}    elem    the element to check
     * @param {mixed}    classes    either a string or an array of class names
     * @return true if the element has all of the classes, false if otherwise
     * @return {boolean}
     */
    has_classes: function element_has_classes(elem, classes)
    {
        if (Util.is_string(classes))
            classes = classes.split(/s+/);
        
        var element_classes = Util.Element.get_class_array(elem);
        return classes.every(function check_one_element_class(class_name) {
            return element_classes.contains(class_name);
        });
    },
    
    /**
     * Returns a string with all of an element's classes or null.
     * @param {Element}    elem
     * @return {string}
     */
    get_all_classes: function get_all_classes_from_element(elem)
    {
        return (Util.is_valid_object(elem))
            ? elem.getAttribute('class') || elem.getAttribute('className')
            : null;
    },
    
    /**
     * Gets all of an element's classes as an array.
     * @param {Element}    elem
     * @return {array}
     */
    get_class_array: function get_array_of_classes_from_element(elem)
    {
        return (elem.className && elem.className.length > 0)
            ? elem.className.split(/s+/)
            : [];
    },
    
    /**
     * Sets all of the classes on an element.
     * @param {Element} elem
     * @param {string} class_names
     * @return {void}
     */
    set_all_classes: function set_all_classes_on_element(elem, class_names)
    {
        elem.className = all_classes;
    },
    
    /**
     * Sets all of the classes on an element.
     * @param {Element} elem
     * @param {array} class_names
     * @return {void}
     */
    set_class_array: function set_array_of_classes_on_element(elem, class_names)
    {
        if (class_names.length == 0)
            Util.Element.remove_all_classes(elem);
        else
            elem.className = class_names.join(' ');
    },
    
    /**
     * Removes all of an element's classes.
     * @param {Element}    elem
     * @return {void}
     */
    remove_all_classes: function remove_all_classes_from_element(elem)
    {
        elem.removeAttribute('className');
        elem.removeAttribute('class');
    },
    
    /**
     * Find all elements below the given root with a matching class name.
     * @param {Element|Document} root    the root element
     * @param {string} classes    the class name(s) to search for
     * @return {array}    an array (NOT a NodeList) of elements
     */
    find_by_class: function find_elements_by_class_name(root, classes)
    {
        if (root.getElementsByClassName) { // use native impl. where available
            return Util.Array.from(root.getElementsByClassName(classes));
        }
        
        function xpath_evaluate(expression)
        {
            var results = [];
            var query;
            var i, length;
            
            if (!document.evaluate || !XPathResult) {
                throw new Util.Unsupported_Error("XPath");
            }
            
            query = document.evaluate(expression, root, null,
                XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
            for (i = 0, length = query.snapshotLength; i < length; i++) {
                results.push(query.snapshotItem(i));
            }
            return results;
        }
        
        classes = classes.toString().replace(/^s*/, '').replace(/s*$/, '');
        if (document.evaluate) {
            function convert(cn) {
                return (cn.length > 0) ? "[contains(concat(' ', @class, ' '), "
                    + "' " + cn + " ')]" : null;
            }
            var expr = classes.split(/s+/).map(convert).join('');
            return (expr.length) ? xpath_evaluate('.//*' + expr) : [];
        } else {
            var found = [];
            var children = root.getElementsByTagName("*")
            var child;
            
            classes = classes.split(/s+/);
            var test = (classes.length == 1)
                ? function(e) { return Util.Element.has_class(e, classes[0]); }
                : function(e) { return Util.Element.has_classes(e, classes); };
            
            for (var i = 0; child = children[i]; i++) {
                if (test(child))
                    found.push(child);
            }
            
            return found;
        }
    },
    
    /**
     * Returns an element's name's prefix or an empty string if there is none.
     * (e.g. <o:p> --> 'o';  <p> --> '')
     * @param {Element}    elem
     * @return {string}
     */
    get_prefix: function get_element_name_prefix(elem)
    {
        function get_gecko_prefix()
        {
            var parts = node.tagName.split(':');
            return (parts.length >= 2) ? parts[0] : '';
        }
        
        return node.prefix || node.scopeName || get_gecko_prefix();
    },
    
    /**
     * Finds the absolute position of the element; i.e. its position relative to
     * the window.
     * @param {HTMLElement} elem
     * @return {object}
     */
    get_position: function get_element_position(elem)
    {
        var pos = {x: 0, y: 0};
        
        // Loop through the offset chain.
        for (var e = elem; e; e = e.offsetParent) {
            pos.x += (Util.is_number(e.offsetLeft))
                 ? e.offsetLeft
                : e.screenLeft;
            pos.y += (Util.is_number(e.offsetTop))
                 ? e.offsetTop
                : e.screenTop;
        }
        
        return pos;
    },
    
    /**
     * For each element out of the given element and its ancestors that has a
     * CSS position of "relative", sums up their x and y offsets and returns
     * them.
     * @param {Window}    window    the element's window
     * @param {HTMLElement}    elem    the element to test
     * @return {object}    x and y offsets
     */
    get_relative_offsets: function get_element_relative_offsets(window, elem)
    {
        if (!Util.Node.is_element(elem) || !Util.is_valid_object(window)) {
            throw new TypeError('Must provide valid window and element ' +
                'objects to Util.Event.get_relative_offsets().');
        }
        
        var pos = {x: 0, y: 0};
        
        for (var e = elem; e && e.nodeName != 'HTML'; e = e.parentNode) {
            var position = Util.Element.get_computed_style(window, e).position;
            if (position == 'relative') {
                pos.x += e.offsetLeft;
                if (!Util.Element._buggy_ie_offset_top())
                    pos.y += e.offsetTop;
            }
        }
        
        return pos;
    },
    
    /**
     * True if the browser is IE в‰¤ 7, which incorrectly calculates elements'
     * offsetTop attribute.
     * @see http://www.quirksmode.org/dom/w3c_cssom.html#offsetParent
     * @type Boolean
     */
    _buggy_ie_offset_top: function buggy_ie_offset_top() {
        var match, major;
        
        if (typeof(buggy_ie_offset_top.result) == 'undefined') {
            if (!Util.Browser.IE) {
                buggy_ie_offset_top.result = false;
            } else {
                match = /^(d)/.exec(Util.Browser.get_version());
                if (match && match.length && match.length >= 1) {
                    major = parseInt(match[1]);
                    buggy_ie_offset_top.result =  (major <= 7);
                } else {
                    buggy_ie_offset_top.result = false;
                }
            }
        }
        
        return buggy_ie_offset_top.result;
    }
};

Util.Element._get_attribute_names = (function has_outer_html() {
    var guinea_pig = document.createElement('P');
    var parser = null;
    var attrs;
    guinea_pig.className = "_foo";
    
    if (guinea_pig.outerHTML && (/_foo/.test(guinea_pig.outerHTML))) {
        return function _get_attribute_names_from_outer_html(el) {
            var result;
            
            if (!parser) {
                parser = new Util.HTML_Parser();
                parser.add_listener('open', function tag_opened(n, attributes) {
                    attrs = Util.Object.names(attributes);
                    parser.halt();
                });
            }
            
            parser.parse(el.outerHTML);
            result = attrs;
            attrs = null;
            return result;
        };
    } else if (Util.Browser.Gecko) {
        // It looks like at least Firefox 3 is giving us the attributes in
        // reversed declaration order, so we'll read them out backwards.
        return function _get_attribute_names_reversed(el) {
            var length = el.attributes.length;
            var attributes = {};
            var a;
            for (var i = (length - 1); i >= 0; i--) {
                a = el.attributes[i];
                if (!a.specified || a.nodeName in attributes)
                    continue;
                attributes[a.nodeName] = true;
            }
            return Util.Object.names(attributes);    
        };
    } else {
        return function _get_attribute_names(el) {
            var length = el.attributes.length;
            var attributes = {};
            var a;
            for (var i = 0; i < length; i++) {
                a = el.attributes[i];
                if (!a.specified || a.nodeName in attributes)
                    continue;
                attributes[a.nodeName] = true;
            }
            return Util.Object.names(attributes);    
        };
    }
})();

// file Util.Event.js
/**
 * Does nothing.
 *
 * @class A container for functions relating to events. (Not that it
 * matters much, but it makes sense for even functions that work
 * primarily on something other than an event (for example,
 * add_event_listener works primarily on a node) to be in here rather
 * than elsewhere (for example, Util.Node) because all evente-related
 * function are in the DOM2+ standards defined in non-core modules,
 * i.e.
 */
Util.Event = function()
{
};

/**
 * Creates a wrapper around a function that ensures it will always be called
 * with the event object as its sole parameter.
 *
 * @param    func    the function to wrap
 */
Util.Event.listener = function(func)
{    
    return function()
    {
        return func(arguments[0] || window.event);
    };
}

/**
 * Adds an event listener to a node. 
 * <p>
 * N.B., for reference, that it is dangerous in IE to attach as a
 * listener a public method of an object. (The browser may crash.) See
 * Loki's Listbox.js for a workaround.
 *
 * @param    node        the node to which to add the event listener
 * @param    type        a string indicating the type of event to listen for, e.g. 'click', 'mouseover', 'submit', etc.
 * @param    listener    a function which will be called when the event is fired, and which receives as a paramater an
 *                      Event object (or, in IE, a Util.Event.DOM_Event object)
 */
Util.Event.add_event_listener = function(node, type, listener)
{
    if (!Util.is_valid_object(node)) {
        throw new TypeError("Cannot listen for a '" + type + "' event on a " +
            "non-object.");
    } else if (!type || !listener) {
        throw new Error("Must provide an event type and a callback function " +
            "to add an event listener.");
    }
    
    if (node.addEventListener) {
        node.addEventListener(type, listener, false);
    } else if (node.attachEvent) {
        node.attachEvent('on' + type, listener);
    } else {
        throw new Util.Unsupported_Error('modern event handling');
    }
};

/**
 * (More intelligently and concisely) adds an event listener to a node.
 * @param {Node}    target    the node to which to add the event listener
 * @param {string}    type    the type of event to listen for
 * @param {function}    listener    the listener function that will be called
 * @param {object}    context    the "this context" in which to call the listener
 * @type void
 */
Util.Event.observe = function(target, type, listener, context)
{
    if (target.addEventListener) {
        if (context) {
            target.addEventListener(type, function event_listener_proxy() {
                listener.apply(context, arguments);
            }, false);
        } else {
            target.addEventListener(type, listener, false);
        }
    } else if (target.attachEvent) {
        target.attachEvent('on' + type, function ie_event_listener_proxy() {
            listener.call(context, (arguments[0] || window.event));
        });
    } else {
        throw new Util.Unsupported_Error('modern event handling');
    }
}

/**
 * Removes an event listener from a node. Doesn't work at present.
 *
 * @param    node        the node from which to remove the event listener
 * @param    type        a string indicating the type of event to stop listening for, e.g. 'click', 'mouseover', 'submit', etc.
 * @param    listener    the listener function to remove
 */
Util.Event.remove_event_listener = function(node, type, listener)
{
    try
    {
        node.removeEventListener(type, listener, false); // I think that with "false" this is equivalent to the IE way below
    }
    catch(e)
    {
        try
        {
            node.detachEvent('on' + type, listener);
        }
        catch(f)
        {
            throw(new Error('Util.Event.remove_event_listener(): Neither the W3C nor the IE way of removing an event listener worked. ' +
                            'When the W3C way was tried, an error with the following message was thrown: <<' + e.message + '>>. ' +
                            'When the IE way was tried, an error with the following message was thrown: <<' + f.message + '>>.'));
        }
    }
};

/**
 * Tests whether the given keyboard event matches the provided key code.
 * @param {Event}    e    the keyboard event
 * @param {integer} key_code    the key code
 * @return {boolean} true if the given event represented the code, false if not
 */
Util.Event.matches_keycode = function matches_keycode(e, key_code)
{
    if (['keydown', 'keyup'].contains(e.type) && e.keyCode == keycode) {
        return true;
    } else if (e.type == 'keypress') {
        var code = (e.charCode)
            ? e.charCode
            : e.keyCode; // Internet Explorer instead puts the ASCII value here.
            
            return key_code == code ||
                (key_code >= 65 && key_code <= 90 && key_code + 32 == code);
    } else {
        throw new TypeError('The given event is not an applicable ' +
            'keyboard event.');
    }
};

/**
 * Gets the mouse coordinates of the given event.
 * @type object
 * @param {Event} event    the mouse event
 * @return {x: (integer), y: (integer)}
 */
Util.Event.get_coordinates = function get_coordinates(event)
{
    var doc = (event.currentTarget || event.srcElement).ownerDocument;
    
    var x = event.pageX || event.clientX + doc.body.scrollLeft +
        doc.documentElement.scrollLeft;
    var y = event.pageY || event.clientY + doc.body.scrollTop +
        doc.documentElement.scrollTop;
        
    return {x: x, y: y};
};

/**
 * Calls the listeners which have been "attached" to the
 * event.currentTarget using add_event_listener. This function is
 * intended for use primarily by add_event_listener.
 *
 * @param    event    the event object, to pass to the listeners
 */
Util.Event.call_wrapped_listeners = function(event)
{
    var node = event.currentTarget;
    var type = event.type;
    var listener, extra_args;

    for ( var i = 0; i < node.Event__listeners[type].length; i++ )
    {
        listener = node.Event__listeners[type][i]['listener'];
        extra_args = node.Event__listeners[type][i]['extra_args'];

        listener(event, extra_args);
    }
};

/**
 * Constructor for a mimic'd DOM Event object, primarly for use in the
 * IE version of Util.Event.add_event_listener. Properties which are
 * initialized below to null are in the W3C spec but haven't yet
 * needed to be implemented in this mimic'd object.
 *
 * @param    currentTarget    the document node which is the target of the event
 * @param    type            the type of the event, e.g. 'click'
 */
Util.Event.IE_DOM_Event = function(currentTarget, type)
{
    this.type = type;
//     this.target = window.event.srcElement; // doesn't work if the event's target belongs to another window than the one referenced by "window", e.g. a popup window
    this.currentTarget = currentTarget;
    this.eventPhase = null;
    this.bubbles = null;
    this.cancelable = null;
    this.timeStamp = null;
    this.initEvent = null;
    this.initEvent = function(eventTypeArg, canBubbleArg, cancelableArg) { return null; };
    this.preventDefault = function() { window.event.returnValue = false; };
    this.stopPropogation = function() { window.event.cancelBubble = true; };
};

Util.Event.prevent_default = function(event)
{
    try // W3C
    {
        event.preventDefault();
    }
    catch(e)
    {
        try // IE
        {
            event.returnValue = false;
            //event.cancelBubble = true;
        }
        catch(f)
        {
            throw('Util.Event.prevent_default: Neither the W3C nor the IE way of preventing the event's default action. ' +
                  'When the W3C way was tried, an error with the following message was thrown: <<' + e.message + '>>. ' +
                  'When the IE way was tried, an error with the following message was thrown: <<' + f.message + '>>.');
        }
    }
    return false;
};

/**
 * Returns the target.
 * Taken from quirksmode.org, by Peter-Paul Koch.
 */
Util.Event.get_target = function get_event_target(e)
{
    var targ;
    //if (!e) var e = window.event;
    if (e.target) targ = e.target;
    else if (e.srcElement) targ = e.srcElement;
    if (targ.nodeType == 3) // defeat Safari bug
        targ = targ.parentNode;
    return targ;
};

// file Util.Object.js
/**
 * Does nothing.
 *
 * @class Container for functions relating to objects.
 */
Util.Object = function()
{
};

/**
 * Returns the names of an object's properties as an array. Ignores properties
 * found on any object.
 */
Util.Object.names = function(obj)
{
    var names = [];
    var bare = {};
    
    // JavaScript doesn't really have a hash or dictionary type, only a
    // generic object type. This is a problem because the variables object
    // we're given can have properties that are intrinsic to objects which
    // shouldn't be added to the query string. To work around this, we
    // create a bare object and ignore any properties in variables that are
    // also found on the bare object.
    
    for (var name in obj) {
        if (name in bare)
            continue;
        names.push(name);
    }
    
    return names;
}

/**
 * Calls the given function once per property in the object. The function
 * should accept the property's name as the first argument and its value as
 * the second.
 */
Util.Object.enumerate = function(obj, func, thisp)
{
    if (!thisp)
        var thisp = null;
    
    Util.Object.names(obj).each(function (name)
    {
        func.call(thisp, name, obj[name]);
    });
}

/**
 * Clones (creates a copy of) the given object.
 */
Util.Object.clone = function(some_object)
{
    var new_obj;
    
    if (!some_object || typeof(some_object) != 'object')
        return some_object;
    
    try {
        new_obj = new some_object.constructor();
    } catch (e) {
        new_obj = new Object();
    }
    
    for (var name in some_object) {
        new_obj[name] = some_object[name];
    }
    
    return new_obj;
}

/**
 * Determines if two objects are equal.
 */
Util.Object.equal = function(a, b)
{
    if (typeof(a) != 'object') {
        return (typeof(b) == 'object')
            ? false
            : (a == b);
    } else if (typeof(b) != 'object') {
        return false;
    }
    
    seen = {};
    
    for (var name in a) {
        if (!(name in b && Util.Object.equal(a[name], b[name])))
            return false;
        seen[name] = true;
    }
    
    for (var name in b) {
        if (!(name in seen))
            return false;
    }
    
    return true;
}

/**
 * Pops up a window whose contents are generated by get_print_r_chunk, q.v.
 *
 * @param    obj                the object to print_r
 * @param    max_deepness    (optional) how many levels of parameters to automatically open. Defaults to 1.
 * @return                    a UL element which has as descendents a representation of the given object
 */
Util.Object.print_r = function(obj, max_deepness)
{
    var alert_win = new Util.Window;
    alert_win.open('', '_blank', 'status=1,scrollbars=1,resizable,width=600,height=300');
    var print_r_chunk = Util.Object.get_print_r_chunk(obj, alert_win.document, alert_win, max_deepness);
    alert_win.body.appendChild(print_r_chunk);
};

/**
 * Generates a UL element which has as descendents a representation of
 * the given object. The representation is similar to that exposed by
 * PHP's print_r or pray.
 *
 * @param    obj                the object to print_r
 * @param    doc_obj            (optional) the document object with which to create the print_r chunk. 
 *                          Defaults to the document refered to by <code>document</code>.
 * @param    max_deepness    (optional) how many levels of parameters to automatically open. Defaults to 1.
 * @return                    a UL element which has as descendents a representation of the given object
 */
Util.Object.get_print_r_chunk = function(obj, doc_obj, win, max_deepness)
{
    if ( doc_obj == null )
    {
        doc_obj = document;
    }

    if ( max_deepness == null )
    {
        max_deepness = 1;
    }


    /**
     * Displays or hides the properties of a property of an object being
     * print_r'd. Should be called only when a click event is fired by the
     * appropriate element in the print_r window.
     *
     * @param    event    The event object passed onclick
     */
    var open_or_close_print_r_ul = function(event, variable)
    {
        event = event == null ? win.event : event;
        var span_elem = event.currentTarget == null ? event.srcElement : event.currentTarget;

        // If open, close
        if ( span_elem.nextSibling != null )
        {
            //alert('open, so close (nextSibling =' + span_elem.nextSibling);
            while ( span_elem.nextSibling != null )
                span_elem.parentNode.removeChild(span_elem.nextSibling);
        }
        // Else (if closed), open
        else
        {
            //alert('closed, so open (variable:' + variable + '); span_elem:' + span_elem);
            span_elem.parentNode.appendChild(
                Util.Object.get_print_r_chunk(variable, span_elem.ownerDocument, 1)
            );
        }
    };


    var ul_elem = doc_obj.createElement('UL');

    for ( var var_name in obj )
    {
        var variable, li_elem;
        try
        {
            variable = obj[var_name];

            li_elem = ul_elem.appendChild(
                doc_obj.createElement('LI')
            );
            span_elem = li_elem.appendChild(
                doc_obj.createElement('SPAN')
            );
            span_elem.appendChild(
                doc_obj.createTextNode(var_name + " => " + variable)
            );
            Util.Event.add_event_listener(span_elem, 'click', function (event) { open_or_close_print_r_ul(event, variable); });
            //span_elem.onclick = open_or_close_print_r_ul;

            var typeof_variable = typeof(variable);
            if ( typeof_variable == "object" &&
                 !( typeof_variable == "string" ||
                    typeof_variable == "boolean" ||
                    typeof_variable == "number" ) )
            {
                if ( max_deepness > 1 )
                {
                    li_elem.appendChild(
                        Util.Object.get_print_r_chunk(variable, doc_obj, win, max_deepness - 1)
                    );
                }
            }
        }
        catch(e)
        {
            // Only stop for fatal errors, because some properties when
            // accessed will always throw an error, and to die for
            // all of these would make print_r useless.
            if ( e.name != 'InternalError' )
            {
                ul_elem.appendChild(
                    doc_obj.createElement('LI')
                ).appendChild(
                    doc_obj.createTextNode(var_name + " => [[[Exception thrown: " + e.message + "]]]")
                );
            }
            else
            {
                throw e;
            }
        }
    }
    return ul_elem;
};

// file Util.OOP.js
/**
 * @class Container for methods that allow standard OOP thinking to be
 * shoehorned into JavaScript, for better or worse.
 */
Util.OOP = {};

/**
 * "Mixes in" an object's properties.
 * @param    {object}    target    The object into which things will be mixed
 * @param    {object}    source    The object providing the properties
 * @type object
 * @return target
 */
Util.OOP.mixin = function(target, source)
{
    var names = Util.Object.names(source);
    for (var i = 0; i < names.length; i++) {
        target[names[i]] = source[names[i]];
    }
    
    return target;
}

/**
 * Sets up inheritance from parent to child. To use:
 * - Create parent and add parent's methods and properties.
 * - Create child
 * - At beginning of child's constructor, call inherits(parent, child)
 * - Add child's new methods and properties
 * - To call method foo in the parent: this.superclass.foo.call(this, params)
 * - Be careful where you use self and this: in inherited methods, self
 *   will still refer to the superclass, whereas this will refer, properly, to the
 *   child class. If you must use self, e.g. for event listeners, define self
 *   only inside methods, not directly inside the constructor. (Note: The existing
 *   code doesn't follow this advice perfectly; follow this advice, not that code.)
 *
 * Changed on 2007-09-13 by EN: Now calls the parent class's constructor! Any
 * arguments that need to be passed to the constructor can be provided after
 * the child and parent.
 *
 * Inspired by but independent of <http://www.crockford.com/javascript/inheritance.html>.
 *
 * The main problem with just doing something like
 *     child.prototype = new parent();
 * is that methods inherited from the parent can't set properties accessible
 * by methods defined in the child.
 */
Util.OOP.inherits = function(child, parent)
{
    var parent_prototype = null;
    var nargs = arguments.length;
    
    if (nargs < 2) {
        throw new TypeError('Must provide a child and a parent class.');
    } else if (nargs == 2) {
        parent_prototype = new parent;
    } else {
        // XXX: Is there really no better way to do this?!
        //      Something involving parent.constructor maybe?
        var arg_list = $R(2, nargs).map(function (i) {
            return 'arguments[' + String(i) + ']';
        });
        eval('parent_prototype = new parent(' + arg_list.join(', ') + ')')
    }
    
    Util.OOP.mixin(child, parent_prototype);
    child.superclass = parent_prototype;
};

/**
 * Sets up inheritance from parent to child, but only copies over the elements
 * in the parent's prototype provided as arguments after the parent class.
 */
Util.OOP.swiss = function(child, parent)
{
    var parent_prototype = new parent;
    for (var i = 2; i < arguments.length; i += 1) {
        var name = arguments[i];
        child[name] = parent_prototype[name];
    }
    return child;
}; 
// file Util.Anchor.js
Util.Anchor = function()
{
};

/**
 * Creates a DOM anchor element and adds the given name attribute. This
 * is necessary because of a bug in IE which doesn't allow the name
 * attribute to be set on created anchor elements.
 *
 * @static
 * @param    params    object containing the following named paramaters:
 *                  <ul>
 *                  <li>doc - the document object with which to create the anchor</li>
 *                  <li>name - the desired name of the anchor</li>
 *                  </ul>
 * @return            a DOM anchor element
 */
Util.Anchor.create_named_anchor = function(params)
{
    var doc = params.document;
    var name = params.name;

    // Make sure required arguments are given
    if ( doc == null || name == '' )
        throw(new Error('Util.Anchor.create_named_anchor: Missing argument.'));

    // First try to create the anchor and add its name attribute
    // normally
    var anchor = doc.createElement('A');
    anchor.setAttribute('name', name);
    

    // If that didn't work, create it in the IE way
    if ( anchor.outerHTML != null && anchor.outerHTML.indexOf('name') == -1 )
    {
        anchor = doc.createElement('<A name="' + name + '">');
    }

    // Make sure it worked
    if ( anchor == null || anchor.getAttribute('name') == '' )
        throw(new Error('Util.Anchor.create_named_anchor: Couldn't create named anchor.'));
        
    return anchor;
};

// file Util.Block.js
/**
 * Defines the behavior of the block level elements with regard to paragraphs.
 * Replaces Util.BLE_Rules.
 */
Util.Block = {
    /**
     * Element is a block-level element.
     * @type Number
     */
    BLOCK: 1,
    
    /**
     * Element is a paragraph. It cannot contain two line breaks in succession.
     */
    PARAGRAPH: 2,
    
    /**
     * Element can contain paragraphs (and, in fact, all inline content should
     * be within them).
     * @type Number
     */
    PARAGRAPH_CONTAINER: 4,
    
    /**
     * Inline content nodes should be direct children of this element unless
     * multiple paragraphs are desired, in which case it should behave as a
     * paragraph container.
     * @type Number
     */
    MULTI_PARAGRAPH_CONTAINER: 8,
    
    /**
     * Directly contains inline content; should not contain paragraphs.
     * @type Number
     */
    INLINE_CONTAINER: 16,
    
    /**
     * Block-level element that may not contain anything.
     * @type Number
     */
    EMPTY: 32,
    
    /**
     * Can exist as either a block-level element or an inline child of a block-
     * level element.
     * @type Number
     */
    MIXED: 64,
    
    /**
     * Whitespace is preserved within these elements.
     * @type Number
     */
    PREFORMATTED: 128,
    
    get_flags: function get_flags(element)
    {
        return (this._get_flag_map()[element.tagName] || 0);
    },
    
    is_block: function is_block(element)
    {
        return !!(this.get_flags(element) & Util.Block.BLOCK);
    },
    
    is_paragraph_container: function is_paragraph_container(element)
    {
        return !!(this.get_flags(element) & Util.Block.PARAGRAPH_CONTAINER);
    },
    
    is_multi_paragraph_container: function is_multi_paragraph_container(element)
    {
        return !!(this.get_flags(element) &
            Util.Block.MULTI_PARAGRAPH_CONTAINER);
    },
    
    is_inline_container: function is_inline_container(element)
    {
        return !!(this.get_flags(element) & Util.Block.INLINE_CONTAINER);
    },
    
    is_empty: function is_empty(element)
    {
        return !!(this.get_flags(element) & Util.Block.EMPTY);
    },
    
    is_mixed: function is_mixed(element)
    {
        return !!(this.get_flags(element) & Util.Block.MIXED);
    },
    
    is_preformatted: function is_preformatted(element)
    {
        return !!(this.get_flags(element) & Util.Block.PREFORMATTED);
    },
    
    /**
     * Accepts either an HTML document or an element and enforces paragraph
     * behavior inside that node and its children.
     * @param {Node}     root        an HTML document or element
     * @param {object}     [settings]  parameters that change enforcement settings
     * @config {object}  [overrides] if specified, allows element flags to be
     *                               overridden
     * @return {void}
     */
    enforce_rules: function enforce_paragraph_rules(root, settings)
    {
        var node;
        var waiting;
        var flags;
        var child;
        var descend;
        
        if (!settings)
            settings = {};
        
        if (root.nodeType == Util.Node.DOCUMENT_NODE) {
            root = root.body;
        } else if (root == root.ownerDocument.documentElement) {
            root = root.ownerDocument.body;
        } else if (root.tagName == 'HEAD') {
            throw new Error('Cannot enforce paragraph rules on a HEAD tag.');
        }
        
        function get_flags(element)
        {
            return (settings.overrides && settings.overrides[element.tagName])
                || Util.Block.get_flags(element);
        }
        
        function is_relevant(node)
        {
            // The regular expression below is different than that used
            // in Util.Node.is_non_whitespace_text_node; the latter does
            // not include spaces. I'm not actually sure which is correct.
            
            return (node.nodeType == Util.Node.ELEMENT_NODE || 
                node.nodeType == Util.Node.TEXT_NODE &&
                /S/.test(node.nodeValue));
        }
        
        function is_br(node)
        {
            return node && node.tagName == 'BR';
        }
        
        function is_breaker(node)
        {
            var breaker = null;
            
            if (!is_br(node))
                return false;
                
            // Mozilla browsers (at least) like to keep a BR tag at the end
            // of all paragraphs. As a result, if the user tries to insert a
            // line break at the end of a paragraph, the HTML will end up as:
            //    <p> ...<br><br></p>
            // This is bad because we will detect this as a "breaker" and
            // possibly insert a new paragraph afterwards and delete both the
            // user's line break and Mozilla's. As a workaround, we will only
            // treat two BR's as a breaker if they do not come at the end of
            // their parent.
                
            for (var s = node.nextSibling; s; s = s.nextSibling) {
                if (!breaker) {
                    if (is_br(s))
                        breaker = [node, s];
                    else if (is_relevant(s))
                        return false;
                } else if (is_relevant(s)) {
                    // The breaker is not at the end of its parent.
                    return breaker;
                }
            }
            
            return false;
        }
        
        function belongs_inside_paragraph(node)
        {
            var ok_types = [Util.Node.TEXT_NODE, Util.Node.COMMENT_NODE];
            var flags;
            
            if (ok_types.contains(node.nodeType))
                return true;
            
            flags = get_flags(node);
            return !(flags & Util.Block.BLOCK) || !!(flags & Util.Block.MIXED);
        }
        
        // Factored out this enforcement because both normal paragraph
        // containers and containers that can only contain 0 or в‰Ґ2 paragraphs
        // both potentially use the same behavior.
        function enforce_container_child(context, node, c)
        {
            var br;
            var next;
            var created_p;
            
            if (!context.p)
                context.p = null;
            if (context.created_p)
                delete context.created_p;
            
            if (br = is_breaker(c)) { // assignment intentional
                context.p = c.ownerDocument.createElement('P');
                next = br[1].nextSibling;
                br.each(function(b) {
                    node.removeChild(b);
                });
                node.insertBefore(context.p, next);
            } else if (belongs_inside_paragraph(c)
                && Util.Node.is_non_whitespace_text_node(c)) 
            {
                if (!context.p && is_relevant(c)) {
                    context.p = c.ownerDocument.createElement('P');
                    context.created_p = context.p;
                    node.insertBefore(context.p, c);
                }
                
                if (context.p) {
                    next = c.nextSibling;
                    context.p.appendChild(c);
                }
            } else if (context.p) {
                delete context.p;
            }
            
            if (!next)
                next = c.nextSibling;
            
            return next;
        }
        
        var enforcers = {
            PARAGRAPH: function enforce_paragraph(node)
            {
                var new_p;
                var next;
                var br;
                
                function create_split_paragraph()
                {
                    var next_s;
                    
                    new_p = node.ownerDocument.createElement('P');
                    for (var c = next; c; c = next_s) {
                        next_s = c.nextSibling;
                        new_p.appendChild(c);
                    }
                    
                    node.parentNode.insertBefore(new_p, node.nextSibling);
                    return new_p;
                }
                
                for (var c = node.firstChild; c; c = next) {
                    next = null;
                    
                    if (!belongs_inside_paragraph(c)) {
                        if (!c.previousSibling) {
                            node.parentNode.insertBefore(c, node);
                        } else {
                            next = c.nextSibling;
                            
                            if (next) {
                                // Create a new paragraph, move all of the
                                // children that followed the breaker into it,
                                // and continue using that paragraph.
                                node = create_split_paragraph();
                                next = node.firstChild;
                                
                                // Move the item that does not belong in the
                                // paragraph outside of it and place it between
                                // the existing paragraph and the new split
                                // paragraph.
                                // (Remember, "node" now refers to the split-off
                                // paragraph.)
                                node.parentNode.insertBefore(c, node);
                            } else {
                                node.parentNode.insertBefore(c,
                                    node.nextSibling);
                            }
                        }
                    } else if (br = is_breaker(c)) { // assignment intentional
                        next = br[1].nextSibling;
                        br.each(function(b) {
                            b.parentNode.removeChild(b);
                        });
                        
                        if (next) {
                            // Create a new paragraph, move all of the
                            // children that followed the breaker into it,
                            // and continue using that paragraph.
                            node = create_split_paragraph();
                            next = node.firstChild;
                        }
                    }
                    
                    if (!next)
                        next = c.nextSibling;
                }
                
                if (!node.hasChildNodes()) {
                    node.parentNode.removeChild(node);
                }
                
                return false;
            },
            
            PARAGRAPH_CONTAINER: function enforce_p_container(node)
            {
                var context = {};
                var next;
                
                for (var c = node.firstChild; c; c = next) {
                    next = enforce_container_child(context, node, c);
                }
                
                return node.hasChildNodes();
            },
            
            MULTI_PARAGRAPH_CONTAINER: function enforce_multi_p_container(node)
            {
                var paragraphs = [];
                var multi = get_paragraph_children(node).length > 1;
                var context = {};
                var br;
                var next;
                
                function get_paragraph_children(node)
                {
                    var paras = [];
                    
                    for (var n = node.firstChild; n; n = n.nextSibling) {
                        if (n.tagName == 'P')
                            paras.push(n);
                    }
                    
                    return paras;
                }
                
                function add_paragraph(para)
                {
                    if (para)
                        paragraphs.push(para);
                    
                    return !!para;
                }
                
                function replace_with_children(node)
                {
                    while (node.firstChild) {
                        node.parentNode.insertBefore(node.firstChild, node);
                    }
                    
                    node.parentNode.removeChild(node);
                }
                
                function create_upto(stop)
                {
                    var para = stop.ownerDocument.createElement('P');
                    
                    var c = node.firstChild;
                    var worthwhile = false;
                    var next;
                    while (c && c != stop) {
                        if (!worthwhile && is_relevant(c))
                            worthwhile = true;
                        
                        next = c.nextSibling;
                        para.appendChild(c);
                        c = next;
                    }
                    
                    if (worthwhile)
                        return node.insertBefore(para, stop);
                    
                    return null;
                }
                
                for (var c = node.firstChild; c; c = next) {
                    if (!multi) {
                        next = c.nextSibling;
                        
                        if (!belongs_inside_paragraph(c)) {
                            multi = add_paragraph(create_upto(c));
                            if (c.tagName == 'P')
                                multi = add_paragraph(c);
                        } else if (br = is_breaker(c)) { // assignment intent.
                            multi = add_paragraph(create_upto(c));
                            next = br[1].nextSibling;
                            br.each(function(b) {
                                b.parentNode.removeChild(b);
                            });
                        }
                    } else {
                        next = enforce_container_child(context, node, c);
                        if (context.created_p)
                            paragraphs.push(context.created_p);
                    }
                }
                
                if (paragraphs.length == 1) {
                    replace_with_children(paragraphs[0]);
                }
                
                return node.hasChildNodes();
            },
            
            INLINE_CONTAINER: function enforce_inline_container(node)
            {
                // When we discover paragraphs in one of these containers, we
                // actually want to replace them with double line breaks.
                
                var next;
                var next_pc;
                
                function add_br_before(n)
                {
                    var br = n.ownerDocument.createElement('BR');
                    n.parentNode.insertBefore(br, n);
                    return br;
                }
                
                function is_basically_first(n)
                {
                    var m = n;
                    while (m = m.previousSibling) { // assignment intentional
                        if (m.nodeType == Util.Node.ELEMENT_NODE) {
                            return false;
                        }
                        
                        if (m.nodeType == Util.Node.TEXT_NODE &&
                            (/S/.test(m.nodeValue)))
                        {
                            return false;
                        }
                    }
                    
                    return true;
                }
                
                for (var c = node.firstChild; c; c = next) {
                    next = c.nextSibling;
                    if (c.tagName == 'P') {
                        if (!is_basically_first(c)) {
                            add_br_before(c);
                            add_br_before(c);
                        }
                        
                        for (var pc = c.firstChild; pc; pc = next_pc) {
                            next_pc = pc.nextSibling;
                            node.insertBefore(pc, c);
                        }
                        
                        node.removeChild(c);
                    }
                }
                
                return false;
            },
            
            EMPTY: function enforce_empty_block_level_element(node)
            {
                while (node.firstChild)
                    node.removeChild(node.firstChild);
                
                return false;
            }
        };
        
        waiting = [root];
        
        while (node = waiting.pop()) { // assignment intentional
            flags = get_flags(node);
            
            if (!flags & Util.Block.BLOCK)
                continue;
                
            descend = true; // default to descend if we don't find an enforcer
                            // for the current node
            for (var name in enforcers) {
                if (flags & Util.Block[name]) {
                    descend = enforcers[name](node);
                    break;
                }
            }
            
            if (!descend)
                continue;
            
            // Add the node's children (if any) to the processing stack.
            for (child = node.lastChild; child; child = child.previousSibling) {
                if (child.nodeType == Util.Node.ELEMENT_NODE)
                    waiting.push(child);
            }
        }
    },
    
    _get_flag_map: function _get_block_flag_map()
    {
        var map;
        var NORMAL = 0;
        
        if (!this._flag_map) {
            // Util.Block.BLOCK is added to all of these at the final step.
            map = {
                P: Util.Block.PARAGRAPH,
                
                BODY: Util.Block.PARAGRAPH_CONTAINER,
                BLOCKQUOTE: Util.Block.PARAGRAPH_CONTAINER,
                FORM: Util.Block.PARAGRAPH_CONTAINER,
                FIELDSET: Util.Block.PARAGRAPH_CONTAINER,
                BUTTON: Util.Block.PARAGRAPH_CONTAINER,
                MAP: Util.Block.PARAGRAPH_CONTAINER,
                NOSCRIPT: Util.Block.PARAGRAPH_CONTAINER,
                DIV: Util.Block.PARAGRAPH_CONTAINER, // changed from multi

                H1: Util.Block.INLINE_CONTAINER,
                H2: Util.Block.INLINE_CONTAINER,
                H3: Util.Block.INLINE_CONTAINER,
                H4: Util.Block.INLINE_CONTAINER,
                H5: Util.Block.INLINE_CONTAINER,
                H6: Util.Block.INLINE_CONTAINER,
                ADDRESS: Util.Block.INLINE_CONTAINER,
                PRE: Util.Block.INLINE_CONTAINER | Util.Block.PREFORMATTED,

                TH: Util.Block.MULTI_PARAGRAPH_CONTAINER,
                TD: Util.Block.MULTI_PARAGRAPH_CONTAINER,
                LI: Util.Block.MULTI_PARAGRAPH_CONTAINER,
                DT: Util.Block.MULTI_PARAGRAPH_CONTAINER,
                DD: Util.Block.MULTI_PARAGRAPH_CONTAINER, // changed from pc
                
                OBJECT: NORMAL,
                
                UL: NORMAL,
                OL: NORMAL,
                DL: NORMAL,
                
                TABLE: NORMAL,
                THEAD: NORMAL,
                TBODY: NORMAL,
                TFOOT: NORMAL,
                TR: NORMAL,
                NOFRAMES: NORMAL,
                
                HR: Util.Block.EMPTY,
                IFRAME: Util.Block.EMPTY,
                PARAM: Util.Block.EMPTY,
                
                // XXX: browsers seem to treat these as inline always
                INS: Util.Block.MIXED,
                DEL: Util.Block.MIXED
            };
            
            this._flag_map = {};
            for (var name in map) {
                this._flag_map[name] = (map[name] | Util.Block.BLOCK);
            }
        }
        
        return this._flag_map;
    }
};

// file Util.Chooser.js
/**
 * Constructs a new chooser.
 * @class Allows items and sets of those items to be easily chosen using
 * a simple string selector.
 * @constructor
 * @author Eric Naeseth
 */
Util.Chooser = function Chooser()
{
    this.sets = {
        all: []
    };
    
    this.items = {};
    this.aliases = {};
    
    var bundled_added = false;
    
    function dealias(aliases, name) {
        while (name in aliases)
            name = aliases[name];
        return name;
    }
    
    /**
     * Retrieves the items requested by the given selector.
     * @param {String} selector selector string
     * @param {Boolean} [lenient=false] if true, will not throw an error on
     * unknown items
     * @return {Object[]} array of chosen items
     * @throws {Error} unless lenient is set to true, throws an error when a
     * selector is provided that does not correspond with an item or a set
     */
    this.get = function get_from_chooser(selector, lenient)
    {
        var working = {};
        var self = this;
        
        if (!bundled_added && Util.is_function(this._add_bundled)) {
            bundled_added = true;
            this._add_bundled();
        }
        
        var operations = {
            '+': function(name) {
                if (name in self.sets) {
                    self.sets[name].each(function (name) {
                        name = dealias(self.aliases, name);
                        if (name in self.sets)
                            Util.OOP.mixin(working, self.get(name));
                        else
                            working[name] = self.items[name];
                    });
                } else if (name in self.items) {
                    working[name] = self.items[name];
                } else if (!lenient) {
                    throw new Error('Unknown item or set "' + name + '".');
                }
            },
            
            '-': function(name) {
                if (name in self.sets) {
                    self.sets[name].each(function (name) {
                        var k;
                        if (name in self.sets) {
                            for (k in self.get(name)) {
                                delete working[dealias(self.aliases, k)];
                            }
                        } else {
                            delete working[dealias(name)];
                        }
                    });
                } else if (name in self.items) {
                    delete working[name];
                } else if (!lenient) {
                    throw new Error('Unknown item or set "' + name + '".');
                }
            }
        };
        
        var operation = operations['+'];
        var part_pattern = /([+-])?s*(w+)/;
        
        (selector || 'default').match(/([+-])?s*(w+)/g).each(function(part) {
            var breakdown = part.match(part_pattern);
            if (!breakdown) {
                throw new Error('Invalid selector component "' + part + '".');
            }
            
            if (breakdown[1]) {
                operation = operations[breakdown[1]];
                if (!operation) {
                    throw new Error('Invalid operator "' + breakdown[1] + '".');
                }
            }
            
            operation(dealias(this.aliases, breakdown[2]));
        }, this);
        
        return working;
    }
    
    /**
     * Registers an item.
     * @param {string} the selectable name under which the item will be
     *   available
     * @param {mixed} the item being registered
     * @return the registered item
     * @type mixed
     */
    this.add = function add_item_to_chooser(name, item)
    {
        if (name in this.items) {
            if (this.items[name] == item)
                return item;
            throw new Error('An item with the name "' + name + '" is ' +
                'already registered.');
        } else if (name in this.sets) {
            throw new Error('A set is registered under the name "' + name +
                '".');
        }
        
        this.items[name] = item;
        this.sets.all.push(name);
        
        return item;
    }
    
    /**
     * Creates an alias.
     * @param {String} actual
     * @param {String} alias
     * @return {void}
     */
    this.alias = function create_alias(actual, alias) {
        this.aliases[alias] = actual;
    }
    
    /**
     * Adds a new set, or adds new members to an existing set.
     * @param {string} the set's name
     * @param {array} the set's members
     * @type void
     */
    this.put_set = function put_set_into_chooser(name, members)
    {
        if (name in this.items) {
            throw new Error('An item is registered under the name "' +
                name + '"; cannot create a set with the same name.');
        }
        
        if (!this.sets[name])
            this.sets[name] = members.slice(0); // make a copy
        else
            this.sets[name].append(members);
    }
}

// file Util.Cookie.js
/**
 * @class Contains helper functions related to cookies.
 * @author Eric Naeseth
 */
Util.Cookie = {
    /**
     * Gets either all available cookies or the value of a specific cookie.
     * @param {string} [name] if only one cookie's value is desired, its name
     *                        may be provided here
     * @return {mixed} either an object whose keys are cookie names and values
     *                 are the corresponding cookie values, or a string
     *                 corresponding to the value of the cookie
     */
    get: function get_cookies(name)
    {
        var cookies = document.cookie.split(';');
        var cookie_pattern = /(S+)=(.+)$/;
        var data = {};
        
        for (var i = 0; i < cookies.length; i++) {
            var match = cookie_pattern.exec(cookies[i]);
            if (!match || !match[1] || !match[2])    
                continue;
            
            if (name && match[1] == name)
                return match[2];
            else if (!name)
                data[match[1]] = match[2];
        }
        
        if (!name)
            return data;
    },
    
    /**
     * Sets a cookie.
     * @param {string} name   the name of the cookie
     * @param {string} value  the cookie's value
     * @param {number} [days] the number of days for which the cookie should
     *                        remain valid; if unspecified, the cookie remains
     *                        valid only for the active browser session
     * @return {void}
     */
    set: function set_cookie(name, value, days)
    {
        var expires = '';
        
        if (days) {
            var date = new Date();
            date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
            
            expires = '; expires=' + date.toGMTString();
        }
        
        document.cookie = name + '=' + value + expires + '; path=/';
    },
    
    /**
     * Deletes a cookie.
     * @param {string} name   the name of the cookie to delete
     * @return {void}
     */
    erase: function erase_cookie(name)
    {
        this.set(name, '', -1);
    }
}; 
// file Util.Document.js
/**
 * Wraps a DOM Document object to provide convenient functions.
 *
 * @class Container for functions relating to nodes.
 */
Util.Document = function(doc)
{
    for (var n in Util.Document) {
        if (n.charAt(0) == '_')
            continue;

        var a = Util.Document[n];
        if (typeof(a) != 'function')
            continue;

        this[n] = a.dynamic_curry(doc);
    }
};

/**
 * Creates an element in the document, optionally setting some attributes on it
 * and adding children.
 * @param    doc            document on which to create the element
 * @param    name        name of the tag to create
 * @param    attrs        any attributes to set on the new element
 * @param    children    any child nodes to add
 */
Util.Document.create_element = function(doc, name, attrs, children)
{
    // Internet Explorer cannot really set the name attribute on
    // an element. It can, however, be set on an element at the time
    // it is created using a proprietary IE syntax, for example:
    //     document.createElement('<INPUT name="foo">')
    // See http://tinyurl.com/8qsj2 for more information.
    function create_normal()
    {
        return doc.createElement(name.toUpperCase());
    }
    
    function create_ie()
    {
        try {
            return doc.createElement('<' + name.toUpperCase() +
                ' name="' + attrs.name + '">');
        } catch (e) {
            return create_normal();
        }
    }
    
    var e = (attrs && attrs.name && Util.Browser.IE)
        ? create_ie()
        : create_normal();
    
    function collapse(i, dom_text)
    {
        switch (typeof(i)) {
            case 'function':
                return collapse(i(), dom_text);
            case 'string':
                return (dom_text) ? doc.createTextNode(i) : i;
            default:
                return i;
        }
    }
    
    function dim(dimension)
    {
        return (typeof(dimension) == 'number') ? dimension + 'px' : dimension;
    }
    
    var style = {};
    
    for (var name in attrs || {}) {
        var dest_name = name;
        
        switch (name) {
            case 'className':
            case 'class':
                // In IE, e.setAttribute('class', x) does not work properly:
                // it will indeed set an attribute named "class" to x, but
                // the CSS for that class won't actually take effect. As a
                // workaround, we just set className directly, which works in
                // all browsers.
                
                // See http://tinyurl.com/yvsqbx for more information.
                
                var klass = attrs[name];
                
                // Allow an array of classes to be passed in.
                if (typeof(klass) != 'string' && klass.join)
                    klass = klass.join(' ');
                    
                e.className = klass;
                continue; // note that this continues the for loop!
            case 'htmlFor':
                dest_name = 'for';
                break;
            case 'style':
                if (typeof(style) == 'object') {
                    style = attrs.style;
                    continue; // note that this continues the for loop!
                }
        }
        
        var a = attrs[name];
        if (typeof(a) == 'boolean') {
            if (a)
                e.setAttribute(dest_name, dest_name);
            else
                continue;
        } else {
            e.setAttribute(dest_name, collapse(a, false));
        }
    }
    
    for (var name in style) {
        // Special cases
        switch (name) {
            case 'box':
                var box = style[name];
                e.style.left = dim(box[0]);
                e.style.top = dim(box[1]);
                e.style.width = dim(box[2]);
                e.style.height = dim(box[3] || box[2]);
                break;
            case 'left':
            case 'top':
            case 'right':
            case 'bottom':
            case 'width':
            case 'height':
                e.style[name] = dim(style[name]);
                break;
            default:
                e.style[name] = style[name];
        }
    }
    
    Util.Array.for_each(children || [], function(c) {
        e.appendChild(collapse(c, true));
    });
    
    return e;
}

/**
 * Make the document editable. Mozilla doesn't support
 * contentEditable. Both IE and Mozilla support
 * designMode. However, in IE if designMode is set on an iframe's
 * contentDocument, the iframe's ownerDocument will be denied
 * permission to access it (even if otherwise it *would* have
 * permission). So for IE we use contentEditable, and for Mozilla
 * designMode.
 * @param {HTMLDocument}    doc
 * @type void
 */
Util.Document.make_editable = function make_editable(doc)
{
    try {
        // Internet Explorer
        doc.body.contentEditable = true;
        // If the document isn't editable, this will throw an
        // error. If the document is editable, this is perfectly
        // harmless.
        doc.queryCommandState('Bold');
    } catch (e) {
        // Gecko (et al?)
        try {
            // Turn on design mode.  N.B.: designMode has to be
            // set after the iframe_elem's src is set (or its
            // document is closed). ... Otherwise the designMode
            // attribute will be reset to "off", and things like
            // execCommand won't work (though, due to Mozilla bug
            // #198155, the iframe's new document will be
            // editable)
            doc.designMode = 'on';
            doc.execCommand('undo', false, null);
            
            try {
                doc.execCommand('useCSS', false, true);
            } catch (no_use_css) {}
        } catch (f) {
            throw new Error('Unable to make the document editable. ' +
                '(' + e + '); (' + f + ')');
        }
    }
}

/**
 * Creates a new range on the document.
 * @param {Document}  doc   document on which the range will be created
 * @return {Range} the new range
 */
Util.Document.create_range = function create_range_on_document(doc)
{
    if (doc.createRange) {
        return doc.createRange();
    } else if (doc.body.createTextRange) {
        return doc.body.createTextRange();
    } else {
        throw new Util.Unsupported_Error('creating a range on a document');
    }
}

/**
 * Gets the HEAD element of a document.
 * @param    doc        document from which to obtain the HEAD
 */
Util.Document.get_head = function get_document_head(doc)
{
    try {
        return doc.getElementsByTagName('HEAD')[0];
    } catch (e) {
        return null;
    }
}

/**
 * Imitates W3CDOM Document.importNode, which IE doesn't
 * implement. See spec for more details.
 *
 * @param    new_document    the document to import the node to
 * @param    node            the node to import
 * @param    deep            boolean indicating whether to import child
 *                            nodes
 */
Util.Document.import_node = function import_node(new_document, node, deep)
{
    if (new_document.importNode) {
        return new_document.importNode(node, deep);
    } else {
        var handlers = {
            // element nodes
            1: function import_element() {
                var new_node = new_document.createElement(node.nodeName);
                
                if (node.attributes && node.attributes.length > 0) {
                    for (var i = 0, len = node.attributes.length; i < len; i++) {
                        var a = node.attributes[i];
                        if (a.specified)
                            new_node.setAttribute(a.name, a.value);
                    }
                }
                
                if (deep) {
                    for (var i = 0, len = node.childNodes.length; i < len; i++) {
                        new_node.appendChild(Util.Document.import_node(new_document, node.childNodes[i], true));
                    }
                }
                
                return new_node;
            },
            
            // attribute nodes
            2: function import_attribute() {
                var new_node = new_document.createAttribute(node.name);
                new_node.value = node.value;
                return new_node;
            },
            
            // text nodes
            3: function import_text() {
                return new_document.createTextNode(node.nodeValue);
            }
        };
        
        if (typeof(handlers[node.nodeType]) == 'undefined')
            throw new Error("Workaround cannot handle the given node's type.");
        
        return handlers[node.nodeType]();
    }
};

/**
 * Append the style sheet at the given location to the head of the
 * given document
 *
 * @param    location    the location of the stylesheet to add
 * @static
 */
Util.Document.append_style_sheet = function append_style_sheet(doc, location)
{
    var head = Util.Document.get_head(doc);
    return head.appendChild(Util.Document.create_element(doc, 'LINK',
        {href: location, rel: 'stylesheet', type: 'text/css'}));
};

/**
 * Gets position/dimensions information of a document.
 * @return {object} an object describing the document's dimensions
 */
Util.Document.get_dimensions = function get_document_dimensions(doc)
{
    return {
        client: {
            width: doc.documentElement.clientWidth || doc.body.clientWidth,
            height: doc.documentElement.clientHeight || doc.body.clientHeight
        },
        
        offset: {
            width: doc.documentElement.offsetWidth || doc.body.offsetWidth,
            height: doc.documentElement.offsetHeight || doc.body.offsetHeight
        },
        
        scroll: {
            width: doc.documentElement.scrollWidth || doc.body.scrollWidth,
            height: doc.documentElement.scrollHeight || doc.body.scrollHeight,
            left: doc.documentElement.scrollLeft || doc.body.scrollLeft,
            top: doc.documentElement.scrollTop || doc.body.scrollTop
        }
    };
}

/**
 * Returns an array (not a DOM NodeList!) of elements that match the given
 * namespace URI and local name.
 *
 * XXX Doesn't work
 */
Util.Document.get_elements_by_tag_name_ns = function(doc, ns_uri, tagname)
{
    var elems = new Array();
    try // W3C
    {
        var all = doc.getElementsByTagNameNS(ns_uri, tagname);
        messagebox('doc' ,doc);
        messagebox('all', all);
        for ( var i = 0; i < all.length; i++ )
            elems.push(all[i]);
    }
    catch(e)
    {
        try // IE
        {
            var all = doc.getElementsByTagName(tagname);
            for ( var i = 0; i < all.length; i++ )
            {
                if ( all[i].tagUrn == ns_uri )
                    elems.push(all[i]);
            }
        }
        catch(f)
        {
            throw('Neither the W3C nor the IE way of getting the element by namespace worked. When the W3C way was tried, an error with the following message was thrown: ' + e.message + '. When the IE way was tried, an error with the following message was thrown: ' + f.message + '.');
        }
    }
    return elems;
};

// file Util.Fieldset.js
/**
 * Creates a chunk containing a fieldset.
 * @constructor
 *
 * @param    params    an object with the following properties:
 *                  <ul>
 *                  <li>document - the DOM document object which will own the created DOM elements</li>
 *                  <li>legend - the desired legend text of the radio</li>
 *                  <li>id - (optional) the id of the DOM fieldset element</li>
 *                  </ul>
 *
 * @class Represents a radio button. Once instantiated, a Radio object
 * has the following properties:
 * <ul>
 * <li>all of the properties given to the constructor in <code>params</code></li>
 * <li>fieldset_elem - the DOM fieldset element. Use this when you want to get at the fieldset element qua fieldset element.</li>
 * <li>legend_elem - the DOM legend element</li>
 * <li>chunk - another reference to the DOM fieldset element. Use this when you want to get at the fieldset element qua chunk, e.g. to append the whole fieldset chunk.</li>
 * </ul>
 */
Util.Fieldset = function(params)
{
    this.document = params.document;
    this.legend = params.legend;
    this.id = params.id;

    // Create fieldset element
    this.fieldset_elem = this.document.createElement('DIV');
    Util.Element.add_class(this.fieldset_elem, 'fieldset');
    if ( this.id != null )
        this.fieldset_elem.setAttribute('id', this.id);

    // Create legend elem
    this.legend_elem = this.document.createElement('DIV');
    Util.Element.add_class(this.legend_elem, 'legend');
    this.legend_elem.appendChild( this.document.createTextNode( this.legend ) );

    // Append legend to fieldset
    this.fieldset_elem.appendChild(this.legend_elem);

    // Create "chunk"
    this.chunk = this.fieldset_elem;


    // Methods

    /**
     * Sets this fieldset's legend.
     *
     * @param    value    the new value
     */
    this.set_legend = function(value)
    {
        Util.Node.remove_child_nodes( this.legend_elem );
        this.legend_elem.appendChild( this.document.createTextNode(value) );
    };
};

// file Util.Fix_Keys.js
Util.Fix_Keys = function()
{
};

Util.Fix_Keys.NO_MERGE = /^(BODY|HEAD|TABLE|TBODY|THEAD|TR|TH|TD)$/;
Util.Fix_Keys.fix_delete_and_backspace = function(e, win)
{
    function is_not_at_end_of_body(rng)
    {
        var start_container = rng.startContainer;
        var start_offset = rng.startOffset;
        var rng2 = Util.Range.create_range(sel);
        rng2.selectNodeContents(start_container.ownerDocument.getElementsByTagName('BODY')[0]);
        rng2.setStart(start_container, start_offset);
        var ret = rng2.toString().length > 0;// != '';
        return ret;
    }

    function is_not_at_beg_of_body(rng)
    {
        var start_container = rng.startContainer;
        var start_offset = rng.startOffset;
        var rng2 = Util.Range.create_range(sel);
        rng2.selectNodeContents(start_container.ownerDocument.getElementsByTagName('BODY')[0]);
        rng2.setEnd(start_container, start_offset);
        var ret = rng2.toString().length > 0;// != '';
        return ret;
    }

    function move_selection_to_end(node, sel)
    {
        var rightmost = Util.Node.get_rightmost_descendent(node);
        Util.Selection.select_node(sel, rightmost);
        Util.Selection.collapse(sel, false); // to end
    }

    function remove_trailing_br(node)
    {
        if ( node.lastChild != null && 
             node.lastChild.nodeType == Util.Node.ELEMENT_NODE && 
             node.lastChild.tagName == 'BR' )
        {
            node.removeChild(node.lastChild);
        }
    }
    
    
    function merge_blocks(one, two)
    {
        while (two.firstChild)
            one.appendChild(two.firstChild);
        two.parentNode.removeChild(two);
    }
    
    /*
     * If the node is a special Loki container (e.g. for a horizontal rule),
     * we shouldn't merge with it.
     */
    function is_container(node)
    {
        return (node && node.nodeType == Util.Node.ELEMENT_NODE &&
            node.getAttribute('loki:container'));
    }
    
    function is_empty_block(node)
    {
        return (Util.Node.is_block_level_element(node) && 
            Util.Node.is_basically_empty(node));
    }
    
    function is_unmergable(node)
    {
        return (is_container(node) ||
            is_empty_block(node) || 
            Util.Element.empty_tag(node));
    }

    function do_merge(one, two, sel)
    {
        function handle_unmergable(node)
        {
            if (is_unmergable(node)) {
                node.parentNode.removeChild(node);
                return true;
            }
            
            return false;
        }
        
        var tags = Util.Fix_Keys.NO_MERGE;
        if (!one || !two || one.nodeName.match(tags) || two.nodeName.match(tags)) {
            return;
        } else if (handle_unmergable(one) || handle_unmergable(two)) {
            return;
        } else {
            remove_trailing_br(one);
            move_selection_to_end(one, sel);
            merge_blocks(one, two);
            e.preventDefault();
        }
    }
    
    function remove_container(container)
    {
        container.parentNode.removeChild(container);
        e.preventDefault();
    }
    
    function remove_if_container(node)
    {
        if (is_container(node))
            remove_container(node);
    }

    var sel = Util.Selection.get_selection(win);
    var rng = Util.Range.create_range(sel);
    var cur_block;
    try {
        cur_block = Util.Range.get_nearest_bl_ancestor_element(rng);
    } catch (e) {
        cur_nlock = null;
    }
    
    function get_neighbor_element(direction)
    {
        if (rng.startContainer != rng.endContainer || rng.startOffset != rng.endOffset)
            return null;
        
        if (rng.startContainer.nodeType == Util.Node.TEXT_NODE) {
            if (direction == Util.Node.NEXT) {
                if (rng.endOffset == rng.endContainer.nodeValue.length)
                    return rng.endContainer.nextSibling;
            } else if (direction == Util.Node.PREVIOUS) {
                if (rng.startOffset == 0)
                    return rng.startContainer.previousSibling;
            }
            
            // If we're in the middle of a text node; well, how did we reach
            // this code?
            return null;
        }
        
        if (direction == Util.Node.NEXT && rng.endContainer.childNodes[rng.endOffset])
            return rng.endContainer.childNodes[rng.endOffset];
        else if (direction == Util.Node.PREVIOUS && rng.startContainer.childNodes[rng.startOffset - 1])
            return rng.startContainer.childNodes[rng.startOffset - 1];
        else
            return null;
    }
    
    function is_named_anchor(element) {
        return (element && element.tagName == 'A' && element.name &&
            !Util.Node.get_last_non_whitespace_child_node(element));
    }
    
    function remove_anchor(anchor) {
        var id = anchor.id, sibling = anchor.previousSibling, i, images;
        
        function is_marker(node) {
            return (node.nodeName == 'IMG' &&
                node.getAttribute('loki:anchor.id') == id);
        }
        
        if (is_marker(sibling)) {
            // easy case: marker is in its original position, we avoid a DOM
            // search
            sibling.parentNode.removeChild(sibling);
        } else {
            images = anchor.ownerDocument.getElementsByTagName('IMG');
            for (i = 0; i < images.length; i++) {
                if (is_marker(images[i])) {
                    images[i].parentNode.removeChild(images[i]);
                    break;
                }
            }
        }
        
        anchor.parentNode.removeChild(anchor);
    }

    if ( rng.collapsed == true && !e.shiftKey )
    {
        var neighbor = null;
        
        if (e.keyCode == e.DOM_VK_DELETE) {
            neighbor = get_neighbor_element(Util.Node.NEXT);
            if (is_named_anchor(neighbor)) {
                remove_anchor(neighbor);
            } else if (cur_block && Util.Range.is_at_end_of_block(rng, cur_block)) {
                do_merge(cur_block, Util.Node.next_element_sibling(cur_block), sel);
            } else if (Util.Range.is_at_end_of_text(rng) && is_container(rng.endContainer.nextSibling)) {
                remove_container(rng.endContainer.nextSibling);
            } else if (neighbor) {
                remove_if_container(neighbor);
            }
        } else if (e.keyCode == e.DOM_VK_BACK_SPACE) {
            neighbor = get_neighbor_element(Util.Node.PREVIOUS);
            
            if (is_named_anchor(neighbor)) {
                remove_anchor(neighbor);
            } else if (cur_block && Util.Range.is_at_beg_of_block(rng, cur_block) && rng.isPointInRange(rng.startContainer, 0)) {
                // Both the above range tests are necessary to avoid
                // merge on B's here: <p>s<b>|a</b>h</p>
                do_merge(Util.Node.previous_element_sibling(cur_block), cur_block, sel);
            } else if (Util.Range.is_at_beg_of_text(rng) && is_container(rng.startContainer.previousSibling)) {
                remove_container(rng.endContainer.nextSibling);
            } else if (neighbor) {
                remove_if_container(neighbor);
            }
        }
    }

    return;
    //mb('rng.startContainer, rng.startContainer.parentNode.lastChild, rng.startContainer.parentNode.firstChild, rng.startOffset, rng.startContainer.length, sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset, rng, sel', [rng.startContainer, rng.startContainer.parentNode.lastChild, rng.startContainer.parentNode.firstChild, rng.startOffset, rng.startContainer.length, sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset, rng, sel]);
};

Util.Fix_Keys.fix_enter_ie = function(e, win, loki)
{
    // Do nothing if enter not pressed
    if (!( !e.shiftKey && e.keyCode == 13 ))
        return true;

    var sel = Util.Selection.get_selection(win);
    var rng = Util.Range.create_range(sel);
    var cur_block = Util.Range.get_nearest_bl_ancestor_element(rng);

    if ( cur_block && cur_block.nodeName == 'PRE' )
    {
        var br_helper = (new UI.BR_Helper).init(loki);
        br_helper.insert_br();
        return false; // prevent default
    }

    // else
    return true; // don't prevent default
};

// file Util.Form.js
/**
 * @constructor
 *
 * @class Form generation without fuss and with validation.
 * @author Eric Naeseth
 */
Util.Form = function(document, params)
{
    var dh = new Util.Document(document); // document helper
    
    this.document = document;
    this._dh = dh;
    this.name = params.name || '(untitled form)';
    this.form_element = params.form || dh.create_element('form',
        {method: params.method || 'POST',
        action: params.action || 'about:blank',
        className: 'generated'});
    this.section_heading_level = params.section_heading_level || 'H3';
    this.live_validation = true;
    
    this.sections = [];
    this.active_section = null;
    
    this.toString = function()
    {
        return '[object Util.Form name=' + this.name +
            ', form_element=' + this.form_element + ']';
    }
    
    /**
     * Constructs and returns a new form section.
     * Form elements cannot be added directly to the form, but must be added
     * to sections. The name parameter is optional, so to simulate a form with
     * no sectional organization, create one single nameless section and add
     * the fields to it.
     */
    this.add_section = function(name)
    {
        if (arguments.length == 0)
            var name = null;
        
        var s = new Util.Form.FormSection(this, name);
        this.sections.push(s);
        this.active_section = s;
        s.append(document, dh);
        
        return s;
    }
}

/**
 * @constructor
 * @class Base class for form sections and compound form fields.
 */
Util.Form.FormElementContainer = function(form)
{
    this.new_container = Util.Function.unimplemented;
    this.fields = [];
    
    this.add_field = function(field)
    {
        var container = this.new_container(form, form.document, form._dh);
        field.append(form, form.document, form._dh, container);
        this.fields.push(field);
        return field;
    }
    
    // convenience methods
    
    this.add_text_field = function(name, params)
    {
        if (!params) var params = {};
        
        return this.add_field(new Util.Form.TextField(name,
            params.exposition || null, params));
    }
    
    this.add_blurb_field = function(name, params)
    {
        if (!params) var params = {};
        
        return this.add_field(new Util.Form.BlurbField(name,
            params.exposition || null, params));
    }
    
    this.add_select_field = function(name, values, params)
    {
        if (!params) var params = {};
        
        return this.add_field(new Util.Form.SelectField(name,
            params.exposition || null, params, values));
    }
    
    this.add_instructions = function(text)
    {
        if (!params) var params = {};
    
        return this.add_field(new Util.Form.Instructions(text));
    }
}

/**
 * @constructor
 * @class A section of a form.
 */
Util.Form.FormSection = function(form, name)
{
    Util.OOP.inherits(this, Util.Form.FormElementContainer, form);
    
    this.name = (arguments.length < 2)
        ? null
        : name;
    var list = null;
    
    this.append = function(doc, dh)
    {
        var fe = form.form_element;
        
        if (this.name) {
            fe.appendChild(dh.create_element(form.section_heading_level,
                {className: 'section_heading'}, [this.name]));
        }
        
        list = dh.create_element('ul', {className: 'form_section'});
        fe.appendChild(list);
    }
    
    this.new_container = function(form, doc, dh)
    {
        var litem = dh.create_element('li');
        list.appendChild(litem);
        return litem;
    }
    
    this.add_compound_field = function()
    {
        return this.add_field(new Util.Form.CompoundField(form));
    }
}

/**
 * @constructor
 * @class A field on a form.
 */
Util.Form.FormField = function(name, exposition, validator)
{
    this.name = name || null;
    this.exposition = exposition || null;
    this.validate = validator || Util.Function.empty;
    this.element = null;
    
    this.append = function(form, doc, dh, target)
    {
        if (this.name) {
            target.appendChild(dh.create_element('label',
                {className: 'description'}, [this.name]));
        }
        
        if (this.exposition) {
            target.appendChild(dh.create_element('p',
                {className: 'exposition'}, [this.exposition]));
        }
        
        this.element = this.create_element(doc, dh);
        target.appendChild(this.element);
    }
    
    this.get_field_name = function() {
        if (arguments.length > 0) {
            var name = arguments[0];
            if (typeof(name) == 'object' && typeof(name.name) == 'string')
                return name.name;
        }
        
        if (typeof(this.name) != 'string') {
            throw new Error('No pretty name for this field is defined.');
        }
        
        return this.name.replace(/W+/, '_').toLowerCase();
    }
    
    this._apply_validation = function(element) {
        var field = this;
        Util.Event.add_event_listener(element, 'change', function(e) {
            field.validate.call(this, e || window.event);
        })
        return element;
    }
    
    this.create_element = Util.Function.unimplemented;
}

Util.Form.TextField = function(name, exposition, params)
{
    Util.OOP.inherits(this, Util.Form.FormField, name, exposition, params.validator);
    
    this.create_element = function(doc, dh)
    {
        return this._apply_validation(dh.create_element('input', {
            type: 'text',
            name: this.get_field_name(params || {}),
            value: params.value || '',
            size: params.size || 20
        }));
    }
}

Util.Form.BlurbField = function(name, exposition, params)
{
    Util.OOP.inherits(this, Util.Form.FormField, name, exposition, params.validator);
    
    this.create_element = function(doc, dh)
    {
        return this._apply_validation(dh.create_element('textarea', {
            name: this.get_field_name(params || {}),
            cols: params.cols || 60,
            rows: params.rows || 5},
            [params.value || '']
        ));
    }
}

Util.Form.SelectField = function(name, exposition, params, values)
{
    Util.OOP.inherits(this, Util.Form.FormField, name, exposition, params.validator);
    
    this.create_element = function(doc, dh)
    {
        var options = [];
        for (var i = 0; i < values.length; i++) {
            var v = values[i];
            var option = dh.create_element('option',
                {value: v.value, selected: (v.selected || false)});
            option.innerHTML = v.text;
            options.push(option);
        }
        
        return this._apply_validation(dh.create_element('select', 
            {name: this.get_field_name(params || {}),
            size: params.size || 1},
            options
        ));
    }
}

Util.Form.CompoundField = function(form)
{
    Util.OOP.inherits(this, Util.Form.FormElementContainer, form);
    
    var container = null;
    var line_break = null;
    
    this.append = function(form, doc, dh, target)
    {
        container = target;
        line_break = dh.create_element('br', {className: 'compound_end'});
        container.appendChild(line_break);
    }
    
    this.new_container = function(form, doc, dh)
    {
        var item = dh.create_element('span');
        container.insertBefore(item, line_break);
        return item;
    }
    
    this.validate = function()
    {
        for (var i = 0; i < this.fields.length; i++) {
            this.fields[i].validate();
        }
    }
}

Util.Form.Instructions = function(text)
{
    Util.OOP.inherits(this, Util.Form.FormField);
    
    this.create_element = function(doc, dh)
    {
        return dh.create_element('p', {className: 'instructions'},
            [text]);
    }

// file Util.HTML_Generator.js
/**
 * Constructs a new HTML generator.
 * @class Generates nicely-formatted HTML by traversing the DOM.
 * @param {Object} [options] generation options
 * @param {Boolean} [options.xhtml=true] generate XHTML output
 * @param {Boolean} [options.escape_non_ascii=true]
 * @param {Boolean} [options.indent_text="t"]
 */
Util.HTML_Generator = function HTMLGenerator(options) {
    if (!options)
        options = {};
    this.xhtml = options.xhtml || true;
    this.escape_non_ascii = options.escape_non_ascii || true;
    this.indent_text = options.indent_text || "t";
};

/**
 * Generates HTML.
 * @param {Node|Node[]} nodes
 * @return {String} the formatted source
 */
Util.HTML_Generator.prototype.generate = function generate_html(nodes) {
    var gen = this;
    var pattern = (gen.escape_non_ascii)
        ? (/[x00-x1Fx80-uFFFF&<>"]/g)
        : (/[x00-x1F&<>"]/g);
    
    function is_relevant(node) {
        if (!node)
            return false;
        return (node.nodeType == Util.Node.ELEMENT_NODE || 
            node.nodeType == Util.Node.TEXT_NODE &&
            /S/.test(node.nodeValue));
    }
    
    function clean_text(text, in_attribute) {
        function html_escape(txt) {
            var c = txt.charCodeAt(0);
            if (c == 9 || c == 10 || c == 13)
                return txt;
            if (c == 34 && !in_attribute) // don't do " -> &quot; unless in attr
                return txt;
            var entity = Util.HTML_Generator.named_entities[c];
            return (typeof(entity) == "string")
                ? '&' + entity + ';'
                : '&#' + c + ';'
        }
        
        return (text) ? text.replace(pattern, html_escape) : '';
    }
    
    function is_whitespace_irrelevant(node) {
        var parent = node.parentNode;
        var parent_is_block = Util.Block.is_block(parent);
        var results = [false, false];
        
        if (parent_is_block) {
            if (node == node.parentNode.firstChild)
                results[0] = true;
            if (node == node.parentNode.lastChild)
                results[1] = true;
            
            if (results[0] && results[1]) {
                return results;
            }
        }
        
        if (node.previousSibling && Util.Block.is_block(node.previousSibling))
            results[0] = true;
        if (node.nextSibling && Util.Block.is_block(node.nextSibling))
            results[1] = true;
        
        return results;
    }
    
    function make_text(buffer, text_node) {
        if (!Util.Node.is_text(text_node))
            throw new TypeError();
        
        var text = text_node.nodeValue, orig_text = text, irw;
        
        if (!buffer.flagged("preformatted")) {
            if (text_node == text_node.parentNode.firstChild)
                text = text.replace(/^[trn]+/g, '');
            if (text_node == text_node.parentNode.lastChild)
                text = text.replace(/[trn]+$/g, '');
            text = text.replace(/(S)[rn]+(S)/g, "$1 $2");
            text = text.replace(/(s)[rn]+|[rn]+(s)/g, "$1$2");
            text = text.replace(/[ ][ ]+/g, ' ');
            
            irw = is_whitespace_irrelevant(text_node);
            if (irw[0])
                text = text.replace(/^[sn]+/, '');
            if (irw[1])
                text = text.replace(/[sn]+$/, '');
        }
        
        text = clean_text(text);
        if (text.length > 0)
            buffer.write(text);
    }
    
    function make_comment(buffer, comment_node) {
        if (comment_node.nodeType != Util.Node.COMMENT_NODE)
            throw new TypeError();
        
        buffer.write('<!--' + clean_text(comment_node.nodeValue) + '-->');
    }
    
    function make_processing_instruction(buffer, pi_node) {
        if (pi_node.nodeType != Util.Node.PROCESSING_INSTRUCTION_NODE)
            throw new TypeError();
        
        buffer.write('<?' + pi_node.target + ' ' + pi_node.data + '?>');
    }
    
    function make_open_tag(buffer, element, xml_self_close) {
        if (!Util.Node.is_element(element))
            throw new TypeError();
            
        buffer.write('<', element.nodeName.toLowerCase());
        
        Util.Object.enumerate(Util.Element.get_attributes(element, true),
            function append_attr(name, value) {
                if (name.charAt(0) == "_")
                    return;
                buffer.write(' ', name, '="', clean_text(value, true), '"');
            }
        );
        
        buffer.write((xml_self_close) ? ' />' : '>');
    }
    
    function make_close_tag(buffer, element) {
        if (!Util.Node.is_element(element))
            throw new TypeError();
            
        buffer.write('</' + element.nodeName.toLowerCase() + '>');
    }
    
    function make_empty_element(buffer, element) {
        if (!Util.Node.is_element(element))
            throw new TypeError();
        
        make_open_tag(buffer, element, gen.xhtml);
        if (element.nodeName == "PARAM")
            buffer.end_line();
    }
    
    function make_inline_element(buffer, element) {
        if (!Util.Node.is_element(element))
            throw new TypeError();
            
        make_open_tag(buffer, element);
        make_nodes(buffer, element.childNodes);
        make_close_tag(buffer, element);
    }
    
    function is_indented_block(element) {
        if (!Util.Block.is_block(element))
            return false;
        
        function is_block(node) {
            return Util.Block.is_block(node);
        }
        
        return (Util.Node.find_children(element, is_block).length > 0);
    }
    
    function make_block_element(buffer, element) {
        if (!Util.Node.is_element(element))
            throw new TypeError();
        
        if (!element.hasChildNodes() || buffer.flagged("preformatted")) {
            make_inline_element(buffer, element);
            return;
        }
        
        if (buffer.flagged('after_indented_block')) {
            buffer.end_line();
        }
        
        var block_children = is_indented_block(element);
        var child_buffer;
        
        buffer.end_line(true);
        make_open_tag(buffer, element);
        
        if (block_children) {
            child_buffer = buffer.spawn();
            make_nodes(child_buffer, element.childNodes);
            child_buffer.close();
            buffer.end_line(true);
        } else {
            make_nodes(buffer, element.childNodes);
        }
        
        make_close_tag(buffer, element);
        buffer.end_line();
        if (block_children)
            buffer.set_flag('after_indented_block', 'write');
    }
    
    function make_pre_element(buffer, element) {
        if (!Util.Node.is_element(element))
            throw new TypeError();
            
        buffer.set_flag('preformatted');
        make_inline_element(buffer, element);
        buffer.end_line(true);
        buffer.clear_flag('preformatted');
    }
    
    function make_element(buffer, element) {
        if (!Util.Node.is_element(element)) {
            throw new TypeError("Tried to make a non-element as an element: " +
                element);
        }
        
        if (is_relevant(element.previousSibling) && is_indented_block(element)) {
            if (!buffer.flagged('after_indented_block')) {
                buffer.end_line();
            }
        }
            
        if (Util.Node.is_tag(element, 'PRE'))
            return make_pre_element(buffer, element);
        else if (!element.hasChildNodes() && Util.Element.empty_tag(element))
            return make_empty_element(buffer, element);
        else if (Util.Block.is_block(element))
            return make_block_element(buffer, element);
        else
            return make_inline_element(buffer, element);
    }
    
    function make_node(buffer, node) {
        if (!Util.is_number(node.nodeType))
            throw new TypeError();
        
        switch (node.nodeType) {
            case Util.Node.TEXT_NODE:
                return make_text(buffer, node);
            case Util.Node.COMMENT_NODE:
                return make_comment(buffer, node);
            case Util.Node.PROCESSING_INSTRUCTION_NODE:
                return make_processing_instruction(buffer, node);
            case Util.Node.ELEMENT_NODE:
                return make_element(buffer, node);
            case Util.Node.DOCUMENT_NODE:
                return make_element(buffer, node.documentElement);
            default:
                return '';
        }
    }
    
    function make_nodes(buffer, nodes) {
        if (!Util.is_enumerable(nodes))
            throw new TypeError();
        
        for (var i = 0; i < nodes.length; i++) {
            make_node(buffer, nodes[i]);
        }
    }
    
    var buffer = new Util.HTML_Generator.Buffer(null, this.indent_text);
    if (!Util.is_enumerable(nodes))
        nodes = [nodes];
    make_nodes(buffer, nodes);
    return buffer.close().read();
};

Util.HTML_Generator.Buffer = function Buffer(parent, indent_text)
{
    this.parent = parent || null;
    this.depth = (parent) ? parent.depth + 1 : 0;
    this.lines = [];
    this.current_line = [];
    this.indent_text = indent_text || (parent && parent.indent_text) || "t";
    this.closed = false;
    this.active_child = null;
    this.flags = {
        'manual': {},
        'write': {},
        'flush': {}
    };
    
    if (parent)
        parent.active_child = this;
}

Util.OOP.mixin(Util.HTML_Generator.Buffer.prototype, {
    flags: null,
    
    _verify_open: function _verify_buffer_is_open() {
        if (this.closed) {
            throw new Error("Buffer is closed!");
        } else if (this.active_child) {
            throw new Error("A child buffer is active!");
        }
    },
    
    _gen_indent: function _buffer_generate_indentation() {
        var indent = new Array(this.depth);
        for (var i = 0; i < this.depth; i++)
            indent[i] = this.indent_text;
        return indent.join('');
    },
    
    spawn: function spawn_child_buffer() {
        this.flush();
        return new Util.HTML_Generator.Buffer(this);
    },
    
    set_flag: function set_buffer_flag(name, cancellation, value) {
        if (cancellation)
            cancellation = cancellation.toLowerCase();
        else
            cancellation = 'manual';
        
        if (typeof(name) != 'string') {
            throw new Error('Illegal buffer flag name "' + name + '".');
        } else if (!cancellation in this.flags) {
            throw new Error('Unknown flag cancellation "' + cancellation +
                '".');
        }
        
        this.clear_flag(name);
        this.flags[cancellation][name] = value || true;
        return this;
    },
    
    get_flag: function get_buffer_flag(name) {
        for (var c in this.flags) {
            var value = this.flags[c][name];
            if (typeof(value) != 'undefined')
                return value;
        }
        
        return undefined;
    },
    
    clear_flag: function clear_buffer_flag(name) {
        for (var c in this.flags) {
            delete this.flags[c][name];
        }
    },
    
    flagged: function is_buffer_flagged(name) {
        return typeof(this.get_flag(name)) != 'undefined';
    },
    
    write: function write_to_buffer(text) {
        var i, arg;
        
        this._verify_open();
        
        for (var flag_name in this.flags.write)
            delete this.flags.write[flag_name];
        
        for (i = 0; i < arguments.length; i++) {
            arg = String(arguments[i]);
            if (arg.length > 0)
                this.current_line.push(arg);
        }
        
        return this;
    },
    
    flush: function flush_buffer(always_flush) {
        var line;
        
        this._verify_open();
        
        for (var flag_name in this.flags.flush)
            delete this.flags.flush[flag_name];
        
        if (this.current_line.length == 0 && !always_flush) {
            return this;
        }
        
        line = this._gen_indent() + this.current_line.join('');
        this.lines.push(line);
        this.current_line = [];
        return this;
    },
    
    end_line: function buffer_end_line(only_if_content) {
        return this.flush(!only_if_content);
    },
    
    close: function close_buffer() {
        this.flush(); // calls _verify_open
        this.closed = true;
        if (this.parent) {
            if (this.parent.closed) // should never happen, but be safe
                throw new Error("Parent buffer is closed!");
            this.parent.lines.append(this.lines);
            this.parent.active_child = null;
        }
        return this;
    },
    
    read: function read_buffer() {
        if (!this.closed) {
            throw new Error("Cannot read buffer contents: buffer still open.");
        }
        return this.lines.join("n");
    }
});

Util.HTML_Generator.named_entities = {
    '34': 'quot', '38': 'amp', '60': 'lt', '62': 'gt', '127': '#127',
    '160': 'nbsp', '161': 'iexcl', '162': 'cent', '163': 'pound', '164':
    'curren', '165': 'yen', '166': 'brvbar', '167': 'sect', '168': 'uml', '169':
    'copy', '170': 'ordf', '171': 'laquo', '172': 'not', '173': 'shy', '174':
    'reg', '175': 'macr', '176': 'deg', '177': 'plusmn', '178': 'sup2', '179':
    'sup3', '180': 'acute', '181': 'micro', '182': 'para', '183': 'middot',
    '184': 'cedil', '185': 'sup1', '186': 'ordm', '187': 'raquo', '188':
    'frac14', '189': 'frac12', '190': 'frac34', '191': 'iquest', '192':
    'Agrave', '193': 'Aacute', '194': 'Acirc', '195': 'Atilde', '196': 'Auml',
    '197': 'Aring', '198': 'AElig', '199': 'Ccedil', '200': 'Egrave', '201':
    'Eacute', '202': 'Ecirc', '203': 'Euml', '204': 'Igrave', '205': 'Iacute',
    '206': 'Icirc', '207': 'Iuml', '208': 'ETH', '209': 'Ntilde', '210':
    'Ograve', '211': 'Oacute', '212': 'Ocirc', '213': 'Otilde', '214': 'Ouml',
    '215': 'times', '216': 'Oslash', '217': 'Ugrave', '218': 'Uacute', '219':
    'Ucirc', '220': 'Uuml', '221': 'Yacute', '222': 'THORN', '223': 'szlig',
    '224': 'agrave', '225': 'aacute', '226': 'acirc', '227': 'atilde', '228':
    'auml', '229': 'aring', '230': 'aelig', '231': 'ccedil', '232': 'egrave',
    '233': 'eacute', '234': 'ecirc', '235': 'euml', '236': 'igrave', '237':
    'iacute', '238': 'icirc', '239': 'iuml', '240': 'eth', '241': 'ntilde',
    '242': 'ograve', '243': 'oacute', '244': 'ocirc', '245': 'otilde', '246':
    'ouml', '247': 'divide', '248': 'oslash', '249': 'ugrave', '250': 'uacute',
    '251': 'ucirc', '252': 'uuml', '253': 'yacute', '254': 'thorn', '255':
    'yuml', '8364': 'euro'
};

// file Util.HTML_Parser.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A SAX-style tolerant HTML parser that doesn't rely on the browser.
 * @author Eric Naeseth
 */
Util.HTML_Parser = function SAX_HTML_Parser()
{
    var data = null;
    var parsing = false;
    var halted = false;
    var position = 0;
    var listeners = {
        open: [],
        close: [],
        text: [],
        cdata: [],
        comment: []
    };
    
    var self_closing_tags = Util.HTML_Parser.self_closing_tags.toSet();
    
    // -- Public Methods --
    
    this.add_listener = function add_html_parse_listener(type, func)
    {
        listeners[type.toLowerCase()].push(func);
    }
    
    // consistency
    this.add_event_listener = this.add_listener;
    
    this.parse = function parse_html(text)
    {
        data = text;
        position = 0;
        var state = starting_state;
        var len = data.length;
        
        parsing = true;
        halted = false;
        do {
            state = state();
        } while (state && position < len && !halted);
        parsing = halted = false;
    }
    
    this.halt = function halt_html_parser()
    {
        if (!parsing)
            return false;
        return (halted = true);
    }
    
    // -- Parsing Functions --
    
    function unscan_character()
    {
        position--;
    }
    
    function unscan_characters(number)
    {
        position -= number;
    }
    
    function ignore_character()
    {
        position++;
    }
    
    function ignore_characters(number)
    {
        position += number;
    }
    
    function scan_character()
    {
        return (position < data.length)
            ? data.charAt(position++)
            : null;
    }
    
    function expect(s)
    {
        var len = s.length;
        if (position + len < data.length && data.indexOf(s, position) == position) {
            position += len;
            return true;
        }
        
        return false;
    }
    
    function scan_until_string(s)
    {
        var start = position;
        position = data.indexOf(s, start);
        if (position < 0)
            position = data.length;
        return data.substring(start, position);
    }
    
    function scan_until_characters(list)
    {
        var start = position;
        while (position < data.length && list.indexOf(data.charAt(position)) < 0) {
            position++;
        }
        return data.substring(start, position);
    }
    
    function ignore_whitespace()
    {
        while (position < data.length && " nrt".indexOf(data.charAt(position)) >= 0) {
            position++;
        }
    }
    
    function character_data(data)
    {
        var cdata_listeners = (listeners.cdata.length > 0)
            ? listeners.cdata
            : listeners.text;
        
        cdata_listeners.each(function(l) {
            l(data);
        });
    }
    
    function text_data(data)
    {
        listeners.text.each(function(l) {
            l(data);
        });
    }
    
    function comment(contents)
    {
        listeners.comment.each(function(l) {
            l(data);
        });
    }
    
    function tag_opened(name, attributes)
    {
        listeners.open.each(function(l) {
            l(name, attributes);
        });
    }
    
    function tag_closed(name)
    {
        listeners.close.each(function(l) {
            l(name);
        });
    }
    
    // -- State Functions --
    
    function starting_state()
    {
        var cdata = scan_until_string('<');
        if (cdata) {
            text_data(cdata);
        }
        
        ignore_character();
        return tag_state;
    }
    
    function tag_state()
    {
        switch (scan_character()) {
            case '/':
                return closing_tag_state;
            case '?':
                return processing_instruction_state;
            case '!':
                return escape_state;
            default:
                unscan_character();
                return opening_tag_state;
        }
    }
    
    function opening_tag_state()
    {
        function parse_attributes()
        {
            var attrs = {};
            
            do {
                ignore_whitespace();
                var name = scan_until_characters("=/> nrt");
                if (!name)
                    break;
                var value = null;
                ignore_whitespace();
                var next_char = scan_character();
                if (next_char == '=') {
                    // value provided; figure out what (if any) quoting style
                    // is in use
                    
                    ignore_whitespace();
                    var quote = scan_character();
                    if (''"'.indexOf(quote) >= 0) {
                        // it's quoted; find the matching quote
                        value = scan_until_string(quote);
                        ignore_character(); // skip over the closer
                    } else {
                        // unquoted; find the end
                        unscan_character();
                        value = scan_until_characters("/> nrt");
                    }
                } else {
                    // value implied (e.g. in <option selected>)
                    unscan_character();
                    value = name;
                }
                
                attrs[name] = value;
            } while (true);
            
            return attrs;
        }
        
        var tag = scan_until_characters("/> nrt");
        if (tag) {
            var attributes = parse_attributes(); // last step ignores whitespace
            tag_opened(tag, attributes);
            
            var next_char = scan_character();
            if (next_char == '/') {
                // self-closing tag (XML-style)
                tag_closed(tag);
                ignore_whitespace();
                next_char = scan_character(); // advance to the "<"
            } else if (tag.toUpperCase() in self_closing_tags) {
                // self-closing tag (known HTML tag)
                tag_closed(tag);
            }
            
            if (next_char != '>') {
                // oh my, what on earth?
                throw new Util.HTML_Parser.Error('Opening tag not terminated ' +
                    'by ">".');
            }
        }
        
        return starting_state;
    }
    
    function closing_tag_state()
    {
        var tag = scan_until_characters('/>');
        if (tag) {
            var next_char = scan_character();
            if (next_char == '/') {
                next_char = scan_character();
                if (next_char != '>') {
                    // oh my, what on earth?
                    throw new Util.HTML_Parser.Error('Closing tag not ' +
                        'terminated by ">".');
                }
            }
            
            tag_closed(tag);
        }
        
        return starting_state;
    }
    
    function escape_state()
    {
        var data;
        
        if (expect('--')) {
            // comment
            data = scan_until_string('-->');
            if (data)
                comment(data);
            ignore_characters(2);
        } else if (expect('[CDATA[')) {
            // CDATA section
            data = scan_until_string(']]>');
            if (data)
                character_data(data);
            ignore_characters(2);
        } else {
            scan_until_string('>');
        }
        
        ignore_character();
        return starting_state;
    }
    
    function processing_instruction_state()
    {
        scan_until_string('?>');
        ignore_characters(2);
        
        return starting_state;
    }
}

/**
 * Constructs a new HTML parse error.
 * @class An HTML parse error.
 * @constructor
 * @extends Error
 */
Util.HTML_Parser.Error = function HTML_Parse_Error(message)
{
    Util.OOP.inherits(this, Error, message);
    this.name = 'HTML_Parse_Error';
}

Util.HTML_Parser.self_closing_tags = ['BR', 'AREA', 'LINK', 'IMG', 'PARAM',
    'HR', 'INPUT', 'COL', 'BASE', 'META'];

// file Util.HTML_Reader.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Reads an HTML file and exposes its document, without
 * displaying it.
 *
 * Structure:
 * - These may be called any time
 *   - add_load_listener
 *   - load
 *   - destroy
 * - These must not be accessed till after load
 *   - document
 *
 */
Util.HTML_Reader = function()
{
    /**
     * @param    document     (optional) the document. Defaults to global document.
     * @param     blank_uri    (optional) the uri to use as a blank uri, which is displayed 
     *                         for an instant before whatever uri is passed to this.load
     *                         is displayed. Defaults to about:blank.
     *                        But NOTE: if blank_uri is left as about:blank, when called
     *                         from a page under https IE will complain about mixing 
     *                        https and http.
     */
    this.init = function(params)
    {
        if (typeof(params) == 'undefined')
            var params = {};
        
        this._owner_document = params.document == null ? document : params.document;
        this._blank_uri = params.blank_uri == null ? 'about:blank' : params.blank_uri;
        this._load_listeners = new Array();

        return this;
    };

    this.add_load_listener = function(listener)
    {
        this._load_listeners.push(listener);
    };

    this.load = function(uri)
    {
        if ( this._iframe == null )
            this._append_iframe(uri);

        this._iframe.src = uri;
    };

    /**
     * If you load a large document, you might want to call this when
     * you're done with it to free up memory.
     */
    this.destroy = function()
    {
        //this._iframe.parentNode.removeChild(this._iframe);
        this._iframe = null;
        this.window = null; // not sure these are necessary, but it doesn't hurt
        this.document = null;
    };

    this._fire_listeners = function()
    {
        this.window = this._iframe.contentWindow;
        this.document = this.window.document;

        for ( var i = 0; i < this._load_listeners.length; i++ )
            this._load_listeners[i]();
    };

    this._append_iframe = function()
    {
        this._iframe = this._owner_document.createElement('IFRAME');
        //this._iframe.setAttribute('style', 'height:1px; width:1px; display:none;');
/*
        this._iframe.style.height = '2px';
        this._iframe.style.width = '2px';
        this._iframe.style.left = '-500px';
        this._iframe.style.position = 'absolute';
*/
        var self = this;
        this._iframe.onload = function() { self._fire_listeners() };
        this._iframe.onreadystatechange = function() 
        {
            if ( self._iframe.readyState == 'complete' )
                self._fire_listeners();
        };
        mb('this._blank_uri: ', this._blank_uri);
        this._iframe.uri = this._blank_uri;
        this._owner_document.body.appendChild(this._iframe);
    };
};

// file Util.HTTP_Reader.js
Util.HTTP_Reader = function()
{
    this._load_listeners = [];
};

/**
 * Loads http(s) data asynchronously.
 * N.B.: This must be asynchronous, in order to deal with an IE bug
 * involving HTTPS over SSL:
 * <http://support.microsoft.com/kb/272359/en>.
 *   (Not sure this is true for XMLHTTP--but async makes much more
 *   sense usually, anyway, so the app doesn't hang.)
 *
 * See <http://developer.apple.com/internet/webcontent/xmlhttpreq.html>
 * for good overview.
 *
 * The actual XMLHttpRequest object will be available as this.request.
 *
 * XXX: This code is icky! Use Util.Request. -EN
 *
 * @param    uri                The URI to load
 * @param    post_data        (optional) string containing post data
 */
Util.HTTP_Reader.prototype.load = function(uri, post_data)
{
    if (window.XMLHttpRequest)
    {
        this.request = new XMLHttpRequest();
    }
    else
    {
        try
        {
            this.request = new ActiveXObject('Microsoft.XMLHTTP');
        }
        catch(e)
        {
            throw "Util.HTTP_Reader.load: Your browser supports neither the W3C method nor the MS method of reading data over http.";
        }
    }
    
    this._really_add_load_listeners();
    
    if (post_data) {
        this.request.open('POST', uri, true);
        this.request.send(post_data);
    } else {
        this.request.open('GET', uri, true);
        this.request.send();
    }
};

/**
 * Adds an onload listener to the data. The normal
 * add_event_listener cannot be used because IE doesn't have a load
 * event for xml documents, but instead has an onreadystatechange
 * event.
 *
 * @param    listener    a function which will be called when the event is fired, and which receives as a paramater the
 *                      request object
 */
Util.HTTP_Reader.prototype.add_load_listener = function(listener)
{
    this._load_listeners.push(listener);
}

Util.HTTP_Reader.prototype._really_add_load_listeners = function()
{
    var self = this;
    
    this.request.onreadystatechange = function()
    {
        var state = self.request.readyState;
        if (state == 4 || state == 'complete') {
            for (var i = 0; i < self._load_listeners.length; i++) {
                self._load_listeners[i](self.request);
            }
        }
    }
};

// file Util.Head.js
/**
 * Does nothing
 * @constructor
 *
 * @class Contains functions pertaining to head elements.
 */
Util.Head = function()
{
};

/**
 * Append the style sheet at the given location with the given id
 *
 * @param    location    the location of the stylesheet to add
 * @static
 */
Util.Head._append_style_sheet = function(location)
{
    var head_elem = this._dialog_window.document.getElementsByTagName('head').item(0);
    var link_elem = this._dialog_window.document.createElement('link');

    link_elem.setAttribute('href', location);
    link_elem.setAttribute('rel', 'stylesheet');
    link_elem.setAttribute('type', 'text/css');

    head_elem.appendChild(link_elem);
};

// file Util.Iframe.js
/**
 * Declares instance variables. <code>this.iframe</code>,
 * <code>this.window</code> <code>this.document</code>, and
 * <code>this.body</code> are not initialized until the method
 * <code>this.open</code> is called.
 *
 * @constructor
 *
 * @class A wrapper to DOM iframe elements. Provides extra and
 * cross-browser functionality.
 */
Util.Iframe = function()
{
    this.iframe_elem;
    this.content_window;
    this.content_document;
    this.body_elem;
};

/**
 * Creates an iframe element and inits instance variables.
 *
 * @param    doc_obj            the document object with which to create the iframe.
 * @param    uri                (optional) the uri of the page to open in the
 *                            iframe. Defaults to about:blank, with the result
 *                            that no page is initially opened in the iframe.
 *                             NOTE: if you plan to use this behind https, as
 *                             we do Loki, you must specify a uri, not just 
 *                             about:blank, or IE will pop up an alert about
 *                             combining https and http.
 */
Util.Iframe.prototype.init = function(doc_obj, uri)
{
    // Provide defaults for optional arguments
    if ( uri == null || uri == '' )
        // When under https, this causes an alert in IE about combining https and http (see above):
        uri = 'about:blank';

    // Creates iframe
    this.iframe_elem = doc_obj.createElement('IFRAME');

    // Set source
    this.iframe_elem.src = uri;

    this.iframe_elem.onload = function()
    {

        alert('loaded'); return true;

    // Set up reference to iframe's content document
    this.content_window = Util.Iframe.get_content_window(this.iframe_elem);
    this.content_document = Util.Iframe.get_content_document(this.iframe_elem);

    // If we just want to load about:blank, there's no need for an
    // asynchronous call. 
    //
    // By writing the document's initial HTML out ourself and then
    // closing the document (that's the important part), we
    // essentially make the "src" loading synchronous rather than
    // asynchronous. And if we're just trying to open an empty window,
    // this is not dangerous. (It might be dangerous otherwise, since
    // a synchronous "src" loading that involved a request to the web
    // server might cause the script to effectively hang if the web
    // server didn't respond.)
    //
    // If we are given a URI to request from the web server, we skip
    // this, so the loading "src" is asynchronous, so before we do
    // anything with the window's contents, we need to make sure that
    // the content document has loaded. One way to do this is to add a
    // "load" event listener, and then do everything we want to in the
    // listener. Beware, though: this can cause royal
    // (cross-)browser-fucked pains.
    if ( uri == '' )
    {
        this.content_document.write('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">' +
                                    '<html><head><title></title></head><body>' +
                                    '</body></html>');
        this.content_document.close();

        // We can only set a reference to the body element if the
        // document has finished loading, and here we can only be sure
        // of that across browsers if we've called document.close().
        //
        // One upshot is that if we are given a URI to load in the
        // iframe, we have to wait until the load event is fired to
        // get a reference to the body tag, and I don't want to muck
        // around with that here. So in that case we just don't get
        // such a reference here. (Notice that the assignment below is
        // still in the if block.) You have to get the reference
        // yourself if you want it.
        this.body_elem = this.content_document.getElementsByTagName('BODY').item(0);
    }

    };
};



Util.Iframe.get_content_window = function(iframe_elem)
{
    return iframe_elem.contentWindow;
};

Util.Iframe.get_content_document = function(iframe_elem)
{
    var content_document;

    if ( iframe_elem.contentDocument != null )
    {
        content_document = iframe_elem.contentDocument;
    }
    else if ( iframe_elem.document != null )
    {
        content_document = iframe_elem.contentWindow.document;
    }
    else
    {
        throw new Error('Util.Iframe.get_content_document: Neither the W3C method of accessing ' +
                        'the iframe's content document ' +
                        'nor a workaround for IE worked.');
    }

    return content_document;
};

// file Util.Image.js
Util.Image = function()
{
};

var image_i = 0;

// Rescales an image such that its width equals the given width, while
// preserving the image's aspect ratio.
Util.Image.set_width = function(img_elem, width)
{
    var ratio = width / img_elem.width;
    img_elem.height = Math.floor( img_elem.height * ratio );
    img_elem.width = width;
};

// Rescales an image such that its height equals the given height,
// while preserving the image's aspect ratio.
Util.Image.set_height = function(img_elem, height)
{
    var ratio = height / img_elem.height;
    img_elem.width = Math.floor( img_elem.width * ratio );
    img_elem.height = height;
};

// Rescales an image to fit within max_width and max_height, while
// preserving the image's aspect ratio.
Util.Image.set_max_size = function(img_elem, max_width, max_height)
{
    // If only the image's width is greater than max_width, rescale
    // based on width
    if ( img_elem.width > max_width && !(img_elem.height > max_height) )
    {
        Util.Image.set_width(img_elem, max_width);
    }
    // If only the image's height is greater than max_height, rescale
    // based on height
    else if ( img_elem.height > max_height && !(img_elem.width > max_width) )
    {
        Util.Image.set_height(img_elem, max_height);
    }
    // If both are greater than their correspondant, ...
    else if ( img_elem.width > max_width && img_elem.height > max_height )
    {
        // If the difference between the image's width and max_width
        // is greater than the difference between the image's height
        // and max_height, rescale based on width
        if ( img_elem.width - max_width > img_elem.height - max_height )
        {
            Util.Image.set_width(img_elem, max_width);
        }
        // Else, rescale based on height
        else
        {
            Util.Image.set_height(img_elem, max_height);
        }
    }
    // Else (if the image's width and height are both less than their
    // correspondants), do nothing
};

// N.B.: I would not offer my life as pledge that this function works.
// (It's never used in Loki as of now, but made sense to write it up
// while writing the above.)
//
// Rescales an image to fit within max_width and max_height, while
// preserving the image's aspect ratio.
Util.Image.set_min_size = function(img_elem, max_width, max_height)
{
    // If only the image's width is less than max_width, rescale
    // based on width
    if ( img_elem.width < max_width && !(img_elem.height < max_height) )
    {
        Util.Image.set_width(img_elem, max_width);
    }
    // If only the image's height is less than max_height, rescale
    // based on height
    else if ( img_elem.height < max_height && !(img_elem.width < max_width) )
    {
        Util.Image.set_height(img_elem, max_height);
    }
    // If both are less than their correspondant, ...
    else if ( img_elem.width < max_width && img_elem.height < max_height )
    {
        // If the difference between the image's width and max_width
        // is greater than the difference between the image's height
        // and max_height, rescale based on width
        if (  max_width - img_elem.width >  max_height - img_elem.height )
        {
            Util.Image.set_width(img_elem, max_width);
        }
        // Else, rescale based on height
        else
        {
            Util.Image.set_height(img_elem, max_height);
        }
    }
    // Else (if the image's width and height are both greater than their
    // correspondants), do nothing
};



// SET MAX SIZE

// If only the image's width is greater than max_width, rescale based on width

// If only the image's height is greater than max_height, rescale based on height

// If both are greater than their correspondant,
//     if ( image's width - max_width > image's height - max_height ), rescale based on width
//     else, rescale based on height

// SET MIN SIZE

// same as for max size, but change "greater" to "less"

// SET SIZE

// If only the image's width is not equal to max_width, rescale based on width.
// If only the image's height is not equal to max_height, rescale based on height.
// If neither is equal to either,
//     if max_width is greater than max_height, rescale based on max_width;
//     else, rescale based on max_height.

// file Util.Input.js
Util.Input = function()
{
};

/**
 * Creates a DOM input element and adds the given name attribute. This
 * is necessary because of a bug in IE which doesn't allow the name
 * attribute to be set on created input elements.
 *
 * @static
 * @param    params    object containing the following named paramaters:
 *                  <ul>
 *                  <li>doc - the document object with which to create the input</li>
 *                  <li>name - the desired name of the input</li>
 *                  <li>checked - (optional) boolean indicating whether the input should be checked</li>
 *                  </ul>
 * @return            a DOM input element
 */
Util.Input.create_named_input = function(params)
{
    var doc = params.document;
    var name = params.name;
    var checked = params.checked;

    // Make sure required arguments are given
    if ( doc == null || name == '' )
        throw(new Error('Util.Input.create_named_input: Missing argument.'));

    // First try to create the input and add its name attribute
    // normally
    var input = doc.createElement('INPUT');
    input.setAttribute('name', name);
    if ( checked )
        input.setAttribute('checked', 'checked');
    

    // If that didn't work, create it in the IE way
    if ( input.outerHTML != null && input.outerHTML.indexOf('name') == -1 )
    {
        var checked_str = checked ? ' checked="checked"' : '';
        input = doc.createElement('<INPUT name="' + name + '"' + checked_str + '>');
    }

    // Make sure it worked
    if ( input == null || input.getAttribute('name') == '' )
        throw(new Error('Util.Input.create_named_input: Couldn't create named input.'));
        
    return input;
}; 
// file Util.JSON.js
Util.JSON = (function JSON() {
    var special = {
        'b': '\b',
        't': '\t',
        'n': '\n',
        'f': '\f',
        'r': '\r',
        '\': '\\'
    };
    var indent = "    ";
    
    function str_repeat(string, count) {
        return count < 1 ? '' : new Array(count + 1).join(string);
    }
    
    function pad_number(num, length, radix) {
        var string = num.toString(radix || 10);
        return str_repeat('0', length - string.length) + string;
    }
    
    var primitive_dumpers = {
        "number": function json_dump_number(num) {
            return isFinite(num) ? num.toString() : null;
        },
        
        "string": function json_dump_string(s) {
            s = s.replace(/[x00-x1f\]/g, function(c) {
                var character = special[c];
                return special[c] || '\u00' + pad_number(c.charCodeAt(0), 2, 16);
            });
            return '"' + s.replace(/"/g, '\"') + '"';
        },
        
        "boolean": function json_dump_boolean(b) {
            return (b) ? "true" : "false";
        },
        
        "undefined": function json_dump_undefined() {
            return "null";
        },
        
        "function": function json_dump_function(fn) {
            return "null";
        }
    };
    
    function json_dump_regexp(re) {
        return primitive_dumpers.string(re.toString());
    }
    
    function is_regexp(value) {
        return (value && typeof(value) == 'object' &&
            typeof(value.test) == "function" &&
            typeof(value.exec) == "function" &&
            typeof(value.global) == "boolean");
    }
    
    function _json_dump_child_value(buf, level, value) {
        var t = typeof(value), end;
        var is_re = is_regexp(value);
        if (value !== null && t == "object" && !is_re) {
            json_dump_object(buf, level + 1, value);
        } else {
            if (value === null)
                value = 'null';
            else if (is_re)
                value = json_dump_regexp(value);
            else
                value = primitive_dumpers[t](value);
            end = buf.length - 1;
            buf[end] = buf[end] + value;
        }
    }
    
    function json_dump_object(buf, level, object) {
        if (typeof(object.each) == "function") {
            json_dump_array(buf, level, object);
            return;
        }
        
        var last = buf.length - 1;
        buf[last] = buf[last] + '{';
        
        var ci = str_repeat(indent, level + 1);
        var name, start, value;
        var keys = Util.Object.names(object), i, t;
        last = keys.length - 1;
        for (i = 0; i < keys.length; i++) {
            name = keys[i];
            value = object[name];
            if (typeof(value) == "function")
                continue;
            buf.push(ci + primitive_dumpers.string(name) + ": ");
            _json_dump_child_value(buf, level, value);
            if (i < last)
                buf[buf.length - 1] = buf[buf.length - 1] + ",";
        }
        
        buf.push(str_repeat(indent, level) + "}");
    }
    
    function json_dump_array(buf, level, array) {
        var last = buf.length - 1;
        buf[last] = buf[last] + '[';
        var ci = str_repeat(indent, level + 1);
        var i, value, last = array.length - 1;
        for (i = 0; i < array.length; i++) {
            value = array[i];
            if (typeof(value) == "function")
                continue;
            buf.push(ci);
            _json_dump_child_value(buf, level, value);
            if (i < last)
                buf[buf.length - 1] = buf[buf.length - 1] + ",";
        }
        
        buf.push(str_repeat(indent, level) + "]");
    }
    
    return {
        dump: function json_dump(object) {
            var t = typeof(object), dumper, buf;
            if (object === null) {
                return 'null';
            } else if (t == "object") {
                if (is_regexp(object))
                    return json_dump_regexp(object);
                buf = [''];
                json_dump_object(buf, 0, object);
                return buf.join("n");
            } else {
                dumper = primitive_dumpers[t];
                if (!dumper)
                    throw new TypeError("Cannot dump to JSON; unknown type " + t + ".");
                return dumper(object);
            }
        }
    };
})(); 
// file Util.Lock.js
/**
 * @class A synchronization object, based on Lamport's Bakery algorithm.
 * @see http://decenturl.com/en.wikipedia/lamport
 * @author Eric Naeseth
 * @constructor
 */
Util.Lock = function(name)
{
    var threads = {};
    var next_id = 0;
    var active_thread = null;
    
    function pair_less_than(a, b, c, d)
    {
        return (a < c) || (a == c && b < d);
    }
    
    function next_number()
    {
        var max = 0;
        
        for (var i in threads) {
            if (threads[i] && threads[i].number && threads[i].number > max)
                max = threads[i].number;
        }
        
        return 1 + max;
    }
    
    this.acquire = function()
    {
        var thread = {
            id: ++next_id,
            entering: false
        };
        
        threads[thread.id] = thread;
        
        thread.entering = true;
        thread.number = next_number();
        thread.entering = false;
        
        for (var i in threads) {
            if (!threads[i])
                continue;
                
            var t = threads[i];
            
            // wait until the thread receives its number
            while (t.entering) { /* wait */ }
            
            // wait until all threads with smaller numbers or with the same
            // number but higher priority finish their work with whatever has
            // been locked
            while (t.number &&
                pair_less_than(t.number, i, thread.number, thread.id))
            {
                // wait
            }
        }
        active_thread = thread;
        // the thread is now locked
    }
    
    this.release = function()
    {
        active_thread.number = 0;
    }

// file Util.RSS.js
/**
 * @class Home to RSS-related facilities.
 */
Util.RSS = {
    
}

/**
 * @class A RSS 2.0 feed reader.
 *
 * @constructor Creates a new RSS 2.0 feed reader for the given URL.
 * @param    url    The URL of the RSS feed. You may pass in a function returning the URI instead
 *                of the URL itself. To permit the chunking of results, this function must accept
 *                two parameters: the offset to start on will be passed in as the first parameter
 *                and the number of items to retrieve will be passed in as the second.
 *
 * @author Eric Naeseth
 */
Util.RSS.Reader = function RSSReader(url)
{
    this.url = url;
    
    var offset = 0;
    var listeners = {
        load: [],
        error: [],
        timeout: []
    };
    var aborted = false;
    
    this.feed = null;
    
    function handle_result(document)
    {
        if (aborted || !document)
            return;
        
        var rss = document.documentElement;
        var channel = (function() {
            try {
                return rss.getElementsByTagName('channel')[0];
            } catch (e) {
                handle_error('RSS feed lacks a channel element!', 0);
            }
        })();
        var items = rss.getElementsByTagName('item');
        
        function get_text(node)
        {
            var text = null;
            
            for (var i = 0; i < node.childNodes.length; i++) {
                var child = node.childNodes[i];
                if (child.nodeType == Util.Node.TEXT_NODE) {
                    if (text)
                        text = text + child.nodeValue;
                    else
                        text = child.nodeValue;
                }
            }
            
            return text || '';
        }
        
        function get_text_child(container, name)
        {
            var nodes = container.getElementsByTagName(name);
            return (nodes.length == 0)
                ? null
                : get_text(nodes[0]);
        }
        
        function to_number(text)
        {
            return (!text || text.length == 0)
                ? null
                : new Number(text);
        }
        
        function to_date(text)
        {
            return (text && text.length > 0)
                ? new Date(text)
                : null;
        }
        
        if (!this.feed) {
            this.feed = new Util.RSS.Feed();
            this.feed.version = rss.getAttribute('version');
            
            this.feed.channel = new Util.RSS.Channel();
            var channel_object = this.feed.channel;
            channel_object.title = get_text_child(channel, 'title');
            channel_object.link = get_text_child(channel, 'link');
            channel_object.description = get_text_child(channel, 'description');
            channel_object.language = get_text_child(channel, 'language');
            channel_object.copyright = get_text_child(channel, 'copyright');
            channel_object.managing_editor = get_text_child(channel, 'managingEditor');
            channel_object.webmaster = get_text_child(channel, 'webMaster');
            channel_object.publication_date = to_date(get_text_child(channel, 'pubDate'));
            channel_object.last_build_date = to_date(get_text_child(channel, 'lastBuildDate'));
            channel_object.category = get_text_child(channel, 'category');
            channel_object.generator = get_text_child(channel, 'generator');
            channel_object.docs = get_text_child(channel, 'docs');
            channel_object.time_to_live = to_number(get_text_child(channel, 'ttl'));
            channel_object.rating = get_text_child(channel, 'rating');
        }
        
        var new_items = [];
        var item_elements = channel.getElementsByTagName('item');
        
        function get_source(node)
        {
            try {
                return {
                    name: get_text(node),
                    url: node.getAttribute('url')
                };
            } catch (e) {
                return null;
            }
        }
        
        function get_enclosure(node)
        {
            try {
                return {
                    url: node.getAttribute('url'),
                    length: to_number(node.getAttribute('length')),
                    type: node.getAttribute('type')
                };
            } catch (e) {
                return null;
            }
        }
        
        for (var i = 0; i < item_elements.length; i++) {
            var item = item_elements[i];
            var item_object = new Util.RSS.Item();
            
            for (var j = 0; j < item.childNodes.length; j++) {
                var node = item.childNodes[j];
                
                if (node.nodeType != Util.Node.ELEMENT_NODE)
                    continue;
                
                var nn = node.nodeName;
                if (nn == 'pubDate') {
                    item_object.publication_date = to_date(get_text(node));
                } else if (nn == 'source') {
                    item_object.source = get_source(node);
                } else if (nn == 'enclosure') {
                    item_object.enclosure = get_enclosure(node);
                } else {
                    item_object[nn] = get_text(node);
                }
            }
            
            new_items.push(item_object);
            this.feed.items.push(item_object);
        }
        
        offset += i;
        
        listeners.load.each(function(l) {
            l(this.feed, new_items);
        }.bind(this));
    }
    
    function handle_error(message, code)
    {
        if (aborted)
            return;
        
        listeners.error.each(function(l) {
            l(message, code);
        });
    }
    
    function handle_timeout()
    {
        listeners.timeout.each(function (l) {
            l('Operation timed out.', 0);
        });
    }
    
    /**
     * Adds an event listener.
     */
    this.add_event_listener = function add_rss_event_listener(type, func)
    {
        if (!listeners[type]) {
            throw new Error('Unknown listener type "' + type + '".');
        }
        
        listeners[type].push(func);
        return true;
    }
    
    /**
     * Loads items from the feed. If the "num" parameter is provided and the URL has been set up
     * to support chunking (see description of the construtor), only requests that many items.
     */
    this.load = function load_rss_feed(num, timeout)
    {
        if (!num)
            var num = null;
        if (!timeout)
            var timeout = null;
            
        aborted = false;
        
        var url = (typeof(this.url) == 'function')
            ? (num ? this.url(offset, num) : this.url())
            : this.url;
        
        this.request = new Util.Request(url, {
            method: 'GET',
            timeout: timeout,
            
            on_success: function(req, t) {
                if (aborted)
                    return;
                if (!(t.responseXML && t.responseXML.documentElement.nodeName == 'rss')) {
                    handle_error('Server did not respond with an RSS document.', 0);
                }
                handle_result.call(this, t.responseXML); 
            }.bind(this),
            
            on_failure: function(req, transport) {
                handle_error(req.get_status_text(), req.get_status());
            },
            
            on_abort: function(req, transport) {
                aborted = true;
            },
            
            on_timeout: function(req, transport) {
                if (listeners.timeout.length > 0) {
                    aborted = true;
                    handle_timeout();
                } else {
                    handle_error(req.get_status_text(), req.get_status());
                    aborted = true;
                }
            }
        });
    }
}

/**
 * @constructor Creates a new feed object.
 *
 * @class An RSS feed.
 * @author Eric Naeseth
 */
Util.RSS.Feed = function RSSFeed()
{
    this.version = null;
    this.channel = null;
    this.items = [];
}

/**
 * @constructor Creates a new channel object.
 *
 * @class An RSS channel.
 * @author Eric Naeseth
 */
Util.RSS.Channel = function RSSChannel()
{
    // required elements
    this.title = null;
    this.link = null;
    this.description = null;
    
    // optional elements
    this.language = null;
    this.copyright = null;
    this.managing_editor = null;
    this.webmaster = null;
    this.publication_date = null;
    this.last_build_date = null;
    this.category = null;
    this.generator = null;
    this.docs = null;
    this.cloud = null;
    this.time_to_live = null;
    this.image = null;
    this.rating = null;
    this.text_input = null;
    this.skip_hours = null;
    this.skip_days = null;
}

/**
 * @constructor Creates a new feed object.
 *
 * @class An RSS feed.
 * @author Eric Naeseth
 */
Util.RSS.Item = function RSSItem()
{
    this.title = null;
    this.link = null;
    this.description = null;
    this.author = null;
    this.category = null;
    this.comments = null;
    this.enclosure = null;
    this.guid = null;
    this.publication_date = null;
    this.source = null;

// file Util.Radio.js
/**
 * Creates a chunk containing a radio button.
 * @constructor
 *
 * @param    params    an object with the following properties:
 *                  <ul>
 *                  <li>document - the DOM document object which will own the created DOM elements
 *                  <li>id - the desired id of the radio's DOM input element</li>
 *                  <li>name - the desired name of the radio's DOM input element</li>
 *                  <li>value - the desired value of the radio's DOM input element</li>
 *                  <li>label - the desired label of the radio</li>
 *                  <li>checked - boolean indicating whether the radio is checked</li>
 *                  </ul>
 *
 * @class Represents a radio button. Once instantiated, a Radio object
 * has the following properties:
 * <ul>
 * <li>all of the properties given to the constructor in <code>params</code></li>
 * <li>id - the id of the DOM input element</li>
 * <li>label_elem - the DOM label element</li>
 * <li>input_elem - the DOM input element</li>
 * <li>chunk - the containing DOM span element. Use this to append the whole radio chunk.</li>
 * </ul>
 */
Util.Radio = function(params)
{
    this.document = params.document;
    this.id = params.id;
    this.name = params.name;
    this.value = params.value;
    this.label = params.label;
    this.checked = params.checked;

    // Create input element
    this.input_elem = Util.Input.create_named_input({document : this.document, name : this.name, checked : this.checked });
    this.input_elem.setAttribute('type', 'radio');
    this.input_elem.setAttribute('id', this.id);
    this.input_elem.setAttribute('value', this.value);

    // Create label elem
    this.label_elem = this.document.createElement('LABEL');
    this.label_elem.appendChild( this.document.createTextNode( this.label ) );
    this.label_elem.setAttribute('for', this.id);

    // Create chunk, and append to it the input and label elems
    this.chunk = this.document.createElement('SPAN');
    this.chunk.appendChild(this.input_elem);
    this.chunk.appendChild(this.label_elem);
};

// file Util.Range.js
/**
 * Does nothing.
 * @constructor
 *
 * @class Group of functions related to ranges. Useful links:
 * <li><a href="http://www.w3.org/TR/2000/REC-DOM-Level-2-Traversal-Range-20001113/ranges.html">W3C range spec</a></li>
 * <li><a href="http://www.mozilla.org/docs/dom/domref/dom_range_ref.html">Mozilla's Range interface reference</a></li>
 * <li><a href="http://msdn.microsoft.com/workshop/author/dhtml/reference/objects/obj_textrange.asp">Microsoft's documentation on TextRange objects</a></li>
 */
Util.Range = function()
{
};

/**
 * Creates a range from a selection.
 *
 * @param    sel        the selection from which to create range.
 * @return            the created range
 */
Util.Range.create_range = function create_range_from_selection(sel)
{
    // Safari only provides ranges for non-collapsed selections, but still
    // populates the (anchor|focus)(Node|Offset) properties of the selection.
    // Using this, if necessary, we can build our own range object.
    // XXX: I don't actually think that this is true anymore, but I hesitate to
    //      delete the code anyway. -Eric
    
    if (Util.is_function(sel.getRangeAt) && Util.is_number(sel.rangeCount)) {
        if (sel.rangeCount > 0) {
            return sel.getRangeAt(0);
        }
        
        // Try and roll our own.
        if (sel.anchorNode && sel.anchorNode.ownerDocument.createRange) {
            var doc = sel.anchorNode.ownerDocument;
            var range = doc.createRange();
            
            // The old Netscape selection object and DOM Range objects differ in
            // how they class the boundaries of the span of nodes. Selections
            // look at where the user started and finished dragging the mouse
            // while ranges look at which end is actually prior to the other in
            // the document. Because it is an error to set the start and end
            // "backwards" on a DOM range, we have to determine this manually.
            
            function create_range(node, offset)
            {
                var r = doc.createRange();
                r.setStart(node, offset);
                r.collapse(true);
                return r;
            }
            
            var anchor_rng = create_range(sel.anchorNode, sel.anchorOffset);
            var focus_rng = create_range(sel.focusNode, sel.focusOffset);
            
            var natural = anchor_rng.compareBoundaryPoints(Range.START_TO_END,
                focus_range) < 0;
            
            if (natural) {
                range.setStart(sel.anchorNode, sel.anchorOffset);
                range.setEnd(sel.focusNode, sel.focusOffset);
            } else {
                range.setStart(sel.focusNode, sel.focusOffset);
                range.setEnd(sel.anchorNode, sel.anchorOffset);
            }
            
            return range;
        } else {
            throw new Util.Unsupported_Error('getting a range from a ' +
                'collapsed selection');
        }
    } else if (sel.createRange) {
        // Internet Explorer TextRange
        return sel.createRange();
    } else {
        throw new Util.Unsupported_Error('creating a range from a selection');
    }
};

Util.Range.is_collapsed = function is_range_collapsed(rng) {
    var undefined;
    
    if (rng.text !== undefined && rng.text !== null)
        return rng.text == '';
    else if (rng.length !== undefined && rng.length !== null)
        return rng.length <= 0;
    else if (rng.collapsed !== undefined && rng.collapsed !== null)
        return rng.collapsed;
    else if (rng.startContainer && rng.endOffset)
        return (rng.startContainer == rng.endContainer &&
            rng.startOffset == rng.endOffset);
    else
        throw new Util.Unsupported_Error('checking if a range is collapsed');
};

/**
 * Gets the ancestor node which surrounds the given range.
 * XXX: probably better usually to use get_start_container, to
 * follow the convention used elsewhere in Loki. -NB
 *
 * @param    rng        the range in question
 * @return            the ancestor node which surrounds the range
 */
Util.Range.get_common_ancestor = function get_range_common_ancestor(rng)
{
    if (rng.commonAncestorContainer) // W3C
        return rng.commonAncestorContainer;
    else if (rng.parentElement) // Internet Explorer TextRange
        return rng.parentElement();
    else if (rng.item) // Internet Explorer ControlRange
        return rng.item(0);
    
    throw new Util.Unsupported_Error('getting a range's common ancestor');
};

/**
 * Returns the boundaries of the range. Uses somewhat different logic than
 * get_start_container; always returns a container and and offset for each
 * end of the range.
 *
 * Note that behavior regarding selections inside of an <input type="text">
 * element is undefined because its text does not exist as a child node of
 * the input element. Gecko won't even allow you to get anything out of the
 * window's selection. WebKit will pull a text node out of thin air for our
 * use. IE's TextRange objects won't be usable for coming up with the
 * representation that we need.
 * 
 * @param {Range}    rng    the range whose boundaries are desired
 * @return {object}
 */
Util.Range.get_boundaries = function get_range_boundaries(rng)
{
    if (!Util.is_valid_object(rng)) {
        throw new TypeError('Must provide a valid object to ' +
            'Util.Range.get_boundaries().');
    }
    
    var dupe; // duplicate of a range
    var parent; // some node's parent element
    
    function get_boundary(side)
    {        
        if (rng[side + 'Container']) {
            // W3C range
            
            return {
                container: rng[side + 'Container'],
                offset: rng[side + 'Offset']
            };
        } else if (rng.duplicate && rng.parentElement) {
            // IE text range
            
            dupe = rng.duplicate();
            dupe.collapse((side == 'start') ? true : false);
            
            // Find the text node in which the now-collapsed selection lies
            // by trying to move its starting point (i.e. the whole thing)
            // back really far, seeing how many characters were actually
            // moved, and then traversing the range's parent element's
            // text node children to find the text node that it refers to.
            
            // Establish a base by finding the position of the parent.
            parent = dupe.parentElement();
            var parent_range =
                parent.ownerDocument.body.createTextRange();
            parent_range.moveToElementText(parent);
            var base = Math.abs(parent_range.move('character',
                -0xFFFFFF));
            
            var offset = (Math.abs(dupe.move('character', -0xFFFFFF))
                - base);
            var travelled = 0;
            
            for (var i = 0; i < parent.childNodes.length; i++) {
                var child = parent.childNodes[i];
                
                if (child.nodeType == Util.Node.ELEMENT_NODE) {
                    // IE counts each interspersed element as occupying
                    // one character. We have to correct for this when
                    // ending within a text node, but it conveniently
                    // allows us to find when we're stopping at an
                    // element.
                    
                    if (travelled < offset) {
                        // Not this element; move on.
                        travelled++;
                        continue;
                    }
                    
                    // Found it! It's an element!
                    return {
                        container: parent,
                        offset: Util.Node.get_offset(child)
                    }
                } else if (child.nodeType != Util.Node.TEXT_NODE) {
                    // Not interested.
                    continue;
                }
                
                var cl = child.nodeValue.length;
                if (travelled + cl < offset) {
                    // The offset doesn't lie with this text node. Add its
                    // length to the distance we've travelled and move on.
                    travelled += cl;
                    continue;
                }
                
                // Found it!
                return {
                    container: child,
                    offset: offset - travelled
                };
            }
            
            // End of the parent
            return {
                container: parent,
                offset: parent.childNodes.length
            };
        } else if (rng.item) {
            // IE control range
            
            // Note that this code is UNTESTED because I could not get
            // Internet Explorer to produce a control selection.
            
            var interesting_index = (side == 'start') ? 0 : (rng.length - 1);
            var node = rng.item(interesting_index);
            parent = node.parentNode;
            
            return {
                container: parent,
                offset: Util.Node.get_offset(node)
            };
        } else {
            throw new Util.Unsupported_Error('ranges');
        }
    }
    
    return {
        start: get_boundary('start'),
        end: get_boundary('end')
    };
};

/**
 * Gets the nearest block-level elements in the ancestry of each boundary of
 * the given range.
 * @param {Range} range the range of which the bounding blocks are desired
 * @param {Boolean} [as_bounds=false] if true, returns an object in the style
 * of {@link Util.Range.get_boundaries} specifying the blocks
 * @return {Object} the bounding blocks
 */
Util.Range.get_boundary_blocks = function get_range_boundary_blocks(range,
    as_bounds)
{
    var bounds = Util.Range.get_boundaries(range);
    var side;
    
    function get_block(boundary) {
        var container = boundary.container;
        var length = container.childNodes.length;
        var start;
        var node;
        
        if (container.nodeType == Util.Node.TEXT_NODE)
            start = container.parentNode;
        else if (container.childNodes[boundary.offset])
            start = container.childNodes[boundary.offset];
        else if (length == 0)
            start = container;
        else
            start = container.childNodes[boundary.offset - 1];
            
        for (var node = start; node; node = node.parentNode) {
            if (Util.Node.is_block(node))
                return node;
        }
        
        throw new Error('Could not find an enclosing block for the range ' +
            'boundary.');
    }
    
    function process_block(block) {
        if (!as_bounds)
            return block;
        
        return {
            container: block.parentNode,
            offset: Util.Node.get_offset(block)
        };
    }
    
    for (side in bounds) {
        bounds[side] = process_block(get_block(bounds[side]));
    }
    return bounds;
};

/**
 * Finds matching elements within the range.
 * @param {Range|Object} rng the range to search in, or a range boundary object
 *        like the one returned from {@link Util.Range.get_boundaries}.
 * @param {Function|String} [matcher] either a matching function or a tag name.
 * @param {Boolean} [up=false] also search up the tree from the range's common
 *        ancestor. It is an error to set this option if there is no matcher.
 * @throws {Error} if up is true but there is no matcher
 * @return {HTMLElement[]} all found matching elements
 */
Util.Range.find_nodes = function find_nodes_in_range(rng, matcher, up) {
    function process_boundary(bound) {
        var length;
        
        if (bound.container.nodeType == Util.Node.TEXT_NODE)
            return bound.container;
        
        if (bound.container.childNodes[bound.offset])
            return bound.container.childNodes[bound.offset];
        
        length = bound.container.childNodes.length;
        if (length == 0 || bound.offset == 0)
            return bound.container;
        else if (bound.offset >= length)
            return bound.container.childNodes[length - 1];
        else
            throw new Error('Unable to process boundary for find_nodes_in_range: ' +
                Util.Node.get_debug_string(bound.container) + ':' + bound.offset);
    }
    
    var bounds = (rng.start && rng.start.container && rng.end.container)
        ? rng
        : Util.Range.get_boundaries(rng);
    var matched_nodes = [];
    var start = process_boundary(bounds.start);
    var end = process_boundary(bounds.end);
    var node;
    var ancestor;
    
    if (!matcher && up) {
        throw new Error('Cannot find nodes that are ancestors of the range ' +
            'if no matcher is selected.');
    }
    
    function next_node(n) {
        if (n.hasChildNodes()) {
            n = n.firstChild;
        } else if (n.nextSibling) {
            n = n.nextSibling;
        } else if (n.parentNode && n.parentNode.nextSibling) {
            n = n.parentNode.nextSibling;
        } else {
            n = null;
        }
        
        return n;
    }
    
    if (typeof(matcher) == 'string')
        matcher = Util.Node.curry_is_tag(matcher);
    else if (!matcher)
        matcher = Util.Function.optimist;
    else if (typeof(matcher) != 'function')
        throw new TypeError('Invalid matcher.');
    
    for (node = start; node; node = next_node(node)) {
        if (matcher(node))
            matched_nodes.push(node);
        if (node == end)
            break;
    }
    
    if (up) {
        ancestor = Util.Range.get_common_ancestor(rng);
        if (!ancestor)
            return matched_nodes;
        if (ancestor == start || ancestor == end)
            ancestor = ancestor.parentNode;
        end = start.ownerDocument;
        for (node = ancestor; node && node != end; node = node.parentNode) {
            if (matcher(node))
                matched_nodes.push(node);
        }
    }
    
    return matched_nodes;
};

/**
 * Returns the start container of the given range (if
 * the given range is a text range) or starting element
 * (i.e., first contained node, if the given range is a control 
 * range)
 * 
 * @param    rng        the range in question
 * @return            the start container of the range
 */
Util.Range.get_start_container = function get_range_start_container(rng)
{
    // Gecko
    try
    {
        // Control range
        //   This is not precisely like IE's control range. But it is
        //   like it in that if one entire element is selected, 
        //   this function returns that element (rng.item(0)),
        //   which does what we want. (Otherwise, for example editing 
        //   images and links breaks.)
        //   
        //   (Note: if this breaks, consult the archived versions--I've
        //   played with this a lot to get it to work right.)
        var frag = rng.cloneContents();
        if (frag && frag.firstChild == frag.lastChild &&
             frag.firstChild != null &&
             frag.firstChild.nodeType != Util.Node.TEXT_NODE &&
             frag.lastChild != null &&
             frag.lastChild.nodeType != Util.Node.TEXT_NODE)
        {
            var siblings = rng.commonAncestorContainer.childNodes;
            for (var i = 0; i < siblings.length; i++)
                if (rng.compareNode(siblings[i]) == rng.NODE_INSIDE)
                    return siblings[i];
        }

        // Text range
        if (rng.startContainer.nodeType == Util.Node.TEXT_NODE) // imitate IE below
            return rng.startContainer.parentNode;
        else
            return rng.startContainer;
    }
    catch(e)
    {
        // IE
        try
        {
            // Control range
            if (rng.item != null)
            {
                return rng.item(0);
            }
            // Text range
            else if (rng.parentElement != null)
            {
                // original, works in most circumstances:
                //return rng.parentElement();
                var rng2 = rng.duplicate();
                rng2.collapse(true); // to start
                return rng2.parentElement();
            }
        }
        catch(f)
        {
            throw(new Error('Util.Range.get_start_container(): Neither the Mozilla nor the IE way of getting the start container worked. ' +
                                'When the Mozilla way was tried, an error with the following message was thrown: <<' + e.message + '>>. ' +
                                'When the IE way was tried, an error with the following message was thrown: <<' + f.message + '>>.'));
        }
    }
};

/**
 * Returns the end container of the given range (if
 * the given range is a text range) or ending element
 * (i.e., last contained node, if the given range is a 
 * control range)
 *
 * @param    rng        the range in question
 * @return            the end container of the range
 */
Util.Range.get_end_container = function get_range_end_container(rng)
{
    // Gecko
    try
    {
        // Control range
        //   This is not precisely like IE's control range. But it is
        //   like it in that if one entire element is selected, 
        //   this function returns that element (rng.item(0)),
        //   which does what we want. (Otherwise, for example editing 
        //   images and links breaks.)
        //   
        //   (Note: if this breaks, consult the archived versions--I've
        //   played with this a lot to get it to work right.)
        //
        //   (Note: this does precisely the same thing as get_start_container
        //   for control ranges, because the range is only considered a control
        //   range if the first and last elements are identical. Previous 
        //   versions didn't work this way.)
        var frag = rng.cloneContents();
        if (frag && frag.firstChild == frag.lastChild &&
             frag.firstChild != null &&
             frag.firstChild.nodeType != Util.Node.TEXT_NODE &&
             frag.lastChild != null &&
             frag.lastChild.nodeType != Util.Node.TEXT_NODE)
        {
            var siblings = rng.commonAncestorContainer.childNodes;
            for (var i = 0; i < siblings.length; i++)
                if (rng.compareNode(siblings[i]) == rng.NODE_INSIDE)
                    return siblings[i];
        }

        // Text range
        if (rng.endContainer.nodeType == Util.Node.TEXT_NODE) // imitate IE below
            return rng.endContainer.parentNode;
        else
            return rng.endContainer;
    }
    catch(e)
    {
        // IE
        try
        {
            // Control range
            if (rng.item != null)
            {
                return rng.item(rng.length - 1);
            }
            // Text range
            else if (rng.parentElement != null)
            {
                var rng2 = rng.duplicate();
                rng2.collapse(false); // to end
                return rng2.parentElement();
            }
        }
        catch(f)
        {
            throw(new Error('Util.Range.get_start_container(): Neither the Mozilla nor the IE way of getting the start container worked. ' +
                                'When the Mozilla way was tried, an error with the following message was thrown: <<' + e.message + '>>. ' +
                                'When the IE way was tried, an error with the following message was thrown: <<' + f.message + '>>.'));
        }
    }
};


/**
 * Deletes the contents of the given range.
 *
 * @param    rng        the range
 */
Util.Range.delete_contents = function delete_range_contents(rng)
{
    if (Util.is_function(rng.deleteContents)) { // W3C
        rng.deleteContents();
    } else if (rng.pasteHTML) { // TextRange
        rng.pasteHTML('');
    } else if (rng.item && rng.remove) { // ControlRange
        while (rng.length > 0) {
            var item = rng.item(0);
            item.parentNode.removeChild(item);
            rng.remove(0);
        }
    } else {
        throw new Util.Unsupported_Error("deleting a range's contents");
    }
};

/**
 * Inserts a node at the beginning of the given range.
 *
 * @param    rng        the range
 * @param    node    the node to insert
 * @return {void}
 */
Util.Range.insert_node = function insert_node_in_range(rng, node)
{
    var bounds;
    var point;
    var target;
    
    if (rng.insertNode) {
        // W3C range
        rng.insertNode(node);
    } else {
        // Internet Explorer range
        bounds = Util.Range.get_boundaries(rng);
        
        if (bounds.start.container.nodeType == Util.Node.TEXT_NODE) {
            // Inserting the node into a text node; split it at the insertion
            // point.
            bounds.start.container.splitText(bounds.start.offset);
            point = bounds.start.container.nextSibling;
            
            // Now the node can be inserted between the two text nodes.
            target = bounds.start.container.parentNode;
        } else {
            point = (bounds.start.container.hasChildNodes())
                ? bounds.start.container.childNodes[bounds.start.offset]
                : null;
            target = bounds.start.container;
        }
        
        // Don't remove this split; insertBefore SHOULD work with a null
        // second argument, but IE8 doesn't accept it.
        if (point) {
            target.insertBefore(node, point);
        } else {
            target.appendChild(node);
        }
    }
};

/**
 * Clones the given range.
 *
 * @param    rng        the range
 * @return            a clone of rng
 */
Util.Range.clone_range = function clone_range(rng)
{
    if (Util.is_function(rng.cloneRange)) {
        return rng.cloneRange();
    } else if (rng.duplicate) {
        return rng.duplicate();
    } else {
        throw new Util.Unsupported_Error("cloning a range");
    }
};

/**
 * Clones the contents of the given range.
 *
 * @param  {Range}  rng       the range whose contents are desired
 * @return {DocumentFragment} the range's contents
 */
Util.Range.clone_contents = function clone_range_contents(rng)
{
    var html;
    var doc;
    var hack;
    var frag;
    
    if (rng.cloneContents) {
        // W3C range
        return rng.cloneContents();
    } else if (html = rng.htmlText) { // assignment intentional
        // IE text range
        // This is just painfully hackish, but the option of writing the code
        // to properly traverse a range and clone its contents is far worse.
        
        doc = rng.parentElement().ownerDocument;
        
        hack = doc.createElement('DIV');
        hack.innerHTML = html;
        
        frag = doc.createDocumentFragment();
        while (hack.firstChild) {
            frag.appendChild(hack.firstChild);
        }
        
        return frag;
    } else {
        throw new Util.Unsupported_Error('cloning the contents of a range');
    }
}

/**
 * Deletes the contents of the given range.
 *
 * @param {Range}  rng   the range whose contents should be deleted
 * @return {void}
 */
Util.Range.delete_contents = function delete_range_contents(rng)
{
    if (rng.deleteContents) {
        // W3C range
        rng.deleteContents();
    } else if (rng.parentElement) {
        // IE text range
        rng.text = ''; // seriously.
    } else {
        throw new Util.Unsupported_Error('deleting the contents of a range');
    }
}

/**
 * Gets the html of the range.
 */
Util.Range.get_html = function get_html_of_range(rng)
{
    var html = '';
    try // Gecko
    {
        var frag = rng.cloneContents();
        var container = rng.startContainer.ownerDocument.createElement('DIV');
        container.appendChild(frag);
        html = container.innerHTML;
    }
    catch(e)
    {
        try // IE
        {
            if (rng.htmlText != null)
                html = rng.htmlText;
            else if (rng.length > 0)
            {
                for (var i = 0; i < rng.length; i++)
                    html += rng.item(i).outerHTML;
            }
        }
        catch(f)
        {
            throw('Util.Range.get_html(): Neither the Gecko nor the IE way of getting the image worked. ' +
                  'When the Gecko way was tried, an error with the following message was thrown: <<' + e.message + '>>. ' +
                  'When the IE way was tried, an error with the following message was thrown: <<' + f.message + '>>.');
        }
    }
    return html;
};

/**
 * Gets the given range's nearest ancestor which is a block-level
 * element
 *
 * @param    rng        the starting range
 * @return            the matching ancestor, if any
 */
Util.Range.get_nearest_bl_ancestor_element =
    function get_nearest_block_level_ancestor_element_of_range(rng)
{
    return Util.Range.get_nearest_ancestor_node(rng, Util.Node.is_block_level_element);
};

/**
 * Gets the given range's nearest ancestor which maches the given
 * test.
 *
 * @param    rng                the starting range
 * @param    boolean_test    the test
 * @return                    the matching ancestor, if any
 */
Util.Range.get_nearest_ancestor_node =
    function get_nearest_ancestor_node_of_range(rng, boolean_test)
{
    // XXX: Do we really want this? -Eric
    var ancestor = Util.Range.get_start_container(rng);
    
    if (!ancestor)
        return null;
    
    if (boolean_test(ancestor)) {
        return ancestor;
    } else {
        return Util.Node.get_nearest_ancestor_node(ancestor, boolean_test);
    }
};

/**
 * Gets the given range's nearest ancestor which is an element whose
 * tagname matches the one given.
 *
 * @param    rng                the starting range
 * @param    tag_name        the desired tag name    
 * @return                    the matching ancestor, if any
 */
Util.Range.get_nearest_ancestor_element_by_tag_name =
    function get_nearest_ancestor_element_of_range_by_tag_name(rng, tag_name)
{
    function boolean_test(node)
    {
        return (node.nodeType == Util.Node.ELEMENT_NODE &&
                 node.tagName == tag_name);
    }
    return Util.Range.get_nearest_ancestor_node(rng, boolean_test);
};

/**
 * Gets clones of the child nodes of the given range. Currently, this
 * will only work under IE if the given range is a controlRange
 * collection, but not if it's a textRange object. (If a textRange is
 * given, no error will be thrown, but an empty array will be
 * returned.)
 *
 * @param    rng        the range whose children to clone
 * @return            an array of clones of the given range's children
 */
Util.Range.get_cloned_children = function clone_children_of_range(rng)
{
    var child_nodes = new Array();
    try
    {
        var doc_frag = rng.cloneContents();
        var node_list = doc_frag.childNodes;
        for (var i = 0; i < node_list.length; i++)
            child_nodes.push(node_list.item(i));
    }
    catch(e)
    {
        try
        {
            if (rng.item) // if this is a controlRange collection rather than a textRange Object
            {
                for (var i = 0; i < rng.length; i++)
                    child_nodes.push(rng.item(i).cloneNode(true));
            }
        }
        catch(f)
        {
            throw(new Error('Util.Range.get_cloned_children(): Neither the W3c nor the Mozilla way of getting the image worked. ' +
                            'When the W3C way was tried, an error with the following message was thrown: <<' + e.message + '>>. ' +
                            'When the IE way was tried, an error with the following message was thrown: <<' + f.message + '>>.'));
        }
    }
    return child_nodes;
};

/**
 * Returns the text contained in the given range.
 */
Util.Range.get_text = function get_range_text(rng)
{
    var text;
    try // Gecko
    {
        text = rng.toString();        
    }
    catch(e)
    {
        try // IE
        {
            if (rng.text != null) // text range
                text = rng.text;
            else // control range
                text = ''; // XXX is this desirable?
        }
        catch(f)
        {
            throw(new Error('Util.Range.get_text(): Neither the Gecko nor the IE way of getting the text worked. ' +
                            'When the Gecko way was tried, an error with the following message was thrown: <<' + e.message + '>>. ' +
                            'When the IE way was tried, an error with the following message was thrown: <<' + f.message + '>>.'));
        }
    }
    return text;
};

// XXX: These two functions might only work for Gecko right now (and only need to)
Util.Range.is_at_end_of_block = function is_range_at_end_of_block(rng, block)
{
    var ret =
        Util.Node.get_rightmost_descendent(block) == 
        Util.Node.get_rightmost_descendent(rng.startContainer) &&
        // either the start container is not a text node, or 
        // the range (i.e. cursor) is at the end of the text node
        (//rng.startContainer.nodeType != Util.Node.TEXT_NODE ||
          rng.startOffset == rng.startContainer.length); // added - 1 // 
    return ret;
};

Util.Range.is_at_beg_of_block = function is_range_at_beginning_of_block(rng, block)
{
    var ret =
        // the start container is on the path to the leftmost descendent of the current block
        Util.Node.get_leftmost_descendent(block) == 
        Util.Node.get_leftmost_descendent(rng.startContainer) &&
        // either the start container is not a text node, or 
        // the range (i.e. cursor) is at the beginning of the text node
        (rng.startContainer.nodeType != Util.Node.TEXT_NODE ||
          rng.startOffset == 0);
    return ret;
};

Util.Range.is_at_end_of_text = function is_range_at_end_of_text(rng)
{
    return (rng.endContainer.nodeType == Util.Node.TEXT_NODE && rng.endOffset == rng.endContainer.length);
};

Util.Range.is_at_beg_of_text = function is_range_of_beginning_of_text(rng)
{
    return (rng.startContainer.nodeType == Util.Node.TEXT_NODE && rng.startOffset == 0);
}

/**
 * @see Util.Range.surrounded_by_node
 */
Util.Range.intersects_node = function range_intersects_node(rng, node)
{
    var doc = node.ownerDocument;
    var node_rng;
    
    if (Util.is_function(rng.intersectsNode)) { // Gecko < 1.9
        return rng.intersectsNode(node);
    } else if (Util.is_function(doc.createRange)) { // W3C
        node_rng = doc.createRange();
        
        try {
            node_rng.selectNode(node);
        } catch (e) {
            node_rng.selectNodeContents(node);
        }
        
        return (rng.compareBoundaryPoints(Range.END_TO_START, node_rng) == -1
            && rng.compareBoundaryPoints(Range.START_TO_END, node_rng) == 1);
    } else if (doc.body.createTextRange) {
        // This *might* work. -Eric
        
        node_rng = doc.body.createTextRange();
        node_rng.moveToNodeText(node);
        
        return (rng.compareEndPoints('EndToStart', node_rng) == -1 &&
            rng.compareEndPoints('StartToEnd', node_rng) == 1);
    } else {
        throw new Util.Unsupported_Error('testing whether a node intersects ' +
            ' a range');
    }
}

// XXX doesn't work, I believe
/**
 * Returns a list of all descendant nodes that match boolean_test.
 */
Util.Range.get_descendant_nodes =
    function get_range_descendant_nodes(rng, boolean_test)
{
    var matches = [];

    // we use depth-first so that the matches are ordered 
    // according to their position in the document
    var search = function(node)
    {
        for (var i = 0; i < node.childNodes.length; i++)
        {
            search(node.childNodes[i]);
            if (Util.Range.intersects_node(rng, node.childNodes[i]) && boolean_test(node))
                matches.push(node.childNodes[i]);
        }
    }

    var ancestor = Util.Range.get_common_ancestor(rng);
    search(ancestor);

    return matches;
};

// XXX doesn't work
Util.Range.get_elements_within_range = Util.Function.unimplemented;
//Util.Range.get_elements_within_range = function(rng, boolean_test)

/**
 * Compares the boundary points of the two given ranges.
 * Modified from <http://msdn.microsoft.com/workshop/author/dhtml/reference/methods/compareendpoints.asp>:
 * @param    how        Util.Range constant that specifies one of the following values:
 *                         START_TO_START    Compare the start of rng1 with the start of rng2.
 *                         START_TO_END    Compare the start of rng1 with the end of rng2.
 *                         END_TO_START    Compare the end of rng1 with the start of rng2.
 *                         END_TO_END        Compare the end of rng1 with the end of rng2.
 * @return            Returns one of the following possible values:
 *                        -1    The end point of rng1 is further to the left than the end point of rng2.
 *                        0    The end point of rng1 is at the same location as the end point of rng2.
 *                        1    The end point of rng1 is further to the right than the end point of rng2.
 */
Util.Range.START_TO_START = 2;
Util.Range.START_TO_END = 3;
Util.Range.END_TO_START = 4;
Util.Range.END_TO_END = 5;
Util.Range.LEFT = -1;
Util.Range.SAME = 0;
Util.Range.RIGHT = 1;
Util.Range.compare_boundary_points =
    function compare_range_boundary_points(rng1, rng2, how)
{
    if (!Util.is_valid_object(rng1, rng2)) {
        throw new TypeError('Two range objects must be passed to ' +
            'Util.Range.compare_boundary_points.');
    }
    
    if (!Util.is_number(how)) {
        throw new TypeError('A Util.Range comparison constant must be passed ' +
            'to Util.Range.compare_boundary_points.')
    }
    
    var real_how;
    if (Util.is_function(rng1.compareBoundaryPoints)) { // W3C
        if (how == Util.Range.START_TO_START)
            real_how = rng1.START_TO_START;
        else if (how == Util.Range.START_TO_END)
            real_how = rng1.START_TO_END;
        else if (how == Util.Range.END_TO_START)
            real_how = rng1.END_TO_START;
        else if (how == Util.Range.END_TO_END)
            real_how = rng1.END_TO_END;

        return rng1.compareBoundaryPoints(real_how, rng2);
    } else if (rng1.compareEndPoints) { // IE
        if (how == Util.Range.START_TO_START)
            real_how = "StartToStart";
        else if (how == Util.Range.START_TO_END)
            real_how = "StartToEnd";
        else if (how == Util.Range.END_TO_START)
            real_how = "EndToStart";
        else if (how == Util.Range.END_TO_END)
            real_how = "EndToEnd";

        return rng1.compareEndPoints(real_how, rng2);
    } else {
        throw new Util.Unsupported_Error("comparing two ranges' boundary " +
            "points");
    }
};

Util.Range.select_node = function range_select_node(rng, node)
{
    if (rng.selectNode) {
        rng.selectNode(node);
    } else {
        Util.Range.set_start_before(rng, node);
        Util.Range.set_start_after(rng, node);
    }
};

/**
 * A good explanation of what this does from <http://www.dotvoid.com/view.php?id=11>:
 * 
 * Sets the startContainer and endContainer to the supplied node 
 * with a startOffset of 0 and an endOffset of the number of child nodes 
 * the node contains or the number of characters that the node contains.
 */
Util.Range.select_node_contents = function range_select_node_contents(rng, node)
{
    if (Util.is_function(rng.selectNodeContents)) {
        rng.selectNodeContents(node);
    } else if (rng.moveToElementText) {
        rng.moveToElementText(node);
    } else {
        throw new Util.Unsupported_Error("selecting a node's contents with a " +
            "range");
    }
};

/**
 * Determines whether or not the range is entirely surrounded by the given
 * element.
 * @param {Range}    rng    range
 * @param {Element}    elem    element
 * @type boolean
 */
Util.Range.surrounded_by_node = 
    function range_surrounded_by_node(rng, elem)
{
    var n_rng;
    var doc = elem.ownerDocument;
    
    if (Util.is_function(doc.createRange)) {
        n_rng = doc.createRange();
        try {
            n_rng.selectNode(elem);
        } catch (e) {
            n_rng.selectNodeContents(elem);
        }
    } else if (doc.body.createTextRange) {
        n_rng = doc.body.createTextRange();
        n_rng.moveToNodeText(elem);
    } else {
        throw new Util.Unsupported_Error('checking if a range is entirely ' +
            'enclosed by an element');
    }
    
    var START_TO_START = Util.Range.START_TO_START;
    var END_TO_END = Util.Range.END_TO_END;
    
    return (Util.Range.compare_boundary_points(rng, n_rng, START_TO_START) >= 0
        && Util.Range.compare_boundary_points(rng, n_rng, END_TO_END) <= 0);
}

/**
 * Determines whether or not the range contains the entirety of the given node.
 * @param {Range}    rng    range
 * @param {Node}    node    node
 * @type boolean
 */
Util.Range.contains_node = function range_contains_node(rng, node)
{
    var n_rng;
    var doc = node.ownerDocument;
    
    if (Util.is_function(doc.createRange)) {
        n_rng = doc.createRange();
        try {
            n_rng.selectNode(node);
        } catch (e) {
            n_rng.selectNodeContents(node);
        }
    } else if (doc.body.createTextRange) {
        n_rng = doc.body.createTextRange();
        n_rng.moveToNodeText(node);
    } else {
        throw new Util.Unsupported_Error('checking if a node is entirely ' +
            'enclosed by a range');
    }
    
    var START_TO_START = Util.Range.START_TO_START;
    var END_TO_END = Util.Range.END_TO_END;
    
    return (Util.Range.compare_boundary_points(n_rng, rng, START_TO_START) >= 0
        && Util.Range.compare_boundary_points(n_rng, rng, END_TO_END) <= 0);
}

/**
 * Gets all blocks that this range encompasses in whole or part,
 * but that do not surround the range. In other words, gets the 
 * blocks that you probably intend to work on when performing a 
 * block-level operation on a range.
 */
Util.Range.get_intersecting_blocks = function get_range_intersecting_blocks(rng)
{
    // INIT

    // Determine start and end blocks
    var start_container = Util.Range.get_start_container(rng);
    var b1;
    if (Util.Node.is_block_level_element(start_container))
        b1 = start_container;
    else
        b1 = Util.Node.get_nearest_bl_ancestor_element(start_container);

    var end_container = Util.Range.get_end_container(rng);
    var b2;
    if (Util.Node.is_block_level_element(end_container))
        b2 = end_container;
    else
        b2 = Util.Node.get_nearest_bl_ancestor_element(end_container);

    // Determine b2_and_ancestors
    var b2_and_ancestors = [];
    var cur_block = b2;
    while (cur_block != null && cur_block.nodeName != 'BODY' && cur_block.nodeName != 'TD')
    {
        b2_and_ancestors.push(cur_block);
        cur_block = cur_block.parentNode;
    }

    // HELPER FUNCTIONS

    function is_b2_or_ancestor(block)
    {
        for (var i = 0; i < b2_and_ancestors.length; i++)
            if (block == b2_and_ancestors[i])
            {
                mb('found match in is_b2_ancestor: block', block);
                return true;
            }
        return false;
    }

    /**
     * Looks for the branch of the DOM tree that is closest to b1, while still
     * containing and either b2 or an ancestor of b2 (and b1 or anancestor of b1).
     * Does this by climbing the tree, starting at b1's parent, looking for an
     * ancestor of b2 among the current branch's child nodes.
     *
     * @return    object with properties branch, b1_or_ancestor, and b2_or_ancestor,
     *             the latter two being children of branch.
     */
    function look_for_closest_branch_common_to_b1_and_b2(branch, b1_or_ancestor)
    {
        // Try this branch
        for (var i = 0; i < branch.childNodes.length; i++)
        {
            var cur = branch.childNodes[i];
            if (is_b2_or_ancestor(cur))
            {
                var b2_or_ancestor = cur;
                return { branch : branch, b1_or_ancestor : b1_or_ancestor, b2_or_ancestor : b2_or_ancestor };
            }
        }

        // Otherwise try parent branch
        return look_for_closest_branch_common_to_b1_and_b2(branch.parentNode, branch);
        // (branch will be the ancestor of b1 among the branch.parentNode.childNodes)
    }

    function get_intersecting_blocks(branch, b1_or_ancestor, b2_or_ancestor)
    {
        var blocks = [];
        var start = false;
        for (var i = 0; i < branch.childNodes.length; i++)
        {
            var cur = branch.childNodes[i];
            if (cur == b1_or_ancestor)
                start = true;
            if (start)
                blocks.push(cur);
            if (cur == b2_or_ancestor)
            {
                start = false;
                break;
            }
        }
        return blocks;
    }

    // DO IT

    var starting_branch = b1.parentNode;
    var ret = look_for_closest_branch_common_to_b1_and_b2(starting_branch, b1)
    return get_intersecting_blocks(ret.branch, ret.b1_or_ancestor, ret.b2_or_ancestor);
};

Util.Range._ie_set_endpoint =
    function _ie_text_range_set_endpoint(rng, which, node, offset)
{
    // Frustratingly, we cannot directly set the absolute end points of an
    // Internet Explorer text range; we can only set them in terms of an end
    // point of another text range. So, we create a text range whose start point 
    // will beat the desired node and offset and then set the given endpoint of
    // the range in terms of our new range.
    
    var marker = rng.parentElement().ownerDocument.body.createTextRange();
    var parent = (node.nodeType == Util.Node.TEXT_NODE)
        ? node.parentNode
        : node;
    var node_of_interest;
    var char_offset;
    
    marker.moveToElementText(parent);
    
    // IE text ranges use the character as their principal unit. So, in order
    // to translate from the W3C container/offset convention, we must find
    // the number of characters a node is located from the start of "parent".
    function find_node_character_offset(node)
    {
        var stack = [parent];
        var offset = 0;
        var o;
        
        while (o = stack.pop()) { // assignment intentional
            if (node && o == node)
                return offset;
            
            if (o.nodeType == Util.Node.TEXT_NODE) {
                offset += o.nodeValue.length;
            } else if (o.nodeType == Util.Node.ELEMENT_NODE) {
                if (o.hasChildNodes()) {
                    for (var i = o.childNodes.length - 1; i >= 0; i--) {
                        stack.push(o.childNodes[i]);
                    }
                } else {
                    offset += 1;
                }
            }
        }
        
        if (!node)
            return offset;
        
        throw new Error('Could not find the node's offset in characters.');
    }
    
    if (node.nodeType == Util.Node.TEXT_NODE) {
        if (offset > node.nodeValue.length) {
            throw new Error('Offset out of bounds.');
        }
        
        char_offset = find_node_character_offset(node);
        char_offset += offset;
    } else {
        if (offset > node.childNodes.length) {
            throw new Error('Offset out of bounds.');
        }
        
        node_of_interest = (offset == node.childNodes.length)
            ? null
            : node.childNodes[offset];
        char_offset = find_node_character_offset(node_of_interest);
    }
    
    marker.move('character', char_offset);
    rng.setEndPoint(which + 'ToEnd', marker);
}

Util.Range.set_start = function set_range_start(rng, start, offset)
{
    if (rng.setStart) {
        // W3C range
        rng.setStart(start, offset);
    } else if (rng.setEndPoint) {
        // IE text range
        Util.Range._ie_set_endpoint(rng, 'Start', start, offset);
    } else {
        throw new Util.Unsupported_Error('setting the start of a range');
    }
};

Util.Range.set_end = function set_range_end(rng, end, offset)
{
    if (rng.setEnd) {
        // W3C range
        rng.setEnd(end, offset);
    } else if (rng.setEndPoint) {
        // IE text range
        Util.Range._ie_set_endpoint(rng, 'End', end, offset);
    } else {
        throw new Util.Unsupported_Error('setting the end of a range');
    }
};

Util.Range.set_start_before = function set_range_start_before(rng, node)
{
    if (rng.setStartBefore) {
        // W3C range
        rng.setStartBefore(node);
    } else {
        // Fake it
        Util.Range.set_start(node.parentNode, Util.Node.get_offset(node));
    }
}

Util.Range.set_start_after = function set_range_start_after(rng, node)
{
    if (rng.setStartAfter) {
        // W3C range
        rng.setStartAfter(node);
    } else {
        // Fake it
        Util.Range.set_start(node.parentNode, Util.Node.get_offset(node) + 1);
    }
}

Util.Range.set_end_before = function set_range_end_before(rng, node)
{
    if (rng.setEndBefore) {
        // W3C range
        rng.setEndBefore(node);
    } else {
        // Fake it
        Util.Range.set_end(node.parentNode, Util.Node.get_offset(node));
    }
}

Util.Range.set_end_after = function set_range_end_after(rng, node)
{
    if (rng.setEndAfter) {
        // W3C range
        rng.setEndAfter(node);
    } else {
        // Fake it
        Util.Range.set_end(node.parentNode, Util.Node.get_offset(node) + 1);
    }

// file Util.Request.js
/**
 * @class  Asynchronus HTTP requests (an XMLHttpRequest wrapper).
 *         Deprecates Util.HTTP_Reader.
 * @author Eric Naeseth
 */
Util.Request = function(url, options)
{
    var self = this;
    var timeout = null;
    var timed_out = false;
    
    this.options = options || {};
        
    for (var option in Util.Request.Default_Options) {
        if (!this.options[option])
            this.options[option] = Util.Request.Default_Options[option];
    }
    
    function create_transport()
    {
        try {
            return new XMLHttpRequest();
        } catch (e) {
            try {
                return new ActiveXObject('Msxml2.XMLHTTP');
            } catch (f) {
                try {
                    return new ActiveXObject('Microsoft.XMLHTTP');
                } catch (g) {
                    throw new Util.Unsupported_Error('XMLHttpRequest');
                }
            }
        }
    }
    
    var empty = Util.Function.empty;
    
    function ready_state_changed()
    {
        var state = self.transport.readyState;
        var name = Util.Request.Events[state];
        
        (self.options['on_' + state] || empty)(self, self.transport);
        
        if (name == 'complete')
            completed();
    }
    
    function completed()
    {
        if (timeout) {
            timeout.cancel();
            timeout = null;
        }
        
        (self.options['on_'] + self.get_status()
            || self.options['on_' + (self.succeeded() ? 'success' : 'failure')]
            || empty)(self, self.transport);
        self.transport.onreadystatechange = empty;
    }
    
    function internal_abort(send_notification)
    {
        this.transport.onreadystatechange = empty;
        
        try {
            if (send_notificiation) {
                try {
                    (this.options.on_abort || empty)(this, this.transport);
                } catch (handler_exception) {
                    // ignore
                }
            }
            
            this.transport.abort();
        } catch (e) {
            // do nothing
        }
    }
    
    this.get_status = function()
    {
        try {
            return this.transport.status || 0;
        } catch (e) {
            return 0;
        }
    }
    
    this.get_status_text = function()
    {
        try {
            return (timed_out)
                ? 'Operation timed out.'
                : (this.transport.statusText || '');
        } catch (e) {
            return '';
        }
    }
    
    this.get_header = function(name)
    {
        try {
            return this.transport.getResponseHeader(name);
        } catch (e) {
            return null;
        }
    }
    
    this.succeeded = function()
    {
        var status = this.get_status();
        return !status || (status >= 200 && status < 300);
    }
    
    this.abort = function()
    {
        internal_abort.call(this, true);
    }
    
    timed_out = false;
    
    if (this.options.timeout) {
        timeout = Util.Scheduler.delay(function() {
            internal_abort.call(this, false);
            (this.options.on_timeout || this.options.on_failure || empty)
                (this, this.transport);
        }.bind(this), this.options.timeout);
    }
    
    this.transport = create_transport();
    this.url = url;
    this.method = this.options.method;
    this.transport.onreadystatechange = ready_state_changed;
    
    try {
        this.transport.open(this.method.toUpperCase(), this.url,
            this.options.asynchronus);
        if (this.options.headers) {
            Util.Object.enumerate(this.options.headers, function(k, v) {
                this.transport.setRequestHeader(k, v);
            }, this);
        }
        this.transport.send(this.options.body || null);
    } catch (e) {
        if (timeout) {
            timeout.cancel();
            timeout = null;
        }
        
        throw e;
    }
    
};

Util.Request.Default_Options = {
    method: 'post',
    asynchronus: true,
    content_type: 'application/x-www-form-urlencoded',
    encoding: 'UTF-8',
    parameters: '',
    timeout: null
};

Util.Request.Events =
    ['uninitialized', 'ready', 'send', 'interactive', 'complete'];

// file Util.Select.js
/**
 * @constructor Nothing
 *
 * @class Represents an HTML select element. Example usage:
 *
 *  var s = new Util.Select({ document : document, loading_str : 'Loading now ...', id : 's_id' });
 *  parent_elem.appendChild(s);
 *  s.start_loading();
 *  s.add_option({ key : 'One', value : 'Two', selected : false });
 *  s.add_option({ key : 'Three', value : 'Four', selected : false });
 *  s.add_option({ key : 'Five', value : 'Six', selected : true });
 *  s.end_loading();
 *
 */
Util.Select = function Select(params)
{
    this.document = params.document;
    this._loading_str = params.loading_str != null ? params.loading_str : 'Loading ...';
    this.id = params.id;

    this._options = [];

    // Create select element
    function default_factory() { return this.document.createElement('SELECT'); }
    
    this.select_elem = (params.factory || default_factory)();
    if ( this.id != null )
        this.select_elem.setAttribute('id', this.id);
        
    function create_loading_option()
    {
        var option = this.document.createElement('OPTION');
        option.value = '';
        option.appendChild(this.document.createTextNode(this._loading_str));
        
        return option;
    }

    // Methods

    /**
     * Start loading. This removes all options, hides the actual select
     * element, and shows a fake "loading" one.
     */
    this.start_loading = function()
    {
        // Remove all options
        while ( this.select_elem.firstChild != null )
            this.select_elem.removeChild(this.select_elem.firstChild);
        this._options = [];

        // Add loading option
        this.select_elem.appendChild(create_loading_option());

/*
        // Create loading element
        this._loading_elem = this.select_elem.cloneNode(true);
        var o = this.document.createElement('OPTION');
        o.appendChild(this.document.createTextNode(this._loading_str));
        this._loading_elem.appendChild(o);

        // Hide actual select element
        if ( this.select_elem.parentNode != null )
            this.select_elem.parentNode.replaceChild(this._loading_elem, this.select_elem);
*/
    };

    /**
     * Adds an option. Does not actually append an option element to the select
     * element. (That happens all at once in end_loading.)
     */
    this.add_option = function(value, key, selected)
    {
        this._options.push({k : key, v : value, s : selected});
    };

    /**
     * Ends loading. This actually creates option elements from the added option
     * key-value pairs, hides the fake "loading" select element, and shows the
     * actual select element.
     */
    this.end_loading = function()
    {
        // Create loading element
        this._loading_elem = this.select_elem.cloneNode(true);
        /*var o = this.document.createElement('OPTION');
        o.appendChild(this.document.createTextNode(this._loading_str));
        this._loading_elem.appendChild(o);*/

        // Hide actual select element
        if ( this.select_elem.parentNode != null )
            this.select_elem.parentNode.replaceChild(this._loading_elem, this.select_elem);


        // Remove all options
        while ( this.select_elem.firstChild != null )
            this.select_elem.removeChild(this.select_elem.firstChild);

        // Add options
        for ( var i = 0; i < this._options.length; i++ )
        {
            var o = this.document.createElement('OPTION');
            o.appendChild(this.document.createTextNode(this._options[i].v));
            o.value = this._options[i].k;
            this.select_elem.appendChild(o);
            o.selected = this._options[i].s;
        }
        /* // Doesn't work in IE:
        var html = '';
        for ( var i = 0; i < this._options.length; i++ )
        {
            var sel = this._options[i].s ? ' selected="selected"' : '';
            html += '<option value="' + this._options[i].k + '"' + sel + '>' + this._options[i].v + '</option>';
        }
        this.select_elem.innerHTML = html;
        */
        this._options = [];


        // Show actual select element
        if ( this._loading_elem.parentNode != null )
            this._loading_elem.parentNode.replaceChild(this.select_elem, this._loading_elem);
    };
};

Util.Select.append_options = function append_options_to_select(el, options)
{
    function add_option(desc) {
        var opt = Util.Document.create_element(el.ownerDocument, 'option',
            {value: desc.v}, [desc.l]);
        el.appendChild(opt);
    }
    
    options.each(add_option);
}

// file Util.Selection.js
Util.Selection = function()
{
};

Util.Selection.CONTROL_TYPE = 1;
Util.Selection.TEXT_TYPE = 2;

/**
 * Gets the current selection in the given window.
 *
 * @param    window_obj    the window object whose selection is desired
 * @return                the current selection
 */
Util.Selection.get_selection = function get_window_selection(window_obj)
{
    if (!Util.is_valid_object(window_obj)) {
        throw new TypeError('Must pass an object to get_selection().');
    }
    
    if (typeof(window_obj.getSelection) == 'function') {
        return window_obj.getSelection();
    } else if (window_obj.document.selection) {
        return window_obj.document.selection;
    } else {
        throw new Util.Unsupported_Error('getting a window's selection');
    }
};

/**
 * Inserts a node at the current selection. The original contents of
 * the selection are is removed. A text node is split if needed.
 *
 * @param    sel                the selection
 * @param    new_node        the node to insert
 */
Util.Selection.paste_node = function paste_node_at_selection(sel, new_node)
{
    // Remember node or last child of node, for selection manipulation below
    if ( new_node.nodeType == Util.Node.DOCUMENT_FRAGMENT_NODE )
        var selectandum = new_node.lastChild;
    else
        var selectandum = new_node;

    // Actually paste the node
    var rng = Util.Range.create_range(sel);
    Util.Range.delete_contents(rng);
    //sel = Util.Selection.get_selection(self._loki.window);
    rng = Util.Range.create_range(sel);
    Util.Range.insert_node(rng, new_node);

    // IE
    if ( Util.Browser.IE )
    {
        rng.collapse(false);
        rng.select();
    }
    // In Gecko, move selection after node
    {
        // Select all first, to avoid the annoying Gecko
        // quasi-random highlighting bug
        try // in case document isn't editable
        {
            selectandum.ownerDocument.execCommand('selectall', false, null);
            Util.Selection.collapse(sel, true); // to beg
        } catch(e) {}

        // Move the cursor where we want it
        Util.Selection.select_node(sel, selectandum); // works
        Util.Selection.collapse(sel, false); // to end
    }
};

/**
 * Removes all ranges from the given selection.
 *
 * @param    sel        the selection
 */
Util.Selection.remove_all_ranges = function clear_selection(sel)
{
    if (sel.removeAllRanges) {
        // Mozilla
        sel.removeAllRanges();
    } else if (sel.empty && !Util.is_boolean(sel.empty)) {
        sel.empty();
    } else {
        throw new Util.Unsupported_Error('clearing a selection');
    }
};

/**
 * Sets the selection to be the current range
 */
Util.Selection.select_range = function select_range(sel, rng)
{
    if (!Util.is_valid_object(sel)) {
        throw new TypeError('A selection must be provided to select_range().');
    } else if (!Util.is_valid_object(rng)) {
        throw new TypeError('A range must be provided to select_range().');
    }
    
    if (Util.is_function(sel.addRange, sel.removeAllRanges)) {
        sel.removeAllRanges();
        sel.addRange(rng);
    } else if (rng.select) {
        rng.select();
    } else {
        throw new Util.Unsupported_Error('selecting a range');
    }
};

/**
 * Selects the given node.
 */
Util.Selection.select_node = function(sel, node)
{
    // Mozilla
    try
    {
        // Select all first, to avoid the annoying Gecko
        // quasi-random highlighting bug
        try // in case document isn't editable
        {
            node.ownerDocument.execCommand('selectall', false, null);
            Util.Selection.collapse(sel, true); // to beg
        } catch(e) {}

        var rng = Util.Range.create_range(sel);
        rng.selectNode(node);
    }
    catch(e)
    {
        // IE
        try
        {
            mb('Util.Selection.select_node: in IE chunk: node', node);
            // This definitely won't work in most cases:
            /*
            if ( node.createTextRange != null )
                var rng = node.createTextRange();
            else if ( node.ownerDocument.body.createControlRange != null )
                var rng = node.ownerDocument.body.createControlRange();
            else
                throw('Util.Selection.select_node: node has neither createTextRange() nor createControlRange().');
            */

            /*
            try
            {
                var rng = node.createTextRange();
            }
            catch(g)
            {
                var rng = node.createControlRange();
            }
            */
            rng.select();
        }
        catch(f)
        {
            throw(new Error('Util.Selection.select_node: Neither the Gecko nor the IE way of selecting the node worked. ' +
                            'When the Gecko way was tried, an error with the following message was thrown: <<' + e.message + '>>. ' +
                            'When the IE way was tried, an error with the following message was thrown: <<' + f.message + '>>.'));
        }
    }
};


/**
 * Selects the contents of the given node. See 
 * Util.Range.select_node_contents for more information.
 */
Util.Selection.select_node_contents = function(sel, node)
{
    var range;
    try {
        range = Util.Range.create_range(sel);
    } catch (e) {
        if (e.name == 'Util.Unsupported_Error' && /collapsed/.test(e.message))
            range = Util.Document.create_range(node.ownerDocument);
        else
            throw e;
    }
    
    Util.Range.select_node_contents(range, node);
    Util.Selection.select_range(sel, range);
};

/**
 * Collapses the given selection.
 *
 * @param    to_start    boolean: true for start, false for end
 */
Util.Selection.collapse = function(sel, to_start)
{
    // Gecko
    try
    {
        if ( to_start )
            sel.collapseToStart();
        else
            sel.collapseToEnd();
    }
    catch(e)
    {
        // IE
        try
        {
            var rng = Util.Range.create_range(sel);
            if ( rng.collapse != null )
            {
                rng.collapse(to_start);
                rng.select();
            }
            // else it's a controlRange, for which collapsing doesn't make sense (?)
        }
        catch(f)
        {
            throw(new Error('Util.Selection.collapse: Neither the Gecko nor the IE way of collapsing the selection worked. ' +
                            'When the Gecko way was tried, an error with the following message was thrown: <<' + e.message + '>>. ' +
                            'When the IE way was tried, an error with the following message was thrown: <<' + f.message + '>>.'));
        }

    }
};

/**
 * Returns whether the given selection is collapsed.
 */
Util.Selection.is_collapsed = function selection_is_collapsed(sel)
{
    if (!Util.is_undefined(sel.isCollapsed))
        return sel.isCollapsed;
        
    if (sel.anchorNode && sel.focusNode) {
        return (sel.anchorNode == sel.focusNode &&
            sel.anchorOffset == sel.focusOffset);
    }
    
    var rng;
    
    try {
        rng = Util.Range.create_range(sel);
    } catch (e) {
        if (e.code == 1)
            return true;
        else
            throw e;
    }
    
    return Util.Range.is_collapsed(rng);
};

/**
 * Creates a bookmark for the current selection: a representation of the state
 * of the selection from which that state can be restored.
 *
 * The returned object should be treated as opaque except for one method:
 * restore(), which reselects whatever was selected when the bookmark was
 * created.
 *
 * @param {Window}    window    the window object
 * @param {Selection} sel    a window selection
 * @param {Range} [rng]    the selected range, if already known
 * @return {object} a bookmark object with a restore() method
 *
 * Algorithm from TinyMCE.
 */
Util.Selection.bookmark = function create_selection_bookmark(window, sel, rng)
{
    if (!rng) {
        // Create the range from the selection if one was not provided.
        // The range should be provided by Loki due to the quirk of Safari
        // explained in the function listen_for_context_changes within UI.Loki.
        
        rng = Util.Range.create_range(sel);
    }
    
    var doc = Util.Selection.get_document(sel, rng);
    var dim = Util.Document.get_dimensions(doc);
    var elem;
    var i;
    var other_range;
    
    if (doc != window.document) {
        throw new Error('The selection and window are for different ' +
            'documents.');
    }
    
    var pos = {
        x: dim.scroll.left,
        y: dim.scroll.top
    }
    
    // Try the native Windows IE text range implementation. This branch was not
    // in the original TinyMCE code.
    if (rng.getBookmark) {
        try {
            var mark_id = rng.getBookmark();
            return {
                range: rng,
                id: mark_id,
                
                restore: function restore_native_ie_bookmark()
                {
                    this.range.moveToBookmark(this.id);
                }
            }
        } catch (e) {
            // Ignore the error and try the other methods.
        }
    }
    
    if (sel.addRange && doc.createRange && doc.createTreeWalker) {
        // W3C Traversal and Range, and Mozilla (et al.) selections
        
        // Returns a bookmark object that only re-scrolls to the marked position
        function position_only_bookmark(position)
        {
            return {
                window: window,
                pos: position,
                
                restore: function restore_position_only_bookmark()
                {
                    if (typeof(console) == 'object') {
                        var message = 'Position-only bookmark used.';
                        
                        if (console.warn)
                            console.warn(message);
                        else if (console.log)
                            console.log(message);
                    }
                    
                    this.window.scrollTo(this.pos.x, this.pos.y);
                }
            }
        }
        
        // Gets the currently selected element or the common ancestor element
        // for the selection's start and end. Taken directly from TinyMCE; I
        // don't understand all of what it's doing.
        function get_node()
        {
            var elem = rng.commonAncestorContainer;
            
            // Handle selection of an image or another control-like element
            // (e.g. an anchor).
            if (!rng.collapsed) {
                var wk = Util.Browser.WebKit;
                var same_container = (rng.startContainer == rng.endContainer ||
                    (wk && rng.startContainer == rng.endContainer.parentNode));
                if (same_container) {
                    if (wk || rng.startOffset - rng.endOffset < 2) {
                        if (rng.startContainer.hasChildNodes()) {
                            elem =
                                rng.startContainer.childNodes[rng.startOffset];
                        }
                            
                    }
                }
            }
            
            while (elem) {
                if (elem.nodeType == Util.Node.ELEMENT_NODE)
                    return elem;
                elem = elem.parentNode;
            }
            
            return null;
        }
        
        // Image selection
        elem = get_node();
        if (elem && elem.nodeName == 'IMG') {
            // TinyMCE does this, though I don't know why.
            return position_only_bookmark(pos);
        }
        
        // Determines the textual position of a range relative to the body,
        // given the range's relevant start and end nodes. Only gives an answer
        // if start and end are both text nodes.
        function get_textual_position(start, end)
        {
            var bounds = {start: undefined, end: undefined};
            var walker = document.createTreeWalker(doc.body,
                NodeFilter.SHOW_TEXT, null, false);
            // Note that the walker will only retrieve text nodes.
            
            for (var p = 0, n = walker.nextNode(); n; n = walker.nextNode()) {
                if (n == start) {
                    // Found the starting node in the tree under the root.
                    // Store the position at which it was found.
                    bounds.start = p;
                }
                
                if (n == end) { // not "else if" in case start == end.
                    // Found the ending node in the tree under the root.
                    // Store the position at which it was found and return the
                    // boundaries.
                    bounds.end = p;
                    return bounds;
                }
                
                if (n.nodeValue)
                    p += n.nodeValue.length;
            }
            
            return null; // Never did find the end node. Eek.
        }
        
        var bounds, start, end;
        if (Util.Selection.is_collapsed(sel)) {
            bounds = get_textual_position(sel.anchorNode, sel.focusNode);
            if (!bounds) {
                return position_only_bookmark(pos);
            }
            
            bounds.start += sel.anchorOffset;
            bounds.end += sel.focusOffset;
        } else {
            bounds = get_textual_position(rng.startContainer, rng.endContainer);
            if (!bounds) {
                return position_only_bookmark(pos);
            }
            
            bounds.start += rng.startOffset;
            bounds.end += rng.endOffset;
        }
        
        return {
            selection: sel,
            window: window,
            document: doc,
            body: doc.body,
            pos: pos,
            start: bounds.start,
            end: bounds.end,
            
            restore: function restore_w3c_bookmark()
            {
                var walker = this.document.createTreeWalker(this.body,
                    NodeFilter.SHOW_TEXT, null, false);
                var bounds = {};
                var pos = 0;
                
                window.scrollTo(this.pos.x, this.pos.y);
                
                while (n = walker.nextNode()) { // assignment intentional
                    if (n.nodeValue)
                        pos += n.nodeValue.length;
                    
                    if (pos >= this.start && !bounds.startNode) {
                        // This is the first time we've reached our marked
                        // starting position. Record the starting node and
                        // offset.
                        bounds.startNode = n;
                        bounds.startOffset = this.start -
                            (pos - n.nodeValue.length);
                    }
                    
                    if (pos >= this.end) { // not "else if" in case start == end
                        // We've reached our ending position. Record the ending
                        // node and offset and stop the search.
                        bounds.endNode = n;
                        bounds.endOffset = this.end -
                            (pos - n.nodeValue.length);
                        
                        break;
                    }
                }
                
                if (!bounds.endNode)
                    return;
                
                var range = this.document.createRange();
                range.setStart(bounds.startNode, bounds.setOffset);
                range.setEnd(bounds.endNode, bounds.endOffset);
                
                this.selection.removeAllRanges();
                this.selection.addRange(range);
                
                if (!Util.Browser.Opera) // ???
                    this.window.focus();
            }
        };
    } else if (rng.length && rng.item) {
        // Internet Explorer control range.
        
        elem = rng.item(0);
        
        // Find the index of the element in the NodeList of elements with its
        // tag name. I'm not sure why this is being done (perhaps it keeps the
        // selected Node object from being retained?), or if it works properly,
        // but I'm just porting the TinyMCE implementation.
        function get_element_index(elem)
        {
            var elements = doc.getElementsByTagName(elem.nodeName);
            for (var i = 0; i < elements.length; i++) {
                if (elements[i] == n)
                    return i;
            }
        }
        
        i = get_element_index(elem);
        if (Util.is_blank(i)) {
            throw new Error('Cannot create bookmark; the selected element ' +
                'cannot be found in the editing document.');
        }
        
        return {
            window: window,
            tag: e.nodeName,
            index: i,
            pos: pos,
            
            restore: function restore_ie_control_range_bookmark()
            {
                var rng = doc.body.createControlRange();
                var elements = doc.getElementsByTagName(this.tag);
                var el = elements[this.index];
                if (!el) {
                    throw new Error('Could not retrieve the bookmark target.');
                }
                
                this.window.scrollTo(this.pos.x, this.pos.y);
                rng.addElement(el);
                rng.select();
            }
        };
    } else if (!Util.is_blank(rng.length) && rng.moveToElementText) {
        // Internet Explorer text range
        
        // Figure out the position of the range. We do this in a slightly crude
        // way, by attempting to move the range backwards by a large number of
        // characters and seeing how many characters we actually moved.
        function find_relative_position(range, collapse_to_start)
        {
            range.collapse(collapse_to_start);
            // TextRange.move() returns the number of units actually moved
            return Math.abs(range.move('character', -0xFFFFFF));
        }
        
        // Establish a baseline by finding the position of the body.
        other_range = doc.body.createTextRange();
        other_range.moveToElementText(doc.body);
        var body_pos = find_relative_position(other_range, true);
        
        // Find how far the start side of the selection is from the selection's
        // base.
        other_range = rng.duplicate();
        var start_pos = find_relative_position(other_range, true);
        
        // Find the length of the range by finding how far the end side is
        // from the base and subtracting the start position from it.
        other_range = rng.duplicate();
        var length = find_relative_position(other_range, false) - start_pos;
        
        return {
            window: window,
            body: doc.body,
            start: start_pos - body_pos, // start pos. of range relative to body
            length: length,
            pos: pos,
            
            restore: function restore_ie_text_range_bookmark()
            {
                // Sanity check
                if (b.start < 0) {
                    throw new Error('Invalid bookmark: starting point is ' +
                        'negative.');
                }
                
                this.window.scrollTo(this.pos.x, this.pos.y);
                
                // Create a new range that we can select.
                var range = this.body.createTextRange();
                range.moveToElementText(this.body);
                range.collapse(true); // collapse to beginning of body
                
                // The move methods are relative, so we first move the range's
                // start forward to the bookmarked start position.
                range.moveStart('character', b.start);
                
                // In doing so, we also moved the end position forward by the
                // same amount (because you can't have a range's end occur
                // before its start). Now all we have to do is move the end of
                // the range forward by the bookmarked length.
                range.moveEnd('character', b.length);
                
                // Done!
                range.select();
            }
        };
    } else {
        throw new Util.Unsupported_Error('bookmarking a selection');
    }
};

/**
 * Gets the selection's owner document.
 * @param {Selection}    sel 
 * @param {Range}    rng    the selected range, if already known
 * @return {Document}
 */
Util.Selection.get_document = function get_selection_document(sel, rng)
{
    if (!rng) {
        // Create the range from the selection if one was not provided.
        // The range should be provided by Loki due to the quirk of Safari
        // explained in the function listen_for_context_changes within UI.Loki.
        
        rng = Util.Range.create_range(sel);
    }
    
    var elem = (sel.anchorNode // Mozilla (and friends) selection object
        || rng.startContainer // W3C Range
        || (rng.parentElement && rng.parentElement())); // IE TextRange
        
    if (!elem) {
        throw new Util.Unsupported_Error("getting a selection's owner " +
            "document");
    }
    
    return elem.ownerDocument;
}

/**
 * Returns the selected element, if any. Otherwise returns null.
 * Imitates FCK code.
 */
Util.Selection.get_selected_element = function(sel)
{
    if ( Util.Selection.get_selection_type(sel) == Util.Selection.CONTROL_TYPE )
    {
        // Gecko
        if ( sel.anchorNode != null && sel.anchorOffset != null )
        {
            return sel.anchorNode.childNodes[sel.anchorOffset];
        }
        // IE
        else
        {
            var rng = Util.Range.create_range(sel);
            if ( rng != null && rng.item != null )
                return rng.item(0);
        }
    }
};

/**
 * Gets the type of currently selection.
 * Imitates FCK code.
 */
Util.Selection.get_selection_type = function(sel)
{
    var type;

    // IE
    if ( sel.type != null )
    {
        if ( sel.type == 'Control' )
            type = Util.Selection.CONTROL_TYPE;
        else
            type = Util.Selection.TEXT_TYPE;
    }

    // Gecko
    else
    {
        type = Util.Selection.TEXT_TYPE;

        if ( sel.rangeCount == 1 )
        {
            var rng = sel.getRangeAt(0);
            if ( rng.startContainer == rng.endContainer && ( rng.endOffset - rng.startOffset ) == 1 )
            {
                type = Util.Selection.CONTROL_TYPE;
            }
        }
    }

    return type;
};

/**
 * Moves the cursor to the end (but still inside) the given
 * node. This is useful to call after performing operations 
 * on nodes.
 */
Util.Selection.move_cursor_to_end = function(sel, node)
{
    // Move cursor
    var rightmost = Util.Node.get_rightmost_descendent(node);
    if ( rightmost.nodeName == 'BR' && rightmost.previousSibling != null )
        rightmost = Util.Node.get_rightmost_descendent(rightmost.previousSibling);
    mb('rightmost', rightmost);

    // XXX This doesn't really work right in IE, although it is close
    // enough for now
    if ( rightmost.nodeType == Util.Node.TEXT_NODE )
        Util.Selection.select_node(sel, rightmost);
    else
        Util.Selection.select_node_contents(sel, rightmost);

    Util.Selection.collapse(sel, false); // to end
};

// file Util.State_Machine.js
/**
 * @constructor Creates a new state machine.
 * @class A "state machine"; an organized way of tracking discrete software states.
 * @author Eric Naeseth
 */
Util.State_Machine = function(states, starting_state, name)
{
    this.states = states || {};
    // I have no idea why this helps keep the machine in sync, but it does:
    this.state = {
        real_state: null,
        
        get: function()
        {
            return this.real_state;
        },
        
        set: function(new_state)
        {
            this.real_state = new_state;
        }
    };
    this.name = name || null;
    this.changing = false;
    this.lock = new Util.Lock(this.name);
    
    this.determine_name = function(state)
    {
        if (!state)
            return '[null]';
        
        for (var name in this.states) {
            if (this.states[name] == state)
                return name;
        }
        
        return '[unknown]';
    }
    
    this.change = function(new_state)
    {
        if (typeof(new_state) == 'string') {
            if (!this.states[new_state])
                throw new Util.State_Machine.Error('Unknown state "' + new_state + '".');
            new_state = this.states[new_state];
        }
        
        this.lock.acquire();
        try {
            var old_state = this.state.get();

            if (old_state) {
                old_state.exit(new_state);
            }

            this.state.set(new_state);
            new_state.enter(old_state);
        } finally {
            this.lock.release();
        }
    }
    
    var machine = this;
    for (var name in this.states) {
        var s = this.states[name];
        
        s.enter = (function(old_entry) {
            return function state_entry_wrapper() {
                if (arguments.length == 0)
                    return machine.change(this);
                return old_entry.apply(this, arguments);
            }
        })(s.enter);
        
        s.machine = this;
    }
    
    if (starting_state)
        this.change(starting_state);
}

Util.State_Machine.Error = function(message)
{
    Util.OOP.inherits(this, Error, message);
    this.name = 'Util.State_Machine.Error';

// file Util.Tabset.js
/**
 * Creates a chunk containing a tabset.
 * @constructor
 *
 * @param    params    an object with the following properties:
 *                  <ul>
 *                  <li>document - the DOM document object which will own the created DOM elements</li>
 *                  <li>id - (optional) the id of the DOM tabset element</li>
 *                  </ul>
 *
 * @class Represents a tabset.
 */
Util.Tabset = function(params)
{
    var self = this;
    this.document = params.document;
    this.id = params.id;

    var _tabs = {}; // each member of tabs should have a tab_elem and a tabpanel_elem 
    var _name_of_selected_tab;
    var _select_listeners = [];

    // Create tabset element
    this.tabset_elem = this.document.createElement('DIV');
    Util.Element.add_class(this.tabset_elem, 'tabset');
    if ( this.id != null )
        this.tabset_elem.setAttribute('id', this.id);

    // Create tabs container
    var _tabs_chunk = this.document.createElement('DIV');
    Util.Element.add_class(_tabs_chunk, 'tabs_chunk');

    // Create and append force_clear_for_ie element
    var _force_clear_for_ie_elem = this.document.createElement('DIV');
    Util.Element.add_class(_force_clear_for_ie_elem, 'force_clear_for_ie');
    _tabs_chunk.appendChild(_force_clear_for_ie_elem);

    // Create and append tabs ul
    var _tabs_ul = this.document.createElement('UL');
    _tabs_chunk.appendChild(_tabs_ul);

    // Create tabpanels container
    var _tabpanels_chunk = this.document.createElement('DIV');
    Util.Element.add_class(_tabpanels_chunk, 'tabpanels_chunk');

    // Append containers to tabset
    this.tabset_elem.appendChild(_tabs_chunk);
    this.tabset_elem.appendChild(_tabpanels_chunk);


    // Methods

    /**
     * Adds a tab to the tabset.
     *
     * @param    name    the new tab's name
     * @param    label    the new tab's label
     */
    this.add_tab = function(name, label)
    {
        // Make entry in list of tabs
        _tabs[name] = {};
        var t = _tabs[name];

        // Create tab element ...
        t.tab_elem = this.document.createElement('LI');
        t.tab_elem.id = t.tab_id = name + '_tab';
        Util.Element.add_class(t.tab_elem, 'tab_chunk');

        // ... and its anchor ...
        var anchor_elem = this.document.createElement('A');
        anchor_elem.href = 'javascript:void(0);';
        t.tab_elem.appendChild(anchor_elem);

        // ... and its label ...
        var label_node = this.document.createTextNode(label);
        anchor_elem.appendChild(label_node);

        // ... with event listeners
        Util.Event.add_event_listener(anchor_elem, 'click', function() { self.select_tab(name); });
        Util.Event.add_event_listener(t.tab_elem, 'mouseover', function() { Util.Element.add_class(t.tab_elem, 'hover'); });
        Util.Event.add_event_listener(t.tab_elem, 'mouseout', function() { Util.Element.remove_class(t.tab_elem, 'hover'); });

        // Create tabpanel element
        t.tabpanel_elem = this.document.createElement('DIV');
        t.tabpanel_elem.id = t.tabpanel_id = name + '_tabpanel';
        Util.Element.add_class(t.tabpanel_elem, 'tabpanel_chunk');

        // Append tab and tabpanel elements
        _tabs_ul.appendChild(t.tab_elem);
        _tabpanels_chunk.appendChild(t.tabpanel_elem);

        // If this is the first tab to be added, select it
        // by default
        if ( _name_of_selected_tab == null )
        {
            this.select_tab(name);
        }
        // Otherwise, re-select the selected tab, in order
        // to refresh the the display
        else
        {
            this.select_tab(this.get_name_of_selected_tab());
        }
    };

    /**
     * Gets the element of the tabpanel whose
     * name is given. Then children can be 
     * appended there.
     *
     * @param    name    the tabpanel's name
     */
    this.get_tabpanel_elem = function(name)
    {
        if ( _tabs[name] == null )
            throw('Util.Tabset.get_tabpanel_elem: no such name.');

        return _tabs[name].tabpanel_elem;
    };

    /**
     * Selects the tab whose name is given.
     *
     * @param    name    the tabpanel's name
     */
    this.select_tab = function(name)
    {
        if ( _tabs[name] == null )
            throw('Util.Tabset.select_tab: no such name.');

        var old_name = _name_of_selected_tab;

        // Hide all tabs and tabpanels
        for ( var i in _tabs )
        {
            Util.Element.remove_class(_tabs[i].tab_elem, 'selected');
            Util.Element.remove_class(_tabs[i].tabpanel_elem, 'selected');
        }

        // Show selected tab and tabpanel
        Util.Element.add_class(_tabs[name].tab_elem, 'selected');
        Util.Element.add_class(_tabs[name].tabpanel_elem, 'selected');

        // Remember name
        _name_of_selected_tab = name;

        // Fire listeners
        for ( var i = 0; i < _select_listeners.length; i++ )
            _select_listeners[i](old_name, _name_of_selected_tab);
    };

    /**
     * Gets the name of the currently selected tab. 
     */
    this.get_name_of_selected_tab = function()
    {
        if ( _name_of_selected_tab == null )
            throw('Util.Tabset.get_name_of_selected_tab: no tab selected.');

        return _name_of_selected_tab;
    };

    /**
     * Adds a listener to be fired whenever a different tab is selected. 
     * Each listener will receive old_name and new_name as arguments.
     */
    this.add_select_listener = function(listener)
    {
        _select_listeners.push(listener);
    };
};

// file Util.URI.js
/**
 * Does nothing.
 *
 * @class Container for functions relating to URIs.
 */
Util.URI = function()
{
    throw new Error("Util.URI objects may not be constructed.");
};

/**
 * Determines whether or not two URI's are equal.
 *
 * Special handling that this function performs:
 *    - Does not distinguish between http and https.
 *     - Domain-relative links are assumed to be relative to the current domain.
 * @param {string|object}
 * @param {string|object}
 * @return {boolean}
 */
Util.URI.equal = function uri_equal(a, b)
{
    var normalize = Util.URI.normalize;
    
    a = normalize(a);
    b = normalize(b);
    
    if (!Util.Object.equal(this.parse_query(a.query), this.parse_query(b.query)))
        return false;
    
    return (a.scheme == b.scheme && a.host == b.host && a.port == b.port &&
        a.user == b.user && a.password == b.password && a.path == b.path &&
        a.fragment == b.fragment);
}

/**
 * Parses a URI into its constituent parts.
 */
Util.URI.parse = function parse_uri(uri)
{
    var match = Util.URI.uri_pattern.exec(uri);
    
    if (!match) {
        throw new Error('Invalid URI: "' + uri + '".');
    }
    
    var authority_match = (typeof(match[4]) == 'string' && match[4].length)
        ? Util.URI.authority_pattern.exec(match[4])
        : [];
    
    // this wouldn't need to be so convoluted if JScript weren't so crappy!
    function get_match(source, index)
    {
        try {
            if (typeof(source[index]) == 'string' && source[index].length) {
                return source[index];
            }
        } catch (e) {
            // ignore and return null below
        }
        
        return null;
    }
    
    var port = get_match(authority_match, 7);
    var host = get_match(authority_match, 5);
    
    return {
        scheme: get_match(match, 2),
        authority: get_match(match, 4),
        user: get_match(authority_match, 2),
        password: get_match(authority_match, 4),
        host: host,
        port: (port ? Number(port) : port),
        path: get_match(match, 5) || (host ? '/' : null),
        query: get_match(match, 7),
        fragment: get_match(match, 9)
    };
}

/**
 * Checks to see if a URI is a URN (such as a mailto:) address.
 */
Util.URI.is_urn = function uri_is_urn(uri) {
    if (typeof(uri) != 'object')
        uri = Util.URI.parse(uri);
    
    return (uri.scheme && uri.path && !uri.authority);
}

/**
 * Parses a query fragment into its constituent variables.
 */
Util.URI.parse_query = function parse_query(fragment)
{
    var vars = {};
    
    if (!fragment)
        return vars;
    
    fragment.replace(/^?/, '').split(/[;&]/).each(function (part) {
        var keyvalue = part.split('='); // we can't simply limit the number of
                                        // splits or we'll use any parts beyond
                                        // the first =
        var key = keyvalue.shift();
        var value = keyvalue.join('='); // undo any damage from the split
        
        vars[key] = value;
    });
    
    return vars;
}

/**
 * Builds a query fragment from an object.
 */
Util.URI.build_query = function build_query(variables)
{
    var parts = [];
    
    Util.Object.enumerate(variables, function(name, value) {
        parts.push(name + '=' + value);
    });
    
    return parts.join('&');
}

/**
 * Builds a URI from a parsed URI object.
 */
Util.URI.build = function build_uri_from_parsed(parsed)
{
    var uri = '';
    if (parsed.scheme)
        uri = parsed.scheme + ':'
    
    if (parsed.authority) {
        uri += '//' + parsed.authority;
    } else if (parsed.host) {
        uri += '//';
        if (parsed.user) {
            uri += parsed.user;
            if (parsed.password)
                uri += ':' + parsed.password;
            uri += '@';
        }
        
        uri += parsed.host;
        if (parsed.port)
            uri += ':' + parsed.port;
    }
    
    if (parsed.path)
        uri += parsed.path;
    if (parsed.query)
        uri += '?' + parsed.query;
    if (parsed.fragment)
        uri += '#' + parsed.fragment;
    
    return uri;
}

/**
 * Safely appends query parameters to an existing URI.
 * Previous occurrences of a query parameter are replaced.
 */
Util.URI.append_to_query = function append_params_to_query(uri, params)
{
    var parsed = Util.URI.parse(uri);
    var query_params = Util.URI.parse_query(parsed.query);
    
    Util.Object.enumerate(params, function(name, value) {
        query_params[name] = value;
    });
    
    parsed.query = Util.URI.build_query(query_params);
    return Util.URI.build(parsed);
}

/**
 * Normalizes a URI, expanding it to an absolute form and removing redundant
 * port information.
 * @param {string|object}    uri    a parsed URI object or a URI string
 * @param {string|object}    [base]    an explicit base URI to use
 * @return {object}    the parsed normalized URI
 */
Util.URI.normalize = function normalize_uri(uri, base)
{
    var path_parts, i;
    
    if (typeof(base) == 'string') {
        base = Util.URI.parse(base);
    } else {
        if (!base)
            base = Util.URI.parse((window.top || window).location);
        else if (Util.is_object(base))
            base = Util.Object.clone(base);
        else if (typeof(base) != 'object' || typeof(base.path) == 'undefined')
            throw new TypeError("Invalid base URI.");
        
        // take the path's basename and add a trailing slash:
        base.path = base.path.split('/').slice(0, -1).join('/') + '/';
    }
    
    if (typeof(uri) != 'string') {
        if (uri.scheme === undefined)
            throw new TypeError("Invalid URI object.");
        uri = Util.Object.clone(uri);
    } else {
        uri = Util.URI.parse(uri);
    }
    
    if (!uri.scheme && uri.scheme != '') {
        uri.scheme = base.scheme;
    } else if (uri.scheme = 'https') {
        if (uri.port == 443)
            uri.port = null;
    }
    
    if (!uri.host)
        uri.host = base.host;
    if (typeof(uri.host) == 'string')
        uri.host = uri.host.toLowerCase();
    
    if (uri.path.charAt(0) != '/' && uri.host == base.host) {
        uri.path = base.path + uri.path;
    }
    
    path_parts = uri.path.split('/');
    uri.path = [];
    for (i = 0; i < path_parts.length; i++) {
        if (path_parts[i] == '.') {
            continue;
        } else if (path_parts[i] == '..') {
            if (uri.path.length <= 1) { // first "/" creates an empty part
                throw new Error('Invalid relative URI: too many parent ' +
                    'directory references (..).');
            }
            uri.path.pop();
        } else {
            uri.path.push(path_parts[i]);
        }
    }
    uri.path = uri.path.join('/');
        
    if (uri.scheme == 'http' && uri.port == 80)
        uri.port = null;
        
    return uri;
}

/**
 * Strips leading "https:" or "http:" from a uri, to avoid warnings about
 * mixing https and http. E.g.: https://apps.carleton.edu/asdf ->
 * //apps.carleton.edu/asdf.
 * 
 * @param    {string}    uri            the uri
 */
Util.URI.strip_https_and_http = function strip_https_and_http(uri)
{
    return (typeof(uri) == 'string')
        ? uri.replace(new RegExp('^https?:', ''), '')
        : null;
};

/**
 * Extracts the domain name from the URI.
 * @param    uri    the URI
 * @return    the domain name or null if an invalid URI was provided
 */
Util.URI.extract_domain = function extract_domain_from_uri(uri)
{
    var match = Util.URI.uri_pattern.exec(uri);
    return (!match || !match[4]) ? null : match[4].toLowerCase();
};

/**
 * Makes the given URI relative to its domain
 * (i.e. strips the protocol and domain).
 */
Util.URI.make_domain_relative = function make_uri_domain_relative(uri)
{
    return uri.replace(Util.URI.protocol_host_pattern, '');
}

Util.URI.uri_pattern =
    new RegExp('^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?',
    'i');
Util.URI.authority_pattern =
    new RegExp('^(([^:@]+)(:([^@]+))?@)?([^:]+)(:(\d+))?$');
Util.URI.protocol_host_pattern =
    new RegExp('^(([^:/?#]+):)?(//([^/?#]*))?', 'i'); 
// file Util.Unsupported_Error.js
/**
 * @class Indicates that an operation is unsupported by the browser.
 * @constructor
 * @param {string}    call
 * @author Eric Naeseth
 */
Util.Unsupported_Error = function UnsupportedError(call)
{
    var error = new Error('No known implementation of ' + call +
        ' is available from this browser.');
    error.name = 'Util.Unsupported_Error';
    return error;

// file Util.Window.js
/**
 * Declares instance variables. <code>this.window</code>,
 * <code>this.document</code>, and <code>this.body</code> are not
 * initialized until the method <code>this.open</code> is called.
 *
 * @constructor
 *
 * @class A wrapper to <code>window</code>. Provides extra and
 * cross-browser functionality.
 */
Util.Window = function()
{
    this.window = null;
    this.document = null;
    this.body = null;
};
Util.Window.FORCE_SYNC = true;
Util.Window.DONT_FORCE_SYNC = false;

/**
 * Opens a window.
 *
 * @param    uri                (optional) the uri of the page to open in the
 *                            window. Defaults to empty string, with the result
 *                            that no page is initially opened in the window.
 *                            But NOTE: if you leave this blank, if this is called 
 *                             from a page under https IE will complain about mixing 
 *                            https and http.
 * @param    window_name        (optional) the name of the window. Defaults to
 *                            '_blank'.
 * @param    window_options    (optional) a string of options as to how the window
 *                          is displayed. This is the same string as is passed
 *                          to window.open. Defaults to a fairly minimal set of
 *                          options.
 * @param    force_async        (optional) if Util.Window.FORCE_ASYNC, forces the 
 *                             function to write over the document at uri with a blank 
 *                             page and close the new document, even if uri isn't ''. This is
 *                            useful if we're behind https, since setting the uri
 *                            to '' from an https page causes IE to warn the user
 *                            about mixing https and http.
 * @return                    returns false if we couldn't open the window (e.g.,
 *                            if it was blocked), or true otherwise
 */
Util.Window.prototype.open = function(uri, window_name, window_options, force_sync)
{
    // Provide defaults for optional arguments
    if ( uri == null )
        uri = '';

    if ( window_name == null )
        window_name = '_blank';

    if ( window_options == null )
        window_options = 'status=1,scrollbars=1,resizable,width=600,height=300';
    
    if ( force_sync == null )
        force_sync = Util.Window.DONT_FORCE_SYNC;

    // Open window
    this.window = window.open(uri, window_name, window_options);

    // Make sure the window opened successfully
    if ( this.window == null )
    {
        alert('I couldn't open a window. Please disable your popup blocker for this page. Then give me another try.');
        return false;
    }

    // Set up reference to window's document
    this.document = this.window.document;

    // By writing the document's initial HTML out ourself and then
    // closing the document (that's the important part), we
    // essentially make the "open" method synchronous rather than
    // asynchronous. And if we're just trying to open an empty window,
    // this is not dangerous. (It might be dangerous otherwise, since
    // a synchronous "open" method that involved a request to the web
    // server might cause the script to effectively hang if the web
    // server didn't respond.)
    //
    // If we are given a URI to request from the web server, we skip
    // this, so the "open" method is asynchronous, so before we do
    // anything with the window's contents, we need to make sure that
    // the content document has loaded. One way to do this is to add a
    // "load" event listener, and then do everything we want to in the
    // listener. Beware, though: this can cause extreme 
    // cross-browser pains.
    if ( uri == '' || force_sync == Util.Window.FORCE_SYNC )
    {
        this.document.write('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">' +
                            '<html><head><title></title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /></head><body>' +
                            '<div id="util_window_error">You tried to reload a dialog page that exists only ephemerally. Please close the dialog and open it again.</div>' +
                            // for debugging; turn off when live (make sure to get the event listener below, too):
                            //'<div><a id="util_window_alert" href="#" onclick="return false;">View virtual source</a></div><hr />' + // the event which pops up the source is added below
                            '<script type="text/javascript">if (window.opener) window.opener._loki_dialog_postback(window);</script>' +
                            '</body></html>');
        this.document.close();

        // We can only set a reference to the body element if the
        // document has finished loading, and here we can only be sure
        // of that across browsers if we've called document.close().
        //
        // One upshot is that if we are given a URI to load in the
        // window, we have to wait until the load event is fired to
        // get a reference to the body tag, and I don't want to muck
        // around with that here. So in that case we just don't get
        // such a reference here. (Notice that the assignment below is
        // still in the if block.) You have to get the reference
        // yourself if you want it.
        this.body = this.document.getElementsByTagName('BODY').item(0);

        // We also add an onclick event to view source which uses
        // Util.Window.alert, not window.alert
        //var self = this;
        //Util.Event.add_event_listener(this.document.getElementById('util_window_alert'), 'click', function() { Util.Window.alert(self.document.getElementsByTagName('html').item(0).innerHTML); });

        // We need the error message because if people do things like
        // press refresh, they just get what's written by
        // document.write above, and that is very confusing. The
        // following line hides the message except after they've
        // pressed reload, because none of this is run on reload.
        this.document.getElementById('util_window_error').style.display = 'none';

//         // for debugging; turn off when live:
//         var a = this.document.createElement('DIV');
//         a.appendChild( this.document.createTextNode('View virtual source') );
//         a.href = '#';
//         var self = this;
//         var handler = function() { Util.Window.alert(self.body.innerHTML); }
//         Util.Event.add_event_listener(a, 'click', function() { handler(); });
//         this.body.appendChild(a);
    }

    return true; // success
};


Util.Window.prototype.add_load_listener = function(listener)
{
        mb('Util.Window.add_load_listener: this', this);
    Util.Event.add_event_listener(this.document, 'load', listener);
};


/**
 * Alerts a message. Supercedes window.alert, since allows scrolling,
 * accepts document nodes rather than just strings, etc.
 *
 * @param    alertandum    the string or document chunk (i.e., node with
 *                      all of its children) to alert
 * @static
 */ 
Util.Window.alert = function(alertandum)
{
    // Open window
    var alert_window = new Util.Window;
    alert_window.open('', '_blank', 'status=1,scrollbars=1,resizable,width=600,height=300');

    // Add the alertatandum to a document chunk
    var doc_chunk = alert_window.document.createElement('DIV'); // use a div because document frags don't work as expected on IE
    if ( typeof(alertandum) == 'string' )
    {
        var text = alertandum.toString();
        var text_arr = text.split("n");
        for ( var i = 0; i < text_arr.length; i++ )
        {
            doc_chunk.appendChild(
                alert_window.document.createElement('DIV')
            ).appendChild(
                alert_window.document.createTextNode(text_arr[i].toString())
            );
        }
    }
    else
    {
        // FIXME: leftover debugging crud
        // alert(alertandum.firstChild.firstChild.firstChild.nodeValue);
        doc_chunk.appendChild(
            Util.Document.import_node(alert_window.document, alertandum, true)
        );
        alert(doc_chunk.firstChild.nodeName);
    }

    // Append the document chunk to the window
    alert_window.body.appendChild(doc_chunk);
};

Util.Window.alert_debug = function(message)
{
    var alert_window = new Util.Window;
    alert_window.open('', '_blank', 'status=1,scrollbars=1,resizable,width=600,height=300');
    
    var text_chunk = alert_window.document.createElement('P');
    text_chunk.style.fontFamily = 'monospace';
    text_chunk.appendChild(alert_window.document.createTextNode(message));
    alert_window.body.appendChild(text_chunk);

// file UI.js
/**
 * Container for objects related to user interface.
 */
function UI()
{
};

// file UI.Activity.js
/**
 * @class Displays an indicator that reassures the user that
 * work of some sort is being done in the background.
 * @author Eric Naeseth
 */
UI.Activity = function(base, document, kind, text) {
    var helper = new Util.Document(document);
    if (base.base_uri) base = base.base_uri;
    
    var kinds = {
        small: function()
        {
            var container = helper.create_element('SPAN', {
                className: 'progress_small'
            }, [helper.create_element('IMG', {src: base + 'images/loading/small.gif'})]);
            
            if (text)
                container.appendChild(document.createTextNode(' ' + text));
            
            return container;
        },
        
        arrows: function()
        {
            var container = helper.create_element('SPAN', {
                className: 'progress_arrows'
            }, [helper.create_element('IMG', {src: base + 'images/loading/arrows.gif'})]);
            
            if (text)
                container.appendChild(document.createTextNode(' ' + text));
            
            return container;
        },
        
        large: function()
        {
            var image = helper.create_element('IMG', {
                src: base + 'images/loading/large.gif'
            });
            var container = helper.create_element('DIV', {
                className: 'progress_large'
            }, [image]);
            
            if (text) {
                container.appendChild(helper.create_element('P', {}, [text]));
            }
            
            return container;
        },
        
        bar: function()
        {
            return helper.create_element('IMG', {
                src: base + 'images/loading/bar.gif'
            });
        },
        
        textual: function()
        {
            var el = helper.create_element('SPAN', {className: 'progress_text'});
            el.innerHTML = text || 'Loading&hellip;';
            return el;
        }
    }
    
    function invalid_type() {
        throw new Error('"' + kind + '" is not a valid kind of activity indicator.');
    }
    
    this.indicator = (kinds[kind] || invalid_type)();
    
    /**
     * Convenience method for appending the indicator as a child of a parent container.
     */
    this.insert = function(container)
    {
        container.appendChild(this.indicator);
    }
    
    /**
     * Convenience method for replacing the indicator with actual content.
     */
    this.replace = function(replacement)
    {
        if (!this.indicator.parentNode)
            return;
        
        this.indicator.parentNode.replaceChild(replacement, this.indicator);
    }
    
    /**
     * Convenience method for removing the indicator.
     */
    this.remove = function()
    {
        if (!this.indicator.parentNode)
            return;
        
        this.indicator.parentNode.removeChild(this.indicator);
    }

// file UI.Align_Helper.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Changes the alignment of block-level elements.
 */
UI.Align_Helper = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Helper);

    this.init = function(loki)
    {
        this._loki = loki;
        this._paragraph_helper = (new UI.Paragraph_Helper()).init(this._loki);
        return this;
    };
    
    function get_alignable_elements()
    {
        var elements;
        var selection;
        var range;
        var bounds;
        
        function find_blocks(scan_ancestors) {
            return Util.Range.find_nodes(bounds, Util.Node.is_block,
                scan_ancestors);
        }
        
        // Ensure that there's a paragraph; that we're not directly within the
        // document's body.
        self._paragraph_helper.possibly_paragraphify();
        
        selection = Util.Selection.get_selection(self._loki.window);
        range = Util.Range.create_range(selection);
        bounds = Util.Range.get_boundary_blocks(range, true);
        
        // First, see if there are any block-level elements within the selected
        // range.
        elements = find_blocks(false);
        if (elements.length)
            return elements;
        
        // Find any that are ancestors of the range.
        return find_blocks(true);
    };

    this.is_alignable = function selection_is_alignable()
    {
        try {
            return !!get_alignable_elements().length;
        } catch (e) {
            return false;
        }
    };
    
    this.align = function align_selection(position)
    {
        var elements = get_alignable_elements();
        
        position = position.toLowerCase();
        if (!['left', 'center', 'right', 'justify'].contains(position)) {
            throw new Error('Invalid position {' + position + '}.');
        }
        
        if (!elements.length)
            return;
        elements.each(function align_element(el) {
            var w = (self._loki.window.document == el.ownerDocument)
                ? self._loki.window
                : Util.Node.get_window(el);
            
            var align = Util.Element.get_computed_style(w, el).textAlign;
            if (align.toLowerCase() == position)
                return;
            
            if (position == 'left') {
                // Try simply removing the inline style, since "left" is
                // probably the default. Check it momentarily, and if the
                // alignment isn't really left, set it explicitly.
                el.style.textAlign = '';
                if (el.style.cssText.length == 0)
                    el.removeAttribute('style');
                (function verify_element_alignment() {
                    var a = Util.Element.get_computed_style(w, el).textAlign;
                    a = a.toLowerCase();
                    // For Mozilla, the default alignment is actually "start",
                    // which is equivalent to left for our purposes.
                    if (a != position && !(position == 'left' && a == 'start'))
                        el.style.textAlign = position;
                }).defer();
            } else {
                el.style.textAlign = position;
            }
        });
    };

    this.align_left = function align_selection_to_left()
    {
        this.align('left');
    };

    this.align_center = function align_selection_to_center()
    {
        this.align('center');
    };

    this.align_right = function align_selection_to_right()
    {
        this.align('right');
    };
};

// file UI.Align_Menugroup.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class representing an align menugroup. 
 */
UI.Align_Menugroup = function()
{
    Util.OOP.inherits(this, UI.Menugroup);

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._align_helper = (new UI.Align_Helper).init(this._loki);
        return this;
    };

    /**
     * Returns an array of menuitems, depending on the current context.
     * May return an empty array.
     */
    this.get_contextual_menuitems = function()
    {
        var menuitems = [];

        var self = this;
        if ( this._align_helper.is_alignable() )
        {
            menuitems.push( (new UI.Menuitem).init({ 
                label : 'Align left',
                //listener : function() { self._loki.exec_command('JustifyLeft'); }
                listener : function() { self._align_helper.align_left(); }
            }) );
            menuitems.push( (new UI.Menuitem).init({ 
                label : 'Align center',
                //listener : function() { self._loki.exec_command('JustifyCenter'); }
                listener : function() { self._align_helper.align_center(); }
            }) );
            menuitems.push( (new UI.Menuitem).init({ 
                label : 'Align right',
                //listener : function() { self._loki.exec_command('JustifyRight'); }
                listener : function() { self._align_helper.align_right(); }
            }) );
        }

        return menuitems;
    };
};

// file UI.Anchor_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class for inserting an anchor.
 */
UI.Anchor_Button = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Button);

    this.image = 'anchor.png';
    this.title = 'Insert named anchor';
    this.click_listener = function() { self._anchor_helper.open_dialog(); };

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._anchor_helper = (new UI.Anchor_Helper).init(this._loki);
        return this;
    };
};

// file UI.Anchor_Dialog.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class An anchor dialog window.
 */
UI.Anchor_Dialog = function()
{
    Util.OOP.inherits(this, UI.Dialog);

    this._dialog_window_width = 615;
    this._dialog_window_height = 200;

    this._set_title = function()
    {
        if ( !this._initially_selected_item )
            this._dialog_window.document.title = 'Insert anchor';
        else
            this._dialog_window.document.title = 'Edit anchor';
    };

    this._populate_main = function()
    {
        this._append_anchor_chunk();
        this._append_submit_and_cancel_chunk();
        this._append_remove_anchor_chunk();
        var self = this;
        setTimeout(function () { self._resize_dialog_window(false, true); }, 1000);
        //this._resize_dialog_window(false, true);
    };

    this._append_anchor_chunk = function()
    {
        this._anchor_input = this._dialog_window.document.createElement('INPUT');
        this._anchor_input.setAttribute('size', '40');
        this._anchor_input.id = 'anchor_input';

        var anchor_label = this._dialog_window.document.createElement('LABEL');
        anchor_label.innerHTML = 'Anchor name: ';
        anchor_label.htmlFor = 'anchor_input';

        var anchor_div = this._dialog_window.document.createElement('DIV');
        Util.Element.add_class(anchor_div, 'field');
        anchor_div.appendChild(anchor_label);
        anchor_div.appendChild(this._anchor_input);

        var long_label = this._dialog_window.document.createElement('DIV');
        Util.Element.add_class(long_label, 'label');
        long_label.appendChild( this._dialog_window.document.createTextNode('Please provide a descriptive name for this anchor. The name should begin with a letter (a-z). The rest of the name can contain letters, numbers, and these characters: hyphens (-), underscores (_), colons(:), and periods(.). Other characters can't be used in an anchor name.') );

        var h1 = this._dialog_window.document.createElement('H1');
        if ( !this._initially_selected_item )
            h1.innerHTML = 'Create anchor';
        else
            h1.innerHTML = 'Edit anchor';

        var fieldset = new Util.Fieldset({legend : '', document : this._dialog_window.document});
        fieldset.fieldset_elem.appendChild(anchor_div);
        fieldset.fieldset_elem.appendChild(long_label);

        this._main_chunk.appendChild(h1);
        this._main_chunk.appendChild(fieldset.chunk);
    };

    this._append_remove_anchor_chunk = function()
    {
        var button = this._dialog_window.document.createElement('BUTTON');
        button.setAttribute('type', 'button');
        button.appendChild( this._dialog_window.document.createTextNode('Remove anchor') );

        var self = this;
        var listener = function()
        {
            /* not really necessary for just an anchor
            if ( confirm('Really remove anchor? WARNING: This cannot be undone.') )
            {
            */
                self._remove_listener();
                self._dialog_window.window.close();
            //}
        }
        Util.Event.add_event_listener(button, 'click', listener);

        // Setup their containing chunk
        var chunk = this._dialog_window.document.createElement('DIV');
        Util.Element.add_class(chunk, 'remove_chunk');
        chunk.appendChild(button);

        // Append the containing chunk
        //this._dialog_window.body.appendChild(chunk);
        this._root.appendChild(chunk);
    };

    this._apply_initially_selected_item = function()
    {
        if ( this._initially_selected_item != null )
        {
            this._anchor_input.value = this._initially_selected_item.name;
        }
    };

    this._internal_submit_listener = function()
    {
        // Get anchor name 
        var anchor_name = this._anchor_input.value;
        if ( anchor_name.replace( new RegExp('[a-zA-Z0-9_:.-]+', ''), '') != '' ||
             !anchor_name.match( new RegExp('^[a-zA-Z]', '') ) )
        {
            this._dialog_window.window.alert('You haven't entered a valid name. The name should begin with a Roman letter, and be followed by any number of digits, hyphens, underscores, colons, periods, and Roman letters. The name should include no other characters.');
            return false;
        }

        this._external_submit_listener({name : anchor_name});
        this._dialog_window.window.close();
    };
};

// file UI.Anchor_Double_Click.js
UI.Anchor_Double_Click = function AnchorDoubleClick() {
    Util.OOP.inherits(this, UI.Double_Click);
    this.helper = null;
    
    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this.helper = (new UI.Anchor_Helper).init(loki);
        return this;
    };
    
    this.double_click = function() {
        if (this.helper.is_selected())
            this.helper.open_dialog();
    };
};

// file UI.Anchor_Helper.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class for helping insert an anchor. Contains code
 * common to both the button and the menu item.
 */
UI.Anchor_Helper = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Helper);

    this.init = function(loki)
    {
        this._loki = loki;
        this._masseuse = (new UI.Anchor_Masseuse()).init(this._loki);
        return this;
    };

    this.is_selected = function()
    {
        return !!this.get_selected_item();
    };
    
    function _get_selected_placeholder()
    {
        var sel = Util.Selection.get_selection(self._loki.window);
        var range = Util.Range.create_range(sel);
         var found = Util.Range.find_nodes(range, self._masseuse.is_placeholder,
            true);
            
        if (found.length == 0) {
            return null;
        } else if (found.length > 1) {
            throw new Util.Multiple_Items_Error('Multiple anchor placeholders' +
                ' are selected.');
        } else {
            return found[0];
        }
    }

    this.get_selected_item = function()
    {
        var placeholder = _get_selected_placeholder();
        return (placeholder)
            ? {name: self._masseuse.get_name_from_placeholder(placeholder)}
            : null;
    };

    this.open_dialog = function()
    {
        var selected_item = self.get_selected_item();
        
        if (!this._dialog)
            this._dialog = new UI.Anchor_Dialog();
    
        this._dialog.init({
            base_uri: self._loki.settings.base_uri,
            submit_listener: self.insert_anchor,
            remove_listener: self.remove_anchor,
            selected_item: selected_item
        });
        this._dialog.open();
    };

    this.insert_anchor = function(anchor_info)
    {
        var selected = _get_selected_placeholder();
        var sel;
        var anchor;
        
        if (selected) {
            self._masseuse.update_name(selected, anchor_info.name);
        } else {
            anchor = self._loki.document.createElement('A');
            anchor.name = anchor_info.name;
            
            sel = Util.Selection.get_selection(self._loki.window);
            Util.Selection.collapse(sel, true); // to beginning
            Util.Selection.paste_node(sel, anchor);
            
            self._masseuse.massage(anchor);
        }
        
        self._loki.window.focus();
    };

    this.remove_anchor = function()
    {
        var selected = _get_selected_placeholder();
        var anchor;
        
        if (!selected)
            return;
        
        anchor = self._masseuse.unmassage(selected);
        if (!anchor.hasChildNodes())
            anchor.parentNode.removeChild(anchor);
        else
            anchor.removeAttribute('name');
    };
};

// file UI.Anchor_Masseuse.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class for inserting an anchor.
 */
UI.Anchor_Masseuse = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Masseuse);
    
    function needs_massaging(node) {
        return !!node.name;
    }
    needs_massaging.tag = 'A';
    
    function needs_unmassaging(node) {
        return !!node.getAttribute('loki:anchor_id');
    }
    needs_unmassaging.tag = 'IMG';

    /**
     * Massages the given node's children, replacing any named anchors with
     * fake images.
     */
    this.massage_node_descendants = function(node)
    {
        var anchors = node.getElementsByTagName(needs_massaging.tag);
        var i, anchor;

        for (i = anchors.length - 1; i >= 0; i--) {
            anchor = anchors[i];
            if (needs_massaging(anchor))
                self.massage(anchor);
        }
    };

    /**
     * Unmassages the given node's descendants, replacing any fake anchor images 
     * with real anchor elements.
     */
    this.unmassage_node_descendants = function(node)
    {
        var fakes = node.getElementsByTagName(needs_unmassaging.tag);
        var i, fake;
        
        // Remove anchors that have had their placeholder images deleted.
        var anchors = node.getElementsByTagName(needs_massaging.tag);
        var anchor;
        var placeholder_map = {}, id;
        
        for (i = 0; i < fakes.length; i++) {
            id = fakes[i].getAttribute('loki:anchor_id');
            if (id)
                placeholder_map[id] = fakes[i];
        }
        
        for (i = anchors.length - 1; i >= 0; i--) {
            anchor = anchors[i];
            if (needs_massaging(anchor) && !placeholder_map[anchor.id])
                anchor.parentNode.removeChild(anchor);
        }

        // Unmassage the placeholders that still exist.
        for (i = fakes.length - 1; i >= 0; i--) {
            fake = fakes[i];
            if (needs_unmassaging(fake))
                self.unmassage(fake);
        }
    };
    
    this.massage = function massage_anchor(anchor)
    {
        var doc = anchor.ownerDocument;
        var placeholder;
        var anchor_id = self.assign_fake_id(anchor);
        
        placeholder = Util.Document.create_element(doc, 'img', {
            className: 'loki__named_anchor',
            title: '#' + anchor.name,
            src: self._loki.settings.base_uri + 'images/nav/anchor.gif',
            style: {width: 12, height: 12},
            'loki:fake': true,
            'loki:anchor_id': anchor_id
        });
        
        return anchor.parentNode.insertBefore(placeholder, anchor);
    };
    
    this.update_name = function update_massaged_anchor_name(placeholder, name) {
        var anchor = self.get_anchor_for_placeholder(placeholder);
        
        placeholder.title = '#' + name;
        if (anchor) {
            if (anchor.id && anchor.id == anchor.name) {
                anchor.id = name;
                placeholder.setAttribute("loki:anchor_id", name);
            }
            anchor.name = name;
        }        
    };
    
    this.unmassage = function unmassage_anchor(placeholder) {
        var anchor = self.get_anchor_for_placeholder(placeholder);
        var actual_id;
        var name;
        var expected_id;
        
        if (!anchor) {
            // The original anchor tag was somehow removed from the document.
            anchor = placeholder.ownerDocument.createElement('A');
            anchor.name = placeholder.title.substr(1); // strips leading "#"
            placeholder.parentNode.replaceChild(anchor, placeholder);
            return anchor;
        }
        
        expected_id = placeholder.getAttribute('loki:anchor_id');
        actual_id = (placeholder.nextSibling) ?
            placeholder.nextSibling.id :
            null;
        self.remove_fake_id(anchor);
        if (actual_id == expected_id) {
            // Relative position has not changed. Simple.
            placeholder.parentNode.removeChild(placeholder);
            return anchor;
        }
        
        // The user has moved the anchor away from its original position.
        if (!anchor.hasChildNodes()) {
            // Bare named anchor; we can just move it to the correct spot.
            placeholder.parentNode.replaceChild(anchor, placeholder);
            return anchor;
        }
        
        // Anchor has child nodes: it must be split, leaving the original anchor
        // without a name and creating a new named anchor at the placeholder's
        // position.
        name = anchor.name;
        anchor.removeAttribute('name');
        
        anchor = placeholder.ownerDocument.createElement('A');
        anchor.name = name;
        
        placeholder.parentNode.replaceChild(anchor, placeholder);
        return anchor;
    };
    
    this.is_placeholder = function is_anchor_placeholder(elem) {
        return (Util.Node.is_tag(elem, needs_unmassaging.tag)
            && needs_unmassaging(elem));
    };
    
    this.get_name_from_placeholder = function get_anchor_name(placeholder) {
        var anchor;
        try {
            anchor = self.get_anchor_for_placeholder(placeholder);
            if (anchor && anchor.name)
                return anchor.name;
        } catch (e) { /* ignore it */ }
        
        return placeholder.title.substr(1); // strips leading "#"
    };
    
    this.get_anchor_for_placeholder = function get_real_anchor(placeholder) {
        var id = placeholder.getAttribute('loki:anchor_id');
        
        if (!id) {
            throw new Error('The placeholder has no associated anchor ID.');
        }
        
        return placeholder.ownerDocument.getElementById(id) || null;
    };
};

// file UI.Anchor_Menugroup.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class representing a clipboard menugroup. 
 */
UI.Anchor_Menugroup = function()
{
    Util.OOP.inherits(this, UI.Menugroup);

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._anchor_helper = (new UI.Anchor_Helper).init(this._loki);
        return this;
    };

    /**
     * Returns an array of menuitems, depending on the current context.
     * May return an empty array.
     */
    this.get_contextual_menuitems = function()
    {
        var menuitems = [];

        var selected_item = this._anchor_helper.get_selected_item();
        if ( selected_item != null )
        {
            menuitems.push( (new UI.Menuitem).init({ 
                label : 'Edit anchor',
                listener : this._anchor_helper.open_dialog 
            }) );
        }

        return menuitems;
    };
};

// file UI.BR_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents "Insert BR" toolbar button.
 */
UI.BR_Button = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Button);

    this.image = 'break.png';
    this.title = 'Single-line break (Shift+Enter)';
    this.click_listener = function() { self._br_helper.insert_br(); };

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._br_helper = (new UI.BR_Helper).init(this._loki);
        return this;
    };
};

// file UI.BR_Helper.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class for helping insert a br. Contains code
 * common to both the button and the menu item.
 */
UI.BR_Helper = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Helper);

    this.insert_br = function()
    {
        var sel = Util.Selection.get_selection(self._loki.window);
        var br = self._loki.document.createElement('BR');
        if ( document.all ) // XXX bad
            Util.Selection.paste_node(sel, br);
        else
            _paste_node_for_br_in_gecko(sel, br);
/*
        var sel = Util.Selection.get_selection(self._loki.window);
        var rng = Util.Range.create_range(sel);
        rng.setStart(br, 0);
        rng.setEnd(br, 0);
*/
//        Util.Selection.select_node(sel, br);
    //    Util.Selection.collapse(sel, false);
/*
        var rng = Util.Range.create_range(sel);
        var rng2 = Util.Range.clone_range(rng);
        Util.Selection.select_node(sel, self._loki.document.documentElement);
        Util.Selection.select_range(sel, rng2);
        //Util.Selection.collapse(sel, true);
*/
        self._loki.window.focus();
    };

    /**
     * This function is intended to work around the problem, in Gecko,
     * that when you click the BR button, a BR is always inserted, but 
     * the cursor doesn't always move down a line until you start typing--
     * which is confusing. This doesn't _totally_ fix that problem, but
     * it's better. XXX more work needed, and get rid of this hack.
     */
    var _paste_node_for_br_in_gecko = function(sel, to_be_inserted)
    {
        //var range = this._create_range(sel);
        var range = Util.Range.create_range(sel);
        // remove the current selection
        sel.removeAllRanges();
        range.deleteContents();
        var node = range.startContainer;
        var pos = range.startOffset;
        //range = this._create_range();
        //var range = Util.Range.create_range(sel);
        range = node.ownerDocument.createRange();
        switch (node.nodeType)
        {
        case 3: // Node.TEXT_NODE
                // we have to split it at the caret position.
            if (to_be_inserted.nodeType == 3)
            {
                // do optimized insertion
                node.insertData(pos, to_be_inserted.data);
                range.setEnd(node, pos + to_be_inserted.length);
                range.setStart(node, pos + to_be_inserted.length);
            }
            else
            {
                node = node.splitText(pos);
                node.parentNode.insertBefore(to_be_inserted, node);
                range.setStart(node, 0);
                range.setEnd(node, 0);
            }
            break;
        case 1: // Node.ELEMENT_NODE
            node = node.childNodes[pos];
            node.parentNode.insertBefore(to_be_inserted, node);
            range.setStart(node, 0);
            range.setEnd(node, 0);
            break;
        }
        sel.addRange(range);
    };
};

// file UI.Blockquote_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents "blockquote" toolbar button.
 */
UI.Blockquote_Button = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Button);

    this.image = 'quote.png';
    this.title = 'Blockquote';
    this.click_listener = function() { self._helper.toggle_blockquote_paragraph(); };
    this.state_querier = function() { return self._helper.query_blockquote_paragraph(); };

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._helper = (new UI.Blockquote_Highlight_Helper).init(this._loki, 'blockquote');
        return this;
    };
};

// file UI.Blockquote_Highlight_Helper.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Helper for blockquote and highlight buttons: contains logic common to both.
 * N.B.: I use "blockquote" below as a convenient shorthand for "blockquote_or_highlight_or_etc".
 */
UI.Blockquote_Highlight_Helper = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Helper);

    /**
     * @param    kind    either "blockquote" or "highlight"
     */
    this.init = function(loki, kind)
    {
        this.superclass.init.call(this, loki);
        this._kind = kind;
        this._paragraph_helper = (new UI.Paragraph_Helper()).init(this._loki);
        return this;
    };

    this.is_blockquoted = function()
    {
        return _get_blockquote_elem() != null;
    };

    this.toggle_blockquote_paragraph = function()
    {
        // Make sure we're not directly within BODY
        self._paragraph_helper.possibly_paragraphify();

        _remove_improper_blockquote_class_from_p();
        var blockquote = _get_blockquote_elem();

        //mb('_toggle_blockquote_paragraph: blockquote', blockquote);
        if ( blockquote == null )
        {
            //if ( self.is_blockquoteable() )
                _blockquote_paragraph();
            // else do nothing
        }
        else
        {
            /* works, but is undesired behavior:
            mb('found blockquote; replacing with children; blockquote:', blockquote);
            Util.Node.replace_with_children(blockquote);
            */
            _unblockquote_paragraph(blockquote);
        }
    };

    /**
     * Sometimes, despite my best efforts, in IE it seems that the callOut
     * class gets transferred from div to p. This seems ludicrous, but 
     * happens. So here we check for a callOut'd p and if found, remove 
     * the callOut, since that's probably what the user will want.
     */
    var _remove_improper_blockquote_class_from_p = function()
    {
        var sel = Util.Selection.get_selection(self._loki.window);
        var rng = Util.Range.create_range(sel);
        var p = Util.Range.get_nearest_ancestor_element_by_tag_name(rng, 'P');

        if ( Util.Element.has_class(p, 'callOut') )
            Util.Element.remove_class(p, 'callOut');
    };

    var _is_blockquote_elem = function(node)
    {
        if ( self._kind == "blockquote" )
            return ( node.nodeType == Util.Node.ELEMENT_NODE &&
                     node.tagName == 'BLOCKQUOTE' );
        else
            return ( node.nodeType == Util.Node.ELEMENT_NODE &&
                     node.tagName == 'DIV' &&
                     Util.Element.has_class(node, 'callOut') );
    };

    var _create_blockquote_elem = function(doc)
    {
        if ( self._kind == "blockquote" )
            return doc.createElement('BLOCKQUOTE');
        else
        {
            var div = doc.createElement('DIV');
            Util.Element.add_class(div, 'callOut');
            return div;
        }
    };

    /**
     * Gets the element contained by current selection 
     * that is blockquoteable. If no such exists, returns null.
     */
    this.is_blockquoteable = function()
    {
        var is_table_elem = function(node)
        {
             return ( (new RegExp('ol', 'i')).test(node.tagName) ||
                      (new RegExp('ul', 'i')).test(node.tagName) ||
                      (new RegExp('li', 'i')).test(node.tagName) ||
                        (new RegExp('td', 'i')).test(node.tagName) ||
                      (new RegExp('table', 'i')).test(node.tagName) );
        };

        var is_highlightable = function(node)
        {
            return ( node.nodeType == Util.Node.ELEMENT_NODE &&
                     Util.Node.is_nestable_block_level_element(node) &&
                     !Util.Node.has_ancestor_node(node, is_table_elem) );
        };

        var sel = Util.Selection.get_selection(self._loki.window);
        var rng = Util.Range.create_range(sel);
        // This doesn't work if, e.g., we have: <body><p>aasd^ad</p><p>asdfas$asdf</p></body>,
        // because the nearest common ancestor is BODY ... :
        //var elem = Util.Range.get_nearest_ancestor_node(rng, is_highlightable);
        var start_container = Util.Range.get_start_container(rng);
        var elem = Util.Node.get_nearest_ancestor_node(start_container, is_highlightable);

        return elem != null;
    };

    var _get_blockquote_elem = function()
    {
        var sel = Util.Selection.get_selection(self._loki.window);
        var rng = Util.Range.create_range(sel);
        return Util.Range.get_nearest_ancestor_node(rng, _is_blockquote_elem);
    };

    /**
     * Blockquotes the current paragraph.
     */
    var _blockquote_paragraph = function()
    {
        var sel = Util.Selection.get_selection(self._loki.window);
        var rng = Util.Range.create_range(sel);
        var blocks = Util.Range.get_intersecting_blocks(rng);

        if ( blocks.length > 0 )
        {
            // Create and append the blockquote elem
            var blockquote = _create_blockquote_elem(blocks[0].ownerDocument);
            blocks[0].parentNode.insertBefore(blockquote, blocks[0]);

            // Append the blocks to the blockquote
            for ( var i = 0; i < blocks.length; i++ )
            {
                blockquote.appendChild(blocks[i]);
            }
        }

        if ( !document.all ) // XXX doesn't work in IE right now, so just make user click in iframe again:
        {
            Util.Selection.move_cursor_to_end(sel, blockquote);
            self._loki.window.focus();
        }
    };

    var _unblockquote_paragraph = function(blockquote)
    {
        var sel = Util.Selection.get_selection(self._loki.window);
        var rng = Util.Range.create_range(sel);
        var blocks = Util.Range.get_intersecting_blocks(rng);

        var blockquote1 = blockquote.cloneNode(false); // clone blockquote elem twice
        var blockquote2 = blockquote.cloneNode(false);
        var non_blockquoted = []; // make array for non-blockquoted nodes
        var node = blockquote.firstChild
        var next;

        // loop through blockquote elem's children, adding each child to first clone
        // until first selected block (or last child) is found
        while ( node != blocks[0] && node != null )
        {
            next = node.nextSibling;
            if ( Util.Node.is_non_whitespace_text_node(node) )
                blockquote1.appendChild(node);
            node = next;
        }

        // keep looping, adding each child to array of non blockquoted
        // children, until last selected block (or last child) is found
        while ( node != blocks[blocks.length - 1] && node != null )
        {
            next = node.nextSibling;
            if ( Util.Node.is_non_whitespace_text_node(node) )
                non_blockquoted.push(node);
            node = next;
        }
        // (add the last non-blockquoted child)
        if ( node != null )
        {
            next = node.nextSibling;
            if ( Util.Node.is_non_whitespace_text_node(node) )
                non_blockquoted.push(node);
            node = next;
        }
        
        // keep looping, adding each child to second clone, 
        // until last child is found
        while ( node != null )
        {
            next = node.nextSibling;
            if ( Util.Node.is_non_whitespace_text_node(node) )
                blockquote2.appendChild(node);
            node = next;
        }


        // replace blockquote with placeholder
        var parent = blockquote.parentNode;
        var placeholder = blockquote.ownerDocument.createElement('DIV');
        parent.replaceChild(placeholder, blockquote);
        
        // insert first clone before placeholder
        if ( blockquote1.childNodes.length > 0 )
            parent.insertBefore(blockquote1, placeholder);

        // insert each element in non-blockquoted array before placeholder
        for ( var i = 0; i < non_blockquoted.length; i++ )
            parent.insertBefore(non_blockquoted[i], placeholder);

        // insert second clone before placeholder
        if ( blockquote2.childNodes.length > 0 )
            parent.insertBefore(blockquote2, placeholder);

        // remove placeholder
        parent.removeChild(placeholder);


        // move cursor
        if ( !document.all ) // XXX doesn't work in IE right now, so just make user click in iframe again:
        {
            Util.Selection.move_cursor_to_end(sel, blockquote2);
            self._loki.window.focus();
        }
    };

    /**
     * Queries whether the current paragraph is highlightable, 
     * or highlighted. Returns accordingly.
     */
    this.query_blockquote_paragraph = function()
    {
        // see UI.Highlight_Button
    };
};

// file UI.Bold_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents "bold" toolbar button.
 */
UI.Bold_Button = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Button);

    this.image = 'strong.png';
    this.title = 'Strong (Ctrl+B)';
    this.click_listener = function() { self._loki.exec_command('Bold'); };
    this.state_querier = function() { return self._loki.query_command_state('Bold'); };
};

// file UI.Bold_Keybinding.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents keybinding.
 */
UI.Bold_Keybinding = function()
{
    Util.OOP.inherits(this, UI.Keybinding);
    this.test = function(e) { return this.matches_keycode(e, 66) && e.ctrlKey; }; // Ctrl-B
    this.action = function() { this._loki.exec_command('Bold'); };
};

// file UI.Bold_Masseuse.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class for massaging strong tags to b tags. The motivation for this is that 
 * you can't edit strong tags, but we want them in the final output.
 */
UI.Bold_Masseuse = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Masseuse);

    /**
     * Massages the given node's children, replacing any named strongs with
     * b elements.
     */
    this.massage_node_descendants = function(node)
    {
        var strongs = node.getElementsByTagName('STRONG');
        for ( var i = strongs.length - 1; i >= 0; i-- )
        {
            var fake = self.get_fake_elem(strongs[i]);
            strongs[i].parentNode.replaceChild(fake, strongs[i]);
        }
    };

    /**
     * Unmassages the given node's descendants, replacing any b elements
     * with real strong elements.
     */
    this.unmassage_node_descendants = function(node)
    {
        var dummies = node.getElementsByTagName('B');
        for ( var i = dummies.length - 1; i >= 0; i-- )
        {
            var real = self.get_real_elem(dummies[i]);
            dummies[i].parentNode.replaceChild(real, dummies[i])
        }
    };

    /**
     * Returns a fake element for the given strong.
     */
    this.get_fake_elem = function(strong)
    {
        var dummy = strong.ownerDocument.createElement('B');
        dummy.setAttribute('loki:fake', 'true');
        // maybe transfer attributes, too
        while ( strong.firstChild != null )
        {
            dummy.appendChild( strong.removeChild(strong.firstChild) );
        }
        return dummy;
    };

    /**
     * If the given fake element is really fake, returns the appropriate 
     * real strong. Else, returns null.
     */
    this.get_real_elem = function(dummy)
    {
        if (dummy != null && dummy.nodeName == 'B') {
            var strong = dummy.ownerDocument.createElement('STRONG');
            // maybe transfer attributes, too
            while ( dummy.firstChild != null )
            {
                strong.appendChild( dummy.removeChild(dummy.firstChild) );
            }
            return strong;
        }
        return null;
    };
};

// file UI.Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents a button. For extending only.
 */
UI.Button = function()
{
    this.image; // string to location in base_uri/img/
    this.title; // string
    this.click_listener; // function
    this.state_querier; // function (optional)
    this.show_on_source_toolbar = false; // boolean (optional)

    this.init = function(loki)
    {
        this._loki = loki;
        return this;
    };
};

// file UI.Cell_Dialog.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A table dialog window..
 */
UI.Cell_Dialog = function()
{
    Util.OOP.inherits(this, UI.Dialog);

    this._dialog_window_width = 615;
    this._dialog_window_width = 585;

    this._bgs = ['bgFFFFCC', 'bgFFFF99', 'bg99CCFF', 'bgCCCCCC', 'bgE8E8E8'];
    this._bg_radios = new Array();

    this._set_title = function()
    {
        this._dialog_window.document.title =  "Table cell properties";
    };

    this._append_style_sheets = function()
    {
        this.superclass._append_style_sheets.call(this);
        //Util.Document.append_style_sheet(this._dialog_window.document, this._base_uri + 'css/cssSelector.css');
        Util.Document.append_style_sheet(this._dialog_window.document, this._base_uri + 'css/Table_Dialog.css');
    };

    this._populate_main = function()
    {
        this._append_td_properties();
        //this._append_table_color_properties();
        this.superclass._populate_main.call(this);
    };

    /**
     * Appends a chunk containing table properties.
     */
    this._append_td_properties = function()
    {
        var self = this;

        // Create generic label element
        var generic_label = this._dialog_window.document.createElement('LABEL');
        Util.Element.add_class(generic_label, 'label');

        // Align
        this._align_select = this._dialog_window.document.createElement('SELECT');
        this._align_select.setAttribute('id', 'align_select');
        
        var align_label = generic_label.cloneNode(false);
        align_label.appendChild( this._dialog_window.document.createTextNode('Alignment: ') );
        align_label.setAttribute('for', 'align_select');

        Util.Select.append_options(this._align_select, [{l : 'Left', v : 'left'}, {l : 'Center', v : 'center'}, {l : 'Right', v : 'right'}]);

        var align_div = this._dialog_window.document.createElement('DIV');
        Util.Element.add_class(align_div, 'field');
        align_div.appendChild(align_label);
        align_div.appendChild(this._align_select);

        // Valign
        this._valign_select = this._dialog_window.document.createElement('SELECT');
        this._valign_select.setAttribute('id', 'valign_select');
        
        var valign_label = generic_label.cloneNode(false);
        valign_label.appendChild( this._dialog_window.document.createTextNode('Vertical alignment: ') );
        valign_label.setAttribute('for', 'valign_select');

        Util.Select.append_options(this._valign_select, [{l : 'Top', v : 'top'}, {l : 'Middle', v : 'middle'}, {l : 'Bottom', v : 'bottom'}]);

        var valign_div = this._dialog_window.document.createElement('DIV');
        Util.Element.add_class(valign_div, 'field');
        valign_div.appendChild(valign_label);
        valign_div.appendChild(this._valign_select);

        // Wrap
        this._wrap_select = this._dialog_window.document.createElement('SELECT');
        this._wrap_select.setAttribute('id', 'wrap_select');

        var wrap_label = generic_label.cloneNode(false);
        wrap_label.appendChild( this._dialog_window.document.createTextNode('Wrap: ') );
        wrap_label.setAttribute('for', 'wrap_select');

        Util.Select.append_options(this._wrap_select, [{l : 'Yes', v : 'yes'}, {l : 'No', v : 'no'}]);

        var wrap_div = this._dialog_window.document.createElement('DIV');
        Util.Element.add_class(wrap_div, 'field');
        wrap_div.appendChild(wrap_label);
        wrap_div.appendChild(this._wrap_select);

        // Create heading
        var h1 = this._dialog_window.document.createElement('H1');
        h1.innerHTML = 'Table cell properties';

        // Create fieldset and its legend
        var fieldset = new Util.Fieldset({legend : '', document : this._dialog_window.document});

        // Append all the above to fieldset
        fieldset.fieldset_elem.appendChild(align_div);
        fieldset.fieldset_elem.appendChild(valign_div);
        fieldset.fieldset_elem.appendChild(wrap_div);

        // Append fieldset chunk to dialog
        this._main_chunk.appendChild(h1);
        this._main_chunk.appendChild(fieldset.chunk);
    };

    /**
     * Appends a chunk containing table color properties.
     */
    this._append_table_color_properties = function()
    {
        // Create generic elements
        var generic_bg_label = this._dialog_window.document.createElement('LABEL');
        Util.Element.add_class(generic_bg_label, 'bg_label');
        //generic_bg_label.appendChild( this._dialog_window.document.createTextNode(' ') );
        generic_bg_label.innerHTML = '&nbsp;';

        var generic_bg_radio = Util.Input.create_named_input({document : this._dialog_window.document, name : 'bg_radio'});
        generic_bg_radio.setAttribute('type', 'radio');

        // Create fieldset and its legend
        var fieldset = new Util.Fieldset({legend : 'Cell color properties:', document : this._dialog_window.document});

        // Create and append the "no bgcolor" radio and label
        this._no_bg_radio = generic_bg_radio.cloneNode(true);
        this._no_bg_radio.setAttribute('id', 'no_bg_radio');

        var no_bg_label = this._dialog_window.document.createElement('LABEL');
        no_bg_label.appendChild( this._dialog_window.document.createTextNode('Use no background color') );
        no_bg_label.setAttribute('for', 'no_bg_radio');
        Util.Element.add_class(no_bg_label, 'label');

        fieldset.fieldset.appendChild(this._no_bg_radio);
        fieldset.fieldset.appendChild(no_bg_label);

        // Create and append the bgcolor radios and labels
        var bg_labels = new Array();
        for ( var i = 0; i < this._bgs.length; i++ )
        {
            bg_labels[i] = generic_bg_label.cloneNode(true);
            bg_labels[i].setAttribute('for', 'bg_' + this._bgs[i] + '_radio');
            Util.Element.add_class(bg_labels[i], this._bgs[i]);

            this._bg_radios[i] = generic_bg_radio.cloneNode(true);
            this._bg_radios[i].setAttribute('id', 'bg_' + this._bgs[i] + '_radio');

            fieldset.fieldset_elem.appendChild(this._bg_radios[i]);
            fieldset.fieldset_elem.appendChild(bg_labels[i]);
        }

        // Append fieldset chunk to dialog
        this._main_chunk.appendChild(fieldset.chunk);
    };

    /**
     * Sets initial values.
     */
    this._apply_initially_selected_item = function()
    {
        messagebox('UI.Cell_Dialog.apply_initially_selelcted_item: initially_selected_item.align', this._initially_selected_item.align);
        messagebox('UI.Cell_Dialog.apply_initially_selelcted_item: initially_selected_item.valign', this._initially_selected_item.valign);

        this._align_select.value = this._initially_selected_item.align == '' ? 'left' : this._initially_selected_item.align;
        this._valign_select.value = this._initially_selected_item.valign == '' ? 'top' : this._initially_selected_item.valign;
        this._wrap_select.value = this._initially_selected_item.wrap == '' ? 'yes' : this._initially_selected_item.wrap;
        
        messagebox('UI.Cell_Dialog.apply_initially_selelcted_item: this._align_select.value', this._align_select.value);
        messagebox('UI.Cell_Dialog.apply_initially_selelcted_item: this._valign_select.value', this._valign_select.value);

        /*
        // Apply background
        this._no_bg_radio.checked = true;
        for ( var i = 0; i < this._bgs.length; i++ )
        {
            if ( this._bgs[i] == this._initially_selected_item.bg )
            {
                this._bg_radios[i].checked = true;
            }
        }
        */
    };

    /**
     * Called as an event listener when the user clicks the submit
     * button. 
     */
    this._internal_submit_listener = function()
    {
        var align = this._align_select.value;
        var valign = this._valign_select.value;
        var wrap = this._wrap_select.value;
        
        /*
        // Determine background
        var bg = '';
        for ( var i = 0; i < this._bgs.length; i++ )
        {
            if ( this._bg_radios[i].checked == true )
            {
                bg = this._bgs[i];
            }
        }
        */

        //this._external_submit_listener({align : align, valign : valign, wrap : wrap, bg : bg});
        this._external_submit_listener({align : align, valign : valign, wrap : wrap});
        this._dialog_window.window.close();
    };
};

// file UI.Center_Align_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents "center align" toolbar button.
 */
UI.Center_Align_Button = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Button);

    this.image = 'align_center.png';
    this.title = 'Center align (Ctrl+E)';
    this.click_listener = function() { self._loki.exec_command('JustifyCenter'); };
    this.state_querier = function() { return self._loki.query_command_state('JustifyCenter'); };
};

// file UI.Center_Align_Keybinding.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents keybinding.
 */
UI.Center_Align_Keybinding = function()
{
    Util.OOP.inherits(this, UI.Keybinding);
    this.test = function(e) { return this.matches_keycode(e, 69) && e.ctrlKey; }; // Ctrl-L
    //this.action = function() { this._loki.exec_command('JustifyCenter'); };
    this.action = function() { this._align_helper.align_center(); };

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._align_helper = (new UI.Align_Helper).init(this._loki);
        return this;
    };
};

// file UI.Clean.js
/**
 * Does nothing.
 * @constructor
 *
 * @class <p>Contains methods related to producing clean, valid,
 * elegant HTML from the mess produced by the designMode = 'on'
 * components. </p>
 *
 * <p>JSDoc doesn't work well with this file. See the code for more
 * details about how it works.</p>
 */
UI.Clean = new Object;

/**
 * Cleans the children of the given root.
 *
 * @param {Element} root             reference to the node whose children should
 *                                   be cleaned
 * @param {object}    settings         Loki settings
 * @param {boolean} [live]           set to true if this clean is being run
 *                                   on content that is actively being edited
 * @param {object}  [block_settings] settings to pass along to
 *                                   Util.Block.enforce_rules
 */
UI.Clean.clean = function(root, settings, live, block_settings)
{
    
    /**
     * Removes the given node from the tree.
     */
    function remove_node(node)
    {
        // if the node's parent is null, it's already been removed
        if ( node.parentNode == null )
            return;

        node.parentNode.removeChild(node);
    }

    /**
     * Remove the tag from the given node. (See description in
     * fxn body how this is done.) E.g.,
     * node.innerHTML = '<p><strong>Well</strong>&emdash;three thousand <em>ducats</em>!</p>'
     *   -->
     * node.innerHTML = '<strong>Well</strong>&emdash;three thousand <em>ducats</em>!'
     */
    function remove_tag(node) 
    {
        Util.Node.replace_with_children(node);
    }

    /**
     * Change the tag of the given node to being one with the given tagname. E.g.,
     * node.innerHTML = '<p><b>Well</b>&emdash;three thousand <em>ducats</em>!</p>'
     *   -->
     * node.innerHTML = '<p><strong>Well</strong>&emdash;three thousand <em>ducats</em>!</p>'
     */
    function change_tag(node, new_tagname)
    {
        // if the node's parent is null, it's already been removed or changed
        // (possibly not necessary here)
        if ( node.parentNode == null )
            return;

        // Create new node
        var new_node = node.ownerDocument.createElement(new_tagname);

        // Take all the children of node and move them, 
        // one at a time, to the new node.
        // Then, node being empty, remove node.
        while ( node.hasChildNodes() )
        {
            new_node.appendChild(node.firstChild);
        }
        node.parentNode.replaceChild(new_node, node);

        // TODO: take all attributes from old node -> new node
    }

    /**
     * Remove the given attributes from the given node.
     */ 
    function remove_attributes(node, attrs)
    {
        try
        {
        for ( var i = 0; i < attrs.length; i++ )
        {
            if ( node.getAttribute(attrs[i]) != null )
                node.removeAttribute(attrs[i]);
        }
        }
        catch(e) { mb('error in remove_attributes: ', e.message); }
    }

    /**
     * Checks whether the given node has the given attributes.
     * Returns false or an array of attrs (names) that are had.
     */
    function has_attributes(node, all_attrs)
    {
        var had_attrs = [];
        if ( node.nodeType == Util.Node.ELEMENT_NODE )
        {
            for ( var i = 0; i < all_attrs.length; i++ )
            {
                // Sometimes in IE node.getAttribute throws an "Invalid argument"
                // error here. I have _no_ idea why, but we want to catch it
                // here so that the rest of the tests run.  XXX figure out why?
                try
                {
                    if ( node.getAttribute(all_attrs[i]) != null )
                        had_attrs.push(all_attrs[i]);
                }
                catch(e) { /*mb('error in has_attributes: [node, e.message]: ', [node, e.message]);*/ }
            }
        }
        
        return ( had_attrs.length > 0 )
            ? had_attrs
            : false;
    }
    
    /**
     * Checks whether the given node is an element node.
     */
    function is_element(node)
    {
        return (node.nodeType == Util.Node.ELEMENT_NODE);
    }

    /**
     * Checks whether the given node has one of the given tagnames.
     */
    function has_tagname(node, tagnames)
    {
        if ( node.nodeType == Util.Node.ELEMENT_NODE )
        {
            for ( var i = 0; i < tagnames.length; i++ )
            {
                if ( node.tagName == tagnames[i] )
                {
                    return true;
                }
            }
        }
        // otherwise
        return false;
    }

    /**
     * Checks whether the given node does not have one of the 
     * given tagnames.
     */
    function doesnt_have_tagname(node, tagnames)
    {
        if ( node.nodeType == Util.Node.ELEMENT_NODE )
        {
            for ( var i = 0; i < tagnames.length; i++ )
            {
                if ( node.tagName == tagnames[i] )
                {
                    return false;
                }
            }
            // otherwise, it's a tag that doesn't have the tagname
            return true
        }
        // otherwise, it's not a tag
        return false;
    }

    /**
     * Checks whether the given node has any classes
     * matching the given strings.
     */
    function has_class(node, strs)
    {
        var matches = [];
        
        if (node.nodeType == Util.Node.ELEMENT_NODE) {
            for (var i = 0; i < strs.length; i++) {
                if (Util.Element.has_class(node, strs[i]))
                    matches.push(strs[i]);
            }
        }
        
        return (matches.length > 0) ? matches : false;
    }

    /**
     * Removes all attributes matching the given strings.
     */
    function remove_class(node, strs)
    {
        for (var i = 0; i < strs.length; i++) {
            Util.Element.remove_class(node, strs[i]);
        }
    }

    /**
     * Checks whether the tag has a given (e.g., MS Office) prefix.
     */
    function has_prefix(node, prefixes)
    {
        if ( node.nodeType == Util.Node.ELEMENT_NODE )
        {
            for ( var i = 0; i < prefixes.length; i++ )
            {
                if ( node.tagName.indexOf(prefixes[i] + ':') == 0 ||
                     node.scopeName == prefixes[i] )
                    return true;
            }
        }
        // otherwise
        return false;
    };
    
    var allowable_tags;
    if (settings.allowable_tags) {
        allowable_tags = settings.allowable_tags.map(function(tag) {
            return tag.toUpperCase();
        }).toSet();
    } else {
        allowable_tags = UI.Clean.default_allowable_tags.toSet();
    }
    
    var acceptable_css;
    if (typeof(settings.allowable_inline_styles) != 'undefined') {
        if ('string' == typeof(settings.allowable_inline_styles)) {
            var macros = {
                'all': true,
                'any': true,
                '*': true,
                'none': false
            };
            acceptable_css = settings.allowable_inline_styles.toLowerCase();
            if (acceptable_css in macros) {
                acceptable_css = macros[acceptable_css];
            } else {
                acceptable_css = acceptable_css.split(/s+/);
            }
        } else if (null === settings.allowable_inline_styles) {
            acceptable_css = UI.Clean.default_allowable_inline_styles;
        } else {
            acceptable_css = settings.allowable_inline_styles;
        }
    } else {
        acceptable_css = UI.Clean.default_allowable_inline_styles;
    }
    
    if (typeof(acceptable_css.join) == 'function') { // it's an array!    
        acceptable_css = get_css_pattern(acceptable_css);
    }
    
    function get_css_pattern(names) {
        names = names.map(Util.regexp_escape).map(function(name) {
            return name.toLowerCase();
        });
        return new RegExp('^(' + names.join('|') + ')');
    }
        
    function is_allowable_tag(node)
    {
        return (node.nodeType != Util.Node.ELEMENT_NODE ||
            node.tagName in allowable_tags);
    }
    
    function is_block(node)
    {
        var wdw = Util.Node.get_window(node);
        if (wdw) {
            try {
                return Util.Element.is_block_level(wdw, node);
            } catch (e) {
                // try using tag name below
            }
        }
        
        return Util.Node.is_block_level_element(node);
    }
    
    function is_within_container(node) {
        for (var n = node; n; n = n.parentNode) {
            if (is_element(n) && n.getAttribute('loki:container'))
                return true;
        }
        
        return false;
    }
    
    function is_on_current_page(uri) {
        if (!uri.host && (!uri.path || (/$./?/.exec(uri.path))))
            return true;
        
        // Mozilla makes us go the extra mile.
        var base = Util.URI.parse(window.location);
        if (base.authority == uri.authority && base.path == uri.path)
            return true;
        
        return false;
    }
    
    function is_same_domain(uri) {
        return (uri.host == Util.URI.extract_domain(window.location));
    }

    var tests =
    [
        // description : a text description of the test and action
        // test : function that is passed node in question, and returns
        //        false if the node doesn`t match, and whatever it wants 
        //        to be passed to the action otherwise.
        // action : function that is passed node and return of action, and 

        {
            description : 'Remove all comment nodes.',
            test : function(node) {
                if (node.nodeType != Util.Node.COMMENT_NODE)
                    return false;
                return !("!" in allowable_tags);
            },
            action : remove_node
        },
        {
            description : 'Remove all style nodes.',
            test : function(node) { return has_tagname(node, ['STYLE']); },
            action : remove_node
        },
        {
            description : 'Remove bad attributes. (v:shape from Ppt)',
            test : function (node) { return has_attributes(node, ['v:shape']); },
            action : remove_attributes
        },
        {
            description: 'Translate align attributes.',
            test: function(node) { return has_attributes(node, ['align']); },
            action: function translate_alignment(el) {
                // Exception: tables and images still use the align attribute.
                if (has_tagname(el, ['TD', 'TH', 'TR', 'TABLE', 'IMG']))
                    return;
                
                el.style.textAlign = el.align.toLowerCase();
                el.removeAttribute('align');
            }
        },
        {
            description: 'Strip unwanted inline styles',
            test: function(node) {
                return acceptable_css !== true && has_attributes(node, ['style']); 
            },
            action: function strip_unwanted_inline_styles(el) {
                if (acceptable_css === false) {
                    el.removeAttribute('style');
                    return;
                }
                
                var rule = /([w-]+)s*:s*([^;]+)(?:;|$)/g;
                var raw = el.style.cssText;
                var accepted = [];
                var match;
                var name;
                
                while (match = rule.exec(raw)) {
                    name = match[1].toLowerCase();
                    if (acceptable_css.test(name)) {
                        accepted.push(name + ": " + match[2] + ";");
                    }
                }
                
                if (accepted.length > 0)
                    el.style.cssText = accepted.join(' ');
                else
                    el.removeAttribute('style');
            }
        },
        {
            description: 'Remove empty Word paragraphs',
            test: function is_empty_word_paragraph(node) {
                // Check node type and tag
                if (!node.tagName || node.tagName != 'P') {
                    return false;
                }
                
                // Check for a Word class
                if (!(/(^|b)Mso/.test(node.className)))
                    return false;
                
                // Check for the paragraph to only contain non-breaking spaces
                // or other whitespace characters.
                var pattern = new RegExp("^[\sxA0]+$", "");
                for (var i = 0; i < node.childNodes.length; i++) {
                    var child = node.childNodes[i];
                    if (child.nodeType == Util.Node.ELEMENT_NODE) {
                        if (!is_empty_word_paragraph(child)) // recurse
                            return false;
                    }
                    
                    if (child.nodeType == Util.Node.TEXT_NODE) {
                        if (!pattern.test(child.data)) {
                            return false;
                        }
                    }
                }
                
                return true;
            },
            action: remove_node
        },
        {
            description: 'Remove Microsoft Word section DIV's',
            test: function is_ms_word_section_div(node) {
                if (!has_tagname(node, ['DIV']))
                    return false;
            
                var pattern = /^Sectiond+$/;
                var classes = Util.Element.get_class_array(node);
                if (!classes.length) {
                    return false;
                }
                
                for (var i = 0; i < classes.length; i++) {
                    if (!pattern.test(classes[i]))
                        return false;
                }
                
                return true;
            },
            action: remove_tag
        },
        {
            description : 'Remove Microsoft Office internal classes.',
            test : is_element,
            action : function strip_ms_office_classes(node)
            {
                var office_pattern = /^(Mso|O|Sectiond+$)/;
                var classes = Util.Element.get_class_array(node);
                var length = classes.length;
                
                for (var i = 0; i < length; i++) {
                    if (office_pattern.test(classes[i]))
                        classes.splice(i, 1); // remove the class
                }
                
                if (classes.length != length)
                    Util.Element.set_class_array(node, classes);
            }
        },
        {
            description : 'Remove unnecessary span elements',
            test : function is_bad_span(node) {
                 return (has_tagname(node, ['SPAN'])
                    && !has_attributes(node, ['class', 'style'])
                    && !is_within_container(node));
            },
            action : remove_tag
        },
        {
            description : 'Remove all miscellaneous non-good tags (strip_tags).',
            test : function(node) { return !is_allowable_tag(node); },
            action : remove_tag
        },
        // STRONG -> B, EM -> I should be in a Masseuse; then exclude B and I here
        // CENTER -> P(align="center")
        // H1, H2 -> H3; H5, H6 -> H4(? or -> P)
        // Axe form elements?
        {
            description : "Remove U unless there's an appropriate option set.",
            test : function(node) { return !settings.options.underline && has_tagname(node, ['U']); },
            action : remove_tag
        },
        {
            description : 'Remove all tags that have Office namespace prefixes.',
            test : function(node) { return has_prefix(node, ['o', 'O', 'w', 'W', 'st1', 'ST1']); },
            action : remove_tag
        },
        {
            description : 'Remove width and height attrs on tables.',
            test : function(node) {
                return has_tagname(node, ['TABLE']); 
            },
            action : function(node) { 
                remove_attributes(node, ['height', 'width']); 
            }
        },
        {
            description: 'Remove width and height attributes from images if so desired.',
            test: function(node) {
                return (!!settings.disallow_image_sizes &&
                    has_tagname(node, ['IMG']));
            },
            action: function(node) {
                remove_attributes(node, ['height', 'width']);
            }
        },
        {
            description: "Normalize all image URI's",
            test: Util.Node.curry_is_tag('IMG'),
            action: function normalize_image_uri(img) {
                if (Util.URI.is_urn(img)) {
                    // Don't normalize URN's (like data:).
                    return;
                }
                var uri = Util.URI.parse(img.src);
                var norm = Util.URI.normalize(img.src);
                if (is_same_domain(uri))
                    norm.scheme = null;
                else
                    norm.scheme = uri.scheme; // undo any changes
                img.src = Util.URI.build(norm);
            }
        },
        {
            description: "Normalize all link URI's",
            test: Util.Node.curry_is_tag('A'),
            action: function normalize_link_uri(link) {
                if (!link.href)
                    return;
                var uri = Util.URI.parse(link.href);
                if (Util.URI.is_urn(uri)) {
                    // Do nothing to URN's (like mailto: addresses).
                    return;
                }
                if (is_on_current_page(uri))
                    return;
                var norm = Util.URI.normalize(uri);
                if (is_same_domain(uri))
                    norm.scheme = null;
                else
                    norm.scheme = uri.scheme; // undo any changes
                link.href = Util.URI.build(norm);
            }
        },
        {
            description: 'Remove unnecessary BR's that are elements' last ' +
                'children',
            run_on_live: false,
            test: function is_last_child_br(node) {
                function get_last_relevant_child(n)
                {
                    var c; // child
                    for (c = n.lastChild; c; c = c.previousSibling) {
                        if (c.nodeType == Util.Node.ELEMENT_NODE) {
                            return c;
                        } else if (c.nodeType == Util.Node.TEXT_NODE) {
                            if (/S/.test(c.nodeValue))
                                return c;
                        }
                    }
                }
                
                return has_tagname(node, ['BR']) && is_block(node.parentNode) &&
                    get_last_relevant_child(node.parentNode) == node;
                
            },
            action: remove_node
        },
        {
            description: 'Remove improperly nested elements',
            run_on_live: false,
            test: function improperly_nested(node)
            {
                function is_nested()
                {
                    var a;
                    for (a = node.parentNode; a; a = a.parentNode) {
                        if (a.tagName == node.tagName)
                            return true;
                    }
                    
                    return false;
                }
                
                return node.tagName in UI.Clean.self_nesting_disallowed &&
                    is_nested();
            },
            action: remove_tag
        }
        // TODO: deal with this?
        // In content pasted from Word, there may be 
        // ...<thead><tr><td>1</td></tr></thead>...
        // instead of
        // ...<thead><tr><th>1</th></tr></thead>...
    ];

    function _clean_recursive(root)
    {
        var children = root.childNodes;
        // we go backwards because remove_tag uses insertBefore,
        // so if we go forwards some nodes will be skipped
        //for ( var i = 0; i < children.length; i++ )
        for ( var i = children.length - 1; i >= 0; i-- )
        {
            var child = children[i];
            _clean_recursive(child); // we need depth-first, or remove_tag
                                     // will cause some nodes to be skipped
            _run_tests(child);
        }
    }

    function _run_tests(node)
    {
        for ( var i = 0; i < tests.length; i++ )
        {
            if (live && false === tests[i].run_on_live)
                continue;
            
            var result = tests[i].test(node);
            if ( result !== false )
            {
                // We do this because we don't want any errors to
                // result in lost content!
                try {
                    tests[i].action(node, result);
                } catch (e) {
                    if (typeof(console) == 'object') {
                        if (console.warn)
                            console.warn(e);
                        else if (console.log)
                            console.log(e);
                    }
                }
            }
        }
    }

    // We do this because we don't want any errors to result in lost content!
    try
    {
        _clean_recursive(root);
        Util.Block.enforce_rules(root, block_settings);
    }
    catch(e)
    {
        if (typeof(console) == 'object') {
            if (console.warn)
                console.warn(e);
            else if (console.log)
                console.log(e);
        }
    }
};

UI.Clean.clean_URI = function clean_URI(uri)
{
    var local = Util.URI.extract_domain(uri) ==
        Util.URI.extract_domain(window.location);
        
    return (local)
        ? Util.URI.strip_https_and_http(uri)
        : uri;
}

UI.Clean.clean_HTML = function clean_HTML(html, settings)
{
    // empty elements (as defined by HTML 4.01)
    var empty_elems = '(br|area|link|img|param|hr|input|col|base|meta)';

    var tests =
    [
        // description : a text description of the test and action
        // test: only do the replacement if this is true 
        //       (optional--if omitted, the replacement will always be performed)
        // pattern : either a regexp or a string to match
        // replacement : a string to replace pattern with

        {
            description : 'Forces all empty elements (with attributes) to include trailing slash',
            //                     [ ]      : whitespace between element name and attrs
            //                     [^>]*    : any chars until one char before the final >
            //                     [^>/]    : the char just before the the final >. 
            //                                This excludes elements that already include trailing slashes.
            test : function() { return settings.use_xhtml },
            pattern : new RegExp('<' + empty_elems + '([ ][^>]*[^>/])>', 'gi'),
            replacement : '<$1$2 />'
        },
        {
            description : 'Forces all empty elements (without any attributes) to include trailing slash',
            test : function() { return settings.use_xhtml },
            pattern : new RegExp('<' + empty_elems + '>', 'gi'),
            replacement : '<$1 />'
        }
    ];


    for (var i in tests) {
        if (!tests[i].test || tests[i].test())
            html = html.replace(tests[i].pattern, tests[i].replacement);
    }

    return html;
};

UI.Clean.default_allowable_tags = 
    ['A', 'ABBR', 'ACRONYM', 'ADDRESS', 'AREA', 'B', 'BDO', 'BIG', 'BLOCKQUOTE',
    'BR', 'BUTTON', 'CAPTION', 'CITE', 'CODE', 'COL', 'COLGROUP', 'DD', 'DEL',
    'DIV', 'DFN', 'DL', 'DT', 'EM', 'FIELDSET', 'FORM', 'H1', 'H2', 'H3', 'H4',
    'H5', 'H6', 'HR', 'I', 'IMG', 'INPUT', 'INS', 'KBD', 'LABEL', 'LI', 'MAP',
    'NOSCRIPT', 'OBJECT', 'OL', 'OPTGROUP', 'OPTION', 'P', 'PARAM', 'PRE', 'Q',
    'SAMP', 'SCRIPT', 'SELECT', 'SMALL', 'SPAN', 'STRONG', 'SUB', 'SUP', 'TABLE',
    'TBODY', 'TD', 'TEXTAREA', 'TFOOT', 'TH', 'THEAD', 'TR', 'TT', 'U', 'UL',
    'VAR'];
    
UI.Clean.default_allowable_inline_styles =
    ['text-align', 'vertical-align', 'float', 'direction', 'display', 'clear',
    'list-style'];

UI.Clean.self_nesting_disallowed =
    ['ABBR', 'ACRONYM', 'ADDRESS', 'AREA', 'B', 'BR', 'BUTTON', 'CAPTION',
    'CODE', 'DEL', 'DFN', 'EM', 'FORM', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
    'HR', 'I', 'IMG', 'INPUT', 'INS', 'KBD', 'LABEL', 'MAP', 'NOSCRIPT',
    'OPTION', 'P', 'PARAM', 'PRE', 'SCRIPT', 'SELECT', 'STRONG', 'TT', 'U',
    'VAR'].toSet();

// file UI.Clean_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents toolbar button.
 */
UI.Clean_Button = function()
{
    Util.OOP.inherits(this, UI.Button);

    this.image = 'cleanup.png';
    this.title = 'Clean up HTML';
    this.click_listener = function()
    {
        UI.Clean.clean(this._loki.body, this._loki.settings, true);
    };
};

// file UI.Clipboard_Helper.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class for helping insert an anchor. Contains code
 * common to both the button and the menu item.
 */
UI.Clipboard_Helper = function ClipboardHelper()
{
    var self = this;
    Util.OOP.inherits(self, UI.Helper);

    this.is_selection_empty = function()
    {
        var sel = Util.Selection.get_selection(this._loki.window);
        return Util.Selection.is_collapsed(sel);
    };

    this.cut = function clipboard_cut()
    {
        if (!self.copy('Cut', 'X'))
            return;
        var sel = Util.Selection.get_selection(self._loki.window);
        var rng = Util.Range.create_range(sel);
        Util.Range.delete_contents(rng);
        self._loki.focus();
    };

    this.copy = function clipboard_copy(command, accel)
    {
        // Get the HTML to copy
        var sel = Util.Selection.get_selection(self._loki.window);
        var rng = Util.Range.create_range(sel);
        var html = Util.Range.get_html(rng);
        //var text = rng.toString();
        
        if (Util.Selection.is_collapsed(sel)) {
            // If nothing is actually selected; do not overwrite the clipboard.
            return false;
        }

        // Unmassage and clean HTML
        var container = self._loki.document.createElement('DIV');
        container.innerHTML = html;
        self._loki.unmassage_node_descendants(container);
        
        // Clean the copied HTML. We pass an override to the block-level element
        // rule enforcer that specifies that inline content within paragraphs do
        // not have to be wrapped in (e.g.) paragraph tags. This prevents inline
        // content that is being copied from being treated as its own paragraph.
        UI.Clean.clean(container, self._loki.settings, false, {
            overrides: {DIV: Util.Block.BLOCK}
        });
        html = container.innerHTML;

        // Move HTML to clipboard
        try {
            if (UI.Clipboard_Helper._gecko) {
                _gecko_copy(html, command || 'Copy', accel || 'C');
                return false;
            } else {
                _ie_copy(html);
            }
        } finally {
            self._loki.focus();
        }
        
        return true;
    };

    this.paste = function clipboard_paste()
    {
        try {
            if (UI.Clipboard_Helper._gecko) {
                _gecko_paste();
            } else {
                _ie_paste();
            }
        } finally {
            self._loki.focus();
        }
    };

    this.delete_it = function() // delete is a reserved word
    {
        var sel = Util.Selection.get_selection(self._loki.window);
        var rng = Util.Range.create_range(sel);
        rng.deleteContents();
        self._loki.focus();
    };

    this.select_all = function()
    {
        self._loki.exec_command('SelectAll');
        self._loki.focus();
    };

    this.is_security_error = function(e)
    {
        return ( e.message != null && e.message.indexOf != null && e.message.indexOf('Clipboard_Helper') > -1 );
    };
    
    function _show_gecko_privileges_warning()
    {
        var message = "Your browser requires that you give explicit permission for " +
            "your clipboard to be accessed, so you may see a security warning " +
            "after dismissing this message. You are free to deny this permssion, " +
            "but if you do, you may be unable to cut, copy, or paste into this " +
            "document.";
        
        UI.Messenger.display_once_per_duration('gecko clipboard warning',
            message, 45);
    }
    
    function _gecko_clipboard_error(command, accel)
    {
        var key;
        if (!self._loki.owner_window.GeckoClipboard) {
            key = ((Util.Browser.Mac) ? 'вЊ�' : 'Ctrl-') + accel;
            alert("In your browser, you must either choose " + command + " " +
                "from the Edit menu, or press " + key + ".");
        }
    }

    function _gecko_copy(html, command, accel)
    {
        _gecko_clipboard_error(command, accel);
    };

    function _ie_copy(html)
    {
        var sel = Util.Selection.get_selection(self._loki.window);
        var rng = Util.Range.create_range(sel);

        // transfer from iframe to editable div
        // select all of editable div
        // copy from editable div
        UI.Clipboard_Helper_Editable_Iframe.contentWindow.document.body.innerHTML = html;
        UI.Clipboard_Helper_Editable_Iframe.contentWindow.document.execCommand("SelectAll", false, null);
        UI.Clipboard_Helper_Editable_Iframe.contentWindow.document.execCommand("Copy", false, null);

        // Reposition cursor
        rng.select();
    };

    function _gecko_paste()
    {
        _gecko_clipboard_error('Paste', 'V');
    };

    function _ie_paste()
    {
        var sel = Util.Selection.get_selection(self._loki.window);
        var rng = Util.Range.create_range(sel);
        var parent = rng.parentElement();
        
        // Ensure that the selection is within the editing document.
        // if (parent && parent.ownerDocument != self._loki.document)
        //     return;

        // Make clipboard iframe editable
        // clear editable div
        // select all of editable div
        // paste into editable div
        UI.Clipboard_Helper_Editable_Iframe.contentWindow.document.body.contentEditable = true;
        UI.Clipboard_Helper_Editable_Iframe.contentWindow.document.body.innerHTML = "";
        UI.Clipboard_Helper_Editable_Iframe.contentWindow.document.execCommand("SelectAll", false, null);
        UI.Clipboard_Helper_Editable_Iframe.contentWindow.document.execCommand("Paste", false, null);

        // Get HTML
        var html = UI.Clipboard_Helper_Editable_Iframe.contentWindow.document.body.innerHTML;

        // Massage and clean HTML
        var nodeName = 'DIV';
        if (rng.text != null && rng.text == "") {
            if (typeof(parent) == 'object' && parent.tagName)
                nodeName = parent.tagName;
        }
        
        function clean(nodeName) {
            var temp = self._loki.document.createElement(nodeName);
            temp.innerHTML = html;
            
            UI.Clean.clean(temp, self._loki.settings);
            self._loki.massage_node_descendants(temp);
            return temp.innerHTML;
        }
        
        var cleanedHTML;
        try {
            cleanedHTML = clean(nodeName);
        } catch (e) {
            if (nodeName != 'DIV')
                cleanedHTML = clean('DIV');
            else
                throw e;
        }

        // Actually paste HTML
        rng.pasteHTML(cleanedHTML);
        rng.select();
    };
};

UI.Clipboard_Helper._gecko = (typeof(Components) == 'object');

// We need to create this iframe as a place to put code that
// Gecko needs to run with special privileges, for which
// privileges Gecko requires that the code be signed.
// (But we don't want to sign _all_ of Loki, because the page
// that invokes the javascript has to be signed with the 
// javascript, and we want to be able to use Loki on dynamic
// pages; sigining dynamic pages would be too inconvenient, not
// to mention slow.)
// We create this here, on the assumption that it will have
// loaded by the time we need it.
//
// For more information about how to sign scripts, see 
// privileged/HOWTO

/** @ignore */
UI.Clipboard_Helper._setup_done = false

/** @ignore */
UI.Clipboard_Helper._setup = function setup_clipboard_helper() {
    var base_uri = (arguments[0]
         ? Util.URI.build(Util.URI.normalize(arguments[0]))
        : null);
    var helper_src = null;
    
    if (UI.Clipboard_Helper._setup_done)
        return;
    
    function watch_onload(func)
    {
        if (typeof(Loki) == "object" && Loki.is_document_ready()) {
            func();
            return;
        }
        
        if (document.addEventListener) {
            document.addEventListener('DOMContentLoaded', func, false);
            window.addEventListener('load', func, false);
        } else if (window.attachEvent) {
            window.attachEvent('onload', func);
        } else {
            window.onload = func;
        }
    }
    
    function create_hidden_iframe(src)
    {
        var called = false;
        var frame = Util.Document.create_element(document, 'iframe',
        {
            src: src,
            style: {
                position: 'absolute',
                box: [-500, -500, 2]
            }
        });
        
        function append_helper_iframe()
        {
            if (called)
                return;
            called = true;
            
            var body = (document.getElementsByTagName('BODY')[0] ||
                document.documentElement);
            body.appendChild(frame);
        }
        
        watch_onload(append_helper_iframe);
        
        return frame;
    }
    
    function make_uri(path)
    {
        if (base_uri.charAt(base_uri.length - 1) == '/')
            return base_uri + path;
        else
            return [base_uri, path].join('/');
    }
    
    if (UI.Clipboard_Helper._gecko) {
        // Gecko
        // Our clipboard support doesn't work there anymore. Dropping it.
    } else {
        // everyone else
        if (typeof(UI__Clipboard_Helper_Editable_Iframe__src) == 'string') {
            // PHP helper is providing this for us.
            helper_src = UI__Clipboard_Helper_Editable_Iframe__src;
        } else if (base_uri) {
            helper_src = make_uri('auxil/loki_blank.html');
        } else {
            return;
        }
        UI.Clipboard_Helper_Editable_Iframe = create_hidden_iframe(helper_src);
    }
    
    UI.Clipboard_Helper._setup_done = true;
}

UI.Clipboard_Helper._setup(); 
// file UI.Clipboard_Menugroup.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class representing a clipboard menugroup. 
 */
UI.Clipboard_Menugroup = function()
{
    Util.OOP.inherits(this, UI.Menugroup);

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._clipboard_helper = (new UI.Clipboard_Helper).init(this._loki);
        return this;
    };

    /**
     * Returns an array of menuitems, depending on the current context.
     * May return an empty array.
     */
    this.get_contextual_menuitems = function()
    {
        var menuitems = [];

        var self = this;
        menuitems.push( (new UI.Menuitem).init({ 
            label : 'Cut',
            listener : function()
            {
                self._clipboard_helper.cut();
            },
            disabled : this._clipboard_helper.is_selection_empty()
        }) );
        menuitems.push( (new UI.Menuitem).init({ 
            label : 'Copy',
            listener : function()
            {
                self._clipboard_helper.copy();
            },
            disabled : this._clipboard_helper.is_selection_empty()
        }) );
        menuitems.push( (new UI.Menuitem).init({ 
            label : 'Paste',
            listener : function()
            {
                self._clipboard_helper.paste();
            }
            //disabled : this._clipboard_helper.is_selection_empty()
        }) );
        menuitems.push( (new UI.Menuitem).init({ 
            label : 'Delete',
            listener : this._clipboard_helper.delete_it,
            disabled : this._clipboard_helper.is_selection_empty()
        }) );

        menuitems.push( (new UI.Separator_Menuitem).init() );

        menuitems.push( (new UI.Menuitem).init({ 
            label : 'Select all',
            listener : this._clipboard_helper.select_all
        }) );

        return menuitems;
    };
};

// file UI.Copy_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents toolbar button.
 */
UI.Copy_Button = function()
{
    Util.OOP.inherits(this, UI.Button);

    this.image = 'copy.png';
    this.title = 'Copy (Ctrl+C)';
    this.click_listener = function()
    {
        this._clipboard_helper.copy();
    };

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._clipboard_helper = (new UI.Clipboard_Helper).init(this._loki);
        return this;
    };
};

// file UI.Copy_Keybinding.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents keybinding.
 */
UI.Copy_Keybinding = function()
{
    Util.OOP.inherits(this, UI.Keybinding);

    this.test = function(e) {
        if (Util.Browser.Gecko && Util.Browser.Windows && !this.loki.owner_window.GeckoClipboard)
            return false;
        return this.matches_keycode(e, 67) && e.ctrlKey;
    }; // Ctrl-C
    
    this.action = function() 
    {
        // try-catch so that if anything should go wrong, copy
        // still happens
        try
        {
            this._clipboard_helper.copy();
            return false;
        }
        catch(e)
        {
            return true;
        }
    };

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._clipboard_helper = (new UI.Clipboard_Helper).init(this._loki);
        return this;
    };
};

// file UI.Cut_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents toolbar button.
 */
UI.Cut_Button = function()
{
    Util.OOP.inherits(this, UI.Button);

    this.image = 'cut.png';
    this.title = 'Cut (Ctrl+X)';
    this.click_listener = function()
    {
        this._clipboard_helper.cut();
    };

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._clipboard_helper = (new UI.Clipboard_Helper).init(this._loki);
        return this;
    };
};

// file UI.Cut_Keybinding.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents keybinding.
 */
UI.Cut_Keybinding = function()
{
    Util.OOP.inherits(this, UI.Keybinding);

    this.test = function(e) {
        if (Util.Browser.Gecko && Util.Browser.Windows && !this.loki.owner_window.GeckoClipboard)
            return false;
        return this.matches_keycode(e, 88) && e.ctrlKey;
    }; // Ctrl-X
    this.action = function() 
    {
        // try-catch so that if anything should go wrong, cut
        // still happens
        try
        {
            this._clipboard_helper.cut();
            return false;
        }
        catch(e)
        {
            return true;
        }
    };

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._clipboard_helper = (new UI.Clipboard_Helper).init(this._loki);
        return this;
    };
};

// file UI.Delete_Element_Keybinding.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents keybinding.
 */
UI.Delete_Element_Keybinding = function()
{
    Util.OOP.inherits(this, UI.Keybinding);

    this.test = function(e) { return ( this.matches_keycode(e, 8) || this.matches_keycode(e, 127) ); }; // Backspace or delete

    this.action = function()
    {
        if ( this._image_helper.is_selected() )
        {
            this._image_helper.remove_image();
            return false; // cancel event's default action
        }
        else if ( this._anchor_helper.is_selected() )
        {
            this._anchor_helper.remove_anchor();
            return false;
        }
        else if ( this._hr_helper.is_selected() )
        {
            this._hr_helper.remove_hr();
            return false;
        }
        else if ( this._table_helper.is_table_selected() && 
                  !this._table_helper.is_cell_selected() && 
                  confirm('Really remove table? WARNING: This cannot be undone.') )
        {
            this._table_helper.remove_table();
            return false;
        }
        else
        {
            // Prevent the following IE bug: "When there is no apparent focus (e.g. when the page first 
            // loads and you haven't done anything yet), clicking below the last element in the Loki 
            // area) and hitting backspace zaps all of the content in the Loki area and you lose the 
            // cursor."
            if (Util.Browser.IE) // not sure this restraint is necessary, but there's 
                                 // no point risking unexpected behavior in Gecko
            {
                this._loki.window.focus();
                //this._loki.exec_command('SelectAll');
                //var sel = Util.Selection.get_selection(this._loki.window);
                //Util.Selection.collapse(sel, false); // to end
            }
        }

        return true; // don't cancel event's default action
    };

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._image_helper = (new UI.Image_Helper).init(this._loki);
        this._anchor_helper = (new UI.Anchor_Helper).init(this._loki);
        this._hr_helper = (new UI.HR_Helper).init(this._loki);
        this._table_helper = (new UI.Table_Helper).init(this._loki);
        return this;
    };
};

// file UI.Dialog.js
/**
 * Declares instance variables. <code>init</code> must be called to
 * initialize instance variables.
 *
 * @constructor
 *
 * @class Base class for classes which represent dialog windows. Example usage:
 * <p>
 * <pre>
 * var dialog = new UI.Image_Dialog;   <br />
 * dialog.init({ data_source : '/fillmorn/feed.rss',   <br />
 *               submit_listener : this._insert_image,    <br />
 *               selected_item : { link : '/global_stock/images/1234.jpg' }      <br />
 * });     <br />
 * dialog.display();
 * </pre>
 */
UI.Dialog = function()
{
    this._external_submit_listener;
    this._data_source;
    this._base_uri;
    this._initially_selected_item;
    this._dialog_window;
    this._doc;
    this._udoc;

    this._dialog_window_width = 600;
    this._dialog_window_height = 300;

    /**
     * Initializes the dialog.
     *
     * @param   params  object containing the following named parameters:
     *                  <ul>
     *                  <li>data_source - the RSS feed from which to read this file</li>
     *                  <li>submit_listener - the function which will be called when
     *                  the dialog's submit button is pressed</li>
     *                  <li>selected_item - an object with the same properties as
     *                  the object passed by this._internal_submit_handler (q.v.) to
     *                  submit_handler (i.e., this._external_submit_handler). Used e.g. to
     *                  determine which if any image is initially selected.</li>
     *                  </ul>
     */
    this.init = function init_dialog(params)
    {
        this._data_source = params.data_source;
        this._base_uri = params.base_uri;
        this._external_submit_listener = params.submit_listener;
        this._remove_listener = params.remove_listener;
        this._initially_selected_item = params.selected_item;

        return this;
    };

    this.open = function open_dialog()
    {
        var self = this;
        
        function populate_dialog() {
            if (self._dialog_window._dialog_populated)
                return;
            
            self._dialog_window._dialog_populated = true;
            
            self._doc = self._dialog_window.window.document;
            self._dialog_window.document = self._doc;
            self._udoc = new Util.Document(self._doc);
            
            self._root =
                self._doc.body.appendChild(self._doc.createElement('DIV'));
            
            // Work around an IE display glitch: don't render until the document
            // has been built.
            if (Util.Browser.IE)
                self._doc.body.style.display = 'none';
            try {
                self._dialog_window.body = self._doc.body;
                self._set_title();
                self._append_style_sheets();
                self._add_dialog_listeners();
                self._append_main_chunk();
                self._apply_initially_selected_item();
            } finally {
                self._doc.body.style.display = '';
            }
        }
        
        var already_open = (this._dialog_window && this._dialog_window.window
            && !this._dialog_window.window.closed);
        
        if (already_open) {
            this._dialog_window.window.focus();
        } else {
            this._dialog_window = new Util.Window;
            var window_opened = this._dialog_window.open(
                this._base_uri + 'auxil/loki_dialog.html',
                '_blank', 'status=1,scrollbars=1,toolbars=1,resizable,width=' +
                    this._dialog_window_width + ',height=' + 
                    this._dialog_window_height + ',dependent=yes,dialog=yes'
            );
            
            if (!window_opened) // popup blocker
                return false;
            _loki_enqueue_dialog(this._dialog_window.window, populate_dialog);
            Util.Event.observe(this._dialog_window.window, 'load',
                populate_dialog);
        }
    };
    
    /**
     * Creates a new activity indicator (UI.Activity) for the dialog.
     */
    this.create_activity_indicator = function(kind, text)
    {
        if (!text)
            var text = null;
        
        return new UI.Activity(this._base_uri, this._dialog_window.document, kind, text);
    }
    
    /**
     * Creates a new form (Util.Form) for the dialog.
     */
    this.create_form = function(params)
    {
        if (!params)
            var params = {};
        return new Util.Form(this.dialog_window.document, params);
    }

    /**
     * Sets the page title
     */
    this._set_title = function() { /* do nothing by default */ };

    /**
     * Appends all the style sheets needed for this dialog.
     */
    this._append_style_sheets = function() { /* do nothing by default */ };

    /**
     * Adds all the dialog event listeners for this dialog.
     */
    this._add_dialog_listeners = function()
    {
        var self = this;
        var enter_unsafe =
            ['TEXTAREA', 'BUTTON', 'SELECT', 'OPTION'].toSet();
    
        
        //Util.Event.add_event_listener(this._dialog_window.body, 'keyup', function(event) 
        this._dialog_window.document.onkeydown = function(event)
        { 
            event = event == null ? self._dialog_window.window.event : event;
            var target = event.srcElement == null ? event.target : event.srcElement;

            // Enter key
            if (event.keyCode == 13 && target && !(target.tagName in enter_unsafe)) {
                self._internal_submit_listener();    
                return false;
            }
            
            if ( event.keyCode == 27 ) // escape
            {
                self._internal_cancel_listener();    
                return false;
            }

            // (IE) Disable refresh shortcut
            // [I should think IE and Gecko could be covered
            // together; but can't figure it out right now, tired.]
            if ( event.ctrlKey == true && event.keyCode == 82 ) // ctrl-r
            {
                return false;
            }
        };
        //});
        this._dialog_window.document.onkeypress = function(event)
        {
            event = event == null ? self._dialog_window.window.event : event;
            // (Gecko) Disable refresh shortcut
            if ( event.ctrlKey == true && event.charCode == 114 ) // ctrl-r
            {
                return false;
            }
        };

        /*
        this._dialog_window.window.onbeforeunload = 
        this._dialog_window.document.body.onbeforeunload = function(event)
        {
            event = event == null ? self._dialog_window.window.event : event;
            event.returnValue = "If you do navigate away, your changes in this dialog will be lost, and the dialog may close.";
            return event.returnValue;
        };

        this._dialog_window.window.onunload = function(event)
        {
            self._internal_cancel_listener();
        };
        */
    };

    /**
     * Appends the main part of the page, i.e. the children of the body element.
     */
    this._append_main_chunk = function()
    {
        this._main_chunk = this._dialog_window.document.createElement('FORM');
        this._main_chunk.action = 'javascript:void(0);';
        //this._dialog_window.body.appendChild(this._main_chunk);
        this._root.appendChild(this._main_chunk);

        this._populate_main();
    };

    /**
     * Stub for adding the main content of the dialog.
     */
    this._populate_main = function()
    {
        this._append_submit_and_cancel_chunk();
    };

    /**
     * Creates and appends a chunk containing submit and cancel
     * buttons. Also attaches 'click' event listeners to the submit and
     * cancel buttons: this._internal_submit_listener for submit, and
     * this._internal_cancel_listener for cancel.
     *
     * @param    submit_text        (optional) the text to use on the submit button. Defaults to "OK".
     * @param    cancel_text        (optional) the text to use on the cancel button. Defaults to "Cancel".
     */
    this._append_submit_and_cancel_chunk = function(submit_text, cancel_text)
    {
        var self = this;
        
        function create_button(text, click_listener) {
            var b = self._udoc.create_element('BUTTON', {type: 'button'}, [text]);
            Util.Event.add_event_listener(b, 'click', click_listener.bind(self));
            return b;
        }
        
        var chunk = this._doc.createElement('DIV');
        Util.Element.add_class(chunk, 'submit_and_cancel_chunk');
        
        var submit = create_button(submit_text || 'OK', this._internal_submit_listener);
        Util.Element.add_class(submit, 'ok');
        chunk.appendChild(submit);
        chunk.appendChild(create_button(cancel_text || 'Cancel', this._internal_cancel_listener));

        this._root.appendChild(chunk);
    };

    /**
     * Apply the initially selected item. Extending functions should do things
     * like setting the link_input's value to the initially_selected_item's uri.
     */
    this._apply_initially_selected_item = function()
    {
    };

    /**
     * This resizes the window to its content. 
     *
     * @param    horizontal    (boolean) Sometimes we don't want to resize horizontally 
     *                        to the content, because since the content is not fixed-width, 
     *                        it will expand to take up the whole screen, which is ugly. So
     *                        false here disables horiz resize.
     * @param    vertical    (boolean) same thing
     *                        
     */
    this._resize_dialog_window = function(horizontal, vertical)
    {
        // Skip IE // XXX bad
        if ( document.all )
            return;

        if ( horizontal == null )
            horizontal = true;
        if ( vertical == null )
            vertical = true;

        // From NPR.org
        var win = this._dialog_window.window;
        var doc = this._dialog_window.document;

        if (win.sizeToContent)    // Gecko
        {
            var w = win.outerWidth;
            var h = win.outerHeight;

            //win.resizeBy(win.innerWidth * 2, win.innerHeight * 2);
            //win.sizeToContent();    
            //win.sizeToContent();    
            //win.resizeBy(win.innerWidth + 10, win.innerHeight + 10);
            win.resizeBy(doc.documentElement.clientWidth + 10 + (win.outerWidth - win.innerWidth) - win.outerWidth, 
                         doc.documentElement.clientHeight + 20 + (win.outerHeight - win.innerHeight) - win.outerHeight);
            //win.resizeBy(this._root.clientWidth + 10 - win.outerWidth, 
            //             this._root.clientHeight + 10 - win.outerHeight);
            //win.resizeBy(win.innerWidth + 10 - win.outerWidth, 
            //             win.innerHeight + 10 - win.outerHeight);
            //win.resizeBy(10,0); 

/*
        try {
            win.scrollBy(1000, 1000);
            if (win.scrollX > 0 || win.scrollY > 0) {
                win.resizeBy(win.innerWidth * 2, win.innerHeight * 2);
                win.sizeToContent();
                win.scrollTo(0, 0);
                var x = parseInt(screen.width / 2.0) - (win.outerWidth / 2.0);
                var y = parseInt(screen.height / 2.0) - (win.outerHeight / 2.0);
                win.moveTo(x, y);
            }
            mb('resized dialog');
        } catch(e) { mb('error in resize_dialog_window:' + e.message); throw(e); }
*/


            if ( !horizontal )
                win.outerWidth = w;
            if ( !vertical )
                win.outerWidth = h;
        }
        else  // IE
        {  
            //old ie method, doesn't work for dialogs:
            win.resizeTo(100,100);  
            docWidth = Math.max(this._main_chunk.offsetWidth + 70, 200);  
            docHeight = Math.max(this._main_chunk.offsetHeight + 40, doc.body.scrollHeight) + 18;
            win.resizeTo(docWidth,docHeight);
            // not tested yet ...:
/*
            docWidth = Math.max(this._main_chunk.offsetWidth + 70, 200);  
            docHeight = Math.max(this._main_chunk.offsetHeight + 40, doc.body.scrollHeight) + 18;
            if ( horizontal )
                win.dialogWidth = docWidth;
            if ( vertical )
                win.dialogHeight = docHeight;
*/
        }
    };

    /**
     * Called as an event listener when the user clicks the submit
     * button. Extending functions should (a) gather information needed
     * to call the function referenced by this._submit_listener, (b) call
     * that function, and (c) close this dialog.
     */
    this._internal_submit_listener = function()
    {
        // Close dialog window
        this._dialog_window.window.close();
    };

    /**
     * Called as an event listener when the user clicks the submit
     * button. Extending functions may (a) gather information needed to
     * call the function referenced by this._submit_listener, and (b) call
     * that function. They should (c) close this dialog.
     */
    this._internal_cancel_listener = function()
    {
        // Close dialog window
        this._dialog_window.window.close();
    };
};

var _loki_dialog_queue = [];
var _loki_unmatched_dialogs = [];

function _loki_enqueue_dialog(dialog_window, onload) {
    var i;
    
    for (i = 0; i < _loki_unmatched_dialogs.length; i++) {
        if (_loki_unmatched_dialogs[i] === dialog_window) {
            _loki_unmatched_dialogs.splice(i, 1);
            onload();
            return;
        }
    }
    
    _loki_dialog_queue.push({window: dialog_window, onload: onload});
}

window._loki_dialog_postback = function(dialog_window) {
    var i, callback, called = false;
    
    for (i = 0; i < _loki_dialog_queue.length; i++) {
        if (_loki_dialog_queue[i].window === dialog_window) {
            callback = _loki_dialog_queue[i].onload;
            _loki_dialog_queue.splice(i, 1);
            
            if (!called) {
                callback();
                called = true;
            }
        }
    }
    
    if (!called) {
        _loki_unmatched_dialogs.push(dialog_window);
    }
};

// file UI.Double_Click.js
/**
 * Declares instance variables.
 * @class A body double-click listener. For extending only.
 */
UI.Double_Click = function DoubleClick()
{
    this.init = function(loki)
    {
        this._loki = loki;
        return this;
    };
    
    this.double_click = function() {
        throw new Error('unimplemented');
    };
};

// file UI.Error_Display.js
/**
 * @class Provides a nicely-formatted inline error display.
 * @constructor
 * @param {HTMLElement} the element into which the message will be inserted
 */
UI.Error_Display = function(message_container)
{
    var doc = message_container.ownerDocument;
    var dh = new Util.Document(doc);
    
    var self = this;
    
    this.display = null;
    
    function create(message, options)
    {
        if ('function' == typeof(options)) {
            options = [['Retry.', options]];
        }
        
        self.display = dh.create_element('p', {className: 'error'});
        self.display.innerHTML = message;
        
        function add_action(text, action) {
            var link = dh.create_element('a', {
                href: '#',
                className: 'action'
            });
            link.innerHTML = text;
            
            Util.Event.add_event_listener(link, 'click', function(e) {
                if (!e)
                    var e = window.event;

                try {
                    action();
                } catch (e) {
                    self.show('That didn't work: ' + (e.message || e), action);
                } finally {
                    return Util.Event.prevent_default(e);
                }
            });
            
            self.display.appendChild(link);
        }
        
        if (options) {
            options.each(function (action) {
               add_action(action[0], action[1]); 
            });
        }
        
        message_container.appendChild(self.display);
    }
    
    function remove()
    {
        if (this.display.parentNode)
            this.display.parentNode.removeChild(this.display);
        this.display = null;
    }
    
    this.show = function(message, retry, retry_text)
    {
        if (!retry)
            var retry = null;
        
        if (this.display)
            remove.call(this);
        
        create.call(this, message, retry, retry_text);
    }
    
    this.clear = function()
    {
        if (this.display)
            remove.call(this);
    }
}

// file UI.Error_State.js
/**
 * @class A canned state for a Util.State_Machine for displaying errors.
 * @see UI.Error_Display
 */
UI.Error_State = function(message_container)
{
    var display = new UI.Error_Display(message_container);
    var error = null;
    
    /**
     * Sets the error message. Note that in order for the message to really be
     * displayed, the machine must enter this state.
     *
     * @param    message    Error message to display (either a string or a
     *                     DocumentFragment).
     * @param    retry    If provided, the error message will include a "retry"
     *                    link that, if clicked on by the user, will call the
     *                    function provided here.
     */
    this.set = function(message, retry)
    {
        error = {message: message, retry: (retry || null)};
    }
    
    this.enter = function()
    {
        if (!error) {
            throw new Error('Entered error state, but there is no error!');
        }

        display.show(error.message, error.retry);
    }
    
    this.exit = function()
    {
        display.clear();
        error = null;
    }

// file UI.Find_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class for a find-and-replace button.
 */
UI.Find_Button = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Button);

    this.image = 'search_replace.png';
    this.title = 'Find and replace (Ctrl+F)';
    this.click_listener = function() { self._find_helper.open_dialog(); };

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._find_helper = (new UI.Find_Helper).init(this._loki);
        return this;
    };
};

// file UI.Find_Dialog.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class An anchor dialog window.
 */
UI.Find_Dialog = function()
{
    Util.OOP.inherits(this, UI.Dialog);

    this._dialog_window_width = 615;
    this._dialog_window_height = 200;

    this.init = function(params)
    {
        this._find_listener = params.find_listener;
        this._replace_listener = params.replace_listener;
        this._replace_all_listener = params.replace_all_listener;
        this._select_beginning_listener = params.select_beginning_listener;
        this.superclass.init.call(this, params);
    };

    this._set_title = function()
    {
        this._dialog_window.document.title = "Find and replace";
    };

    this._append_style_sheets = function()
    {
        this.superclass._append_style_sheets.call(this);
        Util.Document.append_style_sheet(this._dialog_window.document, this._base_uri + 'css/Find_Dialog.css');
    };

    this._populate_main = function()
    {
        this._append_find_chunk();
        this._append_submit_and_cancel_chunk();
        var self = this;
        setTimeout(function () { self._resize_dialog_window(true, true); }, 1000);
        //this._resize_dialog_window(false, true);
    };

    this._append_find_chunk = function()
    {
        var self = this;

        // Create Search input and label
        this._search_input = this._dialog_window.document.createElement('INPUT');
        this._search_input.setAttribute('size', '40');
        this._search_input.setAttribute('id', 'search_input');
        //this._search_input.value = 'as'; // XXX tmp

        var search_label = this._dialog_window.document.createElement('LABEL');
        Util.Element.add_class(search_label, 'label');
        search_label.setAttribute('for', 'search_input');
        search_label.innerHTML = 'Search&nbsp;for:&nbsp;';
        //search_label.appendChild( this._dialog_window.document.createTextNode('Search for: ') );

        var search_div = this._dialog_window.document.createElement('DIV');
        Util.Element.add_class(search_div, 'field');
        search_div.appendChild(search_label);
        search_div.appendChild(this._search_input);
        
        // Create Replace input and label
        this._replace_input = this._dialog_window.document.createElement('INPUT');
        this._replace_input.setAttribute('size', '40');
        this._replace_input.setAttribute('id', 'replace_input');
        //this._replace_input.value = 'hmm'; // XXX tmp

        var replace_label = this._dialog_window.document.createElement('LABEL');
        Util.Element.add_class(replace_label, 'label');
        replace_label.setAttribute('for', 'replace_input');
        replace_label.innerHTML = 'Replace&nbsp;with:&nbsp;';
        //replace_label.appendChild( this._dialog_window.document.createTextNode('Replace with: ') );

        var replace_div = this._dialog_window.document.createElement('DIV');
        Util.Element.add_class(replace_div, 'field');
        replace_div.appendChild(replace_label);
        replace_div.appendChild(this._replace_input);

        // Create Match Case checkbox and label
        this._matchcase_checkbox = this._dialog_window.document.createElement('INPUT');
        this._matchcase_checkbox.setAttribute('type', 'checkbox');
        this._matchcase_checkbox.setAttribute('id', 'matchcase_checkbox');

        var matchcase_label = this._dialog_window.document.createElement('LABEL');
        Util.Element.add_class(matchcase_label, 'label');
        matchcase_label.setAttribute('for', 'matchcase_checkbox');
        matchcase_label.appendChild( this._dialog_window.document.createTextNode('Match case') );

        // Create match case div
        var matchcase_div = this._dialog_window.document.createElement('DIV');
        Util.Element.add_class(matchcase_div, 'field');
        matchcase_div.appendChild(this._matchcase_checkbox);
        matchcase_div.appendChild(matchcase_label);

        // Create options div
        var options_div = this._dialog_window.document.createElement('DIV');
        Util.Element.add_class(options_div, 'options');
        options_div.appendChild(search_div);
        options_div.appendChild(replace_div);
        options_div.appendChild(matchcase_div);


        // Create Find Next button
        this._find_button = this._dialog_window.document.createElement('BUTTON');
        Util.Element.add_class(this._find_button, 'ok');
        this._find_button.setAttribute('type', 'submit');
        this._find_button.appendChild(this._dialog_window.document.createTextNode('Find Next'));
        Util.Event.add_event_listener(this._find_button, 'click', 
            function(event)
            {
                // Since this is a submit button (in order for "enter" in the inputs
                // to cause this button to be fired), the javascript:void(0) form
                // will be submitted when this button in clicked, and in FF 1.0 
                // that causes an error about transferring data from an encrypted
                // page over an unencrypted connection.
                // So prevent the form from being submitted.
                if ( event.preventDefault )
                    event.preventDefault();

                var ret = self._find_listener( self._search_input.value, 
                                               self._matchcase_checkbox.checked, 
                                               false, //self._findbackwards_checkbox.checked,
                                               true );
                if ( ret == UI.Find_Helper.NOT_FOUND && 
                     self._dialog_window.window.confirm('Match not found. Continue from beginning?') )
                {
                    self._select_beginning_listener();
                    var ret = self._find_listener( self._search_input.value, 
                                                   self._matchcase_checkbox.checked, 
                                                   false, //self._findbackwards_checkbox.checked,
                                                   true );
                    if ( ret == UI.Find_Helper.NOT_FOUND )
                        self._dialog_window.window.alert('Match not found.');
                }
            }
        );

        // Create Replace button
        this._replace_button = this._dialog_window.document.createElement('BUTTON');
        this._replace_button.setAttribute('type', 'button');
        this._replace_button.appendChild(this._dialog_window.document.createTextNode('Replace'));
        Util.Event.add_event_listener(this._replace_button, 'click', 
            function()
            {
                var ret = self._replace_listener( self._search_input.value, 
                                                  self._replace_input.value, 
                                                  self._matchcase_checkbox.checked, 
                                                  false, //self._findbackwards_checkbox.checked,
                                                  true );
                if ( ret == UI.Find_Helper.NOT_FOUND && 
                     self._dialog_window.window.confirm('Match not found. Continue from beginning?') )
                {
                    self._select_beginning_listener();
                    var ret = self._replace_listener( self._search_input.value, 
                                                      self._replace_input.value, 
                                                      self._matchcase_checkbox.checked, 
                                                      false, //self._findbackwards_checkbox.checked,
                                                      true );
                    if ( ret == UI.Find_Helper.NOT_FOUND )
                        self._dialog_window.window.alert('Match not found.');
                }

                if ( ret == UI.Find_Helper.REPLACED_LAST_MATCH && 
                     self._dialog_window.window.confirm('Replaced last match. Continue from beginning?') )
                {
                    self._select_beginning_listener();
                    var ret = self._find_listener( self._search_input.value, 
                                                   self._matchcase_checkbox.checked, 
                                                   false, //self._findbackwards_checkbox.checked,
                                                   true );
                    if ( ret == UI.Find_Helper.NOT_FOUND )
                        self._dialog_window.window.alert('Match not found.');
                }
            }
        );

        // Create Replace All button
        this._replaceall_button = this._dialog_window.document.createElement('BUTTON');
        this._replaceall_button.setAttribute('type', 'button');
        this._replaceall_button.appendChild(this._dialog_window.document.createTextNode('Replace All'));
        Util.Event.add_event_listener(this._replaceall_button, 'click', 
            function()
            {
                var i = self._replace_all_listener( self._search_input.value, 
                                                    self._replace_input.value, 
                                                    self._matchcase_checkbox.checked, 
                                                    false, //self._findbackwards_checkbox.checked,
                                                    true );
                if ( i < 1 )
                    self._dialog_window.window.alert('Not found.');
                else
                    self._dialog_window.window.alert('Replaced ' + i + ' instances.');
            }
        );

        /*
        // Create Cancel button
        this._cancel_button = this._dialog_window.document.createElement('BUTTON');
        this._cancel_button.setAttribute('type', 'button');
        this._cancel_button.appendChild(this._dialog_window.document.createTextNode('Close'));
        Util.Event.add_event_listener(this._cancel_button, 'click', function() { self._internal_cancel_listener(); });
        */

        // Create actions div
        var actions_div = this._dialog_window.document.createElement('DIV');
        Util.Element.add_class(actions_div, 'actions');

        var actions_ul = this._dialog_window.document.createElement('UL');
        actions_div.appendChild(actions_ul);

        var find_button_li = this._dialog_window.document.createElement('LI');
        var replace_button_li = this._dialog_window.document.createElement('LI');
        var replaceall_button_li = this._dialog_window.document.createElement('LI');
        actions_ul.appendChild(find_button_li);
        actions_ul.appendChild(replace_button_li);
        actions_ul.appendChild(replaceall_button_li);

        find_button_li.appendChild(this._find_button);
        replace_button_li.appendChild(this._replace_button);
        replaceall_button_li.appendChild(this._replaceall_button);

    /*
        // Create Find Backwards checkbox and label
        this._findbackwards_checkbox = this._dialog_window.document.createElement('INPUT');
        this._findbackwards_checkbox.setAttribute('type', 'checkbox');
        this._findbackwards_checkbox.setAttribute('id', 'findbackwards_checkbox');

        var findbackwards_label = this._dialog_window.document.createElement('LABEL');
        Util.Element.add_class(findbackwards_label, 'label');
        findbackwards_label.setAttribute('for', 'findbackwards_checkbox');
        findbackwards_label.appendChild( this._dialog_window.document.createTextNode('Find backwards') );
    */

        // Create heading
        var h1 = this._dialog_window.document.createElement('H1');
        h1.innerHTML = 'Find and replace';

        // Create fieldset and its legend
        var fieldset = new Util.Fieldset({legend : '', document : this._dialog_window.document});

        // Append options and actions to fieldset
        fieldset.fieldset_elem.appendChild(options_div);
        fieldset.fieldset_elem.appendChild(actions_div);
    /*
        fieldset.fieldset_elem.appendChild(this._findbackwards_checkbox);
        fieldset.fieldset_elem.appendChild(findbackwards_label);
    */

        // Append h1 and fieldset chunk to dialog
        this._main_chunk.appendChild(h1);
        this._main_chunk.appendChild(fieldset.chunk);
    };

    this._append_submit_and_cancel_chunk = function(submit_text, cancel_text)
    {
        // Init submit and cancel text
        submit_text = submit_text == null ? 'OK' : submit_text;
        cancel_text = cancel_text == null ? 'Close' : cancel_text;


        // Setup submit and cancel buttons

        var submit_button = this._dialog_window.document.createElement('BUTTON');
        var cancel_button = this._dialog_window.document.createElement('BUTTON');

        submit_button.setAttribute('type', 'button');
        cancel_button.setAttribute('type', 'button');

        submit_button.appendChild( this._dialog_window.document.createTextNode(submit_text) );
        cancel_button.appendChild( this._dialog_window.document.createTextNode(cancel_text) );

        var self = this;
        Util.Event.add_event_listener(submit_button, 'click', function() { self._internal_submit_listener(); });
        Util.Event.add_event_listener(cancel_button, 'click', function() { self._internal_cancel_listener(); });

        Util.Element.add_class(submit_button, 'ok');
        

        // Setup their containing chunk
        var submit_and_cancel_chunk = this._dialog_window.document.createElement('DIV');
        Util.Element.add_class(submit_and_cancel_chunk, 'submit_and_cancel_chunk');
        submit_and_cancel_chunk.appendChild(cancel_button);
        //submit_and_cancel_chunk.appendChild(submit_button);


        // Append their containing chunk
        //this._dialog_window.body.appendChild(submit_and_cancel_chunk);
        this._root.appendChild(submit_and_cancel_chunk);
    };
};

// file UI.Find_Helper.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class for finding and replacing.
 */
UI.Find_Helper = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Helper);

    this.open_dialog = function()
    {
        if ( this._dialog == null )
            this._dialog = new UI.Find_Dialog;
        this._dialog.init({ base_uri : self._loki.settings.base_uri,
                                    find_listener : self.find,
                                    replace_listener : self.replace,
                                    replace_all_listener : self.replace_all,
                                   select_beginning_listener : self.select_beginning });
        this._dialog.open();
    };

    this.find = function(search_str, match_case, match_backwards, wrap)
    {
        try // Gecko
        {
            // window.find( searchString, caseSensitive, backwards, wrapAround, showDialog, wholeWord, searchInFrames ) ;
            var was_found = self._loki.window.find(search_str, match_case, match_backwards, true, false, false);
            return was_found ? UI.Find_Helper.FOUND : UI.Find_Helper.NOT_FOUND;
    //oEditor.FCK.EditorWindow.find( document.getElementById('txtFind').value, bCase, false, false, bWord, false, false ) ;
        }
        catch(e)
        {
            try // IE
            {
                var flags = 0;
                //if ( whole_words_only )
                //    flags += 2;
                if ( match_case )
                    flags += 4;

                var sel = Util.Selection.get_selection(self._loki.window);
                var rng = Util.Range.create_range(sel);

                if ( rng != null )
                {
                    rng.collapse(false);
                    var was_found = rng.findText(search_str, 10000000, flags);
                    if ( was_found )
                        rng.select();
                }

                return was_found ? UI.Find_Helper.FOUND : UI.Find_Helper.NOT_FOUND;
            }
            catch(f)
            {
                throw(new Error('UI.Find_Helper.find: Neither the Gecko nor the IE way of finding text worked. When the Mozilla way was tried, an error with the following message was thrown: <<' + e.message + '>>. When the IE way was tried, an error with the following message was thrown: <<' + f.message + '>>.'));
            }
        }
    };

    this.replace = function(search_str, replace_str, match_case, match_backwards, wrap)
    {
        var sel = Util.Selection.get_selection(self._loki.window);
        var rng = Util.Range.create_range(sel);
        
        // If the search string isn't already selected,
        // this is presumably the first time the user is 
        // clicking the "replace" button (and hasn't already
        // clicked "find"), so we need to do that before we
        // replace anything.
        if ( Util.Range.get_text(rng).toLowerCase() != search_str.toLowerCase() )
        {
            /*
            if ( match_backwards )
                Util.Selection.collapse(sel, false); // to end
            else
                Util.Selection.collapse(sel, true); // to start

            var matched = self.find(search_str, match_case, match_backwards, wrap);
            if ( matched == UI.Find_Helper.NOT_FOUND )
                return UI.Find_Helper.NOT_FOUND;
            */

            return self.find(search_str, match_case, match_backwards, wrap);
        }
        else
        {
            sel = Util.Selection.get_selection(self._loki.window);
            Util.Selection.paste_node(sel, self._loki.document.createTextNode(replace_str));

            var matched = self.find(search_str, match_case, match_backwards, wrap);
            if ( matched == UI.Find_Helper.NOT_FOUND )
                return UI.Find_Helper.REPLACED_LAST_MATCH;

            return UI.Find_Helper.REPLACED;
        }
    };

    this.replace_all = function(search_str, replace_str, match_case, match_backwards)
    {
        self.select_beginning();

        var matched = true;
        var i = 0;
        while ( matched != UI.Find_Helper.NOT_FOUND && i < 500 ) // to be safe
        {
            matched = self.replace(search_str, replace_str, match_case, match_backwards, false);
            if ( matched == UI.Find_Helper.REPLACED || matched == UI.Find_Helper.REPLACED_LAST_MATCH )
                i++;
        }
        return i;
    };

    this.select_beginning = function()
    {
        sel = Util.Selection.get_selection(self._loki.window);
        //Util.Selection.select_node(sel, self._loki.document.getElementsByTagName('BODY')[0]);
        Util.Selection.select_node_contents(sel, self._loki.document.getElementsByTagName('BODY')[0]);
        Util.Selection.collapse(sel, true);
    };
};

UI.Find_Helper.FOUND = 1;
UI.Find_Helper.NOT_FOUND = 2;
UI.Find_Helper.REPLACED = 3;
UI.Find_Helper.REPLACED_LAST_MATCH = 4;

// file UI.Find_Keybinding.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents keybinding.
 */
UI.Find_Keybinding = function()
{
    Util.OOP.inherits(this, UI.Keybinding);

    this.test = function(e) { return ( this.matches_keycode(e, 70) || this.matches_keycode(e, 72) ) && e.ctrlKey; }; // Ctrl-F or Ctrl-H
    this.action = function() { this._find_helper.open_dialog(); };

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._find_helper = (new UI.Find_Helper).init(this._loki);
        return this;
    };
};

// file UI.HR_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents "Insert HR" toolbar button.
 */
UI.HR_Button = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Button);

    this.image = 'hr.png';
    this.title = 'Horizontal rule';
    this.click_listener = function() { self._hr_helper.insert_hr(); };

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._hr_helper = (new UI.HR_Helper).init(this._loki);
        return this;
    };
};

// file UI.HR_Helper.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class for helping insert an hr. Contains code
 * common to both the button and the menu item.
 */
UI.HR_Helper = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Helper);
    
    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._masseuse = (new UI.HR_Masseuse).init(this._loki);
        return this;
    };

    this.is_selected = function()
    {
        return !!_get_selected_hr();
    };

    var _get_selected_hr = function()
    {
        var sel = Util.Selection.get_selection(self._loki.window);
        var rng = Util.Range.create_range(sel);
        return Util.Range.get_nearest_ancestor_element_by_tag_name(rng, 'HR');
    };

    this.insert_hr = function()
    {
        var sel = Util.Selection.get_selection(self._loki.window);
        var hr = self._loki.document.createElement('HR');
        Util.Selection.paste_node(sel, self._masseuse.wrap(hr));
        //Util.Selection.select_node(sel, hr);
        //Util.Selection.collapse(sel, false);
        window.focus();
        self._loki.window.focus();
    };

    this.remove_hr = function()
    {
        var sel = Util.Selection.get_selection(self._loki.window);
        var rng = Util.Range.create_range(sel);
        var hr = Util.Range.get_nearest_ancestor_element_by_tag_name(rng, 'HR');
        var target = self._removal_target(hr);

        // Move cursor
        Util.Selection.select_node(sel, target);
        Util.Selection.collapse(sel, false); // to end
        self._loki.window.focus();

        if ( target.parentNode != null )
            target.parentNode.removeChild(target);
    };
    
    this._removal_target = function(hr)
    {
        var p = hr.parentNode;
        return (Util.Node.is_tag(p, 'DIV') && 'hr' == p.getAttribute('loki:container'))
            ? p
            : hr;
    };
};

// file UI.HR_Masseuse.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class for making horizontal rule elements easier to delete.
 */
UI.HR_Masseuse = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Masseuse);
    
    this.massage_node_descendants = function(node)
    {
        Util.Array.for_each(node.getElementsByTagName('HR'),
            self.massage_node, self);
    };
    
    this.unmassage_node_descendants = function(node)
    {
        var div_elements = Util.Array.from(node.getElementsByTagName('DIV'));
        
        div_elements.each(function(div) {
            if (div.getAttribute('loki:container') == 'hr') {
                this.unmassage_node(div);
            }
        }, self);
    };
    
    this.massage_node = function(node)
    {
        var container = self._create_container(node);
        node.parentNode.replaceChild(container, node);
        container.appendChild(node);
        self._add_delete_button(container);
    };
    
    this.wrap = function(node)
    {
        var container = self._create_container(node);
        container.appendChild(node);
        self._add_delete_button(container);
        
        return container;
    };
    
    this.unmassage_node = function(node)
    {
        var r = self.get_real(node) || node.ownerDocument.createElement('HR');
        node.parentNode.replaceChild(r, node);
    };
    
    this.get_real = function(node)
    {
        return Util.Node.get_last_child_node(node,
            Util.Node.curry_is_tag('HR'));
    }
    
    this._create_container = function(node)
    {
        var div = node.ownerDocument.createElement('DIV');
        Util.Element.add_class(div, 'loki__hr_container');
        div.setAttribute('loki:fake', 'true');
        div.setAttribute('loki:container', 'hr');
        return div;
    };
    
    this._add_delete_button = function(container)
    {
        var doc = container.ownerDocument;
        var link = doc.createElement('A');
        link.title = 'Click to remove this horizontal line.'
        Util.Element.add_class(link, 'loki__delete');
        
        /*var span = doc.createElement('SPAN');
        span.appendChild(doc.createTextNode('Remove'));
        link.appendChild(span);*/
        
        Util.Event.add_event_listener(container, 'mouseover', function() {
            link.style.display = 'block';
        });
        
        Util.Event.add_event_listener(container, 'mouseout', function() {
            link.style.display = '';
        });
        
        Util.Event.add_event_listener(link, 'click', function(e) {
            if (!e) var e = window.event;
            
            container.parentNode.removeChild(container);
            
            return Util.Event.prevent_default(e);
        })
        
        container.appendChild(link);
    };
}; 
// file UI.Headline_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents "headline" toolbar button.
 */
UI.Headline_Button = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Button);

    this.image = 'head.png';
    this.title = 'Heading';
    this.click_listener = function() { self._loki.toggle_block('h3'); };
    this.state_querier = function() { return self._loki.query_command_state('FormatBlock') == 'h3'; };
};

// file UI.Headline_Menugroup.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class representing a headline menugroup. 
 */
UI.Headline_Menugroup = function()
{
    Util.OOP.inherits(this, UI.Menugroup);

    /**
     * Returns an array of menuitems, depending on the current context.
     * May return an empty array.
     */
    this.get_contextual_menuitems = function()
    {
        var menuitems = [];

        var self = this;
        if ( this._is_h3() )
        {
            menuitems.push( (new UI.Menuitem).init({ 
                //label : 'Subordinate headline',
                label : 'Change to minor heading (h4)',
                listener : function() { self._toggle_h4(); }
            }) );
            menuitems.push( (new UI.Menuitem).init({ 
                label : 'Remove headline',
                listener : function() { self._toggle_h3(); }
            }) );
        }
        else if ( this._is_h4() )
        {
            menuitems.push( (new UI.Menuitem).init({ 
                //label : 'Superordinate headline',
                label : 'Change to major heading (h3)',
                listener : function() { self._toggle_h3(); }
            }) );
            menuitems.push( (new UI.Menuitem).init({ 
                label : 'Remove headline',
                listener : function() { self._toggle_h4(); }
            }) );
        }

        return menuitems;
    };

    this._toggle_h3 = function()
    {
        this._loki.toggle_block('h3');
    };

    this._toggle_h4 = function()
    {
        this._loki.toggle_block('h4');
    };

    this._is_h3 = function()
    {
        return this._loki.query_command_value('FormatBlock') == 'h3';
    };

    this._is_h4 = function()
    {
        return this._loki.query_command_value('FormatBlock') == 'h4';
    };
};

// file UI.Helper.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class for helping perform action. Contains code
 * common to both the button and the menugroup for doing whatever
 * the action is.
 */
UI.Helper = function()
{    
    this.init = function(loki)
    {
        this._loki = loki;
        return this;
    };
};

// file UI.Highlight_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents "highlight" toolbar button.
 */
UI.Highlight_Button = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Button);

    this.image = 'highlight.png';
    this.title = 'Highlight';
    this.click_listener = function() { self._helper.toggle_blockquote_paragraph(); };
    this.state_querier = function() { return self._helper.query_blockquote_paragraph(); };

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._helper = (new UI.Blockquote_Highlight_Helper).init(this._loki, 'highlight');
        return this;
    };
};

// file UI.Highlight_Menugroup.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class representing an align menugroup. 
 */
UI.Highlight_Menugroup = function()
{
    Util.OOP.inherits(this, UI.Menugroup);

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._helper = (new UI.Blockquote_Highlight_Helper).init(this._loki, 'highlight');
        return this;
    };

    /**
     * Returns an array of menuitems, depending on the current context.
     * May return an empty array.
     */
    this.get_contextual_menuitems = function()
    {
        var menuitems = [];

        var self = this;
        if ( this._helper.is_blockquoteable() )
        {
            menuitems.push( (new UI.Menuitem).init({ 
                label : 'Highlight',
                listener : function() { self._helper.toggle_blockquote_paragraph(); }
            }) );
        }

        return menuitems;
    };
};

// file UI.Image_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class for inserting an image.
 */
UI.Image_Button = function()
{
    var self = this;
    Util.OOP.inherits(this, UI.Button);

    this.image = 'image.png';
    this.title = 'Insert image';
    this.click_listener = function() { self._helper.open_dialog(); };

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._helper = (new UI.Image_Helper).init(this._loki);
        return this;
    };
};

// file UI.Image_Dialog.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class An image dialog window.
 */
UI.Image_Dialog = function()
{
    Util.OOP.inherits(this, UI.Dialog);

    this._dialog_window_width = 625;
    this._dialog_window_height = 600;

    this.init = function(params)
    {
        // use rss integration only if data_source is given:
        this._use_rss = !!params.data_source;
        this.superclass.init.call(this, params);
        return this;
    };

    this._set_title = function()
    {
        if ( !this._initially_selected_item )
            this._dialog_window.document.title = 'Insert image';
        else
            this._dialog_window.document.title = 'Edit image';
    };

    this._append_style_sheets = function()
    {
        this.superclass._append_style_sheets.call(this);
        Util.Document.append_style_sheet(this._dialog_window.document, this._base_uri + 'css/Listbox.css');
        Util.Document.append_style_sheet(this._dialog_window.document, this._base_uri + 'css/Image_Listbox.css');
        Util.Document.append_style_sheet(this._dialog_window.document, this._base_uri + 'css/Tabset.css');
    };

    this._populate_main = function()
    {
        this._append_heading();
        this._append_tabset();
        if ( this._use_rss )
            this._append_image_listbox();
        this._append_image_custom();
        if ( this._use_rss )
            this._append_image_options_chunk('listbox');
        this._append_image_options_chunk('custom');
        this._append_remove_image_chunk();
        var self = this;
        setTimeout(function () { self._resize_dialog_window(false, true); }, 1000);
        this.superclass._populate_main.call(this);
    };

    this._append_heading = function()
    {
        var h1 = this._dialog_window.document.createElement('H1');
        if ( !this._initially_selected_item )
            h1.innerHTML = 'Insert:';
        else
            h1.innerHTML = 'Edit:';
        this._main_chunk.appendChild(h1);
    };

    this._append_tabset = function()
    {
        this._tabset = new Util.Tabset({document : this._dialog_window.document});        
        if ( this._use_rss )
            this._tabset.add_tab('listbox', 'existing image');
        this._tabset.add_tab('custom', 'image at web address');
        this._main_chunk.appendChild(this._tabset.tabset_elem);
    };

    this._append_image_listbox = function()
    {
        // Instantiate a listbox to display the images 
        this._image_listbox = new UI.Image_Listbox;
        this._image_listbox.init('image_listbox', this._dialog_window.document,
            {chunk_transfer_size: 500});

        // Append the listbox's root element. (Do
        // this here rather than later so that the listbox items are
        // displayed as they load.)
        var listbox_elem = this._image_listbox.get_listbox_elem();
        this._tabset.get_tabpanel_elem('listbox').appendChild(listbox_elem);

        // Setup test for initially selected item
        var self = this;
        function is_initially_selected(item)
        {            
            if ( !item || !item.link || !self._initially_selected_item || !self._initially_selected_item.uri )
                return false;
            else
            {
                var item_uri = Util.URI.strip_https_and_http(item.link);
                var enclosure_uri = (item.enclosure)
                    ? Util.URI.strip_https_and_http(item.enclosure.url)
                    : null;
                var initial_uri = Util.URI.strip_https_and_http(self._initially_selected_item.uri);
                
                if (item_uri == initial_uri || enclosure_uri == initial_uri) {
                    self._tabset.select_tab('listbox');
                    return true;
                } else {
                    return false;
                }
            }
        };
        
        function url_maker(offset, num)
        {
            return Util.URI.append_to_query(this._data_source,
                {start: offset, num: num});
        }
        
        this._image_listbox.add_event_listener('change', function() {
            var item = this._image_listbox.get_selected_item();
            
            if (item.enclosure) {
                this._listbox_size_chunk.style.display = '';
                
                if (this._initially_selected_item && this._initially_selected_item.uri) {
                    var isu = Util.URI.strip_https_and_http(
                        this._initially_selected_item.uri);

                    if (isu == Util.URI.strip_https_and_http(item.enclosure.url)) {
                        this._listbox_tn_size_radio.input_elem.checked = true;
                        this._listbox_full_size_radio.input_elem.checked = false;
                    } else if (isu == Util.URI.strip_https_and_http(item.link)) {
                        this._listbox_tn_size_radio.input_elem.checked = false;
                        this._listbox_full_size_radio.input_elem.checked = true;
                    }
                }
            } else {
                this._listbox_size_chunk.style.display = 'none';
            }
        }.bind(this));

        // Append to the listbox items retrieved using an RSS feed
        var reader = new Util.RSS.Reader(url_maker.bind(this));
        this._image_listbox.load_rss_feed(reader, is_initially_selected)
    };

    this._append_image_custom = function()
    {
        // Create widgets
        var custom_uri_label = this._doc.createElement('LABEL');
        custom_uri_label.appendChild(this._doc.createTextNode('Location: '));
        custom_uri_label.htmlFor = 'custom_uri_input';

        this._custom_uri_input = this._doc.createElement('INPUT');
        this._custom_uri_input.id = 'custom_uri_input';
        this._custom_uri_input.type = 'text';
        this._custom_uri_input.setAttribute('size', '40');

        var custom_uri_div = this._doc.createElement('DIV');
        custom_uri_div.appendChild(custom_uri_label);
        custom_uri_div.appendChild(this._custom_uri_input);

        var custom_alt_label = this._doc.createElement('LABEL');
        custom_alt_label.appendChild(this._doc.createTextNode('Description: '));
        custom_alt_label.htmlFor = 'custom_alt_input';

        this._custom_alt_input = this._doc.createElement('INPUT');
        this._custom_alt_input.id = 'custom_alt_input';
        this._custom_alt_input.type = 'text';
        this._custom_alt_input.setAttribute('size', '40');

        var custom_alt_label2 = this._doc.createElement('DIV');
        custom_alt_label2.appendChild(this._doc.createTextNode('This description will be used if the image cannot be displayed or the user is visually disabled.'));

        var custom_alt_div = this._doc.createElement('DIV');
        custom_alt_div.appendChild(custom_alt_label);
        custom_alt_div.appendChild(this._custom_alt_input);

        // Create table
        var table = this._doc.createElement('TABLE');
        var tbody = table.appendChild(this._doc.createElement('TBODY'));

        var tr = tbody.appendChild(this._doc.createElement('TR'));
        var td = tr.appendChild(this._doc.createElement('TD'))
        td.appendChild(custom_uri_label);
        var td = tr.appendChild(this._doc.createElement('TD'))
        td.appendChild(this._custom_uri_input);

        var tr = tbody.appendChild(this._doc.createElement('TR'));
        var td = tr.appendChild(this._doc.createElement('TD'))
        td.appendChild(custom_alt_label);
        var td = tr.appendChild(this._doc.createElement('TD'))
        td.appendChild(this._custom_alt_input);

        // Append it all
        var custom_tabpanel = this._tabset.get_tabpanel_elem('custom');
        var fieldset = new Util.Fieldset({legend : '', document : this._dialog_window.document});
        custom_tabpanel.appendChild(fieldset.fieldset_elem);
        fieldset.fieldset_elem.appendChild(table);

        // Init
        if ( !this._initially_selected_item || !this._initially_selected_item.uri ) 
        {
            this._custom_uri_input.value = 'http://';
        }
        else
        {
            this._tabset.select_tab('custom');
            this._custom_uri_input.value = this._initially_selected_item.uri;
            this._custom_alt_input.value = this._initially_selected_item.alt;
        }
    };

    /**
     * Appends a chunk containing image options.
     */
    this._append_image_options_chunk = function(tabname)
    {
        // Create fieldset
        var fieldset = new Util.Fieldset({legend : 'Image options', document : this._dialog_window.document});

        // Add to fieldset
        if ( tabname == 'listbox' )
            fieldset.fieldset_elem.appendChild(this._create_size_chunk(tabname));
        fieldset.fieldset_elem.appendChild(this._create_align_chunk(tabname));
        //this._append_border_chunk();

        // We need to add a dummy div styled clear:both so the CSS works
        // right
        var clearer = this._dialog_window.document.createElement('DIV');
        Util.Element.add_class(clearer, 'clearer');
        fieldset.fieldset_elem.appendChild(clearer);

        // Append it all
        this._tabset.get_tabpanel_elem(tabname).appendChild(fieldset.fieldset_elem);
    };
    

    /**
     * Creates a chunk containing radio inputs asking whether to use a
     * thumbnail or full-sized image.
     */
    this._create_size_chunk = function(tabname)
    {
        // Create radios
        this['_' + tabname + '_tn_size_radio'] = new Util.Radio({
            id : tabname + '_tn_size_radio', 
            tabname : tabname + '_size', 
            label : 'Thumbnail', 
            value : 'tn', 
            checked: true, 
            document : this._dialog_window.document
        });
        this['_' + tabname + '_full_size_radio'] = new Util.Radio({
            id : tabname + '_full_size_radio', 
            tabname : tabname + '_size', 
            label : 'Full', 
            value : 'full',  
            checked: false, 
            document : this._dialog_window.document
        });

        // Create fieldset and its legend
        var fieldset = new Util.Fieldset({legend : 'Size', document : this._dialog_window.document});

        // Append radios and labels to fieldset
        fieldset.fieldset_elem.appendChild(this['_' + tabname + '_tn_size_radio'].chunk);
        fieldset.fieldset_elem.appendChild(this['_' + tabname + '_full_size_radio'].chunk);

        this['_' + tabname + '_size_chunk'] = fieldset.chunk;

        // Return fieldset chunk
        return fieldset.chunk;
    };

    /**
     * Creates a chunk containing image align options.
     */
    this._create_align_chunk = function(tabname)
    {
        // Check for initial value
        if ( this._initially_selected_item &&
             this._initially_selected_item.align )
        {
            var align_left = this._initially_selected_item.align == 'left';
            var align_right = this._initially_selected_item.align == 'right';
        }
        var align_none = !align_left && !align_right;

        // Create radios
        this['_' + tabname + '_align_none_radio'] = new Util.Radio({
            id : tabname + '_align_none_radio', 
            name : tabname + '_align', 
            label : 'None', 
            value : 'none', 
            checked : align_none, 
            document : this._dialog_window.document
        });
        this['_' + tabname + '_align_left_radio'] = new Util.Radio({
            id : tabname + '_align_left_radio', 
            name : tabname + '_align', 
            label : 'Left', 
            value : 'left', 
            checked : align_left, 
            document : this._dialog_window.document
        });
        this['_' + tabname + '_align_right_radio'] = new Util.Radio({
            id : tabname + '_align_right_radio', 
            name : tabname + '_align', 
            label : 'Right', 
            value : 'right', 
            checked : align_right, 
            document : this._dialog_window.document
        });

        // Create fieldset and its legend
        var fieldset = new Util.Fieldset({legend : 'Alignment', document : this._dialog_window.document});

        // Append radios and labels to fieldset
        fieldset.fieldset_elem.appendChild(this['_' + tabname + '_align_none_radio'].chunk);
        fieldset.fieldset_elem.appendChild(this['_' + tabname + '_align_left_radio'].chunk);
        fieldset.fieldset_elem.appendChild(this['_' + tabname + '_align_right_radio'].chunk);

        // Return fieldset chunk
        return fieldset.chunk;
    };

    /**
     * Appends a chunk containing image border options.
     */
    this._append_border_chunk = function()
    {
        // Create radios
        this._border_yes_radio = new Util.Radio({id : 'border_yes_radio', name : 'border', label : 'Yes', value : 'yes', checked: true, document : this._dialog_window.document});
        this._border_no_radio = new Util.Radio({id : 'border_no_radio', name : 'border', label : 'No', value : 'no', document : this._dialog_window.document});

        // Create fieldset and its legend
        var fieldset = new Util.Fieldset({legend : 'Border', document : this._dialog_window.document});

        // Append radios and labels to fieldset
        fieldset.fieldset_elem.appendChild(this._border_yes_radio.chunk);
        fieldset.fieldset_elem.appendChild(this._border_no_radio.chunk);

        // Append fieldset chunk to dialog
        this._image_options_chunk.appendChild(fieldset.chunk);
    };

    /**
     * Creates and appends a chunk containing a "remove image" button. 
     * Also attaches 'click' event listeners to the button.
     */
    this._append_remove_image_chunk = function()
    {
        var button = this._dialog_window.document.createElement('BUTTON');
        button.setAttribute('type', 'button');
        button.appendChild( this._dialog_window.document.createTextNode('Remove image') );

        var self = this;
        Util.Event.add_event_listener(button, 'click', function() {
            self._remove_listener();
            self._dialog_window.window.close();
        });

        // Setup their containing chunk
        var chunk = this._dialog_window.document.createElement('DIV');
        Util.Element.add_class(chunk, 'remove_chunk');
        chunk.appendChild(button);

        // Append the containing chunk
        this._dialog_window.body.appendChild(chunk);
    };

    /**
     * Called as an event listener when the user clicks the submit
     * button. 
     */
    this._internal_submit_listener = function()
    {
        if ( this._tabset.get_name_of_selected_tab() == 'listbox' )
        {
            // Get selected item
            var img_item = this._image_listbox.get_selected_item();
            if ( img_item == null )
            {
                this._dialog_window.window.alert('Please select an image to insert.');
                return false;
            }

            // Determine uri
            var uri = (img_item.enclosure && this._listbox_tn_size_radio.input_elem.checked)
                ? Util.URI.strip_https_and_http(img_item.enclosure.url)
                : Util.URI.strip_https_and_http(img_item.link);

            // Determine alt text
            var alt = img_item.title;
        }
        else // if ( this._tabset.get_name_of_selected_tab() == 'custom' )
        {
            var uri = this._custom_uri_input.value;
            var alt = this._custom_alt_input.value;

            if ( uri == '' )
            {
                this._dialog_window.window.alert("Please enter the image's location.");
                return false;
            }
            if ( alt == '' )
            {
                this._dialog_window.window.alert("Please enter the image's description (alt text).");
                return false;
            }
        }

        // Determine align
        var tabname = this._tabset.get_name_of_selected_tab()
        var align;
        if ( this['_' + tabname + '_align_left_radio'].input_elem.checked )
            align = 'left';
        else if ( this['_' + tabname + '_align_right_radio'].input_elem.checked )
            align = 'right';
        else //if ( this['_' + tabname + '_align_none_radio'].input_elem.checked )
            align = '';

    /*
        // Determine border
        var border;
        if ( this._border_yes_radio.input_elem.checked )
            border = 'yes';
        else //if ( this._border_no_radio.input_elem.checked )
            border = 'no';
    */

        // TODO: Determine height and width of image

        // Call external event listener
        this._external_submit_listener({uri : uri, alt : alt, align : align});

        // Close dialog window
        this._dialog_window.window.close();
    };
};

// file UI.Image_Double_Click.js
UI.Image_Double_Click = function ImageDoubleClick() {
    Util.OOP.inherits(this, UI.Double_Click);
    this.helper = null;
    
    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this.helper = (new UI.Image_Helper).init(loki);
        return this;
    };
    
    this.double_click = function() {
        if (this.helper.is_selected())
            this.helper.open_dialog();
    };
};

// file UI.Image_Helper.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class for helping insert an image. Contains code
 * common to both the button and the menu item.
 */
UI.Image_Helper = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Helper);

    this.init = function(loki)
    {
        this._loki = loki;
        this._image_masseuse = (new UI.Image_Masseuse()).init(this._loki);
        return this;
    };
    
    this.get_selected_image = function get_selected_image()
    {
        var sel = Util.Selection.get_selection(self._loki.window);
        var rng = Util.Range.create_range(sel);
        
        var images;
        var image;
        var real_image;
        var anchor_masseuse = (new UI.Anchor_Masseuse).init(this._loki);
        
        function is_valid_image(node) {
            if (!Util.Node.is_tag(node, 'IMG'))
                return false;
            
            return !anchor_masseuse.is_placeholder(node);
        }
        
        images = Util.Range.find_nodes(rng, is_valid_image, true);
        
        if (!images || !images.length) {
            return null;
        } else if (images.length > 1) {
            throw new UI.Multiple_Items_Error('Multiple images are currently ' +
                'selected.');
        }
        
        image = images[0];
        
        return this._image_masseuse.realize_elem(image);
    };
    
    this.get_selected_item = function get_selected_image_info()
    {
        var image = this.get_selected_image();
        if (!image)
            return null;
        
        return {
            uri: image.src,
            alt: image.alt,
            align: image.align
        };
    };

    this.is_selected = function image_is_selected()
    {
        try {
            return !!this.get_selected_image();
        } catch (e) {
            if (e.name == 'UI.Multiple_Items_Error')
                return true;
            throw e;
        }
    };
    
    this.open_dialog = function open_image_dialog()
    {
        var selected_image;
        
        try {
            selected_image = this.get_selected_item();
        } catch (e) {
            if (e.name == 'UI.Multiple_Items_Error') {
                alert('Multiple images are currently selected. Please narrow ' +
                    'down your selection so that it only contains one image.');
                return;
            } else {
                throw e;
            }
        }
        
        if (!this._image_dialog)
            this._image_dialog = new UI.Image_Dialog();
        
        this._image_dialog.init({
            data_source: self._loki.settings.images_feed,
            base_uri: self._loki.settings.base_uri,
            submit_listener: self.insert_image,
            remove_listener: self.remove_image,
            selected_item: selected_image
        });
        this._image_dialog.open();
    };
    
    this.insert_image = function insert_image(params)
    {
        var image, clean_src, selected_image, sel, range;
        
        image = self._loki.document.createElement('IMG');
        clean_src = UI.Clean.clean_URI(params.uri);
        
        image.src = clean_src;
        image.alt = params.alt;
        
        if (params.align)
            image.align = params.align;
        
        image = self._image_masseuse.get_fake_elem(image);
        
        self._loki.window.focus();
        selected_image = self.get_selected_image();
        if (selected_image) {
            selected_image.parentNode.replaceChild(image, selected_image);
        } else {
            sel = Util.Selection.get_selection(self._loki.window);
            rng = Util.Range.create_range(sel);
            
            Util.Range.delete_contents(rng);
            Util.Range.insert_node(rng, image);
        }
    };

    this.remove_image = function remove_image()
    {
        var image, sel;
        
        image = self.get_selected_image();
        
        if (!image)
            return false;
        
        sel = Util.Selection.get_selection(self._loki.window);

        // Move cursor
        Util.Selection.select_node(sel, image);
        Util.Selection.collapse(sel, false); // to end
        self._loki.window.focus();

        if (image.parentNode)
            image.parentNode.removeChild(image);
        return true;
    };
};

// file UI.Image_Listbox.js
/**
 * Does nothing, since all necessary instance variables are declared in Listbox's constructor.
 *
 * @constructor
 *
 * @class Represents a listbox for images. This was designed for use
 * in Loki's image-insertion dialog box, but may be useful for other
 * applications.
 */
UI.Image_Listbox = function()
{
    Util.OOP.inherits(this, UI.Listbox);
    
    /**
     * Creates the document chunk for each item. Differs from
     * Listbox._create_item_chunk in that it displays the image at
     * <code>item.link</code>. Requires that each <code>item</code> contain
     * at least <code>title</code>, <code>description</code>, and
     * <code>link</code> properties.
     *
     * @param    item    the item from which to create the chunk
     * @private
     */
    this._create_item_chunk = function(item)
    {
        function use_enclosure_url()
        {
            if (!item.enclosure || !item.enclosure.type || !item.enclosure.url)
                return false;
            
            return item.enclosure.type.match(/^image//);
        }
        
        //var item_chunk = this._doc_obj.createElement('DIV');
        var item_chunk = this._doc_obj.createElement('A');
        item_chunk.href = 'javascript:void(0);';
        Util.Element.add_class(item_chunk, 'item_chunk');

        // Image
        var image_elem = this._doc_obj.createElement('IMG');
        var uri = (use_enclosure_url())
            ? item.enclosure.url
            : item.link;
        var src = Util.URI.strip_https_and_http(uri);
        image_elem.setAttribute('src', src);
        image_elem.setAttribute('alt', '[Image: ' + item.title + ']');
        Util.Image.set_max_size(image_elem, 125, 125); // this needs to be here for IE, and in the load handler for Gecko
        Util.Event.add_event_listener(image_elem, 'load', function() { Util.Image.set_max_size(image_elem, 125, 125); });
        item_chunk.appendChild(image_elem);

        // Title
        var title_elem = this._doc_obj.createElement('DIV');

        var title_label_elem = this._doc_obj.createElement('STRONG');
        title_elem.appendChild(title_label_elem);

        var title_value_elem = this._doc_obj.createElement('SPAN');
        title_value_elem.appendChild(
            this._doc_obj.createTextNode(item.title)
        );
        title_elem.appendChild(title_value_elem);

        item_chunk.appendChild(title_elem);

        return item_chunk;
    }
    
    /**
     * Modify the item chunk as appropriate for its place in the set of
     * currently displayed items. In particular, we need to add a class to
     * every third item_chunk.
     *
     * @param    item_chunk    the item_chunk to modify
     * @param    cur_i        the index of this item in relation to other items
     *                      in the current display
     */
    this._modify_item_chunk = function(item_chunk, cur_i)
    {
        if ( cur_i % 4 == 0 )
        {
            var doc = item_chunk.ownerDocument;
            var spacer_elem = doc.createElement('DIV');
            Util.Element.add_class(spacer_elem, 'force_clear_for_ie');
            item_chunk.parentNode.insertBefore(spacer_elem, item_chunk);
    //        Util.Element.add_class(item_chunk, 'force_clear_for_ie');
        }
    }
};

// file UI.Image_Masseuse.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class for inserting an image.
 */
UI.Image_Masseuse = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Masseuse);
            
    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._unsecured = /^http:/;
        return this;
    };

    /**
     * Massages the given node's descendants.
     */
    this.massage_node_descendants = function(node)
    {
        self.secure_node_descendants(node);
    };
    
    this.secure_node_descendants = function(node)
    {
        Util.Array.for_each(node.getElementsByTagName('IMG'),
            self.secure_node, self);
    };
    
    this.secure_node = function(img)
    {
        var placeholder = self.get_fake_elem(img);
        if (placeholder.src !== img.src)
            img.parentNode.replaceChild(placeholder, img);
    };
    
    this.get_fake_elem = function(img)
    {
        var placeholder, src = img.getAttribute('src');
        if (src == null)
            return;
        
        var my_url = self._loki.owner_window.location;
        if (!self._unsecured.test(my_url) && self._unsecured.test(src)) {
            placeholder = img.cloneNode(false);
            
            if (Util.URI.extract_domain(src) == self._loki.editor_domain()) {
                new_src = Util.URI.strip_https_and_http(src);
            } else if (self._loki.settings.sanitize_unsecured) {
                new_src = self._loki.settings.base_uri +
                    'images/insecure_image.gif';
                placeholder.setAttribute('loki:src', img.src);
                placeholder.setAttribute('loki:fake', 'true');
            } else {
                return img;
            }
            
            placeholder.src = new_src;
            
            return placeholder;
        }
        
        return img;
    };

    /**
     * Unmassages the given node's descendants.
     */
    this.unmassage_node_descendants = function(node)
    {
        Util.Array.for_each(node.getElementsByTagName('IMG'),
            self.unmassage_node, self);
    };
    
    this.unmassage_node = function(img)
    {
        var real = self.get_real_elem(img);
        if (real && real.src != img.src)
            img.parentNode.replaceChild(real, img);
    };
    
    this.get_real_elem = function(img)
    {
        var src, real;
        
        if (!img)
            return null;
        
        src = img.getAttribute('loki:src');
        if (!src)
            return null;
        
        real = img.ownerDocument.createElement('IMG');
        if (img.title)
            real.title = img.title;
        if (img.alt)
            real.alt = img.alt;
        real.src = src;
        
        return real;
    };
    
    /**
     * If "img" is a fake element, returns its corresponding real element,
     * otherwise return the element itself.
     */
    this.realize_elem = function(img)
    {
        return this.get_real_elem(img) || img;
    }
};

// file UI.Image_Menugroup.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class representing a menugroup. 
 */
UI.Image_Menugroup = function()
{
    Util.OOP.inherits(this, UI.Menugroup);

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._helper = (new UI.Image_Helper).init(this._loki);
        return this;
    };

    /**
     * Returns an array of menuitems, depending on the current context.
     * May return an empty array.
     */
    this.get_contextual_menuitems = function()
    {
        var self = this;
        var menuitems = [];

        var selected_item = this._helper.get_selected_item();
        if ( selected_item != null )
        {
            menuitems.push( (new UI.Menuitem).init({ 
                label : 'Edit image',
                listener : function() { self._helper.open_dialog() }
            }) );
        }

        return menuitems;
    };
};

// file UI.Indent_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents "indent" toolbar button.
 */
UI.Indent_Button = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Button);

    this.image = 'indent.png';
    this.title = 'Indent list item(s)';
    this.helper = null;
    
    this.click_listener = function indent_button_onclick() 
    {
        // Only indent if we're inside a UL or OL 
        // (Do this to avoid misuse of BLOCKQUOTEs.)
        
        if (!this._helper)
            this.helper = (new UI.List_Helper).init(this._loki);
        
        var list = this.helper.get_ancestor_list();
        var li = this.helper.get_list_item();
        var sib;
        
        if (list) {
            // Don't indent first element in a list, if it is not in a nested list.
            // This is because in such a situation, Gecko "indents" by surrounding
            // the UL/OL with a BLOCKQUOTE tag. I.e. <ul><li>as|df</li></ul>
            // --> <blockquote><ul><li>as|df</li></ul></blockquote>
            
            sib = Util.Node.get_nearest_non_whitespace_sibling_node(li,
                Util.Node.PREVIOUS);
            if (sib || this.helper.get_more_distant_list(list)) {
                this.helper.indent();
            } else {
                UI.Messenger.display_once('indent_first_li',
                    "The first item in a list cannot be indented.");
            }
        } else {
            this.helper.nag_about_indent_use();
        }
    };
};

// file UI.Italic_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents "italic" toolbar button.
 */
UI.Italic_Button = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Button);

    this.image = 'em.png';
    this.title = 'Emphasis (Ctrl+I)';
    this.click_listener = function() { self._loki.exec_command('Italic'); };
    this.state_querier = function() { return self._loki.query_command_state('Italic'); };
};

// file UI.Italic_Keybinding.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents keybinding.
 */
UI.Italic_Keybinding = function()
{
    Util.OOP.inherits(this, UI.Keybinding);
    this.test = function(e) { return this.matches_keycode(e, 73) && e.ctrlKey; }; // Ctrl-I
    this.action = function() { this._loki.exec_command('Italic'); };
};

// file UI.Italic_Masseuse.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class for massaging em tags to i tags. The motivation for this is that 
 * you can't edit em tags, but we want them in the final output.
 */
UI.Italic_Masseuse = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Masseuse);

    /**
     * Massages the given node's children, replacing any named ems with
     * i elements.
     */
    this.massage_node_descendants = function(node)
    {
        var ems = node.getElementsByTagName('EM');
        for ( var i = ems.length - 1; i >= 0; i-- )
        {
            var fake = self.get_fake_elem(ems[i]);
            ems[i].parentNode.replaceChild(fake, ems[i]);
        }
    };

    /**
     * Unmassages the given node's descendants, replacing any i elements
     * with real em elements.
     */
    this.unmassage_node_descendants = function(node)
    {
        var dummies = node.getElementsByTagName('I');
        for ( var i = dummies.length - 1; i >= 0; i-- )
        {
            var real = self.get_real_elem(dummies[i]);
            dummies[i].parentNode.replaceChild(real, dummies[i])
        }
    };

    /**
     * Returns a fake element for the given em.
     */
    this.get_fake_elem = function(em)
    {
        var dummy = em.ownerDocument.createElement('I');
        dummy.setAttribute('loki:fake', 'true');
        // maybe transfer attributes, too
        while ( em.firstChild != null )
        {
            dummy.appendChild( em.removeChild(em.firstChild) );
        }
        return dummy;
    };

    /**
     * If the given fake element is really fake, returns the appropriate 
     * real em. Else, returns null.
     */
    this.get_real_elem = function(dummy)
    {
        if (dummy != null && dummy.nodeName == 'I')
        {
            var em = dummy.ownerDocument.createElement('EM');
            // maybe transfer attributes, too
            while ( dummy.firstChild != null )
            {
                em.appendChild( dummy.removeChild(dummy.firstChild) );
            }
            return em;
        }
        return null;
    };
};

// file UI.Keybinding.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents a keybinding. For extending only.
 */
UI.Keybinding = function()
{
    this.test; // function
    this.action; // function

    this.init = function(loki)
    {
        this._loki = loki;
        return this;
    };

    /**
     * Returns whether the given keycode matches that 
     * of the given event. 
     */
    this.matches_keycode = function(e, keycode, XXX)
    {
        /*
        if ( e.keyCode == keycode ||  // keydown (IE)
             ( e.keyCode == 0 &&      // keypress (Gecko)
               ( e.charCode == keycode ||
                 ( ( e.charCode >= 65 || e.charCode <= 90 ) && // is uppercase alpha
                     e.charCode == keycode + 32 ) ) ) ) // keypress (Gecko)
        */

        if ( e.type == 'keydown' && e.keyCode == keycode ) // IE
            return true;
        else if ( e.type == 'keypress' && (e.charCode == keycode || (((e.charCode >= 65 || e.charCode <= 90) && e.charCode == keycode + 32))) ) // Gecko
            return true;
        else
            return false;
    //this.test = function(e) { return ( e.charCode == 98 || e.charCode == 66 ) && e.ctrlKey; }; // Ctrl-B
    //this.test = function(e) { return ( e.keyCode == 98 || e.charCode == 66 ) && e.ctrlKey; }; // Ctrl-B
    };
};

// file UI.Left_Align_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents "left align" toolbar button.
 */
UI.Left_Align_Button = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Button);

    this.image = 'align_left.png';
    this.title = 'Left align (Ctrl-L)';
    this.click_listener = function() { self._loki.exec_command('JustifyLeft'); };
    this.state_querier = function() { return self._loki.query_command_state('JustifyLeft'); };
};

// file UI.Left_Align_Keybinding.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents keybinding.
 */
UI.Left_Align_Keybinding = function()
{
    Util.OOP.inherits(this, UI.Keybinding);
    this.test = function(e) { return this.matches_keycode(e, 76) && e.ctrlKey; }; // Ctrl-L
    //this.action = function() { this._loki.exec_command('JustifyLeft'); };
    this.action = function() { this._align_helper.align_left(); };

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._align_helper = (new UI.Align_Helper).init(this._loki);
        return this;
    };
};

// file UI.Link_Dialog.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class An email link dialog window.
 */
UI.Link_Dialog = function()
{
    Util.OOP.inherits(this, UI.Dialog);

    /**
     * Populates the main chunk. You'll want to do something more
     * here in descendents.
     */
    this._populate_main = function()
    {
        this._append_link_information_chunk()
        this._append_submit_and_cancel_chunk();
        this._append_remove_link_chunk();
    };

    /**
     * Appends a chunk with extra options for links.
     */
    this._append_link_information_chunk = function()
    {
        // Link title
        this._link_title_input = this._dialog_window.document.createElement('INPUT');
        this._link_title_input.size = 40;
        this._link_title_input.id = 'link_title_input';
        this._link_title_input.value = this._initially_selected_item.title;

        var lt_label = this._dialog_window.document.createElement('LABEL');
        var strong = this._dialog_window.document.createElement('STRONG');
        strong.appendChild( this._dialog_window.document.createTextNode('Description: ') );
        lt_label.appendChild(strong);
        lt_label.htmlFor = 'link_title_input';

        lt_comment = this._dialog_window.document.createElement('DIV');
        Util.Element.add_class(lt_comment, 'comment');
        lt_comment.innerHTML = '(Will appear in some browsers when mouse is held over link.)';

        var lt_chunk = this._dialog_window.document.createElement('DIV');
        lt_chunk.appendChild(lt_label);
        lt_chunk.appendChild(this._link_title_input);
        lt_chunk.appendChild(lt_comment);

        // "Other options"
        this._other_options_chunk = this._dialog_window.document.createElement('DIV');
        this._other_options_chunk.id = 'other_options';
        if ( this._initially_selected_item.new_window == true )
            this._other_options_chunk.style.display = 'block';
        else
            this._other_options_chunk.style.display = 'none';

        var other_options_label = this._dialog_window.document.createElement('H3');
        var other_options_a = this._dialog_window.document.createElement('A');
        other_options_a.href = 'javascript:void(0);';
        other_options_a.innerHTML = 'More Options';
        var self = this;
        Util.Event.add_event_listener(other_options_a, 'click', function() {
            if ( self._other_options_chunk.style.display == 'none' )
                self._other_options_chunk.style.display = 'block';
            else
                self._other_options_chunk.style.display = 'none';
        });
        other_options_label.appendChild(other_options_a);
        
        // Checkbox
        this._new_window_checkbox = this._dialog_window.document.createElement('INPUT');
        this._new_window_checkbox.type = 'checkbox';
        this._new_window_checkbox.id = 'new_window_checkbox';
        this._new_window_checkbox.checked = this._initially_selected_item.new_window;

        var nw_label = this._dialog_window.document.createElement('LABEL');
        nw_label.appendChild( this._dialog_window.document.createTextNode('Open in new browser window') );
        nw_label.htmlFor = 'new_window_checkbox';

        var nw_chunk = this._dialog_window.document.createElement('DIV');
        nw_chunk.appendChild(this._new_window_checkbox);
        nw_chunk.appendChild(nw_label);

        this._other_options_chunk.appendChild(nw_chunk);

        // Create fieldset and its legend, and append to fieldset
        var fieldset = new Util.Fieldset({legend : 'Link information', document : this._dialog_window.document});
        fieldset.fieldset_elem.appendChild(lt_chunk);
        fieldset.fieldset_elem.appendChild(other_options_label);
        fieldset.fieldset_elem.appendChild(this._other_options_chunk);

        // Append fieldset chunk to dialog
        this._main_chunk.appendChild(fieldset.chunk);
    };

    /**
     * Creates and appends a chunk containing a "remove link" button. 
     * Also attaches 'click' event listeners to the button.
     */
    this._append_remove_link_chunk = function()
    {
        var button = this._dialog_window.document.createElement('BUTTON');
        button.setAttribute('type', 'button');
        button.appendChild( this._dialog_window.document.createTextNode('Remove link') );

        var self = this;
        var listener = function()
        {
            self._external_submit_listener({uri : '', new_window : false, title : ''});
            self._dialog_window.window.close();
        }
        Util.Event.add_event_listener(button, 'click', listener);

        // Setup their containing chunk
        var chunk = this._dialog_window.document.createElement('DIV');
        Util.Element.add_class(chunk, 'remove_chunk');
        chunk.appendChild(button);

        // Append the containing chunk
        this._dialog_window.body.appendChild(chunk);
    };

    /**
     * Called as an event listener when the user clicks the submit
     * button. You'll want to do something more here in descendents.
     */
    this._internal_submit_listener = function()
    {
        // Call external event listener
        this._external_submit_listener({uri : '', // in descendents change this
                                        new_window : this._new_window_checkbox.checked, 
                                        title : this._link_title_input.value});

        // Close dialog window
        this._dialog_window.window.close();
    };
};

// file UI.Link_Double_Click.js
UI.Link_Double_Click = function LinkDoubleClick() {
    Util.OOP.inherits(this, UI.Double_Click);
    this.helper = null;
    
    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this.helper = (new UI.Link_Helper).init(loki);
        return this;
    };
    
    this.double_click = function() {
        if (this.helper.is_selected())
            this.helper.open_page_link_dialog();
    };
};

// file UI.Link_Helper.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class for helping insert link. Contains code
 * common to both the button and the menu item.
 */
UI.Link_Helper = function()
{
    var self = this;
    Util.OOP.inherits(this, UI.Helper);

    this.check_for_linkable_selection = function()
    {
        var sel = Util.Selection.get_selection(self._loki.window);
        var rng = Util.Range.create_range(sel);
        return ( !Util.Selection.is_collapsed(sel) || self.is_selected() )
    };

    /**
     * Opens the page link dialog.
     */
    this.open_page_link_dialog = function()
    {
        if ( !this.check_for_linkable_selection() )
        {
            alert('First select some text that you want to make into a link.');
            return;
        }

        if ( this._page_link_dialog == null )
            this._page_link_dialog = new UI.Page_Link_Dialog();
        this._page_link_dialog.init(self._loki,
                                    { base_uri : this._loki.settings.base_uri,
                                      anchor_names : this.get_anchor_names(),
                                      submit_listener : this.insert_link,
                                      selected_item : this.get_selected_item(),
                                      sites_feed : this._loki.settings.sites_feed,
                                      finder_feed : this._loki.settings.finder_feed,
                                      default_site_regexp : 
                                        this._loki.settings.default_site_regexp,
                                      default_type_regexp : 
                                        this._loki.settings.default_type_regexp });
        this._page_link_dialog.open();
    };

    /**
     * Returns info about the selected link, if any.
     */
    this.get_selected_item = function()
    {
        var sel = Util.Selection.get_selection(this._loki.window);
        var rng = Util.Range.create_range(sel);

        // Look around selection
        var uri = '', new_window = null, title = '';
        var ancestor = Util.Range.get_nearest_ancestor_element_by_tag_name(rng, 'A');
        
        // (Maybe temporary) hack for IE, because the above doesn't work for 
        // some reason if a link is double-clicked
        // 
        // Probably the reason the above doesn't work is that get_nearest_ancestor_node
        // uses get_start_container, which, in IE, collapses a duplicate of the range
        // to front, then gets parentElement of that range. When we doubleclick on a link
        // the text of the entire link (assuming it is one word long) is selected. When a 
        // range is made from such a selection, it is considered _inside_ the A tag, which 
        // is what we want and I, at least, expect. But when the range is collapsed, it 
        // ends up (improperly, I think) _before_ the A tag.
        if ( ancestor == null && rng.parentElement && rng.parentElement().nodeName == 'A' )
        {
            ancestor = rng.parentElement();
        }

        if ( ancestor != null )
        {
            uri = ancestor.getAttribute('href');
            new_window = ( ancestor.getAttribute('target') &&
                           ancestor.getAttribute('target') != '_self' &&
                           ancestor.getAttribute('target') != '_parent' &&
                           ancestor.getAttribute('target') != '_top' );
            title = ancestor.getAttribute('title');
        }

        uri = uri.replace( new RegExp('%7E', 'g'), '~' ); //so that users of older versions of Mozilla are not confused by this substitution
        var httpless_uri = Util.URI.strip_https_and_http(uri);

        var selected_item = { uri : uri, httpless_uri : httpless_uri, new_window : new_window, title : title };
        return selected_item;
    };

    this.is_selected = function()
    {
        return ( this.get_selected_item().uri != '' );
    };

    /**
     * Returns an array of the names of named anchors in the current document.
     */
    this.get_anchor_names = function()
    {
        var anchor_names = new Array();

        var anchor_masseuse = (new UI.Anchor_Masseuse).init(this._loki);
        anchor_masseuse.unmassage_body();

        var anchors = this._loki.document.getElementsByTagName('A');
        for ( var i = 0; i < anchors.length; i++ )
        {
            if ( anchors[i].getAttribute('name') ) // && anchors[i].href == false )
            {
                anchor_names.push(anchors[i].name);
            }
        }
        
        anchor_masseuse.massage_body();
        
        return anchor_names;
    };

    /**
     * Inserts a link. Params contains uri, and optionally
     * new_window, title, and onclick. If uri is empty string,
     * any link is removed.
     */
    this.insert_link = function(params)
    {
        var uri = params.uri;
        var new_window = params.new_window || false;
        var title = params.title || '';
        var onclick = params.onclick || '';
        
        var tags;

        // If the selection is inside an existing link, select that link
        var sel = Util.Selection.get_selection(self._loki.window);
        var rng = Util.Range.create_range(sel);
        var ancestor = Util.Range.get_nearest_ancestor_element_by_tag_name(rng, 'A');
        if (ancestor && ancestor.getAttribute('href')) {
            tags = [ancestor];
        } else {
            self._loki.exec_command('CreateLink', false, 'hel_temp_uri');
            var links = self._loki.document.getElementsByTagName('A');
            tags = [];
            
            for (var i = 0; i < links.length; i++) {
                if (links[i].getAttribute('href') == 'hel_temp_uri') {
                    tags.push(links[i]);
                }
            }
        }
        
        if (!uri || !uri.length) {
            // If no URI received, remove the links.
            tags.each(function remove_link(tag) {
                Util.Node.replace_with_children(tag);
            });
        } else {
            // Update link attributes.
            tags.each(function update_link(tag) {
                function set_attribute(name, value) {
                    if (value && value.length > 0)
                        tag.setAttribute(name, value);
                    else
                        tag.removeAttribute(name);
                }
                
                set_attribute('href', uri);
                set_attribute('target', (new_window) ? '_blank' : null);
                set_attribute('title', title);
                set_attribute('loki:onclick', onclick);
            });
            
            // Collapse selection to end so people can see the link and
            // to avoid a Gecko bug that the anchor tag is only sort of
            // selected (such that if you click the anchor toolbar button
            // again without moving the selection at all first, the new
            // link is not recognized).
            var sel = Util.Selection.get_selection(self._loki.window);
            Util.Selection.collapse(sel, false); // to end
        }
    };
};

// file UI.Link_Menugroup.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class representing a clipboard menugroup. 
 */
UI.Link_Menugroup = function()
{
    Util.OOP.inherits(this, UI.Menugroup);

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._link_helper = (new UI.Link_Helper).init(this._loki);
        return this;
    };

    /**
     * Returns an array of menuitems, depending on the current context.
     * May return an empty array.
     */
    this.get_contextual_menuitems = function()
    {
        var menuitems = [];

        var selected_item = this._link_helper.get_selected_item();
        if ( selected_item != null && selected_item.uri != '' )
        {
            var self = this;
            menuitems.push( (new UI.Menuitem).init({
                label : 'Edit link', 
                //listener : function() { self._link_helper.open_dialog_by_context() } 
                listener : function() { self._link_helper.open_page_link_dialog() } 
            }) );
        }
        else if ( this._link_helper.check_for_linkable_selection() )
        {
            var self = this;
            menuitems.push( (new UI.Menuitem).init({
                label : 'Create link', 
                //listener : function() { self._link_helper.open_dialog_by_context() } 
                listener : function() { self._link_helper.open_page_link_dialog() } 
            }) );
        }
        return menuitems;
    };
};

// file UI.List_Helper.js
/**
 * Helps with list functionality.
 * @author Eric Naeseth
 */
UI.List_Helper = function ListHelper()
{
    Util.OOP.inherits(this, UI.Helper);
    
    this.indent = function indent_list()
    {
        this._loki.exec_command('Indent');
        this._loki.document.normalize();
    };
    
    this.outdent = function outdent_list()
    {
        this._loki.exec_command('Outdent');
    };
    
    this.get_ancestor_list = function get_ancestor_list_of_selected_range()
    {
        var sel = Util.Selection.get_selection(this._loki.window);
        var range = Util.Range.create_range(sel);
        
        return Util.Range.get_nearest_ancestor_element_by_tag_name(range, 'UL')
            || Util.Range.get_nearest_ancestor_element_by_tag_name(range, 'OL');
    };
    
    this.get_list_item = function get_list_item_for_selected_range()
    {
        var sel = Util.Selection.get_selection(this._loki.window);
        var range = Util.Range.create_range(sel);
        
        return Util.Range.get_nearest_ancestor_element_by_tag_name(range, 'LI');
    };
    
    this.get_more_distant_list = function get_list_ancestor_of_list(list)
    {
        return Util.Node.get_nearest_ancestor_element_by_tag_name(list, 'UL')
            || Util.Node.get_nearest_ancestor_element_by_tag_name(list, 'OL');
    };
    
    this.nag_about_indent_use = function nag_about_indent_use()
    {
        UI.Messenger.display_once('indent_use_nag',
            'The indent and unindent buttons can only be used to indent and' +
            ' outdent list items; in particular, it cannot be used to indent' +
            ' paragraphs.');
    };

// file UI.Listbox.js
/**
 * Declares instance variables. You must call <code>init</code> to
 * initialize instance variables.
 *
 * @constructor
 *
 * @class Represents a listbox. Is intended to replace native HTML
 * elements like select boxes or checkboxes, but (a) be able to
 * display more complicated items, and (b) be more easy to navigate,
 * by having a built in filter and pager.
 *
 * @author    Nathanael Fillmore
 * @author    Eric Naeseth
 * @version 2007-10-16
 * 
 */
UI.Listbox = function()
{
    // Permanent listbox instance properties
    this._doc_obj = null; // reference to the document object for the document this listbox will be added to
    this._root_elem = null; // the root listbox element
    this._items = []; // holds the list items (their data, that is, not their document fragments)
    this._item_chunks = []; // holds the document chunk for each list item
    this._selected_index = null; // holds index in this._items of the currently selected item

    this._filtered_indices = []; // holds indices of the items which match the _filter_string
    this._cur_page_num = null;
    this._num_results_per_page = null;
    this._filter_string = null;

    this._items_chunk_elem = null;
    this._next_page_elem = null;
    this._prev_page_elem = null;
    this._page_num_elem = null;

    this._event_listeners = {};
};

/**
 * Initializes instance variables.  Also appends chunks for the
 * various parts of the listbox to the root element.
 *
 * @param    listbox_id    the desired id of the root listbox HTML element.  
 * @param    doc_obj        a reference to the document object for the document
 *                      this listbox will be added to.
 * @param    options        behavior options
 */
UI.Listbox.prototype.init = function(listbox_id, doc_obj, options)
{
    if (!options)
        var options = {};
    
    // Permanent listbox instance properties
    this._doc_obj = doc_obj;
    this._create_root_elem(listbox_id);
    this._error_display = new UI.Error_Display(this._root_elem);
    this._chunks = [];

    // Current state of listbox
    this._cur_page_num = 0; // zero-based
    this._chunk_transfer_size = options.chunk_transfer_size || 16;
    this._transfer_timeout = options.transfer_timeout || 10;
    this._num_results_per_page = options.results_per_page || 8;
    this._filter_string = options.filter_string || '';
    this._selected_index = -1;

    // Append chunks
    this._append_page_chunk();
    this._append_filter_chunk();
    this._append_items_chunk();
};

/**
 * Adds an item to the listbox. (It isn't displayed, though, until
 * refresh is called.)
 *
 * @param    item    the item to append. Item should have whatever properties
 *                  set are needed by this._create_item_chunk. For Listbox proper, 
 *                  these are title and description, but for extensions these
 *                  might be different.
 */
UI.Listbox.prototype.append_item = function(item)
{
    this._items.push(item);
};

/**
 * Inserts an item to the listbox at the specified index. See on
 * append_item() for more info
 *
 * @param    item    the item to insert
 * @param    index    the desired index of this item. The indices of all
 *                  items with indices greater than index will be
 *                  increased by 1.
 */
UI.Listbox.prototype.insert_item = function(item, index)
{
    this._items.splice(index, 0, item);
};

/**
 * Removes an item from the listbox.
 */
UI.Listbox.prototype.remove_item = function(index)
{
    // Remove item
    this._items.splice(index, 1);
    this._item_chunks.splice(index, 1);

    // Fix selected index
    if ( this._selected_index == index )
        this._selected_index = -1;
    else if ( this._selected_index > index )
        this._selected_index--;
};

/**
 * Removes all items from the listbox.
 */
UI.Listbox.prototype.remove_all_items = function()
{
    while ( this._items.length > 0 )
        this.remove_item(0);
};

/**
 * Returns the item at the given index.
 *
 * @return    the item at the given index
 */
UI.Listbox.prototype.get_item = function(index)
{
    return this._items[index];
};

/**
 * Returns the index of the given item. (Note: this is obviously a lot
 * slower than get_item, so it's better to keep track of the index
 * of the item you want than to keep track of the item itself and get
 * its index with this method.)
 *
 * @param    item    the item to get the index of 
 * @return            index of the given item
 * @throws    Error    if no item is found
 */
UI.Listbox.prototype.get_index_of_item = function(item)
{
    for ( var i = 0; i < this._items.length; i++ )
    {
        if ( this._items[i] == item )
            return i;
    }
    throw new Error("UI.Listbox.get_index_of_item: no such item was found");
};

/**
 * Sets which item is currently selected, based on the given index.
 *
 * @param    index            the index of the item to select
 */
UI.Listbox.prototype.select_item_by_index = function(index, dont_refresh, debug)
{
    var item_chunk = this._get_item_chunk(index);

    // Deselect old item, if there is one
    if ( this.get_selected_index() != -1 )
    {
        var formerly_selected_item_chunk = this._item_chunks[ this.get_selected_index() ];
        Util.Element.remove_class(formerly_selected_item_chunk, 'selected');
    }

    // Select new item
    this._selected_index = index;
    Util.Element.add_class(item_chunk, 'selected');

    // Trigger change listeners
    var self = this;
    (function() {
        self._trigger_event_listeners('change');
    }).defer();
};

/**
 * Returns the index of the currently selected item. (For
 * Multiple_Listbox, use get_selected_indices() instead.)
 *
 * @return        index of the currently selected item, or -1 if
 *              no item is currently selected
 */
UI.Listbox.prototype.get_selected_index = function()
{
    return this._selected_index;
};

/**
 * Returns the currently selected item. (For Multiple_Listbox, use
 * get_selected_items() instead.)
 *
 * @return        the currently selected item, or null if no item is
 *              currently selected
 */
UI.Listbox.prototype.get_selected_item = function()
{
    var selected_index = this.get_selected_index();

    if ( selected_index > -1 )
        return this.get_item( selected_index );
    else
        return null;
};

/**
 * Returns the number of items in the listbox.
 *
 * @return    the number of items in the listbox
 */
UI.Listbox.prototype.get_length = function()
{
    return this._items.length;
};

/**
 * Changes the current page such that the selected item is displayed.
 */
UI.Listbox.prototype.page_to_selected_item = function()
{
    var desired_page_num = Math.floor(this.get_selected_index() /
                                      this._num_results_per_page);
    this._cur_page_num = desired_page_num;    
    this.refresh();
};

/**
 * Refreshes the listbox to reflect added items, changed filters,
 * current page number, and so on.
 */
UI.Listbox.prototype.refresh = function()
{
    this._refresh_items_chunk();
    this._refresh_page_chunk();
};

/**
 * Returns the root element of the listbox, which can then be added to
 * the document tree as appropriate.
 *
 * @return        the root element of the listbox
 */
UI.Listbox.prototype.get_listbox_elem = function()
{
    messagebox('UI.Listbox: this._root_elem', this._root_elem);
    return this._root_elem;
};

/**
 * Loads items from a RSS reader.
 * @param    reader    The Util.RSS.Reader object
 * @param    is_selected    (optional) Boolean-returning function that will be
 *                         called with each RSS item to determine if it should
 *                        be initially selected
 */
UI.Listbox.prototype.load_rss_feed = function(reader, is_selected)
{
    var items_added = 0;
    var original_length = this._items.length;
    
    if (!is_selected) {
        var is_selected = function() { return false; };
    }
    var already_selected = this._selected_index >= 0;
    
    var load_more = (function()
    {
        reader.load(this._chunk_transfer_size, this._transfer_timeout);
    }).bind(this);
    
    var retry = (function()
    {
        for (var i = original_length; i < this._items.length; i++) {
            this.remove_item(original_length);
        }
        
        this.load_rss_feed(reader, is_selected);
    }).bind(this);
    
    function handle_error(error_msg, code)
    {
        if (code) {
            error_msg += ' (HTTP Error ' + code + ')';
        }
        this._report_error('Failed to load items: ' + error_msg, retry);
    }
    
    reader.add_event_listener('timeout', function() {
        handle_error('Failed to load items: The operation timed out.', 0);
    }.bind(this));
    
    reader.add_event_listener('load', function(reader, items) {
        var selected = null;
        
        items.each(function(item) {
            this.append_item(item);
            
            // Determine if the current item should start out selected
            // (don't bother doing this if we already have a selected item)
            if (selected === null && !already_selected && is_selected(item)) {
                selected = original_length + items_added;
            }
            
            items_added++;
        }, this);
        
        // Display the newly-added items
        this.refresh();
        
        // Select the item marked as initially selected, if any
        if (selected !== null) {
            this.select_item_by_index(selected);
            this.page_to_selected_item();
        }
        
        if (items.length > 0) {
            try {
                load_more();
            } catch (e) {
                handle_error('Failed to load the next group of items: ' +
                    (e.message || e.description || e), 0);
            }
        }
    }.bind(this));
    
    reader.add_event_listener('error', handle_error.bind(this));
    
    // Load the first chunk
    try {
        load_more();
    } catch (e) {
        handle_error('Failed to load the first group of items: ' +
            (e.message || e.description || e), 0)
    }
}

/**
 * Adds a listener to be called on some event.
 */
UI.Listbox.prototype.add_event_listener = function(event_type, listener)
{
    if ( this._event_listeners[event_type] == null )
        this._event_listeners[event_type] = new Array();

    this._event_listeners[event_type].push(listener);
};

/**
 * Triggers the event listeners.
 */
UI.Listbox.prototype._trigger_event_listeners = function(event_type)
{
    if ( this._event_listeners[event_type] != null )
    {
        for ( var i = 0; i < this._event_listeners[event_type].length; i++ )
        {
            this._event_listeners[event_type][i]();
        }
    }
};

UI.Listbox.prototype._report_error = function(error, retry)
{
    if (!retry)
        var retry = null;
    
    while (this._root_elem.firstChild) {
        this._chunks.push(this._root_elem.firstChild);
        this._root_elem.removeChild(this._root_elem.firstChild);
    }
    
    this._error_display.show(error, retry);
}

UI.Listbox.prototype._clear_error = function()
{
    this._error_display.clear();
    
    for (var i = 0; i < this._chunks.length; i++) {
        this._root_elem.appendChild(this._chunks.shift());
    }
}


///////////////////////////////////
//
// ROOT SECTION
//
///////////////////////////////////

/**
 * Creates the root element.
 *
 * @param    listbox_id    the id of the root element
 */
UI.Listbox.prototype._create_root_elem = function(listbox_id)
{
    messagebox('Listbox: this._doc_obj', this._doc_obj);
    this._root_elem = this._doc_obj.createElement('DIV');
    messagebox('Listbox: created root elem', this._root_elem);
    this._root_elem.id = listbox_id;
    Util.Element.add_class(this._root_elem, 'listbox');
    messagebox('Listbox: created root elem', this._root_elem);
};

///////////////////////////////////
//
// FILTER SECTION
//
///////////////////////////////////

/**
 * Appends to the root_elem the chunk which holds the filter.
 *
 * @private
 */
UI.Listbox.prototype._append_filter_chunk = function()
{
    // create filter chunk
    var filter_chunk_elem = this._doc_obj.createElement('DIV');
    Util.Element.add_class(filter_chunk_elem, 'filter_chunk');

    // create label
    var filter_label_elem = this._doc_obj.createElement('SPAN');
    Util.Element.add_class(filter_label_elem, 'label');
    filter_label_elem.appendChild( this._doc_obj.createTextNode('Search:') );

    // create input elem ... 
    this._filter_input_elem = this._doc_obj.createElement('INPUT');
    this._filter_input_elem.setAttribute('size', '20');
    this._filter_input_elem.setAttribute('name', 'filter_input_elem');

    // .. and create event listeners to check the filter ...
    var self = this;
    var event_listener = function() { self._set_filter_string( self._filter_input_elem.value ); };

    // ... and add the listeners to the input elem
    Util.Event.add_event_listener(this._filter_input_elem, 'mouseup', event_listener);
    Util.Event.add_event_listener(this._filter_input_elem, 'change', event_listener);
    Util.Event.add_event_listener(this._filter_input_elem, 'keyup', event_listener);
    Util.Event.add_event_listener(this._filter_input_elem, 'click', event_listener);

    // ... and disable pressing enter
    var event_listener = function(event)
    {
        event = event == null ? _window.event : event;
        return ( event.keyCode != event.DOM_VK_RETURN &&
                 event.keyCode != event.DOM_VK_ENTER );
    };
    this._filter_input_elem.onkeydown = event_listener;
    this._filter_input_elem.onkeypress = event_listener;
    this._filter_input_elem.onkeyup = event_listener;

    // append label and input elem
    filter_chunk_elem.appendChild(filter_label_elem);
    filter_chunk_elem.appendChild(this._filter_input_elem);
    
    // append filter chunk
    this._root_elem.appendChild(filter_chunk_elem);
};

/**
 * Sets the filter string, resets the cur_page to the first one, and
 * tells the listbox to display appropriate items. Usually called from
 * an event listener on filter_input_elem.
 *
 * @private
 */
UI.Listbox.prototype._set_filter_string = function(filter_string)
{
    // only change things if the filter_string is different from
    // what's already there
    if ( this._filter_string != filter_string )
    {
        this._filter_string = filter_string;
        this._cur_page_num = 0;
        this.refresh();
    }
};

/**
 * Sets this._filtered_indices to contain indices of only those items
 * which match the current filter.
 *
 * @private
 */
UI.Listbox.prototype._update_filtered_indices = function()
{
    this._filtered_indices = new Array();
    
    function matches_filter(obj, filter)
    {
        var bare = {}; // see Util.Object.names() for justification
        
        for (var name in obj) {
            if (name in bare)
                continue;
            
            var value = obj[name];
            if (value == null)
                continue;
            
            var t = typeof(value);
            
            if (t == 'object' && matches_filter(value, filter))
                return true;
            if (t == 'function')
                continue;
            if (t != 'string')
                value = String(value);
            
            if (value.toLowerCase().indexOf(filter) >= 0)
                return true;
        }
    }

    if ( this._filter_string == '' )
    {
        for ( var i = 0; i < this._items.length; i++ )
            this._filtered_indices.push(i);
    }
    else
    {
        var cur_item, item_property_name, item_property_lc;
        var filter_string_lc = this._filter_string.toLowerCase();
        for ( var i = 0; i < this._items.length; i++ )
        {
            cur_item = this._items[i];
            
            if (matches_filter(cur_item, filter_string_lc))
                this._filtered_indices.push(i);
        }
    }
};

///////////////////////////////////
//
// ITEMS SECTION
//
///////////////////////////////////

/**
 * Appends to the root_elem the chunk which holds the list of items
 *
 * @private
 */
UI.Listbox.prototype._append_items_chunk = function()
{
    this._items_chunk_elem = this._doc_obj.createElement('DIV');
    Util.Element.add_class(this._items_chunk_elem, 'items_chunk');
    this._root_elem.appendChild(this._items_chunk_elem);
};

/**
 * Clears out the children of items_chunk, and replaces them with
 * chunks made from items which match the current filter/page.  (N.B.:
 * _append_items_chunk must be called before this.)
 *
 * @private
 */
UI.Listbox.prototype._refresh_items_chunk = function()
{
    // Determine starting and ending indices
    var starting_index = this._cur_page_num * this._num_results_per_page;
    var ending_index = (this._cur_page_num + 1) * this._num_results_per_page;

    // Make sure to use items which match the current filter
    this._update_filtered_indices();

    // Clear list of old displayed items 
    Util.Node.remove_child_nodes(this._items_chunk_elem);

    // Display new list of items
    var item_index, item, item_chunk;
    for ( var i = starting_index; i < ending_index && i < this._filtered_indices.length; i++ )
    {
        item_index = this._filtered_indices[i];
        item_chunk = this._get_item_chunk(item_index);
        this._items_chunk_elem.appendChild(item_chunk);
        this._modify_item_chunk(item_chunk, i);
    }

    // Display message if there are no items
    if ( this._filtered_indices.length == 0 )
    {
        var no_items_chunk = this._get_no_items_chunk();
        this._items_chunk_elem.appendChild(no_items_chunk);
    }
};

/**
 * Returns a chunk to be displayed when no items match the current
 * filter criteria, etc.
 *
 * @return        the chunk
 * @private
 */
UI.Listbox.prototype._get_no_items_chunk = function()
{
    var item_chunk = this._doc_obj.createElement('DIV');
    item_chunk.appendChild( this._doc_obj.createTextNode('No matching items.') );
    return item_chunk;
};

/**
 * If an item chunk corresponding to the given index has already been
 * created, returns that item chunk; otherwise, creates one. If you
 * want to muck with how item chunks are created, overload
 * create_item_chunk rather than this method.
 *
 * @param    item_index    the index of the item for which to get an item_chunk
 * @private
 */
UI.Listbox.prototype._get_item_chunk = function(item_index)
{
    var item = this._items[item_index];
    var item_chunk;
    
    if ( this._item_chunks[item_index] != null )
    {
        item_chunk = this._item_chunks[item_index];
    }
    else
    {
        item_chunk = this._create_item_chunk(item);
        this._add_event_listeners_to_item_chunk(item_chunk, item_index);

        this._item_chunks[item_index] = item_chunk;
    }

    return item_chunk;
};

/**
 * Modify the item chunk as appropriate for its place in the set of
 * currently displayed items. (In Image_Listbox, for example, we need
 * to add a class to every third item_chunk.)
 *
 * @param    item_chunk    the item_chunk to modify
 * @param    cur_i        the index of this item in relation to other items
 *                      in the current display
 */
UI.Listbox.prototype._modify_item_chunk = function(item_chunk, cur_i)
{
};

/**
 * Creates a document chunk for the given item.  N.B.: This is a
 * useful method to overload.
 *
 * @param    item    the item for which to create a document chunk
 * @return            the created chunk
 * @private
 */
UI.Listbox.prototype._create_item_chunk = function(item)
{
    //var item_chunk = this._doc_obj.createElement('DIV');
    var item_chunk = this._doc_obj.createElement('A');
    item_chunk.href = 'javascript:void(0);';
    Util.Element.add_class(item_chunk, 'item_chunk');
    item_chunk.appendChild(
        this._doc_obj.createTextNode('Title: ' + item.title + '; description: ' + item.description)
    );
    return item_chunk;
};

/**
 * This adds the appropriate event listeners to the given item_chunk.
 * N.B.: This is a useful method to overload.
 *
 * @param    item_chunk    the item_chunk to which the event listeners will be added
 * @param    item_index    the index of the item (in the array this._items)
 * @private
 */
UI.Listbox.prototype._add_event_listeners_to_item_chunk = function(item_chunk, item_index)
{
    // Hover
    Util.Event.add_event_listener(item_chunk, 'mouseover', function() { Util.Element.add_class(item_chunk, 'hover'); });
    Util.Event.add_event_listener(item_chunk, 'mouseout', function() { Util.Element.remove_class(item_chunk, 'hover'); });

    // Select
    var self = this;
    Util.Event.add_event_listener(item_chunk, 'click', function() { self.select_item_by_index(item_index); });
};

/**
 * Returns true if this item is selected, false otherwise.
 *
 * @param    item    the item which may be selected
 * @return            true if the given item is selected, false otherwise
 * @deprecated        use the public methods above instead
 * @private
 */
UI.Listbox.prototype._is_item_selected = function(item)
{
    for ( var i = 0; i < this._selected_items.length; i++ )
    {
        if ( item == this._selected_items[i] )
            return true;
    }
    return false;
};



///////////////////////////////////
//
// PAGE SECTION
//
///////////////////////////////////

/**
 * Appends to the root_elem the chunk which holds (a) information
 * about which page of items we're currently on, and (b) controls to
 * change pages
 *
 * @private
 */
UI.Listbox.prototype._append_page_chunk = function()
{
    var self = this;

    // create page chunk
    var page_chunk_elem = this._doc_obj.createElement('DIV');
    Util.Element.add_class(page_chunk_elem, 'page_chunk');

    // create and append prev page elem.
    this._prev_page_elem = this._doc_obj.createElement('A');
    this._prev_page_elem.href = 'javascript:void(0);';
    this._prev_page_elem.onclick = function() { self._goto_prev_page(); return false; };
    this._prev_page_elem.appendChild(this._doc_obj.createTextNode('<< Prev'));
    page_chunk_elem.appendChild(this._prev_page_elem);

    this._page_num_elem = this._doc_obj.createElement('SPAN');
    page_chunk_elem.appendChild(this._page_num_elem);

    // create and append next page elem
    this._next_page_elem = this._doc_obj.createElement('A');
    this._next_page_elem.href = 'javascript:void(0);';
    this._next_page_elem.onclick = function() { self._goto_next_page(); return false; };
    this._next_page_elem.appendChild(this._doc_obj.createTextNode('Next >>'));
    page_chunk_elem.appendChild(this._next_page_elem);

    // append page chunk
    this._root_elem.appendChild(page_chunk_elem);
};

/**
 * Refreshes the page chunk with current information. For example, if
 * a user added a filter and there are now fewer pages than there were
 * before, this causes that to be reflected.
 *
 * TEMP: you might want to just gray out the text, rather than hide
 * the element entirely
 *
 * @private
 */
UI.Listbox.prototype._refresh_page_chunk = function()
{
    var total_num_of_pages = Math.ceil( this._filtered_indices.length / this._num_results_per_page );

    // Calculate displayable cur page num
    if ( total_num_of_pages == 0 )
        displayable_cur_page_num = 0;
    else
        displayable_cur_page_num = this._cur_page_num + 1; // +1 because cur_page_num is zero-based

    // Show or hide prev page elem
    if ( displayable_cur_page_num <= 1 )
        this._prev_page_elem.style.visibility = 'hidden';
    else
        this._prev_page_elem.style.visibility = 'visible';

    // Display the current page number and the total number of pages
    if ( this._page_num_elem.hasChildNodes() )
        this._page_num_elem.removeChild(this._page_num_elem.firstChild);

    this._page_num_elem.appendChild(
        this._doc_obj.createTextNode(' ' + displayable_cur_page_num  + ' of ' + total_num_of_pages + ' ')
    );

    // Show or hide next page elem
    if ( displayable_cur_page_num >= total_num_of_pages )
        this._next_page_elem.style.visibility = 'hidden';
    else
        this._next_page_elem.style.visibility = 'visible';
};

/**
 *
 * Displays the next page of items in items_chunk. Is called onclick
 * of the prev_page_elem.
 *
 * @private
 */
UI.Listbox.prototype._goto_prev_page = function()
{
    this._cur_page_num--;
    this.refresh();
};

/**
 * Displays the previous page of items in items_chunk. Is called
 * onclick of the next_page_elem.
 *
 * @private
 */
UI.Listbox.prototype._goto_next_page = function()
{
    this._cur_page_num++;
    this.refresh();
};

// file UI.Masseuse.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents a body masseuse, to replace elements 
 * inconvenient to edit with fake elements that are convenient 
 * to edit. For extending only.
 */
UI.Masseuse = function()
{
    this._loki;

    /**
     * Massages the given node's descendants, replacing any elements inconvenient 
     * to edit with convenient ones.
     */
    this.massage_node_descendants = function(node)
    {
    };
    
    /**
     * Unmassages the given node's descendants, replacing any convenient but fake
     * elements with real ones.
     */
    this.unmassage_node_descendants = function(node)
    {
    };

    /**
     * For convenience.
     */
    this.massage_body = function()
    {
        this.massage_node_descendants(this._loki.document);
    };

    /**
     * For convenience.
     */
    this.unmassage_body = function()
    {
        this.unmassage_node_descendants(this._loki.document);
    };
};

UI.Masseuse.prototype.init = function(loki)
{
    this._loki = loki;
    return this;
};

UI.Masseuse.prototype.assign_fake_id = function assign_fake_element_id(elem) {
    var base = 'az';
    
    function random_int(min, max) {
        return Math.floor(Math.random() * (max - min + 1)) + min;
    }
    
    function generate_id(length) {
        var i, id = '_loki_', c;
        if (!length)
            length = 6
        for (i = 0; i < length; ++i) {
            c = random_int(base.charCodeAt(0), base.charCodeAt(1));
            id += String.fromCharCode(c);
        }
        return (elem.ownerDocument.getElementById(id))
            ? generate_id(length)
            : id;
    }
    
    if (!elem.id)
        elem.id = generate_id();
    return elem.id;
};

UI.Masseuse.prototype.remove_fake_id = function remove_fake_element_id(elem) {
    var pattern = /^_loki_[a-z]+$/;
    if (elem.id && pattern.test(elem.id))
        elem.removeAttribute('id');
};

// file UI.Menu.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents a menu.
 */
UI.Menu = function()
{
    var self = this;
    var _loki;
    var _chunk;
    var _menuitems = new Array();

    self.init = function(loki)
    {
        _loki = loki;
        return self;
    };

    self.add_menuitem = function(menuitem)
    {
        _menuitems.push(menuitem);
    };

    self.add_menuitems = function(menuitems)
    {
        var i, length;
        if (menuitems) {
            for (i = 0, length = menuitems.length; i < length; ++i)
                self.add_menuitem(menuitems[i]);
        }
    };

    var _get_chunk = function(popup_document)
    {
        var menu_chunk = popup_document.createElement('DIV');
        Util.Event.add_event_listener(menu_chunk, 'contextmenu', 
            function(event)
            { 
                // Stop the normal context menu from displaying
                try { event.preventDefault(); } catch(e) {} // Gecko
                return false; // IE
            });
        menu_chunk.style.zindex = 1000;
        Util.Element.add_class(menu_chunk, 'contextmenu');

        for ( var i = 0; i < _menuitems.length; i++ )
        {
            menu_chunk.appendChild(_menuitems[i].get_chunk(popup_document));
        }

        //menu_chunk.innerHTML = 'This is the context menu.'
        return menu_chunk;
    };

    /**
     * Renders the menu.
     * 
     * Much of this code, especially the Gecko part, is lightly 
     * modified from FCK; some parts are modified from TinyMCE;
     * some parts come from Brian's Loki menu code.
     */
    self.display = function(click_event)
    {
        if (_loki.owner_window.createPopup) {
            // Make the popup and append the menu to it
            var popup = _loki.owner_window.createPopup();
            var menu_chunk = _get_chunk(popup.document);
            var popup_body = popup.document.body;
            Util.Element.add_class(popup_body, 'loki');
            Util.Document.append_style_sheet(popup.document, _loki.settings.base_uri + 'css/Loki.css');
            popup_body.appendChild(menu_chunk);

            // Get width and height of the menu
            //
            // We use this hack (first appending a copy of the menu directly in the document,
            // and getting its width and height from there rather than from the copy of
            // the menu appended to the popup) because we append the "Loki.css" style sheet to 
            // the popup, but that may not have loaded by the time we want to find the width 
            // and height (even though it will probably be stored in the cache). Since "Loki.css"
            // has already been loaded for the main editor window, we can reliably get the dimensions
            // there.
            //
            // We surround the menu chunk here in a table so that the menu chunk div shrinks
            // in width as appropriate--since divs normally expand width-wise as much as they
            // can.
            var tmp_container = _loki.owner_document.createElement('DIV');
            tmp_container.style.position = 'absolute';
            tmp_container.innerHTML = '<table><tbody><tr><td></td></tr></tbody></table>';
            var tmp_menu_chunk = _get_chunk(_loki.owner_document);
            tmp_container.firstChild.firstChild.firstChild.firstChild.appendChild(tmp_menu_chunk);
            _loki.root.appendChild(tmp_container);
            var width = tmp_menu_chunk.offsetWidth;
            var height = tmp_menu_chunk.offsetHeight;
            _loki.root.removeChild(tmp_container);

            // This simple method of getting width and height would work, if we hadn't
            // loaded a stylesheet for the popup (see above):
            // (NB: we could also use setTimeout for the below, but that would break if 
            // the style sheet wasn't stored in the cache and thus had to be actually
            // downloaded.)
            //popup.show(x, y, 1, 1);
            //var width = menu_chunk.offsetWidth;
            //var height = menu_chunk.offsetHeight;

            Util.Event.add_event_listener(popup.document, 'click', function() { popup.hide(); });

            // Show the popup
            popup.show(click_event.screenX, click_event.screenY, width, height);
        } else {
            // Determine the coordinates at which the menu should be displayed.
            var frame_pos = Util.Element.get_position(_loki.iframe);
            var event_pos = {x: click_event.clientX, y: click_event.clientY};
            var root_offset = Util.Element.get_relative_offsets(_loki.owner_window, _loki.root);

            var x = frame_pos.x + event_pos.x - root_offset.x;
            var y = frame_pos.y + event_pos.y - root_offset.y;
            
            // Create menu, hidden
            var menu_chunk = _get_chunk(_loki.owner_document);
            _loki.root.appendChild(menu_chunk);
            menu_chunk.style.position = 'absolute';
            menu_chunk.style.visibility = 'hidden';

            // Position menu
            menu_chunk.style.left = (x - 1) + 'px';
            menu_chunk.style.top = (y - 1) + 'px';

            // Watch the "click" event for all windows to close the menu
            function close_menu() {
                var w;
                
                if (menu_chunk.parentNode) {
                    menu_chunk.parentNode.removeChild(menu_chunk);
                    
                    var w = _loki.window;
                    while (w) {
                        w.document.removeEventListener('click', close_menu, false);
                        w.document.removeEventListener('contextmenu', close_menu, false);
                        w = (w != w.parent) ? w.parent : null;
                    }
                }
            }
            
            function add_close_listeners() {
                var w = _loki.window;
                while (w) {
                    w.document.addEventListener('click', close_menu, false);
                    w.document.addEventListener('contextmenu', close_menu, false);
                    w = (w != w.parent) ? w.parent : null;
                }
            }
            
            add_close_listeners.defer();
    
            // Show menu
            menu_chunk.style.visibility    = '';
        }
    }

// file UI.Menugroup.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents a menugroup. For extending only.
 */
UI.Menugroup = function()
{
    var self = this;
    this._loki;

    this.init = function(loki)
    {
        this._loki = loki;
        return this;
    };

    /**
     * Returns an array of menuitems, depending on the current context.
     * May return an empty array.
     */
    this.get_contextual_menuitems = function()
    {
    };
};

// file UI.Menuitem.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents a menuitem. Can be extended or used as it is.
 */
UI.Menuitem = function()
{
    var label, listener, disabled;

    /**
     * Inits the menuitem. Params:
     *    label        string (should not contain HTML)
     *    listener    function
     *    disabled    (optional) boolean
     */
    this.init = function(params)
    {
        if (!params || !params.label || !params.listener) {
            throw new Error('Insufficient information to construct a menu item.');
        }

        label = params.label;
        listener = params.listener;
        disabled = !!params.disabled;

        return this;
    };

    /**
     * Returns an appendable chunk to render the menuitem.
     * @return {HTMLElement} chunk
     */
    this.get_chunk = function(doc)
    {
        var container;
        
        if (disabled) {
            container = doc.createElement('SPAN');
            Util.Element.add_class(container, 'disabled');
        } else {
            container = doc.createElement('A');
            container.href = 'javascript:void(0);';
            Util.Element.add_class(container, 'menuitem');
            Util.Event.add_event_listener(container, 'click', listener);
        }
        
        container.innerHTML = label.replace(' ', '&nbsp;');
        return container;
    };
    
    /**
     * Gets the menu item's label.
     * @return {String}
     */
    this.get_label = function()
    {
        return label;
    }
    
    /**
     * Gets the menu item's click listener.
     * @return {Function}
     */
    this.get_listener = function()
    {
        return listener;
    }
    
    /**
     * Returns true if the menu item is disabled, false if otherwise.
     * @return {Boolean}
     */
    this.is_disabled = function() {
        return disabled;
    }
};

// file UI.Messenger.js
/**
 * @class Displays informative messages to the user.
 * @author Eric Naeseth
 */
UI.Messenger = {
    /**
     * Displays a message.
     * @param {string}  message  the message to be displayed
     * @return {void}
     */
    display: function display_message(message)
    {
        // It'd be nice to have a non-alert implementation of this someday. -EN
        alert(message);
    },
    
    /**
     * Displays a message only once for the current user session.
     * This works by setting a session cookie when the message is first
     * displayed. If, when this function is called again, the cookie already
     * exists, the message is not displayed.
     * @param {string}  id       a fixed ID that can be used to identify this
     *                           message in a cookie name
     * @param {string}  message  the message to be displayed
     * @return {boolean} true if the message was actually displayed, false if
     *                   not
     */
    display_once: function display_message_once_per_session(id, message)
    {
        return this.display_once_per_duration(id, message, null);
    },
    
    /**
     * Displays a message only once for at least some number of days.
     * This works by setting a cookie with an expiration date when the message
     * is first displayed. If, when this function is called again, the cookie
     * already exists, the message is not displayed.
     * @param {string}  id       a fixed ID that can be used to identify this
     *                           message in a cookie name
     * @param {string}  message  the message to be displayed
     * @param {number}  days     the number of days for which the message should
     *                           not be shown
     * @return {boolean} true if the message was actually displayed, false if
     *                   not
     */
    display_once_per_duration:
        function display_message_once_per_duration(id, message, days)
    {
        if (!navigator.cookieEnabled)
            return false;
        
        var cookie_name = '_loki2_pmsg_' + id.replace(/W+/g, '_');
        
        var displayed = Boolean(Util.Cookie.get(cookie_name));
        
        if (!displayed)
            this.display(message);
        
        Util.Cookie.set(cookie_name, 'displayed', days);
        
        return !displayed;
    }

// file UI.Multiple_Items_Error.js
UI.Multiple_Items_Error = function MultipleItemsError(message) {
    var err = new Error(message);
    err.name = 'UI.Multiple_Items_Error';
    return err;
};

UI.Multiple_Items_Error.prototype = new Error();

// file UI.OL_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents "ol" toolbar button.
 */
UI.OL_Button = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Button);

    this.image = 'ol.png';
    this.title = 'Ordered list';
    this.click_listener = function() { self._loki.toggle_list('ol'); };
};

// file UI.Options.js
UI.Options = function()
{
}


// file UI.Outdent_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents "outdent" toolbar button.
 */
UI.Outdent_Button = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Button);

    this.image = 'outdent.png';
    this.title = 'Unindent list item(s)';
    this.helper = null;
    
    this.click_listener = function outdent_button_onclick() 
    {
        // Only outdent if we're inside a UL or OL 
        // (Do this to avoid misuse of BLOCKQUOTEs.)
        
        if (!this._helper)
            this.helper = (new UI.List_Helper).init(this._loki);
            
        if (this.helper.get_ancestor_list()) {
            this.helper.outdent();
        } else {
            this.helper.nag_about_indent_use();
        }
    };
    
};

// file UI.Page_Link_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents "link to page" toolbar button.
 */
UI.Page_Link_Button = function()
{
    var self = this;
    Util.OOP.inherits(this, UI.Button);

    this.image = 'link.png';
    this.title = 'Insert link (Ctrl+K)';
    this.click_listener = function() { self._helper.open_page_link_dialog(); };

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._helper = (new UI.Link_Helper).init(this._loki);
        return this;
    };
};

// file UI.Page_Link_Dialog.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class An email link dialog window. 
 *
 */
UI.Page_Link_Dialog = function()
{
    //Util.OOP.inherits(this, UI.Link_Dialog);
    Util.OOP.inherits(this, UI.Dialog);

    this._dialog_window_width = 615;
    this._dialog_window_height = 410;
    this._CURRENT_PAGE_STR = '(current page)';
    this._LOADING_STR = 'Loading...';
    this._RSS_TAB_STR = 'an existing item';
    this._CUSTOM_TAB_STR = 'a web address';
    this._EMAIL_TAB_STR = 'an email address';

    /**
     * Initializes the dialog.
     *
     * @param    params    object containing the following named paramaters in addition
     *                  to those initialized in UI.Dialog.init, q.v.:
     *                  <ul>
     *                  </ul>
     */
    this.init = function(loki, params)
    {
        this._loki = loki;
        
        this._anchor_names = params.anchor_names;
        this._sites_feed = params.sites_feed;
        this._finder_feed = params.finder_feed;
        this._default_site_regexp = params.default_site_regexp;
        this._default_type_regexp = params.default_type_regexp;
        // use rss integration only if sites_feed and finder_feed are given:
        this._use_rss = params.sites_feed && params.finder_feed;
        
        this._initially_selected_nameless_uri = null;
        this._initially_selected_name = null;

        // used because we want to perform certain actions only
        // when the dialog is first starting up, and others only
        // when the dialog *isn't* first starting up.
        this._links_already_loaded_once = false;
        this._anchors_already_loaded_once = false;

        this._link_information = [];

        this.superclass.init.call(this, params);
        return this;
    };

    this._set_title = function()
    {
        if ( this._initially_selected_item.uri == '' )
            this._dialog_window.document.title = "Create a Link";
        else
            this._dialog_window.document.title = "Edit a Link";
    };

    this._append_style_sheets = function()
    {
        this.superclass._append_style_sheets.call(this);
        Util.Document.append_style_sheet(this._dialog_window.document, this._base_uri + 'css/Tabset.css');
        Util.Document.append_style_sheet(this._dialog_window.document, this._base_uri + 'css/Link_Dialog.css');
    };

    this._populate_main = function()
    {
        this.item_selector = new UI.Page_Link_Selector(this);
        
        this._append_heading();
        this._append_tabset();
        if ( this._use_rss )
            this._append_rss_tab();
        this._append_email_tab();
        this._append_custom_tab();
        //this._append_main_links_chunk();
        this._append_link_information_chunk();
        this._append_submit_and_cancel_chunk();
        this._append_remove_link_chunk();
        
        this._sanity_error_displays = null;
        
        this._sites_error_display = (this._use_rss)
            ? new UI.Error_Display(this._doc.getElementById('sites_pane'))
            : null;
    };

    this._append_heading = function()
    {
        var h1 = this._dialog_window.document.createElement('H1');
        if ( this._initially_selected_item.uri == '' )
            h1.innerHTML = 'Make a link to:';
        else
            h1.innerHTML = 'Edit link to:';
        this._main_chunk.appendChild(h1);
    };

    this._append_tabset = function()
    {
        this._tabset = new Util.Tabset({document : this._dialog_window.document});        
        if ( this._use_rss )
            this._tabset.add_tab('rss', this._RSS_TAB_STR);
        this._tabset.add_tab('custom', this._CUSTOM_TAB_STR);
        this._tabset.add_tab('email', this._EMAIL_TAB_STR);
        var self = this;
        this._tabset.add_select_listener(function(old_tab, new_tab) { self._update_link_information(old_tab, new_tab); });
        this._main_chunk.appendChild(this._tabset.tabset_elem);
    };

    this._append_rss_tab = function()
    {
        var container = this._doc.createElement('DIV');
        this._tabset.get_tabpanel_elem('rss').appendChild(container);

        // Sites pane
        var sites_pane = this._doc.createElement('DIV');
        sites_pane.id = 'sites_pane';
        container.appendChild(sites_pane);
        
        this._sites_progress = this.create_activity_indicator('textual', 'Loading sites&hellip;');
        this._sites_progress.insert(sites_pane);
        return;
    };

    this._append_custom_tab = function()
    {
        var container = this._doc.createElement('DIV');
        this._tabset.get_tabpanel_elem('custom').appendChild(container);

        var label = this._doc.createElement('LABEL');
        label.htmlFor = 'custom_input';
        label.innerHTML = 'Destination web address: ';
        container.appendChild(label);

        // adding this via innerHTML above doesn't work in Gecko for some reason
        this._custom_input = this._doc.createElement('INPUT');
        this._custom_input.id = 'custom_input';
        this._custom_input.type = 'text';
        this._custom_input.setAttribute('size', '40');
        // XXX: maybe this should go in apply_initially_selected_item
        if ( this._initially_selected_item.uri != '' && 
             this._initially_selected_item.uri.search != null &&
             this._initially_selected_item.uri.search( new RegExp('^mailto:') ) == -1 )
        {
            this._custom_input.value = this._initially_selected_item.uri;
        }
        else
        {
            this._custom_input.value = 'http://';
        }
        container.appendChild(this._custom_input);    
    };

    this._append_email_tab = function()
    {
        var container = this._doc.createElement('DIV');
        this._tabset.get_tabpanel_elem('email').appendChild(container);

        var label = this._doc.createElement('LABEL');
        label.innerHTML = 'Email address: ';
        label.htmlFor = 'email_input';
        container.appendChild(label);

        this._email_input = this._doc.createElement('INPUT');
        this._email_input.id = 'email_input';
        this._email_input.type = 'text';
        this._email_input.setAttribute('size', '40');
        // XXX: maybe this should go in apply_initially_selected_item
        if ( this._initially_selected_item.uri != null &&
             this._initially_selected_item.uri.search != null &&
             this._initially_selected_item.uri.search( new RegExp('^mailto:') ) > -1 )
        {
            this._email_input.value = this._initially_selected_item.uri.replace(new RegExp('^mailto:'), '');
        }
        container.appendChild(this._email_input);

        //var label = this._doc.createElement('DIV');
        //label.innerHTML = 'Please enter the recipient's whole email address, including the "@carleton.edu" or "@acs.carleton.edu"';
        //container.appendChild(label);
    };

    this._set_link_title = function(new_title)
    {
        if ( new_title == this._CURRENT_PAGE_STR || 
             new_title == this._LOADING_STR )
            this._set_link_title_input_value('');
        else
            this._set_link_title_input_value(new_title);
    };

    this._compare_uris = function(uri_a, uri_b)
    {
        return uri_a == uri_b;

        // doesn't work right, I think:

        function split_uri(uri)
        {
            if ( uri == null || uri.split == null )
                return false;

            var u = {};

            // Discard any #name
            var arr = uri.split('#', 2);
            uri = arr[0];

            // Split pre and post ?
            arr = uri.split('?', 2);
            u.pre = arr[0];
            u.post = arr[1];

            // Split post arguments
            u.post = u.post.split('&');

            return u;
        }

        var a = split_uri(uri_a);
        var b = split_uri(uri_b);

        // Check that the splitting worked
        if ( !a || !b )
            return false;
        if ( a.pre != b.pre )
            return false;
        if ( a.post.length != b.post.length )
            return false;

        for ( var i = 0; i < a.pre.length; i++ )
        {
            var matched = false;
            for ( var j = 0; j < b.pre.length; j++ )
            {
                if ( a.pre[i] == b.pre[j] )
                {
                    matched = true;
                    // this messes up i
                    //a.pre.splice(i, 1);
                    //b.pre.splice(j, 1);
                    //a.pre[i] == '';
                    //b.pre[j] == '';
                    continue;
                }
            }
            if ( !matched )
                return false;
        }

        return true;
    };
    
    this._sanitize_uri = function(uri)
    {
        return (Util.URI.extract_domain(uri) == this._loki.editor_domain())
            ? Util.URI.make_domain_relative(uri)
            : uri;
    }

    this._load_finder = function(feed_uri)
    {
        // Split name from uri
        var a = this._initially_selected_item.httpless_uri.split('#');
        this._initially_selected_nameless_uri = a[0];
        this._initially_selected_name = a.length > 1 ? a[1] : '';
        
        if (a.length > 1 && a[0].length == 0) {
            // We have an anchor but nothing else; this means that the user
            // linked to an anchor on the current item. In this case, we should
            // simply skip going through the finder and proceed as if this
            // were a new link.
            
            this._load_sites(this._sites_feed);
            return;
        }

        // Add initially selected uri
        var self = this;
        var add_initially_selected_uri = function(uri)
        {
            var connector = ( uri.indexOf('?') > -1 ) ? '&' : '?';
            return uri + connector + 'url=' + 
                encodeURIComponent(self._initially_selected_nameless_uri);
        };

        // Load finder
        feed_uri = add_initially_selected_uri(feed_uri)
        var reader = new Util.RSS.Reader(feed_uri);
        var select = this._doc.getElementById('sites_select') || null;
        var error_display = this._sites_error_display;
        var sites_pane = this._doc.getElementById('sites_pane');
        
        error_display.clear();
        
        function report_error(message) {
            this._sites_progress.remove();
            if (select && select.parentNode)
                select.parentNode.removeChild(select);
            
            error_display.show('Failed to load finder: ' + message, function() {
                this._load_finder(feed_uri);
            }.bind(this));
        }
        
        reader.add_event_listener('load', function(feed, new_items) {
            var site_uri, type_uri;
            
            new_items.each(function(item) {
                if (item.title == 'site_feed')
                    site_uri = item.link;
                else if (item.title == 'type_feed')
                    type_uri = item.link;
            }, this);
        

            // ... then set them if found
            // We make sure to at least set them to null because they may
            // already be set from some previous opening of the dialog.
            this._initially_selected_site_uri = site_uri || null;
            this._initially_selected_type_uri = type_uri || null;

            // Trigger listener
            this._finder_listener();
        }.bind(this));
        reader.add_event_listener('error', report_error.bind(this));
        reader.add_event_listener('timeout', function() {
            report_error.call(this, 'Failed to check the origin of the link ' +
                'within a reasonable amount of time.');
        }.bind(this));
        
        try {
            reader.load(null, 20 /* 20 = 20 seconds until timeout */);
        } catch (e) {
            var message = e.message || e;
            report_error(message);
        }
    };

    this._load_sites = function(feed_uri)
    {
        var sites_pane = this._doc.getElementById('sites_pane');
        
        /*
        function make_uri(offset, num)
        {
            var connector = (uri.indexOf('?') > -1) ? '&' : '?';
            return feed_uri + connector + 'start=' + offset + '&num=' + num;
        }
        */
        
        var reader = new Util.RSS.Reader(feed_uri);
        var select = this._doc.getElementById('sites_select') || null;
        var error_display = this._sites_error_display;
        
        error_display.clear();
        
        function report_error(message) {
            this._sites_progress.remove();
            if (select && select.parentNode)
                select.parentNode.removeChild(select);
            
            error_display.show('Failed to load sites: ' + message, function() {
                this._load_sites(feed_uri);
            }.bind(this));
        }
        
        reader.add_event_listener('load', function(feed, new_items)
        {
            function load_site()
            {
                if (select.selectedIndex <= 0) {
                    this.item_selector.revert();
                } else {
                    var o = select.options[select.selectedIndex];
                    this.item_selector.load(o.text, o.value);
                }
            }
            
            if (new_items.length == 0) {
                report_error('No sites are available to choose from.');
            }
            
            if (!select) {
                sites_pane.appendChild(this._udoc.create_element('label', {
                    htmlFor: 'sites_select'
                }, ['Site:']));
                select = this._udoc.create_element('select', {id: 'sites_select', size: 1});
                select.appendChild(this._udoc.create_element('option', {}, ''));
                
                Util.Event.add_event_listener(select, 'change', load_site.bind(this));
            }
            
            new_items.each(function(item) {
                var uri = this._sanitize_uri(item.link);
                var selected = (this._initially_selected_site_uri)
                    ? item.link == this._initially_selected_site_uri
                    : this._default_site_regexp.test(item.link);
                
                var option = this._udoc.create_element('option', {value: uri,
                        selected: selected});
                option.innerHTML = item.title;
                
                select.appendChild(option);
            }.bind(this));
            
            this._sites_progress.remove();
            
            if (select.parentNode != sites_pane)
                sites_pane.appendChild(select);
            
            this.item_selector.insert(sites_pane.parentNode);
            
            if (select.selectedIndex > 0) {
                // Delay this step by a trivial amount to allow the browser
                // to continue execution and render the current state of the
                // page.
                
                var self = this;
                Util.Scheduler.defer(function() {
                    load_site.call(self);
                });
            }
                
        }.bind(this));
        
        reader.add_event_listener('error', report_error.bind(this));
        reader.add_event_listener('timeout', function() {
            report_error.call(this, 'Failed to load the list of sites within a reasonable amount of time.');
        }.bind(this));
        
        try {
            reader.load(null, 10 /* 10 = 10 seconds until timeout */);
        } catch (e) {
            var message = e.message || e;
            report_error(message);
        }
    };

    /**
     * Called as an event listener when the user clicks the submit
     * button. 
     */
    this._internal_submit_listener = function()
    {
        var self = this;
        var tab_name = this._tabset.get_name_of_selected_tab();
        
        if (!this._sanity_error_displays) {
            this._sanity_error_displays = {};
        }
        
        function get_error_display() {
            if (!self._sanity_error_displays[tab_name]) {
                self._sanity_error_displays[tab_name] = new UI.Error_Display(
                    self._tabset.get_tabpanel_elem(tab_name));
            }
            
            return self._sanity_error_displays[tab_name];
        }
        
        if (!this._initially_selected_item.uri) {
            UI.Page_Link_Dialog._default_tab = tab_name;
        }
        
        function do_submission() {
            // Call external event listener
            self._external_submit_listener({
                uri: uri,
                new_window: self._new_window_checkbox.checked,
                title: self._link_title_input.value
            });

            // Close dialog window
            self._dialog_window.window.close();
        }
        
        function capitalize(s) {
            return s.charAt(0).toUpperCase() + s.substr(1).toLowerCase();
        }

        var uri, match, display_uri, actions;
        var errdisp = get_error_display();
        var verb = (!this._initially_selected_item.uri) ? 'insert' : 'save';
        if (tab_name == 'rss') {
            uri = this.item_selector.get_uri();
            if (!uri) {
                this._dialog_window.window.alert('Please select a page to be linked to.');
                return false;
            }
        } else if (tab_name == 'custom') {
            uri = this._custom_input.value;
            
            // Check for an email address here.
            if (!(/^mailto:/).test(uri) && (/@/).test(uri) && !(///).test(uri)) {
                function fix_email() {
                    self._email_input.value = uri;
                    self._tabset.select_tab('email');
                    errdisp.clear();
                }
                
                actions = [
                    ["Take me to the right place for an email address.", fix_email],
                    ["No, " + verb + " the link as-is.", do_submission]
                ];
                errdisp.show("If you want to link to an email address, you " +
                    "should use the "" + this._EMAIL_TAB_STR + "" tab " +
                    "instead.", actions);
                return;
            }
            
            // Check for a link to the local system.
            if ((/^file:/).test(uri) || (/[A-Za-z]:\/).test(uri)) {
                errdisp.show("That link points to a file on your computer. " +
                    "It will not work if it is clicked on from any other " +
                    "computer. You should upload the file to the Web first. " +
                    "(If you need help doing that, contact your site " +
                    "administrator.)", [[
                        "Ignore this warning and link to the local file.",
                            do_submission
                    ]]);
                return;
            }
            
            // Check for weird-protocol links.
            match = /^(w+):/.exec(uri);
            if (match && !(/^(?:https?|mailto|ftp):/.test(uri))) {
                actions = [[
                    "I understand; " + verb + " the link anyway.", do_submission
                ]];
                errdisp.show("This link uses the the <strong>" +
                    match[1].toLowerCase() + "</strong> protocol. Web " +
                    "browsers may not be able to open this link directly.",
                    actions);
                return;
            }
            
            // Check for an empty link.
            if (uri.replace(/^w+:(?://)?(?:www.?)?/, '').length <= 0) {
                errdisp.show("You haven't entered anything to link to.",
                    [["Ignore this warning and " + verb + " the link anyway.",
                        do_submission]]);
                return;
            }
            
            // Check for a cross-domain link with no protocol.
            if (!(/^#/).test(uri) && !(/^w+:/).test(uri) && (/^[^/]+.[A-Za-z]+/).test(uri)) {
                if (uri.length > 20) {
                    display_uri = uri.substr(0, 20) + '&hellip;';
                } else {
                    display_uri = uri;
                }
                
                function add_scheme() {
                    self._custom_input.value = 'http://' + uri;
                    errdisp.clear();
                }
                
                actions = [
                    ["Fix it.", add_scheme],
                    [capitalize(verb) + " the link as-is.",
                        do_submission]
                ];
                errdisp.show("Did you mean to link to link to the Web site "
                    + "<strong>http://</strong>" + display_uri + '? If you ' +
                    'did, the link won't work without the http:// at the ' +
                    'beginning.', actions);
                return;
            }
        } else if (tab_name == 'email') {
            uri = this._email_input.value;
            if (!(/@/).test(uri) || ((/^w+:/).test(uri) && !(/^mailto:/).test(uri)) || (/^www./).test(uri)) {
                if (uri.length > 20) {
                    display_uri = uri.substr(0, 20) + '&hellip;';
                } else {
                    display_uri = uri;
                }
                
                function fix_non_email() {
                    self._custom_input.value = uri;
                    self._tabset.select_tab('custom');
                    errdisp.clear();
                }
                
                actions = [
                    ["Take me to the right place for a Web page link.", fix_non_email],
                    ["No, " + verb + " the link as-is.", do_submission]
                ];
                errdisp.show("You've asked to link to an email address, " +
                    "but " + uri + " doesn't look like one (maybe it's a Web " +
                    "page?). Are you sure you want to continue?", actions);
                return;
            }
            
            if (!(/^mailto:/).test(uri))
                uri = "mailto:" + uri;
        } else {
            throw new Error('Bizarre error: unknown tab "' + tab_name + '".');
        }
        
        // We made it to the end! Let's go through with it.
        do_submission();
    };
    
    this._determine_tab = function determine_tab(use_rss)
    {
        if (arguments.length == 0)
            use_rss = this._use_rss;
        
        if (!this._initially_selected_item.uri) {
            return UI.Page_Link_Dialog._default_tab || (use_rss && 'rss') ||
                'custom';
        } else if (use_rss) {
            return 'rss';
        } else if (/^mailto:/.test(this._initially_selected_item.uri)) {
            return 'email';
        } else {
            return 'custom';
        }
    }
    
    this._select_tab = function select_tab(tab)
    {
        this._tabset.select_tab(tab);
        this._initialize_link_information(tab);
    }

    this._apply_initially_selected_item = function()
    {    
        var tab = this._determine_tab();
        
        if (tab == 'rss' && this._initially_selected_item.uri) {
            this._load_finder(this._finder_feed);
        } else {
            this._select_tab(tab);
            if (this._sites_feed && this._use_rss)
                this._load_sites(this._sites_feed);
        }
    };

    this._finder_listener = function()
    {
        if (!this._use_rss || !this._initially_selected_site_uri) {
            // Not found (or RSS not in use at all, which would be odd...)
            this._select_tab(this._determine_tab(false));
        } else {
            this._select_tab('rss');
        }
        
        this._load_sites(this._sites_feed);
    };

    /**
     * When a tab other than the RSS one is selected,
     * when the SELECT elements in the RSS tab switch
     * to "Loading ..." and back to displaying elements,
     * IE displays them on whatever tab is currently selected
     * as well as on the hidden RSS tab.
     * 
     * This function avoids that by re-selecting the
     * currently selected tab. But we don't re-select the
     * RSS tab if it's selected, because re-selecting that
     * tab causes the document to flicker, and we the bug
     * doesn't surface there anyway.
     *
     * XXX: At some point it might make sense to hack more
     * on Util.Select to avoid this bug altogether. I think
     * the solution would be to never add or remove options
     * from a displayed select--but hiding and reshowing
     * the selects gets complicated because so much in
     * this dialog is done asynchronously.
     *
     * XXX: This has been maybe neutered by my changes to this dialog. -EN
     */
    this._workaround_ie_select_display_bug = function()
    {
        if (window.attachEvent && !window.opera) // XXX: icky IE detection
        {
            var tab_name = this._tabset.get_name_of_selected_tab();
            if ( tab_name != 'rss' )
            {
                this._tabset.select_tab(tab_name);
                this._initialize_link_information(tab_name);
            }
        }
    }

    /**
     * Appends a chunk with extra options for links.
     */
    this._append_link_information_chunk = function()
    {
        // Link title
        this._link_title_input = this._dialog_window.document.createElement('INPUT');
        this._link_title_input.size = 40;
        this._link_title_input.id = 'link_title_input';

        var lt_label = this._dialog_window.document.createElement('LABEL');
        var strong = this._dialog_window.document.createElement('STRONG');
        strong.appendChild( this._dialog_window.document.createTextNode('Description: ') );
        lt_label.appendChild(strong);
        lt_label.htmlFor = 'link_title_input';

        lt_comment = this._dialog_window.document.createElement('DIV');
        Util.Element.add_class(lt_comment, 'comment');
        lt_comment.innerHTML = '(Will appear in some browsers when mouse is held over link.)';

        var lt_chunk = this._dialog_window.document.createElement('DIV');
        lt_chunk.appendChild(lt_label);
        lt_chunk.appendChild(this._link_title_input);
        lt_chunk.appendChild(lt_comment);

        // "Other options"
        this._other_options_chunk = this._dialog_window.document.createElement('DIV');
        this._other_options_chunk.id = 'other_options';
        if ( this._initially_selected_item.new_window == true )
            this._other_options_chunk.style.display = 'block';
        else
            this._other_options_chunk.style.display = 'none';

        var other_options_label = this._dialog_window.document.createElement('H3');
        var other_options_a = this._udoc.create_element('A',
            {href: 'javascript:void(0)'},
            ['More Options']);
            
        var self = this;
        Util.Event.add_event_listener(other_options_a, 'click', function() {
            if (self._other_options_chunk.style.display == 'none') {
                self._other_options_chunk.style.display = 'block';
                other_options_a.firstChild.nodeValue = 'Fewer Options'
            } else {
                self._other_options_chunk.style.display = 'none';
                other_options_a.firstChild.nodeValue = 'More Options'
            }
        });
        other_options_label.appendChild(other_options_a);
        
        // Checkbox
        this._new_window_checkbox = this._dialog_window.document.createElement('INPUT');
        this._new_window_checkbox.type = 'checkbox';
        this._new_window_checkbox.id = 'new_window_checkbox';

        var nw_label = this._dialog_window.document.createElement('LABEL');
        nw_label.appendChild( this._dialog_window.document.createTextNode('Open in new browser window') );
        nw_label.htmlFor = 'new_window_checkbox';

        var nw_chunk = this._dialog_window.document.createElement('DIV');
        nw_chunk.appendChild(this._new_window_checkbox);
        nw_chunk.appendChild(nw_label);

        this._other_options_chunk.appendChild(nw_chunk);

        // Create fieldset and its legend, and append to fieldset
        var fieldset = new Util.Fieldset({legend : 'Link information', document : this._dialog_window.document});
        fieldset.fieldset_elem.appendChild(lt_chunk);
        fieldset.fieldset_elem.appendChild(other_options_label);
        fieldset.fieldset_elem.appendChild(this._other_options_chunk);

        // Append fieldset chunk to dialog
        this._main_chunk.appendChild(fieldset.chunk);
    };

    /**
     * During initialization, as the various feeds load, the selected tab may change several
     * times. We only want whichever tab is ultimately selected to have the initially set
     * link information--the other tabs should have default values. So this function is
     * called every time a tab change occurs during init, and changes the newly selected
     * tab's information to the initial information, and the other tabs' information to 
     * defaults.
     */
    this._initialize_link_information = function(tab_name)
    {
        // Set all tabs to default values
        ['rss', 'custom', 'email'].each(function (name) {
            this._link_information[name] = {
                link_title: '',
                new_window: ''
            }
        }, this);

        // set given tab to initial values
        this._link_information[tab_name] =
        {
            link_title : this._initially_selected_item.title,
            new_window : this._initially_selected_item.new_window
        }

        this._set_link_title_input_value(this._initially_selected_item.title);
        this._new_window_checkbox.checked = this._initially_selected_item.new_window;
    }
    
    this._set_link_title_input_value = function(value)
    {
        this._link_title_input.value = value || '';
    }

    /**
     * Updates the link information depending on which tab is selected. It's a little
     * hack-y to have this outside of the tabset, perhaps ... but it was requested late 
     * in the game, so I'm just doing this quick and dirty.
     */
    this._update_link_information = function(old_name, new_name)
    {
        // save old information
        this._link_information[old_name] =
        {
            link_title : this._link_title_input.value,
            new_window : this._new_window_checkbox.checked
        };

        // set new information
        if ( this._link_information[new_name] != null )
        {
            this._set_link_title_input_value(this._link_information[new_name].link_title);
            this._new_window_checkbox.checked = this._link_information[new_name].new_window;
        }
        else
        {
            this._set_link_title_input_value('');
            this._new_window_checkbox.checked = false;
        }
    };
    
    this._update_link_title = function update_link_title(tab_name, title)
    {
        var info;
        var active = (this._tabset.get_name_of_selected_tab() == tab_name);
        if (!(info = this._link_information[tab_name])) {
            info = this._link_information[tab_name] = {
                link_title: '',
                new_window: (active && this._new_window_checkbox.checked)
            };
        }
        
        info.link_title = title;
        if (active)
            this._set_link_title(title);
    }

    /**
     * Creates and appends a chunk containing a "remove link" button. 
     * Also attaches 'click' event listeners to the button.
     */
    this._append_remove_link_chunk = function()
    {
        var button = this._dialog_window.document.createElement('BUTTON');
        button.setAttribute('type', 'button');
        button.appendChild( this._dialog_window.document.createTextNode('Remove link') );

        var self = this;
        var listener = function()
        {
            self._external_submit_listener({uri : '', new_window : false, title : ''});
            self._dialog_window.window.close();
        };
        Util.Event.add_event_listener(button, 'click', listener);

        // Setup their containing chunk
        var chunk = this._dialog_window.document.createElement('DIV');
        Util.Element.add_class(chunk, 'remove_chunk');
        chunk.appendChild(button);

        // Append the containing chunk
        this._dialog_window.body.appendChild(chunk);
    };
}

UI.Page_Link_Dialog._default_tab = null;

// file UI.Page_Link_Keybinding.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents keybinding.
 */
UI.Page_Link_Keybinding = function()
{
    Util.OOP.inherits(this, UI.Keybinding);

    this.test = function(e) { return this.matches_keycode(e, 75) && e.ctrlKey; }; // Ctrl-K
    this.action = function() { this._link_helper.open_page_link_dialog(); };

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._link_helper = (new UI.Link_Helper).init(loki);
        return this;
    };
};

// file UI.Page_Link_Selector.js

/**
 * @class Used by UI.Page_Link_Dialog to allow selection of an item of various
 * types on a site.
 *
 * Replaces a ton of poorly-written code that used to exist directly in
 * UI.Page_Link_Dialog.
 *
 * @author Eric Naeseth
 */
UI.Page_Link_Selector = function(dialog)
{
    var doc = dialog._doc;
    var dh = dialog._udoc;
    
    var wrapper = dh.create_element('div', {id: 'pane_wrapper'});
    var message = new UI.Page_Link_Selector.Message_Display(wrapper);
    var please_choose = doc.createTextNode('Please choose a site from the above box.');
    var site = {url: null, name: null};
    
    var error = null;
    var types = [];
    
    this.dialog = dialog;
    
    this.get_uri = function()
    {
        function get_field(name)
        {
            var list = doc.getElementsByName(name);
            return (!list || list.length == 0)
                ? null
                : list[0];
        }
        
        var item_select = get_field('item');
        var anchor_select = get_field('anchor');
        
        function get_anchor()
        {
            var i;
            
            if (!anchor_select)
                return '';
                
            i = anchor_select.selectedIndex || -1;
            
            if (anchor_select.value)
                return '#' + anchor_select.value;
            else if (i > 0)
                return '#' + anchor_select.options[i].value;
            
            return '';
        }
        
        if (!item_select)
            return null;
        
        var url = item_select.options[item_select.selectedIndex].value;
        var anchor = get_anchor();
        
        if (url.length == 0) {
            if (anchor.length == 0)
                return null;
        } else {
            var parsed_uri = Util.URI.parse(url);
            if (!parsed_uri.authority) {
                url = '//' + this.dialog._loki.editor_domain() + url;
            }
        }
        
        return url + anchor;
    }
    
    // Be advised: Util.State_Machine wraps the states' enter() methods;
    // when some_state.enter() is called directly, it's equivalent to
    // calling machine.change(some_state).
    Util.OOP.inherits(this, Util.State_Machine, {
        initial: {
            enter: function() {
                message.insert();
                message.setText(please_choose);
            },
            
            exit: function() {
                message.remove();
            }
        },
        
        loading_site: {
            enter: function()
            {
                types = [];
                
                message.insert();
                message.setHTML('Loading &ldquo;' + site.name +
                    '&rdquo&hellip;');
                
                var reader = new Util.RSS.Reader(site.url);
                var machine = this.machine;
                
                reader.add_event_listener('load', function(feed)
                {
                    if (feed.items.length == 0) {
                        machine.states.error.set('No link types are available' +
                            ' to choose from.', function() {
                                machine.change('loading_site')
                            });
                        machine.states.error.enter();
                        return;
                    }
                    
                    feed.items.each(function(item) {
                        types.push({
                            name: (item.plural_title || item.title),
                            instance_name: item.title,
                            url: dialog._sanitize_uri(item.link),
                            is_default: (dialog._initially_selected_type_uri)
                                ? Util.URI.equal(item.link, dialog._initially_selected_type_uri)
                                : dialog._default_type_regexp.test(item.link)
                        });
                    });
                    
                    types.sort(function(a, b) {
                        return (a.name == b.name)
                            ? 0
                            : (a.name < b.name ? -1 : 1);
                    });
                    
                    machine.change('interactive');
                });
                
                reader.add_event_listener('error', function (error_msg, code)
                {
                    machine.states.error.set('Failed to load the site: ' + error_msg,
                        function() {
                            machine.change('loading_site');
                        }
                    );
                    machine.states.error.enter();
                });
                
                reader.add_event_listener('timeout', function() {
                    machine.states.error.set('Failed to load the site: ' +
                        'The operation timed out.',
                        function() {
                            machine.change('loading_site');
                        }
                    );
                    machine.states.error.enter();
                });
                
                try {
                    reader.load(null, 10 /* 10 = 10 seconds until timeout */);
                } catch (e) {
                    (function report_error_shortly() {
                        machine.states.error.set('Failed to load the site: ' + 
                            (e.message || e),
                            function() {
                                machine.change('loading_site');
                            }
                        );
                        machine.states.error.enter();
                    }).defer(); // defer to prevent state machine deadlock
                }
                
            },
            
            exit: function(new_state)
            {
                if (new_state != this.machine.states.interactive)
                    message.remove();
            }
        },
        
        interactive: {
            types_pane: null,
            types_list: null,
            
            links_pane: null,
            arbiter: new UI.Page_Link_Selector.Item_Selector(dialog, wrapper),
            
            enter: function(old_state)
            {
                this.types_list = dh.create_element('ul',
                    {id: 'types_pane_ul'});

                var prev_selected_li = null;
                function select_type(type, li) {
                    if (prev_selected_li) {
                        Util.Element.remove_class(prev_selected_li,
                            'selected');
                    }
                    
                    Util.Element.add_class(li, 'selected');
                    prev_selected_li = li;
                    this.arbiter.load(type);
                }

                var selected_type = null;
                types.each(function(type) {
                    var link = dh.create_element('a', {}, [type.name]);

                    var item = dh.create_element('li', {}, [link]);
                    this.types_list.appendChild(item);
                    
                    Util.Event.add_event_listener(link, 'click', function(e)
                    {
                        try {
                            dialog._update_link_title('rss', '');
                            select_type.call(this, type, item);
                        } finally {
                            Util.Event.prevent_default(e || window.event);
                        }
                    }.bind(this));
                    
                    if (type.is_default)
                        selected_type = [type, item];
                }.bind(this));

                this.types_pane = dh.create_element('div', {id: 'types_pane'},
                    [this.types_list]);
                
                if (old_state == this.machine.states.loading_site)
                    message.remove();

                Util.Element.add_class(wrapper, 'contains_types');
                wrapper.appendChild(this.types_pane);
                this.arbiter.change('message');
                
                if (selected_type) {
                    // Delay this step by a trivial amount to allow the browser
                    // to continue execution and render the current state of the
                    // page.
                    
                    (function() {
                        select_type.apply(this, selected_type);
                    }).bind(this).defer();
                }
                    
            },
            
            exit: function()
            {
                this.arbiter.states.inactive.enter();
                
                wrapper.removeChild(this.types_pane);
                Util.Element.remove_class(wrapper, 'contains_types');
            }
        },
        
        error: new UI.Error_State(wrapper)
    }, 'initial', 'Type selector');
    
    this.insert = function(container)
    {
        container.appendChild(wrapper);
    }
    
    this.remove = function()
    {
        if (wrapper.parentNode)
            wrapper.parentNode.removeChild(wrapper);
    }

    this.revert = function()
    {
        this.states.initial.enter();
    }
    
    this.load = function(site_name, site_url)
    {
        site.name = site_name;
        site.url = site_url;
        this.states.loading_site.enter();
    }
    
    this.reload = function()
    {
        this.states.loading_site.enter();
    }
}

/**
 * @class Chooses the item.
 */
UI.Page_Link_Selector.Item_Selector = function(dialog, wrapper)
{
    var doc = wrapper.ownerDocument;
    var dh = new Util.Document(doc);
    
    var message = new UI.Page_Link_Selector.Message_Display(wrapper);
    var please_choose = doc.createTextNode(
        'Please choose the type of item to which you want to link.');
    
    var inline_p_name = null;
    var type = null;
    var error = null;
    var items = null;
    var uris_to_items = null;
    
    this.load = function(new_type)
    {
        type = new_type;
        inline_p_name = type.name.toLowerCase();
        this.change('loading');
    }
    
    Util.OOP.inherits(this, Util.State_Machine, {
        inactive: {
            enter: function() {
                
            },
            
            exit: function() {
                
            }
        },
        
        message: {
            enter: function() {
                message.insert();
                message.setText(please_choose);
            },
            
            exit: function() {
                message.remove();
            }
        },
        
        loading: {
            enter: function() {
                message.insert();
                message.setHTML('Loading ' + inline_p_name + '&hellip;');
                
                var reader = new Util.RSS.Reader(type.url);
                var machine = this.machine;
                var initial_uri = // XXX: REASON HACK
                    Util.URI.strip_https_and_http(dialog._initially_selected_nameless_uri);

                reader.add_event_listener('load', function(feed)
                {
                    items = [];
                    uris_to_items = {};
                    
                    if (type.is_default) {
                        // XXX: this is kinda hackish
                        items.push(
                            {
                                value: '',
                                text: '(current ' + type.instance_name.toLowerCase() + ')'
                            }
                        );
                    } else if (feed.items.length == 0) {
                        machine.states.error.set('No ' +
                            type.name.toLowerCase() + ' are available to ' +
                            'choose from.', function() {
                                machine.change('loading')
                            });
                        machine.states.error.enter();
                        return;
                    }
                    
                    // We are not sorting the feed items because the server
                    // might be doing fancy things (e.g. nesting).
                    
                    feed.items.each(function(item) {
                        var uri = dialog._sanitize_uri(item.link);
                        var item = {
                            title: item.title,
                            text: item.selector_text || item.title,
                            value: uri,
                            selected: (initial_uri)
                                ? Util.URI.equal(initial_uri, item.link)
                                : false
                        };
                        items.push(item);
                        uris_to_items[uri] = item;
                    });

                    machine.states.interactive.enter();
                });

                reader.add_event_listener('error', function (error_msg, code)
                {
                    machine.states.error.set('Failed to load the ' + 
                        inline_p_name + ': ' + error_msg,
                        function() {
                            machine.change('loading');
                        }
                    );
                    machine.states.error.enter();
                });
                
                reader.add_event_listener('timeout', function() {
                    machine.states.error.set('Failed to load the ' + 
                        inline_p_name + ': The operation timed out.',
                        function() {
                            machine.change('loading');
                        }
                    );
                    machine.states.error.enter();
                });

                try {
                    reader.load(null, 10 /* 10 = 10 seconds until timeout */);
                } catch (e) {
                    (function report_error_shortly() {
                        machine.states.error.set('Failed to load the ' + 
                            inline_p_name + ': ' + (e.message || e),
                            function() {
                                machine.change('loading');
                            }
                        );
                        machine.states.error.enter();
                    }).defer(); // defer to prevent state machine deadlock
                }
            },
            
            exit: function() {
                message.remove();
            }
        },
        
        interactive: {
            form: null,
            pane: null,
            
            enter: function()
            {
                this.pane = dh.create_element('form', {className: 'generated', id: 'links_pane'});
                
                this.form = new Util.Form(doc, {
                    name: 'Item Selector',
                    form: this.pane
                });

                var section = this.form.add_section();
                var select = section.add_select_field(type.instance_name,
                    items, {name: 'item'});
                    
                function item_changed()
                {
                    var el = select.element;
                    var option = el.options[el.selectedIndex];
                    var item = uris_to_items[option.value];
                    var title;
                    var initial = dialog._sanitize_uri(dialog._initially_selected_item.uri);
                    
                    if (initial == option.value) {
                        title = dialog._initially_selected_item.title;
                    } else {
                        // "item" may not be set if we're on the current page
                        title = (item) ? item.title : '';
                    }
                    
                    dialog._update_link_title('rss', title);
                }
                    
                Util.Event.add_event_listener(select.element, 'change',
                    item_changed);
                item_changed();
                
                wrapper.appendChild(this.form.form_element);
                
                // XXX: wonky in IE; neglect it for now.
                if (!Util.Browser.IE) {
                    (function () {
                        var select_box = select.element;
                        var needed_width = select_box.offsetLeft + select_box.offsetWidth;
                        var dialog_window = dialog._dialog_window.window;

                        var width_diff;
                        var height;
                        var dd = dialog_window.document;

                        if (dialog_window.outerHeight) {
                            width_diff =
                                (dialog_window.outerWidth - dialog_window.innerWidth);
                            height = dialog_window.outerHeight;
                        } else if (dd.documentElement && dd.documentElement.clientHeight) {
                            width_diff = 0;
                            height = dd.documentElement.clientHeight;
                        } else if (dd.body.clientHeight) {
                            width_diff = 0;
                            height = dd.body.clientHeight;
                        } else {
                            return;
                        }

                        var ideal_width = needed_width + 55 + width_diff;
                        var screen = dialog_window.screen;
                        var screen_x = dialog_window.screenX - screen.left;
                        
                        if (screen_x + ideal_width >= screen.availWidth - 10) {
                            ideal_width =
                                window.screen.availWidth - screen_x - 10;
                        }

                        dialog_window.resizeTo(
                            [dialog._dialog_window_width, ideal_width].max(),
                            height);
                    }).delay(.15);
                }
                
                
                function AnchorField()
                {
                    Util.OOP.inherits(this, Util.Form.FormField, "Anchor");
                    
                    var state = 'loading';
                    var container = null;
                    var present = null;
                    
                    var activity = dialog.create_activity_indicator('bar');
                    // TODO: display a text input box instead of the message
                    var message = dh.create_element('p',
                        {style: {margin: '0px', fontStyle: 'italic'}},
                        ['(No anchors were found.)']);
                    var selector = null;
                    var entry = null;
                    
                    function show_no_anchors_message()
                    {
                        if (state != 'none') {
                            present.parentNode.removeChild(present);
                            present = message;
                            container.appendChild(present);
                            state = 'none';
                        }
                    }
                        
                    function show_anchors(anchors)
                    {
                        if (anchors.length == 0) {
                            show_no_anchors_message();
                            return;
                        }
                        
                        if (state == 'interactive') {
                            while (selector.childNodes.length > 0)
                                selector.removeChild(selector.firstChild);
                        } else {
                            selector = dh.create_element('select', 
                                {name: 'anchor', size: 1});
                            present.parentNode.removeChild(present);
                            present = selector;
                            container.appendChild(present);
                            state = 'interactive';
                        }
                        
                        selector.appendChild(dh.create_element('option',
                            {value: ''}, ['(none)']));
                        
                        anchors.each(function(a) {
                            selector.appendChild(dh.create_element('option',
                                {
                                    value: a,
                                    selected: (dialog._initially_selected_name == a)
                                }, [a]));
                        });
                    }
                    
                    function show_manual_entry()
                    {
                        if (!entry) {
                            entry = dh.create_element('input',
                                {name: 'anchor', type: 'text', size: 15});
                            if (dialog._initially_selected_name)
                                entry.value = dialog._initially_selected_name;
                        }
                        
                        if (present)
                            present.parentNode.removeChild(present);
                        present = entry;
                        container.appendChild(present);
                        state = 'interactive';
                    }
                    
                    this.load = function(url)
                    {
                        if (state != 'loading') {
                            present.parentNode.removeChild(present);
                            present = activity.indicator;
                            container.appendChild(present);
                            state = 'loading';
                        }
                        
                        if (url == '') {
                            // use the current document's anchors
                            show_anchors(dialog._anchor_names);
                        } else {
                            var request = null;
                            
                            function nothing_found()
                            {
                                request.abort();
                                show_manual_entry();
                            }
                            
                            function is_html_type()
                            {
                                var type = request.get_header('Content-Type');
                                if (!type)
                                    return false;
                                
                                var acceptable_types =
                                    ['text/html', 'text/xml', 'application/xml',
                                    'application/xhtml+xml'];
                                
                                return acceptable_types.find(function (t) {
                                    return (type.indexOf(t) >= 0);
                                });
                            }
                            
                            var options = {
                                method: 'get',
                                timeout: 10,
                                
                                on_interactive: function(request)
                                {
                                    if (!request.successful() || !is_html_type())
                                        nothing_found();
                                },
                                
                                on_failure: function()
                                {
                                    nothing_found();
                                },
                                
                                on_success: function(request, transport)
                                {
                                    if (!is_html_type())
                                        nothing_found();
                                    
                                    var parser = new Util.HTML_Parser();
                                    var names = [];

                                    parser.add_listener('open', function(tag, params) {
                                        if (tag.toUpperCase() == 'A') {
                                            if (params.name && !params.href)
                                                names.push(params.name);
                                        }
                                    })
                                    parser.parse(transport.responseText);
                                    
                                    show_anchors(names);
                                }
                            };
                            
                            try {
                                request = new Util.Request(url, options);
                            } catch (e) {
                                show_manual_entry();
                            }
                            
                        }
                    }
                    
                    var really_append = this.append;
                    this.append = function(form, doc, dh, target)
                    {
                        container = target;
                        really_append.call(this, form, doc, dh, target);
                    }
                    
                    this.create_element = function(doc, dh)
                    {
                        present = activity.indicator;
                        return present;
                    }
                }
                
                var af = new AnchorField();
                section.add_field(af);
                
                function load_anchors()
                {
                    var se = select.element;
                    af.load(se.options[se.selectedIndex].value);
                }
                
                Util.Event.add_event_listener(select.element, 'change', function() {
                    load_anchors();
                });
                load_anchors();
            },
            
            exit: function()
            {
                if (this.form) {
                    this.form = null;
                }
                
                if (this.pane)
                    this.pane.parentNode.removeChild(this.pane);
            }
        },
        
        error: new UI.Error_State(wrapper)
    }, 'inactive', 'Item selector');
}

/**
 * @class Displays an instructional or loading message.
 */
UI.Page_Link_Selector.Message_Display = function(wrapper)
{
    var doc = wrapper.ownerDocument;
    var message = Util.Document.create_element(doc, 'p', {className: 'message'});

    this.insert = function() {
        if (message.parentNode != wrapper)
            wrapper.appendChild(message);
    }

    this.remove = function() {
        if (message.parentNode)
            message.parentNode.removeChild(message);
    }

    this.setText = function(text)
    {
        if (typeof(text) == 'string')
            text = doc.createTextNode(text);

        while (message.childNodes.length > 0)
            message.removeChild(message.firstChild);

        message.appendChild(text);
    }
    
    this.setHTML = function(html)
    {
        while (message.childNodes.length > 0)
            message.removeChild(message.firstChild);
        
        message.innerHTML = html;
    }
}

// file UI.Paragraph_Helper.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Paragraph helper
 */
UI.Paragraph_Helper = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Helper);

    this.needs_paragraphifying = function(node)
    {
        return node != null && node.nodeName == 'BODY';
        //return ( Util.Node.get_nearest_bl_ancestor_element(node).nodeName == 'BODY' )
    };

    this.possibly_paragraphify = function()
    {
        var sel = Util.Selection.get_selection(self._loki.window);
        var rng = Util.Range.create_range(sel);
        var container = Util.Range.get_start_container(rng);

        if ( this.needs_paragraphifying(container) )
        {
            this._loki.toggle_block('p');
        }
    };
};

// file UI.Paste_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents toolbar button.
 */
UI.Paste_Button = function()
{
    Util.OOP.inherits(this, UI.Button);

    this.image = 'paste.png';
    this.title = 'Paste (Ctrl+V)';
    this.click_listener = function()
    {
        this._clipboard_helper.paste();
    };

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._clipboard_helper = (new UI.Clipboard_Helper).init(this._loki);
        return this;
    };
};

// file UI.Paste_Keybinding.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents keybinding.
 */
UI.Paste_Keybinding = function()
{
    Util.OOP.inherits(this, UI.Keybinding);

    this.test = function(e) {
        if (Util.Browser.Gecko && Util.Browser.Windows && !this.loki.owner_window.GeckoClipboard)
            return false;
        return this.matches_keycode(e, 86) && e.ctrlKey;
    }; //Ctrl-V
    this.action = function() 
    {
        // try-catch so that if anything should go wrong, paste
        // still happens
        try
        {
            this._clipboard_helper.paste();
            return false;
        }
        catch(e)
        {
            return true;
        }
    };

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._clipboard_helper = (new UI.Clipboard_Helper).init(this._loki);
        return this;
    };
};

// file UI.Pre_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents "pre" toolbar button.
 */
UI.Pre_Button = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Button);

    this.image = 'pre.png';
    this.title = 'Preformatted';
    this.click_listener = function() { self._loki.toggle_block('pre'); };
    this.state_querier = function() { return self._loki.query_command_state('FormatBlock') == 'pre'; };
};

// file UI.Raw_Source_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents "source" toolbar button.
 */
UI.Raw_Source_Button = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Button);

    this.image = 'debug_source.png';
    this.title = 'Alert raw source';
    this.show_on_source_toolbar = true;
    this.click_listener = function() { Util.Window.alert_debug(self._loki.get_dirty_html()); };
};

// file UI.Right_Align_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents "right align" toolbar button.
 */
UI.Right_Align_Button = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Button);

    this.image = 'align_right.png';
    this.title = 'Right align (Ctrl+R)';
    this.click_listener = function() { self._loki.exec_command('JustifyRight'); };
    this.state_querier = function() { return self._loki.query_command_state('JustifyRight'); };
};

// file UI.Right_Align_Keybinding.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents keybinding.
 */
UI.Right_Align_Keybinding = function()
{
    Util.OOP.inherits(this, UI.Keybinding);
    this.test = function(e) { return this.matches_keycode(e, 82) && e.ctrlKey; }; // Ctrl-R
    //this.action = function() { this._loki.exec_command('JustifyRight'); };
    this.action = function() { this._align_helper.align_right(); };

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._align_helper = (new UI.Align_Helper).init(this._loki);
        return this;
    };
};

// file UI.Separator_Menuitem.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents a menuitem. Can be extended or used as it is.
 */
UI.Separator_Menuitem = function()
{
    var _label;

    this.init = function()
    {
        return this;
    };

    /**
     * Returns an appendable chunk to render the menuitem.
     */
    this.get_chunk = function(doc)
    {
        var sep = doc.createElement('HR');
        Util.Element.add_class(sep, 'separator_menuitem');
        return sep;
    };
};

// file UI.Shift_Tab_Keybinding.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents keybinding.
 */
UI.Shift_Tab_Keybinding = function()
{
    Util.OOP.inherits(this, UI.Keybinding);

    this.test = function(e) { return e.keyCode == 9 && e.shiftKey && this._tab_helper.is_no_default(); }; // Tab
    this.action = function() { this._tab_helper.shift_tab(); };

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._tab_helper = (new UI.Tab_Helper).init(this._loki);
        return this;
    };
};

// file UI.Source_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents "source" toolbar button.
 */
UI.Source_Button = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Button);

    this.image = 'source.png';
    this.title = 'Toggle HTML source view';
    this.show_on_source_toolbar = true;
    this.click_listener = function() {
        try {
            self._loki.toggle_iframe_textarea(); 
        } catch (e) {
            var sent = self._loki.crashed(e);
            alert("An error occurred that prevented your document's HTML " +
                "from being generated." +
                (sent ? " A report of this error has been sent." : "") +
                "nnTechnical details:n" +
                self._loki.describe_error(e));
        }
        
    };
};

// file UI.Spell_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class for running spell check.
 */
UI.Spell_Button = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Button);

    this.image = 'spellCheck.gif';
    this.title = 'Spell check (F7)';
    this.click_listener = function() { self._spell_helper.open_dialog(); };

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._spell_helper = (new UI.Spell_Helper).init(this._loki);
        return this;
    };
};

// file UI.Spell_Dialog.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A spell dialog window.
 */
UI.Spell_Dialog = function()
{
    Util.OOP.inherits(this, UI.Dialog);
    var self = this;

    this._dialog_window_width = 800;
    this._dialog_window_height = 300;

    this.init = function(params)
    {
        this._spell_uri = params.spell_uri;
        this._uncorrected_html = params.uncorrected_html;
        this.superclass.init.call(this, params);
    };

    this._append_style_sheets = function()
    {
        this.superclass._append_style_sheets.call(this);
        Util.Document.append_style_sheet(this._dialog_window.document, this._base_uri + 'css/Spell_Dialog.css');
    };

    /**
     * Called when the iframe finishes loading the spellchecked document.
     */
    this.finish_init_async = function(suggestion_list, words)
    {
        this._suggestion_list = suggestion_list;
        this._spell_iframe_document = Util.Iframe.get_content_document(this._spell_iframe);
        //this._words = Util.Document.get_elements_by_tag_name_ns(this._spell_iframe_document, 'http://www.carleton.edu/spell', 'WORD');
        this._words = words;
        messagebox('words', this._words);
        this._current_word_index = -1; // incremented to 0 in this._next
        this._done = false;
        this._enable_buttons();
        this._next();
    };

    this._set_title = function()
    {
        this._dialog_window.document.title = "Spell check";
    };

    this._populate_main = function()
    {
        this._append_spell_chunk();
        this._load_spell_data();
        this._append_submit_and_cancel_chunk('Apply Changes', 'Cancel Changes');
        var self = this;
        setTimeout(function () { self._resize_dialog_window(false, true); }, 1000);
    };

    this._append_spell_chunk = function()
    {
        var doc = this._dialog_window.document;
        var self = this;

        // Options

        var misspelled_label = doc.createElement('LABEL');
        misspelled_label.htmlFor = 'misspelled_input';
        misspelled_label.innerHTML = 'Misspelled word:';

        this._misspelled_input = doc.createElement('INPUT');
        this._misspelled_input.id = 'misspelled_input';
        this._misspelled_input.disabled = true;

        var replacement_label = doc.createElement('LABEL');
        replacement_label.htmlFor = 'replacement_input';
        replacement_label.innerHTML = 'Replacement:';

        this._replacement_input = doc.createElement('INPUT');
        this._replacement_input.id = 'replacement_input';

        var suggestions_label = doc.createElement('LABEL');
        suggestions_label.htmlFor = 'suggestions_select';
        suggestions_label.innerHTML = 'Suggestions:';

        this._suggestions_select = doc.createElement('SELECT');
        this._suggestions_select.id = 'suggestions_select';
        this._suggestions_select.size = 5;
        Util.Event.add_event_listener(this._suggestions_select, 'change', function() { self._replacement_input.value = self._suggestions_select.value; });

        var options_div = doc.createElement('DIV');
        Util.Element.add_class(options_div, 'options');
        options_div.appendChild(misspelled_label);
        options_div.appendChild(this._misspelled_input);
        options_div.appendChild(replacement_label);
        options_div.appendChild(this._replacement_input);
        options_div.appendChild(suggestions_label);
        options_div.appendChild(this._suggestions_select);

        // Actions

        this._replace_button = doc.createElement('BUTTON');
        this._replace_button.setAttribute('type', 'button');
        this._replace_button.appendChild( doc.createTextNode('Replace') );
        Util.Event.add_event_listener(this._replace_button, 'click', function(e) { self.replace(); });

        this._replace_all_button = doc.createElement('BUTTON');
        this._replace_all_button.setAttribute('type', 'button');
        this._replace_all_button.appendChild( doc.createTextNode('Replace all') );
        Util.Event.add_event_listener(this._replace_all_button, 'click', function() { self.replace_all(); });

        this._ignore_button = doc.createElement('BUTTON');
        this._ignore_button.setAttribute('type', 'button');
        this._ignore_button.appendChild( doc.createTextNode('Ignore') );
        Util.Event.add_event_listener(this._ignore_button, 'click', function() { self.ignore(); });

        this._ignore_all_button = doc.createElement('BUTTON');
        this._ignore_all_button.setAttribute('type', 'button');
        this._ignore_all_button.appendChild( doc.createTextNode('Ignore all') );
        Util.Event.add_event_listener(this._ignore_all_button, 'click', function() { self.ignore_all(); });

        this._disable_buttons();

        var replace_div = doc.createElement('DIV');
        Util.Element.add_class(replace_div, 'replace');
        replace_div.appendChild(this._replace_button);
        replace_div.appendChild(this._replace_all_button);

        var ignore_div = doc.createElement('DIV');
        Util.Element.add_class(ignore_div, 'ignore');
        ignore_div.appendChild(this._ignore_button);
        ignore_div.appendChild(this._ignore_all_button);

        var actions_div = doc.createElement('DIV');
        Util.Element.add_class(actions_div, 'actions');
        actions_div.appendChild(replace_div);
        actions_div.appendChild(ignore_div);

        // Document

        var spell_label = doc.createElement('DIV');
        spell_label.innerHTML = 'Document:';

        this._spell_iframe = doc.createElement('IFRAME');
        this._spell_iframe.setAttribute('style', 'width:100%; height:20ex;'); // XXX tmp
        //Util.Event.add_event_listener(this._spell_iframe, 'load', function() { self.finish_init_async() });
        this._dialog_window.window.do_onframeload = function(suggestion_list, words) { self.finish_init_async(suggestion_list, words); };
        this._spell_iframe.src = this._base_uri + 'auxil/loki_blank.html';

        /* The old way:
        this._spell_iframe = doc.createElement('IFRAME');
        this._spell_iframe.setAttribute('style', 'width:100%; height:20ex;'); // XXX tmp
        //Util.Event.add_event_listener(this._spell_iframe, 'load', function() { self.finish_init_async() });
        this._dialog_window.window.do_onframeload = function(suggestion_list, words) { self.finish_init_async(suggestion_list, words); };
        this._spell_iframe.src = this._base_uri + this._spell_uri + '?text=' + encodeURIComponent(this._uncorrected_html);
        */

        var spell_container = doc.createElement('DIV'); // XXX tmp
        spell_container.setAttribute('style', 'width:100%; height:20ex;'); // XXX tmp
        spell_container.appendChild(this._spell_iframe); // XXX tmp

        var document_div = doc.createElement('DIV');
        Util.Element.add_class(document_div, 'document');
        document_div.appendChild(spell_label);
        //document_div.appendChild(this._spell_iframe);
        document_div.appendChild(spell_container);

        // (the div-based layout breaks in IE--the iframe wraps no matter 
        // how wide the dialog--, and I can't figure out how to fix it, 
        // so just make a table)
        var table = this._dialog_window.document.createElement('TABLE');
        table.setAttribute('cellspacing', '0px');
        table.setAttribute('cellpadding', '0px');
        table.setAttribute('border', '0px');
        table.setAttribute('width', '100%');
        var tbody = this._dialog_window.document.createElement('TBODY');
        var tr = this._dialog_window.document.createElement('TR');
        var td = this._dialog_window.document.createElement('TD');
        td.setAttribute('valign', 'top');

        var options_td = td.cloneNode(true);
        Util.Element.add_class(options_td, 'options_td');
        options_td.appendChild(options_div);

        var actions_td = td.cloneNode(true);
        Util.Element.add_class(actions_td, 'actions_td');
        actions_td.appendChild(actions_div);

        var document_td = td.cloneNode(true);
        Util.Element.add_class(document_td, 'document_td');
        document_td.appendChild(document_div);

        tr.appendChild(options_td);
        tr.appendChild(actions_td);
        tr.appendChild(document_td);
        tbody.appendChild(tr);
        table.appendChild(tbody);

        // Heading and fieldset
        var h1 = this._dialog_window.document.createElement('H1');
        h1.innerHTML = 'Spell check';
        this._main_chunk.appendChild(h1);

        var fieldset = new Util.Fieldset({legend : '', document : this._dialog_window.document});
        fieldset.fieldset_elem.appendChild(table);
        /*
        fieldset.fieldset_elem.appendChild(options_div);
        fieldset.fieldset_elem.appendChild(actions_div);
        fieldset.fieldset_elem.appendChild(document_div);
        */
        this._main_chunk.appendChild(fieldset.chunk);
    };

    this._load_spell_data = function()
    {
        this._spell_http_reader = new Util.HTTP_Reader;
        var self = this;
        this._spell_http_reader.add_load_listener(function () { self._load_spell_data_async(); });
        this._spell_http_reader.load(this._base_uri + this._spell_uri, this._uncorrected_html);
        
    };

    this._load_spell_data_async = function()
    {
        var iframe_doc = Util.Iframe.get_content_document(this._spell_iframe);
        var iframe_html = this._spell_http_reader.request.responseText;
        iframe_doc.write(iframe_html);
        iframe_doc.close();
        var iframe_win = Util.Iframe.get_content_window(this._spell_iframe);
        if ( document.all ) // This works for IE. XXX this is sort of a hack
            setTimeout(function() { iframe_win.spell_iframe__do_onload(); }, 1000);
    };

    this._enable_buttons = function()
    {
        this._replace_button.disabled = false;
        this._replace_all_button.disabled = false;
        this._ignore_button.disabled = false;
        this._ignore_all_button.disabled = false;
    };

    this._disable_buttons = function()
    {
        this._replace_button.disabled = true;
        this._replace_all_button.disabled = true;
        this._ignore_button.disabled = true;
        this._ignore_all_button.disabled = true;
    };

    this._internal_submit_listener = function()
    {
        var html = this._spell_iframe_document.getElementsByTagName('BODY')[0].innerHTML;
        // XXX use dom?
        html = html.replace(new RegExp('<spell:word( [^>]*)>', 'gi'), '');
        html = html.replace(new RegExp('</spell:word>', 'gi'), '');
        html = html.replace(new RegExp('<?xml( [^>]*)spell( [^>]*)>', 'gi'), '');
        this._external_submit_listener({corrected_html : html});
        this._dialog_window.window.close();
    };


    this.replace = function()
    {
        if ( this._done )
            return;

        var word = this._words[this._current_word_index];
        word.innerHTML = this._replacement_input.value;
        word.setAttribute('
done', 'done');
        this._next();
    };

    this.replace_all = function()
    {
        if ( this._done )
            return;

        var word = this._words[this._current_word_index];
        // When we write to innerHTML below, <word> will, sadly, be
        // destroyed and recreated (although our indices for this._words 
        // will still work in the updated NodeList), so our
        // reference to it will be lost.
        // Therefore we get what we want from <word> here.
        var word_innerHTML = word.innerHTML;
        for ( var i = 0; i < this._words.length; i++ )
        {
            var cur = this._words[i];
            if ( !cur.getAttribute('
done') && cur.innerHTML == word_innerHTML )
            {
                cur.innerHTML = this._replacement_input.value;
                cur.setAttribute('
done', 'done');
            }
        }
        this._next();
    };

    this.ignore = function()
    {
        if ( this._done )
            return;

        this._next();
    };

    // not sure if this one is working
    this.ignore_all = function()
    {
        if ( this._done )
            return;

        var word = this._words[this._current_word_index];
        for ( var i = 0; i < this._words.length; i++ )
        {
            var cur = this._words[i];
            if ( !cur.getAttribute('
done') && cur.innerHTML == word.innerHTML )
            {
                cur.setAttribute('
done', 'done');
            }
        }
        this._next();
    };

    this._next = function()
    {
        // 1. Unhighlight the old word and unload suggestions for it
        if ( this._current_word_index > -1 )
        {
            Util.Element.remove_all_classes(this._words[this._current_word_index]);
        }    
        while ( this._suggestions_select.firstChild != null )
            this._suggestions_select.removeChild(this._suggestions_select.firstChild);

        // 2. Advance word_index
        do
        {
            this._current_word_index++;
            if ( this._current_word_index >= this._words.length )
            {
                if ( this._words.length == 0 )
                    this._dialog_window.window.alert('
No misspelled words have been found.');
                else
                    this._dialog_window.window.alert('
All words have been corrected or ignored.');
                this._disable_buttons();
                this._misspelled_input.value = '';
                this._replacement_input.value = '';
                this._replacement_input.disabled = true;
                this._suggestions_select.disabled = true;
                this._done = true;
                return false;
            }
        }
        while ( this._words[this._current_word_index].getAttribute('
done') )

        // 3. Highlight and scroll to the new word
        var word = this._words[this._current_word_index];
        Util.Element.add_class(word, '
current');
        /*
        var text_iframe_window = window.frames[0]; // I don'
t know how to get at the scroll_to_word function using 
        text_iframe_window
.scroll_to_word(word);   // W3 stuff like document.getElementById('text_iframe').
        
*/
        
// XXX try this
        
var spell_iframe_window Util.Iframe.get_content_window(this._spell_iframe);
        
spell_iframe_window.scroll_to_word(word);

        
// 4. Load suggestions into the suggestions listbox and the replacement textbox
        
var suggestions = eval( 'this._suggestion_list.' word.getAttribute('id') );
        if ( 
suggestions.length )
        {
            for (var 
0suggestions.lengthi++)
            {
                var 
the_item this._dialog_window.document.createElement('OPTION');
                
this._suggestions_select.appendChild(the_item);
                
the_item.value suggestions[i];
                
the_item.innerHTML suggestions[i];
            }
            
this._suggestions_select.selectedIndex 0;
            
this._replacement_input.value this._suggestions_select.value;
        }
        else
        {
            
this._replacement_input.value word.innerHTML;
        }

        
// 5. Update misspelled word textbox
        
this._misspelled_input.value word.innerHTML;
    };
};

// file UI.Spell_Helper.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class for helping insert an spell. Most
 * of this and the other spell check code is verbatim
 * from the old version (first: 14/May/2004) of Loki.
 */
UI.Spell_Helper = function()
{
    var 
self this;
    
Util.OOP.inherits(selfUI.Helper);

    
this.open_dialog = function()
    {
        if ( 
this._dialog == null )
            
this._dialog = new UI.Spell_Dialog;
        
this._dialog.init({ base_uri self._loki.settings.base_uri,
                            
submit_listener self.update_body,
                            
uncorrected_html self._loki.get_html(),
                            
spell_uri 'auxil/spell_iframe.php' });
        
this._dialog.open();
    };

    
this.update_body = function(spell_info)
    {
        
self._loki.set_html(spell_info.corrected_html);

        var 
sel Util.Selection.get_selection(self._loki.window);
        
Util.Selection.move_cursor_to_end(selself._loki.body);
        
self._loki.window.focus();
    };
};

// file UI.Spell_Keybinding.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents keybinding.
 */
UI.Spell_Keybinding = function()
{
    
Util.OOP.inherits(thisUI.Keybinding);
    
this.test = function(e) { return e.keyCode == 118; }; // F7
    
this.action = function() { this._spell_helper.open_dialog(); };

    
this.init = function(loki)
    {
        
this.superclass.init.call(thisloki);
        
this._spell_helper = (new UI.Spell_Helper).init(this._loki);
        return 
this;
    };
};

// file UI.Tab_Helper.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class for helping insert an anchor. Contains code
 * common to both the button and the menu item.
 */
UI.Tab_Helper = function()
{
    var 
self this;
    
Util.OOP.inherits(selfUI.Helper);

    
this.is_no_default = function()
    {
        
// not in table
        
var sel Util.Selection.get_selection(self._loki.window);
        var 
rng Util.Range.create_range(sel);
        if ( 
Util.Range.get_nearest_ancestor_element_by_tag_name(rng'TABLE') != null )
            return 
false;

        
// not at beg of li
        
var li Util.Range.get_nearest_ancestor_element_by_tag_name(rng'LI');
        if ( 
li != null && Util.Range.is_at_beg_of_block(rngli) )
            return 
false;

        
// not in pre
        
if ( Util.Range.get_nearest_ancestor_element_by_tag_name(rng'PRE') != null )
            return 
false;

        return 
true;
    };

    
this.focus_next = function()
    {
        var 
form this._loki.hidden.form;
        for ( var 
0form.elements.lengthi++ )
        {
            if ( 
form.elements[i] == this._loki.hidden &&
                 
form.elements.length )
            {
                var 
next_elem form.elements[1];
                
next_elem.focus();
            }
        }
    };
};

// file UI.Tab_Keybinding.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents keybinding.
 */
UI.Tab_Keybinding = function()
{
    
Util.OOP.inherits(thisUI.Keybinding);

    
this.test = function(e) { return e.keyCode == && !e.shiftKey && 
                              !
document.all &&  // XXX: bad
                              
this._tab_helper.is_no_default(); }; // Tab
    
this.action = function() { this._tab_helper.focus_next(); };

    
this.init = function(loki)
    {
        
this.superclass.init.call(thisloki);
        
this._tab_helper = (new UI.Tab_Helper).init(this._loki);
        return 
this;
    };
};

// file UI.Table_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class for inserting an table.
 */
UI.Table_Button = function()
{
    var 
self this;
    
Util.OOP.inherits(selfUI.Button);

    
this.image 'table.png';
    
this.title 'Insert table';
    
this.click_listener = function() { self._table_helper.open_table_dialog(); };

    
this.init = function(loki)
    {
        
this.superclass.init.call(thisloki);
        
this._table_helper = (new UI.Table_Helper).init(this._loki);
        return 
this;
    };
};

// file UI.Table_Dialog.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A table dialog window..
 */
UI.Table_Dialog = function()
{
    
Util.OOP.inherits(thisUI.Dialog);

    
this._dialog_window_width 615;
    
this._dialog_window_width 585;

    
this._bgs = ['bgFFFFCC''bgFFFF99''bg99CCFF''bgCCCCCC''bgE8E8E8'];
    
this._bg_radios = new Array();
    
//this._desc_blank = '(Write your summary here.)';
    
this._desc_blank '';

    
this._set_title = function()
    {
        if ( 
this._initially_selected_item.is_new )
            
this._dialog_window.document.title 'Make a table';
        else
            
this._dialog_window.document.title 'Table properties';
    };

    
this._append_style_sheets = function()
    {
        
this.superclass._append_style_sheets.call(this);
        
//Util.Document.append_style_sheet(this._dialog_window.document, this._base_uri + 'css/cssSelector.css');
        
Util.Document.append_style_sheet(this._dialog_window.documentthis._base_uri 'css/Table_Dialog.css');
    };

    
this._populate_main = function()
    {
        
this._append_table_properties();
        
this._append_table_color_properties();
        
this._append_remove_table_button();
        
this.superclass._populate_main.call(this);
    };

    
/**
     * Appends a chunk containing table properties.
     */
    
this._append_table_properties = function()
    {
        
// Create function to check for digit
        
var self this;
        var 
is_digit = function(event
        {
            
// Gecko uses keyCode for alphanumeric codes, charCode for special codes.
            // IE uses charCode for alphanumeric codes, and doesn`t use keyCode at all.
            
event event == null self._dialog_window.window.event event;
            var 
char_code event.charCode == null event.keyCode event.charCode;
            
// In Gecko, char_code (== event.keyCode) will be 0 if a special key has been pressed.
            
return char_code == || ( char_code >= 48 && char_code <=57 ); // is digit
        
};

        
// Create generic label element
        
var generic_label this._dialog_window.document.createElement('LABEL');
        
Util.Element.add_class(generic_label'label');

        
// Create rows input
        
this._rows_input this._dialog_window.document.createElement('INPUT');
        
this._rows_input.size 3;
        
this._rows_input.maxlength 2;
        
this._rows_input.id 'rows_input';
        
this._rows_input.onkeypress is_digit;
        
this._rows_input.value this._initially_selected_item.rows == null this._initially_selected_item.rows;

        var 
self this;
        if ( 
this._initially_selected_item.is_new == false )
        {
            
Util.Event.add_event_listener(this._rows_input'change', function()
            {
                if ( 
self._rows_input.value self._initially_selected_item.rows )
                {
                    
self._dialog_window.window.alert('Sorry, you cannot decrease the number of rows here--otherwise, you might accidentally delete data. nnIf you really want to remove a row, right click in it and select "Delete row".');
                    
self._rows_input.value self._initially_selected_item.rows;
                    
self._rows_input.focus();
                }
            });
        }
        else 
        {
            
Util.Event.add_event_listener(this._rows_input'change', function()
            {
                if ( 
self._rows_input.value )
                {
                    
self._dialog_window.window.alert('Sorry, at least two rows are required.');
                    
self._rows_input.value 2;
                    
self._rows_input.focus();
                }
            });
        }

        
// Create rows label
        
var rows_label generic_label.cloneNode(false);
        
rows_label.appendChildthis._dialog_window.document.createTextNode('Rows: ') );
        
rows_label.htmlFor 'rows_input';

        
// Create cols input
        
this._cols_input this._rows_input.cloneNode(false);
        
this._cols_input.id 'cols_input';
        
this._cols_input.onkeypress is_digit;
        
this._cols_input.value this._initially_selected_item.cols == null this._initially_selected_item.cols;

        var 
self this;
        if ( 
this._initially_selected_item.is_new == false )
        {
            
Util.Event.add_event_listener(this._cols_input'change', function()
            {
                if ( 
self._cols_input.value self._initially_selected_item.cols )
                {
                    
self._dialog_window.window.alert('Sorry, you cannot decrease the number of columns here--otherwise, you might accidentally delete data. nnIf you really want to remove a column, right click in it and select "Delete column".');
                    
self._cols_input.value self._initially_selected_item.cols;
                    
self._cols_input.focus();
                }
            });
        }
        else
        {
            
Util.Event.add_event_listener(this._cols_input'change', function()
            {
                if ( 
self._cols_input.value )
                {
                    
self._dialog_window.window.alert('Sorry, at least two columns are required.');
                    
self._cols_input.value 2;
                    
self._cols_input.focus();
                }
            });
        }

        
// Create cols label
        
var cols_label generic_label.cloneNode(false);
        
cols_label.appendChildthis._dialog_window.document.createTextNode('  Columns: ') );
        
cols_label.htmlFor 'cols_input';

        
// Create rows and cols div
        
var rows_and_cols_div this._dialog_window.document.createElement('DIV');
        
Util.Element.add_class(rows_and_cols_div'field');
        
rows_and_cols_div.appendChild(rows_label);
        
rows_and_cols_div.appendChild(self._rows_input);
        
rows_and_cols_div.appendChild(cols_label);
        
rows_and_cols_div.appendChild(self._cols_input);

        
// Create border input
        
this._border_checkbox this._dialog_window.document.createElement('INPUT');
        
this._border_checkbox.type 'checkbox';
        
this._border_checkbox.id 'border_checkbox';
        
this._border_checkbox.checked this._initially_selected_item.border == null false this._initially_selected_item.border;

        
// Create border label
        
var border_label generic_label.cloneNode(false);
        
border_label.appendChildthis._dialog_window.document.createTextNode('Show border:') );
        
border_label.htmlFor 'border_checkbox';

        
// Create border div
        
var border_div this._dialog_window.document.createElement('DIV');
        
Util.Element.add_class(border_div'field');
        
border_div.appendChild(border_label);
        
border_div.appendChild(self._border_checkbox);

        
// Create description textarea
        
this._desc_textarea this._dialog_window.document.createElement('TEXTAREA');
        
this._desc_textarea.cols 25;
        
this._desc_textarea.rows '5';
        
this._desc_textarea.id 'desc_textarea';
        
this._desc_textarea.value this._initially_selected_item.desc == null self._desc_blank this._initially_selected_item.desc;
        
/* // This would toggle desc_blank onfocus/blur
        var self = this;
        Util.Event.add_event_listener(this._desc_textarea, 'focus', function()
        {
            if ( self._desc_textarea.value == self._desc_blank )
                self._desc_textarea.value = '';
        });
        Util.Event.add_event_listener(this._desc_textarea, 'blur', function()
        {
            if ( self._desc_textarea.value == '' )
                self._desc_textarea.value = self._desc_blank;
        });
        */

        // Create description label
        
var desc_label generic_label.cloneNode(false);
        
desc_label.appendChildthis._dialog_window.document.createTextNode('Summarize the contents of this table:') );
        
desc_label.htmlFor 'desc_textarea';

        
// Create description div
        
var desc_div this._dialog_window.document.createElement('DIV');
        
Util.Element.add_class(desc_div'field');
        
desc_div.appendChild(desc_label);
        
desc_div.appendChild(this._dialog_window.document.createElement('BR'));
        
desc_div.appendChild(self._desc_textarea);

        
// Create heading
        
var h1 this._dialog_window.document.createElement('H1');
        if ( 
this._initially_selected_item.is_new )
            
h1.innerHTML 'Make a table';
        else
            
h1.innerHTML 'Table properties';

        
// Create fieldset and its legend
        
var fieldset = new Util.Fieldset({legend ''document this._dialog_window.document});

        
// Append all the above to fieldset
        
fieldset.fieldset_elem.appendChild(rows_and_cols_div);
        
fieldset.fieldset_elem.appendChild(desc_div);
        
fieldset.fieldset_elem.appendChild(border_div);

        
// Append fieldset chunk to dialog
        
this._main_chunk.appendChild(h1);
        
this._main_chunk.appendChild(fieldset.chunk);
    };

    
/**
     * Appends a chunk containing table color properties.
     */
    
this._append_table_color_properties = function()
    {
        
//
        // We only show the bg section if the table being
        // edited already has a bg, and the user might want
        // to get rid of it. (for legacy)
        //
        
if ( this._initially_selected_item.bg )
        {
            
// Create generic elements
            
var generic_bg_label this._dialog_window.document.createElement('LABEL');
            
Util.Element.add_class(generic_bg_label'bg_label');
            
//generic_bg_label.appendChild( this._dialog_window.document.createTextNode(' ') );
            
generic_bg_label.innerHTML '&nbsp;';

            var 
generic_bg_radio Util.Input.create_named_input({document this._dialog_window.documentname 'bg_radio'});
            
generic_bg_radio.type 'radio';

            
// Create fieldset and its legend
            
var fieldset = new Util.Fieldset({legend 'Table color properties:'document this._dialog_window.document});

            
// Create the "remove bgcolor" radio and label
            
this._no_bg_radio generic_bg_radio.cloneNode(true);
            
this._no_bg_radio.id 'no_bg_radio';

            var 
no_bg_label this._dialog_window.document.createElement('LABEL');
            
no_bg_label.appendChildthis._dialog_window.document.createTextNode('Remove background color') );
            
no_bg_label.htmlFor 'no_bg_radio';
            
Util.Element.add_class(no_bg_label'label');

            
// Create the "keep bgcolor" radio and label
            
this._keep_bg_radio generic_bg_radio.cloneNode(true);
            
this._keep_bg_radio.id 'keep_bg_radio';
            
this._keep_bg_radio.checked true// otherwise we wouldn't be showing any of this at all

            
var keep_bg_label this._dialog_window.document.createElement('LABEL');
            
keep_bg_label.appendChildthis._dialog_window.document.createTextNode('Keep background color') );
            
keep_bg_label.htmlFor 'keep_bg_radio';
            
Util.Element.add_class(keep_bg_label'label');

            
// Append them
            
fieldset.fieldset_elem.appendChild(this._no_bg_radio);
            
fieldset.fieldset_elem.appendChild(no_bg_label);
            
fieldset.fieldset_elem.appendChild(this._keep_bg_radio);
            
fieldset.fieldset_elem.appendChild(keep_bg_label);

            
// Append fieldset chunk to dialog
            
this._main_chunk.appendChild(fieldset.chunk);
        }
        
        
/* Uncomment if bgs are reinstated
        // Create generic elements
        var generic_bg_label = this._dialog_window.document.createElement('LABEL');
        Util.Element.add_class(generic_bg_label, 'bg_label');
        //generic_bg_label.appendChild( this._dialog_window.document.createTextNode(' ') );
        generic_bg_label.innerHTML = '&nbsp;';

        var generic_bg_radio = Util.Input.create_named_input({document : this._dialog_window.document, name : 'bg_radio'});
        generic_bg_radio.type = 'radio';

        // Create fieldset and its legend
        var fieldset = new Util.Fieldset({legend : 'Table color properties:', document : this._dialog_window.document});

        // Create and append the "no bgcolor" radio and label
        this._no_bg_radio = generic_bg_radio.cloneNode(true);
        this._no_bg_radio.id = 'no_bg_radio';

        var no_bg_label = this._dialog_window.document.createElement('LABEL');
        no_bg_label.appendChild( this._dialog_window.document.createTextNode('Use no background color') );
        no_bg_label.htmlFor = 'no_bg_radio';
        Util.Element.add_class(no_bg_label, 'label');

        fieldset.fieldset_elem.appendChild(this._no_bg_radio);
        fieldset.fieldset_elem.appendChild(no_bg_label);

        // Create and append the bgcolor radios and labels
        var bg_labels = new Array();
        for ( var i = 0; i < this._bgs.length; i++ )
        {
            bg_labels[i] = generic_bg_label.cloneNode(true);
            bg_labels[i].htmlFor = 'bg_' + this._bgs[i] + '_radio';
            Util.Element.add_class(bg_labels[i], this._bgs[i]);

            this._bg_radios[i] = generic_bg_radio.cloneNode(true);
            this._bg_radios[i].id = 'bg_' + this._bgs[i] + '_radio';

            fieldset.fieldset_elem.appendChild(this._bg_radios[i]);
            fieldset.fieldset_elem.appendChild(bg_labels[i]);
        }

        // Append fieldset chunk to dialog
        this._main_chunk.appendChild(fieldset.chunk);
        */
    
};

    
/**
     * Creates and appends a chunk containing a "remove table" button. 
     * Also attaches 'click' event listeners to the button.
     */
    
this._append_remove_table_button = function()
    {
        var 
button this._dialog_window.document.createElement('BUTTON');
        
button.setAttribute('type''button');
        
button.appendChildthis._dialog_window.document.createTextNode('Remove table') );

        var 
self this;
        var 
listener = function()
        {
            if ( 
confirm('Really remove table? WARNING: This cannot be undone.') )
            {
                
self._remove_listener();
                
self._dialog_window.window.close();
            }
        }
        
Util.Event.add_event_listener(button'click'listener);

        
// Setup their containing chunk
        
var chunk this._dialog_window.document.createElement('DIV');
        
Util.Element.add_class(chunk'remove_chunk');
        
chunk.appendChild(button);

        
// Append the containing chunk
        
this._dialog_window.body.appendChild(chunk);
    };

    
this._apply_initially_selected_item = function()
    {
        
// Apply background
        // (we have to set checked after all the radios are added;
        // otherwise, IE will uncheck what we check.)
        /* Uncomment if bgs are reinstated
        this._no_bg_radio.checked = true;
        for ( var i = 0; i < this._bgs.length; i++ )
        {
            if ( this._bgs[i] == this._initially_selected_item.bg )
            {
                this._bg_radios[i].checked = true;
            }
        }
        */
    
};

    
this._internal_submit_listener = function()
    {
        
// Determine rows
        
if ( this._rows_input.value == '' )
        {
            
this._dialog_window.window.alert('Please specify a number of rows.');
            
this._rows_input.focus();
            return 
false;
        }
        var 
rows this._rows_input.value;

        
// Determine cols
        
if ( this._cols_input.value == '' )
        {
            
this._dialog_window.window.alert('Please specify a number of columns.');
            
this._cols_input.focus();
            return 
false;
        }
        var 
cols this._cols_input.value;
            
        
// Determine border
        
var border this._border_checkbox.checked true false;
        
        
// Determine description
        
if ( this._desc_textarea.value == this._desc_blank || this._desc_textarea.value == '' )
        {
            
this._dialog_window.window.alert('Please provide a brief summary of the data in the table.');
            
this._desc_textarea.focus();
            return 
false;
        }
        var 
desc this._desc_textarea.value;
        
        
// Determine whether the user wants to keep 
        // the background (for legacy)
        
var bg false;
        if ( 
this._keep_bg_radio != null )
            
bg this._keep_bg_radio.checked;
        
/* Uncomment if bgs are reinstated
        // Determine background
        var bg = '';
        for ( var i = 0; i < this._bgs.length; i++ )
        {
            if ( this._bg_radios[i].checked == true )
            {
                bg = this._bgs[i];
            }
        }
        */

        // Call external event listener
        
this._external_submit_listener({rows rowscols colsborder borderdesc descbg bg});

        
// Close dialog window
        
this._dialog_window.window.close();
    };
};

// file UI.Table_Helper.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class for helping insert an table. Contains code
 * common to both the button and the menu item. 
 * 
 * Note: keep in mind that table.createTHead() creates _or gets_ 
 * the table's THEAD elem.
 */
UI.Table_Helper = function()
{
    var 
self this;
    
Util.OOP.inherits(selfUI.Helper);

    
this.init = function(loki)
    {
        
this._loki loki;
        
this._table_masseuse = (new UI.Table_Masseuse()).init(self._loki);
        return 
this;
    };

    
this.is_table_selected = function()
    {
        var 
sel Util.Selection.get_selection(self._loki.window);
        var 
rng Util.Range.create_range(sel);
        return 
Util.Range.get_nearest_ancestor_element_by_tag_name(rng'TABLE') != null;
    };

    var 
_cell_boolean_test = function(node)
    {
        return ( 
node.nodeType == Util.Node.ELEMENT_NODE &&
                 ( 
node.tagName == 'TD' || node.tagName == 'TH' ) );
    };

    
this.is_cell_selected = function()
    {
        var 
sel Util.Selection.get_selection(self._loki.window);
        var 
rng Util.Range.create_range(sel);
        return 
Util.Range.get_nearest_ancestor_node(rng_cell_boolean_test) != null;
    };

    
/**
     * use is_cell_selected unless you want TD specifically 
     */
    
this.is_td_selected = function()
    {
        var 
sel Util.Selection.get_selection(self._loki.window);
        var 
rng Util.Range.create_range(sel);
        return 
Util.Range.get_nearest_ancestor_element_by_tag_name(rng'TD') != null;
    };

    
/**
     * use is_cell_selected unless you want TH specifically 
     */
    
this.is_th_selected = function()
    {
        var 
sel Util.Selection.get_selection(self._loki.window);
        var 
rng Util.Range.create_range(sel);
        return 
Util.Range.get_nearest_ancestor_element_by_tag_name(rng'TH') != null;
    };

    
this.get_selected_table_item = function()
    {
        var 
sel Util.Selection.get_selection(self._loki.window);
        var 
rng Util.Range.create_range(sel);

        var 
selected_item;
        var 
selected_table Util.Range.get_nearest_ancestor_element_by_tag_name(rng'TABLE');
        if ( 
selected_table != null )
        {
            var 
selected_tbody selected_table.getElementsByTagName('TBODY')[0];

            
/* Uncomment if bgs are reinstated
            var bg, classes = Util.Element.get_all_classes(selected_table).split(' ');
            for ( var i = 0; i < classes.length; i++ )
                if ( classes[i].indexOf('bg') === 0 )
                    bg = classes[i];
            */
            // Check whether any bg is present at all (for legacy)
            
var classes_str Util.Element.get_all_classes(selected_table) + ' ' +
                              
Util.Element.get_all_classes(selected_table.rows[0].cells[0]);
            var 
classes classes_str.split(' ');
            var 
bg;
            for ( var 
0classes.lengthi++ )
                if ( 
classes[i].indexOf('bg') === )
                    
bg true;

            
selected_item = { rows selected_tbody.rows.length,
                              
cols selected_tbody.rows[0].cells.length,
                              
border selected_table.getAttribute('border') > 0,
                              
desc selected_table.getAttribute('summary'), 
                              
bg bg,
                              
is_new false };
        }
        else
        {
            
selected_item = { rows 2
                              
cols 3
                              
border false
                              
desc null,
                              
bg false,
                              
is_new true };
        }

        return 
selected_item;
    };

    
this.get_thead_rows = function()
    {
        var 
sel Util.Selection.get_selection(self._loki.window);
        var 
rng Util.Range.create_range(sel);
        var 
selected_table Util.Range.get_nearest_ancestor_element_by_tag_name(rng'TABLE');
        return 
selected_table.createTHead().rows;
    };

    
this.get_selected_cell_item = function(tagname)
    {
        var 
sel Util.Selection.get_selection(self._loki.window);
        var 
rng Util.Range.create_range(sel);

        var 
selected_item;
        var 
selected_cell Util.Range.get_nearest_ancestor_node(rng_cell_boolean_test);
        if ( 
selected_cell != null )
            
selected_item = { align selected_cell.getAttribute('align'),
                              
valign selected_cell.getAttribute('valign'),
                              
wrap selected_cell.getAttribute('noWrap') == null || selected_cell.getAttribute('noWrap') == '' 'yes' 'no' };
            
//selected_item = { colspan : selected_cell.getAttribute('colspan'),
            //                  rowspan : selected_cell.getAttribute('rowspan') };
        
else
            
selected_item = { align 'left'
                              
valign 'top',
                              
wrap 'yes' };

        return 
selected_item;
    };

    
this.open_table_dialog = function()
    {
        var 
selected_item self.get_selected_table_item();

        if ( 
this._table_dialog == null )
            
this._table_dialog = new UI.Table_Dialog;
        
this._table_dialog.init({ base_uri self._loki.settings.base_uri,
                            
submit_listener self.insert_table,
                            
remove_listener self.remove_table,
                            
selected_item selected_item });
        
this._table_dialog.open();
    };

    
this.open_cell_dialog = function()
    {
        var 
selected_item self.get_selected_cell_item();

        if ( 
this._cell_dialog == null )
            
this._cell_dialog = new UI.Cell_Dialog;
        
this._cell_dialog.init({ base_uri self._loki.settings.base_uri,
                         
submit_listener self.update_cell,
                         
selected_item selected_item });
        
this._cell_dialog.open();
    };

    
/**
     * Adds a tr to the given tbody after the given row index.
     * Index of -1 to insert at end.
     * tbody doesn't actually have to be a tbody--it can be a thead (or table), too.
     * Returns the tr.
     */
    
var _insert_tr = function(tbodyindex)
    {
        return 
tbody.insertRow(index);
    };

    
/**
     * Adds a td to the given tr after the given cell index.
     * Index of -1 to insert at end.
     * Returns the td.
     */
    
var _insert_td = function(trindex)
    {
        var 
td tr.ownerDocument.createElement('TD');
        
td.setAttribute('align''left');
        
td.setAttribute('valign''top');
        if ( 
index == -|| index >= tr.childNodes.length )
            
tr.appendChild(td);
        else
            
tr.insertBefore(tdtr.childNodes[index]);
        return 
td;
    };

    
/**
     * Adds a th to the given tr after the given cell index.
     * Index of -1 to insert at end.
     * Returns the td.
     */
    
var _insert_th = function(trindex)
    {
        var 
td tr.ownerDocument.createElement('TH');
        
//td.setAttribute('align', 'left');
        
td.setAttribute('valign''top');
        if ( 
index == -|| index >= tr.childNodes.length )
            
tr.appendChild(td);
        else
            
tr.insertBefore(tdtr.childNodes[index]);
        return 
td;
    };

    
this.insert_table = function(table_info)
    {
        if ( 
self.is_table_selected() )
        {
            var 
sel Util.Selection.get_selection(self._loki.window);
            var 
rng Util.Range.create_range(sel);
            var 
table Util.Range.get_nearest_ancestor_element_by_tag_name(rng'TABLE');
            var 
tbody table.getElementsByTagName('TBODY')[0];

            
table.setAttribute('border'table_info.border '1' '0');
            
table.setAttribute('summary'table_info.desc);

            
/* Uncomment if bgs are reinstated
            var classes = Util.Element.get_all_classes(table).split(' ');
            for ( var i = 0; i < classes.length; i++ )
                if ( classes[i].indexOf('bg') === 0 )
                    Util.Element.remove_class(table, classes[i]);
            Util.Element.add_class(table, table_info.bg);
            */
            // Remove bg color if asked (for legacy)
            
if ( table_info.bg == false )
            {
                var 
classes_str Util.Element.get_all_classes(table);
                if ( 
classes_str != null )
                {
                    var 
classes classes_str.split(' ');
                    for ( var 
0classes.lengthi++ )
                        if ( 
classes[i].indexOf('bg') === )
                            
Util.Element.remove_class(tableclasses[i]);
                }
            }

            
// Update rows and cols
            
var old_info self.get_selected_table_item();
            for ( var 
old_info.rowstable_info.rowsi++ )
                
_actually_insert_row(tbodyi);
            for ( var 
old_info.colstable_info.colsi++ )
                
_actually_insert_column(tablei);
        }
        else
        {
            
// Create the table
            
var table self._loki.document.createElement('TABLE');
            
table.setAttribute('cellpadding''5');
            
table.setAttribute('cellspacing''0');
            
table.setAttribute('border'table_info.border '1' '0');
            
table.setAttribute('summary'table_info.desc);
            
/* Uncomment if bgs are reinstated
            Util.Element.add_class(table, table_info.bg);
            */

            // ... and tbody and thead
            
var tbody self._loki.document.createElement('TBODY');
            
table.appendChild(tbody);
            var 
thead table.createTHead();

            
// Populate the table ... with a row of ths ...
            
var tr _insert_tr(thead, -1);
            for ( var 
0table_info.colsj++ )
            {
                
_insert_th(tr, -1);
            }
            
// ... and rows of tds
            
for ( var 0table_info.rowsi++ )
            {
                var 
tr _insert_tr(tbody, -1);
                for ( var 
0table_info.colsj++ )
                {
                    
_insert_td(tr, -1);
                }
            }

            
// Insert the table
            
var sel Util.Selection.get_selection(self._loki.window);
            
Util.Selection.paste_node(seltable);
            
self._loki.window.focus();
        }

        
self._table_masseuse.massage_elem(table);
    };

    
this.update_cell = function(cell_info)
    {
        if ( 
self.is_cell_selected() )
        {
            var 
sel Util.Selection.get_selection(self._loki.window);
            var 
rng Util.Range.create_range(sel);
            var 
table Util.Range.get_nearest_ancestor_element_by_tag_name(rng'TABLE');
            var 
cell Util.Range.get_nearest_ancestor_node(rng_cell_boolean_test);

            
cell.setAttribute('align'cell_info.align);
            
cell.setAttribute('valign'cell_info.valign);

            if ( 
cell_info.wrap == 'yes' )
            {
                
//if ( cell.getAttribute('noWrap') != '' )
                    
cell.removeAttribute('noWrap');
            }
            else
                
cell.setAttribute('noWrap''noWrap');

            
/* NB: this was commented before bgs were gotten rid of.
               If bgs are reinstated, keep this commented.
            var classes = (Util.Element.get_all_classes(cell) == null ? '' : Util.Element.get_all_classes(cell)).split(' ');
            for ( var i = 0; i < classes.length; i++ )
                if ( classes[i].indexOf('bg') === 0 )
                    Util.Element.remove_class(cell, classes[i]);
            Util.Element.add_class(cell, cell_info.bg);
            */
        
}
        
self._table_masseuse.massage_elem(table);
    };

    function 
_get_column_index(trtd)
    {
        var 
col_index;
        for ( var 
0tr.cells.lengthi++ )
        {
            if ( 
tr.cells[i] == td )
                
col_index i;
        }
        return 
col_index;
    }

    var 
_actually_insert_column = function(tablecol_index)
    {
        var 
thead table.createTHead();
        var 
tbody table.getElementsByTagName('TBODY')[0];
        
        for ( var 
0thead.rows.lengthi++ )
        {
            var 
index thead.rows[i].cells[col_index 1] != null col_index : -1;
            var 
new_th _insert_th(thead.rows[i], col_index);
        }
        for ( var 
0tbody.rows.lengthi++ )
        {
            var 
index tbody.rows[i].cells[col_index 1] != null col_index : -1;
            var 
new_td _insert_td(tbody.rows[i], col_index);
        }
    };

    
this.insert_column = function()
    {
        if ( 
self.is_cell_selected() )
        {
            var 
sel Util.Selection.get_selection(self._loki.window);
            var 
rng Util.Range.create_range(sel);
            var 
table Util.Range.get_nearest_ancestor_element_by_tag_name(rng'TABLE');
            var 
tr Util.Range.get_nearest_ancestor_element_by_tag_name(rng'TR');
            var 
cell Util.Range.get_nearest_ancestor_node(rng_cell_boolean_test);
            var 
col_index _get_column_index(trcell) + 1;
    
            
_actually_insert_column(tablecol_index);
        }
        
self._table_masseuse.massage_elem(table);
    };

    
this.delete_column = function()
    {
        if ( 
self.is_cell_selected() )
        {
            var 
sel Util.Selection.get_selection(self._loki.window);
            var 
rng Util.Range.create_range(sel);
            var 
table Util.Range.get_nearest_ancestor_element_by_tag_name(rng'TABLE');
            var 
tr Util.Range.get_nearest_ancestor_element_by_tag_name(rng'TR');
            var 
cell Util.Range.get_nearest_ancestor_node(rng_cell_boolean_test);
            var 
col_index _get_column_index(trcell);

            for ( var 
0table.rows.lengthi++ )
            {
                
// this needed to manage colspans across multiple columns
                
var cur_row table.rows[i];
                var 
cur_cell cur_row.cells[col_index];
                if ( 
cur_cell.colSpan != )
                {
                    
colspan cur_cell.getAttribute("colspan");
                    var 
new_cell cur_row.insertCell(iCol+1);
                    
new_cell.colSpan colspan 1;
                    
new_cell.innerHTML cur_cell.innerHTML// XXX: should clone children instead
                
}
        
                try { 
table.rows[i].deleteCell(col_index); } catch(e) {}        
            }
        }
        
self._table_masseuse.massage_elem(table);
    };

    
this.merge_columns = function()
    {
        if ( 
self.is_cell_selected() )
        {
            var 
sel Util.Selection.get_selection(self._loki.window);
            var 
rng Util.Range.create_range(sel);
            var 
table Util.Range.get_nearest_ancestor_element_by_tag_name(rng'TABLE');
            var 
tr Util.Range.get_nearest_ancestor_element_by_tag_name(rng'TR');
            var 
cell Util.Range.get_nearest_ancestor_node(rng_cell_boolean_test);
            var 
next_cell cell.nextSibling;

            if ( 
next_cell != null )
            {
                var 
colspan1 cell.getAttribute("colspan");
                var 
colspan2 next_cell.getAttribute("colspan");
        
                
cell.colspan colspan1 colspan2;
                
cell.innerHTML += next_cell.innerHTML;
                
table.rows[tr.rowIndex].deleteCell(next_cell.cellIndex);
            }
        }
        
self._table_masseuse.massage_elem(table);
    };

    function 
_get_num_of_columns(tbody)
    {
        var 
n_colspan 0;
        var 
n_cols tbody.rows[0].cells.length;
        for (var 
0n_colsi++ )
        {
            
n_colspan += tbody.rows[0].cells[i].colSpan;
        }
        return 
n_colspan;
    }

    var 
_actually_insert_row = function(tbodyrow_index)
    {
        var 
num_of_cols _get_num_of_columns(tbody);
        var 
new_tr _insert_tr(tbodyrow_index);
        for ( var 
0num_of_colsi++ )
        {
            
_insert_td(new_tri);
        }
    };

    
this.insert_row = function()
    {
        if ( 
self.is_cell_selected() )
        {
            var 
sel Util.Selection.get_selection(self._loki.window);
            var 
rng Util.Range.create_range(sel);
            var 
table Util.Range.get_nearest_ancestor_element_by_tag_name(rng'TABLE');
            var 
tbody table.getElementsByTagName('TBODY')[0];
            var 
thead table.createTHead();
            var 
tr Util.Range.get_nearest_ancestor_element_by_tag_name(rng'TR');
            var 
row_index tr.rowIndex thead.rows.length 1;

            
_actually_insert_row(tbodyrow_index);
        }
        
self._table_masseuse.massage_elem(table);
    };

    
this.delete_row = function()
    {
        if ( 
self.is_cell_selected() )
        {
            var 
sel Util.Selection.get_selection(self._loki.window);
            var 
rng Util.Range.create_range(sel);
            var 
table Util.Range.get_nearest_ancestor_element_by_tag_name(rng'TABLE');
            var 
tr Util.Range.get_nearest_ancestor_element_by_tag_name(rng'TR');
            
table.deleteRow(tr.rowIndex);
        }
        
self._table_masseuse.massage_elem(table);
    };

    
this.convert_row_to_header = function()
    {
        if ( 
self.is_cell_selected() )
        {
            var 
sel Util.Selection.get_selection(self._loki.window);
            var 
rng Util.Range.create_range(sel);
            var 
table Util.Range.get_nearest_ancestor_element_by_tag_name(rng'TABLE');
            var 
tr Util.Range.get_nearest_ancestor_element_by_tag_name(rng'TR');
            var 
thead table.createTHead();
            var 
thead_tr table.ownerDocument.createElement('TR');
            
thead.appendChild(thead_tr);
            for ( var 
0tr.cells.lengthi++ )
            {
                var 
td tr.cells[i];
                var 
th table.ownerDocument.createElement('TH');
                while ( 
td.firstChild != null )
                    
th.appendChildtd.removeChild(td.firstChild ) );
                
thead_tr.appendChild(th);
            }
            
table.deleteRow(tr.rowIndex);
        }
        
self._table_masseuse.massage_elem(table);
    };

    
this.remove_table = function()
    {
        var 
sel Util.Selection.get_selection(self._loki.window);
        var 
rng Util.Range.create_range(sel);
        var 
table Util.Range.get_nearest_ancestor_element_by_tag_name(rng'TABLE');

        
// Move cursor
        
Util.Selection.select_node(seltable);
        
Util.Selection.collapse(selfalse); // to end
        
self._loki.window.focus();

        if ( 
table.parentNode != null )
            
table.parentNode.removeChild(table);
    };
};

/*

Public methods:
--------------
insert_table
insert_row
insert_column
convert_row_to_header
delete_row
delete_column
update_table_attrs
update_td_attrs

The general approach:
--------------------
Make the real element, then masseuse.get_fake_elem, then append that.

*/

// file UI.Table_Masseuse.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class for massaging a table.
 */
UI.Table_Masseuse = function TableMasseuse()
{
    
Util.OOP.inherits(thisUI.Masseuse);
    
    var 
empty_header_text 'Column title';
    
    
/*
     * Ensures that the given table follows the thead/tbody/tfoot structure.
     */
    
function normalize_table_structure(tablefirst_row_is_head)
    {        
        function 
get_bodies() {
            var 
bodies;
            var 
c;
            
            
// the tBodies property might be broken under IE8
            
if (table.tBodies)
                return 
table.tBodies;
            
            
bodies = [];
            for (var 
0table.childNodes.lengthi++) {
                
table.childNodes[i];
                
                if (
Util.Node.is_tag(c'TBODY'))
                    
bodies.push(c);
            }
            
            return 
bodies;
        }
        
        function 
get_first_row()
        {
            var 
source get_bodies()[0] || table;
            
            for (var 
source.firstChildcc.nextSibling) {
                if (
Util.Node.is_tag(c'TR'))
                    return 
c;
            }
            
            return 
null;
        }
        
        function 
promote_row(rowwhere)
        {
            var 
method = ('createT' where.charAt(0).toUpperCase() 
                + 
where.substr(1));
            var 
dest table[method]();
            
            if (!
row)
                return 
false;
            
            
dest.insertBefore(rowdest.firstChild);
            return 
true;
        }
        
        function 
is_header_row(row) {
            var 
maybe false;
            
            for (var 
row.firstChildcc.nextSibling) {
                if (
Util.Node.is_tag(c'TD'))
                    return 
false;
                if (!
maybe && Util.Node.is_tag(c'TH'))
                    
maybe true;
            }
            
            return 
maybe;
        }
        
        function 
count_columns(row) {
            var 
count 0;
            
            for (var 
row.firstChildcc.nextSibling) {
                if (
Util.Node.is_tag(c'TD') || Util.Node.is_tag(c'TH'))
                    
count++;
            }
            
            return 
count;
        }
        
        function 
create_header_row(cells) {
            var 
row table.ownerDocument.createElement('TR');
            
            for (var 
0cellsi++) {
                
row.appendChild(row.ownerDocument.createElement('TH'));
            }
            
            return 
row;
        }
        
        function 
fill_in_empty_cells(row)
        {
            var 
empty_pat = /^(s|&nbsp;|<br[^>]*>)+$/i;
            
            for (var 
row.firstChildcc.nextSibling) {
                if (!
Util.Node.is_tag(c'TD') && !Util.Node.is_tag(c'TH'))
                    continue;
                
                if (!
c.hasChildNodes() || empty_pat.test(c.innerHTML))
                    
c.innerHTML empty_header_text;
            }
        }
        
        if (!
Util.Node.is_tag(table'TABLE')) {
            throw new 
TypeError("Cannot normalize the table structure of a " +
                
"non-table.");
        }
        
        if (
first_row_is_head) {
            
promote_row(get_first_row(), 'head');
        }
        
        var 
head table.createTHead();
        var 
head_valid true;
        if (
head.getElementsByTagName("TR").length == 0) {
            
// See if the first row of the table is actually a header row.
            
var candidate get_first_row();
            if (
candidate && is_header_row(candidate)) {
                
promote_row(candidate'head');
            } else {
                
head_valid false// don't worry about the lack of header
                /*
                // Create an empty header row.
                var hr = create_header_row(count_columns(candidate));
                head.appendChild(hr);
                */
            
}
        }
        
        var 
bodies get_bodies();
        if (
bodies.length == 0) {
            var 
body table.ownerDocument.createElement('TBODY');
            
head.parentNode.insertBefore(bodyhead.nextSibling);
            
            for (var 
table.firstChildcc.nextSibling) {
                if (
Util.Node.is_tag(c'TR'))
                    
body.appendChild(c);
            }
        }
        
        if (!
head_valid) {
            
table.deleteTHead();
        } else {
            for (var 
head.firstChildcc.nextSibling) {
                if (
Util.Node.is_tag(c'TR'))
                    
fill_in_empty_cells(c);
            }
        }
        
        return 
table;
    }
    
    
this.massage_node_descendants =
        function 
massage_table_node_descendants(node)
    {
        var 
tables node.getElementsByTagName('TABLE');
        if (!
tables.length)
            return;
        
        for (var 
tables.length 1>= 0i--) {
            
massage_table(tables[i]);
        }
    }
    
    
this.unmassage_node_descendants =
        function 
unmassage_table_node_descendants(node)
    {
        var 
tables node.getElementsByTagName('TABLE');
        if (!
tables.length)
            return;
        
        for (var 
tables.length 1>= 0i--) {
            
unmassage_table(tables[i]);
        }
    }
    
    function 
massage_table(table)
    {    
        if (!
table.getAttribute('border'))
            
Util.Element.add_class(table'loki__borderless_table');
        
        
// Add trailing <br /> in Gecko, for better display and editing
        
if (Util.Browser.Gecko) {
            
// First, try innerHTML
            
var h;
            if (
table.innerHTML != null && table.innerHTML != '')
            {
                
table.innerHTML;
                
h.replace( new RegExp('(<td[ ]?[^>]*>)[ ]*(</td>)''gi'), '$1<br />$2' );
                
h.replace( new RegExp('(<th[ ]?[^>]*>)[ ]*(</th>)''gi'), '$1<br />$2' );
                
table.innerHTML h;
            }
            
// But sometimes (namely, when the table is first created in Gecko), 
            // innerHTML is mysteriously not available. In that case, we use the
            // slower DOM method, which on large tables can cause Gecko to display
            // the "Something is causing this script to run slowly; do you want to 
            // kill it?" alert:
            
for ( var 0table.rows.lengthi++ )
            {
                var 
row table.rows[i];
                for ( var 
0row.cells.lengthj++ )
                {
                    var 
cell row.cells[j];
                    if ( !( 
cell.lastChild != null &&
                            
cell.lastChild.nodeType == Util.Node.ELEMENT_NODE &&
                            
cell.lastChild.tagName == 'BR' ) )
                    {
                        
cell.appendChildcell.ownerDocument.createElement('BR') );
                    }
                }
            }
        }
        
        
normalize_table_structure(tablefalse);
    }
    
    function 
unmassage_table(table)
    {
        
Util.Element.remove_class(table'loki__borderless_table');
        
        
// Remove trailing <br /> in Gecko
        
if (Util.Browser.Gecko) {
            var 
table.innerHTML;
            
h.replace(/<brs*/?>(</t[dh]>)/gi, '$1');
            table.innerHTML = h;

            /*
            for ( var i = 0; i < table.rows.length; i++ )
            {
                var row = table.rows[i];
                for ( var j = 0; j < row.cells.length; j++ )
                {
                    var cell = row.cells[j];
                    if ( cell.lastChild != null &&
                         cell.lastChild.nodeType == Util.Node.ELEMENT_NODE &&
                         cell.lastChild.tagName == 'BR' )
                    {
                        cell.removeChild(cell.lastChild);
                    }
                }
            }
            */
        }
    }
    
    this.massage_elem = massage_table;
    this.unmassage_elem = unmassage_table;
};

// file UI.Table_Menugroup.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class representing a clipboard menugroup. 
 */
UI.Table_Menugroup = function()
{
    Util.OOP.inherits(this, UI.Menugroup);

    this.init = function(loki)
    {
        this.superclass.init.call(this, loki);
        this._table_helper = (new UI.Table_Helper).init(this._loki);
        return this;
    };

    /**
     * Returns an array of menuitems, depending on the current context.
     * May return an empty array.
     */
    this.get_contextual_menuitems = function()
    {
        var menuitems = new Array();

        if ( this._table_helper.is_table_selected() )
        {
            menuitems.push( (new UI.Menuitem).init({ 
                label : 'Table properties',
                listener : this._table_helper.open_table_dialog 
            }) );
        }

        if ( this._table_helper.is_th_selected() )
        {
            var table_item = this._table_helper.get_selected_table_item();

            menuitems.push( (new UI.Menuitem).init({ 
                label : 'Cell properties',
                listener : this._table_helper.open_cell_dialog 
            }) );

            menuitems.push( (new UI.Separator_Menuitem).init() );

            menuitems.push( (new UI.Menuitem).init({ 
                label : 'Insert column',
                listener : this._table_helper.insert_column
            }) );

            if ( table_item.cols > 2 )
            {
                menuitems.push( (new UI.Menuitem).init({ 
                    label : 'Delete column',
                    listener : this._table_helper.delete_column
                }) );

                menuitems.push( (new UI.Menuitem).init({ 
                    label : 'Merge columns',
                    listener : this._table_helper.merge_columns
                }) );
            }

            menuitems.push( (new UI.Separator_Menuitem).init() );

            if ( this._table_helper.get_thead_rows().length > 1 )
            {
                menuitems.push( (new UI.Menuitem).init({ 
                    label : 'Delete row',
                    listener : this._table_helper.delete_row 
                }) );
            }
        }

        if ( this._table_helper.is_td_selected() )
        {
            var table_item = this._table_helper.get_selected_table_item();

            menuitems.push( (new UI.Menuitem).init({ 
                label : 'Cell properties',
                listener : this._table_helper.open_cell_dialog 
            }) );

            menuitems.push( (new UI.Separator_Menuitem).init() );

            menuitems.push( (new UI.Menuitem).init({ 
                label : 'Insert column',
                listener : this._table_helper.insert_column
            }) );

            if ( table_item.cols > 2 )
            {
                menuitems.push( (new UI.Menuitem).init({ 
                    label : 'Delete column',
                    listener : this._table_helper.delete_column
                }) );

                menuitems.push( (new UI.Menuitem).init({ 
                    label : 'Merge columns',
                    listener : this._table_helper.merge_columns
                }) );
            }

            menuitems.push( (new UI.Separator_Menuitem).init() );

            menuitems.push( (new UI.Menuitem).init({ 
                label : 'Insert row',
                listener : this._table_helper.insert_row 
            }) );

            if ( table_item.rows > 2 )
            {
                menuitems.push( (new UI.Menuitem).init({ 
                    label : 'Delete row',
                    listener : this._table_helper.delete_row 
                }) );

                menuitems.push( (new UI.Menuitem).init({ 
                    label : 'Convert row to header',
                    listener : this._table_helper.convert_row_to_header
                }) );
            }
        }

        return menuitems;
    };
};

// file UI.UL_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents "ul" toolbar button.
 */
UI.UL_Button = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Button);

    this.image = 'ul.png';
    this.title = 'Unordered list';
    this.click_listener = function() { self._loki.toggle_list('ul'); };
};

// file UI.UL_OL_Masseuse.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class A class for massaging a table.
 */
UI.UL_OL_Masseuse = function()
{
    var self = this;
    var _tagnames = ['UL', 'OL'];
    var is_li = Util.Node.curry_is_tag('LI');
    Util.OOP.inherits(self, UI.Masseuse);

    this.massage_node_descendants = function(node)
    {
        _tagnames.each(function massage_list_tag_descendants(tag) {
            var lists = $A(node.getElementsByTagName(tag));
            var i, length;
            for (i = 0, length = lists.length; i < length; ++i) {
                self.massage_elem(lists[i]);
            }
        });
    };
    
    this.unmassage_node_descendants = function(node)
    {
        _tagnames.each(function unmassage_list_tag_descendants(tag) {
            var lists = $A(node.getElementsByTagName(tag));
            var i, length;
            for (i = 0, length = lists.length; i < length; ++i) {
                self.unmassage_elem(lists[i]);
            }
        });
    };

    // <ul><li>out<ul><li>in</li></ul></li><li>out again</li></ul>
    //   -->
    // <ul><li>out</li><ul><li>in</li></ul><li>out again</li></ul>
    this.massage_elem = function massage_list(list)
    {
        var parent = list.parentNode;
        var next_item;
        if (parent.nodeName == 'LI') {
            next_item = Util.Node.next_matching_sibling(parent, is_li);
            parent.parentNode.insertBefore(list, next_item);
        }
    };

    // <ul><li>out</li><ul><li>in</li></ul><li>out again</li></ul>
    //   -->
    // <ul><li>out<ul><li>in</li></ul></li><li>out again</li></ul>
    this.unmassage_elem = function unmassage_list(list)
    {
        var prev_item;
        
        if (_tagnames.contains(list.parentNode.nodeName)) {
            prev_item = Util.Node.previous_matching_sibling(list, is_li);
            
            if (!prev_item) {
                prev_item = list.ownerDocument.createElement('LI');
                list.parentNode.insertBefore(prev_item, list);
            }
            prev_item.appendChild(list);
        }
    };
};

// file UI.Underline_Button.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents "underline" toolbar button.
 */
UI.Underline_Button = function()
{
    var self = this;
    Util.OOP.inherits(self, UI.Button);

    this.image = 'underline.png';
    this.title = 'Underline (Ctrl+U)';
    this.click_listener = function() { self._loki.exec_command('Underline'); };
    this.state_querier = function() { return self._loki.query_command_state('Underline'); };
};

// file UI.Underline_Keybinding.js
/**
 * Declares instance variables.
 *
 * @constructor
 *
 * @class Represents keybinding.
 */
UI.Underline_Keybinding = function()
{
    Util.OOP.inherits(this, UI.Keybinding);
    this.test = function(e) { return this.matches_keycode(e, 85) && e.ctrlKey; }; // Ctrl-U
    this.action = function() { this._loki.exec_command('Underline'); };
};

// file UI.Loki.js
/**
 * Declares instance variables. <code>init</code> must be called to initialize them.
 * @constructor
 *
 * @class A WYSIWYG HTML editor.
 */
UI.Loki = function Loki()
{
    var _owner_window;
    var _owner_document; // that of _textarea etc.
    var _window;     //
    var _document;   // _window, _document, and _body are those of _iframe's content
    var _body;       //

    var _root;               // + root (div)
    var _toolbar;            // |--- toolbar (div)
    var _textarea_toolbar;   // |--- textarea_toolbar (div)
    var _textarea;           // |---get_nearest_bl_ancestor_element textarea
    var _statusbar;          // |--- statusbar (div)
    var _grippy_wrapper;     // |--+ grippy_wrapper (div)
    var _grippy;             //    |--- grippy (img)
    var _iframe_wrapper;     // |--+ iframe_wrapper (table)
    var _iframe;             //    |--- iframe
    var _hidden;             // |--- hidden (input)

    var _settings;
    var _options;
    var _use_p_hack;
    var _state_change_listeners = [];
    var _masseuses = [];
    var _menugroups = [];
    var _keybindings = [];
    var _editor_domain;
    var _html_generator = null;

    var self = this;


    /**
     * Returns the (cleaned-up) HTML of the document currently being edited.
     *
     * @returns {String} the HTML of the document currently being edited.
     */
    this.get_html = function()
    {
        var html;
        
        _unmassage_body();
        UI.Clean.clean(_body, _settings);
        if (_html_generator)
            html = _html_generator.generate(_body.childNodes);
        else
            html = _body.innerHTML;
        html = UI.Clean.clean_HTML(html, _settings);
        _massage_body();
        return html;
    };

    this.get_dirty_html = function()
    {
        return _body.innerHTML;
    };

    /**
     * Sets the HTML of the document.
     *
     * @param    html    the HTML of the document
     */
    this.set_html = function(html)
    {
        _body.innerHTML = html;
        UI.Clean.clean(_body, _settings);
        _massage_body();
    };
    
    this.crash_report = function editor_generate_crash_report(exc)
    {
        var s = Util.Object.clone(this.settings);
        delete s.options;
        
        return {
            version: this.version,
            report_version: "1.0",
            user_agent: navigator.userAgent,
            platform: navigator.platform,
            settings: s,
            options: Util.Object.names(this.options),
            'exception': exc,
            document: this.get_dirty_html()
        };
    };
    
    this.crashed = function loki_editor_crashed(exc)
    {
        var report_uri = _settings.crash_report_uri;
        if (!report_uri)
            return false;
        
        new Util.Request(report_uri, {
            method: "POST",
            headers: {'Content-Type': 'application/json'},
            body: Util.JSON.dump(self.crash_report(exc))
        });
        return true;
    };

    /**
     * Copies the value of the iframe to the value of the textarea.
     */
    this.copy_iframe_to_hidden = function()
    {
        _hidden.value = self.get_html();
    };

    /**
     * Returns whether the textarea (vs the editable iframe)
     * is currently active.
     */
    var _is_textarea_active = function()
    {
        return _textarea.parentNode == _root;
    };

    /**
     * Toggles textarea and iframe.
     */
    this.toggle_iframe_textarea = function()
    {
        if ( _is_textarea_active() )
        {
            self.textarea_to_iframe();
        }
        else
        {
            self.iframe_to_textarea();
        }
    };
    
    /**
     * Shows textarea instead of iframe.
     */
    this.iframe_to_textarea = function()
    {
        _textarea.value = self.get_html(); // this runs the cleaning code
        _root.replaceChild(_textarea, _iframe_wrapper);
        _root.removeChild(_hidden);

        // recreate the toolbars before swapping in the new one,
        // in order to get rid of any lingering "hover"-class'd buttons.
        old_toolbar = _toolbar;
        _create_toolbars(); 
        _root.replaceChild(_textarea_toolbar, old_toolbar);
        _textarea.focus();
    };

    /**
     * Shows iframe instead of textarea.
     */
    this.textarea_to_iframe = function()
    {
        self.set_html(_textarea.value);
        _root.replaceChild(_iframe_wrapper, _textarea);
        _root.appendChild(_hidden);
        _init_async();

        // recreate the toolbars before swapping in the new one,
        // in order to get rid of any lingering "hover"-class'd buttons.
        old_toolbar = _textarea_toolbar;
        _create_toolbars();
        _root.replaceChild(_toolbar, old_toolbar);
        _window.focus();
    };
    
    function enumerate_options(property) {
        var key, results = [];
        
        if (_options) {
            for (key in _options) {
                if (!property)
                    results.append(_options[key]);
                else if (_options[key][property])
                    results.append(_options[key][property]);
            }
        }
        
        return results;
    }
    
    /**
     * Sets focus to the editing window.
     * @return {void}
     */
    this.focus = function focus_on_loki()
    {
        var doc = _owner_document;
        
        if (_is_textarea_active()) {
            if ((!doc.hasFocus || doc.hasFocus()) && _textarea == doc.activeElement)
                return;
            _textarea.focus();
        } else if (!_window) {
            throw new Error('Invalid Loki state: cannot focus; Loki window ' +
                'does not yet exist.');
        } else if (Util.Browser.IE) {
            _body.setActive();
            _window.focus();
        } else {
            _window.focus();
        }
    }


    /**
     * Initializes instance variables.
     *
     * @param {HTMLTextAreaElement} textarea the textarea to replace with Loki
     * @param {Object} settings Loki settings
     * @returns {UI.Loki} this Loki instance
     * @see http://code.google.com/p/loki-editor/wiki/Settings
     */
    this.init = function init_loki(textarea, settings)
    {
        // Incompatible browser check.
        if (!(Util.Browser.IE || Util.Browser.Gecko)) {
            throw new Error('The Loki HTML editor does not currently support ' +
                'your browser.');
        } else if (!textarea) {
            throw new Error('Cannot initialize Loki without a textarea.');
        } else if (!textarea.form) {
            throw new Error('Cannot initialize Loki because the textarea ' +
                Util.Node.get_debug_string(textarea) + ' does not belong to ' +
                'a form.');
        }
        
        if (settings.options && Util.Browser.WebKit) {
            // WebKit doesn't implement underlining in a way that works for us,
            // and our clipboard support is currently IE only.
            settings.options += ' -underline -clipboard';
        }
        
        _settings = (settings) ? Util.Object.clone(settings) : {};
        self.options = _options = UI.Loki.Options.get(_settings.options || 'default', true);
        _settings.options = _options;
        
        ['site', 'type'].each(function cleanup_default_regexp(which) {
            var setting = 'default_' + which + '_regexp';
            if (!_settings[setting])
                return;
            if (!(_settings[setting].exec && _settings[setting].test)) {
                _settings[setting] = new RegExp(_settings[setting]);
            }
        });
        
        if (!_settings.base_uri) {
            _settings.base_uri = autodetect_base_uri();
        }
        
        if (!_settings.html_generator || _settings.html_generator == 'default')
            _settings.html_generator = 'browser';
        else
            _settings.html_generator = _settings.html_generator.toLowerCase();
            
        if (_settings.html_generator == 'loki') {
            _html_generator = new Util.HTML_Generator({
                xhtml: _settings.use_xhtml || false,
                indent_text: "    "
            });
        } else if (_settings.html_generator != 'browser') {
            throw new Error('Unknown HTML generator "' +
                _settings.html_generator + '"; cannot instantiate Loki.');
        }
        
        UI.Clipboard_Helper._setup(_settings.base_uri);
        
        _textarea = textarea;
        _owner_window = window;
        _owner_document = _textarea.ownerDocument;

        _use_p_hacks = _use_p_hacks();

        // Create the various elements
        _create_root();
        _create_toolbars();
        _create_iframe();
        if ( _options.statusbar )
            _create_statusbar();
        _create_grippy();
        _create_hidden();

        // And append them to root
        _root.appendChild( _toolbar );
        _root.appendChild( _iframe_wrapper );
        if ( _options.statusbar )
            _root.appendChild( _statusbar );
        _root.appendChild( _grippy_wrapper );
        _root.appendChild( _hidden );

        // Replace the textarea with root
        _replace_textarea();

        // Append style sheets
        _append_owner_document_style_sheets();

        // Add document massagers
        _add_masseuses();

        // Init possible menugroups, for the context menu
        _init_menugroups();

        // Continue the initialization, but asynchronously
        _init_async();
        
        return self;
    };
    
    /*
     * Attempts to automatically detect the Loki base URI.
     */
    function autodetect_base_uri()
    {
        var scripts = document.getElementsByTagName('SCRIPT');
        var pattern = /bloki.jsb.*$/;
        
        for (var i = 0; i < scripts.length; i++) {
            if (pattern.test(scripts[i].src)) {
                // Found Loki!
                return scripts[i].src.replace(pattern, '');
            }
        }
        
        throw new Error("Unable to automatically determine the Loki base URI." +
            " Please set it explicitly.");
    }
    
    /**
     * Finishes initializing instance variables, but does so
     * asynchronously. All initing that requires _window or _document
     * to be available should be done in this function, because this
     * function waits until _window and _document are available to do
     * anything.
     */
    var _init_async = function()
    {
        try
        {
            // Try to init references to iframe content's window and
            // document ...
            try
            {
                _window = _iframe.contentWindow;
                _document = _window.document;
                if ( _window == null || _document == null )
                    throw(new Error('UI.Loki._init_iframe: Couldn't init iframe. Will try again.'));
            }
            // ... but if the window or document aren't available yet
            // (because the 'about:blank' document hasn't finished
            // loading), try again in a few milliseconds.
            //
            // Be sure that if you change the name of the present
            // function, you also change what you call in setTimeout
            // below.
            catch(f)
            {
                setTimeout(_init_async, 10);
                return;
            }

            // Do things that require _window or _document

            // Write out a blank document
            _clear_document();

            _document.close();

            // Append style sheets for the iframe
            _append_document_style_sheets();

            // Init reference to that document's body
            _body = _document.getElementsByTagName('BODY').item(0);
            Util.Element.add_class(_body, 'contentMain'); // so front-end stylesheets work

            // Add public members // XXX the private ones should just be replaced to public ones
            self.window = _window;
            self.document = _document;
            self.body = _body;
            self.owner_window = _owner_window;
            self.owner_document = _owner_document;
            self.root = _root;
            self.iframe = _iframe;
            self.hidden = _hidden;
            self.settings = _settings;
            self.exec_command = _exec_command;
            self.query_command_state = _query_command_state;
            self.query_command_value = _query_command_value;
            
            // Set body's html to textarea's value
            self.set_html( _textarea.value );

            // Make the document editable
            _make_document_editable();

            // Add certain event listeners to the document and elsewhere
            _add_double_click_listeners();
            _add_document_listeners();
            _add_state_change_listeners();
            _add_grippy_listeners();

            // Add keybindings
            _add_keybindings();
        }
        catch(e)
        {
            // If anything goes wrong during initialization, first
            // revert to the textarea before re-throwing the error
            try {
                self.iframe_to_textarea();
            } catch (desperation) {
                // If even that doesn't work, go all the way back.
                _root.parentNode.replaceChild(_textarea, _root);
            }
            
            throw e;
        }
    };
    
    /**
     * Returns the domain under which this editor instance exists.
     */
    this.editor_domain = function()
    {
        if (null == self._editor_domain) {
            self._editor_domain = Util.URI.extract_domain(window.location);
        }
        
        return self._editor_domain;
    };

    /**
     *
     */
    var _use_p_hacks = function()
    {
        return navigator.product == 'Gecko';
    };

    /**
     * Creates the root element for Loki.
     */
    var _create_root = function()
    {
        _root = _owner_document.createElement('DIV');
        Util.Element.add_class(_root, 'loki');
    };

    /**
     * Creates the toolbar, populated with the appropriate buttons.
     */
    var _create_toolbars = function()
    {
        // Create the toolbar itself
        _toolbar = _owner_document.createElement('DIV');
        _textarea_toolbar = _owner_document.createElement('DIV');
        Util.Element.add_class(_toolbar, 'toolbar');
        Util.Element.add_class(_textarea_toolbar, 'toolbar');

        // Function to add a button to a the toolbars
        function add_button(button_class)
        {
            var b = new button_class();
            b.init(self);

            function create_button()
            {
                var button = _owner_document.createElement('A'), img, img_src;
                button.href = 'javascript:void(0);';

                Util.Event.add_event_listener(button, 'mouseover', function() { Util.Element.add_class(button, 'hover'); });
                Util.Event.add_event_listener(button, 'mouseout', function() { Util.Element.remove_class(button, 'hover'); });
                Util.Event.add_event_listener(button, 'mousedown', function() { Util.Element.add_class(button, 'active'); });
                Util.Event.add_event_listener(button, 'mouseup', function() { Util.Element.remove_class(button, 'active'); });
                Util.Event.add_event_listener(button, 'click', function() { b.click_listener(); });

                img_src = _settings.base_uri + 'images/toolbar/' + b.image;

                // Apply PNG fix.
                if (Util.Browser.IE && /MSIE 6/.test(navigator.userAgent)) {
                    button.title = b.title;
                    img = _owner_document.createElement('SPAN');
                    img_src = Util.URI.build(Util.URI.normalize(img_src));
                    img.className = 'loki_filtered_button';
                    img.style.filter = "progid:" +
                        "DXImageTransform.Microsoft.AlphaImageLoader(src='" +
                        img_src + "', sizingMethod='image')";
                    img.setAttribute('unselectable', 'on');
                } else {
                    img = _owner_document.createElement('IMG');
                    img.src = img_src;
                    img.title = b.title;
                    img.border = 0;
                    img.setAttribute('unselectable', 'on')
                }
                
                button.appendChild(img);
                return button;
            };

            _toolbar.appendChild(create_button());
            if ( b.show_on_source_toolbar == true )
                _textarea_toolbar.appendChild(create_button());
        };

        // Add each button to the toolbars
        enumerate_options('buttons').each(add_button);
    };

    /**
     * Creates the iframe
     */
    var _create_iframe = function()
    {
        _iframe_wrapper = _owner_document.createElement('DIV');
        Util.Element.add_class(_iframe_wrapper, 'iframe_wrapper');

        _iframe = _owner_document.createElement('IFRAME');
        _iframe.src = 'javascript:""';
        _iframe.frameBorder = '0'; // otherwise, IE puts an extra border around the iframe that css cannot erase

        _iframe_wrapper.appendChild(_iframe);

        // Take styles from textarea
        var h = _textarea.clientHeight;
        //_set_height(h);
        // We also need to try again in a second, because in some 
        // versions of FF (e.g. 1.0.6 on win, and some on mac), 
        // the above doesn't work
        setTimeout( function () { _set_height(h); }, 1000 );
        //_set_width(_textarea.clientWidth); // XXX you should check here whether it's width = 100% (or another percentage), then actually copy that; otherwise you can base the new width on clientWidth as here.
    };

    /**
     * Creates the statusbar
     */
    var _create_statusbar = function()
    {
        _statusbar = _owner_document.createElement('DIV');
        Util.Element.add_class(_statusbar, 'statusbar');
    };

    /**
     * Creates the grippy
     */
    var _create_grippy = function()
    {
        // Actually create the elem
        _grippy_wrapper = _owner_document.createElement('DIV');
        Util.Element.add_class(_grippy_wrapper, 'grippy_wrapper');
        _grippy = _owner_document.createElement('IMG');
        _grippy.src = _settings.base_uri + 'images/grippy.gif';
        Util.Element.add_class(_grippy, 'grippy');
        _grippy_wrapper.appendChild(_grippy);
        //_grippy.innerHTML = 'grippy';
    };

    /**
     * Adds listeners to make the grippy actually resize the document.
     */
    var _add_grippy_listeners = function()
    {
        var orig_coords;
        Util.Event.add_event_listener(_grippy, 'mousedown', start_resize);

        // The point of this resize mask is to catch the mouseups with _owner_document,
        // not the iframe's _document, because the coordinates returned when the mouseup is in
        // the iframe's _document, the returned coordinates are buggy in Gecko. If we figure out
        // how to calculate those coordinates accurately--I'm pretty sure it is possible, just
        // tricky--we could remove this resize_mask code.
        var resize_mask = _owner_document.createElement('DIV');
        resize_mask.setAttribute('style', 'position: absolute; top: 0px; left: 0px; height: 20000px; width: 20000px; background: transparent; z-index: 10000;');

        function start_resize(event)
        {
            event = event == null ? window.event : event;
            orig_coords = prev_coords = determine_coords(event);
            Util.Event.add_event_listener(_owner_document, 'mousemove', resize);
            Util.Event.add_event_listener(_owner_document, 'mouseup', stop_resize);
            Util.Event.add_event_listener(_document, 'mousemove', resize);
            Util.Event.add_event_listener(_document, 'mouseup', stop_resize);

            if ( !Util.Browser.IE ) // XXX bad
                _owner_document.documentElement.appendChild(resize_mask);

            return Util.Event.prevent_default(event);
        }
        function resize(event)
        {
            event = event == null ? window.event : event;
            return Util.Event.prevent_default(event);
        }
        function stop_resize(event)
        {
            event = event == null ? window.event : event;

            if ( !Util.Browser.IE ) // XXX bad
                _owner_document.documentElement.removeChild(resize_mask);

            var coords = determine_coords(event);
            //_iframe_wrapper.style.height = _iframe_wrapper.clientHeight + ( coords.y - orig_coords.y ) + 'px';
            _set_height(_get_height() + (coords.y - orig_coords.y));

            Util.Event.remove_event_listener(_owner_document, 'mousemove', resize);
            Util.Event.remove_event_listener(_owner_document, 'mouseup', stop_resize);
            Util.Event.remove_event_listener(_document, 'mousemove', resize);
            Util.Event.remove_event_listener(_document, 'mouseup', stop_resize);
            orig_coords = null;

            return Util.Event.prevent_default(event);
        }
        function determine_coords(event)
        {
            //// Modified from the _show_contextmenu function below.
            //// XXX: Maybe combine this code with that slightly different
            //// code into a fxn in Util.Event, if it's not too difficult.
            //
            // Determine coordinates
            // (Code modified from TinyMCE.)
            var x, y;
            if ( event.pageX != null ) // Gecko
            {
                // If the event is fired from within the iframe,
                // add iframe's position to the reported position.
                var pos;
                var target = Util.Event.get_target(event);
                if ( target.ownerDocument == _document )
                    pos = Util.Element.get_position(_iframe);
                else
                    pos = { x : 0, y : 0 };

                var body = _owner_document.body;
                /// works, sort of:
                //x = pos.x + (event.clientX - body.scrollLeft);
                //y = pos.y + (event.clientY - body.scrollTop);
                x = pos.x + event.pageX;
                y = pos.y + event.pageY;
            }
            else // IE
            {
                /// works, sort of:
                x = event.screenX + 2;
                y = event.screenY + 2;
                ////x = event.clientX + body.scrollLeft.
                ////x = event.clientY + body.scrollTop;
            }
            return { x : x, y : y };
        }
    };

    /**
     * This sets the height of both the possibly editable areas, whether
     * the textarea or iframe.
     */
    var _set_height = function(new_height)
    {
        if ( new_height > 40 )
            _iframe_wrapper.style.height = _textarea.style.height = new_height + 'px';
    };

    /**
     * This gets the height of the actually editable area, whether
     * the textarea or iframe (their heights should always be the same,
     * but whichever is not currently in the document hierarchy will have
     * its height reported incorrectly).
     */
    var _get_height = function()
    {
        return (_is_textarea_active() ? _textarea : _iframe_wrapper).clientHeight;
    };

    /**
     * This sets the width of both the possibly editable areas, whether
     * the textarea or iframe.
     */
    var _set_width = function(new_width)
    {
        if ( new_width > 40 )
        {
            _iframe_wrapper.style.width = _textarea.style.width = new_width + 'px';
            _root.style.width = new_width + 2 + 'px'; // XXX what this number should be changes depending on style sheet..
        }
    };

    /**
     * Creates the hidden element for Loki, and sets the hidden
     * element's name, id, and value to those of the textarea element.
     */
    var _create_hidden = function()
    {
        _hidden = _owner_document.createElement('INPUT');
        _hidden.setAttribute('type', 'hidden');

        if ( _textarea.getAttribute('id') )
            _hidden.setAttribute('id', _textarea.getAttribute('id'));

        if ( _textarea.getAttribute('name') )
            _hidden.setAttribute('name', _textarea.getAttribute('name'));

        if ( _textarea.getAttribute('value') )
            _hidden.setAttribute('value', _textarea.getAttribute('value'));
    };

    /**
     * Replaces the textarea with the root.
     */
    var _replace_textarea = function()
    {
        _textarea.parentNode.replaceChild(_root, _textarea);
    };

    /**
     * Append style sheets to format the main Loki box (not for
     * dialogs etc.) to owner_document's head.
     */
    var _append_owner_document_style_sheets = function()
    {
        Util.Document.append_style_sheet(_owner_document, _settings.base_uri + 'css/Loki.css');
    };

    /**
     * Append style sheets to format the innards of the loki iframe
     */
    var _append_document_style_sheets = function()
    {
        var add = Util.Document.append_style_sheet.curry(_document);
        
        add((_settings.base_uri || '') + 'css/Loki_Document.css');
        
        (_settings.document_style_sheets || []).each(function (sheet) {
            add(sheet);
        });
    };
    
    /**
     * Write out blank document. The key here is that we *close*
     * the document. That way, we don't have to wait for any more
     * load events, dealing with which is exceedingly annoying due
     * to cross-browser issues. Cf note in Util.Window.open.
     */
    var _clear_document = function()
    {
        var html = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"n'+
            't"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">n'+
            '<html>nt<head xmlns="http://www.w3.org/1999/xhtml">n'+
            't<title>Loki editing document</title>n</head>n'+
            '<body></body>n</html>';
            
        _document.open();
        _document.write(html);
        _document.close();
    };

    /**
     * Make the document editable. Mozilla doesn't support
     * contentEditable. Both IE and Mozilla support
     * designMode. However, in IE if designMode is set on an iframe's
     * contentDocument, the iframe's ownerDocument will be denied
     * permission to access it (even if otherwise it *would* have
     * permission). So for IE we use contentEditable, and for Mozilla
     * designMode.
     */
    var _make_document_editable = function()
    {
        if (Util.Browser.IE) {
            _body.contentEditable = true;
            try {
                // If the document isn't really editable, this will throw an
                // error. If the document is editable, this is perfectly
                // harmless.
                _query_command_state('Bold');
            } catch (e) {
                throw new Util.Unsupported_Error('rich text editing');
            }
        } else {
            _document.designMode = 'On';
            try {
                _document.execCommand('undo', false, null);
                _document.execCommand('useCSS', false, true);
            } catch (e) {
                throw new Util.Unsupported_Error('rich text editing');
            }
        }
    };

    /**
     * Add masseuses. The purpose of a masseuse is to replace elements 
     * inconvenient to edit with fake elements that are convenient 
     * to edit, and vice versa. 
     *
     * This is the wrong place to add code designed to clean up bad 
     * HTML; that should be done in UI.Clean.
     */
    var _add_masseuses = function()
    {
        function add_masseuse(masseuse_class)
        {
            var masseuse = new masseuse_class();
            masseuse.init(self);
            _masseuses.push(masseuse);
        }
        
        enumerate_options('masseuses').each(add_masseuse);
    };

    /**
     * Run the massage_node_descendants methods of the masseuses 
     * added in _add_masseuses on _body.
     */
    var _massage_body = function()
    {
        _massage_node_descendants(_body);
    };

    /**
     * Run the unmassage_node_descendants methods of the masseuses 
     * added in _add_masseuses on _body.
     */
    var _unmassage_body = function()
    {
        _unmassage_node_descendants(_body);
    };

    /**
     * Run the massage_node_descendants methods of the masseuses 
     * added in _add_masseuses.
     */
    var _massage_node_descendants = this.massage_node_descendants = function(node)
    {
        for ( var i = 0; i < _masseuses.length; i++ )
        {
            _masseuses[i].massage_node_descendants(node);
        }
    };

    /**
     * Run the unmassage_node_descendants methods of the masseuses 
     * added in _add_masseuses.
     */
    var _unmassage_node_descendants = this.unmassage_node_descendants = function(node)
    {
        for ( var i = 0; i < _masseuses.length; i++ )
        {
            _masseuses[i].unmassage_node_descendants(node);
        }
    };
    
    function _add_double_click_listeners()
    {
        function add(listener_class) {
            var listener = (new listener_class()).init(self);
            Util.Event.observe(_body, 'dblclick', function(ev) {
                listener.double_click(ev);
            });
        }
        
        enumerate_options('double_click_listeners').each(add);
    }
    
    this.describe_error = function describe_error(ex) {
        // The following probably only works under Mozilla.
        function get_stack_trace(e) {
            if (typeof(e.stack) != "string")
                return null;
            
            var stack = [];
            var raw_parts = e.stack.split("n");
            var URI = Util.URI;
            var base = URI.build(URI.normalize(self.settings.base_uri));
            
            return raw_parts.map(function parse_stack_trace_element(l) {
                var pos = l.lastIndexOf("@");
                var source = l.substr(0, pos);
                var location = l.substr(pos + 1);
                
                if (source.charAt(0) == "(")
                    source = "anonymous_fn" + source;
                
                pos = location.lastIndexOf(":");
                var file = location.substr(0, pos);
                var line = parseInt(location.substr(pos + 1));
                
                if (file.indexOf(base) == 0)
                    file = file.substr(base.length);
                
                return {
                    source: source,
                    file: file,
                    line: line
                };
            });
        }
        
        var message;
        if (ex.message) {
            message = ex.message;
        } else {
            try {
                message = ex.toString();
            } catch (e) {
                // Why not just test for toString? Because IE will throw an
                // exception.
                message = '(unable to get exception message)';
            }
        }
        var stack = get_stack_trace(ex);
        
        if (stack) {
            for (var i = 0; i < 4; i++) {
                message += ("n" + stack[i].source + "t" +
                    stack[i].file + ":" + stack[i].line);
            }
        }
        
        return message;
    }

    /**
     * Add certain event listeners to the document, e.g. to listen to
     * key strokes, mouse clicks, and so on.
     */
    var _add_document_listeners = function()
    {
        // added NF 10/14 for TinyMCE
        var control = new TinyMCEControl();
        control.init(_window, _iframe, self);
        var tinyMCE = new TinyMCE();
        tinyMCE.init(_window, control);
        
        var paste_keyup = false; // a keyup event listener has been registered
        var mod_key = (Util.Browser.Mac ? 'meta' : 'ctrl') + 'Key';
        var mod_key_pressed = null;
        
        function move_past_nbsp(direction) {
            var sel = Util.Selection.get_selection(self.window);
            var range = Util.Range.create_range(sel);
            
            if (!Util.Range.is_collapsed(range))
                return false;
            
            var bounds = Util.Range.get_boundaries(range);
            var node, pos, must_move = false, value;
            
            function is_at_edge() {
                if (pos <= 1)
                    return true;
                
                if (node.nodeType == Util.Node.TEXT_NODE) {
                    return (pos >= node.nodeValue.length - 1);
                } else {
                    return (pos >= node.childNodes.length - 1);
                }
            }
            
            if (bounds.start.container.nodeType == Util.Node.TEXT_NODE) {
                node = bounds.start.container;
                value = node.nodeValue;
                if ((direction < 0 && bounds.start.offset > 0) || (direction > 0 && bounds.end.offset < value.length)) {
                    pos = bounds.start.offset;
                    if (direction < 0)
                        pos--;
                    if (node.nodeValue.charCodeAt(pos) != 160 || !is_at_edge())
                        return false;
                    else
                        must_move = true;
                }
            }
            
            if (!must_move) {
                if (bounds.start.container.nodeType == Util.Node.TEXT_NODE) {
                    node = bounds.start.container;
                    node = (direction < 0) ? node.previousSibling : node.nextSibling;
                } else {
                    node = bounds.start.container.childNodes[bounds.start.offset]
                }
                if (!node)
                    return false;
                    
                while (true) {
                    if (!node)
                        return false;
                    if (node.nodeType != Util.Node.TEXT_NODE)
                        return false;
                    value = node.nodeValue;
                    if (value.length == 0) {
                        // try the neighboring node
                        node = (direction < 0) ?
                            node.previousSibling :
                            node.nextSibling;
                        continue;
                    }
                
                    pos = (direction < 0) ? value.length - 1 : 0;
                    if (value.charCodeAt(pos) != 160 || !is_at_edge())
                        return false;
                    break;
                }
            }
            
            if (direction > 0 && node.nodeType == Util.Node.TEXT_NODE) {
                node = Util.Node.next_element_sibling(node.parentNode);
                if (!node)
                    return false;
                pos = 0;
            }
            
            range = Util.Document.create_range(self.document);
            try {
                Util.Range.set_start(range, node, pos);
                range.collapse(true /* to start */);
                Util.Selection.select_range(sel, range);
            } catch (e) {
                return false;
            }
            return true;
        }
        
        Util.Event.add_event_listener(_document, 'mouseup', function() {
            move_past_nbsp(-1);
        });
        Util.Event.add_event_listener(_document, 'keyup', function(ev) {
            if (ev.keyCode == 37)
                move_past_nbsp(-1);
        });
        Util.Event.add_event_listener(_document, 'keydown', function(ev) {
            if (ev.keyCode == 39) {
                if (move_past_nbsp(1)) {
                    return Util.Event.prevent_default(ev);
                }
            }
        });

        var paragraph_helper = (new UI.Paragraph_Helper).init(self);
        Util.Event.add_event_listener(_document, 'keypress', function(event)
        {
            if (!event)
                event = window.event;
            if (!event.metaKey && !event.ctrlKey)
                paragraph_helper.possibly_paragraphify();
            if (Util.Browser.IE) {
                return Util.Fix_Keys.fix_enter_ie(event, _window, self);
            } else {
                Util.Fix_Keys.fix_delete_and_backspace(event, _window);
                tinyMCE.handleEvent(event);
            }
        });

        Util.Event.add_event_listener(_document, 'contextmenu', function(event) 
        {
            return _show_contextmenu(event || _window.event);
        });
        
        if (Util.Browser.IE) {
            function select_end(sel, range, el) {
                var c, text, length;
                for (c = el.lastChild; c; c = c.previousSibling) {
                    if (c.nodeType == Util.Node.ELEMENT_NODE) {
                        if (c.nodeName in Util.Element.empty) {
                            Util.Range.set_start_after(range, c);
                            Util.Range.set_end_after(range, c);
                            Util.Selection.select_range(sel, range);
                            return true;
                        } else if (select_end(sel, range, c)) {
                            return true;
                        }
                    } else if (c.nodeType == Util.Node.TEXT_NODE) {
                        length = c.nodeValue.length;
                        Util.Range.set_start(range, c, length);
                        Util.Range.set_end(range, c, length);
                        Util.Selection.select_range(sel, range);
                        return true;
                    }
                }
                
                text = el.ownerDocument.createTextNode('');
                el.insertBefore(text, el.lastChild);
                
                Util.Range.set_start(range, text, 0);
                Util.Range.set_end(range, text, 0);
                Util.Selection.select_range(sel, range);
                return true;
            }
            
            Util.Event.observe(_document, 'mouseup', function(event) {
                var sel, range;
                
                if (event.srcElement.tagName == 'HTML') {
                    self.focus();
                    
                    sel = Util.Selection.get_selection(_window);
                    range = Util.Document.create_range(_document);
                    select_end(sel, range, _body);
                    
                    event.cancelBubble = true;
                    event.returnValue = false;
                }
            });
        }
        
        if ( _options.statusbar )
        {
            Util.Event.add_event_listener(_document, 'keyup', function() { _update_statusbar(); });
            Util.Event.add_event_listener(_document, 'click', function() { _update_statusbar(); });
            Util.Event.add_event_listener(_toolbar, 'click', function() { _update_statusbar(); });
        }
        
        function perform_cleanup()
        {
            _unmassage_body();
            UI.Clean.clean(_body, _settings, true);
            _massage_body();
        }
        
        function handle_paste_event(ev)
        {
            if (paste_keyup && ev.type == 'paste') {
                // If the browser is capable of generating actual paste
                // events, then remove the DOMNodeInserted handler.
                
                Util.Event.remove_event_listener(_document, 'keydown',
                    key_pressed);
                Util.Event.remove_event_listener(_document, 'keyup',
                    key_raised);
                paste_keyup = false;
            }
            
            perform_cleanup.defer();
        }
        
        // Q: Eric, why is there all this code to accomplish the simple task
        //    of figuring out if the user pressed (Command|Ctrl)+V?
        // A: Firefox/Mac does not always give us a keydown event for when
        //    Cmd+V is pressed. We can't simply look for a Cmd+V keyup, as
        //    it's perfectly acceptable to release the command key before
        //    the V key, so the V's keyup event may have metaKey set to
        //    false. Therefore, we look for a Command keydown and store the
        //    time at which it happened. If we get a keyup for V within 2
        //    seconds of this, run a cleanup.
        
        function key_pressed(ev)
        {
            if (!paste_keyup)
                return;
            if (ev[mod_key]) {
                // We might be starting a paste.
                mod_key_pressed = (new Date()).getTime();
            }
        }
        
        function key_raised(ev)
        {
            if (!paste_keyup)
                return;
            if (mod_key_pressed && ev.keyCode == 86 /* V */) {
                if (mod_key_pressed + 2000 >= (new Date()).getTime())
                    perform_cleanup();
                mod_key_pressed = null;
            }
        }
        
        Util.Event.observe(_document.body, 'paste', handle_paste_event);
        if (Util.Browser.IE || (Util.Browser.Gecko && /rv:1.9/.test(navigator.userAgent))) {
            // We know that we have paste events.
            paste_keyup = false;
        } else {
            paste_keyup = true;
            Util.Event.add_event_listener(_document, 'keydown', key_pressed);
            Util.Event.add_event_listener(_document, 'keyup', key_raised);
        }
        
        function submit_handler(ev)
        {
            try {
                self.copy_iframe_to_hidden();
            } catch (ex) {
                Util.Event.prevent_default(ev);
                var sent = self.crashed(ex);
                alert("An error occurred that prevented your document from " +
                    "being safely submitted." +
                    (sent ? " A report of this error has been sent." : "") +
                    "nnTechnical details:n" +
                    self.describe_error(ex));
                
                if (typeof(console) == 'object' && 'error' in console) {
                    console.error('Failed to generate HTML:',
                        ex);
                }
                
                throw ex;
                return false;
            }
            
            return true;
        }
        
        
        // this copies the changes made in the iframe back to the hidden form element
        Util.Event.add_event_listener(_hidden.form, 'submit',
            Util.Event.listener(submit_handler));
    };

    /**
     * Add listeners to all events which might change the state of the
     * window (e.g., change where the current selection is in the
     * document tree). This is useful for updating the toolbar
     * (updating which buttons appear depressed) and the statusbar.
     *
     * The listeners added are stored in _state_change_listeners. We
     * store them there and then add them all at once at the end of
     * initialization (when this function should be called) instead of
     * just adding them when we need them because it is convenient to
     * add some of the listeners before _document actually points at
     * some non-null thing.
     *
     * I do not like the name "state_change", but couldn't come up
     * with anything better.
     */
    var _add_state_change_listeners = function()
    {
        // I commented this out because it makes Loki really slow
        /*
        for ( var i = 0; i < _state_change_listeners.length; i++ )
        {
            Util.Event.add_event_listener(_document, 'keyup', function() { _state_change_listeners[i]; });
            Util.Event.add_event_listener(_document, 'click', function() { _state_change_listeners[i]; });
            Util.Event.add_event_listener(_toolbar, 'click', function() { _state_change_listeners[i]; });
        }
        */
    };

    /**
     * Update the statusbar with our current place in the document tree.
     */
    var _update_statusbar = function()
    {
        var sel = Util.Selection.get_selection(_window);
        var rng = Util.Range.create_range(sel);
        var cur_node = Util.Range.get_common_ancestor(rng);
        var status = '';
        var i = 0;
        
        do
        {
            if ( i > 0 )
                status = ' > ' + status;

            if ( cur_node.nodeType == Util.Node.TEXT_NODE )
                status = '[TEXT]' + status;
            else if ( cur_node.nodeType == Util.Node.ELEMENT_NODE )
                status = cur_node.tagName + status;

            cur_node = cur_node.parentNode;
            i++;
        }
        while ( cur_node != null &&
                ( cur_node.nodeType != Util.Node.ELEMENT_NODE ||
                  cur_node.tagName != 'HTML' ) )

        _statusbar.innerHTML = status;
    };

    var _add_keybindings = function()
    {
        function add_keybinding(keybinding_class)
        {
            var keybinding = (new keybinding_class).init(self);
            _keybindings.push(keybinding);
        };

        // return value indicates whether to continue bubbling of event or not
        function fire_keybindings(event)
        {
            var i, keybinding, length = _keybindings.length;
            for (i = 0; i < length; ++i) {
                keybinding = _keybindings[i];
                if (keybinding.test(event)) {
                    var should_bubble = keybinding.action();
                    return (typeof(should_bubble) == "boolean")
                        ? should_bubble
                        : false; // don't bubble
                }
            }
            
            return true; // bubble
        };

        enumerate_options('keybindings').each(add_keybinding);
        add_keybinding(UI.Delete_Element_Keybinding); // Delete image, anchor, HR, or table when selected
        add_keybinding(UI.Tab_Keybinding); // Tab

        // We need to listen for different key events for IE and Gecko,
        // because their default actions are on different events.
        var firer, event_name;
        if (Util.Browser.IE) {
            event_name = 'keydown';
            firer = function ie_fire_keybindings(event) {
                if (!fire_keybindings(event)) {
                    event.cancelBubble = true;
                    return Util.Event.prevent_default(event);
                }
                return true;
            };
        } else {
            event_name = 'keypress';
            firer = function gecko_fire_keybindings(event) {
                return (fire_keybindings(event))
                    ? true
                    : Util.Event.prevent_default(event);
            };
        }
        Util.Event.observe(_document, event_name, firer);
    };

    var _init_menugroups = function()
    {
        function add_menugroup(menugroup_class)
        {
            var menugroup = (new menugroup_class).init(self);
            _menugroups.push(menugroup);
        }
        
        enumerate_options('menugroups').each(add_menugroup);
    };

    /**
     * Shows a context menu.
     */
    var _show_contextmenu = function(event)
    {
        var menu = (new UI.Menu).init(self);
        var i, menuitems, added = false;

        // Get appropriate menuitems
        for (i = 0; i < _menugroups.length; i++) {
            try {
                menuitems = _menugroups[i].get_contextual_menuitems();
            } catch (e) {
                if (typeof(console) == 'object' && 'warn' in console) {
                    console.warn('Failed to add menugroup', i, '.', e);
                }
            }
            
            if (menuitems && menuitems.length > 0) {
                if (!added)
                    added = true;
                else
                    menu.add_menuitem((new UI.Separator_Menuitem).init());

                menu.add_menuitems(menuitems);
            }
        }
        
        menu.display(event);

        Util.Event.prevent_default(event);
        return false; // IE
    };

    /**
     * Runs execCommand on _document. The motivation for this wrapper
     * is to avoid issues when execCommand is used in event listeners.
     * (If _document isn't yet initialized when "function() {
     * _document.execCommand(xxx) }" is added as an event listener, an
     * error results, because (in addition to its arguments) the
     * listener when executed has access only to those variables which
     * it had access to when it was defined.
     *
     * Also consult <a href="http://www.mozilla.org/editor/midas-spec.html">Mozilla's</a>
     * and <a href="http://msdn.microsoft.com/workshop/author/dhtml/reference/methods/execcommand.asp">IE's</a>
     * documentation.
     *
     * @param    command        the command to execute
     * @param    iface        boolean indicating whether to use an interface. Not
     *                      supported by Mozilla, so always provide false.
     * @param    value        the value to pass the command
     */
    var _exec_command = function(command, iface, value)
    {
        _window.focus();
        _document.execCommand(command, iface, value);
        _window.focus();
    };

    /**
     * Returns the value of _document.queryCommandValue (see the
     * links on execCommands doc for more info). But first modifies
     * the return value so that IE's is the same as Mozilla's. (On
     * this see <a href="http://www.mozilla.org/editor/ie2midas.html">here</a>, 
     * bullet 8.)
     *
     * See also on _exec_command.
     *
     * @param    command        the command whose value to query (this only works for 
     *                      some of the commands)
     * @return                the (possibly-modified) return value of queryCommandValue
     */
    var _query_command_value = function(command)
    {
        // Not sure if the window.focus is actually helpful here ...
        // and it makes annoying things happen like dialogs popping up
        // behind the editor's containing window.
        //_window.focus();
        var value = _document.queryCommandValue(command);
        
        if ( command == 'FormatBlock' )
        {
            var mappings = 
            {
                // IE : Mozilla
                'Normal' : 'p',
                'Formatted' : 'pre',
                'Heading 1' : 'h1',
                'Heading 2' : 'h2',
                'Heading 3' : 'h3',
                'Heading 4' : 'h4',
                'Heading 5' : 'h5',
                'Heading 6' : 'h6',
                'Preformatted' : 'pre',
                'Address' : 'address'
            };
            
            if (value === false) {
                // WebKit doesn't appear to implement querying FormatBlock,
                // so we'll do it ourselves.
                var ancestry = get_selection_ancestry();
                value = ancestry.find(function(value) {
                    var key;
                    for (key in mappings) {
                        if (mappings[key] == value)
                             return true;
                    }
                });
            } else if (value in mappings) {
                value = mappings[value];
            }
        }
        
        return value;
    }
    
    function get_selection_ancestry() {
        var sel = Util.Selection.get_selection(self.window);
        var range = Util.Range.create_range(sel);
        var ancestor = Util.Range.get_common_ancestor(range);
        
        var ancestry = [];
        var node;
        for (node = ancestor; node; node = node.parentNode) {
            if (node.nodeType == Util.Node.ELEMENT_NODE)
                ancestry.push(node.nodeName.toLowerCase());
        }
        
        return ancestry;
    }

    /**
     * See on _exec_command.
     */
    var _query_command_state = function(command)
    {
        // Not sure if the window.focus is actually helpful here ...
        // and it makes annoying things happen like dialogs popping up
        // behind the editor's containing window.
        //_window.focus();
        return _document.queryCommandState(command);
    }

    /**
     * Formats a block as specified if it's not so, and if it is so,
     * formats it as a normal paragraph.
     *
     * @param   tag     the tag name corresponding to how you want
     *                  the block to be formatted. See <code>mappings</code>
     *                  variable inside the function.
     *
     */
    this.toggle_block = function(tag)
    {
        var tag_string = (_query_command_value('FormatBlock') != tag)
            ? '<' + tag + '>'
            : '<p>';
        
        _exec_command('FormatBlock', false, tag_string);
        _window.focus();
    };

    /**
     * Formats a block as a list of the given type if it's not so, and
     * if it is so, formats it as a normal paragraph. This is
     * necessary because in Mozilla, if a block is already formatted
     * as a list, the Insert[Un]orderedList commands simply remove the
     * block's block-level formatting, rather than changing it to a
     * paragraph.
     *
     * @param   tag     the tag name corresponding to how you want
     *                  the block to be formatted. See mappings variable 
     *                  inside the function
     */
    this.toggle_list = function(tag)
    {
        var command = tag == 'ol' ? 'InsertOrderedList' : 'InsertUnorderedList';

        if ( _query_command_state(command) )
        {
            _exec_command(command); // turn off the list
            this.toggle_block('p');
        }
        else
        {
            _exec_command(command); // turn on the list
        }
    };
};
UI.Loki.prototype.version = "2.0.4";

UI.Loki.Options = new Util.Chooser();
UI.Loki.Options._add_bundled = function add_bundled_loki_options() {
    this.add('bold', {
        buttons: [UI.Bold_Button],
        masseuses: [UI.Bold_Masseuse],
        keybindings: [UI.Bold_Keybinding]
    });
    this.add('italic', {
        buttons: [UI.Italic_Button],
        masseuses: [UI.Italic_Masseuse],
        keybindings: [UI.Italic_Keybinding]
    });
    this.add('underline', {
        buttons: [UI.Underline_Button],
        keybindings: [UI.Underline_Keybinding]
    });
    this.add('headings', {
        buttons: [UI.Headline_Button],
        menugroups: [UI.Headline_Menugroup],
        keybindings: []
    });
    this.add('pre', {
        buttons: [UI.Pre_Button]
    });
    this.add('br', {
        buttons: [UI.BR_Button]
    });
    this.add('hr', {
        buttons: [UI.HR_Button],
        masseuses: [UI.HR_Masseuse]
    });
    this.add('clipboard', {
        buttons: [UI.Cut_Button, UI.Copy_Button, UI.Paste_Button],
        menugroups: [UI.Clipboard_Menugroup],
        keybindings: [UI.Cut_Keybinding, UI.Copy_Keybinding, UI.Paste_Keybinding]
    });
    this.add('highlight', {
        buttons: [UI.Highlight_Button]
    });
    this.add('align', {
        // buttons: [UI.Left_Align_Button, UI.Center_Align_Button, UI.Right_Align_Button],
        menugroups: [UI.Align_Menugroup],
        keybindings: [UI.Left_Align_Keybinding, UI.Center_Align_Keybinding, UI.Right_Align_Keybinding]
    });
    this.add('blockquotes', {
        buttons: [UI.Blockquote_Button]
    });
    this.add('lists', {
        // masseuses: [UI.UL_OL_Masseuse],
        buttons: [UI.OL_Button, UI.UL_Button, UI.Indent_Button, UI.Outdent_Button]
    });
    this.add('find', {
        buttons: [UI.Find_Button],
        keybindings: [UI.Find_Keybinding]
    });
    this.add('tables', {
        buttons: [UI.Table_Button],
        masseuses: [UI.Table_Masseuse],
        menugroups: [UI.Table_Menugroup]
    });
    this.add('images', {
        buttons: [UI.Image_Button],
        masseuses: [UI.Image_Masseuse],
        double_click_listeners: [UI.Image_Double_Click]
    });
    this.add('links', {
        buttons: [UI.Page_Link_Button],
        menugroups: [UI.Link_Menugroup],
        keybindings: [UI.Page_Link_Keybinding],
        double_click_listeners: [UI.Link_Double_Click]
    });
    this.add('anchors', {
        buttons: [UI.Anchor_Button],
        masseuses: [UI.Anchor_Masseuse],
        menugroups: [UI.Anchor_Menugroup],
        double_click_listeners: [UI.Anchor_Double_Click]
    });
    this.add('cleanup', {
        buttons: [UI.Clean_Button]
    });
    this.add('source', {
        buttons: [UI.Source_Button]
    });
    this.add('debug', {
        buttons: [UI.Raw_Source_Button]
    });
    //this.add('statusbar', true);
    
    // Some of these aliases are for installer sanity, while others are for
    // Loki 1 compatibility.
    this.alias('bold', 'strong');
    this.alias('italic', 'em');
    this.alias('tables', 'table');
    this.alias('images', 'image');
    this.alias('links', 'link');
    this.alias('lists', 'list');
    this.alias('blockquotes', 'blockquote');
    this.alias('anchors', 'anchor');
    this.alias('headings', 'heading');
    this.alias('headings', 'headlines');
    this.alias('headings', 'headline');
    this.alias('br', 'linebreaks');
    this.alias('br', 'linebreak');
    this.alias('find', 'findtext');
    
    this.put_set('default', ['strong', 'em', 'headline', 'br', 'hr',
        'highlight', 'align', 'blockquotes', 'lists', 'find', 'images',
        'links', 'cleanup']);
    this.put_set('power', ['strong', 'em', 'headline', 'br', 'hr', 'pre',
        'clipboard', 'highlight', 'align', 'blockquotes', 'lists',
        'find', 'tables', 'images', 'links', 'anchors', 'cleanup', 'source']);
    this.put_set('developer', ['power', 'debug']);
};

var Loki = {
    /**
     * Converts the given textarea to an instance of the Loki WYSIWYG editor.
     * @param {HTMLTextAreaElement} area a TEXTAREA element or the ID of one
     * @param {object} [settings] Loki settings
     * @param {function} [callback] a function that will be called when the
     *        conversion is finished
     * @see UI.Loki#init
     * @see http://code.google.com/p/loki-editor/wiki/Settings
     * @returns {void}
     */
    convert_textarea: function loki_convert_textarea(area, settings,
        callback)
    {
        Loki.convert_textareas([area], settings || {}, callback || null);
    },
    
    /**
     * Converts the given textareas to instances of the Loki WYSIWYG editor.
     * @param {HTMLTextAreaElement[]} areas an array of TEXTAREA elements to
     * convert, or the ID's of the elements
     * @param {object} [settings] Loki settings
     * @param {function} [callback] a function that will be called as the
     *        conversions are finished
     * @see UI.Loki#init
     * @see http://code.google.com/p/loki-editor/wiki/Settings
     * @returns {void}
     */
    convert_textareas: function loki_convert_textareas(areas, settings,
        callback)
    {    
        var area;
        var instance;
        
        for (var i = 0; i < areas.length; i++) {
            if (typeof(areas[i]) == 'string') {
                area = document.getElementById(areas[i]);
                if (!area) {
                    if (Loki._loaded) {
                        throw new Error('No element with the ID of "' +
                            areas[i] + '" exists in the document.');
                    }
                    Loki._pend(areas[i], settings || {}, callback || null);
                    continue;
                }
            } else {
                area = areas[i];
            }
            
            if (!Util.Node.is_tag(area, "TEXTAREA")) {
                throw new TypeError("Unable to convert a non-textarea to a " +
                    "Loki instance.");
            }
            
            instance = (new UI.Loki).init(area, settings || {});
            
            if (callback) {
                callback(instance, area);
            }
        }
    },
    
    /**
     * Converts all of the textareas in the document which have the specified
     * class(es).
     * @param {string} classes    one or more class names
     * @param {object} [settings] Loki settings
     * @param {function} [callback] a function that will be called as the
     *        conversions are finished
     * @returns {void}
     */
    convert_textareas_by_class: function loki_convert_classed_textareas(classes,
        settings, callback)
    {
        function get_textareas()
        {
            return Util.Element.find_by_class(document, classes);
        }
        
        if (this._loaded) {
            Loki.convert_textareas(get_textareas(), settings, callback);
        } else {
            Loki._pend(get_textareas, settings || {}, callback || null);
        }
    },
    
    /**
     * Converts all of the textareas on the document into Loki instances.
     * @param {object} [settings] Loki settings
     * @param {function} [callback] a function that will be called as the
     *        conversions are finished
     * @see UI.Loki#init
     * @see http://code.google.com/p/loki-editor/wiki/Settings
     * @returns {void}
     */
    convert_all_textareas: function loki_convert_all_textareas(settings,
        callback)
    {
        if (this._loaded) {
            Loki.convert_textareas(document.getElementsByTagName("TEXTAREA"),
                settings || {}, callback);
        } else {
            Loki._pend(null, settings || {}, callback || null);
        }
        
    },
    
    /**
     * Returns true if the DOM is ready.
     * @returns {boolean}
     */
    is_document_ready: function is_document_ready()
    {
        return this._loaded;
    },
    
    /**
     * The Loki version.
     * @type string
     */
    version: "2.0.4",
    
    /** @private */
    _pending: [],
    /** @private */
    _loaded: false,
    
    /** @private */
    _pend: function loki_pend_textarea(area, settings, callback) {
        this._pending.push([area, settings, callback]);
    },
    
    /** @private */
    _finish_conversions: function loki_finish_conversions() {
        var a;
        
        if (this._loaded)
            return false;
        this._loaded = true;
        
        while (a = this._pending.pop()) {
            if (a[0] == null) {
                Loki.convert_all_textareas(a[1], a[2]);
                return true;
            } else if (typeof(a[0]) == 'function') {
                Loki.convert_textareas(a[0](), a[1], a[2]);
            } else {
                Loki.convert_textarea(a[0], a[1], a[2]);
            }
        }
        
        return true;
    }
};

(function loki_wait_for_load() {
    var done = Loki._finish_conversions.bind(Loki);
    Util.Event.observe(document, 'DOMContentLoaded', done);
    Util.Event.observe(window, 'load', done);
})();
Онлайн: 0
Реклама