Commit b47026fd authored by Hay Kranen's avatar Hay Kranen Committed by Sindre Sorhus

Close GH-122: Added a Stapes.js example.

parent 90305011
html,
body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
color: inherit;
-webkit-appearance: none;
/*-moz-appearance: none;*/
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #eeeeee url('bg.png');
color: #4d4d4d;
width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
#todoapp {
background: #fff;
background: rgba(255, 255, 255, 0.9);
margin: 130px 0 40px 0;
border: 1px solid #ccc;
position: relative;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.15);
}
#todoapp:before {
content: '';
border-left: 1px solid #f5d6d6;
border-right: 1px solid #f5d6d6;
width: 2px;
position: absolute;
top: 0;
left: 40px;
height: 100%;
}
#todoapp h1 {
position: absolute;
top: -120px;
width: 100%;
font-size: 70px;
font-weight: bold;
text-align: center;
color: #b3b3b3;
color: rgba(255, 255, 255, 0.3);
text-shadow: -1px -1px rgba(0, 0, 0, 0.2);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
-ms-text-rendering: optimizeLegibility;
-o-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
#header {
padding-top: 15px;
border-radius: inherit;
}
#todoapp header:before {
content: '';
position: absolute;
top: 0;
right: 0;
left: 0;
height: 15px;
z-index: 2;
border-bottom: 1px solid #6c615c;
background: #8d7d77;
background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8)));
background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: -moz-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: -o-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: -ms-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');
border-radius: inherit;
}
#todoapp input::-webkit-input-placeholder {
font-style: italic;
}
#todoapp input:-moz-placeholder {
font-style: italic;
color: #a9a9a9;
}
#new-todo,
.edit {
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
line-height: 1.4em;
border: 0;
outline: none;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
#new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.02);
position: relative;
z-index: 2;
box-shadow: none;
}
#main {
position: relative;
z-index: 2;
border-top: 1px dotted #adadad;
}
label[for='toggle-all'] {
display: none;
}
#toggle-all {
position: absolute;
top: -42px;
left: 12px;
text-align: center;
-webkit-appearance: none;
/*-moz-appearance: none;*/
-ms-appearance: none;
-o-appearance: none;
appearance: none;
-webkit-transform: rotate(90deg);
/*-moz-transform: rotate(90deg);*/
-ms-transform: rotate(90deg);
/*-o-transform: rotate(90deg);*/
transform: rotate(90deg);
}
#toggle-all:before {
content: '»';
font-size: 28px;
color: #d9d9d9;
padding: 0 25px 7px;
}
#toggle-all:checked:before {
color: #737373;
}
/* Need this ugly hack, since only
WebKit supports styling of inputs */
@media screen and (-webkit-min-device-pixel-ratio:0) {
#toggle-all {
top: -52px;
left: -11px;
}
}
#todo-list {
margin: 0;
padding: 0;
list-style: none;
}
#todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px dotted #ccc;
}
#todo-list li:last-child {
border-bottom: none;
}
#todo-list li.editing {
border-bottom: none;
padding: 0;
}
#todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
margin: 0 0 0 43px;
}
#todo-list li.editing .view {
display: none;
}
#todo-list li .toggle {
text-align: center;
width: 35px;
-webkit-appearance: none;
/*-moz-appearance: none;*/
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
#todo-list li .toggle:after {
font-size: 18px;
content: '✔';
line-height: 40px;
font-size: 20px;
color: #d9d9d9;
text-shadow: 0 -1px 0 #bfbfbf;
}
#todo-list li .toggle:checked:after {
color: #85ada7;
text-shadow: 0 1px 0 #669991;
bottom: 1px;
position: relative;
}
#todo-list li label {
word-break: break-word;
margin: 20px 15px;
display: inline-block;
-webkit-transition: color 0.4s;
-moz-transition: color 0.4s;
-ms-transition: color 0.4s;
-o-transition: color 0.4s;
transition: color 0.4s;
}
#todo-list li.complete label {
color: #a9a9a9;
text-decoration: line-through;
}
#todo-list li .destroy {
display: none;
position: absolute;
top: 10px;
right: 10px;
width: 40px;
height: 40px;
font-size: 22px;
color: #a88a8a;
-webkit-transition: all 0.2s;
-moz-transition: all 0.2s;
-ms-transition: all 0.2s;
-o-transition: all 0.2s;
transition: all 0.2s;
}
#todo-list li .destroy:hover {
text-shadow: 0 0 1px #000,
0 0 10px rgba(199, 107, 107, 0.8);
-webkit-transform: scale(1.3);
-moz-transform: scale(1.3);
-ms-transform: scale(1.3);
-o-transform: scale(1.3);
transform: scale(1.3);
}
#todo-list li .destroy:after {
content: '✖';
}
#todo-list li:hover .destroy {
display: block;
}
#todo-list li .edit {
display: none;
}
#todo-list li.editing:last-child {
margin-bottom: -1px;
}
#footer {
color: #777;
padding: 0 15px;
position: absolute;
right: 0;
bottom: -31px;
left: 0;
z-index: 1;
text-align: center;
}
#footer:before {
content: '';
position: absolute;
right: 0;
bottom: 31px;
left: 0;
height: 100px;
z-index: -1;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3),
0 6px 0 -3px rgba(255, 255, 255, 0.8),
0 7px 1px -3px rgba(0, 0, 0, 0.3),
0 42px 0 -6px rgba(255, 255, 255, 0.8),
0 43px 2px -6px rgba(0, 0, 0, 0.2);
}
#todo-count {
float: left;
text-align: left;
}
#filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
#filters li {
display: inline;
}
#filters li a {
color: #83756f;
margin: 2px;
text-decoration: none;
}
#filters li a.selected {
font-weight: bold;
}
#clear-completed {
float: right;
line-height: 20px;
text-decoration: none;
background: rgba(0, 0, 0, 0.1);
font-size: 11px;
padding: 0 10px;
position: relative;
border-radius: 3px;
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2);
}
#clear-completed:hover {
background: rgba(0, 0, 0, 0.15);
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3);
}
#info {
margin: 65px auto 0;
color: #a6a6a6;
font-size: 12px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);
text-align: center;
}
#info a {
color: inherit;
}
\ No newline at end of file
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Stapes.js • TodoMVC</title> <title>Stapes.js • TodoMVC</title>
<link rel="stylesheet" href="css/base.css"> <link rel="stylesheet" href="../../../assets/base.css">
</head> </head>
<body> <body>
<section id="todoapp"> <section id="todoapp">
...@@ -11,16 +11,13 @@ ...@@ -11,16 +11,13 @@
<h1>todos</h1> <h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?" autofocus> <input id="new-todo" placeholder="What needs to be done?" autofocus>
</header> </header>
<!-- this section should hidden by default and shown when there are todos -->
<section id="main"> <section id="main">
<input id="toggle-all" type="checkbox"> <input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label> <label for="toggle-all">Mark all as complete</label>
<ul id="todo-list"></ul> <ul id="todo-list"></ul>
</section> </section>
<!-- this footer should hidden by default and shown when there are todos -->
<footer id="footer"> <footer id="footer">
<span id="todo-count"><strong>1</strong> item left</span> <span id="todo-count"><strong>0</strong> item left</span>
<!-- remove this if you don't implement routing -->
<ul id="filters"> <ul id="filters">
<li> <li>
<a class="selected" href="#/">All</a> <a class="selected" href="#/">All</a>
...@@ -37,18 +34,29 @@ ...@@ -37,18 +34,29 @@
</section> </section>
<footer id="info"> <footer id="info">
<p>Double-click to edit a todo</p> <p>Double-click to edit a todo</p>
<!-- change this out with your name and url ↓ -->
<p>Created by <a href="http://github.com/hay/stapes/">Hay Kranen</a>.</p> <p>Created by <a href="http://github.com/hay/stapes/">Hay Kranen</a>.</p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a>.</p>
</footer> </footer>
<!-- scripts here -->
<script src="js/lib/jquery.js"></script> <script id="todo-template" type="text/x-handlebars-template">
<script src="js/lib/mustache.js"></script> {{#todos}}
<li class="{{#completed}}completed{{/completed}}" data-id="{{id}}">
<div class="view">
<input class="toggle" type="checkbox" {{#completed}}checked="checked"{{/completed}}>
<label>{{title}}</label>
<button class="destroy"></button>
</div>
<input class="edit" value="{{title}}">
</li>
{{/todos}}
</script>
<script src="../../../assets/jquery.min.js"></script>
<script src="../../../assets/handlebars.min.js"></script>
<script src="js/lib/stapes.js"></script> <script src="js/lib/stapes.js"></script>
<script src="js/todoController.js"></script> <script src="js/controllers/todoController.js"></script>
<script src="js/todoModel.js"></script> <script src="js/models/todoModel.js"></script>
<script src="js/todoView.js"></script> <script src="js/views/todoView.js"></script>
<script src="js/todoStore.js"></script> <script src="js/stores/todoStore.js"></script>
<script> <script>
TodoController.init(); TodoController.init();
</script> </script>
......
'use strict';
var TodoController = Stapes.create().extend({ var TodoController = Stapes.create().extend({
"bindEventHandlers" : function() { 'bindEventHandlers': function() {
this.model.on({ this.on({
"change" : function() { 'change:state': function(state) {
this.store.save( this.model.getAll() ); this.view.setActiveRoute(state);
this.view.render( this.model.getAllAsArray() ); this.renderAll();
this.view.showLeft( this.model.getLeft().length ); }
});
if (this.model.getAllAsArray().length > 0) { this.model.on({
this.view.show(); 'change' : function() {
} else { this.renderAll();
this.view.hide();
}
}, },
"change ready" : function() { 'change ready': function() {
this.view.showClearCompleted( this.model.getComplete().length ); this.view.showClearCompleted( this.model.getComplete().length );
} }
}, this); }, this);
this.view.on({ this.view.on({
"clearcompleted" : function() { 'clearcompleted': function() {
this.model.clearCompleted(); this.model.clearCompleted();
}, },
"edittodo" : function(id) { 'edittodo': function(id) {
this.model.update(id, function(item) { this.view.makeEditable(id);
item.edit = true;
return item;
});
}, },
"ready" : function() { 'ready': function() {
this.model.set( this.store.load() ); this.model.set( this.store.load() );
}, },
"statechange" : function(state) { 'statechange': function(state) {
switch(state) { this.set('state', state);
case "all":
this.view.render( this.model.getAllAsArray() );
break;
case "active":
this.view.render( this.model.getLeft() );
break;
case "completed":
this.view.render( this.model.getComplete() );
break;
}
}, },
"taskadd" : function(task) { 'todoadd': function(todo) {
this.model.addTask(task); this.model.addTodo(todo);
this.view.clearInput(); this.view.clearInput();
}, },
"taskdelete" : function(id) { 'tododelete': function(id) {
this.model.remove(id); this.model.remove(id);
}, },
"taskdone taskundone" : function(id, e) { 'todocompleted todouncompleted': function(id, e) {
this.model.update(id, function(item) { this.model.update(id, function(item) {
item.complete = e.type === "taskdone"; item.completed = (e.type === 'todocompleted');
return item; return item;
}); });
}, },
"taskedit" : function(data) { 'todoedit': function(data) {
this.model.update(data.id, function(item) { if (data.title === "") {
item.name = data.name; this.model.remove(data.id);
item.edit = false; } else {
return item; this.model.update(data.id, function(item) {
}); item.title = data.title;
return item;
});
}
}, },
"doneall undoneall" : function(alldone) { 'completedall uncompletedall': function(completedall, e) {
this.model.update(function(item) { this.model.update(function(item) {
item.complete = alldone; item.completed = completedall;
return item; return item;
}); });
} }
}, this); }, this);
}, },
"init" : function() { renderAll: function() {
this.store.save( this.model.getAll() );
this.view.render( this.model.getItemsByState( this.get('state') ) );
this.view.showLeft( this.model.getLeft().length );
if ( this.model.getAllAsArray().length > 0 ) {
this.view.show();
} else {
this.view.hide();
}
},
'init': function() {
this.model = TodoModel; this.model = TodoModel;
this.view = TodoView; this.view = TodoView;
this.store = TodoStore; this.store = TodoStore;
...@@ -91,5 +93,9 @@ var TodoController = Stapes.create().extend({ ...@@ -91,5 +93,9 @@ var TodoController = Stapes.create().extend({
this.model.init(); this.model.init();
this.view.init(); this.view.init();
this.store.init(); this.store.init();
// Initial state from the URL
this.set('state', this.view.getState());
} }
}); });
\ No newline at end of file
This diff is collapsed.
/*
mustache.js — Logic-less templates in JavaScript
See http://mustache.github.com/ for more info.
*/
var Mustache = function() {
var Renderer = function() {};
Renderer.prototype = {
otag: "{{",
ctag: "}}",
pragmas: {},
buffer: [],
pragmas_implemented: {
"IMPLICIT-ITERATOR": true
},
context: {},
render: function(template, context, partials, in_recursion) {
// reset buffer & set context
if(!in_recursion) {
this.context = context;
this.buffer = []; // TODO: make this non-lazy
}
// fail fast
if(!this.includes("", template)) {
if(in_recursion) {
return template;
} else {
this.send(template);
return;
}
}
template = this.render_pragmas(template);
var html = this.render_section(template, context, partials);
if(in_recursion) {
return this.render_tags(html, context, partials, in_recursion);
}
this.render_tags(html, context, partials, in_recursion);
},
/*
Sends parsed lines
*/
send: function(line) {
if(line != "") {
this.buffer.push(line);
}
},
/*
Looks for %PRAGMAS
*/
render_pragmas: function(template) {
// no pragmas
if(!this.includes("%", template)) {
return template;
}
var that = this;
var regex = new RegExp(this.otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" +
this.ctag);
return template.replace(regex, function(match, pragma, options) {
if(!that.pragmas_implemented[pragma]) {
throw({message:
"This implementation of mustache doesn't understand the '" +
pragma + "' pragma"});
}
that.pragmas[pragma] = {};
if(options) {
var opts = options.split("=");
that.pragmas[pragma][opts[0]] = opts[1];
}
return "";
// ignore unknown pragmas silently
});
},
/*
Tries to find a partial in the curent scope and render it
*/
render_partial: function(name, context, partials) {
name = this.trim(name);
if(!partials || partials[name] === undefined) {
throw({message: "unknown_partial '" + name + "'"});
}
if(typeof(context[name]) != "object") {
return this.render(partials[name], context, partials, true);
}
return this.render(partials[name], context[name], partials, true);
},
/*
Renders inverted (^) and normal (#) sections
*/
render_section: function(template, context, partials) {
if(!this.includes("#", template) && !this.includes("^", template)) {
return template;
}
var that = this;
// CSW - Added "+?" so it finds the tighest bound, not the widest
var regex = new RegExp(this.otag + "(\\^|\\#)\\s*(.+)\\s*" + this.ctag +
"\n*([\\s\\S]+?)" + this.otag + "\\/\\s*\\2\\s*" + this.ctag +
"\\s*", "mg");
// for each {{#foo}}{{/foo}} section do...
return template.replace(regex, function(match, type, name, content) {
var value = that.find(name, context);
if(type == "^") { // inverted section
if(!value || that.is_array(value) && value.length === 0) {
// false or empty list, render it
return that.render(content, context, partials, true);
} else {
return "";
}
} else if(type == "#") { // normal section
if(that.is_array(value)) { // Enumerable, Let's loop!
return that.map(value, function(row) {
return that.render(content, that.create_context(row),
partials, true);
}).join("");
} else if(that.is_object(value)) { // Object, Use it as subcontext!
return that.render(content, that.create_context(value),
partials, true);
} else if(typeof value === "function") {
// higher order section
return value.call(context, content, function(text) {
return that.render(text, context, partials, true);
});
} else if(value) { // boolean section
return that.render(content, context, partials, true);
} else {
return "";
}
}
});
},
/*
Replace {{foo}} and friends with values from our view
*/
render_tags: function(template, context, partials, in_recursion) {
// tit for tat
var that = this;
var new_regex = function() {
return new RegExp(that.otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" +
that.ctag + "+", "g");
};
var regex = new_regex();
var tag_replace_callback = function(match, operator, name) {
switch(operator) {
case "!": // ignore comments
return "";
case "=": // set new delimiters, rebuild the replace regexp
that.set_delimiters(name);
regex = new_regex();
return "";
case ">": // render partial
return that.render_partial(name, context, partials);
case "{": // the triple mustache is unescaped
return that.find(name, context);
default: // escape the value
return that.escape(that.find(name, context));
}
};
var lines = template.split("\n");
for(var i = 0; i < lines.length; i++) {
lines[i] = lines[i].replace(regex, tag_replace_callback, this);
if(!in_recursion) {
this.send(lines[i]);
}
}
if(in_recursion) {
return lines.join("\n");
}
},
set_delimiters: function(delimiters) {
var dels = delimiters.split(" ");
this.otag = this.escape_regex(dels[0]);
this.ctag = this.escape_regex(dels[1]);
},
escape_regex: function(text) {
// thank you Simon Willison
if(!arguments.callee.sRE) {
var specials = [
'/', '.', '*', '+', '?', '|',
'(', ')', '[', ']', '{', '}', '\\'
];
arguments.callee.sRE = new RegExp(
'(\\' + specials.join('|\\') + ')', 'g'
);
}
return text.replace(arguments.callee.sRE, '\\$1');
},
/*
find `name` in current `context`. That is find me a value
from the view object
*/
find: function(name, context) {
name = this.trim(name);
// Checks whether a value is thruthy or false or 0
function is_kinda_truthy(bool) {
return bool === false || bool === 0 || bool;
}
var value;
if(is_kinda_truthy(context[name])) {
value = context[name];
} else if(is_kinda_truthy(this.context[name])) {
value = this.context[name];
}
if(typeof value === "function") {
return value.apply(context);
}
if(value !== undefined) {
return value;
}
// silently ignore unkown variables
return "";
},
// Utility methods
/* includes tag */
includes: function(needle, haystack) {
return haystack.indexOf(this.otag + needle) != -1;
},
/*
Does away with nasty characters
*/
escape: function(s) {
s = String(s === null ? "" : s);
return s.replace(/&(?!\w+;)|["<>\\]/g, function(s) {
switch(s) {
case "&": return "&amp;";
case "\\": return "\\\\";
case '"': return '\"';
case "<": return "&lt;";
case ">": return "&gt;";
default: return s;
}
});
},
// by @langalex, support for arrays of strings
create_context: function(_context) {
if(this.is_object(_context)) {
return _context;
} else {
var iterator = ".";
if(this.pragmas["IMPLICIT-ITERATOR"]) {
iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator;
}
var ctx = {};
ctx[iterator] = _context;
return ctx;
}
},
is_object: function(a) {
return a && typeof a == "object";
},
is_array: function(a) {
return Object.prototype.toString.call(a) === '[object Array]';
},
/*
Gets rid of leading and trailing whitespace
*/
trim: function(s) {
return s.replace(/^\s*|\s*$/g, "");
},
/*
Why, why, why? Because IE. Cry, cry cry.
*/
map: function(array, fn) {
if (typeof array.map == "function") {
return array.map(fn);
} else {
var r = [];
var l = array.length;
for(var i = 0; i < l; i++) {
r.push(fn(array[i]));
}
return r;
}
}
};
return({
name: "mustache.js",
version: "0.3.0",
/*
Turns a template and view into HTML
*/
to_html: function(template, view, partials, send_fun) {
var renderer = new Renderer();
if(send_fun) {
renderer.send = send_fun;
}
renderer.render(template, view, partials);
if(!send_fun) {
return renderer.buffer.join("\n");
}
}
});
}();
if (typeof define !== "undefined" && define.amd) {
define(Mustache);
}
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
// Stapes.js : http://hay.github.com/stapes // Stapes.js : http://hay.github.com/stapes
(function() { (function() {
var VERSION = "0.3"; var VERSION = "0.4pre";
/** Utility functions /** Utility functions
* *
...@@ -238,7 +238,7 @@ ...@@ -238,7 +238,7 @@
}, },
emit : function(types, data) { emit : function(types, data) {
data = data || null; data = (typeof data === "undefined") ? null : data;
util.each(types.split(" "), util.bind(function(type) { util.each(types.split(" "), util.bind(function(type) {
// First 'all' type events: is there an 'all' handler in the // First 'all' type events: is there an 'all' handler in the
......
'use strict';
var TodoModel = Stapes.create().extend({
'addTodo': function(title) {
this.push({
'completed' : false,
'title' : title
});
},
'clearCompleted': function() {
this.remove(function(item) {
return item.completed === true;
});
},
// Returns items on the basis of the current state
'getItemsByState' : function(state) {
state = state || "all"; // default
return this.itemStates[ state ].call(this);
},
'getComplete': function() {
return this.filter(function(item) {
return item.completed === true;
});
},
'getLeft': function() {
return this.filter(function(item) {
return item.completed === false;
});
},
'itemStates': {
'all' : function() {
return this.getAllAsArray();
},
'active': function() {
return this.getLeft();
},
'completed': function() {
return this.getComplete();
}
}
});
\ No newline at end of file
'use strict';
var TodoStore = Stapes.create().extend({ var TodoStore = Stapes.create().extend({
"init" : function() { 'init': function() {
if (!"localStorage" in window) { if (!'localStorage' in window) return;
throw new Error("Your browser doesn't support localStorage");
}
this.emit('ready'); this.emit('ready');
}, },
"load" : function() { 'load': function() {
var result = window.localStorage['todos-stapes']; var result = window.localStorage['todos-stapes'];
if (result) { if (result) {
...@@ -15,7 +14,7 @@ var TodoStore = Stapes.create().extend({ ...@@ -15,7 +14,7 @@ var TodoStore = Stapes.create().extend({
} }
}, },
"save" : function(data) { 'save': function(data) {
localStorage['todos-stapes'] = JSON.stringify( data ); localStorage['todos-stapes'] = JSON.stringify( data );
} }
}); });
\ No newline at end of file
var TodoModel = Stapes.create().extend({
"addTask" : function(name) {
this.push({
"complete" : false,
"name" : name,
"edit" : false
});
},
"clearCompleted" : function() {
this.remove(function(item) {
return item.complete === true;
});
},
"getComplete" : function() {
return this.filter(function(item) {
return item.complete === true;
});
},
"getLeft" : function() {
return this.filter(function(item) {
return item.complete === false;
});
}
});
\ No newline at end of file
(function() {
var todoView = Stapes.create(),
taskTmpl;
function bindEventHandlers() {
$("#new-todo").on('keyup', function(e) {
if (e.which === 13 && $(this).val() !== "") {
e.preventDefault();
todoView.emit('taskadd', $(this).val());
}
});
$("#todo-list").on('click', '.destroy', function() {
todoView.emit('taskdelete', $(this).parents('.item').data('id'));
});
$("#todo-list").on('click', 'input.toggle', function(e) {
var event = $(this).is(':checked') ? 'taskdone' : 'taskundone';
todoView.emit(event, $(this).parents('.item').data('id'));
});
$("#todo-list").on('dblclick', 'li', function(e) {
todoView.emit('edittodo', $(this).data('id'));
});
$("#todo-list").on('keyup', 'input.todo-input', function(e) {
if (e.which === 13) {
e.preventDefault();
todoView.emit('taskedit', {
id : $(this).parents(".item").data('id'),
name : $(this).val()
});
}
});
$("#clear-completed").on('click', function() {
todoView.emit('clearcompleted');
});
$("#toggle-all").on('click', function() {
var isChecked = $(this).is(':checked');
todoView.emit( isChecked ? 'doneall' : 'undoneall', isChecked === true);
});
window.onhashchange = function() {
var hash = window.location.hash.replace('#/', '');
// If we get the 'all' menu item we don't get anything in the hash,
// so we need to do this...
hash = (hash === "") ? "all" : hash;
todoView.emit('statechange', hash);
}
}
function loadTemplates(cb) {
var root = "//" + window.location.host + window.location.pathname;
$.get('templates/task.html', function(tmpl) {
cb(function(view) {
return Mustache.to_html(tmpl, view);
});
});
}
todoView.extend({
"clearInput" : function() {
$("#new-todo").val('');
},
"hide" : function() {
$("#main, footer").hide();
},
"init" : function() {
bindEventHandlers();
loadTemplates(function(tmpl) {
taskTmpl = tmpl;
todoView.emit('ready');
});
},
"render" : function(tasks) {
var html = taskTmpl({ "tasks" : tasks });
$("#todo-list").html( html );
},
"show" : function() {
$("#main, footer").show();
},
"showClearCompleted" : function(completed) {
var bool = completed > 0;
$("#clear-completed").toggle(bool);
$("#clear-completed").html('Clear completed (' + completed + ')');
},
"showLeft" : function(left) {
var word = (left > 1) ? "items" : "item";
$("#todo-count").html('<strong>' + left + '</strong> ' + word + ' left');
}
});
window.TodoView = todoView;
})();
\ No newline at end of file
'use strict';
(function() {
var todoView = Stapes.create(),
todoTmpl,
ENTER_KEY_KEYCODE = 13;
function bindEventHandlers() {
$('#new-todo').on('keyup', function(e) {
var todoVal = $.trim($(this).val());
if (e.which === ENTER_KEY_KEYCODE && todoVal !== '') {
e.preventDefault();
todoView.emit('todoadd', todoVal);
}
});
$('#todo-list').on('click', '.destroy', function() {
todoView.emit('tododelete', $(this).parents('li').data('id'));
});
$('#todo-list').on('click', 'input.toggle', function(e) {
var event = $(e.target).is(':checked') ? 'todocompleted' : 'todouncompleted';
todoView.emit(event, $(this).parents('li').data('id'));
});
$('#todo-list').on('dblclick', 'li', function(e) {
todoView.emit('edittodo', $(this).data('id'));
});
$('#todo-list').on('keyup focusout', 'input.edit', function(e) {
if (e.type === 'keyup') {
if (e.which === ENTER_KEY_KEYCODE) {
e.preventDefault();
} else {
return false;
}
}
todoView.emit('todoedit', {
id : $(this).parents('li').data('id'),
title : $.trim($(this).val())
});
});
$('#clear-completed').on('click', function() {
todoView.emit('clearcompleted');
});
$('#toggle-all').on('click', function() {
var isChecked = $(this).is(':checked');
todoView.emit( isChecked ? 'completedall' : 'uncompletedall', isChecked);
});
window.onhashchange = function() {
todoView.emit('statechange', todoView.getState());
};
}
function loadTemplates() {
todoTmpl = Handlebars.compile( $('#todo-template').html() );
}
todoView.extend({
'clearInput': function() {
$('#new-todo').val('');
},
'getState': function() {
return window.location.hash.replace('#/', '') || 'all';
},
'hide': function() {
$('#main, #footer').hide();
},
'init': function() {
bindEventHandlers();
loadTemplates();
this.emit('ready');
},
'makeEditable' : function(id) {
var $item = $('#todo-list li[data-id=' + id + ']');
$item.addClass('editing').find('input.edit').focus();
},
'render': function(todos) {
var html = todoTmpl({ 'todos' : todos });
$('#todo-list').html( html );
},
'setActiveRoute': function(route) {
// 'all' doesnt have a href="#/all"
route = (route === 'all') ? '' : route;
$('#filters a').removeClass('selected').filter('[href="#/' + route + '"]').addClass('selected');
},
'show': function() {
$('#main, footer').show();
},
'showClearCompleted': function(completed) {
var bool = completed > 0;
$('#clear-completed').toggle(bool);
$('#clear-completed').html('Clear completed (' + completed + ')');
},
'showLeft': function(left) {
var word = (left === 1) ? 'item' : 'items';
$('#todo-count').html('<strong>' + left + '</strong> ' + word + ' left');
$("#toggle-all").get(0).checked = (left === 0);
}
});
window.TodoView = todoView;
})();
\ No newline at end of file
{{#tasks}}
<li class="item {{#complete}}complete{{/complete}}" data-id="{{id}}">
{{#edit}}
<div class="edit" style="display:block;">
<input class="todo-input" type="text" value="{{name}}" />
</div>
{{/edit}}
{{^edit}}
<div class="view">
<input class="toggle" type="checkbox" {{#complete}}checked="checked"{{/complete}}>
<label>{{name}}</label>
<button class="destroy"></button>
</div>
<input class="edit" value="{{name}}">
{{/edit}}
</li>
{{/tasks}}
\ No newline at end of file
html,
body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
color: inherit;
-webkit-appearance: none;
/*-moz-appearance: none;*/
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #eeeeee url('bg.png');
color: #4d4d4d;
width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
#todoapp {
background: #fff;
background: rgba(255, 255, 255, 0.9);
margin: 130px 0 40px 0;
border: 1px solid #ccc;
position: relative;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.15);
}
#todoapp:before {
content: '';
border-left: 1px solid #f5d6d6;
border-right: 1px solid #f5d6d6;
width: 2px;
position: absolute;
top: 0;
left: 40px;
height: 100%;
}
#todoapp h1 {
position: absolute;
top: -120px;
width: 100%;
font-size: 70px;
font-weight: bold;
text-align: center;
color: #b3b3b3;
color: rgba(255, 255, 255, 0.3);
text-shadow: -1px -1px rgba(0, 0, 0, 0.2);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
-ms-text-rendering: optimizeLegibility;
-o-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
#header {
padding-top: 15px;
border-radius: inherit;
}
#todoapp header:before {
content: '';
position: absolute;
top: 0;
right: 0;
left: 0;
height: 15px;
z-index: 2;
border-bottom: 1px solid #6c615c;
background: #8d7d77;
background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8)));
background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: -moz-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: -o-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: -ms-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');
border-radius: inherit;
}
#todoapp input::-webkit-input-placeholder {
font-style: italic;
}
#todoapp input:-moz-placeholder {
font-style: italic;
color: #a9a9a9;
}
#new-todo,
.edit {
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
line-height: 1.4em;
border: 0;
outline: none;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
#new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.02);
position: relative;
z-index: 2;
box-shadow: none;
}
#main {
position: relative;
z-index: 2;
border-top: 1px dotted #adadad;
}
label[for='toggle-all'] {
display: none;
}
#toggle-all {
position: absolute;
top: -42px;
left: 12px;
text-align: center;
-webkit-appearance: none;
/*-moz-appearance: none;*/
-ms-appearance: none;
-o-appearance: none;
appearance: none;
-webkit-transform: rotate(90deg);
/*-moz-transform: rotate(90deg);*/
-ms-transform: rotate(90deg);
/*-o-transform: rotate(90deg);*/
transform: rotate(90deg);
}
#toggle-all:before {
content: '»';
font-size: 28px;
color: #d9d9d9;
padding: 0 25px 7px;
}
#toggle-all:checked:before {
color: #737373;
}
/* Need this ugly hack, since only
WebKit supports styling of inputs */
@media screen and (-webkit-min-device-pixel-ratio:0) {
#toggle-all {
top: -52px;
left: -11px;
}
}
#todo-list {
margin: 0;
padding: 0;
list-style: none;
}
#todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px dotted #ccc;
}
#todo-list li:last-child {
border-bottom: none;
}
#todo-list li.editing {
border-bottom: none;
padding: 0;
}
#todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
margin: 0 0 0 43px;
}
#todo-list li.editing .view {
display: none;
}
#todo-list li .toggle {
text-align: center;
width: 35px;
-webkit-appearance: none;
/*-moz-appearance: none;*/
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
#todo-list li .toggle:after {
font-size: 18px;
content: '✔';
line-height: 40px;
font-size: 20px;
color: #d9d9d9;
text-shadow: 0 -1px 0 #bfbfbf;
}
#todo-list li .toggle:checked:after {
color: #85ada7;
text-shadow: 0 1px 0 #669991;
bottom: 1px;
position: relative;
}
#todo-list li label {
word-break: break-word;
margin: 20px 15px;
display: inline-block;
-webkit-transition: color 0.4s;
-moz-transition: color 0.4s;
-ms-transition: color 0.4s;
-o-transition: color 0.4s;
transition: color 0.4s;
}
#todo-list li.complete label {
color: #a9a9a9;
text-decoration: line-through;
}
#todo-list li .destroy {
display: none;
position: absolute;
top: 10px;
right: 10px;
width: 40px;
height: 40px;
font-size: 22px;
color: #a88a8a;
-webkit-transition: all 0.2s;
-moz-transition: all 0.2s;
-ms-transition: all 0.2s;
-o-transition: all 0.2s;
transition: all 0.2s;
}
#todo-list li .destroy:hover {
text-shadow: 0 0 1px #000,
0 0 10px rgba(199, 107, 107, 0.8);
-webkit-transform: scale(1.3);
-moz-transform: scale(1.3);
-ms-transform: scale(1.3);
-o-transform: scale(1.3);
transform: scale(1.3);
}
#todo-list li .destroy:after {
content: '✖';
}
#todo-list li:hover .destroy {
display: block;
}
#todo-list li .edit {
display: none;
}
#todo-list li.editing:last-child {
margin-bottom: -1px;
}
#footer {
color: #777;
padding: 0 15px;
position: absolute;
right: 0;
bottom: -31px;
left: 0;
z-index: 1;
text-align: center;
}
#footer:before {
content: '';
position: absolute;
right: 0;
bottom: 31px;
left: 0;
height: 100px;
z-index: -1;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3),
0 6px 0 -3px rgba(255, 255, 255, 0.8),
0 7px 1px -3px rgba(0, 0, 0, 0.3),
0 42px 0 -6px rgba(255, 255, 255, 0.8),
0 43px 2px -6px rgba(0, 0, 0, 0.2);
}
#todo-count {
float: left;
text-align: left;
}
#filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
#filters li {
display: inline;
}
#filters li a {
color: #83756f;
margin: 2px;
text-decoration: none;
}
#filters li a.selected {
font-weight: bold;
}
#clear-completed {
float: right;
line-height: 20px;
text-decoration: none;
background: rgba(0, 0, 0, 0.1);
font-size: 11px;
padding: 0 10px;
position: relative;
border-radius: 3px;
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2);
}
#clear-completed:hover {
background: rgba(0, 0, 0, 0.15);
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3);
}
#info {
margin: 65px auto 0;
color: #a6a6a6;
font-size: 12px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);
text-align: center;
}
#info a {
color: inherit;
}
\ No newline at end of file
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Stapes.js • TodoMVC</title> <title>Stapes.js • TodoMVC</title>
<link rel="stylesheet" href="css/base.css"> <link rel="stylesheet" href="../../../assets/base.css">
</head> </head>
<body> <body>
<section id="todoapp"> <section id="todoapp">
...@@ -11,16 +11,13 @@ ...@@ -11,16 +11,13 @@
<h1>todos</h1> <h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?" autofocus> <input id="new-todo" placeholder="What needs to be done?" autofocus>
</header> </header>
<!-- this section should hidden by default and shown when there are todos -->
<section id="main"> <section id="main">
<input id="toggle-all" type="checkbox"> <input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label> <label for="toggle-all">Mark all as complete</label>
<ul id="todo-list"></ul> <ul id="todo-list"></ul>
</section> </section>
<!-- this footer should hidden by default and shown when there are todos -->
<footer id="footer"> <footer id="footer">
<span id="todo-count"><strong>1</strong> item left</span> <span id="todo-count"><strong>0</strong> item left</span>
<!-- remove this if you don't implement routing -->
<ul id="filters"> <ul id="filters">
<li> <li>
<a class="selected" href="#/">All</a> <a class="selected" href="#/">All</a>
...@@ -37,11 +34,26 @@ ...@@ -37,11 +34,26 @@
</section> </section>
<footer id="info"> <footer id="info">
<p>Double-click to edit a todo</p> <p>Double-click to edit a todo</p>
<!-- change this out with your name and url ↓ -->
<p>Created by <a href="http://github.com/hay/stapes/">Hay Kranen</a>.</p> <p>Created by <a href="http://github.com/hay/stapes/">Hay Kranen</a>.</p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer> </footer>
<script id="todo-template" type="text/x-handlebars-template">
{{#todos}}
<li class="{{#completed}}completed{{/completed}}" data-id="{{id}}">
<div class="view">
<input class="toggle" type="checkbox" {{#completed}}checked="checked"{{/completed}}>
<label>{{title}}</label>
<button class="destroy"></button>
</div>
<input class="edit" value="{{title}}">
</li>
{{/todos}}
</script>
<script src="../../../assets/handlebars.min.js"></script>
<script src="js/lib/stapes.js"></script>
<script src="js/lib/zepto.js"></script> <script src="js/lib/zepto.js"></script>
<script src="js/lib/require.js" data-main="js/app"></script> <script src="js/lib/require.js" data-main="js/app"></script>
</body> </body>
</html> </html>
\ No newline at end of file
require( require(
["todoController"], ["controllers/todoController"],
function(todoController) { function(todoController) {
todoController.init(); todoController.init();
} }
......
'use strict';
define( define(
["lib/stapes", "todoModel", "todoView", "todoStore"], ['lib/stapes', 'models/todoModel', 'views/todoView', 'stores/todoStore'],
function(Stapes, TodoModel, TodoView, TodoStore) { function(Stapes, TodoModel, TodoView, TodoStore) {
return Stapes.create().extend({ return Stapes.create().extend({
"bindEventHandlers" : function() { 'bindEventHandlers': function() {
this.model.on({ this.on({
"change" : function() { 'change:state': function(state) {
this.store.save( this.model.getAll() ); this.view.setActiveRoute(state);
this.view.render( this.model.getAllAsArray() ); this.renderAll();
this.view.showLeft( this.model.getLeft().length ); }
});
if (this.model.getAllAsArray().length > 0) { this.model.on({
this.view.show(); 'change' : function() {
} else { this.renderAll();
this.view.hide();
}
}, },
"change ready" : function() { 'change ready': function() {
this.view.showClearCompleted( this.model.getComplete().length ); this.view.showClearCompleted( this.model.getComplete().length );
} }
}, this); }, this);
this.view.on({ this.view.on({
"clearcompleted" : function() { 'clearcompleted': function() {
this.model.clearCompleted(); this.model.clearCompleted();
}, },
"edittodo" : function(id) { 'edittodo': function(id) {
this.model.update(id, function(item) { this.view.makeEditable(id);
item.edit = true;
return item;
});
}, },
"ready" : function() { 'ready': function() {
this.model.set( this.store.load() ); this.model.set( this.store.load() );
}, },
"statechange" : function(state) { 'statechange': function(state) {
switch(state) { this.set('state', state);
case "all":
this.view.render( this.model.getAllAsArray() );
break;
case "active":
this.view.render( this.model.getLeft() );
break;
case "completed":
this.view.render( this.model.getComplete() );
break;
}
}, },
"taskadd" : function(task) { 'todoadd': function(todo) {
this.model.addTask(task); this.model.addTodo(todo);
this.view.clearInput(); this.view.clearInput();
}, },
"taskdelete" : function(id) { 'tododelete': function(id) {
this.model.remove(id); this.model.remove(id);
}, },
"taskdone taskundone" : function(id, e) { 'todocompleted todouncompleted': function(id, e) {
this.model.update(id, function(item) { this.model.update(id, function(item) {
item.complete = e.type === "taskdone"; item.completed = (e.type === 'todocompleted');
return item; return item;
}); });
}, },
"taskedit" : function(data) { 'todoedit': function(data) {
this.model.update(data.id, function(item) { if (data.title === "") {
item.name = data.name; this.model.remove(data.id);
item.edit = false; } else {
return item; this.model.update(data.id, function(item) {
}); item.title = data.title;
return item;
});
}
}, },
"doneall undoneall" : function(alldone) { 'completedall uncompletedall': function(completedall, e) {
this.model.update(function(item) { this.model.update(function(item) {
item.complete = alldone; item.completed = completedall;
return item; return item;
}); });
} }
}, this); }, this);
}, },
"init" : function() { renderAll: function() {
this.store.save( this.model.getAll() );
this.view.render( this.model.getItemsByState( this.get('state') ) );
this.view.showLeft( this.model.getLeft().length );
if ( this.model.getAllAsArray().length > 0 ) {
this.view.show();
} else {
this.view.hide();
}
},
'init': function() {
this.model = TodoModel; this.model = TodoModel;
this.view = TodoView; this.view = TodoView;
this.store = TodoStore; this.store = TodoStore;
...@@ -94,6 +96,10 @@ function(Stapes, TodoModel, TodoView, TodoStore) { ...@@ -94,6 +96,10 @@ function(Stapes, TodoModel, TodoView, TodoStore) {
this.model.init(); this.model.init();
this.view.init(); this.view.init();
this.store.init(); this.store.init();
// Initial state from the URL
this.set('state', this.view.getState());
} }
}); });
}); });
\ No newline at end of file
/*
mustache.js — Logic-less templates in JavaScript
See http://mustache.github.com/ for more info.
*/
var Mustache = function() {
var Renderer = function() {};
Renderer.prototype = {
otag: "{{",
ctag: "}}",
pragmas: {},
buffer: [],
pragmas_implemented: {
"IMPLICIT-ITERATOR": true
},
context: {},
render: function(template, context, partials, in_recursion) {
// reset buffer & set context
if(!in_recursion) {
this.context = context;
this.buffer = []; // TODO: make this non-lazy
}
// fail fast
if(!this.includes("", template)) {
if(in_recursion) {
return template;
} else {
this.send(template);
return;
}
}
template = this.render_pragmas(template);
var html = this.render_section(template, context, partials);
if(in_recursion) {
return this.render_tags(html, context, partials, in_recursion);
}
this.render_tags(html, context, partials, in_recursion);
},
/*
Sends parsed lines
*/
send: function(line) {
if(line != "") {
this.buffer.push(line);
}
},
/*
Looks for %PRAGMAS
*/
render_pragmas: function(template) {
// no pragmas
if(!this.includes("%", template)) {
return template;
}
var that = this;
var regex = new RegExp(this.otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" +
this.ctag);
return template.replace(regex, function(match, pragma, options) {
if(!that.pragmas_implemented[pragma]) {
throw({message:
"This implementation of mustache doesn't understand the '" +
pragma + "' pragma"});
}
that.pragmas[pragma] = {};
if(options) {
var opts = options.split("=");
that.pragmas[pragma][opts[0]] = opts[1];
}
return "";
// ignore unknown pragmas silently
});
},
/*
Tries to find a partial in the curent scope and render it
*/
render_partial: function(name, context, partials) {
name = this.trim(name);
if(!partials || partials[name] === undefined) {
throw({message: "unknown_partial '" + name + "'"});
}
if(typeof(context[name]) != "object") {
return this.render(partials[name], context, partials, true);
}
return this.render(partials[name], context[name], partials, true);
},
/*
Renders inverted (^) and normal (#) sections
*/
render_section: function(template, context, partials) {
if(!this.includes("#", template) && !this.includes("^", template)) {
return template;
}
var that = this;
// CSW - Added "+?" so it finds the tighest bound, not the widest
var regex = new RegExp(this.otag + "(\\^|\\#)\\s*(.+)\\s*" + this.ctag +
"\n*([\\s\\S]+?)" + this.otag + "\\/\\s*\\2\\s*" + this.ctag +
"\\s*", "mg");
// for each {{#foo}}{{/foo}} section do...
return template.replace(regex, function(match, type, name, content) {
var value = that.find(name, context);
if(type == "^") { // inverted section
if(!value || that.is_array(value) && value.length === 0) {
// false or empty list, render it
return that.render(content, context, partials, true);
} else {
return "";
}
} else if(type == "#") { // normal section
if(that.is_array(value)) { // Enumerable, Let's loop!
return that.map(value, function(row) {
return that.render(content, that.create_context(row),
partials, true);
}).join("");
} else if(that.is_object(value)) { // Object, Use it as subcontext!
return that.render(content, that.create_context(value),
partials, true);
} else if(typeof value === "function") {
// higher order section
return value.call(context, content, function(text) {
return that.render(text, context, partials, true);
});
} else if(value) { // boolean section
return that.render(content, context, partials, true);
} else {
return "";
}
}
});
},
/*
Replace {{foo}} and friends with values from our view
*/
render_tags: function(template, context, partials, in_recursion) {
// tit for tat
var that = this;
var new_regex = function() {
return new RegExp(that.otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" +
that.ctag + "+", "g");
};
var regex = new_regex();
var tag_replace_callback = function(match, operator, name) {
switch(operator) {
case "!": // ignore comments
return "";
case "=": // set new delimiters, rebuild the replace regexp
that.set_delimiters(name);
regex = new_regex();
return "";
case ">": // render partial
return that.render_partial(name, context, partials);
case "{": // the triple mustache is unescaped
return that.find(name, context);
default: // escape the value
return that.escape(that.find(name, context));
}
};
var lines = template.split("\n");
for(var i = 0; i < lines.length; i++) {
lines[i] = lines[i].replace(regex, tag_replace_callback, this);
if(!in_recursion) {
this.send(lines[i]);
}
}
if(in_recursion) {
return lines.join("\n");
}
},
set_delimiters: function(delimiters) {
var dels = delimiters.split(" ");
this.otag = this.escape_regex(dels[0]);
this.ctag = this.escape_regex(dels[1]);
},
escape_regex: function(text) {
// thank you Simon Willison
if(!arguments.callee.sRE) {
var specials = [
'/', '.', '*', '+', '?', '|',
'(', ')', '[', ']', '{', '}', '\\'
];
arguments.callee.sRE = new RegExp(
'(\\' + specials.join('|\\') + ')', 'g'
);
}
return text.replace(arguments.callee.sRE, '\\$1');
},
/*
find `name` in current `context`. That is find me a value
from the view object
*/
find: function(name, context) {
name = this.trim(name);
// Checks whether a value is thruthy or false or 0
function is_kinda_truthy(bool) {
return bool === false || bool === 0 || bool;
}
var value;
if(is_kinda_truthy(context[name])) {
value = context[name];
} else if(is_kinda_truthy(this.context[name])) {
value = this.context[name];
}
if(typeof value === "function") {
return value.apply(context);
}
if(value !== undefined) {
return value;
}
// silently ignore unkown variables
return "";
},
// Utility methods
/* includes tag */
includes: function(needle, haystack) {
return haystack.indexOf(this.otag + needle) != -1;
},
/*
Does away with nasty characters
*/
escape: function(s) {
s = String(s === null ? "" : s);
return s.replace(/&(?!\w+;)|["<>\\]/g, function(s) {
switch(s) {
case "&": return "&amp;";
case "\\": return "\\\\";
case '"': return '\"';
case "<": return "&lt;";
case ">": return "&gt;";
default: return s;
}
});
},
// by @langalex, support for arrays of strings
create_context: function(_context) {
if(this.is_object(_context)) {
return _context;
} else {
var iterator = ".";
if(this.pragmas["IMPLICIT-ITERATOR"]) {
iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator;
}
var ctx = {};
ctx[iterator] = _context;
return ctx;
}
},
is_object: function(a) {
return a && typeof a == "object";
},
is_array: function(a) {
return Object.prototype.toString.call(a) === '[object Array]';
},
/*
Gets rid of leading and trailing whitespace
*/
trim: function(s) {
return s.replace(/^\s*|\s*$/g, "");
},
/*
Why, why, why? Because IE. Cry, cry cry.
*/
map: function(array, fn) {
if (typeof array.map == "function") {
return array.map(fn);
} else {
var r = [];
var l = array.length;
for(var i = 0; i < l; i++) {
r.push(fn(array[i]));
}
return r;
}
}
};
return({
name: "mustache.js",
version: "0.3.0",
/*
Turns a template and view into HTML
*/
to_html: function(template, view, partials, send_fun) {
var renderer = new Renderer();
if(send_fun) {
renderer.send = send_fun;
}
renderer.render(template, view, partials);
if(!send_fun) {
return renderer.buffer.join("\n");
}
}
});
}();
if (typeof define !== "undefined" && define.amd) {
define(Mustache);
}
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
// Stapes.js : http://hay.github.com/stapes // Stapes.js : http://hay.github.com/stapes
(function() { (function() {
var VERSION = "0.3"; var VERSION = "0.4pre";
/** Utility functions /** Utility functions
* *
...@@ -238,7 +238,7 @@ ...@@ -238,7 +238,7 @@
}, },
emit : function(types, data) { emit : function(types, data) {
data = data || null; data = (typeof data === "undefined") ? null : data;
util.each(types.split(" "), util.bind(function(type) { util.each(types.split(" "), util.bind(function(type) {
// First 'all' type events: is there an 'all' handler in the // First 'all' type events: is there an 'all' handler in the
......
'use strict';
define(['lib/stapes'], function(Stapes) {
return Stapes.create().extend({
'addTodo': function(title) {
this.push({
'completed' : false,
'title' : title
});
},
'clearCompleted': function() {
this.remove(function(item) {
return item.completed === true;
});
},
// Returns items on the basis of the current state
'getItemsByState' : function(state) {
state = state || "all"; // default
return this.itemStates[ state ].call(this);
},
'getComplete': function() {
return this.filter(function(item) {
return item.completed === true;
});
},
'getLeft': function() {
return this.filter(function(item) {
return item.completed === false;
});
},
'itemStates': {
'all' : function() {
return this.getAllAsArray();
},
'active': function() {
return this.getLeft();
},
'completed': function() {
return this.getComplete();
}
}
});
});
\ No newline at end of file
define(["lib/stapes"], function(Stapes) { 'use strict';
define(['lib/stapes'], function(Stapes) {
return Stapes.create().extend({ return Stapes.create().extend({
"init" : function() { 'init': function() {
if (!"localStorage" in window) { if (!'localStorage' in window) return;
throw new Error("Your browser doesn't support localStorage");
}
this.emit('ready'); this.emit('ready');
}, },
"load" : function() { 'load': function() {
var result = window.localStorage['todos-stapes']; var result = window.localStorage['todos-stapes'];
if (result) { if (result) {
...@@ -16,7 +15,7 @@ define(["lib/stapes"], function(Stapes) { ...@@ -16,7 +15,7 @@ define(["lib/stapes"], function(Stapes) {
} }
}, },
"save" : function(data) { 'save': function(data) {
localStorage['todos-stapes'] = JSON.stringify( data ); localStorage['todos-stapes'] = JSON.stringify( data );
} }
}); });
......
define(["lib/stapes"], function(Stapes) {
return Stapes.create().extend({
"addTask" : function(name) {
this.push({
"complete" : false,
"name" : name,
"edit" : false
});
},
"clearCompleted" : function() {
this.remove(function(item) {
return item.complete === true;
});
},
"getComplete" : function() {
return this.filter(function(item) {
return item.complete === true;
});
},
"getLeft" : function() {
return this.filter(function(item) {
return item.complete === false;
});
}
});
});
\ No newline at end of file
define(["lib/stapes", "lib/mustache"], function(Stapes, Mustache) {
var todoView = Stapes.create(),
taskTmpl;
function bindEventHandlers() {
$("#new-todo").on('keyup', function(e) {
if (e.which === 13 && $(this).val() !== "") {
e.preventDefault();
todoView.emit('taskadd', $(this).val());
}
});
$("#todo-list").on('click', '.destroy', function() {
todoView.emit('taskdelete', $(this).parents('.item').data('id'));
});
$("#todo-list").on('click', 'input.toggle', function(e) {
var event = $(this).is(':checked') ? 'taskdone' : 'taskundone';
todoView.emit(event, $(this).parents('.item').data('id'));
});
$("#todo-list").on('dblclick', 'li', function(e) {
todoView.emit('edittodo', $(this).data('id'));
});
$("#todo-list").on('keyup', 'input.todo-input', function(e) {
if (e.which === 13) {
e.preventDefault();
todoView.emit('taskedit', {
id : $(this).parents(".item").data('id'),
name : $(this).val()
});
}
});
$("#clear-completed").on('click', function() {
todoView.emit('clearcompleted');
});
$("#toggle-all").on('click', function() {
var isChecked = $(this).is(':checked');
todoView.emit( isChecked ? 'doneall' : 'undoneall', isChecked === true);
});
window.onhashchange = function() {
var hash = window.location.hash.replace('#/', '');
// If we get the 'all' menu item we don't get anything in the hash,
// so we need to do this...
hash = (hash === "") ? "all" : hash;
todoView.emit('statechange', hash);
}
}
function loadTemplates(cb) {
var root = "//" + window.location.host + window.location.pathname;
$.get('templates/task.html', function(tmpl) {
cb(function(view) {
return Mustache.to_html(tmpl, view);
});
});
}
todoView.extend({
"clearInput" : function() {
$("#new-todo").val('');
},
"hide" : function() {
$("#main, footer").hide();
},
"init" : function() {
bindEventHandlers();
loadTemplates(function(tmpl) {
taskTmpl = tmpl;
todoView.emit('ready');
});
},
"render" : function(tasks) {
var html = taskTmpl({ "tasks" : tasks });
$("#todo-list").html( html );
},
"show" : function() {
$("#main, footer").show();
},
"showClearCompleted" : function(completed) {
var bool = completed > 0;
$("#clear-completed").toggle(bool);
$("#clear-completed").html('Clear completed (' + completed + ')');
},
"showLeft" : function(left) {
var word = (left > 1) ? "items" : "item";
$("#todo-count").html('<strong>' + left + '</strong> ' + word + ' left');
}
});
return todoView;
});
\ No newline at end of file
'use strict';
define(['lib/stapes'], function(Stapes) {
var todoView = Stapes.create(),
todoTmpl,
ENTER_KEY_KEYCODE = 13;
function bindEventHandlers() {
$('#new-todo').on('keyup', function(e) {
var todoVal = $.trim($(this).val());
if (e.which === ENTER_KEY_KEYCODE && todoVal !== '') {
e.preventDefault();
todoView.emit('todoadd', todoVal);
}
});
$('#todo-list').on('click', '.destroy', function() {
todoView.emit('tododelete', $(this).parents('li').data('id'));
});
$('#todo-list').on('click', 'input.toggle', function(e) {
var event = $(e.target).is(':checked') ? 'todocompleted' : 'todouncompleted';
todoView.emit(event, $(this).parents('li').data('id'));
});
$('#todo-list').on('dblclick', 'li', function(e) {
todoView.emit('edittodo', $(this).data('id'));
});
$('#todo-list').on('keyup focusout', 'input.edit', function(e) {
if (e.type === 'keyup') {
if (e.which === ENTER_KEY_KEYCODE) {
e.preventDefault();
} else {
return false;
}
}
todoView.emit('todoedit', {
id : $(this).parents('li').data('id'),
title : $.trim($(this).val())
});
});
$('#clear-completed').on('click', function() {
todoView.emit('clearcompleted');
});
$('#toggle-all').on('click', function() {
var isChecked = $(this).is(':checked');
todoView.emit( isChecked ? 'completedall' : 'uncompletedall', isChecked);
});
window.onhashchange = function() {
todoView.emit('statechange', todoView.getState());
};
}
function loadTemplates() {
todoTmpl = Handlebars.compile( $('#todo-template').html() );
}
todoView.extend({
'clearInput': function() {
$('#new-todo').val('');
},
'getState': function() {
return window.location.hash.replace('#/', '') || 'all';
},
'hide': function() {
$('#main, #footer').hide();
},
'init': function() {
bindEventHandlers();
loadTemplates();
this.emit('ready');
},
'makeEditable' : function(id) {
// Zepto is a little quirky here and both doesn't accept filter()
// and $('#todo-list li[data-id=' + id + ']') :(
var $item;
$('#todo-list li').each(function() {
if ($(this).data('id') === id) $item = $(this);
});
$item.addClass('editing').find('input.edit').focus();
},
'render': function(todos) {
var html = todoTmpl({ 'todos' : todos });
$('#todo-list').html( html );
},
'setActiveRoute': function(route) {
// 'all' doesnt have a href="#/all"
route = (route === 'all') ? '' : route;
$('#filters a').removeClass('selected').filter('[href="#/' + route + '"]').addClass('selected');
},
'show': function() {
$('#main, footer').show();
},
'showClearCompleted': function(completed) {
var bool = completed > 0;
$('#clear-completed').toggle(bool);
$('#clear-completed').html('Clear completed (' + completed + ')');
},
'showLeft': function(left) {
var word = (left === 1) ? 'item' : 'items';
$('#todo-count').html('<strong>' + left + '</strong> ' + word + ' left');
$("#toggle-all").get(0).checked = (left === 0);
}
});
return todoView;
});
\ No newline at end of file
{{#tasks}}
<li class="item {{#complete}}complete{{/complete}}" data-id="{{id}}">
{{#edit}}
<div class="edit" style="display:block;">
<input class="todo-input" type="text" value="{{name}}" />
</div>
{{/edit}}
{{^edit}}
<div class="view">
<input class="toggle" type="checkbox" {{#complete}}checked="checked"{{/complete}}>
<label>{{name}}</label>
<button class="destroy"></button>
</div>
<input class="edit" value="{{name}}">
{{/edit}}
</li>
{{/tasks}}
\ 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