UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

1,150 lines (1,045 loc) 95.1 kB
/*! * 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