jQuery Time Entry with Time Navigation Keys

Posted by Rick Strahl on West-Wind See other posts from West-Wind or by Rick Strahl
Published on Wed, 30 Nov 2011 10:23:33 GMT Indexed on 2011/11/30 17:53 UTC
Read the original article Hit count: 1079

Filed under:
|

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>
        /// <example>
        /// var proxy = new ServiceProxy("JsonStockService.svc/");
        /// proxy.invoke("GetStockQuote",{symbol:"msft"},function(quote) { alert(result.LastPrice); },onPageError);
        ///</example>
        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());

            //alert($(this).val() + " " + time.toString() + " " + time.date.toString());

            switch (e.keyCode) {
                case 78: // [N]ow                        
                    time = new Time(new Date()); break;
                case 109: case 189:  // - 
                    time.addMinutes(-1);
                    break;
                case 107: case 187: // +
                    time.addMinutes(1);
                    break;
                case 72: //H
                    time.addHours(-1);
                    break;
                case 82: //R
                    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);
    }


    String.prototype.padL = function (width, pad) {
        if (!width || width < 1)
            return this;

        if (!pad) pad = " ";
        var length = width - this.length
        if (length < 1) return this.substr(0, width);

        return (String.repeat(pad, length) + this).substr(0, width);
    }
    String.repeat = function (chr, count) {
        var str = "";
        for (var x = 0; x < count; x++) { str += chr };
        return str;
    }

})(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>
    /// <example>
    /// var proxy = new ServiceProxy("JsonStockService.svc/");
    /// proxy.invoke("GetStockQuote",{symbol:"msft"},function(quote) { alert(result.LastPrice); },onPageError);
    ///</example>
    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:
                d = new Date(year, month, day - 1); break;
            case 107: case 187:
                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 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

© Rick Strahl, West Wind Technologies, 2005-2011
Posted in jQuery  HTML  

© West-Wind or respective owner

Related posts about jQuery

Related posts about html