/// <reference path="/Scripts/jquery-1.12.4.js"/>
jQuery(function ($) {
"use strict";
var AppStorage = {
read: function () {
/// <returns type="Array"/>
return JSON.parse(localStorage.getItem("todoList") || "null") || [];
},
write: function (newList) {
localStorage.setItem("todoList", JSON.stringify(newList));
}
}, AppModel = {
todoList: null,
count: null,
doneCount: 0,
allDone: false
}, combineTransformations = (function () {
var promiseJoin = function (f, g) {
/// <param name="f" type="Function"/>
/// <param name="g" type="Function"/>
return function (x) {
/// <returns type="Promise"/>
return f(x).then(g);
};
}, idTransformation = function (x) {
var d = $.Deferred();
d.resolve(x);
return d.promise();
};
return function (transformations) {
/// <param name="transformations" type="Array"/>
var ret = idTransformation;
$.each(transformations || [], function (i, val) {
ret = promiseJoin(ret, val);
});
return ret;
};
})(), storageActionMap = {
precreate: function (list, item) {
/// <summary>returns the transformed item and list.</summary>
if (item.type !== "todo") {
return { list: list, item: item };
}
var val = item.value,
obj = { id: +new Date() };
$.extend(obj, val);
return {
list: [obj].concat(list),
item: {
action: "create",
value: obj,
id: obj.id,
type: "todo"
}
};
}, preupdate: function (list, item) {
if (item.type !== "todo") {
return { list: list, item: item };
}
var id = item.id,
retList = $.map(list, function (element, index) {
if (id !== element.id) { return element; }
return $.extend({}, element, item.value);
});
return {
list: retList,
item: {
action: "update",
value: item.value,
id: id,
type: "todo"
}
};
}, predestroy: function (list, item) {
/// <summary>returns the transformed item and list.</summary>
if (item.type !== "todo") {
return { list: list, item: item };
}
var id = item.id,
retList = $.grep(list, function (element) {
return id !== element.id;
});
return {
list: retList,
item: {
action: "destroy",
type: "todo",
id: id
}
};
}, initiate: function (list, item) {
/// <param name="list" type="Array">The todo list. Initiated from storage.</param>
if (item.type !== "todoList") {
return { list: list, item: item };
}
return {
list: list,
item: {
action: "fill",
type: "todoList",
value: list
}
}
}, clearcompleted: function (list, item) {
if (item.type !== "todoList") {
return { list: list, item: item };
}
var ret = [], retList = $.grep(list, function (element) {
if (element.done) {
ret.push({
action: "destroy",
type: "todo",
id: element.id
});
return false;
}
return true;
});
return {
list: retList,
item: ret
};
}, markall: function (list, item) {
if (item.type !== "todoList") {
return { list: list, item: item };
}
var ret = [], retList = $.map(list, function (element) {
var doneObj = { done: item.value };
if (element.done !== item.value) {
ret.push({
action: "update",
type: "todo",
id: element.id,
value: doneObj
});
return $.extend({}, element, doneObj);
}
return element;
});
return {
list: retList,
item: ret
};
}
}, storageTransformation = function (updates) {
/// <summary>Not implemented.</summary>
/// <param name="updates" type="Array"/>
var list = null,
should = false,
transformed = [].concat.apply([], $.map(updates, function (item, i) {
/// <summary>Updates the list and returns the item per iteration.</summary>
/// <param name="val" type="Object"/>
// Do something
var temp;
if (!item) {
return [];
}
if (item.action in storageActionMap) {
if (!should) {
should = true;
list = AppStorage.read();
}
temp = storageActionMap[item.action](list, item);
list = temp.list || list;
return temp.item || [];
}
return item;
})), d = $.Deferred();
if (should) {
AppStorage.write(list);
}
setTimeout(function () {
d.resolve(should ? transformed.concat({
type: "todoList",
action: "update",
value: list
}) : transformed);
}, 1);
return d.promise();
}, getHashSub = function (hash) {
return hash.replace(/^#(?:!\/)?/, "");
}, hashTest = function (done) {
/// <param name="done" type="Boolean"/>
switch (getHashSub(location.hash)) {
case "completed":
return done;
case "remaining":
return !done;
}
return true;
}, modelTransformation = (function () {
var list = [];
return function (updates) {
/// <summary>Not implemented. Update model and computed properties.</summary>
/// <param name="updates" type="Array"/>
var count, doneCount, ret = {
action: "update",
type: "counter"
}, should = false, prevList = list, retUpdates;
$.each(updates, function (index, item) {
if (/^(?:fill|update)$/.test(item.action) && item.type === "todoList") {
list = item.value;
return false;
}
});
retUpdates = [].concat.apply([], $.map(updates, function (update) {
var todoItem, prevItem;
if (update.type === "todo") {
if (update.id) {
todoItem = $.grep(list, function (todo, i) {
return todo.id === update.id;
})[0];
}
if (update.action === "cancel") {
return {
type: "todo",
action: "update",
id: update.id,
value: {
editing: false,
content: todoItem && todoItem.content
}
};
}
if (update.action === "create") {
if (!hashTest(update.value.done)) {
return [];
}
}
if (update.action === "update") {
if (todoItem && !hashTest(todoItem.done)) {
return {
type: "todo",
action: "destroy",
id: update.id
};
}
if (("done" in update.value) && hashTest(update.value.done)) {
prevItem = $.grep(prevList, function (todo, i) {
return todo.id === update.id;
})[0];
if (!hashTest(prevItem.done)) {
return {
type: "todo",
action: "viewinsert",
value: todoItem,
id: update.id
};
}
}
}
}
if (update.action === "update" && update.type === "hash") {
return [update, {
type: "todoList",
action: "viewfill",
value: $.grep(list, function (item, index) {
return hashTest(item.done);
})
}];
}
return update;
}));
AppModel.todoList = list;
count = list.length;
doneCount = $.grep(list, function (item) {
return item.done;
}).length;
retUpdates.push({
type: "allDone",
action: "update",
value: count > 0 && (doneCount === count)
});
if (count !== AppModel.count) {
AppModel.count = count;
should = true;
ret.count = count;
}
if (doneCount !== AppModel.doneCount) {
AppModel.doneCount = doneCount;
should = true;
ret.doneCount = doneCount;
}
return should ? retUpdates.concat(ret) : retUpdates;
};
})(), viewElements = {
todoForm: $("#todoForm"),
todoItemTemplate: $($("#todoItemSource").html()),
listArea: $("#listArea"),
clearCompleted: $("#clearCompleted"),
linkArea: $("#hashLinks"),
allMarker: $("#markAll")
}, viewActionMaps = (function () {
var makeTodoElement = function (itemValue) {
var item = viewElements.todoItemTemplate.clone();
item.attr("data-todo-id", itemValue.id);
if (itemValue.content) {
item.find("[data-role=content]").text(itemValue.content);
item.find("[data-role=todoInput]").val(itemValue.content);
}
if (itemValue.done) {
item.addClass("todo-item--done");
item.find("[data-role=doneCheck]").prop("checked", true);
}
return item.get(0);
};
return {
form: {
update: function (update) {
var formInput = viewElements.todoForm.find("[data-role=formInput]");
// alert(formInput.val());
formInput.val(update.value);
return update;
}
}, todo: {
create: function (update) {
viewElements.listArea.prepend($(makeTodoElement(update.value)).addClass("todo-item--animate").get(0));
}, viewinsert: function (update) {
var el = makeTodoElement(update.value), justOlder = viewElements.listArea.find("[data-todo-id]").filter(function () {
return update.id >= +$(this).attr("data-todo-id");
}).get(0);
if (justOlder) {
$(justOlder).before(el);
} else {
viewElements.listArea.prepend(el);
}
}, destroy: function (update) {
viewElements.listArea.find("[data-todo-id=" + update.id + "]").remove();
}, update: function (update) {
var updateValue = update.value,
elementUpdated = viewElements.listArea.find("[data-todo-id=" + update.id + "]");
if ("done" in updateValue) {
elementUpdated[updateValue.done ? "addClass" : "removeClass"]("todo-item--done");
elementUpdated.find("[data-role=doneCheck]").prop("checked", updateValue.done);
}
if ("editing" in updateValue) {
elementUpdated[updateValue.editing ? "addClass" : "removeClass"]("todo-item--editing");
}
if ("content" in updateValue) {
elementUpdated.find("[data-role=content]").text(updateValue.content);
elementUpdated.find("[data-role=todoInput]").val(updateValue.content);
}
}
}, todoList: {
viewfill: function (update) {
var items = update.value.slice(0);
items.sort(function (x, y) {
return y.id - x.id;
});
$($.map(items, function (todo, index) {
return makeTodoElement(todo);
})).prependTo(viewElements.listArea.empty());
}
}, counter: {
update: function (update) {
if ("doneCount" in update) {
viewElements.clearCompleted.text("Clear " + update.doneCount + " completed")[update.doneCount ? "show" : "hide"]();
}
if ("count" in update) {
viewElements.allMarker.parent("label")[update.count ? "show" : "hide"]();
}
}
}, hash: {
update: function () {
var match = getHashSub(location.hash) || "all";
viewElements.linkArea.find("[data-hash-link]").each(function (i, el) {
$(this)[$(this).attr("data-hash-link") === match ? "addClass" : "removeClass"]("todo-status__link--current");
});
}
}, allDone: {
update: function (update) {
viewElements.allMarker.prop("checked", update.value);
}
}
};
})(), viewTransformation = function (updates) {
/// <summary>Not implemented.</summary>
/// <param name="updates" type="Array"/>
return $.map(updates, function (update, index) {
var subMap = viewActionMaps[update.type], action = subMap && subMap[update.action];
if (action) {
return action(update);
}
return update;
});
}, performTransformations = combineTransformations([
storageTransformation,
modelTransformation,
viewTransformation
]);
viewElements.todoForm.on("submit", function () {
// alert(1);
var inputValue = $(this).find("[data-role=formInput]").val();
if (inputValue) {
performTransformations([
{
action: "update",
type: "form",
value: ""
}, {
action: "precreate",
type: "todo",
value: {
content: inputValue,
done: false
}
}
]);
}
return false;
});
viewElements.allMarker.on("change", function () {
performTransformations([
{
type: "todoList",
action: "markall",
value: $(this).prop("checked")
}
]);
});
viewElements.clearCompleted.on("click", function () {
performTransformations([
{
action: "clearcompleted",
type: "todoList"
}
]);
return false;
});
viewElements.listArea.on("click", "[data-role=destroyTodo]", function () {
var todoId = +$(this).parents("[data-todo-id]").attr("data-todo-id");
performTransformations([
{
action: "predestroy",
type: "todo",
id: todoId
}
]);
}).on("change", "[data-role=doneCheck]", function () {
var todoId = +$(this).parents("[data-todo-id]").attr("data-todo-id");
performTransformations([
{
action: "preupdate",
type: "todo",
id: todoId,
value: {
done: $(this).prop("checked")
}
}
]);
}).on("dblclick", "[data-role=content]", function () {
var todoId = +$(this).parents("[data-todo-id]").attr("data-todo-id");
performTransformations([
{
action: "update", // Update without syncing to storage
type: "todo",
id: todoId,
value: {
editing: true
}
}
]);
}).on("keyup", "[data-role=todoInput]", function (e) {
/// <param name="e" type="KeyboardEvent"/>
var todoId = +$(this).parents("[data-todo-id]").attr("data-todo-id"), that = this;
if (e.keyCode === 13) {
this.blur();
} if (e.keyCode === 27) {
$(this).data("canceled", true);
performTransformations([
{
action: "cancel",
type: "todo",
id: todoId
}
]).then(function () {
that.blur();
setTimeout(function () {
$(that).data("canceled", false);
}, 0);
});
}
}).on("blur", "[data-role=todoInput]", function (e) {
var todoId = +$(this).parents("[data-todo-id]").attr("data-todo-id"), inputValue = $(this).val();
if ($(this).data("canceled")) {
return;
}
performTransformations(inputValue ? [
{
action: "preupdate",
type: "todo",
id: todoId,
value: {
content: inputValue
}
}, {
action: "update",
type: "todo",
id: todoId,
value: {
editing: false
}
}
] : [
{
action: "cancel",
type: "todo",
id: todoId
}
]);
});
performTransformations([
{
type: "todoList",
action: "initiate"
}
]);
$(window).on("hashchange", function () {
performTransformations([
{
type: "hash",
action: "update"
}
]);
}).trigger("hashchange");
});
Update 2: Mark all as done added
Screenshot: [The same as above]
Code:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Todo list</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="/styles/todo.css" />
</head>
<body>
<div class="app-container">
<h1 class="app-title">Todo list</h1>
<div class="app-area">
<form class="todo-form" id="todoForm">
<label class="todo-form__label">
<input type="checkbox" id="markAll" />
</label>
<input class="todo-form__input" placeholder="What needs to be done…" data-role="formInput" />
<div class="todo-form__btnArea"><button type="submit" class="todo-form__btn">Add</button></div>
</form>
<ul class="todo-list" id="listArea"></ul>
</div>
<p class="todo-status">
<a href="#" id="clearCompleted" class="todo-status__link todo-status__clearLink"></a>
<span id="hashLinks">
<a href="#!/" data-hash-link="all" class="todo-status__link">All</a> · <a href="#!/completed" data-hash-link="completed" class="todo-status__link">Completed</a> · <a href="#!/remaining" data-hash-link="remaining" class="todo-status__link">Remaining</a>
</span>
</p>
</div>
<script id="todoItemSource" type="text/plain">
<li class="todo-item">
<label class="todo-item__label">
<input type="checkbox" data-role="doneCheck" />
</label>
<div class="todo-item__content">
<button type="button" class="todo-item__clear" data-role="destroyTodo"></button>
<div class="todo-item__text" data-role="content"></div>
<label class="todo-item__inputArea">
<input class="todo-item__input" data-role="todoInput" />
</label>
</div>
</li>
</script>
<script src="/Scripts/jquery-1.12.4.min.js"></script>
<script>
jQuery(function ($) {
"use strict";
var AppStorage = {
read: function () {
/// <returns type="Array"/>
return JSON.parse(localStorage.getItem("todoList") || "null") || [];
},
write: function (newList) {
localStorage.setItem("todoList", JSON.stringify(newList));
}
}, AppModel = {
todoList: null,
count: 0,
doneCount: 0,
allDone: false
}, combineTransformations = (function () {
var promiseJoin = function (f, g) {
/// <param name="f" type="Function"/>
/// <param name="g" type="Function"/>
return function (x) {
/// <returns type="Promise"/>
return f(x).then(g);
};
}, idTransformation = function (x) {
var d = $.Deferred();
d.resolve(x);
return d.promise();
};
return function (transformations) {
/// <param name="transformations" type="Array"/>
var ret = idTransformation;
$.each(transformations || [], function (i, val) {
ret = promiseJoin(ret, val);
});
return ret;
};
})(), storageActionMap = {
precreate: function (list, item) {
/// <summary>returns the transformed item and list.</summary>
if (item.type !== "todo") {
return { list: list, item: item };
}
var val = item.value,
obj = { id: +new Date() };
$.extend(obj, val);
return {
list: [obj].concat(list),
item: {
action: "create",
value: obj,
id: obj.id,
type: "todo"
}
};
}, preupdate: function (list, item) {
if (item.type !== "todo") {
return { list: list, item: item };
}
var id = item.id,
retList = $.map(list, function (element, index) {
if (id !== element.id) { return element; }
return $.extend({}, element, item.value);
});
return {
list: retList,
item: {
action: "update",
value: item.value,
id: id,
type: "todo"
}
};
}, predestroy: function (list, item) {
/// <summary>returns the transformed item and list.</summary>
if (item.type !== "todo") {
return { list: list, item: item };
}
var id = item.id,
retList = $.grep(list, function (element) {
return id !== element.id;
});
return {
list: retList,
item: {
action: "destroy",
type: "todo",
id: id
}
};
}, initiate: function (list, item) {
/// <param name="list" type="Array">The todo list. Initiated from storage.</param>
if (item.type !== "todoList") {
return { list: list, item: item };
}
return {
list: list,
item: {
action: "fill",
type: "todoList",
value: list
}
}
}, clearcompleted: function (list, item) {
if (item.type !== "todoList") {
return { list: list, item: item };
}
var ret = [], retList = $.grep(list, function (element) {
if (element.done) {
ret.push({
action: "destroy",
type: "todo",
id: element.id
});
return false;
}
return true;
});
return {
list: retList,
item: ret
};
}, markall: function (list, item) {
if (item.type !== "todoList") {
return { list: list, item: item };
}
var ret = [], retList = $.map(list, function (element) {
var doneObj = { done: item.value };
if (element.done !== item.value) {
ret.push({
action: "update",
type: "todo",
id: element.id,
value: doneObj
});
return $.extend({}, element, doneObj);
}
return element;
});
return {
list: retList,
item: ret
};
}
}, storageTransformation = function (updates) {
/// <summary>Not implemented.</summary>
/// <param name="updates" type="Array"/>
var list = null,
should = false,
transformed = [].concat.apply([], $.map(updates, function (item, i) {
/// <summary>Updates the list and returns the item per iteration.</summary>
/// <param name="val" type="Object"/>
// Do something
var temp;
if (!item) {
return [];
}
if (item.action in storageActionMap) {
if (!should) {
should = true;
list = AppStorage.read();
}
temp = storageActionMap[item.action](list, item);
list = temp.list || list;
return temp.item || [];
}
return item;
})), d = $.Deferred();
if (should) {
AppStorage.write(list);
}
setTimeout(function () {
d.resolve(should ? transformed.concat({
type: "todoList",
action: "update",
value: list
}) : transformed);
}, 1);
return d.promise();
}, getHashSub = function (hash) {
return hash.replace(/^#(?:!\/)?/, "");
}, hashTest = function (done) {
/// <param name="done" type="Boolean"/>
switch (getHashSub(location.hash)) {
case "completed":
return done;
case "remaining":
return !done;
}
return true;
}, modelTransformation = (function () {
var list = [];
return function (updates) {
/// <summary>Not implemented. Update model and computed properties.</summary>
/// <param name="updates" type="Array"/>
var count, doneCount, ret = {
action: "update",
type: "counter"
}, should = false, prevList = list, retUpdates;
$.each(updates, function (index, item) {
if (/^(?:fill|update)$/.test(item.action) && item.type === "todoList") {
list = item.value;
return false;
}
});
retUpdates = [].concat.apply([], $.map(updates, function (update) {
var todoItem, prevItem;
if (update.type === "todo") {
if (update.id) {
todoItem = $.grep(list, function (todo, i) {
return todo.id === update.id;
})[0];
}
if (update.action === "cancel") {
return {
type: "todo",
action: "update",
id: update.id,
value: {
editing: false,
content: todoItem && todoItem.content
}
};
}
if (update.action === "create") {
if (!hashTest(update.value.done)) {
return [];
}
}
if (update.action === "update") {
if (todoItem && !hashTest(todoItem.done)) {
return {
type: "todo",
action: "destroy",
id: update.id
};
}
if (("done" in update.value) && hashTest(update.value.done)) {
prevItem = $.grep(prevList, function (todo, i) {
return todo.id === update.id;
})[0];
if (!hashTest(prevItem.done)) {
return {
type: "todo",
action: "create",
value: todoItem
};
}
}
}
}
if (update.action === "update" && update.type === "hash") {
return [update, {
type: "todoList",
action: "viewfill",
value: $.grep(list, function (item, index) {
return hashTest(item.done);
})
}];
}
return update;
}));
AppModel.todoList = list;
count = list.length;
doneCount = $.grep(list, function (item) {
return item.done;
}).length;
retUpdates.push({
type: "allDone",
action: "update",
value: count > 0 && (doneCount === count)
});
if (count !== AppModel.count) {
AppModel.count = count;
should = true;
ret.count = count;
}
if (doneCount !== AppModel.doneCount) {
AppModel.doneCount = doneCount;
should = true;
ret.doneCount = doneCount;
}
return should ? retUpdates.concat(ret) : retUpdates;
};
})(), viewElements = {
todoForm: $("#todoForm"),
todoItemTemplate: $($("#todoItemSource").html()),
listArea: $("#listArea"),
clearCompleted: $("#clearCompleted"),
linkArea: $("#hashLinks"),
allMarker: $("#markAll")
}, viewActionMaps = (function () {
var makeTodoElement = function (itemValue) {
var item = viewElements.todoItemTemplate.clone();
item.attr("data-todo-id", itemValue.id);
if (itemValue.content) {
item.find("[data-role=content]").text(itemValue.content);
item.find("[data-role=todoInput]").val(itemValue.content);
}
if (itemValue.done) {
item.addClass("todo-item--done");
item.find("[data-role=doneCheck]").prop("checked", true);
}
return item.get(0);
};
return {
form: {
update: function (update) {
var formInput = viewElements.todoForm.find("[data-role=formInput]");
// alert(formInput.val());
formInput.val(update.value);
return update;
}
}, todo: {
create: function (update) {
if (hashTest(update.value.done)) {
viewElements.listArea.prepend($(makeTodoElement(update.value)).addClass("todo-item--animate").get(0));
}
}, destroy: function (update) {
viewElements.listArea.find("[data-todo-id=" + update.id + "]").remove();
}, update: function (update) {
var updateValue = update.value,
elementUpdated = viewElements.listArea.find("[data-todo-id=" + update.id + "]");
if ("done" in updateValue) {
elementUpdated[updateValue.done ? "addClass" : "removeClass"]("todo-item--done");
elementUpdated.find("[data-role=doneCheck]").prop("checked", updateValue.done);
}
if ("editing" in updateValue) {
elementUpdated[updateValue.editing ? "addClass" : "removeClass"]("todo-item--editing");
}
if ("content" in updateValue) {
elementUpdated.find("[data-role=content]").text(updateValue.content);
elementUpdated.find("[data-role=todoInput]").val(updateValue.content);
}
}
}, todoList: {
viewfill: function (update) {
var items = update.value.slice(0);
items.sort(function (x, y) {
return y.id - x.id;
});
$($.map(items, function (todo, index) {
return makeTodoElement(todo);
})).prependTo(viewElements.listArea.empty());
}
}, counter: {
update: function (update) {
if ("doneCount" in update) {
viewElements.clearCompleted.text("Clear " + update.doneCount + " completed")[update.doneCount ? "show" : "hide"]();
}
}
}, hash: {
update: function () {
var match = getHashSub(location.hash) || "all";
viewElements.linkArea.find("[data-hash-link]").each(function (i, el) {
$(this)[$(this).attr("data-hash-link") === match ? "addClass" : "removeClass"]("todo-status__link--current");
});
}
}, allDone: {
update: function (update) {
viewElements.allMarker.prop("checked", update.value);
}
}
};
})(), viewTransformation = function (updates) {
/// <summary>Not implemented.</summary>
/// <param name="updates" type="Array"/>
return $.map(updates, function (update, index) {
var subMap = viewActionMaps[update.type], action = subMap && subMap[update.action];
if (action) {
return action(update);
}
return update;
});
}, performTransformations = combineTransformations([
storageTransformation,
modelTransformation,
viewTransformation
]);
viewElements.todoForm.on("submit", function () {
// alert(1);
var inputValue = $(this).find("[data-role=formInput]").val();
if (inputValue) {
performTransformations([
{
action: "update",
type: "form",
value: ""
}, {
action: "precreate",
type: "todo",
value: {
content: inputValue,
done: false
}
}
]);
}
return false;
});
viewElements.allMarker.on("change", function () {
performTransformations([
{
type: "todoList",
action: "markall",
value: $(this).prop("checked")
}
]);
});
viewElements.clearCompleted.on("click", function () {
performTransformations([
{
action: "clearcompleted",
type: "todoList"
}
]);
return false;
});
viewElements.listArea.on("click", "[data-role=destroyTodo]", function () {
var todoId = +$(this).parents("[data-todo-id]").attr("data-todo-id");
performTransformations([
{
action: "predestroy",
type: "todo",
id: todoId
}
]);
}).on("change", "[data-role=doneCheck]", function () {
var todoId = +$(this).parents("[data-todo-id]").attr("data-todo-id");
performTransformations([
{
action: "preupdate",
type: "todo",
id: todoId,
value: {
done: $(this).prop("checked")
}
}
]);
}).on("dblclick", "[data-role=content]", function () {
var todoId = +$(this).parents("[data-todo-id]").attr("data-todo-id");
performTransformations([
{
action: "update", // Update without syncing to storage
type: "todo",
id: todoId,
value: {
editing: true
}
}
]);
}).on("keyup", "[data-role=todoInput]", function (e) {
/// <param name="e" type="KeyboardEvent"/>
var todoId = +$(this).parents("[data-todo-id]").attr("data-todo-id"), that = this;
if (e.keyCode === 13) {
this.blur();
} if (e.keyCode === 27) {
$(this).data("canceled", true);
performTransformations([
{
action: "cancel",
type: "todo",
id: todoId
}
]).then(function () {
that.blur();
setTimeout(function () {
$(that).data("canceled", false);
}, 0);
});
}
}).on("blur", "[data-role=todoInput]", function (e) {
var todoId = +$(this).parents("[data-todo-id]").attr("data-todo-id"), inputValue = $(this).val();
if ($(this).data("canceled")) {
return;
}
performTransformations(inputValue ? [
{
action: "preupdate",
type: "todo",
id: todoId,
value: {
content: inputValue
}
}, {
action: "update",
type: "todo",
id: todoId,
value: {
editing: false
}
}
] : [
{
action: "cancel",
type: "todo",
id: todoId
}
]);
});
performTransformations([
{
type: "todoList",
action: "initiate"
}
]);
$(window).on("hashchange", function () {
performTransformations([
{
type: "hash",
action: "update"
}
]);
}).trigger("hashchange");
});
</script>
</body>
</html>
Update 1: Without “mark all as done”
Screenshot:
Code:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Todo list</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="/styles/todo.css" />
</head>
<body>
<div class="app-container" id="appArea">
<h1 class="app-title">Todo list</h1>
<div class="app-area">
<form class="todo-form" id="todoForm">
<input class="todo-form__input" placeholder="What needs to be done…" data-role="formInput" />
<div class="todo-form__btnArea"><button type="submit" class="todo-form__btn">Add</button></div>
</form>
<ul class="todo-list" id="listArea"></ul>
</div>
<p class="todo-status">
<a href="#" id="clearCompleted" class="todo-status__link todo-status__clearLink"></a>
<span id="hashLinks">
<a href="#!/" data-hash-link="all" class="todo-status__link">All</a> · <a href="#!/completed" data-hash-link="completed" class="todo-status__link">Completed</a> · <a href="#!/remaining" data-hash-link="remaining" class="todo-status__link">Remaining</a>
</span>
</p>
</div>
<script id="todoItemSource" type="text/plain">
<li class="todo-item">
<label class="todo-item__label">
<input type="checkbox" data-role="doneCheck" />
</label>
<div class="todo-item__content">
<button type="button" class="todo-item__clear" data-role="destroyTodo"></button>
<div class="todo-item__text" data-role="content"></div>
<label class="todo-item__inputArea">
<input class="todo-item__input" data-role="todoInput" />
</label>
</div>
</li>
</script>
<script src="/Scripts/jquery-1.12.4.min.js"></script>
<script>
jQuery(function ($) {
"use strict";
var AppStorage = {
read: function () {
/// <returns type="Array"/>
return JSON.parse(localStorage.getItem("todoList") || "null") || [];
},
write: function (newList) {
localStorage.setItem("todoList", JSON.stringify(newList));
}
}, AppModel = {
todoList: null,
count: 0,
doneCount: 0
}, combineTransformations = (function () {
var promiseJoin = function (f, g) {
/// <param name="f" type="Function"/>
/// <param name="g" type="Function"/>
return function (x) {
/// <returns type="Promise"/>
return f(x).then(g);
};
}, idTransformation = function (x) {
var d = $.Deferred();
d.resolve(x);
return d.promise();
};
return function (transformations) {
/// <param name="transformations" type="Array"/>
var ret = idTransformation;
$.each(transformations || [], function (i, val) {
ret = promiseJoin(ret, val);
});
return ret;
};
})(), storageActionMap = {
precreate: function (list, item) {
/// <summary>returns the transformed item and list.</summary>
if (item.type !== "todo") {
return { list: list, item: item };
}
var val = item.value,
obj = { id: +new Date() };
$.extend(obj, val);
return {
list: [obj].concat(list),
item: {
action: "create",
value: obj,
id: obj.id,
type: "todo"
}
};
}, preupdate: function (list, item) {
if (item.type !== "todo") {
return { list: list, item: item };
}
var id = item.id,
retList = $.map(list, function (element, index) {
if (id !== element.id) { return element; }
return $.extend({}, element, item.value);
});
return {
list: retList,
item: {
action: "update",
value: item.value,
id: id,
type: "todo"
}
};
}, predestroy: function (list, item) {
/// <summary>returns the transformed item and list.</summary>
if (item.type !== "todo") {
return { list: list, item: item };
}
var id = item.id,
retList = $.grep(list, function (element) {
return id !== element.id;
});
return {
list: retList,
item: {
action: "destroy",
type: "todo",
id: id
}
};
}, initiate: function (list, item) {
/// <param name="list" type="Array">The todo list. Initiated from storage.</param>
if (item.type !== "todoList") {
return { list: list, item: item };
}
return {
list: list,
item: {
action: "fill",
type: "todoList",
value: list
}
}
}, clearcompleted: function (list, item) {
if (item.type !== "todoList") {
return { list: list, item: item };
}
var ret = [], retList = $.grep(list, function (element) {
if (element.done) {
ret.push({
action: "destroy",
type: "todo",
id: element.id
});
return false;
}
return true;
});
return {
list: retList,
item: ret
};
}
}, storageTransformation = function (updates) {
/// <summary>Not implemented.</summary>
/// <param name="updates" type="Array"/>
var list = null,
should = false,
transformed = [].concat.apply([], $.map(updates, function (item, i) {
/// <summary>Updates the list and returns the item per iteration.</summary>
/// <param name="val" type="Object"/>
// Do something
var temp;
if (!item) {
return [];
}
if (item.action in storageActionMap) {
if (!should) {
should = true;
list = AppStorage.read();
}
temp = storageActionMap[item.action](list, item);
list = temp.list || list;
return temp.item || [];
}
return item;
})), d = $.Deferred();
if (should) {
AppStorage.write(list);
}
setTimeout(function () {
d.resolve(should ? transformed.concat({
type: "todoList",
action: "update",
value: list
}) : transformed);
}, 1);
return d.promise();
}, getHashSub = function (hash) {
return hash.replace(/^#(?:!\/)?/, "");
}, hashTest = function (done) {
/// <param name="done" type="Boolean"/>
switch (getHashSub(location.hash)) {
case "completed":
return done;
case "remaining":
return !done;
}
return true;
}, modelTransformation = (function () {
var list = [];
return function (updates) {
/// <summary>Not implemented. Update model and computed properties.</summary>
/// <param name="updates" type="Array"/>
var count, doneCount, ret = {
action: "update",
type: "counter"
}, should = false, prevList = list, retUpdates;
$.each(updates, function (index, item) {
if (/^(?:fill|update)$/.test(item.action) && item.type === "todoList") {
list = item.value;
return false;
}
});
retUpdates = [].concat.apply([], $.map(updates, function (update) {
var todoItem, prevItem;
if (update.type === "todo") {
if (update.id) {
todoItem = $.grep(list, function (todo, i) {
return todo.id === update.id;
})[0];
}
if (update.action === "cancel") {
return {
type: "todo",
action: "update",
id: update.id,
value: {
editing: false,
content: todoItem && todoItem.content
}
};
}
if (update.action === "create") {
if (!hashTest(update.value.done)) {
return [];
}
}
if (update.action === "update") {
if (todoItem && !hashTest(todoItem.done)) {
return {
type: "todo",
action: "destroy",
id: update.id
};
}
if (("done" in update.value) && hashTest(update.value.done)) {
prevItem = $.grep(prevList, function (todo, i) {
return todo.id === update.id;
})[0];
if (!hashTest(prevItem.done)) {
return {
type: "todo",
action: "create",
value: todoItem
};
}
}
}
}
if (update.action === "update" && update.type === "hash") {
return [update, {
type: "todoList",
action: "viewfill",
value: $.grep(list, function (item, index) {
return hashTest(item.done);
})
}];
}
return update;
}));
AppModel.todoList = list;
count = list.length;
doneCount = $.grep(list, function (item) {
return item.done;
}).length;
if (count !== AppModel.count) {
AppModel.count = count;
should = true;
ret.count = count;
}
if (doneCount !== AppModel.doneCount) {
AppModel.doneCount = doneCount;
should = true;
ret.doneCount = doneCount;
}
return should ? retUpdates.concat(ret) : retUpdates;
};
})(), viewElements = {
todoForm: $("#todoForm"),
todoItemTemplate: $($("#todoItemSource").html()),
listArea: $("#listArea"),
clearCompleted: $("#clearCompleted"),
linkArea: $("#hashLinks")
}, viewActionMaps = (function () {
var makeTodoElement = function (itemValue) {
var item = viewElements.todoItemTemplate.clone();
item.attr("data-todo-id", itemValue.id);
if (itemValue.content) {
item.find("[data-role=content]").text(itemValue.content);
item.find("[data-role=todoInput]").val(itemValue.content);
}
if (itemValue.done) {
item.addClass("todo-item--done");
item.find("[data-role=doneCheck]").prop("checked", true);
}
return item.get(0);
};
return {
form: {
update: function (update) {
var formInput = viewElements.todoForm.find("[data-role=formInput]");
// alert(formInput.val());
formInput.val(update.value);
return update;
}
}, todo: {
create: function (update) {
if (hashTest(update.value.done)) {
viewElements.listArea.prepend($(makeTodoElement(update.value)).addClass("todo-item--animate").get(0));
}
}, destroy: function (update) {
viewElements.listArea.find("[data-todo-id=" + update.id + "]").remove();
}, update: function (update) {
var updateValue = update.value,
elementUpdated = viewElements.listArea.find("[data-todo-id=" + update.id + "]");
if ("done" in updateValue) {
elementUpdated[updateValue.done ? "addClass" : "removeClass"]("todo-item--done");
}
if ("editing" in updateValue) {
elementUpdated[updateValue.editing ? "addClass" : "removeClass"]("todo-item--editing");
}
if ("content" in updateValue) {
elementUpdated.find("[data-role=content]").text(updateValue.content);
elementUpdated.find("[data-role=todoInput]").val(updateValue.content);
}
}
}, todoList: {
viewfill: function (update) {
var items = update.value.slice(0);
items.sort(function (x, y) {
return y.id - x.id;
});
$($.map(items, function (todo, index) {
return makeTodoElement(todo);
})).prependTo(viewElements.listArea.empty());
}
}, counter: {
update: function (update) {
if ("doneCount" in update) {
viewElements.clearCompleted.text("Clear " + update.doneCount + " completed")[update.doneCount ? "show" : "hide"]();
}
}
}, hash: {
update: function () {
var match = getHashSub(location.hash)||"all";
viewElements.linkArea.find("[data-hash-link]").each(function (i, el) {
$(this)[$(this).attr("data-hash-link") === match ? "addClass" : "removeClass"]("todo-status__link--current");
});
}
}
};
})(), viewTransformation = function (updates) {
/// <summary>Not implemented.</summary>
/// <param name="updates" type="Array"/>
return $.map(updates, function (update, index) {
var subMap = viewActionMaps[update.type], action = subMap && subMap[update.action];
if (action) {
return action(update);
}
return update;
});
}, performTransformations = combineTransformations([
storageTransformation,
modelTransformation,
viewTransformation
]);
viewElements.todoForm.on("submit", function () {
// alert(1);
var inputValue = $(this).find("[data-role=formInput]").val();
if (inputValue) {
performTransformations([
{
action: "update",
type: "form",
value: ""
}, {
action: "precreate",
type: "todo",
value: {
content: inputValue,
done: false
}
}
]);
}
return false;
});
viewElements.clearCompleted.on("click", function () {
performTransformations([
{
action: "clearcompleted",
type: "todoList"
}
]);
return false;
});
viewElements.listArea.on("click", "[data-role=destroyTodo]", function () {
var todoId = +$(this).parents("[data-todo-id]").attr("data-todo-id");
performTransformations([
{
action: "predestroy",
type: "todo",
id: todoId
}
]);
}).on("change", "[data-role=doneCheck]", function () {
var todoId = +$(this).parents("[data-todo-id]").attr("data-todo-id");
performTransformations([
{
action: "preupdate",
type: "todo",
id: todoId,
value: {
done: $(this).prop("checked")
}
}
]);
}).on("dblclick", "[data-role=content]", function () {
var todoId = +$(this).parents("[data-todo-id]").attr("data-todo-id");
performTransformations([
{
action: "update", // Update without syncing to storage
type: "todo",
id: todoId,
value: {
editing: true
}
}
]);
}).on("keyup", "[data-role=todoInput]", function (e) {
/// <param name="e" type="KeyboardEvent"/>
var todoId = +$(this).parents("[data-todo-id]").attr("data-todo-id"), that = this;
if (e.keyCode === 13) {
this.blur();
} if (e.keyCode === 27) {
$(this).data("canceled", true);
performTransformations([
{
action: "cancel",
type: "todo",
id: todoId
}
]).then(function () {
that.blur();
setTimeout(function () {
$(that).data("canceled", false);
}, 0);
});
}
}).on("blur", "[data-role=todoInput]", function (e) {
var todoId = +$(this).parents("[data-todo-id]").attr("data-todo-id"), inputValue = $(this).val();
if ($(this).data("canceled")) {
return;
}
performTransformations([
inputValue ? {
action: "preupdate",
type: "todo",
id: todoId,
value: {
content: inputValue
}
} : null, {
action: "update",
type: "todo",
id: todoId,
value: {
editing: false
}
}
]);
});
performTransformations([
{
type: "todoList",
action: "initiate"
}
]);
$(window).on("hashchange", function () {
performTransformations([
{
type: "hash",
action: "update"
}
]);
}).trigger("hashchange");
});
</script>
</body>
</html>
Original idea
var combineTransformations = (function () {
var promiseJoin = function (f, g) {
/// <param name="f" type="Function"/>
/// <param name="g" type="Function"/>
return function (x) {
/// <returns type="Promise"/>
return f(x).then(g);
};
}, idTransformation = function (x) {
var d = $.Deferred();
d.resolve(x);
return d.promise();
};
return function (transformations) {
/// <param name="transformations" type="Array"/>
var ret = idTransformation;
$.each(transformations || [], function (i, val) {
ret = promiseJoin(ret, val);
});
return ret;
};
})(), storageTransformation = function (updates) {
/// <summary>Not implemented.</summary>
/// <param name="updates" type="Array"/>
return $.when.apply($, $.map(updates, function (val, i) {
/// <param name="val" type="Object"/>
// Do something
})).then(function () {
return [].concat.apply([], arguments);
});
}, modelTransformation = function (updates) {
/// <summary>Not implemented.</summary>
/// <param name="updates" type="Array"/>
}, viewTransformation = function (updates) {
/// <summary>Not implemented.</summary>
/// <param name="updates" type="Array"/>
}, appTransformations = combineTransformations([
storageTransformation,
modelTransformation,
viewTransformation
]);
// Assume implemented
// When any data has any update, just call
appTransformations([{
type: "Todo",
action: "Preupdate",
id: "2333",
value: {
content: "Learn jQuery."
}
}]);
So that the appTransformations() function would automatically pop whatever is updated to the storage, the model, and the view and let the transformers correctly transform the updates to the next level.
Update 3: Features & fixes
Screenshot:
Code:
HTML:
JavaScript:
Update 2: Mark all as done added
Screenshot: [The same as above]
Code:
Update 1: Without “mark all as done”
Screenshot:
Code:
Original idea
So that the
appTransformations()
function would automatically pop whatever is updated to the storage, the model, and the view and let the transformers correctly transform the updates to the next level.