UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

1,306 lines (1,166 loc) 78.7 kB
/*! * OpenUI5 * (c) Copyright 2026 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', 'sap/ui/core/ElementRegistry', './mvc/View', './mvc/ViewType', './mvc/_ViewFactory', './mvc/XMLProcessingMode', './mvc/EventHandlerResolver', './ExtensionPoint', './StashedControlSupport', 'sap/ui/base/SyncPromise', 'sap/base/future', '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/base/DesignTime', 'sap/ui/core/Lib' ], function( DataType, BindingInfo, CustomData, Component, ElementRegistry, View, ViewType, _ViewFactory, XMLProcessingMode, EventHandlerResolver, ExtensionPoint, StashedControlSupport, SyncPromise, future, Log, ObjectPath, assert, LoaderExtensions, JSTokenizer, each, isEmptyObject, DesignTime, Library ) { "use strict"; function parseScalarType(sType, sValue, sName, oContext, oRequireModules, aTypePromises, mAdditionalBindableValues) { var bResolveTypesAsync = !!aTypePromises; var oBindingInfo; // check for a binding expression (string) var oBindingParseResult = BindingInfo.parse(sValue, oContext, /*bUnescape*/true, /*bTolerateFunctionsNotFound*/false, /*bStaticContext*/false, /*bPreferContext*/false, oRequireModules, /* bResolveTypesAsync: Whether we want the type classes to be resolved, true if async == true, false otherwise */ bResolveTypesAsync, mAdditionalBindableValues); // asynchronously resolved types result in a Promise we need to unwrap here if (bResolveTypesAsync && oBindingParseResult) { aTypePromises.push(oBindingParseResult.resolved); oBindingInfo = oBindingParseResult.bindingInfo; } else { oBindingInfo = oBindingParseResult; } 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)) { future.errorThrows("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 {function} fnCallback The callback to apply * @param {boolean} bAsync The strategy to choose * @returns {function} The created function * @private * @ui5-transform-hint replace-param bAsync true */ function getHandleChildrenStrategy(fnCallback, bAsync) { // 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 * @deprecated As of version 1.56 */ 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)) { if (attr.name === "controllerName" && attr.value.startsWith("module:")) { oView["_controllerModuleName"] = attr.value; } else { 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 * @deprecated */ 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 * @ui5-transform-hint replace-param bAsync true */ XMLTemplateProcessor.enrichTemplateIdsPromise = function (xmlNode, oView, bAsync) { return parseTemplate(xmlNode, oView, true, undefined, 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 * @deprecated As of version 1.56 because sync XMLView parsing is deprecated. */ 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 * @ui5-restricted sap.ui.core.Fragment, sap.ui.core.mvc.XMLView * @ui5-transform-hint replace-param bAsync true */ XMLTemplateProcessor.parseTemplatePromise = function(xmlNode, oView, bAsync, oParseConfig) { return parseTemplate(xmlNode, oView, false, oParseConfig, bAsync).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; }); }; /** * Checks whether the given module has an invalid module content. * Invalid in the sense of the XMLTP means: a Promise. * * @param {string} sModulePath the module to check for validity * @param {any} vContent the module content to check for validity */ function validateModuleContent(sModulePath, vContent) { if (vContent instanceof Promise) { throw new Error(`The module '${sModulePath}' returns a Promise where a control class was expected. Promises as module content are not supported. Please also refer to https://ui5.sap.com/#/topic/0cb44d7a147640a0890cefa5fd7c7f8e.`); } } /** * Validate the parsed require context object * * The require context object should be an object. Every key in the object should be a valid * identifier. Every key shouldn't contain '.' or shouldn't start with '$'. For latter case, future message is logged. * 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) { const rIdentifier = /^[a-zA-Z_][a-zA-Z0-9_$]*$/; const oResult = { throwError: false, errorMessage: "" }; if (!oRequireContext || typeof oRequireContext !== "object") { oResult.errorMessage = "core:require in XMLView can't be parsed to a valid object"; oResult.throwError = true; return oResult; } for (const sKey of Object.keys(oRequireContext)) { if (!rIdentifier.test(sKey)) { // '.' is not allowed to use in sKey oResult.errorMessage = `core:require in XMLView contains an invalid identifier: '${sKey}'`; if (!sKey.startsWith("$")) { // otherwise future log oResult.throwError = true; } return oResult; } const sValue = oRequireContext[sKey]; if (!sValue || typeof sValue !== "string") { // The value should be a non-empty string oResult.errorMessage = `core:require in XMLView contains an invalid value '${sValue}' under key '${sKey}'`; oResult.throwError = true; return oResult; } } return oResult; } /** * 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. * @ui5-transform-hint replace-param bAsync true */ function parseAndLoadRequireContext(xmlNode, bAsync) { var sCoreContext = xmlNode.getAttributeNS(CORE_NAMESPACE, "require"), oRequireContext, oModules, oResult; if (sCoreContext) { try { oRequireContext = JSTokenizer.parseJS(sCoreContext); } catch (e) { Log.error("Require attribute can't be parsed on Node: ", xmlNode.nodeName); throw e; } oResult = validateRequireContext(oRequireContext); if (oResult.errorMessage) { const sErrorMessage = `${oResult.errorMessage} on Node: ${xmlNode.nodeName}`; if (oResult.throwError) { throw new Error(sErrorMessage); } else { future.fatalThrows(`${sErrorMessage}. Keys that begin with '$' are reserved by the framework.`); } } 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); } } } } /** * @private * @ui5-transform-hint replace-param bAsync true */ function fnTriggerExtensionPointProvider(oTargetControl, mAggregationsWithExtensionPoints, bAsync) { 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 * @ui5-transform-hint replace-param bAsync true */ function parseTemplate(xmlNode, oView, bEnrichFullIds, oParseConfig, bAsync) { // 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(), collectControl = (pContent) => aResult.push(pContent); // object containing reserved values for binding formatter functions. const mAdditionalBindableValues = { "$control": null, "$controller": oView._oContainingView.oController }; /** * @deprecated since version 1.120 because the support of HTML and SVG nodes is deprecated */ const 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]]); } }; /** * @deprecated since 1.120 because the support of HTML and SVG in XMLView is deprecated */ if (oParseConfig?.settings?.requireContext) { // We might have a set of already resolved "core:require" modules given from outside. // This only happens when a new XMLView instance is used as a wrapper for HTML nodes, in this case // the "core:require" modules need to be propagated down into the nested XMLView. // We now need to merge the set of passed "core:require" modules with the ones defined on our root element, // with our own modules having priority in case of duplicate aliases. pResultChain = pResultChain.then((mRequireContext) => { return Object.assign({}, oParseConfig.settings.requireContext, mRequireContext); }); } /** * @deprecated because the 'Sequential' Mode is used by default and it's the only mode that will be supported * in the next major release */ (() => { 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 = DesignTime.isDesignModeEnabled(); 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 /** * @ui5-transform-hint replace-local false */ const bCreateViewWrapper = oView.isA("sap.ui.core.mvc.XMLView") && (node.namespaceURI === XHTML_NAMESPACE || node.namespaceURI === SVG_NAMESPACE); if (bCreateViewWrapper) { // 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) { future.errorThrows("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(function(node, childNode, mOptions) { if (childNode.nodeType === 1 /* Element Node*/) { return createControls(childNode, mOptions.chain, null /*closest binding*/, undefined /* aggregation info*/, { rootArea: true }); } }, bAsync); pResultChain = pChain.then(function() { return handleChildren(node, { chain: pChain }); }); } return bWrapped; } function scopedRunWithOwner(fnCreation) { if (oView.fnScopedRunWithOwner) { // We need to use the already created scoped runWithOwner function from the outer view instance. // This way, the nested views are receiving the correct Owner component, across asynchronous calls. return oView.fnScopedRunWithOwner(fnCreation); } else { return fnCreation(); } } /** * 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); } }); sClassName = sClassName || sNamespaceURI + "." + sLocalName; /** * Validates if a control class is available and provides error feedback otherwise. * @param {sap.ui.core.Control|undefined} fnClass control class or undefined if not returned as module content for its sap.ui.define factory * @return {sap.ui.core.Control|undefined} the resolved class. */ function validateClass(fnClass, sResourceName) { if (!fnClass) { let sErrorLogMessage = `Control '${sClassName}' did not return a class definition from sap.ui.define.`; /** * Some modules might not return a class definition, so we fallback to the global namespace. * This is against the AMD definition, but is required for backward compatibility. * @deprecated */ (() => { fnClass = ObjectPath.get(sClassName); if (fnClass) { sErrorLogMessage += " The control class was instead retrieved via a deprecated access to the global namespace. This fallback behavior will be removed in the next major version (2.0)."; } })(); future.errorThrows(`XMLTemplateProcessor: ${sErrorLogMessage}`); } validateModuleContent(sResourceName, fnClass); return fnClass; } var sResourceName = sClassName.replace(/\./g, "/"); var oClassObject = sap.ui.require(sResourceName); if (!oClassObject) { /** * Synchronous loading of control class * @deprecated since 1.120 */ if (!bAsync) { oClassObject = sap.ui.requireSync(sResourceName); // legacy-relevant: Sync path oClassObject = validateClass(oClassObject, sResourceName); return oClassObject; } return new Promise(function(resolve, reject) { sap.ui.require([sResourceName], function(oClassObject) { try { oClassObject = validateClass(oClassObject, sResourceName); resolve(oClassObject); } catch (e) { reject(e); } }, reject); }); } else { // even when retrieving a class with sap.ui.require, we should validate the module content validateModuleContent(sResourceName, 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; /** * @ui5-transform-hint replace-local false */ const bRenderText = node.nodeType === 3 /* TEXT_NODE */ && bRenderingRelevant; if ( node.nodeType === 1 /* ELEMENT_NODE */ ) { // Using native HTML in future is not allowed. We need to check explicitely in order to throw if (node.namespaceURI === XHTML_NAMESPACE || node.namespaceURI === SVG_NAMESPACE) { future.warningThrows(`${oView.getId()}: Using native HTML content in XMLViews is deprecated.`); } /** * Differentiate between SAPUI5 and plain-HTML children * @ui5-transform-hint replace-local false */ const isNativeContent = node.namespaceURI === XHTML_NAMESPACE || node.namespaceURI === SVG_NAMESPACE; if (isNativeContent) { 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]; // id and core:require should not be output to DOM if ( attr.name !== "id" && (attr.localName !== "require" || attr.namespaceURI !== CORE_NAMESPACE)) { 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(); var pSelfRequireContext = parseAndLoadRequireContext(node, bAsync); if (pSelfRequireContext) { pRequireContext = SyncPromise.all([pRequireContext, pSelfRequireContext]) .then(function(aContexts) { return Object.assign({}, ...aContexts); }); } // write children // For HTMLTemplateElement nodes, skip the associated DocumentFragment node var oContent = node instanceof HTMLTemplateElement ? node.content : node; var handleChildren = getHandleChildrenStrategy(function (node, childNode, mOptions) { return createControls(childNode, mOptions.chain, mOptions.closestBinding, mOptions.aggregation, mOptions.config); }, bAsync); 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, oRequireContext) { var mViewParameters = { id: id ? getId(oView, node, id) : undefined, xmlNode: node, requireContext: oRequireContext, containingView: oView._oContainingView }; /** * @deprecated because the 'Sequential' Mode is used by default and it's the only mode that will be supported * in the next major release * * add processing mode, so it can be propagated to subviews inside the HTML block */ mViewParameters.processingMode = oView._sProcessingMode; // running with owner component return scopedRunWithOwner(function() { return new oViewClass(mViewParameters); }); }; return pRequireContext.then(function(oRequireContext) { if (bAsync) { return new Promise(function (resolve, reject) { sap.ui.require(["sap/ui/core/mvc/XMLView"], function(XMLView) { resolve([fnCreateView(XMLView, oRequireContext)]); }, reject); }); } else { var XMLView = sap.ui.requireSync("sap/ui/core/mvc/XMLView"); // legacy-relevant: Sync path return [fnCreateView(XMLView, oRequireContext)]; } }); } } } else { pResult = createControlOrExtension(node, pRequireContext, oClosestBinding); if (bRenderingRelevant) { collectControl(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 (bRenderText) { 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(scopedRunWithOwner(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]); });