UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

1,235 lines (1,104 loc) 63.7 kB
/*! * OpenUI5 * (c) Copyright 2009-2021 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ /*global HTMLTemplateElement, DocumentFragment, Promise*/ sap.ui.define([ 'sap/ui/thirdparty/jquery', 'sap/ui/base/DataType', 'sap/ui/base/ManagedObject', '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/util/values', 'sap/base/assert', 'sap/base/security/encodeXML', 'sap/base/util/LoaderExtensions', 'sap/base/util/JSTokenizer', 'sap/base/util/isEmptyObject' ], function( jQuery, DataType, ManagedObject, CustomData, Component, View, ViewType, XMLProcessingMode, EventHandlerResolver, ExtensionPoint, StashedControlSupport, SyncPromise, Log, ObjectPath, values, assert, encodeXML, LoaderExtensions, JSTokenizer, isEmptyObject ) { "use strict"; function parseScalarType(sType, sValue, sName, oContext, oRequireModules) { // check for a binding expression (string) var oBindingInfo = ManagedObject.bindingParser(sValue, oContext, /*bUnescape*/true, /*bTolerateFunctionsNotFound*/false, /*bStaticContext*/false, /*bPreferContext*/false, oRequireModules); if ( oBindingInfo && typeof oBindingInfo === "object" ) { return oBindingInfo; } var vValue = sValue = 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" ? ManagedObject.bindingParser.escape(vValue) : vValue; } function localName(xmlNode) { // localName for standard browsers, baseName for IE, nodeName in the absence of namespaces return xmlNode.localName || xmlNode.baseName || 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"; /** * 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/"; /** * 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, oAggregation, mAggregations, pRequireContext, oClosestBinding) { var childNode, vChild, aChildren = []; for (childNode = node.firstChild; childNode; childNode = childNode.nextSibling) { vChild = fnCallback(node, oAggregation, mAggregations, childNode, false, pRequireContext, oClosestBinding); if (vChild) { aChildren.push(vChild.unwrap()); } } return SyncPromise.resolve(aChildren); } // async strategy ensures processing order by chaining the callbacks function asyncStrategy(node, oAggregation, mAggregations, pRequireContext, oClosestBinding) { var childNode, pChain = Promise.resolve(), aChildPromises = [pRequireContext]; for (childNode = node.firstChild; childNode; childNode = childNode.nextSibling) { pChain = pChain.then(fnCallback.bind(null, node, oAggregation, mAggregations, childNode, false, pRequireContext, oClosestBinding)); 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 only the attributes of the XML root node (View!) and fills them into the given settings object. * Children are parsed later on after the controller has been set. * TODO cannot handle event handlers in the root node * * @param {Element} xmlNode the XML element representing the View * @param {sap.ui.core.mvc.XMLView} oView the View to consider when parsing the attributes * @param {object} mSettings the settings object which should be enriched with the suitable attributes from the XML node * @return undefined */ XMLTemplateProcessor.parseViewAttributes = function(xmlNode, oView, mSettings) { var mAllProperties = oView.getMetadata().getAllProperties(); for ( var i = 0; i < xmlNode.attributes.length; i++) { var attr = xmlNode.attributes[i]; if (attr.name === 'controllerName') { oView._controllerName = attr.value; } else if (attr.name === 'resourceBundleName') { oView._resourceBundleName = attr.value; } else if (attr.name === 'resourceBundleUrl') { oView._resourceBundleUrl = attr.value; } else if (attr.name === 'resourceBundleLocale') { oView._resourceBundleLocale = attr.value; } else if (attr.name === 'resourceBundleAlias') { oView._resourceBundleAlias = attr.value; } else if (attr.name === 'class') { oView.addStyleClass(attr.value); } else if (!mSettings[attr.name] && mAllProperties[attr.name]) { mSettings[attr.name] = parseScalarType(mAllProperties[attr.name].type, attr.value, attr.name, oView._oContainingView.oController); } } }; /** * 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 * @return {Array} an array containing Controls and/or plain HTML element strings */ XMLTemplateProcessor.parseTemplate = function(xmlNode, oView) { return XMLTemplateProcessor.parseTemplatePromise(xmlNode, oView, false).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() { var p = SyncPromise.resolve(arguments[0]); if (oView.isA("sap.ui.core.Fragment")) { return p; } // args is the result array of the XMLTP's parsing. // It contains strings like "tabs/linebreaks/..." AND control instances // Additionally it also includes ExtensionPoint placeholder objects if an ExtensionPoint is present in the top-level of the View. var args = arguments; // 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 not flex changes exist) if (oView.isA("sap.ui.core.mvc.View") && oView._epInfo && oView._epInfo.all.length > 0) { p = fnTriggerExtensionPointProvider(bAsync, oView, { "content": oView._epInfo.all }); } // We need to remove ExtensionPoint placeholders from result array, // otherwise the XMLViewRenderer will stumble over them. return p.then(function() { // TODO: might be refactored into resolveResultPromises()? if (Array.isArray(args[0])) { args[0] = args[0].filter(function(e) { return !e._isExtensionPoint; }); } return args[0]; }); }); }; /** * 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 invalide 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(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]); }); 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(); // define internal namespace on root node xmlNode.setAttributeNS(XMLNS_NAMESPACE, "xmlns:" + sInternalPrefix, UI5_INTERNAL_NAMESPACE); 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 = sap.ui.getCore().getConfiguration().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 }; } var sCurrentName = oView.sViewName || oView._sFragmentName; // TODO: should Fragments and Views be separated here? 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; } if (oView.isSubView()) { parseNode(xmlNode, true, false, pResultChain); } else { if (xmlNode.localName === "View" && xmlNode.namespaceURI !== "sap.ui.core.mvc") { // it's not <core:View>, it's <mvc:View> !!! Log.warning("XMLView root node must have the 'sap.ui.core.mvc' namespace, not '" + xmlNode.namespaceURI + "'" + (sCurrentName ? " (View name: " + sCurrentName + ")" : "")); } parseChildren(xmlNode, false, false, 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); function identity(sId) { return sId; } function createId(sId) { return oView._oContainingView.createId(sId); } /** * Parses an XML node that might represent a UI5 control or simple XHTML. * XHTML will be added to the aResult array as a sequence of strings, * UI5 controls will be instantiated and added as controls * * @param {Element} xmlNode the XML node to parse * @param {boolean} bRoot whether this node is the root node * @param {boolean} bIgnoreTopLevelTextNodes * @param {Promise} pRequireContext Promise which resolves with the loaded modules from require context * @returns {Promise} resolving with the content of the parsed node, which is a tree structure containing DOM Strings & UI5 Controls */ function parseNode(xmlNode, bRoot, bIgnoreTopLevelTextNodes, pRequireContext) { if ( xmlNode.nodeType === 1 /* ELEMENT_NODE */ ) { var sLocalName = localName(xmlNode); if (xmlNode.namespaceURI === XHTML_NAMESPACE || xmlNode.namespaceURI === SVG_NAMESPACE) { // write opening tag aResult.push("<" + sLocalName + " "); // write attributes var bHasId = false; for (var i = 0; i < xmlNode.attributes.length; i++) { var attr = xmlNode.attributes[i]; var value = attr.value; if (attr.name === "id") { bHasId = true; value = getId(oView, xmlNode); } aResult.push(attr.name + "=\"" + encodeXML(value) + "\" "); } if ( bRoot === true ) { aResult.push("data-sap-ui-preserve" + "=\"" + oView.getId() + "\" "); if (!bHasId) { aResult.push("id" + "=\"" + oView.getId() + "\" "); } } aResult.push(">"); // write children var oContent = xmlNode; if (window.HTMLTemplateElement && xmlNode instanceof HTMLTemplateElement && xmlNode.content instanceof DocumentFragment) { // <template> support (HTMLTemplateElement has no childNodes, but a content node which contains the childNodes) oContent = xmlNode.content; } parseChildren(oContent, false, false, pRequireContext); aResult.push("</" + sLocalName + ">"); } else if (sLocalName === "FragmentDefinition" && xmlNode.namespaceURI === CORE_NAMESPACE) { // a Fragment element - which is not turned into a control itself. Only its content is parsed. parseChildren(xmlNode, false, true, pRequireContext); // TODO: check if this branch is required or can be handled by the below one } else { // assumption: an ELEMENT_NODE with non-XHTML namespace is an SAPUI5 control and the namespace equals the library name pResultChain = pResultChain.then(function() { // Chaining the Promises as we need to make sure the order in which the XML DOM nodes are processed is fixed (depth-first, pre-order). // The order of processing (and Promise resolution) is mandatory for keeping the order of the UI5 Controls' aggregation fixed and compatible. return createControlOrExtension(xmlNode, pRequireContext).then(function(aChildControls) { for (var i = 0; i < aChildControls.length; i++) { var oChild = aChildControls[i]; // only views have a content aggregation if (oView.getMetadata().hasAggregation("content")) { // track extensionpoint information for root-level children of the view oView._epInfo = oView._epInfo || { contentControlsCount: 0, last: null, all: [] }; // child node is a placeholder for an ExtensionPoint // only in Flexibility scenario if an ExtensionProvider is given! if (oChild._isExtensionPoint) { oChild.index = oView._epInfo.contentControlsCount; oChild.targetControl = oView; oChild.aggregationName = "content"; if (oView._epInfo.last) { oView._epInfo.last._nextSibling = oChild; } oView._epInfo.last = oChild; oView._epInfo.all.push(oChild); } else { // regular UI5 Controls can be added to the content aggregation directly oView._epInfo.contentControlsCount++; oView.addAggregation("content", oChild); } // can oView really have an association called "content"? } else if (oView.getMetadata().hasAssociation(("content"))) { oView.addAssociation("content", oChild); } } return aChildControls; }); }); aResult.push(pResultChain); } } else if (xmlNode.nodeType === 3 /* TEXT_NODE */ && !bIgnoreTopLevelTextNodes) { var text = xmlNode.textContent || xmlNode.text, parentName = localName(xmlNode.parentNode); if (text) { if (parentName != "style") { text = encodeXML(text); } aResult.push(text); } } } /** * Parses the children of an XML node. * * @param {Element} xmlNode the xml node which will be parsed * @param {boolean} bRoot * @param {boolean} bIgnoreToplevelTextNodes * @param {Promise} pRequireContext Promise which resolves with the loaded modules from require context * @returns {Promise[]} each resolving to the according child nodes content */ function parseChildren(xmlNode, bRoot, bIgnoreToplevelTextNodes, pRequireContext) { var children = xmlNode.childNodes; for (var i = 0; i < children.length; i++) { parseNode(children[i], bRoot, bIgnoreToplevelTextNodes, pRequireContext); } } /** * 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 = sap.ui.getCore().getLoadedLibraries(); jQuery.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); 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 * @return {Promise} resolving to an array with 0..n controls * @private */ function createControls(node, pRequireContext, oClosestBinding) { // differentiate between SAPUI5 and plain-HTML children if (node.namespaceURI === XHTML_NAMESPACE || node.namespaceURI === SVG_NAMESPACE ) { 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); }; 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"); return SyncPromise.resolve([fnCreateView(XMLView)]); } } } else { // non-HTML (SAPUI5) control return createControlOrExtension(node, pRequireContext, oClosestBinding); } } /** * 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 * @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; // @evo-todo: The factory call needs to be refactored into a proper async/sync switch. // @evo-todo: The ExtensionPoint module is actually the sap.ui.extensionpoint function. // We still call _factory for skipping the deprecation warning for now. 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; }); }); return SyncPromise.resolve(oView.fnScopedRunWithOwner ? oView.fnScopedRunWithOwner(fnExtensionPointFactory) : fnExtensionPointFactory()); } } else { // a plain and simple regular UI5 control var vClass = findControlClass(node.namespaceURI, localName(node)); 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. * * @return {Promise} resolving to an array with 0..n controls created from a node * @private */ function createRegularControls(node, oClass, pRequireContext, oClosestBinding) { 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"; // remove stashed attribute as it is an uknown property. if (!bEnrichFullIds) { node.removeAttribute("stashed"); } if (!oClass) { return SyncPromise.resolve([]); } var oMetadata = oClass.getMetadata(); var mKnownSettings = oMetadata.getAllSettings(); var pSelfRequireContext = parseAndLoadRequireContext(node, bAsync); // 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; } 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; // 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") { // 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 = ManagedObject.bindingParser(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) })); } 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 = ManagedObject.bindingParser(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 (!bStashedControl) { mSettings[sName] = createId(sValue); // use the value as ID } } else if (oInfo && oInfo._iKind === 4 /* MULTIPLE_ASSOCIATION */ ) { // we support "," and " " to separate IDs and filter out empty IDs if (!bStashedControl) { mSettings[sName] = sValue.split(/[\s,]+/g).filter(identity).map(createId); } } else if (oInfo && oInfo._iKind === 5 /* EVENT */ ) { // EVENT if (!bStashedControl) { var aEventHandlers = []; EventHandlerResolver.parse(sValue).forEach(function (sEventHandler) { // eslint-disable-line no-loop-func var vEventHandler = EventHandlerResolver.resolveEventHandler(sEventHandler, oView._oContainingView.oController, oRequireModules); // TODO: can this be made async? (to avoid the hard resolver dependency) if (vEventHandler) { aEventHandlers.push(vEventHandler); } else { Log.warning(oView + ": event handler function \"" + sEventHandler + "\" is not a function or does not exist in the controller."); } }); if (aEventHandlers.length) { mSettings[sName] = aEventHandlers; } } } else if (oInfo && oInfo._iKind === -1) { // SPECIAL SETTING - currently only allowed for: // - View's async setting if (View.prototype.isPrototypeOf(oClass.prototype) && sName == "async") { mSettings[sName] = parseScalarType(oInfo.type, sValue, sName, oView._oContainingView.oController, oRequireModules); } else { Log.warning(oView + ": setting '" + sName + "' for class " + oMetadata.getName() + " (value:'" + sValue + "') is not supported"); } } else { assert(sName === 'xmlns', oView + ": encountered unknown setting '" + sName + "' for class " + oMetadata.getName() + " (value:'" + sValue + "')"); if (XMLTemplateProcessor._supportInfo) { XMLTemplateProcessor._supportInfo({ context : node, env : { caller:"createRegularControls", error: true, info: "unknown setting '" + sName + "' for class " + oMetadata.getName() } }); } } } //add custom settings as custom data "sap-ui-custom-settings" if (mCustomSettings) { aCustomData.push(new CustomData({ key:"sap-ui-custom-settings", value: mCustomSettings })); } if (aCustomData.length > 0) { mSettings.customData = aCustomData; } } return oRequireModules; }).catch(function(oError) { // Errors caught here are expected UI5 issues, e.g. DataType errors, broken BindingSyntax, missing event handler functions etc. // we enrich the error message with XML information, e.g. the node causing the issue if (!oError.isEnriched) { var sType = oView.getMetadata().isA("sap.ui.core.mvc.View") ? "View" : "Fragment"; var sNodeSerialization = node && node.cloneNode(false).outerHTML; // Logging the error like this cuts away the stack trace, // but provides better information for applications. // For Framework debugging, we would have to look at the error object anyway. oError = new Error( "Error found in " + sType + " (id: '" + oView.getId() + "').\nXML node: '" + sNodeSerialization + "':\n" + oError ); oError.isEnriched = true; // TODO: Can be enriched with additional info for a support rule (not yet implemented) Log.error(oError); } // [COMPATIBILITY] // sync: we just log the error and keep on processing // asnyc: throw the error, so the parseTempate Promise will reject if (bAsync && oView._sProcessingMode !== XMLProcessingMode.SequentialLegacy) { throw oError; } }); /** * The way how handleChildren works determines parallel or sequential processing * * @return {Promise} resolving to an array with 0..n controls created from a node * @private */ // the actual handleChildren function depends on the processing mode var handleChildren = getHandleChildrenStrategy(bAsync, handleChild); /** * @return {Promise} resolving to an array with 0..n controls created from a node * @private */ function handleChild(node, oAggregation, mAggregations, childNode, bActivate, pRequireContext, oClosestBinding) { var oNamedAggregation, fnCreateStashedControl; // inspect only element nodes if (childNode.nodeType === 1 /* ELEMENT_NODE */) { if (childNode.namespaceURI === XML_COMPOSITE_NAMESPACE) { mSettings[localName(childNode)] = childNode.querySelector("*"); return; } // check for a named aggregation (must have the same namespace as the parent and an aggregation with the same name must exist) oNamedAggregation = childNode.namespaceURI === ns && mAggregations && mAggregations[localName(childNode)]; if (oNamedAggregation) { // the children of the current childNode are aggregated controls (or HTML) below the named aggregation return handleChildren(childNode, oNamedAggregation, false, pRequireContext, oClosestBinding); } else if (oAggregation) { // TODO consider moving this to a place where HTML and SVG nodes can be handled properly // create a StashedControl for inactive controls, which is not placed in an aggregation if (!bActivate && childNode.getAttribute("stashed") === "true" && !bEnrichFullIds) { var oStashedNode = childNode; // remove child-nodes... childNode = childNode.cloneNode(); // remove stashed attribute as it is an uknown property. oStashedNode.removeAttribute("stashed"); fnCreateStashedControl = function() { var sControlId = getId(oView, childNode); StashedControlSupport.createStashedControl({ wrapperId: sControlId, fnCreate: function() { // EVO-Todo: stashed control-support is still mandatory SYNC // this means we need to switch back the view processing to synchronous too // at this point everything is sync again