Updated 2015-02-25 to version 3.0, with the new subclassing code.

Updated 2015-02-16 to document changes in default URL and need for separate CSS file.

Updated 2015-02-13 to document additional parameters.

Updated 2015-01-28 to link to github.

Just what the world needs—another date picker

Download the code from github. You need jquery.ui.subclass.js, jquery.textpopup.js, /inc/jquery.flexcal.js and flexcal.css.

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 a data URL (so no waiting for AJAX loading), which contains:

Note that all the required CSS is the flexcal.css 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. Setting 'class' to fontawesome makes the calendar use Font Awesome icons rather than the jQuery UI ones.

The flexcal options themselves are:

calendars {Array(String || Object)}
Array of calendars to display. Each string is a key into the $.bililite.flexcal.l10n object. Default is ['en'], which means that the default is one calendar, using the localization of $.bililite.flexcal.l10n.en. Calendar names are always of the form lowercase two-letter language code, then optionally an upper case two letter country code, then optionally a lower case calendar system name (default: 'gregorian'), all delimited by hyphens. Thus, 'zh-TW-islamic'. The code will try to find a localization in the jQuery UI datepicker or in Keith Woods's calendarspicker if available. If the string cannot be recognized as a calendar name, it is assumed to mean {name: name}, a localization object where everything is the default except the displayed name of the calendar.
If the item is an object then it is the localization object itself. If the item is itself an array, then each item is recursively evaluated and all the resulting localization objects are $.extended together.
Thus, calendars: ['en', 'he-jewish', ['ar-islamic', 'Arabic']] displays three calendars, an English Gregorian calendar, a Hebrew Jewish calendar (both with their default names) and an Arabic Islamic calendar named 'Arabic' (the string is not a calendar name, so it is interpreted as {name: 'Arabic'} which modifies the $.bililite.flexcal.l10n['ar-islamic'] object.
tab {Number}
Index into the calendars array of the one to display when initially shown
current {Date|String}
If the element value is not set to a valid date, then use this as the initial display date.
filter {undefined|Function}
Function to filter dates. Function is of the form f(d) where d is a Date object; it should return true enable that date or false to disable it. this is set to the <a> element that contains the date number. See the examples below.
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. Default is 'conditional'.
reposition {true|false}
True to reposition the calendar with every transition; useful if the calendar is above the text element and transitions to months with more weeks obscures the box. Default is True.
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
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}
Number of the year of d
m {Number}
0-indexed number of the month of d
d {Number}
Date of the month of d
dow {Number}
0-indexed number of the day of the week of the first day of the month. Note that this is not necessarily just first.getDay(), since this calendar system may use some other "week" system.
toDate {Function}
Function that takes an object {y: y, m: m, d: d} representing the date in this calendar and returns the corresponding Javascript Date. Not currently used; I plan on implementing some sort of date parsing (Issue #4) that will use this. It is optional; if not present, will use a binary search to find the right date. If the values given are invalid, should either return new Date(undefined) or do some error correcting the way new Date(y, m, d) does, e.g. turning 0 dates into the last day of the previous month.
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
firstDay {Number}
Day of the week (0-indexed) that the calendar should start. Not currently implemented (Issue #5), always assumes 0.
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}
fromYears {Function}
Inverse of years, above. Function to convert a string into a number. If undefined, will not parse year strings but only allow numbers. Not currently used; I plan on implementing some sort of date parsing (Issue #4) that will use this.
dates{Function}
Function to convert a date number to the displayed string. Default is function(n) {return n}
fromDates {Function}
Inverse of years, above. Function to convert a string into a number. If undefined, will not parse date strings but only allow numbers.. Not currently used; I plan on implementing some sort of date parsing (Issue #4) that will use this.

// default l10n calendar
$.bililite.flexcal.prototype.options.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 API (publicly accessible methods) is documented in a later post.

The plugin comes with three localizations defined (the ones I wanted): $.bililite.flexcal.l10n.en (English-language civil calendar), $.bililite.flexcal.l10n.jewish (Jewish calendar with English names) and $.bililite.flexcal.l10n['he-jewish'] (Jewish calendar with Hebrew names), and two calendar-generating functions: $.bililite.flexcal.calendars.gregorian and $.bililite.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 it doesn't work that easily.

Examples

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

<input id="date4"/>
$.getScript('https://rawgit.com/jquery/jquery-ui/master/ui/i18n/datepicker-fr.js', function(){
  $.datepicker.setDefaults($.datepicker.regional['']);
  $.bililite.flexcal.l10n.fr = $.datepicker.regional.fr;
  $('#date4').flexcal({
    position: 'rt',
    calendars: [['fr', 'Français'], ['en','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. 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', 'French']],
  reposition: false
}).flexcal('box').draggable({cancel: '.ui-state-default', cursor: 'move'});

$('#date8').flexcal({
  position: 'lt',
  calendars: ['en',  ['fr', 'French']],
  reposition: false
}).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"/>

$.widget('bililite.fancyflexcal', $.bililite.flexcal, {
  format: function(d){
    return $.datepicker.formatDate (this.options.dateFormat, d);
  },
  options: {
    dateFormat: $.datepicker.W3C
  }
});

$('#date9').fancyflexcal();
$('#date10').fancyflexcal({dateFormat: 'D, M d, yy'});
Filtering dates

Filter dates with the filter option:

<input id="date11"/>
// allow weekdays only
$('#date11').flexcal({
  filter: function(d){ return d.getDay() != 0 && d.getDay() != 6; }
});

Using the filter option to manipulate the css of the calendar:

<input id="date12"/>
// Put a border around every other Thursday (it's payday!)
$('#date12').flexcal({
  filter: function(d){
    // is it Thursday and is it an even numbered week since the epoch?
    if (d.getDay() == 4 && Math.floor(d.getTime()/(1000*60*60*24*7)) %2 == 0){
      $(this).css('border', '2px solid purple'); // put a nice border on it
    }
    return true; // don't disable anything
  }
});
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="', d.toString(), '">',
    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="date13"/>

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

51 Comments

  1. Tova says:

    Thank you !
    Now in Explorer in order to close the date picker I need to double click the selected date… any idea? Your example works fine…

    Sorry for driving you crazy

Leave a Reply


Warning: Undefined variable $user_ID in /home/public/blog/wp-content/themes/evanescence/comments.php on line 75