Commit 29a5abcd authored by Addy Osmani's avatar Addy Osmani

Merge pull request #510 from OscarGodson/gh-pages

VanillaJS app rewrite - landed!
parents b8365f4b 3e59664c
{
"name": "todomvc-vanillajs",
"version": "0.0.0",
"dependencies": {
"todomvc-common": "~0.1.4",
"director": "~1.2.0"
}
}
//
// Generated on Sun Dec 16 2012 22:47:05 GMT-0500 (EST) by Nodejitsu, Inc (Using Codesurgeon).
// Version 1.1.9
//
(function (exports) {
/*
* browser.js: Browser specific functionality for director.
*
* (C) 2011, Nodejitsu Inc.
* MIT LICENSE
*
*/
if (!Array.prototype.filter) {
Array.prototype.filter = function(filter, that) {
var other = [], v;
for (var i = 0, n = this.length; i < n; i++) {
if (i in this && filter.call(that, v = this[i], i, this)) {
other.push(v);
}
}
return other;
};
}
if (!Array.isArray){
Array.isArray = function(obj) {
return Object.prototype.toString.call(obj) === '[object Array]';
};
}
var dloc = document.location;
function dlocHashEmpty() {
// Non-IE browsers return '' when the address bar shows '#'; Director's logic
// assumes both mean empty.
return dloc.hash === '' || dloc.hash === '#';
}
var listener = {
mode: 'modern',
hash: dloc.hash,
history: false,
check: function () {
var h = dloc.hash;
if (h != this.hash) {
this.hash = h;
this.onHashChanged();
}
},
fire: function () {
if (this.mode === 'modern') {
this.history === true ? window.onpopstate() : window.onhashchange();
}
else {
this.onHashChanged();
}
},
init: function (fn, history) {
var self = this;
this.history = history;
if (!Router.listeners) {
Router.listeners = [];
}
function onchange(onChangeEvent) {
for (var i = 0, l = Router.listeners.length; i < l; i++) {
Router.listeners[i](onChangeEvent);
}
}
//note IE8 is being counted as 'modern' because it has the hashchange event
if ('onhashchange' in window && (document.documentMode === undefined
|| document.documentMode > 7)) {
// At least for now HTML5 history is available for 'modern' browsers only
if (this.history === true) {
// There is an old bug in Chrome that causes onpopstate to fire even
// upon initial page load. Since the handler is run manually in init(),
// this would cause Chrome to run it twise. Currently the only
// workaround seems to be to set the handler after the initial page load
// http://code.google.com/p/chromium/issues/detail?id=63040
setTimeout(function() {
window.onpopstate = onchange;
}, 500);
}
else {
window.onhashchange = onchange;
}
this.mode = 'modern';
}
else {
//
// IE support, based on a concept by Erik Arvidson ...
//
var frame = document.createElement('iframe');
frame.id = 'state-frame';
frame.style.display = 'none';
document.body.appendChild(frame);
this.writeFrame('');
if ('onpropertychange' in document && 'attachEvent' in document) {
document.attachEvent('onpropertychange', function () {
if (event.propertyName === 'location') {
self.check();
}
});
}
window.setInterval(function () { self.check(); }, 50);
this.onHashChanged = onchange;
this.mode = 'legacy';
}
Router.listeners.push(fn);
return this.mode;
},
destroy: function (fn) {
if (!Router || !Router.listeners) {
return;
}
var listeners = Router.listeners;
for (var i = listeners.length - 1; i >= 0; i--) {
if (listeners[i] === fn) {
listeners.splice(i, 1);
}
}
},
setHash: function (s) {
// Mozilla always adds an entry to the history
if (this.mode === 'legacy') {
this.writeFrame(s);
}
if (this.history === true) {
window.history.pushState({}, document.title, s);
// Fire an onpopstate event manually since pushing does not obviously
// trigger the pop event.
this.fire();
} else {
dloc.hash = (s[0] === '/') ? s : '/' + s;
}
return this;
},
writeFrame: function (s) {
// IE support...
var f = document.getElementById('state-frame');
var d = f.contentDocument || f.contentWindow.document;
d.open();
d.write("<script>_hash = '" + s + "'; onload = parent.listener.syncHash;<script>");
d.close();
},
syncHash: function () {
// IE support...
var s = this._hash;
if (s != dloc.hash) {
dloc.hash = s;
}
return this;
},
onHashChanged: function () {}
};
var Router = exports.Router = function (routes) {
if (!(this instanceof Router)) return new Router(routes);
this.params = {};
this.routes = {};
this.methods = ['on', 'once', 'after', 'before'];
this.scope = [];
this._methods = {};
this._insert = this.insert;
this.insert = this.insertEx;
this.historySupport = (window.history != null ? window.history.pushState : null) != null
this.configure();
this.mount(routes || {});
};
Router.prototype.init = function (r) {
var self = this;
this.handler = function(onChangeEvent) {
var newURL = onChangeEvent && onChangeEvent.newURL || window.location.hash;
var url = self.history === true ? self.getPath() : newURL.replace(/.*#/, '');
self.dispatch('on', url);
};
listener.init(this.handler, this.history);
if (this.history === false) {
if (dlocHashEmpty() && r) {
dloc.hash = r;
} else if (!dlocHashEmpty()) {
self.dispatch('on', dloc.hash.replace(/^#/, ''));
}
}
else {
var routeTo = dlocHashEmpty() && r ? r : !dlocHashEmpty() ? dloc.hash.replace(/^#/, '') : null;
if (routeTo) {
window.history.replaceState({}, document.title, routeTo);
}
// Router has been initialized, but due to the chrome bug it will not
// yet actually route HTML5 history state changes. Thus, decide if should route.
if (routeTo || this.run_in_init === true) {
this.handler();
}
}
return this;
};
Router.prototype.explode = function () {
var v = this.history === true ? this.getPath() : dloc.hash;
if (v.charAt(1) === '/') { v=v.slice(1) }
return v.slice(1, v.length).split("/");
};
Router.prototype.setRoute = function (i, v, val) {
var url = this.explode();
if (typeof i === 'number' && typeof v === 'string') {
url[i] = v;
}
else if (typeof val === 'string') {
url.splice(i, v, s);
}
else {
url = [i];
}
listener.setHash(url.join('/'));
return url;
};
//
// ### function insertEx(method, path, route, parent)
// #### @method {string} Method to insert the specific `route`.
// #### @path {Array} Parsed path to insert the `route` at.
// #### @route {Array|function} Route handlers to insert.
// #### @parent {Object} **Optional** Parent "routes" to insert into.
// insert a callback that will only occur once per the matched route.
//
Router.prototype.insertEx = function(method, path, route, parent) {
if (method === "once") {
method = "on";
route = function(route) {
var once = false;
return function() {
if (once) return;
once = true;
return route.apply(this, arguments);
};
}(route);
}
return this._insert(method, path, route, parent);
};
Router.prototype.getRoute = function (v) {
var ret = v;
if (typeof v === "number") {
ret = this.explode()[v];
}
else if (typeof v === "string"){
var h = this.explode();
ret = h.indexOf(v);
}
else {
ret = this.explode();
}
return ret;
};
Router.prototype.destroy = function () {
listener.destroy(this.handler);
return this;
};
Router.prototype.getPath = function () {
var path = window.location.pathname;
if (path.substr(0, 1) !== '/') {
path = '/' + path;
}
return path;
};
function _every(arr, iterator) {
for (var i = 0; i < arr.length; i += 1) {
if (iterator(arr[i], i, arr) === false) {
return;
}
}
}
function _flatten(arr) {
var flat = [];
for (var i = 0, n = arr.length; i < n; i++) {
flat = flat.concat(arr[i]);
}
return flat;
}
function _asyncEverySeries(arr, iterator, callback) {
if (!arr.length) {
return callback();
}
var completed = 0;
(function iterate() {
iterator(arr[completed], function(err) {
if (err || err === false) {
callback(err);
callback = function() {};
} else {
completed += 1;
if (completed === arr.length) {
callback();
} else {
iterate();
}
}
});
})();
}
function paramifyString(str, params, mod) {
mod = str;
for (var param in params) {
if (params.hasOwnProperty(param)) {
mod = params[param](str);
if (mod !== str) {
break;
}
}
}
return mod === str ? "([._a-zA-Z0-9-]+)" : mod;
}
function regifyString(str, params) {
var matches, last = 0, out = "";
while (matches = str.substr(last).match(/[^\w\d\- %@&]*\*[^\w\d\- %@&]*/)) {
last = matches.index + matches[0].length;
matches[0] = matches[0].replace(/^\*/, "([_.()!\\ %@&a-zA-Z0-9-]+)");
out += str.substr(0, matches.index) + matches[0];
}
str = out += str.substr(last);
var captures = str.match(/:([^\/]+)/ig), length;
if (captures) {
length = captures.length;
for (var i = 0; i < length; i++) {
str = str.replace(captures[i], paramifyString(captures[i], params));
}
}
return str;
}
function terminator(routes, delimiter, start, stop) {
var last = 0, left = 0, right = 0, start = (start || "(").toString(), stop = (stop || ")").toString(), i;
for (i = 0; i < routes.length; i++) {
var chunk = routes[i];
if (chunk.indexOf(start, last) > chunk.indexOf(stop, last) || ~chunk.indexOf(start, last) && !~chunk.indexOf(stop, last) || !~chunk.indexOf(start, last) && ~chunk.indexOf(stop, last)) {
left = chunk.indexOf(start, last);
right = chunk.indexOf(stop, last);
if (~left && !~right || !~left && ~right) {
var tmp = routes.slice(0, (i || 1) + 1).join(delimiter);
routes = [ tmp ].concat(routes.slice((i || 1) + 1));
}
last = (right > left ? right : left) + 1;
i = 0;
} else {
last = 0;
}
}
return routes;
}
Router.prototype.configure = function(options) {
options = options || {};
for (var i = 0; i < this.methods.length; i++) {
this._methods[this.methods[i]] = true;
}
this.recurse = options.recurse || this.recurse || false;
this.async = options.async || false;
this.delimiter = options.delimiter || "/";
this.strict = typeof options.strict === "undefined" ? true : options.strict;
this.notfound = options.notfound;
this.resource = options.resource;
this.history = options.html5history && this.historySupport || false;
this.run_in_init = this.history === true && options.run_handler_in_init !== false;
this.every = {
after: options.after || null,
before: options.before || null,
on: options.on || null
};
return this;
};
Router.prototype.param = function(token, matcher) {
if (token[0] !== ":") {
token = ":" + token;
}
var compiled = new RegExp(token, "g");
this.params[token] = function(str) {
return str.replace(compiled, matcher.source || matcher);
};
};
Router.prototype.on = Router.prototype.route = function(method, path, route) {
var self = this;
if (!route && typeof path == "function") {
route = path;
path = method;
method = "on";
}
if (Array.isArray(path)) {
return path.forEach(function(p) {
self.on(method, p, route);
});
}
if (path.source) {
path = path.source.replace(/\\\//ig, "/");
}
if (Array.isArray(method)) {
return method.forEach(function(m) {
self.on(m.toLowerCase(), path, route);
});
}
path = path.split(new RegExp(this.delimiter));
path = terminator(path, this.delimiter);
this.insert(method, this.scope.concat(path), route);
};
Router.prototype.dispatch = function(method, path, callback) {
var self = this, fns = this.traverse(method, path, this.routes, ""), invoked = this._invoked, after;
this._invoked = true;
if (!fns || fns.length === 0) {
this.last = [];
if (typeof this.notfound === "function") {
this.invoke([ this.notfound ], {
method: method,
path: path
}, callback);
}
return false;
}
if (this.recurse === "forward") {
fns = fns.reverse();
}
function updateAndInvoke() {
self.last = fns.after;
self.invoke(self.runlist(fns), self, callback);
}
after = this.every && this.every.after ? [ this.every.after ].concat(this.last) : [ this.last ];
if (after && after.length > 0 && invoked) {
if (this.async) {
this.invoke(after, this, updateAndInvoke);
} else {
this.invoke(after, this);
updateAndInvoke();
}
return true;
}
updateAndInvoke();
return true;
};
Router.prototype.invoke = function(fns, thisArg, callback) {
var self = this;
if (this.async) {
_asyncEverySeries(fns, function apply(fn, next) {
if (Array.isArray(fn)) {
return _asyncEverySeries(fn, apply, next);
} else if (typeof fn == "function") {
fn.apply(thisArg, fns.captures.concat(next));
}
}, function() {
if (callback) {
callback.apply(thisArg, arguments);
}
});
} else {
_every(fns, function apply(fn) {
if (Array.isArray(fn)) {
return _every(fn, apply);
} else if (typeof fn === "function") {
return fn.apply(thisArg, fns.captures || []);
} else if (typeof fn === "string" && self.resource) {
self.resource[fn].apply(thisArg, fns.captures || []);
}
});
}
};
Router.prototype.traverse = function(method, path, routes, regexp, filter) {
var fns = [], current, exact, match, next, that;
function filterRoutes(routes) {
if (!filter) {
return routes;
}
function deepCopy(source) {
var result = [];
for (var i = 0; i < source.length; i++) {
result[i] = Array.isArray(source[i]) ? deepCopy(source[i]) : source[i];
}
return result;
}
function applyFilter(fns) {
for (var i = fns.length - 1; i >= 0; i--) {
if (Array.isArray(fns[i])) {
applyFilter(fns[i]);
if (fns[i].length === 0) {
fns.splice(i, 1);
}
} else {
if (!filter(fns[i])) {
fns.splice(i, 1);
}
}
}
}
var newRoutes = deepCopy(routes);
newRoutes.matched = routes.matched;
newRoutes.captures = routes.captures;
newRoutes.after = routes.after.filter(filter);
applyFilter(newRoutes);
return newRoutes;
}
if (path === this.delimiter && routes[method]) {
next = [ [ routes.before, routes[method] ].filter(Boolean) ];
next.after = [ routes.after ].filter(Boolean);
next.matched = true;
next.captures = [];
return filterRoutes(next);
}
for (var r in routes) {
if (routes.hasOwnProperty(r) && (!this._methods[r] || this._methods[r] && typeof routes[r] === "object" && !Array.isArray(routes[r]))) {
current = exact = regexp + this.delimiter + r;
if (!this.strict) {
exact += "[" + this.delimiter + "]?";
}
match = path.match(new RegExp("^" + exact));
if (!match) {
continue;
}
if (match[0] && match[0] == path && routes[r][method]) {
next = [ [ routes[r].before, routes[r][method] ].filter(Boolean) ];
next.after = [ routes[r].after ].filter(Boolean);
next.matched = true;
next.captures = match.slice(1);
if (this.recurse && routes === this.routes) {
next.push([ routes.before, routes.on ].filter(Boolean));
next.after = next.after.concat([ routes.after ].filter(Boolean));
}
return filterRoutes(next);
}
next = this.traverse(method, path, routes[r], current);
if (next.matched) {
if (next.length > 0) {
fns = fns.concat(next);
}
if (this.recurse) {
fns.push([ routes[r].before, routes[r].on ].filter(Boolean));
next.after = next.after.concat([ routes[r].after ].filter(Boolean));
if (routes === this.routes) {
fns.push([ routes["before"], routes["on"] ].filter(Boolean));
next.after = next.after.concat([ routes["after"] ].filter(Boolean));
}
}
fns.matched = true;
fns.captures = next.captures;
fns.after = next.after;
return filterRoutes(fns);
}
}
}
return false;
};
Router.prototype.insert = function(method, path, route, parent) {
var methodType, parentType, isArray, nested, part;
path = path.filter(function(p) {
return p && p.length > 0;
});
parent = parent || this.routes;
part = path.shift();
if (/\:|\*/.test(part) && !/\\d|\\w/.test(part)) {
part = regifyString(part, this.params);
}
if (path.length > 0) {
parent[part] = parent[part] || {};
return this.insert(method, path, route, parent[part]);
}
if (!part && !path.length && parent === this.routes) {
methodType = typeof parent[method];
switch (methodType) {
case "function":
parent[method] = [ parent[method], route ];
return;
case "object":
parent[method].push(route);
return;
case "undefined":
parent[method] = route;
return;
}
return;
}
parentType = typeof parent[part];
isArray = Array.isArray(parent[part]);
if (parent[part] && !isArray && parentType == "object") {
methodType = typeof parent[part][method];
switch (methodType) {
case "function":
parent[part][method] = [ parent[part][method], route ];
return;
case "object":
parent[part][method].push(route);
return;
case "undefined":
parent[part][method] = route;
return;
}
} else if (parentType == "undefined") {
nested = {};
nested[method] = route;
parent[part] = nested;
return;
}
throw new Error("Invalid route context: " + parentType);
};
Router.prototype.extend = function(methods) {
var self = this, len = methods.length, i;
function extend(method) {
self._methods[method] = true;
self[method] = function() {
var extra = arguments.length === 1 ? [ method, "" ] : [ method ];
self.on.apply(self, extra.concat(Array.prototype.slice.call(arguments)));
};
}
for (i = 0; i < len; i++) {
extend(methods[i]);
}
};
Router.prototype.runlist = function(fns) {
var runlist = this.every && this.every.before ? [ this.every.before ].concat(_flatten(fns)) : _flatten(fns);
if (this.every && this.every.on) {
runlist.push(this.every.on);
}
runlist.captures = fns.captures;
runlist.source = fns.source;
return runlist;
};
Router.prototype.mount = function(routes, path) {
if (!routes || typeof routes !== "object" || Array.isArray(routes)) {
return;
}
var self = this;
path = path || [];
if (!Array.isArray(path)) {
path = path.split(self.delimiter);
}
function insertOrMount(route, local) {
var rename = route, parts = route.split(self.delimiter), routeType = typeof routes[route], isRoute = parts[0] === "" || !self._methods[parts[0]], event = isRoute ? "on" : rename;
if (isRoute) {
rename = rename.slice((rename.match(new RegExp(self.delimiter)) || [ "" ])[0].length);
parts.shift();
}
if (isRoute && routeType === "object" && !Array.isArray(routes[route])) {
local = local.concat(parts);
self.mount(routes[route], local);
return;
}
if (isRoute) {
local = local.concat(rename.split(self.delimiter));
local = terminator(local, self.delimiter);
}
self.insert(event, local, routes[route]);
}
for (var route in routes) {
if (routes.hasOwnProperty(route)) {
insertOrMount(route, path.slice(0));
}
}
};
}(typeof exports === "object" ? exports : window));
\ 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: #eaeaea 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 input::-webkit-input-placeholder {
font-style: italic;
}
#todoapp input:-moz-placeholder {
font-style: italic;
color: #a9a9a9;
}
#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;
}
#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-top-left-radius: 1px;
border-top-right-radius: 1px;
}
#new-todo,
.edit {
position: relative;
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);
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: -4px;
width: 40px;
text-align: center;
border: none; /* Mobile Safari */
}
#toggle-all:before {
content: '»';
font-size: 28px;
color: #d9d9d9;
padding: 0 25px 7px;
}
#toggle-all:checked:before {
color: #737373;
}
#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: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
/*-moz-appearance: none;*/
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
#todo-list li .toggle:after {
content: '✔';
line-height: 43px; /* 40 + a couple of pixels visual adjustment */
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;
padding: 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
-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.completed label {
color: #a9a9a9;
text-decoration: line-through;
}
#todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
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;
height: 20px;
z-index: 1;
text-align: center;
}
#footer:before {
content: '';
position: absolute;
right: 0;
bottom: 31px;
left: 0;
height: 50px;
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 43px 0 -6px rgba(255, 255, 255, 0.8),
0 44px 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;
position: relative;
line-height: 20px;
text-decoration: none;
background: rgba(0, 0, 0, 0.1);
font-size: 11px;
padding: 0 10px;
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;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox and Opera
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
#toggle-all,
#todo-list li .toggle {
background: none;
}
#todo-list li .toggle {
height: 40px;
}
#toggle-all {
top: -56px;
left: -15px;
width: 65px;
height: 41px;
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
}
.hidden{
display:none;
}
(function () {
'use strict';
if (location.hostname === 'todomvc.com') {
window._gaq = [['_setAccount','UA-31081062-1'],['_trackPageview']];(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src='//www.google-analytics.com/ga.js';s.parentNode.insertBefore(g,s)}(document,'script'));
}
function getSourcePath() {
// If accessed via addyosmani.github.io/todomvc/, strip the project path.
if (location.hostname.indexOf('github.io') > 0) {
return location.pathname.replace(/todomvc\//, '');
}
return location.pathname;
}
function appendSourceLink() {
var sourceLink = document.createElement('a');
var paragraph = document.createElement('p');
var footer = document.getElementById('info');
var urlBase = 'https://github.com/addyosmani/todomvc/tree/gh-pages';
if (footer) {
sourceLink.href = urlBase + getSourcePath();
sourceLink.appendChild(document.createTextNode('Check out the source'));
paragraph.appendChild(sourceLink);
footer.appendChild(paragraph);
}
}
function redirect() {
if (location.hostname === 'addyosmani.github.io') {
location.href = location.href.replace('addyosmani.github.io/todomvc', 'todomvc.com');
}
}
appendSourceLink();
redirect();
})();
......@@ -4,10 +4,7 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>VanillaJS • TodoMVC</title>
<link rel="stylesheet" href="../../assets/base.css">
<!--[if IE]>
<script src="../../assets/ie.js"></script>
<![endif]-->
<link rel="stylesheet" href="bower_components/todomvc-common/base.css">
</head>
<body>
<section id="todoapp">
......@@ -22,16 +19,35 @@
</section>
<footer id="footer">
<span id="todo-count"></span>
<ul id="filters">
<li>
<a href="#/">All</a>
</li>
<li>
<a href="#/active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
</li>
</ul>
<button id="clear-completed">Clear completed</button>
</footer>
</section>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="http://twitter.com/ffesseler">Florian Fesseler</a></p>
<p>Cleanup, edits by <a href="http://github.com/boushley">Aaron Boushley</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
<p>Created by <a href="http://twitter.com/oscargodson">Oscar Godson</a></p>
</footer>
<script src="../../assets/base.js"></script>
<script src="bower_components/todomvc-common/base.js"></script>
<script src="bower_components/director/build/director.js"></script>
<script>
// Bootstrap app data
window.app = {};
</script>
<script src="js/helpers.js"></script>
<script src="js/store.js"></script>
<script src="js/model.js"></script>
<script src="js/view.js"></script>
<script src="js/controller.js"></script>
<script src="js/app.js"></script>
</body>
</html>
/*global Store, Model, View, Controller, $$ */
(function () {
'use strict';
var todos = [],
stat = {},
ENTER_KEY = 13;
window.addEventListener('load', windowLoadHandler, false);
function Todo(title, completed) {
this.id = getUuid();
this.title = title;
this.completed = completed;
}
function Stat() {
this.todoLeft = 0;
this.todoCompleted = 0;
this.totalTodo = 0;
}
function windowLoadHandler() {
loadTodos();
refreshData();
addEventListeners();
}
function addEventListeners() {
document.getElementById('new-todo').addEventListener('keypress', newTodoKeyPressHandler, false);
document.getElementById('toggle-all').addEventListener('change', toggleAllChangeHandler, false);
}
function inputEditTodoKeyPressHandler(event) {
var inputEditTodo = event.target,
trimmedText = inputEditTodo.value.trim(),
todoId = event.target.id.slice(6);
if (trimmedText) {
if (event.keyCode === ENTER_KEY) {
editTodo(todoId, trimmedText);
}
} else {
removeTodoById(todoId);
refreshData();
}
}
function inputEditTodoBlurHandler(event) {
var inputEditTodo = event.target,
todoId = event.target.id.slice(6);
editTodo(todoId, inputEditTodo.value);
}
function newTodoKeyPressHandler(event) {
if (event.keyCode === ENTER_KEY) {
addTodo(document.getElementById('new-todo').value);
}
}
function toggleAllChangeHandler(event) {
for (var i in todos) {
todos[i].completed = event.target.checked;
}
refreshData();
}
function spanDeleteClickHandler(event) {
removeTodoById(event.target.getAttribute('data-todo-id'));
refreshData();
}
function hrefClearClickHandler() {
removeTodosCompleted();
refreshData();
}
function todoContentHandler(event) {
var todoId = event.target.getAttribute('data-todo-id'),
div = document.getElementById('li_' + todoId),
inputEditTodo = document.getElementById('input_' + todoId);
div.className = 'editing';
inputEditTodo.focus();
}
function checkboxChangeHandler(event) {
var checkbox = event.target,
todo = getTodoById(checkbox.getAttribute('data-todo-id'));
todo.completed = checkbox.checked;
refreshData();
}
function loadTodos() {
if (!localStorage.getItem('todos-vanillajs')) {
localStorage.setItem('todos-vanillajs', JSON.stringify([]));
}
todos = JSON.parse(localStorage.getItem('todos-vanillajs'));
}
function addTodo(text) {
var trimmedText = text.trim();
if (trimmedText) {
var todo = new Todo(trimmedText, false);
todos.push(todo);
refreshData();
}
}
function editTodo(todoId, text) {
var i, l;
for (i = 0, l = todos.length; i < l; i++) {
if (todos[i].id === todoId) {
todos[i].title = text;
}
}
refreshData();
}
function removeTodoById(id) {
var i = todos.length;
while (i--) {
if (todos[i].id === id) {
todos.splice(i, 1);
}
/**
* Sets up a brand new Todo list.
*
* @param {string} name The name of your new to do list.
*/
function Todo(name) {
this.storage = new app.Store(name);
this.model = new app.Model(this.storage);
this.view = new app.View();
this.controller = new app.Controller(this.model, this.view);
}
var todo = new Todo('todos-vanillajs');
/**
* Finds the model ID of the clicked DOM element
*
* @param {object} target The starting point in the DOM for it to try to find
* the ID of the model.
*/
function lookupId(target) {
var lookup = target;
while (lookup.nodeName !== 'LI') {
lookup = lookup.parentNode;
}
}
function removeTodosCompleted() {
var i = todos.length;
while (i--) {
console.log(i);
if (todos[i].completed) {
todos.splice(i, 1);
}
}
}
function getTodoById(id) {
var i, l;
for (i = 0, l = todos.length; i < l; i++) {
if (todos[i].id === id) {
return todos[i];
}
}
}
function refreshData() {
saveTodos();
computeStats();
redrawTodosUI();
redrawStatsUI();
changeToggleAllCheckboxState();
return lookup.dataset.id;
}
function saveTodos() {
localStorage.setItem('todos-vanillajs', JSON.stringify(todos));
}
function computeStats() {
var i, l;
// When the enter key is pressed fire the addItem method.
$$('#new-todo').addEventListener('keypress', function (e) {
todo.controller.addItem(e);
});
stat = new Stat();
stat.totalTodo = todos.length;
// A delegation event. Will check what item was clicked whenever you click on any
// part of a list item.
$$('#todo-list').addEventListener('click', function (e) {
var target = e.target;
for (i = 0, l = todos.length; i < l; i++) {
if (todos[i].completed) {
stat.todoCompleted++;
}
// If you click a destroy button
if (target.className.indexOf('destroy') > -1) {
todo.controller.removeItem(lookupId(target));
}
stat.todoLeft = stat.totalTodo - stat.todoCompleted;
}
function redrawTodosUI() {
var todo, checkbox, label, deleteLink, divDisplay, inputEditTodo, li, i, l,
ul = document.getElementById('todo-list');
document.getElementById('main').style.display = todos.length ? 'block' : 'none';
ul.innerHTML = '';
document.getElementById('new-todo').value = '';
for (i = 0, l = todos.length; i < l; i++) {
todo = todos[i];
// create checkbox
checkbox = document.createElement('input');
checkbox.className = 'toggle';
checkbox.setAttribute('data-todo-id', todo.id);
checkbox.type = 'checkbox';
checkbox.addEventListener('change', checkboxChangeHandler);
// create div text
label = document.createElement('label');
label.setAttribute('data-todo-id', todo.id);
label.appendChild(document.createTextNode(todo.title));
label.addEventListener('dblclick', todoContentHandler);
// create delete button
deleteLink = document.createElement('button');
deleteLink.className = 'destroy';
deleteLink.setAttribute('data-todo-id', todo.id);
deleteLink.addEventListener('click', spanDeleteClickHandler);
// create divDisplay
divDisplay = document.createElement('div');
divDisplay.className = 'view';
divDisplay.setAttribute('data-todo-id', todo.id);
divDisplay.appendChild(checkbox);
divDisplay.appendChild(label);
divDisplay.appendChild(deleteLink);
// create todo input
inputEditTodo = document.createElement('input');
inputEditTodo.id = 'input_' + todo.id;
inputEditTodo.className = 'edit';
inputEditTodo.value = todo.title;
inputEditTodo.addEventListener('keypress', inputEditTodoKeyPressHandler);
inputEditTodo.addEventListener('blur', inputEditTodoBlurHandler);
// create li
li = document.createElement('li');
li.id = 'li_' + todo.id;
li.appendChild(divDisplay);
li.appendChild(inputEditTodo);
if (todo.completed) {
li.className += 'completed';
checkbox.checked = true;
}
ul.appendChild(li);
// If you click the checkmark
if (target.className.indexOf('toggle') > -1) {
todo.controller.toggleComplete(lookupId(target), target);
}
}
function changeToggleAllCheckboxState() {
var toggleAll = document.getElementById('toggle-all');
});
toggleAll.checked = stat.todoCompleted === todos.length;
}
function redrawStatsUI() {
removeChildren(document.getElementsByTagName('footer')[0]);
document.getElementById('footer').style.display = todos.length ? 'block' : 'none';
if (stat.todoCompleted) {
drawTodoClear();
}
$$('#todo-list').addEventListener('dblclick', function (e) {
var target = e.target;
if (stat.totalTodo) {
drawTodoCount();
if (target.nodeName === 'LABEL') {
todo.controller.editItem(lookupId(target), target);
}
}
function drawTodoCount() {
var number = document.createElement('strong'),
remaining = document.createElement('span'),
text = ' ' + (stat.todoLeft === 1 ? 'item' : 'items') + ' left';
});
// create remaining count
number.innerHTML = stat.todoLeft;
$$('#toggle-all').addEventListener('click', function (e) {
todo.controller.toggleAll(e);
});
remaining.id = 'todo-count';
remaining.appendChild(number);
remaining.appendChild(document.createTextNode(text));
document.getElementsByTagName('footer')[0].appendChild(remaining);
}
function drawTodoClear() {
var buttonClear = document.createElement('button');
buttonClear.id = 'clear-completed';
buttonClear.addEventListener('click', hrefClearClickHandler);
buttonClear.innerHTML = 'Clear completed (' + stat.todoCompleted + ')';
document.getElementsByTagName('footer')[0].appendChild(buttonClear);
}
function removeChildren(node) {
node.innerHTML = '';
}
function getUuid() {
var i, random,
uuid = '';
for (i = 0; i < 32; i++) {
random = Math.random() * 16 | 0;
if (i === 8 || i === 12 || i === 16 || i === 20) {
uuid += '-';
}
uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random)).toString(16);
}
return uuid;
}
$$('#clear-completed').addEventListener('click', function () {
todo.controller.removeCompletedItems();
});
})();
/*global Router, $$, $ */
(function (window) {
'use strict';
/**
* Takes a model and view and acts as the controller between them
*
* @constructor
* @param {object} model The model constructor
* @param {object} view The view constructor
*/
function Controller(model, view) {
this.model = model;
this.view = view;
this.ENTER_KEY = 13;
this.ESCAPE_KEY = 27;
this.$main = $$('#main');
this.$toggleAll = $$('#toggle-all');
this.$todoList = $$('#todo-list');
this.$todoItemCounter = $$('#todo-count');
this.$clearCompleted = $$('#clear-completed');
this.$footer = $$('#footer');
this.router = new Router();
this.router.init();
window.addEventListener('load', function () {
this._updateFilterState();
}.bind(this));
// Couldn't figure out how to get flatiron to run some code on all pages. I
// tried '*', but then it overwrites ALL handlers for all the other pages
// and only runs this.
window.addEventListener('hashchange', function () {
this._updateFilterState();
}.bind(this));
// Make sure on page load we start with a hash to trigger the flatiron and
// onhashchange routes
if (window.location.href.indexOf('#') === -1) {
window.location.hash = '#/';
}
}
/**
* An event to fire on load. Will get all items and display them in the
* todo-list
*/
Controller.prototype.showAll = function () {
this.model.read(function (data) {
this.$todoList.innerHTML = this.view.show(data);
}.bind(this));
};
/**
* Renders all active tasks
*/
Controller.prototype.showActive = function () {
this.model.read({ completed: 0 }, function (data) {
this.$todoList.innerHTML = this.view.show(data);
}.bind(this));
};
/**
* Renders all completed tasks
*/
Controller.prototype.showCompleted = function () {
this.model.read({ completed: 1 }, function (data) {
this.$todoList.innerHTML = this.view.show(data);
}.bind(this));
};
/**
* An event to fire whenever you want to add an item. Simply pass in the event
* object and it'll handle the DOM insertion and saving of the new item.
*
* @param {object} e The event object
*/
Controller.prototype.addItem = function (e) {
var input = $$('#new-todo');
var title = title || '';
if (e.keyCode === this.ENTER_KEY) {
if (e.target.value.trim() === '') {
return;
}
this.model.create(e.target.value, function (data) {
input.value = '';
this._filter(true);
}.bind(this));
}
};
/**
* Hides the label text and creates an input to edit the title of the item.
* When you hit enter or blur out of the input it saves it and updates the UI
* with the new name.
*
* @param {number} id The id of the item to edit
* @param {object} label The label you want to edit the text of
*/
Controller.prototype.editItem = function (id, label) {
var li = label;
// This finds the <label>'s parent <li>
while (li.nodeName !== 'LI') {
li = li.parentNode;
}
var onSaveHandler = function () {
var value = input.value.trim();
var discarding = input.dataset.discard;
if (value.length && !discarding) {
this.model.update(id, { title: input.value });
// Instead of re-rendering the whole view just update
// this piece of it
label.innerHTML = value;
} else if (value.length === 0) {
// No value was entered in the input. We'll remove the todo item.
this.removeItem(id);
}
// Remove the input since we no longer need it
// Less DOM means faster rendering
li.removeChild(input);
// Remove the editing class
li.className = li.className.replace('editing', '');
}.bind(this);
// Append the editing class
li.className = li.className + ' editing';
var input = document.createElement('input');
input.className = 'edit';
// Get the innerHTML of the label instead of requesting the data from the
// ORM. If this were a real DB this would save a lot of time and would avoid
// a spinner gif.
input.value = label.innerHTML;
li.appendChild(input);
input.addEventListener('blur', onSaveHandler);
input.addEventListener('keypress', function (e) {
if (e.keyCode === this.ENTER_KEY) {
// Remove the cursor from the input when you hit enter just like if it
// were a real form
input.blur();
}
if (e.keyCode === this.ESCAPE_KEY) {
// Discard the changes
input.dataset.discard = true;
input.blur();
}
}.bind(this));
input.focus();
};
/**
* By giving it an ID it'll find the DOM element matching that ID,
* remove it from the DOM and also remove it from storage.
*
* @param {number} id The ID of the item to remove from the DOM and
* storage
*/
Controller.prototype.removeItem = function (id) {
this.model.remove(id, function () {
this.$todoList.removeChild($$('[data-id="' + id + '"]'));
}.bind(this));
this._filter();
};
/**
* Will remove all completed items from the DOM and storage.
*/
Controller.prototype.removeCompletedItems = function () {
this.model.read({ completed: 1 }, function (data) {
data.forEach(function (item) {
this.removeItem(item.id);
}.bind(this));
}.bind(this));
this._filter();
};
/**
* Give it an ID of a model and a checkbox and it will update the item
* in storage based on the checkbox's state.
*
* @param {number} id The ID of the element to complete or uncomplete
* @param {object} checkbox The checkbox to check the state of complete
* or not
* @param {boolean|undefined} silent Prevent re-filtering the todo items
*/
Controller.prototype.toggleComplete = function (id, checkbox, silent) {
var completed = checkbox.checked ? 1 : 0;
this.model.update(id, { completed: completed }, function () {
var listItem = $$('[data-id="' + id + '"]');
if (!listItem) {
return;
}
listItem.className = completed ? 'completed' : '';
// In case it was toggled from an event and not by clicking the checkbox
listItem.querySelector('input').checked = completed;
});
if (!silent) {
this._filter();
}
};
/**
* Will toggle ALL checkboxe's on/off state and completeness of models.
* Just pass in the event object.
*
* @param {object} e The event object
*/
Controller.prototype.toggleAll = function (e) {
var completed = e.target.checked ? 1 : 0;
var query = 0;
if (completed === 0) {
query = 1;
}
this.model.read({ completed: query }, function (data) {
data.forEach(function (item) {
this.toggleComplete(item.id, e.target, true);
}.bind(this));
}.bind(this));
this._filter();
};
/**
* Updates the pieces of the page which change depending on the remaining
* number of todos.
*/
Controller.prototype._updateCount = function () {
var todos = this.model.getCount();
this.$todoItemCounter.innerHTML = this.view.itemCounter(todos.active);
this.$clearCompleted.innerHTML = this.view.clearCompletedButton(todos.completed);
this.$clearCompleted.style.display = todos.completed > 0 ? 'block' : 'none';
this.$toggleAll.checked = todos.completed === todos.total;
this._toggleFrame(todos);
};
/**
* The main body and footer elements should not be visible when there are no
* todos left.
*
* @param {object} todos Contains a count of all todos, and their statuses.
*/
Controller.prototype._toggleFrame = function (todos) {
var frameDisplay = this.$main.style.display;
var frameVisible = frameDisplay === 'block' || frameDisplay === '';
if (todos.total === 0 && frameVisible) {
this.$main.style.display = 'none';
this.$footer.style.display = 'none';
}
if (todos.total > 0 && !frameVisible) {
this.$main.style.display = 'block';
this.$footer.style.display = 'block';
}
};
/**
* Re-filters the todo items, based on the active route.
* @param {boolean|undefined} force forces a re-painting of todo items.
*/
Controller.prototype._filter = function (force) {
var activeRoute = this._activeRoute.charAt(0).toUpperCase() + this._activeRoute.substr(1);
// Update the elements on the page, which change with each completed todo
this._updateCount();
// If the last active route isn't "All", or we're switching routes, we
// re-create the todo item elements, calling:
// this.show[All|Active|Completed]();
if (force || this._lastActiveRoute !== 'All' || this._lastActiveRoute !== activeRoute) {
this['show' + activeRoute]();
}
this._lastActiveRoute = activeRoute;
};
/**
* Simply updates the filter nav's selected states
*/
Controller.prototype._updateFilterState = function () {
var currentPage = this._getCurrentPage() || '';
// Store a reference to the active route, allowing us to re-filter todo
// items as they are marked complete or incomplete.
this._activeRoute = currentPage;
if (currentPage === '') {
this._activeRoute = 'All';
}
this._filter();
// Remove all other selected states. We loop through all of them in case the
// UI gets in a funky state with two selected.
$('#filters .selected').each(function (item) {
item.className = '';
});
$$('#filters [href="#/' + currentPage + '"]').className = 'selected';
};
/**
* A getter for getting the current page
*/
Controller.prototype._getCurrentPage = function () {
return document.location.hash.split('/')[1];
};
// Export to window
window.app.Controller = Controller;
})(window);
(function (window) {
'use strict';
// Cache the querySelector/All for easier and faster reuse
window.$ = document.querySelectorAll.bind(document);
window.$$ = document.querySelector.bind(document);
// Allow for looping on Objects by chaining:
// $('.foo').each(function () {})
Object.prototype.each = function (callback) {
for (var x in this) {
if (this.hasOwnProperty(x)) {
callback.call(this, this[x]);
}
}
};
})(window);
(function (window) {
'use strict';
/**
* Creates a new Model instance and hooks up the storage.
*
* @constructor
* @param {object} storage A reference to the client side storage class
*/
function Model(storage) {
this.storage = storage;
}
/**
* Creates a new todo model
*
* @param {string} [title] The title of the task
* @param {function} [callback] The callback to fire after the model is created
*/
Model.prototype.create = function (title, callback) {
title = title || '';
callback = callback || function () {};
var newItem = {
title: title.trim(),
completed: 0
};
this.storage.save(newItem, callback);
};
/**
* Finds and returns a model in storage. If no query is given it'll simply
* return everything. If you pass in a string or number it'll look that up as
* the ID of the model to find. Lastly, you can pass it an object to match
* against.
*
* @param {string|number|object} [query] A query to match models against
* @param {function} [callback] The callback to fire after the model is found
*
* @example
* model.read(1, func); // Will find the model with an ID of 1
* model.read('1'); // Same as above
* //Below will find a model with foo equalling bar and hello equalling world.
* model.read({ foo: 'bar', hello: 'world' });
*/
Model.prototype.read = function (query, callback) {
var queryType = typeof query;
callback = callback || function () {};
if (queryType === 'function') {
callback = query;
return this.storage.findAll(callback);
} else if (queryType === 'string' || queryType === 'number') {
this.storage.find({ id: query }, callback);
} else {
this.storage.find(query, callback);
}
};
/**
* Updates a model by giving it an ID, data to update, and a callback to fire when
* the update is complete.
*
* @param {number} id The id of the model to update
* @param {object} data The properties to update and their new value
* @param {function} callback The callback to fire when the update is complete.
*/
Model.prototype.update = function (id, data, callback) {
this.storage.save(id, data, callback);
};
/**
* Removes a model from storage
*
* @param {number} id The ID of the model to remove
* @param {function} callback The callback to fire when the removal is complete.
*/
Model.prototype.remove = function (id, callback) {
this.storage.remove(id, callback);
};
/**
* WARNING: Will remove ALL data from storage.
*
* @param {function} callback The callback to fire when the storage is wiped.
*/
Model.prototype.removeAll = function (callback) {
this.storage.drop(callback);
};
/**
* Returns a count of all todos
*/
Model.prototype.getCount = function () {
var todos = {
active: 0,
completed: 0,
total: 0
};
this.storage.findAll(function (data) {
data.each(function (todo) {
if (todo.completed === 1) {
todos.completed++;
} else {
todos.active++;
}
todos.total++;
});
});
return todos;
};
// Export to window
window.app.Model = Model;
})(window);
/*jshint eqeqeq:false */
(function (window) {
'use strict';
/**
* Creates a new client side storage object and will create an empty
* collection if no collection already exists.
*
* @param {string} name The name of our DB we want to use
* @param {function} callback Our fake DB uses callbacks because in
* real life you probably would be making AJAX calls
*/
function Store(name, callback) {
var data;
var dbName;
callback = callback || function () {};
dbName = this._dbName = name;
if (!localStorage[dbName]) {
data = {
todos: []
};
localStorage[dbName] = JSON.stringify(data);
}
callback.call(this, JSON.parse(localStorage[dbName]));
}
/**
* Finds items based on a query given as a JS object
*
* @param {object} query The query to match against (i.e. {foo: 'bar'})
* @param {function} callback The callback to fire when the query has
* completed running
*
* @example
* db.find({foo: 'bar', hello: 'world'}, function (data) {
* // data will return any items that have foo: bar and
* // hello: world in their properties
* });
*/
Store.prototype.find = function (query, callback) {
if (!callback) {
return;
}
var todos = JSON.parse(localStorage[this._dbName]).todos;
callback.call(this, todos.filter(function (todo) {
for (var q in query) {
return query[q] === todo[q];
}
}));
};
/**
* Will retrieve all data from the collection
*
* @param {function} callback The callback to fire upon retrieving data
*/
Store.prototype.findAll = function (callback) {
callback = callback || function () {};
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
};
/**
* Will save the given data to the DB. If no item exists it will create a new
* item, otherwise it'll simply update an existing item's properties
*
* @param {number} id An optional param to enter an ID of an item to update
* @param {object} data The data to save back into the DB
* @param {function} callback The callback to fire after saving
*/
Store.prototype.save = function (id, updateData, callback) {
var data = JSON.parse(localStorage[this._dbName]);
var todos = data.todos;
callback = callback || function () {};
// If an ID was actually given, find the item and update each property
if (typeof id !== 'object') {
for (var i = 0; i < todos.length; i++) {
if (todos[i].id == id) {
for (var x in updateData) {
todos[i][x] = updateData[x];
}
}
}
localStorage[this._dbName] = JSON.stringify(data);
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
} else {
callback = updateData;
updateData = id;
// Generate an ID
updateData.id = new Date().getTime();
todos.push(updateData);
localStorage[this._dbName] = JSON.stringify(data);
callback.call(this, [updateData]);
}
};
/**
* Will remove an item from the Store based on its ID
*
* @param {number} id The ID of the item you want to remove
* @param {function} callback The callback to fire after saving
*/
Store.prototype.remove = function (id, callback) {
var data = JSON.parse(localStorage[this._dbName]);
var todos = data.todos;
for (var i = 0; i < todos.length; i++) {
if (todos[i].id == id) {
todos.splice(i, 1);
break;
}
}
localStorage[this._dbName] = JSON.stringify(data);
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
};
/**
* Will drop all storage and start fresh
*
* @param {function} callback The callback to fire after dropping the data
*/
Store.prototype.drop = function (callback) {
localStorage[this._dbName] = JSON.stringify({todos: []});
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
};
// Export to window
window.app.Store = Store;
})(window);
/*jshint laxbreak:true */
(function (window) {
'use strict';
/**
* Sets up defaults for all the View methods such as a default template
*
* @constructor
*/
function View() {
this.defaultTemplate
= '<li data-id="{{id}}" class="{{completed}}">'
+ '<div class="view">'
+ '<input class="toggle" type="checkbox" {{checked}}>'
+ '<label>{{title}}</label>'
+ '<button class="destroy"></button>'
+ '</div>'
+ '</li>';
}
/**
* Creates an <li> HTML string and returns it for placement in your app.
*
* NOTE: In real life you should be using a templating engine such as Mustache
* or Handlebars, however, this is a vanilla JS example.
*
* @param {object} data The object containing keys you want to find in the
* template to replace.
* @returns {string} HTML String of an <li> element
*
* @example
* view.show({
* id: 1,
* title: "Hello World",
* completed: 0,
* });
*/
View.prototype.show = function (data) {
var i, l;
var view = '';
for (i = 0, l = data.length; i < l; i++) {
var template = this.defaultTemplate;
var completed = '';
var checked = '';
if (data[i].completed === 1) {
completed = 'completed';
checked = 'checked';
}
template = template.replace('{{id}}', data[i].id);
template = template.replace('{{title}}', data[i].title);
template = template.replace('{{completed}}', completed);
template = template.replace('{{checked}}', checked);
view = view + template;
}
return view;
};
/**
* Displays a counter of how many to dos are left to complete
*
* @param {number} activeTodos The number of active todos.
* @returns {string} String containing the count
*/
View.prototype.itemCounter = function (activeTodos) {
var plural = activeTodos === 1 ? '' : 's';
return '<strong>' + activeTodos + '</strong> item' + plural + ' left';
};
/**
* Updates the text within the "Clear completed" button
*
* @param {[type]} completedTodos The number of completed todos.
* @returns {string} String containing the count
*/
View.prototype.clearCompletedButton = function (completedTodos) {
if (completedTodos > 0) {
return 'Clear completed (' + completedTodos + ')';
} else {
return '';
}
};
// Export to window
window.app.View = View;
})(window);
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