1.项目结构
2.源码分析
taskAtHand.html
<!DOCTYPE html>
<html>
<head>
<title>Task@Hand</title>
<link href="taskAtHand.css" rel="StyleSheet" type="text/css" />
<link id="theme-style" href="themes/blue.css" rel="StyleSheet" type="text/css" />
<script src="lib/jquery-1.10.2.js" type="text/javascript"></script>
<script src="appStorage.js" type="text/javascript"></script>
<script src="taskAtHand.js" type="text/javascript"></script>
</head>
<body>
<div id="app">
<header>Task@Hand</header>
<div id="toolbar">
<label for="theme">Theme</label>
<select id="theme" title="Select theme">
<option value="blue">Blue</option>
<option value="green">Green</option>
<option value="brown">Brown</option>
<option value="red">Red</option>
</select>
</div>
<div id="main">
<div id="add-task">
<label for="new-task-name">Add a task</label>
<input type="text" id="new-task-name" title="Enter a task name" placeholder="Enter a task name" autofocus/>
</div>
<ul id="task-list">
</ul>
</div>
<footer>
</footer>
</div>
<div id="templates" class="hidden">
<ul id="task-template">
<li class="task">
<button class="toggle-details">+</button>
<span class="task-name"></span>
<input type="text" class="task-name hidden"/>
<div class="tools">
<button class="delete" title="Delete">X</button>
<button class="move-up" title="Up">^</button>
<button class="move-down" title="Down">v</button>
</div>
<div class="details">
<label>Start date:</label>
<input type="date"/><br/>
<label>Due date:</label>
<input type="date"/><br/>
<label>Status:</label>
<select>
<option value="0">None</option>
<option value="1">Not Started</option>
<option value="2">Started</option>
<option value="3">Completed</option>
</select><br/>
<label>Priority:</label>
<select>
<option value="0">None</option>
<option value="1">Low</option>
<option value="2">Normal</option>
<option value="3">High</option>
</select><br/>
<label>% Complete:</label>
<input type="number" min="0" max="100" step="10" value="0"/>
</div>
</li>
</ul>
</div>
</body>
</html>
taskAtHand.js
"use strict";
function TaskAtHandApp()
{
var version = "v3.1",
appStorage = new AppStorage("taskAtHand");
function setStatus (message)
{
$("app>footer").text(message);
}
function saveTaskList()
{
var tasks = [];
$("#task-list .task span.task-name").each(function() {
tasks.push($(this).text())
});
appStorage.setValue("taskList", tasks);
}
function addTask()
{
var taskName = $("#new-task-name").val();
if (taskName)
{
addTaskElement(taskName);
// Reset the field
$("#new-task-name").val("").focus();
saveTaskList();
}
}
function addTaskElement(taskName)
{
var $task = $("#task-template .task").clone();
$("span.task-name", $task).text(taskName);
$("#task-list").append($task);
// Task events
$task.click(function() { onSelectTask($task); });
// Button events
$("button.delete", $task).click(function() { removeTask($task); });
$("button.move-up", $task).click(function() { moveTask($task, true); });
$("button.move-down", $task).click(function() { moveTask($task, false); });
$("button.toggle-details", $task).click(function() { toggleDetails($task); });
// Task name events
$("span.task-name", $task).click(function() { onEditTaskName($(this)); });
$("input.task-name", $task).change(function() { onChangeTaskName($(this)); })
.blur(function() { $(this).hide().siblings("span.task-name").show(); });
}
//to toggle visibility of details
function toggleDetails($task)
{
$(".details", $task).slideToggle();
$("button.toggle-details", $task).toggleClass("expanded");
}
function removeTask($task)
{
$task.remove();
saveTaskList();
}
function moveTask($task, moveUp)
{
if (moveUp)
{
$task.insertBefore($task.prev());
}
else
{
$task.insertAfter($task.next());
}
saveTaskList();
}
function onSelectTask($task)
{
if ($task)
{
// Unselect other tasks
$task.siblings(".selected").removeClass("selected");
// Select this task
$task.addClass("selected");
}
}
function onEditTaskName($span)
{
$span.hide()
.siblings("input.task-name").val($span.text()).show().focus();
}
function onChangeTaskName($input)
{
$input.hide();
var $span = $input.siblings("span.task-name");
if ($input.val())
{
$span.text($input.val());
saveTaskList();
}
$span.show();
}
function loadTaskList()
{
var tasks = appStorage.getValue("taskList");
if (tasks)
{
for (var i in tasks)
{
addTaskElement(tasks[i]);
}
}
}
function onChangeTheme()
{
var theme = $("#theme>option").filter(":selected").val();
setTheme(theme);
appStorage.setValue("theme", theme);
}
/*gets the <link id="theme-style"> element and changes its href
attribute to the new stylesheet's URL */
/*when page loads, new stylesheet loads and apply its styles over
the existing ones*/
function setTheme(theme)
{
$("#theme-style").attr("href", "themes/" + theme + ".css");
}
/*gets theme name from localStorage*/
function loadTheme()
{
var theme = appStorage.getValue("theme");
if (theme)
{
setTheme(theme);
$("#theme>option[value=" + theme + "]").attr("selected","selected");
}
}
this.start = function()
{
$("#new-task-name").keypress(function(e)
{
if (e.which == 13) // Enter key
{
addTask();
return false;
}
})
.focus();
loadTheme();
$("#theme").change(onChangeTheme);
$("#app>header").append(version);
loadTaskList();
setStatus("ready");
};
}
$(function()
{
window.app = new TaskAtHandApp();
window.app.start();
});
taskAthand.css
body
{
font: 1em Verdana, Geneva, sans-serif;
padding: 0;
margin: 5px;
color: Black;
background-color: WhiteSmoke;
}
div
{
padding: 0;
margin: 0;
}
button
{
cursor: pointer;
}
.hidden
{
display: none;
}
/* The App */
#app
{
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow: auto;
margin: 4px;
background-color: #bbc;
background: -webkit-linear-gradient(top, #bbc, #558);
background: -moz-linear-gradient(top, #bbc, #558);
background: -ms-linear-gradient(top, #bbc, #558);
background: linear-gradient(top, #bbc, #558);
}
#app>header
{
padding: 0 0.5em;
font-size: 1.5em;
color: WhiteSmoke;
background-color: #006;
box-shadow: 2px 3px 3px rgba(0, 0, 0, 0.6);
}
#app>footer
{
position: absolute;
bottom: 0;
font-size: 0.75em;
padding: 0.25em;
color: WhiteSmoke;
background-color: #006;
}
#toolbar
{
padding: 0.25em;
font-size: 0.8em;
color: WhiteSmoke;
background-color: rgba(0, 0, 0, 0.4);
}
/*centers the input fields*/
#main
{
min-width: 9em;
max-width: 25em;
margin: 1em auto;
}
#add-task label
{
display: block;
font-weight: bold;
}
/*have the text input filled to take up the entire width of the main section*/
#new-task-name
{
font-size: 1em;
display: block;
width: 98%;
}
/*align the task-list width with the text input field*/
#task-list
{
padding: 0;
}
/*give a bg color, rounded corners, and a shadow*/
#task-list .task
{
position: relative;
list-style: none;
padding: 0.5em;
margin: 0.25em;
background-color: beige;
border-radius: 4px;
box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.6);
/*provide visual feedback to user when they select a task*/
-webkit-transition: all 0.25s ease;
-moz-transition: all 0.25s ease;
-o-transition: all 0.25s ease;
transition: all 0.25s ease;
}
#task-list .task:hover
{
background-color: white;
}
/*increase padding to make element bigger, add a border, and change bg color*/
#task-list .task.selected
{
padding: 0.6em 0.5em;
border: 2px solid orange;
border-radius: 6px;
background-color: white;
}
#task-list .task input.task-name
{
margin: 0;
font-size: 1em;
}
/*add a border around the task buttons to group them, and move them over
to the upper-right side of task element*/
#task-list .task .tools
{
position: absolute;
top: 0.25em;
right: 0.25em;
border: 1px solid black;
border-radius: 2px;
opacity: 0; /*hide the task action buttons until the user moves
the mouse over a task or selects a task*/
padding: 1px;
-webkit-transition: all 0.25s ease;
-moz-transition: all 0.25s ease;
-o-transition: all 0.25s ease;
transition: all 0.25s ease;
}
/*make the task buttons appear to fade in when the user hovers over a task*/
#task-list .task:hover .tools,
/*add a selector to make the task buttons show up when a task is selected*/
#task-list .task.selected .tools
{
opacity: 1;
}
/*replace arrows with icons*/
#task-list .task .tools button,
#task-list .task .toggle-details
{
margin: 0;
padding: 0;
width: 16px;
height: 16px;
background: url(images/icons.png) no-repeat;
border: none;
color: transparent;
}
#task-list .task .tools button.delete
{
background-position: 0 0;
}
#task-list .task .tools button.move-up
{
background-position: -16px 0;
}
#task-list .task .tools button.move-down
{
background-position: -32px 0;
}
#task-list .task button.toggle-details
{
background-position: 0 -16px;
}
#task-list .task button.toggle-details.expanded
{
background-position: -16px -16px;
}
#task-list .task .details
{
display: none;
background-color: gray;
color: white;
border-radius: 4px;
margin-top: 0.5em;
padding: .25em;
overflow: auto;
}
#task-list .task .details label
{
width: 8em;
text-align: right;
display: inline-block;
vertical-align: top;
font-size: .8em;
}
#task-list .task .details select
{
width: 8em;
}
appStorage.js
function AppStorage(appName)
{
var prefix = (appName ? appName + "." : "");
/** Determine if local storage available */
this.localStorageSupported = (('localStorage' in window) && window['localStorage']);
/** Sets the value with the specified key into localStorage */
this.setValue = function(key, val)
{
if (this.localStorageSupported) localStorage.setItem(prefix + key, JSON.stringify(val));
return this;
};
/**
* Gets the value with the specified key from localStorage
* @returns The value or null if not found
*/
this.getValue = function(key)
{
if (this.localStorageSupported) return JSON.parse(localStorage.getItem(prefix + key));
else return null;
};
/** Removes the value with the specified key */
this.removeValue = function(key)
{
if (this.localStorageSupported) localStorage.removeItem(prefix + key);
return this;
};
/** Removes all items associated with the app */
this.removeAll = function()
{
var keys = this.getKeys();
for (var i in keys)
{
this.remove(keys[i]);
}
return this;
};
/**
* If the specified key has a value in localStorage
* @returns True if the key has a value
*/
this.contains = function(key)
{
return this.get(key) !== null;
};
/**
* Gets the keys from localStorage for the application that optionally match a filter
* returns An array of keys
*/
this.getKeys = function(filter)
{
var keys = [];
if (this.localStorageSupported)
{
for (var key in localStorage)
{
if (isAppKey(key))
{
// Remove the prefix from the key
if (prefix) key = key.slice(prefix.length);
// Check the filter
if (!filter || filter(key))
{
keys.push(key);
}
}
}
}
return keys;
};
function isAppKey(key)
{
if (prefix)
{
return key.indexOf(prefix) === 0;
}
return true;
};
}
3.运行效果
4.源码地址