//jquery.view.js
(function( $ ) {
// converts to an ok dom id
var toId = function( src ) {
return src.replace(/^\/\//, "").replace(/[\/\.]/g, "_");
},
// used for hookup ids
id = 1;
/**
* @class jQuery.View
* @tag core
* @plugin jquery/view
* @test jquery/view/qunit.html
* @download dist/jquery.view.js
*
* View provides a uniform interface for using templates with
* jQuery. When template engines [jQuery.View.register register]
* themselves, you are able to:
*
* - Use views with jQuery extensions [jQuery.fn.after after], [jQuery.fn.append append],
* [jQuery.fn.before before], [jQuery.fn.html html], [jQuery.fn.prepend prepend],
* [jQuery.fn.replaceWith replaceWith], [jQuery.fn.text text].
* - Template loading from html elements and external files.
* - Synchronous and asynchronous template loading.
* - Deferred Rendering.
* - Template caching.
* - Bundling of processed templates in production builds.
* - Hookup jquery plugins directly in the template.
*
* ## Use
*
*
* When using views, you're almost always wanting to insert the results
* of a rendered template into the page. jQuery.View overwrites the
* jQuery modifiers so using a view is as easy as:
*
* $("#foo").html('mytemplate.ejs',{message: 'hello world'})
*
* This code:
*
* - Loads the template a 'mytemplate.ejs'. It might look like:
*
"
* $('.foo').html(html);
*
*
* @param {Function} cb a callback function to be called with the element
* @param {Number} the hookup number
*/
hookup: function( cb ) {
var myid = ++id;
$view.hookups[myid] = cb;
return myid;
},
/**
* @attribute cached
* @hide
* Cached are put in this object
*/
cached: {},
/**
* @attribute cache
* Should the views be cached or reloaded from the server. Defaults to true.
*/
cache: true,
/**
* @function register
* Registers a template engine to be used with
* view helpers and compression.
*
* ## Example
*
* @codestart
* $.View.register({
* suffix : "tmpl",
* renderer: function( id, text ) {
* return function(data){
* return jQuery.render( text, data );
* }
* },
* script: function( id, text ) {
* var tmpl = $.tmpl(text).toString();
* return "function(data){return ("+
* tmpl+
* ").call(jQuery, jQuery, data); }";
* }
* })
* @codeend
* Here's what each property does:
*
* * suffix - files that use this suffix will be processed by this template engine
* * renderer - returns a function that will render the template provided by text
* * script - returns a string form of the processed template function.
*
* @param {Object} info a object of method and properties
*
* that enable template integration:
*
* - suffix - the view extension. EX: 'ejs'
* - script(id, src) - a function that returns a string that when evaluated returns a function that can be
* used as the render (i.e. have func.call(data, data, helpers) called on it).
* - renderer(id, text) - a function that takes the id of the template and the text of the template and
* returns a render function.
*
*/
register: function( info ) {
this.types["." + info.suffix] = info;
},
types: {},
/**
* @attribute ext
* The default suffix to use if none is provided in the view's url.
* This is set to .ejs by default.
*/
ext: ".ejs",
/**
* Returns the text that
* @hide
* @param {Object} type
* @param {Object} id
* @param {Object} src
*/
registerScript: function( type, id, src ) {
return "$.View.preload('" + id + "'," + $view.types["." + type].script(id, src) + ");";
},
/**
* @hide
* Called by a production script to pre-load a renderer function
* into the view cache.
* @param {String} id
* @param {Function} renderer
*/
preload: function( id, renderer ) {
$view.cached[id] = function( data, helpers ) {
return renderer.call(data, data, helpers);
};
}
});
//---- ADD jQUERY HELPERS -----
//converts jquery functions to use views
var convert, modify, isTemplate, getCallback, hookupView, funcs;
convert = function( func_name ) {
var old = $.fn[func_name];
$.fn[func_name] = function() {
var args = $.makeArray(arguments),
callbackNum,
callback,
self = this,
result;
//check if a template
if ( isTemplate(args) ) {
// if we should operate async
if ((callbackNum = getCallback(args))) {
callback = args[callbackNum];
args[callbackNum] = function( result ) {
modify.call(self, [result], old);
callback.call(self, result);
};
$view.apply($view, args);
return this;
}
result = $view.apply($view, args);
if(!isDeferred( result ) ){
args = [result];
}else{
result.done(function(res){
modify.call(self, [res], old);
})
return this;
}
//otherwise do the template now
}
return modify.call(this, args, old);
};
};
// modifies the html of the element
modify = function( args, old ) {
var res, stub, hooks;
//check if there are new hookups
for ( var hasHookups in $view.hookups ) {
break;
}
//if there are hookups, get jQuery object
if ( hasHookups ) {
hooks = $view.hookups;
$view.hookups = {};
args[0] = $(args[0]);
}
res = old.apply(this, args);
//now hookup hookups
if ( hasHookups ) {
hookupView(args[0], hooks);
}
return res;
};
// returns true or false if the args indicate a template is being used
isTemplate = function( args ) {
var secArgType = typeof args[1];
return typeof args[0] == "string" && (secArgType == 'object' || secArgType == 'function') && !args[1].nodeType && !args[1].jquery;
};
//returns the callback if there is one (for async view use)
getCallback = function( args ) {
return typeof args[3] === 'function' ? 3 : typeof args[2] === 'function' && 2;
};
hookupView = function( els , hooks) {
//remove all hookups
var hookupEls,
len, i = 0,
id, func;
els = els.filter(function(){
return this.nodeType != 3; //filter out text nodes
})
hookupEls = els.add("[data-view-id]", els);
len = hookupEls.length;
for (; i < len; i++ ) {
if ( hookupEls[i].getAttribute && (id = hookupEls[i].getAttribute('data-view-id')) && (func = hooks[id]) ) {
func(hookupEls[i], id);
delete hooks[id];
hookupEls[i].removeAttribute('data-view-id');
}
}
//copy remaining hooks back
$.extend($view.hookups, hooks);
};
/**
* @add jQuery.fn
*/
funcs = [
/**
* @function prepend
* @parent jQuery.View
* abc
*/
"prepend",
/**
* @function append
* @parent jQuery.View
* abc
*/
"append",
/**
* @function after
* @parent jQuery.View
* abc
*/
"after",
/**
* @function before
* @parent jQuery.View
* abc
*/
"before",
/**
* @function text
* @parent jQuery.View
* abc
*/
"text",
/**
* @function html
* @parent jQuery.View
* abc
*/
"html",
/**
* @function replaceWith
* @parent jQuery.View
* abc
*/
"replaceWith",
"val"];
//go through helper funcs and convert
for ( var i = 0; i < funcs.length; i++ ) {
convert(funcs[i]);
}
})(jQuery);
//jquery.lang.js
(function( $ ) {
// Several of the methods in this plugin use code adapated from Prototype
// Prototype JavaScript framework, version 1.6.0.1
// (c) 2005-2007 Sam Stephenson
var regs = {
undHash: /_|-/,
colons: /::/,
words: /([A-Z]+)([A-Z][a-z])/g,
lowUp: /([a-z\d])([A-Z])/g,
dash: /([a-z\d])([A-Z])/g,
replacer: /\{([^\}]+)\}/g,
dot: /\./
},
getNext = function(current, nextPart, add){
return current[nextPart] || ( add && (current[nextPart] = {}) );
},
isContainer = function(current){
var type = typeof current;
return type && ( type == 'function' || type == 'object' );
},
getObject = function( objectName, roots, add ) {
var parts = objectName ? objectName.split(regs.dot) : [],
length = parts.length,
currents = $.isArray(roots) ? roots : [roots || window],
current,
ret,
i,
c = 0,
type;
if(length == 0){
return currents[0];
}
while(current = currents[c++]){
for (i =0; i < length - 1 && isContainer(current); i++ ) {
current = getNext(current, parts[i], add);
}
if( isContainer(current) ) {
ret = getNext(current, parts[i], add);
if( ret !== undefined ) {
if ( add === false ) {
delete current[parts[i]];
}
return ret;
}
}
}
},
/**
* @class jQuery.String
*
* A collection of useful string helpers.
*
*/
str = $.String = $.extend( $.String || {} , {
/**
* @function
* Gets an object from a string.
* @param {String} name the name of the object to look for
* @param {Array} [roots] an array of root objects to look for the name
* @param {Boolean} [add] true to add missing objects to
* the path. false to remove found properties. undefined to
* not modify the root object
*/
getObject : getObject,
/**
* Capitalizes a string
* @param {String} s the string.
* @return {String} a string with the first character capitalized.
*/
capitalize: function( s, cache ) {
return s.charAt(0).toUpperCase() + s.substr(1);
},
/**
* Capitalizes a string from something undercored. Examples:
* @codestart
* jQuery.String.camelize("one_two") //-> "oneTwo"
* "three-four".camelize() //-> threeFour
* @codeend
* @param {String} s
* @return {String} a the camelized string
*/
camelize: function( s ) {
s = str.classize(s);
return s.charAt(0).toLowerCase() + s.substr(1);
},
/**
* Like camelize, but the first part is also capitalized
* @param {String} s
* @return {String} the classized string
*/
classize: function( s , join) {
var parts = s.split(regs.undHash),
i = 0;
for (; i < parts.length; i++ ) {
parts[i] = str.capitalize(parts[i]);
}
return parts.join(join || '');
},
/**
* Like [jQuery.String.classize|classize], but a space separates each 'word'
* @codestart
* jQuery.String.niceName("one_two") //-> "One Two"
* @codeend
* @param {String} s
* @return {String} the niceName
*/
niceName: function( s ) {
str.classize(parts[i],' ');
},
/**
* Underscores a string.
* @codestart
* jQuery.String.underscore("OneTwo") //-> "one_two"
* @codeend
* @param {String} s
* @return {String} the underscored string
*/
underscore: function( s ) {
return s.replace(regs.colons, '/').replace(regs.words, '$1_$2').replace(regs.lowUp, '$1_$2').replace(regs.dash, '_').toLowerCase();
},
/**
* Returns a string with {param} replaced values from data.
*
* $.String.sub("foo {bar}",{bar: "far"})
* //-> "foo far"
*
* @param {String} s The string to replace
* @param {Object} data The data to be used to look for properties. If it's an array, multiple
* objects can be used.
* @param {Boolean} [remove] if a match is found, remove the property from the object
*/
sub: function( s, data, remove ) {
var obs = [];
obs.push(s.replace(regs.replacer, function( whole, inside ) {
//convert inside to type
var ob = getObject(inside, data, typeof remove == 'boolean' ? !remove : remove),
type = typeof ob;
if((type === 'object' || type === 'function') && type !== null){
obs.push(ob);
return "";
}else{
return ""+ob;
}
}));
return obs.length <= 1 ? obs[0] : obs;
}
});
})(jQuery);
//jquery.lang.rsplit.js
(function( $ ) {
/**
* @add jQuery.String
*/
$.String.
/**
* Splits a string with a regex correctly cross browser
*
* $.String.rsplit("a.b.c.d", /\./) //-> ['a','b','c','d']
*
* @param {String} string The string to split
* @param {RegExp} regex A regular expression
* @return {Array} An array of strings
*/
rsplit = function( string, regex ) {
var result = regex.exec(string),
retArr = [],
first_idx, last_idx;
while ( result !== null ) {
first_idx = result.index;
last_idx = regex.lastIndex;
if ( first_idx !== 0 ) {
retArr.push(string.substring(0, first_idx));
string = string.slice(first_idx);
}
retArr.push(result[0]);
string = string.slice(result[0].length);
result = regex.exec(string);
}
if ( string !== '' ) {
retArr.push(string);
}
return retArr;
};
})(jQuery);
//jquery.view.ejs.js
(function( $ ) {
var myEval = function(script){
eval(script);
},
chop = function( string ) {
return string.substr(0, string.length - 1);
},
rSplit = $.String.rsplit,
extend = $.extend,
isArray = $.isArray,
clean = function( content ) {
return content.replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/"/g, '\\"');
}
// from prototype http://www.prototypejs.org/
escapeHTML = function(content){
return content.replace(/&/g,'&')
.replace(//g,'>')
.replace(/"/g, '"')
.replace(/'/g, "'");
},
EJS = function( options ) {
//returns a renderer function
if ( this.constructor != EJS ) {
var ejs = new EJS(options);
return function( data, helpers ) {
return ejs.render(data, helpers);
};
}
//so we can set the processor
if ( typeof options == "function" ) {
this.template = {};
this.template.process = options;
return;
}
//set options on self
extend(this, EJS.options, options);
this.template = compile(this.text, this.type, this.name);
};
/**
* @class jQuery.EJS
*
* @plugin jquery/view/ejs
* @parent jQuery.View
* @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/view/ejs/ejs.js
* @test jquery/view/ejs/qunit.html
*
*
* Ejs provides
ERB
* style client side templates. Use them with controllers to easily build html and inject
* it into the DOM.
*
* ### Example
*
* The following generates a list of tasks:
*
* @codestart html
* <ul>
* <% for(var i = 0; i < tasks.length; i++){ %>
* <li class="task <%= tasks[i].identity %>"><%= tasks[i].name %></li>
* <% } %>
* </ul>
* @codeend
*
* For the following examples, we assume this view is in
'views\tasks\list.ejs'.
*
*
* ## Use
*
* ### Loading and Rendering EJS:
*
* You should use EJS through the helper functions [jQuery.View] provides such as:
*
* - [jQuery.fn.after after]
* - [jQuery.fn.append append]
* - [jQuery.fn.before before]
* - [jQuery.fn.html html],
* - [jQuery.fn.prepend prepend],
* - [jQuery.fn.replaceWith replaceWith], and
* - [jQuery.fn.text text].
*
* or [jQuery.Controller.prototype.view].
*
* ### Syntax
*
* EJS uses 5 types of tags:
*
* -
<% CODE %>
- Runs JS Code.
* For example:
*
* <% alert('hello world') %>
*
* -
<%= CODE %>
- Runs JS Code and writes the result into the result of the template.
* For example:
*
*
<%= 'hello world' %>
*
* -
<%~ CODE %>
- Runs JS Code and writes the _escaped_ result into the result of the template.
* For example:
*
* <%~ 'hello world' %>
*
* -
<%%= CODE %>
- Writes <%= CODE %> to the result of the template. This is very useful for generators.
*
* <%%= 'hello world' %>
*
* -
<%# CODE %>
- Used for comments. This does nothing.
*
* <%# 'hello world' %>
*
* ## Hooking up controllers
*
* After drawing some html, you often want to add other widgets and plugins inside that html.
* View makes this easy. You just have to return the Contoller class you want to be hooked up.
*
* @codestart
* <ul <%= Mxui.Tabs%>>...<ul>
* @codeend
*
* You can even hook up multiple controllers:
*
* @codestart
* <ul <%= [Mxui.Tabs, Mxui.Filler]%>>...<ul>
* @codeend
*
*
View Helpers
* View Helpers return html code. View by default only comes with
* [jQuery.EJS.Helpers.prototype.view view] and [jQuery.EJS.Helpers.prototype.text text].
* You can include more with the view/helpers plugin. But, you can easily make your own!
* Learn how in the [jQuery.EJS.Helpers Helpers] page.
*
* @constructor Creates a new view
* @param {Object} options A hash with the following options
*
* Option | Default | Description |
*
* url |
* |
* loads the template from a file. This path should be relative to [jQuery.root].
* |
*
*
* text |
* |
* uses the provided text as the template. Example:
new View({text: '<%=user%>'})
* |
*
*
* element |
* |
* loads a template from the innerHTML or value of the element.
* |
*
*
* type |
* '<' |
* type of magic tags. Options are '<' or '['
* |
*
*
* name |
* the element ID or url |
* an optional name that is used for caching.
* |
*
*
* cache |
* true in production mode, false in other modes |
* true to cache template.
* |
*
*
*
*/
$.EJS = EJS;
/**
* @Prototype
*/
EJS.prototype = {
constructor: EJS,
/**
* Renders an object with extra view helpers attached to the view.
* @param {Object} object data to be rendered
* @param {Object} extra_helpers an object with additonal view helpers
* @return {String} returns the result of the string
*/
render: function( object, extraHelpers ) {
object = object || {};
this._extra_helpers = extraHelpers;
var v = new EJS.Helpers(object, extraHelpers || {});
return this.template.process.call(object, object, v);
}
};
/* @Static */
EJS.
/**
* Used to convert what's in <%= %> magic tags to a string
* to be inserted in the rendered output.
*
* Typically, it's a string, and the string is just inserted. However,
* if it's a function or an object with a hookup method, it can potentially be
* be ran on the element after it's inserted into the page.
*
* This is a very nice way of adding functionality through the view.
* Usually this is done with [jQuery.EJS.Helpers.prototype.plugin]
* but the following fades in the div element after it has been inserted:
*
* @codestart
* <%= function(el){$(el).fadeIn()} %>
* @codeend
*
* @param {String|Object|Function} input the value in between the
* write majic tags: <%= %>
* @return {String} returns the content to be added to the rendered
* output. The content is different depending on the type:
*
* * string - a bac
* * foo - bar
*/
text = function( input ) {
if ( typeof input == 'string' ) {
return input;
}
if ( input === null || input === undefined ) {
return '';
}
var hook =
(input.hookup && function( el, id ) {
input.hookup.call(input, el, id);
})
||
(typeof input == 'function' && input)
||
(isArray(input) && function( el, id ) {
for ( var i = 0; i < input.length; i++ ) {
var stub;
stub = input[i].hookup ? input[i].hookup(el, id) : input[i](el, id);
}
});
if(hook){
return "data-view-id='" + $.View.hookup(hook) + "'";
}
return input.toString ? input.toString() : "";
};
EJS.clean = function(text){
//return sanatized text
if(typeof text == 'string'){
return escapeHTML(text)
}else{
return "";
}
}
//returns something you can call scan on
var scan = function(scanner, source, block ) {
var source_split = rSplit(source, /\n/),
i=0;
for (; i < source_split.length; i++ ) {
scanline(scanner, source_split[i], block);
}
},
scanline= function(scanner, line, block ) {
scanner.lines++;
var line_split = rSplit(line, scanner.splitter),
token;
for ( var i = 0; i < line_split.length; i++ ) {
token = line_split[i];
if ( token !== null ) {
block(token, scanner);
}
}
},
makeScanner = function(left, right){
var scanner = {};
extend(scanner, {
left: left + '%',
right: '%' + right,
dLeft: left + '%%',
dRight: '%%' + right,
eeLeft : left + '%==',
eLeft: left + '%=',
cmnt: left + '%#',
cleanLeft: left+"%~",
scan : scan,
lines : 0
});
scanner.splitter = new RegExp("(" + [scanner.dLeft, scanner.dRight, scanner.eeLeft, scanner.eLeft, scanner.cleanLeft,
scanner.cmnt, scanner.left, scanner.right + '\n', scanner.right, '\n'].join(")|(").
replace(/\[/g,"\\[").replace(/\]/g,"\\]") + ")");
return scanner;
},
// compiles a template
compile = function( source, left, name ) {
source = source.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
//normalize line endings
left = left || '<';
var put_cmd = "___v1ew.push(",
insert_cmd = put_cmd,
buff = new EJS.Buffer(['var ___v1ew = [];'], []),
content = '',
put = function( content ) {
buff.push(put_cmd, '"', clean(content), '");');
},
startTag = null,
empty = function(){
content = ''
};
scan( makeScanner(left, left === '[' ? ']' : '>') ,
source||"",
function( token, scanner ) {
// if we don't have a start pair
if ( startTag === null ) {
switch ( token ) {
case '\n':
content = content + "\n";
put(content);
buff.cr();
empty();
break;
case scanner.left:
case scanner.eLeft:
case scanner.eeLeft:
case scanner.cleanLeft:
case scanner.cmnt:
startTag = token;
if ( content.length > 0 ) {
put(content);
}
empty();
break;
// replace <%% with <%
case scanner.dLeft:
content += scanner.left;
break;
default:
content += token;
break;
}
}
else {
switch ( token ) {
case scanner.right:
switch ( startTag ) {
case scanner.left:
if ( content[content.length - 1] == '\n' ) {
content = chop(content);
buff.push(content, ";");
buff.cr();
}
else {
buff.push(content, ";");
}
break;
case scanner.cleanLeft :
buff.push(insert_cmd, "(jQuery.EJS.clean(", content, ")));");
break;
case scanner.eLeft:
buff.push(insert_cmd, "(jQuery.EJS.text(", content, ")));");
break;
case scanner.eeLeft:
buff.push(insert_cmd, "(jQuery.EJS.text(", content, ")));");
break;
}
startTag = null;
empty();
break;
case scanner.dRight:
content += scanner.right;
break;
default:
content += token;
break;
}
}
})
if ( content.length > 0 ) {
// Should be content.dump in Ruby
buff.push(put_cmd, '"', clean(content) + '");');
}
var template = buff.close(),
out = {
out : 'try { with(_VIEW) { with (_CONTEXT) {' + template + " return ___v1ew.join('');}}}catch(e){e.lineNumber=null;throw e;}"
};
//use eval instead of creating a function, b/c it is easier to debug
myEval.call(out,'this.process = (function(_CONTEXT,_VIEW){' + out.out + '});\r\n//@ sourceURL='+name+".js");
return out;
};
// a line and script buffer
// we use this so we know line numbers when there
// is an error.
// pre and post are setup and teardown for the buffer
EJS.Buffer = function( pre_cmd, post ) {
this.line = [];
this.script = [];
this.post = post;
// add the pre commands to the first line
this.push.apply(this, pre_cmd);
};
EJS.Buffer.prototype = {
//need to maintain your own semi-colons (for performance)
push: function() {
this.line.push.apply(this.line, arguments);
},
cr: function() {
this.script.push(this.line.join(''), "\n");
this.line = [];
},
//returns the script too
close: function() {
var stub;
if ( this.line.length > 0 ) {
this.script.push(this.line.join(''));
this.line = [];
}
stub = this.post.length && this.push.apply(this, this.post);
this.script.push(";"); //makes sure we always have an ending /
return this.script.join("");
}
};
//type, cache, folder
/**
* @attribute options
* Sets default options for all views
*
* Option | Default | Description |
*
* type |
* '<' |
* type of magic tags. Options are '<' or '['
* |
*
*
* cache |
* true in production mode, false in other modes |
* true to cache template.
* |
*
*
*
*/
EJS.options = {
type: '<',
ext: '.ejs'
};
/**
* @class jQuery.EJS.Helpers
* @parent jQuery.EJS
* By adding functions to jQuery.EJS.Helpers.prototype, those functions will be available in the
* views.
* @constructor Creates a view helper. This function is called internally. You should never call it.
* @param {Object} data The data passed to the view. Helpers have access to it through this._data
*/
EJS.Helpers = function( data, extras ) {
this._data = data;
this._extras = extras;
extend(this, extras);
};
/* @prototype*/
EJS.Helpers.prototype = {
/**
* Hooks up a jQuery plugin on.
* @param {String} name the plugin name
*/
plugin: function( name ) {
var args = $.makeArray(arguments),
widget = args.shift();
return function( el ) {
var jq = $(el);
jq[widget].apply(jq, args);
};
},
/**
* Renders a partial view. This is deprecated in favor of
$.View()
.
*/
view: function( url, data, helpers ) {
helpers = helpers || this._extras;
data = data || this._data;
return $.View(url, data, helpers); //new EJS(options).render(data, helpers);
}
};
$.View.register({
suffix: "ejs",
//returns a function that renders the view
script: function( id, src ) {
return "jQuery.EJS(function(_CONTEXT,_VIEW) { " + new EJS({
text: src
}).template.out + " })";
},
renderer: function( id, text ) {
var ejs = new EJS({
text: text,
name: id
});
return function( data, helpers ) {
return ejs.render.call(ejs, data, helpers);
};
}
});
})(jQuery);