_MenuBase.js.uncompressed.js 13 KB

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