Commit e6f14976 authored by Oscar Godson's avatar Oscar Godson Committed by Stephen Sawchuk

TodoMVC app in vanilla JS. No, that's not a framework.

parent 8ea9aef8
{
"name": "todomvc-vanillajs",
"version": "0.0.0",
"dependencies": {
"todomvc-common": "~0.1.4",
"director": "~1.2.0"
}
}
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 @@ ...@@ -4,10 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>VanillaJS • TodoMVC</title> <title>VanillaJS • TodoMVC</title>
<link rel="stylesheet" href="../../assets/base.css"> <link rel="stylesheet" href="components/todomvc-common/base.css">
<!--[if IE]>
<script src="../../assets/ie.js"></script>
<![endif]-->
</head> </head>
<body> <body>
<section id="todoapp"> <section id="todoapp">
...@@ -22,16 +19,31 @@ ...@@ -22,16 +19,31 @@
</section> </section>
<footer id="footer"> <footer id="footer">
<span id="todo-count"></span> <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> <button id="clear-completed">Clear completed</button>
</footer> </footer>
</section> </section>
<footer id="info"> <footer id="info">
<p>Double-click to edit a todo</p> <p>Double-click to edit a todo</p>
<p>Created by <a href="http://twitter.com/ffesseler">Florian Fesseler</a></p> <p>Created by <a href="http://twitter.com/oscargodson">Oscar Godson</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>
</footer> </footer>
<script src="../../assets/base.js"></script> <script src="components/todomvc-common/base.js"></script>
<script src="components/director/build/director.js"></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> <script src="js/app.js"></script>
</body> </body>
</html> </html>
/*global Store, Model, View, Controller, $$ */
(function () { (function () {
'use strict'; 'use strict';
var todos = [], /**
stat = {}, * Sets up a brand new Todo list.
ENTER_KEY = 13; *
* @param {string} name The name of your new to do list.
window.addEventListener('load', windowLoadHandler, false); */
function Todo(name) {
function Todo(title, completed) { this.storage = new Store(name);
this.id = getUuid(); this.model = new Model(this.storage);
this.title = title; this.view = new View();
this.completed = completed; this.controller = new Controller(this.model, this.view);
} }
function Stat() { var todo = new Todo('todos-vanillajs');
this.todoLeft = 0;
this.todoCompleted = 0; /**
this.totalTodo = 0; * Finds the model ID of the clicked DOM element
} *
* @param {object} target The starting point in the DOM for it to try to find
function windowLoadHandler() { * the ID of the model.
loadTodos(); */
refreshData(); function lookupId(target) {
addEventListeners(); var lookup = target;
}
while (lookup.nodeName !== 'LI') {
function addEventListeners() { lookup = lookup.parentNode;
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);
}
} }
}
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++) { return lookup.dataset.id;
if (todos[i].id === id) {
return todos[i];
}
}
}
function refreshData() {
saveTodos();
computeStats();
redrawTodosUI();
redrawStatsUI();
changeToggleAllCheckboxState();
} }
function saveTodos() { // When the enter key is pressed fire the addItem method.
localStorage.setItem('todos-vanillajs', JSON.stringify(todos)); $$('#new-todo').addEventListener('keypress', function (e) {
} todo.controller.addItem(e);
});
function computeStats() {
var i, l;
stat = new Stat(); // A delegation event. Will check what item was clicked whenever you click on any
stat.totalTodo = todos.length; // 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 you click a destroy button
if (todos[i].completed) { if (target.className.indexOf('destroy') > -1) {
stat.todoCompleted++; todo.controller.removeItem(lookupId(target));
}
} }
stat.todoLeft = stat.totalTodo - stat.todoCompleted; // If you click the checkmark
} if (target.className.indexOf('toggle') > -1) {
todo.controller.toggleComplete(lookupId(target), target);
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);
} }
}
function changeToggleAllCheckboxState() { });
var toggleAll = document.getElementById('toggle-all');
toggleAll.checked = stat.todoCompleted === todos.length; $$('#todo-list').addEventListener('dblclick', function (e) {
} var target = e.target;
function redrawStatsUI() {
removeChildren(document.getElementsByTagName('footer')[0]);
document.getElementById('footer').style.display = todos.length ? 'block' : 'none';
if (stat.todoCompleted) {
drawTodoClear();
}
if (stat.totalTodo) { if (target.nodeName === 'LABEL') {
drawTodoCount(); 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 $$('#toggle-all').addEventListener('click', function (e) {
number.innerHTML = stat.todoLeft; todo.controller.toggleAll(e);
});
remaining.id = 'todo-count'; $$('#clear-completed').addEventListener('click', function () {
remaining.appendChild(number); todo.controller.removeCompletedItems();
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;
}
})(); })();
/*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) {
this.model.create(e.target.value, function (data) {
// We want to make sure we don't add incomplete
// items to the completed tab when you go to
// add an item and you're viewing the completed
// items
if (this._getCurrentPage() !== 'completed') {
this.$todoList.innerHTML = this.$todoList.innerHTML + this.view.show(data);
}
input.value = '';
}.bind(this));
}
this._filter();
};
/**
* 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.
*/
Controller.prototype._filter = function () {
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 (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.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.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) {
var data = JSON.parse(localStorage[this._dbName]).todos;
var items = [];
var found;
callback = callback || function () {};
for (var i = 0; i < data.length; i++) {
for (var q in query) {
if (query[q] !== data[i][q]) {
found = false;
break;
} else {
found = true;
}
}
if (found) {
items.push(data[i]);
}
}
callback.call(this, items);
};
/**
* 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.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.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