diff --git a/index.html b/index.html index 7dbffb5..eeb15dd 100644 --- a/index.html +++ b/index.html @@ -3,45 +3,14 @@ - + - - - - -
-

Tasks

-
-
-
-
- - -
- -
- -
    -
-
-
diff --git a/index.js b/index.js index 31e7582..344bf23 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,11 @@ (function($) { + //events $(document).on('submit', '#add-task-form', function(ev) { ev.preventDefault(); - var newTask = '
  • ' + $('#task').val() + '
  • '; - $('#task-list').append(newTask); + + $('#task-list').append('views/task-item.ejs', {title: $('#task').val()}); }); + + //initial page setup + $('.container').html('views/tasks.ejs', {}); })(jQuery) \ No newline at end of file diff --git a/libs/ejs.js b/libs/ejs.js new file mode 100644 index 0000000..f469660 --- /dev/null +++ b/libs/ejs.js @@ -0,0 +1,1777 @@ +(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); \ No newline at end of file diff --git a/views/task-item.ejs b/views/task-item.ejs new file mode 100644 index 0000000..47a3346 --- /dev/null +++ b/views/task-item.ejs @@ -0,0 +1 @@ +
  • <%= title %>
  • \ No newline at end of file diff --git a/views/tasks.ejs b/views/tasks.ejs new file mode 100644 index 0000000..1ea969e --- /dev/null +++ b/views/tasks.ejs @@ -0,0 +1,16 @@ +

    Tasks

    +
    +
    +
    +
    + + +
    + +
    + + +
      +
    +
    +
    \ No newline at end of file