610 lines
16 KiB
JavaScript
610 lines
16 KiB
JavaScript
|
|
|
|
/* Creates a new Task object. */
|
|
function Task(id ,name, description) {
|
|
this._name = name;
|
|
this._taskid = id;
|
|
this._description = description;
|
|
this._state = Task.State.STOPPED;
|
|
this._timeSpentInPreviousIterations = 0;
|
|
this._startDate = 0;
|
|
this._endDate = 0;
|
|
this._currentIterationStartTime = 0;
|
|
this._customTimeSpent = 0;
|
|
this._onChange = null;
|
|
}
|
|
|
|
/* Possible task states. */
|
|
Task.State = {
|
|
STOPPED: "stopped",
|
|
RUNNING: "running"
|
|
}
|
|
|
|
Task.prototype = {
|
|
/* Returns the task name. */
|
|
getName: function() {
|
|
return this._name;
|
|
},
|
|
|
|
/* Returns the task description. */
|
|
getDescription: function() {
|
|
if (this._description == "NaN") {
|
|
this._description = ""
|
|
}
|
|
return this._description;
|
|
},
|
|
|
|
/* Returns the task state. */
|
|
getState: function() {
|
|
return this._state;
|
|
},
|
|
|
|
/* Is the task stopped? */
|
|
isStopped: function() {
|
|
return this._state == Task.State.STOPPED;
|
|
},
|
|
|
|
/* Is the task running? */
|
|
isRunning: function() {
|
|
return this._state == Task.State.RUNNING;
|
|
},
|
|
|
|
/*
|
|
* Sets the "onChange" event handler. The "onChange" event is fired when the
|
|
* task is started, stopped, or reset.
|
|
*/
|
|
setOnChange: function(onChange) {
|
|
this._onChange = onChange;
|
|
},
|
|
|
|
/*
|
|
* Returns the time spent on the task in the current work iteration. Works
|
|
* correctly only when the task is running.
|
|
*/
|
|
_getCurrentIterationTime: function() {
|
|
return (new Date).getTime() - this._currentIterationStartTime;
|
|
},
|
|
|
|
/*
|
|
* Returns the total time spent on the task. This includes time spent in
|
|
* the current work iteration if the task is running.
|
|
*/
|
|
getTimeSpent: function() {
|
|
var result = this._timeSpentInPreviousIterations;
|
|
if (this._state == Task.State.RUNNING) {
|
|
result += this._getCurrentIterationTime();
|
|
}
|
|
return result;
|
|
},
|
|
|
|
/* Calls the "onChange" event handler if set. */
|
|
_callOnChange: function() {
|
|
if (typeof this._onChange == "function") {
|
|
this._onChange();
|
|
}
|
|
},
|
|
|
|
/* Starts a new task work iteration. */
|
|
start: function() {
|
|
if (this._state == Task.State.RUNNING) { return };
|
|
if (this._startDate == 0) {this._startDate = new Date()}
|
|
this._state = Task.State.RUNNING;
|
|
this._currentIterationStartTime = (new Date).getTime();
|
|
this._callOnChange();
|
|
},
|
|
|
|
/* Stops the current task work iteration. */
|
|
stop: function() {
|
|
if (this._state == Task.State.STOPPED) { return };
|
|
|
|
this._state = Task.State.STOPPED;
|
|
this._timeSpentInPreviousIterations += this._getCurrentIterationTime();
|
|
this._currentIterationStartTime = 0;
|
|
this._endDate = new Date();
|
|
this._callOnChange();
|
|
},
|
|
|
|
/* Stops the current task work iteration and resets the time data. */
|
|
reset: function() {
|
|
this.stop();
|
|
this._timeSpentInPreviousIterations = 0;
|
|
this._callOnChange();
|
|
},
|
|
save: function(task){
|
|
console.log(task)
|
|
|
|
$.ajax({
|
|
type: "POST",
|
|
data: {
|
|
taskname: task._name,
|
|
taskid: task._taskid,
|
|
description: task._description,
|
|
start: task._startDate,
|
|
end: task._endDate,
|
|
duration: task._timeSpentInPreviousIterations,
|
|
},
|
|
url: "{{ path('app_timer_create') }}",
|
|
success: function (response) {
|
|
response=JSON.parse(response);
|
|
if(response.return=="KO") {
|
|
$("#dataTable_wrapper").append("<div class='alert alert-danger' style='margin: 5px 0px'>"+response.error+"</div>");
|
|
}else{
|
|
location.reload();
|
|
}
|
|
}
|
|
});
|
|
},
|
|
/* Serializes the task into a string. */
|
|
serialize: function() {
|
|
/*
|
|
* Originally, I wanted to use "toSource" and "eval" for serialization and
|
|
* deserialization, but "toSource" is not supported by WebKit, so I resorted
|
|
* to ugly hackery...
|
|
*/
|
|
return [
|
|
encodeURIComponent(this._name),
|
|
this._state,
|
|
this._timeSpentInPreviousIterations,
|
|
this._currentIterationStartTime,
|
|
this._startDate,
|
|
this._endDate,
|
|
this._taskid,
|
|
this._description,
|
|
].join("&");
|
|
},
|
|
|
|
/* Deserializes the task from a string. */
|
|
deserialize: function(serialized) {
|
|
var parts = serialized.split("&");
|
|
this._name = decodeURIComponent(parts[0]);
|
|
this._state = parts[1];
|
|
this._timeSpentInPreviousIterations = parseInt(parts[2]);
|
|
this._currentIterationStartTime = parseInt(parts[3]);
|
|
this._startDate = parseInt(parts[4]);
|
|
this._endDate = parseInt(parts[5]);
|
|
this._taskid = parseInt(parts[6]);
|
|
this._description = parseInt(parts[7]);
|
|
|
|
}
|
|
}
|
|
|
|
/* ===== Tasks ===== */
|
|
|
|
/* The Tasks class represents a list of tasks. */
|
|
|
|
/* Creates a new Tasks object. */
|
|
function Tasks() {
|
|
this._tasks = [];
|
|
|
|
this._onAdd = null;
|
|
this._onRemove = null;
|
|
}
|
|
|
|
Tasks.prototype = {
|
|
/*
|
|
* Sets the "onAdd" event handler. The "onAdd" event is fired when a task
|
|
* is added to the list.
|
|
*/
|
|
setOnAdd: function(onAdd) {
|
|
this._onAdd = onAdd;
|
|
},
|
|
|
|
/*
|
|
* Sets the "onRemove" event handler. The "onRemove" event is fired when a
|
|
* task is removed from the list.
|
|
*/
|
|
setOnRemove: function(onRemove) {
|
|
this._onRemove = onRemove;
|
|
},
|
|
|
|
/* Returns the length of the task list. */
|
|
length: function() {
|
|
return this._tasks.length
|
|
},
|
|
|
|
/*
|
|
* Returns index-th task in the list, or "undefined" if the index is out of
|
|
* bounds.
|
|
*/
|
|
get: function(index) {
|
|
return this._tasks[index];
|
|
},
|
|
|
|
/*
|
|
* Calls the callback function for each task in the list. The function is
|
|
* called with three parameters - the task, its index and the task list
|
|
* object. This is modeled after "Array.forEach" in JavaScript 1.6.
|
|
*/
|
|
forEach: function(callback) {
|
|
for (var i = 0; i < this._tasks.length; i++) {
|
|
callback(this._tasks[i], i, this);
|
|
}
|
|
},
|
|
|
|
/* Calls the "onAdd" event handler if set. */
|
|
_callOnAdd: function(task) {
|
|
if (typeof this._onAdd == "function") {
|
|
this._onAdd(task);
|
|
}
|
|
},
|
|
|
|
/* Adds a new task to the end of the list. */
|
|
add: function(task) {
|
|
this._tasks.push(task);
|
|
this._callOnAdd(task);
|
|
},
|
|
|
|
/* Calls the "onRemove" event handler if set. */
|
|
_callOnRemove: function(index) {
|
|
if (typeof this._onRemove == "function") {
|
|
this._onRemove(index);
|
|
}
|
|
},
|
|
|
|
/*
|
|
* Removes index-th task from the list. Does not do anything if the index
|
|
* is out of bounds.
|
|
*/
|
|
remove: function(index) {
|
|
this._callOnRemove(index);
|
|
this._tasks.splice(index, 1);
|
|
},
|
|
|
|
|
|
|
|
/* Serializes the list of tasks into a string. */
|
|
serialize: function() {
|
|
var serializedTasks = [];
|
|
this.forEach(function(task) {
|
|
serializedTasks.push(task.serialize());
|
|
});
|
|
return serializedTasks.join("|");
|
|
},
|
|
|
|
/* Deserializes the list of tasks from a string. */
|
|
deserialize: function(serialized) {
|
|
/*
|
|
* Repeatedly use "remove" so the "onRemove" event is triggered. Do the same
|
|
* with the "add" method below.
|
|
*/
|
|
while (this._tasks.length > 0) {
|
|
this.remove(0);
|
|
}
|
|
|
|
var serializedTasks = serialized.split("|");
|
|
for (var i = 0; i < serializedTasks.length; i++) {
|
|
var task = new Task(0 ,"", "");
|
|
task.deserialize(serializedTasks[i]);
|
|
this.add(task);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ===== Extensions ===== */
|
|
|
|
/*
|
|
* Pads this string with another string on the left until the resulting string
|
|
* has specified length. If the padding string has more than one character, the
|
|
* resulting string may be longer than desired (the padding string is not
|
|
* truncated and it is only prepended as a whole). Bad API, I know, but it's
|
|
* good enough for me.
|
|
*/
|
|
String.prototype.pad = function(length, padding) {
|
|
var result = this;
|
|
while (result.length < length) {
|
|
result = padding + result;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/* ===== Task List + DOM Storage ===== */
|
|
|
|
/* The list of tasks. */
|
|
var tasks = new Tasks();
|
|
|
|
/* The last value of the serialized task list string. */
|
|
var lastSerializedTasksString;
|
|
|
|
/*
|
|
* The key under which the serialized task list string is stored in the DOM
|
|
* Storage.
|
|
*/
|
|
var TASKS_DOM_STORAGE_KEY = "timerTasks";
|
|
|
|
/*
|
|
* Returns DOM Storage used by the application, or "null" if the browser does
|
|
* not support DOM Storage.
|
|
*/
|
|
function getStorage() {
|
|
/*
|
|
* HTML 5 says that the persistent storage is available in the
|
|
* "window.localStorage" attribute, however Firefox implements older version
|
|
* of the proposal, which uses "window.globalStorage" indexed by the domain
|
|
* name. We deal with this situation here as gracefully as possible (i.e.
|
|
* without concrete browser detection and with forward compatibility).
|
|
*
|
|
* For more information, see:
|
|
*
|
|
* http://www.whatwg.org/specs/web-apps/current-work/#storage
|
|
* https://developer.mozilla.org/En/DOM/Storage
|
|
*/
|
|
if (window.localStorage !== undefined) {
|
|
return window.localStorage;
|
|
} else if (window.globalStorage !== undefined) {
|
|
return window.globalStorage[location.hostname];
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Saves the task list into a DOM Storage. Also updates the value of the
|
|
* "lastSerializedTasksString" variable.
|
|
*/
|
|
function saveTasks() {
|
|
var serializedTasksString = tasks.serialize();
|
|
getStorage()[TASKS_DOM_STORAGE_KEY] = serializedTasksString;
|
|
lastSerializedTasksString = serializedTasksString;
|
|
}
|
|
|
|
/*
|
|
* Loads the serialized task list string from the DOM Storage. Returns
|
|
* "undefined" if the storage does not contain the string (this happens when
|
|
* running the application for the first time).
|
|
*/
|
|
function loadSerializedTasksString() {
|
|
var storedValue = getStorage()[TASKS_DOM_STORAGE_KEY];
|
|
/*
|
|
* The spec says "null" should be returned when the key is not found, but some
|
|
* browsers return "undefined" instead. Maybe it was in some earlier version
|
|
* of the spec (I didn't bother to check).
|
|
*/
|
|
if (storedValue !== null && storedValue !== undefined && storedValue.length > 0) {
|
|
/*
|
|
* The values retrieved from "globalStorage" use one more level of
|
|
* indirection.
|
|
*/
|
|
return (window.localStorage === undefined) ? storedValue.value : storedValue;
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Loads the task list from the DOM Storage. Also updates the value of the
|
|
* "lastSerializedTasksString" variable.
|
|
*/
|
|
function loadTasks() {
|
|
var serializedTasksString = loadSerializedTasksString();
|
|
if (serializedTasksString !== undefined) {
|
|
tasks.deserialize(serializedTasksString);
|
|
lastSerializedTasksString = serializedTasksString;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Was the task list changed outside of the application? Detects the change
|
|
* by comparing the current serialized task list string in the DOM Storage
|
|
* with a kept old value.
|
|
*/
|
|
function tasksHaveChangedOutsideApplication() {
|
|
var serializedTasksString = loadSerializedTasksString();
|
|
if (serializedTasksString != lastSerializedTasksString) {
|
|
return true
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* ===== View ===== */
|
|
|
|
/* Some time constants. */
|
|
var MILISECONDS_IN_SECOND = 1000;
|
|
var MILISECONDS_IN_MINUTE = 60 * MILISECONDS_IN_SECOND;
|
|
var MINUTES_IN_HOUR = 60;
|
|
|
|
/* Formats the time in the H:MM format. */
|
|
function formatTime(time) {
|
|
var timeInMinutes = time / MILISECONDS_IN_MINUTE;
|
|
var hours = Math.floor(timeInMinutes / MINUTES_IN_HOUR);
|
|
var minutes = Math.floor(timeInMinutes - hours * MINUTES_IN_HOUR);
|
|
return hours + ":" + String(minutes).pad(2, "0");
|
|
}
|
|
|
|
/*
|
|
* Computes the URL of the image in the start/stop link according to the task
|
|
* state.
|
|
*/
|
|
function computeStartStopLinkImageUrl(state) {
|
|
switch (state) {
|
|
case Task.State.STOPPED:
|
|
return "fa-play";
|
|
case Task.State.RUNNING:
|
|
return "fa-stop";
|
|
default:
|
|
throw "Invalid task state."
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Builds the HTML element of the row in the task table corresponding to the
|
|
* specified task and index.
|
|
*/
|
|
function buildTaskRow(task, index) {
|
|
var result = $("<tr />");
|
|
|
|
var startStopLink = $(
|
|
"<a href='#' class='start-stop-link' title='Start/stop'>"
|
|
+ "<i class='fa " + computeStartStopLinkImageUrl(task.getState()) + "'></i>"
|
|
|
|
);
|
|
startStopLink.click(function() {
|
|
switch (task.getState()) {
|
|
case Task.State.STOPPED:
|
|
task.start();
|
|
break;
|
|
case Task.State.RUNNING:
|
|
task.stop();
|
|
break;
|
|
default:
|
|
throw "Invalid task state."
|
|
}
|
|
saveTasks();
|
|
return false;
|
|
});
|
|
|
|
var resetLink = $(
|
|
"<a href='#' title='Reset'>"
|
|
+ "<i class='fa fa-undo'></i>"
|
|
);
|
|
resetLink.click(function() {
|
|
task.reset();
|
|
saveTasks();
|
|
return false;
|
|
});
|
|
|
|
var deleteLink = $(
|
|
"<a href='#' title='Delete' class='deletetask'>"
|
|
+ "<i class='fa fa-trash-alt' class='deletetask'></i>"
|
|
|
|
);
|
|
deleteLink.click(function() {
|
|
if (confirm("Do you really want to delete task \"" + task.getName() + "\"?")) {
|
|
tasks.remove(index);
|
|
saveTasks();
|
|
}
|
|
return false;
|
|
});
|
|
|
|
var saveLink = $(
|
|
"<a href='#' title='Save' class='savetask'>"
|
|
+ "<i class='fa fa-download' class='savetask'></i>"
|
|
);
|
|
saveLink.click(function() {
|
|
tasks.remove(index);
|
|
saveTasks();
|
|
task.save(task);
|
|
return false;
|
|
});
|
|
result
|
|
.addClass("state-" + task.getState())
|
|
.append($("<td class='task-name' />").text(task.getName()))
|
|
.append($("<td class='task-description' />").text(task.getDescription()))
|
|
.append($("<td class='task-time' />").text(formatTime(task.getTimeSpent())))
|
|
.append($("<td class='task-actions' />")
|
|
.append(startStopLink)
|
|
.append(" ")
|
|
.append(resetLink)
|
|
.append(" ")
|
|
.append(saveLink)
|
|
.append(" ")
|
|
.append(deleteLink)
|
|
);
|
|
|
|
return result;
|
|
}
|
|
|
|
/* Finds row with the specified index in the task table. */
|
|
function findRowWithIndex(index) {
|
|
return $("#task-table").find("tr").slice(1).eq(index);
|
|
}
|
|
|
|
/*
|
|
* Updates the row in the task table corresponding to a task according to
|
|
* its state.
|
|
*/
|
|
function updateTaskRow(row, task) {
|
|
if (task.isStopped()) {
|
|
row.removeClass("state-running");
|
|
row.addClass("state-stopped");
|
|
} else if (task.isRunning()) {
|
|
row.removeClass("state-stopped");
|
|
row.addClass("state-running");
|
|
}
|
|
|
|
row.find(".task-time").text(formatTime(task.getTimeSpent()))
|
|
|
|
row.find(".start-stop-link i").removeClass();
|
|
row.find(".start-stop-link i").addClass("fa "+computeStartStopLinkImageUrl(task.getState()));
|
|
}
|
|
|
|
/* ===== Initialization ===== */
|
|
|
|
/* Initializes event handlers on the task list. */
|
|
function initializeTasksEventHandlers() {
|
|
tasks.setOnAdd(function(task) {
|
|
var row = buildTaskRow(task, tasks.length() - 1);
|
|
$("#task-table").append(row);
|
|
task.setOnChange(function() {
|
|
updateTaskRow(row, task);
|
|
});
|
|
});
|
|
|
|
tasks.setOnRemove(function(index) {
|
|
findRowWithIndex(index).remove();
|
|
if (tasks.length() == 1 ) {
|
|
$( "#task-table" ).css({"display":"none"});
|
|
}
|
|
});
|
|
}
|
|
|
|
/* Initializes GUI event handlers. */
|
|
function initializeGuiEventHandlers() {
|
|
$( "#addtimer" ).click(function() {
|
|
displayTaskAdd();
|
|
});
|
|
|
|
$( "#timer-desc" ).keypress(function( event ) {
|
|
if ( event.which == 13 ) {
|
|
event.preventDefault();
|
|
displayTaskAdd();
|
|
}
|
|
});
|
|
|
|
}
|
|
function displayTaskAdd() {
|
|
$( "#task-table" ).css({"display":"inline"});
|
|
var taskName = $("#timer-task option:selected").text();
|
|
var taskId = $("#timer-task option:selected").val();
|
|
var taskDesc = $("#timer-desc").val();
|
|
|
|
var task = new Task(taskId, taskName, taskDesc);
|
|
tasks.add(task);
|
|
saveTasks();
|
|
$("#timer-desc").val("");
|
|
}
|
|
/*
|
|
* Initializes a timer used to update task times and detect changes in the
|
|
* data stored in the DOM Storage.
|
|
*/
|
|
function initializeTimer() {
|
|
setInterval(function() {
|
|
tasks.forEach(function(task, index) {
|
|
updateTaskRow(findRowWithIndex(index), task);
|
|
});
|
|
|
|
if (tasksHaveChangedOutsideApplication()) {
|
|
loadTasks();
|
|
}
|
|
}, 10 * MILISECONDS_IN_SECOND);
|
|
}
|
|
|
|
/* Initializes the application. */
|
|
$(document).ready(function(){
|
|
try {
|
|
if (!getStorage()) {
|
|
alert("Timer requires a browser with DOM Storage support, such as Firefox 3+ or Safari 4+.");
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
alert("Timer does not work with file: URLs in Firefox.");
|
|
return;
|
|
}
|
|
|
|
initializeTasksEventHandlers();
|
|
loadTasks();
|
|
initializeGuiEventHandlers();
|
|
initializeTimer();
|
|
});
|
|
|
|
|