1777 lines
55 KiB
JavaScript
1777 lines
55 KiB
JavaScript
(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: /\./
|
|
},
|
|
// gets the nextPart property from current
|
|
// add - if true and nextPart doesnt exist, create it as an empty object
|
|
getNext = function(current, nextPart, add){
|
|
return current[nextPart] !== undefined ? current[nextPart] : ( add && (current[nextPart] = {}) );
|
|
},
|
|
// returns true if the object can have properties (no nulls)
|
|
isContainer = function(current){
|
|
var type = typeof current;
|
|
return current && ( type == 'function' || type == 'object' );
|
|
},
|
|
// a reference
|
|
getObject,
|
|
/**
|
|
* @class jQuery.String
|
|
* @parent jquerymx.lang
|
|
*
|
|
* A collection of useful string helpers. Available helpers are:
|
|
* <ul>
|
|
* <li>[jQuery.String.capitalize|capitalize]: Capitalizes a string (some_string » Some_string)</li>
|
|
* <li>[jQuery.String.camelize|camelize]: Capitalizes a string from something undercored
|
|
* (some_string » someString, some-string » someString)</li>
|
|
* <li>[jQuery.String.classize|classize]: Like [jQuery.String.camelize|camelize],
|
|
* but the first part is also capitalized (some_string » SomeString)</li>
|
|
* <li>[jQuery.String.niceName|niceName]: Like [jQuery.String.classize|classize], but a space separates each 'word' (some_string » Some String)</li>
|
|
* <li>[jQuery.String.underscore|underscore]: Underscores a string (SomeString » some_string)</li>
|
|
* <li>[jQuery.String.sub|sub]: Returns a string with {param} replaced values from data.
|
|
* <code><pre>
|
|
* $.String.sub("foo {bar}",{bar: "far"})
|
|
* //-> "foo far"</pre></code>
|
|
* </li>
|
|
* </ul>
|
|
*
|
|
*/
|
|
str = $.String = $.extend( $.String || {} , {
|
|
|
|
|
|
/**
|
|
* @function getObject
|
|
* Gets an object from a string. It can also modify objects on the
|
|
* 'object path' by removing or adding properties.
|
|
*
|
|
* Foo = {Bar: {Zar: {"Ted"}}}
|
|
* $.String.getObject("Foo.Bar.Zar") //-> "Ted"
|
|
*
|
|
* @param {String} name the name of the object to look for
|
|
* @param {Array} [roots] an array of root objects to look for the
|
|
* name. If roots is not provided, the window is used.
|
|
* @param {Boolean} [add] true to add missing objects to
|
|
* the path. false to remove found properties. undefined to
|
|
* not modify the root object
|
|
* @return {Object} The object.
|
|
*/
|
|
getObject : getObject = function( name, roots, add ) {
|
|
|
|
// the parts of the name we are looking up
|
|
// ['App','Models','Recipe']
|
|
var parts = name ? name.split(regs.dot) : [],
|
|
length = parts.length,
|
|
current,
|
|
ret,
|
|
i,
|
|
r = 0,
|
|
type;
|
|
|
|
// make sure roots is an array
|
|
roots = $.isArray(roots) ? roots : [roots || window];
|
|
|
|
if(length == 0){
|
|
return roots[0];
|
|
}
|
|
// for each root, mark it as current
|
|
while( current = roots[r++] ) {
|
|
// walk current to the 2nd to last object
|
|
// or until there is not a container
|
|
for (i =0; i < length - 1 && isContainer(current); i++ ) {
|
|
current = getNext(current, parts[i], add);
|
|
}
|
|
// if we can get a property from the 2nd to last object
|
|
if( isContainer(current) ) {
|
|
|
|
// get (and possibly set) the property
|
|
ret = getNext(current, parts[i], add);
|
|
|
|
// if there is a value, we exit
|
|
if( ret !== undefined ) {
|
|
// if add is false, delete the property
|
|
if ( add === false ) {
|
|
delete current[parts[i]];
|
|
}
|
|
return ret;
|
|
|
|
}
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* 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 [jQuery.String.camelize|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 ) {
|
|
return str.classize(s,' ');
|
|
},
|
|
|
|
/**
|
|
* 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 = [],
|
|
remove = typeof remove == 'boolean' ? !remove : remove;
|
|
obs.push(s.replace(regs.replacer, function( whole, inside ) {
|
|
//convert inside to type
|
|
var ob = getObject(inside, data, remove);
|
|
|
|
// if a container, push into objs (which will return objects found)
|
|
if( isContainer(ob) ){
|
|
obs.push(ob);
|
|
return "";
|
|
}else{
|
|
return ""+ob;
|
|
}
|
|
}));
|
|
|
|
return obs.length <= 1 ? obs[0] : obs;
|
|
},
|
|
_regs : regs
|
|
});
|
|
})(jQuery);
|
|
(function( $ ) {
|
|
|
|
// a path like string into something that's ok for an element ID
|
|
var toId = function( src ) {
|
|
return src.replace(/^\/\//, "").replace(/[\/\.]/g, "_");
|
|
},
|
|
makeArray = $.makeArray,
|
|
// used for hookup ids
|
|
id = 1;
|
|
// this might be useful for testing if html
|
|
// htmlTest = /^[\s\n\r\xA0]*<(.|[\r\n])*>[\s\n\r\xA0]*$/
|
|
/**
|
|
* @class jQuery.View
|
|
* @parent jquerymx
|
|
* @plugin jquery/view
|
|
* @test jquery/view/qunit.html
|
|
* @download dist/jquery.view.js
|
|
*
|
|
* @description A JavaScript template framework.
|
|
*
|
|
* 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.
|
|
* - [view.deferreds Deferred Rendering].
|
|
* - Template caching.
|
|
* - Bundling of processed templates in production builds.
|
|
* - Hookup jquery plugins directly in the template.
|
|
*
|
|
* The [mvc.view Get Started with jQueryMX] has a good walkthrough of $.View.
|
|
*
|
|
* ## 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:
|
|
* <pre><code><h2><%= message %></h2></pre></code>
|
|
*
|
|
* - Renders it with {message: 'hello world'}, resulting in:
|
|
* <pre><code><div id='foo'>"<h2>hello world</h2></div></pre></code>
|
|
*
|
|
* - Inserts the result into the foo element. Foo might look like:
|
|
* <pre><code><div id='foo'><h2>hello world</h2></div></pre></code>
|
|
*
|
|
* ## jQuery Modifiers
|
|
*
|
|
* You can use a template with the following jQuery modifiers:
|
|
*
|
|
* <table>
|
|
* <tr><td>[jQuery.fn.after after]</td><td> <code>$('#bar').after('temp.jaml',{});</code></td></tr>
|
|
* <tr><td>[jQuery.fn.append append] </td><td> <code>$('#bar').append('temp.jaml',{});</code></td></tr>
|
|
* <tr><td>[jQuery.fn.before before] </td><td> <code>$('#bar').before('temp.jaml',{});</code></td></tr>
|
|
* <tr><td>[jQuery.fn.html html] </td><td> <code>$('#bar').html('temp.jaml',{});</code></td></tr>
|
|
* <tr><td>[jQuery.fn.prepend prepend] </td><td> <code>$('#bar').prepend('temp.jaml',{});</code></td></tr>
|
|
* <tr><td>[jQuery.fn.replaceWith replaceWith] </td><td> <code>$('#bar').replaceWith('temp.jaml',{});</code></td></tr>
|
|
* <tr><td>[jQuery.fn.text text] </td><td> <code>$('#bar').text('temp.jaml',{});</code></td></tr>
|
|
* </table>
|
|
*
|
|
* You always have to pass a string and an object (or function) for the jQuery modifier
|
|
* to user a template.
|
|
*
|
|
* ## Template Locations
|
|
*
|
|
* View can load from script tags or from files.
|
|
*
|
|
* ## From Script Tags
|
|
*
|
|
* To load from a script tag, create a script tag with your template and an id like:
|
|
*
|
|
* <pre><code><script type='text/ejs' id='recipes'>
|
|
* <% for(var i=0; i < recipes.length; i++){ %>
|
|
* <li><%=recipes[i].name %></li>
|
|
* <%} %>
|
|
* </script></code></pre>
|
|
*
|
|
* Render with this template like:
|
|
*
|
|
* @codestart
|
|
* $("#foo").html('recipes',recipeData)
|
|
* @codeend
|
|
*
|
|
* Notice we passed the id of the element we want to render.
|
|
*
|
|
* ## From File
|
|
*
|
|
* You can pass the path of a template file location like:
|
|
*
|
|
* $("#foo").html('templates/recipes.ejs',recipeData)
|
|
*
|
|
* However, you typically want to make the template work from whatever page they
|
|
* are called from. To do this, use // to look up templates from JMVC root:
|
|
*
|
|
* $("#foo").html('//app/views/recipes.ejs',recipeData)
|
|
*
|
|
* Finally, the [jQuery.Controller.prototype.view controller/view] plugin can make looking
|
|
* up a thread (and adding helpers) even easier:
|
|
*
|
|
* $("#foo").html( this.view('recipes', recipeData) )
|
|
*
|
|
* ## Packaging Templates
|
|
*
|
|
* If you're making heavy use of templates, you want to organize
|
|
* them in files so they can be reused between pages and applications.
|
|
*
|
|
* But, this organization would come at a high price
|
|
* if the browser has to
|
|
* retrieve each template individually. The additional
|
|
* HTTP requests would slow down your app.
|
|
*
|
|
* Fortunately, [steal.static.views steal.views] can build templates
|
|
* into your production files. You just have to point to the view file like:
|
|
*
|
|
* steal.views('path/to/the/view.ejs');
|
|
*
|
|
* ## Asynchronous
|
|
*
|
|
* By default, retrieving requests is done synchronously. This is
|
|
* fine because StealJS packages view templates with your JS download.
|
|
*
|
|
* However, some people might not be using StealJS or want to delay loading
|
|
* templates until necessary. If you have the need, you can
|
|
* provide a callback paramter like:
|
|
*
|
|
* $("#foo").html('recipes',recipeData, function(result){
|
|
* this.fadeIn()
|
|
* });
|
|
*
|
|
* The callback function will be called with the result of the
|
|
* rendered template and 'this' will be set to the original jQuery object.
|
|
*
|
|
* ## Deferreds (3.0.6)
|
|
*
|
|
* If you pass deferreds to $.View or any of the jQuery
|
|
* modifiers, the view will wait until all deferreds resolve before
|
|
* rendering the view. This makes it a one-liner to make a request and
|
|
* use the result to render a template.
|
|
*
|
|
* The following makes a request for todos in parallel with the
|
|
* todos.ejs template. Once todos and template have been loaded, it with
|
|
* render the view with the todos.
|
|
*
|
|
* $('#todos').html("todos.ejs",Todo.findAll());
|
|
*
|
|
* ## Just Render Templates
|
|
*
|
|
* Sometimes, you just want to get the result of a rendered
|
|
* template without inserting it, you can do this with $.View:
|
|
*
|
|
* var out = $.View('path/to/template.jaml',{});
|
|
*
|
|
* ## Preloading Templates
|
|
*
|
|
* You can preload templates asynchronously like:
|
|
*
|
|
* $.get('path/to/template.jaml',{},function(){},'view');
|
|
*
|
|
* ## Supported Template Engines
|
|
*
|
|
* JavaScriptMVC comes with the following template languages:
|
|
*
|
|
* - EmbeddedJS
|
|
* <pre><code><h2><%= message %></h2></code></pre>
|
|
*
|
|
* - JAML
|
|
* <pre><code>h2(data.message);</code></pre>
|
|
*
|
|
* - Micro
|
|
* <pre><code><h2>{%= message %}</h2></code></pre>
|
|
*
|
|
* - jQuery.Tmpl
|
|
* <pre><code><h2>${message}</h2></code></pre>
|
|
|
|
*
|
|
* The popular <a href='http://awardwinningfjords.com/2010/08/09/mustache-for-javascriptmvc-3.html'>Mustache</a>
|
|
* template engine is supported in a 2nd party plugin.
|
|
*
|
|
* ## Using other Template Engines
|
|
*
|
|
* It's easy to integrate your favorite template into $.View and Steal. Read
|
|
* how in [jQuery.View.register].
|
|
*
|
|
* @constructor
|
|
*
|
|
* Looks up a template, processes it, caches it, then renders the template
|
|
* with data and optional helpers.
|
|
*
|
|
* With [stealjs StealJS], views are typically bundled in the production build.
|
|
* This makes it ok to use views synchronously like:
|
|
*
|
|
* @codestart
|
|
* $.View("//myplugin/views/init.ejs",{message: "Hello World"})
|
|
* @codeend
|
|
*
|
|
* If you aren't using StealJS, it's best to use views asynchronously like:
|
|
*
|
|
* @codestart
|
|
* $.View("//myplugin/views/init.ejs",
|
|
* {message: "Hello World"}, function(result){
|
|
* // do something with result
|
|
* })
|
|
* @codeend
|
|
*
|
|
* @param {String} view The url or id of an element to use as the template's source.
|
|
* @param {Object} data The data to be passed to the view.
|
|
* @param {Object} [helpers] Optional helper functions the view might use. Not all
|
|
* templates support helpers.
|
|
* @param {Object} [callback] Optional callback function. If present, the template is
|
|
* retrieved asynchronously. This is a good idea if you aren't compressing the templates
|
|
* into your view.
|
|
* @return {String} The rendered result of the view or if deferreds
|
|
* are passed, a deferred that will resolve to
|
|
* the rendered result of the view.
|
|
*/
|
|
var $view = $.View = function( view, data, helpers, callback ) {
|
|
// if helpers is a function, it is actually a callback
|
|
if ( typeof helpers === 'function' ) {
|
|
callback = helpers;
|
|
helpers = undefined;
|
|
}
|
|
|
|
// see if we got passed any deferreds
|
|
var deferreds = getDeferreds(data);
|
|
|
|
|
|
if ( deferreds.length ) { // does data contain any deferreds?
|
|
// the deferred that resolves into the rendered content ...
|
|
var deferred = $.Deferred();
|
|
|
|
// add the view request to the list of deferreds
|
|
deferreds.push(get(view, true))
|
|
|
|
// wait for the view and all deferreds to finish
|
|
$.when.apply($, deferreds).then(function( resolved ) {
|
|
// get all the resolved deferreds
|
|
var objs = makeArray(arguments),
|
|
// renderer is last [0] is the data
|
|
renderer = objs.pop()[0],
|
|
// the result of the template rendering with data
|
|
result;
|
|
|
|
// make data look like the resolved deferreds
|
|
if ( isDeferred(data) ) {
|
|
data = usefulPart(resolved);
|
|
}
|
|
else {
|
|
// go through each prop in data again,
|
|
// replace the defferreds with what they resolved to
|
|
for ( var prop in data ) {
|
|
if ( isDeferred(data[prop]) ) {
|
|
data[prop] = usefulPart(objs.shift());
|
|
}
|
|
}
|
|
}
|
|
// get the rendered result
|
|
result = renderer(data, helpers);
|
|
|
|
//resolve with the rendered view
|
|
deferred.resolve(result);
|
|
// if there's a callback, call it back with the result
|
|
callback && callback(result);
|
|
});
|
|
// return the deferred ....
|
|
return deferred.promise();
|
|
}
|
|
else {
|
|
// no deferreds, render this bad boy
|
|
var response,
|
|
// if there's a callback function
|
|
async = typeof callback === "function",
|
|
// get the 'view' type
|
|
deferred = get(view, async);
|
|
|
|
// if we are async,
|
|
if ( async ) {
|
|
// return the deferred
|
|
response = deferred;
|
|
// and callback callback with the rendered result
|
|
deferred.done(function( renderer ) {
|
|
callback(renderer(data, helpers))
|
|
})
|
|
} else {
|
|
// otherwise, the deferred is complete, so
|
|
// set response to the result of the rendering
|
|
deferred.done(function( renderer ) {
|
|
response = renderer(data, helpers);
|
|
});
|
|
}
|
|
|
|
return response;
|
|
}
|
|
},
|
|
// makes sure there's a template, if not, has steal provide a warning
|
|
checkText = function( text, url ) {
|
|
if (!text.match(/[^\s]/) ) {
|
|
|
|
throw "$.View ERROR: There is no template or an empty template at " + url;
|
|
}
|
|
},
|
|
// returns a 'view' renderer deferred
|
|
// url - the url to the view template
|
|
// async - if the ajax request should be synchronous
|
|
get = function( url, async ) {
|
|
return $.ajax({
|
|
url: url,
|
|
dataType: "view",
|
|
async: async
|
|
});
|
|
},
|
|
// returns true if something looks like a deferred
|
|
isDeferred = function( obj ) {
|
|
return obj && $.isFunction(obj.always) // check if obj is a $.Deferred
|
|
},
|
|
// gets an array of deferreds from an object
|
|
// this only goes one level deep
|
|
getDeferreds = function( data ) {
|
|
var deferreds = [];
|
|
|
|
// pull out deferreds
|
|
if ( isDeferred(data) ) {
|
|
return [data]
|
|
} else {
|
|
for ( var prop in data ) {
|
|
if ( isDeferred(data[prop]) ) {
|
|
deferreds.push(data[prop]);
|
|
}
|
|
}
|
|
}
|
|
return deferreds;
|
|
},
|
|
// gets the useful part of deferred
|
|
// this is for Models and $.ajax that resolve to array (with success and such)
|
|
// returns the useful, content part
|
|
usefulPart = function( resolved ) {
|
|
return $.isArray(resolved) && resolved.length === 3 && resolved[1] === 'success' ? resolved[0] : resolved
|
|
};
|
|
|
|
|
|
|
|
// you can request a view renderer (a function you pass data to and get html)
|
|
// Creates a 'view' transport. These resolve to a 'view' renderer
|
|
// a 'view' renderer takes data and returns a string result.
|
|
// For example:
|
|
//
|
|
// $.ajax({dataType : 'view', src: 'foo.ejs'}).then(function(renderer){
|
|
// renderer({message: 'hello world'})
|
|
// })
|
|
$.ajaxTransport("view", function( options, orig ) {
|
|
// the url (or possibly id) of the view content
|
|
var url = orig.url,
|
|
// check if a suffix exists (ex: "foo.ejs")
|
|
suffix = url.match(/\.[\w\d]+$/),
|
|
type,
|
|
// if we are reading a script element for the content of the template
|
|
// el will be set to that script element
|
|
el,
|
|
// a unique identifier for the view (used for caching)
|
|
// this is typically derived from the element id or
|
|
// the url for the template
|
|
id,
|
|
// the AJAX request used to retrieve the template content
|
|
jqXHR,
|
|
// used to generate the response
|
|
response = function( text ) {
|
|
// get the renderer function
|
|
var func = type.renderer(id, text);
|
|
// cache if if we are caching
|
|
if ( $view.cache ) {
|
|
$view.cached[id] = func;
|
|
}
|
|
// return the objects for the response's dataTypes
|
|
// (in this case view)
|
|
return {
|
|
view: func
|
|
};
|
|
};
|
|
|
|
// if we have an inline template, derive the suffix from the 'text/???' part
|
|
// this only supports '<script></script>' tags
|
|
if ( el = document.getElementById(url) ) {
|
|
suffix = "."+el.type.match(/\/(x\-)?(.+)/)[2];
|
|
}
|
|
|
|
// if there is no suffix, add one
|
|
if (!suffix ) {
|
|
suffix = $view.ext;
|
|
url = url + $view.ext;
|
|
}
|
|
|
|
// convert to a unique and valid id
|
|
id = toId(url);
|
|
|
|
// if a absolute path, use steal to get it
|
|
// you should only be using // if you are using steal
|
|
if ( url.match(/^\/\//) ) {
|
|
var sub = url.substr(2);
|
|
url = typeof steal === "undefined" ?
|
|
url = "/" + sub :
|
|
steal.root.mapJoin(sub) +'';
|
|
}
|
|
|
|
//set the template engine type
|
|
type = $view.types[suffix];
|
|
|
|
// return the ajax transport contract: http://api.jquery.com/extending-ajax/
|
|
return {
|
|
send: function( headers, callback ) {
|
|
// if it is cached,
|
|
if ( $view.cached[id] ) {
|
|
// return the catched renderer
|
|
return callback(200, "success", {
|
|
view: $view.cached[id]
|
|
});
|
|
|
|
// otherwise if we are getting this from a script elment
|
|
} else if ( el ) {
|
|
// resolve immediately with the element's innerHTML
|
|
callback(200, "success", response(el.innerHTML));
|
|
} else {
|
|
// make an ajax request for text
|
|
jqXHR = $.ajax({
|
|
async: orig.async,
|
|
url: url,
|
|
dataType: "text",
|
|
error: function() {
|
|
checkText("", url);
|
|
callback(404);
|
|
},
|
|
success: function( text ) {
|
|
// make sure we got some text back
|
|
checkText(text, url);
|
|
// cache and send back text
|
|
callback(200, "success", response(text))
|
|
}
|
|
});
|
|
}
|
|
},
|
|
abort: function() {
|
|
jqXHR && jqXHR.abort();
|
|
}
|
|
}
|
|
})
|
|
$.extend($view, {
|
|
/**
|
|
* @attribute hookups
|
|
* @hide
|
|
* A list of pending 'hookups'
|
|
*/
|
|
hookups: {},
|
|
/**
|
|
* @function hookup
|
|
* Registers a hookup function that can be called back after the html is
|
|
* put on the page. Typically this is handled by the template engine. Currently
|
|
* only EJS supports this functionality.
|
|
*
|
|
* var id = $.View.hookup(function(el){
|
|
* //do something with el
|
|
* }),
|
|
* html = "<div data-view-id='"+id+"'>"
|
|
* $('.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",
|
|
* plugin : "jquery/view/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:
|
|
*
|
|
* * plugin - the location of the plugin
|
|
* * 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:
|
|
* <ul>
|
|
* <li>plugin - the location of the plugin. EX: 'jquery/view/ejs'</li>
|
|
* <li>suffix - the view extension. EX: 'ejs'</li>
|
|
* <li>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).</li>
|
|
* <li>renderer(id, text) - a function that takes the id of the template and the text of the template and
|
|
* returns a render function.</li>
|
|
* </ul>
|
|
*/
|
|
register: function( info ) {
|
|
this.types["." + info.suffix] = info;
|
|
|
|
if ( window.steal ) {
|
|
steal.type(info.suffix + " view js", function( options, success, error ) {
|
|
var type = $view.types["." + options.type],
|
|
id = toId(options.rootSrc+'');
|
|
|
|
options.text = type.script(id, options.text)
|
|
success();
|
|
})
|
|
}
|
|
},
|
|
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);
|
|
};
|
|
}
|
|
|
|
});
|
|
if ( window.steal ) {
|
|
steal.type("view js", function( options, success, error ) {
|
|
var type = $view.types["." + options.type],
|
|
id = toId(options.rootSrc+'');
|
|
|
|
options.text = "steal('" + (type.plugin || "jquery/view/" + options.type) + "').then(function($){" + "$.View.preload('" + id + "'," + options.text + ");\n})";
|
|
success();
|
|
})
|
|
}
|
|
|
|
//---- ADD jQUERY HELPERS -----
|
|
//converts jquery functions to use views
|
|
var convert, modify, isTemplate, isHTML, isDOM, getCallback, hookupView, funcs,
|
|
// text and val cannot produce an element, so don't run hookups on them
|
|
noHookup = {'val':true,'text':true};
|
|
|
|
convert = function( func_name ) {
|
|
// save the old jQuery helper
|
|
var old = $.fn[func_name];
|
|
|
|
// replace it wiht our new helper
|
|
$.fn[func_name] = function() {
|
|
|
|
var args = makeArray(arguments),
|
|
callbackNum,
|
|
callback,
|
|
self = this,
|
|
result;
|
|
|
|
// if the first arg is a deferred
|
|
// wait until it finishes, and call
|
|
// modify with the result
|
|
if ( isDeferred(args[0]) ) {
|
|
args[0].done(function( res ) {
|
|
modify.call(self, [res], old);
|
|
})
|
|
return this;
|
|
}
|
|
//check if a template
|
|
else 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;
|
|
}
|
|
// call view with args (there might be deferreds)
|
|
result = $view.apply($view, args);
|
|
|
|
// if we got a string back
|
|
if (!isDeferred(result) ) {
|
|
// we are going to call the old method with that string
|
|
args = [result];
|
|
} else {
|
|
// if there is a deferred, wait until it is done before calling modify
|
|
result.done(function( res ) {
|
|
modify.call(self, [res], old);
|
|
})
|
|
return this;
|
|
}
|
|
}
|
|
return noHookup[func_name] ? old.apply(this,args) :
|
|
modify.call(this, args, old);
|
|
};
|
|
};
|
|
|
|
// modifies the content of the element
|
|
// but also will run any hookup
|
|
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 && args[0] && isHTML(args[0]) ) {
|
|
hooks = $view.hookups;
|
|
$view.hookups = {};
|
|
args[0] = $(args[0]);
|
|
}
|
|
res = old.apply(this, args);
|
|
|
|
//now hookup the hookups
|
|
if ( hooks
|
|
/* && args.length*/
|
|
) {
|
|
hookupView(args[0], hooks);
|
|
}
|
|
return res;
|
|
};
|
|
|
|
// returns true or false if the args indicate a template is being used
|
|
// $('#foo').html('/path/to/template.ejs',{data})
|
|
// in general, we want to make sure the first arg is a string
|
|
// and the second arg is data
|
|
isTemplate = function( args ) {
|
|
// save the second arg type
|
|
var secArgType = typeof args[1];
|
|
|
|
// the first arg is a string
|
|
return typeof args[0] == "string" &&
|
|
// the second arg is an object or function
|
|
(secArgType == 'object' || secArgType == 'function') &&
|
|
// but it is not a dom element
|
|
!isDOM(args[1]);
|
|
};
|
|
// returns true if the arg is a jQuery object or HTMLElement
|
|
isDOM = function(arg){
|
|
return arg.nodeType || arg.jquery
|
|
};
|
|
// returns whether the argument is some sort of HTML data
|
|
isHTML = function( arg ) {
|
|
if ( isDOM(arg) ) {
|
|
// if jQuery object or DOM node we're good
|
|
return true;
|
|
} else if ( typeof arg === "string" ) {
|
|
// if string, do a quick sanity check that we're HTML
|
|
arg = $.trim(arg);
|
|
return arg.substr(0, 1) === "<" && arg.substr(arg.length - 1, 1) === ">" && arg.length >= 3;
|
|
} else {
|
|
// don't know what you are
|
|
return false;
|
|
}
|
|
};
|
|
|
|
//returns the callback arg number 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
|
|
* @parent jQuery.View
|
|
* Called on a jQuery collection that was rendered with $.View with pending hookups. $.View can render a
|
|
* template with hookups, but not actually perform the hookup, because it returns a string without actual DOM
|
|
* elements to hook up to. So hookup performs the hookup and clears the pending hookups, preventing errors in
|
|
* future templates.
|
|
*
|
|
* @codestart
|
|
* $($.View('//views/recipes.ejs',recipeData)).hookup()
|
|
* @codeend
|
|
*/
|
|
$.fn.hookup = function() {
|
|
var hooks = $view.hookups;
|
|
$view.hookups = {};
|
|
hookupView(this, hooks);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* @add jQuery.fn
|
|
*/
|
|
$.each([
|
|
/**
|
|
* @function prepend
|
|
* @parent jQuery.View
|
|
*
|
|
* Extending the original [http://api.jquery.com/prepend/ jQuery().prepend()]
|
|
* to render [jQuery.View] templates inserted at the beginning of each element in the set of matched elements.
|
|
*
|
|
* $('#test').prepend('path/to/template.ejs', { name : 'javascriptmvc' });
|
|
*
|
|
* @param {String|Object|Function} content A template filename or the id of a view script tag
|
|
* or a DOM element, array of elements, HTML string, or jQuery object.
|
|
* @param {Object} [data] The data to render the view with.
|
|
* If rendering a view template this parameter always has to be present
|
|
* (use the empty object initializer {} for no data).
|
|
*/
|
|
"prepend",
|
|
/**
|
|
* @function append
|
|
* @parent jQuery.View
|
|
*
|
|
* Extending the original [http://api.jquery.com/append/ jQuery().append()]
|
|
* to render [jQuery.View] templates inserted at the end of each element in the set of matched elements.
|
|
*
|
|
* $('#test').append('path/to/template.ejs', { name : 'javascriptmvc' });
|
|
*
|
|
* @param {String|Object|Function} content A template filename or the id of a view script tag
|
|
* or a DOM element, array of elements, HTML string, or jQuery object.
|
|
* @param {Object} [data] The data to render the view with.
|
|
* If rendering a view template this parameter always has to be present
|
|
* (use the empty object initializer {} for no data).
|
|
*/
|
|
"append",
|
|
/**
|
|
* @function after
|
|
* @parent jQuery.View
|
|
*
|
|
* Extending the original [http://api.jquery.com/after/ jQuery().after()]
|
|
* to render [jQuery.View] templates inserted after each element in the set of matched elements.
|
|
*
|
|
* $('#test').after('path/to/template.ejs', { name : 'javascriptmvc' });
|
|
*
|
|
* @param {String|Object|Function} content A template filename or the id of a view script tag
|
|
* or a DOM element, array of elements, HTML string, or jQuery object.
|
|
* @param {Object} [data] The data to render the view with.
|
|
* If rendering a view template this parameter always has to be present
|
|
* (use the empty object initializer {} for no data).
|
|
*/
|
|
"after",
|
|
/**
|
|
* @function before
|
|
* @parent jQuery.View
|
|
*
|
|
* Extending the original [http://api.jquery.com/before/ jQuery().before()]
|
|
* to render [jQuery.View] templates inserted before each element in the set of matched elements.
|
|
*
|
|
* $('#test').before('path/to/template.ejs', { name : 'javascriptmvc' });
|
|
*
|
|
* @param {String|Object|Function} content A template filename or the id of a view script tag
|
|
* or a DOM element, array of elements, HTML string, or jQuery object.
|
|
* @param {Object} [data] The data to render the view with.
|
|
* If rendering a view template this parameter always has to be present
|
|
* (use the empty object initializer {} for no data).
|
|
*/
|
|
"before",
|
|
/**
|
|
* @function text
|
|
* @parent jQuery.View
|
|
*
|
|
* Extending the original [http://api.jquery.com/text/ jQuery().text()]
|
|
* to render [jQuery.View] templates as the content of each matched element.
|
|
* Unlike [jQuery.fn.html] jQuery.fn.text also works with XML, escaping the provided
|
|
* string as necessary.
|
|
*
|
|
* $('#test').text('path/to/template.ejs', { name : 'javascriptmvc' });
|
|
*
|
|
* @param {String|Object|Function} content A template filename or the id of a view script tag
|
|
* or a DOM element, array of elements, HTML string, or jQuery object.
|
|
* @param {Object} [data] The data to render the view with.
|
|
* If rendering a view template this parameter always has to be present
|
|
* (use the empty object initializer {} for no data).
|
|
*/
|
|
"text",
|
|
/**
|
|
* @function html
|
|
* @parent jQuery.View
|
|
*
|
|
* Extending the original [http://api.jquery.com/html/ jQuery().html()]
|
|
* to render [jQuery.View] templates as the content of each matched element.
|
|
*
|
|
* $('#test').html('path/to/template.ejs', { name : 'javascriptmvc' });
|
|
*
|
|
* @param {String|Object|Function} content A template filename or the id of a view script tag
|
|
* or a DOM element, array of elements, HTML string, or jQuery object.
|
|
* @param {Object} [data] The data to render the view with.
|
|
* If rendering a view template this parameter always has to be present
|
|
* (use the empty object initializer {} for no data).
|
|
*/
|
|
"html",
|
|
/**
|
|
* @function replaceWith
|
|
* @parent jQuery.View
|
|
*
|
|
* Extending the original [http://api.jquery.com/replaceWith/ jQuery().replaceWith()]
|
|
* to render [jQuery.View] templates replacing each element in the set of matched elements.
|
|
*
|
|
* $('#test').replaceWith('path/to/template.ejs', { name : 'javascriptmvc' });
|
|
*
|
|
* @param {String|Object|Function} content A template filename or the id of a view script tag
|
|
* or a DOM element, array of elements, HTML string, or jQuery object.
|
|
* @param {Object} [data] The data to render the view with.
|
|
* If rendering a view template this parameter always has to be present
|
|
* (use the empty object initializer {} for no data).
|
|
*/
|
|
"replaceWith", "val"],function(i, func){
|
|
convert(func);
|
|
});
|
|
|
|
//go through helper funcs and convert
|
|
|
|
|
|
})(jQuery);
|
|
(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);
|
|
(function( $ ) {
|
|
|
|
// HELPER METHODS ==============
|
|
var myEval = function( script ) {
|
|
eval(script);
|
|
},
|
|
// removes the last character from a string
|
|
// this is no longer needed
|
|
// chop = function( string ) {
|
|
// return string.substr(0, string.length - 1);
|
|
//},
|
|
rSplit = $.String.rsplit,
|
|
extend = $.extend,
|
|
isArray = $.isArray,
|
|
// regular expressions for caching
|
|
returnReg = /\r\n/g,
|
|
retReg = /\r/g,
|
|
newReg = /\n/g,
|
|
nReg = /\n/,
|
|
slashReg = /\\/g,
|
|
quoteReg = /"/g,
|
|
singleQuoteReg = /'/g,
|
|
tabReg = /\t/g,
|
|
leftBracket = /\{/g,
|
|
rightBracket = /\}/g,
|
|
quickFunc = /\s*\(([\$\w]+)\)\s*->([^\n]*)/,
|
|
// escapes characters starting with \
|
|
clean = function( content ) {
|
|
return content.replace(slashReg, '\\\\').replace(newReg, '\\n').replace(quoteReg, '\\"').replace(tabReg, '\\t');
|
|
},
|
|
// escapes html
|
|
// - from prototype http://www.prototypejs.org/
|
|
escapeHTML = function( content ) {
|
|
return content.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(quoteReg, '"').replace(singleQuoteReg, "'");
|
|
},
|
|
$View = $.View,
|
|
bracketNum = function(content){
|
|
var lefts = content.match(leftBracket),
|
|
rights = content.match(rightBracket);
|
|
|
|
return (lefts ? lefts.length : 0) -
|
|
(rights ? rights.length : 0);
|
|
},
|
|
/**
|
|
* @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 <a href="http://www.ruby-doc.org/stdlib/libdoc/erb/rdoc/">ERB</a>
|
|
* 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 <i>'views\tasks\list.ejs'</i>.
|
|
*
|
|
*
|
|
* ## 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><% CODE %></code> - Runs JS Code.
|
|
* For example:
|
|
*
|
|
* <% alert('hello world') %>
|
|
*
|
|
* - <code><%= CODE %></code> - Runs JS Code and writes the _escaped_ result into the result of the template.
|
|
* For example:
|
|
*
|
|
* <h1><%= 'hello world' %></h1>
|
|
*
|
|
* - <code><%== CODE %></code> - Runs JS Code and writes the _unescaped_ result into the result of the template.
|
|
* For example:
|
|
*
|
|
* <h1><%== '<span>hello world</span>' %></h1>
|
|
*
|
|
* - <code><%%= CODE %></code> - Writes <%= CODE %> to the result of the template. This is very useful for generators.
|
|
*
|
|
* <%%= 'hello world' %>
|
|
*
|
|
* - <code><%# CODE %></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
|
|
*
|
|
* To hook up a controller with options or any other jQuery plugin use the
|
|
* [jQuery.EJS.Helpers.prototype.plugin | plugin view helper]:
|
|
*
|
|
* @codestart
|
|
* <ul <%= plugin('mxui_tabs', { option: 'value' }) %>>...<ul>
|
|
* @codeend
|
|
*
|
|
* Don't add a semicolon when using view helpers.
|
|
*
|
|
*
|
|
* <h2>View Helpers</h2>
|
|
* 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
|
|
* <table class="options">
|
|
* <tbody><tr><th>Option</th><th>Default</th><th>Description</th></tr>
|
|
* <tr>
|
|
* <td>text</td>
|
|
* <td> </td>
|
|
* <td>uses the provided text as the template. Example:<br/><code>new View({text: '<%=user%>'})</code>
|
|
* </td>
|
|
* </tr>
|
|
* <tr>
|
|
* <td>type</td>
|
|
* <td>'<'</td>
|
|
* <td>type of magic tags. Options are '<' or '['
|
|
* </td>
|
|
* </tr>
|
|
* <tr>
|
|
* <td>name</td>
|
|
* <td>the element ID or url </td>
|
|
* <td>an optional name that is used for caching.
|
|
* </td>
|
|
* </tr>
|
|
* </tbody></table>
|
|
*/
|
|
EJS = function( options ) {
|
|
// If called without new, return a function that
|
|
// renders with data and helpers like
|
|
// EJS({text: '<%= message %>'})({message: 'foo'});
|
|
// this is useful for steal's build system
|
|
if ( this.constructor != EJS ) {
|
|
var ejs = new EJS(options);
|
|
return function( data, helpers ) {
|
|
return ejs.render(data, helpers);
|
|
};
|
|
}
|
|
// if we get a function directly, it probably is coming from
|
|
// a steal-packaged view
|
|
if ( typeof options == "function" ) {
|
|
this.template = {
|
|
fn: options
|
|
};
|
|
return;
|
|
}
|
|
//set options on self
|
|
extend(this, EJS.options, options);
|
|
this.template = compile(this.text, this.type, this.name);
|
|
};
|
|
// add EJS to jQuery if it exists
|
|
window.jQuery && (jQuery.EJS = EJS);
|
|
/**
|
|
* @Prototype
|
|
*/
|
|
EJS.prototype.
|
|
/**
|
|
* Renders an object with view helpers attached to the view.
|
|
*
|
|
* new EJS({text: "<%= message %>"}).render({
|
|
* message: "foo"
|
|
* },{helper: function(){ ... }})
|
|
*
|
|
* @param {Object} object data to be rendered
|
|
* @param {Object} [extraHelpers] an object with 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.fn.call(object, object, v);
|
|
};
|
|
/**
|
|
* @Static
|
|
*/
|
|
|
|
extend(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 magic tags: <%= %>
|
|
* @return {String} returns the content to be added to the rendered
|
|
* output. The content is different depending on the type:
|
|
*
|
|
* * string - the original string
|
|
* * null or undefined - the empty string ""
|
|
* * an object with a hookup method - the attribute "data-view-id='XX'", where XX is a hookup number for jQuery.View
|
|
* * a function - the attribute "data-view-id='XX'", where XX is a hookup number for jQuery.View
|
|
* * an array - the attribute "data-view-id='XX'", where XX is a hookup number for jQuery.View
|
|
*/
|
|
text: function( input ) {
|
|
// if it's a string, return
|
|
if ( typeof input == 'string' ) {
|
|
return input;
|
|
}
|
|
// if has no value
|
|
if ( input === null || input === undefined ) {
|
|
return '';
|
|
}
|
|
|
|
// if it's an object, and it has a hookup method
|
|
var hook = (input.hookup &&
|
|
// make a function call the hookup method
|
|
|
|
function( el, id ) {
|
|
input.hookup.call(input, el, id);
|
|
}) ||
|
|
// or if it's a function, just use the input
|
|
(typeof input == 'function' && input) ||
|
|
// of it its an array, make a function that calls hookup or the function
|
|
// on each item in the array
|
|
(isArray(input) &&
|
|
function( el, id ) {
|
|
for ( var i = 0; i < input.length; i++ ) {
|
|
input[i].hookup ? input[i].hookup(el, id) : input[i](el, id);
|
|
}
|
|
});
|
|
// finally, if there is a funciton to hookup on some dom
|
|
// pass it to hookup to get the data-view-id back
|
|
if ( hook ) {
|
|
return "data-view-id='" + $View.hookup(hook) + "'";
|
|
}
|
|
// finally, if all else false, toString it
|
|
return input.toString ? input.toString() : "";
|
|
},
|
|
/**
|
|
* Escapes the text provided as html if it's a string.
|
|
* Otherwise, the value is passed to EJS.text(text).
|
|
*
|
|
* @param {String|Object|Array|Function} text to escape. Otherwise,
|
|
* the result of [jQuery.EJS.text] is returned.
|
|
* @return {String} the escaped text or likely a $.View data-view-id attribute.
|
|
*/
|
|
clean: function( text ) {
|
|
//return sanatized text
|
|
if ( typeof text == 'string' ) {
|
|
return escapeHTML(text);
|
|
} else if ( typeof text == 'number' ) {
|
|
return text;
|
|
} else {
|
|
return EJS.text(text);
|
|
}
|
|
},
|
|
/**
|
|
* @attribute options
|
|
* Sets default options for all views.
|
|
*
|
|
* $.EJS.options.type = '['
|
|
*
|
|
* Only one option is currently supported: type.
|
|
*
|
|
* Type is the left hand magic tag.
|
|
*/
|
|
options: {
|
|
type: '<',
|
|
ext: '.ejs'
|
|
}
|
|
});
|
|
// ========= SCANNING CODE =========
|
|
// Given a scanner, and source content, calls block with each token
|
|
// scanner - an object of magicTagName : values
|
|
// source - the source you want to scan
|
|
// block - function(token, scanner), called with each token
|
|
var scan = function( scanner, source, block ) {
|
|
// split on /\n/ to have new lines on their own line.
|
|
var source_split = rSplit(source, nReg),
|
|
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);
|
|
}
|
|
}
|
|
},
|
|
// creates a 'scanner' object. This creates
|
|
// values for the left and right magic tags
|
|
// it's splitter property is a regexp that splits content
|
|
// by all tags
|
|
makeScanner = function( left, right ) {
|
|
var scanner = {};
|
|
extend(scanner, {
|
|
left: left + '%',
|
|
right: '%' + right,
|
|
dLeft: left + '%%',
|
|
dRight: '%%' + right,
|
|
eeLeft: left + '%==',
|
|
eLeft: left + '%=',
|
|
cmnt: left + '%#',
|
|
scan: scan,
|
|
lines: 0
|
|
});
|
|
scanner.splitter = new RegExp("(" + [scanner.dLeft, scanner.dRight, scanner.eeLeft, scanner.eLeft, scanner.cmnt, scanner.left, scanner.right + '\n', scanner.right, '\n'].join(")|(").
|
|
replace(/\[/g, "\\[").replace(/\]/g, "\\]") + ")");
|
|
return scanner;
|
|
},
|
|
// compiles a template where
|
|
// source - template text
|
|
// left - the left magic tag
|
|
// name - the name of the template (for debugging)
|
|
// returns an object like: {out : "", fn : function(){ ... }} where
|
|
// out - the converted JS source of the view
|
|
// fn - a function made from the JS source
|
|
compile = function( source, left, name ) {
|
|
// make everything only use \n
|
|
source = source.replace(returnReg, "\n").replace(retReg, "\n");
|
|
// if no left is given, assume <
|
|
left = left || '<';
|
|
|
|
// put and insert cmds are used for adding content to the template
|
|
// currently they are identical, I am not sure why
|
|
var put_cmd = "___v1ew.push(",
|
|
insert_cmd = put_cmd,
|
|
// the text that starts the view code (or block function)
|
|
startTxt = 'var ___v1ew = [];',
|
|
// the text that ends the view code (or block function)
|
|
finishTxt = "return ___v1ew.join('')",
|
|
// initialize a buffer
|
|
buff = new EJS.Buffer([startTxt], []),
|
|
// content is used as the current 'processing' string
|
|
// this is the content between magic tags
|
|
content = '',
|
|
// adds something to be inserted into the view template
|
|
// this comes out looking like __v1ew.push("CONENT")
|
|
put = function( content ) {
|
|
buff.push(put_cmd, '"', clean(content), '");');
|
|
},
|
|
// the starting magic tag
|
|
startTag = null,
|
|
// cleans the running content
|
|
empty = function() {
|
|
content = ''
|
|
},
|
|
// what comes after clean or text
|
|
doubleParen = "));",
|
|
// a stack used to keep track of how we should end a bracket }
|
|
// once we have a <%= %> with a leftBracket
|
|
// we store how the file should end here (either '))' or ';' )
|
|
endStack =[];
|
|
|
|
// start going token to token
|
|
scan(makeScanner(left, left === '[' ? ']' : '>'), source || "", function( token, scanner ) {
|
|
// if we don't have a start pair
|
|
var bn;
|
|
if ( startTag === null ) {
|
|
switch ( token ) {
|
|
case '\n':
|
|
content = content + "\n";
|
|
put(content);
|
|
buff.cr();
|
|
empty();
|
|
break;
|
|
// set start tag, add previous content (if there is some)
|
|
// clean content
|
|
case scanner.left:
|
|
case scanner.eLeft:
|
|
case scanner.eeLeft:
|
|
case scanner.cmnt:
|
|
// a new line, just add whatever content w/i a clean
|
|
// reset everything
|
|
startTag = token;
|
|
if ( content.length > 0 ) {
|
|
put(content);
|
|
}
|
|
empty();
|
|
break;
|
|
|
|
case scanner.dLeft:
|
|
// replace <%% with <%
|
|
content += scanner.left;
|
|
break;
|
|
default:
|
|
content += token;
|
|
break;
|
|
}
|
|
}
|
|
else {
|
|
//we have a start tag
|
|
switch ( token ) {
|
|
case scanner.right:
|
|
// %>
|
|
switch ( startTag ) {
|
|
case scanner.left:
|
|
// <%
|
|
|
|
// get the number of { minus }
|
|
bn = bracketNum(content);
|
|
// how are we ending this statement
|
|
var last =
|
|
// if the stack has value and we are ending a block
|
|
endStack.length && bn == -1 ?
|
|
// use the last item in the block stack
|
|
endStack.pop() :
|
|
// or use the default ending
|
|
";";
|
|
|
|
// if we are ending a returning block
|
|
// add the finish text which returns the result of the
|
|
// block
|
|
if(last === doubleParen) {
|
|
buff.push(finishTxt)
|
|
}
|
|
// add the remaining content
|
|
buff.push(content, last);
|
|
|
|
// if we have a block, start counting
|
|
if(bn === 1 ){
|
|
endStack.push(";")
|
|
}
|
|
break;
|
|
case scanner.eLeft:
|
|
// <%= clean content
|
|
bn = bracketNum(content);
|
|
if( bn ) {
|
|
endStack.push(doubleParen)
|
|
}
|
|
if(quickFunc.test(content)){
|
|
var parts = content.match(quickFunc)
|
|
content = "function(__){var "+parts[1]+"=$(__);"+parts[2]+"}"
|
|
}
|
|
buff.push(insert_cmd, "jQuery.EJS.clean(", content,bn ? startTxt : doubleParen);
|
|
break;
|
|
case scanner.eeLeft:
|
|
// <%== content
|
|
|
|
// get the number of { minus }
|
|
bn = bracketNum(content);
|
|
// if we have more {, it means there is a block
|
|
if( bn ){
|
|
// when we return to the same # of { vs } end wiht a doubleParen
|
|
endStack.push(doubleParen)
|
|
}
|
|
|
|
buff.push(insert_cmd, "jQuery.EJS.text(", content,
|
|
// if we have a block
|
|
bn ?
|
|
// start w/ startTxt "var _v1ew = [])"
|
|
startTxt :
|
|
// if not, add doubleParent to close push and text
|
|
doubleParen
|
|
);
|
|
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 + " "+finishTxt+"}}}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.fn = (function(_CONTEXT,_VIEW){' + out.out + '});\r\n//# sourceURL="' + name + '.js"');
|
|
|
|
return out;
|
|
};
|
|
|
|
|
|
// A Buffer used to add content to.
|
|
// This is useful for performance and simplifying the
|
|
// code above.
|
|
// We also can use this so we know line numbers when there
|
|
// is an error.
|
|
// pre_cmd - code that sets up the buffer
|
|
// post - code that finalizes the buffer
|
|
EJS.Buffer = function( pre_cmd, post ) {
|
|
// the current line we are on
|
|
this.line = [];
|
|
// the combined content added to this buffer
|
|
this.script = [];
|
|
// content at the end of the buffer
|
|
this.post = post;
|
|
// add the pre commands to the first line
|
|
this.push.apply(this, pre_cmd);
|
|
};
|
|
EJS.Buffer.prototype = {
|
|
// add content to this line
|
|
// need to maintain your own semi-colons (for performance)
|
|
push: function() {
|
|
this.line.push.apply(this.line, arguments);
|
|
},
|
|
// starts a new line
|
|
cr: function() {
|
|
this.script.push(this.line.join(''), "\n");
|
|
this.line = [];
|
|
},
|
|
//returns the script too
|
|
close: function() {
|
|
// if we have ending line content, add it to the script
|
|
if ( this.line.length > 0 ) {
|
|
this.script.push(this.line.join(''));
|
|
this.line = [];
|
|
}
|
|
// if we have ending content, add it
|
|
this.post.length && this.push.apply(this, this.post);
|
|
// always end in a ;
|
|
this.script.push(";");
|
|
return this.script.join("");
|
|
}
|
|
|
|
};
|
|
|
|
/**
|
|
* @class jQuery.EJS.Helpers
|
|
* @parent jQuery.EJS
|
|
* By adding functions to jQuery.EJS.Helpers.prototype, those functions will be available in the
|
|
* views.
|
|
*
|
|
* The following helper converts a given string to upper case:
|
|
*
|
|
* $.EJS.Helpers.prototype.toUpper = function(params)
|
|
* {
|
|
* return params.toUpperCase();
|
|
* }
|
|
*
|
|
* Use it like this in any EJS template:
|
|
*
|
|
* <%= toUpper('javascriptmvc') %>
|
|
*
|
|
* To access the current DOM element return a function that takes the element as a parameter:
|
|
*
|
|
* $.EJS.Helpers.prototype.upperHtml = function(params)
|
|
* {
|
|
* return function(el) {
|
|
* $(el).html(params.toUpperCase());
|
|
* }
|
|
* }
|
|
*
|
|
* In your EJS view you can then call the helper on an element tag:
|
|
*
|
|
* <div <%= upperHtml('javascriptmvc') %>></div>
|
|
*
|
|
*
|
|
* @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 <code>$.View()</code>.
|
|
*/
|
|
view: function( url, data, helpers ) {
|
|
helpers = helpers || this._extras;
|
|
data = data || this._data;
|
|
return $View(url, data, helpers); //new EJS(options).render(data, helpers);
|
|
}
|
|
};
|
|
|
|
// options for steal's build
|
|
$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,
|
|
name: id
|
|
}).template.out + " })";
|
|
},
|
|
renderer: function( id, text ) {
|
|
return EJS({
|
|
text: text,
|
|
name: id
|
|
});
|
|
}
|
|
});
|
|
})(jQuery); |