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