UNPKG

toloframework

Version:

Javascript/HTML/CSS compiler for Firefox OS or nodewebkit apps using modules in the nodejs style.

453 lines (403 loc) 13.5 kB
"use strict"; /** * @export * @function * @param {object} container - Object which will hold properties. */ module.exports = constructor; /** * @export .isLinkable * Look if an object has a property manager assigned to it and own a * property which name is `propertyName`. */ module.exports.isLinkable = isLinkable; module.exports.getAllAttributesNames = getAllAttributesNames; /** * Return the linkable properties which holds this value, or `null`. */ module.exports.getProperties = getProperties; /** * Create a readonly attribute. * * @param {object} target - Object which will bear the readonly attribute. * @param {object} attribs - Attributes with their values. * @returns {undefined} */ module.exports.readonly = readonly; const Event = require( "tfw.event" ); let ID = 0; // A container with linkable properties has an attribute with this // name. This attribute own the list of linkable properties. const 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. const 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 ); }; /** * Look if `propertyName` is a linkable property of this object. */ PropertyManager.prototype.isLinkable = function ( propertyName ) { var container = this._container; if ( !container ) return false; if ( typeof container[ CONTAINER_SYMBOL ] === 'undefined' ) return false; return typeof container[ CONTAINER_SYMBOL ]._props[ propertyName ] !== 'undefined'; }; /** * @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. */ function constructor( container ) { try { if ( typeof container === 'undefined' ) { fail( "Argument `container` is mandatory!" ); } if ( typeof container !== 'object' ) return null; let pm = container[ CONTAINER_SYMBOL ]; if ( !pm ) { pm = new PropertyManager( container ); container[ CONTAINER_SYMBOL ] = pm; } return pm; } catch ( ex ) { console.error( ex ); return null; } } /** * @export .isLinkable * Look if an object has a property manager assigned to it and own a * property which name is `propertyName`. */ function isLinkable( container, propertyName ) { if ( typeof container[ CONTAINER_SYMBOL ] === "undefined" ) return false; if ( typeof propertyName !== 'string' ) return true; return typeof container[ CONTAINER_SYMBOL ]._props[ propertyName ] !== "undefined"; }; function getAllAttributesNames( container ) { if ( container[ CONTAINER_SYMBOL ] === undefined ) return []; return Object.keys( container[ CONTAINER_SYMBOL ]._props ); }; /** * Return the linkable properties which holds this value, or `null`. */ function getProperties( 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; } /** * Create a readonly attribute. * * @param {object} target - Object which will bear the readonly attribute. * @param {object} attribs - Attributes with their values. * @returns {undefined} */ function readonly( target, attribs ) { const attribNames = Object.keys( attribs ); attribNames.forEach( function ( attName ) { const attValue = attribs[ attName ]; if ( typeof attValue === 'function' ) { Object.defineProperty( target, attName, { set() { throw Error( `Attribute "${attName}" is read only!` ); }, get: attValue, configurable: false, enumerable: true } ); } else { Object.defineProperty( target, attName, { value: attValue, writable: false, configurable: false, enumerable: true } ); } } ); }