@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,390 lines (1,211 loc) • 128 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2009-2023 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
/* global Set, HTMLElement */
// Provides helper class sap.ui.core.Popup
sap.ui.define([
'sap/ui/Device',
'sap/ui/base/Event',
'sap/ui/base/ManagedObject',
'sap/ui/base/Object',
'sap/ui/base/ObjectPool',
'./Control',
'./Element',
'./FocusHandler',
'./IntervalTrigger',
'./RenderManager',
'./ResizeHandler',
'./UIArea',
'./library',
"sap/base/assert",
"sap/base/Log",
"sap/base/util/Version",
"sap/base/util/uid",
"sap/base/util/extend",
"sap/base/util/each",
"sap/base/util/deepExtend",
"sap/ui/thirdparty/jquery",
"sap/ui/events/F6Navigation",
"sap/ui/events/isMouseEventDelayed",
"sap/ui/base/EventProvider",
"sap/ui/core/Configuration",
"sap/ui/dom/jquery/control", // jQuery Plugin "control"
"sap/ui/dom/jquery/Focusable", // jQuery Plugin "firstFocusableDomRef"
"sap/ui/dom/jquery/rect" // jQuery Plugin "rect"
], function(
Device,
Event,
ManagedObject,
BaseObject,
ObjectPool,
Control,
Element,
FocusHandler,
IntervalTrigger,
RenderManager,
ResizeHandler,
UIArea,
library,
assert,
Log,
Version,
uid,
extend,
each,
deepExtend,
jQuery,
F6Navigation,
isMouseEventDelayed,
EventProvider,
Configuration
//control
//Focusable
//rect
) {
"use strict";
// shortcut for sap.ui.core.CSSSize
var CSSSize = library.CSSSize;
// shortcut for sap.ui.core.OpenState
var OpenState = library.OpenState;
// shortcut for sap.ui.core.Collision
var Collision = library.Collision;
var oStaticUIArea;
var vGlobalWithinArea;
var oResizeObserver;
var sResizeHandlerIdAttribute = "sapUiPopupResize";
if (window.ResizeObserver) {
oResizeObserver = new window.ResizeObserver(function(aEntries){
adaptSizeAndPosition(jQuery("#sap-ui-blocklayer-popup"), aEntries[0].target);
});
} else {
oResizeObserver = {
observe: function(oElement) {
var sHandlerId = oElement.dataset[sResizeHandlerIdAttribute];
if (!sHandlerId) {
sHandlerId = ResizeHandler.register(oElement, function(oEvent) {
adaptSizeAndPosition(jQuery("#sap-ui-blocklayer-popup"), oEvent.target);
});
oElement.dataset[sResizeHandlerIdAttribute] = sHandlerId;
}
},
unobserve: function(oElement) {
var sHandlerId = oElement.dataset[sResizeHandlerIdAttribute];
if (sHandlerId) {
ResizeHandler.deregister(sHandlerId);
delete oElement.dataset[sResizeHandlerIdAttribute];
}
}
};
}
function getStaticUIArea() {
if (oStaticUIArea) {
return oStaticUIArea;
}
var oStaticAreaRef, oControl;
try {
oStaticAreaRef = UIArea.getStaticAreaRef();
// only a facade of the static UIArea is returned that contains only the public methods
oStaticUIArea = UIArea.registry.get(oStaticAreaRef.id);
} catch (e) {
Log.error(e);
throw new Error("Popup cannot be opened because static UIArea cannot be determined.");
}
oControl = new Control();
oStaticUIArea.addDependent(oControl);
// get the real instance (not facade) of the static UIArea
oStaticUIArea = oControl.getUIArea();
oControl.destroy();
return oStaticUIArea;
}
/**
* Convert the different types of 'within' values to a DOM element or the window object.
*
* @param {string | sap.ui.core.Element | Element | Window} [vWithin] the given within
* @returns {Element | Window} the DOM element or Window object that represents the given within
* @private
*/
function convertWithin(vWithin) {
var oWithin;
if (typeof vWithin === "string") {
oWithin = document.querySelector(vWithin);
} else if (vWithin instanceof Element) {
oWithin = vWithin.getDomRef();
} else {
oWithin = vWithin;
}
return oWithin || window;
}
/**
* Remove the size and position CSS properties from the blocklayer DOM element
*
* @param {jQuery} $BlockRef the block layer jQuery object
* @private
*/
function clearSizeAndPosition($BlockRef) {
// left and top are set by jQuery.position
// width and height are set by function 'adaptSizeAndPosition'
// All of them need to be removed for being prepared to show a full screen blocklayer
var aProperties = ["left", "top", "width", "height"];
if ($BlockRef[0]) {
$BlockRef[0].classList.remove("sapUiBLyWithin");
aProperties.forEach(function(sProperty) {
$BlockRef[0].style.removeProperty(sProperty);
});
}
}
/**
* Change the size and position of the blocklayer to the same values as for the given 'within'.
*
* @param {jQuery} $BlockRef the jQuery object that contains the blocklayer DOM element
* @param {Element} oWithin the DOM element of the given within
* @private
*/
function adaptSizeAndPosition($BlockRef, oWithin) {
var oClientRect = oWithin.getBoundingClientRect();
$BlockRef
.css({
width: oClientRect.width,
height: oClientRect.height
})
.addClass("sapUiBLyWithin")
.position({
my: "left top",
at: "left top",
of: oWithin
});
}
/**
* Creates an instance of <code>sap.ui.core.Popup</code> that can be used to open controls as a Popup,
* visually appearing in front of other controls.
*
* @class Popup Class is a helper class for controls that want themselves or
* parts of themselves or even other aggregated or composed controls
* or plain HTML content to popup on the screen like menus, dialogs, drop down boxes.
*
* It allows the controls to be aligned to other DOM elements
* using the {@link sap.ui.core.Popup.Dock} method. With it you can define where
* the popup should be docked. One can dock the popup to the top, bottom, left or right side
* of another DOM element.
*
* In the case that the popup has no space to show itself in the view port
* of the current window, it tries to open itself to the inverted direction.
*
* <strong>Since 1.12.3</strong>, it is possible to add further DOM-element-IDs that can get the focus when
* <code>autoclose</code> or <code>modal</code> is enabled. E.g. the <code>RichTextEditor</code> with running
* TinyMCE uses this method to be able to focus the popups of the TinyMCE if the <code>RichTextEditor</code> runs
* within a <code>Popup</code>/<code>Dialog</code> etc.
*
* To provide an additional DOM element that can get the focus the following should be done:
* <pre>
* // create an object with the corresponding DOM-ID
* var oObject = {
* id : "this_is_the_most_valuable_id_of_the_DOM_element"
* };
*
* // add the event prefix for adding an element to the ID of the corresponding Popup
* var sEventId = "sap.ui.core.Popup.addFocusableContent-" + oPopup.getId();
*
* // fire the event with the created event-ID and the object with the DOM-ID
* sap.ui.getCore().getEventBus().publish("sap.ui", sEventId, oObject);
* </pre>
*
* <strong>Since 1.75</strong>, DOM elements which have the attribute
* <code>data-sap-ui-integration-popup-content</code> are considered to be part of all opened popups. Those DOM
* elements can get the focus without causing the autoclose popup to be closed or the modal popup to take the focus
* back to itself. Additionally, a further DOM query selector can be provided by using
* {@link sap.ui.core.Popup.addExternalContent} to make the DOM elements which match the selector be considered as
* part of all opened popups. Please be aware that the Popup implementation only checks if a DOM element is marked
* with the attribute <code>data-sap-ui-integration-popup-content</code>. The actual attribute value is not checked. To
* prevent a DOM element from matching, you must remove the attribute itself. Setting the attribute to a falsy value
* is not enough in this case.
*
* @param {sap.ui.core.Control | sap.ui.core.Element | Element} oContent the content to render in the popup. In case of sap.ui.core.Element or DOMNode, the content must be present in the page (i.e. rendered). In case of sap.ui.core.Control, the Popup ensures rendering before opening.
* @param {boolean} [bModal=false] whether the popup should be opened in a modal way (i.e. with blocking background). Setting this to "true" effectively blocks all attempts to focus content outside the modal popup. A modal popup also automatically sets the focus back to whatever was focused when the popup opened.
* @param {boolean} [bShadow=true] whether the popup should be have a visual shadow underneath (shadow appearance depends on active theme and browser support)
* @param {boolean} [bAutoClose=false] whether the popup should automatically close when the focus moves out of the popup
*
* @public
* @class
* @alias sap.ui.core.Popup
* @extends sap.ui.base.ManagedObject
*/
var Popup = ManagedObject.extend("sap.ui.core.Popup", {
constructor: function (oContent, bModal, bShadow, bAutoClose) {
assert(arguments.length == 0 || (oContent && typeof oContent === "object"), "oContent must be an object or there may be no arguments at all");
assert((bModal === undefined || bModal === true || bModal === false), "bModal must be true, false, or undefined");
assert((bShadow === undefined || bShadow === true || bShadow === false), "bShadow must be true, false, or undefined");
assert((bAutoClose === undefined || bAutoClose === true || bAutoClose === false), "bAutoClose must be true, false, or undefined");
ManagedObject.apply(this);
this._popupUID = uid(); // internal ID to make event handlers unique
this.bOpen = false; // true exactly if the Popup is opening, open, or closing
this.eOpenState = OpenState.CLOSED;
this._mEvents = {};
this._mEvents["sap.ui.core.Popup.addFocusableContent-" + this._popupUID] = this._addFocusableArea;
this._mEvents["sap.ui.core.Popup.removeFocusableContent-" + this._popupUID] = this._removeFocusableArea;
this._mEvents["sap.ui.core.Popup.closePopup-" + this._popupUID] = this._closePopup;
this._mEvents["sap.ui.core.Popup.onFocusEvent-" + this._popupUID] = this.onFocusEvent;
this._mEvents["sap.ui.core.Popup.increaseZIndex-" + this._popupUID] = this._increaseMyZIndex;
this._mEvents["sap.ui.core.Popup.contains-" + this._popupUID] = this._containsEventBusWrapper;
this._mEvents["sap.ui.core.Popup.extendFocusInfo-" + this._popupUID] = this._extendFocusInfoEventBusWrapper;
if (oContent) {
this.setContent(oContent);
}
this._oDefaultPosition = {
my: Popup.Dock.CenterCenter,
at: Popup.Dock.CenterCenter,
of: document,
offset: "0 0",
collision: "flip"
};
this._oPosition = Object.assign({},this._oDefaultPosition);
this._bModal = !!bModal;
this._oPreviousFocus = null;
this._sInitialFocusId = null;
this._bShadow = typeof (bShadow) === "boolean" ? bShadow : true;
this._bAutoClose = !!bAutoClose;
this._animations = { open: null, close: null };
this._durations = { open: "fast", close: "fast" };
this._iZIndex = -1;
this.setNavigationMode();
//autoclose handler for mobile or desktop browser in touch mode
//this function needs to be put onto the instance other than the prototype because functions on the prototype are treated as same function and can't be bound twice.
if (this.touchEnabled) {
this._fAutoCloseHandler = function(oEvent) {
// Suppress the delayed mouse event from mobile browser
if (oEvent.isMarked("delayedMouseEvent") || oEvent.isMarked("cancelAutoClose")) {
return;
}
// call the close handler only when it's fully opened
// this also prevents calling close while closing
if (this.eOpenState === OpenState.CLOSING || this.eOpenState === OpenState.CLOSED) {
return;
}
if (!this._contains(oEvent.target)) {
this.close();
}
}.bind(this);
}
this._F6NavigationHandler = function(oEvent) {
var oSettings = {},
sMode = this._sF6NavMode,
oDockElement;
// DOCK mode only possible for non-modal popups with valid dock element
if (sMode == "DOCK") {
if (this._bModal) {
sMode = "NONE";
} else if (this._oLastPosition && this._oLastPosition.of) {
oDockElement = this._getOfDom(this._oLastPosition.of);
if (!oDockElement || oDockElement === document ){
oDockElement = null;
sMode = "NONE";
}
}
}
// Define navigation settings according to specified mode
switch (sMode) {
case "SCOPE":
oSettings.scope = this._$()[0]; // Search scope for next F6 target is the popup itself
break;
case "DOCK":
oSettings.target = oDockElement; // Starting point for searching the next F6 target is the dock element
var $DockPopup = jQuery(oDockElement).parents("[data-sap-ui-popup]");
oSettings.scope = $DockPopup.length ? $DockPopup[0] : null; // Search scope is the parent popup (if exists, otherwise the document)
break;
default: //"NONE" and others
oSettings.skip = true; // Ignore the F6 key event
}
F6Navigation.handleF6GroupNavigation(oEvent, oSettings);
}.bind(this);
},
metadata : {
library: "sap.ui.core",
publicMethods : ["open", "close",
"setContent", "getContent",
"setPosition",
"setShadow", "setModal", "getModal", "setAutoClose", "setAutoCloseAreas", "setExtraContent",
"isOpen", "getAutoClose", "getOpenState", "setAnimations", "setDurations",
"attachOpened", "attachClosed", "detachOpened", "detachClosed"],
associations : {
"childPopups" : {
type : "sap.ui.core.Popup",
multiple : true,
visibility: "hidden"
}
},
events : {
"opened" : {},
"closed" : {}
}
}
});
Popup.prototype.getChildPopups = function() {
return this.getAssociation("childPopups", []);
};
Popup.prototype.addChildPopup = function(vChildPopup) {
return this.addAssociation("childPopups", vChildPopup);
};
Popup.prototype.removeChildPopup = function(vChildPopup) {
return this.removeAssociation("childPopups", vChildPopup);
};
// stack used for storing z-indices for blocklayer
Popup.blStack = [];
/**
* Enumeration providing options for docking of some element to another.
*
* "Right" and "Left" will stay the same in RTL mode, but "Begin" and "End" will flip to the other side ("Begin" is "Right" in RTL).
*
* @public
* @enum {string}
*/
Popup.Dock = {
/**
* @public
* @type {string}
*/
BeginTop : "begin top",
/**
* @public
* @type {string}
*/
BeginCenter : "begin center",
/**
* @public
* @type {string}
*/
BeginBottom : "begin bottom",
/**
* @public
* @type {string}
*/
LeftTop : "left top",
/**
* @public
* @type {string}
*/
LeftCenter : "left center",
/**
* @public
* @type {string}
*/
LeftBottom : "left bottom",
/**
* @public
* @type {string}
*/
CenterTop : "center top",
/**
* @public
* @type {string}
*/
CenterCenter : "center center",
/**
* @public
* @type {string}
*/
CenterBottom : "center bottom",
/**
* @public
* @type {string}
*/
RightTop : "right top",
/**
* @public
* @type {string}
*/
RightCenter : "right center",
/**
* @public
* @type {string}
*/
RightBottom : "right bottom",
/**
* @public
* @type {string}
*/
EndTop : "end top",
/**
* @public
* @type {string}
*/
EndCenter : "end center",
/**
* @public
* @type {string}
*/
EndBottom : "end bottom"
};
/**
* This property changes how the autoClose behaves on the Popup.
*
* When it's set to true, the Popup will be closed when tap outside of the Popup.
* Otherwise it will close as soon as the focus leaves the Popup.
*
* The default value of this property is determined by checking whether the devices supports touch event. The touch
* event is used in case the touch interface is the only source for handling user interaction by checking
* (!Device.system.combi). Since iPadOS 13, a desktop mode is introduced on iPad which sets this property with
* false. However, the "button" tag loses focus again after it's tapped in Safari browser which makes the
* "autoclose" feature in Popup behave wrongly. Therefore this property is always true in touch supported Safari
* browser.
*
* @static
* @type {boolean}
* @private
*/
Popup.prototype.touchEnabled = Device.support.touch && (Device.browser.safari || !Device.system.combi);
/**
* On mobile device, the browser may set the focus to somewhere else after
* the restoring of focus from Popup. This behavior should be prevented in
* order to make sure that the focus is restored to the right DOM element in
* mobile environment.
*
* @type {boolean}
* @private
*/
Popup.prototype.preventBrowserFocus = Device.support.touch && !Device.system.combi;
//****************************************************
// Layer et al.
//****************************************************
/**
* This constructor of the Popup.Layer class
*
* @class
* @private
* @name sap.ui.core.Popup.Layer
*/
BaseObject.extend("sap.ui.core.Popup.Layer", {
constructor: function() {
var sDomString = this.getDomString();
this._$Ref = jQuery(sDomString).appendTo(sap.ui.getCore().getStaticAreaRef());
}
});
/**
* Initializes the popup layer by adding z-index and visibility to the popup layer
* and insert the popup directly after the given <code>oRef</code> element
*
* @param {jQuery} oRef The element as a jQuery object
* @param {int} iZIndex The z-index value
* @private
* @name sap.ui.core.Popup.Layer#init
* @function
*/
Popup.Layer.prototype.init = function(oRef, iZIndex) {
this._$Ref.css({
"visibility" : "visible",
"z-index" : iZIndex
});
this.update(oRef, iZIndex);
this._$Ref.insertAfter(oRef).show();
};
/**
* Update the popup layer with the given <code>iZIndex</code> on the given <code>oRef</code> element
*
* @param {jQuery} oRef The element as a jQuery object
* @param {int} iZIndex The z-index value
* @protected
* @name sap.ui.core.Popup.Layer#update
* @function
*/
Popup.Layer.prototype.update = function(/** jQuery */oRef, iZIndex){
if (oRef.length) {
var oRect = oRef.rect();
this._$Ref.css({
"left" : oRect.left,
"top" : oRect.top
});
if (oRef.css("right") != "auto" && oRef.css("right") != "inherit") {
this._$Ref.css({
"right" : oRef.css("right"),
"width" : "auto"
});
} else {
this._$Ref.css({
"width" : oRect.width,
"right" : "auto"
});
}
if (oRef.css("bottom") != "auto" && oRef.css("bottom") != "inherit") {
this._$Ref.css({
"bottom" : oRef.css("bottom"),
"height" : "auto"
});
} else {
this._$Ref.css({
"height" : oRect.height,
"bottom" : "auto"
});
}
if (typeof (iZIndex) === "number") {
this._$Ref.css("z-index", iZIndex);
}
}
};
/**
* Resets the popup layer by hidding and assigning to the static area
*
* @private
* @name sap.ui.core.Popup.Layer#reset
* @function
*/
Popup.Layer.prototype.reset = function(){
if (this._$Ref.length) {
this._$Ref[0].style.display = "none";
this._$Ref[0].style.visibility = "hidden";
this._$Ref.appendTo(sap.ui.getCore().getStaticAreaRef());
}
};
/**
* Must be overwritten by sub class.
*
* @abstract
* @returns {string} The DOM string
* @name sap.ui.core.Popup.Layer#getDomString
* @function
*/
Popup.Layer.prototype.getDomString = function(){
Log.error("sap.ui.core.Popup.Layer: getDomString function must be overwritten!");
return "";
};
// End of Layer
//****************************************************
// ShieldLayer et al.
//****************************************************
/**
* @class
* @private
* @name sap.ui.core.Popup.ShieldLayer
*/
Popup.Layer.extend("sap.ui.core.Popup.ShieldLayer", {
constructor: function() {
Popup.Layer.apply(this);
}
});
Popup.ShieldLayer.prototype.getDomString = function(){
return "<div class=\"sapUiPopupShield\" id=\"sap-ui-shieldlayer-" + uid() + "\"></div>";
};
/**
* Facility for reuse of created shield layers.
* @type sap.ui.base.ObjectPool
* @private
*/
Popup.prototype.oShieldLayerPool = new ObjectPool(Popup.ShieldLayer);
//End of ShieldLayer
// Begin of Popup-Stacking facilities
(function() {
var iLastZIndex = 0;
// TODO: Implement Number.SAFE_MAX_INTEGER (Math.pow(2, 53) -1) when ECMAScript 6 is mostly supported
var iMaxInteger = Math.pow(2, 32) - 1;
/**
* Set an initial z-index that should be used by all Popup so all Popups start at least
* with the set z-index.
* If the given z-index is lower than any current available z-index the highest z-index will be used.
*
* @param {number} iInitialZIndex is the initial z-index
* @public
* @since 1.30.0
*/
Popup.setInitialZIndex = function(iInitialZIndex){
if (iInitialZIndex >= iMaxInteger) {
throw new Error("Z-index can't be higher than Number.MAX_SAFE_INTEGER");
}
iLastZIndex = Math.max(iInitialZIndex, this.getLastZIndex());
};
/**
* Returns the last z-index that has been handed out. does not increase the internal z-index counter.
*
* @returns {number} The z-index value
* @public
*/
Popup.getLastZIndex = function(){
return iLastZIndex;
};
/**
* Returns the last z-index that has been handed out. does not increase the internal z-index counter.
*
* @returns {number} Th z-index value
* @public
*/
Popup.prototype.getLastZIndex = function(){
return Popup.getLastZIndex();
};
/**
* Returns the next available z-index on top of the existing/previous popups. Each call increases the internal z-index counter and the returned z-index.
*
* @returns {number} the next z-index on top of the Popup stack
* @public
*/
Popup.getNextZIndex = function(){
iLastZIndex += 10;
if (iLastZIndex >= iMaxInteger) {
throw new Error("Z-index can't be higher than Number.MAX_SAFE_INTEGER");
}
return iLastZIndex;
};
/**
* Returns the next available z-index on top of the existing/previous popups. Each call increases the internal z-index counter and the returned z-index.
*
* @returns {number} the next z-index on top of the Popup stack
* @public
*/
Popup.prototype.getNextZIndex = function(){
return Popup.getNextZIndex();
};
}());
// End of Popup-Stacking facilities
/**
* This function compares two different objects (created via jQuery(DOM-ref).rect()).
* If the left, top, width or height differs more than a set puffer this function
* will return false.
*
* @param {object} oRectOne the first object
* @param {object} oRectTwo the other object
* @return {boolean} if the given objects are equal
* @private
*/
var fnRectEqual = function(oRectOne, oRectTwo) {
if ((!oRectOne && oRectTwo) || (oRectOne && !oRectTwo)) {
return false;
}
if (!oRectOne && !oRectTwo) {
return true;
}
var iPuffer = 3;
var iLeft = Math.abs(oRectOne.left - oRectTwo.left);
var iTop = Math.abs(oRectOne.top - oRectTwo.top);
var iWidth = Math.abs(oRectOne.width - oRectTwo.width);
var iHeight = Math.abs(oRectOne.height - oRectTwo.height);
// check if the of has moved more pixels than set in the puffer
// Puffer is needed if the opener changed its position only by 1 pixel
if (iLeft > iPuffer || iTop > iPuffer || iWidth > iPuffer || iHeight > iPuffer) {
return false;
}
return true;
};
/**
* Opens the popup's content at the position either specified here or beforehand via {@link #setPosition}.
* Content must be capable of being positioned via "position:absolute;"
* All parameters are optional (open() may be called without any parameters). iDuration may just be omitted, but if any of "at", "of", "offset", "collision" is given, also the preceding positional parameters ("my", at",...) must be given.
*
* If the Popup's OpenState is different from "CLOSED" (i.e. if the Popup is already open, opening or closing), the call is ignored.
*
* @param {int} [iDuration=jQuery.fx.speed.fast] animation duration in milliseconds. For <code>iDuration</code> == 0 the opening happens synchronously without animation.
* @param {sap.ui.core.Popup.Dock} [my=sap.ui.core.Popup.Dock.CenterCenter] the popup content's reference position for docking
* @param {sap.ui.core.Popup.Dock} [at=sap.ui.core.Popup.Dock.CenterCenter] the "of" element's reference point for docking to
* @param {string | sap.ui.core.Element | Element | jQuery | jQuery.Event} [of=document] specifies the reference element to which the given content should dock to
* @param {string} [offset='0 0'] the offset relative to the docking point, specified as a string with space-separated pixel values (e.g. "10 0" to move the popup 10 pixels to the right). If the docking of both "my" and "at" are both RTL-sensitive ("begin" or "end"), this offset is automatically mirrored in the RTL case as well.
* @param {sap.ui.core.Collision} [collision='flip'] defines how the position of an element should be adjusted in case it overflows the within area in some direction.
* @param {string | sap.ui.core.Element | Element | Window} [within=Window] defines the area the popup should be placed in. This affects the collision detection.
* @param {boolean | function | null} [followOf=false] defines whether the popup should follow the dock reference when the reference changes its position.
* @ui5-omissible-params iDuration
* @public
*/
Popup.prototype.open = function(iDuration, my, at, of, offset, collision, within, followOf) {
assert(this.oContent, "Popup content must have been set by now");
// other asserts follow after parameter shifting
if (this.eOpenState != OpenState.CLOSED) {
return;
}
// iDuration is optional... if not given:
if (typeof (iDuration) == "string") {
followOf = within;
within = collision;
collision = offset;
offset = of;
of = at;
at = my;
my = iDuration;
iDuration = -1;
}
// compatibility: map the parameter "within" to "followOf"
if (typeof within === "boolean" || typeof within === "function" || within === Popup.CLOSE_ON_SCROLL) {
followOf = within;
within = undefined;
}
// if no arguments are passed iDuration has to be set to -1
if (iDuration === undefined) {
iDuration = -1;
}
// all other parameters must be given if any subsequent parameter is given, hence no more shifting
// now every parameter should be in the right variable
assert(iDuration === -1 || (typeof iDuration === "number" && iDuration % 1 == 0), "iDuration must be an integer (or omitted)"); // omitted results in -1
assert(my === undefined || typeof my === "string", "my must be a string or empty");
assert(at === undefined || typeof at === "string", "at must be a string or empty");
assert(!of || typeof of === "object" || typeof of === "function", "of must be empty or an object");
assert(!offset || typeof offset === "string", "offset must be empty or a string");
assert(!collision || Collision.isValid(collision), "collision must be empty or of type sap.ui.core.Collision");
assert(!within || within === window || typeof within === "string" || within instanceof Element || within instanceof HTMLElement, "within must be either empty, or the global window object, or a string, or a sap.ui.core.Element, or a DOM element");
assert(!followOf || typeof followOf === "boolean" || typeof followOf === "function" || followOf === Popup.CLOSE_ON_SCROLL, "followOf must be either empty or a boolean");
this.eOpenState = OpenState.OPENING;
var oStaticUIArea = getStaticUIArea(),
oUIArea;
// If the content is a control and has no parent, add it to the static UIArea. This makes automatic rerendering
// after invalidation work. When the popup closes, the content is removed again from the static UIArea.
//
// If the content has a parent, but the parent isn't connected to any UIArea, the getUIArea function is
// overwritten to return the static UIArea to make further rerendering works. The function is deleted once the
// popup is closed.
this._bContentAddedToStatic = false;
this._bUIAreaPatched = false;
if (this.oContent instanceof Control) {
if (!this.oContent.getParent()) {
oStaticUIArea.addContent(this.oContent, true);
this._bContentAddedToStatic = true;
} else if (!this.oContent.getUIArea()) {
this.oContent.getUIArea = function() {
return oStaticUIArea;
};
this._bUIAreaPatched = true;
}
oUIArea = this.oContent.getUIArea();
if (Popup._bEnableUIAreaCheck && oUIArea.getRootNode().id !== oStaticUIArea.getRootNode().id) {
// the variable 'sap.ui.core.Popup._bEnableUIAreaCheck' isn't defined anywhere. To enable this check this variable
// has to be defined within the console or somehow else.
Log.warning("The Popup content is NOT connected with the static-UIArea and may not work properly!");
}
}
// save current focused element to restore the focus after closing
this._oPreviousFocus = Popup.getCurrentFocusInfo();
// It is mandatory to check if the new Popup runs within another Popup because
// if this new Popup is rendered via 'this._$(true)' and focused (happens e.g. if
// the Datepicker runs in a Popup and the corresponding Calendar will also open
// in a Popup. Then the corresponding date will be focused immediately. If the
// Calendar-Popup wasn't added to the previous Popup as child it is impossible to
// check in 'onFocusEvent' properly if the focus is being set to a Calendar-Popup which is
// a child of a Popup.
if (this.isInPopup(of) || this.isInPopup(this._oPosition.of)) {
var sParentId = this.getParentPopupId(of) || this.getParentPopupId(this._oPosition.of);
var sChildId = "";
var oContent = this.getContent();
if (oContent instanceof Element) {
sChildId = oContent.getId();
} else if (typeof oContent === "object") {
sChildId = oContent.id;
}
this.addChildToPopup(sParentId, sChildId);
this.addChildToPopup(sParentId, this._popupUID);
}
var $Ref = this._$(true);
var iRealDuration = "fast";
if ((iDuration === 0) || (iDuration > 0)) {
iRealDuration = iDuration;
} else if ((this._durations.open === 0) || (this._durations.open > 0)) {
iRealDuration = this._durations.open;
}
// Ensure right position is used for this call
var _oPosition;
if (my || at || of || offset || collision || within) {
_oPosition = this._createPosition(my, at, of, offset, collision, within);
// position object has to be set accordingly otherwise "oPosition.of" of a DOM-reference
// would be the "document" even if a proper "of" was provided
this._oPosition = _oPosition;
} else {
_oPosition = this._oPosition;
if (!this._bOwnWithin && vGlobalWithinArea) {
this._oPosition.within = vGlobalWithinArea;
}
}
if (!_oPosition.of) {
_oPosition.of = this._oPosition.of || document;
}
this._iZIndex = this._iZIndex === this.getLastZIndex() ? this._iZIndex : this.getNextZIndex();
var oStaticArea = sap.ui.getCore().getStaticAreaRef();
$Ref.css({
"position" : "absolute",
"visibility" : "hidden"
});
if (!($Ref[0].parentNode == oStaticArea)) { // do not move in DOM if not required - otherwise this destroys e.g. the RichTextEditor
$Ref.appendTo(oStaticArea);
}
$Ref.css("z-index", this._iZIndex);
Log.debug("position popup content " + $Ref.attr("id") + " at " + JSON.stringify(_oPosition.at));
this._applyPosition(_oPosition);
if (followOf !== undefined) {
this.setFollowOf(followOf);
}
// and show the popup content
$Ref.toggleClass("sapUiShd", this._bShadow);
var oDomRef = $Ref[0];
if (oDomRef) {
oDomRef.style.display = "none";
oDomRef.style.visibility = "visible";
}
var bNoAnimation = iRealDuration == 0;
this._duringOpen(!bNoAnimation);
if (bNoAnimation) { // do not animate if there is a duration == 0
this._opened();
} else if (this._animations.open) { // if custom animation is defined, call it
this._animations.open.call(null, $Ref, iRealDuration, this._opened.bind(this));
} else { // otherwise play the default animation
$Ref.fadeIn(iRealDuration, this._opened.bind(this));
}
};
Popup.prototype._getDomRefToFocus = function() {
var $Ref = this._$(/* bForceReRender */false, /* bGetOnly */true),
oDomRefToFocus,
oControl;
if (this._shouldGetFocusAfterOpen()) {
if (this._sInitialFocusId) {
oControl = sap.ui.getCore().byId(this._sInitialFocusId);
if (oControl) {
oDomRefToFocus = oControl.getFocusDomRef();
}
oDomRefToFocus = oDomRefToFocus || window.document.getElementById(this._sInitialFocusId);
}
oDomRefToFocus = oDomRefToFocus || $Ref.firstFocusableDomRef();
}
return oDomRefToFocus;
};
/**
* This function is called after the open animation has been finished.
* It sets the DOM really to 'visible', sets the focus inside the Popup,
* registers the 'followOf-Handler' and fires the 'opened' event.
*
* @fires sap.ui.core.Popup#opened
* @private
*/
Popup.prototype._opened = function() {
// If the popup's state is changed again after 'open' function is called,
// for example, the 'close' is called before the opening animation finishes,
// it's needed to immediately return from this function.
if (this.eOpenState !== OpenState.OPENING) {
return;
}
// internal status that any animation has been finished should set to true;
this.bOpen = true;
var $Ref = this._$(/* bForceReRender */false, /* bGetOnly */true);
if ($Ref[0] && $Ref[0].style) {
$Ref[0].style.display = "block";
}
// in modal and auto-close case the focus needs to be in the popup; provide this generic implementation as helper, but users can change the focus in the "opened" event handler
if (this._shouldGetFocusAfterOpen()) {
var domRefToFocus = this._getDomRefToFocus();
if (domRefToFocus) {
domRefToFocus.focus();
}
// if the opener was focused but it exceeds the current window width
// the window will scroll/reposition accordingly.
// When this popup registers the followOf-Handler the check if the
// opener moved will result in that the opener moved due to the focus
// and scrolling of the browser. So it is necessary to resize/reposition
// the popup right after the focus.
var oCurrentOfRef = this._getOfDom(this._oLastPosition.of);
var oCurrentOfRect = jQuery(oCurrentOfRef).rect();
if (this._oLastOfRect && oCurrentOfRect && !fnRectEqual(this._oLastOfRect, oCurrentOfRect)) {
this._applyPosition(this._oLastPosition);
}
}
this.eOpenState = OpenState.OPEN;
// set and register listener of 'followOf' (given via Popup.open()) only when
// the popup has been opened already. Otherwise checking the opener's positio
// starts to early
if (this.getFollowOf()) {
Popup.DockTrigger.addListener(Popup.checkDocking, this);
}
// notify that opening has completed
this.fireOpened();
};
/**
* This function is called before or during the Popup opens. Here the registration
* of events and delegates takes place and the corresponding flags for the Popup are set.
*
* @param {boolean} bOpenAnimated Determines if the opening has animation
*
* @private
*/
Popup.prototype._duringOpen = function(bOpenAnimated) {
var $Ref = this._$(/* bForceReRender */false, /* bGetOnly */true),
oStaticArea = sap.ui.getCore().getStaticAreaRef(),
oFirstFocusableInStaticArea = document.getElementById(oStaticArea.id + "-firstfe");
Popup._clearSelection();
this._setupUserSelection();
// shield layer is needed for mobile devices whose browser fires the mouse
// events with delay after touch events to prevent the delayed mouse events
// from reaching the dom element in popup while it's being open.
if (isMouseEventDelayed()) {
if (this._oTopShieldLayer) {
// very extreme case where the same popop is opened and closed again
// before the 500ms timed out. Reuse the same shield layer and clear
// the timeout
clearTimeout(this._iTopShieldRemoveTimer);
this._iTopShieldRemoveTimer = null;
} else {
this._oTopShieldLayer = this.oShieldLayerPool.borrowObject($Ref, this._iZIndex + 1);
}
// hide the shield layer after the delayed mouse events are fired.
this._iTopShieldRemoveTimer = setTimeout(function(){
this.oShieldLayerPool.returnObject(this._oTopShieldLayer);
this._oTopShieldLayer = null;
this._iTopShieldRemoveTimer = null;
}.bind(this), 500);
}
if (this._bModal) {
this._showBlockLayer();
}
// When it runs on a mobile device, the focus doesn't need to be set into the popup area immediately after opening
// the popup. It even causes some rendering problem in iOS safari when the focus is set here.
if (!this.touchEnabled
// When the open process is animated, the focus should be moved out of the previous focused element during the
// opening animation. Otherwise, it's not needed to shift the focus because the focus will be set into the popup
// in the same call stack in function "_opened"
&& bOpenAnimated
// some application or test create the static UIArea div by itself and therefore the first focusable element
// is not available
&& oFirstFocusableInStaticArea
// Some popup scenarios requires the blur of the previous focused element should be done
// at the end of this open method to first show the block layer which changes the top most displayed popup
&& this._shouldGetFocusAfterOpen()
// when the current active element is in a popup, it's not blurred at this position because the focus isn't
// set to the new popup yet and blurring in the previous popup will mess up the modal or autoclose in the
// previous popup
&& !this.isInPopup(document.activeElement)
// If the focus needs to be set into the popup and it's different than the current document active element
// (the focus may stay with the current active element when the initial focus id is set), the current active
// element is blurred here to prevent it from getting further events during the opening animation of the
// popup
&& this._getDomRefToFocus() !== document.activeElement) {
/* actively move the focus to the static UI area to blur the previous focused element after popup is open.
The focus will be moved into the popup once the popup opening animation is finished
*/
/* In safari no scrolling happens when focus() is called on DOM elements with height: 0px.
In firefox and chrome no scrolling has to be forced with preventScroll: true in this scenario.
*/
oFirstFocusableInStaticArea.focus({preventScroll: true});
}
// add Delegate to hosted content for handling of events (e.g. onfocusin)
if (this.oContent instanceof Element) {
this.oContent.addDelegate(this);
}
this.bOpen = true;
this._activateFocusHandle();
this._$(false, true).on("keydown", this._F6NavigationHandler);
};
Popup.prototype._shouldGetFocusAfterOpen = function() {
return this._bModal || this._bAutoClose || this._sInitialFocusId;
};
/**
* Checks whether the given DOM element is contained in the current popup or
* one of the child popups.
*
* @param {Element} oDomRef The DOM element for which the check is performed
* @returns {boolean} Whether the given DOM element is contained
*/
Popup.prototype._contains = function(oDomRef) {
var oPopupDomRef = this._$().get(0);
if (!oPopupDomRef) {
return false;
}
var bContains = oPopupDomRef.contains(oDomRef);
var aChildPopups;
if (!bContains) {
aChildPopups = this.getChildPopups();
bContains = aChildPopups.some(function(sChildID) {
// sChildID can either be the popup id or the DOM id
// therefore we need to try with document.getElementById to check the DOM id case first
// only when it doesn't contain the given DOM, we publish an event to the event bus
var oContainDomRef = (sChildID ? window.document.getElementById(sChildID) : null);
var bContains = oContainDomRef && oContainDomRef.contains(oDomRef);
if (!bContains) {
var sEventId = "sap.ui.core.Popup.contains-" + sChildID;
var oData = {
domRef: oDomRef
};
sap.ui.getCore().getEventBus().publish("sap.ui", sEventId, oData);
bContains = oData.contains;
}
return bContains;
});
}
if (!bContains) {
oPopupExtraContentSelectorSet.forEach(function(sSelector) {
bContains = bContains || jQuery(oDomRef).closest(sSelector).length > 0;
});
}
return bContains;
};
// Wrapper of _contains method for the event bus
Popup.prototype._containsEventBusWrapper = function(sChannel, sEvent, oData) {
oData.contains = this._contains(oData.domRef);
};
/**
* Handles the focus/blur events.
*
* @param {document#event} oBrowserEvent the browser event
* @private
*/
Popup.prototype.onFocusEvent = function(oBrowserEvent) {
var oEvent = jQuery.event.fix(oBrowserEvent);
if (arguments.length > 1 && arguments[1] === "sap.ui.core.Popup.onFocusEvent-" + this._popupUID) {
// if forwarding a focus event to this Popup via EventBus by any child Popup
oEvent = jQuery.event.fix(arguments[2]);
}
var type = (oEvent.type == "focus" || oEvent.type == "activate") ? "focus" : "blur";
var bContains = false;
if (type == "focus") {
var oDomRef = this._$().get(0);
if (oDomRef) {
bContains = this._contains(oEvent.target);
Log.debug("focus event on " + oEvent.target.id + ", contains: " + bContains);
if (this._bModal && !bContains) { // case: modal popup and focus has gone somewhere else in the document
// The popup is modal, but the focus has moved to a part of the document that is NOT inside the popup
// check whether this modal popup is the topmost one
var bTopMost = Popup.blStack.length > 0 && Popup.blStack[Popup.blStack.length - 1].popup === this;
if (bTopMost) {
// if in desktop browser or the DOM node which has the focus is input outside the popup,
// focus on the last blurred element
if (Device.system.desktop || jQuery(oEvent.target).is(":input")) {
if (this.oLastBlurredElement) {
// If a DOM element inside the popup was blurred, the focus should be set
// after the current call stack is finished because the existing timer for
// autoclose popup is cancelled by setting the focus here.
//
// Suppose an autoclose popup is opened within a modal popup. Clicking
// on the block layer should wait the autoclose popup to first close then
// set the focus back to the lasted blurred element.
setTimeout(function() {
if (this.oLastBlurredElement) {
this.oLastBlurredElement.focus();
}
}.bind(this), 0);
} else {
// If the focus is set to somewhere else without a blurred element in popup,
// the focus is set to the root DOM element of the popup
oDomRef.focus();
}
}
}
} else if (this._bAutoClose && bContains && this._sTimeoutId) { // case: autoclose popup and focus has returned into the popup immediately
// focus has returned, so it did only move inside the popup => clear timeout
clearTimeout(this._sTimeoutId);
this._sTimeoutId = null;
}
}
} else if (type == "blur") { // an element inside the popup is loosing focus - remember in case we need to re-set
Log.debug("blur event on " + oEvent.target.id);
if (this._bModal) {
this.oLastBlurredElement = oEvent.target;
} else if (this._bAutoClose) {
// focus/blur for handling autoclose is disabled for desktop browsers which are not in the touch simulation mode
// create timeout for closing the popup if there is no focus immediately returning to the popup
if (!this.touchEnabled && !this._sTimeoutId) {
// If Popup has focus and we click outside of the browser, in Chrome the blur event is fired, but the focused element is still in the Popup and is the same as the focused that triggers the blur event.
// if the DOM element that fires the blur event is the same as the currently focused element, just return
// because in Chrome when the browser looses focus, it fires the blur event of the
// dom element that has the focus before, but document.activeElement is still this element
if (oEvent.target === document.activeElement) {
return;
}
var iDuration = typeof this._durations.close === "string" ? 0 : this._durations.close;
// provide some additional event-parameters: closingDuration, where this delayed call comes from
this._sTimeoutId = setTimeout(function(){
this.close(iDuration, "autocloseBlur");
var oOf = this._oLastPosition && this._oLastPosition.of;
if (oOf) {
var sParentPopupId = this.getParentPopupId(oOf);
if (sParentPopupId) {
// Also inform the parent popup that the focus is lost from the child popup
// Parent popup can check whether the current focused element is inside the parent popup. If it's still inside the
// parent popup, it keeps open, otherwise parent popup is also closed.
var sEventId = "sap.ui.core.Popup.onFocusEvent-" + sParentPopupId;
sap.ui.getCore().getEventBus().publish("sap.ui", sEventId, oEvent);
}
}
}.bind(this), iDuration);
}
}
}
};
/**
* Sets the ID of the element that should be focused once the popup opens.
* If the given ID is the ID of an existing Control, this Control's focusDomRef will be focused instead, which may be an HTML element with a different ID (usually a sub-element inside the Control).
* If no existing element ID is supplied and the Popup is modal or auto-close, the Popup will instead focus the first focusable element.
*
* @param {string} sId the ID of the DOM element to focus
* @public
*/
Popup.prototype.setInitialFocusId = function(sId) {
assert(!sId || typeof sId === "string", "sId must be a string or empty");
this._sInitialFocusId = sId;
};
/**
* Closes the popup.
*
* If the Popup is already closed or in the process of closing, calling this method does nothing.
* If the Popup is in the process of being opened and closed with a duration of 0, calling this method does nothing.
* If the Popup is in the process of being opened and closed with an animation duration, the animation will be chained, but this functionality is dangerous,
* may lead to inconsistent behavior and is thus not recommended and may even be removed.
*
* @param {int} [iDuration=jQuery.fx.speed.fast] Animation duration in milliseconds. For <code>iDuration</code> == 0 the closing happens synchronously without animation.
* @public
*/
Popup.prototype.close = function(iDuration) {
if (Popup._autoCloseDebug) {
return;
}
if (this._sTimeoutId) {
clearTimeout(this._sTimeoutId);
this._sTimeoutId = null;
}
assert(iDuration === undefined || (typeof iDuration === "number" && (iDuration % 1 == 0)), "iDuration must be empty or an integer");
if (this.eOpenState == OpenState.CLOSED || this.eOpenState == OpenState.CLOSING) {
return;
} // also close when OPENING
// the above will queue the animations (close only after opening), but may lead to the CLOSED event happening before the OPENED event
var iRealDuration = "fast";
if ((iDuration === 0) || (iDuration > 0)) {
iRealDuration = iDuration;
} else if ((this._durations.close === 0) || (this._durations.close > 0)) {
iRealDuration = this._durations.close;
}
//if(this.eOpenState != sap.ui.core.OpenState.OPEN) return; // this is the more conservative approach: to only close when the Popup is OPEN
this.eOpenState = OpenState.CLOSING;
if (this.getFollowOf()) {
Popup.DockTrigger.removeListener(Popup.checkDocking, this);
}
if (this.oContent) {
// If we added the content control to the static UIArea,
// then we should remove it again now.
// Assumption: application did not move the content in the meantime!
if (this._bContentAddedToStatic ) {
//Fix for RTE in PopUp
sap.ui.getCore().getEventBus().publish("sap.ui","__beforePopupClose", { domNode : this._$().get(0) });
var oStatic = UIArea.getStaticAreaRef();
oStatic = UIArea.registry.get(oStatic.id);
oStatic.removeContent(oStatic.indexOfContent(this.oContent), true);
} else if (this._bUIAreaPatched) { // if the getUIArea function is patched, delete it
delete this.oContent.getUIArea;
}
}
this._bContentAddedToStatic = false;
this._bUIAreaPatched = false;
this._sTimeoutId = null;
this._deactivateFocusHandle();
this._$(false, true).off("keydown", this._F6NavigationHandler);
if (this.oContent instanceof Element) {
this.oContent.removeDelegate(this);
}
var $Ref = this._$();
// unsubscribe the event listeners from EventBus
if (this._bEventBusEventsRegistered) {
this._unregisterEventBusEvents();
}
// shield layer is needed for mobile devices whose browser fires the mouse events with delay after touch events
// to prevent the delayed mouse events from reaching the underneath DOM element.
if (isMouseEventDelayed()) {
if (this._oBottomShieldLayer) {
// very extreme case where the same popop is opened and closed again before the 500ms timed out.
// reuse the same shield layer and clear the timeout
clearTimeout(this._iBottomShieldRemoveTimer);
this._iBottomShieldRemoveTimer = null;
} else {
this._oBottomShieldLayer = this.oShieldLayerPool.borrowObject($Ref, this._iZIndex - 3);
}
// hide the shield layer after the delayed mouse events are fired.
this._iBottomShieldRemoveTimer = setTimeout(function(){
this.oShieldLayerPool.returnObject(this._oBottomShieldLayer);
this._oBottomShieldLayer = null;
this._iBottomShieldRemoveTimer = null;
}.bind(this), 500);
}
// Check if this instance is a child Popup. If true de-register this from
// the parent