Tuesday, 29 June 2010

Overriding the style of a jQuery UI datepicker

Recently, in my spare time, I’ve been working on a simple booking system for a friend of mine. I had the need to present a date selector using jQuery UI’s date picker, but I wanted to change the default behaviour as follows;

  • Dates in the past can’t be selected and must be stylised
  • Dates in the future can be selected, but the calendar should highlight any dates that are not available
  • I don’t want to go hunting and changing my jQuery UI theme styles, nor do I want to modify any jQuery UI code

The starting point – vanilla datepicker

Adding the following code;

$("#startDate").datepicker({
    numberOfMonths: 3,
    showButtonPanel: true,
    dateFormat: 'dd MM yy',
});

Provided the following default behaviour;

image

Disabling dates in the past and dates that have bookings already

So this part is pretty simple, the component provides us with two hooks of note – onChangeMonthYear which is fired when the user navigates the calendar between months or years etc (but not on first display) and the second – beforeShowDay, which is called before an actual day is rendered into the control and allows you to specify whether the date is available, any extra css to apply and a tooltip to show.

I could use the onChangeMonthYear event to load my known events via a JSON call and then check the variables in the beforeShowDay event, but to be honest I’m going to only be interested in future bookings and there aren’t going to be a massive volume. As such, I can afford to load all the events using a single ajax call during page start up and then interrogate the result in beforeShowDay.

So, my code to implement the datepicker now looks like this;

$("#startDate").datepicker({
        numberOfMonths : 3,
        showButtonPanel : true,
        dateFormat : 'dd MM yy',
        beforeShowDay : calendarDayShow
    });

with the following function;

  1. function calendarDayShow(targetDate)
  2. {
  3.     var availableResult = [true, '', ''];
  4.     var bookedResult = [false, '', ''];
  5.  
  6.     var now = new Date();
  7.     if (targetDate < now) return bookedResult;
  8.  
  9.     var targetYear = targetDate.getFullYear();
  10.     var targetMonth = targetDate.getMonth();
  11.     var targetDay = targetDate.getDate();
  12.     if (typeof (availability[targetYear]) == "undefined") return availableResult;
  13.     if (typeof (availability[targetYear][targetMonth]) == "undefined") return availableResult;
  14.     if (typeof (availability[targetYear][targetMonth][targetDay]) == "undefined") return availableResult;
  15.     return bookedResult;
  16. }

The component expects this function to return an array in the format of [<<availability>>, <<css>>, <<tooltip>>]. It’s worth noting that my availability data is stored as a JSON object in this hierarchy; (If there is an entry, that date is booked).

  • YYYY
    • MM
    • MM
      • DD = true
      • DD = true
  • eg: 2010
    • 6
      • 15 = true
      • 16 = true

So, on the above code, lines 3+4 are defining the available and booked responses. Line 6-7 checks if the date being rendered is in the past, and if it is, it returns a booked result to prevent it from being selected. This now results in the following;

image

Notice that 1st Aug and 12 July are booked in this example.

Making it look how I want it.

This is all well and good, but I want past dates to appear as disabled and booked dates to appear with a red X through them, like this end result:

image

I’m pretty sure you already know the answer – the beforeShowDay event expects us to pass back availability, and extra CSS classes to apply plus any tooltip we want. So we change our function thus;

function calendarDayShow(targetDate)
{
    var availableResult = [true, '', ''];
    var bookedResult = [false, 'bookedDayCalendar', 'Booked'];

    var now = new Date();
    if (targetDate < now) return [false, 'pastDayCalendar', 'Can\'t make bookings in the past!'];

    var targetYear = targetDate.getFullYear();
    var targetMonth = targetDate.getMonth();
    var targetDay = targetDate.getDate();
    if (typeof (availability[targetYear]) == "undefined") return availableResult;
    if (typeof (availability[targetYear][targetMonth]) == "undefined") return availableResult;
    if (typeof (availability[targetYear][targetMonth][targetDay]) == "undefined") return availableResult;
    return bookedResult;
}

So we’re now returning the extra CSS. We define this extra CSS is our site’s stylesheet;

/* Overides for jquery UI */
.bookedDayCalendar { opacity: 1; }
.bookedDayCalendar span
{
    background-color: Black;
    background-position: center center;
    background-repeat: no-repeat;
    background-image: url(booked.png);
    color: #aaa;
    border: none;
}
.pastDayCalendar { opactiy: 0.85; }
.pastDayCalendar span
{
    background-color: Black;
    background-image: none;
    text-decoration: line-through;
}

And voila, we get….

image

hmmm – not quite the desired result! The problem here is many of the styles are inherited from jquery UI, our reset.css and so on, and some of those styles are taking precedence over our new styles. As we are confident we want these attributes on this particular class, we can ensure they are applied with preference by adding the !important moniker to them in CSS;

.bookedDayCalendar { opacity: 1 !important; }
.bookedDayCalendar span
{
    background-color: Black !important;
    background-position: center center !important;
    background-repeat: no-repeat !important;
    background-image: url(booked.png) !important;
    color: #aaa !important;
    border: none !important;
}
.pastDayCalendar { opactiy: 0.85 !important; }
.pastDayCalendar span
{
    background-color: Black !important;
    background-image: none !important;
    text-decoration: line-through !important;
}

And we get the desired result.

image

Addendum;

Of course, you’ll want to use filter:Alpha(Opacity=…. for internet explorer;

.bookedDayCalendar { opacity: 1 !important; filter:Alpha(Opacity=100) !important; }
.bookedDayCalendar span
{
    background-color: Black !important;
    background-position: center center !important;
    background-repeat: no-repeat !important;
    background-image: url(booked.png) !important;
    color: #aaa !important;
    border: none !important;
}
.pastDayCalendar { opactiy: 0.85 !important; filter:Alpha(Opacity=85) !important; }
.pastDayCalendar span
{
    background-color: Black !important;
    background-image: none !important;
    text-decoration: line-through !important;
}

2 comments:

  1. Thank You man!

    ReplyDelete
  2. Firstly, great post!

    Can this be modified to cancel out booked dates from an sql database? If so, how would one do this.

    ReplyDelete