UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

437 lines (366 loc) 14.2 kB
/*! * OpenUI5 * (c) Copyright 2009-2023 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 = {}; // The controls which should not be referenced by a "for" attribute (Specified in the HTML standard). // Extend when needed. var NON_LABELABLE_CONTROLS = [ "sap.ui.comp.navpopover.SmartLink", "sap.m.Link", "sap.m.Label", "sap.m.Text", "sap.ui.webc.main.Label", "sap.ui.webc.main.Link" ]; // Returns the control for the given id (if available) and invalidates it if desired function toControl(sId, bInvalidate) { if (!sId) { return null; } var oControl = sap.ui.getCore().byId(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(label) { var sId = label.getLabelFor() || label._sAlternativeId || ''; return sId; } // Updates the mapping tables for the given label, in destroy case only a cleanup is done function refreshMapping(oLabel, bDestroy){ var sLabelId = oLabel.getId(); var sOldId = oLabel.__sLabeledControl; var sNewId = bDestroy ? null : findLabelForControl(oLabel); if (sOldId == sNewId) { return; } //Invalidate the label itself (see setLabelFor, setAlternativeLabelFor) if (!bDestroy) { oLabel.invalidate(); } //Update the label to control mapping (1-1 mapping) if (sNewId) { oLabel.__sLabeledControl = sNewId; } 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) { aLabelsOfControl = aLabelsOfControl.filter(function(sCurrentLabelId) { return sCurrentLabelId != sLabelId; }); if (aLabelsOfControl.length) { CONTROL_TO_LABELS_MAPPING[sOldId] = aLabelsOfControl; } else { delete CONTROL_TO_LABELS_MAPPING[sOldId]; } } } if (sNewId) { aLabelsOfControl = CONTROL_TO_LABELS_MAPPING[sNewId] || []; aLabelsOfControl.push(sLabelId); CONTROL_TO_LABELS_MAPPING[sNewId] = aLabelsOfControl; } //Invalidate related controls var oOldControl = toControl(sOldId, true); var oNewControl = toControl(sNewId, 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; } var sName = oControl.getMetadata().getName(); return NON_LABELABLE_CONTROLS.indexOf(sName) < 0; } /** * Helper functionality for enhancement of a <code>Label</code> with common label functionality. * * @see sap.ui.core.LabelEnablement#enrich * * @author SAP SE * @version 1.111.5 * @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 || !oLabel.getLabelForRendering) { return; } var sControlId = oLabel.getLabelForRendering(); if (!sControlId) { return; } var oControl = toControl(sControlId); if (oControl && oControl.getIdForLabel) { // for some controls the label must point to a special HTML element, not the outer one. sControlId = oControl.getIdForLabel(); } // The "for" attribute should only reference labelable HTML elements. if (sControlId && isLabelableControl(oControl)) { oRenderManager.attr("for", sControlId); } }; /** * 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] || []; }; /** * 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; for (var i = 0; i < aLabelIds.length; i++) { oLabel = sap.ui.getCore().byId(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 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); return isLabelableControl(oControl) ? sId : ""; }; 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);