Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

first commit

  • Loading branch information...
commit 6ed3cb24d8b59a3bcc9ccbec7e41f4a8b6e5f987 0 parents
@maccman authored
73 app/application.js
@@ -0,0 +1,73 @@
+(function($, Spine){
+
+ var con = new Spine.Controller("#tasks");
+
+ con.events = {
+ "change .item input[type=checkbox]": "toggle",
+ "click .item .destroy": "destroy",
+ "dblclick .item .view": "edit",
+ "keypress .item input[type=text]": "closeEdit",
+
+ "submit form": "create",
+ "click .clear": "clear",
+ "render .items": "render"
+ };
+
+ // Show tasks
+ con.load(function(){
+ this.tasks = this.el.find(".items");
+ this.count = this.el.find(".countVal");
+ this.input = this.el.find("form input");
+
+ this.tasks.link(Task, function(){
+ var elements = $("#taskTemplate").tmpl(Task.all());
+
+ $(this).empty();
+ $(this).append(elements);
+ });
+ });
+
+ con.extend({
+ toggle: function(e){
+ var task = $(e.target).item();
+ task.done = !task.done;
+ task.save();
+ },
+
+ destroy: function(e){
+ $(e.target).item().destroy();
+ },
+
+ edit: function(e){
+ var item = $(e.target).parents(".item");
+ item.addClass("editing");
+ item.find("input").focus();
+ },
+
+ closeEdit: function(e){
+ if ( e.keyCode != 13 ) return;
+ $(e.target).parents("li").removeClass("editing");
+ $(e.target).item().updateAttributes({name: $(e.target).val()});
+ },
+
+ create: function(e){
+ Task.create({name: this.input.val()});
+ this.input.val("");
+ return false;
+ },
+
+ clear: function(){
+ Task.destroyDone();
+ },
+
+ render: function(){
+ this.count.text(Task.active().length);
+ }
+ });
+
+ // Initial render
+ con.load(function(){
+ this.tasks.render();
+ });
+
+})(jQuery, Spine);
22 app/models/task.js
@@ -0,0 +1,22 @@
+// Create the Task model.
+var Task = Spine.Model.setup("Task", ["name", "done"]);
+
+// Persist model between page reloads.
+Task.extend(Spine.Model.Local);
+
+Task.extend({
+ // Return all active tasks.
+ active: function(){
+ return(this.select(function(item){ return !item.done; }));
+ },
+
+ // Return all done tasks.
+ done: function(){
+ return(this.select(function(item){ return !!item.done; }));
+ },
+
+ // Clear all done tasks.
+ destroyDone: function(){
+ this.done().forEach(function(rec){ rec.destroy() });
+ }
+});
153 css/application.css
@@ -0,0 +1,153 @@
+html, body {
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ font-family: "Helvetica Neue", helvetica, arial, sans-serif;
+ font-size: 14px;
+ line-height: 1.4em;
+ background: #eeeeee;
+ color: #333333;
+}
+
+#views {
+ width: 520px;
+ margin: 0 auto 40px auto;
+ background: white;
+
+ -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
+ -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
+ -o-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
+ box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
+
+ -moz-border-radius: 0 0 5px 5px;
+ -o-border-radius: 0 0 5px 5px;
+ -webkit-border-radius: 0 0 5px 5px;
+ border-radius: 0 0 5px 5px;
+}
+
+#tasks {
+ padding: 20px;
+}
+
+#tasks h1 {
+ font-size: 36px;
+ font-weight: bold;
+ text-align: center;
+ padding: 0 0 10px 0;
+}
+
+#tasks input[type="text"] {
+ width: 466px;
+ font-size: 24px;
+ font-family: inherit;
+ line-height: 1.4em;
+ border: 0;
+ outline: none;
+ padding: 6px;
+ border: 1px solid #999999;
+
+ -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
+ -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
+ -o-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
+ box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
+}
+
+#tasks input::-webkit-input-placeholder {
+ font-style: italic;
+}
+
+#tasks .items {
+ margin: 10px 0;
+}
+
+#tasks .item {
+ padding: 15px 20px 15px 0;
+ position: relative;
+ font-size: 24px;
+ border-bottom: 1px solid #cccccc;
+}
+
+#tasks .item.done span {
+ color: #777777;
+ text-decoration: line-through;
+}
+
+#tasks .item .destroy {
+ position: absolute;
+ right: 10px;
+ top: 16px;
+ display: none;
+ cursor: pointer;
+ width: 20px;
+ height: 20px;
+ background: url(../images/destroy.png) no-repeat center center;
+}
+
+#tasks .item:hover .destroy {
+ display: block;
+}
+
+#tasks .item .edit { display: none; }
+#tasks .item.editing .edit { display: block; }
+#tasks .item.editing .view { display: none; }
+
+#tasks footer {
+ display: block;
+ margin: 20px -20px -20px -20px;
+ overflow: hidden;
+
+ color: #555555;
+ background: #f4fce8;
+ border-top: 1px solid #ededed;
+ padding: 0 20px;
+ line-height: 36px;
+
+ -moz-border-radius: 0 0 5px 5px;
+ -o-border-radius: 0 0 5px 5px;
+ -webkit-border-radius: 0 0 5px 5px;
+ border-radius: 0 0 5px 5px;
+}
+
+#tasks .clear {
+ display: block;
+ float: right;
+ line-height: 20px;
+ text-decoration: none;
+
+ background: rgba(0, 0, 0, 0.1);
+ color: #555555;
+ font-size: 11px;
+ margin-top: 8px;
+ padding: 0 10px 1px;
+
+ -moz-border-radius: 12px;
+ -webkit-border-radius: 12px;
+ -o-border-radius: 12px;
+ border-radius: 12px;
+
+ -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
+ -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
+ -o-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
+ box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
+
+ cursor: pointer;
+}
+
+#tasks .clear:hover {
+ background: rgba(0, 0, 0, 0.15);
+ -moz-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
+ -webkit-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
+ -o-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
+ box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
+}
+
+#tasks .clear:active {
+ position: relative;
+ top: 1px;
+}
+
+#tasks .count span {
+ font-weight: bold;
+}
BIN  images/destroy.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 index.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <link rel="stylesheet" href="css/application.css" type="text/css" charset="utf-8">
+
+ <script src="lib/jquery.js" type="text/javascript" charset="utf-8"></script>
+ <script src="lib/jquery.tmpl.js" type="text/javascript" charset="utf-8"></script>
+ <script src="lib/jquery.link.js" type="text/javascript" charset="utf-8"></script>
+
+ <script src="lib/spine.js" type="text/javascript" charset="utf-8"></script>
+ <script src="lib/spine.model.local.js" type="text/javascript" charset="utf-8"></script>
+
+ <script src="app/models/task.js" type="text/javascript" charset="utf-8"></script>
+ <script src="app/application.js" type="text/javascript" charset="utf-8"></script>
+
+ <script type="text/x-jquery-tmpl" id="taskTemplate">
+ <div class="item {{if done}}done{{/if}}">
+ <div class="view">
+ <input type="checkbox" {{if done}}checked="checked"{{/if}}>
+ <span>${name}</span> <a class="destroy"></a>
+ </div>
+
+ <div class="edit">
+ <input type="text" value="${name}">
+ </div>
+ </div>
+ </script>
+</head>
+<body>
+ <div id="views">
+ <div id="tasks">
+ <h1>Todos</h1>
+
+ <form>
+ <input type="text" placeholder="What needs to be done?">
+ </form>
+
+ <div class="items"></div>
+
+ <footer>
+ <a class="clear">Clear completed</a>
+ <div class="count"><span class="countVal"></span> left</div>
+ </footer>
+ </div>
+ </div>
+</body>
+</html>
8,316 lib/jquery.js
8,316 additions, 0 deletions not shown
68 lib/jquery.link.js
@@ -0,0 +1,68 @@
+//
+// JLink does two things:
+// * adds a 'change' callback onto standard objects
+// * add a utility function to bind up jQuery elements and objects
+//
+// var object = {
+// name: "Test Name"
+// };
+//
+// $("#user").link(object, function(e, data){
+// $(this).empty();
+// $(this).append($("#userTmpl").tmpl(data));
+// });
+//
+// object.change();
+
+(function($){
+
+ var checkChange = function(ob){
+ if ("change" in ob && typeof ob.change != "function")
+ throw("change is already defined in " + ob);
+ return( !!ob.change );
+ }
+
+ $.addChange = function(ob){
+ if (checkChange(ob)) return;
+
+ ob.change = function(callback){
+ if (callback) {
+ if ( !this._change ) this._change = [];
+ this._change.push(callback);
+ } else {
+ if ( !this._change ) return;
+ for (var i=0; i < this._change.length; i++)
+ this._change[i].apply(this);
+ }
+ };
+ };
+
+ $.fn.link = $.fn.jlink = function(object, callback){
+ var element = $(this);
+
+ if ( !object )
+ throw("You must provide an object")
+
+ $.addChange(object);
+
+ object.change(function(){
+ element.trigger("render", this);
+ });
+
+ if ( callback )
+ element.bind("render", callback);
+
+ return element;
+ };
+
+ $.fn.render = function(callback) {
+ callback ? this.bind("render", callback) : this.trigger("render");
+ };
+
+ $.fn.item = function(){
+ var item = $(this).tmplItem().data;
+ if (typeof item.reload == "function")
+ item = item.reload();
+ return item;
+ };
+})(jQuery);
490 lib/jquery.tmpl.js
@@ -0,0 +1,490 @@
+/*!
+ * jQuery Templates Plugin
+ * http://github.com/jquery/jquery-tmpl
+ *
+ * Copyright Software Freedom Conservancy, Inc.
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ */
+(function( jQuery, undefined ){
+ var oldManip = jQuery.fn.domManip, tmplItmAtt = "_tmplitem", htmlExpr = /^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /,
+ newTmplItems = {}, wrappedItems = {}, appendToTmplItems, topTmplItem = { key: 0, data: {} }, itemKey = 0, cloneIndex = 0, stack = [];
+
+ function newTmplItem( options, parentItem, fn, data ) {
+ // Returns a template item data structure for a new rendered instance of a template (a 'template item').
+ // The content field is a hierarchical array of strings and nested items (to be
+ // removed and replaced by nodes field of dom elements, once inserted in DOM).
+ var newItem = {
+ data: data || (parentItem ? parentItem.data : {}),
+ _wrap: parentItem ? parentItem._wrap : null,
+ tmpl: null,
+ parent: parentItem || null,
+ nodes: [],
+ calls: tiCalls,
+ nest: tiNest,
+ wrap: tiWrap,
+ html: tiHtml,
+ update: tiUpdate
+ };
+ if ( options ) {
+ jQuery.extend( newItem, options, { nodes: [], parent: parentItem } );
+ }
+ if ( fn ) {
+ // Build the hierarchical content to be used during insertion into DOM
+ newItem.tmpl = fn;
+ newItem._ctnt = newItem._ctnt || newItem.tmpl( jQuery, newItem );
+ newItem.key = ++itemKey;
+ // Keep track of new template item, until it is stored as jQuery Data on DOM element
+ (stack.length ? wrappedItems : newTmplItems)[itemKey] = newItem;
+ }
+ return newItem;
+ }
+
+ // Override appendTo etc., in order to provide support for targeting multiple elements. (This code would disappear if integrated in jquery core).
+ jQuery.each({
+ appendTo: "append",
+ prependTo: "prepend",
+ insertBefore: "before",
+ insertAfter: "after",
+ replaceAll: "replaceWith"
+ }, function( name, original ) {
+ jQuery.fn[ name ] = function( selector ) {
+ var ret = [], insert = jQuery( selector ), elems, i, l, tmplItems,
+ parent = this.length === 1 && this[0].parentNode;
+
+ appendToTmplItems = newTmplItems || {};
+ if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) {
+ insert[ original ]( this[0] );
+ ret = this;
+ } else {
+ for ( i = 0, l = insert.length; i < l; i++ ) {
+ cloneIndex = i;
+ elems = (i > 0 ? this.clone(true) : this).get();
+ jQuery.fn[ original ].apply( jQuery(insert[i]), elems );
+ ret = ret.concat( elems );
+ }
+ cloneIndex = 0;
+ ret = this.pushStack( ret, name, insert.selector );
+ }
+ tmplItems = appendToTmplItems;
+ appendToTmplItems = null;
+ jQuery.tmpl.complete( tmplItems );
+ return ret;
+ };
+ });
+
+ jQuery.fn.extend({
+ // Use first wrapped element as template markup.
+ // Return wrapped set of template items, obtained by rendering template against data.
+ tmpl: function( data, options, parentItem ) {
+ return jQuery.tmpl( this[0], data, options, parentItem );
+ },
+
+ // Find which rendered template item the first wrapped DOM element belongs to
+ tmplItem: function() {
+ return jQuery.tmplItem( this[0] );
+ },
+
+ // Consider the first wrapped element as a template declaration, and get the compiled template or store it as a named template.
+ template: function( name ) {
+ return jQuery.template( name, this[0] );
+ },
+
+ domManip: function( args, table, callback, options ) {
+ // This appears to be a bug in the appendTo, etc. implementation
+ // it should be doing .call() instead of .apply(). See #6227
+ if ( args[0] && args[0].nodeType ) {
+ var dmArgs = jQuery.makeArray( arguments ), argsLength = args.length, i = 0, tmplItem;
+ while ( i < argsLength && !(tmplItem = jQuery.data( args[i++], "tmplItem" ))) {}
+ if ( argsLength > 1 ) {
+ dmArgs[0] = [jQuery.makeArray( args )];
+ }
+ if ( tmplItem && cloneIndex ) {
+ dmArgs[2] = function( fragClone ) {
+ // Handler called by oldManip when rendered template has been inserted into DOM.
+ jQuery.tmpl.afterManip( this, fragClone, callback );
+ };
+ }
+ oldManip.apply( this, dmArgs );
+ } else {
+ oldManip.apply( this, arguments );
+ }
+ cloneIndex = 0;
+ if ( !appendToTmplItems ) {
+ jQuery.tmpl.complete( newTmplItems );
+ }
+ return this;
+ }
+ });
+
+ jQuery.extend({
+ // Return wrapped set of template items, obtained by rendering template against data.
+ tmpl: function( tmpl, data, options, parentItem ) {
+ var ret, topLevel = !parentItem;
+ if ( topLevel ) {
+ // This is a top-level tmpl call (not from a nested template using {{tmpl}})
+ parentItem = topTmplItem;
+ if ( typeof tmpl != "function" )
+ tmpl = jQuery.template[tmpl] || jQuery.template( null, tmpl );
+ wrappedItems = {}; // Any wrapped items will be rebuilt, since this is top level
+ } else if ( !tmpl ) {
+ // The template item is already associated with DOM - this is a refresh.
+ // Re-evaluate rendered template for the parentItem
+ tmpl = parentItem.tmpl;
+ newTmplItems[parentItem.key] = parentItem;
+ parentItem.nodes = [];
+ if ( parentItem.wrapped ) {
+ updateWrapped( parentItem, parentItem.wrapped );
+ }
+ // Rebuild, without creating a new template item
+ return jQuery( build( parentItem, null, parentItem.tmpl( jQuery, parentItem ) ));
+ }
+ if ( !tmpl ) {
+ return []; // Could throw...
+ }
+ if ( typeof data === "function" ) {
+ data = data.call( parentItem || {} );
+ }
+ if ( options && options.wrapped ) {
+ updateWrapped( options, options.wrapped );
+ }
+ ret = jQuery.isArray( data ) ?
+ jQuery.map( data, function( dataItem ) {
+ return dataItem ? newTmplItem( options, parentItem, tmpl, dataItem ) : null;
+ }) :
+ [ newTmplItem( options, parentItem, tmpl, data ) ];
+ return topLevel ? jQuery( build( parentItem, null, ret ) ) : ret;
+ },
+
+ // Return rendered template item for an element.
+ tmplItem: function( elem ) {
+ var tmplItem;
+ if ( elem instanceof jQuery ) {
+ elem = elem[0];
+ }
+ while ( elem && elem.nodeType === 1 && !(tmplItem = jQuery.data( elem, "tmplItem" )) && (elem = elem.parentNode) ) {}
+ return tmplItem || topTmplItem;
+ },
+
+ // Set:
+ // Use $.template( name, tmpl ) to cache a named template,
+ // where tmpl is a template string, a script element or a jQuery instance wrapping a script element, etc.
+ // Use $( "selector" ).template( name ) to provide access by name to a script block template declaration.
+
+ // Get:
+ // Use $.template( name ) to access a cached template.
+ // Also $( selectorToScriptBlock ).template(), or $.template( null, templateString )
+ // will return the compiled template, without adding a name reference.
+ // If templateString includes at least one HTML tag, $.template( templateString ) is equivalent
+ // to $.template( null, templateString )
+ template: function( name, tmpl ) {
+ if (tmpl) {
+ // Compile template and associate with name
+ if ( typeof tmpl === "string" ) {
+ // This is an HTML string being passed directly in.
+ tmpl = buildTmplFn( tmpl )
+ } else if ( tmpl instanceof jQuery ) {
+ tmpl = tmpl[0] || {};
+ }
+ if ( tmpl.nodeType ) {
+ // If this is a template block, use cached copy, or generate tmpl function and cache.
+ tmpl = jQuery.data( tmpl, "tmpl" ) || jQuery.data( tmpl, "tmpl", buildTmplFn( tmpl.innerHTML ));
+ }
+ return typeof name === "string" ? (jQuery.template[name] = tmpl) : tmpl;
+ }
+ // Return named compiled template
+ return name ? (typeof name !== "string" ? jQuery.template( null, name ):
+ (jQuery.template[name] ||
+ // If not in map, treat as a selector. (If integrated with core, use quickExpr.exec)
+ jQuery.template( null, htmlExpr.test( name ) ? name : jQuery( name )))) : null;
+ },
+
+ encode: function( text ) {
+ // Do HTML encoding replacing < > & and ' and " by corresponding entities.
+ return ("" + text).split("<").join("&lt;").split(">").join("&gt;").split('"').join("&#34;").split("'").join("&#39;");
+ }
+ });
+
+ jQuery.extend( jQuery.tmpl, {
+ tag: {
+ "tmpl": {
+ _default: { $2: "null" },
+ open: "if($notnull_1){_=_.concat($item.nest($1,$2));}"
+ // tmpl target parameter can be of type function, so use $1, not $1a (so not auto detection of functions)
+ // This means that {{tmpl foo}} treats foo as a template (which IS a function).
+ // Explicit parens can be used if foo is a function that returns a template: {{tmpl foo()}}.
+ },
+ "wrap": {
+ _default: { $2: "null" },
+ open: "$item.calls(_,$1,$2);_=[];",
+ close: "call=$item.calls();_=call._.concat($item.wrap(call,_));"
+ },
+ "each": {
+ _default: { $2: "$index, $value" },
+ open: "if($notnull_1){$.each($1a,function($2){with(this){",
+ close: "}});}"
+ },
+ "if": {
+ open: "if(($notnull_1) && $1a){",
+ close: "}"
+ },
+ "else": {
+ _default: { $1: "true" },
+ open: "}else if(($notnull_1) && $1a){"
+ },
+ "html": {
+ // Unecoded expression evaluation.
+ open: "if($notnull_1){_.push($1a);}"
+ },
+ "=": {
+ // Encoded expression evaluation. Abbreviated form is ${}.
+ _default: { $1: "$data" },
+ open: "if($notnull_1){_.push($.encode($1a));}"
+ },
+ "!": {
+ // Comment tag. Skipped by parser
+ open: ""
+ }
+ },
+
+ // This stub can be overridden, e.g. in jquery.tmplPlus for providing rendered events
+ complete: function( items ) {
+ newTmplItems = {};
+ },
+
+ // Call this from code which overrides domManip, or equivalent
+ // Manage cloning/storing template items etc.
+ afterManip: function afterManip( elem, fragClone, callback ) {
+ // Provides cloned fragment ready for fixup prior to and after insertion into DOM
+ var content = fragClone.nodeType === 11 ?
+ jQuery.makeArray(fragClone.childNodes) :
+ fragClone.nodeType === 1 ? [fragClone] : [];
+
+ // Return fragment to original caller (e.g. append) for DOM insertion
+ callback.call( elem, fragClone );
+
+ // Fragment has been inserted:- Add inserted nodes to tmplItem data structure. Replace inserted element annotations by jQuery.data.
+ storeTmplItems( content );
+ cloneIndex++;
+ }
+ });
+
+ //========================== Private helper functions, used by code above ==========================
+
+ function build( tmplItem, nested, content ) {
+ // Convert hierarchical content into flat string array
+ // and finally return array of fragments ready for DOM insertion
+ var frag, ret = content ? jQuery.map( content, function( item ) {
+ return (typeof item === "string") ?
+ // Insert template item annotations, to be converted to jQuery.data( "tmplItem" ) when elems are inserted into DOM.
+ (tmplItem.key ? item.replace( /(<\w+)(?=[\s>])(?![^>]*_tmplitem)([^>]*)/g, "$1 " + tmplItmAtt + "=\"" + tmplItem.key + "\" $2" ) : item) :
+ // This is a child template item. Build nested template.
+ build( item, tmplItem, item._ctnt );
+ }) :
+ // If content is not defined, insert tmplItem directly. Not a template item. May be a string, or a string array, e.g. from {{html $item.html()}}.
+ tmplItem;
+ if ( nested ) {
+ return ret;
+ }
+
+ // top-level template
+ ret = ret.join("");
+
+ // Support templates which have initial or final text nodes, or consist only of text
+ // Also support HTML entities within the HTML markup.
+ ret.replace( /^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/, function( all, before, middle, after) {
+ frag = jQuery( middle ).get();
+
+ storeTmplItems( frag );
+ if ( before ) {
+ frag = unencode( before ).concat(frag);
+ }
+ if ( after ) {
+ frag = frag.concat(unencode( after ));
+ }
+ });
+ return frag ? frag : unencode( ret );
+ }
+
+ function unencode( text ) {
+ // Use createElement, since createTextNode will not render HTML entities correctly
+ var el = document.createElement( "div" );
+ el.innerHTML = text;
+ return jQuery.makeArray(el.childNodes);
+ }
+
+ // Generate a reusable function that will serve to render a template against data
+ function buildTmplFn( markup ) {
+ return new Function("jQuery","$item",
+ "var $=jQuery,call,_=[],$data=$item.data;" +
+
+ // Introduce the data as local variables using with(){}
+ "with($data){_.push('" +
+
+ // Convert the template into pure JavaScript
+ jQuery.trim(markup)
+ .replace( /([\\'])/g, "\\$1" )
+ .replace( /[\r\t\n]/g, " " )
+ .replace( /\$\{([^\}]*)\}/g, "{{= $1}}" )
+ .replace( /\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g,
+ function( all, slash, type, fnargs, target, parens, args ) {
+ var tag = jQuery.tmpl.tag[ type ], def, expr, exprAutoFnDetect;
+ if ( !tag ) {
+ throw "Template command not found: " + type;
+ }
+ def = tag._default || [];
+ if ( parens && !/\w$/.test(target)) {
+ target += parens;
+ parens = "";
+ }
+ if ( target ) {
+ target = unescape( target );
+ args = args ? ("," + unescape( args ) + ")") : (parens ? ")" : "");
+ // Support for target being things like a.toLowerCase();
+ // In that case don't call with template item as 'this' pointer. Just evaluate...
+ expr = parens ? (target.indexOf(".") > -1 ? target + parens : ("(" + target + ").call($data" + args)) : target;
+ exprAutoFnDetect = parens ? expr : "(typeof(" + target + ")==='function'?(" + target + ").call($item):(" + target + "))";
+ } else {
+ exprAutoFnDetect = expr = def.$1 || "null";
+ }
+ fnargs = unescape( fnargs );
+ return "');" +
+ tag[ slash ? "close" : "open" ]
+ .split( "$notnull_1" ).join( target ? "typeof(" + target + ")!=='undefined' && (" + target + ")!=null" : "true" )
+ .split( "$1a" ).join( exprAutoFnDetect )
+ .split( "$1" ).join( expr )
+ .split( "$2" ).join( fnargs ?
+ fnargs.replace( /\s*([^\(]+)\s*(\((.*?)\))?/g, function( all, name, parens, params ) {
+ params = params ? ("," + params + ")") : (parens ? ")" : "");
+ return params ? ("(" + name + ").call($item" + params) : all;
+ })
+ : (def.$2||"")
+ ) +
+ "_.push('";
+ }) +
+ "');}return _;"
+ );
+ }
+ function updateWrapped( options, wrapped ) {
+ // Build the wrapped content.
+ options._wrap = build( options, true,
+ // Suport imperative scenario in which options.wrapped can be set to a selector or an HTML string.
+ jQuery.isArray( wrapped ) ? wrapped : [htmlExpr.test( wrapped ) ? wrapped : jQuery( wrapped ).html()]
+ ).join("");
+ }
+
+ function unescape( args ) {
+ return args ? args.replace( /\\'/g, "'").replace(/\\\\/g, "\\" ) : null;
+ }
+ function outerHtml( elem ) {
+ var div = document.createElement("div");
+ div.appendChild( elem.cloneNode(true) );
+ return div.innerHTML;
+ }
+
+ // Store template items in jQuery.data(), ensuring a unique tmplItem data data structure for each rendered template instance.
+ function storeTmplItems( content ) {
+ var keySuffix = "_" + cloneIndex, elem, elems, newClonedItems = {}, i, l, m;
+ for ( i = 0, l = content.length; i < l; i++ ) {
+ if ( (elem = content[i]).nodeType !== 1 ) {
+ continue;
+ }
+ elems = elem.getElementsByTagName("*");
+ for ( m = elems.length - 1; m >= 0; m-- ) {
+ processItemKey( elems[m] );
+ }
+ processItemKey( elem );
+ }
+ function processItemKey( el ) {
+ var pntKey, pntNode = el, pntItem, tmplItem, key;
+ // Ensure that each rendered template inserted into the DOM has its own template item,
+ if ( (key = el.getAttribute( tmplItmAtt ))) {
+ while ( pntNode.parentNode && (pntNode = pntNode.parentNode).nodeType === 1 && !(pntKey = pntNode.getAttribute( tmplItmAtt ))) { }
+ if ( pntKey !== key ) {
+ // The next ancestor with a _tmplitem expando is on a different key than this one.
+ // So this is a top-level element within this template item
+ // Set pntNode to the key of the parentNode, or to 0 if pntNode.parentNode is null, or pntNode is a fragment.
+ pntNode = pntNode.parentNode ? (pntNode.nodeType === 11 ? 0 : (pntNode.getAttribute( tmplItmAtt ) || 0)) : 0;
+ if ( !(tmplItem = newTmplItems[key]) ) {
+ // The item is for wrapped content, and was copied from the temporary parent wrappedItem.
+ tmplItem = wrappedItems[key];
+ tmplItem = newTmplItem( tmplItem, newTmplItems[pntNode]||wrappedItems[pntNode], null, true );
+ tmplItem.key = ++itemKey;
+ newTmplItems[itemKey] = tmplItem;
+ }
+ if ( cloneIndex ) {
+ cloneTmplItem( key );
+ }
+ }
+ el.removeAttribute( tmplItmAtt );
+ } else if ( cloneIndex && (tmplItem = jQuery.data( el, "tmplItem" )) ) {
+ // This was a rendered element, cloned during append or appendTo etc.
+ // TmplItem stored in jQuery data has already been cloned in cloneCopyEvent. We must replace it with a fresh cloned tmplItem.
+ cloneTmplItem( tmplItem.key );
+ newTmplItems[tmplItem.key] = tmplItem;
+ pntNode = jQuery.data( el.parentNode, "tmplItem" );
+ pntNode = pntNode ? pntNode.key : 0;
+ }
+ if ( tmplItem ) {
+ pntItem = tmplItem;
+ // Find the template item of the parent element.
+ // (Using !=, not !==, since pntItem.key is number, and pntNode may be a string)
+ while ( pntItem && pntItem.key != pntNode ) {
+ // Add this element as a top-level node for this rendered template item, as well as for any
+ // ancestor items between this item and the item of its parent element
+ pntItem.nodes.push( el );
+ pntItem = pntItem.parent;
+ }
+ // Delete content built during rendering - reduce API surface area and memory use, and avoid exposing of stale data after rendering...
+ delete tmplItem._ctnt;
+ delete tmplItem._wrap;
+ // Store template item as jQuery data on the element
+ jQuery.data( el, "tmplItem", tmplItem );
+ }
+ function cloneTmplItem( key ) {
+ key = key + keySuffix;
+ tmplItem = newClonedItems[key] =
+ (newClonedItems[key] || newTmplItem( tmplItem, newTmplItems[tmplItem.parent.key + keySuffix] || tmplItem.parent, null, true ));
+ }
+ }
+ }
+
+ //---- Helper functions for template item ----
+
+ function tiCalls( content, tmpl, data, options ) {
+ if ( !content ) {
+ return stack.pop();
+ }
+ stack.push({ _: content, tmpl: tmpl, item:this, data: data, options: options });
+ }
+
+ function tiNest( tmpl, data, options ) {
+ // nested template, using {{tmpl}} tag
+ return jQuery.tmpl( jQuery.template( tmpl ), data, options, this );
+ }
+
+ function tiWrap( call, wrapped ) {
+ // nested template, using {{wrap}} tag
+ var options = call.options || {};
+ options.wrapped = wrapped;
+ // Apply the template, which may incorporate wrapped content,
+ return jQuery.tmpl( jQuery.template( call.tmpl ), call.data, options, call.item );
+ }
+
+ function tiHtml( filter, textOnly ) {
+ var wrapped = this._wrap;
+ return jQuery.map(
+ jQuery( jQuery.isArray( wrapped ) ? wrapped.join("") : wrapped ).filter( filter || "*" ),
+ function(e) {
+ return textOnly ?
+ e.innerText || e.textContent :
+ e.outerHTML || outerHtml(e);
+ });
+ }
+
+ function tiUpdate() {
+ var coll = this.nodes;
+ jQuery.tmpl( null, null, null, this).insertBefore( coll[0] );
+ jQuery( coll ).remove();
+ }
+})( jQuery );
394 lib/spine.js
@@ -0,0 +1,394 @@
+(function($){
+
+ var Spine;
+ if (typeof exports !== 'undefined') {
+ Spine = exports;
+ } else {
+ Spine = this.Spine = {};
+ }
+
+ // PubSub
+
+ var PubSub = Spine.PubSub = {
+ setup: function(){
+ this.o = $({});
+ },
+
+ subscribe: function() {
+ this.o.bind.apply( this.o, arguments );
+ },
+
+ publish: function() {
+ this.o.trigger.apply( this.o, arguments );
+ }
+ };
+
+ // Classes (or prototypial inheritors)
+
+ if (typeof Object.create !== "function")
+ Object.create = function(o) {
+ function F() {}
+ F.prototype = o;
+ return new F();
+ };
+
+ var Klass = Spine.Klass = {
+ init: function(){},
+
+ prototype: {
+ init: function(){}
+ },
+
+ create: function(){
+ var object = Object.create(this);
+ object.parent = this;
+ object.init.apply(object, arguments);
+ return object;
+ },
+
+ inst: function(){
+ var instance = Object.create(this.prototype);
+ instance.parent = this;
+ instance.init.apply(instance, arguments);
+ return instance;
+ },
+
+ proxy: function(func){
+ var thisObject = this;
+ return(function(){
+ return func.apply(thisObject, arguments);
+ });
+ },
+
+ include: function(obj){
+ var included = obj.included || obj.setup;
+
+ delete obj.included;
+ delete obj.extended;
+ delete obj.setup;
+
+ for(var i in obj)
+ this.fn[i] = obj[i];
+ if (included) included.apply(this);
+ },
+
+ extend: function(obj){
+ var extended = obj.extended || obj.setup;
+
+ delete obj.included;
+ delete obj.extended;
+ delete obj.setup;
+
+ for(var i in obj)
+ this[i] = obj[i];
+ if (extended) extended.apply(this);
+ delete extended;
+ }
+ };
+
+ Klass.fn = Klass.prototype;
+ Klass.fn.proxy = Klass.proxy;
+
+ // Models
+
+ Spine.guid = function(){
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+ var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
+ return v.toString(16);
+ }).toUpperCase();
+ };
+
+ var Model = Spine.Model = Klass.create();
+
+ Model.extend(PubSub);
+
+ // Alias create
+ Model.createSub = Model.create;
+ Model.setup = function(name, atts){
+ var model = Model.createSub();
+ if (name) model.name = name;
+ if (atts) model.attributes = atts;
+ return model;
+ };
+
+ Model.extend({
+ init: function(){
+ this.records = {};
+ this.attributes = [];
+ },
+
+ find: function(id){
+ var record = this.records[id];
+ if ( !record ) throw("Unknown record");
+ return record.dup();
+ },
+
+ exists: function(id){
+ try {
+ return this.find(id);
+ } catch (e) {
+ return false;
+ }
+ },
+
+ populate: function(values){
+ // Reset model & records
+ this.records = {};
+
+ for (var i=0, il = values.length; i < il; i++) {
+ var record = this.inst(values[i]);
+ record.newRecord = false;
+ this.records[record.id] = record;
+ }
+ },
+
+ select: function(callback){
+ var result = [];
+
+ for (var key in this.records)
+ if (callback(this.records[key]))
+ result.push(this.records[key]);
+
+ return this.dupArray(result);
+ },
+
+ findByAttribute: function(name, value){
+ for (var key in this.records)
+ if (this.records[key][name] == value)
+ return this.records[key].dup();
+ },
+
+ findAllByAttribute: function(name, value){
+ return(this.select(function(item){
+ return(item[name] == value);
+ }));
+ },
+
+ each: function(callback){
+ for (var key in this.records) {
+ callback(this.records[key]);
+ }
+ },
+
+ all: function(){
+ return this.dupArray(this.recordsValues());
+ },
+
+ first: function(){
+ var record = this.recordsValues()[0];
+ return(record && record.dup());
+ },
+
+ last: function(){
+ var values = this.recordsValues()
+ var record = values[values.length - 1];
+ return(record && record.dup());
+ },
+
+ count: function(){
+ return this.recordsValues().length;
+ },
+
+ deleteAll: function(){
+ for (var key in this.records)
+ delete this.records[key];
+ },
+
+ destroyAll: function(){
+ for (var key in this.records)
+ this.records[key].destroy();
+ },
+
+ update: function(id, atts){
+ this.find(id).updateAttributes(atts);
+ },
+
+ create: function(atts){
+ var record = this.inst(atts);
+ record.save();
+ return record;
+ },
+
+ destroy: function(id){
+ this.find(id).destroy();
+ },
+
+ // Private
+
+ recordsValues: function(){
+ var result = []
+ for (var key in this.records)
+ result.push(this.records[key])
+ return result;
+ },
+
+ dupArray: function(array){
+ return array.map(function(item){
+ return item.dup();
+ });
+ }
+ });
+
+ Model.extend({
+ extended: function() {
+ this.subscribe("create update destroy", this.proxy(function(e, record){
+ this.publish("change", record);
+ }));
+ },
+
+ change: function(callback){
+ if (typeof callback == "function")
+ this.subscribe("change", callback);
+ else
+ this.publish("change", callback);
+ }
+ });
+
+ Model.include({
+ newRecord: true,
+
+ init: function(atts){
+ if (atts) this.load(atts);
+ },
+
+ isNew: function(){
+ return this.newRecord;
+ },
+
+ validate: function(){ },
+
+ load: function(atts){
+ for(var name in atts)
+ this[name] = atts[name];
+ },
+
+ attributes: function(){
+ var result = {};
+ for(var i in this.parent.attributes) {
+ var attr = this.parent.attributes[i];
+ result[attr] = this[attr];
+ }
+ result.id = this.id;
+ return result;
+ },
+
+ eql: function(rec){
+ return(rec && rec.id === this.id &&
+ rec.parent === this.parent);
+ },
+
+ save: function(){
+ if (this.validate() == false) return false;
+ this.publish("beforeSave");
+ this.newRecord ? this.create() : this.update();
+ this.publish("afterSave");
+ this.publish("save");
+ },
+
+ updateAttribute: function(name, value){
+ this[name] = value;
+ return this.save();
+ },
+
+ updateAttributes: function(atts){
+ this.load(atts);
+ return this.save();
+ },
+
+ destroy: function(){
+ this.publish("beforeDestroy");
+ delete this.parent.records[this.id];
+ this.publish("afterDestroy");
+ this.publish("destroy");
+ },
+
+ dup: function(){
+ return Object.create(this);
+ },
+
+ reload: function(){
+ return(this.parent.find(this.id));
+ },
+
+ toJSON: function(){
+ return(this.attributes());
+ },
+
+ change: function(callback){
+ if (typeof callback == "function")
+ this.parent.change(this.proxy(function(e, record){
+ if (this.eql(record)) callback(record);
+ }));
+ else
+ this.parent.change(this);
+ },
+
+ // Private
+
+ update: function(){
+ this.publish("beforeUpdate");
+ this.parent.records[this.id] = this.dup();
+ this.publish("afterUpdate");
+ this.publish("update");
+ },
+
+ generateID: function(){
+ return Spine.guid();
+ },
+
+ create: function(){
+ this.publish("beforeCreate");
+ if ( !this.id ) this.id = this.generateID();
+ this.newRecord = false;
+ this.parent.records[this.id] = this.dup();
+ this.publish("afterCreate");
+ this.publish("create");
+ },
+
+ publish: function(channel){
+ this.parent.publish(channel, this);
+ }
+ });
+
+ // Controllers
+
+ var Controller = Spine.Controller = function(selector){
+ this.load(function(){
+ if (selector) this.el = $(selector);
+ if (this.el && this.events) this.delegateEvents();
+ });
+ };
+
+ Controller.fn = Controller.prototype;
+
+ Controller.fn.proxy = function(func){
+ return $.proxy(func, this);
+ };
+
+ Controller.fn.load = function(func){
+ $(this.proxy(func));
+ };
+
+ Controller.fn.extend = function(ob){
+ $.extend(this, ob);
+ };
+
+ var eventSplitter = /^(\w+)\s*(.*)$/;
+
+ Controller.fn.delegateEvents = function(){
+ for (var key in this.events) {
+ var methodName = this.events[key];
+ var method = this.proxy(this[methodName]);
+
+ var match = key.match(eventSplitter);
+ var eventName = match[1], selector = match[2];
+
+ if (selector === '') {
+ this.el.bind(eventName, method);
+ } else {
+ this.el.delegate(selector, eventName, method);
+ }
+ }
+ };
+
+})(jQuery);
18 lib/spine.model.local.js
@@ -0,0 +1,18 @@
+Spine.Model.Local = {
+ extended: function(){
+ this.loadLocal();
+ jQuery(window).unload(this.proxy(function(){ this.saveLocal(); }))
+ },
+
+ saveLocal: function(){
+ var result = JSON.stringify(this.recordsValues());
+ localStorage[this.name] = result;
+ },
+
+ loadLocal: function(){
+ var result = localStorage[this.name];
+ if ( !result ) return;
+ var result = JSON.parse(result);
+ this.populate(result);
+ }
+};
Please sign in to comment.
Something went wrong with that request. Please try again.