Commit 89c41418 authored by Pascal Hartig's avatar Pascal Hartig

Merge pull request #1075 from podefr/master

fix #1071: Olives: Update Olives and Emily to latest 1.x versions
parents b9c3b488 9df98e41
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
"name": "todomvc-olives", "name": "todomvc-olives",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"olives": "~1.4.0", "olives": "~1.6.0",
"emily": "~1.3.5", "emily": "~1.8.1",
"requirejs": "~2.1.5", "requirejs": "~2.1.5",
"todomvc-common": "~0.3.0" "todomvc-common": "~0.3.0"
} }
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
*/ */
/** /**
* Emily * Emily.js - http://flams.github.com/emily/
* Copyright(c) 2012-2013 Olivier Scherrer <pode.fr@gmail.com> * Copyright(c) 2012-2013 Olivier Scherrer <pode.fr@gmail.com>
* MIT Licensed * MIT Licensed
*/ */
...@@ -19,303 +19,372 @@ define('Tools',[], ...@@ -19,303 +19,372 @@ define('Tools',[],
*/ */
function Tools(){ function Tools(){
return {
/**
* For applications that don't run in a browser, window is not the global object.
* This function returns the global object wherever the application runs.
* @returns {Object} the global object
*/
getGlobal: function getGlobal() {
var func = function() {
return this;
};
return func.call(null);
},
/** /**
* Mixes an object into another * Get the closest number in an array
* @param {Object} source object to get values from * @param {Number} item the base number
* @param {Object} destination object to mix values into * @param {Array} array the array to search into
* @param {Boolean} optional, set to true to prevent overriding * @param {Function} getDiff returns the difference between the base number and
* @returns {Object} the destination object * and the currently read item in the array. The item which returned the smallest difference wins.
*/ * @private
mixin: function mixin(source, destination, dontOverride) { */
this.loop(source, function (value, idx) { function _getClosest(item, array, getDiff) {
if (!destination[idx] || !dontOverride) { var closest,
destination[idx] = source[idx]; diff;
}
});
return destination;
},
/** if (!array) {
* Count the number of properties in an object return;
* It doesn't look up in the prototype chain }
* @param {Object} object the object to count
* @returns {Number}
*/
count: function count(object) {
var nbItems = 0;
this.loop(object, function () {
nbItems++;
});
return nbItems; array.forEach(function (comparedItem, comparedItemIndex) {
}, var thisDiff = getDiff(comparedItem, item);
/** if (thisDiff >= 0 && (typeof diff == "undefined" || thisDiff < diff)) {
* Compares the properties of two objects and returns true if they're the same diff = thisDiff;
* It's doesn't do it recursively closest = comparedItemIndex;
* @param {Object} first object }
* @param {Object} second object });
* @returns {Boolean} true if the two objets have the same properties
*/
compareObjects: function compareObjects(object1, object2) {
var getOwnProperties = function (object) {
return Object.getOwnPropertyNames(object).sort().join("");
};
return getOwnProperties(object1) == getOwnProperties(object2);
},
/** return closest;
* Compares two numbers and tells if the first one is bigger (1), smaller (-1) or equal (0) }
* @param {Number} number1 the first number
* @param {Number} number2 the second number
* @returns 1 if number1>number2, -1 if number2>number1, 0 if equal
*/
compareNumbers: function compareNumbers(number1, number2) {
if (number1>number2) {
return 1;
} else if (number1<number2) {
return -1;
} else {
return 0;
}
},
/** return {
* Transform array-like objects to array, such as nodeLists or arguments /**
* @param {Array-like object} * For applications that don't run in a browser, window is not the global object.
* @returns {Array} * This function returns the global object wherever the application runs.
*/ * @returns {Object} the global object
toArray: function toArray(array) { */
return [].slice.call(array); getGlobal: function getGlobal() {
}, var func = function() {
return this;
};
return func.call(null);
},
/** /**
* Small adapter for looping over objects and arrays * Mixes an object into another
* Warning: it's not meant to be used with nodeList * @param {Object} source object to get values from
* To use with nodeList, convert to array first * @param {Object} destination object to mix values into
* @param {Array/Object} iterated the array or object to loop through * @param {Boolean} optional, set to true to prevent overriding
* @param {Function} callback the function to execute for each iteration * @returns {Object} the destination object
* @param {Object} scope the scope in which to execute the callback */
* @returns {Boolean} true if executed mixin: function mixin(source, destination, dontOverride) {
*/ this.loop(source, function (value, idx) {
loop: function loop(iterated, callback, scope) { if (!destination[idx] || !dontOverride) {
var i, destination[idx] = source[idx];
length; }
});
if (iterated instanceof Object && callback instanceof Function) { return destination;
if (iterated instanceof Array) { },
for (i=0; i<iterated.length; i++) {
callback.call(scope, iterated[i], i, iterated);
}
} else {
for (i in iterated) {
if (iterated.hasOwnProperty(i)) {
callback.call(scope, iterated[i], i, iterated);
}
}
}
return true;
} else {
return false;
}
},
/** /**
* Make a diff between two objects * Count the number of properties in an object
* @param {Array/Object} before is the object as it was before * It doesn't look up in the prototype chain
* @param {Array/Object} after is what it is now * @param {Object} object the object to count
* @example: * @returns {Number}
* With objects: */
* count: function count(object) {
* before = {a:1, b:2, c:3, d:4, f:6} var nbItems = 0;
* after = {a:1, b:20, d: 4, e: 5} this.loop(object, function () {
* will return : nbItems++;
* { });
* unchanged: ["a", "d"],
* updated: ["b"],
* deleted: ["f"],
* added: ["e"]
* }
*
* It also works with Arrays:
*
* before = [10, 20, 30]
* after = [15, 20]
* will return :
* {
* unchanged: [1],
* updated: [0],
* deleted: [2],
* added: []
* }
*
* @returns object
*/
objectsDiffs : function objectsDiffs(before, after) {
if (before instanceof Object && after instanceof Object) {
var unchanged = [],
updated = [],
deleted = [],
added = [];
// Look through the after object
this.loop(after, function (value, idx) {
// To get the added
if (typeof before[idx] == "undefined") {
added.push(idx);
// The updated
} else if (value !== before[idx]) {
updated.push(idx);
// And the unchanged
} else if (value === before[idx]) {
unchanged.push(idx);
}
});
// Loop through the before object
this.loop(before, function (value, idx) {
// To get the deleted
if (typeof after[idx] == "undefined") {
deleted.push(idx);
}
});
return {
updated: updated,
unchanged: unchanged,
added: added,
deleted: deleted
};
} else { return nbItems;
return false; },
}
},
/** /**
* Transforms Arrays and Objects into valid JSON * Compares the properties of two objects and returns true if they're the same
* @param {Object/Array} object the object to JSONify * It's doesn't do it recursively
* @returns the JSONified object or false if failed * @param {Object} first object
*/ * @param {Object} second object
jsonify: function jsonify(object) { * @returns {Boolean} true if the two objets have the same properties
if (object instanceof Object) { */
return JSON.parse(JSON.stringify(object)); compareObjects: function compareObjects(object1, object2) {
} else { var getOwnProperties = function (object) {
return false; return Object.getOwnPropertyNames(object).sort().join("");
} };
}, return getOwnProperties(object1) == getOwnProperties(object2);
},
/** /**
* Clone an Array or an Object * Compares two numbers and tells if the first one is bigger (1), smaller (-1) or equal (0)
* @param {Array/Object} object the object to clone * @param {Number} number1 the first number
* @returns {Array/Object} the cloned object * @param {Number} number2 the second number
*/ * @returns 1 if number1>number2, -1 if number2>number1, 0 if equal
clone: function clone(object) { */
if (object instanceof Array) { compareNumbers: function compareNumbers(number1, number2) {
return object.slice(0); if (number1>number2) {
} else if (typeof object == "object" && object !== null && !(object instanceof RegExp)) { return 1;
return this.mixin(object, {}); } else if (number1<number2) {
} else { return -1;
return false; } else {
} return 0;
}, }
},
/**
* Transform array-like objects to array, such as nodeLists or arguments
* @param {Array-like object}
* @returns {Array}
*/
toArray: function toArray(array) {
return [].slice.call(array);
},
/** /**
* * Small adapter for looping over objects and arrays
* * Warning: it's not meant to be used with nodeList
* * To use with nodeList, convert to array first
* * @param {Array/Object} iterated the array or object to loop through
* Refactoring needed for the following * @param {Function} callback the function to execute for each iteration
* * @param {Object} scope the scope in which to execute the callback
* * @returns {Boolean} true if executed
* */
* loop: function loop(iterated, callback, scope) {
* var i,
*/ length;
if (iterated instanceof Object && callback instanceof Function) {
if (iterated instanceof Array) {
for (i=0; i<iterated.length; i++) {
callback.call(scope, iterated[i], i, iterated);
}
} else {
for (i in iterated) {
if (iterated.hasOwnProperty(i)) {
callback.call(scope, iterated[i], i, iterated);
}
}
}
return true;
} else {
return false;
}
},
/** /**
* Get the property of an object nested in one or more objects * Make a diff between two objects
* given an object such as a.b.c.d = 5, getNestedProperty(a, "b.c.d") will return 5. * @param {Array/Object} before is the object as it was before
* @param {Object} object the object to get the property from * @param {Array/Object} after is what it is now
* @param {String} property the path to the property as a string * @example:
* @returns the object or the the property value if found * With objects:
*/ *
getNestedProperty: function getNestedProperty(object, property) { * before = {a:1, b:2, c:3, d:4, f:6}
if (object && object instanceof Object) { * after = {a:1, b:20, d: 4, e: 5}
if (typeof property == "string" && property != "") { * will return :
var split = property.split("."); * {
return split.reduce(function (obj, prop) { * unchanged: ["a", "d"],
return obj && obj[prop]; * updated: ["b"],
}, object); * deleted: ["f"],
} else if (typeof property == "number") { * added: ["e"]
return object[property]; * }
} else { *
return object; * It also works with Arrays:
} *
} else { * before = [10, 20, 30]
return object; * after = [15, 20]
} * will return :
}, * {
* unchanged: [1],
* updated: [0],
* deleted: [2],
* added: []
* }
*
* @returns object
*/
objectsDiffs : function objectsDiffs(before, after) {
if (before instanceof Object && after instanceof Object) {
var unchanged = [],
updated = [],
deleted = [],
added = [];
// Look through the after object
this.loop(after, function (value, idx) {
// To get the added
if (typeof before[idx] == "undefined") {
added.push(idx);
// The updated
} else if (value !== before[idx]) {
updated.push(idx);
// And the unchanged
} else if (value === before[idx]) {
unchanged.push(idx);
}
});
// Loop through the before object
this.loop(before, function (value, idx) {
// To get the deleted
if (typeof after[idx] == "undefined") {
deleted.push(idx);
}
});
return {
updated: updated,
unchanged: unchanged,
added: added,
deleted: deleted
};
} else {
return false;
}
},
/** /**
* Set the property of an object nested in one or more objects * Transforms Arrays and Objects into valid JSON
* If the property doesn't exist, it gets created. * @param {Object/Array} object the object to JSONify
* @param {Object} object * @returns the JSONified object or false if failed
* @param {String} property */
* @param value the value to set jsonify: function jsonify(object) {
* @returns object if no assignment was made or the value if the assignment was made if (object instanceof Object) {
*/ return JSON.parse(JSON.stringify(object));
setNestedProperty: function setNestedProperty(object, property, value) { } else {
if (object && object instanceof Object) { return false;
if (typeof property == "string" && property != "") { }
var split = property.split("."); },
return split.reduce(function (obj, prop, idx) {
obj[prop] = obj[prop] || {};
if (split.length == (idx + 1)) {
obj[prop] = value;
}
return obj[prop];
}, object);
} else if (typeof property == "number") {
object[property] = value;
return object[property];
} else {
return object;
}
} else {
return object;
}
}
/**
* Clone an Array or an Object
* @param {Array/Object} object the object to clone
* @returns {Array/Object} the cloned object
*/
clone: function clone(object) {
if (object instanceof Array) {
return object.slice(0);
} else if (typeof object == "object" && object !== null && !(object instanceof RegExp)) {
return this.mixin(object, {});
} else {
return false;
}
},
}; /**
*
*
*
*
* Refactoring needed for the following
*
*
*
*
*
*/
/**
* Get the property of an object nested in one or more objects
* given an object such as a.b.c.d = 5, getNestedProperty(a, "b.c.d") will return 5.
* @param {Object} object the object to get the property from
* @param {String} property the path to the property as a string
* @returns the object or the the property value if found
*/
getNestedProperty: function getNestedProperty(object, property) {
if (object && object instanceof Object) {
if (typeof property == "string" && property !== "") {
var split = property.split(".");
return split.reduce(function (obj, prop) {
return obj && obj[prop];
}, object);
} else if (typeof property == "number") {
return object[property];
} else {
return object;
}
} else {
return object;
}
},
/**
* Set the property of an object nested in one or more objects
* If the property doesn't exist, it gets created.
* @param {Object} object
* @param {String} property
* @param value the value to set
* @returns object if no assignment was made or the value if the assignment was made
*/
setNestedProperty: function setNestedProperty(object, property, value) {
if (object && object instanceof Object) {
if (typeof property == "string" && property !== "") {
var split = property.split(".");
return split.reduce(function (obj, prop, idx) {
obj[prop] = obj[prop] || {};
if (split.length == (idx + 1)) {
obj[prop] = value;
}
return obj[prop];
}, object);
} else if (typeof property == "number") {
object[property] = value;
return object[property];
} else {
return object;
}
} else {
return object;
}
},
/**
* Get the closest number in an array given a base number
* Example: closest(30, [20, 0, 50, 29]) will return 3 as 29 is the closest item
* @param {Number} item the base number
* @param {Array} array the array of numbers to search into
* @returns {Number} the index of the closest item in the array
*/
closest: function closest(item, array) {
return _getClosest(item, array, function (comparedItem, item) {
return Math.abs(comparedItem - item);
});
},
/**
* Get the closest greater number in an array given a base number
* Example: closest(30, [20, 0, 50, 29]) will return 2 as 50 is the closest greater item
* @param {Number} item the base number
* @param {Array} array the array of numbers to search into
* @returns {Number} the index of the closest item in the array
*/
closestGreater: function closestGreater(item, array) {
return _getClosest(item, array, function (comparedItem, item) {
return comparedItem - item;
});
},
/**
* Get the closest lower number in an array given a base number
* Example: closest(30, [20, 0, 50, 29]) will return 0 as 20 is the closest lower item
* @param {Number} item the base number
* @param {Array} array the array of numbers to search into
* @returns {Number} the index of the closest item in the array
*/
closestLower: function closestLower(item, array) {
return _getClosest(item, array, function (comparedItem, item) {
return item - comparedItem;
});
}
};
}); });
/** /**
* Emily * Emily.js - http://flams.github.com/emily/
* Copyright(c) 2012-2013 Olivier Scherrer <pode.fr@gmail.com> * Copyright(c) 2012-2013 Olivier Scherrer <pode.fr@gmail.com>
* MIT Licensed * MIT Licensed
*/ */
...@@ -330,6 +399,8 @@ define('Observable',["Tools"], ...@@ -330,6 +399,8 @@ define('Observable',["Tools"],
*/ */
function Observable(Tools) { function Observable(Tools) {
/** /**
* Defines the Observable * Defines the Observable
* @private * @private
...@@ -363,6 +434,21 @@ function Observable(Tools) { ...@@ -363,6 +434,21 @@ function Observable(Tools) {
} }
}; };
/**
* Listen to an event just once before removing the handler
* @param {String} topic the topic to observe
* @param {Function} callback the callback to execute
* @param {Object} scope the scope in which to execute the callback
* @returns handle
*/
this.once = function once(topic, callback, scope) {
var handle = this.watch(topic, function () {
callback.apply(scope, arguments);
this.unwatch(handle);
}, this);
return handle;
};
/** /**
* Remove an observer * Remove an observer
* @param {Handle} handle returned by the watch method * @param {Handle} handle returned by the watch method
...@@ -398,14 +484,16 @@ function Observable(Tools) { ...@@ -398,14 +484,16 @@ function Observable(Tools) {
if (observers) { if (observers) {
Tools.loop(observers, function (value) { Tools.loop(observers, function (value) {
try { try {
value && value[0].apply(value[1] || null, args); if (value) {
value[0].apply(value[1] || null, args);
}
} catch (err) { } } catch (err) { }
}); });
return true; return true;
} else { } else {
return false; return false;
} }
}, };
/** /**
* Check if topic has the described observer * Check if topic has the described observer
...@@ -444,7 +532,7 @@ function Observable(Tools) { ...@@ -444,7 +532,7 @@ function Observable(Tools) {
}); });
/** /**
* Emily * Emily.js - http://flams.github.com/emily/
* Copyright(c) 2012-2013 Olivier Scherrer <pode.fr@gmail.com> * Copyright(c) 2012-2013 Olivier Scherrer <pode.fr@gmail.com>
* MIT Licensed * MIT Licensed
*/ */
...@@ -456,7 +544,9 @@ define('StateMachine',["Tools"], ...@@ -456,7 +544,9 @@ define('StateMachine',["Tools"],
*/ */
function StateMachine(Tools) { function StateMachine(Tools) {
/**
/**
* @param initState {String} the initial state * @param initState {String} the initial state
* @param diagram {Object} the diagram that describes the state machine * @param diagram {Object} the diagram that describes the state machine
* @example * @example
...@@ -475,228 +565,229 @@ function StateMachine(Tools) { ...@@ -475,228 +565,229 @@ function StateMachine(Tools) {
* *
* @return the stateMachine object * @return the stateMachine object
*/ */
function StateMachineConstructor($initState, $diagram) { function StateMachineConstructor($initState, $diagram) {
/**
* The list of states
* @private
*/
var _states = {},
/** /**
* The current state * The list of states
* @private * @private
*/ */
_currentState = ""; var _states = {},
/** /**
* Set the initialization state * The current state
* @param {String} name the name of the init state * @private
* @returns {Boolean} */
*/ _currentState = "";
this.init = function init(name) {
if (_states[name]) {
_currentState = name;
return true;
} else {
return false;
}
};
/** /**
* Add a new state * Set the initialization state
* @private * @param {String} name the name of the init state
* @param {String} name the name of the state * @returns {Boolean}
* @returns {State} a new state */
*/ this.init = function init(name) {
this.add = function add(name) { if (_states[name]) {
if (!_states[name]) { _currentState = name;
return _states[name] = new Transition(); return true;
} else { } else {
return _states[name]; return false;
} }
}; };
/** /**
* Get an existing state * Add a new state
* @private * @private
* @param {String} name the name of the state * @param {String} name the name of the state
* @returns {State} the state * @returns {State} a new state
*/ */
this.get = function get(name) { this.add = function add(name) {
return _states[name]; if (!_states[name]) {
}; var transition = _states[name] = new Transition();
return transition;
} else {
return _states[name];
}
};
/** /**
* Get the current state * Get an existing state
* @returns {String} * @private
*/ * @param {String} name the name of the state
this.getCurrent = function getCurrent() { * @returns {State} the state
return _currentState; */
}; this.get = function get(name) {
return _states[name];
};
/** /**
* Tell if the state machine has the given state * Get the current state
* @param {String} state the name of the state * @returns {String}
* @returns {Boolean} true if it has the given state */
*/ this.getCurrent = function getCurrent() {
this.has = function has(state) { return _currentState;
return _states.hasOwnProperty(state); };
};
/** /**
* Advances the state machine to a given state * Tell if the state machine has the given state
* @param {String} state the name of the state to advance the state machine to * @param {String} state the name of the state
* @returns {Boolean} true if it has the given state * @returns {Boolean} true if it has the given state
*/ */
this.advance = function advance(state) { this.has = function has(state) {
if (this.has(state)) { return _states.hasOwnProperty(state);
_currentState = state; };
return true;
} else {
return false;
}
};
/** /**
* Pass an event to the state machine * Advances the state machine to a given state
* @param {String} name the name of the event * @param {String} state the name of the state to advance the state machine to
* @returns {Boolean} true if the event exists in the current state * @returns {Boolean} true if it has the given state
*/ */
this.event = function event(name) { this.advance = function advance(state) {
var nextState; if (this.has(state)) {
_currentState = state;
return true;
} else {
return false;
}
};
nextState = _states[_currentState].event.apply(_states[_currentState].event, Tools.toArray(arguments)); /**
// False means that there's no such event * Pass an event to the state machine
// But undefined means that the state doesn't change * @param {String} name the name of the event
if (nextState === false) { * @returns {Boolean} true if the event exists in the current state
return false; */
} else { this.event = function event(name) {
// There could be no next state, so the current one remains var nextState;
if (nextState) {
// Call the exit action if any nextState = _states[_currentState].event.apply(_states[_currentState].event, Tools.toArray(arguments));
_states[_currentState].event("exit"); // False means that there's no such event
_currentState = nextState; // But undefined means that the state doesn't change
// Call the new state's entry action if any if (nextState === false) {
_states[_currentState].event("entry"); return false;
} } else {
return true; // There could be no next state, so the current one remains
} if (nextState) {
}; // Call the exit action if any
_states[_currentState].event("exit");
_currentState = nextState;
// Call the new state's entry action if any
_states[_currentState].event("entry");
}
return true;
}
};
/** /**
* Initializes the StateMachine with the given diagram * Initializes the StateMachine with the given diagram
*/ */
Tools.loop($diagram, function (transition, state) { Tools.loop($diagram, function (transition, state) {
var myState = this.add(state); var myState = this.add(state);
transition.forEach(function (params){ transition.forEach(function (params){
myState.add.apply(null, params); myState.add.apply(null, params);
}); });
}, this); }, this);
/** /**
* Sets its initial state * Sets its initial state
*/ */
this.init($initState); this.init($initState);
} }
/** /**
* Each state has associated transitions * Each state has associated transitions
* @constructor * @constructor
*/ */
function Transition() { function Transition() {
/** /**
* The list of transitions associated to a state * The list of transitions associated to a state
* @private * @private
*/ */
var _transitions = {}; var _transitions = {};
/** /**
* Add a new transition * Add a new transition
* @private * @private
* @param {String} event the event that will trigger the transition * @param {String} event the event that will trigger the transition
* @param {Function} action the function that is executed * @param {Function} action the function that is executed
* @param {Object} scope [optional] the scope in which to execute the action * @param {Object} scope [optional] the scope in which to execute the action
* @param {String} next [optional] the name of the state to transit to. * @param {String} next [optional] the name of the state to transit to.
* @returns {Boolean} true if success, false if the transition already exists * @returns {Boolean} true if success, false if the transition already exists
*/ */
this.add = function add(event, action, scope, next) { this.add = function add(event, action, scope, next) {
var arr = []; var arr = [];
if (_transitions[event]) { if (_transitions[event]) {
return false; return false;
} }
if (typeof event == "string" if (typeof event == "string" &&
&& typeof action == "function") { typeof action == "function") {
arr[0] = action; arr[0] = action;
if (typeof scope == "object") { if (typeof scope == "object") {
arr[1] = scope; arr[1] = scope;
} }
if (typeof scope == "string") { if (typeof scope == "string") {
arr[2] = scope; arr[2] = scope;
} }
if (typeof next == "string") { if (typeof next == "string") {
arr[2] = next; arr[2] = next;
} }
_transitions[event] = arr; _transitions[event] = arr;
return true; return true;
} }
return false; return false;
}; };
/** /**
* Check if a transition can be triggered with given event * Check if a transition can be triggered with given event
* @private * @private
* @param {String} event the name of the event * @param {String} event the name of the event
* @returns {Boolean} true if exists * @returns {Boolean} true if exists
*/ */
this.has = function has(event) { this.has = function has(event) {
return !!_transitions[event]; return !!_transitions[event];
}; };
/** /**
* Get a transition from it's event * Get a transition from it's event
* @private * @private
* @param {String} event the name of the event * @param {String} event the name of the event
* @return the transition * @return the transition
*/ */
this.get = function get(event) { this.get = function get(event) {
return _transitions[event] || false; return _transitions[event] || false;
}; };
/** /**
* Execute the action associated to the given event * Execute the action associated to the given event
* @param {String} event the name of the event * @param {String} event the name of the event
* @param {params} params to pass to the action * @param {params} params to pass to the action
* @private * @private
* @returns false if error, the next state or undefined if success (that sounds weird) * @returns false if error, the next state or undefined if success (that sounds weird)
*/ */
this.event = function event(event) { this.event = function event(newEvent) {
var _transition = _transitions[event]; var _transition = _transitions[newEvent];
if (_transition) { if (_transition) {
_transition[0].apply(_transition[1], Tools.toArray(arguments).slice(1)); _transition[0].apply(_transition[1], Tools.toArray(arguments).slice(1));
return _transition[2]; return _transition[2];
} else { } else {
return false; return false;
} }
}; };
}; }
return StateMachineConstructor; return StateMachineConstructor;
}); });
/** /**
* Emily * Emily.js - http://flams.github.com/emily/
* Copyright(c) 2012-2013 Olivier Scherrer <pode.fr@gmail.com> * Copyright(c) 2012-2013 Olivier Scherrer <pode.fr@gmail.com>
* MIT Licensed * MIT Licensed
*/ */
...@@ -708,107 +799,109 @@ define('Promise',["Observable", "StateMachine"], ...@@ -708,107 +799,109 @@ define('Promise',["Observable", "StateMachine"],
*/ */
function Promise(Observable, StateMachine) { function Promise(Observable, StateMachine) {
return function PromiseConstructor() {
/**
* The fulfilled value
* @private
*/
var _value = null,
/** return function PromiseConstructor() {
* The rejection reason
* @private
*/
_reason = null,
/** /**
* The funky observable * The fulfilled value
* @private * @private
*/ */
_observable = new Observable, var _value = null,
/** /**
* The state machine States & transitions * The rejection reason
* @private * @private
*/ */
_states = { _reason = null,
// The promise is pending
"Pending": [
// It can only be fulfilled when pending
["fulfill", function onFulfill(value) {
_value = value;
_observable.notify("fulfill", value);
// Then it transits to the fulfilled state
}, "Fulfilled"],
// it can only be rejected when pending
["reject", function onReject(reason) {
_reason = reason;
_observable.notify("reject", reason);
// Then it transits to the rejected state
}, "Rejected"],
// When pending, add the resolver to an observable
["toFulfill", function toFulfill(resolver) {
_observable.watch("fulfill", resolver);
}],
// When pending, add the resolver to an observable
["toReject", function toReject(resolver) {
_observable.watch("reject", resolver);
}]],
// When fulfilled,
"Fulfilled": [
// We directly call the resolver with the value
["toFulfill", function toFulfill(resolver) {
setTimeout(function () {
resolver(_value);
}, 0);
}]],
// When rejected
"Rejected": [
// We directly call the resolver with the reason
["toReject", function toReject(resolver) {
setTimeout(function () {
resolver(_reason);
}, 0);
}]]
},
/** /**
* The stateMachine * The funky observable
* @private * @private
*/ */
_stateMachine = new StateMachine("Pending", _states); _observable = new Observable(),
/** /**
* Fulfilled the promise. * The state machine States & transitions
* A promise can be fulfilld only once. * @private
* @param the fulfillment value */
* @returns the promise _states = {
*/
this.fulfill = function fulfill(value) { // The promise is pending
_stateMachine.event("fulfill", value); "Pending": [
return this;
}; // It can only be fulfilled when pending
["fulfill", function onFulfill(value) {
_value = value;
_observable.notify("fulfill", value);
// Then it transits to the fulfilled state
}, "Fulfilled"],
// it can only be rejected when pending
["reject", function onReject(reason) {
_reason = reason;
_observable.notify("reject", reason);
// Then it transits to the rejected state
}, "Rejected"],
// When pending, add the resolver to an observable
["toFulfill", function toFulfill(resolver) {
_observable.watch("fulfill", resolver);
}],
// When pending, add the resolver to an observable
["toReject", function toReject(resolver) {
_observable.watch("reject", resolver);
}]],
// When fulfilled,
"Fulfilled": [
// We directly call the resolver with the value
["toFulfill", function toFulfill(resolver) {
setTimeout(function () {
resolver(_value);
}, 0);
}]],
// When rejected
"Rejected": [
// We directly call the resolver with the reason
["toReject", function toReject(resolver) {
setTimeout(function () {
resolver(_reason);
}, 0);
}]]
},
/** /**
* Reject the promise. * The stateMachine
* A promise can be rejected only once. * @private
* @param the rejection value */
* @returns true if the rejection function was called _stateMachine = new StateMachine("Pending", _states);
*/
this.reject = function reject(reason) {
_stateMachine.event("reject", reason);
return this;
};
/** /**
* Fulfilled the promise.
* A promise can be fulfilld only once.
* @param the fulfillment value
* @returns the promise
*/
this.fulfill = function fulfill(value) {
_stateMachine.event("fulfill", value);
return this;
};
/**
* Reject the promise.
* A promise can be rejected only once.
* @param the rejection value
* @returns true if the rejection function was called
*/
this.reject = function reject(reason) {
_stateMachine.event("reject", reason);
return this;
};
/**
* The callbacks to call after fulfillment or rejection * The callbacks to call after fulfillment or rejection
* @param {Function} fulfillmentCallback the first parameter is a success function, it can be followed by a scope * @param {Function} fulfillmentCallback the first parameter is a success function, it can be followed by a scope
* @param {Function} the second, or third parameter is the rejection callback, it can also be followed by a scope * @param {Function} the second, or third parameter is the rejection callback, it can also be followed by a scope
...@@ -822,88 +915,88 @@ function Promise(Observable, StateMachine) { ...@@ -822,88 +915,88 @@ function Promise(Observable, StateMachine) {
* @returns {Promise} the new promise * @returns {Promise} the new promise
*/ */
this.then = function then() { this.then = function then() {
var promise = new PromiseConstructor; var promise = new PromiseConstructor();
// If a fulfillment callback is given // If a fulfillment callback is given
if (arguments[0] instanceof Function) { if (arguments[0] instanceof Function) {
// If the second argument is also a function, then no scope is given // If the second argument is also a function, then no scope is given
if (arguments[1] instanceof Function) { if (arguments[1] instanceof Function) {
_stateMachine.event("toFulfill", this.makeResolver(promise, arguments[0])); _stateMachine.event("toFulfill", this.makeResolver(promise, arguments[0]));
} else { } else {
// If the second argument is not a function, it's the scope // If the second argument is not a function, it's the scope
_stateMachine.event("toFulfill", this.makeResolver(promise, arguments[0], arguments[1])); _stateMachine.event("toFulfill", this.makeResolver(promise, arguments[0], arguments[1]));
} }
} else { } else {
// If no fulfillment callback given, give a default one // If no fulfillment callback given, give a default one
_stateMachine.event("toFulfill", this.makeResolver(promise, function () { _stateMachine.event("toFulfill", this.makeResolver(promise, function () {
promise.fulfill(_value); promise.fulfill(_value);
})); }));
} }
// if the second arguments is a callback, it's the rejection one, and the next argument is the scope // if the second arguments is a callback, it's the rejection one, and the next argument is the scope
if (arguments[1] instanceof Function) { if (arguments[1] instanceof Function) {
_stateMachine.event("toReject", this.makeResolver(promise, arguments[1], arguments[2])); _stateMachine.event("toReject", this.makeResolver(promise, arguments[1], arguments[2]));
} }
// if the third arguments is a callback, it's the rejection one, and the next arguments is the sopce // if the third arguments is a callback, it's the rejection one, and the next arguments is the sopce
if (arguments[2] instanceof Function) { if (arguments[2] instanceof Function) {
_stateMachine.event("toReject", this.makeResolver(promise, arguments[2], arguments[3])); _stateMachine.event("toReject", this.makeResolver(promise, arguments[2], arguments[3]));
} }
// If no rejection callback is given, give a default one // If no rejection callback is given, give a default one
if (!(arguments[1] instanceof Function) && if (!(arguments[1] instanceof Function) &&
!(arguments[2] instanceof Function)) { !(arguments[2] instanceof Function)) {
_stateMachine.event("toReject", this.makeResolver(promise, function () { _stateMachine.event("toReject", this.makeResolver(promise, function () {
promise.reject(_reason); promise.reject(_reason);
})); }));
} }
return promise; return promise;
}; };
/** /**
* Synchronize this promise with a thenable * Synchronize this promise with a thenable
* @returns {Boolean} false if the given sync is not a thenable * @returns {Boolean} false if the given sync is not a thenable
*/ */
this.sync = function sync(syncWith) { this.sync = function sync(syncWith) {
if (syncWith instanceof Object && syncWith.then) { if (syncWith instanceof Object && syncWith.then) {
var onFulfilled = function onFulfilled(value) { var onFulfilled = function onFulfilled(value) {
this.fulfill(value); this.fulfill(value);
}, },
onRejected = function onRejected(reason) { onRejected = function onRejected(reason) {
this.reject(reason); this.reject(reason);
}; };
syncWith.then(onFulfilled.bind(this), syncWith.then(onFulfilled.bind(this),
onRejected.bind(this)); onRejected.bind(this));
return true; return true;
} else { } else {
return false; return false;
} }
}; };
/** /**
* Make a resolver * Make a resolver
* for debugging only * for debugging only
* @private * @private
* @returns {Function} a closure * @returns {Function} a closure
*/ */
this.makeResolver = function makeResolver(promise, func, scope) { this.makeResolver = function makeResolver(promise, func, scope) {
return function resolver(value) { return function resolver(value) {
var returnedPromise; var returnedPromise;
try { try {
returnedPromise = func.call(scope, value); returnedPromise = func.call(scope, value);
if (!promise.sync(returnedPromise)) { if (!promise.sync(returnedPromise)) {
promise.fulfill(returnedPromise); promise.fulfill(returnedPromise);
} }
} catch (err) { } catch (err) {
promise.reject(err); promise.reject(err);
} }
} };
}; };
/** /**
...@@ -912,7 +1005,7 @@ function Promise(Observable, StateMachine) { ...@@ -912,7 +1005,7 @@ function Promise(Observable, StateMachine) {
* @private * @private
*/ */
this.getReason = function getReason() { this.getReason = function getReason() {
return _reason; return _reason;
}; };
/** /**
...@@ -921,40 +1014,40 @@ function Promise(Observable, StateMachine) { ...@@ -921,40 +1014,40 @@ function Promise(Observable, StateMachine) {
* @private * @private
*/ */
this.getValue = function getValue() { this.getValue = function getValue() {
return _value; return _value;
}; };
/** /**
* Get the promise's observable * Get the promise's observable
* for debugging only * for debugging only
* @private * @private
* @returns {Observable} * @returns {Observable}
*/ */
this.getObservable = function getObservable() { this.getObservable = function getObservable() {
return _observable; return _observable;
}; };
/** /**
* Get the promise's stateMachine * Get the promise's stateMachine
* for debugging only * for debugging only
* @private * @private
* @returns {StateMachine} * @returns {StateMachine}
*/ */
this.getStateMachine = function getStateMachine() { this.getStateMachine = function getStateMachine() {
return _stateMachine; return _stateMachine;
}; };
/** /**
* Get the statesMachine's states * Get the statesMachine's states
* for debugging only * for debugging only
* @private * @private
* @returns {Object} * @returns {Object}
*/ */
this.getStates = function getStates() { this.getStates = function getStates() {
return _states; return _states;
}; };
} };
...@@ -962,7 +1055,7 @@ function Promise(Observable, StateMachine) { ...@@ -962,7 +1055,7 @@ function Promise(Observable, StateMachine) {
}); });
/** /**
* Emily * Emily.js - http://flams.github.com/emily/
* Copyright(c) 2012-2013 Olivier Scherrer <pode.fr@gmail.com> * Copyright(c) 2012-2013 Olivier Scherrer <pode.fr@gmail.com>
* MIT Licensed * MIT Licensed
*/ */
...@@ -975,297 +1068,379 @@ define('Store',["Observable", "Tools"], ...@@ -975,297 +1068,379 @@ define('Store',["Observable", "Tools"],
*/ */
function Store(Observable, Tools) { function Store(Observable, Tools) {
/**
* Defines the Store
* @param {Array/Object} the data to initialize the store with
* @returns
*/
return function StoreConstructor($data) {
/** /**
* Where the data is stored * Defines the Store
* @private * @param {Array/Object} the data to initialize the store with
*/ * @returns
var _data = Tools.clone($data) || {}, */
return function StoreConstructor($data) {
/** /**
* The observable for publishing changes on the store iself * Where the data is stored
* @private * @private
*/ */
_storeObservable = new Observable(), var _data = Tools.clone($data) || {},
/** /**
* The observable for publishing changes on a value * The observable for publishing changes on the store iself
* @private * @private
*/ */
_valueObservable = new Observable(), _storeObservable = new Observable(),
/** /**
* Gets the difference between two objects and notifies them * The observable for publishing changes on a value
* @private * @private
* @param {Object} previousData */
*/ _valueObservable = new Observable(),
_notifyDiffs = function _notifyDiffs(previousData) {
var diffs = Tools.objectsDiffs(previousData, _data);
["updated",
"deleted",
"added"].forEach(function (value) {
diffs[value].forEach(function (dataIndex) {
_storeObservable.notify(value, dataIndex, _data[dataIndex]);
_valueObservable.notify(dataIndex, _data[dataIndex], value);
});
});
};
/** /**
* Get the number of items in the store * Saves the handles for the subscriptions of the computed properties
* @returns {Number} the number of items in the store * @private
*/ */
this.getNbItems = function() { _computed = [],
return _data instanceof Array ? _data.length : Tools.count(_data);
};
/** /**
* Count is an alias for getNbItems * Gets the difference between two objects and notifies them
* @returns {Number} the number of items in the store * @private
*/ * @param {Object} previousData
this.count = this.getNbItems; */
_notifyDiffs = function _notifyDiffs(previousData) {
var diffs = Tools.objectsDiffs(previousData, _data);
["updated",
"deleted",
"added"].forEach(function (value) {
diffs[value].forEach(function (dataIndex) {
_storeObservable.notify(value, dataIndex, _data[dataIndex]);
_valueObservable.notify(dataIndex, _data[dataIndex], value);
});
});
};
/** /**
* Get a value from its index * Get the number of items in the store
* @param {String} name the name of the index * @returns {Number} the number of items in the store
* @returns the value */
*/ this.getNbItems = function() {
this.get = function get(name) { return _data instanceof Array ? _data.length : Tools.count(_data);
return _data[name]; };
};
/** /**
* Checks if the store has a given value * Count is an alias for getNbItems
* @param {String} name the name of the index * @returns {Number} the number of items in the store
* @returns {Boolean} true if the value exists */
*/ this.count = this.getNbItems;
this.has = function has(name) {
return _data.hasOwnProperty(name);
};
/** /**
* Set a new value and overrides an existing one * Get a value from its index
* @param {String} name the name of the index * @param {String} name the name of the index
* @param value the value to assign * @returns the value
* @returns true if value is set */
*/ this.get = function get(name) {
this.set = function set(name, value) { return _data[name];
var hasPrevious, };
previousValue,
action;
if (typeof name != "undefined") {
hasPrevious = this.has(name);
previousValue = this.get(name);
_data[name] = value;
action = hasPrevious ? "updated" : "added";
_storeObservable.notify(action, name, _data[name], previousValue);
_valueObservable.notify(name, _data[name], action, previousValue);
return true;
} else {
return false;
}
};
/** /**
* Update the property of an item. * Checks if the store has a given value
* @param {String} name the name of the index * @param {String} name the name of the index
* @param {String} property the property to modify. * @returns {Boolean} true if the value exists
* @param value the value to assign */
* @returns false if the Store has no name index this.has = function has(name) {
*/ return _data.hasOwnProperty(name);
this.update = function update(name, property, value) { };
var item;
if (this.has(name)) {
item = this.get(name);
Tools.setNestedProperty(item, property, value);
_storeObservable.notify("updated", property, value);
_valueObservable.notify(name, item, "updated");
return true;
} else {
return false;
}
};
/** /**
* Delete value from its index * Set a new value and overrides an existing one
* @param {String} name the name of the index from which to delete the value * @param {String} name the name of the index
* @returns true if successfully deleted. * @param value the value to assign
*/ * @returns true if value is set
this.del = function del(name) { */
if (this.has(name)) { this.set = function set(name, value) {
if (!this.alter("splice", name, 1)) { var hasPrevious,
delete _data[name]; previousValue,
_storeObservable.notify("deleted", name); action;
_valueObservable.notify(name, _data[name], "deleted");
} if (typeof name != "undefined") {
return true; hasPrevious = this.has(name);
} else { previousValue = this.get(name);
return false; _data[name] = value;
} action = hasPrevious ? "updated" : "added";
}; _storeObservable.notify(action, name, _data[name], previousValue);
_valueObservable.notify(name, _data[name], action, previousValue);
return true;
} else {
return false;
}
};
/** /**
* Delete multiple indexes. Prefer this one over multiple del calls. * Update the property of an item.
* @param {Array} * @param {String} name the name of the index
* @returns false if param is not an array. * @param {String} property the property to modify.
*/ * @param value the value to assign
this.delAll = function delAll(indexes) { * @returns false if the Store has no name index
if (indexes instanceof Array) { */
// Indexes must be removed from the greatest to the lowest this.update = function update(name, property, value) {
// To avoid trying to remove indexes that don't exist. var item;
// i.e: given [0, 1, 2], remove 1, then 2, 2 doesn't exist anymore if (this.has(name)) {
indexes.sort(Tools.compareNumbers) item = this.get(name);
.reverse() Tools.setNestedProperty(item, property, value);
.forEach(this.del, this); _storeObservable.notify("updated", property, value);
return true; _valueObservable.notify(name, item, "updated");
} else { return true;
return false; } else {
} return false;
}; }
};
/** /**
* Alter the data be calling one of it's method * Delete value from its index
* When the modifications are done, it notifies on changes. * @param {String} name the name of the index from which to delete the value
* @param {String} func the name of the method * @returns true if successfully deleted.
* @returns the result of the method call */
*/ this.del = function del(name) {
this.alter = function alter(func) { if (this.has(name)) {
var apply, if (!this.alter("splice", name, 1)) {
previousData; delete _data[name];
_storeObservable.notify("deleted", name);
if (_data[func]) { _valueObservable.notify(name, _data[name], "deleted");
previousData = Tools.clone(_data); }
apply = _data[func].apply(_data, Array.prototype.slice.call(arguments, 1)); return true;
_notifyDiffs(previousData); } else {
return apply; return false;
} else { }
return false; };
}
};
/** /**
* proxy is an alias for alter * Delete multiple indexes. Prefer this one over multiple del calls.
*/ * @param {Array}
this.proxy = this.alter; * @returns false if param is not an array.
*/
this.delAll = function delAll(indexes) {
if (indexes instanceof Array) {
// Indexes must be removed from the greatest to the lowest
// To avoid trying to remove indexes that don't exist.
// i.e: given [0, 1, 2], remove 1, then 2, 2 doesn't exist anymore
indexes.sort(Tools.compareNumbers)
.reverse()
.forEach(this.del, this);
return true;
} else {
return false;
}
};
/** /**
* Watch the store's modifications * Alter the data by calling one of it's method
* @param {String} added/updated/deleted * When the modifications are done, it notifies on changes.
* @param {Function} func the function to execute * If the function called doesn't alter the data, consider using proxy instead
* @param {Object} scope the scope in which to execute the function * which is much, much faster.
* @returns {Handle} the subscribe's handler to use to stop watching * @param {String} func the name of the method
*/ * @params {*} any number of params to be given to the func
this.watch = function watch(name, func, scope) { * @returns the result of the method call
return _storeObservable.watch(name, func, scope); */
}; this.alter = function alter(func) {
var apply,
previousData;
if (_data[func]) {
previousData = Tools.clone(_data);
apply = this.proxy.apply(this, arguments);
_notifyDiffs(previousData);
_storeObservable.notify("altered", _data, previousData);
return apply;
} else {
return false;
}
};
/** /**
* Unwatch the store modifications * Proxy is similar to alter but doesn't trigger events.
* @param {Handle} handle the handler returned by the watch function * It's preferable to call proxy for functions that don't
* @returns * update the interal data source, like slice or filter.
*/ * @param {String} func the name of the method
this.unwatch = function unwatch(handle) { * @params {*} any number of params to be given to the func
return _storeObservable.unwatch(handle); * @returns the result of the method call
}; */
this.proxy = function proxy(func) {
if (_data[func]) {
return _data[func].apply(_data, Array.prototype.slice.call(arguments, 1));
} else {
return false;
}
};
/** /**
* Get the observable used for watching store's modifications * Watch the store's modifications
* Should be used only for debugging * @param {String} added/updated/deleted
* @returns {Observable} the Observable * @param {Function} func the function to execute
*/ * @param {Object} scope the scope in which to execute the function
this.getStoreObservable = function getStoreObservable() { * @returns {Handle} the subscribe's handler to use to stop watching
return _storeObservable; */
}; this.watch = function watch(name, func, scope) {
return _storeObservable.watch(name, func, scope);
};
/** /**
* Watch a value's modifications * Unwatch the store modifications
* @param {String} name the name of the value to watch for * @param {Handle} handle the handler returned by the watch function
* @param {Function} func the function to execute * @returns
* @param {Object} scope the scope in which to execute the function */
* @returns handler to pass to unwatchValue this.unwatch = function unwatch(handle) {
*/ return _storeObservable.unwatch(handle);
this.watchValue = function watchValue(name, func, scope) { };
return _valueObservable.watch(name, func, scope);
};
/** /**
* Unwatch the value's modifications * Get the observable used for watching store's modifications
* @param {Handler} handler the handler returned by the watchValue function * Should be used only for debugging
* @private * @returns {Observable} the Observable
* @returns true if unwatched */
*/ this.getStoreObservable = function getStoreObservable() {
this.unwatchValue = function unwatchValue(handler) { return _storeObservable;
return _valueObservable.unwatch(handler); };
};
/** /**
* Get the observable used for watching value's modifications * Watch a value's modifications
* Should be used only for debugging * @param {String} name the name of the value to watch for
* @private * @param {Function} func the function to execute
* @returns {Observable} the Observable * @param {Object} scope the scope in which to execute the function
*/ * @returns handler to pass to unwatchValue
this.getValueObservable = function getValueObservable() { */
return _valueObservable; this.watchValue = function watchValue(name, func, scope) {
}; return _valueObservable.watch(name, func, scope);
};
/** /**
* Loop through the data * Unwatch the value's modifications
* @param {Function} func the function to execute on each data * @param {Handler} handler the handler returned by the watchValue function
* @param {Object} scope the scope in wich to run the callback * @private
*/ * @returns true if unwatched
this.loop = function loop(func, scope) { */
Tools.loop(_data, func, scope); this.unwatchValue = function unwatchValue(handler) {
}; return _valueObservable.unwatch(handler);
};
/** /**
* Reset all data and get notifications on changes * Get the observable used for watching value's modifications
* @param {Arra/Object} data the new data * Should be used only for debugging
* @returns {Boolean} * @private
*/ * @returns {Observable} the Observable
this.reset = function reset(data) { */
if (data instanceof Object) { this.getValueObservable = function getValueObservable() {
var previousData = Tools.clone(_data); return _valueObservable;
_data = Tools.clone(data) || {}; };
_notifyDiffs(previousData);
return true;
} else {
return false;
}
}; /**
* Loop through the data
* @param {Function} func the function to execute on each data
* @param {Object} scope the scope in wich to run the callback
*/
this.loop = function loop(func, scope) {
Tools.loop(_data, func, scope);
};
/** /**
* Returns a JSON version of the data * Reset all data and get notifications on changes
* Use dump if you want all the data as a plain js object * @param {Arra/Object} data the new data
* @returns {String} the JSON * @returns {Boolean}
*/ */
this.toJSON = function toJSON() { this.reset = function reset(data) {
return JSON.stringify(_data); if (data instanceof Object) {
}; var previousData = Tools.clone(_data);
_data = Tools.clone(data) || {};
_notifyDiffs(previousData);
_storeObservable.notify("resetted", _data, previousData);
return true;
} else {
return false;
}
/** };
* Returns the store's data
* @returns {Object} the data /**
*/ * Compute a new property from other properties.
this.dump = function dump() { * The computed property will look exactly similar to any none
return _data; * computed property, it can be watched upon.
}; * @param {String} name the name of the computed property
}; * @param {Array} computeFrom a list of properties to compute from
* @param {Function} callback the callback to compute the property
* @param {Object} scope the scope in which to execute the callback
* @returns {Boolean} false if wrong params given to the function
*/
this.compute = function compute(name, computeFrom, callback, scope) {
var args = [];
if (typeof name == "string" &&
typeof computeFrom == "object" &&
typeof callback == "function" &&
!this.isCompute(name)) {
_computed[name] = [];
Tools.loop(computeFrom, function (property) {
_computed[name].push(this.watchValue(property, function () {
this.set(name, callback.call(scope));
}, this));
}, this);
this.set(name, callback.call(scope));
return true;
} else {
return false;
}
};
/**
* Remove a computed property
* @param {String} name the name of the computed to remove
* @returns {Boolean} true if the property is removed
*/
this.removeCompute = function removeCompute(name) {
if (this.isCompute(name)) {
Tools.loop(_computed[name], function (handle) {
this.unwatchValue(handle);
}, this);
this.del(name);
return true;
} else {
return false;
}
};
/**
* Tells if a property is a computed property
* @param {String} name the name of the property to test
* @returns {Boolean} true if it's a computed property
*/
this.isCompute = function isCompute(name) {
return !!_computed[name];
};
/**
* Returns a JSON version of the data
* Use dump if you want all the data as a plain js object
* @returns {String} the JSON
*/
this.toJSON = function toJSON() {
return JSON.stringify(_data);
};
/**
* Returns the store's data
* @returns {Object} the data
*/
this.dump = function dump() {
return _data;
};
};
}); });
/** /**
* Emily * Emily.js - http://flams.github.com/emily/
* Copyright(c) 2012-2013 Olivier Scherrer <pode.fr@gmail.com> * Copyright(c) 2012-2013 Olivier Scherrer <pode.fr@gmail.com>
* MIT Licensed * MIT Licensed
*/ */
define('Transport',[], define('Transport',[],
/** /**
* @class * @class
...@@ -1275,96 +1450,339 @@ define('Transport',[], ...@@ -1275,96 +1450,339 @@ define('Transport',[],
*/ */
function Transport() { function Transport() {
/**
* Create a Transport
* @param {Emily Store} [optionanl] $reqHandlers an object containing the request handlers
* @returns
*/
return function TransportConstructor($reqHandlers) {
/** /**
* The request handlers * Create a Transport
* @private * @param {Emily Store} [optionanl] $reqHandlers an object containing the request handlers
*/ * @returns
var _reqHandlers = null; */
return function TransportConstructor($reqHandlers) {
/** /**
* Set the requests handlers object * The request handlers
* @param {Emily Store} reqHandlers an object containing the requests handlers * @private
* @returns */
*/ var _reqHandlers = null;
this.setReqHandlers = function setReqHandlers(reqHandlers) {
if (reqHandlers instanceof Object) {
_reqHandlers = reqHandlers;
return true;
} else {
return false;
}
};
/** /**
* Get the requests handlers * Set the requests handlers object
* @returns{ Emily Store} reqHandlers the object containing the requests handlers * @param {Emily Store} reqHandlers an object containing the requests handlers
*/ * @returns
this.getReqHandlers = function getReqHandlers() { */
return _reqHandlers; this.setReqHandlers = function setReqHandlers(reqHandlers) {
}; if (reqHandlers instanceof Object) {
_reqHandlers = reqHandlers;
return true;
} else {
return false;
}
};
/** /**
* Issue a request to a request handler * Get the requests handlers
* @param {String} reqHandler the name of the request handler to issue the request to * @returns{ Emily Store} reqHandlers the object containing the requests handlers
* @param {Object} data the data, or payload, to send to the request handler */
* @param {Function} callback the function to execute with the result this.getReqHandlers = function getReqHandlers() {
* @param {Object} scope the scope in which to execute the callback return _reqHandlers;
* @returns };
*/
this.request = function request(reqHandler, data, callback, scope) {
if (_reqHandlers.has(reqHandler)
&& typeof data != "undefined") {
_reqHandlers.get(reqHandler)(data, function () { /**
callback && callback.apply(scope, arguments); * Issue a request to a request handler
}); * @param {String} reqHandler the name of the request handler to issue the request to
return true; * @param {Object} data the data, or payload, to send to the request handler
} else { * @param {Function} callback the function to execute with the result
return false; * @param {Object} scope the scope in which to execute the callback
} * @returns
}; */
this.request = function request(reqHandler, data, callback, scope) {
if (_reqHandlers.has(reqHandler) &&
typeof data != "undefined") {
_reqHandlers.get(reqHandler)(data, function () {
if (callback) {
callback.apply(scope, arguments);
}
});
return true;
} else {
return false;
}
};
/** /**
* Issue a request to a reqHandler but keep listening for the response as it can be sent in several chunks * Issue a request to a reqHandler but keep listening for the response as it can be sent in several chunks
* or remain open as long as the abort funciton is not called * or remain open as long as the abort funciton is not called
* @param {String} reqHandler the name of the request handler to issue the request to * @param {String} reqHandler the name of the request handler to issue the request to
* @param {Object} data the data, or payload, to send to the request handler * @param {Object} data the data, or payload, to send to the request handler
* @param {Function} callback the function to execute with the result * @param {Function} callback the function to execute with the result
* @param {Object} scope the scope in which to execute the callback * @param {Object} scope the scope in which to execute the callback
* @returns {Function} the abort function to call to stop listening * @returns {Function} the abort function to call to stop listening
*/ */
this.listen = function listen(reqHandler, data, callback, scope) { this.listen = function listen(reqHandler, data, callback, scope) {
if (_reqHandlers.has(reqHandler) if (_reqHandlers.has(reqHandler) &&
&& typeof data != "undefined" typeof data != "undefined" &&
&& typeof callback == "function") { typeof callback == "function") {
var func = function () { var func = function () {
callback.apply(scope, arguments); callback.apply(scope, arguments);
}, },
abort; abort;
abort = _reqHandlers.get(reqHandler)(data, func, func); abort = _reqHandlers.get(reqHandler)(data, func, func);
return function () { return function () {
if (typeof abort == "function") { if (typeof abort == "function") {
abort(); abort();
} else if (typeof abort == "object" && typeof abort.func == "function") { } else if (typeof abort == "object" && typeof abort.func == "function") {
abort.func.call(abort.scope); abort.func.call(abort.scope);
} }
}; };
} else { } else {
return false; return false;
} }
}; };
this.setReqHandlers($reqHandlers); this.setReqHandlers($reqHandlers);
}; };
}); });
/**
* Emily.js - http://flams.github.com/emily/
* Copyright(c) 2012-2013 Olivier Scherrer <pode.fr@gmail.com>
* MIT Licensed
*/
define('Router',["Observable", "Store", "Tools"],
/**
* @class
* Routing allows for navigating in an application by defining routes.
*/
function Router(Observable, Store, Tools) {
return function RouterConstructor() {
/**
* The routes observable (the applications use it)
* @private
*/
var _routes = new Observable(),
/**
* The events observable (used by Routing)
* @private
*/
_events = new Observable(),
/**
* The routing history
* @private
*/
_history = new Store([]),
/**
* For navigating through the history, remembers the current position
* @private
*/
_currentPos = -1,
/**
* The depth of the history
* @private
*/
_maxHistory = 10;
/**
* Only for debugging
* @private
*/
this.getRoutesObservable = function getRoutesObservable() {
return _routes;
};
/**
* Only for debugging
* @private
*/
this.getEventsObservable = function getEventsObservable() {
return _events;
};
/**
* Set the maximum length of history
* As the user navigates through the application, the
* routeur keeps track of the history. Set the depth of the history
* depending on your need and the amount of memory that you can allocate it
* @param {Number} maxHistory the depth of history
* @returns {Boolean} true if maxHistory is equal or greater than 0
*/
this.setMaxHistory = function setMaxHistory(maxHistory) {
if (maxHistory >= 0) {
_maxHistory = maxHistory;
return true;
} else {
return false;
}
};
/**
* Get the current max history setting
* @returns {Number} the depth of history
*/
this.getMaxHistory = function getMaxHistory() {
return _maxHistory;
};
/**
* Set a new route
* @param {String} route the name of the route
* @param {Function} func the function to be execute when navigating to the route
* @param {Object} scope the scope in which to execute the function
* @returns a handle to remove the route
*/
this.set = function set() {
return _routes.watch.apply(_routes, arguments);
};
/**
* Remove a route
* @param {Object} handle the handle provided by the set method
* @returns true if successfully removed
*/
this.unset = function unset(handle) {
return _routes.unwatch(handle);
};
/**
* Navigate to a route
* @param {String} route the route to navigate to
* @param {*} *params
* @returns
*/
this.navigate = function get(route, params) {
if (this.load.apply(this, arguments)) {
// Before adding a new route to the history, we must clear the forward history
_history.proxy("splice", _currentPos +1, _history.count());
_history.proxy("push", Tools.toArray(arguments));
this.ensureMaxHistory(_history);
_currentPos = _history.count() -1;
return true;
} else {
return false;
}
};
/**
* Ensure that history doesn't grow bigger than the max history setting
* @param {Store} history the history store
* @private
*/
this.ensureMaxHistory = function ensureMaxHistory(history) {
var count = history.count(),
max = this.getMaxHistory(),
excess = count - max;
if (excess > 0) {
history.proxy("splice", 0, excess);
}
};
/**
* Actually loads the route
* @private
*/
this.load = function load() {
var copy = Tools.toArray(arguments);
if (_routes.notify.apply(_routes, copy)) {
copy.unshift("route");
_events.notify.apply(_events, copy);
return true;
} else {
return false;
}
};
/**
* Watch for route changes
* @param {Function} func the func to execute when the route changes
* @param {Object} scope the scope in which to execute the function
* @returns {Object} the handle to unwatch for route changes
*/
this.watch = function watch(func, scope) {
return _events.watch("route", func, scope);
};
/**
* Unwatch routes changes
* @param {Object} handle the handle was returned by the watch function
* @returns true if unwatch
*/
this.unwatch = function unwatch(handle) {
return _events.unwatch(handle);
};
/**
* Get the history store, for debugging only
* @private
*/
this.getHistoryStore = function getHistoryStore() {
return _history;
};
/**
* Get the current length of history
* @returns {Number} the length of history
*/
this.getHistoryCount = function getHistoryCount() {
return _history.count();
};
/**
* Flush the entire history
*/
this.clearHistory = function clearHistory() {
_history.reset([]);
};
/**
* Go back and forth in the history
* @param {Number} nb the amount of history to rewind/forward
* @returns true if history exists
*/
this.go = function go(nb) {
var history = _history.get(_currentPos + nb);
if (history) {
_currentPos += nb;
this.load.apply(this, history);
return true;
} else {
return false;
}
};
/**
* Go back in the history, short for go(-1)
* @returns
*/
this.back = function back() {
return this.go(-1);
};
/**
* Go forward in the history, short for go(1)
* @returns
*/
this.forward = function forward() {
return this.go(1);
};
};
});
\ No newline at end of file
...@@ -12,6 +12,8 @@ ...@@ -12,6 +12,8 @@
define('DomUtils',["Tools"], function (Tools) { define('DomUtils',["Tools"], function (Tools) {
return { return {
/** /**
* Returns a NodeList including the given dom node, * Returns a NodeList including the given dom node,
...@@ -127,375 +129,406 @@ define('Bind.plugin',["Store", "Observable", "Tools", "DomUtils"], ...@@ -127,375 +129,406 @@ define('Bind.plugin',["Store", "Observable", "Tools", "DomUtils"],
*/ */
function BindPlugin(Store, Observable, Tools, DomUtils) { function BindPlugin(Store, Observable, Tools, DomUtils) {
return function BindPluginConstructor($model, $bindings) {
/** return function BindPluginConstructor($model, $bindings) {
* The model to watch
* @private /**
*/ * The model to watch
var _model = null, * @private
*/
/** var _model = null,
* The list of custom bindings
* @private /**
*/ * The list of custom bindings
_bindings = {}, * @private
*/
/** _bindings = {},
* The list of itemRenderers
* each foreach has its itemRenderer /**
* @private * The list of itemRenderers
*/ * each foreach has its itemRenderer
_itemRenderers = {}; * @private
*/
/** _itemRenderers = {},
* The observers handlers
* for debugging only /**
* @private * The observers handlers
*/ * @private
this.observers = {}; */
_observers = {};
/**
* Define the model to watch for /**
* @param {Store} model the model to watch for changes * Exposed for debugging purpose
* @returns {Boolean} true if the model was set * @private
*/ */
this.setModel = function setModel(model) { this.observers = _observers;
if (model instanceof Store) {
// Set the model function _removeObserversForId(id) {
_model = model; if (_observers[id]) {
return true; _observers[id].forEach(function (handler) {
} else { _model.unwatchValue(handler);
return false; });
} delete _observers[id];
}; }
}
/**
* Get the store that is watched for /**
* for debugging only * Define the model to watch for
* @private * @param {Store} model the model to watch for changes
* @returns the Store * @returns {Boolean} true if the model was set
*/ */
this.getModel = function getModel() { this.setModel = function setModel(model) {
return _model; if (model instanceof Store) {
}; // Set the model
_model = model;
/** return true;
* The item renderer defines a dom node that can be duplicated } else {
* It is made available for debugging purpose, don't use it return false;
* @private }
*/ };
this.ItemRenderer = function ItemRenderer($plugins, $rootNode) {
/**
/** * Get the store that is watched for
* The node that will be cloned * for debugging only
* @private * @private
*/ * @returns the Store
var _node = null, */
this.getModel = function getModel() {
/** return _model;
* The object that contains plugins.name and plugins.apply };
* @private
*/ /**
_plugins = null, * The item renderer defines a dom node that can be duplicated
* It is made available for debugging purpose, don't use it
/** * @private
* The _rootNode where to append the created items */
* @private this.ItemRenderer = function ItemRenderer($plugins, $rootNode) {
*/
_rootNode = null, /**
* The node that will be cloned
/** * @private
* The lower boundary */
* @private var _node = null,
*/
_start = null, /**
* The object that contains plugins.name and plugins.apply
/** * @private
* The number of item to display */
* @private _plugins = null,
*/
_nb = null; /**
* The _rootNode where to append the created items
/** * @private
* Set the duplicated node */
* @private _rootNode = null,
*/
this.setRenderer = function setRenderer(node) { /**
_node = node; * The lower boundary
return true; * @private
}; */
_start = null,
/**
* Returns the node that is going to be used for rendering /**
* @private * The number of item to display
* @returns the node that is duplicated * @private
*/ */
this.getRenderer = function getRenderer() { _nb = null;
return _node;
}; /**
* Set the duplicated node
/** * @private
* Sets the rootNode and gets the node to copy */
* @private this.setRenderer = function setRenderer(node) {
* @param {HTMLElement|SVGElement} rootNode _node = node;
* @returns return true;
*/ };
this.setRootNode = function setRootNode(rootNode) {
var renderer; /**
if (DomUtils.isAcceptedType(rootNode)) { * Returns the node that is going to be used for rendering
_rootNode = rootNode; * @private
renderer = _rootNode.querySelector("*"); * @returns the node that is duplicated
this.setRenderer(renderer); */
renderer && _rootNode.removeChild(renderer); this.getRenderer = function getRenderer() {
return true; return _node;
} else { };
return false;
} /**
}; * Sets the rootNode and gets the node to copy
* @private
/** * @param {HTMLElement|SVGElement} rootNode
* Gets the rootNode * @returns
* @private */
* @returns _rootNode this.setRootNode = function setRootNode(rootNode) {
*/ var renderer;
this.getRootNode = function getRootNode() { if (DomUtils.isAcceptedType(rootNode)) {
return _rootNode; _rootNode = rootNode;
}; renderer = _rootNode.querySelector("*");
this.setRenderer(renderer);
/** if (renderer) {
* Set the plugins objet that contains the name and the apply function _rootNode.removeChild(renderer);
* @private }
* @param plugins return true;
* @returns true } else {
*/ return false;
this.setPlugins = function setPlugins(plugins) { }
_plugins = plugins; };
return true;
}; /**
* Gets the rootNode
/** * @private
* Get the plugins object * @returns _rootNode
* @private */
* @returns the plugins object this.getRootNode = function getRootNode() {
*/ return _rootNode;
this.getPlugins = function getPlugins() { };
return _plugins;
}; /**
* Set the plugins objet that contains the name and the apply function
/** * @private
* The nodes created from the items are stored here * @param plugins
* @private * @returns true
*/ */
this.items = new Store([]); this.setPlugins = function setPlugins(plugins) {
_plugins = plugins;
/** return true;
* Set the start limit };
* @private
* @param {Number} start the value to start rendering the items from /**
* @returns the value * Get the plugins object
*/ * @private
this.setStart = function setStart(start) { * @returns the plugins object
return _start = parseInt(start, 10); */
}; this.getPlugins = function getPlugins() {
return _plugins;
/** };
* Get the start value
* @private /**
* @returns the start value * The nodes created from the items are stored here
*/ * @private
this.getStart = function getStart() { */
return _start; this.items = {};
};
/**
/** * Set the start limit
* Set the number of item to display * @private
* @private * @param {Number} start the value to start rendering the items from
* @param {Number/String} nb the number of item to display or "*" for all * @returns the value
* @returns the value */
*/ this.setStart = function setStart(start) {
this.setNb = function setNb(nb) { _start = parseInt(start, 10);
return _nb = nb == "*" ? nb : parseInt(nb, 10); return _start;
}; };
/** /**
* Get the number of item to display * Get the start value
* @private * @private
* @returns the value * @returns the start value
*/ */
this.getNb = function getNb() { this.getStart = function getStart() {
return _nb; return _start;
}; };
/** /**
* Adds a new item and adds it in the items list * Set the number of item to display
* @private * @private
* @param {Number} id the id of the item * @param {Number/String} nb the number of item to display or "*" for all
* @returns * @returns the value
*/ */
this.addItem = function addItem(id) { this.setNb = function setNb(nb) {
var node, _nb = nb == "*" ? nb : parseInt(nb, 10);
next; return _nb;
};
if (typeof id == "number" && !this.items.get(id)) {
node = this.create(id); /**
if (node) { * Get the number of item to display
// IE (until 9) apparently fails to appendChild when insertBefore's second argument is null, hence this. * @private
next = this.getNextItem(id); * @returns the value
next ? _rootNode.insertBefore(node, next) : _rootNode.appendChild(node); */
return true; this.getNb = function getNb() {
} else { return _nb;
return false; };
}
} else { /**
return false; * Adds a new item and adds it in the items list
} * @private
}; * @param {Number} id the id of the item
* @returns
/** */
* Get the next item in the item store given an id. this.addItem = function addItem(id) {
* @private var node,
* @param {Number} id the id to start from next;
* @returns
*/ if (typeof id == "number" && !this.items[id]) {
this.getNextItem = function getNextItem(id) { next = this.getNextItem(id);
return this.items.alter("slice", id+1).filter(function (value) { node = this.create(id);
if (DomUtils.isAcceptedType(value)) { if (node) {
return true; // IE (until 9) apparently fails to appendChild when insertBefore's second argument is null, hence this.
} if (next) {
})[0]; _rootNode.insertBefore(node, next);
}; } else {
_rootNode.appendChild(node);
/** }
* Remove an item from the dom and the items list return true;
* @private } else {
* @param {Number} id the id of the item to remove return false;
* @returns }
*/ } else {
this.removeItem = function removeItem(id) { return false;
var item = this.items.get(id); }
if (item) { };
_rootNode.removeChild(item);
this.items.set(id); /**
return true; * Get the next item in the item store given an id.
} else { * @private
return false; * @param {Number} id the id to start from
} * @returns
}; */
this.getNextItem = function getNextItem(id) {
/** var keys = Object.keys(this.items).map(function (string) {
* create a new node. Actually makes a clone of the initial one return Number(string);
* and adds pluginname_id to each node, then calls plugins.apply to apply all plugins }),
* @private closest = Tools.closestGreater(id, keys),
* @param id closestId = keys[closest];
* @param pluginName
* @returns the associated node // Only return if different
*/ if (closestId != id) {
this.create = function create(id) { return this.items[closestId];
if (_model.has(id)) { } else {
var newNode = _node.cloneNode(true), return;
nodes = DomUtils.getNodes(newNode); }
};
Tools.toArray(nodes).forEach(function (child) {
child.setAttribute("data-" + _plugins.name+"_id", id); /**
}); * Remove an item from the dom and the items list
* @private
this.items.set(id, newNode); * @param {Number} id the id of the item to remove
_plugins.apply(newNode); * @returns
return newNode; */
} this.removeItem = function removeItem(id) {
}; var item = this.items[id];
if (item) {
/** _rootNode.removeChild(item);
* Renders the dom tree, adds nodes that are in the boundaries delete this.items[id];
* and removes the others _removeObserversForId(id);
* @private return true;
* @returns true boundaries are set } else {
*/ return false;
this.render = function render() { }
// If the number of items to render is all (*) };
// Then get the number of items
var _tmpNb = _nb == "*" ? _model.getNbItems() : _nb; /**
* create a new node. Actually makes a clone of the initial one
// This will store the items to remove * and adds pluginname_id to each node, then calls plugins.apply to apply all plugins
var marked = []; * @private
* @param id
// Render only if boundaries have been set * @param pluginName
if (_nb !== null && _start !== null) { * @returns the associated node
*/
// Loop through the existing items this.create = function create(id) {
this.items.loop(function (value, idx) { if (_model.has(id)) {
// If an item is out of the boundary var newNode = _node.cloneNode(true),
if (idx < _start || idx >= (_start + _tmpNb) || !_model.has(idx)) { nodes = DomUtils.getNodes(newNode);
// Mark it
marked.push(idx); Tools.toArray(nodes).forEach(function (child) {
} child.setAttribute("data-" + _plugins.name+"_id", id);
}, this); });
// Remove the marked item from the highest id to the lowest this.items[id] = newNode;
// Doing this will avoid the id change during removal _plugins.apply(newNode);
// (removing id 2 will make id 3 becoming 2) return newNode;
marked.sort(Tools.compareNumbers).reverse().forEach(this.removeItem, this); }
};
// Now that we have removed the old nodes
// Add the missing one /**
for (var i=_start, l=_tmpNb+_start; i<l; i++) { * Renders the dom tree, adds nodes that are in the boundaries
this.addItem(i); * and removes the others
} * @private
return true; * @returns true boundaries are set
} else { */
return false; this.render = function render() {
} // If the number of items to render is all (*)
}; // Then get the number of items
var _tmpNb = _nb == "*" ? _model.getNbItems() : _nb;
this.setPlugins($plugins);
this.setRootNode($rootNode); // This will store the items to remove
}; var marked = [];
/** // Render only if boundaries have been set
* Save an itemRenderer according to its id if (_nb !== null && _start !== null) {
* @private
* @param {String} id the id of the itemRenderer // Loop through the existing items
* @param {ItemRenderer} itemRenderer an itemRenderer object Tools.loop(this.items, function (value, idx) {
*/ // If an item is out of the boundary
this.setItemRenderer = function setItemRenderer(id, itemRenderer) { idx = Number(idx);
id = id || "default";
_itemRenderers[id] = itemRenderer; if (idx < _start || idx >= (_start + _tmpNb) || !_model.has(idx)) {
}; // Mark it
marked.push(idx);
/** }
* Get an itemRenderer }, this);
* @private
* @param {String} id the name of the itemRenderer // Remove the marked item from the highest id to the lowest
* @returns the itemRenderer // Doing this will avoid the id change during removal
*/ // (removing id 2 will make id 3 becoming 2)
this.getItemRenderer = function getItemRenderer(id) { marked.sort(Tools.compareNumbers).reverse().forEach(this.removeItem, this);
return _itemRenderers[id];
}; // Now that we have removed the old nodes
// Add the missing one
/** for (var i=_start, l=_tmpNb+_start; i<l; i++) {
* Expands the inner dom nodes of a given dom node, filling it with model's values this.addItem(i);
* @param {HTMLElement|SVGElement} node the dom node to apply foreach to }
*/ return true;
this.foreach = function foreach(node, idItemRenderer, start, nb) { } else {
var itemRenderer = new this.ItemRenderer(this.plugins, node); return false;
}
itemRenderer.setStart(start || 0); };
itemRenderer.setNb(nb || "*");
this.setPlugins($plugins);
itemRenderer.render(); this.setRootNode($rootNode);
};
// Add the newly created item
/**
* Save an itemRenderer according to its id
* @private
* @param {String} id the id of the itemRenderer
* @param {ItemRenderer} itemRenderer an itemRenderer object
*/
this.setItemRenderer = function setItemRenderer(id, itemRenderer) {
id = id || "default";
_itemRenderers[id] = itemRenderer;
};
/**
* Get an itemRenderer
* @private
* @param {String} id the name of the itemRenderer
* @returns the itemRenderer
*/
this.getItemRenderer = function getItemRenderer(id) {
return _itemRenderers[id];
};
/**
* Expands the inner dom nodes of a given dom node, filling it with model's values
* @param {HTMLElement|SVGElement} node the dom node to apply foreach to
*/
this.foreach = function foreach(node, idItemRenderer, start, nb) {
var itemRenderer = new this.ItemRenderer(this.plugins, node);
itemRenderer.setStart(start || 0);
itemRenderer.setNb(nb || "*");
itemRenderer.render();
// Add the newly created item
_model.watch("added", itemRenderer.render, itemRenderer); _model.watch("added", itemRenderer.render, itemRenderer);
// If an item is deleted // If an item is deleted
_model.watch("deleted", function (idx) { _model.watch("deleted", function (idx) {
itemRenderer.render(); itemRenderer.render();
// Also remove all observers // Also remove all observers
this.observers[idx] && this.observers[idx].forEach(function (handler) { _removeObserversForId(idx);
_model.unwatchValue(handler);
}, this);
delete this.observers[idx];
},this); },this);
this.setItemRenderer(idItemRenderer, itemRenderer); this.setItemRenderer(idItemRenderer, itemRenderer);
...@@ -508,13 +541,13 @@ function BindPlugin(Store, Observable, Tools, DomUtils) { ...@@ -508,13 +541,13 @@ function BindPlugin(Store, Observable, Tools, DomUtils) {
* @returns true if the foreach exists * @returns true if the foreach exists
*/ */
this.updateStart = function updateStart(id, start) { this.updateStart = function updateStart(id, start) {
var itemRenderer = this.getItemRenderer(id); var itemRenderer = this.getItemRenderer(id);
if (itemRenderer) { if (itemRenderer) {
itemRenderer.setStart(start); itemRenderer.setStart(start);
return true; return true;
} else { } else {
return false; return false;
} }
}; };
/** /**
...@@ -524,13 +557,13 @@ function BindPlugin(Store, Observable, Tools, DomUtils) { ...@@ -524,13 +557,13 @@ function BindPlugin(Store, Observable, Tools, DomUtils) {
* @returns true if the foreach exists * @returns true if the foreach exists
*/ */
this.updateNb = function updateNb(id, nb) { this.updateNb = function updateNb(id, nb) {
var itemRenderer = this.getItemRenderer(id); var itemRenderer = this.getItemRenderer(id);
if (itemRenderer) { if (itemRenderer) {
itemRenderer.setNb(nb); itemRenderer.setNb(nb);
return true; return true;
} else { } else {
return false; return false;
} }
}; };
/** /**
...@@ -539,202 +572,202 @@ function BindPlugin(Store, Observable, Tools, DomUtils) { ...@@ -539,202 +572,202 @@ function BindPlugin(Store, Observable, Tools, DomUtils) {
* @returns true if the foreach exists * @returns true if the foreach exists
*/ */
this.refresh = function refresh(id) { this.refresh = function refresh(id) {
var itemRenderer = this.getItemRenderer(id); var itemRenderer = this.getItemRenderer(id);
if (itemRenderer) { if (itemRenderer) {
itemRenderer.render(); itemRenderer.render();
return true; return true;
} else { } else {
return false; return false;
} }
}; };
/** /**
* Both ways binding between a dom node attributes and the model * Both ways binding between a dom node attributes and the model
* @param {HTMLElement|SVGElement} node the dom node to apply the plugin to * @param {HTMLElement|SVGElement} node the dom node to apply the plugin to
* @param {String} name the name of the property to look for in the model's value * @param {String} name the name of the property to look for in the model's value
* @returns * @returns
*/ */
this.bind = function bind(node, property, name) { this.bind = function bind(node, property, name) {
// Name can be unset if the value of a row is plain text // Name can be unset if the value of a row is plain text
name = name || ""; name = name || "";
// In case of an array-like model the id is the index of the model's item to look for. // In case of an array-like model the id is the index of the model's item to look for.
// The _id is added by the foreach function // The _id is added by the foreach function
var id = node.getAttribute("data-" + this.plugins.name+"_id"), var id = node.getAttribute("data-" + this.plugins.name+"_id"),
// Else, it is the first element of the following // Else, it is the first element of the following
split = name.split("."), split = name.split("."),
// So the index of the model is either id or the first element of split // So the index of the model is either id or the first element of split
modelIdx = id || split.shift(), modelIdx = id || split.shift(),
// And the name of the property to look for in the value is // And the name of the property to look for in the value is
prop = id ? name : split.join("."), prop = id ? name : split.join("."),
// Get the model's value // Get the model's value
get = Tools.getNestedProperty(_model.get(modelIdx), prop), get = Tools.getNestedProperty(_model.get(modelIdx), prop),
// When calling bind like bind:newBinding,param1, param2... we need to get them // When calling bind like bind:newBinding,param1, param2... we need to get them
extraParam = Tools.toArray(arguments).slice(3); extraParam = Tools.toArray(arguments).slice(3);
// 0 and false are acceptable falsy values // 0 and false are acceptable falsy values
if (get || get === 0 || get === false) { if (get || get === 0 || get === false) {
// If the binding hasn't been overriden // If the binding hasn't been overriden
if (!this.execBinding.apply(this, if (!this.execBinding.apply(this,
[node, property, get] [node, property, get]
// Extra params are passed to the new binding too // Extra params are passed to the new binding too
.concat(extraParam))) { .concat(extraParam))) {
// Execute the default one which is a simple assignation // Execute the default one which is a simple assignation
//node[property] = get; //node[property] = get;
DomUtils.setAttribute(node, property, get); DomUtils.setAttribute(node, property, get);
} }
} }
// Only watch for changes (double way data binding) if the binding // Only watch for changes (double way data binding) if the binding
// has not been redefined // has not been redefined
if (!this.hasBinding(property)) { if (!this.hasBinding(property)) {
node.addEventListener("change", function (event) { node.addEventListener("change", function (event) {
if (_model.has(modelIdx)) { if (_model.has(modelIdx)) {
if (prop) { if (prop) {
_model.update(modelIdx, name, node[property]); _model.update(modelIdx, name, node[property]);
} else { } else {
_model.set(modelIdx, node[property]); _model.set(modelIdx, node[property]);
} }
} }
}, true); }, true);
} }
// Watch for changes // Watch for changes
this.observers[modelIdx] = this.observers[modelIdx] || []; this.observers[modelIdx] = this.observers[modelIdx] || [];
this.observers[modelIdx].push(_model.watchValue(modelIdx, function (value) { this.observers[modelIdx].push(_model.watchValue(modelIdx, function (value) {
if (!this.execBinding.apply(this, if (!this.execBinding.apply(this,
[node, property, Tools.getNestedProperty(value, prop)] [node, property, Tools.getNestedProperty(value, prop)]
// passing extra params too // passing extra params too
.concat(extraParam))) { .concat(extraParam))) {
//node[property] = Tools.getNestedProperty(value, prop); //node[property] = Tools.getNestedProperty(value, prop);
DomUtils.setAttribute(node, property, Tools.getNestedProperty(value, prop)); DomUtils.setAttribute(node, property, Tools.getNestedProperty(value, prop));
} }
}, this)); }, this));
}; };
/** /**
* Set the node's value into the model, the name is the model's property * Set the node's value into the model, the name is the model's property
* @private * @private
* @param {HTMLElement|SVGElement} node * @param {HTMLElement|SVGElement} node
* @returns true if the property is added * @returns true if the property is added
*/ */
this.set = function set(node) { this.set = function set(node) {
if (DomUtils.isAcceptedType(node) && node.name) { if (DomUtils.isAcceptedType(node) && node.name) {
_model.set(node.name, node.value); _model.set(node.name, node.value);
return true; return true;
} else { } else {
return false; return false;
} }
}; };
this.getItemIndex = function getElementId(dom) { this.getItemIndex = function getElementId(dom) {
var dataset = DomUtils.getDataset(dom); var dataset = DomUtils.getDataset(dom);
if (dataset && typeof dataset[this.plugins.name + "_id"] != "undefined") { if (dataset && typeof dataset[this.plugins.name + "_id"] != "undefined") {
return +dataset[this.plugins.name + "_id"]; return +dataset[this.plugins.name + "_id"];
} else { } else {
return false; return false;
} }
}; };
/** /**
* Prevents the submit and set the model with all form's inputs * Prevents the submit and set the model with all form's inputs
* @param {HTMLFormElement} form * @param {HTMLFormElement} DOMfrom
* @returns true if valid form * @returns true if valid form
*/ */
this.form = function form(form) { this.form = function form(DOMform) {
if (form && form.nodeName == "FORM") { if (DOMform && DOMform.nodeName == "FORM") {
var that = this; var that = this;
form.addEventListener("submit", function (event) { DOMform.addEventListener("submit", function (event) {
Tools.toArray(form.querySelectorAll("[name]")).forEach(that.set, that); Tools.toArray(DOMform.querySelectorAll("[name]")).forEach(that.set, that);
event.preventDefault(); event.preventDefault();
}, true); }, true);
return true; return true;
} else { } else {
return false; return false;
} }
}; };
/** /**
* Add a new way to handle a binding * Add a new way to handle a binding
* @param {String} name of the binding * @param {String} name of the binding
* @param {Function} binding the function to handle the binding * @param {Function} binding the function to handle the binding
* @returns * @returns
*/ */
this.addBinding = function addBinding(name, binding) { this.addBinding = function addBinding(name, binding) {
if (name && typeof name == "string" && typeof binding == "function") { if (name && typeof name == "string" && typeof binding == "function") {
_bindings[name] = binding; _bindings[name] = binding;
return true; return true;
} else { } else {
return false; return false;
} }
}; };
/** /**
* Execute a binding * Execute a binding
* Only used by the plugin * Only used by the plugin
* @private * @private
* @param {HTMLElement} node the dom node on which to execute the binding * @param {HTMLElement} node the dom node on which to execute the binding
* @param {String} name the name of the binding * @param {String} name the name of the binding
* @param {Any type} value the value to pass to the function * @param {Any type} value the value to pass to the function
* @returns * @returns
*/ */
this.execBinding = function execBinding(node, name) { this.execBinding = function execBinding(node, name) {
if (this.hasBinding(name)) { if (this.hasBinding(name)) {
_bindings[name].apply(node, Array.prototype.slice.call(arguments, 2)); _bindings[name].apply(node, Array.prototype.slice.call(arguments, 2));
return true; return true;
} else { } else {
return false; return false;
} }
}; };
/** /**
* Check if the binding exists * Check if the binding exists
* @private * @private
* @param {String} name the name of the binding * @param {String} name the name of the binding
* @returns * @returns
*/ */
this.hasBinding = function hasBinding(name) { this.hasBinding = function hasBinding(name) {
return _bindings.hasOwnProperty(name); return _bindings.hasOwnProperty(name);
}; };
/** /**
* Get a binding * Get a binding
* For debugging only * For debugging only
* @private * @private
* @param {String} name the name of the binding * @param {String} name the name of the binding
* @returns * @returns
*/ */
this.getBinding = function getBinding(name) { this.getBinding = function getBinding(name) {
return _bindings[name]; return _bindings[name];
}; };
/** /**
* Add multiple binding at once * Add multiple binding at once
* @param {Object} list the list of bindings to add * @param {Object} list the list of bindings to add
* @returns * @returns
*/ */
this.addBindings = function addBindings(list) { this.addBindings = function addBindings(list) {
return Tools.loop(list, function (binding, name) { return Tools.loop(list, function (binding, name) {
this.addBinding(name, binding); this.addBinding(name, binding);
}, this); }, this);
}; };
// Inits the model // Inits the model
this.setModel($model); this.setModel($model);
// Inits bindings // Inits bindings
this.addBindings($bindings); this.addBindings($bindings);
}; };
}); });
...@@ -754,122 +787,124 @@ define('Event.plugin',["DomUtils"], ...@@ -754,122 +787,124 @@ define('Event.plugin',["DomUtils"],
*/ */
function EventPlugin(Utils) { function EventPlugin(Utils) {
/**
* The event plugin constructor.
* ex: new EventPlugin({method: function(){} ...}, false); /**
* @param {Object} the object that has the event handling methods * The event plugin constructor.
* @param {Boolean} $isMobile if the event handler has to map with touch events * ex: new EventPlugin({method: function(){} ...}, false);
*/ * @param {Object} the object that has the event handling methods
return function EventPluginConstructor($parent, $isMobile) { * @param {Boolean} $isMobile if the event handler has to map with touch events
*/
/** return function EventPluginConstructor($parent, $isMobile) {
* The parent callback
* @private /**
*/ * The parent callback
var _parent = null, * @private
*/
/** var _parent = null,
* The mapping object.
* @private /**
*/ * The mapping object.
_map = { * @private
"mousedown" : "touchstart", */
"mouseup" : "touchend", _map = {
"mousemove" : "touchmove" "mousedown" : "touchstart",
}, "mouseup" : "touchend",
"mousemove" : "touchmove"
/** },
* Is touch device.
* @private /**
*/ * Is touch device.
_isMobile = !!$isMobile; * @private
*/
/** _isMobile = !!$isMobile;
* Add mapped event listener (for testing purpose).
* @private /**
*/ * Add mapped event listener (for testing purpose).
this.addEventListener = function addEventListener(node, event, callback, useCapture) { * @private
node.addEventListener(this.map(event), callback, !!useCapture); */
}; this.addEventListener = function addEventListener(node, event, callback, useCapture) {
node.addEventListener(this.map(event), callback, !!useCapture);
/** };
* Listen to DOM events.
* @param {Object} node DOM node /**
* @param {String} name event's name * Listen to DOM events.
* @param {String} listener callback's name * @param {Object} node DOM node
* @param {String} useCapture string * @param {String} name event's name
*/ * @param {String} listener callback's name
this.listen = function listen(node, name, listener, useCapture) { * @param {String} useCapture string
this.addEventListener(node, name, function(e){ */
_parent[listener].call(_parent, e, node); this.listen = function listen(node, name, listener, useCapture) {
}, !!useCapture); this.addEventListener(node, name, function(e){
}; _parent[listener].call(_parent, e, node);
}, !!useCapture);
/** };
* Delegate the event handling to a parent DOM element
* @param {Object} node DOM node /**
* @param {String} selector CSS3 selector to the element that listens to the event * Delegate the event handling to a parent DOM element
* @param {String} name event's name * @param {Object} node DOM node
* @param {String} listener callback's name * @param {String} selector CSS3 selector to the element that listens to the event
* @param {String} useCapture string * @param {String} name event's name
*/ * @param {String} listener callback's name
this.delegate = function delegate(node, selector, name, listener, useCapture) { * @param {String} useCapture string
this.addEventListener(node, name, function(event){ */
if (Utils.matches(node, selector, event.target)) { this.delegate = function delegate(node, selector, name, listener, useCapture) {
_parent[listener].call(_parent, event, node); this.addEventListener(node, name, function(event){
} if (Utils.matches(node, selector, event.target)) {
}, !!useCapture); _parent[listener].call(_parent, event, node);
}; }
}, !!useCapture);
/** };
* Get the parent object.
* @return {Object} the parent object /**
*/ * Get the parent object.
this.getParent = function getParent() { * @return {Object} the parent object
return _parent; */
}; this.getParent = function getParent() {
return _parent;
/** };
* Set the parent object.
* The parent object is an object which the functions are called by node listeners. /**
* @param {Object} the parent object * Set the parent object.
* @return true if object has been set * The parent object is an object which the functions are called by node listeners.
*/ * @param {Object} the parent object
this.setParent = function setParent(parent) { * @return true if object has been set
if (parent instanceof Object){ */
_parent = parent; this.setParent = function setParent(parent) {
return true; if (parent instanceof Object){
} _parent = parent;
return false; return true;
}; }
return false;
/** };
* Get event mapping.
* @param {String} event's name /**
* @return the mapped event's name * Get event mapping.
*/ * @param {String} event's name
this.map = function map(name) { * @return the mapped event's name
return _isMobile ? (_map[name] || name) : name; */
}; this.map = function map(name) {
return _isMobile ? (_map[name] || name) : name;
/** };
* Set event mapping.
* @param {String} event's name /**
* @param {String} event's value * Set event mapping.
* @return true if mapped * @param {String} event's name
*/ * @param {String} event's value
this.setMap = function setMap(name, value) { * @return true if mapped
if (typeof name == "string" && */
typeof value == "string") { this.setMap = function setMap(name, value) {
_map[name] = value; if (typeof name == "string" &&
return true; typeof value == "string") {
} _map[name] = value;
return false; return true;
}; }
return false;
//init };
this.setParent($parent);
}; //init
this.setParent($parent);
};
}); });
...@@ -890,6 +925,8 @@ define('LocalStore',["Store", "Tools"], ...@@ -890,6 +925,8 @@ define('LocalStore',["Store", "Tools"],
*/ */
function LocalStore(Store, Tools) { function LocalStore(Store, Tools) {
function LocalStoreConstructor() { function LocalStoreConstructor() {
/** /**
...@@ -971,7 +1008,7 @@ function LocalStore(Store, Tools) { ...@@ -971,7 +1008,7 @@ function LocalStore(Store, Tools) {
return function LocalStoreFactory(init) { return function LocalStoreFactory(init) {
LocalStoreConstructor.prototype = new Store(init); LocalStoreConstructor.prototype = new Store(init);
return new LocalStoreConstructor; return new LocalStoreConstructor();
}; };
}); });
...@@ -995,6 +1032,8 @@ define('Plugins',["Tools", "DomUtils"], ...@@ -995,6 +1032,8 @@ define('Plugins',["Tools", "DomUtils"],
*/ */
function Plugins(Tools, DomUtils) { function Plugins(Tools, DomUtils) {
return function PluginsConstructor($plugins) { return function PluginsConstructor($plugins) {
/** /**
...@@ -1147,277 +1186,285 @@ define('OObject',["StateMachine", "Store", "Plugins", "DomUtils", "Tools"], ...@@ -1147,277 +1186,285 @@ define('OObject',["StateMachine", "Store", "Plugins", "DomUtils", "Tools"],
*/ */
function OObject(StateMachine, Store, Plugins, DomUtils, Tools) { function OObject(StateMachine, Store, Plugins, DomUtils, Tools) {
return function OObjectConstructor(otherStore) {
return function OObjectConstructor(otherStore) {
/**
* This function creates the dom of the UI from its template
* It then queries the dom for data- attributes
* It can't be executed if the template is not set
* @private
*/
var render = function render(UI) {
// The place where the template will be created
// is either the currentPlace where the node is placed
// or a temporary div
var baseNode = _currentPlace || document.createElement("div");
// If the template is set
if (UI.template) {
// In this function, the thisObject is the UI's prototype
// UI is the UI that has OObject as prototype
if (typeof UI.template == "string") {
// Let the browser do the parsing, can't be faster & easier.
baseNode.innerHTML = UI.template.trim();
} else if (DomUtils.isAcceptedType(UI.template)) {
// If it's already an HTML element
baseNode.appendChild(UI.template);
}
// The UI must be placed in a unique dom node
// If not, there can't be multiple UIs placed in the same parentNode
// as it wouldn't be possible to know which node would belong to which UI
// This is probably a DOM limitation.
if (baseNode.childNodes.length > 1) {
throw new Error("UI.template should have only one parent node");
} else {
UI.dom = baseNode.childNodes[0];
}
UI.plugins.apply(UI.dom);
} else {
// An explicit message I hope
throw new Error("UI.template must be set prior to render");
}
},
/**
* This function appends the dom tree to the given dom node.
* This dom node should be somewhere in the dom of the application
* @private
*/
place = function place(UI, DOMplace, beforeNode) {
if (DOMplace) {
// IE (until 9) apparently fails to appendChild when insertBefore's second argument is null, hence this.
if (beforeNode) {
DOMplace.insertBefore(UI.dom, beforeNode);
} else {
DOMplace.appendChild(UI.dom);
}
// Also save the new place, so next renderings
// will be made inside it
_currentPlace = DOMplace;
}
},
/**
* Does rendering & placing in one function
* @private
*/
renderNPlace = function renderNPlace(UI, dom) {
render(UI);
place.apply(null, Tools.toArray(arguments));
},
/**
* This stores the current place
* If this is set, this is the place where new templates
* will be appended
* @private
*/
_currentPlace = null,
/**
* The UI's stateMachine.
* Much better than if(stuff) do(stuff) else if (!stuff and stuff but not stouff) do (otherstuff)
* Please open an issue if you want to propose a better one
* @private
*/
_stateMachine = new StateMachine("Init", {
"Init": [["render", render, this, "Rendered"],
["place", renderNPlace, this, "Rendered"]],
"Rendered": [["place", place, this],
["render", render, this]]
});
/**
* The UI's Store
* It has set/get/del/has/watch/unwatch methods
* @see Emily's doc for more info on how it works.
*/
this.model = otherStore instanceof Store ? otherStore : new Store();
/**
* The module that will manage the plugins for this UI
* @see Olives/Plugins' doc for more info on how it works.
*/
this.plugins = new Plugins();
/**
* Describes the template, can either be like "&lt;p&gt;&lt;/p&gt;" or HTMLElements
* @type string or HTMLElement|SVGElement
*/
this.template = null;
/**
* This will hold the dom nodes built from the template.
*/
this.dom = null;
/**
* Place the UI in a given dom node
* @param node the node on which to append the UI
* @param beforeNode the dom before which to append the UI
*/
this.place = function place(node, beforeNode) {
_stateMachine.event("place", this, node, beforeNode);
};
/**
* Renders the template to dom nodes and applies the plugins on it
* It requires the template to be set first
*/
this.render = function render() {
_stateMachine.event("render", this);
};
/**
* Set the UI's template from a DOM element
* @param {HTMLElement|SVGElement} dom the dom element that'll become the template of the UI
* @returns true if dom is an HTMLElement|SVGElement
*/
this.setTemplateFromDom = function setTemplateFromDom(dom) {
if (DomUtils.isAcceptedType(dom)) {
this.template = dom;
return true;
} else {
return false;
}
};
/**
* Transforms dom nodes into a UI.
* It basically does a setTemplateFromDOM, then a place
* It's a helper function
* @param {HTMLElement|SVGElement} node the dom to transform to a UI
* @returns true if dom is an HTMLElement|SVGElement
*/
this.alive = function alive(dom) {
if (DomUtils.isAcceptedType(dom)) {
this.setTemplateFromDom(dom);
this.place(dom.parentNode, dom.nextElementSibling);
return true;
} else {
return false;
}
};
/**
* Get the current dom node where the UI is placed.
* for debugging purpose
* @private
* @return {HTMLElement} node the dom where the UI is placed.
*/
this.getCurrentPlace = function(){
return _currentPlace;
};
};
/** });
* This function creates the dom of the UI from its template
* It then queries the dom for data- attributes
* It can't be executed if the template is not set
* @private
*/
var render = function render(UI) {
// The place where the template will be created
// is either the currentPlace where the node is placed
// or a temporary div
var baseNode = _currentPlace || document.createElement("div");
// If the template is set
if (UI.template) {
// In this function, the thisObject is the UI's prototype
// UI is the UI that has OObject as prototype
if (typeof UI.template == "string") {
// Let the browser do the parsing, can't be faster & easier.
baseNode.innerHTML = UI.template.trim();
} else if (DomUtils.isAcceptedType(UI.template)) {
// If it's already an HTML element
baseNode.appendChild(UI.template);
}
// The UI must be placed in a unique dom node /**
// If not, there can't be multiple UIs placed in the same parentNode * Olives http://flams.github.com/olives
// as it wouldn't be possible to know which node would belong to which UI * The MIT License (MIT)
// This is probably a DOM limitation. * Copyright (c) 2012-2013 Olivier Scherrer <pode.fr@gmail.com> - Olivier Wietrich <olivier.wietrich@gmail.com>
if (baseNode.childNodes.length > 1) { */
throw Error("UI.template should have only one parent node");
} else {
UI.dom = baseNode.childNodes[0];
}
UI.plugins.apply(UI.dom); define('Place.plugin',["OObject", "Tools"],
/**
* @class
* Place plugin places OObject in the DOM.
* @requires OObject, Tools
*/
function PlacePlugin(OObject, Tools) {
} else {
// An explicit message I hope
throw Error("UI.template must be set prior to render"); /**
} * Intilialize a Place.plugin with a list of OObjects
}, * @param {Object} $uis a list of OObjects such as:
* {
* "header": new OObject(),
* "list": new OObject()
* }
* @Constructor
*/
return function PlacePluginConstructor($uis) {
/**
* The list of uis currently set in this place plugin
* @private
*/
var _uis = {};
/**
* Attach an OObject to this DOM element
* @param {HTML|SVGElement} node the dom node where to attach the OObject
* @param {String} the name of the OObject to attach
* @throws {NoSuchOObject} an error if there's no OObject for the given name
*/
this.place = function place(node, name) {
if (_uis[name] instanceof OObject) {
_uis[name].place(node);
} else {
throw new Error(name + " is not an OObject UI in place:"+name);
}
};
/**
* Add an OObject that can be attached to a dom element
* @param {String} the name of the OObject to add to the list
* @param {OObject} ui the OObject to add the list
* @returns {Boolean} true if the OObject was added
*/
this.set = function set(name, ui) {
if (typeof name == "string" && ui instanceof OObject) {
_uis[name] = ui;
return true;
} else {
return false;
}
};
/**
* Add multiple dom elements at once
* @param {Object} $uis a list of OObjects such as:
* {
* "header": new OObject(),
* "list": new OObject()
* }
*/
this.setAll = function setAll(uis) {
Tools.loop(uis, function (ui, name) {
this.set(name, ui);
}, this);
};
/**
* Returns an OObject from the list given its name
* @param {String} the name of the OObject to get
* @returns {OObject} OObject for the given name
*/
this.get = function get(name) {
return _uis[name];
};
this.setAll($uis);
};
/** });
* This function appends the dom tree to the given dom node.
* This dom node should be somewhere in the dom of the application
* @private
*/
place = function place(UI, place, beforeNode) {
if (place) {
// IE (until 9) apparently fails to appendChild when insertBefore's second argument is null, hence this.
beforeNode ? place.insertBefore(UI.dom, beforeNode) : place.appendChild(UI.dom);
// Also save the new place, so next renderings
// will be made inside it
_currentPlace = place;
}
},
/** /**
* Does rendering & placing in one function * Olives http://flams.github.com/olives
* @private * The MIT License (MIT)
*/ * Copyright (c) 2012-2013 Olivier Scherrer <pode.fr@gmail.com> - Olivier Wietrich <olivier.wietrich@gmail.com>
renderNPlace = function renderNPlace(UI, dom) { */
render(UI);
place.apply(null, Tools.toArray(arguments));
},
/**
* This stores the current place
* If this is set, this is the place where new templates
* will be appended
* @private
*/
_currentPlace = null,
/**
* The UI's stateMachine.
* Much better than if(stuff) do(stuff) else if (!stuff and stuff but not stouff) do (otherstuff)
* Please open an issue if you want to propose a better one
* @private
*/
_stateMachine = new StateMachine("Init", {
"Init": [["render", render, this, "Rendered"],
["place", renderNPlace, this, "Rendered"]],
"Rendered": [["place", place, this],
["render", render, this]]
});
/**
* The UI's Store
* It has set/get/del/has/watch/unwatch methods
* @see Emily's doc for more info on how it works.
*/
this.model = otherStore instanceof Store ? otherStore : new Store;
/**
* The module that will manage the plugins for this UI
* @see Olives/Plugins' doc for more info on how it works.
*/
this.plugins = new Plugins();
/**
* Describes the template, can either be like "&lt;p&gt;&lt;/p&gt;" or HTMLElements
* @type string or HTMLElement|SVGElement
*/
this.template = null;
/**
* This will hold the dom nodes built from the template.
*/
this.dom = null;
/**
* Place the UI in a given dom node
* @param node the node on which to append the UI
* @param beforeNode the dom before which to append the UI
*/
this.place = function place(node, beforeNode) {
_stateMachine.event("place", this, node, beforeNode);
};
/**
* Renders the template to dom nodes and applies the plugins on it
* It requires the template to be set first
*/
this.render = function render() {
_stateMachine.event("render", this);
};
/**
* Set the UI's template from a DOM element
* @param {HTMLElement|SVGElement} dom the dom element that'll become the template of the UI
* @returns true if dom is an HTMLElement|SVGElement
*/
this.setTemplateFromDom = function setTemplateFromDom(dom) {
if (DomUtils.isAcceptedType(dom)) {
this.template = dom;
return true;
} else {
return false;
}
};
/**
* Transforms dom nodes into a UI.
* It basically does a setTemplateFromDOM, then a place
* It's a helper function
* @param {HTMLElement|SVGElement} node the dom to transform to a UI
* @returns true if dom is an HTMLElement|SVGElement
*/
this.alive = function alive(dom) {
if (DomUtils.isAcceptedType(dom)) {
this.setTemplateFromDom(dom);
this.place(dom.parentNode, dom.nextElementSibling);
return true;
} else {
return false;
}
};
/**
* Get the current dom node where the UI is placed.
* for debugging purpose
* @private
* @return {HTMLElement} node the dom where the UI is placed.
*/
this.getCurrentPlace = function(){
return _currentPlace;
};
};
});
/**
* Olives http://flams.github.com/olives
* The MIT License (MIT)
* Copyright (c) 2012-2013 Olivier Scherrer <pode.fr@gmail.com> - Olivier Wietrich <olivier.wietrich@gmail.com>
*/
define('Place.plugin',["OObject", "Tools"],
/**
* @class
* Place plugin places OObject in the DOM.
* @requires OObject, Tools
*/
function PlacePlugin(OObject, Tools) {
/**
* Intilialize a Place.plugin with a list of OObjects
* @param {Object} $uis a list of OObjects such as:
* {
* "header": new OObject(),
* "list": new OObject()
* }
* @Constructor
*/
return function PlacePluginConstructor($uis) {
/**
* The list of uis currently set in this place plugin
* @private
*/
var _uis = {};
/**
* Attach an OObject to this DOM element
* @param {HTML|SVGElement} node the dom node where to attach the OObject
* @param {String} the name of the OObject to attach
* @throws {NoSuchOObject} an error if there's no OObject for the given name
*/
this.place = function place(node, name) {
if (_uis[name] instanceof OObject) {
_uis[name].place(node);
} else {
throw new Error(name + " is not an OObject UI in place:"+name);
}
};
/**
* Add an OObject that can be attached to a dom element
* @param {String} the name of the OObject to add to the list
* @param {OObject} ui the OObject to add the list
* @returns {Boolean} true if the OObject was added
*/
this.set = function set(name, ui) {
if (typeof name == "string" && ui instanceof OObject) {
_uis[name] = ui;
return true;
} else {
return false;
}
};
/**
* Add multiple dom elements at once
* @param {Object} $uis a list of OObjects such as:
* {
* "header": new OObject(),
* "list": new OObject()
* }
*/
this.setAll = function setAll(uis) {
Tools.loop(uis, function (ui, name) {
this.set(name, ui);
}, this);
};
/**
* Returns an OObject from the list given its name
* @param {String} the name of the OObject to get
* @returns {OObject} OObject for the given name
*/
this.get = function get(name) {
return _uis[name];
};
this.setAll($uis);
};
});
/**
* Olives http://flams.github.com/olives
* The MIT License (MIT)
* Copyright (c) 2012-2013 Olivier Scherrer <pode.fr@gmail.com> - Olivier Wietrich <olivier.wietrich@gmail.com>
*/
define('SocketIOTransport',["Observable", "Tools"], define('SocketIOTransport',["Observable", "Tools"],
/** /**
...@@ -1427,6 +1474,8 @@ define('SocketIOTransport',["Observable", "Tools"], ...@@ -1427,6 +1474,8 @@ define('SocketIOTransport',["Observable", "Tools"],
*/ */
function SocketIOTransport(Observable, Tools) { function SocketIOTransport(Observable, Tools) {
/** /**
* Defines the SocketIOTransport * Defines the SocketIOTransport
* @private * @private
...@@ -1462,7 +1511,7 @@ function SocketIOTransport(Observable, Tools) { ...@@ -1462,7 +1511,7 @@ function SocketIOTransport(Observable, Tools) {
*/ */
this.getSocket = function getSocket() { this.getSocket = function getSocket() {
return _socket; return _socket;
}, };
/** /**
* Subscribe to a socket event * Subscribe to a socket event
...@@ -1471,7 +1520,7 @@ function SocketIOTransport(Observable, Tools) { ...@@ -1471,7 +1520,7 @@ function SocketIOTransport(Observable, Tools) {
*/ */
this.on = function on(event, func) { this.on = function on(event, func) {
return _socket.on(event, func); return _socket.on(event, func);
}, };
/** /**
* Subscribe to a socket event but disconnect as soon as it fires. * Subscribe to a socket event but disconnect as soon as it fires.
...@@ -1510,15 +1559,17 @@ function SocketIOTransport(Observable, Tools) { ...@@ -1510,15 +1559,17 @@ function SocketIOTransport(Observable, Tools) {
* @param {Object} scope the scope in which to execute the callback * @param {Object} scope the scope in which to execute the callback
*/ */
this.request = function request(channel, data, func, scope) { this.request = function request(channel, data, func, scope) {
if (typeof channel == "string" if (typeof channel == "string" &&
&& typeof data != "undefined") { typeof data != "undefined") {
var reqData = { var reqData = {
eventId: Date.now() + Math.floor(Math.random()*1e6), eventId: Date.now() + Math.floor(Math.random()*1e6),
data: data data: data
}, },
boundCallback = function () { boundCallback = function () {
func && func.apply(scope || null, arguments); if (func) {
func.apply(scope || null, arguments);
}
}; };
this.once(reqData.eventId, boundCallback); this.once(reqData.eventId, boundCallback);
...@@ -1540,9 +1591,9 @@ function SocketIOTransport(Observable, Tools) { ...@@ -1540,9 +1591,9 @@ function SocketIOTransport(Observable, Tools) {
* @returns * @returns
*/ */
this.listen = function listen(channel, data, func, scope) { this.listen = function listen(channel, data, func, scope) {
if (typeof channel == "string" if (typeof channel == "string" &&
&& typeof data != "undefined" typeof data != "undefined" &&
&& typeof func == "function") { typeof func == "function") {
var reqData = { var reqData = {
eventId: Date.now() + Math.floor(Math.random()*1e6), eventId: Date.now() + Math.floor(Math.random()*1e6),
...@@ -1550,7 +1601,9 @@ function SocketIOTransport(Observable, Tools) { ...@@ -1550,7 +1601,9 @@ function SocketIOTransport(Observable, Tools) {
keepAlive: true keepAlive: true
}, },
boundCallback = function () { boundCallback = function () {
func && func.apply(scope || null, arguments); if (func) {
func.apply(scope || null, arguments);
}
}, },
that = this; that = this;
...@@ -1573,3 +1626,504 @@ function SocketIOTransport(Observable, Tools) { ...@@ -1573,3 +1626,504 @@ function SocketIOTransport(Observable, Tools) {
this.setSocket($socket); this.setSocket($socket);
}; };
}); });
/**
* Olives http://flams.github.com/olives
* The MIT License (MIT)
* Copyright (c) 2012-2013 Olivier Scherrer <pode.fr@gmail.com> - Olivier Wietrich <olivier.wietrich@gmail.com>
*/
define('Stack',['Tools'],
/**
* @class
* A Stack is a tool for managing DOM elements as groups. Within a group, dom elements
* can be added, removed, moved around. The group can be moved to another parent node
* while keeping the DOM elements in the same order, excluding the parent dom elements's
* children that are not in the Stack.
*/
function Stack() {
var Tools = require("Tools");
return function StackConstructor($parent) {
/**
* The parent DOM element is a documentFragment by default
* @private
*/
var _parent = document.createDocumentFragment(),
/**
* The place where the dom elements hide
* @private
*/
_hidePlace = document.createElement("div"),
/**
* The list of dom elements that are part of the stack
* Helps for excluding elements that are not part of it
* @private
*/
_childNodes = [],
_lastTransit = null;
/**
* Add a DOM element to the stack. It will be appended.
* @param {HTMLElement} dom the DOM element to add
* @returns {HTMLElement} dom
*/
this.add = function add(dom) {
if (!this.has(dom) && dom instanceof HTMLElement) {
_parent.appendChild(dom);
_childNodes.push(dom);
return dom;
} else {
return false;
}
};
/**
* Remove a DOM element from the stack.
* @param {HTMLElement} dom the DOM element to remove
* @returns {HTMLElement} dom
*/
this.remove = function remove(dom) {
var index;
if (this.has(dom)) {
index = _childNodes.indexOf(dom);
_parent.removeChild(dom);
_childNodes.splice(index, 1);
return dom;
} else {
return false;
}
};
/**
* Place a stack by appending its DOM elements to a new parent
* @param {HTMLElement} newParentDom the new DOM element to append the stack to
* @returns {HTMLElement} newParentDom
*/
this.place = function place(newParentDom) {
if (newParentDom instanceof HTMLElement) {
[].slice.call(_parent.childNodes).forEach(function (childDom) {
if (this.has(childDom)) {
newParentDom.appendChild(childDom);
}
}, this);
return this._setParent(newParentDom);
} else {
return false;
}
};
/**
* Move an element up in the stack
* @param {HTMLElement} dom the dom element to move up
* @returns {HTMLElement} dom
*/
this.up = function up(dom) {
if (this.has(dom)) {
var domPosition = this.getPosition(dom);
this.move(dom, domPosition + 1);
return dom;
} else {
return false;
}
};
/**
* Move an element down in the stack
* @param {HTMLElement} dom the dom element to move down
* @returns {HTMLElement} dom
*/
this.down = function down(dom) {
if (this.has(dom)) {
var domPosition = this.getPosition(dom);
this.move(dom, domPosition - 1);
return dom;
} else {
return false;
}
};
/**
* Move an element that is already in the stack to a new position
* @param {HTMLElement} dom the dom element to move
* @param {Number} position the position to which to move the DOM element
* @returns {HTMLElement} dom
*/
this.move = function move(dom, position) {
if (this.has(dom)) {
var domIndex = _childNodes.indexOf(dom);
_childNodes.splice(domIndex, 1);
// Preventing a bug in IE when insertBefore is not given a valid
// second argument
var nextElement = getNextElementInDom(position);
if (nextElement) {
_parent.insertBefore(dom, nextElement);
} else {
_parent.appendChild(dom);
}
_childNodes.splice(position, 0, dom);
return dom;
} else {
return false;
}
};
function getNextElementInDom(position) {
if (position >= _childNodes.length) {
return;
}
var nextElement = _childNodes[position];
if (Tools.toArray(_parent.childNodes).indexOf(nextElement) == -1) {
return getNextElementInDom(position +1);
} else {
return nextElement;
}
}
/**
* Insert a new element at a specific position in the stack
* @param {HTMLElement} dom the dom element to insert
* @param {Number} position the position to which to insert the DOM element
* @returns {HTMLElement} dom
*/
this.insert = function insert(dom, position) {
if (!this.has(dom) && dom instanceof HTMLElement) {
_childNodes.splice(position, 0, dom);
_parent.insertBefore(dom, _parent.childNodes[position]);
return dom;
} else {
return false;
}
};
/**
* Get the position of an element in the stack
* @param {HTMLElement} dom the dom to get the position from
* @returns {HTMLElement} dom
*/
this.getPosition = function getPosition(dom) {
return _childNodes.indexOf(dom);
};
/**
* Count the number of elements in a stack
* @returns {Number} the number of items
*/
this.count = function count() {
return _parent.childNodes.length;
};
/**
* Tells if a DOM element is in the stack
* @param {HTMLElement} dom the dom to tell if its in the stack
* @returns {HTMLElement} dom
*/
this.has = function has(childDom) {
return this.getPosition(childDom) >= 0;
};
/**
* Hide a dom element that was previously added to the stack
* It will be taken out of the dom until displayed again
* @param {HTMLElement} dom the dom to hide
* @return {boolean} if dom element is in the stack
*/
this.hide = function hide(dom) {
if (this.has(dom)) {
_hidePlace.appendChild(dom);
return true;
} else {
return false;
}
};
/**
* Show a dom element that was previously hidden
* It will be added back to the dom
* @param {HTMLElement} dom the dom to show
* @return {boolean} if dom element is current hidden
*/
this.show = function show(dom) {
if (this.has(dom) && dom.parentNode === _hidePlace) {
this.move(dom, _childNodes.indexOf(dom));
return true;
} else {
return false;
}
};
/**
* Helper function for hiding all the dom elements
*/
this.hideAll = function hideAll() {
_childNodes.forEach(this.hide, this);
};
/**
* Helper function for showing all the dom elements
*/
this.showAll = function showAll() {
_childNodes.forEach(this.show, this);
};
/**
* Get the parent node that a stack is currently attached to
* @returns {HTMLElement} parent node
*/
this.getParent = function _getParent() {
return _parent;
};
/**
* Set the parent element (without appending the stacks dom elements to)
* @private
*/
this._setParent = function _setParent(parent) {
if (parent instanceof HTMLElement) {
_parent = parent;
return _parent;
} else {
return false;
}
};
/**
* Get the place where the DOM elements are hidden
* @private
*/
this.getHidePlace = function getHidePlace() {
return _hidePlace;
};
/**
* Set the place where the DOM elements are hidden
* @private
*/
this.setHidePlace = function setHidePlace(hidePlace) {
if (hidePlace instanceof HTMLElement) {
_hidePlace = hidePlace;
return true;
} else {
return false;
}
};
/**
* Get the last dom element that the stack transitted to
* @returns {HTMLElement} the last dom element
*/
this.getLastTransit = function getLastTransit() {
return _lastTransit;
};
/**
* Transit between views, will show the new one and hide the previous
* element that the stack transitted to, if any.
* @param {HTMLElement} dom the element to transit to
* @returns {Boolean} false if the element can't be shown
*/
this.transit = function transit(dom) {
if (_lastTransit) {
this.hide(_lastTransit);
}
if (this.show(dom)) {
_lastTransit = dom;
return true;
} else {
return false;
}
};
this._setParent($parent);
};
});
/**
* Olives http://flams.github.com/olives
* The MIT License (MIT)
* Copyright (c) 2012-2013 Olivier Scherrer <pode.fr@gmail.com> - Olivier Wietrich <olivier.wietrich@gmail.com>
*/
define('LocationRouter',["Router", "Tools"],
/**
* @class
* A locationRouter is a router which navigates to the route defined in the URL and updates this URL
* while navigating. It's a subtype of Emily's Router
*/
function LocationRouter(Router, Tools) {
function LocationRouterConstructor() {
/**
* The handle on the watch
* @private
*/
var _watchHandle,
/**
* The default route to navigate to when nothing is supplied in the url
* @private
*/
_defaultRoute = "",
/**
* The last route that was navigated to
* @private
*/
_lastRoute = window.location.hash;
/**
* Navigates to the current hash or to the default route if none is supplied in the url
* @private
*/
/*jshint validthis:true*/
function doNavigate() {
if (window.location.hash) {
var parsedHash = this.parse(window.location.hash);
this.navigate.apply(this, parsedHash);
} else {
this.navigate(_defaultRoute);
}
}
/**
* Set the default route to navigate to when nothing is defined in the url
* @param {String} defaultRoute the defaultRoute to navigate to
* @returns {Boolean} true if it's not an empty string
*/
this.setDefaultRoute = function setDefaultRoute(defaultRoute) {
if (defaultRoute && typeof defaultRoute == "string") {
_defaultRoute = defaultRoute;
return true;
} else {
return false;
}
};
/**
* Get the currently set default route
* @returns {String} the default route
*/
this.getDefaultRoute = function getDefaultRoute() {
return _defaultRoute;
};
/**
* The function that parses the url to determine the route to navigate to.
* It has a default behavior explained below, but can be overriden as long as
* it has the same contract.
* @param {String} hash the hash coming from window.location.has
* @returns {Array} has to return an array with the list of arguments to call
* navigate with. The first item of the array must be the name of the route.
*
* Example: #album/holiday/2013
* will navigate to the route "album" and give two arguments "holiday" and "2013"
*/
this.parse = function parse(hash) {
return hash.split("#").pop().split("/");
};
/**
* The function that converts, or serialises the route and its arguments to a valid URL.
* It has a default behavior below, but can be overriden as long as it has the same contract.
* @param {Array} args the list of arguments to serialize
* @returns {String} the serialized arguments to add to the url hashmark
*
* Example:
* ["album", "holiday", "2013"];
* will give "album/holiday/2013"
*
*/
this.toUrl = function toUrl(args) {
return args.join("/");
};
/**
* When all the routes and handlers have been defined, start the location router
* so it parses the URL and navigates to the corresponding route.
* It will also start listening to route changes and hashmark changes to navigate.
* While navigating, the hashmark itself will also change to reflect the current route state
*/
this.start = function start(defaultRoute) {
this.setDefaultRoute(defaultRoute);
doNavigate.call(this);
this.bindOnHashChange();
this.bindOnRouteChange();
};
/**
* Remove the events handler for cleaning.
*/
this.destroy = function destroy() {
this.unwatch(_watchHandle);
window.removeEventListener("hashchange", this.boundOnHashChange, true);
};
/**
* Parse the hash and navigate to the corresponding url
* @private
*/
this.onHashChange = function onHashChange() {
if (window.location.hash != _lastRoute) {
doNavigate.call(this);
}
};
/**
* The bound version of onHashChange for add/removeEventListener
* @private
*/
this.boundOnHashChange = this.onHashChange.bind(this);
/**
* Add an event listener to hashchange to navigate to the corresponding route
* when it changes
* @private
*/
this.bindOnHashChange = function bindOnHashChange() {
window.addEventListener("hashchange", this.boundOnHashChange, true);
};
/**
* Watch route change events from the router to update the location
* @private
*/
this.bindOnRouteChange = function bindOnRouteChange() {
_watchHandle = this.watch(this.onRouteChange, this);
};
/**
* The handler for when the route changes
* It updates the location
* @private
*/
this.onRouteChange = function onRouteChange() {
window.location.hash = this.toUrl(Tools.toArray(arguments));
_lastRoute = window.location.hash;
};
this.getLastRoute = function getLastRoute() {
return _lastRoute;
};
}
return function LocationRouterFactory() {
LocationRouterConstructor.prototype = new Router();
return new LocationRouterConstructor();
};
});
...@@ -13,375 +13,406 @@ define(["Store", "Observable", "Tools", "DomUtils"], ...@@ -13,375 +13,406 @@ define(["Store", "Observable", "Tools", "DomUtils"],
*/ */
function BindPlugin(Store, Observable, Tools, DomUtils) { function BindPlugin(Store, Observable, Tools, DomUtils) {
return function BindPluginConstructor($model, $bindings) { "use strict";
/** return function BindPluginConstructor($model, $bindings) {
* The model to watch
* @private /**
*/ * The model to watch
var _model = null, * @private
*/
/** var _model = null,
* The list of custom bindings
* @private /**
*/ * The list of custom bindings
_bindings = {}, * @private
*/
/** _bindings = {},
* The list of itemRenderers
* each foreach has its itemRenderer /**
* @private * The list of itemRenderers
*/ * each foreach has its itemRenderer
_itemRenderers = {}; * @private
*/
/** _itemRenderers = {},
* The observers handlers
* for debugging only /**
* @private * The observers handlers
*/ * @private
this.observers = {}; */
_observers = {};
/**
* Define the model to watch for /**
* @param {Store} model the model to watch for changes * Exposed for debugging purpose
* @returns {Boolean} true if the model was set * @private
*/ */
this.setModel = function setModel(model) { this.observers = _observers;
if (model instanceof Store) {
// Set the model function _removeObserversForId(id) {
_model = model; if (_observers[id]) {
return true; _observers[id].forEach(function (handler) {
} else { _model.unwatchValue(handler);
return false; });
} delete _observers[id];
}; }
}
/**
* Get the store that is watched for /**
* for debugging only * Define the model to watch for
* @private * @param {Store} model the model to watch for changes
* @returns the Store * @returns {Boolean} true if the model was set
*/ */
this.getModel = function getModel() { this.setModel = function setModel(model) {
return _model; if (model instanceof Store) {
}; // Set the model
_model = model;
/** return true;
* The item renderer defines a dom node that can be duplicated } else {
* It is made available for debugging purpose, don't use it return false;
* @private }
*/ };
this.ItemRenderer = function ItemRenderer($plugins, $rootNode) {
/**
/** * Get the store that is watched for
* The node that will be cloned * for debugging only
* @private * @private
*/ * @returns the Store
var _node = null, */
this.getModel = function getModel() {
/** return _model;
* The object that contains plugins.name and plugins.apply };
* @private
*/ /**
_plugins = null, * The item renderer defines a dom node that can be duplicated
* It is made available for debugging purpose, don't use it
/** * @private
* The _rootNode where to append the created items */
* @private this.ItemRenderer = function ItemRenderer($plugins, $rootNode) {
*/
_rootNode = null, /**
* The node that will be cloned
/** * @private
* The lower boundary */
* @private var _node = null,
*/
_start = null, /**
* The object that contains plugins.name and plugins.apply
/** * @private
* The number of item to display */
* @private _plugins = null,
*/
_nb = null; /**
* The _rootNode where to append the created items
/** * @private
* Set the duplicated node */
* @private _rootNode = null,
*/
this.setRenderer = function setRenderer(node) { /**
_node = node; * The lower boundary
return true; * @private
}; */
_start = null,
/**
* Returns the node that is going to be used for rendering /**
* @private * The number of item to display
* @returns the node that is duplicated * @private
*/ */
this.getRenderer = function getRenderer() { _nb = null;
return _node;
}; /**
* Set the duplicated node
/** * @private
* Sets the rootNode and gets the node to copy */
* @private this.setRenderer = function setRenderer(node) {
* @param {HTMLElement|SVGElement} rootNode _node = node;
* @returns return true;
*/ };
this.setRootNode = function setRootNode(rootNode) {
var renderer; /**
if (DomUtils.isAcceptedType(rootNode)) { * Returns the node that is going to be used for rendering
_rootNode = rootNode; * @private
renderer = _rootNode.querySelector("*"); * @returns the node that is duplicated
this.setRenderer(renderer); */
renderer && _rootNode.removeChild(renderer); this.getRenderer = function getRenderer() {
return true; return _node;
} else { };
return false;
} /**
}; * Sets the rootNode and gets the node to copy
* @private
/** * @param {HTMLElement|SVGElement} rootNode
* Gets the rootNode * @returns
* @private */
* @returns _rootNode this.setRootNode = function setRootNode(rootNode) {
*/ var renderer;
this.getRootNode = function getRootNode() { if (DomUtils.isAcceptedType(rootNode)) {
return _rootNode; _rootNode = rootNode;
}; renderer = _rootNode.querySelector("*");
this.setRenderer(renderer);
/** if (renderer) {
* Set the plugins objet that contains the name and the apply function _rootNode.removeChild(renderer);
* @private }
* @param plugins return true;
* @returns true } else {
*/ return false;
this.setPlugins = function setPlugins(plugins) { }
_plugins = plugins; };
return true;
}; /**
* Gets the rootNode
/** * @private
* Get the plugins object * @returns _rootNode
* @private */
* @returns the plugins object this.getRootNode = function getRootNode() {
*/ return _rootNode;
this.getPlugins = function getPlugins() { };
return _plugins;
}; /**
* Set the plugins objet that contains the name and the apply function
/** * @private
* The nodes created from the items are stored here * @param plugins
* @private * @returns true
*/ */
this.items = new Store([]); this.setPlugins = function setPlugins(plugins) {
_plugins = plugins;
/** return true;
* Set the start limit };
* @private
* @param {Number} start the value to start rendering the items from /**
* @returns the value * Get the plugins object
*/ * @private
this.setStart = function setStart(start) { * @returns the plugins object
return _start = parseInt(start, 10); */
}; this.getPlugins = function getPlugins() {
return _plugins;
/** };
* Get the start value
* @private /**
* @returns the start value * The nodes created from the items are stored here
*/ * @private
this.getStart = function getStart() { */
return _start; this.items = {};
};
/**
/** * Set the start limit
* Set the number of item to display * @private
* @private * @param {Number} start the value to start rendering the items from
* @param {Number/String} nb the number of item to display or "*" for all * @returns the value
* @returns the value */
*/ this.setStart = function setStart(start) {
this.setNb = function setNb(nb) { _start = parseInt(start, 10);
return _nb = nb == "*" ? nb : parseInt(nb, 10); return _start;
}; };
/** /**
* Get the number of item to display * Get the start value
* @private * @private
* @returns the value * @returns the start value
*/ */
this.getNb = function getNb() { this.getStart = function getStart() {
return _nb; return _start;
}; };
/** /**
* Adds a new item and adds it in the items list * Set the number of item to display
* @private * @private
* @param {Number} id the id of the item * @param {Number/String} nb the number of item to display or "*" for all
* @returns * @returns the value
*/ */
this.addItem = function addItem(id) { this.setNb = function setNb(nb) {
var node, _nb = nb == "*" ? nb : parseInt(nb, 10);
next; return _nb;
};
if (typeof id == "number" && !this.items.get(id)) {
node = this.create(id); /**
if (node) { * Get the number of item to display
// IE (until 9) apparently fails to appendChild when insertBefore's second argument is null, hence this. * @private
next = this.getNextItem(id); * @returns the value
next ? _rootNode.insertBefore(node, next) : _rootNode.appendChild(node); */
return true; this.getNb = function getNb() {
} else { return _nb;
return false; };
}
} else { /**
return false; * Adds a new item and adds it in the items list
} * @private
}; * @param {Number} id the id of the item
* @returns
/** */
* Get the next item in the item store given an id. this.addItem = function addItem(id) {
* @private var node,
* @param {Number} id the id to start from next;
* @returns
*/ if (typeof id == "number" && !this.items[id]) {
this.getNextItem = function getNextItem(id) { next = this.getNextItem(id);
return this.items.alter("slice", id+1).filter(function (value) { node = this.create(id);
if (DomUtils.isAcceptedType(value)) { if (node) {
return true; // IE (until 9) apparently fails to appendChild when insertBefore's second argument is null, hence this.
} if (next) {
})[0]; _rootNode.insertBefore(node, next);
}; } else {
_rootNode.appendChild(node);
/** }
* Remove an item from the dom and the items list return true;
* @private } else {
* @param {Number} id the id of the item to remove return false;
* @returns }
*/ } else {
this.removeItem = function removeItem(id) { return false;
var item = this.items.get(id); }
if (item) { };
_rootNode.removeChild(item);
this.items.set(id); /**
return true; * Get the next item in the item store given an id.
} else { * @private
return false; * @param {Number} id the id to start from
} * @returns
}; */
this.getNextItem = function getNextItem(id) {
/** var keys = Object.keys(this.items).map(function (string) {
* create a new node. Actually makes a clone of the initial one return Number(string);
* and adds pluginname_id to each node, then calls plugins.apply to apply all plugins }),
* @private closest = Tools.closestGreater(id, keys),
* @param id closestId = keys[closest];
* @param pluginName
* @returns the associated node // Only return if different
*/ if (closestId != id) {
this.create = function create(id) { return this.items[closestId];
if (_model.has(id)) { } else {
var newNode = _node.cloneNode(true), return;
nodes = DomUtils.getNodes(newNode); }
};
Tools.toArray(nodes).forEach(function (child) {
child.setAttribute("data-" + _plugins.name+"_id", id); /**
}); * Remove an item from the dom and the items list
* @private
this.items.set(id, newNode); * @param {Number} id the id of the item to remove
_plugins.apply(newNode); * @returns
return newNode; */
} this.removeItem = function removeItem(id) {
}; var item = this.items[id];
if (item) {
/** _rootNode.removeChild(item);
* Renders the dom tree, adds nodes that are in the boundaries delete this.items[id];
* and removes the others _removeObserversForId(id);
* @private return true;
* @returns true boundaries are set } else {
*/ return false;
this.render = function render() { }
// If the number of items to render is all (*) };
// Then get the number of items
var _tmpNb = _nb == "*" ? _model.getNbItems() : _nb; /**
* create a new node. Actually makes a clone of the initial one
// This will store the items to remove * and adds pluginname_id to each node, then calls plugins.apply to apply all plugins
var marked = []; * @private
* @param id
// Render only if boundaries have been set * @param pluginName
if (_nb !== null && _start !== null) { * @returns the associated node
*/
// Loop through the existing items this.create = function create(id) {
this.items.loop(function (value, idx) { if (_model.has(id)) {
// If an item is out of the boundary var newNode = _node.cloneNode(true),
if (idx < _start || idx >= (_start + _tmpNb) || !_model.has(idx)) { nodes = DomUtils.getNodes(newNode);
// Mark it
marked.push(idx); Tools.toArray(nodes).forEach(function (child) {
} child.setAttribute("data-" + _plugins.name+"_id", id);
}, this); });
// Remove the marked item from the highest id to the lowest this.items[id] = newNode;
// Doing this will avoid the id change during removal _plugins.apply(newNode);
// (removing id 2 will make id 3 becoming 2) return newNode;
marked.sort(Tools.compareNumbers).reverse().forEach(this.removeItem, this); }
};
// Now that we have removed the old nodes
// Add the missing one /**
for (var i=_start, l=_tmpNb+_start; i<l; i++) { * Renders the dom tree, adds nodes that are in the boundaries
this.addItem(i); * and removes the others
} * @private
return true; * @returns true boundaries are set
} else { */
return false; this.render = function render() {
} // If the number of items to render is all (*)
}; // Then get the number of items
var _tmpNb = _nb == "*" ? _model.getNbItems() : _nb;
this.setPlugins($plugins);
this.setRootNode($rootNode); // This will store the items to remove
}; var marked = [];
/** // Render only if boundaries have been set
* Save an itemRenderer according to its id if (_nb !== null && _start !== null) {
* @private
* @param {String} id the id of the itemRenderer // Loop through the existing items
* @param {ItemRenderer} itemRenderer an itemRenderer object Tools.loop(this.items, function (value, idx) {
*/ // If an item is out of the boundary
this.setItemRenderer = function setItemRenderer(id, itemRenderer) { idx = Number(idx);
id = id || "default";
_itemRenderers[id] = itemRenderer; if (idx < _start || idx >= (_start + _tmpNb) || !_model.has(idx)) {
}; // Mark it
marked.push(idx);
/** }
* Get an itemRenderer }, this);
* @private
* @param {String} id the name of the itemRenderer // Remove the marked item from the highest id to the lowest
* @returns the itemRenderer // Doing this will avoid the id change during removal
*/ // (removing id 2 will make id 3 becoming 2)
this.getItemRenderer = function getItemRenderer(id) { marked.sort(Tools.compareNumbers).reverse().forEach(this.removeItem, this);
return _itemRenderers[id];
}; // Now that we have removed the old nodes
// Add the missing one
/** for (var i=_start, l=_tmpNb+_start; i<l; i++) {
* Expands the inner dom nodes of a given dom node, filling it with model's values this.addItem(i);
* @param {HTMLElement|SVGElement} node the dom node to apply foreach to }
*/ return true;
this.foreach = function foreach(node, idItemRenderer, start, nb) { } else {
var itemRenderer = new this.ItemRenderer(this.plugins, node); return false;
}
itemRenderer.setStart(start || 0); };
itemRenderer.setNb(nb || "*");
this.setPlugins($plugins);
itemRenderer.render(); this.setRootNode($rootNode);
};
// Add the newly created item
/**
* Save an itemRenderer according to its id
* @private
* @param {String} id the id of the itemRenderer
* @param {ItemRenderer} itemRenderer an itemRenderer object
*/
this.setItemRenderer = function setItemRenderer(id, itemRenderer) {
id = id || "default";
_itemRenderers[id] = itemRenderer;
};
/**
* Get an itemRenderer
* @private
* @param {String} id the name of the itemRenderer
* @returns the itemRenderer
*/
this.getItemRenderer = function getItemRenderer(id) {
return _itemRenderers[id];
};
/**
* Expands the inner dom nodes of a given dom node, filling it with model's values
* @param {HTMLElement|SVGElement} node the dom node to apply foreach to
*/
this.foreach = function foreach(node, idItemRenderer, start, nb) {
var itemRenderer = new this.ItemRenderer(this.plugins, node);
itemRenderer.setStart(start || 0);
itemRenderer.setNb(nb || "*");
itemRenderer.render();
// Add the newly created item
_model.watch("added", itemRenderer.render, itemRenderer); _model.watch("added", itemRenderer.render, itemRenderer);
// If an item is deleted // If an item is deleted
_model.watch("deleted", function (idx) { _model.watch("deleted", function (idx) {
itemRenderer.render(); itemRenderer.render();
// Also remove all observers // Also remove all observers
this.observers[idx] && this.observers[idx].forEach(function (handler) { _removeObserversForId(idx);
_model.unwatchValue(handler);
}, this);
delete this.observers[idx];
},this); },this);
this.setItemRenderer(idItemRenderer, itemRenderer); this.setItemRenderer(idItemRenderer, itemRenderer);
...@@ -394,13 +425,13 @@ function BindPlugin(Store, Observable, Tools, DomUtils) { ...@@ -394,13 +425,13 @@ function BindPlugin(Store, Observable, Tools, DomUtils) {
* @returns true if the foreach exists * @returns true if the foreach exists
*/ */
this.updateStart = function updateStart(id, start) { this.updateStart = function updateStart(id, start) {
var itemRenderer = this.getItemRenderer(id); var itemRenderer = this.getItemRenderer(id);
if (itemRenderer) { if (itemRenderer) {
itemRenderer.setStart(start); itemRenderer.setStart(start);
return true; return true;
} else { } else {
return false; return false;
} }
}; };
/** /**
...@@ -410,13 +441,13 @@ function BindPlugin(Store, Observable, Tools, DomUtils) { ...@@ -410,13 +441,13 @@ function BindPlugin(Store, Observable, Tools, DomUtils) {
* @returns true if the foreach exists * @returns true if the foreach exists
*/ */
this.updateNb = function updateNb(id, nb) { this.updateNb = function updateNb(id, nb) {
var itemRenderer = this.getItemRenderer(id); var itemRenderer = this.getItemRenderer(id);
if (itemRenderer) { if (itemRenderer) {
itemRenderer.setNb(nb); itemRenderer.setNb(nb);
return true; return true;
} else { } else {
return false; return false;
} }
}; };
/** /**
...@@ -425,201 +456,201 @@ function BindPlugin(Store, Observable, Tools, DomUtils) { ...@@ -425,201 +456,201 @@ function BindPlugin(Store, Observable, Tools, DomUtils) {
* @returns true if the foreach exists * @returns true if the foreach exists
*/ */
this.refresh = function refresh(id) { this.refresh = function refresh(id) {
var itemRenderer = this.getItemRenderer(id); var itemRenderer = this.getItemRenderer(id);
if (itemRenderer) { if (itemRenderer) {
itemRenderer.render(); itemRenderer.render();
return true; return true;
} else { } else {
return false; return false;
} }
}; };
/** /**
* Both ways binding between a dom node attributes and the model * Both ways binding between a dom node attributes and the model
* @param {HTMLElement|SVGElement} node the dom node to apply the plugin to * @param {HTMLElement|SVGElement} node the dom node to apply the plugin to
* @param {String} name the name of the property to look for in the model's value * @param {String} name the name of the property to look for in the model's value
* @returns * @returns
*/ */
this.bind = function bind(node, property, name) { this.bind = function bind(node, property, name) {
// Name can be unset if the value of a row is plain text // Name can be unset if the value of a row is plain text
name = name || ""; name = name || "";
// In case of an array-like model the id is the index of the model's item to look for. // In case of an array-like model the id is the index of the model's item to look for.
// The _id is added by the foreach function // The _id is added by the foreach function
var id = node.getAttribute("data-" + this.plugins.name+"_id"), var id = node.getAttribute("data-" + this.plugins.name+"_id"),
// Else, it is the first element of the following // Else, it is the first element of the following
split = name.split("."), split = name.split("."),
// So the index of the model is either id or the first element of split // So the index of the model is either id or the first element of split
modelIdx = id || split.shift(), modelIdx = id || split.shift(),
// And the name of the property to look for in the value is // And the name of the property to look for in the value is
prop = id ? name : split.join("."), prop = id ? name : split.join("."),
// Get the model's value // Get the model's value
get = Tools.getNestedProperty(_model.get(modelIdx), prop), get = Tools.getNestedProperty(_model.get(modelIdx), prop),
// When calling bind like bind:newBinding,param1, param2... we need to get them // When calling bind like bind:newBinding,param1, param2... we need to get them
extraParam = Tools.toArray(arguments).slice(3); extraParam = Tools.toArray(arguments).slice(3);
// 0 and false are acceptable falsy values // 0 and false are acceptable falsy values
if (get || get === 0 || get === false) { if (get || get === 0 || get === false) {
// If the binding hasn't been overriden // If the binding hasn't been overriden
if (!this.execBinding.apply(this, if (!this.execBinding.apply(this,
[node, property, get] [node, property, get]
// Extra params are passed to the new binding too // Extra params are passed to the new binding too
.concat(extraParam))) { .concat(extraParam))) {
// Execute the default one which is a simple assignation // Execute the default one which is a simple assignation
//node[property] = get; //node[property] = get;
DomUtils.setAttribute(node, property, get); DomUtils.setAttribute(node, property, get);
} }
} }
// Only watch for changes (double way data binding) if the binding // Only watch for changes (double way data binding) if the binding
// has not been redefined // has not been redefined
if (!this.hasBinding(property)) { if (!this.hasBinding(property)) {
node.addEventListener("change", function (event) { node.addEventListener("change", function (event) {
if (_model.has(modelIdx)) { if (_model.has(modelIdx)) {
if (prop) { if (prop) {
_model.update(modelIdx, name, node[property]); _model.update(modelIdx, name, node[property]);
} else { } else {
_model.set(modelIdx, node[property]); _model.set(modelIdx, node[property]);
} }
} }
}, true); }, true);
} }
// Watch for changes // Watch for changes
this.observers[modelIdx] = this.observers[modelIdx] || []; this.observers[modelIdx] = this.observers[modelIdx] || [];
this.observers[modelIdx].push(_model.watchValue(modelIdx, function (value) { this.observers[modelIdx].push(_model.watchValue(modelIdx, function (value) {
if (!this.execBinding.apply(this, if (!this.execBinding.apply(this,
[node, property, Tools.getNestedProperty(value, prop)] [node, property, Tools.getNestedProperty(value, prop)]
// passing extra params too // passing extra params too
.concat(extraParam))) { .concat(extraParam))) {
//node[property] = Tools.getNestedProperty(value, prop); //node[property] = Tools.getNestedProperty(value, prop);
DomUtils.setAttribute(node, property, Tools.getNestedProperty(value, prop)); DomUtils.setAttribute(node, property, Tools.getNestedProperty(value, prop));
} }
}, this)); }, this));
}; };
/** /**
* Set the node's value into the model, the name is the model's property * Set the node's value into the model, the name is the model's property
* @private * @private
* @param {HTMLElement|SVGElement} node * @param {HTMLElement|SVGElement} node
* @returns true if the property is added * @returns true if the property is added
*/ */
this.set = function set(node) { this.set = function set(node) {
if (DomUtils.isAcceptedType(node) && node.name) { if (DomUtils.isAcceptedType(node) && node.name) {
_model.set(node.name, node.value); _model.set(node.name, node.value);
return true; return true;
} else { } else {
return false; return false;
} }
}; };
this.getItemIndex = function getElementId(dom) { this.getItemIndex = function getElementId(dom) {
var dataset = DomUtils.getDataset(dom); var dataset = DomUtils.getDataset(dom);
if (dataset && typeof dataset[this.plugins.name + "_id"] != "undefined") { if (dataset && typeof dataset[this.plugins.name + "_id"] != "undefined") {
return +dataset[this.plugins.name + "_id"]; return +dataset[this.plugins.name + "_id"];
} else { } else {
return false; return false;
} }
}; };
/** /**
* Prevents the submit and set the model with all form's inputs * Prevents the submit and set the model with all form's inputs
* @param {HTMLFormElement} form * @param {HTMLFormElement} DOMfrom
* @returns true if valid form * @returns true if valid form
*/ */
this.form = function form(form) { this.form = function form(DOMform) {
if (form && form.nodeName == "FORM") { if (DOMform && DOMform.nodeName == "FORM") {
var that = this; var that = this;
form.addEventListener("submit", function (event) { DOMform.addEventListener("submit", function (event) {
Tools.toArray(form.querySelectorAll("[name]")).forEach(that.set, that); Tools.toArray(DOMform.querySelectorAll("[name]")).forEach(that.set, that);
event.preventDefault(); event.preventDefault();
}, true); }, true);
return true; return true;
} else { } else {
return false; return false;
} }
}; };
/** /**
* Add a new way to handle a binding * Add a new way to handle a binding
* @param {String} name of the binding * @param {String} name of the binding
* @param {Function} binding the function to handle the binding * @param {Function} binding the function to handle the binding
* @returns * @returns
*/ */
this.addBinding = function addBinding(name, binding) { this.addBinding = function addBinding(name, binding) {
if (name && typeof name == "string" && typeof binding == "function") { if (name && typeof name == "string" && typeof binding == "function") {
_bindings[name] = binding; _bindings[name] = binding;
return true; return true;
} else { } else {
return false; return false;
} }
}; };
/** /**
* Execute a binding * Execute a binding
* Only used by the plugin * Only used by the plugin
* @private * @private
* @param {HTMLElement} node the dom node on which to execute the binding * @param {HTMLElement} node the dom node on which to execute the binding
* @param {String} name the name of the binding * @param {String} name the name of the binding
* @param {Any type} value the value to pass to the function * @param {Any type} value the value to pass to the function
* @returns * @returns
*/ */
this.execBinding = function execBinding(node, name) { this.execBinding = function execBinding(node, name) {
if (this.hasBinding(name)) { if (this.hasBinding(name)) {
_bindings[name].apply(node, Array.prototype.slice.call(arguments, 2)); _bindings[name].apply(node, Array.prototype.slice.call(arguments, 2));
return true; return true;
} else { } else {
return false; return false;
} }
}; };
/** /**
* Check if the binding exists * Check if the binding exists
* @private * @private
* @param {String} name the name of the binding * @param {String} name the name of the binding
* @returns * @returns
*/ */
this.hasBinding = function hasBinding(name) { this.hasBinding = function hasBinding(name) {
return _bindings.hasOwnProperty(name); return _bindings.hasOwnProperty(name);
}; };
/** /**
* Get a binding * Get a binding
* For debugging only * For debugging only
* @private * @private
* @param {String} name the name of the binding * @param {String} name the name of the binding
* @returns * @returns
*/ */
this.getBinding = function getBinding(name) { this.getBinding = function getBinding(name) {
return _bindings[name]; return _bindings[name];
}; };
/** /**
* Add multiple binding at once * Add multiple binding at once
* @param {Object} list the list of bindings to add * @param {Object} list the list of bindings to add
* @returns * @returns
*/ */
this.addBindings = function addBindings(list) { this.addBindings = function addBindings(list) {
return Tools.loop(list, function (binding, name) { return Tools.loop(list, function (binding, name) {
this.addBinding(name, binding); this.addBinding(name, binding);
}, this); }, this);
}; };
// Inits the model // Inits the model
this.setModel($model); this.setModel($model);
// Inits bindings // Inits bindings
this.addBindings($bindings); this.addBindings($bindings);
}; };
}); });
...@@ -6,6 +6,8 @@ ...@@ -6,6 +6,8 @@
define(["Tools"], function (Tools) { define(["Tools"], function (Tools) {
"use strict";
return { return {
/** /**
* Returns a NodeList including the given dom node, * Returns a NodeList including the given dom node,
......
...@@ -14,121 +14,123 @@ define(["DomUtils"], ...@@ -14,121 +14,123 @@ define(["DomUtils"],
*/ */
function EventPlugin(Utils) { function EventPlugin(Utils) {
/** "use strict";
* The event plugin constructor.
* ex: new EventPlugin({method: function(){} ...}, false); /**
* @param {Object} the object that has the event handling methods * The event plugin constructor.
* @param {Boolean} $isMobile if the event handler has to map with touch events * ex: new EventPlugin({method: function(){} ...}, false);
*/ * @param {Object} the object that has the event handling methods
return function EventPluginConstructor($parent, $isMobile) { * @param {Boolean} $isMobile if the event handler has to map with touch events
*/
/** return function EventPluginConstructor($parent, $isMobile) {
* The parent callback
* @private /**
*/ * The parent callback
var _parent = null, * @private
*/
/** var _parent = null,
* The mapping object.
* @private /**
*/ * The mapping object.
_map = { * @private
"mousedown" : "touchstart", */
"mouseup" : "touchend", _map = {
"mousemove" : "touchmove" "mousedown" : "touchstart",
}, "mouseup" : "touchend",
"mousemove" : "touchmove"
/** },
* Is touch device.
* @private /**
*/ * Is touch device.
_isMobile = !!$isMobile; * @private
*/
/** _isMobile = !!$isMobile;
* Add mapped event listener (for testing purpose).
* @private /**
*/ * Add mapped event listener (for testing purpose).
this.addEventListener = function addEventListener(node, event, callback, useCapture) { * @private
node.addEventListener(this.map(event), callback, !!useCapture); */
}; this.addEventListener = function addEventListener(node, event, callback, useCapture) {
node.addEventListener(this.map(event), callback, !!useCapture);
/** };
* Listen to DOM events.
* @param {Object} node DOM node /**
* @param {String} name event's name * Listen to DOM events.
* @param {String} listener callback's name * @param {Object} node DOM node
* @param {String} useCapture string * @param {String} name event's name
*/ * @param {String} listener callback's name
this.listen = function listen(node, name, listener, useCapture) { * @param {String} useCapture string
this.addEventListener(node, name, function(e){ */
_parent[listener].call(_parent, e, node); this.listen = function listen(node, name, listener, useCapture) {
}, !!useCapture); this.addEventListener(node, name, function(e){
}; _parent[listener].call(_parent, e, node);
}, !!useCapture);
/** };
* Delegate the event handling to a parent DOM element
* @param {Object} node DOM node /**
* @param {String} selector CSS3 selector to the element that listens to the event * Delegate the event handling to a parent DOM element
* @param {String} name event's name * @param {Object} node DOM node
* @param {String} listener callback's name * @param {String} selector CSS3 selector to the element that listens to the event
* @param {String} useCapture string * @param {String} name event's name
*/ * @param {String} listener callback's name
this.delegate = function delegate(node, selector, name, listener, useCapture) { * @param {String} useCapture string
this.addEventListener(node, name, function(event){ */
if (Utils.matches(node, selector, event.target)) { this.delegate = function delegate(node, selector, name, listener, useCapture) {
_parent[listener].call(_parent, event, node); this.addEventListener(node, name, function(event){
} if (Utils.matches(node, selector, event.target)) {
}, !!useCapture); _parent[listener].call(_parent, event, node);
}; }
}, !!useCapture);
/** };
* Get the parent object.
* @return {Object} the parent object /**
*/ * Get the parent object.
this.getParent = function getParent() { * @return {Object} the parent object
return _parent; */
}; this.getParent = function getParent() {
return _parent;
/** };
* Set the parent object.
* The parent object is an object which the functions are called by node listeners. /**
* @param {Object} the parent object * Set the parent object.
* @return true if object has been set * The parent object is an object which the functions are called by node listeners.
*/ * @param {Object} the parent object
this.setParent = function setParent(parent) { * @return true if object has been set
if (parent instanceof Object){ */
_parent = parent; this.setParent = function setParent(parent) {
return true; if (parent instanceof Object){
} _parent = parent;
return false; return true;
}; }
return false;
/** };
* Get event mapping.
* @param {String} event's name /**
* @return the mapped event's name * Get event mapping.
*/ * @param {String} event's name
this.map = function map(name) { * @return the mapped event's name
return _isMobile ? (_map[name] || name) : name; */
}; this.map = function map(name) {
return _isMobile ? (_map[name] || name) : name;
/** };
* Set event mapping.
* @param {String} event's name /**
* @param {String} event's value * Set event mapping.
* @return true if mapped * @param {String} event's name
*/ * @param {String} event's value
this.setMap = function setMap(name, value) { * @return true if mapped
if (typeof name == "string" && */
typeof value == "string") { this.setMap = function setMap(name, value) {
_map[name] = value; if (typeof name == "string" &&
return true; typeof value == "string") {
} _map[name] = value;
return false; return true;
}; }
return false;
//init };
this.setParent($parent);
}; //init
this.setParent($parent);
};
}); });
...@@ -15,6 +15,8 @@ define(["Store", "Tools"], ...@@ -15,6 +15,8 @@ define(["Store", "Tools"],
*/ */
function LocalStore(Store, Tools) { function LocalStore(Store, Tools) {
"use strict";
function LocalStoreConstructor() { function LocalStoreConstructor() {
/** /**
...@@ -96,7 +98,7 @@ function LocalStore(Store, Tools) { ...@@ -96,7 +98,7 @@ function LocalStore(Store, Tools) {
return function LocalStoreFactory(init) { return function LocalStoreFactory(init) {
LocalStoreConstructor.prototype = new Store(init); LocalStoreConstructor.prototype = new Store(init);
return new LocalStoreConstructor; return new LocalStoreConstructor();
}; };
}); });
...@@ -13,179 +13,185 @@ define(["StateMachine", "Store", "Plugins", "DomUtils", "Tools"], ...@@ -13,179 +13,185 @@ define(["StateMachine", "Store", "Plugins", "DomUtils", "Tools"],
*/ */
function OObject(StateMachine, Store, Plugins, DomUtils, Tools) { function OObject(StateMachine, Store, Plugins, DomUtils, Tools) {
return function OObjectConstructor(otherStore) { "use strict";
/** return function OObjectConstructor(otherStore) {
* This function creates the dom of the UI from its template
* It then queries the dom for data- attributes /**
* It can't be executed if the template is not set * This function creates the dom of the UI from its template
* @private * It then queries the dom for data- attributes
*/ * It can't be executed if the template is not set
var render = function render(UI) { * @private
*/
// The place where the template will be created var render = function render(UI) {
// is either the currentPlace where the node is placed
// or a temporary div // The place where the template will be created
var baseNode = _currentPlace || document.createElement("div"); // is either the currentPlace where the node is placed
// or a temporary div
// If the template is set var baseNode = _currentPlace || document.createElement("div");
if (UI.template) {
// In this function, the thisObject is the UI's prototype // If the template is set
// UI is the UI that has OObject as prototype if (UI.template) {
if (typeof UI.template == "string") { // In this function, the thisObject is the UI's prototype
// Let the browser do the parsing, can't be faster & easier. // UI is the UI that has OObject as prototype
baseNode.innerHTML = UI.template.trim(); if (typeof UI.template == "string") {
} else if (DomUtils.isAcceptedType(UI.template)) { // Let the browser do the parsing, can't be faster & easier.
// If it's already an HTML element baseNode.innerHTML = UI.template.trim();
baseNode.appendChild(UI.template); } else if (DomUtils.isAcceptedType(UI.template)) {
} // If it's already an HTML element
baseNode.appendChild(UI.template);
// The UI must be placed in a unique dom node }
// If not, there can't be multiple UIs placed in the same parentNode
// as it wouldn't be possible to know which node would belong to which UI // The UI must be placed in a unique dom node
// This is probably a DOM limitation. // If not, there can't be multiple UIs placed in the same parentNode
if (baseNode.childNodes.length > 1) { // as it wouldn't be possible to know which node would belong to which UI
throw Error("UI.template should have only one parent node"); // This is probably a DOM limitation.
} else { if (baseNode.childNodes.length > 1) {
UI.dom = baseNode.childNodes[0]; throw new Error("UI.template should have only one parent node");
} } else {
UI.dom = baseNode.childNodes[0];
UI.plugins.apply(UI.dom); }
} else { UI.plugins.apply(UI.dom);
// An explicit message I hope
throw Error("UI.template must be set prior to render"); } else {
} // An explicit message I hope
}, throw new Error("UI.template must be set prior to render");
}
/** },
* This function appends the dom tree to the given dom node.
* This dom node should be somewhere in the dom of the application /**
* @private * This function appends the dom tree to the given dom node.
*/ * This dom node should be somewhere in the dom of the application
place = function place(UI, place, beforeNode) { * @private
if (place) { */
// IE (until 9) apparently fails to appendChild when insertBefore's second argument is null, hence this. place = function place(UI, DOMplace, beforeNode) {
beforeNode ? place.insertBefore(UI.dom, beforeNode) : place.appendChild(UI.dom); if (DOMplace) {
// Also save the new place, so next renderings // IE (until 9) apparently fails to appendChild when insertBefore's second argument is null, hence this.
// will be made inside it if (beforeNode) {
_currentPlace = place; DOMplace.insertBefore(UI.dom, beforeNode);
} } else {
}, DOMplace.appendChild(UI.dom);
}
/** // Also save the new place, so next renderings
* Does rendering & placing in one function // will be made inside it
* @private _currentPlace = DOMplace;
*/ }
renderNPlace = function renderNPlace(UI, dom) { },
render(UI);
place.apply(null, Tools.toArray(arguments)); /**
}, * Does rendering & placing in one function
* @private
/** */
* This stores the current place renderNPlace = function renderNPlace(UI, dom) {
* If this is set, this is the place where new templates render(UI);
* will be appended place.apply(null, Tools.toArray(arguments));
* @private },
*/
_currentPlace = null, /**
* This stores the current place
/** * If this is set, this is the place where new templates
* The UI's stateMachine. * will be appended
* Much better than if(stuff) do(stuff) else if (!stuff and stuff but not stouff) do (otherstuff) * @private
* Please open an issue if you want to propose a better one */
* @private _currentPlace = null,
*/
_stateMachine = new StateMachine("Init", { /**
"Init": [["render", render, this, "Rendered"], * The UI's stateMachine.
["place", renderNPlace, this, "Rendered"]], * Much better than if(stuff) do(stuff) else if (!stuff and stuff but not stouff) do (otherstuff)
"Rendered": [["place", place, this], * Please open an issue if you want to propose a better one
["render", render, this]] * @private
}); */
_stateMachine = new StateMachine("Init", {
/** "Init": [["render", render, this, "Rendered"],
* The UI's Store ["place", renderNPlace, this, "Rendered"]],
* It has set/get/del/has/watch/unwatch methods "Rendered": [["place", place, this],
* @see Emily's doc for more info on how it works. ["render", render, this]]
*/ });
this.model = otherStore instanceof Store ? otherStore : new Store;
/**
/** * The UI's Store
* The module that will manage the plugins for this UI * It has set/get/del/has/watch/unwatch methods
* @see Olives/Plugins' doc for more info on how it works. * @see Emily's doc for more info on how it works.
*/ */
this.plugins = new Plugins(); this.model = otherStore instanceof Store ? otherStore : new Store();
/** /**
* Describes the template, can either be like "&lt;p&gt;&lt;/p&gt;" or HTMLElements * The module that will manage the plugins for this UI
* @type string or HTMLElement|SVGElement * @see Olives/Plugins' doc for more info on how it works.
*/ */
this.template = null; this.plugins = new Plugins();
/** /**
* This will hold the dom nodes built from the template. * Describes the template, can either be like "&lt;p&gt;&lt;/p&gt;" or HTMLElements
*/ * @type string or HTMLElement|SVGElement
this.dom = null; */
this.template = null;
/**
* Place the UI in a given dom node /**
* @param node the node on which to append the UI * This will hold the dom nodes built from the template.
* @param beforeNode the dom before which to append the UI */
*/ this.dom = null;
this.place = function place(node, beforeNode) {
_stateMachine.event("place", this, node, beforeNode); /**
}; * Place the UI in a given dom node
* @param node the node on which to append the UI
/** * @param beforeNode the dom before which to append the UI
* Renders the template to dom nodes and applies the plugins on it */
* It requires the template to be set first this.place = function place(node, beforeNode) {
*/ _stateMachine.event("place", this, node, beforeNode);
this.render = function render() { };
_stateMachine.event("render", this);
}; /**
* Renders the template to dom nodes and applies the plugins on it
/** * It requires the template to be set first
* Set the UI's template from a DOM element */
* @param {HTMLElement|SVGElement} dom the dom element that'll become the template of the UI this.render = function render() {
* @returns true if dom is an HTMLElement|SVGElement _stateMachine.event("render", this);
*/ };
this.setTemplateFromDom = function setTemplateFromDom(dom) {
if (DomUtils.isAcceptedType(dom)) { /**
this.template = dom; * Set the UI's template from a DOM element
return true; * @param {HTMLElement|SVGElement} dom the dom element that'll become the template of the UI
} else { * @returns true if dom is an HTMLElement|SVGElement
return false; */
} this.setTemplateFromDom = function setTemplateFromDom(dom) {
}; if (DomUtils.isAcceptedType(dom)) {
this.template = dom;
/** return true;
* Transforms dom nodes into a UI. } else {
* It basically does a setTemplateFromDOM, then a place return false;
* It's a helper function }
* @param {HTMLElement|SVGElement} node the dom to transform to a UI };
* @returns true if dom is an HTMLElement|SVGElement
*/ /**
this.alive = function alive(dom) { * Transforms dom nodes into a UI.
if (DomUtils.isAcceptedType(dom)) { * It basically does a setTemplateFromDOM, then a place
this.setTemplateFromDom(dom); * It's a helper function
this.place(dom.parentNode, dom.nextElementSibling); * @param {HTMLElement|SVGElement} node the dom to transform to a UI
return true; * @returns true if dom is an HTMLElement|SVGElement
} else { */
return false; this.alive = function alive(dom) {
} if (DomUtils.isAcceptedType(dom)) {
this.setTemplateFromDom(dom);
}; this.place(dom.parentNode, dom.nextElementSibling);
return true;
/** } else {
* Get the current dom node where the UI is placed. return false;
* for debugging purpose }
* @private
* @return {HTMLElement} node the dom where the UI is placed. };
*/
this.getCurrentPlace = function(){ /**
return _currentPlace; * Get the current dom node where the UI is placed.
}; * for debugging purpose
* @private
}; * @return {HTMLElement} node the dom where the UI is placed.
*/
this.getCurrentPlace = function(){
return _currentPlace;
};
};
}); });
...@@ -12,77 +12,79 @@ define(["OObject", "Tools"], ...@@ -12,77 +12,79 @@ define(["OObject", "Tools"],
*/ */
function PlacePlugin(OObject, Tools) { function PlacePlugin(OObject, Tools) {
/** "use strict";
* Intilialize a Place.plugin with a list of OObjects
* @param {Object} $uis a list of OObjects such as:
* {
* "header": new OObject(),
* "list": new OObject()
* }
* @Constructor
*/
return function PlacePluginConstructor($uis) {
/** /**
* The list of uis currently set in this place plugin * Intilialize a Place.plugin with a list of OObjects
* @private * @param {Object} $uis a list of OObjects such as:
*/ * {
var _uis = {}; * "header": new OObject(),
* "list": new OObject()
* }
* @Constructor
*/
return function PlacePluginConstructor($uis) {
/** /**
* Attach an OObject to this DOM element * The list of uis currently set in this place plugin
* @param {HTML|SVGElement} node the dom node where to attach the OObject * @private
* @param {String} the name of the OObject to attach */
* @throws {NoSuchOObject} an error if there's no OObject for the given name var _uis = {};
*/
this.place = function place(node, name) {
if (_uis[name] instanceof OObject) {
_uis[name].place(node);
} else {
throw new Error(name + " is not an OObject UI in place:"+name);
}
};
/** /**
* Add an OObject that can be attached to a dom element * Attach an OObject to this DOM element
* @param {String} the name of the OObject to add to the list * @param {HTML|SVGElement} node the dom node where to attach the OObject
* @param {OObject} ui the OObject to add the list * @param {String} the name of the OObject to attach
* @returns {Boolean} true if the OObject was added * @throws {NoSuchOObject} an error if there's no OObject for the given name
*/ */
this.set = function set(name, ui) { this.place = function place(node, name) {
if (typeof name == "string" && ui instanceof OObject) { if (_uis[name] instanceof OObject) {
_uis[name] = ui; _uis[name].place(node);
return true; } else {
} else { throw new Error(name + " is not an OObject UI in place:"+name);
return false; }
} };
};
/** /**
* Add multiple dom elements at once * Add an OObject that can be attached to a dom element
* @param {Object} $uis a list of OObjects such as: * @param {String} the name of the OObject to add to the list
* { * @param {OObject} ui the OObject to add the list
* "header": new OObject(), * @returns {Boolean} true if the OObject was added
* "list": new OObject() */
* } this.set = function set(name, ui) {
*/ if (typeof name == "string" && ui instanceof OObject) {
this.setAll = function setAll(uis) { _uis[name] = ui;
Tools.loop(uis, function (ui, name) { return true;
this.set(name, ui); } else {
}, this); return false;
}; }
};
/** /**
* Returns an OObject from the list given its name * Add multiple dom elements at once
* @param {String} the name of the OObject to get * @param {Object} $uis a list of OObjects such as:
* @returns {OObject} OObject for the given name * {
*/ * "header": new OObject(),
this.get = function get(name) { * "list": new OObject()
return _uis[name]; * }
}; */
this.setAll = function setAll(uis) {
Tools.loop(uis, function (ui, name) {
this.set(name, ui);
}, this);
};
this.setAll($uis); /**
* Returns an OObject from the list given its name
* @param {String} the name of the OObject to get
* @returns {OObject} OObject for the given name
*/
this.get = function get(name) {
return _uis[name];
};
}; this.setAll($uis);
};
}); });
...@@ -17,6 +17,8 @@ define(["Tools", "DomUtils"], ...@@ -17,6 +17,8 @@ define(["Tools", "DomUtils"],
*/ */
function Plugins(Tools, DomUtils) { function Plugins(Tools, DomUtils) {
"use strict";
return function PluginsConstructor($plugins) { return function PluginsConstructor($plugins) {
/** /**
......
...@@ -12,6 +12,8 @@ define(["Observable", "Tools"], ...@@ -12,6 +12,8 @@ define(["Observable", "Tools"],
*/ */
function SocketIOTransport(Observable, Tools) { function SocketIOTransport(Observable, Tools) {
"use strict";
/** /**
* Defines the SocketIOTransport * Defines the SocketIOTransport
* @private * @private
...@@ -47,7 +49,7 @@ function SocketIOTransport(Observable, Tools) { ...@@ -47,7 +49,7 @@ function SocketIOTransport(Observable, Tools) {
*/ */
this.getSocket = function getSocket() { this.getSocket = function getSocket() {
return _socket; return _socket;
}, };
/** /**
* Subscribe to a socket event * Subscribe to a socket event
...@@ -56,7 +58,7 @@ function SocketIOTransport(Observable, Tools) { ...@@ -56,7 +58,7 @@ function SocketIOTransport(Observable, Tools) {
*/ */
this.on = function on(event, func) { this.on = function on(event, func) {
return _socket.on(event, func); return _socket.on(event, func);
}, };
/** /**
* Subscribe to a socket event but disconnect as soon as it fires. * Subscribe to a socket event but disconnect as soon as it fires.
...@@ -95,15 +97,17 @@ function SocketIOTransport(Observable, Tools) { ...@@ -95,15 +97,17 @@ function SocketIOTransport(Observable, Tools) {
* @param {Object} scope the scope in which to execute the callback * @param {Object} scope the scope in which to execute the callback
*/ */
this.request = function request(channel, data, func, scope) { this.request = function request(channel, data, func, scope) {
if (typeof channel == "string" if (typeof channel == "string" &&
&& typeof data != "undefined") { typeof data != "undefined") {
var reqData = { var reqData = {
eventId: Date.now() + Math.floor(Math.random()*1e6), eventId: Date.now() + Math.floor(Math.random()*1e6),
data: data data: data
}, },
boundCallback = function () { boundCallback = function () {
func && func.apply(scope || null, arguments); if (func) {
func.apply(scope || null, arguments);
}
}; };
this.once(reqData.eventId, boundCallback); this.once(reqData.eventId, boundCallback);
...@@ -125,9 +129,9 @@ function SocketIOTransport(Observable, Tools) { ...@@ -125,9 +129,9 @@ function SocketIOTransport(Observable, Tools) {
* @returns * @returns
*/ */
this.listen = function listen(channel, data, func, scope) { this.listen = function listen(channel, data, func, scope) {
if (typeof channel == "string" if (typeof channel == "string" &&
&& typeof data != "undefined" typeof data != "undefined" &&
&& typeof func == "function") { typeof func == "function") {
var reqData = { var reqData = {
eventId: Date.now() + Math.floor(Math.random()*1e6), eventId: Date.now() + Math.floor(Math.random()*1e6),
...@@ -135,7 +139,9 @@ function SocketIOTransport(Observable, Tools) { ...@@ -135,7 +139,9 @@ function SocketIOTransport(Observable, Tools) {
keepAlive: true keepAlive: true
}, },
boundCallback = function () { boundCallback = function () {
func && func.apply(scope || null, arguments); if (func) {
func.apply(scope || null, arguments);
}
}, },
that = this; that = this;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment