@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,150 lines (1,045 loc) • 95.1 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2026 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
// Provides the base class for all controls and UI elements.
sap.ui.define([
'../base/DataType',
'../base/Object',
'../base/ManagedObject',
'./ElementHooks',
'./ElementMetadata',
'./FocusMode',
'../Device',
"sap/ui/dom/findTabbable",
"sap/ui/performance/trace/Interaction",
"sap/base/future",
"sap/base/assert",
"sap/ui/thirdparty/jquery",
"sap/ui/events/F6Navigation",
"sap/ui/util/_enforceNoReturnValue",
"./RenderManager",
"./Rendering",
"./EnabledPropagator",
"./ElementRegistry",
"./Theming",
"sap/ui/core/util/_LocalizationHelper"
],
function(
DataType,
BaseObject,
ManagedObject,
ElementHooks,
ElementMetadata,
FocusMode,
Device,
findTabbable,
Interaction,
future,
assert,
jQuery,
F6Navigation,
_enforceNoReturnValue,
RenderManager,
Rendering,
EnabledPropagator,
ElementRegistry,
Theming,
_LocalizationHelper
) {
"use strict";
/**
* Constructs and initializes a UI Element with the given <code>sId</code> and settings.
*
*
* <h3>Uniqueness of IDs</h3>
*
* Each <code>Element</code> must have an ID. If no <code>sId</code> or <code>mSettings.id</code> is
* given at construction time, a new ID will be created automatically. The IDs of all elements that exist
* at the same time in the same window must be different. Providing an ID which is already used by another
* element throws an error.
*
* When an element is created from a declarative source (e.g. XMLView), then an ID defined in that
* declarative source needs to be unique only within the declarative source. Declarative views will
* prefix that ID with their own ID (and some separator) before constructing the element.
* Programmatically created views (JSViews) can do the same with the {@link sap.ui.core.mvc.View#createId} API.
* Similarly, UIComponents can prefix the IDs of elements created in their context with their own ID.
* Also see {@link sap.ui.core.UIComponent#getAutoPrefixId UIComponent#getAutoPrefixId}.
*
*
* <h3>Settings</h3>
* If the optional <code>mSettings</code> are given, they must be a JSON-like object (object literal)
* that defines values for properties, aggregations, associations or events keyed by their name.
*
* <b>Valid Names:</b>
*
* The property (key) names supported in the object literal are exactly the (case sensitive)
* names documented in the JSDoc for the properties, aggregations, associations and events
* of the control and its base classes. Note that for 0..n aggregations and associations this
* usually is the plural name, whereas it is the singular name in case of 0..1 relations.
*
* Each subclass should document the set of supported names in its constructor documentation.
*
* <b>Valid Values:</b>
*
* <ul>
* <li>for normal properties, the value has to be of the correct simple type (no type conversion occurs)</li>
* <li>for 0..1 aggregations, the value has to be an instance of the aggregated control or element type</li>
* <li>for 0..n aggregations, the value has to be an array of instances of the aggregated type</li>
* <li>for 0..1 associations, an instance of the associated type or an id (string) is accepted</li>
* <li>0..n associations are not supported yet</li>
* <li>for events either a function (event handler) is accepted or an array of length 2
* where the first element is a function and the 2nd element is an object to invoke the method on.</li>
* </ul>
*
* Special aggregation <code>dependents</code> is connected to the lifecycle management and databinding,
* but not rendered automatically and can be used for popups or other dependent controls or elements.
* This allows the definition of popup controls in declarative views and enables propagation of model
* and context information to them.
*
* @param {string} [sId] id for the new control; generated automatically if no non-empty id is given
* Note: this can be omitted, no matter whether <code>mSettings</code> will be given or not!
* @param {object} [mSettings] optional map/JSON-object with initial property values, aggregated objects etc. for the new element
*
* @abstract
*
* @class Base Class for UI Elements.
*
* <code>Element</code> is the most basic building block for UI5 UIs. An <code>Element</code> has state like a
* <code>ManagedObject</code>, it has a unique ID by which the framework remembers it. It can have associated
* DOM, but it can't render itself. Only {@link sap.ui.core.Control Controls} can render themselves and also
* take care of rendering <code>Elements</code> that they aggregate as children. If an <code>Element</code>
* has been rendered, its related DOM gets the same ID as the <code>Element</code> and thereby can be retrieved
* via API. When the state of an <code>Element</code> changes, it informs its parent <code>Control</code> which
* usually re-renders then.
*
* <h3>Dispatching Events</h3>
*
* The UI5 framework already registers generic listeners for common browser events, such as <code>click</code>
* or <code>keydown</code>. When called, the generic listener first determines the corresponding target element
* using {@link jQuery#control}. Then it checks whether the element has an event handler method for the event.
* An event handler method by convention has the same name as the event, but prefixed with "on": Method
* <code>onclick</code> is the handler for the <code>click</code> event, method <code>onkeydown</code> the handler
* for the <code>keydown</code> event and so on. If there is such a method, it will be called with the original
* event as the only parameter. If the element has a list of delegates registered, their handler functions will
* be called the same way, where present. The set of implemented handlers might differ between element and
* delegates. Not each handler implemented by an element has to be implemented by its delegates, and delegates
* can implement handlers that the corresponding element doesn't implement.
*
* A list of browser events that are handled that way can be found in {@link module:sap/ui/events/ControlEvents}.
* Additionally, the framework dispatches pseudo events ({@link module:sap/ui/events/PseudoEvents}) using the same
* naming convention. Last but not least, some framework events are also dispatched that way, e.g.
* <code>BeforeRendering</code>, <code>AfterRendering</code> (only for controls) and <code>ThemeChanged</code>.
*
* If further browser events are needed, controls can register listeners on the DOM using native APIs in their
* <code>onAfterRendering</code> handler. If needed, they can do this for their aggregated elements as well.
* If events might fire often (e.g. <code>mousemove</code>), it is best practice to register them only while
* needed, and deregister afterwards. Anyhow, any registered listeners must be cleaned up in the
* <code>onBeforeRendering</code> listener and before destruction in the <code>exit</code> hook.
*
* @extends sap.ui.base.ManagedObject
* @author SAP SE
* @version 1.147.0
* @public
* @alias sap.ui.core.Element
*/
var Element = ManagedObject.extend("sap.ui.core.Element", {
metadata : {
stereotype : "element",
"abstract" : true,
publicMethods : [ "getId", "getMetadata", "getTooltip_AsString", "getTooltip_Text", "getModel", "setModel", "hasModel", "bindElement", "unbindElement", "getElementBinding", "prop", "getLayoutData", "setLayoutData" ],
library : "sap.ui.core",
aggregations : {
/**
* The tooltip that should be shown for this Element.
*
* In the most simple case, a tooltip is a string that will be rendered by the control and
* displayed by the browser when the mouse pointer hovers over the control's DOM. In this
* variant, <code>tooltip</code> behaves like a simple control property.
*
* Controls need to explicitly support this kind of tooltip as they have to render it,
* but most controls do. Exceptions will be documented for the corresponding controls
* (e.g. <code>sap.ui.core.HTML</code> does not support tooltips).
*
* Alternatively, <code>tooltip</code> can act like a 0..1 aggregation and can be set to a
* tooltip control (an instance of a subclass of <code>sap.ui.core.TooltipBase</code>). In
* that case, the framework will take care of rendering the tooltip control in a popup-like
* manner. Such a tooltip control can display arbitrary content, not only a string.
*
* UI5 currently does not provide a recommended implementation of <code>TooltipBase</code>
* as the use of content-rich tooltips is discouraged by the Fiori Design Guidelines.
* Existing subclasses of <code>TooltipBase</code> therefore have been deprecated.
*
* See the section {@link https://experience.sap.com/fiori-design-web/using-tooltips/ Using Tooltips}
* in the Fiori Design Guideline.
*/
tooltip : {type : "sap.ui.core.TooltipBase", altTypes : ["string"], multiple : false},
/**
* Custom Data, a data structure like a map containing arbitrary key value pairs.
*
* @default sap/ui/core/CustomData
*/
customData : {type : "sap.ui.core.CustomData", multiple : true, singularName : "customData"},
/**
* Defines the layout constraints for this control when it is used inside a Layout.
* LayoutData classes are typed classes and must match the embedding Layout.
* See VariantLayoutData for aggregating multiple alternative LayoutData instances to a single Element.
*/
layoutData : {type : "sap.ui.core.LayoutData", multiple : false, singularName : "layoutData"},
/**
* Dependents are not rendered, but their databinding context and lifecycle are bound to the aggregating ManagedObject.
* @since 1.19
*/
dependents : {type : "sap.ui.base.ManagedObject", multiple : true},
/**
* Defines the drag-and-drop configuration.
* <b>Note:</b> This configuration might be ignored due to control {@link sap.ui.core.Element.extend metadata} restrictions.
*
* @since 1.56
*/
dragDropConfig : {type : "sap.ui.core.dnd.DragDropBase", multiple : true, singularName : "dragDropConfig"}
},
associations : {
/**
* Reference to the element to show the field help for this control; if unset, field help is
* show on the control itself.
*/
fieldHelpDisplay : {type: "sap.ui.core.Element", multiple: false}
}
},
constructor : function(sId, mSettings) {
ManagedObject.apply(this, arguments);
this._iRenderingDelegateCount = 0;
},
renderer : null // Element has no renderer
}, /* Metadata constructor */ ElementMetadata);
ElementRegistry.init(Element);
/**
* Creates metadata for a UI Element by extending the Object Metadata.
*
* @param {string} sClassName name of the class to build the metadata for
* @param {object} oStaticInfo static information used to build the metadata
* @param {function} [fnMetaImpl=sap.ui.core.ElementMetadata] constructor to be used for the metadata
* @return {sap.ui.core.ElementMetadata} the created metadata
* @static
* @public
* @deprecated Since 1.3.1. Use the static <code>extend</code> method of the desired base class (e.g. {@link sap.ui.core.Element.extend})
*/
Element.defineClass = function(sClassName, oStaticInfo, fnMetaImpl) {
// create and attach metadata but with an Element specific implementation
return BaseObject.defineClass(sClassName, oStaticInfo, fnMetaImpl || ElementMetadata);
};
/**
* Elements don't have a facade and therefore return themselves as their interface.
*
* @returns {this} <code>this</code> as there's no facade for elements
* @see sap.ui.base.Object#getInterface
* @public
*/
Element.prototype.getInterface = function() {
return this;
};
/**
* @typedef {sap.ui.base.ManagedObject.MetadataOptions} sap.ui.core.Element.MetadataOptions
*
* The structure of the "metadata" object which is passed when inheriting from sap.ui.core.Element using its static "extend" method.
* See {@link sap.ui.core.Element.extend} for details on its usage.
*
* @property {boolean | sap.ui.core.Element.MetadataOptions.DnD} [dnd=false]
* Defines draggable and droppable configuration of the element.
* The following boolean properties can be provided in the given object literal to configure drag-and-drop behavior of the element
* (see {@link sap.ui.core.Element.MetadataOptions.DnD DnD} for details): draggable, droppable
* If the <code>dnd</code> property is of type Boolean, then the <code>draggable</code> and <code>droppable</code> configuration are both set to this Boolean value.
*
* @public
*/
/**
* @typedef {object} sap.ui.core.Element.MetadataOptions.DnD
*
* An object literal configuring the drag&drop capabilities of a class derived from sap.ui.core.Element.
* See {@link sap.ui.core.Element.MetadataOptions MetadataOptions} for details on its usage.
*
* @property {boolean} [draggable=false] Defines whether the element is draggable or not. The default value is <code>false</code>.
* @property {boolean} [droppable=false] Defines whether the element is droppable (it allows being dropped on by a draggable element) or not. The default value is <code>false</code>.
*
* @public
*/
/**
* Defines a new subclass of Element with the name <code>sClassName</code> and enriches it with
* the information contained in <code>oClassInfo</code>.
*
* <code>oClassInfo</code> can contain the same information that {@link sap.ui.base.ManagedObject.extend} already accepts,
* plus the <code>dnd</code> property in the metadata object literal to configure drag-and-drop behavior
* (see {@link sap.ui.core.Element.MetadataOptions MetadataOptions} for details). Objects describing aggregations can also
* have a <code>dnd</code> property when used for a class extending <code>Element</code>
* (see {@link sap.ui.base.ManagedObject.MetadataOptions.AggregationDnD AggregationDnD}).
*
* Example:
* <pre>
* Element.extend('sap.mylib.MyElement', {
* metadata : {
* library : 'sap.mylib',
* properties : {
* value : 'string',
* width : 'sap.ui.core.CSSSize'
* },
* dnd : { draggable: true, droppable: false },
* aggregations : {
* items : { type: 'sap.ui.core.Control', multiple : true, dnd : {draggable: false, droppable: true, layout: "Horizontal" } },
* header : {type : "sap.ui.core.Control", multiple : false, dnd : true },
* }
* }
* });
* </pre>
*
* @param {string} sClassName Name of the class to be created
* @param {object} [oClassInfo] Object literal with information about the class
* @param {sap.ui.core.Element.MetadataOptions} [oClassInfo.metadata] the metadata object describing the class: properties, aggregations, events etc.
* @param {function} [FNMetaImpl] Constructor function for the metadata object. If not given, it defaults to <code>sap.ui.core.ElementMetadata</code>.
* @returns {function} Created class / constructor function
*
* @public
* @static
* @name sap.ui.core.Element.extend
* @function
*/
/**
* Dispatches the given event, usually a browser event or a UI5 pseudo event.
*
* @param {jQuery.Event} oEvent The event
* @private
*/
Element.prototype._handleEvent = function (oEvent) {
var that = this,
sHandlerName = "on" + oEvent.type;
function each(aDelegates) {
var i,l,oDelegate;
if ( aDelegates && (l = aDelegates.length) > 0 ) {
// To be robust against concurrent modifications of the delegates list, we loop over a copy.
// When there is only a single entry, the loop is safe without a copy (length is determined only once!)
aDelegates = l === 1 ? aDelegates : aDelegates.slice();
for (i = 0; i < l; i++ ) {
if (oEvent.isImmediateHandlerPropagationStopped()) {
return;
}
oDelegate = aDelegates[i].oDelegate;
if (oDelegate[sHandlerName]) {
oDelegate[sHandlerName].call(aDelegates[i].vThis === true ? that : aDelegates[i].vThis || oDelegate, oEvent);
}
}
}
}
each(this.aBeforeDelegates);
if ( oEvent.isImmediateHandlerPropagationStopped() ) {
return;
}
if ( this[sHandlerName] ) {
if (oEvent._bNoReturnValue) {
// fatal throw if listener isn't allowed to have a return value
_enforceNoReturnValue(this[sHandlerName](oEvent), /*mLogInfo=*/{ name: sHandlerName, component: this.getId() });
} else {
this[sHandlerName](oEvent);
}
}
each(this.aDelegates);
};
/**
* Initializes the element instance after creation.
*
* Applications must not call this hook method directly, it is called by the framework
* while the constructor of an element is executed.
*
* Subclasses of Element should override this hook to implement any necessary initialization.
*
* @returns {void|undefined} This hook method must not have a return value. Return value <code>void</code> is deprecated since 1.120, as it does not force functions to <b>not</b> return something.
* This implies that, for instance, no async function returning a Promise should be used.
*
* <b>Note:</b> While the return type is currently <code>void|undefined</code>, any
* implementation of this hook must not return anything but undefined. Any other
* return value will cause an error log in this version of UI5 and will fail in future
* major versions of UI5.
* @protected
*/
Element.prototype.init = function() {
// Before adding any implementation, please remember that this method was first implemented in release 1.54.
// Therefore, many subclasses will not call this method at all.
return undefined;
};
/**
* Hook method for cleaning up the element instance before destruction.
*
* Applications must not call this hook method directly, it is called by the framework
* when the element is {@link #destroy destroyed}.
*
* Subclasses of Element should override this hook to implement any necessary cleanup.
*
* <pre>
* exit: function() {
* // ... do any further cleanups of your subclass e.g. detach events...
* this.$().off("click", this.handleClick);
*
* if (Element.prototype.exit) {
* Element.prototype.exit.apply(this, arguments);
* }
* }
* </pre>
*
* For a more detailed description how to to use the exit hook, see Section
* {@link topic:d4ac0edbc467483585d0c53a282505a5 exit() Method} in the documentation.
*
* @returns {void|undefined} This hook method must not have a return value. Return value <code>void</code> is deprecated since 1.120, as it does not force functions to <b>not</b> return something.
* This implies that, for instance, no async function returning a Promise should be used.
*
* <b>Note:</b> While the return type is currently <code>void|undefined</code>, any
* implementation of this hook must not return anything but undefined. Any other
* return value will cause an error log in this version of UI5 and will fail in future
* major versions of UI5.
* @protected
*/
Element.prototype.exit = function() {
// Before adding any implementation, please remember that this method was first implemented in release 1.54.
// Therefore, many subclasses will not call this method at all.
return undefined;
};
/**
* Creates a new Element from the given data.
*
* If <code>vData</code> is an Element already, that element is returned.
* If <code>vData</code> is an object (literal), then a new element is created with <code>vData</code> as settings.
* The type of the element is either determined by a property named <code>Type</code> in the <code>vData</code> or
* by a type information in the <code>oKeyInfo</code> object
* @param {sap.ui.core.Element|object} vData Data to create the element from
* @param {object} [oKeyInfo] An entity information (e.g. aggregation info)
* @param {string} [oKeyInfo.type] Type info for the entity
* @returns {sap.ui.core.Element}
* The newly created <code>Element</code>
* @public
* @static
* @deprecated As of 1.44, use the more flexible {@link sap.ui.base.ManagedObject.create}.
* @function
* @ts-skip
*/
Element.create = ManagedObject.create;
/**
* Returns a simple string representation of this element.
*
* Mainly useful for tracing purposes.
* @public
* @return {string} a string description of this element
*/
Element.prototype.toString = function() {
return "Element " + this.getMetadata().getName() + "#" + this.sId;
};
/**
* Returns the best suitable DOM Element that represents this UI5 Element.
* By default the DOM Element with the same ID as this Element is returned.
* Subclasses should override this method if the lookup via id is not sufficient.
*
* Note that such a DOM Element does not necessarily exist in all cases.
* Some elements or controls might not have a DOM representation at all (e.g.
* a naive FlowLayout) while others might not have one due to their current
* state (e.g. an initial, not yet rendered control).
*
* If an ID suffix is given, the ID of this Element is concatenated with the suffix
* (separated by a single hyphen) and the DOM node with that compound ID will be returned.
* This matches the UI5 naming convention for named inner DOM nodes of a control.
*
* @param {string} [sSuffix] ID suffix to get the DOMRef for
* @returns {Element|null} The Element's DOM Element, sub DOM Element or <code>null</code>
* @protected
*/
Element.prototype.getDomRef = function(sSuffix) {
return document.getElementById(sSuffix ? this.getId() + "-" + sSuffix : this.getId());
};
/**
* Returns the best suitable DOM node that represents this Element wrapped as jQuery object.
* I.e. the element returned by {@link sap.ui.core.Element#getDomRef} is wrapped and returned.
*
* If an ID suffix is given, the ID of this Element is concatenated with the suffix
* (separated by a single hyphen) and the DOM node with that compound ID will be wrapped by jQuery.
* This matches the UI5 naming convention for named inner DOM nodes of a control.
*
* @param {string} [sSuffix] ID suffix to get a jQuery object for
* @return {jQuery} The jQuery wrapped element's DOM reference
* @protected
*/
Element.prototype.$ = function(sSuffix) {
return jQuery(this.getDomRef(sSuffix));
};
/**
* Checks whether this element has an active parent.
*
* @returns {boolean} Whether this element has an active parent
* @private
*/
Element.prototype.isActive = function() {
return this.oParent && this.oParent.isActive();
};
/**
* This function either calls set[sPropertyName] or get[sPropertyName] with the specified property name
* depending if an <code>oValue</code> is provided or not.
*
* @param {string} sPropertyName name of the property to set
* @param {any} [oValue] value to set the property to
* @return {any|this} Returns <code>this</code> to allow method chaining in case of setter and the property value in case of getter
* @public
* @deprecated Since 1.28.0 The contract of this method is not fully defined and its write capabilities overlap with applySettings
*/
Element.prototype.prop = function(sPropertyName, oValue) {
var oPropertyInfo = this.getMetadata().getAllSettings()[sPropertyName];
if (oPropertyInfo) {
if (arguments.length == 1) {
// getter
return this[oPropertyInfo._sGetter]();
} else {
// setter
this[oPropertyInfo._sMutator](oValue);
return this;
}
}
};
/*
* Intercept any changes for properties named "enabled" and "visible".
*
* If a change for "enabled" property is detected, inform all descendants that use the `EnabledPropagator`
* so that they can recalculate their own, derived enabled state.
* This is required in the context of rendering V4 to make the state of controls/elements
* self-contained again when they're using the `EnabledPropagator` mixin.
*
* Fires "focusfail" event, if the "enabled" or "visible" property is changed to "false" and the element was focused.
*/
Element.prototype.setProperty = function(sPropertyName, vValue, bSuppressInvalidate) {
if ((sPropertyName != "enabled" && sPropertyName != "visible") || bSuppressInvalidate) {
return ManagedObject.prototype.setProperty.apply(this, arguments);
}
if (sPropertyName == "enabled") {
var bOldEnabled = this.mProperties.enabled;
ManagedObject.prototype.setProperty.apply(this, arguments);
if (bOldEnabled != this.mProperties.enabled) {
// the EnabledPropagator knows better which descendants to update
EnabledPropagator.updateDescendants(this);
}
} else if (sPropertyName === "visible") {
ManagedObject.prototype.setProperty.apply(this, arguments);
if (vValue === false && this.getDomRef()?.contains(document.activeElement)) {
Element.fireFocusFail.call(this, FocusMode.RENDERING_PENDING);
}
}
return this;
};
function _focusTarget(oOriginalDomRef, oFocusTarget) {
// In the meantime, the focus could be set somewhere else.
// If that element is focusable, then we don't steal the focus from it
if (oOriginalDomRef?.contains(document.activeElement) || !jQuery(document.activeElement).is(":sapFocusable")) {
oFocusTarget?.focus({
preventScroll: true
});
}
}
/**
* Handles the 'focusfail' event by attempting to find and focus on a tabbable element.
* The 'focusfail' event is triggered when the current element, which initially holds the focus,
* becomes disabled, invisible, or destroyed. The event is received by the parent of the element that failed
* to retain the focus.
*
* @param {Event} oEvent - The event object containing the source element that failed to gain focus.
* @protected
*/
Element.prototype.onfocusfail = function(oEvent) {
// oEvent._skipArea is set when all controls in an aggregation are
// removed/destroyed (via 'removeAllAggregation'). We need to skip the
// entire aggregation area since all controls' DOM elements will be
// removed and no focusable element can be found within
// oEvent._skipArea
//
// oEvent._skipArea is used as start point when it exists and
// the following 'findTabbable' call starts with its next/previous
// sibling
let oDomRef = oEvent._skipArea || oEvent.srcControl.getDomRef();
const oOriginalDomRef = oDomRef;
let oParent = this;
let oParentDomRef = oParent.getDomRef();
let oRes;
let oFocusTarget;
do {
if (oParentDomRef?.contains(oDomRef)) {
// Search for a tabbable element forward (to the right)
oRes = findTabbable(oDomRef, {
scope: oParentDomRef,
forward: true,
skipChild: true
});
// If no element is found, search backward (to the left)
if (oRes?.startOver) {
oRes = findTabbable(oDomRef, {
scope: oParentDomRef,
forward: false
});
}
oFocusTarget = oRes?.element;
// Reached the parent DOM which is tabbable, stop searching
if (oFocusTarget === oParentDomRef) {
break;
}
// Move up to the parent's siblings
oDomRef = oParentDomRef;
oParent = oParent?.getParent();
oParentDomRef = oParent?.getDomRef?.();
} else {
// If the lost focus element is outside the parent, look for the parent's first focusable element (including the parent itself)
if (jQuery(oParentDomRef).is(":sapFocusable")) {
// If the parent is focusable, we can focus it
oFocusTarget = oParentDomRef;
} else {
oFocusTarget = oParentDomRef && jQuery(oParentDomRef).firstFocusableDomRef();
}
break;
}
} while ((!oRes || oRes.startOver) && oDomRef);
// Apply focus to the found target
if (oFocusTarget) {
switch (oEvent.mode) {
case FocusMode.SYNC:
_focusTarget(oOriginalDomRef, oFocusTarget);
break;
case FocusMode.RENDERING_PENDING:
Rendering.addPrerenderingTask(() => {
_focusTarget(oOriginalDomRef, oFocusTarget);
});
break;
case FocusMode.DEFAULT:
default:
Promise.resolve().then(() => {
_focusTarget(oOriginalDomRef, oFocusTarget);
});
break;
}
}
};
Element.prototype.insertDependent = function(oElement, iIndex) {
this.insertAggregation("dependents", oElement, iIndex, true);
return this; // explicitly return 'this' to fix controls that override insertAggregation wrongly
};
Element.prototype.addDependent = function(oElement) {
this.addAggregation("dependents", oElement, true);
return this; // explicitly return 'this' to fix controls that override addAggregation wrongly
};
Element.prototype.removeDependent = function(vElement) {
return this.removeAggregation("dependents", vElement, true);
};
Element.prototype.removeAllDependents = function() {
return this.removeAllAggregation("dependents", true);
};
Element.prototype.destroyDependents = function() {
this.destroyAggregation("dependents", true);
return this; // explicitly return 'this' to fix controls that override destroyAggregation wrongly
};
/**
* Helper to identify the entire aggregation area that should be skipped when searchin for focusable elements.
* If the currently focused element is part of the aggregation being removed or destroyed,
* the entire aggregation area needs to be skipped sinceh its DOM element will be removed
* leaving no focusable element within the aggregation.
*
* @param {sap.ui.base.ManagedObject[]} aChildren The children that belong to the aggregation
* @returns {HTMLElement|null} Returns the DOM which needs to be skipped, or 'null' if no relevant area is found.
*/
function searchAggregationAreaToSkip(aChildren) {
let oSkipArea = null;
for (let i = 0; i < aChildren.length; i++) {
const oChild = aChildren[i];
const oDomRef = oChild?.getDomRef?.();
if (oDomRef) {
if (!oSkipArea) {
oSkipArea = oDomRef.parentElement;
} else {
while (oSkipArea && !oSkipArea.contains(oDomRef)) {
oSkipArea = oSkipArea.parentElement;
}
}
}
}
return oSkipArea;
}
/**
* Determines if the DOM removal needs to be performed synchronously.
*
* @param {boolean|string} bSuppressInvalidate - Whether invalidation is suppressed. If set to "KeepDom", the DOM is retained.
* @param {boolean} bHasNoParent Whether the element has no parent. If true, it suggests that the element is being removed from the DOM tree.
* @returns {boolean} Returns true, if synchronous DOM removal is needed; otherwise 'false'.
*/
function needSyncDomRemoval(bSuppressInvalidate, bHasNoParent) {
const bKeepDom = (bSuppressInvalidate === "KeepDom");
const oDomRef = this.getDomRef();
// Conditions that require sync DOM removal
return (bSuppressInvalidate === true || // Explicit supression of invalidation
(!bKeepDom && bHasNoParent) || // No parent and DOM should not be kept
this.isA("sap.ui.core.PopupInterface") || // The element is a popup
RenderManager.isPreservedContent(oDomRef)); // The element is part of the 'preserve' area.
}
/**
* Checks for a focused child within the provided children (array or single object)
* and fired the focus fail event if necessary.
*
* @param {sap.ui.base.ManagedObject[]|sap.ui.base.ManagedObject} vChildren The children to check. Can be an array or a single object.
* @param {boolean} bSuppressInvalidate If true, this ManagedObject is not marked as changed
*/
function checkAndFireFocusFail(vChildren, bSuppressInvalidate) {
let oFocusedChild = null;
let oSkipArea = null;
if (Array.isArray(vChildren)) {
for (let i = 0; i < vChildren.length; i++) {
const oChild = vChildren[i];
const oDomRef = oChild.getDomRef?.();
if (oDomRef?.contains(document.activeElement)) {
oFocusedChild = oChild;
}
}
if (oFocusedChild) {
oSkipArea = searchAggregationAreaToSkip(vChildren);
}
} else if (vChildren instanceof ManagedObject) {
oFocusedChild = vChildren;
}
if (!oFocusedChild) {
return;
}
const oDomRef = oFocusedChild.getDomRef?.();
if (oDomRef?.contains?.(document.activeElement) && !bSuppressInvalidate) {
// Determin if the DOM removal needs to happen sync or async
const bSyncRemoval = needSyncDomRemoval.call(oFocusedChild, bSuppressInvalidate, !this);
const sFocusMode = bSyncRemoval ? FocusMode.SYNC : FocusMode.RENDERING_PENDING;
if (!this._bIsBeingDestroyed) {
Element.fireFocusFail.call(oFocusedChild, sFocusMode, this, oSkipArea);
}
}
}
/**
* Sets a new object in the named 0..1 aggregation of this ManagedObject and marks this ManagedObject as changed.
* Manages the focus handling if the current aggregation is removed (i.e., when the object is set to <code>null</code>).
* If the previous object in the aggregation was focused, a "focusfail" event is triggered.
*
* @param {string}
* sAggregationName name of an 0..1 aggregation
* @param {sap.ui.base.ManagedObject}
* oObject the managed object that is set as aggregated object
* @param {boolean}
* [bSuppressInvalidate=true] if true, this ManagedObject is not marked as changed
* @returns {this} Returns <code>this</code> to allow method chaining
* @throws {Error}
* @protected
*/
Element.prototype.setAggregation = function(sAggregationName, oObject, bSuppressInvalidate) {
// Get current aggregation for the specified aggregation name before aggregation change
const oChild = this.getAggregation(sAggregationName);
// Call parent method to perform actual aggregation change
const vResult = ManagedObject.prototype.setAggregation.call(this, sAggregationName, oObject, bSuppressInvalidate);
if (oChild && oObject == null) {
checkAndFireFocusFail.call(this, oChild, bSuppressInvalidate);
}
return vResult;
};
/**
* Removes an object from the aggregation named <code>sAggregationName</code> with cardinality 0..n and manages
* focus handling in case the removed object was focused. If the removed object held the focus, a "focusfail" event
* is triggered to proper focus redirection.
*
* @param {string}
* sAggregationName the string identifying the aggregation that the given object should be removed from
* @param {int | string | sap.ui.base.ManagedObject}
* vObject the position or ID of the ManagedObject that should be removed or that ManagedObject itself;
* if <code>vObject</code> is invalid, a negative value or a value greater or equal than the current size
* of the aggregation, nothing is removed.
* @param {boolean}
* [bSuppressInvalidate=false] if true, this ManagedObject is not marked as changed
* @returns {sap.ui.base.ManagedObject|null} the removed object or <code>null</code>
* @protected
*/
Element.prototype.removeAggregation = function(sAggregationName, vObject, bSuppressInvalidate) {
const vResult = ManagedObject.prototype.removeAggregation.call(this, sAggregationName, vObject, bSuppressInvalidate);
checkAndFireFocusFail.call(this, vResult, bSuppressInvalidate);
return vResult;
};
/**
* Removes all child elements of a specified aggregation and handles focus management for elements that are currently focused.
* If the currently focused element belongs to the aggregation being removed, a "focusfail" event is triggered to shift the
* focus to a relevant element.
*
* @param {string} sAggregationName The name of the aggregation
* @param {boolean} [bSuppressInvalidate=false] If true, this ManagedObject is not marked as changed
* @returns {sap.ui.base.ManagedObject[]} An array of the removed elements (might be empty)
* @protected
*/
Element.prototype.removeAllAggregation = function(sAggregationName, bSuppressInvalidate) {
const aChildren = ManagedObject.prototype.removeAllAggregation.call(this, sAggregationName, bSuppressInvalidate);
checkAndFireFocusFail.call(this, aChildren, bSuppressInvalidate);
return aChildren;
};
/**
* Destroys all child elements of a specified aggregation and handles focus management for elements that are currently focused.
* If the currently focused element belongs to the aggregation being destroyed, a "focusfail" event is triggered to shift the
* focus to a relevant element.
*
* @param {string} sAggregationName The name of the aggregation
* @param {boolean} [bSuppressInvalidate=false] If true, this ManagedObject is not marked as changed
* @returns {this} Returns <code>this</code> to allow method chaining
* @protected
*/
Element.prototype.destroyAggregation = function(sAggregationName, bSuppressInvalidate) {
const aChildren = this.getAggregation(sAggregationName);
checkAndFireFocusFail.call(this, aChildren, bSuppressInvalidate);
return ManagedObject.prototype.destroyAggregation.call(this, sAggregationName, bSuppressInvalidate);
};
/**
* This triggers immediate rerendering of its parent and thus of itself and its children.
*
* @deprecated As of 1.70, using this method is no longer recommended, but calling it still
* causes a re-rendering of the element. Synchronous DOM updates via this method have several
* drawbacks: they only work when the control has been rendered before (no initial rendering
* possible), multiple state changes won't be combined automatically into a single re-rendering,
* they might cause additional layout thrashing, standard invalidation might cause another
* async re-rendering.
*
* The recommended alternative is to rely on invalidation and standard re-rendering.
*
* As <code>sap.ui.core.Element</code> "bubbles up" the rerender, changes to
* child-<code>Elements</code> will also result in immediate rerendering of the whole sub tree.
* @protected
*/
Element.prototype.rerender = function() {
if (this.oParent) {
this.oParent.rerender();
}
};
/**
* Returns the UI area of this element, if any.
*
* @return {sap.ui.core.UIArea|null} The UI area of this element or <code>null</code>
* @private
*/
Element.prototype.getUIArea = function() {
return this.oParent ? this.oParent.getUIArea() : null;
};
/**
* Fires a "focusfail" event to handle focus redirection when the current element loses focus due to a state change
* (e.g., disabled, invisible, or destroyed). The event is propagated to the parent of the current element to manage
* the focus shift.
*
* @param {string} sFocusHandlingMode The mode of focus handling, determining whether the focus should be handled sync or async.
* @param {sap.ui.core.Element} oParent The parent element that will handle the "focusfail" event.
* @param {HTMLElement} [oSkipArea=null] Optional DOM area to be skipped during focus redirection.
*
* @protected
*/
Element.fireFocusFail = function(sFocusHandlingMode, oParent, oSkipArea) {
const oEvent = jQuery.Event("focusfail");
oEvent.srcControl = this;
oEvent.mode = sFocusHandlingMode || FocusMode.DEFAULT;
oEvent._skipArea = oSkipArea;
oParent ??= this.getParent();
if (oParent && !oParent._bIsBeingDestroyed) {
oParent._handleEvent?.(oEvent);
}
};
/**
* Cleans up the resources associated with this element and all its children.
*
* After an element has been destroyed, it can no longer be used in the UI!
*
* Applications should call this method if they don't need the element any longer.
*
* @param {boolean} [bSuppressInvalidate=false] If <code>true</code>, this ManagedObject and all its ancestors won't be invalidated.
* <br>This flag should be used only during control development to optimize invalidation procedures.
* It should not be used by any application code.
* @public
*/
Element.prototype.destroy = function(bSuppressInvalidate) {
// ignore repeated calls
if (this.bIsDestroyed) {
return;
}
// determine whether parent exists or not
var bHasNoParent = !this.getParent();
// update the focus information (potentially) stored by the central UI5 focus handling
updateFocusInfo(this);
ManagedObject.prototype.destroy.call(this, bSuppressInvalidate);
// wrap custom data API to avoid creating new objects
this.data = noCustomDataAfterDestroy;
// exit early if there is no control DOM to remove
var oDomRef = this.getDomRef();
if (!oDomRef) {
return;
}
// Determine whether to remove the control DOM from the DOM Tree or not:
// If parent invalidation is not possible, either bSuppressInvalidate=true or there is no parent to invalidate then we must remove the control DOM synchronously.
// Controls that implement marker interface sap.ui.core.PopupInterface are by contract not rendered by their parent so we cannot keep the DOM of these controls.
// If the control is destroyed while its content is in the preserved area then we must remove DOM synchronously since we cannot invalidate the preserved area.
if (needSyncDomRemoval.call(this, bSuppressInvalidate, bHasNoParent)) {
jQuery(oDomRef).remove();
} else {
// Make sure that the control DOM won't get preserved after it is destroyed (even if bSuppressInvalidate="KeepDom")
oDomRef.removeAttribute("data-sap-ui-preserve");
if (bSuppressInvalidate !== "KeepDom") {
// On destroy we do not remove the control DOM synchronously and just let the invalidation happen on the parent.
// At the next tick of the RenderManager, control DOM nodes will be removed via rerendering of the parent anyway.
// To make this new behavior more compatible we are changing the id of the control's DOM and all child nodes that start with the control id.
oDomRef.id = "sap-ui-destroyed-" + this.getId();
for (var i = 0, aDomRefs = oDomRef.querySelectorAll('[id^="' + this.getId() + '-"]'); i < aDomRefs.length; i++) {
aDomRefs[i].id = "sap-ui-destroyed-" + aDomRefs[i].id;
}
}
}
};
/*
* Class <code>sap.ui.core.Element</code> intercepts fireEvent calls to enforce an 'id' property
* and to notify others like interaction detection etc.
*/
Element.prototype.fireEvent = function(sEventId, mParameters, bAllowPreventDefault, bEnableEventBubbling) {
if (this.hasListeners(sEventId)) {
Interaction.notifyStepStart(sEventId, this);
}
// get optional parameters right
if (typeof mParameters === 'boolean') {
bEnableEventBubbling = bAllowPreventDefault;
bAllowPreventDefault = mParameters;
mParameters = null;
}
mParameters = mParameters || {};
mParameters.id = mParameters.id || this.getId();
ElementHooks.interceptEvent?.(sEventId, this, mParameters);
return ManagedObject.prototype.fireEvent.call(this, sEventId, mParameters, bAllowPreventDefault, bEnableEventBubbling);
};
/**
* Updates the count of rendering-related delegates and if the given threshold is reached,
* informs the RenderManager` to enable/disable rendering V4 for the element.
*
* @param {sap.ui.core.Element} oElement The element instance
* @param {object} oDelegate The delegate instance
* @param {iThresholdCount} iThresholdCount Whether the delegate has been added=1 or removed=0.
* At the same time serves as threshold when to inform the `RenderManager`.
* @private
*/
function updateRenderingDelegate(oElement, oDelegate, iThresholdCount) {
if (oDelegate.canSkipRendering || !(oDelegate.onAfterRendering || oDelegate.onBeforeRendering)) {
return;
}
oElement._iRenderingDelegateCount += (iThresholdCount || -1);
if (oElement.bOutput === true && oElement._iRenderingDelegateCount == iThresholdCount) {
RenderManager.canSkipRendering(oElement, 1 /* update skip-the-rendering DOM marker, only if the apiVersion is 4 */);
}
}
/**
* Returns whether the element has rendering-related delegates that might prevent skipping the rendering.
*
* @returns {boolean}
* @private
* @ui5-restricted sap.ui.core.RenderManager
*/
Element.prototype.hasRenderingDelegate = function() {
return Boolean(this._iRenderingDelegateCount);
};
/**
* Adds a delegate that listens to the events of this element.
*
* Note that the default behavior (delegate attachments are not cloned when a control is cloned) is usually the desired behavior in control development
* where each control instance typically creates a delegate and adds it to itself. (As opposed to application development where the application may add
* one delegate to a template and then expects aggregation binding to add the same delegate to all cloned elements.)
*
* To avoid double registrations, all registrations of the given delegate are first removed and then the delegate is added.
*
* @param {object} oDelegate the delegate object
* @param {boolean} [bCallBefore=false] if true, the delegate event listeners are called before the event listeners of the element; default is "false". In order to also set bClone, this parameter must be given.
* @param {object} [oThis=oDelegate] if given, this object will be the "this" context in the listener methods; default is the delegate object itself
* @param {boolean} [bClone=false] if true, this delegate will also be attached to any clones of this element; default is "false"
* @returns {this} Returns <code>this</code> to allow method chaining
* @private
*/
Element.prototype.addDelegate = function (oDelegate, bCallBefore, oThis, bClone) {
assert(oDelegate, "oDelegate must be not null or undefined");
if (!oDelegate) {
return this;
}
this.removeDelegate(oDelegate);
// shift parameters
if (typeof bCallBefore === "object") {
bClone = oThis;
oThis = bCallBefore;
bCallBefore = false;
}
if (typeof oThis === "boolean") {
bClone = oThis;
oThis = undefined;
}
(bCallBefore ? this.aBeforeDelegates : this.aDelegates).push({oDelegate:oDelegate, bClone: !!bClone, vThis: ((oThis === this) ? true : oThis)}); // special case: if this element is the given context, set a flag, so this also works after cloning (it should be the cloned element then, not the given one)
updateRenderingDelegate(this, oDelegate, 1);
return this;
};
/**
* Removes the given delegate from this element.
*
* This method will remove all registrations of the given delegate, not only one.
* If the delegate was marked to be cloned and this element has been cloned, the delegate will not be removed from any clones.
*
* @param {object} oDelegate the delegate object
* @returns {this} Returns <code>this</code> to allow method chaining
* @private
*/
Element.prototype.removeDelegate = function (oDelegate) {
var i;
for (i = 0; i < this.aDelegates.length; i++) {
if (this.aDelegates[i].oDelegate == oDelegate) {
this.aDelegates.splice(i, 1);
updateRenderingDelegate(this, oDelegate, 0);
i--; // One element removed means the next element now has the index of the current one
}
}
for (i = 0; i < this.aBeforeDelegates.length; i++) {
if (this.aBeforeDelegates[i].oDelegate == oDelegate) {
this.aBeforeDelegates.splice(i, 1);
updateRenderingDelegate(this, oDelegate, 0);
i--; // One element removed means the next element now has the index of the current one
}
}
return this;
};
/**
* Adds a delegate that can listen to the browser-, pseudo- and framework events that are handled by this
* <code>Element</code> (as opposed to events which are fired by this <code>Element</code>).
*
* Delegates are simple objects that can have an arbitrary number of event handler methods. See the section
* "Handling of Events" in the {@link #constructor} documentation to learn how events will be dispatched
* and how event handler methods have to be named to be found.
*
* If multiple delegates are registered for the same element, they will be called in the order of their
* registration. Double registrations are prevented. Before a delegate is added, all registrations of the same
* delegate (no matter what value for <code>oThis</code> was used for their registration) are removed and only
* then the delegate is added. Note that this might change the position of the delegate in the list of delegates.
*
* When an element is cloned, all its event delegates will be added to the clone. This behavior is well-suited
* for applications which want to add delegates that also work with templates in aggregation bindings.
* For control development, the internal <code>addDelegate</code> method may be more suitable. Delegates added
* via that method are not cloned automatically, as typically each control instance takes care of adding its
* own delegates.
*
* <strong>Important:</strong> If event delegates were added, the delegate will still be called even if
* the event was processed and/or cancelled via <code>preventDefault</code> by the Element or another event delegate.
* <code>preventDefault</code> only prevents the event from bubbling.
* It should be checked e.g. in the event delegate's listener whether an Element is still enabled via <code>getEnabled</code>.
* Additionally there might be other things that delegates need to check depending on the event
* (e.g. not adding a key twice to an output string etc.).
*
* See {@link topic:bdf3e9818cd84d37a18ee5680e97e1c1 Event Handler Methods} for a general explanation of
* event handling in controls.
*
* <b>Note:</b> Setting the special <code>canSkipRendering</code> property to <code>true</code> for the event delegate
* object itself lets the framework know that the <code>onBeforeRendering</code> and <code>onAfterRendering</code>
* event handlers of the delegate are compatible with the contract of {@link sap.ui.core.RenderManager Renderer.apiVersion 4}.
* See example "Adding a rendering delegate...".
*
* @example <caption>Adding a delegate for the keydown and afterRendering event</caption>
* <pre>
* var oDelegate = {
* onkeydown: function(){
* // Act when the keydown event is fired on the element
* },
* onAfterRen