_MenuBase.js 12 KB

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