# Menu.js

    /**
     * @class Ext.menu.Menu
     * @extends Ext.util.Observable
     * A menu object.  This is the container to which you add all other menu items.  Menu can also serve a as a base class
     * when you want a specialzed menu based off of another component (like {@link Ext.menu.DateMenu} for example).
     * @constructor
     * Creates a new Menu
     * @param {Object} config Configuration options
     */
    Ext.menu.Menu = function(config){
        Ext.apply(this, config);
        this.id = this.id || Ext.id();
        this.addEvents({
            /**
             * @event beforeshow
             * Fires before this menu is displayed
             * @param {Ext.menu.Menu} this
             */
            beforeshow : true,
            /**
             * @event beforehide
             * Fires before this menu is hidden
             * @param {Ext.menu.Menu} this
             */
            beforehide : true,
            /**
             * @event show
             * Fires after this menu is displayed
             * @param {Ext.menu.Menu} this
             */
            show : true,
            /**
             * @event hide
             * Fires after this menu is hidden
             * @param {Ext.menu.Menu} this
             */
            hide : true,
            /**
             * @event click
             * Fires when this menu is clicked (or when the enter key is pressed while it is active)
             * @param {Ext.menu.Menu} this
             * @param {Ext.menu.Item} menuItem The menu item that was clicked
             * @param {Ext.EventObject} e
             */
            click : true,
            /**
             * @event mouseover
             * Fires when the mouse is hovering over this menu
             * @param {Ext.menu.Menu} this
             * @param {Ext.EventObject} e
             * @param {Ext.menu.Item} menuItem The menu item that was clicked
             */
            mouseover : true,
            /**
             * @event mouseout
             * Fires when the mouse exits this menu
             * @param {Ext.menu.Menu} this
             * @param {Ext.EventObject} e
             * @param {Ext.menu.Item} menuItem The menu item that was clicked
             */
            mouseout : true,
            /**
             * @event itemclick
             * Fires when a menu item contained in this menu is clicked
             * @param {Ext.menu.BaseItem} baseItem The BaseItem that was clicked
             * @param {Ext.EventObject} e
             */
            itemclick: true
        });
        Ext.menu.MenuMgr.register(this);
        var mis = this.items;
        this.items = new Ext.util.MixedCollection();
        if(mis){
            this.add.apply(this, mis);
        }
    };

    Ext.extend(Ext.menu.Menu, Ext.util.Observable, {
        /**
         * @cfg {Number} minWidth The minimum width of the menu in pixels (defaults to 120)
         */
        minWidth : 120,
        /**
         * @cfg {Boolean/String} shadow True or "sides" for the default effect, "frame" for 4-way shadow, and "drop"
         * for bottom-right shadow (defaults to "sides")
         */
        shadow : "sides",
        /**
         * @cfg {String} subMenuAlign The {@link Ext.Element#alignTo} anchor position value to use for submenus of
         * this menu (defaults to "tl-tr?")
         */
        subMenuAlign : "tl-tr?",
        /**
         * @cfg {String} defaultAlign The default {@link Ext.Element#alignTo) anchor position value for this menu
         * relative to its element of origin (defaults to "tl-bl?")
         */
        defaultAlign : "tl-bl?",
        /**
         * @cfg {Boolean} allowOtherMenus True to allow multiple menus to be displayed at the same time (defaults to false)
         */
        allowOtherMenus : false,

        hidden:true,

        // private
        render : function(){
            if(this.el){
                return;
            }
            var el = this.el = new Ext.Layer({
                cls: "x-menu",
                shadow:this.shadow,
                constrain: false,
                parentEl: this.parentEl || document.body,
                zindex:15000
            });

            this.keyNav = new Ext.menu.MenuNav(this);

            if(this.plain){
                el.addClass("x-menu-plain");
            }
            if(this.cls){
                el.addClass(this.cls);
            }
            // generic focus element
            this.focusEl = el.createChild({
                tag: "a", cls: "x-menu-focus", href: "#", onclick: "return false;", tabIndex:"-1"
            });
            var ul = el.createChild({tag: "ul", cls: "x-menu-list"});
            ul.on("click", this.onClick, this);
            ul.on("mouseover", this.onMouseOver, this);
            ul.on("mouseout", this.onMouseOut, this);
            this.items.each(function(item){
                var li = document.createElement("li");
                li.className = "x-menu-list-item";
                ul.dom.appendChild(li);
                item.render(li, this);
            }, this);
            this.ul = ul;
            this.autoWidth();
        },

        // private
        autoWidth : function(){
            var el = this.el, ul = this.ul;
            if(!el){
                return;
            }
            var w = this.width;
            if(w){
                el.setWidth(w);
            }else if(Ext.isIE){
                el.setWidth(this.minWidth);
                var t = el.dom.offsetWidth; // force recalc
                el.setWidth(ul.getWidth()+el.getFrameWidth("lr"));
            }
        },

        // private
        delayAutoWidth : function(){
            if(this.rendered){
                if(!this.awTask){
                    this.awTask = new Ext.util.DelayedTask(this.autoWidth, this);
                }
                this.awTask.delay(20);
            }
        },

        // private
        findTargetItem : function(e){
            var t = e.getTarget(".x-menu-list-item", this.ul,  true);
            if(t && t.menuItemId){
                return this.items.get(t.menuItemId);
            }
        },

        // private
        onClick : function(e){
            var t;
            if(t = this.findTargetItem(e)){
                t.onClick(e);
                this.fireEvent("click", this, t, e);
            }
        },

        // private
        setActiveItem : function(item, autoExpand){
            if(item != this.activeItem){
                if(this.activeItem){
                    this.activeItem.deactivate();
                }
                this.activeItem = item;
                item.activate(autoExpand);
            }else if(autoExpand){
                item.expandMenu();
            }
        },

        // private
        tryActivate : function(start, step){
            var items = this.items;
            for(var i = start, len = items.length; i >= 0 && i < len; i+= step){
                var item = items.get(i);
                if(!item.disabled && item.canActivate){
                    this.setActiveItem(item, false);
                    return item;
                }
            }
            return false;
        },

        // private
        onMouseOver : function(e){
            var t;
            if(t = this.findTargetItem(e)){
                if(t.canActivate && !t.disabled){
                    this.setActiveItem(t, true);
                }
            }
            this.fireEvent("mouseover", this, e, t);
        },

        // private
        onMouseOut : function(e){
            var t;
            if(t = this.findTargetItem(e)){
                if(t == this.activeItem && t.shouldDeactivate(e)){
                    this.activeItem.deactivate();
                    delete this.activeItem;
                }
            }
            this.fireEvent("mouseout", this, e, t);
        },

        /**
         * Read-only.  Returns true if the menu is currently displayed, else false.
         * @type Boolean
         */
        isVisible : function(){
            return this.el && !this.hidden;
        },

        /**
         * Displays this menu relative to another element
         * @param {String/HTMLElement/Ext.Element} element The element to align to
         * @param {String} position (optional) The {@link Ext.Element#alignTo} anchor position to use in aligning to
         * the element (defaults to this.defaultAlign)
         * @param {Ext.menu.Menu} parentMenu (optional) This menu's parent menu, if applicable (defaults to undefined)
         */
        show : function(el, pos, parentMenu){
            this.parentMenu = parentMenu;
            if(!this.el){
                this.render();
            }
            this.fireEvent("beforeshow", this);
            this.showAt(this.el.getAlignToXY(el, pos || this.defaultAlign), parentMenu, false);
        },

        /**
         * Displays this menu at a specific xy position
         * @param {Array} xyPosition Contains X & Y [x, y] values for the position at which to show the menu (coordinates are page-based)
         * @param {Ext.menu.Menu} parentMenu (optional) This menu's parent menu, if applicable (defaults to undefined)
         */
        showAt : function(xy, parentMenu, /* private: */_e){
            this.parentMenu = parentMenu;
            if(!this.el){
                this.render();
            }
            if(_e !== false){
                this.fireEvent("beforeshow", this);
                xy = this.el.adjustForConstraints(xy);
            }
            this.el.setXY(xy);
            this.el.show();
            this.hidden = false;
            this.focus();
            this.fireEvent("show", this);
        },

        focus : function(){
            if(!this.hidden){
                this.doFocus.defer(50, this);
            }
        },

        doFocus : function(){
            if(!this.hidden){
                this.focusEl.focus();
            }
        },

        /**
         * Hides this menu and optionally all parent menus
         * @param {Boolean} deep (optional) True to hide all parent menus recursively, if any (defaults to false)
         */
        hide : function(deep){
            if(this.el && this.isVisible()){
                this.fireEvent("beforehide", this);
                if(this.activeItem){
                    this.activeItem.deactivate();
                    this.activeItem = null;
                }
                this.el.hide();
                this.hidden = true;
                this.fireEvent("hide", this);
            }
            if(deep === true && this.parentMenu){
                this.parentMenu.hide(true);
            }
        },

        /**
         * Addds one or more items of any type supported by the Menu class, or that can be converted into menu items.
         * Any of the following are valid:
         * <ul>
         * <li>Any menu item object based on {@link Ext.menu.Item}</li>
         * <li>An HTMLElement object which will be converted to a menu item</li>
         * <li>A menu item config object that will be created as a new menu item</li>
         * <li>A string, which can either be '-' or 'separator' to add a menu separator, otherwise
         * it will be converted into a {@link Ext.menu.TextItem} and added</li>
         * </ul>
         * Usage:
         * <pre><code>
    // Create the menu
    var menu = new Ext.menu.Menu();

    // Create a menu item to add by reference
    var menuItem = new Ext.menu.Item({ text: 'New Item!' });

    // Add a bunch of items at once using different methods.
    // Only the last item added will be returned.
    var item = menu.add(
        menuItem,                // add existing item by ref
        'Dynamic Item',          // new TextItem
        '-',                     // new separator
        { text: 'Config Item' }  // new item by config
    );
    </code></pre>
         * @param {Mixed} args One or more menu items, menu item configs or other objects that can be converted to menu items
         * @return {Ext.menu.Item} The menu item that was added, or the last one if multiple items were added
         */
        add : function(){
            var a = arguments, l = a.length, item;
            for(var i = 0; i < l; i++){
                var el = a[i];
                if(el.render){ // some kind of Item
                    item = this.addItem(el);
                }else if(typeof el == "string"){ // string
                    if(el == "separator" || el == "-"){
                        item = this.addSeparator();
                    }else{
                        item = this.addText(el);
                    }
                }else if(el.tagName || el.el){ // element
                    item = this.addElement(el);
                }else if(typeof el == "object"){ // must be menu item config?
                    item = this.addMenuItem(el);
                }
            }
            return item;
        },

        /**
         * Returns this menu's underlying {@link Ext.Element} object
         * @return {Ext.Element} The element
         */
        getEl : function(){
            if(!this.el){
                this.render();
            }
            return this.el;
        },

        /**
         * Adds a separator bar to the menu
         * @return {Ext.menu.Item} The menu item that was added
         */
        addSeparator : function(){
            return this.addItem(new Ext.menu.Separator());
        },

        /**
         * Adds an {@link Ext.Element} object to the menu
         * @param {String/HTMLElement/Ext.Element} el The element or DOM node to add, or its id
         * @return {Ext.menu.Item} The menu item that was added
         */
        addElement : function(el){
            return this.addItem(new Ext.menu.BaseItem(el));
        },

        /**
         * Adds an existing object based on {@link Ext.menu.Item} to the menu
         * @param {Ext.menu.Item} item The menu item to add
         * @return {Ext.menu.Item} The menu item that was added
         */
        addItem : function(item){
            this.items.add(item);
            if(this.ul){
                var li = document.createElement("li");
                li.className = "x-menu-list-item";
                this.ul.dom.appendChild(li);
                item.render(li, this);
                this.delayAutoWidth();
            }
            return item;
        },

        /**
         * Creates a new {@link Ext.menu.Item} based an the supplied config object and adds it to the menu
         * @param {Object} config A MenuItem config object
         * @return {Ext.menu.Item} The menu item that was added
         */
        addMenuItem : function(config){
            if(!(config instanceof Ext.menu.Item)){
                if(typeof config.checked == "boolean"){ // must be check menu item config?
                    config = new Ext.menu.CheckItem(config);
                }else{
                    config = new Ext.menu.Item(config);
                }
            }
            return this.addItem(config);
        },

        /**
         * Creates a new {@link Ext.menu.TextItem} with the supplied text and adds it to the menu
         * @param {String} text The text to display in the menu item
         * @return {Ext.menu.Item} The menu item that was added
         */
        addText : function(text){
            return this.addItem(new Ext.menu.TextItem(text));
        },

        /**
         * Inserts an existing object based on {@link Ext.menu.Item} to the menu at a specified index
         * @param {Number} index The index in the menu's list of current items where the new item should be inserted
         * @param {Ext.menu.Item} item The menu item to add
         * @return {Ext.menu.Item} The menu item that was added
         */
        insert : function(index, item){
            this.items.insert(index, item);
            if(this.ul){
                var li = document.createElement("li");
                li.className = "x-menu-list-item";
                this.ul.dom.insertBefore(li, this.ul.dom.childNodes[index]);
                item.render(li, this);
                this.delayAutoWidth();
            }
            return item;
        },

        /**
         * Removes an {@link Ext.menu.Item} from the menu and destroys the object
         * @param {Ext.menu.Item} item The menu item to remove
         */
        remove : function(item){
            this.items.removeKey(item.id);
            item.destroy();
        },

        /**
         * Removes and destroys all items in the menu
         */
        removeAll : function(){
            var f;
            while(f = this.items.first()){
                this.remove(f);
            }
        }
    });

    // MenuNav is a private utility class used internally by the Menu
    Ext.menu.MenuNav = function(menu){
        Ext.menu.MenuNav.superclass.constructor.call(this, menu.el);
        this.scope = this.menu = menu;
    };

    Ext.extend(Ext.menu.MenuNav, Ext.KeyNav, {
        doRelay : function(e, h){
            var k = e.getKey();
            if(!this.menu.activeItem && e.isNavKeyPress() && k != e.SPACE && k != e.RETURN){
                this.menu.tryActivate(0, 1);
                return false;
            }
            return h.call(this.scope || this, e, this.menu);
        },

        up : function(e, m){
            if(!m.tryActivate(m.items.indexOf(m.activeItem)-1, -1)){
                m.tryActivate(m.items.length-1, -1);
            }
        },

        down : function(e, m){
            if(!m.tryActivate(m.items.indexOf(m.activeItem)+1, 1)){
                m.tryActivate(0, 1);
            }
        },

        right : function(e, m){
            if(m.activeItem){
                m.activeItem.expandMenu(true);
            }
        },

        left : function(e, m){
            m.hide();
            if(m.parentMenu && m.parentMenu.activeItem){
                m.parentMenu.activeItem.activate();
            }
        },

        enter : function(e, m){
            if(m.activeItem){
                e.stopPropagation();
                m.activeItem.onClick(e);
                m.fireEvent("click", this, m.activeItem);
                return true;
            }
        }
    });

Ext - Copyright © 2006-2007 Ext JS, LLC
All rights reserved.