8
\$\begingroup\$

Here is the link to the jsFiddle and below is the code:

JS:

$(function() {

    var hours = minutes = seconds = milliseconds = 0;
    var prev_hours = prev_minutes = prev_seconds = prev_milliseconds = undefined;
    var timeUpdate;

    // Start/Pause/Resume button onClick
    $("#start_pause_resume").button().click(function(){
        // Start button
        if($(this).text() == "Start"){  // check button label
            $(this).html("<span class='ui-button-text'>Pause</span>");
            updateTime(0,0,0,0);
        }
        // Pause button
        else if($(this).text() == "Pause"){
            clearInterval(timeUpdate);
            $(this).html("<span class='ui-button-text'>Resume</span>");
        }
        // Resume button        
        else if($(this).text() == "Resume"){
            prev_hours = parseInt($("#hours").html());
            prev_minutes = parseInt($("#minutes").html());
            prev_seconds = parseInt($("#seconds").html());
            prev_milliseconds = parseInt($("#milliseconds").html());

            updateTime(prev_hours, prev_minutes, prev_seconds, prev_milliseconds);

            $(this).html("<span class='ui-button-text'>Pause</span>");
        }
    });

    // Reset button onClick
    $("#reset").button().click(function(){
        if(timeUpdate) clearInterval(timeUpdate);
        setStopwatch(0,0,0,0);
        $("#start_pause_resume").html("<span class='ui-button-text'>Start</span>");      
    });

    // Update time in stopwatch periodically - every 25ms
    function updateTime(prev_hours, prev_minutes, prev_seconds, prev_milliseconds){
        var startTime = new Date();    // fetch current time

        timeUpdate = setInterval(function () {
            var timeElapsed = new Date().getTime() - startTime.getTime();    // calculate the time elapsed in milliseconds

            // calculate hours                
            hours = parseInt(timeElapsed / 1000 / 60 / 60) + prev_hours;

            // calculate minutes
            minutes = parseInt(timeElapsed / 1000 / 60) + prev_minutes;
            if (minutes > 60) minutes %= 60;

            // calculate seconds
            seconds = parseInt(timeElapsed / 1000) + prev_seconds;
            if (seconds > 60) seconds %= 60;

            // calculate milliseconds 
            milliseconds = timeElapsed + prev_milliseconds;
            if (milliseconds > 1000) milliseconds %= 1000;

            // set the stopwatch
            setStopwatch(hours, minutes, seconds, milliseconds);

        }, 25); // update time in stopwatch after every 25ms

    }

    // Set the time in stopwatch
    function setStopwatch(hours, minutes, seconds, milliseconds){
        $("#hours").html(prependZero(hours, 2));
        $("#minutes").html(prependZero(minutes, 2));
        $("#seconds").html(prependZero(seconds, 2));
        $("#milliseconds").html(prependZero(milliseconds, 3));
    }

    // Prepend zeros to the digits in stopwatch
    function prependZero(time, length) {
        time = new String(time);    // stringify time
        return new Array(Math.max(length - time.length + 1, 0)).join("0") + time;
    }
});

HTML:

<div id="time">
    <span id="hours">00</span> :
    <span id="minutes">00</span> :
    <span id="seconds">00</span> ::
    <span id="milliseconds">000</span>
</div>
<div id="controls">
    <button id="start_pause_resume">Start</button>
    <button id="reset">Reset</button>
</div>

CSS:

body {
    font-family:"Arial", Helvetica, sans-serif;
    text-align: center;
}
#controls{
    font-size: 12px;
}
#time {
    font-size: 150%;
}
\$\endgroup\$
8
\$\begingroup\$

Chess clock anyone? :D

HTML:

<!-- 
Never assume just one. Prepare for more than one always. 
With that, we use classes
-->

<div class="stopwatch" data-autostart="false">
    <div class="time">
        <span class="hours"></span> : 
        <span class="minutes"></span> : 
        <span class="seconds"></span> :: 
        <span class="milliseconds"></span>
    </div>
    <div class="controls">
        <!-- Some configurability -->
        <button class="toggle" data-pausetext="Pause" data-resumetext="Resume">Start</button>
        <button class="reset">Reset</button>
    </div>
</div>

CSS:

/*
Always prefix your styles with a unique selector your widget has
to prevent your styles from affecting other elements of the same
selector pattern. In this case, only those under .stopwatch gets
affected.
*/
.stopwatch .controls {
    font-size: 12px;
}

/* I'd rather stick to CSS rather than JS  for styling */

.stopwatch .controls button{
    padding: 5px 15px;
    background :#EEE;
    border: 3px solid #06C;
    border-radius: 5px
}

.stopwatch .time {
    font-size: 150%;
}

JS:

$(function () {

    // Never assume one widget is just used once in the page. You might
    // think of adding a second one. So, we adjust accordingly.

    $('.stopwatch').each(function () {

        // Cache very important elements, especially the ones used always
        var element = $(this);
        var running = element.data('autostart');
        var hoursElement = element.find('.hours');
        var minutesElement = element.find('.minutes');
        var secondsElement = element.find('.seconds');
        var millisecondsElement = element.find('.milliseconds');
        var toggleElement = element.find('.toggle');
        var resetElement = element.find('.reset');
        var pauseText = toggleElement.data('pausetext');
        var resumeText = toggleElement.data('resumetext');
        var startText = toggleElement.text();

        // And it's better to keep the state of time in variables 
        // than parsing them from the html.
        var hours, minutes, seconds, milliseconds, timer;

        function prependZero(time, length) {
            // Quick way to turn number to string is to prepend it with a string
            // Also, a quick way to turn floats to integers is to complement with 0
            time = '' + (time | 0);
            // And strings have length too. Prepend 0 until right.
            while (time.length < length) time = '0' + time;
            return time;
        }

        function setStopwatch(hours, minutes, seconds, milliseconds) {
            // Using text(). html() will construct HTML when it finds one, overhead.
            hoursElement.text(prependZero(hours, 2));
            minutesElement.text(prependZero(minutes, 2));
            secondsElement.text(prependZero(seconds, 2));
            millisecondsElement.text(prependZero(milliseconds, 3));
        }

        // Update time in stopwatch periodically - every 25ms
        function runTimer() {
            // Using ES5 Date.now() to get current timestamp            
            var startTime = Date.now();
            var prevHours = hours;
            var prevMinutes = minutes;
            var prevSeconds = seconds;
            var prevMilliseconds = milliseconds;

            timer = setInterval(function () {
                var timeElapsed = Date.now() - startTime;

                hours = (timeElapsed / 3600000) + prevHours;
                minutes = ((timeElapsed / 60000) + prevMinutes) % 60;
                seconds = ((timeElapsed / 1000) + prevSeconds) % 60;
                milliseconds = (timeElapsed + prevMilliseconds) % 1000;

                setStopwatch(hours, minutes, seconds, milliseconds);
            }, 25);
        }

        // Split out timer functions into functions.
        // Easier to read and write down responsibilities
        function run() {
            running = true;
            runTimer();
            toggleElement.text(pauseText);
        }

        function pause() {
            running = false;
            clearTimeout(timer);
            toggleElement.text(resumeText);
        }

        function reset() {
            running = false;
            pause();
            hours = minutes = seconds = milliseconds = 0;
            setStopwatch(hours, minutes, seconds, milliseconds);
            toggleElement.text(startText);
        }

        // And button handlers merely call out the responsibilities
        toggleElement.on('click', function () {
            (running) ? pause() : run();
        });

        resetElement.on('click', function () {
            reset();
        });

        // Another advantageous thing about factoring out functions is that
        // They are reusable, callable elsewhere.
        reset();
        if(running) run();
    });

});
\$\endgroup\$
  • \$\begingroup\$ In your HTML, why is class being used instead of id? What is data-autostart, data-pausetext and data-resumetext? In your JS, how is the selection of element better than the way I did it? \$\endgroup\$ – Rahul Desai Apr 29 '14 at 3:58
  • \$\begingroup\$ @RahulDesai id is used if the element is only one. But if you scale the widget, like say if you need more than one, then the widget will break. Using classes allow you to target more than one, since classes can be used on many elements. \$\endgroup\$ – Joseph Apr 29 '14 at 4:46
  • \$\begingroup\$ @RahulDesai The data-* is one way to configure settings in the widget. As long as you follow the right class names and data-* configs, you can write widgets without writing JS. That's how Bootstrap does it. \$\endgroup\$ – Joseph Apr 29 '14 at 4:48
  • \$\begingroup\$ @RahulDesai Elements should be cached. Everytime you do $(selector), depending on the selector, jQuery is actually running through each element in the DOM, checking if the element matches. If you do that often, it's slow. You're picking up the same elements, fetch it once and reuse the same reference. \$\endgroup\$ – Joseph Apr 29 '14 at 4:50
  • \$\begingroup\$ I did not understand how is doing var element = $(this); and using element for the rest of the code different from $(selector). Isnt it the same? What does this stand for in this context? \$\endgroup\$ – Rahul Desai Apr 29 '14 at 5:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.