@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
437 lines (366 loc) • 14.2 kB
JavaScript
/*!
* 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("<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);