PDA

View Full Version : Multi-Month Calendar



aungii
12 Dec 2007, 1:23 AM
Description
-----------

Multi-month calendar
1. Configurable no. of month
2. Define the event dates which will show as selected


Code

/*
* Ext JS Library 2.0 RC 1
* Copyright(c) 2006-2007, Ext JS, LLC.
* licensing@extjs.com
*
* http://extjs.com/license
*/

/**
* @class Ext.MultiMonthCalendar
* @extends Ext.Component
* Multi-month Calendar
* @constructor
* @param {Object} config The config object
*/
Ext.ux.MultiMonthCalendar = Ext.extend(Ext.Component, {
/**
* @cfg {Date} minDate
* Minimum allowable date (JavaScript date object, defaults to null)
*/
minDate : null,
/**
* @cfg {Date} maxDate
* Maximum allowable date (JavaScript date object, defaults to null)
*/
maxDate : null,
/**
* @cfg {String} minText
* The error text to display if the minDate validation fails (defaults to "This date is before the minimum date")
*/
minText : "This date is before the minimum date",
/**
* @cfg {String} maxText
* The error text to display if the maxDate validation fails (defaults to "This date is after the maximum date")
*/
maxText : "This date is after the maximum date",
/**
* @cfg {String} format
* The default date format string which can be overriden for localization support. The format must be
* valid according to {@link Date#parseDate} (defaults to 'm/d/y').
*/
format : "m/d/y",
/**
* @cfg {Array} disabledDays
* An array of days to disable, 0-based. For example, [0, 6] disables Sunday and Saturday (defaults to null).
*/
disabledDays : null,
/**
* @cfg {String} disabledDaysText
* The tooltip to display when the date falls on a disabled day (defaults to "")
*/
disabledDaysText : "",
/**
* @cfg {RegExp} disabledDatesRE
* JavaScript regular expression used to disable a pattern of dates (defaults to null)
*/
disabledDatesRE : null,
/**
* @cfg {String} disabledDatesText
* The tooltip text to display when the date falls on a disabled date (defaults to "")
*/
disabledDatesText : "",
/**
* @cfg {Boolean} constrainToViewport
* True to constrain the date picker to the viewport (defaults to true)
*/
constrainToViewport : true,
/**
* @cfg {Array} monthNames
* An array of textual month names which can be overriden for localization support (defaults to Date.monthNames)
*/
monthNames : Date.monthNames,
/**
* @cfg {Array} dayNames
* An array of textual day names which can be overriden for localization support (defaults to Date.dayNames)
*/
dayNames : Date.dayNames,
/**
* @cfg {String} nextText
* The next month navigation button tooltip (defaults to 'Next Month (Control+Right)')
*/
nextText: 'Next Month (Control+Right)',
/**
* @cfg {String} prevText
* The previous month navigation button tooltip (defaults to 'Previous Month (Control+Left)')
*/
prevText: 'Previous Month (Control+Left)',
/**
* @cfg {Number} startDay
* Day index at which the week should begin, 0-based (defaults to 0, which is Sunday)
*/
startDay : 0,
/**
* @cfg {Number} noOfMonth
* No of Month to be displayed
*/
noOfMonth : 2,
/**
* @cfg {Array} eventDates
* List of Dates which have an event (show as selected in UI)
*/
eventDates : null,

initComponent : function(){
Ext.ux.MultiMonthCalendar.superclass.initComponent.call(this);
this.value = this.value ?
this.value.clearTime() : new Date().clearTime();
this.initDisabledDays();
},

// private
initDisabledDays : function(){
if(!this.disabledDatesRE && this.disabledDates){
var dd = this.disabledDates;
var re = "(?:";
for(var i = 0; i < dd.length; i++){
re += dd[i];
if(i != dd.length-1) re += "|";
}
this.disabledDatesRE = new RegExp(re + ")");
}
},

/**
* Sets the value of the date field
* @param {Date} value The date to set
*/

setValue : function(value){
var old = this.value;
this.value = value.clearTime(true);
if(this.el){
this.update(this.value);
}
},

/**
* Gets the current selected value of the date field
* @return {Date} The selected date
*/
getValue : function(){
return this.value;
},


// private
focus : function(){
if(this.el){
this.update(this.activeDate);
}
},

// private
onRender : function(container, position){
var m = ["<table cellspacing='0'><tr>"];
for(var x=0; x<this.noOfMonth; x++) {
m.push("<td><table cellspacing='0'><tr>");
if(x==0) {
m.push("<td class='x-date-left'><a href='#' title='", this.prevText ,"'> </a></td>");
} else {
m.push("<td class='x-date-left'>&nbsp;</td>");
}
m.push("<td class='x-date-middle' align='center'><span id='monthLabel" + x + "'></span></td>");
if(x == this.noOfMonth-1) {
m.push("<td class='x-date-right'><a href='#' title='", this.nextText ,"'> </a></td>");
} else {
m.push("<td class='x-date-right'>&nbsp;</td>");
}
m.push("</tr><tr><td colspan='3'><table class='x-date-inner' id='inner-date"+x+"' cellspacing='0'><thead><tr>");
var dn = this.dayNames;
for(var i = 0; i < 7; i++){
var d = this.startDay+i;
if(d > 6){
d = d-7;
}
m.push("<th><span>", dn[d].substr(0,1), "</span></th>");
}
m[m.length] = "</tr></thead><tbody><tr>";
for(var i = 0; i < 42; i++) {
if(i % 7 == 0 && i != 0){
m[m.length] = "</tr><tr>";
}
m[m.length] = '<td><a href="#" hidefocus="on" class="x-date-date" tabIndex="1"><em><span></span></em></a></td>';
}
m[m.length] = '</tr></tbody></table></td></tr></table></td>';
if(x != this.noOfMonth-1) {
m[m.length] = "<td width='3'></td>";
}
}
m[m.length] = "</tr></table>";
var el = document.createElement("div");
el.className = "x-date-picker";
el.innerHTML = m.join("");

container.dom.insertBefore(el, position);

this.el = Ext.get(el);
this.eventEl = Ext.get(el.firstChild);

new Ext.util.ClickRepeater(this.el.child("td.x-date-left a"), {
handler: this.showPrevMonth,
scope: this,
preventDefault:true,
stopDefault:true
});

new Ext.util.ClickRepeater(this.el.child("td.x-date-right a"), {
handler: this.showNextMonth,
scope: this,
preventDefault:true,
stopDefault:true
});

var kn = new Ext.KeyNav(this.eventEl, {
"left" : function(e){
e.ctrlKey ?
this.showPrevMonth() :
this.update(this.activeDate.add("d", -1));
},

"right" : function(e){
e.ctrlKey ?
this.showNextMonth() :
this.update(this.activeDate.add("d", 1));
},

"pageUp" : function(e){
this.showNextMonth();
},

"pageDown" : function(e){
this.showPrevMonth();
},

"enter" : function(e){
e.stopPropagation();
return true;
},
scope : this
});

this.cellsArray = new Array();
this.textNodesArray = new Array();
for(var x=0; x< this.noOfMonth; x++) {
var cells = Ext.get('inner-date'+x).select("tbody td");
var textNodes = Ext.get('inner-date'+x).query("tbody span");
this.cellsArray[x] = cells;
this.textNodesArray[x] = textNodes;
}

if(Ext.isIE){
this.el.repaint();
}
this.update(this.value);
},

// private
showPrevMonth : function(e){
this.update(this.activeDate.add("mo", -1));
},

// private
showNextMonth : function(e){
this.update(this.activeDate.add("mo", 1));
},

// private
update : function(date){
this.activeDate = date;
for(var x=0;x<this.noOfMonth;x++) {
var days = date.getDaysInMonth();
var firstOfMonth = date.getFirstDateOfMonth();
var startingPos = firstOfMonth.getDay()-this.startDay;

if(startingPos <= this.startDay){
startingPos += 7;
}

var pm = date.add("mo", -1);
var prevStart = pm.getDaysInMonth()-startingPos;

var cells = this.cellsArray[x].elements;
var textEls = this.textNodesArray[x];
days += startingPos;

// convert everything to numbers so it's fast
var day = 86400000;
var d = (new Date(pm.getFullYear(), pm.getMonth(), prevStart)).clearTime();
var min = this.minDate ? this.minDate.clearTime() : Number.NEGATIVE_INFINITY;
var max = this.maxDate ? this.maxDate.clearTime() : Number.POSITIVE_INFINITY;
var ddMatch = this.disabledDatesRE;
var ddText = this.disabledDatesText;
var ddays = this.disabledDays ? this.disabledDays.join("") : false;
var ddaysText = this.disabledDaysText;
var format = this.format;

var setCellClass = function(cal, cell){
cell.title = "";
var t = d.getTime();
cell.firstChild.dateValue = t;

// disabling
if(t < min) {
cell.className = " x-date-disabled";
cell.title = cal.minText;
return;
}
if(t > max) {
cell.className = " x-date-disabled";
cell.title = cal.maxText;
return;
}
if(ddays){
if(ddays.indexOf(d.getDay()) != -1){
cell.title = ddaysText;
cell.className = " x-date-disabled";
}
}
if(ddMatch && format){
var fvalue = d.dateFormat(format);
if(ddMatch.test(fvalue)){
cell.title = ddText.replace("%0", fvalue);
cell.className = " x-date-disabled";
}
}
//Only active days need to be selected
if(cal.eventDates && (cell.className.indexOf('x-date-active') != -1)) {
for(var y=0; y < cal.eventDates.length; y++) {
var evtDate = cal.eventDates[y].clearTime().getTime();
if(t == evtDate) {
cell.className += " x-date-selected";
break;
}
}
}
};

var i = 0;
for(; i < startingPos; i++) {
textEls[i].innerHTML = (++prevStart);
d.setDate(d.getDate()+1);
cells[i].className = "x-date-prevday";
setCellClass(this, cells[i]);
}
for(; i < days; i++){
intDay = i - startingPos + 1;
textEls[i].innerHTML = (intDay);
d.setDate(d.getDate()+1);
cells[i].className = "x-date-active";
setCellClass(this, cells[i]);
}
var extraDays = 0;
for(; i < 42; i++) {
textEls[i].innerHTML = (++extraDays);
d.setDate(d.getDate()+1);
cells[i].className = "x-date-nextday";
setCellClass(this, cells[i]);
}
var monthLabel = Ext.get('monthLabel' + x);
monthLabel.update(this.monthNames[date.getMonth()] + " " + date.getFullYear());

if(!this.internalRender){
var main = this.el.dom.firstChild;
var w = main.offsetWidth;
this.el.setWidth(w + this.el.getBorderWidth("lr"));
Ext.fly(main).setWidth(w);
this.internalRender = true;
// opera does not respect the auto grow header center column
// then, after it gets a width opera refuses to recalculate
// without a second pass
if(Ext.isOpera && !this.secondPass){
main.rows[0].cells[1].style.width = (w - (main.rows[0].cells[0].offsetWidth+main.rows[0].cells[2].offsetWidth)) + "px";
this.secondPass = true;
this.update.defer(10, this, [date]);
}
}
date = date.add('mo',1);
}
}
});
Ext.reg('mmcalendar', Ext.ux.MultiMonthCalendar);


CSS

.x-date-right, .x-date-left {
height:16px;
}

Usage

Ext.onReady(function() {

var evtDate = new Array();
for(var x=0; x < 10; x++) {
evtDate[x] = new Date(2007, 9, 1).add('d',x*5);
}
new Ext.FormPanel({
id: 'multi-cal-panel',
title: "Multi Calendars",
layout: 'table',
width: 600,
layoutConfig: {
columns: 3
},
defaults: {
style: {
margin: '10px'
}
},
items: [{
xtype: 'mmcalendar',
value: new Date(2007, 9, 1),
noOfMonth : 3,
eventDates : evtDate
}],
renderTo: Ext.getBody()
});
Ext.QuickTips.init();
});

brian.moeskau
12 Dec 2007, 11:25 AM
Nice! I assume that this should be in the 2.0 Extensions forum, correct? Also, you should add a page to the wiki if you get a chance: http://extjs.com/learn/Ext_Extensions

franckxx
12 Dec 2007, 2:49 PM
wouahh ! nice improvement !

thx, it's look great !

DigitalSkyline
12 Dec 2007, 5:00 PM
Cool. questions... do the calendars always show in a single row? Would it then be possible to show 12 months, say in a 3x4/4x3 grid? See where I'm going w/this interrogatory?

aungii
12 Dec 2007, 7:15 PM
Brian : Thanks for the moving to the correct forum and sorry for my mistake. It should be under 2.0 extension. I will add the extension page on wiki as soon as possible. :)

DigitalSkyline : You got a good point. Yes, This version show all the months in one row.
For 4x3 or 3x4 grid, we may need to consider for navigation. Right now, "prev month" button is on first month, and "next month" button is on
last month. We can move the navigation button out of the calendar with seperate row on the top. I will try to modify it in coming version.

Update : http://extjs.com/learn/Extension:Multi_Month_Calendar

cocorossello
13 Dec 2007, 3:34 AM
Hi,

Thx for the extension.

It would be interesting to configure a "layout" for the calendars, so you can do a 4x3 calendar, show 3 calendars in a column, etc....

It will be a limited TableLayout, where you can configure the number of rows and columns and 1 calendar will be in each cell of the table.

I will look how i can do that with your extension

wdvee
21 Dec 2007, 1:13 PM
Thanks for this extension, I was looking for something like this!

One question: how would you register handlers for the select or click event? I would like to register a "selection" handler that gets the selected date and adds it to an array of selected dates for multiple selection support.

Cheers

wdvee
21 Dec 2007, 2:28 PM
Thanks for this extension, I was looking for something like this!

One question: how would you register handlers for the select or click event? I would like to register a "selection" handler that gets the selected date and adds it to an array of selected dates for multiple selection support.

Cheers

Found it! I found the relevant code in DatePicker.js, and got the 'select' event to bubble up out of the MultiMonthCalendar. I'm working on the multiple selections now, and functions to get/set those.

aungii
26 Dec 2007, 1:56 AM
Can define no. of month to be displayed in a row.

Changes :
1. Adding one more attribute named "noOfMonthPerRow"
2. Modify initComponent() and onRender()
3. Adding more CSS.

Updated Code :

.....
/**
* @cfg {Array} eventDates
* List of Dates which have an event (show as selected in UI)
*/
eventDates : null,

/**
* @cfg {Array} noOfMonthPerRow
* No. Of Month to be displayed in a row
*/
noOfMonthPerRow : 3,

initComponent : function(){
Ext.ux.MultiMonthCalendar.superclass.initComponent.call(this);
this.value = this.value ?
this.value.clearTime() : new Date().clearTime();
this.initDisabledDays();
this.noOfMonthPerRow = this.noOfMonthPerRow > this.noOfMonth ?this.noOfMonth : this.noOfMonthPerRow
},
...

...
onRender : function(container, position){
var m = ["<table cellspacing='0'>"];
if(this.noOfMonthPerRow > 1) {
m.push("<tr><td class='x-date-left'><a href='#' title='", this.prevText ,"'> </a></td>");
m.push("<td class='x-date-left' colspan='"+ eval(this.noOfMonthPerRow *2 -3) +"'></td>");
m.push("<td class='x-date-right'><a href='#' title='", this.nextText ,"'> </a></td></tr><tr>");
} else {
//Special case of only one month
m.push("<tr><td><table cellspacing='0' width='100%'><tr><td class='x-date-left'><a href='#' title='", this.prevText ,"'> </a></td>");
m.push("<td class='x-date-right'><a href='#' title='", this.nextText ,"'> </a></td></tr></table></td></tr><tr>");
}
for(var x=0; x<this.noOfMonth; x++) {
m.push("<td><table border='1' cellspacing='0'><tr>");
m.push("<td class='x-date-middle' align='center'><span id='monthLabel" + x + "'></span></td>");
m.push("</tr><tr><td><table class='x-date-inner' id='inner-date"+x+"' cellspacing='0'><thead><tr>");
var dn = this.dayNames;
for(var i = 0; i < 7; i++){
var d = this.startDay+i;
if(d > 6){
d = d-7;
}
m.push("<th><span>", dn[d].substr(0,1), "</span></th>");
}
m[m.length] = "</tr></thead><tbody><tr>";
for(var i = 0; i < 42; i++) {
if(i % 7 == 0 && i != 0){
m[m.length] = "</tr><tr>";
}
m[m.length] = '<td><a href="#" hidefocus="on" class="x-date-date" tabIndex="1"><em><span></span></em></a></td>';
}
m[m.length] = '</tr></tbody></table></td></tr></table></td>';
if(x != this.noOfMonth-1) {
m[m.length] = "<td width='3'></td>";
}
if( (x+1) % this.noOfMonthPerRow == 0 && x!= 0) {
m[m.length] = "</tr><tr>";
}
}
m[m.length] = "</tr></table>";
var el = document.createElement("div");
el.className = "x-date-picker";
el.innerHTML = m.join("");
....


CSS :


.x-date-right, .x-date-left {
height:16px;
}
.x-date-right a {
float:right;
}


Usage :


...
items: [{
xtype: 'mmcalendar',
value: new Date(2007, 9, 1),
noOfMonth : 6,
noOfMonthPerRow : 3,
eventDates : evtDate
}],
...

DigitalSkyline
26 Dec 2007, 8:17 AM
Nice work, can't wait to give it a try. =D>

cocorossello
26 Dec 2007, 2:51 PM
Great job. Thx for sharing!

LukeChi
26 Dec 2007, 7:03 PM
This is great! thanks for sharing~

abudhahir
27 Dec 2007, 4:35 AM
Hi All Here
I am trying to put one month with all the navigational capabilities.
The problem here is I am till now not able to set the calendar to use all the available size( width and Height) of the contianing panel.
Can Anybody help?
Thanks

DigitalSkyline
27 Dec 2007, 12:22 PM
Pretty sure that is handled by css.

DigitalSkyline
27 Dec 2007, 12:22 PM
hiccup!

Juanito
9 Jan 2008, 9:11 AM
Hello, first of all, thanks a lot for this widget. I am currently using YUI's CalendarGroup and had really wanted to switch to Ext's date picker for its ease of switching year/month and to be able to get rid of YUI files in our app. I was planning on writing this myself but I am glad you beat me to it.

One thing that (I think) is still missing in comparison to YUI calendars is the ability to specify custom styles on specific dates. We use that in our time entry app to highlight pay periods and the current period. YUI has two options to do that, a render event (where you can modify the css of cells) and you can specify renderers for specific dates (which turns out to be too slow when you add too many renderers)

If you don't plan on doing anything like that, I will (after higher priority issues we are working on) and post it here.

Thanks again!

DeeZ
26 Mar 2008, 5:24 AM
Hi,

I needed the same layout with selectable days . I slightly changed the code to add this feature.

Just unzip the file in your "examples" directory to test it.

I tested it successfully wiht ExtJS 2.0.2 on FF 2, FF 3b4, IE 7, Safari 3.1.
A bug occurs on IE8b1 and I don't know why ...

DeeZ

luigif
27 Mar 2008, 3:27 AM
There is a minor bug appearing when noOfMonthPerRow=1 and noOfMonth>1 causing the calendar to be incorrectly displayed.
It happens for example when it is required a single column of many months.

To correct it, you need to modify this lines in the onRender routine from:


if(x != this.noOfMonth-1) {
m[m.length] = "<td width='3'></td>";
}
if( (x+1) % this.noOfMonthPerRow == 0 && x!= 0) {
m[m.length] = "</tr><tr>";
}

to:


if(x != this.noOfMonth-1 && this.noOfMonthPerRow > 1) {
m[m.length] = "<td width='3'></td>";
}
if( (x+1) % this.noOfMonthPerRow == 0) {
m[m.length] = "</tr><tr>";
}

allowing it to work also when x=0.

alexpgh
2 May 2008, 11:50 AM
Is it possible to define event handling for each selected date? For example, it would be nice to have a tooltip on mouseover describing what does each selected date represents....

wm003
2 May 2008, 12:23 PM
Is it possible to define event handling for each selected date? For example, it would be nice to have a tooltip on mouseover describing what does each selected date represents....

Try DatePickerPlus (http://extjs.com/forum/showthread.php?p=148587#post148587). it has support for custom css and tooltips for definable dates aswell as eventhandling for every dateselection:)

alexpgh
5 May 2008, 4:21 AM
Thanks a lot! This is exactly what I need :)

meza
16 Jun 2008, 4:54 AM
Hi there!

I've updated the stuff to fit my needs.
Maybe it comes handy to others.



initComponent : function(){
Ext.ux.MultiMonthCalendar.superclass.initComponent.call(this);
this.value = this.value ? this.value.clearTime() : new Date().clearTime();
this.initDisabledDays();
this.noOfMonthPerRow = this.noOfMonthPerRow > this.noOfMonth ?this.noOfMonth : this.noOfMonthPerRow
this.addEvents({
'change':true
,'add':true
,'remove':true
})




handleDateClick : function(e, t){
e.stopEvent();
var pn = t.parentNode;
if(t.dateValue && pn.className.indexOf("x-date-disabled")<0){
if ( (pn.className.indexOf("x-date-prevday")<0) && pn.className.indexOf("x-date-nextday")<0) {
if (pn.className.indexOf("x-date-selected")<0) {
pn.className += " x-date-selected";
var oldDates = this.eventDates;
this.addEventDate(t.dateValue);
var newDates = this.eventDates;
this.fireEvent('change',{
'method':'add',
'currentDate':t.dateValue,
'newDates':newDates,
'oldDates':oldDates
});
this.fireEvent('add',{
'currentDate':t.dateValue,
'newDates':newDates,
'oldDates':oldDates
});
} else {
pn.className = "x-date-active";
var oldDates = this.eventDates;
this.removeEventDate(t.dateValue);
var newDates = this.eventDates;
this.fireEvent('change',{
'method':'remove',
'currentDate':t.dateValue,
'newDates':newDates,
'oldDates':oldDates
});

this.fireEvent('remove',{
'currentDate':t.dateValue,
'newDates':newDates,
'oldDates':oldDates
});

}
}
}
},



usage:


items: [{
xtype: 'mmcalendar',
value: new Date(2008, 0, 1),
noOfMonth : 3,
noOfMonthPerRow : 3,
eventDates : evtDate,
listeners: {
'change':{
fn:function(params){
//..whateva'
}
}
}
,'add':{
fn:function(params){
//..whateva'
}
}
}
,'remove':{
fn:function(params){
//..whateva'
}
}
}

}]

jimmynsantoso
24 Apr 2012, 6:54 PM
how format for eventDates ? have a sample?

thanks but this is very good code