@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
664 lines (600 loc) • 21.7 kB
JavaScript
/*!
* 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);