/*!
* Copyright (c) 2009 Francesco Mele jsbeans@francescomele.com
*
* This Software is licenced under the LGPL Licence (GNU Lesser General
* Public License).
* In addition to the LGPL Licence the Software is subject to the
* following conditions:
*
* i every modification must be public and comunicated to the Author
* ii every "jsbean" added to this library must be self consistent
* except for the dependence from jsbeans-x.x.x.js
* iii copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
/**
* <code>jsbeans.mvc</code> is a small MVC (Model-View-Controller) Framework.<br/>
* Here are APIs, see <a href="../util/script_other/mvc/docs/index.html">reference</a> for usage or take a look at a <a href="../jsbeans.mvc.html">sample app</a>.
* @namespace jsbeans
* @class mvc
* @static
* */
jsbeans.mvc = {
/**
* Main responsable for rendering a content.<br/>
* It provides utility methods to match Model against a corresponding View.<br/>
* Should be the last call in a custom controller function/method, after creating or fetching a Model.
* @namespace jsbeans.mvc
* @class controller
* @static
*/
controller: {
/**
* A local cache of generic template Functions. See <a href="https://github.com/olado/doT">doU.js</a> documentation for a complete reference.
* @static
* */
cache: [],
/**
* It matches {@param} <code class="param">model</code> against {@param} <code class="param">view</code> and then invokes {@param} <code class="param">callback</code>.<br/>
* Internally it used <code>view.load</code> and <code><a href="https://github.com/olado/doT">doU.js</a></code> Javascript Template Engine, version 0.1.2. There's no need to include in your libraries as it has been directly included in source (doU license permits it).
* @method execute
* @param model {Object | JSON} data in Object or JSON format. May be local or remote, via AJAX request.
* @param view {String} the name of used View remotely loaded in <code>data-templatePath</code> custom attribute of <code>view</code> script tag (<code><script type="text/javascript" src="jsbeans/mvc.js" data-templatePath="views"></script></code>) or inline into document if <code>inline="true"</code> has been set (<code><script type="text/javascript" src="jsbeans/mvc.js" inline="true"></script></code>).
* @param callback {Function} a Function fired after View load and its match against Model. It takes the result of the matching as argument.<br/>
* Usually <code>callback(html)</code> simply does a <code>document.getElementById('anID').innerHTML = html;</code>
* @static
* */
execute: function(model, v, clbk) {
jsbeans.mvc.view.load(v, function(tmpl) {
if (jsbeans.mvc.controller.cache[v] == null || typeof jsbeans.mvc.controller.cache[v] == "undefined") {
// source from doU.js version 0.1.2
var str = '', tb = "{{", te = "}}", m, l;
var arr = tmpl.replace(/\s*<!\[CDATA\[\s*|\s*\]\]>\s*|[\r\n\t]|(\/\*[\s\S]*?\*\/)/g, '').split(tb).join(te +'\x1b').split(te);
for (m=0,l=arr.length; m < l; m++) {
str += arr[m].charAt(0) !== '\x1b' ?
"out+='" + arr[m].replace(/(\\|["'])/g, '\\$1') + "'" : (arr[m].charAt(1) === '=' ?
';out+=(' + arr[m].substr(2) + ');' : (arr[m].charAt(1) === '!' ?
';out+=(' + arr[m].substr(2) + ").toString().replace(/&(?!\\w+;)/g, '&').split('<').join('<').split('>').join('>').split('" + '"' + "').join('"').split(" + '"' + "'" + '"' + ").join(''').split('/').join('/');" : ';' + arr[m].substr(1)));
}
str = ('var out="";'+str+';return out;').split("out+='';").join('').split('var out="";out+=').join('var out=');
try {
jsbeans.mvc.controller.cache[v] = new Function("data", str);
}
catch (e) {
// hide errors as usual
//alert("Error:\n" + e.message);
}
}
clbk(jsbeans.mvc.controller.cache[v](model));
});
}
},
/**
* Simple small collection of utility methods to manage Object.
* @namespace jsbeans.mvc
* @class model
* @static
*/
model: {
/**
* Tries to create a new instance of {@param} <code class="param">model</code> evaluating <code>new <model>()</code>
* @method create
* @param model {String} a name of a class
* @return {Object} a new instance of <code><model></code>. <code>null</code> otherwise
* @static
* */
create: function(m) {
try {
eval("var res = new " + m + "();");
return res;
}
catch (e) {
return null;
}
},
/**
* Cicles through {@param} <code class="param">json</code> keys and stores corresponding values into {@param} <code class="param">object</code>.<br/>
* Note that {@param} <code class="param">object</code> will have new values only if it already has same properties, or a superset, of {@param} <code class="param">json</code>. It does <strong>not</strong> add new property on {@param} <code class="param">object</code>.
* @param json {JSON} a set of key/value pairs in JSON format
* @param object {Object} an instance of an Object.
* @static
* */
populate: function(json, obj) {
for (var p in json) {
if (obj.hasOwnProperty(p)) {
obj[p] = json[p];
}
}
},
/**
* Merges objects passed as arguments. In other words the returning object will be a JSON resulting as union of all objects passed, with no duplicates.<br/>
* If first argument is an <code>Array</code> no other argument is taken into account and the merge process will cycle through that Array.<br/>
* Any subsequent key will override previously key/value pair.
* @param first {JSON | Object | Array} first (and unique in case of type Array) object to cycle through
* @param [object] {arguments[1..n]} optional arguments of objects to merge. Ignored when <code class="param">first</code> is of type <code>Array</code>
* @return {JSON} merged object. An empty JSON if <code class="param">first</code> is <code>null</code> or <code>undefined</code>
* @static
* */
merge: function(o) {
if (o != null && typeof(o) != "undefined") {
var objs = arguments;
if (o.constructor == Array) {
objs = o;
}
var res = {};
for (var i = 0, obj; obj = objs[i]; i++) {
for (var p in obj) {
res[p] = obj[p];
}
}
return res;
}
return {};
}
},
/**
* Loads and store views.<br/>
* It contains useful methods to achieve performance and a simple XmlHttpRequest Object to decouple the framework from any external library.
* @namespace jsbeans.mvc
* @class view
* @static
*/
view: {
/**
* A local cache of loaded views. Turns to be optimal for performance reasons.
* @property cache
* @type Array
* @default []
* @static
* */
cache: [],
/**
* Stores value of the path where templates (views) are found
* @property templatePath
* @type String
* @default null
* @static
* */
templatePath: null,
/**
* Tells if a view is in an inline script tag.
* @property inline
* @type Boolean
* @default false
* @static
* */
inline: false,
/**
* Loads templates in two different ways:
* <ol>
* <li>remotely from server (via a GET)</li>
* <li>locally in page (via a document.getElementById)</li>
* </ol>
* and stores them locally.<br/>
* In the first case templates must be located as defined in <code>data-templatePath</code> custom attribute of view script tag,
* in the second one you <strong>must</strong> use the custom attribute <code>inline</code> set to <code>true</code>.<br/>
* You cannot use both in a mixed way. In other words choose your storage: remote XOR local. Local takes priority over remote if both defined.<br/>
* To prevent errors remote calls are enqueued in a FIFO. If a call is in progress waits 50ms before retrying.
* @method load
* @param view {String} the name of the view to be loaded. For remote views the file must have <code>html</code> extension.
* @param callback {Function} a function fired after template is loaded
* @static
* */
load: function(v, callback) {
if (jsbeans.mvc.view.cache[v] == null || typeof jsbeans.mvc.view.cache[v] == "undefined") {
if (jsbeans.mvc.view.inline) {
var tmpl = document.getElementById(v);
if (tmpl != null && typeof(tmpl) != "undefined") {
jsbeans.mvc.view.cache[v] = tmpl.innerHTML;
callback(jsbeans.mvc.view.cache[v]);
}
}
else {
var q = {view: v, callback: callback};
if (!jsbeans.mvc.view.queue.has(q)) {
jsbeans.mvc.view.queue.objects.push(q);
}
if (jsbeans.mvc.view.queue.processing) {
setTimeout(function() {
jsbeans.mvc.view.load(v, callback);
}, 50);
return;
}
jsbeans.mvc.view.queue.doProcess();
}
}
else {
callback(jsbeans.mvc.view.cache[v]);
}
},
/**
* A preloader for templates.<br/>
* May accept a single name or an <code>Array</code> of names.
* As {@method} <code class="methd">load</code> it uses queue for remote templates.
* @method fetch
* @param view {String | Array} the view, or views in case of Array, to load
* @static
* */
fetch: function(v) {
var views = v.constructor == Array ? v : [v];
if (jsbeans.mvc.view.inline) {
for (var i = 0, view; view = views[i]; i++) {
var tmpl = document.getElementById(view);
if (tmpl != null && typeof(tmpl) != "undefined") {
jsbeans.mvc.view.cache[view] = tmpl.innerHTML;
}
}
}
else {
var xhr = jsbeans.mvc.view.xhr();
for (var i = 0, view; view = views[i]; i++) {
// empty function as we just need to fecth templates, not to use them
var callback = function() {};
var q = {view: v, callback: callback};
if (!jsbeans.mvc.view.queue.has(q)) {
jsbeans.mvc.view.queue.objects.push(q);
}
if (jsbeans.mvc.view.queue.processing) {
setTimeout(function() {
jsbeans.mvc.view.load(view, callback);
}, 50);
return;
}
jsbeans.mvc.view.queue.doProcess();
}
}
},
/**
* XmlHttpRequest Object stored locally after first use.
* @static
* @private
* */
_xhr: null,
/**
* Create, then stores, and returns a XmlHttpRequest Object.
* @method xhr
* @return a simple implementation of XmlHttpRequest Object
* @static
* */
xhr: function() {
if (jsbeans.mvc.view._xhr == null) {
var res = null;
try {
res = new XMLHttpRequest();
}
catch (e) {
try {
res = new ActiveXObject("Msxml2.XMLHTTP");
}
catch (e) {
try {
res = new ActiveXObject("Microsoft.XMLHTTP");
}
catch (e) {
alert("Your browser does not support AJAX!");
return null;
}
}
}
jsbeans.mvc.view._xhr = res;
}
return jsbeans.mvc.view._xhr;
},
/**
* Queue repository and manager for remote templates.<br/>
* Plaese note: <strong>this class is not intended to be used directly</strong>.
* @namespace jsbeans.mvc.view
* @class queue
* @static
* */
queue: {
/**
* Tells if a request is in progress or not
* @property processing
* @type Boolean
* @default false
* @static
* */
processing: false,
/**
* JSON objects in the queue waiting to be procedeed or in progress
* @property objects
* @type Array
* @default []
* @static
* */
objects: [],
/**
* Real loader for templates.<br/>
* It is responsable to make AJAX requests and set <code class="param">jsbeans.mvc.queue.processing</code> accordingly to each call.<br/>
* Use it if you want to force queue to load next template.
* @method doProcess
* @static
* */
doProcess: function() {
if (jsbeans.mvc.view.queue.objects.length > 0) {
jsbeans.mvc.view.queue.processing = true;
var cur = jsbeans.mvc.view.queue.objects.shift();
var xhr = jsbeans.mvc.view.xhr();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
// setTimeout(function() {
jsbeans.mvc.view.cache[cur.view] = xhr.responseText;
cur.callback(jsbeans.mvc.view.cache[cur.view]);
jsbeans.mvc.view.queue.processing = false;
// }, 0);
}
};
xhr.open("GET", jsbeans.mvc.view.templatePath + "/" + cur.view + ".html", true);
xhr.send(null);
}
else {
jsbeans.mvc.view.queue.processing = false;
}
},
/**
* Checks if a template has been pushed in the queue.<br/>
* By now it simply checks using view name.
* @method has
* @return {boolean} Returns <code>true</code> if view is already in the list ready to be processed
* @static
* */
has: function(check) {
var c = check.view;
for (var i = 0, obj; obj = jsbeans.mvc.view.queue.objects[i]; i++) {
if (c == obj.view) {
return true;
}
}
return false;
}
}
}
};
(function() {
var scripts = document.getElementsByTagName("script");
for (var i = 0, scr; scr = scripts[i]; i++) {
var src = scr.getAttribute("src");
if (src != null && typeof(src) != "undefined" && src.indexOf("mvc.js") != -1) {
var path = scr.getAttribute("data-templatePath");
if (path != null && typeof path != "undefined") {
jsbeans.mvc.view.templatePath = path;
}
var inline = scr.getAttribute("inline");
if (inline != null && typeof inline != "undefined") {
jsbeans.mvc.view.inline = inline;
}
break;
}
}
// still null, fallback to empty
if (jsbeans.mvc.view.templatePath == null) {
jsbeans.mvc.view.templatePath = "";
}
})();