You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
441 lines
16 KiB
441 lines
16 KiB
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) |
|
"dojo/touch" // use of dojoClick |
|
], 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 = (parseFloat(style.top) + dy) + "px"; |
|
if(style.right == "auto"){ |
|
style.left = (parseFloat(style.left) + dx) + "px"; |
|
}else{ |
|
style.right = (parseFloat(style.right) - 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 <div> 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); |
|
|
|
// Workaround iOS problem where clicking a Menu can focus an <input> (or click a button) behind it. |
|
// Need to be careful though that you can still focus <input>'s and click <button>'s in a TooltipDialog. |
|
// Also, be careful not to break (native) scrolling of dropdown like ComboBox's options list. |
|
if("ontouchend" in document) { |
|
on(wrapper, "touchend", function (evt){ |
|
if(!/^(input|button|textarea)$/i.test(evt.target.tagName)) { |
|
evt.preventDefault(); |
|
} |
|
}); |
|
} |
|
|
|
// Calling evt.preventDefault() suppresses the native click event on most browsers. However, it doesn't |
|
// suppress the synthetic click event emitted by dojo/touch. In order for clicks in popups to work |
|
// consistently, always use dojo/touch in popups. See #18150. |
|
wrapper.dojoClick = 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, |
|
overflowY: "visible", // and set overflowY to "auto". |
|
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 <table> (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.bgIframe) { |
|
// push the iframe back onto the stack. |
|
widget.bgIframe.destroy(); |
|
delete widget.bgIframe; |
|
} |
|
|
|
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()); |
|
});
|
|
|