Home > jQuery UI Widgets, Reviews > Selecting Ranges with the jQuery UI Datepicker

Selecting Ranges with the jQuery UI Datepicker

November 20, 2012 Leave a comment Go to comments

The current supported method of selecting a date range with the jQuery UI Datepicker is to use two pickers which represent the start and end of the range. In certain scenarios, where there is a large range, using two fields is a preferable choice. However, for shorter ranges, having the ability to allow the user to pick it from one box seems like a feasible alternative.

The Google Analytics report date filter takes a kind of hybrid approach. The UI has one field representing the range with a drop down menu that shows several different range selection options. There are two fields to enter a range but you can also select the range from the picker and achieve the same result. Its a fairly specific use case but an interesting approach worth considering:

I did find a jQuery plugin that provided this feature at eyecon.ro/datepicker/. I tested the plugin to see how it worked and ran into several issues. Typically, you don’t have to specify any options to use a widget, however, minimally, you need to specify the date option otherwise it will throw an error. Additionally, the code referenced the depreciated jQuery.curCSS(). That needed to change to get it to work with a more recent version of jQuery. There are some other quirks as well - its last build was in 2009 so its probably not as well maintained as one would like. Regardless of the issues, the demos provided some ideas on how to modify the jQuery Datepicker to provide a similar feature.

With a little work, I was able to get the jQuery UI Datepicker to select more than one date. It was a little frustrating trying to deal with the two modes of operation - inline in a DIV or attached to an input field. In the former, the picker is always visible while, in the latter, it only appears when focusing on the input and hides once the user selects a date. I unfortunately needed a hybrid behavior:

  • Show the picker when the input receives focus. However, don't hide the picker once a date is selected, instead stay visible to allow the user to pick a second date to complete the range.
  • Show the selected range in the picker by highlighting the date included in the range. Additionally, update the input field with the selected range as the user picks different dates.
  • Only hide the picker when the user explicitly says they are done selecting the range.

The first feature can be accomplished using an inline picker, manually positioning it under the input field, and handling the binding to the focus event outside of the pickers built-in functionality. Here's the setup:

<div id="jrange" class="dates">
   <input />
   <div></div>
</div>

  // global variables to track the date range
  var cur = -1, prv = -1;
  
  // Create the picker and align it to the bottom of the input field 
  // Hide it for later
  $('#jrange div')
        .datepicker();
        .position({
             my: 'left top',
             at: 'left bottom',
             of: $('#jrange input')
   	  })
   	.hide();
  
  // Listen for focus on the input field and show the picker
  $('#jrange input').on('focus', function (e) {
        $('#jrange div').show();
     });

That part is pretty straight forward - it mimics the functionality that would happen automatically if the picker was attached to the input field. However, since I don't want the picker to hide once a date is selected or have that date set in the input field, I have to attach it inline to the DIV.

The next step is to use several of the callback function available in the Datepicker widget to manage the range selection. Two variables are needed to capture the current day selected and remember the last day selected. Each time a day is selected, the onSelect callback is called. The bulk of the logic needs to go in this function:

  $('#jrange div')
    .datepicker({
       onSelect: function ( dateText, inst ) {
             var d1, d2;

             prv = +cur;
             cur = inst.selectedDay;
             if ( prv == -1 || prv == cur ) {
                prv = cur;
                $('#jrange input').val( dateText );
             } else {
                d1 = $.datepicker.formatDate( 
                       'mm/dd/yy', 
                       new Date( inst.selectedYear, inst.selectedMonth, Math.min(prv,cur) ), 
                       {} 
                    );

                d2 = $.datepicker.formatDate( 
                       'mm/dd/yy',
                       new Date( inst.selectedYear, inst.selectedMonth, Math.max(prv,cur) ), 
                       {} 
                    );
                $('#jrange input').val( d1+' - '+d2 );
             }
       }
    });

This code will update the global range variable and update the input box with the current selected range (or just the one date if only one day is selected). This captures the range but what about showing it on the actual picker? The beforeShowDay callback provides the necessary hook to achieve this feature. Its called for each day in the picker and enables you to set a class on the TD that represents the date passed in as a parameter:

  $('#jrange div')
    .datepicker({
      beforeShowDay: function ( date ) {
            return [true, 
                ( (date.getDate() >= Math.min(prv, cur) && date.getDate() <= Math.max(prv, cur)) ? 
                            'date-range-selected' : '')];
         }
    });

Once the class is set, a small bit of carefully crafted CSS styles are needed to set the background color of the links in the table cells:

.date-range-selected > .ui-state-active,
.date-range-selected > .ui-state-default {
   background: none;
   background-color: lightsteelblue;
}

So far, I have been able to utilize all the built-in options of the Datepicker to make this feature work. However, hiding the picker proved to be a challenge. Since, internally to the picker, it is considered inline, any logic that would allow the picker to be hidden is disabled. This includes clicking outside the picker, the close button that can optionally be enabled, or any other method normally available when the picker is attached to the input field. I decided to enable the button line and manually add the close button back onto that line myself. Unfortunately, it can't be done only once when the widget is created because the HTML for the picker is constantly being regenerated which would immediately remove the button. Instead, I added an option for an onAfterUpdate callback to the Datepicker and added a proxy to the internal _updateDatepicker function to call that callback. This will enable me to add the button back each time the calendar HTML is rebuilt:

$.datepicker._defaults.onAfterUpdate = null;

var datepicker__updateDatepicker = $.datepicker._updateDatepicker;
$.datepicker._updateDatepicker = function( inst ) {
	datepicker__updateDatepicker.call( this, inst );

	var onAfterUpdate = this._get(inst, 'onAfterUpdate');
	if (onAfterUpdate)
		onAfterUpdate.apply((inst.input ? inst.input[0] : null),
			[(inst.input ? inst.input.val() : ''), inst]);
}

The implementation of the callback simply adds the same button HTML that the widget would create if it were in non-inline mode:

  $('#jrange div')
    .datepicker({
       onAfterUpdate: function ( inst ) {
             $('<button type="button" class="ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all" data-handler="hide" data-event="click">Done</button>')
                .appendTo($('#jrange div .ui-datepicker-buttonpane'))
                .on('click', function () { $('#jrange div').hide(); });
         }
    });

The full source of this implementation and a demo are on my sandbox. There are probably some improvements that can be made to make it easier to reuse the customizations and allow it to work properly with other non-range pickers. However, its a good proof of concept that illustrates how to provide range selection in one Datepicker widget.

About these ads
  1. December 1, 2012 at 2:51 am

    This is a nice script. What Jquery version will support for this?

    • December 1, 2012 at 11:08 am

      The example requires jQuery UI 1.9 or later. jQuery UI 1.9 requires jQuery 1.6 or later. I personally use the latest versions off the CDN which are jQuery UI 1.9.2 and jQuery 1.8.3.

  2. February 28, 2013 at 2:05 pm

    Nice work.. but it seems to only work within the same month.. .how could it be changed to select one date in a month and move several months ahead in same calendar and select the same date?

    • March 1, 2013 at 9:02 am

      The demo was only tracking the day of the month not the full date and, when the month changes, it resets the selection. To make it work over multiple months, onChangeMonthYear needs to be removed to stop resetting the selection and the actual selected date needs to be tracked. I updated the demo to do this. Now it stores the number of milliseconds since midnight Jan 1, 1970 in the cur/prv variables and uses those to determine the selection range and date string to display in the input box.

  3. March 7, 2013 at 7:13 pm

    Would you be able to add support for preselecting a date range during the init?

    • March 10, 2013 at 1:06 pm

      Yes, you’d have to add some code to the onfocus handler for the input to check the contents of the field, parse it, and then setup cur/prv to contain the values corresponding to the date(s) present. I used the built in $.datepicker.parseDate() function available in the jQuery UI library. If you want something fancier, you can also use MomentJS. Since this picker is capable of allowing one day to be selected, both cases need to be handled. Additionally, the jQuery UI parse throws errors if the date can’t be parsed so you need a try/catch to deal with that. Finally, after everything is initialized, you have to call the refresh method on the datepicker so it rebuilds the calendar. Otherwise, nothing will appear selected when its shown.

         $('#jrange input').on('focus', function (e) {
               var v = this.value,
                   d;
      
               try {
                  if ( v.indexOf(' - ') > -1 ) {
                     d = v.split(' - ');
      
                     prv = $.datepicker.parseDate( 'mm/dd/yy', d[0] ).getTime();
                     cur = $.datepicker.parseDate( 'mm/dd/yy', d[1] ).getTime();
      
                  } else if ( v.length > 0 ) {
                     prv = cur = $.datepicker.parseDate( 'mm/dd/yy', v ).getTime();
                  }
               } catch ( e ) {
                  cur = prv = -1;
               }
      
               if ( cur > -1 )
                  $('#jrange div').datepicker('setDate', new Date(cur));
      
               $('#jrange div').datepicker('refresh').show();
            });
      

      I've updated the demo with this code for reference.

  4. Tatiana Levandovska
    April 3, 2013 at 8:01 pm

    Hi,
    line
    prv = +cur;
    seems to have a typo.

    • April 9, 2013 at 11:40 am

      Its a leftover type coercion from when I was using strings to represent the days. Its no longer necessary so I removed it. Thanks.

  5. Melkior
    April 15, 2013 at 1:55 pm

    i would say this is one of best plugin i found so far.
    woudl there be possible to use more than one calendar, for example to display three months in a row ?
    as well in this case if selection would be across multiple months it would be visibly easier to select/display.

    • April 16, 2013 at 12:23 pm

      You should be able to use the Datepicker’s numberOfMonths config option with this customization. I tried it out and the selection will work correctly:

         $('#jrange div')
            .datepicker({
                  ...
                  numberOfMonths: 3,
                  ...
      

      • Aswathy
        June 29, 2013 at 3:52 am

        Hi Ben,

        your code is wonderful…could you help me out..i need a calendar which will be able to have the future dates disabled…

        Cheers,
        Aswathy

      • July 1, 2013 at 9:31 am

        Check out the demo on the jQueryUI site related to disabling dates

  6. Michele
    April 19, 2013 at 4:36 pm

    Thank you so much I have been looking for something like this for non-stop for days! Do you know of any way to add a class to the first and last range items so they can be styled differently?

    • April 21, 2013 at 7:52 am

      The beforeShowDay callback provides an opportunity to style each cell of the calendar grid. Right now, this example simply sets a class on a day if its part of the range. You could change the logic to add different classes if the day is the first/last day of the range.

  7. Yarik
    April 23, 2013 at 4:49 am

    Hi. Thanks for this plugin modification. Really useful. It works really great though I found date range selection a bit annoying. It might be better after selection date range being able to start from single date selection like when you select a date the very first time.

    • April 24, 2013 at 11:04 am

      There are definitely different usage patterns that could be implemented. You’d probably have to tinker with the prv assignment to reset the range when applicable. I haven’t tried it, but you might need another variable to determine the selection state or it could be inferred from the value of prv/cur.

  8. May 5, 2013 at 5:06 pm

    Hi Ben, thank you so much for the plugin, it’s really useful !
    How could i do to always display the datepicker, without the focus on the input ?
    Cheers.

    • May 6, 2013 at 12:47 pm

      The picker is setup to use the inline mode of the datepicker and rendered into a DIV that is immediately hidden. The focus simply shows that DIV. Remove the hide()/show() calls from the example and it should load as visible and stay that way.

      • Emile
        May 7, 2013 at 4:30 am

        Thanks !

  9. Michele C
    May 6, 2013 at 1:15 pm

    Hi Ben,

    Thanks for the amazing datepicker! I am trying to figure out how to add a clear button to the page. I used this code:

    $(‘#clearDates’).on(‘click’, function(){
    //dates = 2 date picker enabled textboxes
    dates.each(function(){
    $.datepicker._clearDate(this);
    });
    });

    When I press clear, the range is cleared but the last date I clicked still has the class: date-renge-selected and current-day. The two input fields also have this value. Any clue what I am doing wrong?

    Thanks

  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: