Revisiting the JavaScript Calendar Control – Part 3

It’s been some time since I started writing this three part series, and it certainly took a while longer to get done than I expected. A busy work schedule does tend to do that, but if it makes anyone feel better, having left the third part hanging did give my conscience a field trip. So let’s do a quick recap and get on with it :)

  • In part 1, we created a jQuery plugin which displays a simple static calendar.
  • In part 2, we added the facility to pick a date, and to switch the month on display.
  • In part 3 (this post), we will add some sanity checks, and the ability to display events on given days.

Code and examples

This post contains three examples, which are linked from the relevant sections. If you’re after the script itself, it can be found here. There is also a list of options that can be used when constructing the calendar.

Restricting the selectable dates

In some cases, we want our users to select dates which meet certain conditions. For example, if we’re taking appointments, we may only want people to pick weekdays in the future. If we’re providing an archive of some sort, we only want people to pick dates in the past.

Limiting selections to weekdays

The weekend restriction (or any other restriction based on the day of the week) can be implemented using just one selector. We’re already marking our days with the day of the week, so all we need to do is select all days marked dw0 or dw6, and add the “disabled” class to them:

257: jQuery(“td.dw0, td.dw6″).addClass(“disabled”);

When we’re assigning event handlers, we tell jQuery to skip all cells marked with this class:

269: jQuery(“td:not(.disabled)”, table).click(selectCell);

Restricting selections to a given range

The date range restriction also uses the “disabled” class to tell jQuery to skip dates which are out of range. To determine which dates are out of range, we select the cell matching the boundary of the restriction (for example, if we set the minimum date to 1st November, 2009, we look for a cell with the classes “d1”, “m10” and “y2009”:

189: jQuery(“td.d” + date.getUTCDate(), table)
190:     .filter(“.m” + date.getUTCMonth())
191:     .filter(“.y” + date.getUTCFullYear())
192:     .addClass(type);

As before, we filter using the least common class first.

When we have marked the edge or edges of the range, we then use jQuery’s sibling selection functions to find and mark all cells which are out of range. In the case of the minimum date restriction:

228: startCell
229:     .prevAll()
230:     .add(startCell.parent().prevAll().children(“td”))
231:     .addClass(“disabled”);

The prevAll() function will give us all cells preceding the start cell in its row (i.e. any preceding days in the same week). After that, we’re adding any cells in the preceding rows, which covers all days before the start date. As the cells will be marked disabled, they will not be assigned any on click behaviour.

The end date uses the same technique, but uses the nextAll() function instead to pick days after the end date.

The above only works if the end and start dates are currently on display. If they are not, we simply check whether the first and last days displayed fall into range, and disable the entire calendar if they are not.

This example shows how the calendar works when date ranges are configured. And now, on to the meatier subject of event display.

Displaying events in the calendar

Since we’re allowing people to click around the months freely, It doesn’t make sense to try and pre-load all the event information – pre-loading 10 years worth of information when your user only needs a month is never a good idea. Instead, we can use AJAX, neatly wrapped up in jQuery’s ajax() function, to request the information for a given month when we need it – that is, any time the display month changes.

145: var date = table.data(“date.active”);
146:
147: jQuery.ajax({
148:     dataType: “json”,
149:     type: table.data(“eventFetchMode”),
150:     url: table.data(“eventSource”),
151:     data: {
152:         month:date.getUTCMonth(),
153:         year:date.getUTCFullYear(),
154:         calendar: table.prev().attr(“id”)
155:     },
156:     cache: false,
157:     success: displayCalendarEvents
158: });

Quite a lot going on there. We’re telling jQuery to request a JSON object (dataType) from a given url. We’re also telling it to pass certain parameters – month, year, and an id (more on this later – I’m not happy with this yet). Depending on the type value, it will generate a Get or a Post request. When the response is received, it will call the displayCalendarEvents function to process the response.

The actual request happens asynchronously. In other words, when the request is fired, the script doesn’t hang around waiting for an answer. It will just go on its merry way, and execute the callback when it gets an answer.

The callback itself just attaches the event data to the appropriate cells, and assigns an additional click handler to them:

167: for (var index in data.events) {
168:     var event = data.events[index];
169:     var eventCell = jQuery(“.d” + event.date, table)
170:         .filter(“.m” + event.month)
171:         .filter(“.y” + event.year);
172:
173:     eventCell.addClass(“event”);
174:     eventCell.data(“event.data”, event);
175:     eventCell
176:         .unbind(“click”)
177:         .click(table.data(“eventSelectionHandler”))
178:         .click(selectCell);
179: }

When adding the event handler, we’re unbinding the click event before we add our handler and restore the normal click handler. This tells our script to run the event selection handler before the normal handler.

Using the default handler for events, our calendar will now pop an alert box when an event cell is clicked.

This example shows how the calendar works with events. Rather than serving json from a dynamic page, as would normally be the case, we’re just serving it a this javascript file, which defines a single event.

Doing more with events

As it is, the calendar doesn’t actually care how the event content returned by the server is structured – you can define the actual event content any way you like. We could, for example, return the content as an object in its own right:

“events”: [
{ "date": 1, "month": 0, "year": 2010, "content": {
"summary" : "Breakfast at Tiffany's",
"location" : "Tiffany's",
"time" : new Date(2010, 0, 1) }}]

In fact, we could take it further and make each content an array of events, allowing us to assign more than one event per day.

To make use of any such event, we’ll need to provide the calendar with a way to understand it, and we can do this by providing an eventSelectionHandler function:

function () {
var event = $(this).data(“event.data”).content;
$(“.vevent .summary”).text(event.summary);
$(“.vevent .location”).text(event.location);
$(“.vevent .dtstart”).text(“”+event.time);
$(“.vevent”).slideDown();
}

This extracts the event content from the cell, and populates some elements with it. The vevent classes are in fact a microformat standard for events, so any microformat-aware browser or device can now read your event. Here’s a screenshot of how the Operator plugin for Firefox handles the event in this example:

vevent

Where to go from here

The actual plugin still has some issues which I’m not pleased with, especially in the way that the calendar identifies itself after an ajax call. One alternative I was considering was to use generate some sort of random id at the time the calendar is created.

And that’s all for now

So that’s it for now. The plugin here is actually quite redundant, but building stuff is always a good learning experience, and going back over one’s old work is certainly a humbling one. I hope you found these posts useful or at least interesting; if you have any opinions or advice you’d like to share, I’m all ears.

14 thoughts on “Revisiting the JavaScript Calendar Control – Part 3

  1. Hi Karl. This is a GREAT project. I’ve got some very strange requirements for my calendar that I’m hoping you can help me with.

    I need to remove the “disabled” class from all weekend dates in the future, while retaining them for the past. It’s only 5 days in the future, so only for the next weekend dates. I was thinking that right after this line:

    jQuery(“td.dw0, td.dw6″).addClass(“disabled”);

    I thought could add a for loop and just do a .removeClass(“disabled”) on the cells.

    So, here’s my question: How do I set the current cell to perform that action? I tried this:

    var currCell = jQuery(“td.end”, table);
    for (i=5; i >= 0; i–) {
    currCell.removeClass(“disabled”);
    currCell = $(“td”).previous(); }

    but the line currCell = $(“td”).previous(); throws a js error.

    I’d really appreciate any help you can provide. Thanks!

  2. My second question is this:

    I am trying to limit the user from being able to move to any month past the maximumdate or any month prior to the minimumDate. Any suggestions on how to check for that in the moveToNextMonth and moveToPreviousMonth functions?

  3. Hi Keith, thank you for your comments! Glad you’re finding a use for this.

    Regarding the first question, this should sort out the weekends before the current date (i.e. weekends in the past):

    if (!table.data("allowWeekends")) {
    var today = new Date();
    jQuery(".d" + today.getUTCDate()).filter(".m" + today.getUTCMonth()).prevAll("td.dw0, td.dw6").addClass("disabled");
    jQuery(".d" + today.getUTCDate()).filter(".m" + today.getUTCMonth()).parent().prevAll().children("td.dw0, td.dw6").addClass("disabled");
    }

    This replaces the allowWeekends section in the jquery.calendar.plugin.js file. The first line here says

    “Get the cells matching the current day, then add the disabled class to all Saturdays and Sundays which come before them in the same row.”
    The second line says
    “Get the cells matching the current day, find the row they are in, and disable all previous weekends.”

    I’m sure it could be more elegant with a bit more work :)

    Regarding your snippet, below; I believe it throws because $(“td”) has no context – it wll give you a collection with every td in the page, and will always try to go to the first one and call previous on it. Additionally, the jquery function for previous is prev(), so I believe what you meant was

    curCell = curCell.prev();

    The direction was good, though I generally prefer not to add something, rather than adding and removing the excess. Just find it neater that way.

    For your second question, you could check the value of activedate against maximum and minimum, and return; for example

    function moveToNextMonth() {
    var table = jQuery(this).parents("table");

    if (table.data("activeDate").getUTCMonth() = table.data("maximumDate").getUTCMonth())
    return;

    changeMonth(table, 1);
    }

    That would cause the method to exit without actually changing month … then you can spice it up by changing the colour of the button, or what have you.

  4. Thanks Karl, I really appreciate the help. Without intellisense and error codes, I’m feeling like a newbie programmer!

    By the way, I noticed one thing about this code: I start the week on a Sunday (not sure if this has anything to do with the bug or not, but thought I’d mention it). When I go to a month where the 1st is on a Saturday, the first week in the display month is the 2nd. I’m betting it has something to do with your work-around for dealing with UTC. Anyway, something to look into.

    I actually had another question about this calendar. Every day will have events associated with them. For the past, there doesn’t have to be any special handling of the event. However, for the 5 future dates, I need to add a class to determine the background when a certain criteria is met (e.g. if a programming class is scheduled that day, and there are available seats, the background should be green, but if all the seats are taken, the background should be red). I’m thinking I can figure out how to add the class for that, and will work on it, but I would appreciate your thoughts as to the best way to handle this.

    Once again, I really appreciate your help!

  5. BTW, it’s “date.active”, not “activeDate” (just in case anyone else if following along) :)

  6. Ack. Okay, the code to disable previous weekend days only disables the previous weekend days for the current month, but if you back up a month, they are not disabled.

    Man, don’tcha just love tracking down little bugs like this? ~groan~ lol

  7. Here is how I fixed it:

    while (today.getUTCMonth() >= table.data(“minimumDate”).getUTCMonth()
    && today.getUTCFullYear() >= table.data(“minimumDate”).getUTCFullYear())
    {
    jQuery(“.d” + today.getUTCDate()).filter(“.m” + today.getUTCMonth()).prevAll(“td.dw0, td.dw6″).addClass(“disabled”);

    jQuery(“.d” + today.getUTCDate()).filter(“.m” + today.getUTCMonth()).parent().prevAll().children(“td.dw0, td.dw6″).addClass(“disabled”);

    today.setMonth(today.getUTCMonth() – 1);
    }

  8. Dang – thanks Keith :) No more coding after bedtime for me :D

    I think you could also check if the current date is displayed. If the current date is NOT visible and the month falls outside the range, then all weekend dates get disabled. That would eliminate the while loop.

  9. Hmm. Okay, I’ll have to think about that one. The nice thing about this particular code is that it only deals with between 3 and 4 months, so any loop will be very quick. But you’re right, eliminating loops is always better.

    I ran into another problem I was hoping you could give me some direction on. For the click event of a particular date, what I’m doing is showing a grid (html table) below the calendar that lists 2 columns. The problem is, now that I have a second table object, in the “return this.each(function() {” function, it tries to call the populateHeader method and pass in the table. It also does this with the table I created to display the details. How can I go about only processing in the calendar table and no other table on the page?

  10. Assuming I understood the problem correctly, you might have overlapping selectors on the two tables.

    If you have, for example,


    <div><!-- calendar goes here --></div>
    <div><!-- events go here --></div>

    and your script is
    $("div").calendar(...)

    That’s the simplest way of reproducing the issue that I could think of.

    Another idea that just occurred:

    <div id=”calendar”><table id=”events”></table></div>

    In this case, both the calendar and the events would end up in the same container, and would be picked up by the same selector (being both tables in the same parent). A quick fix to this would be to move the table out of the calendar’s container, or wrap the calendar container in such a way that the two tables are no longer siblings.

  11. The body of the HTML file is this:

    The body of the HTML file is this:

    $(function() {
    // Create a new calendar with the given date selected.
    var minProcessDate = new Date();
    minProcessDate.setDate(minProcessDate.getDate() – 90);
    minProcessDate.setDate(1);
    var maxProcessDate = new Date();
    maxProcessDate.setDate(maxProcessDate.getDate() + 5);

    $(“#target”).calendar(new Date(), {
    className: “redzone-calendar”,
    allowWeekends: false,
    minimumDate: minProcessDate,
    maximumDate: maxProcessDate
    });

    // Switch the display to the current date.
    $(“table”).calendar(new Date());
    });

    Cash In

    DHTML Class
    $1.00

    And the function is this:

    return this.each(function() {
    var target = jQuery(this);
    var isNew = !target.data(“date.active”);

    if (!target.is(“table”) || isNew) {
    var table = createTable(target, options);
    populateHeader(table);
    setSelectedDate(table, date);
    }
    else {
    setActiveDate(target, date);
    }
    });

  12. I just emailed you all the files, so you can see directly what is happening.

    I REALLY appreciate all your help on this!

  13. In case there are people following along: there is a performance issue that Karl and I worked out. It appears that the update function is calling the bindCellBehaviour method every time a date is picked. This is required for the first update call, but no subsequent call. To fix this, add a class-level variable named cellsBound at the top of the js file and set it’s default value to false. Then, change the update function to be:

    function update(table, repopulate) {
    if (repopulate) populateBody(table);
    populateCaption(table);
    decorate(table);
    if (!cellsBound) {
    bindCellBehaviour(table);
    cellsBound = true;
    }
    }

  14. For some reason, after moving the code into our ASP project, the bindCellBehaviour stopped working correctly. How I fixed it was to move the call into the populateBody method. I think this is a better solution all around, since you don’t have to check and set the cellsBound variable. This sort of thing always strikes me as “hacky.”

    Another “hacky” thing that I haven’t figured out a way to get around was the binding of the selectCell call to each TD. This code worked in my HTML page, but then again, when I moved it to my project, the filtering stopped working, so all the cells (even the disabled weekend cells) executed the Click event. I tried everything from filtering on the class to adding a “disabled” attribute and filtering on that. I also tried doing an “unbind” on any cell with that class or attribute, but the copy of jQuery that is in the app just isn’t having any of it.

    The way I worked around that is in the selectCell method, I added this code at the beginning:

    if ($(this).hasClass(‘disabled’)) return;

    I don’t like it, but it works. Hope this helps someone out there that comes across this thread.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>