UNPKG

toloframework

Version:

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

419 lines (366 loc) 13.5 kB
"use strict"; /** @module tfw.binding.link */ require('tfw.binding.link', function (require, module, exports) { var _ = function () { var D = { "en": {}, "fr": {} }, X = require("$").intl; function _() { return X(D, arguments); } _.all = D; return _; }(); "use strict"; 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 Link(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; } module.exports._ = _; });