source: Dev/trunk/src/client/dijit/_MenuBase.js @ 529

Last change on this file since 529 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 14.5 KB
Line 
1define([
2        "dojo/_base/array", // array.indexOf
3        "dojo/_base/declare", // declare
4        "dojo/dom", // dom.isDescendant domClass.replace
5        "dojo/dom-attr",
6        "dojo/dom-class", // domClass.replace
7        "dojo/_base/lang", // lang.hitch
8        "dojo/mouse", // mouse.enter, mouse.leave
9        "dojo/on",
10        "dojo/window",
11        "./a11yclick",
12        "./registry",
13        "./_Widget",
14        "./_CssStateMixin",
15        "./_KeyNavContainer",
16        "./_TemplatedMixin"
17], function(array, declare, dom, domAttr, domClass, lang, mouse, on, winUtils, a11yclick,
18                        registry, _Widget, _CssStateMixin, _KeyNavContainer, _TemplatedMixin){
19
20        // module:
21        //              dijit/_MenuBase
22
23        return declare("dijit._MenuBase", [_Widget, _TemplatedMixin, _KeyNavContainer, _CssStateMixin], {
24                // summary:
25                //              Abstract base class for Menu and MenuBar.
26                //              Subclass should implement _onUpArrow(), _onDownArrow(), _onLeftArrow(), and _onRightArrow().
27
28                // selected: dijit/MenuItem
29                //              Currently selected (a.k.a. highlighted) MenuItem, or null if no MenuItem is selected.
30                //              If a submenu is open, will be set to MenuItem that displayed the submenu.   OTOH, if
31                //              this Menu is in passive mode (i.e. hasn't been clicked yet), will be null, because
32                //              "selected" is not merely "hovered".
33                selected: null,
34                _setSelectedAttr: function(item){
35                        if(this.selected != item){
36                                if(this.selected){
37                                        this.selected._setSelected(false);
38                                        this._onChildDeselect(this.selected);
39                                }
40                                if(item){
41                                        item._setSelected(true);
42                                }
43                                this._set("selected", item);
44                        }
45                },
46
47                // activated: [readonly] Boolean
48                //              This Menu has been clicked (mouse or via space/arrow key) or opened as a submenu,
49                //              so mere mouseover will open submenus.  Focusing a menu via TAB does NOT automatically make it active
50                //              since TAB is a navigation operation and not a selection one.
51                //              For Windows apps, pressing the ALT key focuses the menubar menus (similar to TAB navigation) but the
52                //              menu is not active (ie no dropdown) until an item is clicked.
53                activated: false,
54                _setActivatedAttr: function(val){
55                        domClass.toggle(this.domNode, "dijitMenuActive", val);
56                        domClass.toggle(this.domNode, "dijitMenuPassive", !val);
57                        this._set("activated", val);
58                },
59
60                // parentMenu: [readonly] Widget
61                //              pointer to menu that displayed me
62                parentMenu: null,
63
64                // popupDelay: Integer
65                //              After a menu has been activated (by clicking on it etc.), number of milliseconds before hovering
66                //              (without clicking) another MenuItem causes that MenuItem's popup to automatically open.
67                popupDelay: 500,
68
69                // passivePopupDelay: Integer
70                //              For a passive (unclicked) Menu, number of milliseconds before hovering (without clicking) will cause
71                //              the popup to open.  Default is Infinity, meaning you need to click the menu to open it.
72                passivePopupDelay: Infinity,
73
74                // autoFocus: Boolean
75                //              A toggle to control whether or not a Menu gets focused when opened as a drop down from a MenuBar
76                //              or DropDownButton/ComboButton.   Note though that it always get focused when opened via the keyboard.
77                autoFocus: false,
78
79                childSelector: function(/*DOMNode*/ node){
80                        // summary:
81                        //              Selector (passed to on.selector()) used to identify MenuItem child widgets, but exclude inert children
82                        //              like MenuSeparator.  If subclass overrides to a string (ex: "> *"), the subclass must require dojo/query.
83                        // tags:
84                        //              protected
85
86                        var widget = registry.byNode(node);
87                        return node.parentNode == this.containerNode && widget && widget.focus;
88                },
89
90                postCreate: function(){
91                        var self = this,
92                                matches = typeof this.childSelector == "string" ? this.childSelector : lang.hitch(this, "childSelector");
93                        this.own(
94                                on(this.containerNode, on.selector(matches, mouse.enter), function(){
95                                        self.onItemHover(registry.byNode(this));
96                                }),
97                                on(this.containerNode, on.selector(matches, mouse.leave), function(){
98                                        self.onItemUnhover(registry.byNode(this));
99                                }),
100                                on(this.containerNode, on.selector(matches, a11yclick), function(evt){
101                                        self.onItemClick(registry.byNode(this), evt);
102                                        evt.stopPropagation();
103                                        evt.preventDefault();
104                                })
105                        );
106                        this.inherited(arguments);
107                },
108
109                onKeyboardSearch: function(/*MenuItem*/ item, /*Event*/ evt, /*String*/ searchString, /*Number*/ numMatches){
110                        // summary:
111                        //              Attach point for notification about when a menu item has been searched for
112                        //              via the keyboard search mechanism.
113                        // tags:
114                        //              protected
115                        this.inherited(arguments);
116                        if(!!item && (numMatches == -1 || (!!item.popup && numMatches == 1))){
117                                this.onItemClick(item, evt);
118                        }
119                },
120
121                _keyboardSearchCompare: function(/*dijit/_WidgetBase*/ item, /*String*/ searchString){
122                        // summary:
123                        //              Compares the searchString to the widget's text label, returning:
124                        //              -1: a high priority match and stop searching
125                        //               0: no match
126                        //               1: a match but keep looking for a higher priority match
127                        // tags:
128                        //              private
129                        if(!!item.shortcutKey){
130                                // accessKey matches have priority
131                                return searchString == item.shortcutKey.toLowerCase() ? -1 : 0;
132                        }
133                        return this.inherited(arguments) ? 1 : 0; // change return value of -1 to 1 so that searching continues
134                },
135
136                onExecute: function(){
137                        // summary:
138                        //              Attach point for notification about when a menu item has been executed.
139                        //              This is an internal mechanism used for Menus to signal to their parent to
140                        //              close them, because they are about to execute the onClick handler.  In
141                        //              general developers should not attach to or override this method.
142                        // tags:
143                        //              protected
144                },
145
146                onCancel: function(/*Boolean*/ /*===== closeAll =====*/){
147                        // summary:
148                        //              Attach point for notification about when the user cancels the current menu
149                        //              This is an internal mechanism used for Menus to signal to their parent to
150                        //              close them.  In general developers should not attach to or override this method.
151                        // tags:
152                        //              protected
153                },
154
155                _moveToPopup: function(/*Event*/ evt){
156                        // summary:
157                        //              This handles the right arrow key (left arrow key on RTL systems),
158                        //              which will either open a submenu, or move to the next item in the
159                        //              ancestor MenuBar
160                        // tags:
161                        //              private
162
163                        if(this.focusedChild && this.focusedChild.popup && !this.focusedChild.disabled){
164                                this.onItemClick(this.focusedChild, evt);
165                        }else{
166                                var topMenu = this._getTopMenu();
167                                if(topMenu && topMenu._isMenuBar){
168                                        topMenu.focusNext();
169                                }
170                        }
171                },
172
173                _onPopupHover: function(/*Event*/ /*===== evt =====*/){
174                        // summary:
175                        //              This handler is called when the mouse moves over the popup.
176                        // tags:
177                        //              private
178
179                        // if the mouse hovers over a menu popup that is in pending-close state,
180                        // then stop the close operation.
181                        // This can't be done in onItemHover since some popup targets don't have MenuItems (e.g. ColorPicker)
182
183                        // highlight the parent menu item pointing to this popup (in case user temporarily moused over another MenuItem)
184                        this.set("selected", this.currentPopupItem);
185
186                        // cancel the pending close (if there is one) (in case user temporarily moused over another MenuItem)
187                        this._stopPendingCloseTimer();
188                },
189
190                onItemHover: function(/*MenuItem*/ item){
191                        // summary:
192                        //              Called when cursor is over a MenuItem.
193                        // tags:
194                        //              protected
195
196                        // Don't do anything unless user has "activated" the menu by:
197                        //              1) clicking it
198                        //              2) opening it from a parent menu (which automatically activates it)
199
200                        if(this.activated){
201                                this.set("selected", item);
202                                if(item.popup && !item.disabled && !this.hover_timer){
203                                        this.hover_timer = this.defer(function(){
204                                                this._openItemPopup(item);
205                                        }, this.popupDelay);
206                                }
207                        }else if(this.passivePopupDelay < Infinity){
208                                if(this.passive_hover_timer){
209                                        this.passive_hover_timer.remove();
210                                }
211                                this.passive_hover_timer = this.defer(function(){
212                                        this.onItemClick(item, {type: "click"});
213                                }, this.passivePopupDelay);
214                        }
215
216                        this._hoveredChild = item;
217
218                        item._set("hovering", true);
219                },
220
221                _onChildDeselect: function(item){
222                        // summary:
223                        //              Called when a child MenuItem becomes deselected.   Setup timer to close its popup.
224
225                        this._stopPopupTimer();
226
227                        // Setup timer to close all popups that are open and descendants of this menu.
228                        // Will be canceled if user quickly moves the mouse over the popup.
229                        if(this.currentPopupItem == item){
230                                this._stopPendingCloseTimer();
231                                this._pendingClose_timer = this.defer(function(){
232                                        this._pendingClose_timer = null;
233                                        this.currentPopupItem = null;
234                                        item._closePopup(); // this calls onClose
235                                }, this.popupDelay);
236                        }
237                },
238
239                onItemUnhover: function(/*MenuItem*/ item){
240                        // summary:
241                        //              Callback fires when mouse exits a MenuItem
242                        // tags:
243                        //              protected
244
245                        if(this._hoveredChild == item){
246                                this._hoveredChild = null;
247                        }
248
249                        if(this.passive_hover_timer){
250                                this.passive_hover_timer.remove();
251                                this.passive_hover_timer = null;
252                        }
253
254                        item._set("hovering", false);
255                },
256
257                _stopPopupTimer: function(){
258                        // summary:
259                        //              Cancels the popup timer because the user has stop hovering
260                        //              on the MenuItem, etc.
261                        // tags:
262                        //              private
263
264                        if(this.hover_timer){
265                                this.hover_timer = this.hover_timer.remove();
266                        }
267                },
268
269                _stopPendingCloseTimer: function(){
270                        // summary:
271                        //              Cancels the pending-close timer because the close has been preempted
272                        // tags:
273                        //              private
274                        if(this._pendingClose_timer){
275                                this._pendingClose_timer = this._pendingClose_timer.remove();
276                        }
277                },
278
279                _getTopMenu: function(){
280                        // summary:
281                        //              Returns the top menu in this chain of Menus
282                        // tags:
283                        //              private
284                        for(var top = this; top.parentMenu; top = top.parentMenu){}
285                        return top;
286                },
287
288                onItemClick: function(/*dijit/_WidgetBase*/ item, /*Event*/ evt){
289                        // summary:
290                        //              Handle clicks on an item.
291                        // tags:
292                        //              private
293
294                        if(this.passive_hover_timer){
295                                this.passive_hover_timer.remove();
296                        }
297
298                        this.focusChild(item);
299
300                        if(item.disabled){
301                                return false;
302                        }
303
304                        if(item.popup){
305                                this.set("selected", item);
306                                this.set("activated", true);
307                                var byKeyboard = /^key/.test(evt._origType || evt.type) ||
308                                        (evt.clientX == 0 && evt.clientY == 0); // detects accessKey like ALT+SHIFT+F, where type is "click"
309                                this._openItemPopup(item, byKeyboard);
310                        }else{
311                                // before calling user defined handler, close hierarchy of menus
312                                // and restore focus to place it was when menu was opened
313                                this.onExecute();
314
315                                // user defined handler for click
316                                item._onClick ? item._onClick(evt) : item.onClick(evt);
317                        }
318                },
319
320                _openItemPopup: function(/*dijit/MenuItem*/ from_item, /*Boolean*/ focus){
321                        // summary:
322                        //              Open the popup to the side of/underneath the current menu item, and optionally focus first item
323                        // tags:
324                        //              protected
325
326                        if(from_item == this.currentPopupItem){
327                                // Specified popup is already being shown, so just return
328                                return;
329                        }
330                        if(this.currentPopupItem){
331                                // If another popup is currently shown, then close it
332                                this._stopPendingCloseTimer();
333                                this.currentPopupItem._closePopup();
334                        }
335                        this._stopPopupTimer();
336
337                        var popup = from_item.popup;
338                        popup.parentMenu = this;
339
340                        // detect mouseover of the popup to handle lazy mouse movements that temporarily focus other menu items\c
341                        this.own(this._mouseoverHandle = on.once(popup.domNode, "mouseover", lang.hitch(this, "_onPopupHover")));
342
343                        var self = this;
344                        from_item._openPopup({
345                                parent: this,
346                                orient: this._orient || ["after", "before"],
347                                onCancel: function(){ // called when the child menu is canceled
348                                        if(focus){
349                                                // put focus back on my node before focused node is hidden
350                                                self.focusChild(from_item);
351                                        }
352
353                                        // close the submenu (be sure this is done _after_ focus is moved)
354                                        self._cleanUp();
355                                },
356                                onExecute: lang.hitch(this, "_cleanUp", true),
357                                onClose: function(){
358                                        // Remove handler created by onItemHover
359                                        if(self._mouseoverHandle){
360                                                self._mouseoverHandle.remove();
361                                                delete self._mouseoverHandle;
362                                        }
363                                }
364                        }, focus);
365
366                        this.currentPopupItem = from_item;
367
368                        // TODO: focusing a popup should clear tabIndex on Menu (and it's child MenuItems), so that neither
369                        // TAB nor SHIFT-TAB returns to the menu.  Only ESC or ENTER should return to the menu.
370                },
371
372                onOpen: function(/*Event*/ /*===== e =====*/){
373                        // summary:
374                        //              Callback when this menu is opened.
375                        //              This is called by the popup manager as notification that the menu
376                        //              was opened.
377                        // tags:
378                        //              private
379
380                        this.isShowingNow = true;
381                        this.set("activated", true);
382                },
383
384                onClose: function(){
385                        // summary:
386                        //              Callback when this menu is closed.
387                        //              This is called by the popup manager as notification that the menu
388                        //              was closed.
389                        // tags:
390                        //              private
391
392                        this.set("activated", false);
393                        this.set("selected", null);
394                        this.isShowingNow = false;
395                        this.parentMenu = null;
396                },
397
398                _closeChild: function(){
399                        // summary:
400                        //              Called when submenu is clicked or focus is lost.  Close hierarchy of menus.
401                        // tags:
402                        //              private
403                        this._stopPopupTimer();
404
405                        if(this.currentPopupItem){
406                                // If focus is on a descendant MenuItem then move focus to me,
407                                // because IE doesn't like it when you display:none a node with focus,
408                                // and also so keyboard users don't lose control.
409                                // Likely, immediately after a user defined onClick handler will move focus somewhere
410                                // else, like a Dialog.
411                                if(this.focused){
412                                        domAttr.set(this.selected.focusNode, "tabIndex", this.tabIndex);
413                                        this.selected.focusNode.focus();
414                                }
415
416                                // Close all popups that are open and descendants of this menu
417                                this.currentPopupItem._closePopup();
418                                this.currentPopupItem = null;
419                        }
420                },
421
422                _onItemFocus: function(/*MenuItem*/ item){
423                        // summary:
424                        //              Called when child of this Menu gets focus from:
425                        //
426                        //              1. clicking it
427                        //              2. tabbing into it
428                        //              3. being opened by a parent menu.
429                        //
430                        //              This is not called just from mouse hover.
431
432                        if(this._hoveredChild && this._hoveredChild != item){
433                                this.onItemUnhover(this._hoveredChild); // any previous mouse movement is trumped by focus selection
434                        }
435                        this.set("selected", item);
436                },
437
438                _onBlur: function(){
439                        // summary:
440                        //              Called when focus is moved away from this Menu and it's submenus.
441                        // tags:
442                        //              protected
443
444                        this._cleanUp(true);
445                        this.inherited(arguments);
446                },
447
448                _cleanUp: function(/*Boolean*/ clearSelectedItem){
449                        // summary:
450                        //              Called when the user is done with this menu.  Closes hierarchy of menus.
451                        // tags:
452                        //              private
453
454                        this._closeChild(); // don't call this.onClose since that's incorrect for MenuBar's that never close
455                        if(typeof this.isShowingNow == 'undefined'){ // non-popup menu doesn't call onClose
456                                this.set("activated", false);
457                        }
458
459                        if(clearSelectedItem){
460                                this.set("selected", null);
461                        }
462                }
463        });
464});
Note: See TracBrowser for help on using the repository browser.