@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,271 lines (1,135 loc) • 71.3 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.
*/
/*global HTMLTemplateElement, Promise */
sap.ui.define([
'sap/ui/base/DataType',
'sap/ui/base/BindingInfo',
'sap/ui/core/CustomData',
'sap/ui/core/Component',
'./mvc/View',
'./mvc/ViewType',
'./mvc/XMLProcessingMode',
'./mvc/EventHandlerResolver',
'./ExtensionPoint',
'./StashedControlSupport',
'sap/ui/base/SyncPromise',
'sap/base/Log',
'sap/base/util/ObjectPath',
'sap/base/assert',
'sap/base/util/LoaderExtensions',
'sap/base/util/JSTokenizer',
'sap/base/util/each',
'sap/base/util/isEmptyObject',
'sap/ui/core/Configuration',
'sap/ui/core/Lib'
],
function(
DataType,
BindingInfo,
CustomData,
Component,
View,
ViewType,
XMLProcessingMode,
EventHandlerResolver,
ExtensionPoint,
StashedControlSupport,
SyncPromise,
Log,
ObjectPath,
assert,
LoaderExtensions,
JSTokenizer,
each,
isEmptyObject,
Configuration,
Library
) {
"use strict";
function parseScalarType(sType, sValue, sName, oContext, oRequireModules) {
// check for a binding expression (string)
var oBindingInfo = BindingInfo.parse(sValue, oContext, /*bUnescape*/true,
/*bTolerateFunctionsNotFound*/false, /*bStaticContext*/false, /*bPreferContext*/false,
oRequireModules);
if ( oBindingInfo && typeof oBindingInfo === "object" ) {
return oBindingInfo;
}
var vValue = sValue = typeof oBindingInfo === "string" ? oBindingInfo : sValue; // oBindingInfo could be an unescaped string
var oType = DataType.getType(sType);
if (oType) {
if (oType instanceof DataType) {
vValue = oType.parseValue(sValue, {
context: oContext,
locals: oRequireModules
});
// if the parsed value is not valid, we don't fail but only log an error
if (!oType.isValid(vValue)) {
Log.error("Value '" + sValue + "' is not valid for type '" + oType.getName() + "'.");
}
}
// else keep original sValue (e.g. for enums)
} else {
throw new Error("Property " + sName + " has unknown type " + sType);
}
// Note: to avoid double resolution of binding expressions, we have to escape string values once again
return typeof vValue === "string" ? BindingInfo.escape(vValue) : vValue;
}
function localName(xmlNode) {
// localName for standard browsers, nodeName in the absence of namespaces
return xmlNode.localName || xmlNode.nodeName;
}
/**
* The official XHTML namespace. Can be used to embed XHTML in an XMLView.
*
* Note: Using this namespace prevents semantic rendering of an XMLView.
* @const
* @private
*/
var XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
/**
* The official XMLNS namespace. Must only be used for xmlns:* attributes.
* @const
* @private
*/
var XMLNS_NAMESPACE = "http://www.w3.org/2000/xmlns/";
/**
* The official SVG namespace. Can be used to embed SVG in an XMLView.
*
* Note: Using this namespace prevents semantic rendering of an XMLView.
* @const
* @private
*/
var SVG_NAMESPACE = "http://www.w3.org/2000/svg";
/**
* XML Namespace of the core library.
*
* This namespace is used to identify some sap.ui.core controls or entities with a special handling
* and for the special require attribute that can be used to load modules.
* @const
* @private
*/
var CORE_NAMESPACE = "sap.ui.core";
/**
* XML Namespace of the mvc relevant controls in the core library.
*
* This namespace is used to identify the view tags within the sap.ui.core.mvc namespace.
* @const
* @private
*/
var CORE_MVC_NAMESPACE = "sap.ui.core.mvc";
/**
* An XML namespace that apps can use to add custom data to a control's XML element.
* The name of the attribute will be used as key, the value as value of a CustomData element.
*
* This namespace is allowed for public usage.
* @const
* @private
*/
var CUSTOM_DATA_NAMESPACE = "http://schemas.sap.com/sapui5/extension/sap.ui.core.CustomData/1";
/**
* An XML namespace that can be used by tooling to add attributes with support information to an element.
* @const
* @private
*/
var SUPPORT_INFO_NAMESPACE = "http://schemas.sap.com/sapui5/extension/sap.ui.core.support.Support.info/1";
/**
* An XML namespace that denotes the XML composite definition.
* Processing of such nodes is skipped.
* @const
* @private
*/
var XML_COMPOSITE_NAMESPACE = "http://schemas.sap.com/sapui5/extension/sap.ui.core.xmlcomposite/1";
/**
* An XML namespace that is used for a marker attribute when a node's ID has been
* prefixed with the view ID (enriched). The marker attribute helps to prevent multiple prefixing.
*
* This namespace is only used inside the XMLTemplateProcessor.
* @const
* @private
*/
var UI5_INTERNAL_NAMESPACE = "http://schemas.sap.com/sapui5/extension/sap.ui.core.Internal/1";
/**
* A prefix for XML namespaces that are reserved for XMLPreprocessor extensions.
* Attributes with a namespace starting with this prefix, are ignored by this class.
* @const
* @private
*/
var PREPROCESSOR_NAMESPACE_PREFIX = "http://schemas.sap.com/sapui5/preprocessorextension/";
/**
* List of attributes that are declared as "special settings" in view's metadata but can be configured on View's
* root tag
*
* @const
* @private
*/
var VIEW_SPECIAL_ATTRIBUTES = ['controllerName', 'resourceBundleName', 'resourceBundleUrl', 'resourceBundleLocale', 'resourceBundleAlias'];
/**
* Pattern that matches the names of all HTML void tags.
* @private
*/
var rVoidTags = /^(?:area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/;
/**
* Creates a function based on the passed mode and callback which applies a callback to each child of a node.
* @param {boolean} bAsync The strategy to choose
* @param {function} fnCallback The callback to apply
* @returns {function} The created function
* @private
*/
function getHandleChildrenStrategy(bAsync, fnCallback) {
// sync strategy ensures processing order by just being sync
function syncStrategy(node, mOptions) {
var pChild,
aChildren = [];
for (var i = 0; i < node.childNodes.length; i++) {
pChild = fnCallback(node, node.childNodes[i], mOptions);
if (pChild) {
aChildren.push(pChild.unwrap());
}
}
return SyncPromise.resolve(aChildren);
}
// async strategy ensures processing order by chaining the callbacks
function asyncStrategy(node, mOptions) {
var pChain = Promise.resolve(),
aChildPromises = [mOptions.chain];
for (var i = 0; i < node.childNodes.length; i++) {
pChain = pChain.then(fnCallback.bind(null, node, node.childNodes[i], mOptions));
aChildPromises.push(pChain);
}
return Promise.all(aChildPromises);
}
return bAsync ? asyncStrategy : syncStrategy;
}
/**
* The XMLTemplateProcessor class is used to load and process Control trees in XML-declarative notation.
*
* @namespace
* @alias sap.ui.core.XMLTemplateProcessor
*/
var XMLTemplateProcessor = {};
/** API METHODS ***/
/**
* Loads an XML template using the module loading mechanism and returns the root XML element of the XML
* document.
*
* @param {string} sTemplateName the template/fragment/view resource to be loaded
* @param {string} [sExtension] the file extension, e.g. "fragment"
* @return {Element} an XML document root element
*/
XMLTemplateProcessor.loadTemplate = function(sTemplateName, sExtension) {
var sResourceName = sTemplateName.replace(/\./g, "/") + ("." + (sExtension || "view") + ".xml");
return LoaderExtensions.loadResource(sResourceName).documentElement;
};
/**
* Loads an XML template using the module loading mechanism and returns a Promise, which resolves with the root XML element of the XML
* document.
*
* @param {string} sTemplateName the template/fragment/view resource to be loaded
* @param {string} [sExtension] the file extension, e.g. "fragment"
* @return {Promise} The promise resolves with the <code>documentElement</code> of the loaded XML file.
* @private
*/
XMLTemplateProcessor.loadTemplatePromise = function(sTemplateName, sExtension) {
var sResourceName = sTemplateName.replace(/\./g, "/") + ("." + (sExtension || "view") + ".xml");
return LoaderExtensions.loadResource(sResourceName, {async: true}).then(function (oResult) {
return oResult.documentElement;
}); // result is the document node
};
/**
* Parses special settings that are supported on the View's root tag but not declared in View's metadata. The
* standard properties, event handlers, aggregations and associations are parsed in the same way as the child nodes
* are parsed.
*
* @param {Element} xmlNode the XML element representing the View
* @param {sap.ui.core.mvc.XMLView} oView the View to consider when parsing the attributes
*/
XMLTemplateProcessor.parseViewAttributes = function(xmlNode, oView) {
var i, attr;
for ( i = 0; i < xmlNode.attributes.length; i++) {
attr = xmlNode.attributes[i];
if (VIEW_SPECIAL_ATTRIBUTES.includes(attr.name)) {
oView["_" + attr.name] = attr.value;
}
}
};
/**
* Parses a complete XML template definition (full node hierarchy) and resolves the ids to their full qualification
*
* @param {Element} xmlNode the XML element representing the View/Fragment
* @param {sap.ui.core.mvc.XMLView|sap.ui.core.Fragment} oView the View/Fragment which corresponds to the parsed XML
* @return {Element} The element enriched with the full ids
* @protected
*/
XMLTemplateProcessor.enrichTemplateIds = function(xmlNode, oView) {
XMLTemplateProcessor.enrichTemplateIdsPromise(xmlNode, oView, false);
return xmlNode;
};
/**
* Parses a complete XML template definition (full node hierarchy) and resolves the ids to their full qualification
*
* @param {Element} xmlNode the XML element representing the View/Fragment
* @param {sap.ui.core.mvc.XMLView|sap.ui.core.Fragment} oView the View/Fragment which corresponds to the parsed XML
* @param {boolean} bAsync Whether or not to perform the template processing asynchronously
* @returns {Promise} which resolves with the xmlNode
* @private
*/
XMLTemplateProcessor.enrichTemplateIdsPromise = function (xmlNode, oView, bAsync) {
return parseTemplate(xmlNode, oView, true, bAsync).then(function() {
return xmlNode;
});
};
/**
* Parses a complete XML template definition (full node hierarchy)
*
* @param {Element} xmlNode the XML element representing the View/Fragment
* @param {sap.ui.core.mvc.XMLView|sap.ui.core.Fragment} oView the View/Fragment which corresponds to the parsed XML
* @param {object} mSettings The settings object that is given to the view's factory method
* @return {Array} an array containing Controls and/or plain HTML element strings
*/
XMLTemplateProcessor.parseTemplate = function(xmlNode, oView, mSettings) {
return XMLTemplateProcessor.parseTemplatePromise(xmlNode, oView, false, { settings: mSettings }).unwrap();
};
/**
* Parses a complete XML template definition (full node hierarchy)
*
* @param {Element} xmlNode the XML element representing the View/Fragment
* @param {sap.ui.core.mvc.XMLView|sap.ui.core.Fragment} oView the View/Fragment which corresponds to the parsed XML
* @param {boolean} bAsync Whether or not to perform the template processing asynchronously
* @param {object} oParseConfig parse configuration options, e.g. settings pre-processor
* @return {Promise} with an array containing Controls and/or plain HTML element strings
* @private
*/
XMLTemplateProcessor.parseTemplatePromise = function(xmlNode, oView, bAsync, oParseConfig) {
return parseTemplate(xmlNode, oView, false, bAsync, oParseConfig).then(function(vResult) {
// vResult is the result array of the XMLTP's parsing.
// Elements in vResult can be:
// * RenderManager Call (Array)
// * Control instance (Object)
// * ExtensionPoint placeholder (Object)
// we only trigger Flex for ExtensionPoints inside Views
// A potential ExtensionPoint provider will resolve any ExtensionPoints with their correct content (or the default content, if no flex changes exist)
if (oView.isA("sap.ui.core.mvc.View")) {
var vContent, i;
// For async views all ExtensionPoints have been resolved.
// Their resulting content needs to be spliced into the rendering array.
// We loop backwards so we don't have to deal with index shifts (EPs can have more than 1 result control).
for (i = vResult.length - 1; i >= 0; i--) {
vContent = vResult[i];
if (vContent && vContent._isExtensionPoint) {
var aSpliceArgs = [i, 1].concat(vContent._aControls);
Array.prototype.splice.apply(vResult, aSpliceArgs);
}
}
}
return vResult;
});
};
/**
* Validate the parsed require context object
*
* The require context object should be an object. Every key in the object should be a valid
* identifier (shouldn't contain '.'). Every value in the object should be a non-empty string.
*
* @param {object} oRequireContext The parsed require context
* @return {string} The error message if the validation fails, otherwise it returns undefined
*/
function validateRequireContext(oRequireContext) {
var sErrorMessage,
rIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
if (!oRequireContext || typeof oRequireContext !== "object") {
sErrorMessage = "core:require in XMLView can't be parsed to a valid object";
} else {
Object.keys(oRequireContext).some(function(sKey) {
if (!rIdentifier.test(sKey)) {
// '.' is not allowed to use in sKey
sErrorMessage = "core:require in XMLView contains invalid identifier: '"
+ sKey + "'";
return true;
}
if (!oRequireContext[sKey] || typeof oRequireContext[sKey] !== "string") {
// The value should be a non-empty string
sErrorMessage = "core:require in XMLView contains invalid value '"
+ oRequireContext[sKey] + "'under key '" + sKey + "'";
return true;
}
});
}
return sErrorMessage;
}
/**
* Extract module information which is defined with the "require" attribute under "sap.ui.core" namespace
* and load the modules when there are some defined
*
* @param {Element} xmlNode The current XMLNode which is being processed
* @param {boolean} bAsync Whether the view is processed asynchronously
*
* @return {Promise|undefined} The promise resolves after all modules are loaded. If the given xml node
* doesn't have require context defined, undefined is returned.
*/
function parseAndLoadRequireContext(xmlNode, bAsync) {
var sCoreContext = xmlNode.getAttributeNS(CORE_NAMESPACE, "require"),
oRequireContext,
oModules,
sErrorMessage;
if (sCoreContext) {
try {
oRequireContext = JSTokenizer.parseJS(sCoreContext);
} catch (e) {
Log.error("Require attribute can't be parsed on Node: ", xmlNode.nodeName);
throw e;
}
sErrorMessage = validateRequireContext(oRequireContext);
if (sErrorMessage) {
throw new Error(sErrorMessage + " on Node: " + xmlNode.nodeName);
}
if (!isEmptyObject(oRequireContext)) {
oModules = {};
if (bAsync) {
return new Promise(function(resolve, reject) {
// check whether all modules have been loaded already, avoids nested setTimeout calls
var bAllLoaded = Object.keys(oRequireContext).reduce(function(bAll, sKey) {
oModules[sKey] = sap.ui.require(oRequireContext[sKey]);
return bAll && oModules[sKey] !== undefined;
}, true);
if ( bAllLoaded ) {
resolve(oModules);
return;
}
// fall back to async loading
sap.ui.require(Object.values(oRequireContext), function() {
var aLoadedModules = arguments;
Object.keys(oRequireContext).forEach(function(sKey, i) {
oModules[sKey] = aLoadedModules[i];
});
resolve(oModules);
}, reject);
});
} else {
Object.keys(oRequireContext).forEach(function(sKey) {
oModules[sKey] = sap.ui.requireSync(oRequireContext[sKey]); // legacy-relevant: Sync path
});
return SyncPromise.resolve(oModules);
}
}
}
}
function fnTriggerExtensionPointProvider(bAsync, oTargetControl, mAggregationsWithExtensionPoints) {
var pProvider = SyncPromise.resolve();
// if no extension points are given, we don't have to do anything here
if (!isEmptyObject(mAggregationsWithExtensionPoints)) {
var aAppliedExtensionPoints = [];
// in the async case we can collect the ExtensionPointProvider promises and
// then can delay the view.loaded() promise until all extension points are
var fnResolveExtensionPoints;
if (bAsync) {
pProvider = new Promise(function(resolve) {
fnResolveExtensionPoints = resolve;
});
}
Object.keys(mAggregationsWithExtensionPoints).forEach(function(sAggregationName) {
var aExtensionPoints = mAggregationsWithExtensionPoints[sAggregationName];
aExtensionPoints.forEach(function(oExtensionPoint) {
oExtensionPoint.targetControl = oTargetControl;
var fnExtClass = sap.ui.require(oExtensionPoint.providerClass);
// apply directly if class was already loaded
if (fnExtClass) {
aAppliedExtensionPoints.push(fnExtClass.applyExtensionPoint(oExtensionPoint));
} else {
// load provider class and apply
var p = new Promise(function(resolve, reject) {
sap.ui.require([oExtensionPoint.providerClass], function(ExtensionPointProvider) {
resolve(ExtensionPointProvider);
}, reject);
}).then(function(ExtensionPointProvider) {
return ExtensionPointProvider.applyExtensionPoint(oExtensionPoint);
});
aAppliedExtensionPoints.push(p);
}
});
});
// we collect the ExtensionProvider Promises
if (bAsync) {
Promise.all(aAppliedExtensionPoints).then(fnResolveExtensionPoints);
}
}
return pProvider;
}
function findNamespacePrefix(node, namespace, prefix) {
var sCandidate = prefix;
for (var iCount = 0; iCount < 100; iCount++) {
var sRegisteredNamespace = node.lookupNamespaceURI(sCandidate);
if (sRegisteredNamespace == null || sRegisteredNamespace === namespace) {
return sCandidate;
}
sCandidate = prefix + iCount;
}
throw new Error("Could not find an unused namespace prefix after 100 tries, giving up");
}
/**
* Parses a complete XML template definition (full node hierarchy)
*
* @param {Element} xmlNode the XML element representing the View/Fragment
* @param {sap.ui.core.mvc.XMLView|sap.ui.core.Fragment} oView the View/Fragment which corresponds to the parsed XML
* @param {boolean} bEnrichFullIds Flag for running in a mode which only resolves the ids and writes them back
* to the xml source.
* @param {boolean} bAsync Whether or not to perform the template processing asynchronously.
* The async processing will only be active in conjunction with the internal XML processing mode set
* to <code>XMLProcessingMode.Sequential</code> or <code>XMLProcessingMode.SequentialLegacy</code>.
* @param {object} oParseConfig parse configuration options, e.g. settings pre-processor
*
* @return {Promise} with an array containing Controls and/or plain HTML element strings
*/
function parseTemplate(xmlNode, oView, bEnrichFullIds, bAsync, oParseConfig) {
// the output of the template parsing, containing strings and promises which resolve to control or control arrays
// later this intermediate state with promises gets resolved to a flat array containing only strings and controls
var aResult = [],
sInternalPrefix = findNamespacePrefix(xmlNode, UI5_INTERNAL_NAMESPACE, "__ui5"),
pResultChain = parseAndLoadRequireContext(xmlNode, bAsync) || SyncPromise.resolve(),
rm = {
openStart: function(tagName, sId) {
aResult.push(["openStart", [tagName, sId]]);
},
voidStart: function(tagName, sId) {
aResult.push(["voidStart", [tagName, sId]]);
},
style: function(name, value) {
aResult.push(["style", [name, value]]);
},
"class": function(clazz) {
aResult.push(["class", [clazz]]);
},
attr: function(name, value) {
aResult.push(["attr", [name, value]]);
},
openEnd: function() {
aResult.push(["openEnd"]);
},
voidEnd: function() {
aResult.push(["voidEnd"]);
},
text: function(str) {
aResult.push(["text", [str]]);
},
unsafeHtml: function(str) {
aResult.push(["unsafeHtml", [str]]);
},
close: function(tagName) {
aResult.push(["close", [tagName]]);
},
renderControl: function(pContent) {
aResult.push(pContent);
}
};
bAsync = bAsync && !!oView._sProcessingMode;
Log.debug("XML processing mode is " + (oView._sProcessingMode || "default") + ".", "", "XMLTemplateProcessor");
Log.debug("XML will be processed " + (bAsync ? "asynchronously" : "synchronously") + ".", "", "XMLTemplateProcessor");
var bDesignMode = Configuration.getDesignMode();
if (bDesignMode) {
oView._sapui_declarativeSourceInfo = {
// the node representing the current control
xmlNode: xmlNode,
// the document root node
xmlRootNode: oView._oContainingView === oView ? xmlNode :
oView._oContainingView._sapui_declarativeSourceInfo.xmlRootNode
};
}
if (!oView.isSubView()) {
// define internal namespace on root node
xmlNode.setAttributeNS(XMLNS_NAMESPACE, "xmlns:" + sInternalPrefix, UI5_INTERNAL_NAMESPACE);
}
var bWrapped = processNode(xmlNode, pResultChain);
// iterate aResult for Promises
// if a Promise is found splice its resolved content at the same position in aResult
// then start over again starting with the position of the last extracted element
//
// Note: the index 'i' is reused for optimization
var i = 0;
function resolveResultPromises() {
for (/* i = index of the unknown content */; i < aResult.length; i++) {
var vElement = aResult[i];
if (vElement && typeof vElement.then === 'function') {
return vElement
// destructive operation, the length of aResult changes
.then(spliceContentIntoResult)
// enter the recursion with the current index (pointing at the new content)
.then(resolveResultPromises);
}
}
return aResult;
}
// replace the Promise with a variable number of contents in aResult
function spliceContentIntoResult(vContent) {
// equivalent to aResult.apply(start, deleteCount, content1, content2...)
var args = [i, 1].concat(vContent);
Array.prototype.splice.apply(aResult, args);
}
// Post-processing of the finalized view content:
// Once this Promise is resolved, we have the full view content available.
// The final output of the parseTemplate call will be an array containing DOM Strings and UI5 Controls.
// Flatten the array so that all promises are resolved and replaced.
return pResultChain
.then(resolveResultPromises)
.then(function(aResult) {
// remove the wrapper node
if (bWrapped) {
var oWrapper = xmlNode.parentNode;
oWrapper.removeChild(xmlNode);
if (oWrapper.parentNode) {
oWrapper.parentNode.replaceChild(xmlNode, oWrapper);
}
}
return aResult;
});
function identity(sId) {
return sId;
}
function createId(sId) {
return oView._oContainingView.createId(sId);
}
function createErrorInfo(node, vError) {
var sType = oView.getMetadata().isA("sap.ui.core.mvc.View") ? "View" : "Fragment";
var sNodeSerialization = node.outerHTML ? node.cloneNode(false).outerHTML : node.textContent;
return "Error found in " + sType + " (id: '" + oView.getId() + "').\nXML node: '" + sNodeSerialization + "':\n" + vError;
}
function normalizeRootNode(node) {
var sNodeName = localName(node),
oWrapper;
// Normalize the view content by wrapping it with either a "View" tag or a "FragmentDefinition" tag to
// simplify the parsing process
if (oView.isA("sap.ui.core.mvc.XMLView") && (node.namespaceURI === XHTML_NAMESPACE || node.namespaceURI === SVG_NAMESPACE)) {
// XHTML or SVG nodes are placed into a sub view without having "View" as root tag
// Wrap the content into a "View" node
oWrapper = node.ownerDocument.createElementNS(CORE_MVC_NAMESPACE, "View");
} else if (oView.isA("sap.ui.core.Fragment") && (sNodeName !== "FragmentDefinition" || node.namespaceURI !== CORE_NAMESPACE)) {
// Wrap the content into a "FragmentDefinition" node for single control node
oWrapper = node.ownerDocument.createElementNS(CORE_NAMESPACE, "FragmentDefinition");
}
if (oWrapper) {
var oOldParent = node.parentNode;
if (oOldParent) {
oOldParent.replaceChild(oWrapper, node);
}
oWrapper.appendChild(node);
}
return oWrapper;
}
function processNode(node, pChain) {
var bWrapped = false,
sCurrentName = oView.sViewName || oView._sFragmentName,
oNewRoot, sNodeName;
if (!sCurrentName) {
var oTopView = oView;
var iLoopCounter = 0; // Make sure there are not infinite loops
while (++iLoopCounter < 1000 && oTopView && oTopView !== oTopView._oContainingView) {
oTopView = oTopView._oContainingView;
}
sCurrentName = oTopView.sViewName;
}
oNewRoot = normalizeRootNode(node);
if (oNewRoot) {
node = oNewRoot;
bWrapped = true;
}
sNodeName = localName(node);
if (oView.isA("sap.ui.core.mvc.XMLView")) {
if ((sNodeName !== "View" && sNodeName !== "XMLView") || node.namespaceURI !== CORE_MVC_NAMESPACE) {
Log.error("XMLView's root node must be 'View' or 'XMLView' and have the namespace 'sap.ui.core.mvc'" + (sCurrentName ? " (View name: " + sCurrentName + ")" : ""));
}
// createRegularControls
pResultChain = pChain.then(function() {
return createRegularControls(node, oView.getMetadata().getClass(), pChain, null, { rootArea: true, rootNode: true });
});
} else {
var handleChildren = getHandleChildrenStrategy(bAsync, function(node, childNode, mOptions) {
if (childNode.nodeType === 1 /* Element Node*/) {
return createControls(childNode, mOptions.chain, null /*closest binding*/, undefined /* aggregation info*/, { rootArea: true });
}
});
pResultChain = pChain.then(function() {
return handleChildren(node, {
chain: pChain
});
});
}
return bWrapped;
}
/**
* Requests the control class if not loaded yet.
* If the View is set to async=true, an async XHR is sent, otherwise a sync XHR.
*
* @param {string} sNamespaceURI
* @param {string} sLocalName
* @returns {function|Promise|undefined} the loaded ControlClass plain or resolved from a Promise
*/
function findControlClass(sNamespaceURI, sLocalName) {
var sClassName;
var mLibraries = Library.all();
each(mLibraries, function(sLibName, oLibrary) {
if ( sNamespaceURI === oLibrary.namespace || sNamespaceURI === oLibrary.name ) {
sClassName = oLibrary.name + "." + ((oLibrary.tagNames && oLibrary.tagNames[sLocalName]) || sLocalName);
}
});
// TODO guess library from sNamespaceURI and load corresponding lib!?
sClassName = sClassName || sNamespaceURI + "." + sLocalName;
// ensure that control and library are loaded
function getObjectFallback(oClassObject) {
// some modules might not return a class definition, so we fallback to the global
// this is against the AMD definition, but is required for backward compatibility
if (!oClassObject) {
Log.error("Control '" + sClassName + "' did not return a class definition from sap.ui.define.", "", "XMLTemplateProcessor");
oClassObject = ObjectPath.get(sClassName);
}
if (!oClassObject) {
Log.error("Can't find object class '" + sClassName + "' for XML-view", "", "XMLTemplateProcessor");
}
return oClassObject;
}
var sResourceName = sClassName.replace(/\./g, "/");
var oClassObject = sap.ui.require(sResourceName);
if (!oClassObject) {
if (bAsync) {
return new Promise(function(resolve, reject) {
sap.ui.require([sResourceName], function(oClassObject) {
oClassObject = getObjectFallback(oClassObject);
resolve(oClassObject);
}, reject);
});
} else {
oClassObject = sap.ui.requireSync(sResourceName); // legacy-relevant: Sync path
oClassObject = getObjectFallback(oClassObject);
}
}
return oClassObject;
}
/**
* Takes an arbitrary node (control or plain HTML) and creates zero or one or more SAPUI5 controls from it,
* iterating over the attributes and child nodes.
*
* @param {Element} node The current XMLNode which is being processed
* @param {Promise} pRequireContext Promise which resolves with the loaded modules from require context
* @param {object} [oClosestBinding] Information on the binding that is closest to currently processed control
* node. Used by the flex extension-point provider to correctly trigger aggregation updates. This is necessary
* for extension-points that are inside a template control of an aggregation.
* @param {Object} [oAggregation] The information of the aggregation to which the control being processed will be added
* @param {object} [oConfig] The config object that contains information which is forwarded during the recursive processing
* @param {boolean} [oConfig.rootArea=false] Indicates whether it's processing the root area of an XMLView
* @param {boolean} [oConfig.rootNode=false] Indicates whether the <code>node</code> is the root node of an XMLView's content
* @return {Promise} resolving to an array with 0..n controls
* @private
*/
function createControls(node, pRequireContext, oClosestBinding, oAggregation, oConfig) {
var bRootArea = oConfig && oConfig.rootArea,
bRootNodeInSubView = oConfig && oConfig.rootNode && oView.isSubView(),
sLocalName = localName(node),
bRenderingRelevant = bRootArea && (oView.isA("sap.ui.core.Fragment") || (oAggregation && oAggregation.name === "content")),
pResult, i;
if ( node.nodeType === 1 /* ELEMENT_NODE */ ) {
// differentiate between SAPUI5 and plain-HTML children
if (node.namespaceURI === XHTML_NAMESPACE || node.namespaceURI === SVG_NAMESPACE ) {
if (bRootArea) {
if (oAggregation && oAggregation.name !== "content") {
Log.error(createErrorInfo(node, "XHTML nodes can only be added to the 'content' aggregation and not to the '" + oAggregation.name + "' aggregation."));
return SyncPromise.resolve([]);
}
if (oConfig && oConfig.contentBound) {
throw new Error(createErrorInfo(node, "No XHTML or SVG node is allowed because the 'content' aggregation is bound."));
}
var bXHTML = node.namespaceURI === XHTML_NAMESPACE;
// determine ID
var sId = node.getAttribute("id");
if ( sId != null ) {
sId = getId(oView, node);
} else {
sId = bRootNodeInSubView ? oView.getId() : undefined;
}
if ( sLocalName === "style" ) {
// We need to remove the namespace prefix from style nodes
// otherwise the style element's content will be output as text and not evaluated as CSS
// We do this by manually 'cloning' the style without the namespace prefix
// original node values
var aAttributes = node.attributes; // array-like 'NamedNodeMap'
var sTextContent = node.textContent;
// 'clone'
node = document.createElement(sLocalName);
node.textContent = sTextContent;
// copy all non-prefixed attributes
// -> prefixed attributes are invalid HTML
for (i = 0; i < aAttributes.length; i++) {
var oAttr = aAttributes[i];
if (!oAttr.prefix) {
node.setAttribute(oAttr.name, oAttr.value);
}
}
// avoid encoding of style content by writing the whole tag as unsafeHtml
// for compatibility reasons, apply the same ID rewriting as for other tags
if ( sId != null ) {
node.setAttribute("id", sId);
}
if ( bRootNodeInSubView ) {
node.setAttribute("data-sap-ui-preserve", oView.getId());
}
rm.unsafeHtml(node.outerHTML);
return SyncPromise.resolve([]);
}
// write opening tag
var bVoid = rVoidTags.test(sLocalName);
if ( bVoid ) {
rm.voidStart(sLocalName, sId);
} else {
rm.openStart(sLocalName, sId);
}
// write attributes
for (i = 0; i < node.attributes.length; i++) {
var attr = node.attributes[i];
if ( attr.name !== "id" ) {
rm.attr(bXHTML ? attr.name.toLowerCase() : attr.name, attr.value);
}
}
if ( bRootNodeInSubView ) {
rm.attr("data-sap-ui-preserve", oView.getId());
}
if ( bVoid ) {
rm.voidEnd();
if ( node.firstChild ) {
Log.error("Content of void HTML element '" + sLocalName + "' will be ignored");
}
} else {
rm.openEnd();
// write children
// For HTMLTemplateElement nodes, skip the associated DocumentFragment node
var oContent = node instanceof HTMLTemplateElement ? node.content : node;
var handleChildren = getHandleChildrenStrategy(bAsync, function (node, childNode, mOptions) {
return createControls(childNode, mOptions.chain, mOptions.closestBinding, mOptions.aggregation, mOptions.config);
});
pResult = handleChildren(oContent, {
chain: pRequireContext,
closestBinding: oClosestBinding,
aggregation: oAggregation,
config: { rootArea: bRootArea }
});
return pResult.then(function(aResults) {
rm.close(sLocalName);
// aResults can contain the following elements:
// * require context object
// * array of control instance(s)
// * undefined
return aResults.reduce(function(acc, vControls) {
if (Array.isArray(vControls)) {
vControls.forEach(function(oControl) {
acc.push(oControl);
});
}
return acc;
}, []);
});
}
} else {
var id = node.attributes['id'] ? node.attributes['id'].textContent || node.attributes['id'].text : null;
if (bEnrichFullIds) {
return XMLTemplateProcessor.enrichTemplateIdsPromise(node, oView, bAsync).then(function(){
// do not create controls
return [];
});
} else {
// plain HTML node - create a new View control
// creates a view instance, but makes sure the new view receives the correct owner component
var fnCreateView = function (oViewClass) {
var mViewParameters = {
id: id ? getId(oView, node, id) : undefined,
xmlNode: node,
containingView: oView._oContainingView,
processingMode: oView._sProcessingMode // add processing mode, so it can be propagated to subviews inside the HTML block
};
// running with owner component
if (oView.fnScopedRunWithOwner) {
return oView.fnScopedRunWithOwner(function () {
return new oViewClass(mViewParameters);
});
}
// no owner component
// (or fully sync path, which handles the owner propagation on a higher level)
return new oViewClass(mViewParameters);
};
return pRequireContext.then(function() {
if (bAsync) {
return new Promise(function (resolve, reject) {
sap.ui.require(["sap/ui/core/mvc/XMLView"], function(XMLView) {
resolve([fnCreateView(XMLView)]);
}, reject);
});
} else {
var XMLView = sap.ui.requireSync("sap/ui/core/mvc/XMLView"); // legacy-relevant: Sync path
return [fnCreateView(XMLView)];
}
});
}
}
} else {
pResult = createControlOrExtension(node, pRequireContext, oClosestBinding);
if (bRenderingRelevant) {
rm.renderControl(pResult);
}
// non-HTML (SAPUI5) control
// we must return the result in either bRootArea=true or the bRootArea=false case because we use the result
// to add the control to the aggregation of its parent control
return pResult;
}
} else if (node.nodeType === 3 /* TEXT_NODE */ && bRenderingRelevant) {
if (!oConfig || !oConfig.contentBound) {
// content aggregation isn't bound
rm.text(node.textContent);
} else if (node.textContent.trim()) {
throw new Error(createErrorInfo(node, "Text node isn't allowed because the 'content' aggregation is bound."));
}
}
return SyncPromise.resolve([]);
}
/**
* Creates 0..n UI5 controls from an XML node which is not plain HTML, but a UI5 node (either control or
* ExtensionPoint). One control for regular controls, zero for ExtensionPoints without configured extension
* and n controls for multi-root Fragments.
*
* @param {Element} node The current XMLNode which is being processed
* @param {Promise} pRequireContext Promise which resolves with the loaded modules from require context
* @param {object} [oClosestBinding] Information on the binding that is closest to currently processed control
* node. Used by the flex extension-point provider to correctly trigger aggregation updates. This is necessary
* for extension-points that are inside a template control of an aggregation.
* @return {Promise} resolving to an array with 0..n controls created from a node
* @private
*/
function createControlOrExtension(node, pRequireContext, oClosestBinding) { // this will also be extended for Fragments with multiple roots
if (localName(node) === "ExtensionPoint" && node.namespaceURI === CORE_NAMESPACE) {
if (bEnrichFullIds) {
// Processing the different types of ExtensionPoints (XML, JS...) is not possible, hence
// they are skipped as well as their potentially overwritten default content.
return SyncPromise.resolve([]);
} else {
// for Views the containing View's name is required to retrieve the according extension configuration,
// whereas for Fragments the actual Fragment's name is required - oView can be either View or Fragment
var oContainer = oView instanceof View ? oView._oContainingView : oView;
// The ExtensionPoint module is actually the sap.ui.extensionpoint function.
// We still call _factory for skipping the deprecation warning.
var fnExtensionPointFactory = ExtensionPoint._factory.bind(null, oContainer, node.getAttribute("name"), function() {
// create extensionpoint with callback function for defaultContent - will only be executed if there is no customizing configured or if customizing is disabled
var pChild = SyncPromise.resolve();
var aChildControlPromises = [];
var children = node.childNodes;
for (var i = 0; i < children.length; i++) {
var oChildNode = children[i];
if (oChildNode.nodeType === 1 /* ELEMENT_NODE */) { // text nodes are ignored - plaintext inside extension points is not supported; no warning log because even whitespace is a text node
// chain the child node creation for sequential processing
pChild = pChild.then(createControls.bind(null, oChildNode, pRequireContext, oClosestBinding));
aChildControlPromises.push(pChild);
}
}
return SyncPromise.all(aChildControlPromises).then(function(aChildControl){
var aDefaultContent = [];
aChildControl.forEach(function(aControls) {
aDefaultContent = aDefaultContent.concat(aControls);
});
return aDefaultContent;
});
}, undefined /* [targetControl] */, undefined /* [aggregationName] */, bAsync);
return SyncPromise.resolve(oView.fnScopedRunWithOwner ? oView.fnScopedRunWithOwner(fnExtensionPointFactory) : fnExtensionPointFactory());
}
} else {
// a plain and simple regular UI5 control
var sLocalName = localName(node);
// [SUPPORT-RULE]: Check, whether the control class name starts with a lower case letter
// Local tag names might be in dot-notation, e.g. "<lib:table.Column />"
var sControlName = sLocalName;
var iControlNameStart = sLocalName.lastIndexOf(".");
if (iControlNameStart >= 0) {
sControlName = sLocalName.substring(iControlNameStart + 1, sLocalName.length);
}
if (/^[a-z].*/.test(sControlName)) {
var sNameOrId = oView.sViewName || oView._sFragmentName || oView.getId();
// View or Fragment
Log.warning("View or Fragment '" + sNameOrId + "' contains a Control tag that starts with lower case '" + sControlName + "'",
oView.getId(),
"sap.ui.core.XMLTemplateProcessor#lowerCase"
);
}
// [/SUPPORT-RULE]
var vClass = findControlClass(node.namespaceURI, sLocalName);
if (vClass && typeof vClass.then === 'function') {
return vClass.then(function (fnClass) {
return createRegularControls(node, fnClass, pRequireContext, oClosestBinding);
});
} else {
// class has already been loaded
return createRegularControls(node, vClass, pRequireContext, oClosestBinding);
}
}
}
/**
* Creates 0..n UI5 controls from an XML node.
* One control for regular controls, zero for ExtensionPoints without configured extension and
* n controls for multi-root Fragments.
*
* @param {Element} node The current XMLNode which is being processed
* @param {function} oClass The constructor of the control that is currently being processed
* @param {Promise} pRequireContext Promise which resolves with the loaded modules from require context
* @param {object} [oClosestBinding] Information on the binding that is closest to currently processed control
* node. Used by the flex extension-point provider to correctly trigger aggregation updates. This is necessary
* for extension-points that are inside a template control of an aggregation.
* @param {object} [oConfig] The config object that contains information which is forwarded during the recursive processing
* @param {boolean} [oConfig.rootArea=false] Indicates whether it's processing the root area of an XMLView
* @param {boolean} [oConfig.rootNode=false] Indicates whether the <code>node</code> is the root node of an XMLView's content
*
* @return {Promise} resolving to an array with 0..n controls created from a node
* @private
*/
function createRegularControls(node, oClass, pRequireContext, oClosestBinding, oConfig) {
var ns = node.namespaceURI,
mSettings = {},
mAggregationsWithExtensionPoints = {},
sStyleClasses = "",
aCustomData = [],
mCustomSettings = null,
sSupportData = null,
// for stashed nodes we need to ignore the following type of attributes:
// 1. Aggregations
// -> might lead to the creation of bindings; also the aggregation template is removed anyway
// 2. Associations
// -> might refer to controls inside the node, which have been removed earlier when the StashedControl was created
// 3. Events
bStashedControl = node.getAttribute("stashed") === "true",
bRootArea = oConfig && oConfig.rootArea,
bViewRootNode = oConfig && oConfig.rootNode,
oRequireContext;
// remove stashed attribute as it is an unknown property.
if (!bEnrichFullIds) {
node.removeAttribute("stashed");
}
if (!oClass) {
return SyncPromise.resolve([]);
}
if (bViewRootNode) {
// although the 'id' isn't needed for mSettings object because the view instance is already created,
// it's still needed for the closestBinding info object
mSettings.id = oView.getId();
}
var oMetadata = oClass.getMetadata();
var mKnownSettings = oMetadata.getAllSettings();
var pSelfRequireContext = !bRootArea ? parseAndLoadRequireContext(node, bAsync) : undefined;
// create new promise only when the current node has core:require defined
if (pSelfRequireContext) {
pRequireContext = SyncPromise.all([pRequireContext, pSelfRequireContext])
.then(function(aRequiredModules) {
return Object.assign({}, aRequiredModules[0], aRequiredModules[1]);
});
}
pRequireContext = pRequireContext.then(function(oRequireModules) {
if (isEmptyObject(oRequireModules)) {
oRequireModules = null;
}
oRequireContext = oRequireModules;
if (!bEnrichFullIds) {
for (var i = 0; i < node.attributes.length; i++) {
var attr = node.attributes[i],
sName = attr.name,
sNamespace = attr.namespaceURI,
oInfo = mKnownSettings[sName],
sValue = attr.value;
if (bViewRootNode && VIEW_SPECIAL_ATTRIBUTES.includes(sName)) {
continue;
}
// apply the value of the attribute to a
// * property,
// * association (id of the control),
// * event (name of the function in the controller) or
// * CustomData element (namespace-prefixed attribute)
if (sName === "id" && !bViewRootNode) { // "id" attribute on View's root node isn't supported
// special handling for ID
mSettings[sName] = getId(oView, node, sValue);
} else if (sName === "class") {
// special handling for CSS classes, which will be added via addStyleClass()
sStyleClasses += sValue;
} else if (sName === "viewName") {
mSettings[sName] = sValue;
} else if (sName === "fragmentName") {
mSettings[sName] = sValue;
mSettings['containingView'] = oView._oContainingView;
} else if ((sName === "binding" && !oInfo) || sName === 'objectBindings' ) {
if (!bStashedControl) {
var oBindingInfo = BindingInfo.parse(sValue, oView._oContainingView.oController);
// TODO reject complex bindings, types, formatters; enable 'parameters'?
if (oBindingInfo) {
mSettings.objectBindings = mSettings.objectBindings || {};
mSettings.objectBindings[oBindingInfo.model || undefined] = oBindingInfo;
}
}
} else if (sName === 'metadataContexts') {
if (!bStashedControl) {
var mMetaContextsInfo = null;
try {
mMetaContextsInfo = XMLTemplateProcessor._calculatedModelMapping(sValue, oView._oContainingView.oController, true);
} catch (e) {
Log.error(oView + ":" + e.message);
}
if (mMetaContextsInfo) {
mSettings.metadataContexts = mMetaContextsInfo;
if (XMLTemplateProcessor._preprocessMetadataContexts) {
XMLTemplateProcessor._preprocessMetadataContexts(oClass.getMetadata().getName(), mSettings, oView._oContainingView.oController);
}
}
}
} else if (sName.indexOf(":") > -1) { // namespace-prefixed attribute found
sNamespace = attr.namespaceURI;
if (sNamespace === CUSTOM_DATA_NAMESPACE) { // CustomData attribute found
var sLocalName = localName(attr);
aCustomData.push(new CustomData({
key:sLocalName,
value:parseScalarType("any", sValue, sLocalName, oView._oContainingView.oController, oRequireModules)
}));
} else if (sNamespace === SUPPORT_INFO_NAMESPACE) {
sSupportData = sValue;
} else if (sNamespace && sNamespace.startsWith(PREPROCESSOR_NAMESPACE_PREFIX)) {
Log.debug(oView + ": XMLView parser ignored preprocessor attribute '" + sName + "' (value: '" + sValue + "')");
} else if (sNamespace === UI5_INTERNAL_NAMESPACE && localName(attr) === "invisible") {
oInfo = mKnownSettings.visible;
if (oInfo && oInfo._iKind === 0 && oInfo.type === "boolean") {
mSettings.visible = false;
}
} else if (sNamespace === CORE_NAMESPACE
|| sNamespace === UI5_INTERNAL_NAMESPACE
|| sName.startsWith("xmlns:") ) {
// ignore namespaced attributes that are handled by the XMLTP itself
} else {
// all other namespaced attributes are kept as custom settings
if (!mCustomSettings) {
mCustomSettings = {};
}
if (!mCustomSettings.hasOwnProperty(attr.namespaceURI)) {
mCustomSettings[attr.namespaceURI] = {};
}
mCustomSettings[attr.namespaceURI][localName(attr)] = attr.nodeValue;
Log.debug(oView + ": XMLView parser encountered unknown attribute '" + sName + "' (value: '" + sValue + "') with unknown namespace, stored as sap-ui-custom-settings of customData");
// TODO: here XMLView could check for namespace handlers registered by the application for this namespace which could modify mSettings according to their interpretation of the attribute
}
} else if (oInfo && oInfo._iKind === 0 /* PROPERTY */ ) {
// other PROPERTY
mSettings[sName] = parseScalarType(oInfo.type, sValue, sName, oView._oContainingView.oController, oRequireModules); // View._oContainingView.oController is null when [...]
// FIXME: ._oContainingView might be the original Fragment for an extension fragment or a fragment in a fragment - so it has no controller bit ITS containingView.
} else if (oInfo && oInfo._iKind === 1 /* SINGLE_AGGREGATION */ && oInfo.altTypes ) {
// AGGREGATION with scalar type (altType)
if (!bStashedControl) {
mSettings[sName] = parseScalarType(oInfo.altTypes[0], sValue, sName, oView._oContainingView.oController, oRequireModules);
}
} else if (oInfo && oInfo._iKind === 2 /* MULTIPLE_AGGREGATION */ ) {
if (!bStashedControl) {
var oBindingInfo = BindingInfo.parse(sValue, oView._oContainingView.oController, false, false, false, false, oRequireModules);
if ( oBindingInfo ) {
mSettings[sName] = oBindingInfo;
} else {
// TODO we now in theory allow more than just a binding path. Update message?
Log.error(oView + ": aggregations with cardinality 0..n only allow binding paths as attribute value (wrong value: " + sName + "='" + sValue + "')");
}
}
} else if (oInfo && oInfo._iKind === 3 /* SINGLE_ASSOCIATION */ ) {
// ASSOCIATION
if