Saturday, April 09, 2011

Extending the jQuery UI datepicker with new shortcuts

The jQuery UI datepicker has a set of default shortcuts to move to the next/previous month (PageDown/PageUp), to today's date (Ctrl+Home), etc. After working with QuickBooks for so long, I got used to the QuickBooks date control shortcuts that use letters instead of Ctrl+key combination like the jQuery UI datepicker. These shortcuts are very intuitive, e.g. the letter W is used for the first day of the Week and the letter K is used for the last day of the weeK. Here, we will show how to extend the datepicker to support the additional shortcuts below.

Shortcut Description
+ Next Day
- Previous Day
T Today
W First day of the Week
K Last day of the weeK
M First day of the Month
H Last day of the montH
Y First day of the Year
R Last day of the yeaR

The ideal solution would be to use the widget factory to subclass the datepicker and extend the _doKeyPress function with the new shortcuts. However, the datepicker does not use the widget factory in jquery-ui 1.8, see more details here. The solution is to extend the datepicker with a new function customKeyPress and then bind it to the datepicker keypress event.
/*!
 * Extensions for jQuery UI 1.8.7 datepicker
 *
 * Copyright 2011, Luis Rocha
 * Released under the MIT license.
 *
 */

// Extends datepicker with shortcuts for today, begin and end of the year and month.
$.extend($.datepicker, { customKeyPress: function (event) {
    var inst = $.datepicker._getInst(event.target);
    var c = String.fromCharCode(event.which).toLowerCase();
    switch (c) {
        case "y":
            // First day of the (y)ear.
            if (inst.selectedDay == 1 && inst.selectedMonth == 0) {
                // First day of previous year.
                $.datepicker._adjustDate(event.target, -12, "M");
            } else if (inst.selectedMonth == 0) {
                // First day of the current year.
                $.datepicker._adjustDate(event.target, 1 - inst.selectedDay, "D");
            } else {
                // First day of the current year.
                inst.selectedDay = 1;
                $.datepicker._adjustDate(event.target, -inst.selectedMonth, "M");
            }
            break;
        case "r":
            // Last day of the yea(r).
            if (inst.selectedDay == 31 && inst.selectedMonth == 11) {
                // Last day of next year.
                $.datepicker._adjustDate(event.target, +12, "M");
            } else if (inst.selectedMonth == 11) {
                // Last day of current year.
                $.datepicker._adjustDate(event.target, 31 - inst.selectedDay, "D");
            } else {
                // Last day of current year.
                inst.selectedDay = 31;
                $.datepicker._adjustDate(event.target, 11 - inst.selectedMonth, "M");
            }
            break;
        case "m":
            // First day of the (m)onth.
            if (inst.selectedDay == 1) {
                $.datepicker._adjustDate(event.target, -1, "M");
            } else {
                $.datepicker._adjustDate(event.target, 1 - inst.selectedDay, "D");
            }
            break;
        case "h":
            // Last day of the mont(h).
            var end = $.datepicker._getDaysInMonth(inst.selectedYear, inst.selectedMonth);
            if (inst.selectedDay == end) {
                var month = (inst.selectedMonth + 1) % 11;
                var year = inst.selectedYear + Math.floor((inst.selectedMonth + 1) / 11);
                inst.selectedDay = $.datepicker._getDaysInMonth(year, month);
                $.datepicker._adjustDate(event.target, +1, "M");
            } else {
                $.datepicker._adjustDate(event.target, end - inst.selectedDay, "D");
            }
            break;
        case "w":
            // First day of the (w)eek.
            var date = new Date(inst.selectedYear, inst.selectedMonth, inst.selectedDay);
            var offset = (date.getDay() > 0 ? date.getDay() : 7);
            $.datepicker._adjustDate(event.target, -offset, "D");
            break;
        case "k":
            // Last day of the wee(k).
            var date = new Date(inst.selectedYear, inst.selectedMonth, inst.selectedDay);
            var offset = (date.getDay() < 6 ? 6 - date.getDay() : 7);
            $.datepicker._adjustDate(event.target, offset, "D");
            break;
        case "t":
            // Today (same as Ctrl+Home).
            $.datepicker._gotoToday(event.target);
            break;
        case "+":
            // Increase day (same as Ctrl+Right).
            $.datepicker._adjustDate(event.target, +1, 'D');
            break;
        case "-":
            // Decrease day (same as Ctrl+Left).
            $.datepicker._adjustDate(event.target, -1, 'D');
            break;
    }
}
});
The following code snippet shows how to bind the customKeyPress function to the datepicker keypress:
$(function() {
    $("#datepicker").datepicker().keypress(function (event) { 
        $.datepicker.customKeyPress(event);
    });
});
Once the datepicker is refactored to use the widget factory (maybe in jquery-ui 1.9??), I will update this code to use the widget factory solution instead. For future updates, see https://github.com/lerocha/jquery-ui-extensions.

A demo of this extended datepicker is available here.