popup.js 13 KB

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