@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
580 lines (503 loc) • 18.8 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2009-2021 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
// Provides class sap.ui.core.DeclarativeSupport
sap.ui.define([
'sap/ui/thirdparty/jquery',
'sap/ui/base/DataType',
'sap/ui/base/ManagedObject',
'./Control',
'./CustomData',
'./HTML',
'./mvc/View',
'./mvc/EventHandlerResolver',
'sap/base/Log',
'sap/base/util/ObjectPath',
'sap/base/assert',
'sap/base/strings/camelize'
],
function(
jQuery,
DataType,
ManagedObject,
Control,
CustomData,
HTML,
View,
EventHandlerResolver,
Log,
ObjectPath,
assert,
camelize
) {
"use strict";
/**
* @class Static class for enabling declarative UI support.
*
* @author Peter Muessig, Tino Butz
* @version 1.87.1
* @since 1.7.0
* @public
* @alias sap.ui.core.DeclarativeSupport
*/
var DeclarativeSupport = {
};
/**
* Defines the attributes of an element that should be handled differently.
* Set key/value pairs. The key indicates the attribute. The value can be of type <code>Boolean</code> or <code>Function</code>.
* When the value is of type <code>Function</code> it will receive three arguments:
* <code>sValue</code> the value of the attribute,
* <code>mSettings</code> the settings of the control
* <code>fnClass</code> the control class
* @private
*/
DeclarativeSupport.attributes = {
"data-sap-ui-type" : true,
"data-sap-ui-id" : true,
"data-sap-ui-aggregation" : true,
"data-sap-ui-default-aggregation" : true,
"data-sap-ui-binding" : function(sValue, mSettings) {
var oBindingInfo = ManagedObject.bindingParser(sValue);
// TODO reject complex bindings, types, formatters; enable 'parameters'?
mSettings.objectBindings = mSettings.objectBindings || {};
mSettings.objectBindings[oBindingInfo.model || undefined] = oBindingInfo;
},
"data-tooltip" : function(sValue, mSettings) {
// special handling for tooltip (which is an aggregation)
// but can also be applied as property
mSettings["tooltip"] = sValue;
},
"tooltip" : function(sValue, mSettings, fnClass) {
// TODO: Remove this key / value when deprecation is removed
mSettings["tooltip"] = sValue;
Log.warning('[Deprecated] Control "' + mSettings.id + '": The attribute "tooltip" is not prefixed with "data-*". Future version of declarative support will only suppport attributes with "data-*" prefix.');
},
"class":true,
"style" : true,
"id" : true
};
/**
* Enhances the given DOM element by parsing the Control and Elements info and creating
* the SAPUI5 controls for them.
*
* @param {Element} oElement the DOM element to compile
* @param {sap.ui.core.mvc.HTMLView} [oView] The view instance to use
* @param {boolean} [isRecursive] Whether the call of the function is recursive.
* @public
*/
DeclarativeSupport.compile = function(oElement, oView, isRecursive) {
// Find all defined classes
var that = this;
jQuery(oElement).find("[data-sap-ui-type]").filter(function() {
return jQuery(this).parents("[data-sap-ui-type]").length === 0;
}).each(function() {
that._compile(this, oView, isRecursive);
});
};
/**
* Enhances the given element by parsing the attributes and child elements.
*
* @param {Element} oElement the element to compile
* @param {sap.ui.core.mvc.HTMLView} [oView] The view instance to use
* @param {boolean} [isRecursive] Whether the call of the function is recursive.
* @private
*/
DeclarativeSupport._compile = function(oElement, oView, isRecursive) {
var $element = jQuery(oElement);
var sType = $element.attr("data-sap-ui-type");
var aControls = [];
var bIsUIArea = sType === "sap.ui.core.UIArea";
if (bIsUIArea) {
// use a UIArea / better performance when rendering multiple controls
// parse and create the controls / children of element
var that = this;
$element.children().each(function() {
var oControl = that._createControl(this, oView);
if (oControl) {
aControls.push(oControl);
}
});
} else {
var oControl = this._createControl(oElement, oView);
if (oControl) {
aControls.push(oControl);
}
}
// remove the old content
$element.empty();
// in case of the root control is not a UIArea we remove all HTML attributes
// for a UIArea we remove only the data HTML attributes and keep the others
// also marks the control as parsed (by removing data-sap-ui-type)
var aAttr = [];
jQuery.each(oElement.attributes, function(iIndex, oAttr) {
var sName = oAttr.name;
if (!bIsUIArea || bIsUIArea && /^data-/g.test(sName.toLowerCase())) {
aAttr.push(sName);
}
});
if (aAttr.length > 0) {
$element.removeAttr(aAttr.join(" "));
}
// add the controls
aControls.forEach(function(oControl) {
if (oControl instanceof Control) {
if (oView && !isRecursive) {
oView.addContent(oControl);
} else {
oControl.placeAt(oElement);
if (oView) {
// Remember the unassociated control so that it can be destroyed on exit of the view
oView.connectControl(oControl);
}
}
}
});
};
/**
* Parses a given DOM ref and converts it into a Control.
* @param {Element} oElement reference to a DOM element
* @param {sap.ui.core.mvc.HTMLView} [oView] The view instance to use.
* @return {sap.ui.core.Control} reference to a Control
* @private
*/
DeclarativeSupport._createControl = function(oElement, oView) {
var $element = jQuery(oElement);
var oControl = null;
var sType = $element.attr("data-sap-ui-type");
if (sType) {
var fnClass = sap.ui.requireSync(sType.replace(/\./g, "/")); // make sure fnClass.getMatadata() is available
fnClass = fnClass || ObjectPath.get(sType);
assert(typeof fnClass !== "undefined", "Class not found: " + sType);
var mSettings = {};
mSettings.id = this._getId($element, oView);
if ( oView && oView._sProcessingMode != null && fnClass.getMetadata().hasSpecialSetting("processingMode") ) {
mSettings.processingMode = oView._sProcessingMode;
}
this._addSettingsForAttributes(mSettings, fnClass, oElement, oView);
this._addSettingsForAggregations(mSettings, fnClass, oElement, oView);
var oControl;
if (View.prototype.isPrototypeOf(fnClass.prototype) && typeof fnClass._sType === "string") {
// for views having a factory function defined we use the factory function!
oControl = View._legacyCreate(mSettings, undefined, fnClass._sType);
} else {
oControl = new fnClass(mSettings);
}
if (oElement.className) {
oControl.addStyleClass(oElement.className);
}
// mark control as parsed
$element.removeAttr("data-sap-ui-type");
} else {
oControl = this._createHtmlControl(oElement, oView);
}
return oControl;
};
/**
* Parses a given DOM ref and converts it into an HTMLControl.
* @param {Element} oElement reference to a DOM element
* @param {sap.ui.core.mvc.HTMLView} [oView] The view instance to use.
* @return {sap.ui.core.HTML} reference to a Control
* @private
*/
DeclarativeSupport._createHtmlControl = function(oElement, oView) {
//include HTML content
var oHTML = new HTML();
oHTML.setDOMContent(oElement);
// check for declarative content
this.compile(oElement, oView, true);
return oHTML;
};
/**
* Adds all defined attributes to the settings object of a control.
*
* @param {object} mSettings reference of the settings of the control
* @param {function} fnClass reference to a Class
* @param {Element} oElement reference to a DOM element
* @param {sap.ui.core.mvc.HTMLView} [oView] The view instance to use.
* @return {object} the settings of the control.
* @throws {Error} if an attribute is not supported
* @private
*/
DeclarativeSupport._addSettingsForAttributes = function(mSettings, fnClass, oElement, oView) {
var that = this;
var oSpecialAttributes = DeclarativeSupport.attributes;
var fnBindingParser = ManagedObject.bindingParser;
var aCustomData = [];
var reCustomData = /^data-custom-data:(.+)/i;
jQuery.each(oElement.attributes, function(iIndex, oAttr) {
var sName = oAttr.name;
var sValue = oAttr.value;
if (!reCustomData.test(sName)) {
// no custom data attribute:
if (typeof oSpecialAttributes[sName] === "undefined") {
sName = that.convertAttributeToSettingName(sName, mSettings.id);
var oProperty = that._getProperty(fnClass, sName);
if (oProperty) {
var oBindingInfo = fnBindingParser(sValue, oView && oView.getController(), true );
if ( oBindingInfo && typeof oBindingInfo === "object" ) {
mSettings[sName] = oBindingInfo;
} else {
mSettings[sName] = that.convertValueToType(that.getPropertyDataType(oProperty), oBindingInfo || sValue);
}
} else if (that._getAssociation(fnClass, sName)) {
var oAssociation = that._getAssociation(fnClass, sName);
if (oAssociation.multiple) {
// we support "," and " " to split between IDs
sValue = sValue.replace(/\s*,\s*|\s+/g, ","); // normalize strings: "id1 , id2 id3" to "id1,id2,id3"
// split array for all ","
mSettings[sName] = sValue.split(",").map(function(sId) {
return oView ? oView.createId(sId) : sId;
});
} else {
mSettings[sName] = oView ? oView.createId(sValue) : sValue; // use the value as ID
}
} else if (that._getAggregation(fnClass, sName)) {
var oAggregation = that._getAggregation(fnClass, sName);
if (oAggregation.multiple) {
var oBindingInfo = fnBindingParser(sValue, oView && oView.getController());
if (oBindingInfo) {
mSettings[sName] = oBindingInfo;
} else {
throw new Error("Aggregation " + sName + " with cardinality 0..n only allows binding paths as attribute value");
}
} else if (oAggregation.altTypes) {
var oBindingInfo = fnBindingParser(sValue, oView && oView.getController(), true);
if ( oBindingInfo && typeof oBindingInfo === "object" ) {
mSettings[sName] = oBindingInfo;
} else {
mSettings[sName] = that.convertValueToType(oAggregation.altTypes[0], oBindingInfo || sValue);
}
} else {
throw new Error("Aggregation " + sName + " not supported");
}
} else if (that._getEvent(fnClass, sName)) {
var oController = oView && (oView._oContainingView || oView).getController();
var vHandler = EventHandlerResolver.resolveEventHandler(sValue, oController);
if ( vHandler ) {
mSettings[sName] = vHandler;
} else {
throw new Error('Control "' + mSettings.id + '": The function "' + sValue + '" for the event "' + sName + '" is not defined');
}
} else {
assert((sName === "id"), "DeclarativeSupport encountered unknown setting '" + sName + "' for class '" + fnClass.getMetadata().getName() + "' (value:'" + sValue + "')");
}
} else if (typeof oSpecialAttributes[sName] === "function") {
oSpecialAttributes[sName](sValue, mSettings, fnClass);
}
} else {
// custom data handling:
// determine the key of the custom data entry
sName = camelize(reCustomData.exec(sName)[1]);
// create a binding info object if necessary
var oBindingInfo = fnBindingParser(sValue, oView && oView.getController());
// create the custom data object
aCustomData.push(new CustomData({
key: sName,
value: oBindingInfo || sValue
}));
}
});
if (aCustomData.length > 0) {
mSettings.customData = aCustomData;
}
return mSettings;
};
/**
* Adds all defined aggregations to the settings object of a control.
*
* @param {object} mSettings reference of the settings of the control
* @param {function} fnClass reference to a Class
* @param {Element} oElement reference to a DOM element
* @param {sap.ui.core.mvc.HTMLView} [oView] The view instance to use.
* @return {object} the settings of the control.
* @private
*/
DeclarativeSupport._addSettingsForAggregations = function(mSettings, fnClass, oElement, oView) {
var $element = jQuery(oElement);
var sDefaultAggregation = this._getDefaultAggregation(fnClass, oElement);
var that = this;
var oAggregations = fnClass.getMetadata().getAllAggregations();
$element.children().each(function() {
// check for an aggregation tag of in case of a specifying the
// aggregation on the parent control this will be used in case
// of no meta tag was found
var $child = jQuery(this);
var sAggregation = $child.attr("data-sap-ui-aggregation");
var sType = $child.attr("data-sap-ui-type");
var bUseDefault = false;
if (!sAggregation) {
bUseDefault = true;
sAggregation = sDefaultAggregation;
}
// add the child to the aggregation
if (sAggregation && oAggregations[sAggregation]) {
var bMultiple = oAggregations[sAggregation].multiple;
var addControl = function(oChildElement) {
var oControl = that._createControl(oChildElement, oView);
if (oControl) {
if (bMultiple) {
// 1..n AGGREGATION
if (!mSettings[sAggregation]) {
mSettings[sAggregation] = [];
}
if ( typeof mSettings[sAggregation].path === "string" ) {
assert(!mSettings[sAggregation].template, "list bindings support only a single template object");
mSettings[sAggregation].template = oControl;
} else {
mSettings[sAggregation].push(oControl);
}
} else {
// 1..1 AGGREGATION
mSettings[sAggregation] = oControl;
}
}
};
if (bUseDefault || (sType && !bUseDefault)) {
addControl(this);
} else {
$child.children().each(function() {
addControl(this);
});
}
}
$child.removeAttr("data-sap-ui-aggregation");
$child.removeAttr("data-sap-ui-type");
});
return mSettings;
};
/**
* Returns the id of the element.
*
* @param {Element} oElement reference to a DOM element
* @param {sap.ui.core.mvc.HTMLView} [oView] The view instance to use.
* @return {string} the id of the element
* @private
*/
DeclarativeSupport._getId = function(oElement, oView) {
var $element = jQuery(oElement);
var sId = $element.attr("id");
if (sId) {
if (oView) {
sId = oView.createId(sId);
// Remember the id for the HTMLView rendering method. This is needed to replace the placeholder div with the actual
// control HTML (Do not use ID as Firefox even finds detached IDs)
$element.attr("data-sap-ui-id", sId);
}
// in case of having an ID retrieve it and clear it in the placeholder
// DOM element to avoid double IDs
$element.attr("id", "");
}
return sId;
};
/**
* Returns the property of a given class and property name.
*
* @param {function} fnClass reference to a Class
* @param {string} sName the name of the property
* @return {object} reference to the property object
* @private
*/
DeclarativeSupport._getProperty = function(fnClass, sName) {
return fnClass.getMetadata().getProperty(sName);
};
/**
* Converts a given value to the right property type.
*
* @param {object} oType the type of the value
* @param {string} sValue the value to convert
* @return {string} the converted value
* @private
*/
DeclarativeSupport.convertValueToType = function(oType, sValue) {
if (oType instanceof DataType) {
sValue = oType.parseValue(sValue);
}
// else return original sValue (e.g. for enums)
// Note: to avoid double resolution of binding expressions, we have to escape string values once again
return typeof sValue === "string" ? ManagedObject.bindingParser.escape(sValue) : sValue;
};
/**
* Returns the data type object for a certain property.
*
* @param {object} oProperty reference to the property object
* @return {object} the type of the property
* @throws {Error} if no type for the property is found
* @private
*/
DeclarativeSupport.getPropertyDataType = function(oProperty) {
var oType = DataType.getType(oProperty.type);
if (!oType) {
throw new Error("Property " + oProperty.name + " has no known type");
}
return oType;
};
/**
* Returns the settings name for a given html attribute (converts data-my-setting to mySetting)
*
* @param {string} sAttribute the name of the attribute
* @param {string} sId the id of the control
* @param {boolean} bDeprecationWarning whether to show a deprecation warning or not
* @return {string} the settings name
* @private
*/
DeclarativeSupport.convertAttributeToSettingName = function(sAttribute, sId, bDeprecationWarning) {
if (sAttribute.indexOf("data-") === 0) {
sAttribute = sAttribute.substr(5);
} else if (bDeprecationWarning) {
Log.warning('[Deprecated] Control "' + sId + '": The attribute "' + sAttribute + '" is not prefixed with "data-*". Future version of declarative support will only suppport attributes with "data-*" prefix.');
} else {
throw new Error('Control "' + sId + '": The attribute "' + sAttribute + '" is not prefixed with "data-*".');
}
return camelize(sAttribute);
};
/**
* Returns the association of a given class and association name.
*
* @param {function} fnClass reference to a Class
* @param {string} sName the name of the association
* @return {object} reference to the association object
* @private
*/
DeclarativeSupport._getAssociation = function(fnClass, sName) {
return fnClass.getMetadata().getAssociation(sName);
};
/**
* Returns the aggregations of a given class and aggregation name.
*
* @param {function} fnClass reference to a Class
* @param {string} sName the name of the association
* @return {object} reference to the association object
* @private
*/
DeclarativeSupport._getAggregation = function(fnClass, sName) {
return fnClass.getMetadata().getAggregation(sName);
};
/**
* Returns the event of a given class and event name.
*
* @param {function} fnClass reference to a Class
* @param {string} sName the name of the event
* @return {object} reference to the event object
* @private
*/
DeclarativeSupport._getEvent = function(fnClass, sName) {
return fnClass.getMetadata().getEvent(sName);
};
/**
* Returns the default aggregation of the control.
*
* @param {function} fnClass reference to a Class
* @param {Element} oElement reference to a DOM element
* @return {string} the default aggregation
* @private
*/
DeclarativeSupport._getDefaultAggregation = function(fnClass, oElement) {
var $element = jQuery(oElement);
var sDefaultAggregation = $element.attr("data-sap-ui-default-aggregation") || fnClass.getMetadata().getDefaultAggregationName();
$element.removeAttr("data-sap-ui-default-aggregation");
return sDefaultAggregation;
};
return DeclarativeSupport;
}, /* bExport= */ true);