_HasDropDown.js 17 KB

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