popup.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. define("dijit/popup", [
  2. "dojo/_base/array", // array.forEach array.some
  3. "dojo/aspect",
  4. "dojo/_base/connect", // connect._keypress
  5. "dojo/_base/declare", // declare
  6. "dojo/dom", // dom.isDescendant
  7. "dojo/dom-attr", // domAttr.set
  8. "dojo/dom-construct", // domConstruct.create domConstruct.destroy
  9. "dojo/dom-geometry", // domGeometry.isBodyLtr
  10. "dojo/dom-style", // domStyle.set
  11. "dojo/_base/event", // event.stop
  12. "dojo/has",
  13. "dojo/keys",
  14. "dojo/_base/lang", // lang.hitch
  15. "dojo/on",
  16. "dojo/_base/window", // win.body
  17. "./place",
  18. "./BackgroundIframe",
  19. "." // dijit (defining dijit.popup to match API doc)
  20. ], function(array, aspect, connect, declare, dom, domAttr, domConstruct, domGeometry, domStyle, event, has, keys, lang, on, win,
  21. place, BackgroundIframe, dijit){
  22. // module:
  23. // dijit/popup
  24. // summary:
  25. // Used to show drop downs (ex: the select list of a ComboBox)
  26. // or popups (ex: right-click context menus)
  27. /*=====
  28. dijit.popup.__OpenArgs = function(){
  29. // popup: Widget
  30. // widget to display
  31. // parent: Widget
  32. // the button etc. that is displaying this popup
  33. // around: DomNode
  34. // DOM node (typically a button); place popup relative to this node. (Specify this *or* "x" and "y" parameters.)
  35. // x: Integer
  36. // Absolute horizontal position (in pixels) to place node at. (Specify this *or* "around" parameter.)
  37. // y: Integer
  38. // Absolute vertical position (in pixels) to place node at. (Specify this *or* "around" parameter.)
  39. // orient: Object|String
  40. // When the around parameter is specified, orient should be a list of positions to try, ex:
  41. // | [ "below", "above" ]
  42. // For backwards compatibility it can also be an (ordered) hash of tuples of the form
  43. // (around-node-corner, popup-node-corner), ex:
  44. // | { "BL": "TL", "TL": "BL" }
  45. // where BL means "bottom left" and "TL" means "top left", etc.
  46. //
  47. // dijit.popup.open() tries to position the popup according to each specified position, in order,
  48. // until the popup appears fully within the viewport.
  49. //
  50. // The default value is ["below", "above"]
  51. //
  52. // When an (x,y) position is specified rather than an around node, orient is either
  53. // "R" or "L". R (for right) means that it tries to put the popup to the right of the mouse,
  54. // specifically positioning the popup's top-right corner at the mouse position, and if that doesn't
  55. // fit in the viewport, then it tries, in order, the bottom-right corner, the top left corner,
  56. // and the top-right corner.
  57. // onCancel: Function
  58. // callback when user has canceled the popup by
  59. // 1. hitting ESC or
  60. // 2. by using the popup widget's proprietary cancel mechanism (like a cancel button in a dialog);
  61. // i.e. whenever popupWidget.onCancel() is called, args.onCancel is called
  62. // onClose: Function
  63. // callback whenever this popup is closed
  64. // onExecute: Function
  65. // callback when user "executed" on the popup/sub-popup by selecting a menu choice, etc. (top menu only)
  66. // padding: dijit.__Position
  67. // adding a buffer around the opening position. This is only useful when around is not set.
  68. this.popup = popup;
  69. this.parent = parent;
  70. this.around = around;
  71. this.x = x;
  72. this.y = y;
  73. this.orient = orient;
  74. this.onCancel = onCancel;
  75. this.onClose = onClose;
  76. this.onExecute = onExecute;
  77. this.padding = padding;
  78. }
  79. =====*/
  80. /*=====
  81. dijit.popup = {
  82. // summary:
  83. // Used to show drop downs (ex: the select list of a ComboBox)
  84. // or popups (ex: right-click context menus).
  85. //
  86. // Access via require(["dijit/popup"], function(popup){ ... }).
  87. moveOffScreen: function(widget){
  88. // summary:
  89. // Moves the popup widget off-screen.
  90. // Do not use this method to hide popups when not in use, because
  91. // that will create an accessibility issue: the offscreen popup is
  92. // still in the tabbing order.
  93. // widget: dijit._WidgetBase
  94. // The widget
  95. },
  96. hide: function(widget){
  97. // summary:
  98. // Hide this popup widget (until it is ready to be shown).
  99. // Initialization for widgets that will be used as popups
  100. //
  101. // Also puts widget inside a wrapper DIV (if not already in one)
  102. //
  103. // If popup widget needs to layout it should
  104. // do so when it is made visible, and popup._onShow() is called.
  105. // widget: dijit._WidgetBase
  106. // The widget
  107. },
  108. open: function(args){
  109. // summary:
  110. // Popup the widget at the specified position
  111. // example:
  112. // opening at the mouse position
  113. // | popup.open({popup: menuWidget, x: evt.pageX, y: evt.pageY});
  114. // example:
  115. // opening the widget as a dropdown
  116. // | popup.open({parent: this, popup: menuWidget, around: this.domNode, onClose: function(){...}});
  117. //
  118. // Note that whatever widget called dijit.popup.open() should also listen to its own _onBlur callback
  119. // (fired from _base/focus.js) to know that focus has moved somewhere else and thus the popup should be closed.
  120. // args: dijit.popup.__OpenArgs
  121. // Parameters
  122. return {}; // Object specifying which position was chosen
  123. },
  124. close: function(popup){
  125. // summary:
  126. // Close specified popup and any popups that it parented.
  127. // If no popup is specified, closes all popups.
  128. // widget: dijit._WidgetBase?
  129. // The widget, optional
  130. }
  131. };
  132. =====*/
  133. function destroyWrapper(){
  134. // summary:
  135. // Function to destroy wrapper when popup widget is destroyed.
  136. // Left in this scope to avoid memory leak on IE8 on refresh page, see #15206.
  137. if(this._popupWrapper){
  138. domConstruct.destroy(this._popupWrapper);
  139. delete this._popupWrapper;
  140. }
  141. }
  142. var PopupManager = declare(null, {
  143. // _stack: dijit._Widget[]
  144. // Stack of currently popped up widgets.
  145. // (someone opened _stack[0], and then it opened _stack[1], etc.)
  146. _stack: [],
  147. // _beginZIndex: Number
  148. // Z-index of the first popup. (If first popup opens other
  149. // popups they get a higher z-index.)
  150. _beginZIndex: 1000,
  151. _idGen: 1,
  152. _createWrapper: function(/*Widget*/ widget){
  153. // summary:
  154. // Initialization for widgets that will be used as popups.
  155. // Puts widget inside a wrapper DIV (if not already in one),
  156. // and returns pointer to that wrapper DIV.
  157. var wrapper = widget._popupWrapper,
  158. node = widget.domNode;
  159. if(!wrapper){
  160. // Create wrapper <div> for when this widget [in the future] will be used as a popup.
  161. // This is done early because of IE bugs where creating/moving DOM nodes causes focus
  162. // to go wonky, see tests/robot/Toolbar.html to reproduce
  163. wrapper = domConstruct.create("div", {
  164. "class":"dijitPopup",
  165. style:{ display: "none"},
  166. role: "presentation"
  167. }, win.body());
  168. wrapper.appendChild(node);
  169. var s = node.style;
  170. s.display = "";
  171. s.visibility = "";
  172. s.position = "";
  173. s.top = "0px";
  174. widget._popupWrapper = wrapper;
  175. aspect.after(widget, "destroy", destroyWrapper, true);
  176. }
  177. return wrapper;
  178. },
  179. moveOffScreen: function(/*Widget*/ widget){
  180. // summary:
  181. // Moves the popup widget off-screen.
  182. // Do not use this method to hide popups when not in use, because
  183. // that will create an accessibility issue: the offscreen popup is
  184. // still in the tabbing order.
  185. // Create wrapper if not already there
  186. var wrapper = this._createWrapper(widget);
  187. domStyle.set(wrapper, {
  188. visibility: "hidden",
  189. top: "-9999px", // prevent transient scrollbar causing misalign (#5776), and initial flash in upper left (#10111)
  190. display: ""
  191. });
  192. },
  193. hide: function(/*Widget*/ widget){
  194. // summary:
  195. // Hide this popup widget (until it is ready to be shown).
  196. // Initialization for widgets that will be used as popups
  197. //
  198. // Also puts widget inside a wrapper DIV (if not already in one)
  199. //
  200. // If popup widget needs to layout it should
  201. // do so when it is made visible, and popup._onShow() is called.
  202. // Create wrapper if not already there
  203. var wrapper = this._createWrapper(widget);
  204. domStyle.set(wrapper, "display", "none");
  205. },
  206. getTopPopup: function(){
  207. // summary:
  208. // Compute the closest ancestor popup that's *not* a child of another popup.
  209. // Ex: For a TooltipDialog with a button that spawns a tree of menus, find the popup of the button.
  210. var stack = this._stack;
  211. for(var pi=stack.length-1; pi > 0 && stack[pi].parent === stack[pi-1].widget; pi--){
  212. /* do nothing, just trying to get right value for pi */
  213. }
  214. return stack[pi];
  215. },
  216. open: function(/*dijit.popup.__OpenArgs*/ args){
  217. // summary:
  218. // Popup the widget at the specified position
  219. //
  220. // example:
  221. // opening at the mouse position
  222. // | popup.open({popup: menuWidget, x: evt.pageX, y: evt.pageY});
  223. //
  224. // example:
  225. // opening the widget as a dropdown
  226. // | popup.open({parent: this, popup: menuWidget, around: this.domNode, onClose: function(){...}});
  227. //
  228. // Note that whatever widget called dijit.popup.open() should also listen to its own _onBlur callback
  229. // (fired from _base/focus.js) to know that focus has moved somewhere else and thus the popup should be closed.
  230. var stack = this._stack,
  231. widget = args.popup,
  232. orient = args.orient || ["below", "below-alt", "above", "above-alt"],
  233. ltr = args.parent ? args.parent.isLeftToRight() : domGeometry.isBodyLtr(),
  234. around = args.around,
  235. id = (args.around && args.around.id) ? (args.around.id+"_dropdown") : ("popup_"+this._idGen++);
  236. // If we are opening a new popup that isn't a child of a currently opened popup, then
  237. // close currently opened popup(s). This should happen automatically when the old popups
  238. // gets the _onBlur() event, except that the _onBlur() event isn't reliable on IE, see [22198].
  239. while(stack.length && (!args.parent || !dom.isDescendant(args.parent.domNode, stack[stack.length-1].widget.domNode))){
  240. this.close(stack[stack.length-1].widget);
  241. }
  242. // Get pointer to popup wrapper, and create wrapper if it doesn't exist
  243. var wrapper = this._createWrapper(widget);
  244. domAttr.set(wrapper, {
  245. id: id,
  246. style: {
  247. zIndex: this._beginZIndex + stack.length
  248. },
  249. "class": "dijitPopup " + (widget.baseClass || widget["class"] || "").split(" ")[0] +"Popup",
  250. dijitPopupParent: args.parent ? args.parent.id : ""
  251. });
  252. if(has("bgIframe") && !widget.bgIframe){
  253. // setting widget.bgIframe triggers cleanup in _Widget.destroy()
  254. widget.bgIframe = new BackgroundIframe(wrapper);
  255. }
  256. // position the wrapper node and make it visible
  257. var best = around ?
  258. place.around(wrapper, around, orient, ltr, widget.orient ? lang.hitch(widget, "orient") : null) :
  259. place.at(wrapper, args, orient == 'R' ? ['TR','BR','TL','BL'] : ['TL','BL','TR','BR'], args.padding);
  260. wrapper.style.display = "";
  261. wrapper.style.visibility = "visible";
  262. widget.domNode.style.visibility = "visible"; // counteract effects from _HasDropDown
  263. var handlers = [];
  264. // provide default escape and tab key handling
  265. // (this will work for any widget, not just menu)
  266. handlers.push(on(wrapper, connect._keypress, lang.hitch(this, function(evt){
  267. if(evt.charOrCode == keys.ESCAPE && args.onCancel){
  268. event.stop(evt);
  269. args.onCancel();
  270. }else if(evt.charOrCode === keys.TAB){
  271. event.stop(evt);
  272. var topPopup = this.getTopPopup();
  273. if(topPopup && topPopup.onCancel){
  274. topPopup.onCancel();
  275. }
  276. }
  277. })));
  278. // watch for cancel/execute events on the popup and notify the caller
  279. // (for a menu, "execute" means clicking an item)
  280. if(widget.onCancel && args.onCancel){
  281. handlers.push(widget.on("cancel", args.onCancel));
  282. }
  283. handlers.push(widget.on(widget.onExecute ? "execute" : "change", lang.hitch(this, function(){
  284. var topPopup = this.getTopPopup();
  285. if(topPopup && topPopup.onExecute){
  286. topPopup.onExecute();
  287. }
  288. })));
  289. stack.push({
  290. widget: widget,
  291. parent: args.parent,
  292. onExecute: args.onExecute,
  293. onCancel: args.onCancel,
  294. onClose: args.onClose,
  295. handlers: handlers
  296. });
  297. if(widget.onOpen){
  298. // TODO: in 2.0 standardize onShow() (used by StackContainer) and onOpen() (used here)
  299. widget.onOpen(best);
  300. }
  301. return best;
  302. },
  303. close: function(/*Widget?*/ popup){
  304. // summary:
  305. // Close specified popup and any popups that it parented.
  306. // If no popup is specified, closes all popups.
  307. var stack = this._stack;
  308. // Basically work backwards from the top of the stack closing popups
  309. // until we hit the specified popup, but IIRC there was some issue where closing
  310. // a popup would cause others to close too. Thus if we are trying to close B in [A,B,C]
  311. // closing C might close B indirectly and then the while() condition will run where stack==[A]...
  312. // so the while condition is constructed defensively.
  313. while((popup && array.some(stack, function(elem){return elem.widget == popup;})) ||
  314. (!popup && stack.length)){
  315. var top = stack.pop(),
  316. widget = top.widget,
  317. onClose = top.onClose;
  318. if(widget.onClose){
  319. // TODO: in 2.0 standardize onHide() (used by StackContainer) and onClose() (used here)
  320. widget.onClose();
  321. }
  322. var h;
  323. while(h = top.handlers.pop()){ h.remove(); }
  324. // Hide the widget and it's wrapper unless it has already been destroyed in above onClose() etc.
  325. if(widget && widget.domNode){
  326. this.hide(widget);
  327. }
  328. if(onClose){
  329. onClose();
  330. }
  331. }
  332. }
  333. });
  334. return (dijit.popup = new PopupManager());
  335. });