(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: * * */ 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: *
<h2><%= message %></h2>
* * - Renders it with {message: 'hello world'}, resulting in: *
<div id='foo'>"<h2>hello world</h2></div>
* * - Inserts the result into the foo element. Foo might look like: *
<div id='foo'><h2>hello world</h2></div>
* * ## jQuery Modifiers * * You can use a template with the following jQuery modifiers: * * * * * * * * * *
[jQuery.fn.after after] $('#bar').after('temp.jaml',{});
[jQuery.fn.append append] $('#bar').append('temp.jaml',{});
[jQuery.fn.before before] $('#bar').before('temp.jaml',{});
[jQuery.fn.html html] $('#bar').html('temp.jaml',{});
[jQuery.fn.prepend prepend] $('#bar').prepend('temp.jaml',{});
[jQuery.fn.replaceWith replaceWith] $('#bar').replaceWith('temp.jaml',{});
[jQuery.fn.text text] $('#bar').text('temp.jaml',{});
* * 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: * *
<script type='text/ejs' id='recipes'>
	 * <% for(var i=0; i < recipes.length; i++){ %>
	 *   <li><%=recipes[i].name %></li>
	 * <%} %>
	 * </script>
* * 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 *
<h2><%= message %></h2>
* * - JAML *
h2(data.message);
* * - Micro *
<h2>{%= message %}</h2>
* * - jQuery.Tmpl *
<h2>${message}</h2>
* * The popular Mustache * 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 '' 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 = "
" * $('.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: * */ 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(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 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 _escaped_ result into the result of the template. * For example: * *

<%= 'hello world' %>

* * - <%== CODE %> - Runs JS Code and writes the _unescaped_ 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 * * 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. * * *

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 * * * * * * * * * * * * * * * * * *
OptionDefaultDescription
text uses the provided text as the template. Example:
new View({text: '<%=user%>'}) *
type'<'type of magic tags. Options are '<' or '[' *
namethe element ID or url an optional name that is used for caching. *
*/ 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: * *
>
* * * @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); } }; // 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);