Friday, March 27, 2009

Enhance Grails g:datePicker with jscalendar

Introduction


(Note 2009-05-17: Calendar plugin 1.2.0-SNAPSHOT broke interface of jscalendar by adding custom code to calendar.js file. You either have to use earlier calendar plugin, replace calendar.js file with original or change the implementation to adapt to Calendar plugin 1.2.0., if possible. Another option is to use original jscalendar without support of calendar plugin at all. Personally I do not like the original - non plugin - calendar.js is modified by plugin developer. Plugin should be wrapper around calendar.js.)



Grails has g:datePicker gsp tag to create editable date and time field.
With g:datePicker you can choose current date and time through combo boxes (selects).



There is also calendar plugin which brings jscalendar into grails. You can open jscalendar (e.g. clicking on calendar picture) and choose date and time. It is not possible to edit date in associated text field (there is JIRA issue for this).



Very often, you would like to have both functionality combined. I have created javascript function that links g:datePicker with jscalendar via onclick action.

The result looks like this (by clicking on image, cale
ndar pop-ups):



I have tried to meet following requirements:
  • reuse g:datePicker configuration in associated jscalendar as much as possible (no need to repeat configuration parameters)
  • automatic synchronization between value in g:datePicker and jscalendar
  • support for internationalization - add possibility to use time format from calendar lang file, use first day of week from calendar lang file
  • simple to use

When user clicks on calendar image/button associated with g:datePicker, jscalendar widget is created and displayed. It already has date (and time in case g:datePicker precision parameter is hour or minute) filled with date corresponding to values in g:datePicker combo boxes. By changing the date (and time) in jscalendar the g:datePicker values change as well. Single click on the date closes calendar.



If javascript is disabled (e.g. with NoScript Firefox add on), user can still select date and time through g:datePicker.

Implementation

Javascript function is called showCalendar and it links jscalendar
with g:datePicker.

g:datePicker accepts following arguments (see documentation):

name, value, precision, years and noSelection

When examining the source code for g:datePicker, you can see it creates several combo box elements with following ids:

${name}_year
${name}_month
${name}_date
${name}_hour
${name}_minute


According to precision parameter, some combo boxes may not be created . Year combo box is always created.

In gsp page you link
showCalendar function with g:datePicker in following way:
<g:datePicker name="reminderAt" value="${eventInstance?.reminderAt}" ></g:datePicker>
<img src="${createLinkTo(
dir: org.codehaus.groovy.grails.plugins.PluginManagerHolder.currentPluginManager().getGrailsPlugin("calendar").getPluginPath(),
file: "/images/skin/calendar.png")}"
id="reminderAt-trigger" alt="Date"
onclick="showCalendar('reminderAt');"/>
(Note: In this example I have tried to re-use calendar image directly from the calendar plugin. That is why the dir path is so cumbersome. If you know of better way how to get plugin's dir, please let me know. You can use path to any other image or use button instead.)

The parameters of showCalendar function are:
  • name of g:datePicker to link function with
  • boolean value indicating if time is displayed in 24h format - this parameter is optional and can be omitted (default is true = 24 hour format - this is in line with g:datePicker format). Preferred way to set time format should be through calendar-{lang}.js (see below).
From calendar-{lang}.js following parameters are used:
  • Calendar._FD - first day of week (if not specified, default value is 0 = Sunday)
  • Calendar._TIME24 - if true, 24h time format is used (if specified it overwrites second parameter of showCalendar function). This parameter is newly introduced by my implementation (as I thing this is lang specific like Calendar._FD) and you have to modify calendar-{lang).js if you want to use it. Otherwise pass time format as second parameter when calling showCalendar.

Calendar creation

Upon creation of jscalendar the fu
nction tries to find out most of the parameters from associated g:datePicker.

Following jscalendar parameters are configured at that time:
  • date corresponding to the date in g:datePicker or today's date if year, month or day in the g:datePicker corresponds to noSelection value
  • allowed year range corresponding to possible values in ${name}_year combo box element
  • presence of time setup - time setup is part of jscalendar only if ${name}_hour element exists
  • time format is set to 24h format by default; it can be changed to 12h format by passing false as second (optional) parameter to the function; if time format is specified in calendar-{lang}.js (Calendar._TIME24), it is used instead, with higher priority.
Calendar is displayed next to ${name}_year combo box element (this element is always present regardless of precision attribute).

Note: Since jscalednar is linked to g:datePicker, there is no need to configure date format in any way, which makes configuration easy.

Selection - event handling:

When date is changed in
jscalendar, event function selected is called. It extracts selected Date and updates g:datePicker combo boxes accordingly.

Source code:

/*
File: calendar-gdatepicker.js
Grails g:datePicker binding to the
DHTML Calendar www.dynarch.com/projects/calendar
version: 1.1
history:
1.1 2009-03-29 firstDayOfWeek is retrieved from calendar-{lang}.js file Calendar._FD
time24 format is retrieved from calendar-{lang}.js file Calendar._TIME24
(Michal Novak)
1.0 2009-03-28 Initial version (Michal Novak)

Copyright 2009, Michal Novak, bubbles.way@gmail.com
http://it-bubbles.blogspot.com/

This script is distributed under the GNU Lesser General Public License.
Read the entire license text here: http://www.gnu.org/licenses/lgpl.html

This script is base on examples on following page:
http://www.dynarch.com/static/jscalendar-1.0/index.html
*/

// return value given by index of combo box element or null if combo box element is null
function comboGetValueAt(comboBoxElem, index) {
if (comboBoxElem != null) {
if (comboBoxElem.options.length > index) {
return comboBoxElem.options[index].value;
}
}
return null;
}

// return selected value of combo box element or null if combo box element is null
function comboSelectedValue(comboBoxElem) {
if (comboBoxElem != null) {
return comboGetValueAt(comboBoxElem, comboBoxElem.selectedIndex)
}
return null;
}

// select value in combo box element (if combo box element is not null)
function selectCombo(comboBoxElem, value) {
if (comboBoxElem != null) {
for (var i = 0; i < comboBoxElem.options.length; i++) {
if (comboBoxElem.options[i].value == value &&
comboBoxElem.options[i].value != "") { //empty string is for "noSelection handling as "" == 0 in js
comboBoxElem.options[i].selected = true;
break
}
}
}
}

// This function gets called when the end-user clicks on some date.
function selected(cal) {
selectCombo(cal.g_year, cal.date.getFullYear())
selectCombo(cal.g_month, cal.date.getMonth() + 1)
selectCombo(cal.g_day, cal.date.getDate())
selectCombo(cal.g_hour, cal.date.getHours())
selectCombo(cal.g_minute, cal.date.getMinutes())
if (cal.dateClicked)
//close the calendar on single-click.
cal.callCloseHandler();
}

// This function gets called when the end-user clicks on the _selected_ date,
// or clicks on the "Close" button. It just hides the calendar without destroying it.
function closeHandler(cal) {
cal.hide() // hide the calendar
_dynarch_popupCalendar = null
}

// This function shows the calendar under the year element of the g:datePicker
// first parameter is id (name) of g:datePicker element
// second parameter (optional) specifies if time should be displayed in 24h format (default is true)
// other necessary configuration is extracted directly from g:datePicker
function showCalendar(datePickerId,time24) {
if (_dynarch_popupCalendar != null) { // we already have some calendar created
_dynarch_popupCalendar.hide() // so we hide it first.
}
// create the calendar
var firstDayOfWeek = Calendar._FD == null?0:Calendar._FS // default is 0 (Sun) if not specified in lang file
time24 = time24 == null?true:false // default time format is true (24h)
time24 = Calendar._TIME24 == null?time24:Calendar._TIME24 // time format can be overwritten from lang file
_dynarch_popupCalendar = new Calendar(firstDayOfWeek, null, selected, closeHandler)
// remember grails date picker elements
_dynarch_popupCalendar.g_year = document.getElementById(datePickerId + '_year')
_dynarch_popupCalendar.g_month = document.getElementById(datePickerId + '_month')
_dynarch_popupCalendar.g_day = document.getElementById(datePickerId + '_day')
_dynarch_popupCalendar.g_hour = document.getElementById(datePickerId + '_hour')
_dynarch_popupCalendar.g_minute = document.getElementById(datePickerId + '_minute')
_dynarch_popupCalendar.showsTime = _dynarch_popupCalendar.g_hour != null // display time only if grails has hour element
_dynarch_popupCalendar.time24 = time24
// find out year range from grails date picker
var minYear = comboGetValueAt(_dynarch_popupCalendar.g_year, 0) // get first item
var maxYear = comboGetValueAt(_dynarch_popupCalendar.g_year, _dynarch_popupCalendar.g_year.options.length - 1) // get first item
_dynarch_popupCalendar.setRange(parseInt(minYear), parseInt(maxYear)) // min/max year allowed.
_dynarch_popupCalendar.create()
var yearValue = comboSelectedValue(_dynarch_popupCalendar.g_year)
var monthValue = comboSelectedValue(_dynarch_popupCalendar.g_month)
var dayValue = comboSelectedValue(_dynarch_popupCalendar.g_day)
var hourValue = comboSelectedValue(_dynarch_popupCalendar.g_hour)
var minuteValue = comboSelectedValue(_dynarch_popupCalendar.g_minute)
// day and month may not be present in grails date picker and if set to null, it makes calendar to break
monthValue = monthValue == null || monthValue == "" ? 0 : parseInt(comboSelectedValue(_dynarch_popupCalendar.g_month)) - 1
dayValue = dayValue == null ? 1 : comboSelectedValue(_dynarch_popupCalendar.g_day)
var ourDate = new Date(yearValue, monthValue, dayValue, hourValue, minuteValue)
// handle grails date picker "noSelection: in year, month or day combo box, in this case set value to today
if (ourDate.getDate() != dayValue || ourDate.getMonth() != monthValue || ourDate.getFullYear() != yearValue) {
ourDate = new Date()
}
_dynarch_popupCalendar.setDate(ourDate)
_dynarch_popupCalendar.showAtElement(_dynarch_popupCalendar.g_year) // show the calendar
}


Download link

Integration with grails


Integration with grails is simple. Copy calendar-gdatepicker.js source file to web-app/js directory of your grails project.
Import jscalendar and calendar-gdatepicker.js scripts on your page.

Since Grails already have calendar plugin, I think the best way is to use this plugin to import jscalendar and jscalendar localization scripts. Then you can choose which localization and theme to use (this requires that calendar plugin is installed in grails).

E.g:
<calendar:resources lang="en" theme="system"/>
(Of course another way is to import jscalendar scripts directly).

Import calendar-gdatepicker.js script:
<script type="text/javascript" src="${createLinkTo(dir:'js',file:'calendar-gdatepicker.js')}"></script>
Now, you are ready to use jscalendar linked with g:datePicker. You only need to declare element (usually after g:datePicker tag) with onclick method set to showCalendar with first parameter specifying the name of g:datePicker. The most appropriate element is usually image or button.

E.g:
<g:datePicker name="reminderAt" value="${eventInstance?.reminderAt}" ></g:datePicker>
<img src="${createLinkTo(
dir: org.codehaus.groovy.grails.plugins.PluginManagerHolder.currentPluginManager().getGrailsPlugin("calendar").getPluginPath(),
file: "/images/skin/calendar.png")}"
id="reminderAt-trigger" alt="Date"
onclick="showCalendar('reminderAt');"/>

In InternetExplorer the jscalendar plugin (ver. 1.1.1) provides styling than makes calendar to stretch. This should be fixed in 1.2.0 according to this issue.

Sample demo project

I have created demo project that displays calendar from the plugin, calendar linked to the g:datePicker with full precision and calendar linked to the g:datePicker with day precision.
Look for calendar-gdatepicker.js and create.gsp files in the project for example code.
If you change lang parameter from "en" to "cs" (in create.gsp) you can see how calendar adjusts for Czech locale (calendar-cs.js is included).




Conclusion

By extending g:datePicker with jscalendar support you can simplify date and time selection. Selection can be done directly in jscalendar, there is no need to go through all combo box elements of g:datePicker (on the other hand, if javascript is disabled, there is still a way to select date and time just through g:datePicker
without jscalendar)

I believe this implementation (or similar) should become part of calendar plugin. New custom tag for linking g:datePicker with jscalendar would simplify usage in gsp files. This tag can have parameter specifying to show button or calendar image. Another benefit would be that there will be no need to import
calendar-gdatepicker.js as this could be handled by plugin's resource tag. I think such integration will resolve this issue as well.

Feel free to use the script if you like it for any purpose. If you modify it, please keep original header information and add description of your modification to the header.

This is my first javascript related work, so please be patient with possible errors or parts that does not conform to best practice rules.

External links: