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