Here is my effort - Covers all Edge Cases
It is a robust well-tested date picker by now and answers the following questions:
- Let days make sense with each month (Dont want 31 of feb selectable , ..)
- Set next thay from START to the END selector
...
But some how the Date() object tell the right date, but both selectors
show each set of Days for the previous one (For example, top 28 days
happen in March instead of FEB)
If you look below I take care of all the above and more using small modular functions:
- maxDay() - given a month and year, it returns the max numbers of days in that month for that year
- createAry() - given min, max and optional array of values returns an
array from min to max or portion of values array between min and max
indices
- days() - given min day and month and year uses maxDay() to get max day. Uses createAry() to return an array ready to use to populate day <select> options
- months() - given min month and an array of month text values (like 'jan', 'feb', etc.) uses createAry() to return an array ready to use to populate month <select> options
- years() - given min year uses MAXYEAR constant and createAry() to return an array ready to use to populate year <select> options
- updateSelectOptions() - given a jQuery DOM <select> element, an array of option texts from day(), month() or year() and selected option index, empties then fills <select>; with array values and selects appropriate option.
- updateTos() - updates the End Date rolling past Start Date if necessary and doing min month, day and year calculations based on Start Date values. Calls updateSelectOptions() for month, day and year giving jQuery <select> DOM element and arrays returned by month(), day() and year() to update <select> options with appropriate text values and selecting correct current option.
- update() - updates either start or end date internally with new Date() using month, day and year from selected <select> options. For Start Date, calls updateSelectOptions() using days() and maxDay() to limit days according to month and year and select current option. Calls updateTos() to update End Date and its <select> options.
jsFiddle:
FIDDLE
HTML:
<form>
<fieldset class="dateField">
<legend>Start Date</legend>
<label for="fromMonth">Month</label>
<select id="fromMonth" class='monthSelect'></select>
<label for="fromDay">Day</label>
<select id="fromDay" class='daySelect'></select>
<label for="fromYear">Year</label>
<select id="fromYear" class='yearSelect'></select>
</fieldset>
<fieldset class="dateField">
<legend>End Date</legend>
<label for="toMonth">Month</label>
<select id="toMonth" class='monthSelect'></select>
<label for="toDay">Day</label>
<select id="toDay" class='daySelect'></select>
<label for="toYear">Year</label>
<select id="toYear" class='yearSelect'></select>
</fieldset>
</form>
<input type="button" id="reset" class="resetButton" value="Reset" />
JavaScript:
'use strict';
var fromDay = $('#fromDay'),
fromMonth = $('#fromMonth'),
fromYear = $('#fromYear'),
toDay = $('#toDay'),
toMonth = $('#toMonth'),
toYear = $('#toYear'),
reset = $('#reset'),
CURDATE = new Date(),
curFromDate,
curToDate,
MINYEAR = 2000,
NUMYEARS = 40,
MAXYEAR = MINYEAR + NUMYEARS - 1,
MAXDATE = new Date(MAXYEAR, 11, 31),
MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
function maxDay(params) { // {month, year}
params.month = params.month || 0;
params.year = params.year || MINYEAR;
return new Date(params.year, (params.month - 0) + 1, 0).getDate();
}
function createAry(params) { // { min, max, values }
var i,
ary = [];
params.values = params.values || [];
if (params.values.length !== 0) {
return params.values.slice(params.min, params.max + 1);
}
for (i = 0; i < params.max - params.min + 1; i++) {
ary[i] = params.min + i;
}
return ary;
}
function days(params) { // {minDay, month, year}
var max;
params.month = params.month || 0;
params.year = params.year || MINYEAR;
max = maxDay({
month: params.month,
year: params.year
});
params.minDay = params.minDay || 1;
return createAry({
min: params.minDay,
max: max
});
}
function months(params) { // {minMonth, months}
params.minMonth = params.minMonth || 0;
return createAry({
min: params.minMonth,
max: 11,
values: MONTHS
})
}
function years(params) { // {minYear}
params.minYear = params.minYear || MINYEAR;
return createAry({
min: params.minYear,
max: MAXYEAR
});
}
function updateSelectOptions(params) { // {select, options, current]
params.select.empty();
params.options.forEach(function (e, i) {
params.select.append($('<option></option>').prop("selected", i === params.current).text(e));
});
}
function updateTos() {
var minDay = 1,
minMonth = 0,
minYear = curFromDate.getFullYear();
if (curToDate <= curFromDate) {
curToDate = new Date(curFromDate.getFullYear(), curFromDate.getMonth(), curFromDate.getDate() + 1);
minYear = curToDate.getFullYear();
if (minYear === curFromDate.getFullYear()) {
minMonth = curToDate.getMonth();
}
if (curFromDate.getMonth() === curToDate.getMonth()) {
minDay = curToDate.getDate();
}
} else if (curFromDate.getFullYear() === curToDate.getFullYear()) {
minMonth = curFromDate.getMonth();
if (curFromDate.getMonth() === curToDate.getMonth()) {
minDay = curFromDate.getDate() + 1;
}
} else if (curFromDate.getDate() === 31 && curFromDate.getMonth() === 11) {
minYear++;
}
updateSelectOptions({
select: toDay,
options: days({
minDay: minDay,
month: curToDate.getMonth(),
year: curToDate.getFullYear()
}),
current: curToDate.getDate() - minDay
});
updateSelectOptions({
select: toMonth,
options: months({
minMonth: minMonth,
months: MONTHS
}),
current: curToDate.getMonth() - minMonth
});
updateSelectOptions({
select: toYear,
options: years({
minYear: minYear
}),
current: curToDate.getFullYear() - minYear
});
}
function update(params) { // {toOrFrom}
var day,
month,
year,
max,
date;
if (params.toOrFrom === 'from') {
day = fromDay.find("option:selected").text();
month = MONTHS.indexOf(fromMonth.find("option:selected").text());
year = fromYear.find("option:selected").text();
} else {
day = toDay.find("option:selected").text();
month = MONTHS.indexOf(toMonth.find("option:selected").text());
year = toYear.find("option:selected").text();
}
max = maxDay({
month: month,
year: year
});
if (day > max) {
day = max;
}
date = new Date(year, month, day);
if (params.toOrFrom === 'from') {
if (date >= MAXDATE) {
alert('The date you entered is later than the latest allowed date. Please enter a different date.');
fromDay.prop("selectedIndex", curFromDate.getDate() - 1);
fromMonth.prop("selectedIndex", curFromDate.getMonth());
fromYear.prop("selectedIndex", curFromDate.getFullYear() - MINYEAR);
return;
}
curFromDate = date;
updateSelectOptions({
select: fromDay,
options: days({
minDay: 1,
month: month,
year: year
}),
current: day - 1
});
} else {
curToDate = date;
}
updateTos();
}
function onFromChange() {
update({
toOrFrom: 'from'
});
}
function onToChange() {
update({
toOrFrom: 'to'
});
}
function init() {
curFromDate = new Date(CURDATE.getFullYear(), CURDATE.getMonth(), CURDATE.getDate());
curToDate = new Date(CURDATE.getFullYear(), CURDATE.getMonth(), CURDATE.getDate());
updateSelectOptions({
select: fromDay,
options: days({
minDay: 1,
month: curFromDate.getMonth(),
year: curFromDate.getFullYear()
}),
current: curFromDate.getDate() - 1
});
updateSelectOptions({
select: fromMonth,
options: months({
months: MONTHS
}),
current: curFromDate.getMonth()
});
updateSelectOptions({
select: fromYear,
options: years({
minYear: MINYEAR
}),
current: curFromDate.getFullYear() - MINYEAR
});
updateTos();
}
fromDay.change(onFromChange);
fromMonth.change(onFromChange);
fromYear.change(onFromChange);
toDay.change(onToChange);
toMonth.change(onToChange);
toYear.change(onToChange);
reset.click(init);
init();
CSS:
fieldset.dateField {
width: 375px;
}
select.daySelect {
width: 45px;
margin: 5px 10px 0 0;
}
select.monthSelect {
width: 55px;
margin: 5px 10px 0 10px;
}
select.yearSelect {
width: 65px;
margin: 5px 10px 0 0;
}
input.resetButton {
margin: 10px 20px 0 0
}