@qooxdoo/framework
Version:
The JS Framework for Coders
1,459 lines (1,337 loc) • 58.3 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2004-2008 1&1 Internet AG, Germany, http://www.1und1.de
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Martin Wittemann (martinwittemann)
************************************************************************ */
/**
* Single-value binding is a core component of the data binding package.
*/
qx.Class.define("qx.data.SingleValueBinding", {
statics: {
/** internal reference for all bindings indexed by source object */
__bindings: {},
/** internal reference for all bindings indexed by target object */
__bindingsByTarget: {},
/**
* The function is responsible for binding a source objects property to
* a target objects property. Both properties have to have the usual qooxdoo
* getter and setter. The source property also needs to fire change-events
* on every change of its value.
* Please keep in mind, that this binding is unidirectional. If you need
* a binding in both directions, you have to use two of this bindings.
*
* It's also possible to bind some kind of a hierarchy as a source. This
* means that you can separate the source properties with a dot and bind
* by that the object referenced to this property chain.
* Example with an object 'a' which has object 'b' stored in its 'child'
* property. Object b has a string property named abc:
* <pre><code>
* qx.data.SingleValueBinding.bind(a, "child.abc", textfield, "value");
* </code></pre>
* In that case, if the property abc of b changes, the textfield will
* automatically contain the new value. Also if the child of a changes, the
* new value (abc of the new child) will be in the textfield.
*
* There is also a possibility of binding an array. Therefore the array
* {@link qx.data.IListData} is needed because this array has change events
* which the native does not. Imagine a qooxdoo object a which has a
* children property containing an array holding more of its own kind.
* Every object has a name property as a string.
* <pre>
* var svb = qx.data.SingleValueBinding;
* // bind the first child's name of 'a' to a textfield
* svb.bind(a, "children[0].name", textfield, "value");
* // bind the last child's name of 'a' to a textfield
* svb.bind(a, "children[last].name", textfield2, "value");
* // also deeper bindings are possible
* svb.bind(a, "children[0].children[0].name", textfield3, "value");
* </pre>
*
* As you can see in this example, the abc property of a's b will be bound
* to the textfield. If now the value of b changed or even the a will get a
* new b, the binding still shows the right value.
*
* @param sourceObject {qx.core.Object} The source of the binding.
* @param sourcePropertyChain {String} The property chain which represents
* the source property.
* @param targetObject {qx.core.Object} The object which the source should
* be bind to.
* @param targetPropertyChain {String} The property chain to the target
* object.
* @param options {Map?null} A map containing the options.
* <li>converter: A converter function which takes four parameters
* and should return the converted value.
* <ol>
* <li>The data to convert</li>
* <li>The corresponding model object, which is only set in case of the use of an controller.</li>
* <li>The source object for the binding</li>
* <li>The target object.</li>
* </ol>
* If no conversion has been done, the given value should be returned.
* e.g. a number to boolean converter
* <code>function(data, model, source, target) {return data > 100;}</code>
* </li>
* <li>onUpdate: A callback function can be given here. This method will be
* called if the binding was updated successful. There will be
* three parameter you do get in that method call.
* <ol>
* <li>The source object</li>
* <li>The target object</li>
* <li>The data</li>
* </ol>
* Here is a sample: <code>onUpdate : function(source, target, data) {...}</code>
* </li>
* <li>onSetFail: A callback function can be given here. This method will
* be called if the set of the value fails.
* </li>
* <li>ignoreConverter: A string which will be matched using the current
* property chain. If it matches, the converter will not be called.
* </li>
*
* @return {var} Returns the internal id for that binding. This can be used
* for referencing the binding or e.g. for removing. This is not an atomic
* id so you can't you use it as a hash-map index.
*
* @throws {qx.core.AssertionError} If the event is no data event or
* there is no property definition for object and property (source and
* target).
*/
bind(
sourceObject,
sourcePropertyChain,
targetObject,
targetPropertyChain,
options
) {
// check for the arguments
if (qx.core.Environment.get("qx.debug")) {
qx.core.Assert.assertObject(sourceObject, "sourceObject");
qx.core.Assert.assertString(sourcePropertyChain, "sourcePropertyChain");
qx.core.Assert.assertObject(targetObject, "targetObject");
qx.core.Assert.assertString(targetPropertyChain, "targetPropertyChain");
}
// set up the target binding
var targetListenerMap = this.__setUpTargetBinding(
sourceObject,
sourcePropertyChain,
targetObject,
targetPropertyChain,
options
);
// get the property names
var propertyNames = sourcePropertyChain.split(".");
// stuff that's needed to store for the listener function
var arrayIndexValues = this.__checkForArrayInPropertyChain(propertyNames);
var sources = [];
var listeners = [];
var listenerIds = [];
var eventNames = [];
var source = sourceObject;
var initialPromise = null;
// add a try catch to make it possible to remove the listeners of the
// chain in case the loop breaks after some listeners already added.
try {
// go through all property names
for (var i = 0; i < propertyNames.length; i++) {
var propertyName = propertyNames[i];
// check for the array
if (arrayIndexValues[i] !== "") {
// push the array change event
eventNames.push("change");
} else {
var eventName = this.__getEventNameForProperty(
source,
propertyName
);
if (!eventName) {
if (i == 0) {
// the root property can not change --> error
throw new qx.core.AssertionError(
"Binding property " +
propertyName +
" of object " +
source +
" not possible: No event available. Full property chain: " +
sourcePropertyChain
);
}
if (
source instanceof qx.core.Object &&
qx.Class.hasProperty(source.constructor, propertyName)
) {
qx.log.Logger.warn(
"Binding property " +
propertyName +
" of object " +
source +
" not possible: No event available. Full property chain: " +
sourcePropertyChain
);
}
// call the converter if no event could be found on binding creation
initialPromise = this.__setInitialValue(
undefined,
targetObject,
targetPropertyChain,
options,
sourceObject
);
break;
}
eventNames.push(eventName);
}
// save the current source
sources[i] = source;
// check for the last property
if (i == propertyNames.length - 1) {
// if it is an array, set the initial value and bind the event
if (arrayIndexValues[i] !== "") {
// get the current value
var itemIndex =
arrayIndexValues[i] === "last"
? source.length - 1
: arrayIndexValues[i];
var currentValue = source.getItem(itemIndex);
// set the initial value
initialPromise = this.__setInitialValue(
currentValue,
targetObject,
targetPropertyChain,
options,
sourceObject
);
// bind the event
listenerIds[i] = this.__bindEventToProperty(
source,
eventNames[i],
targetObject,
targetPropertyChain,
options,
arrayIndexValues[i]
);
} else {
// try to set the initial value
if (
propertyNames[i] != null &&
source["get" + qx.lang.String.firstUp(propertyNames[i])] != null
) {
var currentValue =
source["get" + qx.lang.String.firstUp(propertyNames[i])]();
initialPromise = this.__setInitialValue(
currentValue,
targetObject,
targetPropertyChain,
options,
sourceObject
);
}
// bind the property
listenerIds[i] = this.__bindEventToProperty(
source,
eventNames[i],
targetObject,
targetPropertyChain,
options
);
}
// if its not the last property
} else {
// create the context for the listener
var context = {
index: i,
propertyNames: propertyNames,
sources: sources,
listenerIds: listenerIds,
arrayIndexValues: arrayIndexValues,
targetObject: targetObject,
targetPropertyChain: targetPropertyChain,
options: options,
listeners: listeners
};
// create a listener
var listener = qx.lang.Function.bind(
this.__chainListener,
this,
context
);
// store the listener for further processing
listeners.push(listener);
// add the chaining listener
listenerIds[i] = source.addListener(eventNames[i], listener);
}
// get and store the next source
if (
source["get" + qx.lang.String.firstUp(propertyNames[i])] == null
) {
source = undefined;
} else if (arrayIndexValues[i] !== "") {
var itemIndex =
arrayIndexValues[i] === "last"
? source.length - 1
: arrayIndexValues[i];
source =
source["get" + qx.lang.String.firstUp(propertyNames[i])](
itemIndex
);
} else {
source = source["get" + qx.lang.String.firstUp(propertyNames[i])]();
// the value should be undefined if we can not find the last part of the property chain
if (source === null && propertyNames.length - 1 != i) {
source = undefined;
}
}
if (!source) {
// call the converter if no source could be found on binding creation
this.__setInitialValue(
source,
targetObject,
targetPropertyChain,
options,
sourceObject
);
break;
}
}
} catch (ex) {
// remove the already added listener
// go through all added listeners (source)
for (var i = 0; i < sources.length; i++) {
// check if a source is available
if (sources[i] && listenerIds[i]) {
sources[i].removeListenerById(listenerIds[i]);
}
}
var targets = targetListenerMap.targets;
var targetIds = targetListenerMap.listenerIds;
// go through all added listeners (target)
for (var i = 0; i < targets.length; i++) {
// check if a target is available
if (targets[i] && targetIds[i]) {
targets[i].removeListenerById(targetIds[i]);
}
}
throw ex;
}
// create the id map
var id = {
type: "deepBinding",
listenerIds: listenerIds,
sources: sources,
targetListenerIds: targetListenerMap.listenerIds,
targets: targetListenerMap.targets,
initialPromise: initialPromise
};
// store the bindings
this.__storeBinding(
id,
sourceObject,
sourcePropertyChain,
targetObject,
targetPropertyChain
);
return id;
},
/**
* Event listener for the chaining of the properties.
*
* @param context {Map} The current context for the listener.
*/
__chainListener(context) {
// invoke the onUpdate method
if (context.options && context.options.onUpdate) {
context.options.onUpdate(
context.sources[context.index],
context.targetObject
);
}
// delete all listener after the current one
for (var j = context.index + 1; j < context.propertyNames.length; j++) {
// remove the old sources
var source = context.sources[j];
context.sources[j] = null;
if (!source) {
continue;
}
// remove the listeners
source.removeListenerById(context.listenerIds[j]);
}
// get the current source
var source = context.sources[context.index];
// add new once after the current one
for (var j = context.index + 1; j < context.propertyNames.length; j++) {
// get and store the new source
if (context.arrayIndexValues[j - 1] !== "") {
source = source[
"get" + qx.lang.String.firstUp(context.propertyNames[j - 1])
](context.arrayIndexValues[j - 1]);
} else {
source =
source[
"get" + qx.lang.String.firstUp(context.propertyNames[j - 1])
]();
}
context.sources[j] = source;
// reset the target object if no new source could be found
if (!source) {
// use the converter if the property chain breaks [BUG# 6880]
if (context.options && context.options.converter) {
var ignoreConverter = false;
// take care of the ignore pattern used for the controller
if (context.options.ignoreConverter) {
// the current property chain as string
var currentSourceChain = context.propertyNames
.slice(0, j)
.join(".");
// match for the current pattern given in the options
var match = currentSourceChain.match(
new RegExp("^" + context.options.ignoreConverter)
);
ignoreConverter = match ? match.length > 0 : false;
}
if (!ignoreConverter) {
this.__setTargetValue(
context.targetObject,
context.targetPropertyChain,
context.options.converter()
);
} else {
this.__resetTargetValue(
context.targetObject,
context.targetPropertyChain
);
}
} else {
this.__resetTargetValue(
context.targetObject,
context.targetPropertyChain
);
}
break;
}
// if its the last property
if (j == context.propertyNames.length - 1) {
// if its an array
if (
qx.Class.implementsInterface(source, qx.data.IListData) &&
context.arrayIndexValues[j] !== ""
) {
// set the initial value
var itemIndex =
context.arrayIndexValues[j] === "last"
? source.length - 1
: context.arrayIndexValues[j];
var currentValue = source.getItem(itemIndex);
this.__setInitialValue(
currentValue,
context.targetObject,
context.targetPropertyChain,
context.options,
context.sources[context.index]
);
// bind the item event to the new target
context.listenerIds[j] = this.__bindEventToProperty(
source,
"change",
context.targetObject,
context.targetPropertyChain,
context.options,
context.arrayIndexValues[j]
);
} else {
if (
context.propertyNames[j] != null &&
source[
"get" + qx.lang.String.firstUp(context.propertyNames[j])
] != null
) {
var currentValue =
source[
"get" + qx.lang.String.firstUp(context.propertyNames[j])
]();
this.__setInitialValue(
currentValue,
context.targetObject,
context.targetPropertyChain,
context.options,
context.sources[context.index]
);
}
var eventName = this.__getEventNameForProperty(
source,
context.propertyNames[j]
);
if (!eventName) {
context.sources[j] = null;
this.__resetTargetValue(
context.targetObject,
context.targetPropertyChain
);
return;
}
// bind the last property to the new target
context.listenerIds[j] = this.__bindEventToProperty(
source,
eventName,
context.targetObject,
context.targetPropertyChain,
context.options
);
}
} else {
// check if a listener already created
if (context.listeners[j] == null) {
var listener = qx.lang.Function.bind(
this.__chainListener,
this,
context
);
// store the listener for further processing
context.listeners.push(listener);
}
// add a new listener
if (qx.Class.implementsInterface(source, qx.data.IListData)) {
var eventName = "change";
} else {
var eventName = this.__getEventNameForProperty(
source,
context.propertyNames[j]
);
}
if (!eventName) {
context.sources[j] = null;
this.__resetTargetValue(
context.targetObject,
context.targetPropertyChain
);
return;
}
context.listenerIds[j] = source.addListener(
eventName,
context.listeners[j]
);
}
}
},
/**
* Internal helper for setting up the listening to the changes on the
* target side of the binding. Only works if the target property is a
* property chain
*
* @param sourceObject {qx.core.Object} The source of the binding.
* @param sourcePropertyChain {String} The property chain which represents
* the source property.
* @param targetObject {qx.core.Object} The object which the source should
* be bind to.
* @param targetPropertyChain {String} The property name of the target
* object.
* @param options {Map} The options map perhaps containing the user defined
* converter.
* @return {var} A map containing the listener ids and the targets.
*/
__setUpTargetBinding(
sourceObject,
sourcePropertyChain,
targetObject,
targetPropertyChain,
options
) {
// get the property names
var propertyNames = targetPropertyChain.split(".");
var arrayIndexValues = this.__checkForArrayInPropertyChain(propertyNames);
var targets = [];
var listeners = [];
var listenerIds = [];
var eventNames = [];
var target = targetObject;
// go through all property names
for (var i = 0; i < propertyNames.length - 1; i++) {
// check for the array
if (arrayIndexValues[i] !== "") {
// push the array change event
eventNames.push("change");
} else {
var eventName = this.__getEventNameForProperty(
target,
propertyNames[i]
);
if (!eventName) {
// if the event names could not be terminated,
// just ignore the target chain listening
break;
}
eventNames.push(eventName);
}
// save the current source
targets[i] = target;
// create a listener
var listener = function () {
// delete all listener after the current one
for (var j = i + 1; j < propertyNames.length - 1; j++) {
// remove the old sources
var target = targets[j];
targets[j] = null;
if (!target) {
continue;
}
// remove the listeners
target.removeListenerById(listenerIds[j]);
}
// get the current target
var target = targets[i];
// add new once after the current one
for (var j = i + 1; j < propertyNames.length - 1; j++) {
var firstUpPropName = qx.lang.String.firstUp(propertyNames[j - 1]);
// get and store the new target
if (arrayIndexValues[j - 1] !== "") {
var currentIndex =
arrayIndexValues[j - 1] === "last"
? target.getLength() - 1
: arrayIndexValues[j - 1];
target = target["get" + firstUpPropName](currentIndex);
} else {
target = target["get" + firstUpPropName]();
}
targets[j] = target;
if (!target) {
break;
}
// check if a listener already created
if (listeners[j] == null) {
// store the listener for further processing
listeners.push(listener);
}
// add a new listener
if (qx.Class.implementsInterface(target, qx.data.IListData)) {
var eventName = "change";
} else {
var eventName =
qx.data.SingleValueBinding.__getEventNameForProperty(
target,
propertyNames[j]
);
if (!eventName) {
// if the event name could not be terminated,
// ignore the rest
break;
}
}
listenerIds[j] = target.addListener(eventName, listeners[j]);
}
qx.data.SingleValueBinding.updateTarget(
sourceObject,
sourcePropertyChain,
targetObject,
targetPropertyChain,
options
);
};
// store the listener for further processing
listeners.push(listener);
// add the chaining listener
listenerIds[i] = target.addListener(eventNames[i], listener);
var firstUpPropName = qx.lang.String.firstUp(propertyNames[i]);
// get and store the next target
if (target["get" + firstUpPropName] == null) {
target = null;
} else if (arrayIndexValues[i] !== "") {
target = target["get" + firstUpPropName](arrayIndexValues[i]);
} else {
target = target["get" + firstUpPropName]();
}
if (!target) {
break;
}
}
return { listenerIds: listenerIds, targets: targets };
},
/**
* Helper for updating the target. Gets the current set data from the source
* and set that on the target.
*
* @param sourceObject {qx.core.Object} The source of the binding.
* @param sourcePropertyChain {String} The property chain which represents
* the source property.
* @param targetObject {qx.core.Object} The object which the source should
* be bind to.
* @param targetPropertyChain {String} The property name of the target
* object.
* @param options {Map} The options map perhaps containing the user defined
* converter.
*
* @internal
*/
updateTarget(
sourceObject,
sourcePropertyChain,
targetObject,
targetPropertyChain,
options
) {
var value = this.resolvePropertyChain(sourceObject, sourcePropertyChain);
// convert the data before setting
value = qx.data.SingleValueBinding.__convertValue(
value,
targetObject,
targetPropertyChain,
options,
sourceObject
);
this.__setTargetValue(targetObject, targetPropertyChain, value);
},
/**
* Internal helper for getting the current set value at the property chain.
*
* @param o {qx.core.Object} The source of the binding.
* @param propertyChain {String} The property chain which represents
* the source property.
* @return {var?undefined} Returns the set value if defined.
*/
resolvePropertyChain(o, propertyChain) {
var properties = this.__getPropertyChainArray(propertyChain);
return this.__getTargetFromChain(o, properties, properties.length);
},
/**
* Tries to return a fitting event name to the given source object and
* property name. First, it assumes that the property name is a real property
* and therefore it checks the property definition for the event. The second
* possibility is to check if there is an event with the given name. The
* third and last possibility checked is if there is an event which is named
* change + propertyName. If this three possibilities fail, an error will be
* thrown.
*
* @param source {qx.core.Object} The source where the property is stored.
* @param propertyName {String} The name of the property.
* @return {String|null} The name of the corresponding event or null.
*/
__getEventNameForProperty(source, propertyName) {
// get the current event name from the property definition
var eventName = this.__getEventForProperty(source, propertyName);
// if no event name could be found
if (eventName == null) {
// check if the propertyName is the event name
if (qx.Class.supportsEvent(source.constructor, propertyName)) {
eventName = propertyName;
// check if the change + propertyName is the event name
} else if (
qx.Class.supportsEvent(
source.constructor,
"change" + qx.lang.String.firstUp(propertyName)
)
) {
eventName = "change" + qx.lang.String.firstUp(propertyName);
} else {
return null;
}
}
return eventName;
},
/**
* Resets the value of the given target after resolving the target property
* chain.
*
* @param targetObject {qx.core.Object} The object where the property chain
* starts.
* @param targetPropertyChain {String} The names of the properties,
* separated with a dot.
*/
__resetTargetValue(targetObject, targetPropertyChain) {
// get the last target object of the chain
var properties = this.__getPropertyChainArray(targetPropertyChain);
var target = this.__getTargetFromChain(targetObject, properties);
if (target != null) {
// get the name of the last property
var lastProperty = properties[properties.length - 1];
// check for an array and set the value to null
var index = this.__getArrayIndex(lastProperty);
if (index) {
this.__setTargetValue(targetObject, targetPropertyChain, null);
return;
}
// try to reset the property
if (
target["reset" + qx.lang.String.firstUp(lastProperty)] != undefined
) {
target["reset" + qx.lang.String.firstUp(lastProperty)]();
} else {
// fallback if no resetter is given (see bug #2456)
if (
typeof target["set" + qx.lang.String.firstUp(lastProperty)] !=
"function"
) {
throw new qx.core.AssertionError(
"No setter for '" + lastProperty + "' on target " + target + "."
);
}
target["set" + qx.lang.String.firstUp(lastProperty)](null);
}
}
},
/**
* Sets the given value to the given target after resolving the
* target property chain.
*
* @param targetObject {qx.core.Object} The object where the property chain
* starts.
* @param targetPropertyChain {String} The names of the properties,
* separated with a dot.
* @param value {var} The value to set.
*/
__setTargetValue(targetObject, targetPropertyChain, value) {
// get the last target object of the chain
var properties = this.__getPropertyChainArray(targetPropertyChain);
var target = this.__getTargetFromChain(targetObject, properties);
if (target) {
// get the name of the last property
var lastProperty = properties[properties.length - 1];
// check for array notation
var index = this.__getArrayIndex(lastProperty);
if (index) {
if (index === "last") {
// check for the 'last' notation
index = target.length - 1;
}
target.setItem(index, value);
} else {
if (
typeof target["set" + qx.lang.String.firstUp(lastProperty)] !=
"function"
) {
throw new qx.core.AssertionError(
"No setter for '" + lastProperty + "' on target " + target + "."
);
}
return target["set" + qx.lang.String.firstUp(lastProperty)](value);
}
}
},
/**
* Returns the index from a property using bracket notation, e.g.
* "[42]" returns "42", "[last]" returns "last"
*
* @param propertyName {String} A property name
* @return {String|null} Array index or null if the property name does
* not use bracket notation
*/
__getArrayIndex(propertyName) {
var arrayExp = /^\[(\d+|last)\]$/;
var arrayMatch = propertyName.match(arrayExp);
if (arrayMatch) {
return arrayMatch[1];
}
return null;
},
/**
* Converts a property chain string into a list of properties and/or
* array indexes
* @param targetPropertyChain {String} property chain
* @return {String[]} Array of property names
*/
__getPropertyChainArray(targetPropertyChain) {
// split properties (dot notation) and array indexes (bracket notation)
return targetPropertyChain
.replace(/\[/g, ".[")
.split(".")
.filter(function (prop) {
return prop !== "";
});
},
/**
* Helper-Function resolving the object on which the last property of the
* chain should be set.
*
* @param targetObject {qx.core.Object} The object where the property chain
* starts.
* @param targetProperties {String[]} Array containing the names of the properties
* @param index {Number?} The array index of the last property to be considered.
* Default: The last item's index
* @return {qx.core.Object|null} The object on which the last property
* should be set.
*/
__getTargetFromChain(targetObject, targetProperties, index) {
index = index || targetProperties.length - 1;
var target = targetObject;
for (var i = 0; target !== null && i < index; i++) {
try {
var property = targetProperties[i];
// array notation
var arrIndex = this.__getArrayIndex(property);
if (arrIndex) {
if (arrIndex === "last") {
// check for the 'last' notation
arrIndex = target.length - 1;
}
target = target.getItem(arrIndex);
} else {
target = target["get" + qx.lang.String.firstUp(property)]();
}
} catch (ex) {
return null;
}
}
return target;
},
/**
* Set the given value to the target property. This method is used for
* initially set the value.
*
* @param value {var} The value to set.
* @param targetObject {qx.core.Object} The object which contains the target
* property.
* @param targetPropertyChain {String} The name of the target property in the
* target object.
* @param options {Map} The options map perhaps containing the user defined
* converter.
* @param sourceObject {qx.core.Object} The source object of the binding (
* used for the onUpdate callback).
*/
__setInitialValue(
value,
targetObject,
targetPropertyChain,
options,
sourceObject
) {
// first convert the initial value
value = this.__convertValue(
value,
targetObject,
targetPropertyChain,
options,
sourceObject
);
// check if the converted value is undefined
if (value === undefined) {
this.__resetTargetValue(targetObject, targetPropertyChain);
}
// only set the initial value if one is given (may be null)
if (value !== undefined) {
try {
var result = this.__setTargetValue(
targetObject,
targetPropertyChain,
value
);
// tell the user that the setter was invoked probably
if (options && options.onUpdate) {
options.onUpdate(sourceObject, targetObject, value);
}
return result;
} catch (e) {
if (!(e instanceof qx.core.ValidationError)) {
throw e;
}
if (options && options.onSetFail) {
options.onSetFail(e);
} else {
qx.log.Logger.warn(
"Failed so set value " +
value +
" on " +
targetObject +
". Error message: " +
e
);
}
}
}
},
/**
* Checks for an array element in the given property names and adapts the
* arrays to fit the algorithm.
*
* @param propertyNames {Array} The array containing the property names.
* Attention, this method can change this parameter!!!
* @return {Array} An array containing the values of the array properties
* corresponding to the property names.
*/
__checkForArrayInPropertyChain(propertyNames) {
// array for the values of the array properties
var arrayIndexValues = [];
// go through all properties and check for array notations
for (var i = 0; i < propertyNames.length; i++) {
var name = propertyNames[i];
// if its an array property in the chain
if (name.endsWith("]")) {
// get the inner value of the array notation
var arrayIndex = name.substring(
name.indexOf("[") + 1,
name.indexOf("]")
);
// check the arrayIndex
if (name.indexOf("]") != name.length - 1) {
throw new Error(
"Please use only one array at a time: " + name + " does not work."
);
}
if (arrayIndex !== "last") {
if (arrayIndex == "" || isNaN(parseInt(arrayIndex, 10))) {
throw new Error(
"No number or 'last' value has been given" +
" in an array binding: " +
name +
" does not work."
);
}
}
// if a property is in front of the array notation
if (name.indexOf("[") != 0) {
// store the property name without the array notation
propertyNames[i] = name.substring(0, name.indexOf("["));
// store the values in the array for the current iteration
arrayIndexValues[i] = "";
// store the properties for the next iteration (the item of the array)
arrayIndexValues[i + 1] = arrayIndex;
propertyNames.splice(i + 1, 0, "item");
// skip the next iteration. its the array item and its already set
i++;
// it the array notation is the beginning
} else {
// store the array index and override the entry in the property names
arrayIndexValues[i] = arrayIndex;
propertyNames.splice(i, 1, "item");
}
} else {
arrayIndexValues[i] = "";
}
}
return arrayIndexValues;
},
/**
* Internal helper method which is actually doing all bindings. That means
* that an event listener will be added to the source object which listens
* to the given event and invokes an set on the target property on the
* targetObject.
* This method does not store the binding in the internal reference store
* so it should NOT be used from outside this class. For an outside usage,
* use {@link #bind}.
*
* @param sourceObject {qx.core.Object} The source of the binding.
* @param sourceEvent {String} The event of the source object which could
* be the change event in common but has to be an
* {@link qx.event.type.Data} event.
* @param targetObject {qx.core.Object} The object which the source should
* be bind to.
* @param targetProperty {String} The property name of the target object.
* @param options {Map} A map containing the options. See
* {@link #bind} for more information.
* @param arrayIndex {String} The index of the given array if its an array
* to bind.
*
* @return {var} Returns the internal id for that binding. This can be used
* for referencing the binding or e.g. for removing. This is not an atomic
* id so you can't you use it as a hash-map index. It's the id which will
* be returned by the {@link qx.core.Object#addListener} method.
* @throws {qx.core.AssertionError} If the event is no data event or
* there is no property definition for the target object and target
* property.
*/
__bindEventToProperty(
sourceObject,
sourceEvent,
targetObject,
targetProperty,
options,
arrayIndex
) {
// checks
if (qx.core.Environment.get("qx.debug")) {
// check for the data event
var eventType = qx.Class.getEventType(
sourceObject.constructor,
sourceEvent
);
qx.core.Assert.assertEquals(
"qx.event.type.Data",
eventType,
sourceEvent +
" is not an data (qx.event.type.Data) event on " +
sourceObject +
"."
);
}
var bindListener = function (arrayIndex, e) {
// if an array value is given
if (arrayIndex !== "") {
//check if its the "last" value
if (arrayIndex === "last") {
arrayIndex = sourceObject.length - 1;
}
// get the data of the array
var data = sourceObject.getItem(arrayIndex);
// reset the target if the data is not set
if (data === undefined) {
qx.data.SingleValueBinding.__resetTargetValue(
targetObject,
targetProperty
);
}
// only do something if the current array has been changed
var start = e.getData().start;
var end = e.getData().end;
if (arrayIndex < start || arrayIndex > end) {
return;
}
} else {
// get the data out of the event
var data = e.getData();
}
// debug message
if (qx.core.Environment.get("qx.debug.databinding")) {
qx.log.Logger.debug(
"Binding executed from " +
sourceObject +
" by " +
sourceEvent +
" to " +
targetObject +
" (" +
targetProperty +
")"
);
qx.log.Logger.debug("Data before conversion: " + data);
}
// convert the data
data = qx.data.SingleValueBinding.__convertValue(
data,
targetObject,
targetProperty,
options,
sourceObject
);
// debug message
if (qx.core.Environment.get("qx.debug.databinding")) {
qx.log.Logger.debug("Data after conversion: " + data);
}
// try to set the value
var result;
try {
if (data !== undefined) {
result = qx.data.SingleValueBinding.__setTargetValue(
targetObject,
targetProperty,
data
);
} else {
result = qx.data.SingleValueBinding.__resetTargetValue(
targetObject,
targetProperty
);
}
// tell the user that the setter was invoked probably
if (options && options.onUpdate) {
options.onUpdate(sourceObject, targetObject, data);
}
} catch (ex) {
if (!(ex instanceof qx.core.ValidationError)) {
throw ex;
}
if (options && options.onSetFail) {
options.onSetFail(ex);
} else {
qx.log.Logger.warn(
"Failed so set value " +
data +
" on " +
targetObject +
". Error message: " +
ex
);
}
}
return result;
};
// check if an array index is given
if (!arrayIndex) {
// if not, signal it a s an empty string
arrayIndex = "";
}
// bind the listener function (make the array index in the listener available)
bindListener = qx.lang.Function.bind(
bindListener,
sourceObject,
arrayIndex
);
// add the listener
var id = sourceObject.addListener(sourceEvent, bindListener);
return id;
},
/**
* This method stores the given value as a binding in the internal structure
* of all bindings.
*
* @param id {var} The listener id of the id for a deeper binding.
* @param sourceObject {qx.core.Object} The source Object of the binding.
* @param sourceEvent {String} The name of the source event.
* @param targetObject {qx.core.Object} The target object.
* @param targetProperty {String} The name of the property on the target
* object.
*/
__storeBinding(
id,
sourceObject,
sourceEvent,
targetObject,
targetProperty
) {
var hash;
// add the listener id to the internal registry
hash = sourceObject.toHashCode();
if (this.__bindings[hash] === undefined) {
this.__bindings[hash] = [];
}
var binding = [
id,
sourceObject,
sourceEvent,
targetObject,
targetProperty
];
this.__bindings[hash].push(binding);
// add same binding data indexed by target object
hash = targetObject.toHashCode();
if (this.__bindingsByTarget[hash] === undefined) {
this.__bindingsByTarget[hash] = [];
}
this.__bindingsByTarget[hash].push(binding);
},
/**
* This method takes the given value, checks if the user has given a
* converter and converts the value to its target type. If no converter is
* given by the user, the {@link #__defaultConversion} will try to convert
* the value.
*
* @param value {var} The value which possibly should be converted.
* @param targetObject {qx.core.Object} The target object.
* @param targetPropertyChain {String} The property name of the target object.
* @param options {Map} The options map which can includes the converter.
* For a detailed information on the map, take a look at
* {@link #bind}.
* @param sourceObject {qx.core.Object} The source object for the binding.
*
* @return {var} The converted value. If no conversion has been done, the
* value property will be returned.
* @throws {qx.core.AssertionError} If there is no property definition
* of the given target object and target property.
*/
__convertValue(
value,
targetObject,
targetPropertyChain,
options,
sourceObject
) {
// do the conversion given by the user
if (options && options.converter) {
var model;
if (targetObject.getModel) {
model = targetObject.getModel();
}
return options.converter(value, model, sourceObject, targetObject);
// try default conversion
} else {
var properties = this.__getPropertyChainArray(targetPropertyChain);
var target = this.__getTargetFromChain(targetObject, properties);
var lastProperty = targetPropertyChain.substring(
targetPropertyChain.lastIndexOf(".") + 1,
targetPropertyChain.length
);
// if no target is currently available, return the original value
if (target == null) {
return value;
}
var propertieDefinition = qx.Class.getPropertyDefinition(
target.constructor,
lastProperty
);
var check =
propertieDefinition == null ? "" : propertieDefinition.check;
return this.__defaultConversion(value, check);
}
},
/**
* Helper method which tries to figure out if the given property on the
* given object does have a change event and if returns the name of it.
*
* @param sourceObject {qx.core.Object} The object to check.
* @param sourceProperty {String} The name of the property.
*
* @return {String} The name of the change event.
* @throws {qx.core.AssertionError} If there is no property definition of
* the given object property pair.
*/
__getEventForProperty(sourceObject, sourceProperty) {
// get the event name
var propertieDefinition = qx.Class.getPropertyDefinition(
sourceObject.constructor,
sourceProperty
);
if (propertieDefinition == null) {
return null;
}
return propertieDefinition.event;
},
/**
* Tries to convert the data to the type given in the targetCheck argument.
*
* @param data {var} The data to convert.
* @param targetCheck {String} The value of the check property. That usually
* contains the target type.
* @return {Integer|String|Float} The converted data
*/
__defaultConversion(data, targetCheck) {
var dataType = qx.lang.Type.getClass(data);
// to integer
if (
(dataType == "Number" || dataType == "String") &&
(targetCheck == "Integer" || targetCheck == "PositiveInteger")
) {
data = parseInt(data, 10);
}
// to string
if (
(dataType == "Boolean" || dataType == "Number" || dataType == "Date") &&
targetCheck == "String"
) {
data = data + "";
}
// to float
if (
(dataType == "Number" || dataType == "String") &&
(targetCheck == "Number" || targetCheck == "PositiveNumber")
) {
data = parseFloat(data);
}
return data;
},
/**
* Removes the binding with the given id from the given sourceObject. The
* id has to be the id returned by any of the bind functions.
*
* @param sourceObject {qx.core.Object} The source object of the binding.
* @param id {var} The id of the binding.
* @throws {Error} If the binding could not be found.
*/
removeBindingFromObject(sourceObject, id) {
// check for a deep binding
if (id.type == "deepBinding") {
// go through all added listeners (source)
for (var i = 0; i < id.sources.length; i++) {
// check if a source is available
if (id.sources[i]) {
if (id.listenerIds[i]) {
id.sources[i].removeListenerById(id.listenerIds[i]);
}
// If the listener id is not available, it is most likely
// caused by some hidden error situation.
// At least an error message should be displayed
else {
sourceObject.error(
"Could not remove deep bindings. Binding id for " +
id.sources[i].classname +
" could not be found!"