toloframework
Version:
Javascript/HTML/CSS compiler for Firefox OS or nodewebkit apps using modules in the nodejs style.
437 lines (395 loc) • 13 kB
JavaScript
;
var PropertyManager = require("tfw.binding.property-manager");
var ID = 0;
/**
* @export
* @class Link
* Bind A with B. A is waiting for value change in B and vice versa.
*
* @param {object} args.A.obj - Object holding properties for input A.
* @param {string} args.A.name - Name of the property of the holding
* object for input A.
* @param {number=0} args.A.delay - Number of milliseconds to wait
* before using the data sent by B. If a new value is sent by B before
* the delay, the previous value is forgotten.
* @param {function=null} args.A.action - Function to execute when A
* received a new value from B.
* @param {string=undefined} args.A.value - If specified, it is the
* value we use whatsover B sent.
* @param {function=undefined} args.A.value - If `value` is a
* function, it will be called with the propertyName as sole argument
* and the return will be used as value for A.
* @param {function=undefined} args.A.converter - Converter for
* values entering the source.
* @param {function=undefined} args.A.filter - Filter for values
* entering the source. If the filter returns `false` the value is not
* set to the source.
* @param {function=undefined} args.A.map - Function to execute on
* each element of the value. Only if this value is an array.
* @param {function=undefined} args.A.header - Function to execute
* before the array will be parsed. Only if this value is an array and
* the property `map` is set.
* @param {function=undefined} args.A.footer - Function to execute
* after the array will be parsed. Only if this value is an array and
* the property `map` is set.
* @param {string|array} args.A.switch - Sometimes, you are
* listening at a property A to change, but want to propagate the
* value of B. It is usefull with buttons which provide action
* properties.
* So this attribute tells the link what property to read instead of
* the one we are listening on. Moreover, if you give an array of
* property names, the value will be an array with the values of all
* asked properties.
* @param {boolean=true} args.A.open - If false, no data will be
* accepted by `A`.
*/
var Link = function( args ) {
try {
var id = ID++;
checkArgs.call( this, args );
var onChangedA = link.call( this, args, id, "A", "B" );
var onChangedB = link.call( this, args, id, "B", "A" );
addDestroyFunction.call( this, onChangedA, onChangedB );
}
catch( ex ) {
console.error("new Link( " + args + " )");
fail( ex, "new Link( <args> ) " + (this.name || "") );
}
};
module.exports = Link;
function link( args, id, emitterKey, receiverKey ) {
var that = this;
var onChanged = [];
args[receiverKey].forEach(function (receiver, receiverIndex) {
if( !receiver.open ) return;
var pmReceiver = PropertyManager( receiver.obj );
args[emitterKey].forEach(function (emitter, emitterIndex) {
if( typeof emitter.name === 'string'
&& typeof receiver.name === 'string'
&& emitter.obj === receiver.obj
&& emitter.name === receiver.name )
{
console.error(
"It is forbidden to bind a property on itself! ("
+ emitterIndex + " -> " + receiverIndex + ")"
);
console.info("[tfw.binding.link] args=", args);
return;
}
var pmEmitter = PropertyManager( emitter.obj );
var slot = actionChanged.bind( that, emitter, receiver, id );
pmEmitter.on( emitter.name, slot );
onChanged.push({ pm: pmEmitter, name: emitter.name, slot: slot });
});
});
return onChanged;
}
function addDestroyFunction( onSrcChanged, onDstChanged ) {
this.destroy = function() {
onSrcChanged.forEach(function (item) {
item.pm.off( item.name, item.slot );
});
onDstChanged.forEach(function (item) {
item.pm.off( item.name, item.slot );
});
};
}
function actionChanged( src, dst, id, value, propertyName, container, wave ) {
if( !Array.isArray( wave ) ) wave = [];
if( this.dbg ) {
console.log( "Link " + this.dbg + ": ", {
src: src, dst: dst, id: id, value: value, propertyName: propertyName, container: container, wave: wave
} );
}
if( hasAlreadyBeenHere( id, wave ) ) {
if( this.dbg ) {
console.log( "...has been BLOCKED by the wave! ", wave );
}
return;
}
var that = this;
var pmSrc = PropertyManager( src.obj );
var pmDst = PropertyManager( dst.obj );
value = processValue( value, src, dst );
value = processSwitch( value, dst, pmSrc );
value = processConverter( value, src, dst );
if( filterFailed( value, src, dst ) ) {
if( this.dbg ) console.log( "...has been FILTERED!" );
return;
}
value = processFormat( value, src, dst );
value = processMap( value, src, dst );
if( typeof dst.delay === 'number' ) {
if( this.debug ) console.log( "...has been DELAYED for " + dst.delay + " ms!" );
clearTimeout( dst._id );
dst._id = setTimeout(function() {
if( that.dbg ) {
console.log( "Link " + that.dbg + " (after " + dst.delay + " ms): ", {
src: src, dst: dst, id: id, value: value, propertyName: propertyName, wave: wave
} );
console.log("...try to change a value. ", {
target: pmDst,
propertyName: dst.name,
value: value,
wave: wave
});
}
pmDst.change( dst.name, value, wave );
}, dst.delay);
} else {
if( this.debug )
console.log("...try to change a value. ", {
target: pmDst,
propertyName: dst.name,
value: value,
wave: wave
});
pmDst.change( dst.name, value, wave );
}
}
function checkArgs( args ) {
try {
if( typeof args.name === 'undefined' ) args.name = args.debug;
if( typeof args.name !== 'string' ) args.name = "Link#" + this.id;
if( typeof args === 'undefined' ) fail("Missing mandatory argument!");
if( typeof args.A === 'undefined' ) fail("Missing `args.A`!");
if( !Array.isArray( args.A ) ) args.A = [args.A];
if( typeof args.B === 'undefined' ) fail("Missing `args.B`!");
if( !Array.isArray( args.B ) ) args.B = [args.B];
var k;
for( k = 0 ; k < args.A.length ; k++ ) {
checkPod( args.A[k], k );
}
for( k = 0 ; k < args.B.length ; k++ ) {
checkPod( args.B[k], k );
}
// For debugging.
this.name = args.name;
this.debug = args.debug;
}
catch( ex ) {
console.error("checkArgs( " + args + " )");
fail( ex, "checkArgs( <args> )" );
}
}
function checkPod( pod, index ) {
try {
if( !pod.action ) {
if( typeof pod.obj === 'undefined' ) fail("Missing `[" + index + "].obj`!");
if( typeof pod.name === 'undefined' ) pod.name = "*";
// Check if the attribute exists.
if( !PropertyManager.isLinkable( pod.obj, pod.name ) )
throw "`" + pod.name + "` is not a linkable attribute.\n"
+ "Valid linkable attributes are: "
+ PropertyManager.getAllAttributesNames( pod.obj ).join(", ") + ".";
}
else if ( typeof pod.action !== 'function' ) {
throw "Attribute `[" + index + "].action` must be a function!";
}
else {
if( typeof pod.obj !== 'undefined' )
throw "[" + index + "].action cannot be defined in the same time of ["
+ index + "].obj! They are exclusive attributes.";
if( typeof pod.name !== 'undefined' )
throw "[" + index + "].action cannot be defined in the same time of ["
+ index + "].name! They are exclusive attributes.";
// An action is emulated by a hollow object.
var hollowObject = {};
PropertyManager( hollowObject ).create("<action>", {
set: pod.action
});
pod.obj = hollowObject;
pod.name = "<action>";
}
if( typeof pod.open === 'undefined' ) pod.open = true;
}
catch( ex ) {
console.error("checkpod(", pod, ", ", index, ")");
fail( ex, "checkpod( <pod>, " + index + ")" );
}
}
function fail( msg, source ) {
if( typeof source === 'undefined' ) {
source = "";
} else {
source = "::" + source;
}
throw msg + "\n" + "[tfw.binding.link" + source + "]";
}
function hasAlreadyBeenHere( id, wave ) {
if( Array.isArray( wave ) ) {
if( wave.indexOf( id ) < 0 ) {
// Remember we took this path.
wave.push( id );
} else {
// We already took this link in this wave.
return true;
}
}
return false;
}
function filterFailed(value, src, dst) {
if( typeof dst.filter === 'function' ) {
try {
if( !dst.filter( value ) ) return true;
}
catch( ex ) {
console.error( ex );
fail(
"Error in filter of link "
+ PropertyManager(src.obj) + "." + src.name
+ " -> "
+ PropertyManager(dst.obj) + "." + dst.name + "!"
);
}
}
return false;
}
function processSwitch( value, dst, pmSrc ) {
if( typeof dst.switch === 'string' ) {
return pmSrc.get( dst.switch );
}
else if( Array.isArray( dst.switch ) ) {
return dst.switch.map(function(name) {
return pmSrc.get( name );
});
}
return value;
}
function processConverter( value, src, dst ) {
if( typeof dst.converter === 'function' ) {
try {
return dst.converter( value );
}
catch( ex ) {
console.error( ex );
fail(
"Error in converter of link "
+ PropertyManager(src.obj) + "." + src.name
+ " -> "
+ PropertyManager(dst.obj) + "." + dst.name + "!"
);
}
}
return value;
}
/**
* `format` is used with an intl function. It must be an array with
* two elements: a function and a string. The function will be called
* with the string as first argument and the `value` as second. The
* result will be the transformed value.
*/
function processFormat( value, src, dst ) {
if( !dst.format ) return value;
try {
if( !Array.isArray( dst.format ) )
throw "Must be an array with two elements!";
var intlFunc = dst.format[0];
if( typeof intlFunc !== 'function' )
throw "First element of the array must be a function!";
var intlId = dst.format[1];
if( typeof intlId !== 'string' )
throw "Second element of the array must be a string!";
return intlFunc( intlId, value );
}
catch( ex ) {
console.error( ex );
fail(
"Error in format of link "
+ PropertyManager(src.obj) + "." + src.name
+ " -> "
+ PropertyManager(dst.obj) + "." + dst.name + "!\n"
+ ex
);
}
}
function processMap( value, src, dst ) {
if( value && typeof value.map === 'function' && typeof dst.map === 'function' ) {
try {
var result = [];
var more = {
context: {},
list: value,
index: 0
};
if( typeof dst.header === 'function' ) {
try {
result.push(dst.header( value, more.context ));
}
catch( ex ) {
console.error("[tfw.binding.link/processMap] Exception while calling a header function: ", ex);
console.error({
value: value,
src: src,
dst: dst,
more: more
});
}
}
value.forEach(function (itm, idx) {
more.index = idx;
try {
result.push( dst.map( itm, more ) );
}
catch( ex ) {
console.error("[tfw.binding.link/processMap] Exception while calling a map function: ", ex);
console.error({
item: itm,
index: idx,
value: value,
src: src,
dst: dst,
result: result,
more: more
});
}
});
if( typeof dst.footer === 'function' ) {
try {
result.push(dst.header( value, more.context ));
}
catch( ex ) {
console.error("[tfw.binding.link/processMap] Exception while calling a footer function: ", ex);
console.error({
value: value,
src: src,
dst: dst,
result: result,
more: more
});
}
}
return result.filter(function( item ) {
return item != null;
});
}
catch( ex ) {
console.error( ex );
fail(
"Error in map of link "
+ PropertyManager(src.obj) + "." + src.name
+ " -> "
+ PropertyManager(dst.obj) + "." + dst.name + "!"
);
}
}
return value;
}
function processValue( value, src, dst ) {
if( typeof dst.value === 'undefined' ) return value;
if( typeof dst.value === 'function' ) {
try {
return dst.value( src.name );
}
catch( ex ) {
console.error( ex );
fail(
"Error in value(" + src.name + ") of link "
+ PropertyManager(src.obj) + "." + src.name
+ " -> "
+ PropertyManager(dst.obj) + "." + dst.name + "!"
);
}
}
return dst.value;
}