// ---------------------------------------------------------------------
// $Id: tabs.js,v 1.3 2008/06/02 06:16:55 cvsuser Exp $
// ---------------------------------------------------------------------
// Tabs, rollover menus, and toggle groups.
// Requires prototype.js.
// Animated functions require scriptaculous.js?load=effects
// ---------------------------------------------------------------------

// Place to store objects that the convenience functions create.
var dnUIPool = dnUIPool || new Array;

// ---------------------------------------------------------------------
// Element groups. Creates a convient way of adding many observers to
// a group of elements.

var dnElementGroup = Class.create({
    initialize: function (elems) {
        this.elems = $A(elems);
    },
    
    addObservers: function (obs) {
        var group = this;
        $H(obs).each(function (ob) {
            group.elems.each(function (elem) {
                if (elem.tagName.toLowerCase() == "a" && elem.href)
                    elem.onclick = function () { return false; };
                elem.observe(ob.key, ob.value);
            });
        });
        
        return this;
    }
});

// ---------------------------------------------------------------------
// Create a two way mapping from elements in one element group to another.

var dnElementMap = Class.create({
    initialize: function (srcElems, destElems) {
        this.srcGroup = srcElems.elems ? srcElems : new dnElementGroup(srcElems);
        this.destGroup = destElems.elems ? destElems : new dnElementGroup(destElems);
        this.map = $H();
        
        var elemMap = this;
        this.srcGroup.elems.each(function (srcElem, i) {
            if (!elemMap.destGroup.elems[i]) return;
            elemMap.map.set($(srcElem).identify(), elemMap.destGroup.elems[i]);
        });
        this.destGroup.elems.each(function (destElem, i) {
            if (!elemMap.srcGroup.elems[i]) return;
            elemMap.map.set($(destElem).identify(), elemMap.srcGroup.elems[i]);
        });
    },
    
    getElementFromMap: function (elem) {
        return $(this.map.get($(elem).identify()));
    }
});

// ---------------------------------------------------------------------
// Given an array of elements, constructs an object that keeps track of
// which of the given elements is currently selected and fires two custom
// events: dnRadio:select and dnRadio:unselect on the elements. Note that
// it is up to the caller to fire dnRadio:select.

var dnRadioGroup = Class.create(dnElementGroup, {
    initialize: function ($super, elems, initial) {
        $super(elems);
        
        this.selClassName;
        initial = initial || 0;
        this.current = this.elems[initial];

        var radioGroup = this;
        this.addObservers({
            "dnRadio:select": function (e) {
                var prev = radioGroup.current;
                var current = radioGroup.current = this;
                if (prev != current) $(prev).fire("dnRadio:unselect");
            }
        });
    },
    
    selectIndex: function (index) {
        $(this.elems[index]).fire("dnRadio:select");
    }
});

// ---------------------------------------------------------------------
// A tab panel controller. Fires "dnTab:select" and "dnTab:unselect" on
// tab elements; "dnPanel:show" and "dnPanel:hide" on the panel elements.
//
// Contstructor arguments:
//     1. tabElems: the list of tab elements
//     2. panelElems: the list of panel elements
//     3. initial: index of the tab that is initially showing in the code
//        default is 0.
//     4. panelObservers: object containing event name to event handler
//        function mapping. You'll want to at least specify "dnPanel:show"
//        and "dnPanel:hide". Defaults to providing simple handlers that
//        toggle the panels' display style property.

var dnTabGroup = Class.create(dnElementMap, {
    initialize: function ($super, tabElems, panelElems, initial, panelObservers) {
        var radioGroup = new dnRadioGroup(tabElems, initial);
        $super(radioGroup, panelElems);
        
        radioGroup.addObservers({
            "click": function (e) {
                $(this).fire("dnRadio:select");
            },
            "dnRadio:select": function (e) {
                $(this).fire("dnTab:select");
            },
            "dnRadio:unselect": function (e) {
                $(this).fire("dnTab:unselect");
            }
        });
        
        var tabGroup = this;
        this.getTabGroup().addObservers({
            "dnTab:select": function (e) {
                tabGroup.getElementFromMap(this).fire("dnPanel:show");
            },
            "dnTab:unselect": function (e) {
                tabGroup.getElementFromMap(this).fire("dnPanel:hide");
            }
        });
        
        this.getPanelGroup().addObservers(panelObservers || {
            "dnPanel:show": function (e) { this.style.display = "block"; },
            "dnPanel:hide": function (e) { this.style.display = "none"; }
        })
    },
    getTabGroup: function () { return this.srcGroup; },
    getPanelGroup: function () { return this.destGroup; },
    selectIndex: function (index) {
        this.getTabGroup().selectIndex(index);
    }
});

// Convenience tab setup function. Adds support for a class to add to the 
// currently active tab, and an unselected class for old implementations.
// New implementations should just default to looking unselected.
function dnSetupTabGroup (tabElems, panelElems, tabSelClass, tabUnselClass, initial) {
    initial = initial || 0;
    var tabs = new dnTabGroup(tabElems, panelElems, initial);
    
    if (tabSelClass || tabUnselClass) {
        tabs.getTabGroup().addObservers({
            "dnTab:select": function (e) {
                if (tabSelClass) $(this).addClassName(tabSelClass);
                if (tabUnselClass) $(this).removeClassName(tabUnselClass);
            },
            "dnTab:unselect": function (e) {
                if (tabSelClass) $(this).removeClassName(tabSelClass);
                if (tabUnselClass) $(this).addClassName(tabUnselClass);
            }
        });
    }
    
    if (tabUnselClass) tabs.getTabGroup().elems.invoke("addClassName", tabUnselClass);
    tabs.selectIndex(initial);
    dnUIPool.push(tabs);
    return tabs;
};

// ---------------------------------------------------------------------
// Rollover menus. Ensure that the menu panels elements are touching their
// corresponding menu bar element. Fires "dnMenu:activate" and
// "dnMenu:deactivate" on menu bar elements; "dnPanel:show" and "dnPanel:hide"
// on the menu panel elements.
// 
// Constructor arguments:
//     1. menuBarElements: the elements that when moused-over cause the menu
//        panel to be shown.
//     2. panelElems: the panels that are revealed.
//     3. panelObservers: Event name to event handler mapping, defaults to
//        standard toggling of panels display style property.

var dnRolloverMenu = Class.create(dnElementMap, {
    initialize: function ($super, menuBarElems, panelElems, panelObservers) {
        var menuBarGroup = new dnElementGroup(menuBarElems);
        $super(menuBarElems, panelElems);
        
        var menu = this;
        this.getMenuBarGroup().addObservers({
            "mouseover": function (e) {
                if (!this.dnMenuActive) $(this).fire("dnMenu:activate");
            },
            "dnMenu:activate": function (e) {
                this.dnMenuActive = true;
                menu.getElementFromMap(this).fire("dnPanel:show");
            },
            "mouseout": function (e) {
                var relatedElem = e.relatedTarget || e.toElement;
                var panel = menu.getElementFromMap(this);
                if (!relatedElem || relatedElem != panel && !$(relatedElem).descendantOf(panel))
                    $(this).fire("dnMenu:deactivate");
            },
            "dnMenu:deactivate": function (e) {
                this.dnMenuActive = false;
                menu.getElementFromMap(this).fire("dnPanel:hide");
            }
        });
        
        this.getPanelGroup().addObservers({
            "mouseout": function (e) {
                var relatedElem = e.relatedElement || e.toElement;
                var menuBarElem = menu.getElementFromMap(this);
                if (relatedElem != menuBarElem)
                    menuBarElem.fire("dnMenu:deactivate");
            }
        });
        
        this.getPanelGroup().addObservers(panelObservers || {
            "dnPanel:show": function (e) { this.style.display = "block"; },
            "dnPanel:hide": function (e) { this.style.display = "none"; }
        });
    },
    getMenuBarGroup: function () { return this.srcGroup; },
    getPanelGroup: function () { return this.destGroup; }
});

// Convenience function for creating rollover menus. Adds support for a class
// to add to the currently active menu bar item.
function dnSetupRolloverMenu (menuElems, panelElems, menuSelClass, panelObservers) {
    var menu = new dnRolloverMenu(menuElems, panelElems, panelObservers);
    
    if (menuSelClass) {
        menu.getMenuBarGroup().addObservers({
            "dnMenu:activate": function (e) { $(this).addClassName(menuSelClass); },
            "dnMenu:deactivate": function (e) { $(this).removeClassName(menuSelClass); }
        });
    }
    
    dnUIPool.push(menu);
    return menu;
};

// Convenience function fore creating animated rollover menus. Note that the
// width of the panel elements should be explicitly set, and not simply
// inherited from the panel elements ancestors. This will allow Scriptaculous
// to correctly calculate the height of the element when its contents wraps.
function dnSetupRolloverMenuAnimated (menuElems, panelElems, menuSelClass) {
    return dnSetupRolloverMenu(menuElems, panelElems, menuSelClass, {
        "dnPanel:show": function (e) {
            $(this).slideDown({
                duration: 0.2,
                queue: { position: "end", scope: $(this).identify() },
                beforeStart: function (e) { e.element.fire("dnPanel:beforeShow"); },
                afterFinish: function (e) { e.element.fire("dnPanel:afterShow"); }
            });
        },
        "dnPanel:hide": function (e) {
            $(this).slideUp({
                duration: 0.1,
                queue: { position: "end", scope: $(this).identify() },
                beforeStart: function (e) { e.element.fire("dnPanel:beforeHide"); },
                afterFinish: function (e) { e.element.fire("dnPanel:afterHide"); }
            });
        }
    });
};

// ---------------------------------------------------------------------
// Toggle elements that show/hide a panel when clicked.
// 
// Constructor arguments:
//     1. toggleElements: the elements that when clicked cause the panel
//        to be shown or hidden.
//     2. panelElems: the panels that are toggled.
//     3. panelObservers: Event name to event handler mapping, defaults to
//        standard toggling of panels display style property.

var dnToggleGroup = Class.create(dnElementMap, {
    initialize: function ($super, toggleElems, panelElems, panelObservers) {
        $super(toggleElems, panelElems);
        
        var toggles = this;
        this.getToggleGroup().addObservers({
            "click": function (e) {
                if (!this.dnActive) {
                    this.dnActive = true;
                    $(this).fire("dnToggle:activate");
                    toggles.getElementFromMap(this).fire("dnPanel:show");
                }
                else {
                    this.dnActive = false;
                    $(this).fire("dnToggle:deactivate");
                    toggles.getElementFromMap(this).fire("dnPanel:hide");
                }
            }
        });
        
        this.getPanelGroup().addObservers(panelObservers || {
            "dnPanel:show": function (e) { this.style.display = "block"; },
            "dnPanel:hide": function (e) { this.style.display = "none"; }
        });
    },
    getToggleGroup: function () { return this.srcGroup; },
    getPanelGroup: function () { return this.destGroup; }
});

// Convenience function for setting up toggle groups. Adds support for a class
// to add to the currently active toggle element.
function dnSetupToggleGroup (toggleElems, panelElems, toggleSelClass, panelObs) {
    var toggles = new dnToggleGroup(toggleElems, panelElems, panelObs);
    
    if (toggleSelClass) {
        toggles.getToggleGroup().addObservers({
            "dnToggle:activate": function (e) { $(this).addClassName(toggleSelClass); },
            "dnToggle:deactivate": function (e) { $(this).removeClassName(toggleSelClass); }
        });
    }
    
    dnUIPool.push(toggles);
    return toggles;
};

// Convenience function for setting up animated toggle groups. Note that the
// width of the panel elements should be explicitly set, and not simply
// inherited from the panel elements ancestors. This will allow Scriptaculous
// to correctly calculate the height of the element when its contents wraps.
function dnSetupToggleGroupAnimated (toggleElems, panelElems, toggleSelClass) {
    return dnSetupToggleGroup(toggleElems, panelElems, toggleSelClass, {
        "dnPanel:show": function (e) {
            $(this).blindDown({
                duration: 0.3,
                queue: { position: "end", scope: $(this).identify() },
                beforeStart: function (e) { e.element.fire("dnPanel:beforeShow"); },
                afterFinish: function (e) { e.element.fire("dnPanel:afterShow"); }
            });
        },
        "dnPanel:hide": function (e) {
            $(this).blindUp({
                duration: 0.2,
                queue: { position: "end", scope: $(this).identify() },
                beforeStart: function (e) { e.element.fire("dnPanel:beforeHide"); },
                afterFinish: function (e) { e.element.fire("dnPanel:afterHide"); }
            });
        }
    });
};

// ---------------------------------------------------------------------

