UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

1,392 lines (1,208 loc) 123 kB
/*! * OpenUI5 * (c) Copyright 2026 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/DataType', 'sap/ui/base/Event', 'sap/ui/base/ManagedObject', 'sap/ui/base/Object', 'sap/ui/base/ObjectPool', './Control', './Element', './EventBus', './FocusHandler', './IntervalTrigger', './RenderManager', './ResizeHandler', './library', './StaticArea', "sap/base/assert", "sap/base/Log", "sap/base/i18n/Localization", "sap/base/util/Version", "sap/base/util/uid", "sap/base/util/extend", "sap/base/util/each", "sap/base/util/deepExtend", "sap/ui/events/F6Navigation", "sap/ui/base/EventProvider", "sap/ui/thirdparty/jquery", "sap/ui/thirdparty/jqueryui/jquery-ui-position", "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, DataType, Event, ManagedObject, BaseObject, ObjectPool, Control, Element, EventBus, FocusHandler, IntervalTrigger, RenderManager, ResizeHandler, library, StaticArea, assert, Log, Localization, Version, uid, extend, each, deepExtend, F6Navigation, EventProvider, jQuery //jquery-ui-position //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"; var oEventBus = new EventBus(); 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 oControl; try { // only a facade of the static UIArea is returned that contains only the public methods oStaticUIArea = StaticArea.getUIArea(); } 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. * * <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 : { /** * Fired when the popup has completely opened (after any animation). * @since 1.2 */ "opened" : {}, /** * Fired when the popup has completely closed (after any animation). * @since 1.2 */ "closed" : {} } } }); /** * @typedef {object} sap.ui.core.Popup.PositionInfo * @public * * @property {object} lastPosition The last position value * @property {DOMRect} lastOfRect The DOMRect of the previous "of" element * @property {DOMRect} currentOfRect The DOMRect of the current "of" element */ 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 = { /** * Docks the popup at the begin of the reference element, at the top. * @public * @type {string} */ BeginTop : "BeginTop", /** * Docks the popup at the begin of the reference element, vertically centered. * @public * @type {string} */ BeginCenter : "BeginCenter", /** * Docks the popup at the begin of the reference element, at the bottom. * @public * @type {string} */ BeginBottom : "BeginBottom", /** * Docks the popup at the left side of the reference element, at the top. * @public * @type {string} */ LeftTop : "LeftTop", /** * Docks the popup at the left side of the reference element, vertically centered. * @public * @type {string} */ LeftCenter : "LeftCenter", /** * Docks the popup at the left side of the reference element, at the bottom. * @public * @type {string} */ LeftBottom : "LeftBottom", /** * Docks the popup horizontally centered to the reference element, at the top. * @public * @type {string} */ CenterTop : "CenterTop", /** * Docks the popup horizontally and vertically centered to the reference element. * @public * @type {string} */ CenterCenter : "CenterCenter", /** * Docks the popup horizontally centered to the reference element, at the bottom. * @public * @type {string} */ CenterBottom : "CenterBottom", /** * Docks the popup at the right side of the reference element, at the top. * @public * @type {string} */ RightTop : "RightTop", /** * Docks the popup at the right side of the reference element, vertically centered. * @public * @type {string} */ RightCenter : "RightCenter", /** * Docks the popup at the right side of the reference element, at the bottom. * @public * @type {string} */ RightBottom : "RightBottom", /** * Docks the popup at the end of the reference element, at the top. * @public * @type {string} */ EndTop : "EndTop", /** * Docks the popup at the end of the reference element, vertically centered. * @public * @type {string} */ EndCenter : "EndCenter", /** * Docks the popup at the end of the reference element, at the bottom. * @public * @type {string} */ EndBottom : "EndBottom" }; DataType.registerEnum("sap.ui.core.Popup.Dock", Popup.Dock); /** * 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; 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(); }; /** * 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=200] 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(sap.ui.core.Popup.PositionInfo) | 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); $Ref.addClass("sapUiPopupInitial"); $Ref.css({ "position" : "absolute", "display": this._getCSSDisplayType(), "visibility": "visible", "opacity": "" // reset the opacity style }); 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 = StaticArea.getDomRef(); 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 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.fadeTo(iRealDuration, 1, this._opened.bind(this)); } }; Popup.prototype._getCSSDisplayType = function () { return this.oContent?._getCSSDisplayType?.() || "block"; }; Popup.prototype._getDomRefToFocus = function() { var $Ref = this._$(/* bForceReRender */false, /* bGetOnly */true), oDomRefToFocus, oControl; if (this._shouldGetFocusAfterOpen()) { if (this._sInitialFocusId) { oControl = Element.getElementById(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); $Ref.removeClass("sapUiPopupInitial"); this._activateFocusHandle(); this._$(false, true).on("keydown", this._F6NavigationHandler); // 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); } this.eOpenState = OpenState.OPEN; // 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) { Popup._clearSelection(); this._setupUserSelection(); if (this._bModal) { this._showBlockLayer(); } // 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); } } // add Delegate to hosted content for handling of events (e.g. onfocusin) if (this.oContent instanceof Element) { this.oContent.addDelegate(this); } this.bOpen = true; }; 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 }; oEventBus.publish("sap.ui", sEventId, oData); bContains = oData.contains; } return bContains; }); } if (!bContains && this._aExtraContent) { bContains = this._aExtraContent.some(({ref}) => { if (ref instanceof Element) { ref = ref.getDomRef(); } if (!ref) { return false; } const aDomAreas = [ref]; if (ref.shadowRoot) { aDomAreas.push(ref.shadowRoot); } return aDomAreas.some((oDomArea) => { return oDomArea.contains?.(oDomRef); }); }); } 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); let oTarget = oEvent.target; // also check the shadow dom active element in case the event target has a shadow dom attached while (!bContains && oTarget.shadowRoot && oTarget.shadowRoot.activeElement) { bContains = this._contains(oTarget.shadowRoot.activeElement); oTarget = oTarget.shadowRoot.activeElement; } 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; oEventBus.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=200] 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 oEventBus.publish("sap.ui","__beforePopupClose", { domNode : this._$().get(0) }); var oStatic = StaticArea.getUIArea(); const iIndex = oStatic.indexOfContent(this.oContent); if (iIndex >= 0) { oStatic.removeContent(iIndex, 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(); } // Check if this instance is a child Popup. If true de-register this from // the parent if (this.isInPopup(this._oLastPosition.of)) { var sParentId = this.getParentPopupId(this._oLastPosition.of); var sChildId = ""; var oContent = this.getContent(); if (oContent instanceof Element) { sChildId = oContent.getId(); } else if (typeof oContent === "object") { sChildId = oContent.id; } this.removeChildFromPopup(sParentId, sChildId); this.removeChildFromPopup(sParentId, this._popupUID); } if (this._bModal && this.preventBrowserFocus) { $Ref.one("mousedown", function(oEvent) { // browser sets the focus after mousedown event // On mobile devices, the restoring of focus may happen before // the delayed mousedown event. // The browser will set the focus to the clicked element again // after restoring of the focus. // Calling 'preventDefault' prevents the browser from setting // the focus after the delayed mousedown event. oEvent.preventDefault(); }); } this._duringClose(); if (iRealDuration == 0) { // iRealDuration == 0 means: no animation! this._closed(); } else if (this._animations.close) { this._animations.close.call(null, $Ref, iRealDuration, this._closed.bind(this)); // play custom animation, if supplied } else { $Ref.fadeOut(iRealDuration, this._closed.bind(this)); // otherwise use jQuery animation } }; /** * This function must be called after a Popup has been closed. * Here the DOM-reference is really hidden and it is ensured that due to * some delayed rendering the DOM is really hidden. * Additionally the focus is set back where it has been before the Popup has * been opened and this Popup will close all its children. * Finally the 'closed' event is being fired. * * @fires sap.ui.core.Popup#closed * @private */ Popup.prototype._closed = function() { var $Ref = this._$(/* bForceReRender */false, /* bGetOnly */true); if (this._bModal) { this._hideBlockLayer(); } Popup._clearSelection(); this._restoreUserSelection(); if ($Ref.length) { var oDomRef = $Ref.get(0); // hide the old DOM ref if (oDomRef) { oDomRef.style.display = "none"; oDomRef.style.visibility = "hidden"; oDomRef.style.left = "0px"; oDomRef.style.top = "0px"; oDomRef.style.right = ""; } // update the DomRef because it could have been re-rendered during closing $Ref = this._$(/* forceRerender */ false, /* only get DOM */ true); oDomRef = $Ref.length ? $Ref[0] : null; if (oDomRef) { // also hide the new DOM ref oDomRef.style.display = "none"; oDomRef.style.visibility = "hidden"; oDomRef.style.left = "0px"; oDomRef.style.top = "0px"; oDomRef.style.right = ""; } } if (this._bModal) { // try to set the focus back to whatever was focused before. Do this here because animation needs to be finished. //- TODO: currently focus is restored only for modal popups. Non modal popups have to do it themselves because the outside focus can change! Popup.applyFocusInfo(this._oPreviousFocus); this._oPreviousFocus = null; this.oLastBlurredElement = null; } this.bOpen = false; this.eOpenState = OpenState.CLOSED; var aChildPopups = this.getChildPopups(); for (var j = 0, l = aChildPopups.length; j < l; j++) { this.closePopup(aChildPopups[j]); } // notify users that the popup is now officially closed this.fireClosed(); }; /** * This stuff is being executed during an animation is going on. But if there * is no animation this stuff has to be done in advance, before ._closed is * called. * * @private */ Popup.prototype._duringClose = function() { //deregister resize handler if (this._resizeListenerId) { ResizeHandler.deregister(this._resizeListenerId); this._resizeListenerId = null; } }; /** * Returns an object containing as much information about the current focus as * possible, or null if no focus is present or no focus information can be gathered. * * @returns {object} oPreviousFocus with the information which control/element * was focused before the Popup has been opened. * If a control was focused the control will add * additional information if the control * implemented 'getFocusInfo'. */ Popup.getCurrentFocusInfo = function() { var _oPreviousFocus = null; var oActiveElement = Element.closestTo(document.activeElement); if (oActiveElement) { _oPreviousFocus = { 'sFocusId' : oActiveElement.getId(), 'oFocusInfo' : oActiveElement.getFocusInfo() }; } else { // not an SAPUI5 control... but if something has focus, save as much information about it as available _oPreviousFocus = { 'sFocusId' : document.activeElement.id, 'oFocusedElement' : document.activeElement, // add empty oFocusInfo to avoid the need for all recipients to check 'oFocusInfo': {} }; } if (_oPreviousFocus) { // Storing the information that this focusInfo is processed by the Popup. // There are two different scenarios using the FocusInfo: // - Keep the value inside an input field if the renderer re-renders the // input // - The Popup focuses the previous focused control/element and uses // the FocusInfo mechanism as well. _oPreviousFocus.popup = this; } return _oPreviousFocus; }; /** * Applies the stored FocusInfo to the control/element where the focus * was before the Popup was opened. * When the FocusInfo has been applied the corresponding control/element * will be focused. * * @param {object} oPreviousFocus is the stored focusInfo that was fetched * from the control (if available) */ Popup.applyFocusInfo = function(oPreviousFocus) { var oOptions = { // this option informs the browser not to sc