toloframework
Version:
Javascript/HTML/CSS compiler for Firefox OS or nodewebkit apps using modules in the nodejs style.
369 lines (327 loc) • 10.5 kB
JavaScript
;
var Event = require("tfw.event");
var ID = 0;
// A container with linkable properties has an attribute with this
// name. This attribute own the list of linkable properties.
var CONTAINER_SYMBOL = "__tfw.property-manager__";
// A linkable property owned by a container has an attribute with this
// name. This attribute give the name of the property and a reference
// to the container.
var PROPERTY_SYMBOL = '__tfw.binding.property-manager__';
/**
*
*/
function PropertyManager( container ) {
Object.defineProperty( this, 'id', {
value: ID++,
writable: false,
configurable: false,
enumerable: true
});
this.name = this.id;
this._props = {};
this._container = container;
}
/**
* @class PropertyManager
* @member set
* Set the inner value of a property without fireing any event.
* @param {string} propertyName.
* @param {any} value.
*/
PropertyManager.prototype.set = function( propertyName, value ) {
this.create( propertyName ).set( value );
};
/**
* @class PropertyManager
* @member get
* @param {string} propertyName.
* @return {any} Inner value of the property.
*
*/
PropertyManager.prototype.get = function( propertyName ) {
return this.create( propertyName ).get();
};
PropertyManager.prototype.propertyId = function( propertyName ) {
return this.create( propertyName ).id;
};
PropertyManager.prototype.fire = function( propertyName, wave ) {
var prop = this.create( propertyName );
prop.event.fire( prop.get(), propertyName, this._container, wave );
};
PropertyManager.prototype.change = function( propertyName, value, wave ) {
if( typeof wave === 'undefined' ) wave = [];
var prop = this.create( propertyName );
var converter = prop.converter;
if( typeof converter === 'function' ) {
value = converter( value );
}
var currentValue = prop.get();
if( prop.alwaysFired || areDifferent( value, currentValue ) ) {
prop.set( value );
var that = this;
exec(prop, function() {
// Fire change event.
that.fire( propertyName, wave );
});
}
};
/**
* @class PropertyManager
* @member converter
* @param {string} propertyName.
* @param {function=undefined} converter - Converter is assigned only
* if it is a function.
* @return {function} Current converter.
*/
PropertyManager.prototype.converter = function( propertyName, converter ) {
var prop = this.create( propertyName );
if( typeof converter === 'function' ) {
prop.converter = converter;
}
else if( typeof converter !== 'undefined' ) {
throw Error(
"[tfw.binding.property-manager::converter] "
+ "`converter` must be of type function or undefined!"
);
}
return prop.converter;
};
PropertyManager.prototype.delay = function( propertyName, delay ) {
var prop = this.create( propertyName );
delay = parseFloat( delay );
if( isNaN( delay ) ) return prop.delay;
prop.delay = delay;
};
/**
* @class PropertyManager
* @member on
* @param {string} propertyName.
* @param {function(val,name,container)} action - Function to execute
* when a property changed.
*/
PropertyManager.prototype.on = function( propertyName, action ) {
var prop = this.create( propertyName );
prop.event.add( action );
};
/**
* @class PropertyManager
* @member off
* @param {string} propertyName.
* @param {function(val,name,container)} action - Function to execute
* when a property changed.
*/
PropertyManager.prototype.off = function( propertyName, action ) {
var prop = this.create( propertyName );
prop.event.remove( action );
};
/**
* @export
* @function
* @param {object} container - Object which will hold properties.
*/
module.exports = function( container ) {
if( typeof container === 'undefined' )
fail("Argument `container` is mandatory!");
var pm = container[CONTAINER_SYMBOL];
if( !pm ) {
pm = new PropertyManager( container );
container[CONTAINER_SYMBOL] = pm;
}
return pm;
};
/**
* @export .isLinkable
* Look if an object has a property manager assigned to it and own a
* property which name is `propertyName`.
*/
module.exports.isLinkable = function( container, propertyName ) {
if( container[CONTAINER_SYMBOL] === undefined ) return false;
if( typeof propertyName !== 'string' ) return true;
return container[CONTAINER_SYMBOL]._props[propertyName] !== undefined;
};
module.exports.getAllAttributesNames = function( container ) {
if( container[CONTAINER_SYMBOL] === undefined ) return [];
return Object.keys( container[CONTAINER_SYMBOL]._props );
};
/**
* Return the linkable properties which holds this value, or `null`.
*/
module.exports.getProperties = function( property ) {
var properties = property[PROPERTY_SYMBOL];
if( !Array.isArray( properties ) ) return null;
return properties;
};
/**
* @export .create
* Create an new linkable property.
* @param {string} propertyName - Name of the property.
* @param {any} options.init - Initial value. Won't fire any change notification.
* @param {function=undefined} options.get - Special getter.
* @param {function=undefined} options.set - Special setter.
* @param {function=undefined} options.cast - Conversion to apply to
* the value before setting it.
*/
PropertyManager.prototype.create = function( propertyName, options ) {
var that = this;
if( typeof propertyName !== 'string' ) fail("propertyName must be a string!");
var p = this._props[propertyName];
if (!p) {
if( typeof options === 'undefined' ) options = {};
p = createNewProperty.call( this, propertyName, options );
}
return p;
};
/**
* Copy all the attributes of `source` into `target`.
*/
function applyAttributesToTarget( source, target ) {
var attName, attValue;
for( attName in source ) {
if( module.exports.isLinkable( target, attName ) ) {
attValue = source[attName];
target[attName] = attValue;
}
}
}
function createNewProperty( propertyName, options ) {
var that = this;
var prop = {
value: undefined,
event: new Event(),
filter: functionOrUndefined( options.filter ),
converter: functionOrUndefined( options.converter ),
delay: castPositiveInteger( options.delay ),
action: null,
alwaysFired: options.alwaysFired ? true : false,
manager: this,
name: propertyName,
timeout: 0
};
prop.get = createGetter( prop, options );
prop.set = createSetter( prop, options, that, propertyName );
this._props[propertyName] = prop;
if( typeof options.init !== 'undefined' ) {
prop.set( options.init );
}
Object.defineProperty(this._container, propertyName, {
get: prop.get.bind( prop ),
set: that.change.bind( that, propertyName ),
enumerable: true, configurable: false
});
return prop;
}
function createGetter( prop, options ) {
if( typeof options.get === 'function' ) {
return function(v) {
var newValue = options.get( v );
addPropToValue( prop, newValue );
return newValue;
};
}
return function(v) {
return prop.value;
};
}
function createSetter( prop, options, that, propertyName ) {
var setter;
if( typeof options.cast === 'function' ) {
if( typeof options.set === 'function' ) {
setter = function(v) {
removePropFromValue( prop, prop.get() );
var castedValue = options.cast( v, that );
addPropToValue( prop, prop.value );
options.set( castedValue );
};
} else {
setter = function(v) {
removePropFromValue( prop, prop.get() );
prop.value = options.cast( v, that );
addPropToValue( prop, prop.value );
};
}
} else {
setter = typeof options.set === 'function' ? options.set : function(v) {
removePropFromValue( prop, prop.get() );
prop.value = v;
addPropToValue( prop, prop.value );
};
}
return setter;
}
/**
* Add an `info` attribute to the property's value. This is useful to
* find the container and the property name from the value.
*/
function addPropToValue( prop, value ) {
if( value === undefined || value === null ) return;
if( value.isContentChangeAware !== true ) return;
var properties = value[PROPERTY_SYMBOL];
if( !Array.isArray( properties ) ) {
properties = [prop];
}
else if( properties.indexOf( prop ) === -1 ) {
properties.push( prop );
}
value[PROPERTY_SYMBOL] = properties;
}
function removePropFromValue( prop, value ) {
if( value === undefined || value === null ) return;
if( value.isContentChangeAware !== true ) return;
var properties = value[PROPERTY_SYMBOL];
if( !Array.isArray( properties ) ) return;
var pos = properties.indexOf( prop );
if( pos === -1 ) return;
properties.splice( pos, 1 );
}
/**
* This is a special property which emit a change event as soon as any
* value is set to it, even if this value has already been set just
* before. Moreover, the value of this attribute is always its name.
* This is used for action properties in buttons, for instance.
*/
PropertyManager.prototype.createAction = function( propertyName, options ) {
if( typeof options === 'undefined' ) options = {};
options.alwaysFired = true;
return this.create( propertyName, options );
};
/**
* Most of the time, `action` is called immediatly without any
* argument. But you can configure your property with a `delay`. If
* you do so, the action is only called after this `delay`(in ms). The
* `action`can be lost if another call to `exec()` occurs before the
* end of the `delay` and the `delay` is reset.
*/
function exec( prop, action ) {
if( !prop.delay ) action();
else {
clearTimeout( prop.timeout );
prop.timeout = setTimeout( action, prop.delay );
}
};
function fail( msg, source ) {
if( typeof source === 'undefined' ) {
source = "";
} else {
source = "::" + source;
}
throw Error("[tfw.binding.property-manager" + source + "] " + msg);
}
function castPositiveInteger( v ) {
if( typeof v !== 'number' ) return 0;
if( isNaN( v ) ) return 0;
return Math.max( 0, Math.floor( v ) );
}
function functionOrUndefined( f ) {
if( typeof f === 'function' ) return f;
return undefined;
}
/**
* A linkable property must link only if we set a new value to it. If
* the value is a List, we need to check if they have a different
* inner array because different Lists can share the same array and in
* this cas, we don't want to fire a changed event.
*/
function areDifferent( oldValue, newValue ) {
return oldValue !== newValue;
}