Commit e7ae5b55 authored by Sindre Sorhus's avatar Sindre Sorhus

Close #127: Spine rewrite. Fixes #70

parents ae5baf13 2d644b75
{print} = require 'util'
{spawn} = require 'child_process'
task 'build', 'Build js/ from src/', ->
coffee = spawn 'coffee', ['-c', '-o', 'js', 'src']
coffee.stderr.on 'data', (data) ->
process.stderr.write data.toString()
coffee.stdout.on 'data', (data) ->
print data.toString()
coffee.on 'exit', (code) ->
callback?() if code is 0
task 'watch', 'Watch src/ for changes', ->
coffee = spawn 'coffee', ['-w', '-c', '-o', 'js', 'src']
coffee.stderr.on 'data', (data) ->
process.stderr.write data.toString()
coffee.stdout.on 'data', (data) ->
print data.toString()
\ No newline at end of file
# Spine.js TodoMVC app
## Getting started
You need [CoffeScript](http://coffeescript.org) to compile if you make changes to the files in the `src` folder.
### Compile
Open Terminal in this folder.
- `cake build` to compile once
- `cake watch` to compile on save
\ No newline at end of file
html,
body {
margin: 0;
padding: 0;
}
body {
font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #eeeeee;
color: #333333;
width: 520px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
}
#todoapp {
background: #fff;
padding: 20px;
margin-bottom: 40px;
-webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
-moz-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
-ms-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;
-webkit-border-radius: 0 0 5px 5px;
-moz-border-radius: 0 0 5px 5px;
-ms-border-radius: 0 0 5px 5px;
-o-border-radius: 0 0 5px 5px;
border-radius: 0 0 5px 5px;
}
#todoapp h1 {
font-size: 36px;
font-weight: bold;
text-align: center;
padding: 0 0 10px 0;
}
#todoapp 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;
-webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
-moz-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
-ms-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;
}
#todoapp input::-webkit-input-placeholder {
font-style: italic;
}
#main {
display: none;
}
#todo-list {
margin: 10px 0;
padding: 0;
list-style: none;
}
#todo-list li {
padding: 18px 20px 18px 0;
position: relative;
font-size: 24px;
border-bottom: 1px solid #cccccc;
}
#todo-list li:last-child {
border-bottom: none;
}
#todo-list li.done label {
color: #777777;
text-decoration: line-through;
}
#todo-list li .destroy {
display: none;
position: absolute;
top: 20px;
right: 10px;
cursor: pointer;
width: 20px;
height: 20px;
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUBAMAAAB/pwA+AAAABGdBTUEAALGPC/xhBQAAACdQTFRFzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMAAAA////zMzMhnu0WAAAAAt0Uk5T5u3pqtV3jFQEKAC0bVelAAAAfUlEQVQI12NYtWpFsc8R865VqxhWrZpyBgg8QcylZ8AgCsjMgTCPrWJYfgYKqhjWwJgaDDVnzpw+c2bPmTPHGWzOnNm95/TuM2cOM/AARXfvBooeZAAp270bRCIz4QoOIGtDMqwJZoUEQzvCYrhzuhhWtUKYEahOX7UK6iEA3A6NUGwCTZIAAAAASUVORK5CYII=') no-repeat center center;
}
#todo-list li:hover .destroy {
display: block;
}
#todo-list li.editing {
border-bottom: none;
margin-top: -1px;
padding: 0;
}
#todo-list li.editing:last-child {
margin-bottom: -1px;
}
#todo-list li.editing .edit {
display: block;
width: 444px;
padding: 13px 15px 14px 20px;
margin: 0;
}
#todo-list li.editing .view {
display: none;
}
#todo-list li .view label {
word-break: break-word;
}
#todo-list li .edit {
display: none;
}
#todoapp footer {
display: none;
margin: 0 -20px -20px -20px;
overflow: hidden;
color: #555555;
background: #f4fce8;
border-top: 1px solid #ededed;
padding: 0 20px;
line-height: 37px;
-webkit-border-radius: 0 0 5px 5px;
-moz-border-radius: 0 0 5px 5px;
-ms-border-radius: 0 0 5px 5px;
-o-border-radius: 0 0 5px 5px;
border-radius: 0 0 5px 5px;
}
#clear-completed {
display: none;
float: right;
line-height: 20px;
text-decoration: none;
background: rgba(0, 0, 0, 0.1);
color: #555555;
font-size: 11px;
margin-top: 8px;
margin-bottom: 8px;
padding: 0 10px 1px;
cursor: pointer;
-webkit-border-radius: 12px;
-moz-border-radius: 12px;
-ms-border-radius: 12px;
-o-border-radius: 12px;
border-radius: 12px;
-webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
-moz-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
-ms-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;
}
#clear-completed:hover {
background: rgba(0, 0, 0, 0.15);
-webkit-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
-moz-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
-ms-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;
}
#clear-completed:active {
position: relative;
top: 1px;
}
#todo-count span {
font-weight: bold;
}
#instructions {
margin: 10px auto;
color: #777777;
text-shadow: rgba(255, 255, 255, 0.8) 0 1px 0;
text-align: center;
}
#instructions a {
color: #336699;
}
#credits {
margin: 30px auto;
color: #999;
text-shadow: rgba(255, 255, 255, 0.8) 0 1px 0;
text-align: center;
}
#credits a {
color: #888;
}
\ No newline at end of file
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;
list-style: none;
}
#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;
margin-bottom: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;
}
/* focus is actually pointless here as it's not
possible to tab to the anchor for some reason */
#tasks .clear:hover, #tasks .clear:focus {
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;
}
#credits {
width: 520px;
margin: 30px auto;
color: #999;
text-shadow: rgba(255, 255, 255, 0.8) 0 1px 0;
text-align: center;
}
#credits a {
color: #888;
}
<!DOCTYPE html> <!doctype html>
<html> <html lang="en">
<head> <head>
<title>Spine.js</title> <meta charset="utf-8">
<link rel="stylesheet" href="css/application.css" type="text/css" charset="utf-8"> <title>Spine.js • TodoMVC</title>
<link rel="stylesheet" href="../../assets/base.css">
<script src="lib/jquery.min.js" type="text/javascript" charset="utf-8"></script>
<script src="lib/jquery.tmpl.min.js" type="text/javascript" charset="utf-8"></script>
<script src="lib/spine.js" type="text/javascript" charset="utf-8"></script>
<script src="lib/local.js" type="text/javascript" charset="utf-8"></script>
<script src="js/controllers/tasks.js" type="text/javascript" charset="utf-8"></script>
<script src="js/models/task.js" type="text/javascript" charset="utf-8"></script>
<script src="js/app.js" type="text/javascript" charset="utf-8"></script>
</head> </head>
<body> <body>
<div id="views"> <section id="todoapp">
<div id="tasks"> <header id="header">
<h1>Todos</h1> <h1>Todos</h1>
<input id="new-todo" placeholder="What needs to be done?">
<form id="new-task"> </header>
<input name="name" type="text" placeholder="What needs to be done?"> <section id="main">
</form> <input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<div class="items"> <ul id="todo-list"></ul>
<script type="text/html" id="task-template"> </section>
<div class="item {{if done}}done{{/if}}"> <footer id="footer">
<div class="view" title="Double click to edit..."> <span id="todo-count"><strong>1</strong> item left</span>
<input type="checkbox" {{if done}}checked="checked"{{/if}}> <ul id="filters">
<span>${name}</span> <a class="destroy"></a> <li>
</div> <a class="selected" href="#/">All</a>
</li>
<form class="edit"> <li>
<input type="text" name="name" value="${name}"> <a href="#/active">Active</a>
</form> </li>
</div> <li>
</script> <a href="#/completed">Completed</a>
</div> </li>
</ul>
<footer> <button id="clear-completed">Clear completed</button>
<a class="clear">Clear completed</a>
<div class="count"><span class="countVal"></span> left</div>
</footer> </footer>
</section>
<footer id="info">
<p>Double-click to edit a todo</p>
<!-- delete this ↓ -->
<p>Inspired by the official <a href="https://github.com/maccman/spine.todos">Spine.Todos</a></p>
<!-- change this out with your name and url ↓ -->
<p>Created by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
</footer>
<script type="text/x-handlebars-template" id="todo-template">
{{#this}}
<li {{#if completed}}class="completed"{{/if}}>
<div class="view">
<input class="toggle" type="checkbox" {{#if completed}}checked{{/if}}>
<label>{{title}}</label>
<button class="destroy"></button>
</div> </div>
</div> <input class="edit" value="{{title}}">
</li>
<div id="credits"> {{/this}}
Based on the official <a href="https://github.com/maccman/spine.todos">Spine.Todos</a>. </script>
</div> <script src="../../assets/jquery.min.js"></script>
<script src="../../assets/handlebars.min.js"></script>
<script src="js/lib/spine.min.js"></script>
<script src="js/lib/route.min.js"></script>
<script src="js/lib/local.js"></script>
<script src="js/controllers/todos.js"></script>
<script src="js/models/todo.js"></script>
<script src="js/app.js"></script>
</body> </body>
</html> </html>
\ No newline at end of file
(function() { (function() {
var TaskApp; var TodoApp,
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; }; __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
__hasProp = Object.prototype.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; };
TaskApp = (function() { TodoApp = (function(_super) {
var ENTER_KEY;
__extends(TaskApp, Spine.Controller); __extends(TodoApp, _super);
TaskApp.prototype.elements = { ENTER_KEY = 13;
'.items': 'tasks',
'.countVal': 'counter', TodoApp.prototype.elements = {
'a.clear': 'clearCompleted', '#new-todo': 'newTodoInput',
'form#new-task input[name=name]': 'newTaskName' '#toggle-all': 'toggleAllElem',
'#main': 'main',
'#todo-list': 'todos',
'#footer': 'footer',
'#todo-count': 'count',
'#filters a': 'filters',
'#clear-completed': 'clearCompleted'
}; };
TaskApp.prototype.events = { TodoApp.prototype.events = {
'submit form#new-task': 'new', 'keyup #new-todo': 'new',
'click a.clear': 'clearCompleted' 'click #toggle-all': 'toggleAll',
'click #clear-completed': 'clearCompleted'
}; };
function TaskApp() { function TodoApp() {
this.toggleClearCompleted = __bind(this.toggleClearCompleted, this); this.renderFooter = __bind(this.renderFooter, this);
this.renderCounter = __bind(this.renderCounter, this); this.toggleElems = __bind(this.toggleElems, this);
this.renderAll = __bind(this.renderAll, this); this.addAll = __bind(this.addAll, this);
this.renderNew = __bind(this.renderNew, this); TaskApp.__super__.constructor.apply(this, arguments); this.addNew = __bind(this.addNew, this); TodoApp.__super__.constructor.apply(this, arguments);
Task.bind('create', this.renderNew); Todo.bind('create', this.addNew);
Task.bind('refresh', this.renderAll); Todo.bind('refresh', this.addAll);
Task.bind('refresh change', this.renderCounter); Todo.bind('refresh change', this.toggleElems);
Task.bind('refresh change', this.toggleClearCompleted); Todo.bind('refresh change', this.renderFooter);
Task.fetch(); Todo.fetch();
this.routes({
'/:filter': function(param) {
this.filter = param.filter;
/*
TODO: Need to figure out why the route doesn't trigger `change` event
*/
Todo.trigger('refresh');
return this.filters.removeClass('selected').filter("[href='#/" + this.filter + "']").addClass('selected');
}
});
}
TodoApp.prototype["new"] = function(e) {
var val;
val = $.trim(this.newTodoInput.val());
if (e.which === ENTER_KEY && val) {
Todo.create({
title: val
});
return this.newTodoInput.val('');
} }
};
TaskApp.prototype["new"] = function(e) { TodoApp.prototype.getByFilter = function() {
e.preventDefault(); switch (this.filter) {
Task.fromForm('form#new-task').save(); case 'active':
return this.newTaskName.val(''); return Todo.active();
case 'completed':
return Todo.completed();
default:
return Todo.all();
}
}; };
TaskApp.prototype.renderNew = function(task) { TodoApp.prototype.addNew = function(todo) {
var view; var view;
view = new Tasks({ view = new Todos({
task: task todo: todo
}); });
return this.tasks.append(view.render().el); return this.todos.append(view.render().el);
}; };
TaskApp.prototype.renderAll = function() { TodoApp.prototype.addAll = function() {
return Task.each(this.renderNew); var todo, _i, _len, _ref, _results;
this.todos.empty();
_ref = this.getByFilter();
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
todo = _ref[_i];
_results.push(this.addNew(todo));
}
return _results;
}; };
TaskApp.prototype.renderCounter = function() { TodoApp.prototype.toggleAll = function(e) {
return this.counter.text(Task.active().length); return Todo.each(function(todo) {
/*
TODO: Model updateAttribute sometimes won't stick:
https://github.com/maccman/spine/issues/219
*/ todo.updateAttribute('completed', e.target.checked);
return todo.trigger('update', todo);
});
}; };
TaskApp.prototype.toggleClearCompleted = function() { TodoApp.prototype.clearCompleted = function() {
if (Task.done().length) { return Todo.destroyCompleted();
return this.clearCompleted.show(); };
} else {
return this.clearCompleted.hide(); TodoApp.prototype.toggleElems = function() {
var isTodos;
isTodos = !!Todo.count();
this.main.toggle(isTodos);
this.footer.toggle(isTodos);
this.clearCompleted.toggle(!!Todo.completed().length);
if (!Todo.completed().length) {
return this.toggleAllElem.removeAttr('checked');
} }
}; };
TaskApp.prototype.clearCompleted = function() { TodoApp.prototype.renderFooter = function() {
return Task.destroyDone(); var active, completed, text;
text = function(count) {
if (count === 1) {
return 'item';
} else {
return 'items';
}
};
active = Todo.active().length;
completed = Todo.completed().length;
this.count.html("<b>" + active + "</b> " + (text(active)) + " left");
return this.clearCompleted.text("Clear completed (" + completed + ")");
}; };
return TaskApp; return TodoApp;
})(); })(Spine.Controller);
$(function() { $(function() {
return new TaskApp({ new TodoApp({
el: $('#tasks') el: $('#todoapp')
}); });
return Spine.Route.setup();
}); });
}).call(this); }).call(this);
(function() {
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; };
window.Tasks = (function() {
var ENTER_KEY, ESCAPE_KEY;
__extends(Tasks, Spine.Controller);
ENTER_KEY = 13;
ESCAPE_KEY = 27;
Tasks.prototype.elements = {
'form.edit': 'form'
};
Tasks.prototype.events = {
'click a.destroy': 'remove',
'click input[type=checkbox]': 'toggleStatus',
'dblclick .view': 'edit',
'keypress input[type=text]': 'finishEditOnEnter',
'blur input[type=text]': 'finishEdit'
};
function Tasks() {
this.render = __bind(this.render, this); Tasks.__super__.constructor.apply(this, arguments);
this.task.bind('update', this.render);
this.task.bind('destroy', this.release);
}
Tasks.prototype.render = function() {
this.replace($('#task-template').tmpl(this.task));
return this;
};
Tasks.prototype.remove = function() {
return this.task.destroy();
};
Tasks.prototype.toggleStatus = function() {
return this.task.updateAttribute('done', !this.task.done);
};
Tasks.prototype.edit = function() {
this.el.addClass('editing');
return this.$('input[name=name]').focus();
};
Tasks.prototype.finishEdit = function() {
this.el.removeClass('editing');
return this.task.fromForm(this.form).save();
};
Tasks.prototype.finishEditOnEnter = function(e) {
var _ref;
if ((_ref = e.keyCode) === ENTER_KEY || _ref === ESCAPE_KEY) {
return this.finishEdit();
}
};
return Tasks;
})();
}).call(this);
(function() {
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
__hasProp = Object.prototype.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; };
window.Todos = (function(_super) {
var ENTER_KEY, TPL;
__extends(Todos, _super);
ENTER_KEY = 13;
TPL = Handlebars.compile($('#todo-template').html());
Todos.prototype.elements = {
'.edit': 'editElem'
};
Todos.prototype.events = {
'click .destroy': 'remove',
'click .toggle': 'toggleStatus',
'dblclick .view': 'edit',
'keyup .edit': 'finishEditOnEnter',
'blur .edit': 'finishEdit'
};
function Todos() {
this.render = __bind(this.render, this); Todos.__super__.constructor.apply(this, arguments);
this.todo.bind('update', this.render);
this.todo.bind('destroy', this.release);
}
Todos.prototype.render = function() {
this.replace(TPL(this.todo));
return this;
};
Todos.prototype.remove = function() {
return this.todo.destroy();
};
Todos.prototype.toggleStatus = function() {
return this.todo.updateAttribute('completed', !this.todo.completed);
};
Todos.prototype.edit = function() {
this.el.addClass('editing');
return this.editElem.focus();
};
Todos.prototype.finishEdit = function() {
var val;
this.el.removeClass('editing');
val = $.trim(this.editElem.val());
if (val) {
return this.todo.updateAttribute('title', val);
} else {
return this.remove();
}
};
Todos.prototype.finishEditOnEnter = function(e) {
if (e.which === ENTER_KEY) return this.finishEdit();
};
return Todos;
})(Spine.Controller);
}).call(this);
(function() {
if (typeof Spine === "undefined" || Spine === null) Spine = require('spine');
var NS = 'spine';
Spine.Model.Local = {
extended: function() {
this.change(this.saveLocal);
return this.fetch(this.loadLocal);
},
saveLocal: function() {
var result;
result = JSON.stringify(this);
return localStorage[NS + '-' + this.className] = result;
},
loadLocal: function() {
var result;
result = localStorage[NS + '-' + this.className];
return this.refresh(result || [], {
clear: true
});
}
};
if (typeof module !== "undefined" && module !== null) {
module.exports = Spine.Model.Local;
}
}).call(this);
\ No newline at end of file
(function(){var f,i,j,h,k,l=Object.prototype.hasOwnProperty,m=function(d,b){function e(){this.constructor=d}for(var a in b)l.call(b,a)&&(d[a]=b[a]);e.prototype=b.prototype;d.prototype=new e;d.__super__=b.prototype;return d},n=Array.prototype.slice;if("undefined"===typeof Spine||null===Spine)Spine=require("spine");f=Spine.$;j=/^#*/;h=/:([\w\d]+)/g;k=/\*([\w\d]+)/g;i=/[-[\]{}()+?.,\\^$|#\s]/g;Spine.Route=function(d){function b(a,b){var c;this.path=a;this.callback=b;this.names=[];if("string"===typeof a){for(h.lastIndex=
0;null!==(c=h.exec(a));)this.names.push(c[1]);a=a.replace(i,"\\$&").replace(h,"([^/]*)").replace(k,"(.*?)");this.route=RegExp("^"+a+"$")}else this.route=a}var e;m(b,d);b.extend(Spine.Events);b.historySupport=null!=(null!=(e=window.history)?e.pushState:void 0);b.routes=[];b.options={trigger:!0,history:!1,shim:!1};b.add=function(a,b){var c,d,g;if("object"===typeof a&&!(a instanceof RegExp)){g=[];for(c in a)d=a[c],g.push(this.add(c,d));return g}return this.routes.push(new this(a,b))};b.setup=function(a){null==
a&&(a={});this.options=f.extend({},this.options,a);this.options.history&&(this.history=this.historySupport&&this.options.history);if(!this.options.shim)return this.history?f(window).bind("popstate",this.change):f(window).bind("hashchange",this.change),this.change()};b.unbind=function(){return this.history?f(window).unbind("popstate",this.change):f(window).unbind("hashchange",this.change)};b.navigate=function(){var a,b,c;a=1<=arguments.length?n.call(arguments,0):[];c={};b=a[a.length-1];"object"===
typeof b?c=a.pop():"boolean"===typeof b&&(c.trigger=a.pop());c=f.extend({},this.options,c);a=a.join("/");if(this.path!==a&&(this.path=a,this.trigger("navigate",this.path),c.trigger&&this.matchRoute(this.path,c),!c.shim))return this.history?history.pushState({},document.title,this.path):window.location.hash=this.path};b.getPath=function(){var a;a=window.location.pathname;"/"!==a.substr(0,1)&&(a="/"+a);return a};b.getHash=function(){return window.location.hash};b.getFragment=function(){return this.getHash().replace(j,
"")};b.getHost=function(){return(document.location+"").replace(this.getPath()+this.getHash(),"")};b.change=function(){var a;a=""!==this.getFragment()?this.getFragment():this.getPath();if(a!==this.path)return this.path=a,this.matchRoute(this.path)};b.matchRoute=function(a,b){var c,d,g,e;e=this.routes;d=0;for(g=e.length;d<g;d++)if(c=e[d],c.match(a,b))return this.trigger("change",c,a),c};b.prototype.match=function(a,b){var c,d,e,f;null==b&&(b={});c=this.route.exec(a);if(!c)return!1;b.match=c;e=c.slice(1);
if(this.names.length){c=0;for(f=e.length;c<f;c++)d=e[c],b[this.names[c]]=d}return!1!==this.callback.call(null,b)};return b}(Spine.Module);Spine.Route.change=Spine.Route.proxy(Spine.Route.change);Spine.Controller.include({route:function(d,b){return Spine.Route.add(d,this.proxy(b))},routes:function(d){var b,e,a;a=[];for(b in d)e=d[b],a.push(this.route(b,e));return a},navigate:function(){return Spine.Route.navigate.apply(Spine.Route,arguments)}});"undefined"!==typeof module&&null!==module&&(module.exports=
Spine.Route)}).call(this);
\ No newline at end of file
This diff is collapsed.
(function() {
var __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; };
window.Task = (function() {
__extends(Task, Spine.Model);
function Task() {
Task.__super__.constructor.apply(this, arguments);
}
Task.configure('Task', 'name', 'done');
Task.extend(Spine.Model.Local);
Task.prototype.validate = function() {
if (!$.trim(this.name)) return 'Task name is required';
};
Task.active = function() {
return this.select(function(task) {
return !task.done;
});
};
Task.done = function() {
return this.select(function(task) {
return !!task.done;
});
};
Task.destroyDone = function() {
var task, _i, _len, _ref, _results;
_ref = this.done();
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
task = _ref[_i];
_results.push(task.destroy());
}
return _results;
};
return Task;
})();
}).call(this);
(function() {
var __hasProp = Object.prototype.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; };
window.Todo = (function(_super) {
__extends(Todo, _super);
function Todo() {
Todo.__super__.constructor.apply(this, arguments);
}
Todo.configure('Todo', 'title', 'completed');
Todo.extend(Spine.Model.Local);
Todo.active = function() {
return this.select(function(todo) {
return !todo.completed;
});
};
Todo.completed = function() {
return this.select(function(todo) {
return !!todo.completed;
});
};
Todo.destroyCompleted = function() {
var todo, _i, _len, _ref, _results;
_ref = this.completed();
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
todo = _ref[_i];
_results.push(todo.destroy());
}
return _results;
};
return Todo;
})(Spine.Model);
}).call(this);
This diff is collapsed.
/*
* jQuery Templates Plugin 1.0.0pre
* http://github.com/jquery/jquery-tmpl
* Requires jQuery 1.4.2
*
* Copyright Software Freedom Conservancy, Inc.
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*/
(function(a){var r=a.fn.domManip,d="_tmplitem",q=/^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /,b={},f={},e,p={key:0,data:{}},i=0,c=0,l=[];function g(g,d,h,e){var c={data:e||(e===0||e===false)?e:d?d.data:{},_wrap:d?d._wrap:null,tmpl:null,parent:d||null,nodes:[],calls:u,nest:w,wrap:x,html:v,update:t};g&&a.extend(c,g,{nodes:[],parent:d});if(h){c.tmpl=h;c._ctnt=c._ctnt||c.tmpl(a,c);c.key=++i;(l.length?f:b)[i]=c}return c}a.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(f,d){a.fn[f]=function(n){var g=[],i=a(n),k,h,m,l,j=this.length===1&&this[0].parentNode;e=b||{};if(j&&j.nodeType===11&&j.childNodes.length===1&&i.length===1){i[d](this[0]);g=this}else{for(h=0,m=i.length;h<m;h++){c=h;k=(h>0?this.clone(true):this).get();a(i[h])[d](k);g=g.concat(k)}c=0;g=this.pushStack(g,f,i.selector)}l=e;e=null;a.tmpl.complete(l);return g}});a.fn.extend({tmpl:function(d,c,b){return a.tmpl(this[0],d,c,b)},tmplItem:function(){return a.tmplItem(this[0])},template:function(b){return a.template(b,this[0])},domManip:function(d,m,k){if(d[0]&&a.isArray(d[0])){var g=a.makeArray(arguments),h=d[0],j=h.length,i=0,f;while(i<j&&!(f=a.data(h[i++],"tmplItem")));if(f&&c)g[2]=function(b){a.tmpl.afterManip(this,b,k)};r.apply(this,g)}else r.apply(this,arguments);c=0;!e&&a.tmpl.complete(b);return this}});a.extend({tmpl:function(d,h,e,c){var i,k=!c;if(k){c=p;d=a.template[d]||a.template(null,d);f={}}else if(!d){d=c.tmpl;b[c.key]=c;c.nodes=[];c.wrapped&&n(c,c.wrapped);return a(j(c,null,c.tmpl(a,c)))}if(!d)return[];if(typeof h==="function")h=h.call(c||{});e&&e.wrapped&&n(e,e.wrapped);i=a.isArray(h)?a.map(h,function(a){return a?g(e,c,d,a):null}):[g(e,c,d,h)];return k?a(j(c,null,i)):i},tmplItem:function(b){var c;if(b instanceof a)b=b[0];while(b&&b.nodeType===1&&!(c=a.data(b,"tmplItem"))&&(b=b.parentNode));return c||p},template:function(c,b){if(b){if(typeof b==="string")b=o(b);else if(b instanceof a)b=b[0]||{};if(b.nodeType)b=a.data(b,"tmpl")||a.data(b,"tmpl",o(b.innerHTML));return typeof c==="string"?(a.template[c]=b):b}return c?typeof c!=="string"?a.template(null,c):a.template[c]||a.template(null,q.test(c)?c:a(c)):null},encode:function(a){return(""+a).split("<").join("&lt;").split(">").join("&gt;").split('"').join("&#34;").split("'").join("&#39;")}});a.extend(a.tmpl,{tag:{tmpl:{_default:{$2:"null"},open:"if($notnull_1){__=__.concat($item.nest($1,$2));}"},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:{open:"if($notnull_1){__.push($1a);}"},"=":{_default:{$1:"$data"},open:"if($notnull_1){__.push($.encode($1a));}"},"!":{open:""}},complete:function(){b={}},afterManip:function(f,b,d){var e=b.nodeType===11?a.makeArray(b.childNodes):b.nodeType===1?[b]:[];d.call(f,b);m(e);c++}});function j(e,g,f){var b,c=f?a.map(f,function(a){return typeof a==="string"?e.key?a.replace(/(<\w+)(?=[\s>])(?![^>]*_tmplitem)([^>]*)/g,"$1 "+d+'="'+e.key+'" $2'):a:j(a,e,a._ctnt)}):e;if(g)return c;c=c.join("");c.replace(/^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/,function(f,c,e,d){b=a(e).get();m(b);if(c)b=k(c).concat(b);if(d)b=b.concat(k(d))});return b?b:k(c)}function k(c){var b=document.createElement("div");b.innerHTML=c;return a.makeArray(b.childNodes)}function o(b){return new Function("jQuery","$item","var $=jQuery,call,__=[],$data=$item.data;with($data){__.push('"+a.trim(b).replace(/([\\'])/g,"\\$1").replace(/[\r\t\n]/g," ").replace(/\$\{([^\}]*)\}/g,"{{= $1}}").replace(/\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g,function(m,l,k,g,b,c,d){var j=a.tmpl.tag[k],i,e,f;if(!j)throw"Unknown template tag: "+k;i=j._default||[];if(c&&!/\w$/.test(b)){b+=c;c=""}if(b){b=h(b);d=d?","+h(d)+")":c?")":"";e=c?b.indexOf(".")>-1?b+h(c):"("+b+").call($item"+d:b;f=c?e:"(typeof("+b+")==='function'?("+b+").call($item):("+b+"))"}else f=e=i.$1||"null";g=h(g);return"');"+j[l?"close":"open"].split("$notnull_1").join(b?"typeof("+b+")!=='undefined' && ("+b+")!=null":"true").split("$1a").join(f).split("$1").join(e).split("$2").join(g||i.$2||"")+"__.push('"})+"');}return __;")}function n(c,b){c._wrap=j(c,true,a.isArray(b)?b:[q.test(b)?b:a(b).html()]).join("")}function h(a){return a?a.replace(/\\'/g,"'").replace(/\\\\/g,"\\"):null}function s(b){var a=document.createElement("div");a.appendChild(b.cloneNode(true));return a.innerHTML}function m(o){var n="_"+c,k,j,l={},e,p,h;for(e=0,p=o.length;e<p;e++){if((k=o[e]).nodeType!==1)continue;j=k.getElementsByTagName("*");for(h=j.length-1;h>=0;h--)m(j[h]);m(k)}function m(j){var p,h=j,k,e,m;if(m=j.getAttribute(d)){while(h.parentNode&&(h=h.parentNode).nodeType===1&&!(p=h.getAttribute(d)));if(p!==m){h=h.parentNode?h.nodeType===11?0:h.getAttribute(d)||0:0;if(!(e=b[m])){e=f[m];e=g(e,b[h]||f[h]);e.key=++i;b[i]=e}c&&o(m)}j.removeAttribute(d)}else if(c&&(e=a.data(j,"tmplItem"))){o(e.key);b[e.key]=e;h=a.data(j.parentNode,"tmplItem");h=h?h.key:0}if(e){k=e;while(k&&k.key!=h){k.nodes.push(j);k=k.parent}delete e._ctnt;delete e._wrap;a.data(j,"tmplItem",e)}function o(a){a=a+n;e=l[a]=l[a]||g(e,b[e.parent.key+n]||e.parent)}}}function u(a,d,c,b){if(!a)return l.pop();l.push({_:a,tmpl:d,item:this,data:c,options:b})}function w(d,c,b){return a.tmpl(a.template(d),c,b,this)}function x(b,d){var c=b.options||{};c.wrapped=d;return a.tmpl(a.template(b.tmpl),b.data,c,b.item)}function v(d,c){var b=this._wrap;return a.map(a(a.isArray(b)?b.join(""):b).filter(d||"*"),function(a){return c?a.innerText||a.textContent:a.outerHTML||s(a)})}function t(){var b=this.nodes;a.tmpl(null,null,null,this).insertBefore(b[0]);a(b).remove()}})(jQuery);
\ No newline at end of file
(function() {
var storageName = 'todos-spine';
if (typeof Spine === "undefined" || Spine === null) {
Spine = require('spine');
}
Spine.Model.Local = {
extended: function() {
this.change(this.saveLocal);
return this.fetch(this.loadLocal);
},
saveLocal: function() {
var result;
result = JSON.stringify(this);
return localStorage[storageName] = result;
},
loadLocal: function() {
var result;
result = localStorage[storageName];
return this.refresh(result || [], {
clear: true
});
}
};
if (typeof module !== "undefined" && module !== null) {
module.exports = Spine.Model.Local;
}
}).call(this);
This diff is collapsed.
class TaskApp extends Spine.Controller class TodoApp extends Spine.Controller
ENTER_KEY = 13
elements: elements:
'.items': 'tasks' '#new-todo': 'newTodoInput'
'.countVal': 'counter' '#toggle-all': 'toggleAllElem'
'a.clear': 'clearCompleted' '#main': 'main'
'form#new-task input[name=name]': 'newTaskName' '#todo-list': 'todos'
'#footer': 'footer'
'#todo-count': 'count'
'#filters a': 'filters'
'#clear-completed': 'clearCompleted'
events: events:
'submit form#new-task': 'new' 'keyup #new-todo': 'new'
'click a.clear': 'clearCompleted' 'click #toggle-all': 'toggleAll'
'click #clear-completed': 'clearCompleted'
constructor: -> constructor: ->
super super
Task.bind 'create', @renderNew Todo.bind 'create', @addNew
Task.bind 'refresh', @renderAll Todo.bind 'refresh', @addAll
Task.bind 'refresh change', @renderCounter Todo.bind 'refresh change', @toggleElems
Task.bind 'refresh change', @toggleClearCompleted Todo.bind 'refresh change', @renderFooter
Task.fetch() Todo.fetch()
@routes
'/:filter': (param) ->
@filter = param.filter
###
TODO: Need to figure out why the route doesn't trigger `change` event
###
Todo.trigger('refresh')
@filters.removeClass('selected')
.filter("[href='#/#{ @filter }']").addClass('selected');
new: (e) -> new: (e) ->
e.preventDefault() val = $.trim @newTodoInput.val()
Task.fromForm('form#new-task').save() if e.which is ENTER_KEY and val
@newTaskName.val('') Todo.create title: val
@newTodoInput.val ''
renderNew: (task) => getByFilter: ->
view = new Tasks(task: task) switch @filter
@tasks.append view.render().el when 'active'
Todo.active()
when 'completed'
Todo.completed()
else
Todo.all()
renderAll: => addNew: (todo) =>
Task.each @renderNew view = new Todos todo: todo
@todos.append view.render().el
renderCounter: => addAll: =>
@counter.text Task.active().length @todos.empty()
@addNew todo for todo in @getByFilter()
toggleClearCompleted: => toggleAll: (e) ->
if Task.done().length Todo.each (todo) ->
@clearCompleted.show() ###
else TODO: Model updateAttribute sometimes won't stick:
@clearCompleted.hide() https://github.com/maccman/spine/issues/219
###
todo.updateAttribute 'completed', e.target.checked
todo.trigger 'update', todo
clearCompleted: -> clearCompleted: ->
Task.destroyDone() Todo.destroyCompleted()
toggleElems: =>
isTodos = !!Todo.count()
@main.toggle isTodos
@footer.toggle isTodos
@clearCompleted.toggle !!Todo.completed().length
@toggleAllElem.removeAttr 'checked' if !Todo.completed().length
renderFooter: =>
text = (count) -> if count is 1 then 'item' else 'items'
active = Todo.active().length
completed = Todo.completed().length
@count.html "<b>#{ active }</b> #{ text active } left"
@clearCompleted.text "Clear completed (#{ completed })"
$ -> $ ->
new TaskApp(el: $('#tasks')) new TodoApp el: $('#todoapp')
Spine.Route.setup()
\ No newline at end of file
class window.Tasks extends Spine.Controller
ENTER_KEY = 13
ESCAPE_KEY = 27
elements:
'form.edit': 'form'
events:
'click a.destroy': 'remove'
'click input[type=checkbox]': 'toggleStatus'
'dblclick .view': 'edit'
'keypress input[type=text]': 'finishEditOnEnter'
'blur input[type=text]': 'finishEdit'
constructor: ->
super
@task.bind 'update', @render
@task.bind 'destroy', @release
render: =>
@replace $('#task-template').tmpl(@task)
this
remove: ->
@task.destroy()
toggleStatus: ->
@task.updateAttribute 'done', !@task.done
edit: ->
@el.addClass('editing')
@$('input[name=name]').focus()
finishEdit: ->
@el.removeClass('editing')
@task.fromForm(@form).save()
finishEditOnEnter: (e) ->
if e.keyCode in [ENTER_KEY, ESCAPE_KEY] then @finishEdit()
class window.Todos extends Spine.Controller
ENTER_KEY = 13
TPL = Handlebars.compile $('#todo-template').html()
elements:
'.edit': 'editElem'
events:
'click .destroy': 'remove'
'click .toggle': 'toggleStatus'
'dblclick .view': 'edit'
'keyup .edit': 'finishEditOnEnter'
'blur .edit': 'finishEdit'
constructor: ->
super
@todo.bind 'update', @render
@todo.bind 'destroy', @release
render: =>
@replace TPL( @todo )
@
remove: ->
@todo.destroy()
toggleStatus: ->
@todo.updateAttribute 'completed', !@todo.completed
edit: ->
@el.addClass 'editing'
@editElem.focus()
finishEdit: ->
@el.removeClass 'editing'
val = $.trim @editElem.val()
if val then @todo.updateAttribute( 'title', val ) else @remove()
finishEditOnEnter: (e) ->
@finishEdit() if e.which is ENTER_KEY
\ No newline at end of file
class window.Task extends Spine.Model
@configure 'Task', 'name', 'done'
@extend Spine.Model.Local
validate: ->
'Task name is required' unless $.trim(@name)
@active: ->
@select (task) -> !task.done
@done: ->
@select (task) -> !!task.done
@destroyDone: ->
task.destroy() for task in @done()
class window.Todo extends Spine.Model
@configure 'Todo', 'title', 'completed'
@extend Spine.Model.Local
@active: ->
@select (todo) -> !todo.completed
@completed: ->
@select (todo) -> !!todo.completed
@destroyCompleted: ->
todo.destroy() for todo in @completed()
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment