define([ "dojo/_base/array", // array.forEach array.some "dojo/aspect", "dojo/_base/declare", // declare "dojo/dom", // dom.isDescendant "dojo/dom-attr", // domAttr.set "dojo/dom-construct", // domConstruct.create domConstruct.destroy "dojo/dom-geometry", // domGeometry.isBodyLtr "dojo/dom-style", // domStyle.set "dojo/has", // has("config-bgIframe") "dojo/keys", "dojo/_base/lang", // lang.hitch "dojo/on", "./place", "./BackgroundIframe", "./Viewport", "./main" // dijit (defining dijit.popup to match API doc) ], function(array, aspect, declare, dom, domAttr, domConstruct, domGeometry, domStyle, has, keys, lang, on, place, BackgroundIframe, Viewport, dijit){ // module: // dijit/popup /*===== var __OpenArgs = { // popup: Widget // widget to display // parent: Widget // the button etc. that is displaying this popup // around: DomNode // DOM node (typically a button); place popup relative to this node. (Specify this *or* "x" and "y" parameters.) // x: Integer // Absolute horizontal position (in pixels) to place node at. (Specify this *or* "around" parameter.) // y: Integer // Absolute vertical position (in pixels) to place node at. (Specify this *or* "around" parameter.) // orient: Object|String // When the around parameter is specified, orient should be a list of positions to try, ex: // | [ "below", "above" ] // For backwards compatibility it can also be an (ordered) hash of tuples of the form // (around-node-corner, popup-node-corner), ex: // | { "BL": "TL", "TL": "BL" } // where BL means "bottom left" and "TL" means "top left", etc. // // dijit/popup.open() tries to position the popup according to each specified position, in order, // until the popup appears fully within the viewport. // // The default value is ["below", "above"] // // When an (x,y) position is specified rather than an around node, orient is either // "R" or "L". R (for right) means that it tries to put the popup to the right of the mouse, // specifically positioning the popup's top-right corner at the mouse position, and if that doesn't // fit in the viewport, then it tries, in order, the bottom-right corner, the top left corner, // and the top-right corner. // onCancel: Function // callback when user has canceled the popup by: // // 1. hitting ESC or // 2. by using the popup widget's proprietary cancel mechanism (like a cancel button in a dialog); // i.e. whenever popupWidget.onCancel() is called, args.onCancel is called // onClose: Function // callback whenever this popup is closed // onExecute: Function // callback when user "executed" on the popup/sub-popup by selecting a menu choice, etc. (top menu only) // padding: place.__Position // adding a buffer around the opening position. This is only useful when around is not set. // maxHeight: Integer // The max height for the popup. Any popup taller than this will have scrollbars. // Set to Infinity for no max height. Default is to limit height to available space in viewport, // above or below the aroundNode or specified x/y position. }; =====*/ function destroyWrapper(){ // summary: // Function to destroy wrapper when popup widget is destroyed. // Left in this scope to avoid memory leak on IE8 on refresh page, see #15206. if(this._popupWrapper){ domConstruct.destroy(this._popupWrapper); delete this._popupWrapper; } } var PopupManager = declare(null, { // summary: // Used to show drop downs (ex: the select list of a ComboBox) // or popups (ex: right-click context menus). // _stack: dijit/_WidgetBase[] // Stack of currently popped up widgets. // (someone opened _stack[0], and then it opened _stack[1], etc.) _stack: [], // _beginZIndex: Number // Z-index of the first popup. (If first popup opens other // popups they get a higher z-index.) _beginZIndex: 1000, _idGen: 1, _repositionAll: function(){ // summary: // If screen has been scrolled, reposition all the popups in the stack. // Then set timer to check again later. if(this._firstAroundNode){ // guard for when clearTimeout() on IE doesn't work var oldPos = this._firstAroundPosition, newPos = domGeometry.position(this._firstAroundNode, true), dx = newPos.x - oldPos.x, dy = newPos.y - oldPos.y; if(dx || dy){ this._firstAroundPosition = newPos; for(var i = 0; i < this._stack.length; i++){ var style = this._stack[i].wrapper.style; style.top = (parseInt(style.top, 10) + dy) + "px"; if(style.right == "auto"){ style.left = (parseInt(style.left, 10) + dx) + "px"; }else{ style.right = (parseInt(style.right, 10) - dx) + "px"; } } } this._aroundMoveListener = setTimeout(lang.hitch(this, "_repositionAll"), dx || dy ? 10 : 50); } }, _createWrapper: function(/*Widget*/ widget){ // summary: // Initialization for widgets that will be used as popups. // Puts widget inside a wrapper DIV (if not already in one), // and returns pointer to that wrapper DIV. var wrapper = widget._popupWrapper, node = widget.domNode; if(!wrapper){ // Create wrapper
for when this widget [in the future] will be used as a popup. // This is done early because of IE bugs where creating/moving DOM nodes causes focus // to go wonky, see tests/robot/Toolbar.html to reproduce wrapper = domConstruct.create("div", { "class": "dijitPopup", style: { display: "none"}, role: "region", "aria-label": widget["aria-label"] || widget.label || widget.name || widget.id }, widget.ownerDocumentBody); wrapper.appendChild(node); var s = node.style; s.display = ""; s.visibility = ""; s.position = ""; s.top = "0px"; widget._popupWrapper = wrapper; aspect.after(widget, "destroy", destroyWrapper, true); } return wrapper; }, moveOffScreen: function(/*Widget*/ widget){ // summary: // Moves the popup widget off-screen. // Do not use this method to hide popups when not in use, because // that will create an accessibility issue: the offscreen popup is // still in the tabbing order. // Create wrapper if not already there var wrapper = this._createWrapper(widget); // Besides setting visibility:hidden, move it out of the viewport, see #5776, #10111, #13604 var ltr = domGeometry.isBodyLtr(widget.ownerDocument), style = { visibility: "hidden", top: "-9999px", display: "" }; style[ltr ? "left" : "right"] = "-9999px"; style[ltr ? "right" : "left"] = "auto"; domStyle.set(wrapper, style); return wrapper; }, hide: function(/*Widget*/ widget){ // summary: // Hide this popup widget (until it is ready to be shown). // Initialization for widgets that will be used as popups // // Also puts widget inside a wrapper DIV (if not already in one) // // If popup widget needs to layout it should // do so when it is made visible, and popup._onShow() is called. // Create wrapper if not already there var wrapper = this._createWrapper(widget); domStyle.set(wrapper, { display: "none", height: "auto", // Open may have limited the height to fit in the viewport overflow: "visible", border: "" // Open() may have moved border from popup to wrapper. }); // Open() may have moved border from popup to wrapper. Move it back. var node = widget.domNode; if("_originalStyle" in node){ node.style.cssText = node._originalStyle; } }, getTopPopup: function(){ // summary: // Compute the closest ancestor popup that's *not* a child of another popup. // Ex: For a TooltipDialog with a button that spawns a tree of menus, find the popup of the button. var stack = this._stack; for(var pi = stack.length - 1; pi > 0 && stack[pi].parent === stack[pi - 1].widget; pi--){ /* do nothing, just trying to get right value for pi */ } return stack[pi]; }, open: function(/*__OpenArgs*/ args){ // summary: // Popup the widget at the specified position // // example: // opening at the mouse position // | popup.open({popup: menuWidget, x: evt.pageX, y: evt.pageY}); // // example: // opening the widget as a dropdown // | popup.open({parent: this, popup: menuWidget, around: this.domNode, onClose: function(){...}}); // // Note that whatever widget called dijit/popup.open() should also listen to its own _onBlur callback // (fired from _base/focus.js) to know that focus has moved somewhere else and thus the popup should be closed. var stack = this._stack, widget = args.popup, node = widget.domNode, orient = args.orient || ["below", "below-alt", "above", "above-alt"], ltr = args.parent ? args.parent.isLeftToRight() : domGeometry.isBodyLtr(widget.ownerDocument), around = args.around, id = (args.around && args.around.id) ? (args.around.id + "_dropdown") : ("popup_" + this._idGen++); // If we are opening a new popup that isn't a child of a currently opened popup, then // close currently opened popup(s). This should happen automatically when the old popups // gets the _onBlur() event, except that the _onBlur() event isn't reliable on IE, see [22198]. while(stack.length && (!args.parent || !dom.isDescendant(args.parent.domNode, stack[stack.length - 1].widget.domNode))){ this.close(stack[stack.length - 1].widget); } // Get pointer to popup wrapper, and create wrapper if it doesn't exist. Remove display:none (but keep // off screen) so we can do sizing calculations. var wrapper = this.moveOffScreen(widget); if(widget.startup && !widget._started){ widget.startup(); // this has to be done after being added to the DOM } // Limit height to space available in viewport either above or below aroundNode (whichever side has more // room), adding scrollbar if necessary. Can't add scrollbar to widget because it may be a (ex: // dijit/Menu), so add to wrapper, and then move popup's border to wrapper so scroll bar inside border. var maxHeight, popupSize = domGeometry.position(node); if("maxHeight" in args && args.maxHeight != -1){ maxHeight = args.maxHeight || Infinity; // map 0 --> infinity for back-compat of _HasDropDown.maxHeight }else{ var viewport = Viewport.getEffectiveBox(this.ownerDocument), aroundPos = around ? domGeometry.position(around, false) : {y: args.y - (args.padding||0), h: (args.padding||0) * 2}; maxHeight = Math.floor(Math.max(aroundPos.y, viewport.h - (aroundPos.y + aroundPos.h))); } if(popupSize.h > maxHeight){ // Get style of popup's border. Unfortunately domStyle.get(node, "border") doesn't work on FF or IE, // and domStyle.get(node, "borderColor") etc. doesn't work on FF, so need to use fully qualified names. var cs = domStyle.getComputedStyle(node), borderStyle = cs.borderLeftWidth + " " + cs.borderLeftStyle + " " + cs.borderLeftColor; domStyle.set(wrapper, { overflowY: "scroll", height: maxHeight + "px", border: borderStyle // so scrollbar is inside border }); node._originalStyle = node.style.cssText; node.style.border = "none"; } domAttr.set(wrapper, { id: id, style: { zIndex: this._beginZIndex + stack.length }, "class": "dijitPopup " + (widget.baseClass || widget["class"] || "").split(" ")[0] + "Popup", dijitPopupParent: args.parent ? args.parent.id : "" }); if(stack.length == 0 && around){ // First element on stack. Save position of aroundNode and setup listener for changes to that position. this._firstAroundNode = around; this._firstAroundPosition = domGeometry.position(around, true); this._aroundMoveListener = setTimeout(lang.hitch(this, "_repositionAll"), 50); } if(has("config-bgIframe") && !widget.bgIframe){ // setting widget.bgIframe triggers cleanup in _WidgetBase.destroyRendering() widget.bgIframe = new BackgroundIframe(wrapper); } // position the wrapper node and make it visible var layoutFunc = widget.orient ? lang.hitch(widget, "orient") : null, best = around ? place.around(wrapper, around, orient, ltr, layoutFunc) : place.at(wrapper, args, orient == 'R' ? ['TR', 'BR', 'TL', 'BL'] : ['TL', 'BL', 'TR', 'BR'], args.padding, layoutFunc); wrapper.style.visibility = "visible"; node.style.visibility = "visible"; // counteract effects from _HasDropDown var handlers = []; // provide default escape and tab key handling // (this will work for any widget, not just menu) handlers.push(on(wrapper, "keydown", lang.hitch(this, function(evt){ if(evt.keyCode == keys.ESCAPE && args.onCancel){ evt.stopPropagation(); evt.preventDefault(); args.onCancel(); }else if(evt.keyCode == keys.TAB){ evt.stopPropagation(); evt.preventDefault(); var topPopup = this.getTopPopup(); if(topPopup && topPopup.onCancel){ topPopup.onCancel(); } } }))); // watch for cancel/execute events on the popup and notify the caller // (for a menu, "execute" means clicking an item) if(widget.onCancel && args.onCancel){ handlers.push(widget.on("cancel", args.onCancel)); } handlers.push(widget.on(widget.onExecute ? "execute" : "change", lang.hitch(this, function(){ var topPopup = this.getTopPopup(); if(topPopup && topPopup.onExecute){ topPopup.onExecute(); } }))); stack.push({ widget: widget, wrapper: wrapper, parent: args.parent, onExecute: args.onExecute, onCancel: args.onCancel, onClose: args.onClose, handlers: handlers }); if(widget.onOpen){ // TODO: in 2.0 standardize onShow() (used by StackContainer) and onOpen() (used here) widget.onOpen(best); } return best; }, close: function(/*Widget?*/ popup){ // summary: // Close specified popup and any popups that it parented. // If no popup is specified, closes all popups. var stack = this._stack; // Basically work backwards from the top of the stack closing popups // until we hit the specified popup, but IIRC there was some issue where closing // a popup would cause others to close too. Thus if we are trying to close B in [A,B,C] // closing C might close B indirectly and then the while() condition will run where stack==[A]... // so the while condition is constructed defensively. while((popup && array.some(stack, function(elem){ return elem.widget == popup; })) || (!popup && stack.length)){ var top = stack.pop(), widget = top.widget, onClose = top.onClose; if(widget.onClose){ // TODO: in 2.0 standardize onHide() (used by StackContainer) and onClose() (used here). // Actually, StackContainer also calls onClose(), but to mean that the pane is being deleted // (i.e. that the TabContainer's tab's [x] icon was clicked) widget.onClose(); } var h; while(h = top.handlers.pop()){ h.remove(); } // Hide the widget and it's wrapper unless it has already been destroyed in above onClose() etc. if(widget && widget.domNode){ this.hide(widget); } if(onClose){ onClose(); } } if(stack.length == 0 && this._aroundMoveListener){ clearTimeout(this._aroundMoveListener); this._firstAroundNode = this._firstAroundPosition = this._aroundMoveListener = null; } } }); return (dijit.popup = new PopupManager()); });