_HasDropDown.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. /*
  2. Copyright (c) 2004-2012, The Dojo Foundation All Rights Reserved.
  3. Available via Academic Free License >= 2.1 OR the modified BSD license.
  4. see: http://dojotoolkit.org/license for details
  5. */
  6. if(!dojo._hasResource["dijit._HasDropDown"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  7. dojo._hasResource["dijit._HasDropDown"] = true;
  8. dojo.provide("dijit._HasDropDown");
  9. dojo.require("dijit._Widget");
  10. dojo.declare("dijit._HasDropDown",
  11. null,
  12. {
  13. // summary:
  14. // Mixin for widgets that need drop down ability.
  15. // _buttonNode: [protected] DomNode
  16. // The button/icon/node to click to display the drop down.
  17. // Can be set via a dojoAttachPoint assignment.
  18. // If missing, then either focusNode or domNode (if focusNode is also missing) will be used.
  19. _buttonNode: null,
  20. // _arrowWrapperNode: [protected] DomNode
  21. // Will set CSS class dijitUpArrow, dijitDownArrow, dijitRightArrow etc. on this node depending
  22. // on where the drop down is set to be positioned.
  23. // Can be set via a dojoAttachPoint assignment.
  24. // If missing, then _buttonNode will be used.
  25. _arrowWrapperNode: null,
  26. // _popupStateNode: [protected] DomNode
  27. // The node to set the popupActive class on.
  28. // Can be set via a dojoAttachPoint assignment.
  29. // If missing, then focusNode or _buttonNode (if focusNode is missing) will be used.
  30. _popupStateNode: null,
  31. // _aroundNode: [protected] DomNode
  32. // The node to display the popup around.
  33. // Can be set via a dojoAttachPoint assignment.
  34. // If missing, then domNode will be used.
  35. _aroundNode: null,
  36. // dropDown: [protected] Widget
  37. // The widget to display as a popup. This widget *must* be
  38. // defined before the startup function is called.
  39. dropDown: null,
  40. // autoWidth: [protected] Boolean
  41. // Set to true to make the drop down at least as wide as this
  42. // widget. Set to false if the drop down should just be its
  43. // default width
  44. autoWidth: true,
  45. // forceWidth: [protected] Boolean
  46. // Set to true to make the drop down exactly as wide as this
  47. // widget. Overrides autoWidth.
  48. forceWidth: false,
  49. // maxHeight: [protected] Integer
  50. // The max height for our dropdown.
  51. // Any dropdown taller than this will have scrollbars.
  52. // Set to 0 for no max height, or -1 to limit height to available space in viewport
  53. maxHeight: 0,
  54. // dropDownPosition: [const] String[]
  55. // This variable controls the position of the drop down.
  56. // It's an array of strings with the following values:
  57. //
  58. // * before: places drop down to the left of the target node/widget, or to the right in
  59. // the case of RTL scripts like Hebrew and Arabic
  60. // * after: places drop down to the right of the target node/widget, or to the left in
  61. // the case of RTL scripts like Hebrew and Arabic
  62. // * above: drop down goes above target node
  63. // * below: drop down goes below target node
  64. //
  65. // The list is positions is tried, in order, until a position is found where the drop down fits
  66. // within the viewport.
  67. //
  68. dropDownPosition: ["below","above"],
  69. // _stopClickEvents: Boolean
  70. // When set to false, the click events will not be stopped, in
  71. // case you want to use them in your subwidget
  72. _stopClickEvents: true,
  73. _onDropDownMouseDown: function(/*Event*/ e){
  74. // summary:
  75. // Callback when the user mousedown's on the arrow icon
  76. if(this.disabled || this.readOnly){ return; }
  77. // Prevent default to stop things like text selection, but don't stop propogation, so that:
  78. // 1. TimeTextBox etc. can focusthe <input> on mousedown
  79. // 2. dropDownButtonActive class applied by _CssStateMixin (on button depress)
  80. // 3. user defined onMouseDown handler fires
  81. e.preventDefault();
  82. this._docHandler = this.connect(dojo.doc, "onmouseup", "_onDropDownMouseUp");
  83. this.toggleDropDown();
  84. },
  85. _onDropDownMouseUp: function(/*Event?*/ e){
  86. // summary:
  87. // Callback when the user lifts their mouse after mouse down on the arrow icon.
  88. // If the drop is a simple menu and the mouse is over the menu, we execute it, otherwise, we focus our
  89. // dropDown node. If the event is missing, then we are not
  90. // a mouseup event.
  91. //
  92. // This is useful for the common mouse movement pattern
  93. // with native browser <select> nodes:
  94. // 1. mouse down on the select node (probably on the arrow)
  95. // 2. move mouse to a menu item while holding down the mouse button
  96. // 3. mouse up. this selects the menu item as though the user had clicked it.
  97. if(e && this._docHandler){
  98. this.disconnect(this._docHandler);
  99. }
  100. var dropDown = this.dropDown, overMenu = false;
  101. if(e && this._opened){
  102. // This code deals with the corner-case when the drop down covers the original widget,
  103. // because it's so large. In that case mouse-up shouldn't select a value from the menu.
  104. // Find out if our target is somewhere in our dropdown widget,
  105. // but not over our _buttonNode (the clickable node)
  106. var c = dojo.position(this._buttonNode, true);
  107. if(!(e.pageX >= c.x && e.pageX <= c.x + c.w) ||
  108. !(e.pageY >= c.y && e.pageY <= c.y + c.h)){
  109. var t = e.target;
  110. while(t && !overMenu){
  111. if(dojo.hasClass(t, "dijitPopup")){
  112. overMenu = true;
  113. }else{
  114. t = t.parentNode;
  115. }
  116. }
  117. if(overMenu){
  118. t = e.target;
  119. if(dropDown.onItemClick){
  120. var menuItem;
  121. while(t && !(menuItem = dijit.byNode(t))){
  122. t = t.parentNode;
  123. }
  124. if(menuItem && menuItem.onClick && menuItem.getParent){
  125. menuItem.getParent().onItemClick(menuItem, e);
  126. }
  127. }
  128. return;
  129. }
  130. }
  131. }
  132. if(this._opened && dropDown.focus && dropDown.autoFocus !== false){
  133. // Focus the dropdown widget - do it on a delay so that we
  134. // don't steal our own focus.
  135. window.setTimeout(dojo.hitch(dropDown, "focus"), 1);
  136. }
  137. },
  138. _onDropDownClick: function(/*Event*/ e){
  139. // the drop down was already opened on mousedown/keydown; just need to call stopEvent()
  140. if(this._stopClickEvents){
  141. dojo.stopEvent(e);
  142. }
  143. },
  144. buildRendering: function(){
  145. this.inherited(arguments);
  146. this._buttonNode = this._buttonNode || this.focusNode || this.domNode;
  147. this._popupStateNode = this._popupStateNode || this.focusNode || this._buttonNode;
  148. // Add a class to the "dijitDownArrowButton" type class to _buttonNode so theme can set direction of arrow
  149. // based on where drop down will normally appear
  150. var defaultPos = {
  151. "after" : this.isLeftToRight() ? "Right" : "Left",
  152. "before" : this.isLeftToRight() ? "Left" : "Right",
  153. "above" : "Up",
  154. "below" : "Down",
  155. "left" : "Left",
  156. "right" : "Right"
  157. }[this.dropDownPosition[0]] || this.dropDownPosition[0] || "Down";
  158. dojo.addClass(this._arrowWrapperNode || this._buttonNode, "dijit" + defaultPos + "ArrowButton");
  159. },
  160. postCreate: function(){
  161. // summary:
  162. // set up nodes and connect our mouse and keypress events
  163. this.inherited(arguments);
  164. this.connect(this._buttonNode, "onmousedown", "_onDropDownMouseDown");
  165. this.connect(this._buttonNode, "onclick", "_onDropDownClick");
  166. this.connect(this.focusNode, "onkeypress", "_onKey");
  167. this.connect(this.focusNode, "onkeyup", "_onKeyUp");
  168. },
  169. destroy: function(){
  170. if(this.dropDown){
  171. // Destroy the drop down, unless it's already been destroyed. This can happen because
  172. // the drop down is a direct child of <body> even though it's logically my child.
  173. if(!this.dropDown._destroyed){
  174. this.dropDown.destroyRecursive();
  175. }
  176. delete this.dropDown;
  177. }
  178. this.inherited(arguments);
  179. },
  180. _onKey: function(/*Event*/ e){
  181. // summary:
  182. // Callback when the user presses a key while focused on the button node
  183. if(this.disabled || this.readOnly){ return; }
  184. var d = this.dropDown, target = e.target;
  185. if(d && this._opened && d.handleKey){
  186. if(d.handleKey(e) === false){
  187. /* false return code means that the drop down handled the key */
  188. dojo.stopEvent(e);
  189. return;
  190. }
  191. }
  192. if(d && this._opened && e.charOrCode == dojo.keys.ESCAPE){
  193. this.closeDropDown();
  194. dojo.stopEvent(e);
  195. }else if(!this._opened &&
  196. (e.charOrCode == dojo.keys.DOWN_ARROW ||
  197. ( (e.charOrCode == dojo.keys.ENTER || e.charOrCode == " ") &&
  198. //ignore enter and space if the event is for a text input
  199. ((target.tagName || "").toLowerCase() !== 'input' ||
  200. (target.type && target.type.toLowerCase() !== 'text'))))){
  201. // Toggle the drop down, but wait until keyup so that the drop down doesn't
  202. // get a stray keyup event, or in the case of key-repeat (because user held
  203. // down key for too long), stray keydown events
  204. this._toggleOnKeyUp = true;
  205. dojo.stopEvent(e);
  206. }
  207. },
  208. _onKeyUp: function(){
  209. if(this._toggleOnKeyUp){
  210. delete this._toggleOnKeyUp;
  211. this.toggleDropDown();
  212. var d = this.dropDown; // drop down may not exist until toggleDropDown() call
  213. if(d && d.focus){
  214. setTimeout(dojo.hitch(d, "focus"), 1);
  215. }
  216. }
  217. },
  218. _onBlur: function(){
  219. // summary:
  220. // Called magically when focus has shifted away from this widget and it's dropdown
  221. // Don't focus on button if the user has explicitly focused on something else (happens
  222. // when user clicks another control causing the current popup to close)..
  223. // But if focus is inside of the drop down then reset focus to me, because IE doesn't like
  224. // it when you display:none a node with focus.
  225. var focusMe = dijit._curFocus && this.dropDown && dojo.isDescendant(dijit._curFocus, this.dropDown.domNode);
  226. this.closeDropDown(focusMe);
  227. this.inherited(arguments);
  228. },
  229. isLoaded: function(){
  230. // summary:
  231. // Returns whether or not the dropdown is loaded. This can
  232. // be overridden in order to force a call to loadDropDown().
  233. // tags:
  234. // protected
  235. return true;
  236. },
  237. loadDropDown: function(/* Function */ loadCallback){
  238. // summary:
  239. // Loads the data for the dropdown, and at some point, calls
  240. // the given callback. This is basically a callback when the
  241. // user presses the down arrow button to open the drop down.
  242. // tags:
  243. // protected
  244. loadCallback();
  245. },
  246. toggleDropDown: function(){
  247. // summary:
  248. // Callback when the user presses the down arrow button or presses
  249. // the down arrow key to open/close the drop down.
  250. // Toggle the drop-down widget; if it is up, close it, if not, open it
  251. // tags:
  252. // protected
  253. if(this.disabled || this.readOnly){ return; }
  254. if(!this._opened){
  255. // If we aren't loaded, load it first so there isn't a flicker
  256. if(!this.isLoaded()){
  257. this.loadDropDown(dojo.hitch(this, "openDropDown"));
  258. return;
  259. }else{
  260. this.openDropDown();
  261. }
  262. }else{
  263. this.closeDropDown();
  264. }
  265. },
  266. openDropDown: function(){
  267. // summary:
  268. // Opens the dropdown for this widget. To be called only when this.dropDown
  269. // has been created and is ready to display (ie, it's data is loaded).
  270. // returns:
  271. // return value of dijit.popup.open()
  272. // tags:
  273. // protected
  274. var dropDown = this.dropDown,
  275. ddNode = dropDown.domNode,
  276. aroundNode = this._aroundNode || this.domNode,
  277. self = this;
  278. // Prepare our popup's height and honor maxHeight if it exists.
  279. // TODO: isn't maxHeight dependent on the return value from dijit.popup.open(),
  280. // ie, dependent on how much space is available (BK)
  281. if(!this._preparedNode){
  282. this._preparedNode = true;
  283. // Check if we have explicitly set width and height on the dropdown widget dom node
  284. if(ddNode.style.width){
  285. this._explicitDDWidth = true;
  286. }
  287. if(ddNode.style.height){
  288. this._explicitDDHeight = true;
  289. }
  290. }
  291. // Code for resizing dropdown (height limitation, or increasing width to match my width)
  292. if(this.maxHeight || this.forceWidth || this.autoWidth){
  293. var myStyle = {
  294. display: "",
  295. visibility: "hidden"
  296. };
  297. if(!this._explicitDDWidth){
  298. myStyle.width = "";
  299. }
  300. if(!this._explicitDDHeight){
  301. myStyle.height = "";
  302. }
  303. dojo.style(ddNode, myStyle);
  304. // Figure out maximum height allowed (if there is a height restriction)
  305. var maxHeight = this.maxHeight;
  306. if(maxHeight == -1){
  307. // limit height to space available in viewport either above or below my domNode
  308. // (whichever side has more room)
  309. var viewport = dojo.window.getBox(),
  310. position = dojo.position(aroundNode, false);
  311. maxHeight = Math.floor(Math.max(position.y, viewport.h - (position.y + position.h)));
  312. }
  313. // Attach dropDown to DOM and make make visibility:hidden rather than display:none
  314. // so we call startup() and also get the size
  315. if(dropDown.startup && !dropDown._started){
  316. dropDown.startup();
  317. }
  318. dijit.popup.moveOffScreen(dropDown);
  319. // Get size of drop down, and determine if vertical scroll bar needed
  320. var mb = dojo._getMarginSize(ddNode);
  321. var overHeight = (maxHeight && mb.h > maxHeight);
  322. dojo.style(ddNode, {
  323. overflowX: "hidden",
  324. overflowY: overHeight ? "auto" : "hidden"
  325. });
  326. if(overHeight){
  327. mb.h = maxHeight;
  328. if("w" in mb){
  329. mb.w += 16; // room for vertical scrollbar
  330. }
  331. }else{
  332. delete mb.h;
  333. }
  334. // Adjust dropdown width to match or be larger than my width
  335. if(this.forceWidth){
  336. mb.w = aroundNode.offsetWidth;
  337. }else if(this.autoWidth){
  338. mb.w = Math.max(mb.w, aroundNode.offsetWidth);
  339. }else{
  340. delete mb.w;
  341. }
  342. // And finally, resize the dropdown to calculated height and width
  343. if(dojo.isFunction(dropDown.resize)){
  344. dropDown.resize(mb);
  345. }else{
  346. dojo.marginBox(ddNode, mb);
  347. }
  348. }
  349. var retVal = dijit.popup.open({
  350. parent: this,
  351. popup: dropDown,
  352. around: aroundNode,
  353. orient: dijit.getPopupAroundAlignment((this.dropDownPosition && this.dropDownPosition.length) ? this.dropDownPosition : ["below"],this.isLeftToRight()),
  354. onExecute: function(){
  355. self.closeDropDown(true);
  356. },
  357. onCancel: function(){
  358. self.closeDropDown(true);
  359. },
  360. onClose: function(){
  361. dojo.attr(self._popupStateNode, "popupActive", false);
  362. dojo.removeClass(self._popupStateNode, "dijitHasDropDownOpen");
  363. self._opened = false;
  364. }
  365. });
  366. dojo.attr(this._popupStateNode, "popupActive", "true");
  367. dojo.addClass(self._popupStateNode, "dijitHasDropDownOpen");
  368. this._opened=true;
  369. // TODO: set this.checked and call setStateClass(), to affect button look while drop down is shown
  370. return retVal;
  371. },
  372. closeDropDown: function(/*Boolean*/ focus){
  373. // summary:
  374. // Closes the drop down on this widget
  375. // focus:
  376. // If true, refocuses the button widget
  377. // tags:
  378. // protected
  379. if(this._opened){
  380. if(focus){ this.focus(); }
  381. dijit.popup.close(this.dropDown);
  382. this._opened = false;
  383. }
  384. }
  385. }
  386. );
  387. }