Tajmkiper, part 2

One important feature that I want for Tajmkiper ("time keeper" written phonetically in Swedish) is that everything should run locally, including data storage. The data model is really simple: I'll write a class called Project with only three properties: name, totalFinishedTime and startedOn

  • Project.name : The name of the project.
  • Project.totalFinishedTime : Number of seconds the project has been running in completed chunks. If this project's time clock is currently running, the totalFinishedTime does not include the number of seconds on the running clock.
  • Project.startedOn : The Unix timestamp (Date.getTime()) for when this project's time clock started, or null if the time clock is not started.

This way the project objects don't need to be constantly update every second. To present the currently elapsed time on the clock, the user interface simply adds totalFinishedTime to the number of seconds that passed since startedOn. Assuming most people don't fiddle around with the system clock, this will also make it perfectly possible to close the browser and reopen it any amount of time later, and the time clock will remember when it was started.

// First version implementation

function Project(name, totalFinishedTime, startedOn) {
    this.name = name;
    this.totalFinishedTime = totalFinishedTime;
    this.startedOn = startedOn;
}

Project.prototype.getTotalTime = function() {
    if (this.startedOn) {
        // Bitwise or with zero forces the result to be an integer
        return this.totalFinishedTime + (Date.now() - this.startedOn) / 1000 | 0;
    } else {
        return this.totalFinishedTime;
    }
};

Project.prototype.stop = function() {
    this.totalFinishedTime = this.getTotalTime();
    this.startedOn = null;
};

Project.prototype.start = function() {
    this.totalFinishedTime = this.getTotalTime();
    this.startedOn = Date.now();
};

var allProjects = [];

function saveProjects() {
    var json = JSON.stringify(allProjects);

    localStorage["projects"] = json;
}

function loadProjects() {
    allProjects = [];

    var json = localStorage["projects"];

    if (json) {
        var temp = JSON.parse(json);
        temp.forEach(function(item) {
            allProjects.push(new Projects(item.name, item.totalFinishedTime, item.startedOn));
        });
    }
}

The next step is to connect the Project objects up to some HTML generation. I could do this using a jQuery templating plugin, but i choose to do it myself, just for the hell of it.

// Second version implementation

function createProjectsHtml() {
    // First, clear any existing content of the #projects UL element
    var ul = document.getElementById("projects");
    ul.innerHTML = "";

    // Go through all projects, create html elements for each, and add them to the UL
    allProjects.forEach(function(project) {
        var li = createProjectElement(project);
        ul.appendChild(li);
    });

    // TODO: Add the #total LI element
}

function createProjectElement(project) {
    // Create an LI element, and store a reference to it in the Project object for later use
    var li = document.createElement("LI");
    project.element = li;

    // Store a reference to the Project object in the clickable link
    var a = document.createElement("A");
    a.href = "#";
    a.projectReference = project;
    li.appendChild(a);
    a.addEventListener("click", onProjectClick, false);

    var header = document.createElement("HEADER");
    header.textContent = project.name;
    a.appendChild(header);

    var timeSpan = document.createElement("SPAN");
    timeSpan.className = "time";
    a.appendChild(timeSpan);

    updateProjectElement(project);

    return li;
}

function updateProjectElement(project) {
    var li = project.element;
    var timeSpan = li.querySelector("span.time");

    var totalTime = project.getTotalTime();
    var timeText = formatTime(totalTime);
    timeSpan.textContent = timeText;

    li.className = (project.startedOn) ? "running" : "";
}

// TODO: Write a formatTime function

var running = null;

function onProjectClick(e) {
    e.preventDefault();

    // Stop the currently running project first
    if (running) {
        running.stop();
        updateProjectElement(project);
    }

    // Start the clicked project
    var project = this.projectReference;
    project.start();
    updateProjectElement(project);
    running = project;

    saveProjects();
}

Now, the projects are clickable, and the html gets updated at each click. What is still missing is a timer function to repeatedly update the running project's time display. Also, now that a reference to the LI element is stored in the Project object, we need to modify the saveProjects function, so that is doesn't try to store any HTML elements.

// Third version implementation

function saveProjects() {
    // Filter out properties to only include array indices, and those properties that need storing
    var json = JSON.stringify(array, function(key, value) {
        if (key == "name" || key == "totalFinishedTime" || key == "startedOn" || key >= 0) {
            // Only certain properties of Project are saved
            return value;
        }

        return undefined;
    });

    localStorage["projects"] = json;
}

function timerFunc() {
    if (running) {
        updateProjectElement(running);
    }

    // Wait for the next second on the clock
    var now = Date.now();
    var millis = now % 1000;
    var wait = 1000 - millis;
    window.setTimeout(timerFunc, wait);
}

document.addEventListener("DOMContentLoaded", function() {
    loadProjects();
    createProjectsHtml();
    timerFunc();
}, false);

Next step

There you have it. What is still missing?

  • Export to CSV
  • Working stop button
  • Clear all timers
  • Remove all projects

EDIT: Tajmkiper is now publicly available to anyone who wants to use it.

Try it: tajmkiper.com

Posted by Anders Tornblad on Category JavaScript Labels
Tweet this