Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

jQuery Time Entry with Time Navigation Keys


:P
On this page:

So, how do you display time values in your Web applications? Displaying date AND time values in applications is lot less standardized than date display only. While date input has become fairly universal with various date picker controls available, time entry continues to be a bit of a non-standardized. In my own applications I tend to use the jQuery UI DatePicker control for date entries and it works well for that. Here's an example:

TimeEntry

The date entry portion is well defined and it makes perfect sense to have a calendar pop up so you can pick a date from a rich UI when necessary. However, time values are much less obvious when it comes to displaying a UI or even just making time entries more useful. There are a slew of time picker controls available but other than adding some visual glitz, they are not really making time entry any easier.

Part of the reason for this is that time entry is usually pretty simple. Clicking on a dropdown of any sort and selecting a value from a long scrolling list tends to take more user interaction than just typing 5 characters (7 if am/pm is used).

Keystrokes can make Time Entry easier

Time entry maybe pretty simple, but I find that adding a few hotkeys to handle date navigation can make it much easier. Specifically it'd be nice to have keys to:

  • Jump to the current time (Now)
  • Increase/decrease minutes
  • Increase/decrease hours

The timeKeys jQuery PlugIn

Some time ago I created a small plugin to handle this scenario. It's non-visual other than tooltip that pops up when you press ? to display the hotkeys that are available:

HelpDropdown

Try it Online

The keys loosely follow the ancient Quicken convention of using the first and last letters of what you're increasing decreasing (ie. H to decrease, R to increase hours and + and - for the base unit or minutes here). All navigation happens via the keystrokes shown above, so it's all non-visual, which I think is the most efficient way to deal with dates.

To hook up the plug-in, start with the textbox:

<input type="text" id="txtTime" name="txtTime" value="12:05 pm"  title="press ? for time options" />

Note the title which might be useful to alert people using the field that additional functionality is available.

To hook up the plugin code is as simple as:

$("#txtTime").timeKeys();

You essentially tie the plugin to any text box control.

Options
The syntax for timeKeys allows for an options map parameter:

$(selector).timeKeys(options);

Options are passed as a parameter map object which can have the following properties:

timeFormat
You can pass in a format string that allows you to format the date. The default is "hh:mm t" which is US time format that shows a 12 hour clock with am/pm. Alternately you can pass in "HH:mm" which uses 24 hour time. HH, hh, mm and t are translated in the format string - you can arrange the format as you see fit.

callback
You can also specify a callback function that is called when the date value has been set. This allows you to either re-format the date or perform post processing (such as displaying highlight if it's after a certain hour for example).

Here's another example that uses both options:

$("#txtTime").timeKeys({ 
    timeFormat: "HH:mm",
    callback: function (time) {
        showStatus("new time is: " + time.toString() + " " + $(this).val() );
    }
});

The plugin code itself is fairly simple. It hooks the keydown event and checks for the various keys that affect time navigation which is straight forward. The bulk of the code however deals with parsing the time value and formatting the output using a Time class that implements parsing, formatting and time navigation methods.

Here's the code for the timeKeys jQuery plug-in:

/// <reference path="jquery.js" />
/// <reference path="ww.jquery.js" />
(function ($) {

    $.fn.timeKeys = function (options) {
        /// <summary>
        /// Attaches a set of hotkeys to time fields
        /// + Add minute - subtract minute
        /// H Subtract Hour R Add houR
        /// ? Show keys
        /// </summary>
        /// <param name="options" type="object">
        /// Options:
        /// timeFormat: "hh:mm t" by default HH:mm alternate
        /// callback: callback handler after time assignment
        /// </param>
        if (this.length < 1) return this;

        var opt = {
            timeFormat: "hh:mm t",
            callback: null
        }
        $.extend(opt, options);

        return this.keydown(function (e) {
            var $el = $(this);

            var time = new Time($el.val());

            switch (e.keyCode) {
                case 78: // [N]ow                        
                    time = new Time(new Date()); break;
                case 109: case 189:  case 39: // -  right
                    time.addMinutes(-1);
                    break;
                case 107: case 187: case 37: // +   left
                    time.addMinutes(1);
                    break;
                case 72: case 40://H  down
                    time.addHours(-1);
                    break;
                case 82: case 38: //R  up
                    time.addHours(1);
                    break;
                case 191: // ?
                    if (e.shiftKey)
                        $(this).tooltip("<b>N</b> Now<br/><b>+</b> add minute<br /><b>-</b> subtract minute<br /><b>H</b> Subtract Hour<br /><b>R</b> add hour", 4000, { isHtml: true });
                    return false;
                default:
                    return true;
            }

            $el.val(time.toString(opt.timeFormat));

            if (opt.callback) {
                // call async and set context in this element
                setTimeout(function () { opt.callback.call($el.get(0), time) }, 1);
            }

            return false;
        });
    }



    Time = function (time, format) {
        /// <summary>
        /// Time object that can parse and format
        /// a time values.
        /// </summary>
        /// <param name="time" type="object">
        /// A time value as a string (12:15pm or 23:01), a Date object
        /// or time value.       /// 
        /// </param>
        /// <param name="format" type="string">
        /// Time format string: 
        /// HH:mm   (23:01)
        /// hh:mm t (11:01 pm)        
        /// </param>
        /// <example>
        /// var time = new Time( new Date());
        /// time.addHours(5);
        /// time.addMinutes(10);
        /// var s = time.toString();
        ///
        /// var time2 = new Time(s);  // parse with constructor
        /// var t = time2.parse("10:15 pm");  // parse with .parse() method
        /// alert( t.hours + " " + t.mins + " " + t.ampm + " " + t.hours25)
        ///</example>

        var _I = this;

        this.date = new Date();
        this.timeFormat = "hh:mm t";
        if (format)
            this.timeFormat = format;

        this.parse = function (time) {
            /// <summary>
            /// Parses time value from a Date object, or string in format of:
            /// 12:12pm or 23:01
            /// </summary>
            /// <param name="time" type="any">
            /// A time value as a string (12:15pm or 23:01), a Date object
            /// or time value.       /// 
            /// </param>
            if (!time)
                return null;

            // Date
            if (time.getDate) {
                var t = {};
                var d = time;
                t.hours24 = d.getHours();
                t.mins = d.getMinutes();
                t.ampm = "am";
                if (t.hours24 > 11) {
                    t.ampm = "pm";
                    if (t.hours24 > 12)
                        t.hours = t.hours24 - 12;
                }
                time = t;
            }

            if (typeof (time) == "string") {
                var parts = time.split(":");

                if (parts < 2)
                    return null;
                var time = {};
                time.hours = parts[0] * 1;
                time.hours24 = time.hours;

                time.mins = parts[1].toLowerCase();
                if (time.mins.indexOf("am") > -1) {
                    time.ampm = "am";
                    time.mins = time.mins.replace("am", "");
                    if (time.hours == 12)
                        time.hours24 = 0;
                }
                else if (time.mins.indexOf("pm") > -1) {
                    time.ampm = "pm";
                    time.mins = time.mins.replace("pm", "");
                    if (time.hours < 12)
                        time.hours24 = time.hours + 12;
                }
                time.mins = time.mins * 1;
            }
            _I.date.setMinutes(time.mins);
            _I.date.setHours(time.hours24);

            return time;
        };
        this.addMinutes = function (mins) {
            /// <summary>
            /// adds minutes to the internally stored time value.       
            /// </summary>
            /// <param name="mins" type="number">
            /// number of minutes to add to the date
            /// </param>
            _I.date.setMinutes(_I.date.getMinutes() + mins);
        }
        this.addHours = function (hours) {
            /// <summary>
            /// adds hours the internally stored time value.       
            /// </summary>
            /// <param name="hours" type="number">
            /// number of hours to add to the date
            /// </param>
            _I.date.setHours(_I.date.getHours() + hours);
        }
        this.getTime = function () {
            /// <summary>
            /// returns a time structure from the currently
            /// stored time value.
            /// Properties: hours, hours24, mins, ampm
            /// </summary>
            return new Time(new Date()); h
        }
        this.toString = function (format) {
            /// <summary>
            /// returns a short time string for the internal date
            /// formats: 12:12 pm or 23:12
            /// </summary>
            /// <param name="format" type="string">
            /// optional format string for date
            /// HH:mm, hh:mm t
            /// </param>
            if (!format)
                format = _I.timeFormat;

            var hours = _I.date.getHours();

            if (format.indexOf("t") > -1) {
                if (hours > 11)
                    format = format.replace("t", "pm")
                else
                    format = format.replace("t", "am")
            }
            if (format.indexOf("HH") > -1)
                format = format.replace("HH", hours.toString().padL(2, "0"));
            if (format.indexOf("hh") > -1) {
                if (hours > 12) hours -= 12;
                if (hours == 0) hours = 12;
                format = format.replace("hh", hours.toString().padL(2, "0"));
            }
            if (format.indexOf("mm") > -1)
                format = format.replace("mm", _I.date.getMinutes().toString().padL(2, "0"));

            return format;
        }

        // construction
        if (time)
            this.time = this.parse(time);
    }  
})(jQuery);

The plugin consists of the actual plugin and the Time class which handles parsing and formatting of the time value via the .parse() and .toString() methods. Code like this always ends up taking up more effort than the actual logic unfortunately. There are libraries out there that can handle this like datejs or even ww.jquery.js (which is what I use) but to keep the code self contained for this post the plugin doesn't rely on external code.

There's one optional exception: The code as is has one dependency on ww.jquery.js  for the tooltip plugin that provides the small popup for all the hotkeys available. You can replace that code with some other mechanism to display hotkeys or simply remove it since that behavior is optional.

While we're at it: A jQuery dateKeys plugIn

Although date entry tends to be much better served with drop down calendars to pick dates from, often it's also easier to pick dates using a few simple hotkeys. Navigation that uses + - for days and M and H for MontH navigation, Y and R for YeaR navigation are a quick way to enter dates without having to resort to using a mouse and clicking around to what you want to find.

Note that this plugin does have a dependency on ww.jquery.js for the date formatting functionality.

$.fn.dateKeys = function (options) {
    /// <summary>
    /// Attaches a set of hotkeys to date 'fields'
    /// + Add day - subtract day
    /// M Subtract Month H Add montH
    /// Y Subtract Year R Add yeaR
    /// ? Show keys
    /// </summary>
    /// <param name="options" type="object">
    /// Options:
    /// dateFormat: "MM/dd/yyyy" by default  "MMM dd, yyyy
    /// callback: callback handler after date assignment
    /// </param>
    if (this.length < 1) return this;

    var opt = {
        dateFormat: "MM/dd/yyyy",
        callback: null
    };
    $.extend(opt, options);

    return this.keydown(function (e) {
        var $el = $(this);
        var d = new Date($el.val());
        if (!d)
            d = new Date(1900, 0, 1, 1, 1);

        var month = d.getMonth();
        var year = d.getFullYear();
        var day = d.getDate();


        switch (e.keyCode) {
            case 84: // [T]oday
                d = new Date(); break;
            case 109: case 189: case 37:
                d = new Date(year, month, day - 1); break;
            case 107: case 187: case 39:
                d = new Date(year, month, day + 1); break;
            case 77: //M
                d = new Date(year, month - 1, day); break;
            case 72: //H
                d = new Date(year, month + 1, day); break;
            case 89: // Y
                d = new Date(year - 1, month, day); break;
            case 82: // R
                d = new Date(year + 1, month, day); break;
            case 191: // ?
                if (e.shiftKey)
                    $el.tooltip("<b>T</b> Today<br/><b>+</b> add day<br /><b>-</b> subtract day<br /><b>M</b> subtract Month<br /><b>H</b> add montH<br/><b>Y</b> subtract Year<br/><b>R</b> add yeaR", 5000, { isHtml: true });
                return false;

            default:
                return true;
        }

        $el.val(d.formatDate(opt.dateFormat));

        if (opt.callback)
        // call async
            setTimeout(function () { opt.callback.call($el.get(0), d); }, 10);

        return false;
    });
}

The logic for this plugin is similar to the timeKeys plugin, but it's a little simpler as it tries to directly parse the date value from a string via new Date(inputString). As mentioned it also uses a helper function from ww.jquery.js to format dates which removes the logic to perform date formatting manually which again reduces the size of the code.

And the Key is…

I've been using both of these plugins in combination with the jQuery UI datepicker for datetime values and I've found that I rarely actually pop up the date picker any more. It's just so much more efficient to use the hotkeys to navigate dates. It's still nice to have the picker around though - it provides the expected behavior for date entry. For time values however I can't justify the UI overhead of a picker that doesn't make it any easier to pick a time. Most people know how to type in a time value and if they want shortcuts keystrokes easily beat out any pop up UI. Hopefully you'll find this as useful as I have found it for my code.

Resources

Posted in jQuery  HTML  

The Voices of Reason


 

Usualdosage
November 30, 2011

# re: jQuery Time Entry with Time Navigation Keys

I like the concept (and I typically always use the jQuery UI DatePicker in my apps) but my only suggestion would be making your hotkeys only require a single keystroke. Everything is laid out nicely until I have to add a minute (+) at which point I have to press two keys. If you're going for typist-efficiency, I'd suggest you use the arrow keys (up=add minute, down=remove miunute, left=remove hour, right=add hour) and that way it can be intuitively entered at lightning speed with a single hand. Just a thought.

Rick Strahl
November 30, 2011

# re: jQuery Time Entry with Time Navigation Keys

@Usualdosage - Hmmm... yes I like that idea! :-) I was trying to stick with an old convention of how Quicken does date entry, which is easy to remember. With the up down, left right you're going to have to remember which is which. I think it might be nice to allow keystroke specification (or keycode).

eric
November 30, 2011

# re: jQuery Time Entry with Time Navigation Keys

Usualdosage, note that '=' is considered a synonym for '+'. Pressing Shift is optional.

Jonathan Rochkind
November 30, 2011

# re: jQuery Time Entry with Time Navigation Keys

Cool. But if you're going to show that tooltip anyhow... why not make it an actual menu, displayed in the manner of an auto-complete menu (even though it's not), and then people on touch devices can use it simply by touching, without bringing up the keyboard to enter a time OR to activate the navigation keys. Folks on touch always want entirely keyless entry if possible -- and you're one step away from making it so, for some times anyway, or if they really want to.

Jason
December 01, 2011

# re: jQuery Time Entry with Time Navigation Keys

While easy to add, your code for the date is missing (as posted in the blog entry..not sure about the download) the handling "Y" and "R" for adjusting the year.

John Livermore
December 01, 2011

# re: jQuery Time Entry with Time Navigation Keys

Nice post. In the past I have done a couple of different things with my time pickers you might find useful. First ditto to the arrow keys suggestion by @Usualdosage. Also, typically time picker controls are used in business applications and users aren't concerned generally with 3:02 PM vs 3:03 PM. Rather 5 or 15 minute increments are sufficient. So rather than having the control inc/dec by 1, it is a much better experience to inc/dec by 5 or 15 minute increments. Lastly, if you allow a period (in addition to a colon) to separate a time entry, then the user can quickly enter the information via their numeric keypad (ie. 8.30 becomes 8:30 AM, 13.30 becomes 1:30 PM).

Pierre-Alain Vigeant
December 01, 2011

# re: jQuery Time Entry with Time Navigation Keys

I was trying to see the inline help. Kept pressing ?, but it kept writing ?. Until I figured that you used the English keyboard layout.

Good work though. But you need internationalization.

jQueryByExample
December 01, 2011

# re: jQuery Time Entry with Time Navigation Keys

Well, great idea and great execution.

But in datepicker control, Y and R keys are not working. I was able to type the keys but rest of the keys worked like charm. I wish I could upload the screenshot.

Note : I was checking the demo in Google chrome.

Rick Strahl
December 02, 2011

# re: jQuery Time Entry with Time Navigation Keys

@all - Many great thoughts. I fixed the Y and R both in the sample and in the download. This is actually some old code and I didn't notice that Y/R was missing.

Arrow keys: Ok, I tend to agree on that. So UpDown would be the 'smallest' unit (ie. minutes and days) and LeftRight would be the next unit up (ie. Hours and Months). Wondering for dates if Year should then also be something different. Ultimately it'd be nice to make the keystrokes configurable, but this is a bit tricky because not all the keys are single keycodes, but keycodes + shift/alt states. Handling command keystrokes in javascript is a pain in the ass.

@Pierre - Hmmm... I would have thought that ? is always the same keycode (or /) I suppose. This goes back to the previous issue. Keycode detection would be a lot easier if we could check the actual character that was input rather than just the keycode. AFAIK, there's no consistent cross browser way to get the character value from the keycode + shift state alone across browser. keydown works for some but not all browsers.

I'll make a few revisions to implement default with arrow keys and supporting . in addition to : syntax.

Rick Strahl
December 06, 2011

# re: jQuery Time Entry with Time Navigation Keys

The problem with arrow keys time/date navigation is that hooking hotkeys to them interferes with regular navigation of text input. If you're just typing in or editing a time value the left and right keys are pretty crucial in entry.

Played around with arrow navigation for a minute and it just didn't feel right if you also allow editing of the field.

Albert van Halen
December 09, 2011

# re: jQuery Time Entry with Time Navigation Keys

Nice thought on alternative input possibilities.

Using your demo in FireFox 8.0 pressing Y / R, subtracts / adds to months as well.

Rick Strahl
December 11, 2011

# re: jQuery Time Entry with Time Navigation Keys

@Albert - thanks fixed. Odd only in my demo, not in the download code or the actual library piece :-)

Marcus
December 11, 2011

# re: jQuery Time Entry with Time Navigation Keys

Nice article, however in addition to the bugs mentioned by others (which you have fixed in your demo but not the article and downloads) I've also noticed that you've accidentally left the following in dateKeys():<code lang="javascript"> /// <example>
/// var proxy = new ServiceProxy("JsonStockService.svc/");
/// proxy.invoke("GetStockQuote",{symbol:"msft"},function(quote) { alert(result.LastPrice); },onPageError);
///</example></xml>

Rick Strahl
December 12, 2011

# re: jQuery Time Entry with Time Navigation Keys

@Marcus - Thanks, updated code in article and the download. Should be fine now.

E.R. Gilmore
January 27, 2012

# re: jQuery Time Entry with Time Navigation Keys

Rick,

I'm now working at a place that does a lot of time-based queries. Would it be hard to modify this to set seconds too? Would you have to do it with another key for the seconds, like Ctrl-Up/Down?

Thanks,

E.R.

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024