]>
Commit | Line | Data |
---|---|---|
f0cfe83e AD |
1 | define("dijit/_MenuBase", [ |
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 | "./popup", | |
13 | "./registry", | |
14 | "./_Widget", | |
15 | "./_KeyNavContainer", | |
16 | "./_TemplatedMixin" | |
17 | ], function(array, declare, dom, domAttr, domClass, lang, mouse, on, winUtils, | |
18 | a11yclick, pm, registry, _Widget, _KeyNavContainer, _TemplatedMixin){ | |
19 | ||
20 | ||
21 | // module: | |
22 | // dijit/_MenuBase | |
23 | ||
24 | return declare("dijit._MenuBase", | |
25 | [_Widget, _TemplatedMixin, _KeyNavContainer], | |
26 | { | |
27 | // summary: | |
28 | // Base class for Menu and MenuBar | |
29 | ||
30 | // parentMenu: [readonly] Widget | |
31 | // pointer to menu that displayed me | |
32 | parentMenu: null, | |
33 | ||
34 | // popupDelay: Integer | |
35 | // number of milliseconds before hovering (without clicking) causes the popup to automatically open. | |
36 | popupDelay: 500, | |
37 | ||
38 | // autoFocus: Boolean | |
39 | // A toggle to control whether or not a Menu gets focused when opened as a drop down from a MenuBar | |
40 | // or DropDownButton/ComboButton. Note though that it always get focused when opened via the keyboard. | |
41 | autoFocus: false, | |
42 | ||
43 | childSelector: function(/*DOMNode*/ node){ | |
44 | // summary: | |
45 | // Selector (passed to on.selector()) used to identify MenuItem child widgets, but exclude inert children | |
46 | // like MenuSeparator. If subclass overrides to a string (ex: "> *"), the subclass must require dojo/query. | |
47 | // tags: | |
48 | // protected | |
49 | ||
50 | var widget = registry.byNode(node); | |
51 | return node.parentNode == this.containerNode && widget && widget.focus; | |
52 | }, | |
53 | ||
54 | postCreate: function(){ | |
55 | var self = this, | |
56 | matches = typeof this.childSelector == "string" ? this.childSelector : lang.hitch(this, "childSelector"); | |
57 | this.own( | |
58 | on(this.containerNode, on.selector(matches, mouse.enter), function(){ | |
59 | self.onItemHover(registry.byNode(this)); | |
60 | }), | |
61 | on(this.containerNode, on.selector(matches, mouse.leave), function(){ | |
62 | self.onItemUnhover(registry.byNode(this)); | |
63 | }), | |
64 | on(this.containerNode, on.selector(matches, a11yclick), function(evt){ | |
65 | self.onItemClick(registry.byNode(this), evt); | |
66 | evt.stopPropagation(); | |
67 | evt.preventDefault(); | |
68 | }) | |
69 | ); | |
70 | this.inherited(arguments); | |
71 | }, | |
72 | ||
73 | onExecute: function(){ | |
74 | // summary: | |
75 | // Attach point for notification about when a menu item has been executed. | |
76 | // This is an internal mechanism used for Menus to signal to their parent to | |
77 | // close them, because they are about to execute the onClick handler. In | |
78 | // general developers should not attach to or override this method. | |
79 | // tags: | |
80 | // protected | |
81 | }, | |
82 | ||
83 | onCancel: function(/*Boolean*/ /*===== closeAll =====*/){ | |
84 | // summary: | |
85 | // Attach point for notification about when the user cancels the current menu | |
86 | // This is an internal mechanism used for Menus to signal to their parent to | |
87 | // close them. In general developers should not attach to or override this method. | |
88 | // tags: | |
89 | // protected | |
90 | }, | |
91 | ||
92 | _moveToPopup: function(/*Event*/ evt){ | |
93 | // summary: | |
94 | // This handles the right arrow key (left arrow key on RTL systems), | |
95 | // which will either open a submenu, or move to the next item in the | |
96 | // ancestor MenuBar | |
97 | // tags: | |
98 | // private | |
99 | ||
100 | if(this.focusedChild && this.focusedChild.popup && !this.focusedChild.disabled){ | |
101 | this.onItemClick(this.focusedChild, evt); | |
102 | }else{ | |
103 | var topMenu = this._getTopMenu(); | |
104 | if(topMenu && topMenu._isMenuBar){ | |
105 | topMenu.focusNext(); | |
106 | } | |
107 | } | |
108 | }, | |
109 | ||
110 | _onPopupHover: function(/*Event*/ /*===== evt =====*/){ | |
111 | // summary: | |
112 | // This handler is called when the mouse moves over the popup. | |
113 | // tags: | |
114 | // private | |
115 | ||
116 | // if the mouse hovers over a menu popup that is in pending-close state, | |
117 | // then stop the close operation. | |
118 | // This can't be done in onItemHover since some popup targets don't have MenuItems (e.g. ColorPicker) | |
119 | if(this.currentPopup && this.currentPopup._pendingClose_timer){ | |
120 | var parentMenu = this.currentPopup.parentMenu; | |
121 | // highlight the parent menu item pointing to this popup | |
122 | if(parentMenu.focusedChild){ | |
123 | parentMenu.focusedChild._setSelected(false); | |
124 | } | |
125 | parentMenu.focusedChild = this.currentPopup.from_item; | |
126 | parentMenu.focusedChild._setSelected(true); | |
127 | // cancel the pending close | |
128 | this._stopPendingCloseTimer(this.currentPopup); | |
129 | } | |
130 | }, | |
131 | ||
132 | onItemHover: function(/*MenuItem*/ item){ | |
133 | // summary: | |
134 | // Called when cursor is over a MenuItem. | |
135 | // tags: | |
136 | // protected | |
137 | ||
138 | // Don't do anything unless user has "activated" the menu by: | |
139 | // 1) clicking it | |
140 | // 2) opening it from a parent menu (which automatically focuses it) | |
141 | if(this.isActive){ | |
142 | this.focusChild(item); | |
143 | if(this.focusedChild.popup && !this.focusedChild.disabled && !this.hover_timer){ | |
144 | this.hover_timer = this.defer("_openPopup", this.popupDelay); | |
145 | } | |
146 | } | |
147 | // if the user is mixing mouse and keyboard navigation, | |
148 | // then the menu may not be active but a menu item has focus, | |
149 | // but it's not the item that the mouse just hovered over. | |
150 | // To avoid both keyboard and mouse selections, use the latest. | |
151 | if(this.focusedChild){ | |
152 | this.focusChild(item); | |
153 | } | |
154 | this._hoveredChild = item; | |
155 | ||
156 | item._set("hovering", true); | |
157 | }, | |
158 | ||
159 | _onChildBlur: function(item){ | |
160 | // summary: | |
161 | // Called when a child MenuItem becomes inactive because focus | |
162 | // has been removed from the MenuItem *and* it's descendant menus. | |
163 | // tags: | |
164 | // private | |
165 | this._stopPopupTimer(); | |
166 | item._setSelected(false); | |
167 | // Close all popups that are open and descendants of this menu | |
168 | var itemPopup = item.popup; | |
169 | if(itemPopup){ | |
170 | this._stopPendingCloseTimer(itemPopup); | |
171 | itemPopup._pendingClose_timer = this.defer(function(){ | |
172 | itemPopup._pendingClose_timer = null; | |
173 | if(itemPopup.parentMenu){ | |
174 | itemPopup.parentMenu.currentPopup = null; | |
175 | } | |
176 | pm.close(itemPopup); // this calls onClose | |
177 | }, this.popupDelay); | |
178 | } | |
179 | }, | |
180 | ||
181 | onItemUnhover: function(/*MenuItem*/ item){ | |
182 | // summary: | |
183 | // Callback fires when mouse exits a MenuItem | |
184 | // tags: | |
185 | // protected | |
186 | ||
187 | if(this.isActive){ | |
188 | this._stopPopupTimer(); | |
189 | } | |
190 | if(this._hoveredChild == item){ this._hoveredChild = null; } | |
191 | ||
192 | item._set("hovering", false); | |
193 | }, | |
194 | ||
195 | _stopPopupTimer: function(){ | |
196 | // summary: | |
197 | // Cancels the popup timer because the user has stop hovering | |
198 | // on the MenuItem, etc. | |
199 | // tags: | |
200 | // private | |
201 | if(this.hover_timer){ | |
202 | this.hover_timer = this.hover_timer.remove(); | |
203 | } | |
204 | }, | |
205 | ||
206 | _stopPendingCloseTimer: function(/*dijit/_WidgetBase*/ popup){ | |
207 | // summary: | |
208 | // Cancels the pending-close timer because the close has been preempted | |
209 | // tags: | |
210 | // private | |
211 | if(popup._pendingClose_timer){ | |
212 | popup._pendingClose_timer = popup._pendingClose_timer.remove(); | |
213 | } | |
214 | }, | |
215 | ||
216 | _stopFocusTimer: function(){ | |
217 | // summary: | |
218 | // Cancels the pending-focus timer because the menu was closed before focus occured | |
219 | // tags: | |
220 | // private | |
221 | if(this._focus_timer){ | |
222 | this._focus_timer = this._focus_timer.remove(); | |
223 | } | |
224 | }, | |
225 | ||
226 | _getTopMenu: function(){ | |
227 | // summary: | |
228 | // Returns the top menu in this chain of Menus | |
229 | // tags: | |
230 | // private | |
231 | for(var top=this; top.parentMenu; top=top.parentMenu); | |
232 | return top; | |
233 | }, | |
234 | ||
235 | onItemClick: function(/*dijit/_WidgetBase*/ item, /*Event*/ evt){ | |
236 | // summary: | |
237 | // Handle clicks on an item. | |
238 | // tags: | |
239 | // private | |
240 | ||
241 | // this can't be done in _onFocus since the _onFocus events occurs asynchronously | |
242 | if(typeof this.isShowingNow == 'undefined'){ // non-popup menu | |
243 | this._markActive(); | |
244 | } | |
245 | ||
246 | this.focusChild(item); | |
247 | ||
248 | if(item.disabled){ return false; } | |
249 | ||
250 | if(item.popup){ | |
251 | this._openPopup(evt.type == "keypress"); | |
252 | }else{ | |
253 | // before calling user defined handler, close hierarchy of menus | |
254 | // and restore focus to place it was when menu was opened | |
255 | this.onExecute(); | |
256 | ||
257 | // user defined handler for click | |
258 | item._onClick ? item._onClick(evt) : item.onClick(evt); | |
259 | } | |
260 | }, | |
261 | ||
262 | _openPopup: function(/*Boolean*/ focus){ | |
263 | // summary: | |
264 | // Open the popup to the side of/underneath the current menu item, and optionally focus first item | |
265 | // tags: | |
266 | // protected | |
267 | ||
268 | this._stopPopupTimer(); | |
269 | var from_item = this.focusedChild; | |
270 | if(!from_item){ return; } // the focused child lost focus since the timer was started | |
271 | var popup = from_item.popup; | |
272 | if(!popup.isShowingNow){ | |
273 | if(this.currentPopup){ | |
274 | this._stopPendingCloseTimer(this.currentPopup); | |
275 | pm.close(this.currentPopup); | |
276 | } | |
277 | popup.parentMenu = this; | |
278 | popup.from_item = from_item; // helps finding the parent item that should be focused for this popup | |
279 | var self = this; | |
280 | pm.open({ | |
281 | parent: this, | |
282 | popup: popup, | |
283 | around: from_item.domNode, | |
284 | orient: this._orient || ["after", "before"], | |
285 | onCancel: function(){ // called when the child menu is canceled | |
286 | // set isActive=false (_closeChild vs _cleanUp) so that subsequent hovering will NOT open child menus | |
287 | // which seems aligned with the UX of most applications (e.g. notepad, wordpad, paint shop pro) | |
288 | self.focusChild(from_item); // put focus back on my node | |
289 | self._cleanUp(); // close the submenu (be sure this is done _after_ focus is moved) | |
290 | from_item._setSelected(true); // oops, _cleanUp() deselected the item | |
291 | self.focusedChild = from_item; // and unset focusedChild | |
292 | }, | |
293 | onExecute: lang.hitch(this, "_cleanUp") | |
294 | }); | |
295 | ||
296 | this.currentPopup = popup; | |
297 | // detect mouseovers to handle lazy mouse movements that temporarily focus other menu items | |
298 | popup.connect(popup.domNode, "onmouseenter", lang.hitch(self, "_onPopupHover")); // cleaned up when the popped-up widget is destroyed on close | |
299 | } | |
300 | ||
301 | if(focus && popup.focus){ | |
302 | // If user is opening the popup via keyboard (right arrow, or down arrow for MenuBar), then focus the popup. | |
303 | // If the cursor happens to collide with the popup, it will generate an onmouseover event | |
304 | // even though the mouse wasn't moved. Use defer() to call popup.focus so that | |
305 | // our focus() call overrides the onmouseover event, rather than vice-versa. (#8742) | |
306 | popup._focus_timer = this.defer(lang.hitch(popup, function(){ | |
307 | this._focus_timer = null; | |
308 | this.focus(); | |
309 | })); | |
310 | } | |
311 | }, | |
312 | ||
313 | _markActive: function(){ | |
314 | // summary: | |
315 | // Mark this menu's state as active. | |
316 | // Called when this Menu gets focus from: | |
317 | // | |
318 | // 1. clicking it (mouse or via space/arrow key) | |
319 | // 2. being opened by a parent menu. | |
320 | // | |
321 | // This is not called just from mouse hover. | |
322 | // Focusing a menu via TAB does NOT automatically set isActive | |
323 | // since TAB is a navigation operation and not a selection one. | |
324 | // For Windows apps, pressing the ALT key focuses the menubar | |
325 | // menus (similar to TAB navigation) but the menu is not active | |
326 | // (ie no dropdown) until an item is clicked. | |
327 | this.isActive = true; | |
328 | domClass.replace(this.domNode, "dijitMenuActive", "dijitMenuPassive"); | |
329 | }, | |
330 | ||
331 | onOpen: function(/*Event*/ /*===== e =====*/){ | |
332 | // summary: | |
333 | // Callback when this menu is opened. | |
334 | // This is called by the popup manager as notification that the menu | |
335 | // was opened. | |
336 | // tags: | |
337 | // private | |
338 | ||
339 | this.isShowingNow = true; | |
340 | this._markActive(); | |
341 | }, | |
342 | ||
343 | _markInactive: function(){ | |
344 | // summary: | |
345 | // Mark this menu's state as inactive. | |
346 | this.isActive = false; // don't do this in _onBlur since the state is pending-close until we get here | |
347 | domClass.replace(this.domNode, "dijitMenuPassive", "dijitMenuActive"); | |
348 | }, | |
349 | ||
350 | onClose: function(){ | |
351 | // summary: | |
352 | // Callback when this menu is closed. | |
353 | // This is called by the popup manager as notification that the menu | |
354 | // was closed. | |
355 | // tags: | |
356 | // private | |
357 | ||
358 | this._stopFocusTimer(); | |
359 | this._markInactive(); | |
360 | this.isShowingNow = false; | |
361 | this.parentMenu = null; | |
362 | }, | |
363 | ||
364 | _closeChild: function(){ | |
365 | // summary: | |
366 | // Called when submenu is clicked or focus is lost. Close hierarchy of menus. | |
367 | // tags: | |
368 | // private | |
369 | this._stopPopupTimer(); | |
370 | ||
371 | if(this.currentPopup){ | |
372 | // If focus is on a descendant MenuItem then move focus to me, | |
373 | // because IE doesn't like it when you display:none a node with focus, | |
374 | // and also so keyboard users don't lose control. | |
375 | // Likely, immediately after a user defined onClick handler will move focus somewhere | |
376 | // else, like a Dialog. | |
377 | if(array.indexOf(this._focusManager.activeStack, this.id) >= 0){ | |
378 | domAttr.set(this.focusedChild.focusNode, "tabIndex", this.tabIndex); | |
379 | this.focusedChild.focusNode.focus(); | |
380 | } | |
381 | // Close all popups that are open and descendants of this menu | |
382 | pm.close(this.currentPopup); | |
383 | this.currentPopup = null; | |
384 | } | |
385 | ||
386 | if(this.focusedChild){ // unhighlight the focused item | |
387 | this.focusedChild._setSelected(false); | |
388 | this.onItemUnhover(this.focusedChild); | |
389 | this.focusedChild = null; | |
390 | } | |
391 | }, | |
392 | ||
393 | _onItemFocus: function(/*MenuItem*/ item){ | |
394 | // summary: | |
395 | // Called when child of this Menu gets focus from: | |
396 | // | |
397 | // 1. clicking it | |
398 | // 2. tabbing into it | |
399 | // 3. being opened by a parent menu. | |
400 | // | |
401 | // This is not called just from mouse hover. | |
402 | if(this._hoveredChild && this._hoveredChild != item){ | |
403 | this.onItemUnhover(this._hoveredChild); // any previous mouse movement is trumped by focus selection | |
404 | } | |
405 | }, | |
406 | ||
407 | _onBlur: function(){ | |
408 | // summary: | |
409 | // Called when focus is moved away from this Menu and it's submenus. | |
410 | // tags: | |
411 | // protected | |
412 | this._cleanUp(); | |
413 | this.inherited(arguments); | |
414 | }, | |
415 | ||
416 | _cleanUp: function(){ | |
417 | // summary: | |
418 | // Called when the user is done with this menu. Closes hierarchy of menus. | |
419 | // tags: | |
420 | // private | |
421 | ||
422 | this._closeChild(); // don't call this.onClose since that's incorrect for MenuBar's that never close | |
423 | if(typeof this.isShowingNow == 'undefined'){ // non-popup menu doesn't call onClose | |
424 | this._markInactive(); | |
425 | } | |
426 | } | |
427 | }); | |
428 | ||
429 | }); |