Skip to content

New jQuery Widget: flexcal

Updated 2009-05-03

Just what the world needs—another date picker

Download the code.

The current incarnation of the jQuery UI datepicker works fine, but I needed something that could handle the Jewish calendar. This is not the same as localizing datepicker to use Hebrew;that just translates the month and day names but uses the same Gregorian calendar as everyone uses. I needed a calendar that could switch between multiple, completely different calendar systems.

The algorithms for converting dates from Jewish to Gregorian are readily available open-source, so that was easy. textpopup is exactly what I need to create the pop-up box for the calendar, and I had already created my own date picker for my old bililite site (don't look at it too closely; it's ugly and I used an old version of Prototype. I was still learning! It works, though). So the pieces were all there.

And so was born flexcal. In its simplest form, it looks like datepicker:

<span>Using <code>datepicker</code>: </span><input id="date1"/>
<br/>
<span>Using <code>flexcal</code>: </span><input id="date2"/>
$('#date1').datepicker();$('#date2').flexcal();

But it can show multiple calendars and allows localization to different calendar systems:

<span>Jewish/civil calendar: </span><input id="date3"/>
$('#date3').flexcal({
  position: 'bl',
  calendars: ['en', 'jewish', 'he-jewish'],
  'class': 'multicalendar'
});

And it allows animated transitions (which I think is in the works for datepicker as well):

<span>Fading transition: </span><input id="date3-1"/>
$('#date3-1').flexcal({
  position: 'bl',
  transition: function(o){
    o.elements.eq(o.currSlide).fadeOut(o.duration);
    o.elements.eq(1-o.currSlide).fadeIn(o.duration);
  },
  transitionOptions: {duration: 'slow'}	
});

Options

It's a subclass of textpopup, so all the options from textpopup and ajaxpopup are available. The url defaults to /inc/flexcal.html, which contains:

Note that all the required CSS is included in the html file, and that it uses the themeable jQuery classes. Setting the 'class' option to multicalendar makes the date rectangles longer, giving more room for the tab bar.

The flexcal options themselves are:

calendars {Array(String || Object)}
Array of calendars to display. Each string is a key into the $.ui.flexcal.l10n object. Default is ['en'], which means that the default is one calendar, using the localization of $.ui.flexcal.l10n.en. If the item is an object then it is the localization object itself
calendarNames {Array(String)}
Names to display on the tabs for the calendars. If empty, uses the default name from the localization object
transitionOptions
Options to pass to the transition function, below
transition {Function}
Function to handle the transition from one calendar or month to another. Signature is function(options), where options is copied from transitionOptions, augmented by the following fields:
options.$cont
jQuery object that is the container for the calendars
options.elements
jQuery object that has two elements, each an absolutely-positioned <div> one on top of the other, that are the calendars to be transitioned
options.currSlide
The index of the current calendar. Thus, options.elements.eq(options.currSlide) is the currently-showing calendar and options.elements.eq(1-options.currSlide) is the currently hidden calendar that needs to be shown
options.rev
Boolean to indicate that the animation should show a "reverse" transition, going to a previous month.
options.l10n
Localization object for the calendar to be displayed (options.elements.eq(1-options.currSlide)). The transition function can use options.l10n.isRTL to determine if "next month" should be animated right-to-left or left-to-right
The default is function(o){ o.elements.eq(o.currSlide).hide(); o.elements.eq(1-o.currSlide).show(); }
hidetabs {true|false|'conditional'}
True to hide the tab bar, false to show it and 'conditional' to hide it if there is only one calendar to display
l10n {Object (see below)}
Localization object to use if one of the fields in the calendar array item is undefined

The localization (l10n for short) object is the key to the whole thing. $.flexcal.l10n contains objects, each of which is a localization object with the following fields (named to match the corresponding fields in $.datepicker.regional):

name {String}
Default display name, if not overridden by calendarNames
calendar {Function}
Calendar generating function, defaults to Gregorian calendar. Takes a Date object, d, and returns an object with the following fields:
first {Date}
First date of the month containing d
last {Date}
Last date of the month containing d
prev {Date}
Date one month before d
next {Date}
Date one month after d
prevYear {Date}
Date one year before d
nextYear {Date}
Date one year after d
y
Number of the year of d
m
0-indexed number of the month of d
dow
0-indexed number of the day of the week of first
monthNames {Array(String)}
Names of the months
dayNamesMin {Array(String)}
Names of the days of the week
isRTL {Boolean}
True if the calendar should display right-to-left
prevText {String}
Wording on the "previous month" button. The CSS for jQuery UI replaces this with an icon but still uses the text for the title. By the jQuery UI guidelines, it should not include an arrow or chevron
nextText {String}
Wording on the "next month" button
years {Function}
Function to convert a year number to the displayed string. Default is function(n) {return n}
dates{Function}
Function to convert a date number to the displayed string. Default is function(n) {return n}

// default l10n calendar
$.ui.flexcal.defaults.l10n = {
	name: 'flexcal',
	calendar: function(d){
		var m = d.getMonth(), y = d.getFullYear(), date = d.getDate(), first = new Date (y, m, 1);
		var prev = new Date (y, m-1, date), next = new Date (y, m+1, date);
		if (prev.getDate() != date) prev = new Date (y, m, 0); // adjust for too-short months
		if (next.getDate() != date) next = new Date (y, m+2, 0);
		return {
			first: first,
			last: new Date (y, m+1, 0),
			prev: prev,
			next: next,
			m: m,
			y: y,
			dow: first.getDay()
		};
	},
	monthNames: ['January','February','March','April','May','June',
		'July','August','September','October','November','December'],
	dayNamesMin: ['Su','Mo','Tu','We','Th','Fr','Sa'],
	isRTL: false,
	prevText: 'Previous',
	nextText: 'Next',
	years: function(n) {return n},
	days: function(n) {return n}
};

The plugin comes with three localizations defined (the ones I wanted): $.ui.flexcal.l10n.en (English-language civil calendar), $.ui.flexcal.l10n.jewish (Jewish calendar with English names) and $.ui.flexcal.l10n['jewish-he'] (Jewish calendar with Hebrew names), and two calendar-generating functions: $.ui.flexcal.calendars.gregorian and $.ui.flexcal.calendars.jewish.

The options object that is passed to the transition function was designed to allow drop-in use of Mike Alsup's excellent cycle plugin, with $(selector).flexcal({transition: $.fn.cycle.next}), though I haven't tested this yet.

Examples

French/English calendar. We grab the French localization from the datepicker svn.

<input id="date4"/>
$.getScript('http://dev.jqueryui.com/export/2435/trunk/ui/i18n/ui.datepicker-fr.js', function(){
  $.datepicker.setDefaults($.datepicker.regional['']);
  $.ui.flexcal.l10n.fr = $.datepicker.regional.fr;
  $('#date4').flexcal({
    position: 'rt',
    calendars: ['fr', 'en'],
    calendarNames: ['Français', 'Anglais']
  });
});

The French Revolutionary calendar. This is just to show off how flexible the widget is; 10-day weeks and 5-day months are no problem. View source to see the calendar algorithms.

<input id="date5"/>
$('#date5').flexcal({
  position: 'rt',
  calendars: ['en', 'jacobin']
});

flexcal with fancier transitions and using my scrollIntoView plugin (put the input box at the bottom of the window to see the scrolling effect)

<input id="date6"/>
$('#date6').flexcal({
	position: 'bl',
	calendars: ['en', 'jewish'],
	transition: function(o){
		var dir = o.rev ^ o.l10n.isRTL;
		var first = o.elements.eq(o.currSlide), second = o.elements.eq(1-o.currSlide);
		var h = o.$cont.height(), w = o.$cont.width();
		first.css({zIndex: 1});
		second.css({zIndex: 0}).show();
		first.animate({foo: 0}, { // the {foo:0} seems necessary because we need to animate some property, even if it isn't real
			duration: o.speed,
			step: function(now, fx){
				if (fx.state == 0){
					fx.start = now = dir ? 0 : w;
					fx.end = dir ? w : 0;
				}
				if (dir){
					first.css('clip', 'rect(0px '+w+'px '+h+'px '+now+'px)');
					second.css('clip', 'rect(0px '+now+'px '+h+'px 0px)');
				}else{
					first.css('clip', 'rect(0px '+now+'px '+h+'px 0px)');
					second.css('clip', 'rect(0px '+w+'px '+h+'px '+now+'px)');
				}
			},
			complete: function() {
				// clip is so inconsistently implemented. This way works in FF3, Opera 9, Safari 3,  IE7
				first.hide().css('clip', 'rect(auto)');
				second.css('clip', 'rect(auto)');
			}
		});
	},
	transitionOptions: {speed: 'slow'},
	shown: function() {$(this).flexcal('box').scrollIntoView()}
});

Using draggable. The option cancel: '.ui-state-default' makes sure that clickable elements aren't used as drag handles. Some position values require you have to do some CSS adjusting to keep the size from changing (draggable sets left and top; textpopup may set right and bottom). A real draggable calendar probably should have the cursor change on hover also.

<input id="date7"/>
<input id="date8"/>
$('#date7').flexcal({
  position: 'rt',
  calendars: ['en', 'fr'],
  calendarNames: ['', 'French']
}).flexcal('box').draggable({cancel: '.ui-state-default', cursor: 'move'});

$('#date8').flexcal({
  position: 'lt',
  calendars: ['en', 'fr'],
  calendarNames: ['', 'French']
}).flexcal('box').draggable({
  cancel: '.ui-state-default',
  cursor: 'move',
  start: function(){
    $(this).css({right: 'auto', bottom: 'auto'});
  }
});

Differences from datepicker

I intentionally gave this widget fewer options than datepicker; I just included what I thought I would need. Since it uses my subclass-able widget framework, it can easily be extended to be more capable. One thing that is still definitely lacking is keyboard accessibility; I don't know anything about that and have to start experimenting. To be added in some later version, for sure.

The power of Extending Widgets

Some examples of extending flexcal to be more datepicker-like.

Date formatting

The format for the date that is inserted into the text box is very simple; mm/dd/yyyy. You can subclass flexcal to use datepicker's formatting (as with any subclassing, you should look at the source code to figure out what the code is doing):

<input id="date9"/>
<input id="date10"/>

$.ui.flexcal.subclass('ui.fancyflexcal', {
  format: function(d){
    return $.datepicker.formatDate (this._getData('dateFormat'), d, $.datepicker.regional['']);
  }
});
$.ui.fancyflexcal.defaults.extend({
  dateFormat: $.datepicker.W3C
});
$('#date9').fancyflexcal();
$('#date10').fancyflexcal({dateFormat: 'D, M d, yy'});
Filtering dates

Filtering dates can be added similarly, or, even better, with aspect-oriented programming:

<input id="date11"/>

// allow weekdays only
$('#date11').flexcal().flexcal('after', '_adjustHTML', function (cal){
  cal.find('a.commit').filter(function(){
    var dow = (new Date(this.rel)).getDay();
    return (dow == 0 || dow == 6);
  }).removeClass('commit')['ui-unclickable']().addClass('ui-state-disabled');
});
Drop-down menus

Creating drop-down menus is a bit more complicated, because we can't assume that all the month names in the monthNames array are present in every year, and the definition of the localization calendar routine does not provide with us with a way to get the alternate calendar date for a given Date. The following routines help:


function option(d, l10n, cal, isMonth, selected){
  return [
    '<option',
    selected ? ' selected="selected"' : '',
    ' value="', $.ui.flexcal.date2string(d), '">',
    isMonth ? l10n.monthNames[cal.m] : l10n.years(cal.y), 
    '</option>'
  ].join('');
}
window.monthSelect = function(currentdate, l10n){
  var f = l10n.calendar;
  var currentcal = f(currentdate), ret = [option(currentdate, l10n, currentcal , true, true)], d = currentdate;
  for (var cal = currentcal; d = cal.prev, cal = f(d), cal.y == currentcal.y; ){
    ret.unshift(option(d, l10n, cal, true, false));
  }
  for (cal = currentcal; d = cal.next, cal = f(d), cal.y == currentcal.y; ){
    ret.push(option(d, l10n, cal, true, false));
  }
  return $('<select>').html(ret.join(''));
};
window.yearSelect = function(currentdate, l10n, n){
  var f = l10n.calendar;
  var currentcal = f(currentdate), ret = [option(currentdate, l10n, currentcal , false, true)], d = currentdate;
  for (var i = 0, cal = currentcal; d = cal.prevYear, cal = f(d), i < n; ++i){
    ret.unshift(option(d, l10n, cal, false, false));
  }
  for (var i = 0, cal = currentcal; d = cal.nextYear, cal = f(d), i < n; ++i){
    ret.push(option(d, l10n, cal, false, false));
  }
  return $('<select>').html(ret.join(''));
};
<input id="date12"/>

$('#date12').flexcal({'class': 'multicalendar', calendars: ['en','he-jewish']}).flexcal('after', '_adjustHTML', function (cal){
  cal.find('.ui-datepicker-month').html(monthSelect(this.d, this.o.l10n));
  cal.find('.ui-datepicker-year').html(yearSelect(this.d, this.o.l10n, 5));
  var self = this;
  cal.find('select').bind('change', function(){self.setDate($(this).val())});
});

{ 9 } Comments

  1. Erik | April 3, 2009 at 2:28 am | Permalink

    Great Widget!

  2. sravan | May 16, 2009 at 3:07 am | Permalink

    i need the code for the clip notes widget.
    could you please mai it to my email id.

  3. Danny | May 17, 2009 at 2:16 pm | Permalink

    @sravan:
    What clip notes widget? I don’t think I ever wrote anything called that.
    –Danny

  4. Axel | August 14, 2009 at 6:59 am | Permalink

    I have a problem with your code. I cannot make my example page run well.

    Could you please give me an email address where I could write to you ?
    I would like to send you my test.html page to check what is missing.

    Regards

  5. Danny | August 14, 2009 at 3:55 pm | Permalink

    @Axel:
    I unfortunately don’t have a lot of free time for support, but I can try to help. It would be easier if you posted a link to your website rather than emailing me code; most problems come from the interaction of little things that are hard to track down without looking at the whole thing in its “natural habitat”.
    –Danny

  6. Ed | September 10, 2009 at 3:08 am | Permalink

    Hi Danny, firstly thanks so much for creating this datepicker – incredibly useful!

    I am having a minor issue with it…I have set it up to use Joda time (http://joda-time.sourceforge.net/) supplied calendar information – so that any calendar system it supports, is supported by flexcal.

    My problem is that AJAX requests are sent off to a JSP file that returns information (using Joda time) about months for a particular system, to then determine what the calendar object returned should be. If the request is too slow, the calendar object falls back to the gregorian object you implemented. What I want to do is that as soon as that request has been processed, refresh the date picker to display the calendar using the correct calendar system.

    I have been trying to do this by literally invoking setDate(the last d) once the request is complete but it keeps flicking back to the date that it first opened with (today’s date). Any ideas? Is there another method I should be invoking to refresh the calendar?

    Thanks in advance.

  7. Danny | September 10, 2009 at 8:36 am | Permalink

    @Ed:
    Using AJAX to get the calendar from the server sounds cool. flexcal as it stands isn’t set up to deal with asynchronous changes, but it ought to be possible to override setdate to do that. Can you send me your Javascript code and I can try to think about it? I’m real busy, so no promises, but I’m intrigued enough to ignore my real responsibilities to look at it.
    At worst, you could do the AJAX synchronously, but that would mean a seriously unresponsive page.
    –Danny (d.wachss@prodigy.net)

  8. Alexandr | December 29, 2009 at 1:22 am | Permalink

    Hi, Examples don`t work

  9. Danny | December 31, 2009 at 9:39 pm | Permalink

    @Alexandr:
    What system are you using? They work for me in IE8 and FF3.5.
    –Danny

Post a Comment

Your email is never published nor shared. Required fields are marked *