This is a re-write of my original DHTML Date Picker code release 7/07
Starting with Version 0.3.3 There are two versions of jCal:
jCal.js
14KB - 3.5KB +gzip(.jsgz)
packer 10KB - 2.7KB +gzip(.jsgz)
jsmin 11KB - 2.8KB +gzip(.jsgz)
includes selectable month and year pulldowns (disabled by default) used in working demo below |
jCal-light.js
10KB - 2.8KB +gzip(.jsgz)
packer 8KB - 2.2KB +gzip(.jsgz)
jsmin 8KB - 2.2KB +gzip(.jsgz)
does not include selectable month and year pulldowns |
WORKING DEMO
Google Code Page http://code.google.com/p/jquery-jcal
jQuery.com Project Page http://plugins.jquery.com/project/jCal
releases
- jCal.0.3.6.zip - LATEST includes jCal.js core, jCal-light.js (lighter version without month or year pulldowns), jsmin’d versions, packer versions, pre-compressed gzip .jsgz versions, jCal.css, jquery.animate.clip.js plugin, example instantiation and images
- jCal.0.3.5.zip - LATEST includes jCal.js core, jCal-light.js (lighter version without month or year pulldowns), jsmin’d versions, packer versions, pre-compressed gzip .jsgz versions, jCal.css, jquery.animate.clip.js plugin, example instantiation and images
- jCal.0.3.4.zip - LATEST includes jCal.js core, jCal-light.js (lighter version without month or year pulldowns), jsmin’d versions, packer versions, pre-compressed gzip .jsgz versions, jCal.css, jquery.animate.clip.js plugin, example instantiation and images
- jCal.0.3.3.zip - LATEST includes jCal.js core, jCal-light.js (lighter version without month or year pulldowns), jCal.css, jquery.animate.clip.js plugin, example instantiation and images
- jCal.0.3.2.zip - LATEST includes jCal.js core, jCal.css, example instantiation and images
- jCal.0.3.1.zip - LATEST includes jCal.js core, jCal.css, example instantiation and images
- jCal.0.3.0.zip (archived version)
- jCal.0.2.3.zip - (archived version with patch from http://dev.jquery.com/ticket/2973)
- jCal.0.2.2.zip (archived version)
- jCal.0.2.1zip (archived version)
- jCal.0.2.zip (archived version)
- jCal.0.1.1.zip (archived version)
tested browsers
- IE7
- FF 2.X-3.1
- IE6.0.2800 (doesn’t mix with pulldowns)
- IE6.0.2900 (doesn’t mix with pulldowns)
- Safari 3.1.1 Known Issues
month and year titles do not set proper block element widths and appear to overlap until navigating to another month. This caveat is present in Safari Win/Mac 3.1.1.fixed as of 0.3.6.
- Google Chrome Win 2.149.27
requirements
- jQuery
- animate clip style property jQuery plugin - this is only needed for jCal “heavy” (not the light version) - this is included in the .zip release
- jQueryColor jQuery Color Animation plugin by Resig himself - still do not know why it’s not in core effects
core assets
changelog
- 0.3.6
- fixed month/year titles in safari and chrome not spacing correctly - this is because the $(window).load() is more reliable in these browsers than the $(document).ready() especially with applied CSS values.
- fixed even propagation and behavior in safari/chrome for the month/year pulldowns
- 0.3.5
- fixed issue with wrong 2nd month displaying if current day of first month was greater than the number of days in the 2nd month. i.e. current date of 7/31/2008 and only 30 days in Sept then Oct would automatically be shown as the next month incorrectly.
- 0.3.4
- 0.3.3
- added pretty combobox-ish pulldowns for months and years for easier navigation of date
- variable scope bug fixes - no more lost or conflicting settings with multiple jCals
- 0.3.2
- speed improvements when scrolling through months and rollover day highlighting - 10% faster
- fixed retention of selected days when scrolling through months when selected days span multiple months
- 0.3.1
- fixed bug when scrolling over months that were completely blacked out via the dCheck functionality.
- fixed the dCheck day to be “zeroed out” i.e. the BOD (Beginning Of Day - 00:00:000GMT)to make the dCheck easier to work with.
- 0.3.0
- Fixed bug with jQuery 1.2.6 without the need of a patched version of the jQuery core. As it turns out the jQuery.fn.clone(true) was misused in my code when a simple jQuery.fn.appendTo() could be used to move elements from one container to another faster. This should not only now with with jQuery 1.2.6 but be significantly faster than previous versions when scrolling through months
- Added ability to access each instantiated calendar’s selected day and selected number of days via the
$('#container').data('day') and $('#container').data('days') cached variables from any other javascript scope
- The selected day(s) are now re-selected when scrolling through months and then scrolling back to the month(s) the day(s) are selected in
- 0.2.3
- 0.2.2
- ability to force selection to start at beginning of week as per dayOffset setting
- 0.2.1
- cutoff sequential days incorrect on last month
- wrong month output in click callback
- added comments to extensible options
- now can configure how month starts, i.e. starts sunday, starts monday + more callback style changes
- added comments to help configure styles
- 0.2 - Added scrolling “carousel” to month navigation
- 0.1.1 - Fixed scope when scrolling through months on multiple calendars
- 0.1 - Initial release
jCal core jQuery plugin code (<14KB of undocumented glory, not jsmin’d, no comments) “heavy” version will support for month and year pulldown selection
/*
* jCal calendar multi-day and multi-month datepicker plugin for jQuery
* version 0.3.6
* Author: Jim Palmer
* Released under MIT license.
*/
(function($) {
$.fn.jCal = function (opt) {
$.jCal(this, opt);
};
$.jCal = function (target, opt) {
opt = $.extend({
day: new Date(), // date to drive first cal
days: 1, // default number of days user can select
showMonths: 1, // how many side-by-side months to show
monthSelect: false, // show selectable month and year ranges via animated comboboxen
dCheck: function (day) { return true; }, // handler for checking if single date is valid or not
callback: function (day, days) { return true; }, // callback function for click on date
selectedBG: 'rgb(0, 143, 214)', // default bgcolor for selected date cell
defaultBG: 'rgb(255, 255, 255)', // default bgcolor for unselected date cell
dayOffset: 0, // 0=week start with sunday, 1=week starts with monday
forceWeek: false, // true=force selection at start of week, false=select days out from selected day
dow: ['S', 'M', 'T', 'W', 'T', 'F', 'S'], // days of week - change this to reflect your dayOffset
ml: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
ms: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
_target: target // target DOM element - no need to set extend this variable
}, opt);
opt.day = new Date(opt.day.getFullYear(), opt.day.getMonth(), 1);
if ( !$(opt._target).data('days') ) $(opt._target).data('days', opt.days);
$(target).stop().empty();
for (var sm=0; sm < opt.showMonths; sm++)
$(target).append('<div class="jCalMo"></div>');
opt.cID = 'c' + $('.jCalMo').length;
$('.jCalMo', target).each(
function (ind) {
drawCalControl($(this), $.extend( {}, opt, { 'ind':ind,
'day':new Date( new Date( opt.day.getTime() ).setMonth( new Date( opt.day.getTime() ).getMonth() + ind ) ) }
));
drawCal($(this), $.extend( {}, opt, { 'ind':ind,
'day':new Date( new Date( opt.day.getTime() ).setMonth( new Date( opt.day.getTime() ).getMonth() + ind ) ) }
));
});
if ( $(opt._target).data('day') && $(opt._target).data('days') )
reSelectDates(target, $(opt._target).data('day'), $(opt._target).data('days'), opt);
};
function drawCalControl (target, opt) {
$(target).append(
'<div class="jCal">' +
( (opt.ind == 0) ? '<div class="left" />' : '' ) +
'<div class="month">' +
'<span class="monthYear">' + opt.day.getFullYear() + '</span>' +
'<span class="monthName">' + opt.ml[opt.day.getMonth()] + '</span>' +
'</div>' +
( (opt.ind == ( opt.showMonths - 1 )) ? '<div class="right" />' : '' ) +
'</div>');
if ( opt.monthSelect )
$(target).find('.jCal .monthName, .jCal .monthYear')
.bind('mouseover', $.extend( {}, opt ),
function (e) {
$(this).removeClass('monthYearHover').removeClass('monthNameHover');
if ( $('.jCalMask', e.data._target).length == 0 ) $(this).addClass( $(this).attr('class') + 'Hover' );
})
.bind('mouseout', function () { $(this).removeClass('monthYearHover').removeClass('monthNameHover'); })
.bind('click', $.extend( {}, opt ),
function (e) {
$('.jCalMo .monthSelector, .jCalMo .monthSelectorShadow').remove();
var monthName = $(this).hasClass('monthName'),
pad = Math.max( parseInt($(this).css('padding-left')), parseInt($(this).css('padding-left'))) || 2,
calcTop = ( ($(this).offset()).top - ( ( monthName ? e.data.day.getMonth() : 2 ) * ( $(this).height() + 0 ) ) );
calcTop = calcTop > 0 ? calcTop : 0;
var topDiff = ($(this).offset()).top - calcTop;
$('<div class="monthSelectorShadow" style="' +
'top:' + $(e.data._target).offset().top + 'px; ' +
'left:' + $(e.data._target).offset().left + 'px; ' +
'width:' + ( $(e.data._target).width() + ( parseInt($(e.data._target).css('paddingLeft')) || 0 ) + ( parseInt($(e.data._target).css('paddingRight')) || 0 ) ) + 'px; ' +
'height:' + ( $(e.data._target).height() + ( parseInt($(e.data._target).css('paddingTop')) || 0 ) + ( parseInt($(e.data._target).css('paddingBottom')) || 0 ) ) + 'px;">' +
'</div>')
.css('opacity',0.01).appendTo( $(this).parent() );
$('<div class="monthSelector" style="' +
'top:' + calcTop + 'px; ' +
'left:' + ( ($(this).offset()).left ) + 'px; ' +
'width:' + ( $(this).width() + ( pad * 2 ) ) + 'px;">' +
'</div>')
.css('opacity',0).appendTo( $(this).parent() );
for (var di = ( monthName ? 0 : -2 ), dd = ( monthName ? 12 : 3 ); di < dd; di++)
$(this).clone().removeClass('monthYearHover').removeClass('monthNameHover').addClass('monthSelect')
.attr( 'id', monthName ? (di + 1) + '_1_' + e.data.day.getFullYear() : (e.data.day.getMonth() + 1) + '_1_' + (e.data.day.getFullYear() + di) )
.html( monthName ? e.data.ml[di] : ( e.data.day.getFullYear() + di ) )
.css( 'top', ( $(this).height() * di ) ).appendTo( $(this).parent().find('.monthSelector') );
var moSel = $(this).parent().find('.monthSelector').get(0), diffOff = $(moSel).height() - ( $(moSel).height() - topDiff );
$(moSel)
.css('clip','rect(' + diffOff + 'px ' + ( $(this).width() + ( pad * 2 ) ) + 'px '+ diffOff + 'px 0px)')
.animate({'opacity':.92,'clip':'rect(0px ' + ( $(this).width() + ( pad * 2 ) ) + 'px ' + $(moSel).height() + 'px 0px)'}, 'fast', function () {
$(this).parent().find('.monthSelectorShadow').bind('mouseover click', function () { $(this).parent().find('.monthSelector').remove(); $(this).remove(); });
})
.parent().find('.monthSelectorShadow').animate({'opacity':.1}, 'fast');
$('.jCalMo .monthSelect', e.data._target).bind('mouseover mouseout click', $.extend( {}, e.data ),
function (e) {
if ( e.type == 'click' )
$(e.data._target).jCal( $.extend(e.data, {day:new Date($(this).attr('id').replace(/_/g, '/'))}) );
else
$(this).toggleClass('monthSelectHover');
});
});
$(target).find('.jCal .left').bind('click', $.extend( {}, opt ),
function (e) {
if ($('.jCalMask', e.data._target).length > 0) return false;
var mD = { w:0, h:0 };
$('.jCalMo', e.data._target).each( function () {
mD.w += $(this).width() + parseInt($(this).css('padding-left')) + parseInt($(this).css('padding-right'));
var cH = $(this).height() + parseInt($(this).css('padding-top')) + parseInt($(this).css('padding-bottom'));
mD.h = ((cH > mD.h) ? cH : mD.h);
} );
$(e.data._target).prepend('<div class="jCalMo"></div>');
e.data.day = new Date( $('div[id*=' + e.data.cID + 'd_]:first', e.data._target).attr('id').replace(e.data.cID + 'd_', '').replace(/_/g, '/') );
e.data.day.setDate(1);
e.data.day.setMonth( e.data.day.getMonth() - 1 );
drawCalControl($('.jCalMo:first', e.data._target), e.data);
drawCal($('.jCalMo:first', e.data._target), e.data);
if (e.data.showMonths > 1) {
$('.right', e.data._target).clone(true).appendTo( $('.jCalMo:eq(1) .jCal', e.data._target) );
$('.left:last, .right:last', e.data._target).remove();
}
$(e.data._target).append('<div class="jCalSpace" style="width:'+mD.w+'px; height:'+mD.h+'px;"></div>');
$('.jCalMo', e.data._target).wrapAll(
'<div class="jCalMask" style="clip:rect(0px '+mD.w+'px '+mD.h+'px 0px); width:'+ ( mD.w + ( mD.w / e.data.showMonths ) ) +'px; height:'+mD.h+'px;">' +
'<div class="jCalMove"></div>' +
'</div>');
$('.jCalMove', e.data._target).css('margin-left', ( ( mD.w / e.data.showMonths ) * -1 ) + 'px').css('opacity', 0.5).animate({ marginLeft:'0px' }, 'fast',
function () {
$(this).children('.jCalMo:not(:last)').appendTo( $(e.data._target) );
$('.jCalSpace, .jCalMask', e.data._target).empty().remove();
if ( $(e.data._target).data('day') )
reSelectDates(e.data._target, $(e.data._target).data('day'), $(e.data._target).data('days'), e.data);
});
});
$(target).find('.jCal .right').bind('click', $.extend( {}, opt ),
function (e) {
if ($('.jCalMask', e.data._target).length > 0) return false;
var mD = { w:0, h:0 };
$('.jCalMo', e.data._target).each( function () {
mD.w += $(this).width() + parseInt($(this).css('padding-left')) + parseInt($(this).css('padding-right'));
var cH = $(this).height() + parseInt($(this).css('padding-top')) + parseInt($(this).css('padding-bottom'));
mD.h = ((cH > mD.h) ? cH : mD.h);
} );
$(e.data._target).append('<div class="jCalMo"></div>');
e.data.day = new Date( $('div[id^=' + e.data.cID + 'd_]:last', e.data._target).attr('id').replace(e.data.cID + 'd_', '').replace(/_/g, '/') );
e.data.day.setDate(1);
e.data.day.setMonth( e.data.day.getMonth() + 1 );
drawCalControl($('.jCalMo:last', e.data._target), e.data);
drawCal($('.jCalMo:last', e.data._target), e.data);
if (e.data.showMonths > 1) {
$('.left', e.data._target).clone(true).prependTo( $('.jCalMo:eq(1) .jCal', e.data._target) );
$('.left:first, .right:first', e.data._target).remove();
}
$(e.data._target).append('<div class="jCalSpace" style="width:'+mD.w+'px; height:'+mD.h+'px;"></div>');
$('.jCalMo', e.data._target).wrapAll(
'<div class="jCalMask" style="clip:rect(0px '+mD.w+'px '+mD.h+'px 0px); width:'+ ( mD.w + ( mD.w / e.data.showMonths ) ) +'px; height:'+mD.h+'px;">' +
'<div class="jCalMove"></div>' +
'</div>');
$('.jCalMove', e.data._target).css('opacity', 0.5).animate({ marginLeft:( ( mD.w / e.data.showMonths ) * -1 ) + 'px' }, 'fast',
function () {
$(this).children('.jCalMo:not(:first)').appendTo( $(e.data._target) );
$('.jCalSpace, .jCalMask', e.data._target).empty().remove();
if ( $(e.data._target).data('day') )
reSelectDates(e.data._target, $(e.data._target).data('day'), $(e.data._target).data('days'), e.data);
$(this).children('.jCalMo:not(:first)').removeClass('');
});
});
$('.jCal', target).each(
function () {
var width = $(this).parent().width() - ( $('.left', this).width() || 0 ) - ( $('.right', this).width() || 0 );
$('.month', this).css('width', width).find('.monthName, .monthYear').css('width', ((width / 2) - 4 ));
});
$(window).load(
function () {
$('.jCal', target).each(
function () {
var width = $(this).parent().width() - ( $('.left', this).width() || 0 ) - ( $('.right', this).width() || 0 );
$('.month', this).css('width', width).find('.monthName, .monthYear').css('width', ((width / 2) - 4 ));
});
});
};
function reSelectDates (target, day, days, opt) {
var fDay = new Date(day.getTime());
var sDay = new Date(day.getTime());
for (var fC = false, di = 0, dC = days; di < dC; di++) {
var dF = $(target).find('div[id*=d_' + (sDay.getMonth() + 1) + '_' + sDay.getDate() + '_' + sDay.getFullYear() + ']');
if ( dF.length > 0 ) {
dF.stop().addClass('selectedDay');
fC = true;
}
sDay.setDate( sDay.getDate() + 1 );
}
if ( fC && typeof opt.callback == 'function' ) opt.callback( day, days );
};
function drawCal (target, opt) {
for (var ds=0, length=opt.dow.length; ds < length; ds++)
$(target).append('<div class="dow">' + opt.dow[ds] + '</div>');
var fd = new Date( new Date( opt.day.getTime() ).setDate(1) );
var ldlm = new Date( new Date( fd.getTime() ).setDate(0) );
var ld = new Date( new Date( new Date( fd.getTime() ).setMonth( fd.getMonth() + 1 ) ).setDate(0) );
var copt = {fd:fd.getDay(), lld:ldlm.getDate(), ld:ld.getDate()};
var offsetDayStart = ( ( copt.fd < opt.dayOffset ) ? ( opt.dayOffset - 7 ) : 1 );
var offsetDayEnd = ( ( ld.getDay() < opt.dayOffset ) ? ( 7 - ld.getDay() ) : ld.getDay() );
for ( var d = offsetDayStart, dE = ( copt.fd + copt.ld + ( 7 - offsetDayEnd ) ); d < dE; d++)
$(target).append(
(( d <= ( copt.fd - opt.dayOffset ) ) ?
'<div id="' + opt.cID + 'd' + d + '" class="pday">' + ( copt.lld - ( ( copt.fd - opt.dayOffset ) - d ) ) + '</div>'
: ( ( d > ( ( copt.fd - opt.dayOffset ) + copt.ld ) ) ?
'<div id="' + opt.cID + 'd' + d + '" class="aday">' + ( d - ( ( copt.fd - opt.dayOffset ) + copt.ld ) ) + '</div>'
: '<div id="' + opt.cID + 'd_' + (fd.getMonth() + 1) + '_' + ( d - ( copt.fd - opt.dayOffset ) ) + '_' + fd.getFullYear() + '" class="' +
( ( opt.dCheck( new Date( (new Date( fd.getTime() )).setDate( d - ( copt.fd - opt.dayOffset ) ) ) ) ) ? 'day' : 'invday' ) +
'">' + ( d - ( copt.fd - opt.dayOffset ) ) + '</div>'
)
)
);
$(target).find('div[id^=' + opt.cID + 'd]:first, div[id^=' + opt.cID + 'd]:nth-child(7n+2)').before( '<br style="clear:both; font-size:0.1em;" />' );
$(target).find('div[id^=' + opt.cID + 'd_]:not(.invday)').bind("mouseover mouseout click", $.extend( {}, opt ),
function(e){
if ($('.jCalMask', e.data._target).length > 0) return false;
var osDate = new Date ( $(this).attr('id').replace(/c[0-9]{1,}d_([0-9]{1,2})_([0-9]{1,2})_([0-9]{4})/, '$1/$2/$3') );
if (e.data.forceWeek) osDate.setDate( osDate.