UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

664 lines (600 loc) 21.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. */ // Provides static class sap.ui.base.BindingParser sap.ui.define([ './ExpressionParser', 'sap/ui/model/BindingMode', 'sap/ui/model/Filter', 'sap/ui/model/Sorter', "sap/base/Log", "sap/base/util/JSTokenizer", "sap/base/util/resolveReference" ], function( ExpressionParser, BindingMode, Filter, Sorter, Log, JSTokenizer, resolveReference ) { "use strict"; /** * @static * @namespace * @alias sap.ui.base.BindingParser */ var BindingParser = { _keepBindingStrings : false }; /** * Regular expression to check for a (new) object literal. */ var rObject = /^\{\s*('|"|)[a-zA-Z$_][a-zA-Z0-9$_]*\1\s*:/; /** * Regular expression to split the binding string into hard coded string fragments and embedded bindings. * * Also handles escaping of '{' and '}'. */ var rFragments = /(\\[\\\{\}])|(\{)/g; /** * Regular expression to escape potential binding chars */ var rBindingChars = /([\\\{\}])/g; /** * Creates a composite formatter which calls <code>fnRootFormatter</code> on the results of the * given formatters, which in turn are called on the original arguments. * * @param {function[]} aFormatters * list of leaf-level formatters * @param {function} [fnRootFormatter] * root level formatter; default: <code>Array.prototype.join(., " ")</code> * @return {function} * a composite formatter */ function composeFormatters(aFormatters, fnRootFormatter) { function formatter() { var i, n = aFormatters.length, aResults = new Array(n); for (i = 0; i < n; i += 1) { aResults[i] = aFormatters[i].apply(this, arguments); } if (fnRootFormatter) { return fnRootFormatter.apply(this, aResults); } // @see sap.ui.model.CompositeBinding#getExternalValue // "default: multiple values are joined together as space separated list if no // formatter or type specified" return n > 1 ? aResults.join(" ") : aResults[0]; } // @see sap.ui.base.ManagedObject#_bindProperty formatter.textFragments = fnRootFormatter && fnRootFormatter.textFragments || "sap.ui.base.BindingParser: composeFormatters"; return formatter; } /** * Helper to create a formatter function. Only used to reduce the closure size of the formatter * * @param {number[]|string[]} aFragments * array of fragments, either a literal text or the index of the binding's part * @returns {function} * a formatter function */ function makeFormatter(aFragments) { var fnFormatter = function() { var aResult = [], l = aFragments.length, i; for (i = 0; i < l; i++) { if ( typeof aFragments[i] === "number" ) { // a numerical fragment references the part with the same number aResult.push(arguments[aFragments[i]]); } else { // anything else is a string fragment aResult.push(aFragments[i]); } } return aResult.join(''); }; fnFormatter.textFragments = aFragments; return fnFormatter; } /** * Creates a binding info object with the given path. * * If the path contains a model specifier (prefix separated with a '>'), * the <code>model</code> property is set as well and the prefix is * removed from the path. * * @param {string} sPath * the given path * @returns {object} * a binding info object */ function makeSimpleBindingInfo(sPath) { var iPos = sPath.indexOf(">"), oBindingInfo = { path : sPath }; if ( iPos > 0 ) { oBindingInfo.model = sPath.slice(0,iPos); oBindingInfo.path = sPath.slice(iPos + 1); } return oBindingInfo; } /** * Delegates to <code>BindingParser.mergeParts</code>, but stifles any errors. * * @param {object} oBindingInfo * a binding info object * @param {string} [sBinding] * the original binding string as a detail for error logs */ function mergeParts(oBindingInfo, sBinding) { try { BindingParser.mergeParts(oBindingInfo); } catch (e) { Log.error("Cannot merge parts: " + e.message, sBinding, "sap.ui.base.BindingParser"); // rely on error in ManagedObject } } function resolveBindingInfo(oEnv, oBindingInfo) { var mVariables = Object.assign({".": oEnv.oContext}, oEnv.mLocals); /* * Resolves a function name to a function. * * Names can consist of multiple segments, separated by dots. * * If the name starts with a dot ('.'), lookup happens within the given context only; * otherwise it will first happen within the given context (only if * <code>bPreferContext</code> is set) and then use <code>mLocals</code> to resolve * the function and finally fall back to the global context (window). * * @param {object} o Object from which the property should be read and resolved * @param {string} sProp name of the property to resolve */ function resolveRef(o,sProp) { if ( typeof o[sProp] === "string" ) { var sName = o[sProp]; o[sProp] = resolveReference(o[sProp], mVariables, { preferDotContext: oEnv.bPreferContext, bindDotContext: !oEnv.bStaticContext }); if (typeof (o[sProp]) !== "function") { if (oEnv.bTolerateFunctionsNotFound) { oEnv.aFunctionsNotFound = oEnv.aFunctionsNotFound || []; oEnv.aFunctionsNotFound.push(sName); } else { Log.error(sProp + " function " + sName + " not found!"); } } } } /* * Resolves a data type name and configuration either to a type constructor or to a type instance. * * The name is resolved locally (against oEnv.oContext) if it starts with a '.', otherwise against * the oEnv.mLocals and if it's still not resolved, against the global context (window). * * The resolution is done inplace. If the name resolves to a function, it is assumed to be the * constructor of a data type. A new instance will be created, using the values of the * properties 'constraints' and 'formatOptions' as parameters of the constructor. * Both properties will be removed from <code>o</code>. * * @param {object} o Object from which a property should be read and resolved */ function resolveType(o) { var FNType; var sType = o.type; if (typeof sType === "string" ) { FNType = resolveReference(sType, mVariables, { bindContext: false }); // TODO find another solution for the type parameters? if (typeof FNType === "function") { o.type = new FNType(o.formatOptions, o.constraints); } else { o.type = FNType; } if (!o.type) { Log.error("Failed to resolve type '" + sType + "'. Maybe not loaded or a typo?"); } // TODO why are formatOptions and constraints also removed for an already instantiated type? // TODO why is a value of type object not validated (instanceof Type) delete o.formatOptions; delete o.constraints; } } /* * Resolves a map of event listeners, keyed by the event name. * * Each listener can be the name of a single function that will be resolved * in the given context (oEnv). */ function resolveEvents(oEvents) { if ( oEvents != null && typeof oEvents === 'object' ) { for ( var sName in oEvents ) { resolveRef(oEvents, sName); } } } /* * Converts filter definitions to sap.ui.model.Filter instances. * * The value of the given property can either be a single filter definition object * which will be fed into the constructor of sap.ui.model.Filter. * Or it can be an array of such objects. * * If any of the filter definition objects contains a property named 'filters', * that property will be resolved as filters recursively. * * A property 'test' will be resolved as function in the given context. */ function resolveFilters(o, sProp) { var v = o[sProp]; if ( Array.isArray(v) ) { v.forEach(function(oObject, iIndex) { resolveFilters(v, iIndex); }); return; } if ( v && typeof v === 'object' ) { resolveRef(v, 'test'); resolveFilters(v, 'filters'); resolveFilters(v, 'condition'); o[sProp] = new Filter(v); } } /* * Converts sorter definitions to sap.ui.model.Sorter instances. * * The value of the given property can either be a single sorter definition object * which then will be fed into the constructor of sap.ui.model.Sorter, or it can * be an array of such objects. * * Properties 'group' and 'comparator' in any of the sorter definitions * will be resolved as functions in the given context (oEnv). */ function resolveSorters(o, sProp) { var v = o[sProp]; if ( Array.isArray(v) ) { v.forEach(function(oObject, iIndex) { resolveSorters(v, iIndex); }); return; } if ( v && typeof v === 'object' ) { resolveRef(v, "group"); resolveRef(v, "comparator"); o[sProp] = new Sorter(v); } } if ( typeof oBindingInfo === 'object' ) { // Note: this resolves deeply nested bindings although CompositeBinding doesn't support them if ( Array.isArray(oBindingInfo.parts) ) { oBindingInfo.parts.forEach(function(oPart) { resolveBindingInfo(oEnv, oPart); }); } resolveType(oBindingInfo); resolveFilters(oBindingInfo,'filters'); resolveSorters(oBindingInfo,'sorter'); resolveEvents(oBindingInfo.events); resolveRef(oBindingInfo,'formatter'); resolveRef(oBindingInfo,'factory'); // list binding resolveRef(oBindingInfo,'groupHeaderFactory'); // list binding } return oBindingInfo; } /** * Determines the binding info for the given string sInput starting at the given iStart and * returns an object with the corresponding binding info as <code>result</code> and the * position where to continue parsing as <code>at</code> property. * * @param {object} oEnv * the "environment" * @param {object} oEnv.oContext * the context object from complexBinding (read-only) * @param {boolean} oEnv.bTolerateFunctionsNotFound * if <code>true</code>, unknown functions are gathered in aFunctionsNotFound, otherwise an * error is logged (read-only) * @param {string[]} oEnv.aFunctionsNotFound * a list of functions that could not be found if oEnv.bTolerateFunctionsNotFound is true * (append only) * @param {string} sInput * The input string from which to resolve an embedded binding * @param {int} iStart * The start index for binding resolution in the input string * @returns {object} * An object with the following properties: * result: The binding info for the embedded binding * at: The position after the last character for the embedded binding in the input string */ function resolveEmbeddedBinding(oEnv, sInput, iStart) { var parseObject = JSTokenizer.parseJS, oParseResult, iEnd; // an embedded binding: check for a property name that would indicate a complex object if ( rObject.test(sInput.slice(iStart)) ) { oParseResult = parseObject(sInput, iStart); resolveBindingInfo(oEnv, oParseResult.result); return oParseResult; } // otherwise it must be a simple binding (path only) iEnd = sInput.indexOf('}', iStart); if ( iEnd < iStart ) { throw new SyntaxError("no closing braces found in '" + sInput + "' after pos:" + iStart); } return { result: makeSimpleBindingInfo(sInput.slice(iStart + 1, iEnd)), at: iEnd + 1 }; } BindingParser.simpleParser = function(sString, oContext) { if ( sString.startsWith("{") && sString.endsWith("}") ) { return makeSimpleBindingInfo(sString.slice(1, -1)); } }; BindingParser.simpleParser.escape = function(sValue) { // there was no escaping defined for the simple parser return sValue; }; /* * @param {boolean} [bTolerateFunctionsNotFound=false] * if true, function names which cannot be resolved to a reference are reported via the * string array <code>functionsNotFound</code> of the result object; else they are logged * as errors * @param {boolean} [bStaticContext=false] * If true, relative function names found via <code>oContext</code> will not be treated as * instance methods of the context, but as static methods. * @param {boolean} [bPreferContext=false] * if true, names without an initial dot are searched in the given context first and then * globally * @param {object} [mLocals] * variables allowed in the expression as map of variable name to its value */ BindingParser.complexParser = function(sString, oContext, bUnescape, bTolerateFunctionsNotFound, bStaticContext, bPreferContext, mLocals) { var b2ndLevelMergedNeeded = false, // whether some 2nd level parts again have parts oBindingInfo = {parts:[]}, bMergeNeeded = false, // whether some top-level parts again have parts oEnv = { oContext: oContext, mLocals: mLocals, aFunctionsNotFound: undefined, // lazy creation bPreferContext : bPreferContext, bStaticContext: bStaticContext, bTolerateFunctionsNotFound: bTolerateFunctionsNotFound }, aFragments = [], bUnescaped, p = 0, m, oEmbeddedBinding; /** * Parses an expression. Sets the flags accordingly. * * @param {string} sInput The input string to parse from * @param {int} iStart The start index * @param {sap.ui.model.BindingMode} oBindingMode the binding mode * @returns {object} a result object with the binding in <code>result</code> and the index * after the last character belonging to the expression in <code>at</code> * @throws SyntaxError if the expression string is invalid */ function expression(sInput, iStart, oBindingMode) { var oBinding = ExpressionParser.parse(resolveEmbeddedBinding.bind(null, oEnv), sString, iStart, null, mLocals || (bStaticContext ? oContext : null)); /** * Recursively sets the mode <code>oBindingMode</code> on the given binding (or its * parts). * * @param {object} oBinding * a binding which may be composite * @param {int} [iIndex] * index provided by <code>forEach</code> */ function setMode(oBinding, iIndex) { if (oBinding.parts) { oBinding.parts.forEach(function (vPart, i) { if (typeof vPart === "string") { vPart = oBinding.parts[i] = {path : vPart}; } setMode(vPart, i); }); b2ndLevelMergedNeeded = b2ndLevelMergedNeeded || iIndex !== undefined; } else { oBinding.mode = oBindingMode; } } if (sInput.charAt(oBinding.at) !== "}") { throw new SyntaxError("Expected '}' and instead saw '" + sInput.charAt(oBinding.at) + "' in expression binding " + sInput + " at position " + oBinding.at); } oBinding.at += 1; if (oBinding.result) { setMode(oBinding.result); } else { aFragments[aFragments.length - 1] = String(oBinding.constant); bUnescaped = true; } return oBinding; } rFragments.lastIndex = 0; //previous parse call may have thrown an Error: reset lastIndex while ( (m = rFragments.exec(sString)) !== null ) { // check for a skipped literal string fragment if ( p < m.index ) { aFragments.push(sString.slice(p, m.index)); } // handle the different kinds of matches if ( m[1] ) { // an escaped opening bracket, closing bracket or backslash aFragments.push(m[1].slice(1)); bUnescaped = true; } else { aFragments.push(oBindingInfo.parts.length); if (sString.indexOf(":=", m.index) === m.index + 1) { oEmbeddedBinding = expression(sString, m.index + 3, BindingMode.OneTime); } else if (sString.charAt(m.index + 1) === "=") { //expression oEmbeddedBinding = expression(sString, m.index + 2, BindingMode.OneWay); } else { oEmbeddedBinding = resolveEmbeddedBinding(oEnv, sString, m.index); } if (oEmbeddedBinding.result) { oBindingInfo.parts.push(oEmbeddedBinding.result); bMergeNeeded = bMergeNeeded || "parts" in oEmbeddedBinding.result; } rFragments.lastIndex = oEmbeddedBinding.at; } // remember where we are p = rFragments.lastIndex; } // check for a trailing literal string fragment if ( p < sString.length ) { aFragments.push(sString.slice(p)); } // only if a part has been found we can return a binding info if (oBindingInfo.parts.length > 0) { // Note: aFragments.length >= 1 if ( aFragments.length === 1 /* implies: && typeof aFragments[0] === "number" */ ) { // special case: a single binding only oBindingInfo = oBindingInfo.parts[0]; bMergeNeeded = b2ndLevelMergedNeeded; } else { // create the formatter function from the fragments oBindingInfo.formatter = makeFormatter(aFragments); } if (bMergeNeeded) { mergeParts(oBindingInfo, sString); } if (BindingParser._keepBindingStrings) { oBindingInfo.bindingString = sString; } if (oEnv.aFunctionsNotFound) { oBindingInfo.functionsNotFound = oEnv.aFunctionsNotFound; } return oBindingInfo; } else if ( bUnescape && bUnescaped ) { return aFragments.join(''); } }; BindingParser.complexParser.escape = function(sValue) { return sValue.replace(rBindingChars, "\\$1"); }; /** * Merges the given binding info object's parts, which may have parts themselves, into a flat * list of parts, taking care of existing formatter functions. If the given binding info does * not have a root formatter, <code>Array.prototype.join(., " ")</code> is used instead. * Parts which are not binding info objects are also supported; they are removed from the * "parts" array and taken care of by the new root-level formatter function, which feeds them * into the old formatter function at the right place. * * Note: Truly hierarchical composite bindings are not yet supported. This method deals with a * special case of a two-level hierarchy which can be turned into a one-level hierarchy. The * precondition is that the parts which have parts themselves are not too complex, i.e. must * have no other properties than "formatter" and "parts". A missing formatter on that level * is replaced with the default <code>Array.prototype.join(., " ")</code>. * * @param {object} oBindingInfo * a binding info object with a possibly empty array of parts and a new formatter function * @throws {Error} * in case precondition is not met * @private */ BindingParser.mergeParts = function (oBindingInfo) { var aFormatters = [], aParts = []; oBindingInfo.parts.forEach(function (vEmbeddedBinding) { var iEnd, fnFormatter = function () { return vEmbeddedBinding; // just return constant value }, sName, iStart = aParts.length; /* * Selects the overall argument corresponding to the current part. * * @returns {any} * the argument at index <code>iStart</code> */ function select() { return arguments[iStart]; } // @see sap.ui.base.ManagedObject#extractBindingInfo if (vEmbeddedBinding && typeof vEmbeddedBinding === "object") { if (vEmbeddedBinding.parts) { for (sName in vEmbeddedBinding) { if (sName !== "formatter" && sName !== "parts") { throw new Error("Unsupported property: " + sName); } } aParts = aParts.concat(vEmbeddedBinding.parts); iEnd = aParts.length; if (vEmbeddedBinding.formatter) { fnFormatter = function () { // old formatter needs to operate on its own slice of overall arguments return vEmbeddedBinding.formatter.apply(this, Array.prototype.slice.call(arguments, iStart, iEnd)); }; } else if (iEnd - iStart > 1) { fnFormatter = function () { // @see sap.ui.model.CompositeBinding#getExternalValue // "default: multiple values are joined together as space separated // list if no formatter or type specified" return Array.prototype.slice.call(arguments, iStart, iEnd).join(" "); }; } else { fnFormatter = select; } } else if ("path" in vEmbeddedBinding) { aParts.push(vEmbeddedBinding); fnFormatter = select; } } aFormatters.push(fnFormatter); }); oBindingInfo.parts = aParts; oBindingInfo.formatter = composeFormatters(aFormatters, oBindingInfo.formatter); }; /** * Parses a string <code>sInput</code> with an expression. The input string is parsed starting * at the index <code>iStart</code> and the return value contains the index after the last * character belonging to the expression. * * @param {string} sInput * the string to be parsed * @param {int} iStart * the index to start parsing * @param {object} [oEnv] * the "environment" (see resolveEmbeddedBinding function for details) * @param {object} [mLocals] * variables allowed in the expression as map of variable name to value * @returns {object} * the parse result with the following properties * <ul> * <li><code>result</code>: the binding info as an object with the properties * <code>formatter</code> (the formatter function to evaluate the expression) and * <code>parts</code> (an array of the referenced bindings)</li> * <li><code>at</code>: the index of the first character after the expression in * <code>sInput</code></li> * </ul> * @throws SyntaxError * If the expression string is invalid or unsupported. The at property of * the error contains the position where parsing failed. * @private */ BindingParser.parseExpression = function (sInput, iStart, oEnv, mLocals) { oEnv = oEnv || {}; if (mLocals) { oEnv.mLocals = mLocals; } return ExpressionParser.parse(resolveEmbeddedBinding.bind(null, oEnv), sInput, iStart, mLocals); }; return BindingParser; }, /* bExport= */ true);