@qooxdoo/framework
Version:
The JS Framework for Coders
1,308 lines (1,169 loc) • 53.4 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 : function(
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++) {
// check for the array
if (arrayIndexValues[i] !== "") {
// push the array change event
eventNames.push("change");
} else {
var eventName = this.__getEventNameForProperty(source, propertyNames[i]);
if (!eventName) {
if (i == 0) { // the root property can not change --> error
throw new qx.core.AssertionError(
"Binding property " + propertyNames[i] + " of object " + source +
" not possible: No event available. "
);
}
// 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 : function(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)) {
// 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 : function(
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 : function(
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 : function(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 : function(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 : function(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 : function(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: function(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: function(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 : function(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 : function(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: function(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 : function(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 : function(
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 : function(
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 : function(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 : function(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 : function(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]) {
id.sources[i].removeListenerById(id.listenerIds[i]);
}
}
// go through all added listeners (target)
for (var i = 0; i < id.targets.length; i++) {
// check if a target is available
if (id.targets[i]) {
id.targets[i].removeListenerById(id.targetListenerIds[i]);
}
}
} else {
// remove the listener
sourceObject.removeListenerById(id);
}
// remove the id from the internal reference system
var bindings = this.getAllBindingsForObject(sourceObject);
// check if the binding exists
if (bindings != undefined) {
for (var i = 0; i < bindings.length; i++) {
if (bindings[i][0] == id) {
// remove binding data from internal reference indexed by target object
var target = bindings[i][3];
if (this.__bindingsByTarget[target.toHashCode()]) {
qx.lang.Array.remove(this.__bindingsByTarget[target.toHashCode()], bindings[i]);
}
// remove binding data from internal reference indexed by source object
var source = bindings[i][1];
if (this.__bindings[source.toHashCode()]) {
qx.lang.Array.remove(this.__bindings[source.toHashCode()], bindings[i]);
}
return;
}
}
}
throw new Error("Binding could not be found!");
},
/**
* Removes all bindings for the given object.
*
* @param object {qx.core.Object} The object of which the bindings should be
* removed.
* @throws {qx.core.AssertionError} If the object is not in the internal
* registry of the bindings.
* @throws {Error} If one of the bindings listed internally can not be
* removed.
*/
removeAllBindingsForObject : function(object) {
// check for the null value
if (qx.core.Environment.get("qx.debug")) {
qx.core.Assert.assertNotNull(object,
"Can not remove the bindings for null object!");
}
// get the bindings
var bindings = this.getAllBindingsForObject(object);
if (bindings != undefined)
{
// remove every binding with the removeBindingFromObject function
for (var i = bindings.length - 1; i >= 0; i--) {
this.removeBindingFromObject(object, bindings[i][0]);
}
}
},
/**
* Removes all bindings between given objects.
*
* @param object {qx.core.Object} The object of which the bindings should be
* removed.
* @param relatedObject {qx.core.Object} The object of which related
* bindings should be removed.
* @throws {qx.core.AssertionError} If the object is not in the internal
* registry of the bindings.
* @throws {Error} If one of the bindings listed internally can not be
* removed.
*/
removeRelatedBindings : function(object, relatedObject) {
// check for the null value
if (qx.core.Environment.get("qx.debug")) {
qx.core.Assert.assertNotNull(object,
"Can not remove the bindings for null object!");
qx.core.Assert.assertNotNull(relatedObject,
"Can not remove the bindings for null object!");
}
// get the bindings
var bindings = this.getAllBindingsForObject(object);
if (bindings != undefined)
{
// remove every binding with the removeBindingFromObject function
for (var i = bindings.length - 1; i >= 0; i--) {
var source = bindings[i][1];
var target = bindings[i][3];
if (source === relatedObject || target === relatedObject) {
this.removeBindingFromObject(object, bindings[i][0]);
}
}
}
},
/**
* Returns an array which lists all bindings.
*
* @param object {qx.core.Object} The object of which the bindings should
* be returned.
*
* @return {Array} An array of binding informations. Every binding
* information is an array itself containing id, sourceObject,
* sourceEvent, targetObject and targetProperty in that order.
*/
getAllBi