_HasDropDown.js.uncompressed.js 17 KB

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