Files
schedule/src/schedule-2.0/assets/js/timer.js
2020-07-30 09:05:05 +02:00

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("&nbsp;&nbsp;")
.append(resetLink)
.append("&nbsp;&nbsp;")
.append(saveLink)
.append("&nbsp;&nbsp;&nbsp;&nbsp;")
.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();
});