commit extjs-2.2.1
[extjs.git] / source / widgets / menu / Menu.js
1 /*\r
2  * Ext JS Library 2.2.1\r
3  * Copyright(c) 2006-2009, Ext JS, LLC.\r
4  * licensing@extjs.com\r
5  * \r
6  * http://extjs.com/license\r
7  */\r
8 \r
9 /**\r
10  * @class Ext.menu.Menu\r
11  * @extends Ext.util.Observable\r
12  * A menu object.  This is the container to which you add all other menu items.  Menu can also serve as a base class\r
13  * when you want a specialized menu based off of another component (like {@link Ext.menu.DateMenu} for example).\r
14  * @constructor\r
15  * Creates a new Menu\r
16  * @param {Object} config Configuration options\r
17  */\r
18 Ext.menu.Menu = function(config){\r
19     if(Ext.isArray(config)){\r
20         config = {items:config};\r
21     }\r
22     Ext.apply(this, config);\r
23     this.id = this.id || Ext.id();\r
24     this.addEvents(\r
25         /**\r
26          * @event beforeshow\r
27          * Fires before this menu is displayed\r
28          * @param {Ext.menu.Menu} this\r
29          */\r
30         'beforeshow',\r
31         /**\r
32          * @event beforehide\r
33          * Fires before this menu is hidden\r
34          * @param {Ext.menu.Menu} this\r
35          */\r
36         'beforehide',\r
37         /**\r
38          * @event show\r
39          * Fires after this menu is displayed\r
40          * @param {Ext.menu.Menu} this\r
41          */\r
42         'show',\r
43         /**\r
44          * @event hide\r
45          * Fires after this menu is hidden\r
46          * @param {Ext.menu.Menu} this\r
47          */\r
48         'hide',\r
49         /**\r
50          * @event click\r
51          * Fires when this menu is clicked (or when the enter key is pressed while it is active)\r
52          * @param {Ext.menu.Menu} this\r
53          * @param {Ext.menu.Item} menuItem The menu item that was clicked\r
54          * @param {Ext.EventObject} e\r
55          */\r
56         'click',\r
57         /**\r
58          * @event mouseover\r
59          * Fires when the mouse is hovering over this menu\r
60          * @param {Ext.menu.Menu} this\r
61          * @param {Ext.EventObject} e\r
62          * @param {Ext.menu.Item} menuItem The menu item that was clicked\r
63          */\r
64         'mouseover',\r
65         /**\r
66          * @event mouseout\r
67          * Fires when the mouse exits this menu\r
68          * @param {Ext.menu.Menu} this\r
69          * @param {Ext.EventObject} e\r
70          * @param {Ext.menu.Item} menuItem The menu item that was clicked\r
71          */\r
72         'mouseout',\r
73         /**\r
74          * @event itemclick\r
75          * Fires when a menu item contained in this menu is clicked\r
76          * @param {Ext.menu.BaseItem} baseItem The BaseItem that was clicked\r
77          * @param {Ext.EventObject} e\r
78          */\r
79         'itemclick'\r
80     );\r
81     Ext.menu.MenuMgr.register(this);\r
82     Ext.menu.Menu.superclass.constructor.call(this);\r
83     var mis = this.items;\r
84     /**\r
85      * A MixedCollection of this Menu's items\r
86      * @property items\r
87      * @type Ext.util.MixedCollection\r
88      */\r
89 \r
90     this.items = new Ext.util.MixedCollection();\r
91     if(mis){\r
92         this.add.apply(this, mis);\r
93     }\r
94 };\r
95 \r
96 Ext.extend(Ext.menu.Menu, Ext.util.Observable, {\r
97     /**\r
98      * @cfg {Object} defaults\r
99      * A config object that will be applied to all items added to this container either via the {@link #items}\r
100      * config or via the {@link #add} method.  The defaults config can contain any number of\r
101      * name/value property pairs to be added to each item, and should be valid for the types of items\r
102      * being added to the menu.\r
103      */\r
104     /**\r
105      * @cfg {Mixed} items\r
106      * An array of items to be added to this menu.  See {@link #add} for a list of valid item types.\r
107      */\r
108     /**\r
109      * @cfg {Number} minWidth The minimum width of the menu in pixels (defaults to 120)\r
110      */\r
111     minWidth : 120,\r
112     /**\r
113      * @cfg {Boolean/String} shadow True or "sides" for the default effect, "frame" for 4-way shadow, and "drop"\r
114      * for bottom-right shadow (defaults to "sides")\r
115      */\r
116     shadow : "sides",\r
117     /**\r
118      * @cfg {String} subMenuAlign The {@link Ext.Element#alignTo} anchor position value to use for submenus of\r
119      * this menu (defaults to "tl-tr?")\r
120      */\r
121     subMenuAlign : "tl-tr?",\r
122     /**\r
123      * @cfg {String} defaultAlign The default {@link Ext.Element#alignTo} anchor position value for this menu\r
124      * relative to its element of origin (defaults to "tl-bl?")\r
125      */\r
126     defaultAlign : "tl-bl?",\r
127     /**\r
128      * @cfg {Boolean} allowOtherMenus True to allow multiple menus to be displayed at the same time (defaults to false)\r
129      */\r
130     allowOtherMenus : false,\r
131     /**\r
132      * @cfg {Boolean} ignoreParentClicks True to ignore clicks on any item in this menu that is a parent item (displays\r
133      * a submenu) so that the submenu is not dismissed when clicking the parent item (defaults to false).\r
134      */\r
135     ignoreParentClicks : false,\r
136 \r
137     // private\r
138     hidden:true,\r
139 \r
140     // private\r
141     createEl : function(){\r
142         return new Ext.Layer({\r
143             cls: "x-menu",\r
144             shadow:this.shadow,\r
145             constrain: false,\r
146             parentEl: this.parentEl || document.body,\r
147             zindex:15000\r
148         });\r
149     },\r
150 \r
151     // private\r
152     render : function(){\r
153         if(this.el){\r
154             return;\r
155         }\r
156         var el = this.el = this.createEl();\r
157 \r
158         if(!this.keyNav){\r
159             this.keyNav = new Ext.menu.MenuNav(this);\r
160         }\r
161         if(this.plain){\r
162             el.addClass("x-menu-plain");\r
163         }\r
164         if(this.cls){\r
165             el.addClass(this.cls);\r
166         }\r
167         // generic focus element\r
168         this.focusEl = el.createChild({\r
169             tag: "a", cls: "x-menu-focus", href: "#", onclick: "return false;", tabIndex:"-1"\r
170         });\r
171         var ul = el.createChild({tag: "ul", cls: "x-menu-list"});\r
172         ul.on("click", this.onClick, this);\r
173         ul.on("mouseover", this.onMouseOver, this);\r
174         ul.on("mouseout", this.onMouseOut, this);\r
175         this.items.each(function(item){\r
176             var li = document.createElement("li");\r
177             li.className = "x-menu-list-item";\r
178             ul.dom.appendChild(li);\r
179             item.render(li, this);\r
180         }, this);\r
181         this.ul = ul;\r
182         this.autoWidth();\r
183     },\r
184 \r
185     // private\r
186     autoWidth : function(){\r
187         var el = this.el, ul = this.ul;\r
188         if(!el){\r
189             return;\r
190         }\r
191         var w = this.width;\r
192         if(w){\r
193             el.setWidth(w);\r
194         }else if(Ext.isIE){\r
195             el.setWidth(this.minWidth);\r
196             var t = el.dom.offsetWidth; // force recalc\r
197             el.setWidth(ul.getWidth()+el.getFrameWidth("lr"));\r
198         }\r
199     },\r
200 \r
201     // private\r
202     delayAutoWidth : function(){\r
203         if(this.el){\r
204             if(!this.awTask){\r
205                 this.awTask = new Ext.util.DelayedTask(this.autoWidth, this);\r
206             }\r
207             this.awTask.delay(20);\r
208         }\r
209     },\r
210 \r
211     // private\r
212     findTargetItem : function(e){\r
213         var t = e.getTarget(".x-menu-list-item", this.ul,  true);\r
214         if(t && t.menuItemId){\r
215             return this.items.get(t.menuItemId);\r
216         }\r
217     },\r
218 \r
219     // private\r
220     onClick : function(e){\r
221         var t;\r
222         if(t = this.findTargetItem(e)){\r
223             if(t.menu && this.ignoreParentClicks){\r
224                 t.expandMenu();\r
225             }else{\r
226                 t.onClick(e);\r
227                 this.fireEvent("click", this, t, e);\r
228             }\r
229         }\r
230     },\r
231 \r
232     // private\r
233     setActiveItem : function(item, autoExpand){\r
234         if(item != this.activeItem){\r
235             if(this.activeItem){\r
236                 this.activeItem.deactivate();\r
237             }\r
238             this.activeItem = item;\r
239             item.activate(autoExpand);\r
240         }else if(autoExpand){\r
241             item.expandMenu();\r
242         }\r
243     },\r
244 \r
245     // private\r
246     tryActivate : function(start, step){\r
247         var items = this.items;\r
248         for(var i = start, len = items.length; i >= 0 && i < len; i+= step){\r
249             var item = items.get(i);\r
250             if(!item.disabled && item.canActivate){\r
251                 this.setActiveItem(item, false);\r
252                 return item;\r
253             }\r
254         }\r
255         return false;\r
256     },\r
257 \r
258     // private\r
259     onMouseOver : function(e){\r
260         var t;\r
261         if(t = this.findTargetItem(e)){\r
262             if(t.canActivate && !t.disabled){\r
263                 this.setActiveItem(t, true);\r
264             }\r
265         }\r
266         this.over = true;\r
267         this.fireEvent("mouseover", this, e, t);\r
268     },\r
269 \r
270     // private\r
271     onMouseOut : function(e){\r
272         var t;\r
273         if(t = this.findTargetItem(e)){\r
274             if(t == this.activeItem && t.shouldDeactivate(e)){\r
275                 this.activeItem.deactivate();\r
276                 delete this.activeItem;\r
277             }\r
278         }\r
279         this.over = false;\r
280         this.fireEvent("mouseout", this, e, t);\r
281     },\r
282 \r
283     /**\r
284      * Read-only.  Returns true if the menu is currently displayed, else false.\r
285      * @type Boolean\r
286      */\r
287     isVisible : function(){\r
288         return this.el && !this.hidden;\r
289     },\r
290 \r
291     /**\r
292      * Displays this menu relative to another element\r
293      * @param {Mixed} element The element to align to\r
294      * @param {String} position (optional) The {@link Ext.Element#alignTo} anchor position to use in aligning to\r
295      * the element (defaults to this.defaultAlign)\r
296      * @param {Ext.menu.Menu} parentMenu (optional) This menu's parent menu, if applicable (defaults to undefined)\r
297      */\r
298     show : function(el, pos, parentMenu){\r
299         this.parentMenu = parentMenu;\r
300         if(!this.el){\r
301             this.render();\r
302         }\r
303         this.fireEvent("beforeshow", this);\r
304         this.showAt(this.el.getAlignToXY(el, pos || this.defaultAlign), parentMenu, false);\r
305     },\r
306 \r
307     /**\r
308      * Displays this menu at a specific xy position\r
309      * @param {Array} xyPosition Contains X & Y [x, y] values for the position at which to show the menu (coordinates are page-based)\r
310      * @param {Ext.menu.Menu} parentMenu (optional) This menu's parent menu, if applicable (defaults to undefined)\r
311      */\r
312     showAt : function(xy, parentMenu, /* private: */_e){\r
313         this.parentMenu = parentMenu;\r
314         if(!this.el){\r
315             this.render();\r
316         }\r
317         if(_e !== false){\r
318             this.fireEvent("beforeshow", this);\r
319             xy = this.el.adjustForConstraints(xy);\r
320         }\r
321         this.el.setXY(xy);\r
322         this.el.show();\r
323         this.hidden = false;\r
324         this.focus();\r
325         this.fireEvent("show", this);\r
326     },\r
327 \r
328 \r
329 \r
330     focus : function(){\r
331         if(!this.hidden){\r
332             this.doFocus.defer(50, this);\r
333         }\r
334     },\r
335 \r
336     doFocus : function(){\r
337         if(!this.hidden){\r
338             this.focusEl.focus();\r
339         }\r
340     },\r
341 \r
342     /**\r
343      * Hides this menu and optionally all parent menus\r
344      * @param {Boolean} deep (optional) True to hide all parent menus recursively, if any (defaults to false)\r
345      */\r
346     hide : function(deep){\r
347         if(this.el && this.isVisible()){\r
348             this.fireEvent("beforehide", this);\r
349             if(this.activeItem){\r
350                 this.activeItem.deactivate();\r
351                 this.activeItem = null;\r
352             }\r
353             this.el.hide();\r
354             this.hidden = true;\r
355             this.fireEvent("hide", this);\r
356         }\r
357         if(deep === true && this.parentMenu){\r
358             this.parentMenu.hide(true);\r
359         }\r
360     },\r
361 \r
362     /**\r
363      * Adds one or more items of any type supported by the Menu class, or that can be converted into menu items.\r
364      * Any of the following are valid:\r
365      * <ul>\r
366      * <li>Any menu item object based on {@link Ext.menu.BaseItem}</li>\r
367      * <li>An HTMLElement object which will be converted to a menu item</li>\r
368      * <li>A menu item config object that will be created as a new menu item</li>\r
369      * <li>A string, which can either be '-' or 'separator' to add a menu separator, otherwise\r
370      * it will be converted into a {@link Ext.menu.TextItem} and added</li>\r
371      * </ul>\r
372      * Usage:\r
373      * <pre><code>\r
374 // Create the menu\r
375 var menu = new Ext.menu.Menu();\r
376 \r
377 // Create a menu item to add by reference\r
378 var menuItem = new Ext.menu.Item({ text: 'New Item!' });\r
379 \r
380 // Add a bunch of items at once using different methods.\r
381 // Only the last item added will be returned.\r
382 var item = menu.add(\r
383     menuItem,                // add existing item by ref\r
384     'Dynamic Item',          // new TextItem\r
385     '-',                     // new separator\r
386     { text: 'Config Item' }  // new item by config\r
387 );\r
388 </code></pre>\r
389      * @param {Mixed} args One or more menu items, menu item configs or other objects that can be converted to menu items\r
390      * @return {Ext.menu.Item} The menu item that was added, or the last one if multiple items were added\r
391      */\r
392     add : function(){\r
393         var a = arguments, l = a.length, item;\r
394         for(var i = 0; i < l; i++){\r
395             var el = a[i];\r
396             if(el.render){ // some kind of Item\r
397                 item = this.addItem(el);\r
398             }else if(typeof el == "string"){ // string\r
399                 if(el == "separator" || el == "-"){\r
400                     item = this.addSeparator();\r
401                 }else{\r
402                     item = this.addText(el);\r
403                 }\r
404             }else if(el.tagName || el.el){ // element\r
405                 item = this.addElement(el);\r
406             }else if(typeof el == "object"){ // must be menu item config?\r
407                 Ext.applyIf(el, this.defaults);\r
408                 item = this.addMenuItem(el);\r
409             }\r
410         }\r
411         return item;\r
412     },\r
413 \r
414     /**\r
415      * Returns this menu's underlying {@link Ext.Element} object\r
416      * @return {Ext.Element} The element\r
417      */\r
418     getEl : function(){\r
419         if(!this.el){\r
420             this.render();\r
421         }\r
422         return this.el;\r
423     },\r
424 \r
425     /**\r
426      * Adds a separator bar to the menu\r
427      * @return {Ext.menu.Item} The menu item that was added\r
428      */\r
429     addSeparator : function(){\r
430         return this.addItem(new Ext.menu.Separator());\r
431     },\r
432 \r
433     /**\r
434      * Adds an {@link Ext.Element} object to the menu\r
435      * @param {Mixed} el The element or DOM node to add, or its id\r
436      * @return {Ext.menu.Item} The menu item that was added\r
437      */\r
438     addElement : function(el){\r
439         return this.addItem(new Ext.menu.BaseItem(el));\r
440     },\r
441 \r
442     /**\r
443      * Adds an existing object based on {@link Ext.menu.BaseItem} to the menu\r
444      * @param {Ext.menu.Item} item The menu item to add\r
445      * @return {Ext.menu.Item} The menu item that was added\r
446      */\r
447     addItem : function(item){\r
448         this.items.add(item);\r
449         if(this.ul){\r
450             var li = document.createElement("li");\r
451             li.className = "x-menu-list-item";\r
452             this.ul.dom.appendChild(li);\r
453             item.render(li, this);\r
454             this.delayAutoWidth();\r
455         }\r
456         return item;\r
457     },\r
458 \r
459     /**\r
460      * Creates a new {@link Ext.menu.Item} based an the supplied config object and adds it to the menu\r
461      * @param {Object} config A MenuItem config object\r
462      * @return {Ext.menu.Item} The menu item that was added\r
463      */\r
464     addMenuItem : function(config){\r
465         if(!(config instanceof Ext.menu.Item)){\r
466             if(typeof config.checked == "boolean"){ // must be check menu item config?\r
467                 config = new Ext.menu.CheckItem(config);\r
468             }else{\r
469                 config = new Ext.menu.Item(config);\r
470             }\r
471         }\r
472         return this.addItem(config);\r
473     },\r
474 \r
475     /**\r
476      * Creates a new {@link Ext.menu.TextItem} with the supplied text and adds it to the menu\r
477      * @param {String} text The text to display in the menu item\r
478      * @return {Ext.menu.Item} The menu item that was added\r
479      */\r
480     addText : function(text){\r
481         return this.addItem(new Ext.menu.TextItem(text));\r
482     },\r
483 \r
484     /**\r
485      * Inserts an existing object based on {@link Ext.menu.BaseItem} to the menu at a specified index\r
486      * @param {Number} index The index in the menu's list of current items where the new item should be inserted\r
487      * @param {Ext.menu.Item} item The menu item to add\r
488      * @return {Ext.menu.Item} The menu item that was added\r
489      */\r
490     insert : function(index, item){\r
491         this.items.insert(index, item);\r
492         if(this.ul){\r
493             var li = document.createElement("li");\r
494             li.className = "x-menu-list-item";\r
495             this.ul.dom.insertBefore(li, this.ul.dom.childNodes[index]);\r
496             item.render(li, this);\r
497             this.delayAutoWidth();\r
498         }\r
499         return item;\r
500     },\r
501 \r
502     /**\r
503      * Removes an {@link Ext.menu.Item} from the menu and destroys the object\r
504      * @param {Ext.menu.Item} item The menu item to remove\r
505      */\r
506     remove : function(item){\r
507         this.items.removeKey(item.id);\r
508         item.destroy();\r
509     },\r
510 \r
511     /**\r
512      * Removes and destroys all items in the menu\r
513      */\r
514     removeAll : function(){\r
515         if(this.items){\r
516                 var f;\r
517                 while(f = this.items.first()){\r
518                     this.remove(f);\r
519                 }\r
520         }\r
521     },\r
522 \r
523     /**\r
524      * Destroys the menu by  unregistering it from {@link Ext.menu.MenuMgr}, purging event listeners,\r
525      * removing all of the menus items, then destroying the underlying {@link Ext.Element}\r
526      */\r
527     destroy : function(){\r
528         this.beforeDestroy();\r
529         Ext.menu.MenuMgr.unregister(this);\r
530         if (this.keyNav) {\r
531                 this.keyNav.disable();\r
532         }\r
533         this.removeAll();\r
534         if (this.ul) {\r
535                 this.ul.removeAllListeners();\r
536         }\r
537         if (this.el) {\r
538                 this.el.destroy();\r
539         }\r
540     },\r
541 \r
542         // private\r
543     beforeDestroy : Ext.emptyFn\r
544 \r
545 });\r
546 \r
547 // MenuNav is a private utility class used internally by the Menu\r
548 Ext.menu.MenuNav = function(menu){\r
549     Ext.menu.MenuNav.superclass.constructor.call(this, menu.el);\r
550     this.scope = this.menu = menu;\r
551 };\r
552 \r
553 Ext.extend(Ext.menu.MenuNav, Ext.KeyNav, {\r
554     doRelay : function(e, h){\r
555         var k = e.getKey();\r
556         if(!this.menu.activeItem && e.isNavKeyPress() && k != e.SPACE && k != e.RETURN){\r
557             this.menu.tryActivate(0, 1);\r
558             return false;\r
559         }\r
560         return h.call(this.scope || this, e, this.menu);\r
561     },\r
562 \r
563     up : function(e, m){\r
564         if(!m.tryActivate(m.items.indexOf(m.activeItem)-1, -1)){\r
565             m.tryActivate(m.items.length-1, -1);\r
566         }\r
567     },\r
568 \r
569     down : function(e, m){\r
570         if(!m.tryActivate(m.items.indexOf(m.activeItem)+1, 1)){\r
571             m.tryActivate(0, 1);\r
572         }\r
573     },\r
574 \r
575     right : function(e, m){\r
576         if(m.activeItem){\r
577             m.activeItem.expandMenu(true);\r
578         }\r
579     },\r
580 \r
581     left : function(e, m){\r
582         m.hide();\r
583         if(m.parentMenu && m.parentMenu.activeItem){\r
584             m.parentMenu.activeItem.activate();\r
585         }\r
586     },\r
587 \r
588     enter : function(e, m){\r
589         if(m.activeItem){\r
590             e.stopPropagation();\r
591             m.activeItem.onClick(e);\r
592             m.fireEvent("click", this, m.activeItem);\r
593             return true;\r
594         }\r
595     }\r
596 });