UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

581 lines (486 loc) 19.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 helper sap.ui.core.LabelEnablement sap.ui.define(['../base/ManagedObject', "sap/base/assert"], function(ManagedObject, assert) { "use strict"; // Mapping between controls and labels var CONTROL_TO_LABELS_MAPPING = {}; // Mapping between the outer control and the inner control when outer control overwrites 'getIdForLabel' const CONTROL_TO_INNERCONTROL_MAPPING = {}; var Element; // Returns the control for the given id (if available) and invalidates it if desired function toControl(sId, bInvalidate) { if (!sId) { return null; } Element ??= sap.ui.require("sap/ui/core/Element"); var oControl = Element.getElementById(sId); // a control must only be invalidated if there is already a DOM Ref. If there is no DOM Ref yet, it will get // rendered later in any case. Elements must always be invalidated because they have no own renderer. if (oControl && bInvalidate && (!oControl.isA('sap.ui.core.Control') || oControl.getDomRef())) { oControl.invalidate(); } return oControl; } function findLabelForControl(oLabel, fnOnAfterRendering) { const sId = oLabel.getLabelFor() || oLabel._sAlternativeId || ''; const oRes = { controlId: sId }; Element ??= sap.ui.require("sap/ui/core/Element"); const oControl = Element.getElementById(sId); if (oControl && typeof oControl.getIdForLabel === "function") { const sDomIdForLabel = oControl.getIdForLabel(); if (sDomIdForLabel !== oControl.getId()) { const oDomForLabel = document.getElementById(sDomIdForLabel); if (!oDomForLabel) { // The inner control based on 'getIdForLabel' isn't rendered yet // Wait for the next rendering and call the given callback const oDelegate = { onAfterRendering: function(oLabel) { this.removeEventDelegate(oDelegate); if (typeof fnOnAfterRendering === "function" && !oLabel.bIsDestroyed) { fnOnAfterRendering(oLabel); } }.bind(oControl, oLabel) }; oControl.addEventDelegate(oDelegate); } else { const oControlForLabel = Element.closestTo(oDomForLabel); if (oControlForLabel) { const sInnerControlId = oControlForLabel.getId(); if (sInnerControlId !== sId) { oRes.innerControlId = sInnerControlId; } } } } } return oRes; } // Updates the mapping tables for the given label, in destroy case only a cleanup is done function refreshMapping(oLabel, bDestroy, bAfterRendering){ var sLabelId = oLabel.getId(); var sOldId = oLabel.__sLabeledControl; var oNewIdInfo = bDestroy ? null : findLabelForControl(oLabel, (oLabel) => { if (!bAfterRendering) { refreshMapping(oLabel, false /* bDestroy */, true /* bAfterRendering */); } }); if (oNewIdInfo && sOldId === oNewIdInfo.controlId && oNewIdInfo.innerControlId === CONTROL_TO_INNERCONTROL_MAPPING[oNewIdInfo.controlId]) { return; } //Invalidate the label itself (see setLabelFor, setAlternativeLabelFor) if (!bDestroy) { oLabel.invalidate(); } //Update the label to control mapping (1-1 mapping) if (oNewIdInfo?.controlId) { oLabel.__sLabeledControl = oNewIdInfo.controlId; } else { delete oLabel.__sLabeledControl; } //Update the control to label mapping (1-n mapping) var aLabelsOfControl; if (sOldId) { aLabelsOfControl = CONTROL_TO_LABELS_MAPPING[sOldId]; if (aLabelsOfControl) { const sInnerControlId = CONTROL_TO_INNERCONTROL_MAPPING[sOldId]; aLabelsOfControl = aLabelsOfControl.filter(function(sCurrentLabelId) { return sCurrentLabelId != sLabelId; }); if (aLabelsOfControl.length) { CONTROL_TO_LABELS_MAPPING[sOldId] = aLabelsOfControl; if (sInnerControlId) { CONTROL_TO_LABELS_MAPPING[sInnerControlId] = aLabelsOfControl; } } else { delete CONTROL_TO_LABELS_MAPPING[sOldId]; if (sInnerControlId) { delete CONTROL_TO_LABELS_MAPPING[sInnerControlId]; delete CONTROL_TO_INNERCONTROL_MAPPING[sOldId]; } } } } if (oNewIdInfo?.controlId) { aLabelsOfControl = CONTROL_TO_LABELS_MAPPING[oNewIdInfo.controlId] || []; aLabelsOfControl.push(sLabelId); CONTROL_TO_LABELS_MAPPING[oNewIdInfo.controlId] = aLabelsOfControl; if (oNewIdInfo.innerControlId) { CONTROL_TO_LABELS_MAPPING[oNewIdInfo.innerControlId] = aLabelsOfControl; CONTROL_TO_INNERCONTROL_MAPPING[oNewIdInfo.controlId] = oNewIdInfo.innerControlId; } else { const sExistingInnerControl = CONTROL_TO_INNERCONTROL_MAPPING[oNewIdInfo.controlId]; if (sExistingInnerControl) { delete CONTROL_TO_LABELS_MAPPING[sExistingInnerControl]; delete CONTROL_TO_INNERCONTROL_MAPPING[oNewIdInfo.controlId]; } } } //Invalidate related controls var oOldControl = toControl(sOldId, true); var oNewControl = toControl(oNewIdInfo?.controlId, true); if (oOldControl) { oLabel.detachRequiredChange(oOldControl); } if (oNewControl) { oLabel.attachRequiredChange(oNewControl); } } // Checks whether enrich function can be applied on the given control or prototype. function checkLabelEnablementPreconditions(oControl) { if (!oControl) { throw new Error("sap.ui.core.LabelEnablement cannot enrich null"); } var oMetadata = oControl.getMetadata(); if (!oMetadata.isInstanceOf("sap.ui.core.Label")) { throw new Error("sap.ui.core.LabelEnablement only supports Controls with interface sap.ui.core.Label"); } var oLabelForAssociation = oMetadata.getAssociation("labelFor"); if (!oLabelForAssociation || oLabelForAssociation.multiple) { throw new Error("sap.ui.core.LabelEnablement only supports Controls with a to-1 association 'labelFor'"); } //Add more detailed checks here ? } // Checks if the control is labelable according to the HTML standard // The labelable HTML elements are: button, input, keygen, meter, output, progress, select, textarea // Related incident 1770049251 function isLabelableControl(oControl) { if (!oControl) { return true; } if (oControl.isA("sap.ui.core.ILabelable")) { return oControl.hasLabelableHTMLElement(); } return true; } /** * Helper functionality for enhancement of a <code>Label</code> with common label functionality. * * @see sap.ui.core.LabelEnablement#enrich * * @author SAP SE * @version 1.147.0 * @protected * @alias sap.ui.core.LabelEnablement * @namespace * @since 1.28.0 */ var LabelEnablement = {}; /** * Helper function for the <code>Label</code> control to render the HTML 'for' attribute. * * This function should be called at the desired location in the renderer code of the <code>Label</code> control. * It can be used with both rendering APIs, with the new semantic rendering API (<code>apiVersion 2</code>) * as well as with the old, string-based API. * * As this method renders an attribute, it can only be called while a start tag is open. For the new semantic * rendering API, this means it can only be called between an <code>openStart/voidStart</code> call and the * corresponding <code>openEnd/voidEnd</code> call. In the context of the old rendering API, it can be called * only after the prefix of a start tag has been written (e.g. after <code>rm.write("&lt;span id=\"foo\"");</code>), * but before the start tag ended, e.g before the right-angle ">" of the start tag has been written. * * @param {sap.ui.core.RenderManager} oRenderManager The RenderManager that can be used for rendering. * @param {sap.ui.core.Label} oLabel The <code>Label</code> for which the 'for' HTML attribute should be rendered. * @protected */ LabelEnablement.writeLabelForAttribute = function(oRenderManager, oLabel) { if (!oLabel) { return; } const oControlInfo = findLabelForControl(oLabel, (oLabel) => { oLabel.invalidate(); }); if (!oControlInfo.controlId) { return; } Element ??= sap.ui.require("sap/ui/core/Element"); const oControl = Element.getElementById(oControlInfo.innerControlId || oControlInfo.controlId); // The "for" attribute should only reference labelable HTML elements. if (oControl && typeof oControl.getIdForLabel === "function" && isLabelableControl(oControl)) { oRenderManager.attr("for", oControl.getIdForLabel()); } }; /** * Returns an array of IDs of the labels referencing the given element. * * @param {sap.ui.core.Element} oElement The element whose referencing labels should be returned * @returns {string[]} an array of ids of the labels referencing the given element * @public */ LabelEnablement.getReferencingLabels = function(oElement){ var sId = oElement ? oElement.getId() : null; if (!sId) { return []; } return CONTROL_TO_LABELS_MAPPING[sId] || []; }; /** * Collect the label texts for the given UI5 Element from the following sources: * * The label returned from the function "getFieldHelpInfo" * * The ids of label controls from labelling controls in LabelEnablement * * The ids of label controls from "ariaLabelledBy" Association * * The label and ids of label controls is enhanced by calling "enhanceAccessibilityState" of the parent control * * @param {sap.ui.core.Element} oElement The UI5 element for which the label texts are collected * @return {string[]} An array of label texts for the given UI5 element * @ui5-restricted sap.ui.core */ LabelEnablement._getLabelTexts = function(oElement) { // gather labels and labelledby ids const mLabelInfo = {}; const oInfo = oElement.getFieldHelpInfo?.(); if (oInfo?.label) { mLabelInfo.label = oInfo.label; } let aLabelIds = LabelEnablement.getReferencingLabels(oElement); if (aLabelIds.length) { mLabelInfo.labelledby = aLabelIds; } if (oElement.getMetadata().getAssociation("ariaLabelledBy")) { aLabelIds = oElement.getAriaLabelledBy(); if (aLabelIds.length) { mLabelInfo.labelledby ??= []; aLabelIds.forEach((sLabelId) => { if (!mLabelInfo.labelledby.includes(sLabelId)) { mLabelInfo.labelledby.push(sLabelId); } }); } } if (mLabelInfo.labelledby?.length) { mLabelInfo.labelledby = mLabelInfo.labelledby.join(" "); } // enhance it with parent control oElement.getParent()?.enhanceAccessibilityState?.(oElement, mLabelInfo); // merge the labels const aLabels = mLabelInfo.label ? [mLabelInfo.label] : []; if (mLabelInfo.labelledby) { mLabelInfo.labelledby.split(" ") .forEach((sLabelId) => { const oLabelControl = Element.getElementById(sLabelId); if (oLabelControl) { const sLabelText = oLabelControl.getText?.() || oLabelControl.getDomRef()?.innerText; if (sLabelText) { aLabels.push(sLabelText); } } }); } return aLabels; }; /** * Returns <code>true</code> when the given control is required (property 'required') or one of its referencing labels, <code>false</code> otherwise. * * @param {sap.ui.core.Element} oElement The element which should be checked for its required state * @returns {boolean} <code>true</code> when the given control is required (property 'required') or one of its referencing labels, <code>false</code> otherwise * @public * @since 1.29.0 */ LabelEnablement.isRequired = function(oElement){ if (checkRequired(oElement)) { return true; } var aLabelIds = LabelEnablement.getReferencingLabels(oElement), oLabel; Element ??= sap.ui.require("sap/ui/core/Element"); for (var i = 0; i < aLabelIds.length; i++) { oLabel = Element.getElementById(aLabelIds[i]); if (checkRequired(oLabel)) { return true; } } return false; }; function checkRequired(oElem) { return !!(oElem && oElem.getRequired && oElem.getRequired()); } /** * This function should be called on a label control to enrich its functionality. * * <b>Usage:</b> * The function can be called with a control prototype: * <code> * sap.ui.core.LabelEnablement.enrich(my.Label.prototype); * </code> * Or the function can be called on instance level in the init function of a label control: * <code> * my.Label.prototype.init: function(){ * sap.ui.core.LabelEnablement.enrich(this); * } * </code> * * <b>Preconditions:</b> * The given control must implement the interface sap.ui.core.Label and have an association 'labelFor' with cardinality 0..1. * This function extends existing API functions. Ensure not to override these extensions AFTER calling this function. * * <b>What does this function do?</b> * * A mechanism is added that ensures that a bidirectional reference between the label and its labeled control is established: * The label references the labeled control via the HTML 'for' attribute (see {@link sap.ui.core.LabelEnablement#writeLabelForAttribute}). * If the labeled control supports the aria-labelledby attribute, a reference to the label is added automatically. * * In addition an alternative to apply a 'for' reference without influencing the labelFor association of the API is applied (e.g. used by Form). * For this purpose the functions setAlternativeLabelFor and getLabelForRendering are added. * * @param {sap.ui.core.Control} oControl the label control which should be enriched with further label functionality. * @throws Error if the given control cannot be enriched to violated preconditions (see above) * @protected */ LabelEnablement.enrich = function(oControl) { //Ensure that enhancement possible checkLabelEnablementPreconditions(oControl); oControl.__orig_setLabelFor = oControl.setLabelFor; oControl.setLabelFor = function(sId) { var res = this.__orig_setLabelFor.apply(this, arguments); refreshMapping(this); return res; }; oControl.__orig_exit = oControl.exit; oControl.exit = function() { this._sAlternativeId = null; refreshMapping(this, true); if (oControl.__orig_exit) { oControl.__orig_exit.apply(this, arguments); } }; // Alternative to apply a for reference without influencing the labelFor association of the API (see e.g. FormElement) oControl.setAlternativeLabelFor = function(sId) { if (sId instanceof ManagedObject) { sId = sId.getId(); } else if (sId != null && typeof sId !== "string") { assert(false, "setAlternativeLabelFor(): sId must be a string, an instance of sap.ui.base.ManagedObject or null"); return this; } this._sAlternativeId = sId; refreshMapping(this); return this; }; // Returns id of the labelled control. The labelFor association is preferred before AlternativeLabelFor. oControl.getLabelForRendering = function() { var sId = this.getLabelFor() || this._sAlternativeId; var oControl = toControl(sId); var oLabelForControl; Element ??= sap.ui.require("sap/ui/core/Element"); if (oControl && !oControl.isA("sap.ui.core.ILabelable") && oControl.getIdForLabel && oControl.getIdForLabel()) { oLabelForControl = Element.getElementById(oControl.getIdForLabel()); if (oLabelForControl) { oControl = oLabelForControl; } } return isLabelableControl(oControl) ? sId : ""; }; oControl.isLabelFor = function(oControl) { var sId = oControl.getId(); var aLabels = CONTROL_TO_LABELS_MAPPING[sId]; return aLabels && aLabels.indexOf(this.getId()) > -1; }; if (!oControl.getMetadata().getProperty("required")) { return; } oControl.__orig_setRequired = oControl.setRequired; oControl.setRequired = function(bRequired) { var bOldRequired = this.getRequired(), oReturn = this.__orig_setRequired.apply(this, arguments); // invalidate the related control only when needed if (this.getRequired() !== bOldRequired) { toControl(this.__sLabeledControl, true); } return oReturn; }; /** * Checks whether the <code>Label</code> itself or the associated control is marked as required (they are mutually exclusive). * * @protected * @returns {boolean} Returns if the Label or the labeled control are required */ oControl.isRequired = function(){ // the value of the local required flag is ORed with the result of a "getRequired" // method of the associated "labelFor" control. If the associated control doesn't // have a getRequired method, this is treated like a return value of "false". var oFor = toControl(this.getLabelForRendering(), false); return checkRequired(this) || checkRequired(oFor); }; /** * Checks whether the <code>Label</code> should be rendered in display only mode. * * In the standard case it just uses the DisplayOnly property of the <code>Label</code>. * * In the Form another type of logic is used. * Maybe later on also the labeled controls might be used to determine the rendering. * * @protected * @returns {boolean} Returns if the Label should be rendered in display only mode */ oControl.isDisplayOnly = function(){ if (this.getDisplayOnly) { return this.getDisplayOnly(); } else { return false; } }; /** * Checks whether the <code>Label</code> should be rendered wrapped instead of trucated. * * In the standard case it just uses the <code>Wrapping</code> property of the <code>Label</code>. * * In the Form another type of logic is used. * * @protected * @returns {boolean} Returns if the Label should be rendered in display only mode */ oControl.isWrapping = function(){ if (this.getWrapping) { return this.getWrapping(); } else { return false; } }; // as in the Form the required change is checked, it'd not needed here oControl.disableRequiredChangeCheck = function(bNoCheck){ this._bNoRequiredChangeCheck = bNoCheck; }; oControl.attachRequiredChange = function(oFor){ if (oFor && !this._bNoRequiredChangeCheck) { if (oFor.getMetadata().getProperty("required")) { oFor.attachEvent("_change", _handleControlChange, this); } this._bRequiredAttached = true; // to do not check again if control has no required property } }; oControl.detachRequiredChange = function(oFor){ if (oFor && !this._bNoRequiredChangeCheck) { if (oFor.getMetadata().getProperty("required")) { oFor.detachEvent("_change", _handleControlChange, this); } this._bRequiredAttached = false; // to do not check again if control has no required property } }; function _handleControlChange(oEvent) { if (oEvent.getParameter("name") == "required") { this.invalidate(); } } oControl.__orig_onAfterRendering = oControl.onAfterRendering; oControl.onAfterRendering = function(oEvent) { var res; if (this.__orig_onAfterRendering) { res = this.__orig_onAfterRendering.apply(this, arguments); } if (!this._bNoRequiredChangeCheck && !this._bRequiredAttached && this.__sLabeledControl) { var oFor = toControl(this.__sLabeledControl, false); this.attachRequiredChange(oFor); } return res; }; }; return LabelEnablement; }, /* bExport= */ true);