UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

1,271 lines (1,135 loc) 71.3 kB
/*! * 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