Flexcal

jQuery UI datepicker

View the Project on GitHub dwachss/flexcal

Localization

Each calendar in the calendars array is a localization object or a string that determines a localization object. Each localization object specifies a calendar system; the most common is the Gregorian calendar, though I created flexcal to be able to use the Jewish calendar. Note that Wikipiedia calls it the "Hebrew calendar", and that seems to be the Unicode standard as well. I am convinced that is wrong; Hebrew is the language, Jewish is the calendar. It would be inappropriate to call the Islamic calendar, for instance, the Arabic calendar.

Be that as it may, the localization object also specifies the names of the months and the days of the week, and the way to format date and year numbers. It is assumed that there are a limited number of months and days in a week (a seven-day week is not assumed) so an array of names is appropriate. Dates in a month and years are assumed to be numeric, but can use numbering systems other than Latin numerals.

Elements

Each localization object contains the elements that are locale-specific; anything not included is taken from thel10n option and its defaults. Most of the items and their names are taken from the jQuery UI datepicker.

name

Name of the calendar to display in the tab bar.

Default value

'flexcal'

calendar

Function that represents the calendar system.

Default value

$.bililite.flexcal.calendars.gregorian

monthNames

Array of month names that are displayed on the calendar.

Default value

['January','February','March','April','May','June', 'July','August','September','October','November','December']

monthNamesShort

Array of abbreviated month names. Not used in displaying the calendar, or in the simplest version of flexcal at all. I plan to expand the format method to allow using these.

Default value

['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']

dayNamesMin

Array of minimized names of the days of the week. This is what is displayed on the calendar.

Default value

['Su','Mo','Tu','We','Th','Fr','Sa']

dayNames

Array of names of the days of the week. Not used in displaying the calendar, or in the simplest version of flexcal at all. I plan to expand the format method to allow using these.

Default value

['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']

dayNamesShort

Array of abbreviated names of the days of the week. Not used in displaying the calendar, or in the simplest version of flexcal at all. I plan to expand the format method to allow using these.

Default value

['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']

prevText

Text to display on the "previous month" button. The stylesheet for the calendar replaces this with an icon and uses this text as the tooltip. Per the jQuery UI guidelines, it should not contain any arrows or chevrons.

Default value

'Previous'

nextText

Text to display on the "next month" button. The stylesheet for the calendar replaces this with an icon and uses this text as the tooltip. Per the jQuery UI guidelines, it should not contain any arrows or chevrons.

Default value

'Next'

isRTL

Boolean that should be true to display the calendar right-to-left.

Default value

false

firstDay

Number of the day of the week (0-indexed) that the calendar should start.

Default value

0

years

Function to convert a year number into a string to display. The caption of the calendar is monthNames[month]+' '+years(year)

Default value

function(n) {return n.toString()}

fromYears

Function to convert a string into a year number. If undefined, then only numbers can be parsed as years. Meant to be the inverse of years. Not used in displaying the calendar, or in the simplest version of flexcal at all. I plan to expand the parse method to allow using this.

Default value

undefined

dates

Function to convert a date number into a string to display. Each <td> in the calendar has the text dates(date)

Default value

function(n) {return n.toString()}

fromdates

Function to convert a string into a date number. If undefined, then only numbers can be parsed as dates. Meant to be the inverse of dates. Not used in displaying the calendar, or in the simplest version of flexcal at all. I plan to expand the parse method to allow using this.

Default value

undefined

dateFormat

String that represents the format to use for converting from a string (the value of the attached text box) to a Date object. See the full description below.

Default value

'm/d/yyyy'

Text Localization

In addition to localizing the calendar, flexcal allows for localizing text in buttons and the like. Any element with a data attribute data-flexcal-l10n will have the value of that attribute + 'Text' used as the key into the localization object, and the result is used as the HTML of the element. Thus

<span data-flexcal-l10n=today ></span>
will have its content replaced with l10n.todayText for the current localization object.

The localizations included in flexcal include todayText and closeText (along with prevText and nextText used in the calendars themselves). jQuery UI datepicker calls the text for "today" currentText, but I think that's wrong.

If setting the attribute from javascript, you can use $(...).attr('data-flexcal-l10n', 'today') or use jQuery's data method: $(...).data('flexcalL10n', 'today'), remembering to camelCase the name of the data key to "flexcalL10n".

See the buttons page for examples.

Calendar systems

A calendar system is represented by a function that takes a Date d object and returns an object with the following fields:

first

Date of the first day of the month containing d.

last

Date of the last day of the month containing d.

prev

Date of the day exactly one month before d.

next

Date of the day exactly one month after d.

prevYear

Date of the day exactly one year before d.

nextYear

Date of the day exactly one year after d.

y

Number of the year of d, in this calendar system. This is passed to years for display.

m

Number (0-indexed) of the month of d, in this calendar system. This is the index into monthNames.

d

Number of the date of d, in this calendar system. This is passed to dates for display.

dow

0-indexed number of the day of the week of the first day of the month (not of d. Note that this is not necessarily just first.getDay(), since this calendar system may use some other "week" system.

toDate

Function that takes an object {y: y, m: m, d: d} representing a date in this calendar and returns the corresponding Javascript Date. Not currently used; I plan to expand the parse method to allow using this. 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.

Examples

I implemented two calendar systems that I wanted, and they are stored in $.bililite.flexcal.calendars: Gregorian and Jewish.

$.bililite.flexcal.calendars.gregorian


	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);
			var nextYearDate = m == 1 && date == 29 ? 28 : date;
			return {
				first: first,
				last: new Date (y, m+1, 0),
				prev: prev,
				next: next,
				prevYear: new Date (y-1, m, nextYearDate),
				nextYear: new Date (y+1, m, nextYearDate),
				m: m,
				y: y,
				d: d,
				dow: first.getDay(),
				toDate: function (d) {return new Date (d.y, d.m, d.d)}
			};
		}
	

$.bililite.flexcal.calendars.jewish

Define a function


		function addDay(d, n){
		if (n === undefined) n = 1;
		return new Date(d.getFullYear(), d.getMonth(), d.getDate()+n);
	}
	
and functions civ2heb(d) that takes a Javascript Date and returns an object {y: y, m: m, d: d} representing the Jewish date, and heb2civ(h) which does the reverse. See the source code for details. The civ2heb function also includes a daysinmonth field that is the number of days in that Jewish month. Then the calendar function is:


	function(d){
			var h = civ2heb(d);
			var roshchodesh = addDay(d, -h.d+1);
			var daysinlastmonth = Math.max(civ2heb(addDay(roshchodesh,-1)).daysinmonth, h.d); //  the min/max() correct for the possibility of other month being too short
			var daysintonextmonth = Math.min(civ2heb(addDay(roshchodesh, h.daysinmonth)).daysinmonth, h.d);
			return {
				first: roshchodesh,
				last: addDay(roshchodesh, h.daysinmonth-1),
				prev: addDay(d, -daysinlastmonth),
				next: addDay(roshchodesh, h.daysinmonth+daysintonextmonth-1),
				prevYear: heb2civ($.extend({}, h, {y: h.y-1})),
				nextYear: heb2civ($.extend({}, h, {y: h.y+1})),
				m: h.m,
				y: h.y,
				d: h.d,
				dow: roshchodesh.getDay(),
				toDate: heb2civ
			};
		}
	

Specifying Strings as Localization Objects

If a string is passed as the localization object, it is assumed to be the key into the $.bililite.flexcal.l10n, which comes with three entries predefined: 'en' (English localization of the Gregorian calendar), 'jewish' (English localization of the Jewish calendar), and 'he-jewish' (Hebrew localization of the Jewish calendar).

Note that the standard I am using is languageCode-calendarSystem, where calendarSystem is the index into the $.bililite.flexcal.calendars array.

Using jQuery UI datepicker

Datepicker comes with a large number of localization files that can be easily used with flexcal. You need to include the localization file, with something like <script src="https://cdn.rawgit.com/jquery/jquery-ui/master/ui/i18n/datepicker-fr.js"></script> to get the French localization. flexcal will then interpret 'fr' as a reference to $.datepicker.regional['fr'].

Note that all the datepicker localizations are for the Gregorian calendar. Note also that including localization file sets the defaults for datepicker to that language, so if you want to actually use datepicker, you may want to call $.datepicker.setDefaults($.datepicker.regional['']) to set them back. Note also that not all the fields that flexcal uses are set in the datepicker localization files, especially name, so you may want to set that separately (see the calendars example).

Using Keith Wood's calendarspicker

Keith Wood is maintaining the jQuery plugin that eventually became the official jQuery UI datepicker. His version, however, handles multiple calendar systems, much like flexcal (though I like my plugin, of course). His code is on github, and the documentation is on his personal site. He has support for many calendar systems, and I wanted to let flexcal use that. I don't use his datepicker code, just the calendar systems.

To use it, include the basic calendar code with <script src="https://cdn.rawgit.com/kbwood/calendars/master/jquery.calendars.js"></script> and all the localization files. To use the Arabic localization, include <script src="https://cdn.rawgit.com/kbwood/calendars/master/jquery.calendars-ar.js"></script> and 'ar' as the localization object. To use the Islamic calendar with Arabic localization, include both <script src="https://cdn.rawgit.com/kbwood/calendars/master/jquery.calendars.islamic.js"></script> and <script src="https://cdn.rawgit.com/kbwood/calendars/master/jquery.calendars.islamic-ar.js"></script>, and use 'islamic' for the Islamic calendar with English localization, and 'ar-islamic' for the Islamic calendar with Arabic localization.

Some localization names use the 2-letter language code plus a country code, like 'en-GB'. flexcal is smart enough to handle that.

Note that the name field in these localization files is only the name of the calendar system, so you may want to set that separately.Note also that the prevText and nextText fields are not set in the calendar localization files, but in his datepicker localization files. So including that requires including the entire datepicker file, <script src="https://cdn.rawgit.com/kbwood/calendars/master/jquery.calendars.picker.js"></script> and the localization file, <script src="https://cdn.rawgit.com/kbwood/calendars/master/jquery.calendars.picker-ar.js"></script>. So as note to require including the whole datepicker (I assume you are using flexcal!) I created a stub that can be included instead, jquery.calendars.picker-mock.js. You still have to include the picker localization file, like jquery.calendars.picker-ar.js.

Undefined Names

Any string that cannot be interpreted as an index into an array of localization objects will be interpreted as simply a name. So 'French' is interpreted as {name: 'French'}. All actual localization names start with a lower case letter.

tol10n

As a utility function, localization objects can be created with a standalone function, $.bililite.flexcal.tol10n(name, defaultL10n). name can be an object, which is taken as the actual localization object, or a string, which is interpreted as above. If it is an array, then each element is evaluated and the results $.extended together. defaultL10n must be an object, and it will be $.extended with the result of evaluating name. If defaultL10n is undefined, $.bililite.flexcal.prototype.options.l10n is used.

Examples

$.bililite.flexcal.tol10n('fr') (if the datepicker localization file is included) returns

{
	calendar: function(d) {...} // $.bililite.flexcal.calendars.gregorian,
	closeText: "Fermer", // used by datepicker, not flexcal
	currentText: "Aujourd'hui",
	dateFormat: "dd/mm/yy",
	dates: function (n) {return n.toString()},
	dayNames: ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"],
 	dayNamesMin: ["D","L","M","M","J","V","S"],
	dayNamesShort: Array["dim.", "lun.", "mar.", "mer.", "jeu.", "ven.", "sam."],
	firstDay: 1,
	isRTL: false,
	monthNames:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],
	monthNamesShort: ["janv.","févr.","mars","avr.","mai","juin","juil.","août","sept.","oct.","nov.","déc."]
	name: "flexcal", // from the default
	nextText: "Suivant",
	prevText: "Précédent",
	showMonthAfterYear: false,
	weekHeader: "Sem.",
	yearSuffix: "",
	years: function (n) {return n.toString()
}

And $.bililite.flexcal.tol10n(['fr', 'French', {dateFormat: 'yyyy-mm-dd'}]) returns

{
	calendar: function(d) {...} // $.bililite.flexcal.calendars.gregorian,
	closeText: "Fermer", // used by datepicker, not flexcal
	currentText: "Aujourd'hui",
	dateFormat: "yyyy-mm-dd", // from the third element
	dates: function (n) {return n.toString()},
	dayNames: ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"],
 	dayNamesMin: ["D","L","M","M","J","V","S"],
	dayNamesShort: Array["dim.", "lun.", "mar.", "mer.", "jeu.", "ven.", "sam."],
	firstDay: 1,
	isRTL: false,
	monthNames:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],
	monthNamesShort: ["janv.","févr.","mars","avr.","mai","juin","juil.","août","sept.","oct.","nov.","déc."]
	name: "French", // from the second element
	nextText: "Suivant",
	prevText: "Précédent",
	showMonthAfterYear: false,
	weekHeader: "Sem.",
	yearSuffix: "",
	years: function (n) {return n.toString()
}

Formatting and Parsing

flexcal as it stands has a minimal formatting/parsing capability. The formatting specification is based on that of Keith Wood's calendars, except for not using 2-digit years. It is also compatible with the jQuery UI formats.

The codes are:

Any other string is interpreted literally.

format

There are three utility functions, $.bililite.flexcal.format(d, format, l10n), $.bililite.flexcal.parse(s, format, l10n) and $.bililite.flexcal.localize(s, l10n).

$.bililite.flexcal.format(d, l10n) takes three arguments (none are optional), a Date d, a formatting string and a localization object l10n. l10n has to be an object; to use a string, run it through tol10n, as $.bililite.flexcal.format(new Date(2015,3,8), 'dd-mm-yyyy', $.bililite.flexcal.tol10n('fr')). It returns a string with the date formatted according to format. In the last example, the result would be '08-04-2015'.

parse

$.bililite.flexcal.parse(s, format, l10n) is the reverse. It takes a string s and tries to parse it according to l10n.dateFormat. It tries to obey Postel's law, "Be conservative in what you send, be liberal in what you accept", and accept anything that looks like the format. It just looks for three numbers and interprets them in the order in the format string. A completely uninterpretable string returns new Date(NaN).

localize

$.bililite.flexcal.localize(s, l10n) is for localizing individual strings. Just returns l10n[s+'Text'] || ''.

I hope to write a more complete formatter/parser in the future (Issue #4).

Examples

See the examples for the calendars option, and the examples in the introduction

.

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.

archaicNumbers

The French Revolutionary Calendar example uses a utility function for translating normal numbers into Roman numerals. $.bililite.flexcal.archaicNumbers(arr) takes an array of equivalent numbers and returns an object {format: formatFunction(n), parse: parseFunction(s)}. formatFunction(n) takes a number and returns a string that is the equivalent. This function can be used for the dates and years options. So:

var convertToRoman = $.bililite.flexcal.archaicNumbers([ 
	[1000, 'M'], 
	[900, 'CM'], 
	[500, 'D'], 
	[400, 'CD'], 
	[100, 'C'], 
	[90, 'XC'], 
	[50, 'L'], 
	[40, 'XL'], 
	[10, 'X'], 
	[9, 'IX'], 
	[5, 'V'], 
	[4, 'IV'], 
	[1, 'I'] 
]);
assert.equal (convertToRoman.format(2015), 'MMXV');

parseFunction(s) does the reverse, parsing a string into a number. This function can be used for the fromDates and fromYears options.So:

assert.equal (convertToRoman.parse('MMXV'), 2015);

The parsing is very liberal in what it accepts and just ignores unrecognized characters. This may not be appropriate for some numeric systems and the output or input may need further processing.