/*****************************************************************************
 *
 * Copyright (c) 2003-2005 Kupu Contributors. All rights reserved.
 *
 * This software is distributed under the terms of the Kupu
 * License. See LICENSE.txt for license text. For a list of Kupu
 * Contributors see CREDITS.txt.
 *
 *****************************************************************************/

// $Id: kupubasetools.js 30992 2006-08-04 12:24:19Z duncan $


//----------------------------------------------------------------------------
//
// Toolboxes
//
//  These are addons for Kupu, simple plugins that implement a certain 
//  interface to provide functionality and control view aspects.
//
//----------------------------------------------------------------------------

//----------------------------------------------------------------------------
// Superclasses
//----------------------------------------------------------------------------

function KupuTool() {
    /* Superclass (or actually more of an interface) for tools 
    
        Tools must implement at least an initialize method and an 
        updateState method, and can implement other methods to add 
        certain extra functionality (e.g. createContextMenuElements).
    */

    this.toolboxes = {};

    // methods
    this.initialize = function(editor) {
        /* Initialize the tool.

            Obviously this can be overriden but it will do
            for the most simple cases
        */
        this.editor = editor;
    };

    this.registerToolBox = function(id, toolbox) {
        /* register a ui box 
        
            Note that this needs to be called *after* the tool has been 
            registered to the KupuEditor
        */
        this.toolboxes[id] = toolbox;
        toolbox.initialize(this, this.editor);
    };
    
    this.updateState = function(selNode, event) {
        /* Is called when user moves cursor to other element 

            Calls the updateState for all toolboxes and may want perform
            some actions itself
        */
        for (id in this.toolboxes) {
            this.toolboxes[id].updateState(selNode, event);
        };
    };

    this.enable = function() {
        // Called when the tool is enabled after a form is dismissed.
    }

    this.disable = function() {
        // Called when the tool is disabled (e.g. for a modal form)
    }
    // private methods
    addEventHandler = addEventHandler;
    
    this._selectSelectItem = function(select, item) {
        this.editor.logMessage(_('Deprecation warning: KupuTool._selectSelectItem'));
    };
    this._fixTabIndex = function(element) {
        var tabIndex = this.editor.getDocument().getEditable().tabIndex-1;
        if (tabIndex && !element.tabIndex) {
            element.tabIndex = tabIndex;
        }
    }
}

function KupuToolBox() {
    /* Superclass for a user-interface object that controls a tool */

    this.initialize = function(tool, editor) {
        /* store a reference to the tool and the editor */
        this.tool = tool;
        this.editor = editor;
    };

    this.updateState = function(selNode, event) {
        /* update the toolbox according to the current iframe's situation */
    };
    
    this._selectSelectItem = function(select, item) {
        this.editor.logMessage(_('Deprecation warning: KupuToolBox._selectSelectItem'));
    };
};

function NoContextMenu(object) {
    /* Decorator for a tool to suppress the context menu */
    object.createContextMenuElements = function(selNode, event) {
        return [];
    }
    return object;
}

// Helper function for enabling/disabling tools
function KupuButtonDisable(button) {
    button = button || this.button;
    button.disabled = "disabled";
    button.className += ' disabled';
}
function KupuButtonEnable(button) {
    button = button || this.button;
    button.disabled = "";
    button.className = button.className.replace(/ *\bdisabled\b/g, '');
}


//----------------------------------------------------------------------------
// Implementations
//----------------------------------------------------------------------------

function KupuButton(buttonid, commandfunc, tool) {
    /* Base prototype for kupu button tools */
    this.buttonid = buttonid;
    this.button = getFromSelector(buttonid);
    this.commandfunc = commandfunc;
    this.tool = tool;

    this.initialize = function(editor) {
        this.editor = editor;
        this._fixTabIndex(this.button);
        addEventHandler(this.button, 'click', this.execCommand, this);
    };

    this.execCommand = function() {
        /* exec this button's command */
        this.commandfunc(this, this.editor, this.tool);
    };

    this.updateState = function(selNode, event) {
        /* override this in subclasses to determine whether a button should
            look 'pressed in' or not
        */
    };
    this.disable = KupuButtonDisable;
    this.enable = KupuButtonEnable;
};

KupuButton.prototype = new KupuTool;
function KupuStateButton(buttonid, commandfunc, checkfunc, offclass, onclass) {
    /* A button that can have two states (e.g. pressed and
       not-pressed) based on CSS classes */
    this.buttonid = buttonid;
    this.button = getFromSelector(buttonid);
    this.commandfunc = commandfunc;
    this.checkfunc = checkfunc;
    this.offclass = offclass;
    this.onclass = onclass;
    this.pressed = false;

    this.execCommand = function() {
        /* exec this button's command */
        this.button.className = (this.pressed ? this.offclass : this.onclass);
        this.pressed = !this.pressed;
        this.editor.focusDocument();
        this.commandfunc(this, this.editor);
    };

    this.updateState = function(selNode, event) {
        /* check if we need to be clicked or unclicked, and update accordingly 
        
            if the state of the button should be changed, we set the class
        */
        var currclass = this.button.className;
        var newclass = null;
        if (this.checkfunc(selNode, this, this.editor, event)) {
            newclass = this.onclass;
            this.pressed = true;
        } else {
            newclass = this.offclass;
            this.pressed = false;
        };
        if (currclass != newclass) {
            this.button.className = newclass;
        };
    };
};

KupuStateButton.prototype = new KupuButton;

/* Same as the state button, but the focusDocument call is delayed.
 * Mozilla&Firefox have a bug on windows which can cause a crash if you
 * change CSS positioning styles on an element which has focus.
 */
function KupuLateFocusStateButton(buttonid, commandfunc, checkfunc, offclass, onclass) {
    KupuStateButton.apply(this, [buttonid, commandfunc, checkfunc, offclass, onclass]);
    this.execCommand = function() {
        /* exec this button's command */
        this.button.className = (this.pressed ? this.offclass : this.onclass);
        this.pressed = !this.pressed;
        this.commandfunc(this, this.editor);
        this.editor.focusDocument();
    };
}
KupuLateFocusStateButton.prototype = new KupuStateButton;

function KupuRemoveElementButton(buttonid, element_name, cssclass) {
    /* A button specialized in removing elements in the current node
       context. Typical usages include removing links, images, etc. */
    this.button = getFromSelector(buttonid);
    this.onclass = 'invisible';
    this.offclass = cssclass;
    this.pressed = false;

    this.commandfunc = function(button, editor) {
        editor.removeNearestParentOfType(editor.getSelectedNode(), element_name);
    };

    this.checkfunc = function(currnode, button, editor, event) {
        var element = editor.getNearestParentOfType(currnode, element_name);
        return (element ? false : true);
    };
};

KupuRemoveElementButton.prototype = new KupuStateButton;

function KupuUI(textstyleselectid) {
    /* View 
    
        This is the main view, which controls most of the toolbar buttons.
        Even though this is probably never going to be removed from the view,
        it was easier to implement this as a plain tool (plugin) as well.
    */
    
    // attributes
    this.tsselect = getFromSelector(textstyleselectid);
    var paraoptions = [];
    var tableoptions = [];
    this.optionstate = -1;
    this.otherstyle = null;
    this.tablestyles = {};
    this.styles = {}; // use an object here so we can use the 'in' operator later on

    this.initialize = function(editor) {
        /* initialize the ui like tools */
        this.editor = editor;
        this.cleanStyles();
        this.enableOptions(false);
        this._fixTabIndex(this.tsselect);
        this._selectevent = addEventHandler(this.tsselect, 'change', this.setTextStyleHandler, this);
    };

    this.getStyles = function() {
        if (!paraoptions) {
            this.cleanStyles();
        }
        return [ paraoptions, tableoptions ];
    }

    this.setTextStyleHandler = function(event) {
        this.setTextStyle(this.tsselect.options[this.tsselect.selectedIndex].value);
    };
    
    // event handlers
    this.basicButtonHandler = function(action) {
        /* event handler for basic actions (toolbar buttons) */
        this.editor.execCommand(action);
        this.editor.updateState();
    };

    this.saveButtonHandler = function() {
        /* handler for the save button */
        this.editor.saveDocument();
    };

    this.saveAndExitButtonHandler = function(redirect_url) {
        /* save the document and, if successful, redirect */
        this.editor.saveDocument(redirect_url);
    };

    this.cutButtonHandler = function() {
        try {
            this.editor.execCommand('Cut');
        } catch (e) {
            if (this.editor.getBrowserName() == 'Mozilla') {
                alert(_('Cutting from JavaScript is disabled on your Mozilla due to security settings. For more information, read http://www.mozilla.org/editor/midasdemo/securityprefs.html'));
            } else {
                throw e;
            };
        };
        this.editor.updateState();
    };

    this.copyButtonHandler = function() {
        try {
            this.editor.execCommand('Copy');
        } catch (e) {
            if (this.editor.getBrowserName() == 'Mozilla') {
                alert(_('Copying from JavaScript is disabled on your Mozilla due to security settings. For more information, read http://www.mozilla.org/editor/midasdemo/securityprefs.html'));
            } else {
                throw e;
            };
        };
        this.editor.updateState();
    };

    this.pasteButtonHandler = function() {
        try {
            this.editor.execCommand('Paste');
        } catch (e) {
            if (this.editor.getBrowserName() == 'Mozilla') {
                alert(_('Pasting from JavaScript is disabled on your Mozilla due to security settings. For more information, read http://www.mozilla.org/editor/midasdemo/securityprefs.html'));
            } else {
                throw e;
            };
        };
        this.editor.updateState();
    };

    this.cleanStyles = function() {
        var options = this.tsselect.options;
        var parastyles = this.styles;
        var tablestyles = this.tablestyles;

        tableoptions.push([options[0].text, 'td|']);
        tablestyles['td'] = 0;
        paraoptions.push([options[0].text, 'p|']);
        parastyles['p'] = 0;
        while (options.length > 1) {
            opt = options[1];
            var v = opt.value;
            if (/^thead|tbody|table|t[rdh]\b/i.test(v)) {
                var otable = tableoptions;
                var styles = tablestyles;
            } else {
                var otable = paraoptions;
                var styles = parastyles;
            }
            if (v.indexOf('|') > -1) {
                var split = v.split('|');
                v = split[0].toLowerCase() + "|" + split[1];
            } else {
                v = v.toLowerCase()+"|";
            };
            otable.push([opt.text, v]);
            styles[v] = otable.length - 1;
            options[1] = null;
        }
        options[0] = null;
    }

    // Remove otherstyle and switch to appropriate style set.
    this.enableOptions = function(inTable) {
        var select = this.tsselect;
        var options = select.options;
        if (this.otherstyle) {
            options[options.length-1] = null;
            this.otherstyle = null;
        }
        if (this.optionstate == inTable) return; /* No change */

        var valid = inTable ? tableoptions : paraoptions;

        while (options.length) options[0] = null;
        this.otherstyle = null;

        for (var i = 0; i < valid.length; i++) {
            var opt = document.createElement('option');
            opt.text = valid[i][0];
            opt.value = valid[i][1];
            options.add(opt);
        }
        select.selectedIndex = 0;
        this.optionstate = inTable;
    }
    
    this.setIndex = function(currnode, tag, index, styles) {
        var className = currnode.className;
        this.styletag = tag;
        this.classname = className;
        var style = tag+'|'+className;

        if (style in styles) {
            return styles[style];
        } else if (!className && tag in styles) {
            return styles[tag];
        }
        return index;
    }

    this.nodeStyle = function(node) {
        var currnode = node;
        var index = -1;
        var options = this.tsselect.options;
        this.styletag = undefined;
        this.classname = '';
        this.intable = false;

        while (currnode) {
            var tag = currnode.nodeName.toLowerCase();

            if (/^body$/.test(tag)) {
                if (!this.styletag) {
                    // Force style setting
                    //this.setTextStyle(options[0].value, true);
                    // Forced style messes up in Firefox: return -1 to
                    // indicate no style 
                    return -1;
                }
                break;
            }
            if (/^(p|div|h.|ul|ol|dl|menu|dir|pre|blockquote|address|center)$/.test(tag)) {
                index = this.setIndex(currnode, tag, index, this.styles);
            }
            if (/^thead|tbody|table|t[rdh]$/.test(tag)) {
                this.intable = true;
                index = this.setIndex(currnode, tag, index, this.tablestyles);

                if (index > 0 || tag=='table') {
                    return index; // Stop processing if in a table
                }
            }
            currnode = currnode.parentNode;
        }
        return index;
    }

    this.updateState = function(selNode) {
        /* set the text-style pulldown */

        // first get the nearest style
        // search the list of nodes like in the original one, break if we encounter a match,
        // this method does some more than the original one since it can handle commands in
        // the form of '<style>|<classname>' next to the plain
        // '<style>' commands
        var index = undefined;
        var mixed = false;
        var styletag, classname;

        var selection = this.editor.getSelection();

        for (var el=selNode.firstChild; el; el=el.nextSibling) {
            if (el.nodeType==1 && selection.containsNode(el)) {
                var i = this.nodeStyle(el);
                if (index===undefined) {
                    index = i;
                    styletag = this.styletag;
                    classname = this.classname;
                }
                if (index != i || styletag!=this.styletag || classname != this.classname) {
                    mixed = true;
                    break;
                }
            }
        };

        if (index===undefined) {
            index = this.nodeStyle(selNode);
        }

        this.enableOptions(this.intable);

        if (index < 0 || mixed) {
            if (mixed) {
                var caption = 'Mixed styles';
            } else if (this.styletag) {
                var caption = 'Other: ' + this.styletag + ' '+ this.classname;
            } else {
                var caption = '<no style>';
            }

            var opt = document.createElement('option');
            opt.text = caption;
            this.otherstyle = opt;
            this.tsselect.options.add(opt);

            index = this.tsselect.length-1;
        }
        this.tsselect.selectedIndex = Math.max(index,0);
    };

    this._cleanNode = function(node) {
                /* Clean up a block style node (e.g. P, DIV, Hn)
                 * Remove trailing whitespace, then also remove up to one
                 * trailing <br>
                 * If the node is now empty, remove the node itself.
                 */
        var len = node.childNodes.length;
        function stripspace() {
            var c;
            while ((c = node.lastChild) && c.nodeType==3 && /^\s*$/.test(c.data)) {
                node.removeChild(c);
            }
        }
        stripspace();
        var c = node.lastChild;
        if (c && c.nodeType==1 && c.tagName=='BR') {
            node.removeChild(c);
        }
        stripspace();
        if (node.childNodes.length==0) {
            node.parentNode.removeChild(node);
        };
    }

    this._cleanCell = function(eltype, classname) {
        var selNode = this.editor.getSelectedNode();
        var el = this.editor.getNearestParentOfType(selNode, eltype);
        if (!el) {
                // Maybe changing type
            el = this.editor.getNearestParentOfType(selNode, eltype=='TD'?'TH':'TD');
        }
        if (!el) return;

            // Remove formatted div or p from a cell
        var node, nxt, n;
        for (node = el.firstChild; node;) {
            if (/DIV|P/.test(node.nodeName)) {
                for (var n = node.firstChild; n;) {
                    var nxt = n.nextSibling;
                    el.insertBefore(n, node); // Move nodes out of div
                    n = nxt;
                }
                nxt = node.nextSibling;
                el.removeChild(node);
                node = nxt;
            } else {
                node = node.nextSibling;
            }
        }
        if (eltype != el.tagName) {
                // Change node type.
            var node = el.ownerDocument.createElement(eltype);
            var parent = el.parentNode;
            parent.insertBefore(node, el);
            while (el.firstChild) {
                node.appendChild(el.firstChild);
            }
            parent.removeChild(el);
            el = node;
        }
            // now set the classname
        if (classname) {
            el.className = classname;
        } else {
            el.removeAttribute("class");
            el.removeAttribute("className");
        }

    }

    this._setClass = function(el, classname) {
        var parent = el.parentNode;
        if (parent.tagName=='DIV') {
            // fixup buggy formatting
            var gp = parent.parentNode;
            if (el != parent.firstChild) {
                var previous = parent.cloneNode(false);
                while (el != parent.firstChild) {
                    previous.appendChild(parent.firstChild);
                }
                gp.insertBefore(previous, parent);
                this._cleanNode(previous);
            }
            gp.insertBefore(el, parent);
            this._cleanNode(el);
            this._cleanNode(parent);
        } else {
            this._cleanNode(el);
        }
        // now set the classname
        if (classname) {
            el.className = classname;
        } else {
            el.removeAttribute("class");
            el.removeAttribute("className");
        }
    }
    this.setTextStyle = function(style, noupdate) {
            /* parse the argument into a type and classname part
               generate a block element accordingly 
            */
        var classname = '';
        var eltype = style.toUpperCase();
        if (style.indexOf('|') > -1) {
            style = style.split('|');
            eltype = style[0].toUpperCase();
            classname = style[1];
        };

        var command = eltype;
            // first create the element, then find it and set the classname
        if (this.editor.getBrowserName() == 'IE') {
            command = '<' + eltype + '>';
        };
        if (/T[RDH]/.test(eltype)) {
            this._cleanCell(eltype, classname);
        }
        else {
            this.editor.getDocument().execCommand('formatblock', command);

                // now get a reference to the element just added
            var selNode = this.editor.getSelectedNode();
            var el = this.editor.getNearestParentOfType(selNode, eltype);
            if (el) {
                this._setClass(el, classname);
            } else {
                var selection = this.editor.getSelection();
                var elements = selNode.getElementsByTagName(eltype);
                for (var i = 0; i < elements.length; i++) {
                    el = elements[i];
                    if (selection.containsNode(el)) {
                        this._setClass(el, classname);
                    }
                }
            }
        }
        if (el) {
            this.editor.getSelection().selectNodeContents(el);
        }
        if (!noupdate) {
            this.editor.updateState();
        }
    };
  
    this.createContextMenuElements = function(selNode, event) {
        var ret = new Array();
        ret.push(new ContextMenuElement(_('Cut'), 
                    this.cutButtonHandler, this));
        ret.push(new ContextMenuElement(_('Copy'), 
                    this.copyButtonHandler, this));
        ret.push(new ContextMenuElement(_('Paste'), 
                    this.pasteButtonHandler, this));
        return ret;
    };
    this.disable = function() {
        this.tsselect.disabled = "disabled";
    }
    this.enable = function() {
        this.tsselect.disabled = "";
    }
}

KupuUI.prototype = new KupuTool;

function ColorchooserTool(fgcolorbuttonid, hlcolorbuttonid, colorchooserid) {
    /* the colorchooser */
    
    this.fgcolorbutton = getFromSelector(fgcolorbuttonid);
    this.hlcolorbutton = getFromSelector(hlcolorbuttonid);
    this.ccwindow = getFromSelector(colorchooserid);
    this.command = null;

    this.initialize = function(editor) {
        /* attach the event handlers */
        this.editor = editor;
        
        this.createColorchooser(this.ccwindow);

        addEventHandler(this.fgcolorbutton, "click", this.openFgColorChooser, this);
        addEventHandler(this.hlcolorbutton, "click", this.openHlColorChooser, this);
        addEventHandler(this.ccwindow, "click", this.chooseColor, this);

        this.hide();

        this.editor.logMessage(_('Colorchooser tool initialized'));
    };

    this.updateState = function(selNode) {
        /* update state of the colorchooser */
        this.hide();
    };

    this.openFgColorChooser = function() {
        /* event handler for opening the colorchooser */
        this.command = "forecolor";
        this.show();
    };

    this.openHlColorChooser = function() {
        /* event handler for closing the colorchooser */
        if (this.editor.getBrowserName() == "IE") {
            this.command = "backcolor";
        } else {
            this.command = "hilitecolor";
        }
        this.show();
    };

    this.chooseColor = function(event) {
        /* event handler for choosing the color */
        var target = _SARISSA_IS_MOZ ? event.target : event.srcElement;
        var cell = this.editor.getNearestParentOfType(target, 'td');
        this.editor.execCommand(this.command, cell.getAttribute('bgColor'));
        this.hide();
    
        this.editor.logMessage(_('Color chosen'));
    };

    this.show = function(command) {
        /* show the colorchooser */
        this.ccwindow.style.display = "block";
    };

    this.hide = function() {
        /* hide the colorchooser */
        this.command = null;
        this.ccwindow.style.display = "none";
    };

    this.createColorchooser = function(table) {
        /* create the colorchooser table */
        
        var chunks = new Array('00', '33', '66', '99', 'CC', 'FF');
        table.setAttribute('id', 'kupu-colorchooser-table');
        table.style.borderWidth = '2px';
        table.style.borderStyle = 'solid';
        table.style.position = 'absolute';
        table.style.cursor = 'default';
        table.style.display = 'none';

        var tbody = document.createElement('tbody');

        for (var i=0; i < 6; i++) {
            var tr = document.createElement('tr');
            var r = chunks[i];
            for (var j=0; j < 6; j++) {
                var g = chunks[j];
                for (var k=0; k < 6; k++) {
                    var b = chunks[k];
                    var color = '#' + r + g + b;
                    var td = document.createElement('td');
                    td.setAttribute('bgColor', color);
                    td.style.backgroundColor = color;
                    td.style.borderWidth = '1px';
                    td.style.borderStyle = 'solid';
                    td.style.fontSize = '1px';
                    td.style.width = '10px';
                    td.style.height = '10px';
                    var text = document.createTextNode('\u00a0');
                    td.appendChild(text);
                    tr.appendChild(td);
                }
            }
            tbody.appendChild(tr);
        }
        table.appendChild(tbody);

        return table;
    };
    this.enable = function() {
        KupuButtonEnable(this.fgcolorbutton);
        KupuButtonEnable(this.hlcolorbutton);
    }
    this.disable = function() {
        KupuButtonDisable(this.fgcolorbutton);
        KupuButtonDisable(this.hlcolorbutton);
    }
}

ColorchooserTool.prototype = new KupuTool;

function PropertyTool(titlefieldid, descfieldid) {
    /* The property tool */

    this.titlefield = getFromSelector(titlefieldid);
    this.descfield = getFromSelector(descfieldid);

    this.initialize = function(editor) {
        /* attach the event handlers and set the initial values */
        this.editor = editor;
        addEventHandler(this.titlefield, "change", this.updateProperties, this);
        addEventHandler(this.descfield, "change", this.updateProperties, this);
        
        // set the fields
        var heads = this.editor.getInnerDocument().getElementsByTagName('head');
        if (!heads[0]) {
            this.editor.logMessage(_('No head in document!'), 1);
        } else {
            var head = heads[0];
            var titles = head.getElementsByTagName('title');
            if (titles.length) {
                this.titlefield.value = titles[0].text;
            }
            var metas = head.getElementsByTagName('meta');
            if (metas.length) {
                for (var i=0; i < metas.length; i++) {
                    var meta = metas[i];
                    if (meta.getAttribute('name') && 
                            meta.getAttribute('name').toLowerCase() == 
                            'description') {
                        this.descfield.value = meta.getAttribute('content');
                        break;
                    }
                }
            }
        }

        this.editor.logMessage(_('Property tool initialized'));
    };

    this.updateProperties = function() {
        /* event handler for updating the properties form */
        var doc = this.editor.getInnerDocument();
        var heads = doc.getElementsByTagName('HEAD');
        if (!heads) {
            this.editor.logMessage(_('No head in document!'), 1);
            return;
        }

        var head = heads[0];

        // set the title
        var titles = head.getElementsByTagName('title');
        if (!titles) {
            var title = doc.createElement('title');
            var text = doc.createTextNode(this.titlefield.value);
            title.appendChild(text);
            head.appendChild(title);
        } else {
            var title = titles[0];
            // IE6 title has no children, and refuses appendChild.
            // Delete and recreate the title.
            if (title.childNodes.length == 0) {
                title.removeNode(true);
                title = doc.createElement('title');
                title.innerText = this.titlefield.value;
                head.appendChild(title);
            } else {
                title.childNodes[0].nodeValue = this.titlefield.value;
            }
        }
        document.title = this.titlefield.value;

        // let's just fulfill the usecase, not think about more properties
        // set the description
        var metas = doc.getElementsByTagName('meta');
        var descset = 0;
        for (var i=0; i < metas.length; i++) {
            var meta = metas[i];
            if (meta.getAttribute('name') && 
                    meta.getAttribute('name').toLowerCase() == 'description') {
                meta.setAttribute('content', this.descfield.value);
                descset = 1;
            }
        }

        if (!descset) {
            var meta = doc.createElement('meta');
            meta.setAttribute('name', 'description');
            meta.setAttribute('content', this.descfield.value);
            head.appendChild(meta);
        }

        this.editor.logMessage(_('Properties modified'));
    };
}

PropertyTool.prototype = new KupuTool;

function LinkTool() {
    /* Add and update hyperlinks */
    
    this.initialize = function(editor) {
        this.editor = editor;
        this.editor.logMessage(_('Link tool initialized'));
    };
    
    this.createLinkHandler = function(event) {
        /* create a link according to a url entered in a popup */
        var linkWindow = openPopup('kupupopups/link.html', 300, 200);
        linkWindow.linktool = this;
        linkWindow.focus();
    };

    this.updateLink = function (linkel, url, type, name, target, title) {
        if (type && type == 'anchor') {
            linkel.removeAttribute('href');
            linkel.setAttribute('name', name);
        } else {
            linkel.href = url;
            if (linkel.innerHTML == "") {
                var doc = this.editor.getInnerDocument();
                linkel.appendChild(doc.createTextNode(title || url));
            }
            if (title) {
                linkel.title = title;
            } else {
                linkel.removeAttribute('title');
            }
            if (target) {
                linkel.setAttribute('target', target);
            }
            else {
                linkel.removeAttribute('target');
            };
            linkel.style.color = this.linkcolor;
        };
    };

    this.formatSelectedLink = function(url, type, name, target, title) {
        var currnode = this.editor.getSelectedNode();

        // selection inside link
        var linkel = this.editor.getNearestParentOfType(currnode, 'A');
        if (linkel) {
            this.updateLink(linkel, url, type, name, target, title);
            return true;
        }

        if (currnode.nodeType!=1) return false;

        // selection contains links
        var linkelements = currnode.getElementsByTagName('A');
        var selection = this.editor.getSelection();
        var containsLink = false;
        for (var i = 0; i < linkelements.length; i++) {
            linkel = linkelements[i];
            if (selection.containsNode(linkel)) {
                this.updateLink(linkel, url, type, name, target, title);
                containsLink = true;
            }
        };
        return containsLink;
    }

    // Can create a link in the following circumstances:
    //   The selection is inside a link:
    //      just update the link attributes.
    //   The selection contains links:
    //      update the attributes of the contained links
    //   No links inside or outside the selection:
    //      create a link around the selection
    //   No selection:
    //      insert a link containing the title
    //
    // the order of the arguments is a bit odd here because of backward
    // compatibility
    this.createLink = function(url, type, name, target, title) {
        if (!this.formatSelectedLink(url, type, name, target, title)) {
            // No links inside or outside.
            this.editor.execCommand("CreateLink", url);
            if (!this.formatSelectedLink(url, type, name, target, title)) {
                // Insert link with no text selected, insert the title
                // or URI instead.
                var doc = this.editor.getInnerDocument();
                linkel = doc.createElement("a");
                linkel.setAttribute('href', url);
                linkel.setAttribute('class', 'generated');
                this.editor.getSelection().replaceWithNode(linkel, true);
                this.updateLink(linkel, url, type, name, target, title);
            };
        }
        this.editor.logMessage(_('Link added'));
        this.editor.updateState();
    };
    
    this.deleteLink = function() {
        /* delete the current link */
        var currnode = this.editor.getSelectedNode();
        var linkel = this.editor.getNearestParentOfType(currnode, 'a');
        if (!linkel) {
            this.editor.logMessage(_('Not inside link'));
            return;
        };
        while (linkel.childNodes.length) {
            linkel.parentNode.insertBefore(linkel.childNodes[0], linkel);
        };
        linkel.parentNode.removeChild(linkel);
        
        this.editor.logMessage(_('Link removed'));
        this.editor.updateState();
    };
    
    this.createContextMenuElements = function(selNode, event) {
        /* create the 'Create link' or 'Remove link' menu elements */
        var ret = new Array();
        var link = this.editor.getNearestParentOfType(selNode, 'a');
        if (link) {
            ret.push(new ContextMenuElement(_('Delete link'), this.deleteLink, this));
        } else {
            ret.push(new ContextMenuElement(_('Create link'), this.createLinkHandler, this));
        };
        return ret;
    };
}

LinkTool.prototype = new KupuTool;

function LinkToolBox(inputid, buttonid, toolboxid, plainclass, activeclass) {
    /* create and edit links */
    
    this.input = getFromSelector(inputid);
    this.button = getFromSelector(buttonid);
    this.toolboxel = getFromSelector(toolboxid);
    this.plainclass = plainclass;
    this.activeclass = activeclass;
    
    this.initialize = function(tool, editor) {
        /* attach the event handlers */
        this.tool = tool;
        this.editor = editor;
        addEventHandler(this.input, "blur", this.updateLink, this);
        addEventHandler(this.button, "click", this.addLink, this);
    };

    this.updateState = function(selNode) {
        /* if we're inside a link, update the input, else empty it */
        var linkel = this.editor.getNearestParentOfType(selNode, 'a');
        if (linkel) {
            // check first before setting a class for backward compatibility
            if (this.toolboxel) {
                this.toolboxel.className = this.activeclass;
            };
            this.input.value = linkel.getAttribute('href');
        } else {
            // check first before setting a class for backward compatibility
            if (this.toolboxel) {
                this.toolboxel.className = this.plainclass;
            };
            this.input.value = '';
        }
    };
    
    this.addLink = function(event) {
        /* add a link */
        var url = this.input.value;
        this.tool.createLink(url);
    };
    
    this.updateLink = function() {
        /* update the current link */
        var currnode = this.editor.getSelectedNode();
        var linkel = this.editor.getNearestParentOfType(currnode, 'A');
        if (!linkel) {
            return;
        }

        var url = this.input.value;
        linkel.setAttribute('href', url);

        this.editor.logMessage(_('Link modified'));
    };
};

LinkToolBox.prototype = new LinkToolBox;

function ImageTool() {
    /* Image tool to add images */
    
    this.initialize = function(editor) {
        /* attach the event handlers */
        this.editor = editor;
        this.editor.logMessage(_('Image tool initialized'));
    };

    this.createImageHandler = function(event) {
        /* create an image according to a url entered in a popup */
        var imageWindow = openPopup('kupupopups/image.html', 300, 200);
        imageWindow.imagetool = this;
        imageWindow.focus();
    };

    this.createImage = function(url, alttext, imgclass) {
        /* create an image */
        var img = this.editor.getInnerDocument().createElement('img');
        img.src = url;
        img.removeAttribute('height');
        img.removeAttribute('width');
        if (alttext) {
            img.alt = alttext;
        };
        if (imgclass) {
            img.className = imgclass;
        };
        img = this.editor.insertNodeAtSelection(img, 1);
        this.editor.logMessage(_('Image inserted'));
        this.editor.updateState();
        return img;
    };

    this.setImageClass = function(imgclass) {
        /* set the class of the selected image */
        var currnode = this.editor.getSelectedNode();
        var currimg = this.editor.getNearestParentOfType(currnode, 'IMG');
        if (currimg) {
            currimg.className = imgclass;
        };
    };

    this.createContextMenuElements = function(selNode, event) {
        return new Array(new ContextMenuElement(_('Create image'), this.createImageHandler, this));
    };
}

ImageTool.prototype = new KupuTool;

function ImageToolBox(inputfieldid, insertbuttonid, classselectid, toolboxid, plainclass, activeclass) {
    /* toolbox for adding images */

    this.inputfield = getFromSelector(inputfieldid);
    this.insertbutton = getFromSelector(insertbuttonid);
    this.classselect = getFromSelector(classselectid);
    this.toolboxel = getFromSelector(toolboxid);
    this.plainclass = plainclass;
    this.activeclass = activeclass;

    this.initialize = function(tool, editor) {
        this.tool = tool;
        this.editor = editor;
        addEventHandler(this.classselect, "change", this.setImageClass, this);
        addEventHandler(this.insertbutton, "click", this.addImage, this);
    };

    this.updateState = function(selNode, event) {
        /* update the state of the toolbox element */
        var imageel = this.editor.getNearestParentOfType(selNode, 'img');
        if (imageel) {
            // check first before setting a class for backward compatibility
            if (this.toolboxel) {
                this.toolboxel.className = this.activeclass;
                this.inputfield.value = imageel.getAttribute('src');
                var imgclass = imageel.className ? imageel.className : 'image-inline';
                selectSelectItem(this.classselect, imgclass);
            };
        } else {
            if (this.toolboxel) {
                this.toolboxel.className = this.plainclass;
            };
        };
    };

    this.addImage = function() {
        /* add an image */
        var url = this.inputfield.value;
        var sel_class = this.classselect.options[this.classselect.selectedIndex].value;
        this.tool.createImage(url, null, sel_class);
        this.editor.focusDocument();
    };

    this.setImageClass = function() {
        /* set the class for the current image */
        var sel_class = this.classselect.options[this.classselect.selectedIndex].value;
        this.tool.setImageClass(sel_class);
        this.editor.focusDocument();
    };
};

ImageToolBox.prototype = new KupuToolBox;

function TableTool() {
    /* The table tool */

    // XXX There are some awfully long methods in here!!
    this.createContextMenuElements = function(selNode, event) {
        var table =  this.editor.getNearestParentOfType(selNode, 'table');
        if (!table) {
            ret = new Array();
            var el = new ContextMenuElement(_('Add table'), this.addPlainTable, this);
            ret.push(el);
            return ret;
        } else {
            var ret = new Array();
            ret.push(new ContextMenuElement(_('Add row'), this.addTableRow, this));
            ret.push(new ContextMenuElement(_('Delete row'), this.delTableRow, this));
            ret.push(new ContextMenuElement(_('Add column'), this.addTableColumn, this));
            ret.push(new ContextMenuElement(_('Delete column'), this.delTableColumn, this));
            ret.push(new ContextMenuElement(_('Delete Table'), this.delTable, this));
            return ret;
        };
    };

    this.addPlainTable = function() {
        /* event handler for the context menu */
        this.createTable(2, 3, 1, 'plain');
    };

    this.createTable = function(rows, cols, makeHeader, tableclass) {
        /* add a table */
        if (rows < 1 || rows > 99 || cols < 1 || cols > 99) {
            this.editor.logMessage(_('Invalid table size'), 1);
            return;
        };

        var doc = this.editor.getInnerDocument();

        table = doc.createElement("table");
        table.className = tableclass;

        // If the user wants a row of headings, make them
        if (makeHeader) {
            var tr = doc.createElement("tr");
            var thead = doc.createElement("thead");
            for (i=0; i < cols; i++) {
                var th = doc.createElement("th");
                th.appendChild(doc.createTextNode("Col " + i+1));
                tr.appendChild(th);
            }
            thead.appendChild(tr);
            table.appendChild(thead);
        }

        tbody = doc.createElement("tbody");
        for (var i=0; i < rows; i++) {
            var tr = doc.createElement("tr");
            for (var j=0; j < cols; j++) {
                var td = doc.createElement("td");
                var content = doc.createTextNode('\u00a0');
                td.appendChild(content);
                tr.appendChild(td);
            }
            tbody.appendChild(tr);
        }
        table.appendChild(tbody);
        this.editor.insertNodeAtSelection(table);

        this._setTableCellHandlers(table);

        this.editor.logMessage(_('Table added'));
        this.editor.updateState();
        return table;
    };

    this._setTableCellHandlers = function(table) {
        // make each cell select its full contents if it's clicked
        addEventHandler(table, 'click', this._selectContentIfEmpty, this);

        var cells = table.getElementsByTagName('td');
        for (var i=0; i < cells.length; i++) {
            addEventHandler(cells[i], 'click', this._selectContentIfEmpty, this);
        };
        
        // select the nbsp in the first cell
        var firstcell = cells[0];
        if (firstcell) {
            var children = firstcell.childNodes;
            if (children.length == 1 && children[0].nodeType == 3 && 
                    children[0].nodeValue == '\xa0') {
                var selection = this.editor.getSelection();
                selection.selectNodeContents(firstcell);
            };
        };
    };
    
    this._selectContentIfEmpty = function() {
        var selNode = this.editor.getSelectedNode();
        var cell = this.editor.getNearestParentOfType(selNode, 'td');
        if (!cell) {
            return;
        };
        var children = cell.childNodes;
        if (children.length == 1 && children[0].nodeType == 3 && 
                children[0].nodeValue == '\xa0') {
            var selection = this.editor.getSelection();
            selection.selectNodeContents(cell);
        };
    };

    this.addTableRow = function() {
        /* Find the current row and add a row after it */
        var currnode = this.editor.getSelectedNode();
        var currtbody = this.editor.getNearestParentOfType(currnode, "TBODY");
        var bodytype = "tbody";
        if (!currtbody) {
            currtbody = this.editor.getNearestParentOfType(currnode, "THEAD");
            bodytype = "thead";
        }
        var parentrow = this.editor.getNearestParentOfType(currnode, "TR");
        var nextrow = parentrow.nextSibling;

        // get the number of cells we should place
        var colcount = 0;
        for (var i=0; i < currtbody.childNodes.length; i++) {
            var el = currtbody.childNodes[i];
            if (el.nodeType != 1) {
                continue;
            }
            if (el.nodeName.toLowerCase() == 'tr') {
                var cols = 0;
                for (var j=0; j < el.childNodes.length; j++) {
                    if (el.childNodes[j].nodeType == 1) {
                        cols++;
                    }
                }
                if (cols > colcount) {
                    colcount = cols;
                }
            }
        }

        var newrow = this.editor.getInnerDocument().createElement("TR");

        for (var i = 0; i < colcount; i++) {
            var newcell;
            if (bodytype == 'tbody') {
                newcell = this.editor.getInnerDocument().createElement("TD");
            } else {
                newcell = this.editor.getInnerDocument().createElement("TH");
            }
            var newcellvalue = this.editor.getInnerDocument().createTextNode("\u00a0");
            newcell.appendChild(newcellvalue);
            newrow.appendChild(newcell);
        }

        if (!nextrow) {
            currtbody.appendChild(newrow);
        } else {
            currtbody.insertBefore(newrow, nextrow);
        }
        
        this.editor.focusDocument();
        this.editor.logMessage(_('Table row added'));
    };

    this.delTableRow = function() {
        /* Find the current row and delete it */
        var currnode = this.editor.getSelectedNode();
        var parentrow = this.editor.getNearestParentOfType(currnode, "TR");
        if (!parentrow) {
            this.editor.logMessage(_('No row to delete'), 1);
            return;
        }

        // move selection aside
        // XXX: doesn't work if parentrow is the only row of thead/tbody/tfoot
        // XXX: doesn't preserve the colindex
        var selection = this.editor.getSelection();
        if (parentrow.nextSibling) {
            selection.selectNodeContents(parentrow.nextSibling.firstChild);
        } else if (parentrow.previousSibling) {
            selection.selectNodeContents(parentrow.previousSibling.firstChild);
        };

        // remove the row
        parentrow.parentNode.removeChild(parentrow);

        this.editor.focusDocument();
        this.editor.logMessage(_('Table row removed'));
    };

    this.addTableColumn = function() {
        /* Add a new column after the current column */
        var currnode = this.editor.getSelectedNode();
        var currtd = this.editor.getNearestParentOfType(currnode, 'TD');
        if (!currtd) {
            currtd = this.editor.getNearestParentOfType(currnode, 'TH');
        }
        if (!currtd) {
            this.editor.logMessage(_('No parentcolumn found!'), 1);
            return;
        }
        var currtr = this.editor.getNearestParentOfType(currnode, 'TR');
        var currtable = this.editor.getNearestParentOfType(currnode, 'TABLE');
        
        // get the current index
        var tdindex = this._getColIndex(currtd);
        // XXX this looks like a debug message, remove
        this.editor.logMessage(_('tdindex: ${tdindex}'));

        // now add a column to all rows
        // first the thead
        var theads = currtable.getElementsByTagName('THEAD');
        if (theads) {
            for (var i=0; i < theads.length; i++) {
                // let's assume table heads only have ths
                var currthead = theads[i];
                for (var j=0; j < currthead.childNodes.length; j++) {
                    var tr = currthead.childNodes[j];
                    if (tr.nodeType != 1) {
                        continue;
                    }
                    var currindex = 0;
                    for (var k=0; k < tr.childNodes.length; k++) {
                        var th = tr.childNodes[k];
                        if (th.nodeType != 1) {
                            continue;
                        }
                        if (currindex == tdindex) {
                            var doc = this.editor.getInnerDocument();
                            var newth = doc.createElement('th');
                            var text = doc.createTextNode('\u00a0');
                            newth.appendChild(text);
                            if (tr.childNodes.length == k+1) {
                                // the column will be on the end of the row
                                tr.appendChild(newth);
                            } else {
                                tr.insertBefore(newth, tr.childNodes[k + 1]);
                            }
                            break;
                        }
                        currindex++;
                    }
                }
            }
        }

        // then the tbody
        var tbodies = currtable.getElementsByTagName('TBODY');
        if (tbodies) {
            for (var i=0; i < tbodies.length; i++) {
                // let's assume table heads only have ths
                var currtbody = tbodies[i];
                for (var j=0; j < currtbody.childNodes.length; j++) {
                    var tr = currtbody.childNodes[j];
                    if (tr.nodeType != 1) {
                        continue;
                    }
                    var currindex = 0;
                    for (var k=0; k < tr.childNodes.length; k++) {
                        var td = tr.childNodes[k];
                        if (td.nodeType != 1) {
                            continue;
                        }
                        if (currindex == tdindex) {
                            var doc = this.editor.getInnerDocument();
                            var newtd = doc.createElement('td');
                            var text = doc.createTextNode('\u00a0');
                            newtd.appendChild(text);
                            if (tr.childNodes.length == k+1) {
                                // the column will be on the end of the row
                                tr.appendChild(newtd);
                            } else {
                                tr.insertBefore(newtd, tr.childNodes[k + 1]);
                            }
                            break;
                        }
                        currindex++;
                    }
                }
            }
        }
        this.editor.focusDocument();
        this.editor.logMessage(_('Table column added'));
    };

    this.delTableColumn = function() {
        /* remove a column */
        var currnode = this.editor.getSelectedNode();
        var currtd = this.editor.getNearestParentOfType(currnode, 'TD');
        if (!currtd) {
            currtd = this.editor.getNearestParentOfType(currnode, 'TH');
        }
        var currcolindex = this._getColIndex(currtd);
        var currtable = this.editor.getNearestParentOfType(currnode, 'TABLE');

        // move selection aside
        var selection = this.editor.getSelection();
        if (currtd.nextSibling) {
            selection.selectNodeContents(currtd.nextSibling);
        } else if (currtd.previousSibling) {
            selection.selectNodeContents(currtd.previousSibling);
        };

        // remove the theaders
        var heads = currtable.getElementsByTagName('THEAD');
        if (heads.length) {
            for (var i=0; i < heads.length; i++) {
                var thead = heads[i];
                for (var j=0; j < thead.childNodes.length; j++) {
                    var tr = thead.childNodes[j];
                    if (tr.nodeType != 1) {
                        continue;
                    }
                    var currindex = 0;
                    for (var k=0; k < tr.childNodes.length; k++) {
                        var th = tr.childNodes[k];
                        if (th.nodeType != 1) {
                            continue;
                        }
                        if (currindex == currcolindex) {
                            tr.removeChild(th);
                            break;
                        }
                        currindex++;
                    }
                }
            }
        }

        // now we remove the column field, a bit harder since we need to take 
        // colspan and rowspan into account XXX Not right, fix theads as well
        var bodies = currtable.getElementsByTagName('TBODY');
        for (var i=0; i < bodies.length; i++) {
            var currtbody = bodies[i];
            var relevant_rowspan = 0;
            for (var j=0; j < currtbody.childNodes.length; j++) {
                var tr = currtbody.childNodes[j];
                if (tr.nodeType != 1) {
                    continue;
                }
                var currindex = 0
                for (var k=0; k < tr.childNodes.length; k++) {
                    var cell = tr.childNodes[k];
                    if (cell.nodeType != 1) {
                        continue;
                    }
                    var colspan = cell.colSpan;
                    if (currindex == currcolindex) {
                        tr.removeChild(cell);
                        break;
                    }
                    currindex++;
                }
            }
        }
        this.editor.focusDocument();
        this.editor.logMessage(_('Table column deleted'));
    };

    this.delTable = function() {
        /* delete the current table */
        var currnode = this.editor.getSelectedNode();
        var table = this.editor.getNearestParentOfType(currnode, 'table');
        if (!table) {
            this.editor.logMessage(_('Not inside a table!'));
            return;
        };
        table.parentNode.removeChild(table);
        this.editor.logMessage(_('Table removed'));
    };

    this.setColumnAlign = function(newalign) {
        /* change the alignment of a full column */
        var currnode = this.editor.getSelectedNode();
        var currtd = this.editor.getNearestParentOfType(currnode, "TD");
        var bodytype = 'tbody';
        if (!currtd) {
            currtd = this.editor.getNearestParentOfType(currnode, "TH");
            bodytype = 'thead';
        }
        var currcolindex = this._getColIndex(currtd);
        var currtable = this.editor.getNearestParentOfType(currnode, "TABLE");

        // unfortunately this is not enough to make the browsers display
        // the align, we need to set it on individual cells as well and
        // mind the rowspan...
        for (var i=0; i < currtable.childNodes.length; i++) {
            var currtbody = currtable.childNodes[i];
            if (currtbody.nodeType != 1 || 
                    (currtbody.nodeName.toUpperCase() != "THEAD" &&
                        currtbody.nodeName.toUpperCase() != "TBODY")) {
                continue;
            }
            for (var j=0; j < currtbody.childNodes.length; j++) {
                var row = currtbody.childNodes[j];
                if (row.nodeType != 1) {
                    continue;
                }
                var index = 0;
                for (var k=0; k < row.childNodes.length; k++) {
                    var cell = row.childNodes[k];
                    if (cell.nodeType != 1) {
                        continue;
                    }
                    if (index == currcolindex) {
                        if (this.editor.config.use_css) {
                            cell.style.textAlign = newalign;
                        } else {
                            cell.setAttribute('align', newalign);
                        }
                        cell.className = 'align-' + newalign;
                    }
                    index++;
                }
            }
        }
    };

    this.setTableClass = function(sel_class) {
        /* set the class for the table */
        var currnode = this.editor.getSelectedNode();
        var currtable = this.editor.getNearestParentOfType(currnode, 'TABLE');

        if (currtable) {
            currtable.className = sel_class;
        }
    };

    this._getColIndex = function(currcell) {
        /* Given a node, return an integer for which column it is */
        var prevsib = currcell.previousSibling;
        var currcolindex = 0;
        while (prevsib) {
            if (prevsib.nodeType == 1 && 
                    (prevsib.tagName.toUpperCase() == "TD" || 
                        prevsib.tagName.toUpperCase() == "TH")) {
                var colspan = prevsib.colSpan;
                if (colspan) {
                    currcolindex += parseInt(colspan);
                } else {
                    currcolindex++;
                }
            }
            prevsib = prevsib.previousSibling;
            if (currcolindex > 30) {
                alert(_("Recursion detected when counting column position"));
                return;
            }
        }

        return currcolindex;
    };

    this._getColumnAlign = function(selNode) {
        /* return the alignment setting of the current column */
        var align;
        var td = this.editor.getNearestParentOfType(selNode, 'td');
        if (!td) {
            td = this.editor.getNearestParentOfType(selNode, 'th');
        };
        if (td) {
            align = td.getAttribute('align');
            if (this.editor.config.use_css) {
                align = td.style.textAlign;
            };
        };
        return align;
    };

    this.fixTable = function(event) {
        /* fix the table so it can be processed by Kupu */
        // since this can be quite a nasty creature we can't just use the
        // helper methods
        
        // first we create a new tbody element
        var currnode = this.editor.getSelectedNode();
        var table = this.editor.getNearestParentOfType(currnode, 'TABLE');
        if (!table) {
            this.editor.logMessage(_('Not inside a table!'));
            return;
        };
        this._fixTableHelper(table);
    };

    this._isBodyRow = function(row) {
        for (var node = row.firstChild; node; node=node.nextSibling) {
            if (/TD/.test(node.nodeName)) {
                return true;
            }
        }
        return false;
    }

    this._cleanCell = function(el) {
        dump('_cleanCell('+el.innerHTML+')\n');
        // Remove formatted div or p from a cell
        var node, nxt, n;
        for (node = el.firstChild; node;) {
            if (/DIV|P/.test(node.nodeName)) {
                for (var n = node.firstChild; n;) {
                    var nxt = n.nextSibling;
                    el.insertBefore(n, node); // Move nodes out of div
                    n = nxt;
                }
                nxt = node.nextSibling;
                el.removeChild(node);
                node = nxt;
            } else {
                node = node.nextSibling;
            }
        }
        var c;
        while (el.firstChild && (c = el.firstChild).nodeType==3 && /^\s+/.test(c.data)) {
            c.data = c.data.replace(/^\s+/, '');
            if (!c.data) {
                el.removeChild(c);
            } else {
                break;
            };
        };
        while (el.lastChild && (c = el.lastChild).nodeType==3 && /\s+$/.test(c.data)) {
            c.data = c.data.replace(/\s+$/, '');
            if (!c.data) {
                el.removeChild(c);
            } else {
                break;
            };
        };
        el.removeAttribute('colSpan');
        el.removeAttribute('rowSpan');
    }
    this._countCols = function(rows, numcols) {
        for (var i=0; i < rows.length; i++) {
            var row = rows[i];
            var currnumcols = 0;
            for (var node = row.firstChild; node; node=node.nextSibling) {
                if (/td|th/i.test(node.nodeName)) {
                    currnumcols += parseInt(node.getAttribute('colSpan') || '1');
                };
            };
            if (currnumcols > numcols) {
                numcols = currnumcols;
            };
        };
        return numcols;
    }

    this._cleanRows = function(rows, container, numcols) {
        // now walk through all rows to clean them up
        for (var i=0; i < rows.length; i++) {
            dump("row "+i+'\n');
            var row = rows[i];
            var doc = this.editor.getInnerDocument();
            var newrow = doc.createElement('tr');
            if (row.className) {
                newrow.className = row.className;
            }
            for (var node = row.firstChild; node;) {
                dump("child\n");
                var nxt = node.nextSibling;
                if (/TD|TH/.test(node.nodeName)) {
                    this._cleanCell(node);
                    newrow.appendChild(node);
                };
                node = nxt;
            };
            if (newrow.childNodes.length) {
                container.appendChild(newrow);
            };
        };
        // now make sure all rows have the correct length
        for (row = container.firstChild; row; row=row.nextSibling) {
            var cellname = row.lastChild.nodeName;
            while (row.childNodes.length < numcols) {
                var cell = doc.createElement(cellname);
                var nbsp = doc.createTextNode('\u00a0');
                cell.appendChild(nbsp);
                row.appendChild(cell);
            };
        };
    };

    this._fixTableHelper = function(table) {
        /* the code to actually fix tables */
        var doc = this.editor.getInnerDocument();
        var thead = doc.createElement('thead');
        var tbody = doc.createElement('tbody');
        var tfoot = doc.createElement('tfoot');

        var table_classes = this.editor.config.table_classes;
        function cleanClassName(name) {
            var allowed_classes = table_classes['class'];
            for (var i = 0; i < allowed_classes.length; i++) {
                var classname = allowed_classes[i];
                classname = classname.classname || classname;
                if (classname==name) return name;
            };
            return allowed_classes[0];
        }
        if (table_classes) {
            table.className = cleanClassName(table.className);
        } else {
            table.removeAttribute('class');
            table.removeAttribute('className');
        };
        table.removeAttribute('border');
        table.removeAttribute('cellpadding');
        table.removeAttribute('cellPadding');
        table.removeAttribute('cellspacing');
        table.removeAttribute('cellSpacing');

        // now get all the rows of the table, the rows can either be
        // direct descendants of the table or inside a 'tbody', 'thead'
        // or 'tfoot' element

        var hrows = [], brows = [], frows = [];
        for (var node = table.firstChild; node; node = node.nextSibling) {
            var nodeName = node.nodeName;
            if (/TR/.test(node.nodeName)) {
                brows.push(node);
            } else if (/THEAD|TBODY|TFOOT/.test(node.nodeName)) {
                var rows = nodeName=='THEAD' ? hrows : nodeName=='TFOOT' ? frows : brows;
                for (var inode = node.firstChild; inode; inode = inode.nextSibling) {
                    if (/TR/.test(inode.nodeName)) {
                        rows.push(inode);
                    };
                };
            };
        };
        /* Extract thead and tfoot from tbody */
        dump('extract head and foot\n');
        while (brows.length && !this._isBodyRow(brows[0])) {
            hrows.push(brows[0]);
            brows.shift();
        }
        while (brows.length && !this._isBodyRow(brows[brows.length-1])) {
            var last = brows[brows.length-1];
            brows.length -= 1;
            frows.unshift(last);
        }
        dump('count cols\n');
        // now find out how many cells our rows should have
        var numcols = this._countCols(hrows, 0);
        numcols = this._countCols(brows, numcols);
        numcols = this._countCols(frows, numcols);

        dump('clean rows\n');
        // now walk through all rows to clean them up
        this._cleanRows(hrows, thead);
        this._cleanRows(brows, tbody);
        this._cleanRows(frows, tfoot);

        // now remove all the old stuff from the table and add the new
        // tbody
        dump('remove old\n');
        while (table.firstChild) {
            table.removeChild(table.firstChild);
        }
        if (hrows.length)
            table.appendChild(thead);
        if (brows.length)
            table.appendChild(tbody);
        if (frows.length)
            table.appendChild(tfoot);
        dump('finish up\n');

        this.editor.focusDocument();
        this.editor.logMessage(_('Table cleaned up'));
    };

    this.fixAllTables = function() {
        /* fix all the tables in the document at once */
        var tables = this.editor.getInnerDocument().getElementsByTagName('table');
        for (var i=0; i < tables.length; i++) {
            this._fixTableHelper(tables[i]);
        };
    };
};

TableTool.prototype = new KupuTool;

function TableToolBox(addtabledivid, edittabledivid, newrowsinputid, 
                    newcolsinputid, makeheaderinputid, classselectid, alignselectid, addtablebuttonid,
                    addrowbuttonid, delrowbuttonid, addcolbuttonid, delcolbuttonid, fixbuttonid,
                    fixallbuttonid, toolboxid, plainclass, activeclass) {
    /* The table tool */

    // XXX There are some awfully long methods in here!!
    

    // a lot of dependencies on html elements here, but most implementations
    // will use them all I guess
    this.addtablediv = getFromSelector(addtabledivid);
    this.edittablediv = getFromSelector(edittabledivid);
    this.newrowsinput = getFromSelector(newrowsinputid);
    this.newcolsinput = getFromSelector(newcolsinputid);
    this.makeheaderinput = getFromSelector(makeheaderinputid);
    this.classselect = getFromSelector(classselectid);
    this.alignselect = getFromSelector(alignselectid);
    this.addtablebutton = getFromSelector(addtablebuttonid);
    this.addrowbutton = getFromSelector(addrowbuttonid);
    this.delrowbutton = getFromSelector(delrowbuttonid);
    this.addcolbutton = getFromSelector(addcolbuttonid);
    this.delcolbutton = getFromSelector(delcolbuttonid);
    this.fixbutton = getFromSelector(fixbuttonid);
    this.fixallbutton = getFromSelector(fixallbuttonid);
    this.toolboxel = getFromSelector(toolboxid);
    this.plainclass = plainclass;
    this.activeclass = activeclass;

    // register event handlers
    this.initialize = function(tool, editor) {
        /* attach the event handlers */
        this.tool = tool;
        this.editor = editor;
        // build the select list of table classes if configured
        if (this.editor.config.table_classes) {
            var classes = this.editor.config.table_classes['class'];
            while (this.classselect.hasChildNodes()) {
                this.classselect.removeChild(this.classselect.firstChild);
            };
            for (var i=0; i < classes.length; i++) {
                var classname = classes[i];
                classname = classname.classname || classname;
                var option = document.createElement('option');
                var content = document.createTextNode(classname);
                option.appendChild(content);
                option.setAttribute('value', classname);
                this.classselect.appendChild(option);
            };
        };
        addEventHandler(this.addtablebutton, "click", this.addTable, this);
        addEventHandler(this.addrowbutton, "click", this.tool.addTableRow, this.tool);
        addEventHandler(this.delrowbutton, "click", this.tool.delTableRow, this.tool);
        addEventHandler(this.addcolbutton, "click", this.tool.addTableColumn, this.tool);
        addEventHandler(this.delcolbutton, "click", this.tool.delTableColumn, this.tool);
        addEventHandler(this.alignselect, "change", this.setColumnAlign, this);
        addEventHandler(this.classselect, "change", this.setTableClass, this);
        addEventHandler(this.fixbutton, "click", this.tool.fixTable, this.tool);
        addEventHandler(this.fixallbutton, "click", this.tool.fixAllTables, this.tool);
        this.addtablediv.style.display = "block";
        this.edittablediv.style.display = "none";
        this.editor.logMessage(_('Table tool initialized'));
    };

    this.updateState = function(selNode) {
        /* update the state (add/edit) and update the pulldowns (if required) */
        var table = this.editor.getNearestParentOfType(selNode, 'table');
        if (table) {
            this.addtablediv.style.display = "none";
            this.edittablediv.style.display = "block";

            var align = this.tool._getColumnAlign(selNode);
            selectSelectItem(this.alignselect, align);
            selectSelectItem(this.classselect, table.className);
            if (this.toolboxel) {
                this.toolboxel.className = this.activeclass;
            };
        } else {
            this.edittablediv.style.display = "none";
            this.addtablediv.style.display = "block";
            this.alignselect.selectedIndex = 0;
            this.classselect.selectedIndex = 0;
            if (this.toolboxel) {
                this.toolboxel.className = this.plainclass;
            };
        };
    };

    this.addTable = function() {
        /* add a table */
        var rows = this.newrowsinput.value;
        var cols = this.newcolsinput.value;
        var makeHeader = this.makeheaderinput.checked;
        // XXX getFromSelector
        var classchooser = getFromSelector("kupu-table-classchooser-add");
        var tableclass = this.classselect.options[this.classselect.selectedIndex].value;
        
        this.tool.createTable(rows, cols, makeHeader, tableclass);
    };

    this.setColumnAlign = function() {
        /* set the alignment of the current column */
        var newalign = this.alignselect.options[this.alignselect.selectedIndex].value;
        this.tool.setColumnAlign(newalign);
    };

    this.setTableClass = function() {
        /* set the class for the current table */
        var sel_class = this.classselect.options[this.classselect.selectedIndex].value;
        if (sel_class) {
            this.tool.setTableClass(sel_class);
        };
    };
};

TableToolBox.prototype = new KupuToolBox;

function ListTool(addulbuttonid, addolbuttonid, ulstyleselectid, olstyleselectid) {
    /* tool to set list styles */

    this.addulbutton = getFromSelector(addulbuttonid);
    this.addolbutton = getFromSelector(addolbuttonid);
    this.ulselect = getFromSelector(ulstyleselectid);
    this.olselect = getFromSelector(olstyleselectid);

    this.style_to_type = {'decimal': '1',
                            'lower-alpha': 'a',
                            'upper-alpha': 'A',
                            'lower-roman': 'i',
                            'upper-roman': 'I',
                            'disc': 'disc',
                            'square': 'square',
                            'circle': 'circle',
                            'none': 'none'
                            };
    this.type_to_style = {'1': 'decimal',
                            'a': 'lower-alpha',
                            'A': 'upper-alpha',
                            'i': 'lower-roman',
                            'I': 'upper-roman',
                            'disc': 'disc',
                            'square': 'square',
                            'circle': 'circle',
                            'none': 'none'
                            };
    
    this.initialize = function(editor) {
        /* attach event handlers */
        this.editor = editor;
        this._fixTabIndex(this.addulbutton);
        this._fixTabIndex(this.addolbutton);
        this._fixTabIndex(this.ulselect);
        this._fixTabIndex(this.olselect);

        addEventHandler(this.addulbutton, "click", this.addUnorderedList, this);
        addEventHandler(this.addolbutton, "click", this.addOrderedList, this);
        addEventHandler(this.ulselect, "change", this.setUnorderedListStyle, this);
        addEventHandler(this.olselect, "change", this.setOrderedListStyle, this);
        this.ulselect.style.display = "none";
        this.olselect.style.display = "none";

        this.editor.logMessage(_('List style tool initialized'));
    };

    this._handleStyles = function(currnode, onselect, offselect) {
        if (this.editor.config.use_css) {
            var currstyle = currnode.style.listStyleType;
        } else {
            var currstyle = this.type_to_style[currnode.getAttribute('type')];
        }
        selectSelectItem(onselect, currstyle);
        offselect.style.display = "none";
        onselect.style.display = "inline";
        offselect.selectedIndex = 0;
    };

    this.updateState = function(selNode) {
        /* update the visibility and selection of the list type pulldowns */
        // we're going to walk through the tree manually since we want to 
        // check on 2 items at the same time
        for (var currnode=selNode; currnode; currnode=currnode.parentNode) {
            var tag = currnode.nodeName.toLowerCase();
            if (tag == 'ul') {
                this._handleStyles(currnode, this.ulselect, this.olselect);
                return;
            } else if (tag == 'ol') {
                this._handleStyles(currnode, this.olselect, this.ulselect);
                return;
            }
        }
        with(this.ulselect) {
            selectedIndex = 0;
            style.display = "none";
        };
        with(this.olselect) {
            selectedIndex = 0;
            style.display = "none";
        };
    };

    this.addList = function(command) {
        this.ulselect.style.display = "inline";
        this.olselect.style.display = "none";
        this.editor.execCommand(command);
        this.editor.focusDocument();
    };
    this.addUnorderedList = function() {
        /* add an unordered list */
        this.addList("insertunorderedlist");
    };

    this.addOrderedList = function() {
        /* add an ordered list */
        this.addList("insertorderedlist");
    };

    this.setListStyle = function(tag, select) {
        /* set the type of an ul */
        var currnode = this.editor.getSelectedNode();
        var l = this.editor.getNearestParentOfType(currnode, tag);
        var style = select.options[select.selectedIndex].value;
        if (this.editor.config.use_css) {
            l.style.listStyleType = style;
        } else {
            l.setAttribute('type', this.style_to_type[style]);
        }
        this.editor.focusDocument();
        this.editor.logMessage(_('List style changed'));
    };

    this.setUnorderedListStyle = function() {
        /* set the type of an ul */
        this.setListStyle('ul', this.ulselect);
    };

    this.setOrderedListStyle = function() {
        /* set the type of an ol */
        this.setListStyle('ol', this.olselect);
    };

    this.enable = function() {
        KupuButtonEnable(this.addulbutton);
        KupuButtonEnable(this.addolbutton);
        this.ulselect.disabled = "";
        this.olselect.disabled = "";
    }
    this.disable = function() {
        KupuButtonDisable(this.addulbutton);
        KupuButtonDisable(this.addolbutton);
        this.ulselect.disabled = "disabled";
        this.olselect.disabled = "disabled";
    }
};

ListTool.prototype = new KupuTool;

function ShowPathTool() {
    /* shows the path to the current element in the status bar */

    this.updateState = function(selNode) {
        /* calculate and display the path */
        var path = '';
        var url = null; // for links we want to display the url too
        var currnode = selNode;
        while (currnode != null && currnode.nodeName != '#document') {
            if (currnode.nodeName.toLowerCase() == 'a') {
                url = currnode.getAttribute('href');
            };
            path = '/' + currnode.nodeName.toLowerCase() + path;
            currnode = currnode.parentNode;
        }
        
        try {
            window.status = url ? 
                    (path.toString() + ' - contains link to \'' + 
                        url.toString() + '\'') :
                    path;
        } catch (e) {
            this.editor.logMessage(_('Could not set status bar message, ' +
                                    'check your browser\'s security settings.'
                                    ), 1);
        };
    };
};

ShowPathTool.prototype = new KupuTool;

function ViewSourceTool() {
    /* tool to provide a 'show source' context menu option */
    this.sourceWindow = null;
    
    this.viewSource = function() {
        /* open a window and write the current contents of the iframe to it */
        if (this.sourceWindow) {
            this.sourceWindow.close();
        };
        this.sourceWindow = window.open('#', 'sourceWindow');
        
        //var transform = this.editor._filterContent(this.editor.getInnerDocument().documentElement);
        //var contents = transform.xml; 
        var contents = '<html>\n' + this.editor.getInnerDocument().documentElement.innerHTML + '\n</html>';
        
        var doc = this.sourceWindow.document;
        doc.write('\xa0');
        doc.close();
        var body = doc.getElementsByTagName("body")[0];
        while (body.hasChildNodes()) {
            body.removeChild(body.firstChild);
        };
        var pre = doc.createElement('pre');
        var textNode = doc.createTextNode(contents);
        body.appendChild(pre);
        pre.appendChild(textNode);
    };
    
    this.createContextMenuElements = function(selNode, event) {
        /* create the context menu element */
        return new Array(new ContextMenuElement(_('View source'), this.viewSource, this));
    };
};

ViewSourceTool.prototype = new KupuTool;

function DefinitionListTool(dlbuttonid) {
    /* a tool for managing definition lists

        the dl elements should behave much like plain lists, and the keypress
        behaviour should be similar
    */

    this.dlbutton = getFromSelector(dlbuttonid);
    
    this.initialize = function(editor) {
        /* initialize the tool */
        this.editor = editor;
        this._fixTabIndex(this.dlbutton);
        addEventHandler(this.dlbutton, 'click', this.createDefinitionList, this);
        addEventHandler(editor.getInnerDocument(), 'keyup', this._keyDownHandler, this);
        addEventHandler(editor.getInnerDocument(), 'keypress', this._keyPressHandler, this);
    };

    // even though the following methods may seem view related, they belong 
    // here, since they describe core functionality rather then view-specific
    // stuff
    this.handleEnterPress = function(selNode) {
        var dl = this.editor.getNearestParentOfType(selNode, 'dl');
        if (dl) {
            var dt = this.editor.getNearestParentOfType(selNode, 'dt');
            if (dt) {
                if (dt.childNodes.length == 1 && dt.childNodes[0].nodeValue == '\xa0') {
                    this.escapeFromDefinitionList(dl, dt, selNode);
                    return;
                };

                var selection = this.editor.getSelection();
                var startoffset = selection.startOffset();
                var endoffset = selection.endOffset(); 
                if (endoffset > startoffset) {
                    // throw away any selected stuff
                    selection.cutChunk(startoffset, endoffset);
                    selection = this.editor.getSelection();
                    startoffset = selection.startOffset();
                };
                
                var ellength = selection.getElementLength(selection.parentElement());
                if (startoffset >= ellength - 1) {
                    // create a new element
                    this.createDefinition(dl, dt);
                } else {
                    var doc = this.editor.getInnerDocument();
                    var newdt = selection.splitNodeAtSelection(dt);
                    var newdd = doc.createElement('dd');
                    while (newdt.hasChildNodes()) {
                        if (newdt.firstChild != newdt.lastChild || newdt.firstChild.nodeName.toLowerCase() != 'br') {
                            newdd.appendChild(newdt.firstChild);
                        };
                    };
                    newdt.parentNode.replaceChild(newdd, newdt);
                    selection.selectNodeContents(newdd);
                    selection.collapse();
                };
            } else {
                var dd = this.editor.getNearestParentOfType(selNode, 'dd');
                if (!dd) {
                    this.editor.logMessage(_('Not inside a definition list element!'));
                    return;
                };
                if (dd.childNodes.length == 1 && dd.childNodes[0].nodeValue == '\xa0') {
                    this.escapeFromDefinitionList(dl, dd, selNode);
                    return;
                };
                var selection = this.editor.getSelection();
                var startoffset = selection.startOffset();
                var endoffset = selection.endOffset();
                if (endoffset > startoffset) {
                    // throw away any selected stuff
                    selection.cutChunk(startoffset, endoffset);
                    selection = this.editor.getSelection();
                    startoffset = selection.startOffset();
                };
                var ellength = selection.getElementLength(selection.parentElement());
                if (startoffset >= ellength - 1) {
                    // create a new element
                    this.createDefinitionTerm(dl, dd);
                } else {
                    // add a break and continue in this element
                    var br = this.editor.getInnerDocument().createElement('br');
                    this.editor.insertNodeAtSelection(br, 1);
                    //var selection = this.editor.getSelection();
                    //selection.moveStart(1);
                    selection.collapse(true);
                };
            };
        };
    };

    this.handleTabPress = function(selNode) {
    };

    this._keyDownHandler = function(event) {
        var selNode = this.editor.getSelectedNode();
        var dl = this.editor.getNearestParentOfType(selNode, 'dl');
        if (!dl) {
            return;
        };
        switch (event.keyCode) {
            case 13:
                if (event.preventDefault) {
                    event.preventDefault();
                } else {
                    event.returnValue = false;
                };
                break;
        };
    };

    this._keyPressHandler = function(event) {
        var selNode = this.editor.getSelectedNode();
        var dl = this.editor.getNearestParentOfType(selNode, 'dl');
        if (!dl) {
            return;
        };
        switch (event.keyCode) {
            case 13:
                this.handleEnterPress(selNode);
                if (event.preventDefault) {
                    event.preventDefault();
                } else {
                    event.returnValue = false;
                };
                break;
            case 9:
                if (event.preventDefault) {
                    event.preventDefault();
                } else {
                    event.returnValue = false;
                };
                this.handleTabPress(selNode);
        };
    };

    this.createDefinitionList = function() {
        /* create a new definition list (dl) */
        var selection = this.editor.getSelection();
        var doc = this.editor.getInnerDocument();

        var selection = this.editor.getSelection();
        var cloned = selection.cloneContents();
        // first get the 'first line' (until the first break) and use it
        // as the dt's content
        var iterator = new NodeIterator(cloned);
        var currnode = null;
        var remove = false;
        while (currnode = iterator.next()) {
            if (currnode.nodeName.toLowerCase() == 'br') {
                remove = true;
            };
            if (remove) {
                var next = currnode;
                while (!next.nextSibling) {
                    next = next.parentNode;
                };
                next = next.nextSibling;
                iterator.setCurrent(next);
                currnode.parentNode.removeChild(currnode);
            };
        };

        var dtcontentcontainer = cloned;
        var collapsetoend = false;
        
        var dl = doc.createElement('dl');
        this.editor.insertNodeAtSelection(dl);
        var dt = this.createDefinitionTerm(dl);
        if (dtcontentcontainer.hasChildNodes()) {
            collapsetoend = true;
            while (dt.hasChildNodes()) {
                dt.removeChild(dt.firstChild);
            };
            while (dtcontentcontainer.hasChildNodes()) {
                dt.appendChild(dtcontentcontainer.firstChild);
            };
        };

        var selection = this.editor.getSelection();
        selection.selectNodeContents(dt);
        selection.collapse(collapsetoend);
    };

    this.createDefinitionTerm = function(dl, dd) {
        /* create a new definition term inside the current dl */
        var doc = this.editor.getInnerDocument();
        var dt = doc.createElement('dt');
        // somehow Mozilla seems to add breaks to all elements...
        if (dd) {
            if (dd.lastChild.nodeName.toLowerCase() == 'br') {
                dd.removeChild(dd.lastChild);
            };
        };
        // dd may be null here, if so we assume this is the first element in 
        // the dl
        if (!dd || dl == dd.lastChild) {
            dl.appendChild(dt);
        } else {
            var nextsibling = dd.nextSibling;
            if (nextsibling) {
                dl.insertBefore(dt, nextsibling);
            } else {
                dl.appendChild(dt);
            };
        };
        var nbsp = doc.createTextNode('\xa0');
        dt.appendChild(nbsp);
        var selection = this.editor.getSelection();
        selection.selectNodeContents(dt);
        selection.collapse();

        this.editor.focusDocument();
        return dt;
    };

    this.createDefinition = function(dl, dt, initial_content) {
        var doc = this.editor.getInnerDocument();
        var dd = doc.createElement('dd');
        var nextsibling = dt.nextSibling;
        // somehow Mozilla seems to add breaks to all elements...
        if (dt) {
            if (dt.lastChild.nodeName.toLowerCase() == 'br') {
                dt.removeChild(dt.lastChild);
            };
        };
        while (nextsibling) {
            var name = nextsibling.nodeName.toLowerCase();
            if (name == 'dd' || name == 'dt') {
                break;
            } else {
                nextsibling = nextsibling.nextSibling;
            };
        };
        if (nextsibling) {
            dl.insertBefore(dd, nextsibling);
            //this._fixStructure(doc, dl, nextsibling);
        } else {
            dl.appendChild(dd);
        };
        if (initial_content) {
            for (var i=0; i < initial_content.length; i++) {
                dd.appendChild(initial_content[i]);
            };
        };
        var nbsp = doc.createTextNode('\xa0');
        dd.appendChild(nbsp);
        var selection = this.editor.getSelection();
        selection.selectNodeContents(dd);
        selection.collapse();
    };

    this.escapeFromDefinitionList = function(dl, currel, selNode) {
        var doc = this.editor.getInnerDocument();
        var p = doc.createElement('p');
        var nbsp = doc.createTextNode('\xa0');
        p.appendChild(nbsp);

        if (dl.lastChild == currel) {
            dl.parentNode.insertBefore(p, dl.nextSibling);
        } else {
            for (var i=0; i < dl.childNodes.length; i++) {
                var child = dl.childNodes[i];
                if (child == currel) {
                    var newdl = this.editor.getInnerDocument().createElement('dl');
                    while (currel.nextSibling) {
                        newdl.appendChild(currel.nextSibling);
                    };
                    dl.parentNode.insertBefore(newdl, dl.nextSibling);
                    dl.parentNode.insertBefore(p, dl.nextSibling);
                };
            };
        };
        currel.parentNode.removeChild(currel);
        var selection = this.editor.getSelection();
        selection.selectNodeContents(p);
        selection.collapse();
        this.editor.focusDocument();
    };

    this._fixStructure = function(doc, dl, offsetnode) {
        /* makes sure the order of the elements is correct */
        var currname = offsetnode.nodeName.toLowerCase();
        var currnode = offsetnode.nextSibling;
        while (currnode) {
            if (currnode.nodeType == 1) {
                var nodename = currnode.nodeName.toLowerCase();
                if (currname == 'dt' && nodename == 'dt') {
                    var dd = doc.createElement('dd');
                    while (currnode.hasChildNodes()) {
                        dd.appendChild(currnode.childNodes[0]);
                    };
                    currnode.parentNode.replaceChild(dd, currnode);
                } else if (currname == 'dd' && nodename == 'dd') {
                    var dt = doc.createElement('dt');
                    while (currnode.hasChildNodes()) {
                        dt.appendChild(currnode.childNodes[0]);
                    };
                    currnode.parentNode.replaceChild(dt, currnode);
                };
            };
            currnode = currnode.nextSibling;
        };
    };
};

DefinitionListTool.prototype = new KupuTool;

function KupuZoomTool(buttonid, firsttab, lasttab) {
    this.button = getFromSelector(buttonid);
    firsttab = firsttab || 'kupu-tb-styles';
    lasttab = lasttab || 'kupu-logo-button';

    this.initialize = function(editor) {
        this.offclass = 'kupu-zoom';
        this.onclass = 'kupu-zoom-pressed';
        this.pressed = false;

        this.baseinitialize(editor);
        this.button.tabIndex = this.editor.document.editable.tabIndex;
        addEventHandler(window, "resize", this.onresize, this);
        addEventHandler(window, "scroll", this.onscroll, this);

        /* Toolbar tabbing */
        var lastbutton = getFromSelector(lasttab);
        var firstbutton = getFromSelector(firsttab);
        var iframe = editor.getInnerDocument();
        this.setTabbing(iframe, firstbutton, lastbutton);
        this.setTabbing(firstbutton, null, editor.getDocument().getWindow());

        this.editor.logMessage(_('Zoom tool initialized'));
    };
};

KupuZoomTool.prototype = new KupuLateFocusStateButton;
KupuZoomTool.prototype.baseinitialize = KupuZoomTool.prototype.initialize;

KupuZoomTool.prototype.onscroll = function() {
    if (!this.zoomed) return;
    /* XXX Problem here: Mozilla doesn't generate onscroll when window is
     * scrolled by focus move or selection. */
    var top = window.pageYOffset!=undefined ? window.pageYOffset : document.documentElement.scrollTop;
    var left = window.pageXOffset!=undefined ? window.pageXOffset : document.documentElement.scrollLeft;
    if (top || left) window.scrollTo(0, 0);
}

// Handle tab pressed from a control.
KupuZoomTool.prototype.setTabbing = function(control, forward, backward) {
    function TabDown(event) {
        if (event.keyCode != 9 || !this.zoomed) return;

        var target = event.shiftKey ? backward : forward;
        if (!target) return;

        if (event.stopPropogation) event.stopPropogation();
        event.cancelBubble = true;
        event.returnValue = false;

        target.focus();
        return false;
    }
    addEventHandler(control, "keydown", TabDown, this);
}

KupuZoomTool.prototype.onresize = function() {
    if (!this.zoomed) return;

    var editor = this.editor;
    var iframe = editor.getDocument().editable;
    var sourcetool = editor.getTool('sourceedittool');
    var sourceArea = sourcetool?sourcetool.getSourceArea():null;

    var fulleditor = iframe.parentNode;
    var body = document.body;

    if (window.innerWidth) {
        var width = window.innerWidth;
        var height = window.innerHeight;
    } else if (document.documentElement) {
        var width = document.documentElement.offsetWidth-5;
        var height = document.documentElement.offsetHeight-5;
    } else {
        var width = document.body.offsetWidth-5;
        var height = document.body.offsetHeight-5;
    }
    width = width + 'px';
    var offset = iframe.offsetTop;
    if (sourceArea && sourceArea.offsetTop) offset = sourceArea.offsetTop-1;
    var nheight = Math.max(height - offset -1/*top border*/, 10);
    nheight = nheight + 'px';
    fulleditor.style.width = width; /*IE needs this*/
    iframe.style.width = width;
    iframe.style.height = nheight;
    if (sourceArea) {
        sourceArea.style.width = width;
        sourceArea.style.height = nheight;
    }
}

KupuZoomTool.prototype.checkfunc = function(selNode, button, editor, event) {
    return this.zoomed;
}

KupuZoomTool.prototype.commandfunc = function(button, editor) {
    /* Toggle zoom state */
    var zoom = button.pressed;
    this.zoomed = zoom;

    var zoomClass = 'kupu-fulleditor-zoomed';
    var iframe = editor.getDocument().getEditable();

    var body = document.body;
    var html = document.getElementsByTagName('html')[0];
    if (zoom) {
        html.style.overflow = 'hidden';
        window.scrollTo(0, 0);
        editor.setClass(zoomClass);
        body.className += ' '+zoomClass;
        this.onresize();
    } else {
        html.style.overflow = '';
        var fulleditor = iframe.parentNode;
        fulleditor.style.width = '';
        body.className = body.className.replace(/ *kupu-fulleditor-zoomed/, '');
        editor.clearClass(zoomClass);

        iframe.style.width = '';
        iframe.style.height = '';

        var sourcetool = editor.getTool('sourceedittool');
        var sourceArea = sourcetool?sourcetool.getSourceArea():null;
        if (sourceArea) {
            sourceArea.style.width = '';
            sourceArea.style.height = '';
        };
    }
    var doc = editor.getInnerDocument();
    // Mozilla needs this. Yes, really!
    doc.designMode=doc.designMode;

    window.scrollTo(0, iframe.offsetTop);
    editor.focusDocument();
}
