UNPKG

toloframework

Version:

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

461 lines (438 loc) 15.2 kB
"use strict"; /** * @module * * Provide all the functions needed for data-binding. * JavaScript object have __attributes__. * Data-binding add a new comcept to objects: __properties__. * A property is an attribute you can listen for get and set operations. * * @example * var DB = require('tfw.data-binding'); * DB.propAddClass( widget, 'visible', 'show' ); */ require("polyfill.string"); const $ = require("dom"), ParseUnit = require("tfw.css").parseUnit, Listeners = require("tfw.listeners"); const ID = '_tfw.data-binding_', converters = { castFunction: function(v) { if (typeof v !== 'function') return null; return v; }, castArray: function(v) { if (Array.isArray(v)) return v; if (v === null || v === undefined) return []; return [v]; }, castBoolean: function(v) { if (typeof v === 'boolean') return v; if (typeof v === 'string') { v = v.trim().toLowerCase(); if (v === '0' || v === 'false' || v === 'no' || v === 'null' || v === 'undefined') { return false; } return true; } if (typeof v === 'number') { return v !== 0 ? true : false; } return null; }, castColor: function(v) { return "" + v; }, castDate: function(v) { if (typeof v === 'number' || typeof v === 'string') return new Date(v); if (v instanceof Date) return v; return new Date(); }, castEnum: function(enumeration) { var lowerCaseEnum = enumeration.map(toLowerCase); return function(v) { if (typeof v === 'number') { return enumeration[Math.floor(v) % enumeration.length]; } if (typeof v !== 'string') return enumeration[0]; var idx = lowerCaseEnum.indexOf(v.trim().toLowerCase()); if (idx < 0) idx = 0; return enumeration[idx]; }; }, castInteger: function(v) { if (typeof v === 'number') { return Math.floor(v); } if (typeof v === 'boolean') return v ? 1 : 0; if (typeof v === 'string') { return parseInt(v); } return Number.NaN; }, castFloat: function(v) { if (typeof v === 'number') { return v; } if (typeof v === 'boolean') return v ? 1 : 0; if (typeof v === 'string') { return parseFloat(v); } return Number.NaN; }, castRegexp: function(v) { if (v instanceof RegExp) return v; if (typeof v === 'string' && v.trim().length != 0) { try { return new RegExp(v); } // Ignore Regular Expression errors. catch (ex) { console.error("[castRegexp] /" + v + "/ ", ex); } }; return null; }, castString: function(v) { if (typeof v === 'string') return v; if (v === undefined || v === null) return ''; return JSON.stringify(v); }, castStringArray: function(v) { if (Array.isArray(v)) return v; if (v === null || v === undefined) return []; if (typeof v === 'string') { return v.split(',').map(trim); } return [JSON.stringify(v)]; }, castUnit: function(v) { if (!v) return { v: 0, u: 'px' }; if (typeof v.v !== 'undefined') { v.v = parseFloat(v.v); if (isNaN(v.v)) return { v: 0, u: 'px' }; if (typeof v.u !== 'string') { v.u = 'px'; } else { v.u = v.u.trim().toLowerCase(); } if (v.u === '') { v.u = 'px'; } return { v: v.v, u: v.u }; } if (typeof v === 'number') return { v: v, u: 'px' }; if (typeof v !== 'string') return { v: 0, u: 'px' }; return ParseUnit('' + v); }, castValidator: function(v) { if (typeof v === 'function') return v; if (typeof v === 'boolean') return function() { return v; }; if (typeof v === 'string' && v.trim().length != 0) { try { var rx = new RegExp(v); return rx.test.bind(rx); } // Ignore Regular Expression errors. catch (ex) { console.error("[castValidator] /" + v + "/ ", ex); } }; return function() { return null; }; } }; /** * @param {any|function} val - Default value, or a specific getter (if `val` is a function). */ function propCast(caster, obj, att, val) { var hasSpecialGetter = typeof val === 'function'; if (typeof obj[ID] === 'undefined') obj[ID] = {}; obj[ID][att] = { value: val, event: new Listeners() }; var setter; if (typeof caster === 'function') { setter = function(v) { v = caster(v); // If there is a special getter, any set will fire. // Otherwise, we fire only if the value has changed. if (hasSpecialGetter || obj[ID][att].value !== v) { obj[ID][att].value = v; obj[ID][att].event.fire(v, obj, att); } }; } else { setter = function(v) { // If there is a special getter, any set will fire. // Otherwise, we fire only if the value has changed. if (hasSpecialGetter || obj[ID][att].value !== v) { obj[ID][att].value = v; obj[ID][att].event.fire(v, obj, att); } }; } var getter = val; if (!hasSpecialGetter) { // Default getter. getter = function() { return obj[ID][att].value; }; } Object.defineProperty(obj, att, { get: getter, set: setter, configurable: false, enumerable: true }); return exports.bind.bind(exports, obj, att); }; /** * @export function fire * * Set a new value and fire the event even if the value has not changed. */ exports.fire = function(obj, att, val) { var currentValue = obj[att]; if (typeof val === 'undefined') val = currentValue; obj[ID][att].value = val; obj[ID][att].event.fire(obj[att], obj, att); }; /** * @export function set * * Set a new value without firing any event. */ exports.set = function(obj, att, val) { if (typeof obj[ID] === 'undefined') obj[ID] = {}; if (typeof obj[ID][att] === 'undefined') obj[ID][att] = {}; obj[ID][att].value = val; }; /** * @export function get * * Get a value without firing any event. */ exports.get = function(obj, att) { if (typeof obj[ID] === 'undefined') return undefined; if (typeof obj[ID][att] === 'undefined') return undefined; return obj[ID][att].value; }; /** * @export function readOnly * @param {object} obj - Object to which we want to add a read only attribute. * @param {string} name - Attribute's name. * @param {function} value - Function to execute anytime someone gets the value of this attribute. * @param {any} value - Constatn value of this attribute. */ exports.readOnly = function(obj, name, value) { if (typeof value === 'function') { Object.defineProperty(obj, name, { get: value, set: function() {}, configurable: false, enumerable: true }); } else { Object.defineProperty(obj, name, { value: value, writtable: false, configurable: false, enumerable: true }); } }; /** * Create a property on which we can bind another property. * * @param {object} obj - Object to which we want to add a property. * @param {string} att - Name of the attribute of `obj`. * */ exports.prop = propCast.bind(null, null); exports.propWidget = function(obj, att, widget, widgetAttribute) { if (typeof widgetAttribute === 'undefined') widgetAttribute = att; if (typeof obj[ID] === 'undefined') obj[ID] = {}; obj[ID][att] = { event: new Listeners() }; exports.bind(widget, widgetAttribute, function(v) { obj[ID][att].event.fire(v, obj, att); }); Object.defineProperty(obj, att, { get: function() { return widget[widgetAttribute]; }, set: function(v) { widget[widgetAttribute] = v; }, configurable: false, enumerable: true }); return exports.bind.bind(exports, obj, att); }; /** * @export @function propToggleClass * Create an enum attribute which toggles a CSS class when assigned. * * @param {array|object} values - If this is an array, we will convert * it into an object. For instance `["show", "hide"]` will become * `{show: "show", hide: "hide"}`. */ exports.propToggleClass = function(target, attribute, values, prefix) { if (typeof prefix !== 'string') prefix = ''; var convertedValues = {}; if (typeof values === 'string') { convertedValues[values] = values; } else if (Array.isArray(values)) { values.forEach(function(itm) { convertedValues[itm] = itm; }); } else { convertedValues = values; } return propCast(null, target, attribute)(function(v) { var key, val; for (key in convertedValues) { val = convertedValues[key]; if (key == v) { $.addClass(target.element, prefix + val); } else { $.removeClass(target.element, prefix + val); } } }); }; /** * @export @function propAddClass * Create a boolean attribute that toggle a CSS class on the `element` attribute of `target`. * If the value id `true`, `className` is added. * @example * DB.propAddClass( this, 'wide', 'fullscreen' ); * DB.propAddClass( this, 'wide' ); */ exports.propAddClass = function(target, attribute, className) { if (typeof className === 'undefined') className = attribute; return propCast(converters.castBoolean, target, attribute)(function(v) { if (v) $.addClass(target.element, className); else $.removeClass(target.element, className); }); }; /** * @export @function propAddClass * Create a boolean attribute that toggle a CSS class on the `element` attribute of `target`. * If the value id `true`, `className` is removed. * @example * DB.propRemoveClass( this, 'visible', 'hide' ); */ exports.propRemoveClass = function(target, attribute, className) { if (typeof className === 'undefined') className = attribute; return propCast(converters.castBoolean, target, attribute)(function(v) { if (v) $.removeClass(target.element, className); else $.addClass(target.element, className); }); }; exports.propArray = propCast.bind(null, converters.castArray); exports.propBoolean = propCast.bind(null, converters.castBoolean); exports.propColor = propCast.bind(null, converters.castColor); exports.propDate = propCast.bind(null, converters.castDate); exports.propEnum = function(enumeration) { return propCast.bind(null, converters.castEnum(enumeration)); }; exports.propFunction = propCast.bind(null, converters.castFunction); exports.propInteger = propCast.bind(null, converters.castInteger); exports.propFloat = propCast.bind(null, converters.castFloat); exports.propRegexp = propCast.bind(null, converters.castRegexp); exports.propString = propCast.bind(null, converters.castString); exports.propStringArray = propCast.bind(null, converters.castStringArray); exports.propUnit = propCast.bind(null, converters.castUnit); exports.propValidator = propCast.bind(null, converters.castValidator); exports.bind = function(srcObj, srcAtt, dstObj, dstAtt, options) { if (typeof srcObj[ID] === 'undefined' || typeof srcObj[ID][srcAtt] === 'undefined') { console.error(JSON.stringify(srcAtt) + " is not a bindable property!", { srcObj: srcObj, srcAtt: srcAtt, dstObj: dstObj, dstAtt: dstAtt, options: options }); throw Error(JSON.stringify(srcAtt) + " is not a bindable property!"); } if (typeof options === 'undefined') options = {}; if (options.value) { options.converter = function() { return options.value; }; } var lambda = typeof dstObj === 'function' ? dstObj : function(v, obj, att) { dstObj[dstAtt] = typeof options.converter === 'function' ? options.converter(v) : v; }; srcObj[ID][srcAtt].event.add(lambda); return options; }; /** * @export.extend * @function extend * @param {object} _def - Default values for properties. * @param {object} _ext - Properties to override. * @param {object} _obj - DOM object whose properties belong. * @param {function} callback - Optional. Functioin to call when a * bindable property of `obj` changes. * @returns {object} A copy of `def` with values of `ext`. */ exports.extend = function(_def, _ext, _obj, callback) { const def = _def || {}, ext = _ext || {}, obj = _obj || {}, out = JSON.parse(JSON.stringify(def)), extKeys = Object.keys(ext); extKeys.forEach((key) => { if (key.charAt(0) === '$') return; const val = ext[key]; if (typeof out[key] === 'undefined') { console.error(`[tfw.data-binding.extend] Undefined argument: "${key}"!`); } else { out[key] = val; } }); if (typeof obj !== 'undefined') { extKeys.forEach((key) => { if (key.charAt(0) !== '$') return; Object.defineProperty(obj, key, { value: ext[key], writable: false, configurable: false, enumerable: false }); }); // Setting values. Object.keys(out).forEach((key) => { if (key.charAt(0) !== '$') { try { obj[key] = out[key]; } catch (ex) { console.error(`Trying to extend property "${key}" of object `, obj); console.error("...with object ", out); console.error("...but got ", ex); } } }); // General callback. if (typeof callback === 'function') { Object.keys(obj[ID]).forEach((key) => { exports.bind(obj, key, callback); }); callback(); } } return out; }; function toLowerCase(txt) { return txt.toLowerCase(); } function trim(txt) { return txt.trim(); } exports.converters = converters;