/*
 * =================================================================
 * Gossamer Links - enhanced directory management system
 *
 *   Website  : http://gossamer-threads.com/
 *   Support  : http://gossamer-threads.com/scripts/support/
 *   Revision : $Id: treecats.js,v 1.29 2007/11/15 01:20:18 brewt Exp $
 *
 * Copyright (c) 2006 Gossamer Threads Inc.  All Rights Reserved.
 * Redistribution in part or in whole strictly prohibited. Please
 * see LICENSE file for full details.
 * =================================================================
 */

/*
<!-- this.objects.workspace -->
<div id="treecats">
    <!-- this.objects.selection -->
    <div class="treecats-selection/treecats-selection-summary treecats-selection-single/treecats-selection-multiple">
        <!-- this.objects.selectionList -->
        <ul>
            <li class="treecats-selection-current"><input type="hidden" name="[this.config.inputName]" value="<id>" /><span>Full/Category</span><a class="treecats-selection-change">[this.lang.change]</a><a class="treecats-selection-remove">[this.lang.remove]</a><a class="treecats-selection-done">[this.lang.done]<a/><a class="treecats-selection-cancel">[this.lang.cancel]</a></li>
            ...
        </ul>
        <!-- this.objects.selectionListAdd -->
        <a class="treecats-selection-add">[this.lang.add]</a>
    </div>

    <!-- this.objects.tree -->
    <div class="treecats-tree">
        <div id="tc-c[id]" class="treecats-category">
            <div class="treecats-category-info treecats-selected">
                <a href="javascript:tc.navigate([id])"><img /></a> <span title="Full/Category"><a href="javascript:tc.select(<id>)">Category Name</a></span>
            </div>
            <div class="treecats-children">
                <div id="tc-c[id]" class="treecats-category">...</div>
                ...
                <ul class="treecats-links">
                    <li class="treecats-selected"><a href="javascript:tc.selectLink([id])" title="[URL]">Link Title</a></li>
                    ...
                </ul>
            </div>
        </div>
    </div>
</div>

TODO
    where to append the div's to (for absolute positioning to work)
    scroll to the selection (perhaps this should be added to _resize)
*/

function treecats(config, lang) {
    this.config = {
// The method to use to browse categories.
//     "normal"
//     "collapsed" - when viewing a category, collapse all categories on the same level
        browseMode        : 'normal',
// This controls whether or not multiple categories may be selected or if links
// are to be selected.  It may be either: "single", "multiple", or "link".
        selectionMode     : 'single',
// When selectionMode is 'multiple', this is the maximum number of categories
// to allow.  Set this to 0 for an unlimited number of categories.
        multipleMax       : 0,
// Whether or not a selection is required.  If a selection is not required, a
// root entry will be displayed (using lang.rootText), allowing the user to
// unselect the current selection.  Note that this option does not force the
// user to make a selection.  This option is only valid if selectionMode is set
// to 'single'.
        selectionRequired : true,
// Whether or not selecting a category will expand its children.
        selectionExpands  : true,
// The URL to treecats.cgi.
        cgiURL            : '',
// Extra query string to add to the url.
        cgiQueryString    : '',
// The URL to the expand.gif, collapse.gif, root.gif and loading.gif images.
        imageURL          : '',
// The name of the treecats object.
        objName           : 'tc',
// The id of the area that treecats will use.
        workspace         : 'treecats',
// The maximum height (in pixels) of the tree area before a scrollbar is
// enabled.  Set this to 0 for no maximum height.
        maxHeight         : 500,
// The form input name.
        inputName         : 'CatLinks.CategoryID'
    };

    this.lang = {
        noSelection     : 'No category selected',
        noSelectionLink : 'No link selected',
        rootText        : 'All Categories',
        expand          : 'Expand',
        collapse        : 'Collapse',
        change          : '[Change]',
        remove          : '[Remove]',
        done            : '[Done]',
        cancel          : '[Cancel]',
        add             : '[Add]',
        loading         : 'Loading',
        duplicate       : 'You have already selected this category.'
    };

    if (config) {
        for (var key in config)
            this.config[key] = config[key];
    }
    if (lang) {
        for (var key in lang)
            this.lang[key] = lang[key];
    }

    this.objects = {
        workspace        : null,
        selection        : null,
        selectionList    : null,
        selectionListAdd : null,
        tree             : null
    };

// A backup of the selection to allow cancel
    this.selectionBackup = null;
// The ID of the selection item currently being used
    this.currentSelectionItem = 0;
// The ID of the category/link that is currently selected
    this.currentSelection = null;
// The links (object) of the previously selected category
    this.links = null;
// Mapping of link ID to category ID
    this.linkToCategory = {};

// Constants for _traverse()
    this.SINGLE = 0;
    this.DOWN   = 1;
    this.UP     = 2;
// Constants for navigate()
    this.AUTO     = 0;
    this.EXPAND   = 1;
    this.COLLAPSE = 2;
}

treecats.prototype.load = function () {
    if (this.config.selectionMode != 'single')
        this.config.selectionRequired = true;

// Accept pre-selected categories to be either inputs already on the page, or
// as arguments to load().  If you use inputs already on the page, then you
// must call load() after all the inputs have been loaded.
    var fetch = [];
    var inputs = document.getElementsByTagName('input');
    for (var i = 0; i < inputs.length; i++) {
        if (inputs[i].name == this.config.inputName) {
            if (inputs[i].value > 0)
                fetch.push(inputs[i].value);
            inputs[i].parentNode.removeChild(inputs[i]);
            i--;
        }
    }
    for (var i = 0; i < arguments.length; i++) {
        if (arguments[i] > 0)
            fetch.push(arguments[i]);
    }

    this.objects.workspace = document.getElementById(this.config.workspace);

    this.objects.selection = document.createElement('div');
    this.objects.selection.className = 'treecats-selection-summary ' + (this.config.selectionMode == 'multiple' ? 'treecats-selection-multiple' : 'treecats-selection-single');
    this.objects.workspace.appendChild(this.objects.selection);

    this.objects.selectionList = document.createElement('ul');
    this.objects.selection.appendChild(this.objects.selectionList);

    if (this.config.selectionMode == 'multiple') {
        this.objects.selectionListAdd = document.createElement('a');
        this.objects.selectionListAdd.href = 'javascript:' + this.config.objName + '.add()';
        this.objects.selectionListAdd.className = 'treecats-selection-add';
        this.objects.selectionListAdd.style.display = 'none';
        this.objects.selectionListAdd.appendChild(document.createTextNode(this.lang.add));
        this.objects.selection.appendChild(this.objects.selectionListAdd);
    }

    this.objects.tree = document.createElement('div');
    this.objects.tree.className = 'treecats-tree';
    this.objects.tree.style.display = 'none';
    this.objects.workspace.appendChild(this.objects.tree);

    if (this.config.cgiURL)
        this.config.cgiURL = this.config.cgiURL.replace(/^https?:\/\/[^\/]+\//, '/');

    if (fetch.length == 0) {
        this._createSelection();
        fetch = [0];
    }
    var type = this.config.selectionMode == 'link' ? 'lid=' : 'cid=';
    this._asyncReq(this._treeHandler, this.config.cgiURL + '/treecats.cgi', type + fetch.join('&' + type) + (this.config.cgiQueryString ? '&' + this.config.cgiQueryString : ''));
}

// Toggle the summary mode display.  Pass in a true value for the cancel
// argument if you wish to revert to the original selection.
treecats.prototype.toggle = function (id, cancel) {
    if (!id)
        return;
    var selected = document.getElementById(this._sid(id));

// Show the tree
    if (this.objects.tree.style.display == 'none') {
        this.currentSelectionItem = id;

// Save the current selection (so the user can cancel)
        this.selectionBackup = { title : selected.childNodes[1].firstChild.nodeValue, value : selected.firstChild.value };

        for (var i = 0; i < this.objects.selectionList.childNodes.length; i++) {
            var curr = this.objects.selectionList.childNodes[i];
// Show the Done/Cancel links on the current selection item
            if (curr == selected)
                curr.childNodes[4].style.display = curr.childNodes[5].style.display = '';
// Hide the Change/Remove links
            curr.childNodes[2].style.display = curr.childNodes[3].style.display = 'none';
        }

        this.collapseAll();
        var sid = selected.firstChild.value || 0;
        if (this.config.selectionMode != 'link') {
            this.expandTo(sid);
            this.select(sid);
        }
        else {
            this.expandTo(this.linkToCategory[sid], true);
            this.selectLink(sid);
        }

        selected.childNodes[1].className = 'treecats-selection-current';
        this.objects.tree.style.display = '';
        this.objects.selection.className = this.objects.selection.className.replace(/treecats-selection-summary /, 'treecats-selection ');
        this._resize();
    }
    else {
// Check to see that the user's selection isn't a duplicate
        if (!cancel && this.config.selectionMode == 'multiple' && selected.firstChild.value > 0) {
            for (var i = 0; i < this.objects.selectionList.childNodes.length; i++) {
                var curr = this.objects.selectionList.childNodes[i];
                if (curr == selected)
                    continue;
                if (curr.firstChild.value == selected.firstChild.value) {
                    alert(this.lang.duplicate);
                    return;
                }
            }
        }

// Restore the previous selection
        if (cancel)
            this._updateSelection(this.selectionBackup.title, this.selectionBackup.value);

        for (var i = 0; i < this.objects.selectionList.childNodes.length; i++) {
            var curr = this.objects.selectionList.childNodes[i];
// Hide the Done/Cancel links
            if (curr == selected)
                curr.childNodes[4].style.display = curr.childNodes[5].style.display = 'none';
// Show the Change/Remove links
            curr.childNodes[2].style.display = '';
            if (this.objects.selectionList.childNodes.length > 1)
                curr.childNodes[3].style.display = '';
        }

        selected.childNodes[1].className = '';
        this.objects.tree.style.display = 'none';
        this.objects.selection.className = this.objects.selection.className.replace(/treecats-selection /, 'treecats-selection-summary ');
    }
    this._updateListAdd();
}

// Select a Category
treecats.prototype.select = function (id) {
    if (this.config.selectionMode == 'link')
        return;

// Unselect the previously selected category
    var already_selected = this.currentSelection == id;
    if (!already_selected) {
        var prevCat = document.getElementById(this._id(this.currentSelection));
        if (prevCat)
            prevCat.firstChild.className = prevCat.firstChild.className.replace(/ ?treecats-selected/, '');
        this.currentSelection = id;
    }

    var cat = document.getElementById(this._id(id));
    if (!cat)
        return;

    if (!already_selected) {
        this._updateSelection(id > 0 ? cat.firstChild.lastChild.title : this.lang.rootText, id);

// Set the selected class on the selection
        cat.firstChild.className += ' treecats-selected';
    }

// Expand the category's children on selection
    if (this.config.selectionExpands && this.objects.tree.style.display != 'none' && id != 0 &&
        (already_selected || cat.childNodes[1].style.display == 'none'))
        this.navigate(id);
}

// Select a link
treecats.prototype.selectLink = function (id) {
    if (this.config.selectionMode != 'link')
        return;

    var link = document.getElementById(this._lid(id));
    if (!link)
        return;

    var already_selected = this.currentSelection == id;
    if (already_selected)
        return;

// The link may be in multiple categories
    var ext = '';
    var count = 0;
    var unselect;
    while (unselect = document.getElementById(this._lid(this.currentSelection + ext))) {
        unselect.className = unselect.className.replace(/ ?treecats-selected/, '');
        count++;
        ext = '-' + count;
    }
    this.currentSelection = id;

    this._updateSelection(link.firstChild.firstChild.nodeValue, id);
    ext = '';
    count = 0;
    var select;
    while (select = document.getElementById(this._lid(id + ext))) {
        select.className += ' treecats-selected';
        count++;
        ext = '-' + count;
    }
}

// Expand or collapse a category.
// state: 0 = auto (this.AUTO), 1 = force expand (this.EXPAND), 2 = force collapse (this.COLLAPSE)
treecats.prototype.navigate = function (id, state) {
    var cat = document.getElementById(this._id(id));
    if (!cat)
        return;

    var me = this;
// Collapse categories on the same level as the current category
    var collapseSiblings = function () {
        if (me.config.browseMode != 'collapsed')
            return;

        var siblings = cat.parentNode.childNodes;
        for (var i = 0; i < siblings.length; i++) {
            if (siblings[i] == cat || !siblings[i].className.match(/treecats-category/))
                continue;
            if (siblings[i].childNodes[1].style.display != 'none')
                me.navigate(me._getId(siblings[i]));
        }
    }

// Can't expand a category if it doesn't have children, but if browseMode is
// collapsed, then siblings may need to be collapsed
    if (cat.firstChild.firstChild.tagName.toLowerCase() != 'a') {
        collapseSiblings();
        return;
    }

// Collapse/Expand a category
    var toggle = function (c) {
        var collapse = c.childNodes[1].style.display == 'none';
        if (state == me.EXPAND)
            collapse = 1;
        else if (state == me.COLLAPSE)
            collapse = 0;
        c.firstChild.firstChild.firstChild.src = me.config.imageURL + (collapse ? '/collapse.gif' : '/expand.gif');
        c.firstChild.firstChild.firstChild.alt = collapse ? '[-]' : '[+]';
        c.firstChild.firstChild.firstChild.title = collapse ? me.lang.collapse : me.lang.expand;
        c.childNodes[1].style.display = collapse ? '' : 'none';
    };

    var toggleChildren = function () {
// Collapse any open categories on the same level
        collapseSiblings();
        toggle(cat);
    };

    var toggleLinks = function () {
        if (me.config.selectionMode != 'link')
            return;

        var links = cat.childNodes[1].lastChild;
        if (!links)
            return;

// Hide the previous category's links if a different category has been selected
        if (me.links && (me.links != links || links.className != 'treecats-links')) {
            me.links.style.display = 'none';
// There are only links in this category, so the category needs to be collapsed
            if (me.links.parentNode.childNodes.length == 1 && me.links.parentNode.style.display != 'none')
                toggle(me.links.parentNode.parentNode);
        }

        if (links.className == 'treecats-links') {
            me.links = links;
            links.style.display = '';
        }
        else
            me.links = null;
    };

// The category has already been fetched
    if (cat.childNodes[1].hasChildNodes() || state == this.COLLAPSE) {
// In link mode, only one category's links are shown at a time, so selecting a
// category that is already expanded shouldn't always collapse it.
// Also, in link mode, a category that has no links should collapse immediately.
        if (this.config.selectionMode != 'link' || cat.childNodes[1].style.display == 'none' || state == this.COLLAPSE ||
            (this.links && this.links == cat.childNodes[1].lastChild) ||
            cat.childNodes[1].lastChild.className != 'treecats-links')
            toggleChildren();
        toggleLinks();
        this._resize();
    }
    else {
        cat.firstChild.firstChild.firstChild.src = this.config.imageURL + '/loading.gif';
        cat.firstChild.firstChild.firstChild.title = this.lang.loading;
        this._asyncReq(function (me, req) {
            toggleChildren();
            me._treeHandler(me, req);
            toggleLinks();
        }, this.config.cgiURL + '/treecats.cgi?id=' + id + (this.config.selectionMode == 'link' ? ';links=1' : '') + (this.config.cgiQueryString ? ';' + this.config.cgiQueryString : ''));
    }
}

// Traverse the category tree.
// dir: 0 = just that one (this.SINGLE), 1 = down (this.DOWN), 2 = up (this.UP)
treecats.prototype._traverse = function (id, func, dir) {
// When selectionRequired is true, there is no category with an ID of 0
    if (id == 0 && this.config.selectionRequired) {
        if (dir == this.UP)
            return;

        for (var i = 0; i < this.objects.tree.childNodes.length; i++)
            this._traverse(this._getId(this.objects.tree.childNodes[i]), func, dir);
        return;
    }

    var cat = document.getElementById(this._id(id));
    if (!cat)
        return;

    func(cat, id);
// Traverse down the tree
    if (dir == 1) {
        for (var i = 0; i < cat.lastChild.childNodes.length; i++) {
            if (cat.lastChild.childNodes[i].className.match(/treecats-category/))
                this._traverse(this._getId(cat.lastChild.childNodes[i]), func, dir);
        }
    }
// Traverse up the tree
    else if (dir == 2) {
        if (cat.parentNode.className.match(/treecats-children/))
            this._traverse(this._getId(cat.parentNode.parentNode), func, dir);
    }
}

// Expand categories from the category up to the root.  Pass in true for
// inclusive if you wish to expand the category itself.
treecats.prototype.expandTo = function (id, inclusive) {
    if (id == 0 || id == undefined)
        return;

    var start;
    if (inclusive)
        start = id;
    else {
        var cat = document.getElementById(this._id(id));
        if (!cat || !cat.parentNode.className.match(/treecats-children/))
            return;
        start = this._getId(cat.parentNode.parentNode);
    }

// Expand the categories from the category's parent and up
    var me = this;
    this._traverse(start, function (cat, id) { me.navigate(id, me.EXPAND); }, this.UP);
// Since we traverse up the tree, the side effect is that in link mode, the
// root node ends up having its links shown.
    if (this.config.selectionMode == 'link')
        this.navigate(id, this.EXPAND);
}

// Collapse all categories
treecats.prototype.collapseAll = function () {
    var me = this;
    this._traverse(0, function (cat, id) { me.navigate(id, me.COLLAPSE); }, this.DOWN);
}

// Add a new category selection item
treecats.prototype.add = function () {
    if (this.config.selectionMode != 'multiple')
        return;

    var sid = this._createSelection();
// Automatically go into selection mode
    this.toggle(sid);
}

// Remove a category selection item that has the supplied id
treecats.prototype.remove = function (id) {
// Don't allow the removal of the last item
    if (this.objects.selectionList.childNodes.length <= 1)
        return;

    var selection = document.getElementById(this._sid(id));
    if (!selection)
        return;
    this.objects.selectionList.removeChild(selection);

// Hide the Remove link
    if (this.objects.selectionList.childNodes.length == 1)
        this.objects.selectionList.firstChild.childNodes[3].style.display = 'none';

    this._updateListAdd();
}

// Create a new category selection item.  If no title is specified, then
// this.lang.noSelection[Link] will be used.
treecats.prototype._createSelection = function (title, value) {
    if (!title)
        title = this.config.selectionRequired ? (this.config.selectionMode != 'link' ? this.lang.noSelection : this.lang.noSelectionLink) : this.lang.rootText;

    var selection = document.createElement('li');

// Find an unused selection item ID
    var sid = 1;
    while (document.getElementById(this._sid(sid)))
        sid++;
    selection.id = this._sid(sid);

    var input = document.createElement('input');
    input.type = 'hidden';
    input.name = this.config.inputName;
    input.value = value > 0 ? value : '';
    selection.appendChild(input);

    var span = document.createElement('span');
    span.appendChild(document.createTextNode(title));
    selection.appendChild(span);

    var change = document.createElement('a');
    change.href = 'javascript:' + this.config.objName + '.toggle(' + sid + ')';
    change.className = 'treecats-selection-change';
    change.appendChild(document.createTextNode(this.lang.change));
    selection.appendChild(change);

    var remove = document.createElement('a');
    remove.href = 'javascript:' + this.config.objName + '.remove(' + sid + ')';
    remove.className = 'treecats-selection-remove';
    if (this.objects.selectionList.childNodes.length == 0)
        remove.style.display = 'none';
    remove.appendChild(document.createTextNode(this.lang.remove));
    selection.appendChild(remove);

    var done = document.createElement('a');
    done.href = 'javascript:' + this.config.objName + '.toggle(' + sid + ')';
    done.className = 'treecats-selection-done';
    done.style.display = 'none';
    done.appendChild(document.createTextNode(this.lang.done));
    selection.appendChild(done);

    var cancel = document.createElement('a');
    cancel.href = 'javascript:' + this.config.objName + '.toggle(' + sid + ',1)';
    cancel.className = 'treecats-selection-cancel';
    cancel.style.display = 'none';
    cancel.appendChild(document.createTextNode(this.lang.cancel));
    selection.appendChild(cancel);

    this.objects.selectionList.appendChild(selection);

// Show the Remove link on all selections
    if (this.objects.selectionList.childNodes.length > 1) {
        for (var i = 0; i < this.objects.selectionList.childNodes.length; i++)
            this.objects.selectionList.childNodes[i].childNodes[3].style.display = '';
    }

    return sid;
}

// Update the current category selection.  If no selectionItem is selected, a
// new selectionItem will be created.
treecats.prototype._updateSelection = function (title, value) {
    if (!title)
        title = this.config.selectionRequired ? (this.config.selectionMode != 'link' ? this.lang.noSelection : this.lang.noSelectionLink) : this.lang.rootText;
// When pre-selecting categories/links (this.currentSelectionItem == 0), the
// selection items need to be created.
    if (this.currentSelectionItem > 0)
        var selected = document.getElementById(this._sid(this.currentSelectionItem));
    else
        var selected = document.getElementById(this._sid(this._createSelection()));
    selected.firstChild.value = value > 0 ? value : '';
    selected.childNodes[1].replaceChild(document.createTextNode(title), selected.childNodes[1].firstChild);
}

treecats.prototype._treeHandler = function (me, req) {
    var cats = req.responseXML.getElementsByTagName('category');
    var father;
    var children;

    var error = req.responseXML.getElementsByTagName('error');
    if (error.length) {
        alert('treecats error: ' + error[0].firstChild.nodeValue);
        return;
    }

// Create a special root entry if needed
    var root = document.getElementById(me._id(0));
    if (!me.config.selectionRequired && !root) {
        var xmldoc = cats[0].ownerDocument;
        var root = xmldoc.createElement('category');
        root.setAttribute('id', 0);
        root.setAttribute('fatherid', -1);
        root.setAttribute('children', -1);
        root.setAttribute('selected', 0);
        root.setAttribute('links', 0);

        var name = xmldoc.createElement('name');
        name.appendChild(xmldoc.createTextNode(me.lang.rootText));
        root.appendChild(name);
        var fname = xmldoc.createElement('fullname');
        fname.appendChild(xmldoc.createTextNode(''));
        root.appendChild(fname);

        cats[0].parentNode.insertBefore(root, cats[0]);
// IE doesn't update the cats array like other browsers do, so refetch results
        cats = cats[0].parentNode.getElementsByTagName('category');
    }

    for (var i = 0; i < cats.length; i++) {
        var id = cats[i].getAttribute('id');
        var fid = cats[i].getAttribute('fatherid');
        var childcount = parseInt(cats[i].getAttribute('children')) || 0;
        var linkcount = parseInt(cats[i].getAttribute('links')) || 0;
        var selected = cats[i].getAttribute('selected') > 0 ? true : false;

        var expandable = childcount;
        if (me.config.selectionMode == 'link') {
            expandable += linkcount;
        }

        if (document.getElementById(me._id(id)))
            continue;

        var cat = document.createElement('div');
        cat.id = me._id(id);
        cat.className = 'treecats-category';

        var cat_info = document.createElement('div');
        cat_info.className = 'treecats-category-info';
        cat.appendChild(cat_info);

        var img = document.createElement('img');
        img.src = me.config.imageURL + (expandable > 0 ? '/expand.gif' : expandable < 0 ? '/root.gif' : '/blank.gif');

        if (expandable > 0) {
            img.alt = '[+]';
            img.title = me.lang.expand;

            var img_link = document.createElement('a');
            img_link.href = 'javascript:' + me.config.objName + '.navigate(' + id + ')';
            img_link.appendChild(img);
            cat_info.appendChild(img_link);
        }
        else
            cat_info.appendChild(img);

        var cat_span = document.createElement('span');
        cat_span.title = cats[i].getElementsByTagName('fullname')[0].firstChild.nodeValue;
        cat_info.appendChild(cat_span);
        var cat_name = document.createTextNode(cats[i].getElementsByTagName('name')[0].firstChild.nodeValue);
// If we're in link selectionMode, then only make the Category linkable if it
// has Links or sub-categories.  Also navigate() instead of select().
        if ((me.config.selectionMode == 'link' && expandable) || me.config.selectionMode != 'link') {
            var cat_link = document.createElement('a');
            cat_link.href = 'javascript:' + me.config.objName + (me.config.selectionMode != 'link' ? '.select' : '.navigate') + '(' + id + ')';
            cat_link.appendChild(cat_name);
            cat_span.appendChild(cat_link);
        }
        else
            cat_span.appendChild(cat_name);

        var cat_child = document.createElement('div');
        cat_child.className = 'treecats-children';
        if (id != 0)
            cat_child.style.display = 'none';
        cat.appendChild(cat_child);

        if (!father || father.id != me._id(fid)) {
            if ((fid == 0 && me.config.selectionRequired) || (id == 0 && !me.config.selectionRequired))
                father = me.objects.tree;
            else
                father = document.getElementById(me._id(fid));
            if (!father) {
                alert('treecats error: Father id (' + fid + ') hasn\'t been created yet!');
                continue;
            }
            if ((fid == 0 && me.config.selectionRequired) || (id == 0 && !me.config.selectionRequired))
                children = father;
            else
                children = father.childNodes[1];
        }
        children.appendChild(cat);

        if (selected && me.config.selectionMode != 'link')
            me._updateSelection(cats[i].getElementsByTagName('fullname')[0].firstChild.nodeValue, id);
    }

    if (me.config.selectionMode == 'link') {
        var links = req.responseXML.getElementsByTagName('link');
        if (links.length) {
            for (var i = 0; i < links.length; i++) {
                var linkid = links[i].getAttribute('id');
                var catid = links[i].getAttribute('catid');
                var selected = links[i].getAttribute('selected') > 0 ? true : false;

                var cat = document.getElementById(me._id(catid));
                var cat_child = cat.lastChild;

                var ul = cat_child.lastChild;
                if (!ul || !ul.className.match(/treecats-links/)) {
                    ul = document.createElement('ul');
                    ul.className = 'treecats-links';
                    ul.style.display = 'none';
                    cat_child.appendChild(ul);
                }

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

// Give links which are in multiple categories a different ID
                var ext = '';
                var count = 0;
                var dupe;
                while (dupe = document.getElementById(me._lid(linkid + ext))) {
// Select this link if it's already selected (from another category)
                    if (li.className != dupe.className)
                        li.className = dupe.className;
                    count++;
                    ext = '-' + count;
                }

                li.id = me._lid(linkid + ext);
                var link = document.createElement('a');
                link.href = 'javascript:' + me.config.objName + '.selectLink(' + linkid + ')';
                link.title = links[i].getElementsByTagName('url')[0].firstChild.nodeValue;
                link.appendChild(document.createTextNode(links[i].getElementsByTagName('name')[0].firstChild.nodeValue));
                li.appendChild(link);
                ul.appendChild(li);

// Check that the link hasn't already been added
                if (selected && !me.linkToCategory[linkid])
                    me._updateSelection(links[i].getElementsByTagName('name')[0].firstChild.nodeValue, linkid);

// Save where the link is so we can expand to the category later on
                me.linkToCategory[linkid] = catid;
            }
        }
    }
    me._updateListAdd();
    me._resize();
}

// Update the visibility of the Add link
treecats.prototype._updateListAdd = function () {
    if (this.config.selectionMode != 'multiple')
        return;

    if (this.config.multipleMax > 0 && this.objects.selectionList.childNodes.length >= this.config.multipleMax)
        this.objects.selectionListAdd.style.display = 'none';
    else
        this.objects.selectionListAdd.style.display = (this.objects.tree.style.display == 'none' && this.objects.selectionList.lastChild.firstChild.value > 0) ? '' : 'none';
}

// Check to see if scrollbars need to be added or removed
treecats.prototype._resize = function () {
    if (this.config.maxHeight > 0) {
        if (this.objects.tree.scrollHeight > this.config.maxHeight) {
            this.objects.tree.style.height = this.config.maxHeight + 'px';
            this.objects.tree.style.overflow = 'scroll';
        }
        else {
            this.objects.tree.style.height = '';
            this.objects.tree.style.overflow = '';
        }
    }
}

// Get the ID from an object
treecats.prototype._getId = function (obj) {
    return obj.id.replace(/^.*?(\d+)$/, '$1');
}

// Create a Category HTML ID
treecats.prototype._id = function (id) {
    return this.config.objName + '-c' + id;
}

// Create a Link HTML ID
treecats.prototype._lid = function (id) {
    return this.config.objName + '-l' + id;
}

// Create a Selection HTML ID
treecats.prototype._sid = function (id) {
    return this.config.objName + '-s' + id;
}

// Perfom an asynchronous request
treecats.prototype._asyncReq = function (handler, url, post_data) {
    var req;
    if (window.XMLHttpRequest)
        req = new XMLHttpRequest();
    else if (window.ActiveXObject) {
        try {
            req = new ActiveXObject("Microsoft.XMLHTTP");
        }
        catch (e) {
            try {
                req = new ActiveXObject("Msxml2.XMLHTTP");
            }
            catch (e) {}
        }
    }

    if (!req) {
        alert('treecats error: Your browser does not support XMLHttpRequest');
        return;
    }

    var me = this;
    req.onreadystatechange = function (e) {
        if (req.readyState == 4) {
            if (req.status == 200)
                handler(me, req);
            else
                alert('treecats error: A ' + req.status + " error occured while retrieving the XML data (" + url + "):\n" + req.statusText);
        }
    };

    try {
        req.open(post_data ? "POST" : "GET", url, true);
    }
    catch (e) {
        alert('treecats error: ' + e + ".\nPerhaps cgiURL does not match the current URL.");
        return;
    }

    if (post_data)
        req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    req.send(post_data);
}
