Skip to content
32 changes: 32 additions & 0 deletions docs/options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ Boolean. Default: false

Whether or not to close the datepicker immediately when a date is selected.

monthNameToNumber
-----------------

Boolean. Default: false

Takes a month name and returns the corresponding month number if set to True. The conversion is used when the date format is 'mm' for the month part.

The function works by filtering the short month names in the current language to find a match with the input month name. If a match is found, current month will be shown as a number.

Example:

If the current language is English and the input month name is "Jun", the function will return 6, since June is the 6th month of the year.

assumeNearbyYear
----------------
Expand Down Expand Up @@ -112,6 +124,25 @@ A function that takes a date as a parameter and returns one of the following val
* ``tooltip``: a tooltip to apply to this year, via the ``title`` HTML attribute


afterInputChange
----------------

Function(inputChange, dateOld, dateNew). Default: $.noop

A function that is called after the input value of the datepicker has changed. It takes three parameters:

* inputChange: A string that indicates the part of the date that was changed. For example, 'yyyy', 'mm', 'dd'.
* dateOld: The old date value before the change.
* dateNew: The new date value after the change.

This function can be used to perform custom operations after the datepicker's input has been changed. For example, you can use it to call a custom function:

```javascript
afterInputChange: function(inputChange, dateOld, dateNew){
customFunc(inputChange, dateOld, dateNew);
}
```

calendarWeeks
-------------

Expand Down Expand Up @@ -567,4 +598,5 @@ todayHighlight false
toggleActive false
weekStart 0 (Sunday)
zIndexOffset 10
afterInputChange
===================== =============
86 changes: 76 additions & 10 deletions js/bootstrap-datepicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@
if (o.startDate instanceof Date)
o.startDate = this._local_to_utc(this._zero_time(o.startDate));
else
o.startDate = DPGlobal.parseDate(o.startDate, format, o.language, o.assumeNearbyYear);
o.startDate = DPGlobal.parseDate(o.startDate, format, o.language, o.assumeNearbyYear, o.monthNameToNumber, o.afterInputChange);
}
else {
o.startDate = -Infinity;
Expand All @@ -271,7 +271,7 @@
if (o.endDate instanceof Date)
o.endDate = this._local_to_utc(this._zero_time(o.endDate));
else
o.endDate = DPGlobal.parseDate(o.endDate, format, o.language, o.assumeNearbyYear);
o.endDate = DPGlobal.parseDate(o.endDate, format, o.language, o.assumeNearbyYear, o.monthNameToNumber, o.afterInputChange);
}
else {
o.endDate = Infinity;
Expand All @@ -286,7 +286,7 @@
o.datesDisabled = o.datesDisabled.split(',');
}
o.datesDisabled = $.map(o.datesDisabled, function(d){
return DPGlobal.parseDate(d, format, o.language, o.assumeNearbyYear);
return DPGlobal.parseDate(d, format, o.language, o.assumeNearbyYear, o.monthNameToNumber, o.afterInputChange);
});

var plc = String(o.orientation).toLowerCase().split(/\s+/g),
Expand Down Expand Up @@ -321,7 +321,7 @@
o.orientation.y = _plc[0] || 'auto';
}
if (o.defaultViewDate instanceof Date || typeof o.defaultViewDate === 'string') {
o.defaultViewDate = DPGlobal.parseDate(o.defaultViewDate, format, o.language, o.assumeNearbyYear);
o.defaultViewDate = DPGlobal.parseDate(o.defaultViewDate, format, o.language, o.assumeNearbyYear, o.monthNameToNumber, o.afterInputChange);
} else if (o.defaultViewDate) {
var year = o.defaultViewDate.year || new Date().getFullYear();
var month = o.defaultViewDate.month || 0;
Expand Down Expand Up @@ -788,9 +788,12 @@
}

dates = $.map(dates, $.proxy(function(date){
return DPGlobal.parseDate(date, this.o.format, this.o.language, this.o.assumeNearbyYear);
return DPGlobal.parseDate(date, this.o.format, this.o.language, this.o.assumeNearbyYear, this.o.monthNameToNumber, this.o.afterInputChange);
}, this));
dates = $.grep(dates, $.proxy(function(date){
if (this.o.afterInputChange !== $.noop){
this.dateWithinRangeLog(date);
}
return (
!this.dateWithinRange(date) ||
!date
Expand Down Expand Up @@ -1409,6 +1412,17 @@
return date >= this.o.startDate && date <= this.o.endDate;
},

dateWithinRangeLog: function(date){
let format = DPGlobal.parseFormat(this.o.format);
let language = this.o.language;
if (date < this.o.startDate) {
this.o.afterInputChange('outOfRange_tooEarly', DPGlobal.formatDate(date, format, language), DPGlobal.formatDate(this.o.startDate, format, language));
}
if (date > this.o.endDate) {
this.o.afterInputChange('outOfRange_tooLate', DPGlobal.formatDate(date, format, language), DPGlobal.formatDate(this.o.endDate, format, language));
}
},

keydown: function(e){
if (!this.picker.is(':visible')){
if (e.keyCode === 40 || e.keyCode === 27) { // allow down to re-show picker
Expand Down Expand Up @@ -1730,12 +1744,14 @@
zIndexOffset: 10,
container: 'body',
immediateUpdates: false,
afterInputChange: $.noop,
title: '',
templates: {
leftArrow: '&#x00AB;',
rightArrow: '&#x00BB;'
},
showWeekDays: true
showWeekDays: true,
monthNameToNumber: false
};
var locale_opts = $.fn.datepicker.locale_opts = [
'format',
Expand Down Expand Up @@ -1802,7 +1818,7 @@
}
return {separators: separators, parts: parts};
},
parseDate: function(date, format, language, assumeNearby){
parseDate: function(date, format, language, assumeNearby, monthNameToNumber, afterInputChange){
if (!date)
return undefined;
if (date instanceof Date)
Expand Down Expand Up @@ -1860,21 +1876,39 @@
setters_order = ['yyyy', 'yy', 'M', 'MM', 'm', 'mm', 'd', 'dd'],
setters_map = {
yyyy: function(d,v){
return d.setUTCFullYear(assumeNearby ? applyNearbyYear(v, assumeNearby) : v);
var updatedYear = d.setUTCFullYear(assumeNearby ? applyNearbyYear(v, assumeNearby) : v);
var updatedYearValue = new Date(updatedYear).getFullYear();
if (afterInputChange !== $.noop && v !== updatedYearValue){
afterInputChange('yyyy', v.toString(), new Date(updatedYear).getFullYear());
}
return updatedYear;
},
m: function(d,v){
if (isNaN(d))
if (isNaN(d)) {
if (afterInputChange !== $.noop){
afterInputChange('m', v.toString(), new Date(d).getDay());
}
return d;
}
v -= 1;
while (v < 0) v += 12;
v %= 12;
d.setUTCMonth(v);
while (d.getUTCMonth() !== v)
d.setUTCDate(d.getUTCDate()-1);
var updatedMonth = new Date(d).getMonth();
if (afterInputChange !== $.noop && v !== updatedMonth){
afterInputChange('m', (v+1).toString(), new Date(d).getMonth()+1);
}
return d;
},
d: function(d,v){
return d.setUTCDate(v);
var updatedDate = d.setUTCDate(v);
var updatedDay = new Date(updatedDate).getDate();
if (afterInputChange !== $.noop && v !== updatedDay){
afterInputChange('d', v.toString(), new Date(updatedDate).getDate());
}
return updatedDate;
}
},
val, filtered;
Expand All @@ -1895,12 +1929,36 @@
p = parts[i].slice(0, m.length);
return m.toLowerCase() === p.toLowerCase();
}

var datePartGetters = {
'yyyy': function(date) { return date.getFullYear(); },
'yy': function(date) { return date.getFullYear().toString().substr(-2); },
'MM': function(date) { return date.getMonth() + 1; },
'M': function(date) { return date.getMonth() + 1; },
'mm': function(date) { return date.getMonth() + 1; },
'm': function(date) { return date.getMonth() + 1; },
'dd': function(date) { return date.getDate(); },
'd': function(date) { return date.getDate(); }
};

if (parts.length === fparts.length){
var cnt;
for (i=0, cnt = fparts.length; i < cnt; i++){
val = parseInt(parts[i], 10);
if(isNaN(parts[i]) && !isNaN(val) && afterInputChange !== $.noop) {
afterInputChange(fparts[i], parts[i], val);
}
part = fparts[i];
if (isNaN(val)){
if (part === 'mm' && monthNameToNumber) {
filtered = $(dates[language].monthsShort).filter(match_part);
if (filtered[0] !== undefined) {
val = $.inArray(filtered[0], dates[language].monthsShort) + 1;
if(!isNaN(val) && afterInputChange !== $.noop){
afterInputChange('mm', parts[i].toString(), val.toString());
}
}
}
switch (part){
case 'MM':
filtered = $(dates[language].months).filter(match_part);
Expand All @@ -1911,6 +1969,10 @@
val = $.inArray(filtered[0], dates[language].monthsShort) + 1;
break;
}
if (afterInputChange !== $.noop && isNaN(val)){
var datePart = datePartGetters[part](date);
afterInputChange(part.toString(), parts[i].toString(), datePart.toString());
}
}
parsed[part] = val;
}
Expand All @@ -1924,6 +1986,10 @@
date = _date;
}
}
} else {
if(afterInputChange !== $.noop){
afterInputChange('noValidDateFound', parts.join(""), DPGlobal.formatDate(date, format, language));
}
}
return date;
},
Expand Down
92 changes: 92 additions & 0 deletions tests/suites/formats.js
Original file line number Diff line number Diff line change
Expand Up @@ -350,3 +350,95 @@ test('Assume nearby year - this century (+ 13 years, threshold = 30)', patch_dat
.datepicker('setValue');
equal(this.input.val(), '02/14/2023');
}));

test('afterInputChange is called with correct parameters when month is updated', patch_date(function(Date){
Date.now = function(){
return UTCDate(2024, 6, 6).getTime();
};
var updatedMonth = "2";
var updatedMonth = "02";

this.input
.val(updatedMonth + '/10/2023')
.datepicker({format: 'mm/dd/yyyy',
afterInputChange: function(inputChange, dateOld, dateNew) {
equal(inputChange, "mm", 'Month should be passed to afterInputChange');
equal(dateOld, updatedMonth, 'Old month should be correctly passed to afterInputChange');
equal(dateNew, updatedMonth, 'New month should be correctly passed to afterInputChange');
}
})
.datepicker('setValue');
equal(this.input.val(), '02/10/2023');
}));

test('afterInputChange is called with correct parameters when day is updated', patch_date(function(Date){
Date.now = function(){
return UTCDate(2024, 6, 6).getTime();
};
var originalDay = "Monday";
var updatedDay = "06";

this.input
.val('02/' + originalDay + '/2023')
.datepicker({format: 'mm/dd/yyyy',
afterInputChange: function(inputChange, dateOld, dateNew) {
equal(inputChange, "dd", 'Day should be passed to afterInputChange');
equal(dateOld, originalDay, 'Old day should be correctly passed to afterInputChange');
equal(dateNew, updatedDay, 'New day should be correctly passed to afterInputChange');
}
})
.datepicker('setValue');
equal(this.input.val(), '02/06/2023');
}));

test('afterInputChange is called with correct parameters when two digit year is updated', patch_date(function(Date){
Date.now = function(){
return UTCDate(2024, 6, 6).getTime();
};
var originalYear = "24";
var updatedYear = "2024";

this.input
.val('02/06/' + originalYear)
.datepicker({format: 'mm/dd/yyyy',
assumeNearbyYear: true,
afterInputChange: function(inputChange, dateOld, dateNew) {
equal(inputChange, "yyyy", 'Day should be passed to afterInputChange');
equal(dateOld, originalYear, 'Old day should be correctly passed to afterInputChange');
equal(dateNew, updatedYear, 'New day should be correctly passed to afterInputChange');
}
})
.datepicker('setValue');
equal(this.input.val(), '02/06/2024');
test('Convert month `February` to number `02`, monthNameToNumber === true', patch_date(function(Date){
Date.now = function(){
return UTCDate(2024, 6, 6).getTime();
};
this.input
.val('February/14/2023')
.datepicker({format: 'mm/dd/yyyy', monthNameToNumber: true})
.datepicker('setValue');
equal(this.input.val(), '02/14/2023');
}));

test('Convert month `Dec` to number `12`, monthNameToNumber === true', patch_date(function(Date){
Date.now = function(){
return UTCDate(2024, 6, 6).getTime();
};
this.input
.val('Dec/14/2023')
.datepicker({format: 'mm/dd/yyyy', monthNameToNumber: true})
.datepicker('setValue');
equal(this.input.val(), '12/14/2023');
}));

test('Non existing moth word could not be converted to number, defaults to current month monthNameToNumber === true', patch_date(function(Date){
Date.now = function(){
return UTCDate(2024, 6, 6).getTime();
};
this.input
.val('Nonsense/14/2023')
.datepicker({format: 'mm/dd/yyyy', monthNameToNumber: true})
.datepicker('setValue');
equal(this.input.val(), '07/14/2023');
}));