UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

1,275 lines (1,204 loc) 74.2 kB
/*! * OpenUI5 * (c) Copyright 2009-2021 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ // Provides object sap.ui.core.util.XMLPreprocessor sap.ui.define([ "sap/base/Log", "sap/base/util/deepExtend", "sap/base/util/JSTokenizer", "sap/base/util/ObjectPath", "sap/ui/base/BindingParser", "sap/ui/base/ManagedObject", "sap/ui/base/SyncPromise", "sap/ui/core/XMLTemplateProcessor", "sap/ui/model/BindingMode", "sap/ui/model/CompositeBinding", "sap/ui/model/Context", "sap/ui/performance/Measurement" ], function (Log, deepExtend, JSTokenizer, ObjectPath, BindingParser, ManagedObject, SyncPromise, XMLTemplateProcessor, BindingMode, CompositeBinding, Context, Measurement) { "use strict"; var sNAMESPACE = "http://schemas.sap.com/sapui5/extension/sap.ui.core.template/1", sXMLPreprocessor = "sap.ui.core.util.XMLPreprocessor", aPerformanceCategories = [sXMLPreprocessor], sPerformanceGetResolvedBinding = sXMLPreprocessor + "/getResolvedBinding", sPerformanceInsertFragment = sXMLPreprocessor + "/insertFragment", sPerformanceProcess = sXMLPreprocessor + ".process", oSyncPromiseResolved = SyncPromise.resolve(), oSyncPromiseResolvedTrue = SyncPromise.resolve(true), fnToString = Object.prototype.toString, mVisitors = {}, // maps "<namespace URI> <local name>" to visitor function /** * <template:with> control holding the models and the bindings. Also used as substitute for * any control during template processing in order to resolve property bindings. Supports * nesting of template instructions. */ With = ManagedObject.extend("sap.ui.core.util._with", { metadata : { library: "sap.ui.core", properties : { any : "any" }, aggregations : { child : {multiple : false, type : "sap.ui.core.util._with"} } }, updateProperty : function () { // Avoid Promise processing in ManagedObject and set Promise as value directly this.setAny(this.mBindingInfos.any.binding.getExternalValue()); } }), /** * <template:repeat> control extending the "with" control by an aggregation which is used to * get the list binding. */ Repeat = With.extend("sap.ui.core.util._repeat", { metadata : { library: "sap.ui.core", aggregations : { list : {multiple : true, type : "n/a", _doesNotRequireFactory : true} } }, updateList : function () { // Override sap.ui.base.ManagedObject#updateAggregation for "list" and do nothing to // avoid that any child objects are created } }); /** * Creates the context interface for a call to the given control's formatter of the binding part * with given index. * * @param {sap.ui.core.util._with} oWithControl * the "with" control * @param {object} mSettings * map/JSON-object with initial property values, etc. * @param {number} [i] * index of part inside a composite binding * @param {sap.ui.model.Binding|sap.ui.model.Binding[]|sap.ui.model.Context} * [vBindingOrContext] * single binding or model context or array of parts; if this parameter is given, * "oWithControl" and "i" are ignored, else it is lazily computed from those two * @returns {sap.ui.core.util.XMLPreprocessor.IContext} * the callback interface */ function createContextInterface(oWithControl, mSettings, i, vBindingOrContext) { /* * Returns the single binding or model context related to the current formatter call. * * @param {number} [iPart] * index of part in case of the root formatter of a composite binding * @returns {sap.ui.model.Binding|sap.ui.model.Context} * single binding or model context */ function getBindingOrContext(iPart) { if (!vBindingOrContext) { // lazy initialization // BEWARE: this is not yet defined when createContextInterface() is called! vBindingOrContext = oWithControl.getBinding("any"); if (vBindingOrContext instanceof CompositeBinding) { vBindingOrContext = vBindingOrContext.getBindings(); if (i !== undefined) { // not a root formatter vBindingOrContext = vBindingOrContext[i]; } } } return Array.isArray(vBindingOrContext) ? vBindingOrContext[iPart] : vBindingOrContext; } /** * Returns the resolved path for the given single binding or model context. * * @param {sap.ui.model.Binding|sap.ui.model.Context} oBindingOrContext * single binding or model context * @returns {string} * the resolved path */ function getPath(oBindingOrContext) { return oBindingOrContext instanceof Context ? oBindingOrContext.getPath() : oBindingOrContext.getModel().resolve( oBindingOrContext.getPath(), oBindingOrContext.getContext()); } /** * Context interface provided by XML template processing as an additional first argument to * any formatter function which opts in to this mechanism. Candidates for such formatter * functions are all those used in binding expressions which are evaluated during XML * template processing, including those used inside template instructions like * <code>&lt;template:if></code>. The formatter function needs to be marked with a property * <code>requiresIContext = true</code> to express that it requires this extended signature * (compared to ordinary formatter functions). The usual arguments are provided after the * first one (currently: the raw value from the model). * * This interface provides callback functions to access the model and path which are needed * to process OData V4 annotations. It initially offers a subset of methods from * {@link sap.ui.model.Context} so that formatters might also be called with a context * object for convenience, e.g. outside of XML template processing (see below for an * exception to this rule). * * <b>Example:</b> Suppose you have a formatter function called "foo" like below and it is * used within an XML template like * <code>&lt;template:if test="{path: '...', formatter: 'foo'}"></code>. * In this case <code>foo</code> is called with arguments <code>oInterface, vRawValue</code> * such that * <code>oInterface.getModel().getObject(oInterface.getPath()) === vRawValue</code> holds. * <pre> * window.foo = function (oInterface, vRawValue) { * //TODO ... * }; * window.foo.requiresIContext = true; * </pre> * * <b>Composite Binding Examples:</b> Suppose you have the same formatter function and it is * used in a composite binding like <code>&lt;Text text="{path: 'Label', formatter: 'foo'}: * {path: 'Value', formatter: 'foo'}"/></code>. * In this case <code>oInterface.getPath()</code> refers to ".../Label" in the 1st call and * ".../Value" in the 2nd call. This means each formatter call knows which part of the * composite binding it belongs to and behaves just as if it was an ordinary binding. * * Suppose your formatter is not used within a part of the composite binding, but at the * root of the composite binding in order to aggregate all parts like <code> * &lt;Text text="{parts: [{path: 'Label'}, {path: 'Value'}], formatter: 'foo'}"/></code>. * In this case <code>oInterface.getPath(0)</code> refers to ".../Label" and * <code>oInterface.getPath(1)</code> refers to ".../Value". This means, the root formatter * can access the ith part of the composite binding at will (since 1.31.0); see also * {@link #.getInterface getInterface}. * The function <code>foo</code> is called with arguments such that <code> * oInterface.getModel(i).getObject(oInterface.getPath(i)) === arguments[i + 1]</code> * holds. * This use is not supported within an expression binding, that is, <code>&lt;Text * text="{= ${parts: [{path: 'Label'}, {path: 'Value'}], formatter: 'foo'} }"/></code> * does not work as expected because the property <code>requiresIContext = true</code> is * ignored. * * To distinguish those two use cases, just check whether <code>oInterface.getModel() === * undefined</code>, in which case the formatter is called on root level of a composite * binding. To find out the number of parts, probe for the smallest non-negative integer * where <code>oInterface.getModel(i) === undefined</code>. * This additional functionality is, of course, not available from * {@link sap.ui.model.Context}, i.e. such formatters MUST be called with an instance of * this context interface. * * @interface * @name sap.ui.core.util.XMLPreprocessor.IContext * @public * @since 1.27.1 */ return /** @lends sap.ui.core.util.XMLPreprocessor.IContext */ { /** * Returns a context interface for the indicated part in case of the root formatter of a * composite binding. The new interface provides access to the original settings, but * only to the model and path of the indicated part: * <pre> * this.getInterface(i).getSetting(sName) === this.getSetting(sName); * this.getInterface(i).getModel() === this.getModel(i); * this.getInterface(i).getPath() === this.getPath(i); * </pre> * * If a path is given, the new interface points to the resolved path as follows: * <pre> * this.getInterface(i, "foo/bar").getPath() === this.getPath(i) + "/foo/bar"; * this.getInterface(i, "/absolute/path").getPath() === "/absolute/path"; * </pre> * A formatter which is not at the root level of a composite binding can also provide a * path, but must not provide an index: * <pre> * this.getInterface("foo/bar").getPath() === this.getPath() + "/foo/bar"; * this.getInterface("/absolute/path").getPath() === "/absolute/path"; * </pre> * Note that at least one argument must be present. * * @param {number} [iPart] * index of part in case of the root formatter of a composite binding * @param {string} [sPath] * a path, interpreted relative to <code>this.getPath(iPart)</code> * @returns {sap.ui.core.util.XMLPreprocessor.IContext} * the context interface related to the indicated part * @throws {Error} * In case an index is given but the current interface does not belong to the root * formatter of a composite binding, or in case the given index is invalid (e.g. * missing or out of range), or in case a path is missing because no index is given, * or in case a path is given but the model cannot not create a binding context * synchronously * @public * @since 1.31.0 */ getInterface : function (iPart, sPath) { var oBaseContext, oBindingOrContext, oModel; if (typeof iPart === "string") { sPath = iPart; iPart = undefined; } getBindingOrContext(); // initialize vBindingOrContext if (Array.isArray(vBindingOrContext)) { if (iPart >= 0 && iPart < vBindingOrContext.length) { oBindingOrContext = vBindingOrContext[iPart]; } else { throw new Error("Invalid index of part: " + iPart); } } else if (iPart !== undefined) { throw new Error("Not the root formatter of a composite binding"); } else if (sPath) { oBindingOrContext = vBindingOrContext; } else { throw new Error("Missing path"); } if (sPath) { oModel = oBindingOrContext.getModel(); if (sPath.charAt(0) !== '/') { // relative path needs a base context oBaseContext = oBindingOrContext instanceof Context ? oBindingOrContext : oModel.createBindingContext(oBindingOrContext.getPath(), oBindingOrContext.getContext()); } oBindingOrContext = oModel.createBindingContext(sPath, oBaseContext); if (!oBindingOrContext) { throw new Error( "Model could not create binding context synchronously: " + oModel); } } return createContextInterface(null, mSettings, undefined, oBindingOrContext); }, /** * Returns the model related to the current formatter call. * * @param {number} [iPart] * index of part in case of the root formatter of a composite binding * (since 1.31.0) * @returns {sap.ui.model.Model} * the model related to the current formatter call, or (since 1.31.0) * <code>undefined</code> in case of a root formatter if no <code>iPart</code> is * given or if <code>iPart</code> is out of range * @public */ getModel : function (iPart) { var oBindingOrContext = getBindingOrContext(iPart); return oBindingOrContext && oBindingOrContext.getModel(); }, /** * Returns the absolute path related to the current formatter call. * * @param {number} [iPart] * index of part in case of the root formatter of a composite binding (since 1.31.0) * @returns {string} * the absolute path related to the current formatter call, or (since 1.31.0) * <code>undefined</code> in case of a root formatter if no <code>iPart</code> is * given or if <code>iPart</code> is out of range * @public */ getPath : function (iPart) { var oBindingOrContext = getBindingOrContext(iPart); return oBindingOrContext && getPath(oBindingOrContext); }, /** * Returns the value of the setting with the given name which was provided to the XML * template processing. * * @param {string} sName * the name of the setting * @returns {any} * the value of the setting * @throws {Error} * if the name is one of the reserved names: "bindingContexts", "models" * @public */ getSetting : function (sName) { if (sName === "bindingContexts" || sName === "models") { throw new Error("Illegal argument: " + sName); } return mSettings[sName]; } }; } /** * Gets the value of the control's "any" property via the given binding info. * * @param {sap.ui.core.util._with} oWithControl * the "with" control * @param {object} oBindingInfo * the binding info * @param {object} mSettings * map/JSON-object with initial property values, etc. * @param {object} oScope * map of currently known aliases * @param {boolean} bAsync * whether async processing is allowed * @returns {sap.ui.base.SyncPromise|null} * a sync promise which resolves with the property value or is rejected with a corresponding * error (for example, an error thrown by a formatter), or <code>null</code> in case the * binding is not ready (because it refers to a model which is not available) */ function getAny(oWithControl, oBindingInfo, mSettings, oScope, bAsync) { var bValueAsPromise = false; /* * Prepares the given binding info or part of it; makes it "one time" and binds its * formatter function (if opted in) to an interface object. * * @param {object} oInfo * a binding info or a part of it * @param {number} i * index of binding info's part (if applicable) */ function prepare(oInfo, i) { var fnFormatter = oInfo.formatter, oModel, sModelName = oInfo.model; if (oInfo.path && oInfo.path.indexOf(">") > 0) { sModelName = oInfo.path.slice(0, oInfo.path.indexOf(">")); } oModel = oWithControl.getModel(sModelName); if (fnFormatter && fnFormatter.requiresIContext === true) { fnFormatter = oInfo.formatter = fnFormatter.bind(null, createContextInterface(oWithControl, mSettings, i)); } // wrap formatter only if there is a formatter and async is allowed and either // - we use $$valueAsPromise ourselves, or // - we are top-level and at least one child has used $$valueAsPromise if (fnFormatter && bAsync && (oModel && oModel.$$valueAsPromise || i === undefined && bValueAsPromise)) { oInfo.formatter = function () { var that = this; return SyncPromise.all(arguments).then(function (aArguments) { return fnFormatter.apply(that, aArguments); }); }; oInfo.formatter.textFragments = fnFormatter.textFragments; } oInfo.mode = BindingMode.OneTime; oInfo.parameters = oInfo.parameters || {}; oInfo.parameters.scope = oScope; if (bAsync && oModel && oModel.$$valueAsPromise) { // opt-in to async behavior bValueAsPromise = oInfo.parameters.$$valueAsPromise = true; } } try { if (oBindingInfo.parts) { oBindingInfo.parts.forEach(prepare); } prepare(oBindingInfo); oWithControl.bindProperty("any", oBindingInfo); return oWithControl.getBinding("any") ? SyncPromise.resolve(oWithControl.getAny()) : null; } catch (e) { return SyncPromise.reject(e); } finally { oWithControl.unbindProperty("any", true); } } /** * Visits the given elements one-by-one, calls the given callback for each of them and stops * and waits for each sync promise returned by the callback before going on to the next element. * If a sync promise resolves with a truthy value, iteration stops and the corresponding element * becomes the result of the returned sync promise. * * @param {any[]} aElements * Whatever elements we want to visit * @param {function} fnCallback * A function to be called with a single element and its index and the array (like * {@link Array#find} does it), returning a {@link sap.ui.base.SyncPromise}. * @returns {sap.ui.base.SyncPromise} * A sync promise which resolves with the first element where the callback's sync promise * resolved with a truthy value, or resolves with <code>undefined</code> as soon as the last * callback's sync promise has resolved, or is rejected with a corresponding error if any * callback returns a rejected sync promise or throws an error * @throws {Error} * If the first callback throws */ function stopAndGo(aElements, fnCallback) { var i = -1; /* * Visits the next element, taking the result of the previous callback into account. * * @param {boolean} bFound * Whether an element was approved by the corresponding callback * @returns {sap.ui.base.SyncPromise|any} * First call returns a <code>sap.ui.base.SyncPromise</code> which resolves with a later * call's result. */ function next(bFound) { if (bFound) { return aElements[i]; } i += 1; if (i < aElements.length) { return fnCallback(aElements[i], i, aElements).then(next); } } return aElements.length ? next() : oSyncPromiseResolved; } /** * Serializes the element with its attributes. * <p> * BEWARE: makes no attempt at encoding, DO NOT use in a security critical manner! * * @param {Element} oElement any XML DOM element * @returns {string} the serialization */ function serializeSingleElement(oElement) { var oAttribute, oAttributesList = oElement.attributes, sText = "<" + oElement.nodeName, i, n; for (i = 0, n = oAttributesList.length; i < n; i += 1) { oAttribute = oAttributesList.item(i); sText += " " + oAttribute.name + '="' + oAttribute.value + '"'; } return sText + (oElement.childNodes.length ? ">" : "/>"); } /** * Wrapper for the "visitNode" function which is sometimes returned by * {@link sap.ui.core.util.XMLPreprocessor.plugIn}. Delegates to the appropriate "visitNode" * function from the callback interface (which is not yet available at plug-in time) and makes * sure no extra arguments are passed. * * @param {Element} oElement * The XML DOM Element * @param {sap.ui.core.util.XMLPreprocessor.ICallback} oInterface * The callback interface * @returns {sap.ui.base.SyncPromise} * A thenable which resolves with <code>undefined</code> as soon as visiting is done, or is * rejected with a corresponding error if visiting fails (since 1.57.0) * * @see sap.ui.core.util.XMLPreprocessor.visitNodeWrapper */ function visitNodeWrapper(oElement, oInterface) { return oInterface.visitNode(oElement); } /** * @classdesc * The XML pre-processor for template instructions in XML views. * * @namespace sap.ui.core.util.XMLPreprocessor * @public * @since 1.27.1 */ return /** @lends sap.ui.core.util.XMLPreprocessor */ { /** * Plug-in the given visitor which is called for each matching XML element. * * @param {function} [fnVisitor] * Visitor function, will be called with the matching XML DOM element and a * {@link sap.ui.core.util.XMLPreprocessor.ICallback callback interface} which uses a map * of currently known variables; must return <code>undefined</code>. * Must be either a function or <code>null</code>, nothing else. * @param {string} sNamespace * The expected namespace URI; must not contain spaces; * "http://schemas.sap.com/sapui5/extension/sap.ui.core.template/1" and "sap.ui.core" are * reserved * @param {string} [sLocalName] * The expected local name; if it is missing, the local name is ignored for a match. * @returns {function} * The visitor function which previously matched elements with the given namespace and * local name, or a function which calls "visitNode" but never <code>null</code> so that * you can safely delegate to it. * In general, you cannot restore the previous state by calling <code>plugIn</code> again * with this function. * @throws {Error} * If visitor or namespace is invalid * * @private */ plugIn : function (fnVisitor, sNamespace, sLocalName) { var fnOldVisitor = mVisitors[sNamespace]; if (fnVisitor !== null && typeof fnVisitor !== "function" || fnVisitor === visitNodeWrapper) { throw new Error("Invalid visitor: " + fnVisitor); } if (!sNamespace || sNamespace === sNAMESPACE || sNamespace === "sap.ui.core" || sNamespace.indexOf(" ") >= 0) { throw new Error("Invalid namespace: " + sNamespace); } Log.debug("Plug-in visitor for namespace '" + sNamespace + "', local name '" + sLocalName + "'", fnVisitor, sXMLPreprocessor); if (sLocalName) { sNamespace = sNamespace + " " + sLocalName; fnOldVisitor = mVisitors[sNamespace] || fnOldVisitor; } mVisitors[sNamespace] = fnVisitor; return fnOldVisitor || visitNodeWrapper; }, /** * @private */ visitNodeWrapper : visitNodeWrapper, /** * Performs template pre-processing on the given XML DOM element. * * @param {Element} oRootElement * the XML DOM element to process * @param {object} oViewInfo * info object of the calling instance * @param {string} oViewInfo.caller * identifies the caller of this preprocessor; used as a prefix for log or exception * messages * @param {string} oViewInfo.componentId * ID of the owning component (since 1.31; needed for extension point support) * @param {string} oViewInfo.name * the view name (since 1.31; needed for extension point support) * @param {boolean} [oViewInfo.sync=false] * whether the view is synchronous (since 1.57.0; needed for asynchronous XML templating) * @param {object} [mSettings={}] * map/JSON-object with initial property values, etc. * @param {object} mSettings.bindingContexts * binding contexts relevant for template pre-processing * @param {object} mSettings.models * models relevant for template pre-processing * @returns {Element|Promise} * <code>oRootElement</code> or a promise which resolves with <code>oRootElement</code> as * soon as processing is done, or is rejected with a corresponding error if processing * fails; since 1.57.0, a promise is returned if and only if processing cannot complete * synchronously * * @private */ process : function (oRootElement, oViewInfo, mSettings) { var sCaller = oViewInfo.caller, bDebug = Log.isLoggable(Log.Level.DEBUG, sXMLPreprocessor), bCallerLoggedForWarnings = bDebug, // debug output already contains caller sCurrentName = oViewInfo.name, // current view or fragment name mFragmentCache = {}, sName, iNestingLevel = 0, oScope = {}, // for BindingParser.complexParser() fnSupportInfo = oViewInfo._supportInfo, bWarning = Log.isLoggable(Log.Level.WARNING, sXMLPreprocessor); /** * Returns a callback interface for visitor functions which provides access to private * {@link sap.ui.core.util.XMLPreprocessor} functionality using the given "with" * control. * * @param {sap.ui.core.util._with} oWithControl * The "with" control * @returns {sap.ui.core.util.XMLPreprocessor.ICallback} * A callback interface */ function createCallbackInterface(oWithControl) { /** * Callback interface for visitor functions which provides access to private * {@link sap.ui.core.util.XMLPreprocessor} functionality using a map of currently * known variables. Initially, these are the variables known to the XML * pre-processor when it reaches the visitor's matching element (see * {@link sap.ui.core.util.XMLPreprocessor.plugIn}). They can be overridden or * replaced via {@link sap.ui.core.util.XMLPreprocessor.ICallback.with}. * * @interface * @name sap.ui.core.util.XMLPreprocessor.ICallback * @private * @see sap.ui.core.util.XMLPreprocessor.plugIn */ return /** @lends sap.ui.core.util.XMLPreprocessor.ICallback */ { /** * Visits the given elements one-by-one, calls the given callback for each of * them and stops and waits for each thenable returned by the callback before * going on to the next element. If a thenable resolves with a truthy value, * iteration stops and the corresponding element becomes the result of the * returned thenable. * * <b>Note:</b> If the visitor function is used for synchronous XML Templating, * the callback must return a sync promise; in other cases, any thenable is OK. * * @param {any[]} aElements * Whatever elements we want to visit * @param {function} fnCallback * A function to be called with a single element and its index and the array * (like {@link Array#find} does it), returning a thenable, preferrably a * {@link sap.ui.base.SyncPromise} * @returns {sap.ui.base.SyncPromise} * A sync promise which resolves with the first element where the callback's * thenable resolved with a truthy value, or resolves with * <code>undefined</code> as soon as the last callback's thenable has * resolved, or is rejected with a corresponding error if any callback returns * a rejected thenable or throws an error */ find : function (aElements, fnCallback) { try { return SyncPromise.resolve(stopAndGo(aElements, fnCallback)); } catch (e) { return SyncPromise.reject(e); } }, /** * Returns the model's context which corresponds to the given simple binding * path. Uses the map of currently known variables. * * @param {string} [sPath=""] * A simple binding path which may include a model name ("a variable"), for * example "var>some/relative/path", but not a binding ("{...}") * @returns {sap.ui.model.Context} * The corresponding context which holds the model and the resolved, absolute * path * @throws {Error} * If a binding is given, if the path refers to an unknown model, or if the * path cannot be resolved (typically because a relative path was given for a * model without a binding context) */ getContext : function (sPath) { var oBindingInfo, oModel, sResolvedPath; sPath = sPath || ""; if (sPath[0] === "{") { throw new Error("Must be a simple path, not a binding: " + sPath); } oBindingInfo = BindingParser.simpleParser("{" + sPath + "}"); oModel = oWithControl.getModel(oBindingInfo.model); if (!oModel) { throw new Error("Unknown model '" + oBindingInfo.model + "': " + sPath); } sResolvedPath = oModel.resolve(oBindingInfo.path, oWithControl.getBindingContext(oBindingInfo.model)); if (!sResolvedPath) { throw new Error("Cannot resolve path: " + sPath); } return oModel.createBindingContext(sResolvedPath); }, /** * Interprets the given XML DOM attribute value as a binding and returns the * resulting value. Takes care of unescaping and thus also of constant * expressions; warnings are logged for (formatter) functions which are not * found. Uses the map of currently known variables. * * @param {string} sValue * An XML DOM attribute value * @param {Element} [oElement] * The XML DOM element the attribute value belongs to (needed only for * warnings which are logged to the console) * @returns {sap.ui.base.SyncPromise|null} * A thenable which resolves with the resulting value, or is rejected with a * corresponding error (for example, an error thrown by a formatter) or * <code>null</code> in case the binding is not ready (because it refers to a * model which is not available) (since 1.57.0) * * @function * @public * @since 1.39.0 */ getResult : function (sValue, oElement) { return getResolvedBinding(sValue, oElement, oWithControl, true); }, /** * Returns the settings object for XML template processing. * * @returns {object} * settings for the XML preprocessor; might contain the properties * "bindingContexts" and "models" and maybe others * * @function * @public * @since 1.41.0 */ getSettings : function () { return mSettings; }, /** * Returns the view info object for XML template processing. * * @returns {object} * info object of the XML preprocessor's calling instance; might contain the * string properties "caller", "componentId", "name" and maybe others * * @function * @public * @since 1.41.0 */ getViewInfo : function () { return deepExtend({}, oViewInfo); }, /** * Inserts the fragment with the given name in place of the given element. Loads * the fragment, takes care of caching (for the current pre-processor run) and * visits the fragment's content once it has been imported into the element's * owner document and put into place. * * @param {string} sFragmentName * The fragment's resolved name * @param {Element} oElement * The XML DOM element to be replaced * @returns {sap.ui.base.SyncPromise} * A thenable which resolves with <code>undefined</code> as soon as the * fragment has been inserted, or is rejected with a corresponding error if * loading or visiting fails (since 1.57.0) * @throws {Error} * If a cycle is detected (same <code>sFragmentName</code> and * {@link sap.ui.core.util.XMLPreprocessor.ICallback}) * * @function * @public * @see #with * @since 1.39.0 */ insertFragment : function (sFragmentName, oElement) { return insertFragment(sFragmentName, oElement, oWithControl); }, /** * Visit the given attribute of the given element. If the attribute value * represents a binding expression that can be resolved, it is replaced with * the resulting value. * * @param {Element} oElement * The XML DOM element * @param {Attr} oAttribute * One of the element's attribute nodes * @returns {sap.ui.base.SyncPromise} * A thenable which resolves with <code>undefined</code> as soon as the * attribute's value has been replaced, or is rejected with a corresponding * error if getting the binding's value fails (since 1.57.0) * * @function * @public * @see sap.ui.core.util.XMLPreprocessor.ICallback.visitAttributes * @since 1.51.0 */ visitAttribute : function (oElement, oAttribute) { return visitAttribute(oElement, oAttribute, oWithControl); }, /** * Visits all attributes of the given element. If an attribute value represents * a binding expression that can be resolved, it is replaced with the resulting * value. * * @param {Element} oElement * The XML DOM element * @returns {sap.ui.base.SyncPromise} * A thenable which resolves with <code>undefined</code> as soon as all * attributes' values have been replaced, or is rejected with a corresponding * error if getting some binding's value fails (since 1.57.0) * * @function * @public * @see sap.ui.core.util.XMLPreprocessor.ICallback.getResult * @since 1.39.0 */ visitAttributes : function (oElement) { return visitAttributes(oElement, oWithControl); }, /** * Visits all child nodes of the given node via {@link * sap.ui.core.util.XMLPreprocessor.ICallback.visitNode visitNode}. * * @param {Node} oNode * The XML DOM node * @returns {sap.ui.base.SyncPromise} * A thenable which resolves with <code>undefined</code> as soon as visiting * is done, or is rejected with a corresponding error if visiting fails * (since 1.57.0) * * @function * @public * @since 1.39.0 */ visitChildNodes : function (oNode) { return visitChildNodes(oNode, oWithControl); }, /** * Visits the given node and either processes a template instruction, calls * a visitor, or simply calls both {@link * sap.ui.core.util.XMLPreprocessor.ICallback.visitAttributes visitAttributes} * and {@link sap.ui.core.util.XMLPreprocessor.ICallback.visitChildNodes * visitChildNodes}. * * @param {Node} oNode * The XML DOM node * @returns {sap.ui.base.SyncPromise} * A thenable which resolves with <code>undefined</code> as soon as visiting * is done, or is rejected with a corresponding error if visiting fails * (since 1.57.0) * * @function * @public * @since 1.39.0 */ visitNode : function (oNode) { try { return visitNode(oNode, oWithControl); } catch (e) { return SyncPromise.reject(e); } }, /** * Returns a callback interface instance for the given map of variables which * override currently known variables of the same name in <code>this</code> * parent interface or replace them altogether. Each variable name becomes a * named model with a corresponding object binding and can be used inside the * XML template in the usual way, that is, with a binding expression like * <code>"{var>some/relative/path}"</code> (see example). * * <b>Example:</b> Suppose the XML pre-processor knows a variable named "old" * and a visitor defines a new variable relative to it as follows. * Then {@link sap.ui.core.util.XMLPreprocessor.ICallback.getResult getResult} * for a binding which refers to the new variable using a relative path * ("{new>relative}") has the same result as for a binding to the old variable * with a compound path ("{old>prefix/relative}"). * * <pre> * oInterface.with({"new" : oInterface.getContext("old>prefix")}) * .getResult("{new>relative}") * === oInterface.getResult("{old>prefix/relative}"); // true * </pre> * * BEWARE: Previous callback interface instances derived from the same parent * (<code>this</code>) become invalid (that is, they forget about inherited * variables) once a new instance is derived. * * @param {object} [mVariables={}] * Map from variable name (string) to value ({@link sap.ui.model.Context}) * @param {boolean} [bReplace=false] * Whether only the given variables are known in the new callback interface * instance, no inherited ones * @returns {sap.ui.core.util.XMLPreprocessor.ICallback} * A callback interface instance * * @function * @public * @see sap.ui.core.util.XMLPreprocessor.ICallback.getResult * @since 1.39.0 */ "with" : function (mVariables, bReplace) { var oContext, bHasVariables = false, sName, oNewWithControl = new With(); if (!bReplace) { oWithControl.setChild(oNewWithControl); } for (sName in mVariables) { oContext = mVariables[sName]; bHasVariables = true; oNewWithControl.setModel(oContext.getModel(), sName); oNewWithControl.bindObject({ model : sName, path : oContext.getPath() }); } return bHasVariables || bReplace ? createCallbackInterface(oNewWithControl) : this; } }; } /* * Outputs a debug message with the current nesting level; takes care not to construct * the message or serialize XML in vain. * * @param {Element} [oElement] * any XML DOM element which is serialized to the details * @param {...string} aTexts * the main text of the message is constructed from the rest of the arguments by * joining them separated by single spaces */ function debug(oElement) { if (bDebug) { Log.debug( getNestingLevel() + Array.prototype.slice.call(arguments, 1).join(" "), oElement && serializeSingleElement(oElement), sXMLPreprocessor); } } /** * Outputs a debug message "Finished" with the given nesting level; takes care not to * serialize XML in vain. * * @param {Element} oElement * any XML DOM element which is serialized to the details */ function debugFinished(oElement) { if (bDebug) { Log.debug(getNestingLevel() + "Finished", "</" + oElement.nodeName + ">", sXMLPreprocessor); } } /** * Throws an error with the given message, prefixing it with the caller identification * (separated by a colon) and appending the serialization of the given XML DOM element. * Additionally logs the message and serialization as error with caller identification * as details. * * @param {string} sMessage * an error message which must end with a space (and take into account that the * serialized XML is appended) * @param {Element} oElement * the XML DOM element */ function error(sMessage, oElement) { sMessage = sMessage + serializeSingleElement(oElement); Log.error(sMessage, sCaller, sXMLPreprocessor); throw new Error(sCaller + ": " + sMessage); } /** * Determines the relevant children for the <template:if> element. * * @param {Element} oIfElement * the <template:if> XML DOM element * @returns {Element[]} * the XML DOM element children (a <then>, zero or more <elseif> and possibly an * <else>) or null if there is no <then> * @throws {Error} * if there is an unexpected child element */ function getIfChildren(oIfElement) { var oChild, aChildren = Array.prototype.filter.call(oIfElement.childNodes, isElementNode), i, n, bFoundElse = false; /* * Tells whether the given XML DOM node is an element node. * * @param {Node} oNode - an XML DOM node * @returns {boolean} whether the given node is an element node */ function isElementNode(oNode) { return oNode.nodeType === 1; } /* * Tells whether the given XML DOM element has the template namespace and the given * local name. * * @param {Element} oElement - an XML DOM element * @param {string} sLocalName - a local name * @returns {boolean} whether the given element has the given name */ function isTemplateElement(oElement, sLocalName) { return oElement.namespaceURI === sNAMESPACE && oElement.localName === sLocalName; } if (!aChildren.length || !isTemplateElement(aChildren[0], "then")) { return null; } for (i = 1, n = aChildren.length; i < n; i += 1) { oChild = aChildren[i]; if (bFoundElse) { error("Expected </" + oIfElement.prefix + ":if>, but instead saw ", oChild); } if (isTemplateElement(oChild, "else")) { bFoundElse = true; } else if (!isTemplateElement(oChild, "elseif")) { error("Expected <" + oIfElement.prefix + ":elseif> or <" + oIfElement.prefix + ":else>, but instead saw ", aChildren[i]); } } return aChildren; } /** * Returns the current nesting level as a string in square brackets with proper spacing. * * @returns {string} * "[<level>] " */ function getNestingLevel() { return (iNestingLevel < 10 ? "[ " : "[") + iNestingLevel + "] "; } /** * Returns a JavaScript object which is identified by a dot-separated sequence of names. * If the given compound name starts with a dot, it is interpreted relative to * <code>oScope</code>. * * @param {string} sName * a dot-separated sequence of names that identify the required object * @returns {object} * a JavaScript object which is identified by a sequence of names */ function getObject(sName) { // Note: ObjectPath.get("", ...) === undefined return sName && sName.charAt(0) === "." ? ObjectPath.get(sName.slice(1), oScope) : ObjectPath.get(sName || "", oScope) || ObjectPath.get(sName || ""); } /** * Interprets the given value as a binding and returns the resulting value; takes care * of unescaping and thus also of constant expressions. * * @param {string} sValue * an XML DOM attribute value * @param {Element} oElement * the XML DOM element * @param {sap.ui.core.util._with} oWithControl * the "with" control * @param {boolean} bMandatory * whether a binding is actually required (e.g. by a <code>template:if</code>) and not * optional (e.g. for {@link resolveAttributeBinding}); if so, the binding parser * unescapes the given value (which is a prerequisite for constant expressions) and * warnings are logged for functions not found * @param {function} [fnCallIfConstant] * optional function to be called in case the return value is obviously a constant, * not influenced by any binding * @returns {sap.ui.base.SyncPromise|null} * a sync promise which resolves with the property value or is rejected with a * corresponding error (for example, an error thrown by a formatter), or * <code>null</code> in case the binding is not ready (because it refers to a model * which is not available) * @throws {Error} * if a formatter returns a promise in sync mode */ function getResolvedBinding(sValue, oElement, oWithControl, bMandatory, fnCallIfConstant) { var vBindingInfo, oPromise; Measurement.average(sPerformanceGetResolvedBinding, "", aPerformanceCategories); try { vBindingInfo = BindingParser.complexParser(sValue, oScope, bMandatory, true, true, true) || sValue; // in case there is no binding and nothing to unescape } catch (e) { return SyncPromise.reject(e); } if (vBindingInfo.functionsNotFound) { if (bMandatory) { warn(oElement, 'Function name(s)', vBindingInfo.functionsNotFound.join(", "), 'not found'); } Measurement.end(sPerformanceGetResolvedBinding); return null; // treat incomplete bindings as unrelated } if (typeof vBindingInfo === "object") { oPromise = getAny(oWithControl, vBindingInfo, mSettings, oScope, !oViewInfo.sync); if (bMandatory && !oPromise) { warn(oElement, 'Binding not ready'); } else if (oViewInfo.sync && oPromise && oPromise.isPending()) { error("Async formatter in sync view in " + sValue + " of ", oElement); } } else { oPromise = SyncPromise.resolve(vBindingInfo); if (fnCallIfConstant) { // string fnCallIfConstant(); } } Measurement.end(sPerformanceGetResolvedBinding); return oPromise; } /** * Inserts the fragment with the given name in place of the given element. Loads the * fragment, takes care of caching (for the current pre-processor run) and visits the * fragment's content once it has been imported into the element's owner document and * put into place. Loading of fragments is asynchronous if the template view is * asynchronous. * * @param {string} sFragmentName * the fragment's resolved name * @param {Element} oElement * the XML DOM element, e.g. <sap.ui.core:Fragment> or <core:ExtensionPoint> * @param {sap.ui.core.util._with} oWithControl * the parent's "with" control * @returns {sap.ui.base.SyncPromise} * A sync promise which resolves with <code>undefined</code> as soon as the fragment * has been inserted, or is rejected with a corresponding error if loading or visiting * fails. * @throws {Error} * If a cycle is detected (same <code>sFragmentName</code> and * <code>oWithControl</code>) */ function insertFragment(sFragmentName, oElement, oWithControl) { var oFragmentPromise, fnLoad = oViewInfo.sync ? XMLTemplateProcessor.loadTemplate : XMLTemplateProcessor.loadTemplatePromise, sPreviousName = sCurrentName; // Note: It is perfectly valid to include the very same fragment again, as long as // the context is changed. So we check for cycles at the current "with" control. // A context change will create a new one. oWithControl.$mFragmentContexts = oWithControl.$mFragmentContexts || {}; if (oWithControl.$mFragmentContexts[sFragmentName]) { error("Cyclic reference to fragment '" + sFragmentName + "' ", oElement); } iNestingLevel++; debug(oElement, "fragmentName =", sFragmentName); oWithControl.$mFragmentContexts[sFragmentName] = true; sCurrentName = sFragmentName; Measurement.average(sPerformanceInsertFragment, "", aPerformanceCategories); // take fragment promise from cache, then import fragment oFragmentPromise = mFragmentCache[sFragmentName]; if (!oFragmentPromise) { mFragmentCache[sFragmentName] = oFragmentPromise = SyncPromise.resolve(fnLoad(sFragmentName, "fragment")); } return oFragmentPromise.then(function (oFragmentElement) { oFragmentElement = oElement.ownerDocument.importNode(oFragmentElement, true); Measurement.end(sPerformanceInsertFragment); return requireFor(oFragmentElement).then(function () { if (oFragmentElement.namespaceURI === "sap.ui.core" && oFragmentElement.localName === "FragmentDefinition") { return liftChildNodes(oFragmentElement, oWithControl, oElement); } oElement.parentNode.insertBefore(oFragmentElement, oElement); return visitNode(oFragmentElement, oWithControl); }); }).then(function () { oElement.parentNode.removeChild(oElement); sCurrentName = sPreviousName; oWithControl.$mFragmentContexts[sFragmentName] = false; debugFinished(oElement); iNestingLevel -= 1; }); } /** * Visits the child nodes of the given parent element. Lifts them up by inserting them * before the target element. * * @param {Element} oParent the XML DOM DOM element * @param {sap.ui.core.util._with} oWithControl the "with" control * @param {Element} [oTarget=oParent] the target DOM element * @returns {sap.ui.base.SyncPromise} * A sync promise which resolves with <code>undefined</code> as soon as visiting and * lifting is done, or is rejected with a corresponding error if visiting fails. */ function liftChildNodes(oParent, oWithControl, oTarget) { return visitChildNodes(oParent, oWithControl).then(function () { var oChild; oTarget = oTarget || oParent; while ((oChild = oParent.firstChild)) { oTarget.parentNode.insertBefore(oChild, oTarget); } }); } /** * Performs the test in the given element. * * @param {Element} oElement * the (<if> or <elseif>) XML DOM element * @param {sap.ui.core.util._with} oWithControl * the "with" control * @returns {sap.ui.base.SyncPromise} * A sync promise which resolves with the test result */ function performTest(oElement, oWithControl) { // constant test conditions are suspicious, but useful during development var fnCallIfConstant = warn.bind(null, oElement, 'Constant test condition'), oPromise = getResolvedBinding(oElement.getAttribute("test"), oElement, oWithControl, true, fnCallIfConstant) || SyncPromise.resolve(false); return oPromise.catch(function (ex) { warn(oElement, 'Error in formatter:', ex); // "test == undefined --> false" in debug log }).then(function (vTest) { var bResult = !!vTest && vTest !== "false"; if (bDebug) { if (typeof vTest === "string") { vTest = JSON.stringify(vTest); } else if (vTest === undefined) { vTest = "undefined"; } else if (Array.isArray(vTest)) { vTest = "[object Array]"; } debug(oElement, "test ==", vTest, "-->", bResult); } return bResult; }); } /** * Load required modules for the given element (a)synchronously, according to its * "template:require" attribute which may contain either a space separated list of * dot-separated module names or a JSON representation of a map from alias to * slash-separated Unified Resource Names (URNs). In the first case, the resulting * modules must be accessed from the global namespace. In the second case, they are * available as local names (AMD style) similar to <template:alias> instructions. * * @param {Element} oElement * any XML DOM element * @returns {sap.ui.base.SyncPromise} * A sync promise which resolves with <code>undefined</code> as soon as all required *