menu position calcs based on align
[roojs1] / Roo / bootstrap / Menu.js
1 /*
2  * - LGPL
3  *
4  * menu
5  * 
6  */
7
8 /**
9  * @class Roo.bootstrap.Menu
10  * @extends Roo.bootstrap.Component
11  * Bootstrap Menu class - container for MenuItems
12  * @cfg {String} type (dropdown|treeview|submenu) type of menu
13  * @cfg {bool} hidden  if the menu should be hidden when rendered.
14  * @cfg {bool} stopEvent (true|false)  Stop event after trigger press (default true)
15  * @cfg {bool} isLink (true|false)  the menu has link disable auto expand and collaspe (default false)
16   * @cfg {bool} hideTrigger (true|false)  default false - hide the carret for trigger.
17   * @cfg {String} align  default tl-bl? == below  - how the menu should be aligned. 
18  
19  * @constructor
20  * Create a new Menu
21  * @param {Object} config The config object
22  */
23
24
25 Roo.bootstrap.Menu = function(config){
26     Roo.bootstrap.Menu.superclass.constructor.call(this, config);
27     if (this.registerMenu && this.type != 'treeview')  {
28         Roo.bootstrap.MenuMgr.register(this);
29     }
30     
31     
32     this.addEvents({
33         /**
34          * @event beforeshow
35          * Fires before this menu is displayed (return false to block)
36          * @param {Roo.menu.Menu} this
37          */
38         beforeshow : true,
39         /**
40          * @event beforehide
41          * Fires before this menu is hidden (return false to block)
42          * @param {Roo.menu.Menu} this
43          */
44         beforehide : true,
45         /**
46          * @event show
47          * Fires after this menu is displayed
48          * @param {Roo.menu.Menu} this
49          */
50         show : true,
51         /**
52          * @event hide
53          * Fires after this menu is hidden
54          * @param {Roo.menu.Menu} this
55          */
56         hide : true,
57         /**
58          * @event click
59          * Fires when this menu is clicked (or when the enter key is pressed while it is active)
60          * @param {Roo.menu.Menu} this
61          * @param {Roo.menu.Item} menuItem The menu item that was clicked
62          * @param {Roo.EventObject} e
63          */
64         click : true,
65         /**
66          * @event mouseover
67          * Fires when the mouse is hovering over 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         mouseover : true,
73         /**
74          * @event mouseout
75          * Fires when the mouse exits this menu
76          * @param {Roo.menu.Menu} this
77          * @param {Roo.EventObject} e
78          * @param {Roo.menu.Item} menuItem The menu item that was clicked
79          */
80         mouseout : true,
81         /**
82          * @event itemclick
83          * Fires when a menu item contained in this menu is clicked
84          * @param {Roo.menu.BaseItem} baseItem The BaseItem that was clicked
85          * @param {Roo.EventObject} e
86          */
87         itemclick: true
88     });
89     this.menuitems = new Roo.util.MixedCollection(false, function(o) { return o.el.id; });
90 };
91
92 Roo.extend(Roo.bootstrap.Menu, Roo.bootstrap.Component,  {
93     
94    /// html : false,
95    
96     triggerEl : false,  // is this set by component builder? -- it should really be fetched from parent()???
97     type: false,
98     /**
99      * @cfg {Boolean} registerMenu True (default) - means that clicking on screen etc. hides it.
100      */
101     registerMenu : true,
102     
103     menuItems :false, // stores the menu items..
104     
105     hidden:true,
106         
107     parentMenu : false,
108     
109     stopEvent : true,
110     
111     isLink : false,
112     
113     container_method : 'getDocumentBody', // so the menu is rendered on the body and zIndex works.
114     
115     hideTrigger : false,
116     
117     align : 'tl-bl?',
118     
119     
120     getChildContainer : function() {
121         return this.el;  
122     },
123     
124     getAutoCreate : function(){
125          
126         //if (['right'].indexOf(this.align)!==-1) {
127         //    cfg.cn[1].cls += ' pull-right'
128         //}
129          
130         var cfg = {
131             tag : 'ul',
132             cls : 'dropdown-menu shadow' ,
133             style : 'z-index:1000'
134             
135         };
136         
137         if (this.type === 'submenu') {
138             cfg.cls = 'submenu active';
139         }
140         if (this.type === 'treeview') {
141             cfg.cls = 'treeview-menu';
142         }
143         
144         return cfg;
145     },
146     initEvents : function() {
147         
148        // Roo.log("ADD event");
149        // Roo.log(this.triggerEl.dom);
150         
151         this.triggerEl.on('click', this.onTriggerClick, this);
152         
153         this.triggerEl.on(Roo.isTouch ? 'touchstart' : 'mouseup', this.onTriggerPress, this);
154         
155         if (!this.hideTrigger) {
156             if (this.triggerEl.hasClass('nav-item') && this.triggerEl.select('.nav-link',true).length) {
157                 // dropdown toggle on the 'a' in BS4?
158                 this.triggerEl.select('.nav-link',true).first().addClass('dropdown-toggle');
159             } else {
160                 this.triggerEl.addClass('dropdown-toggle');
161             }
162         }
163         if (Roo.isTouch) {
164             this.el.on('touchstart'  , this.onTouch, this);
165         }
166         this.el.on('click' , this.onClick, this);
167
168         this.el.on("mouseover", this.onMouseOver, this);
169         this.el.on("mouseout", this.onMouseOut, this);
170         
171     },
172     
173     findTargetItem : function(e)
174     {
175         var t = e.getTarget(".dropdown-menu-item", this.el,  true);
176         if(!t){
177             return false;
178         }
179         //Roo.log(t);         Roo.log(t.id);
180         if(t && t.id){
181             //Roo.log(this.menuitems);
182             return this.menuitems.get(t.id);
183             
184             //return this.items.get(t.menuItemId);
185         }
186         
187         return false;
188     },
189     
190     onTouch : function(e) 
191     {
192         Roo.log("menu.onTouch");
193         //e.stopEvent(); this make the user popdown broken
194         this.onClick(e);
195     },
196     
197     onClick : function(e)
198     {
199         Roo.log("menu.onClick");
200         
201         var t = this.findTargetItem(e);
202         if(!t || t.isContainer){
203             return;
204         }
205         Roo.log(e);
206         /*
207         if (Roo.isTouch && e.type == 'touchstart' && t.menu  && !t.disabled) {
208             if(t == this.activeItem && t.shouldDeactivate(e)){
209                 this.activeItem.deactivate();
210                 delete this.activeItem;
211                 return;
212             }
213             if(t.canActivate){
214                 this.setActiveItem(t, true);
215             }
216             return;
217             
218             
219         }
220         */
221        
222         Roo.log('pass click event');
223         
224         t.onClick(e);
225         
226         this.fireEvent("click", this, t, e);
227         
228         var _this = this;
229         
230         if(!t.href.length || t.href == '#'){
231             (function() { _this.hide(); }).defer(100);
232         }
233         
234     },
235     
236     onMouseOver : function(e){
237         var t  = this.findTargetItem(e);
238         //Roo.log(t);
239         //if(t){
240         //    if(t.canActivate && !t.disabled){
241         //        this.setActiveItem(t, true);
242         //    }
243         //}
244         
245         this.fireEvent("mouseover", this, e, t);
246     },
247     isVisible : function(){
248         return !this.hidden;
249     },
250     onMouseOut : function(e){
251         var t  = this.findTargetItem(e);
252         
253         //if(t ){
254         //    if(t == this.activeItem && t.shouldDeactivate(e)){
255         //        this.activeItem.deactivate();
256         //        delete this.activeItem;
257         //    }
258         //}
259         this.fireEvent("mouseout", this, e, t);
260     },
261     
262     
263     /**
264      * Displays this menu relative to another element
265      * @param {String/HTMLElement/Roo.Element} element The element to align to
266      * @param {String} position (optional) The {@link Roo.Element#alignTo} anchor position to use in aligning to
267      * the element (defaults to this.defaultAlign)
268      * @param {Roo.menu.Menu} parentMenu (optional) This menu's parent menu, if applicable (defaults to undefined)
269      */
270     show : function(el, pos, parentMenu)
271     {
272         if (false === this.fireEvent("beforeshow", this)) {
273             Roo.log("show canceled");
274             return;
275         }
276         this.parentMenu = parentMenu;
277         if(!this.el){
278             this.render();
279         }
280         this.el.addClass('show'); // show otherwise we do not know how big we are..
281          
282         var xy = this.el.getAlignToXY(el, pos);
283         
284         // bl-tl << left align  below
285         // tl-bl << left align 
286         
287         if(this.el.getWidth() + xy[0] >= Roo.lib.Dom.getViewWidth()){
288             // if it goes to far to the right.. -> align left.
289             xy = this.el.getAlignToXY(el, this.align.replace('/l/g', 'r'))
290         }
291         if(xy[0] < 0){
292             // was left align - go right?
293             xy = this.el.getAlignToXY(el, this.align.replace('/r/g', 'l'))
294         }
295         
296         // goes down the bottom
297         if(this.el.getHeight() + xy[1] >= Roo.lib.Dom.getViewHeight() ||
298            xy[1]  < 0 ){
299             var a = this.align.replace('?', '').split('-');
300             xy = this.el.getAlignToXY(el, a[1]  + '-' + a[0] + '?')
301             
302         }
303         
304         this.showAt(  xy , parentMenu, false);
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         
321         //this.el.show();
322         this.hideMenuItems();
323         this.hidden = false;
324         this.triggerEl.addClass('open');
325         this.el.addClass('show');
326         
327         
328         
329         // reassign x when hitting right
330         
331         // reassign y when hitting bottom
332         
333         // but the list may align on trigger left or trigger top... should it be a properity?
334         
335         if(this.el.getStyle('top') != 'auto' && this.el.getStyle('top').slice(-1) != "%"){
336             this.el.setXY(xy);
337         }
338         
339         this.focus();
340         this.fireEvent("show", this);
341     },
342     
343     focus : function(){
344         return;
345         if(!this.hidden){
346             this.doFocus.defer(50, this);
347         }
348     },
349
350     doFocus : function(){
351         if(!this.hidden){
352             this.focusEl.focus();
353         }
354     },
355
356     /**
357      * Hides this menu and optionally all parent menus
358      * @param {Boolean} deep (optional) True to hide all parent menus recursively, if any (defaults to false)
359      */
360     hide : function(deep)
361     {
362         if (false === this.fireEvent("beforehide", this)) {
363             Roo.log("hide canceled");
364             return;
365         }
366         this.hideMenuItems();
367         if(this.el && this.isVisible()){
368            
369             if(this.activeItem){
370                 this.activeItem.deactivate();
371                 this.activeItem = null;
372             }
373             this.triggerEl.removeClass('open');;
374             this.el.removeClass('show');
375             this.hidden = true;
376             this.fireEvent("hide", this);
377         }
378         if(deep === true && this.parentMenu){
379             this.parentMenu.hide(true);
380         }
381     },
382     
383     onTriggerClick : function(e)
384     {
385         Roo.log('trigger click');
386         
387         var target = e.getTarget();
388         
389         Roo.log(target.nodeName.toLowerCase());
390         
391         if(target.nodeName.toLowerCase() === 'i'){
392             e.preventDefault();
393         }
394         
395     },
396     
397     onTriggerPress  : function(e)
398     {
399         Roo.log('trigger press');
400         //Roo.log(e.getTarget());
401        // Roo.log(this.triggerEl.dom);
402        
403         // trigger only occurs on normal menu's -- if it's a treeview or dropdown... do not hide/show..
404         var pel = Roo.get(e.getTarget());
405         if (pel.findParent('.dropdown-menu') || pel.findParent('.treeview-menu') ) {
406             Roo.log('is treeview or dropdown?');
407             return;
408         }
409         
410         if(e.getTarget().nodeName.toLowerCase() !== 'i' && this.isLink){
411             return;
412         }
413         
414         if (this.isVisible()) {
415             Roo.log('hide');
416             this.hide();
417         } else {
418             Roo.log('show');
419              
420             this.show(this.triggerEl, this.align, false);
421         }
422         
423         if(this.stopEvent || e.getTarget().nodeName.toLowerCase() === 'i'){
424             e.stopEvent();
425         }
426         
427     },
428        
429     
430     hideMenuItems : function()
431     {
432         Roo.log("hide Menu Items");
433         if (!this.el) { 
434             return;
435         }
436         
437         this.el.select('.open',true).each(function(aa) {
438             
439             aa.removeClass('open');
440          
441         });
442     },
443     addxtypeChild : function (tree, cntr) {
444         var comp= Roo.bootstrap.Menu.superclass.addxtypeChild.call(this, tree, cntr);
445           
446         this.menuitems.add(comp);
447         return comp;
448
449     },
450     getEl : function()
451     {
452         Roo.log(this.el);
453         return this.el;
454     },
455     
456     clear : function()
457     {
458         this.getEl().dom.innerHTML = '';
459         this.menuitems.clear();
460     }
461 });
462
463  
464