diff --git a/src/datepicker/datepicker.js b/src/datepicker/datepicker.js
index 0a227a6bde..549d523fcf 100644
--- a/src/datepicker/datepicker.js
+++ b/src/datepicker/datepicker.js
@@ -31,66 +31,172 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
// Modes chain
this.modes = ['day', 'month', 'year'];
- // Interpolated configuration attributes
- angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle'], function(key) {
- self[key] = angular.isDefined($attrs[key]) ? $interpolate($attrs[key])($scope.$parent) : datepickerConfig[key];
- });
+ if ($attrs.datepickerOptions) {
+ angular.forEach([
+ 'formatDay',
+ 'formatDayHeader',
+ 'formatDayTitle',
+ 'formatMonth',
+ 'formatMonthTitle',
+ 'formatYear',
+ 'initDate',
+ 'maxDate',
+ 'maxMode',
+ 'minDate',
+ 'minMode',
+ 'showWeeks',
+ 'shortcutPropagation',
+ 'startingDay',
+ 'yearColumns',
+ 'yearRows'
+ ], function(key) {
+ switch (key) {
+ case 'formatDay':
+ case 'formatDayHeader':
+ case 'formatDayTitle':
+ case 'formatMonth':
+ case 'formatMonthTitle':
+ case 'formatYear':
+ self[key] = angular.isDefined($scope.datepickerOptions[key]) ? $interpolate($scope.datepickerOptions[key])($scope.$parent) : datepickerConfig[key];
+ break;
+ case 'showWeeks':
+ case 'shortcutPropagation':
+ case 'yearColumns':
+ case 'yearRows':
+ self[key] = angular.isDefined($scope.datepickerOptions[key]) ?
+ $scope.datepickerOptions[key] : datepickerConfig[key];
+ break;
+ case 'startingDay':
+ if (angular.isDefined($scope.datepickerOptions.startingDay)) {
+ self.startingDay = $scope.datepickerOptions.startingDay;
+ } else if (angular.isNumber(datepickerConfig.startingDay)) {
+ self.startingDay = datepickerConfig.startingDay;
+ } else {
+ self.startingDay = ($locale.DATETIME_FORMATS.FIRSTDAYOFWEEK + 8) % 7;
+ }
- // Evaled configuration attributes
- angular.forEach(['showWeeks', 'yearRows', 'yearColumns', 'shortcutPropagation'], function(key) {
- self[key] = angular.isDefined($attrs[key]) ?
- $scope.$parent.$eval($attrs[key]) : datepickerConfig[key];
- });
+ break;
+ case 'maxDate':
+ case 'minDate':
+ if ($scope.datepickerOptions[key]) {
+ $scope.$watch(function() { return $scope.datepickerOptions[key]; }, function(value) {
+ if (value) {
+ if (angular.isDate(value)) {
+ self[key] = dateParser.fromTimezone(new Date(value), ngModelOptions.timezone);
+ } else {
+ self[key] = new Date(dateFilter(value, 'medium'));
+ }
+ } else {
+ self[key] = null;
+ }
+
+ self.refreshView();
+ });
+ } else {
+ self[key] = datepickerConfig[key] ? dateParser.fromTimezone(new Date(datepickerConfig[key]), ngModelOptions.timezone) : null;
+ }
- if (angular.isDefined($attrs.startingDay)) {
- self.startingDay = $scope.$parent.$eval($attrs.startingDay);
- } else if (angular.isNumber(datepickerConfig.startingDay)) {
- self.startingDay = datepickerConfig.startingDay;
+ break;
+ case 'maxMode':
+ case 'minMode':
+ if ($scope.datepickerOptions[key]) {
+ $scope.$watch(function() { return $scope.datepickerOptions[key]; }, function(value) {
+ self[key] = $scope[key] = angular.isDefined(value) ? value : datepickerOptions[key];
+ if (key === 'minMode' && self.modes.indexOf($scope.datepickerMode) < self.modes.indexOf(self[key]) ||
+ key === 'maxMode' && self.modes.indexOf($scope.datepickerMode) > self.modes.indexOf(self[key])) {
+ $scope.datepickerMode = self[key];
+ }
+ });
+ } else {
+ self[key] = $scope[key] = datepickerConfig[key] || null;
+ }
+
+ break;
+ case 'initDate':
+ if ($scope.datepickerOptions.initDate) {
+ this.activeDate = dateParser.fromTimezone($scope.datepickerOptions.initDate, ngModelOptions.timezone) || new Date();
+ $scope.$watch(function() { return $scope.datepickerOptions.initDate; }, function(initDate) {
+ if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) {
+ self.activeDate = dateParser.fromTimezone(initDate, ngModelOptions.timezone);
+ self.refreshView();
+ }
+ });
+ } else {
+ this.activeDate = new Date();
+ }
+ }
+ });
} else {
- self.startingDay = ($locale.DATETIME_FORMATS.FIRSTDAYOFWEEK + 8) % 7;
- }
+ // Interpolated configuration attributes
+ angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle'], function(key) {
+ self[key] = angular.isDefined($attrs[key]) ? $interpolate($attrs[key])($scope.$parent) : datepickerConfig[key];
+ });
- // Watchable date attributes
- angular.forEach(['minDate', 'maxDate'], function(key) {
- if ($attrs[key]) {
- watchListeners.push($scope.$parent.$watch($attrs[key], function(value) {
- self[key] = value ? angular.isDate(value) ? dateParser.fromTimezone(new Date(value), ngModelOptions.timezone) : new Date(dateFilter(value, 'medium')) : null;
- self.refreshView();
- }));
+ // Evaled configuration attributes
+ angular.forEach(['showWeeks', 'yearRows', 'yearColumns', 'shortcutPropagation'], function(key) {
+ self[key] = angular.isDefined($attrs[key]) ?
+ $scope.$parent.$eval($attrs[key]) : datepickerConfig[key];
+ });
+
+ if (angular.isDefined($attrs.startingDay)) {
+ self.startingDay = $scope.$parent.$eval($attrs.startingDay);
+ } else if (angular.isNumber(datepickerConfig.startingDay)) {
+ self.startingDay = datepickerConfig.startingDay;
} else {
- self[key] = datepickerConfig[key] ? dateParser.fromTimezone(new Date(datepickerConfig[key]), ngModelOptions.timezone) : null;
+ self.startingDay = ($locale.DATETIME_FORMATS.FIRSTDAYOFWEEK + 8) % 7;
}
- });
- angular.forEach(['minMode', 'maxMode'], function(key) {
- if ($attrs[key]) {
- watchListeners.push($scope.$parent.$watch($attrs[key], function(value) {
- self[key] = $scope[key] = angular.isDefined(value) ? value : $attrs[key];
- if (key === 'minMode' && self.modes.indexOf($scope.datepickerMode) < self.modes.indexOf(self[key]) ||
- key === 'maxMode' && self.modes.indexOf($scope.datepickerMode) > self.modes.indexOf(self[key])) {
- $scope.datepickerMode = self[key];
+ // Watchable date attributes
+ angular.forEach(['minDate', 'maxDate'], function(key) {
+ if ($attrs[key]) {
+ watchListeners.push($scope.$parent.$watch($attrs[key], function(value) {
+ if (value) {
+ if (angular.isDate(value)) {
+ self[key] = dateParser.fromTimezone(new Date(value), ngModelOptions.timezone);
+ } else {
+ self[key] = new Date(dateFilter(value, 'medium'));
+ }
+ } else {
+ self[key] = null;
+ }
+
+ self.refreshView();
+ }));
+ } else {
+ self[key] = datepickerConfig[key] ? dateParser.fromTimezone(new Date(datepickerConfig[key]), ngModelOptions.timezone) : null;
+ }
+ });
+
+ angular.forEach(['minMode', 'maxMode'], function(key) {
+ if ($attrs[key]) {
+ watchListeners.push($scope.$parent.$watch($attrs[key], function(value) {
+ self[key] = $scope[key] = angular.isDefined(value) ? value : $attrs[key];
+ if (key === 'minMode' && self.modes.indexOf($scope.datepickerMode) < self.modes.indexOf(self[key]) ||
+ key === 'maxMode' && self.modes.indexOf($scope.datepickerMode) > self.modes.indexOf(self[key])) {
+ $scope.datepickerMode = self[key];
+ }
+ }));
+ } else {
+ self[key] = $scope[key] = datepickerConfig[key] || null;
+ }
+ });
+
+ if (angular.isDefined($attrs.initDate)) {
+ this.activeDate = dateParser.fromTimezone($scope.$parent.$eval($attrs.initDate), ngModelOptions.timezone) || new Date();
+ watchListeners.push($scope.$parent.$watch($attrs.initDate, function(initDate) {
+ if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) {
+ self.activeDate = dateParser.fromTimezone(initDate, ngModelOptions.timezone);
+ self.refreshView();
}
}));
} else {
- self[key] = $scope[key] = datepickerConfig[key] || null;
+ this.activeDate = new Date();
}
- });
+ }
$scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode;
$scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000);
- if (angular.isDefined($attrs.initDate)) {
- this.activeDate = dateParser.fromTimezone($scope.$parent.$eval($attrs.initDate), ngModelOptions.timezone) || new Date();
- watchListeners.push($scope.$parent.$watch($attrs.initDate, function(initDate) {
- if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) {
- self.activeDate = dateParser.fromTimezone(initDate, ngModelOptions.timezone);
- self.refreshView();
- }
- }));
- } else {
- this.activeDate = new Date();
- }
-
$scope.disabled = angular.isDefined($attrs.disabled) || false;
if (angular.isDefined($attrs.ngDisabled)) {
watchListeners.push($scope.$parent.$watch($attrs.ngDisabled, function(disabled) {
@@ -501,6 +607,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
},
scope: {
datepickerMode: '=?',
+ datepickerOptions: '=?',
dateDisabled: '&',
customClass: '&',
shortcutPropagation: '&?'
diff --git a/src/datepicker/docs/readme.md b/src/datepicker/docs/readme.md
index b686bc725d..277e2e61ed 100644
--- a/src/datepicker/docs/readme.md
+++ b/src/datepicker/docs/readme.md
@@ -27,6 +27,29 @@ The datepicker has 3 modes:
_(Default: `day`)_ -
Current mode of the datepicker _(day|month|year)_. Can be used to initialize the datepicker in a specific mode.
+* `datepicker-options`
+ $ -
+ An optional object to configure the datepicker in one place. If this attribute is used, all supported options must be specified instead of the attributes.
+
+ The supported options are:
+
+ - formatDay
+ - formatDayHeader
+ - formatDayTitle
+ - formatMonth
+ - formatMonthTitle
+ - formatYear
+ - initDate
+ - maxDate
+ - maxMode
+ - minDate
+ - minMode
+ - shortcutPropagation
+ - showWeeks
+ - startingDay
+ - yearColumns
+ - yearRows
+
* `format-day`
C
_(Default: `dd`)_ -
diff --git a/src/datepicker/test/datepicker.spec.js b/src/datepicker/test/datepicker.spec.js
index b2a9263ebf..84c79cf643 100644
--- a/src/datepicker/test/datepicker.spec.js
+++ b/src/datepicker/test/datepicker.spec.js
@@ -866,7 +866,301 @@ describe('datepicker', function() {
});
});
- describe('attribute `starting-day`', function () {
+ describe('attribute `datepicker-options`', function() {
+ describe('startingDay', function() {
+ beforeEach(function() {
+ $rootScope.datepickerOptions = {
+ startingDay: 1
+ };
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+ });
+
+ it('shows the day labels rotated', function() {
+ expect(getLabels(true)).toEqual(['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']);
+ });
+
+ it('renders the calendar days correctly', function() {
+ expect(getOptions(true)).toEqual([
+ ['30', '31', '01', '02', '03', '04', '05'],
+ ['06', '07', '08', '09', '10', '11', '12'],
+ ['13', '14', '15', '16', '17', '18', '19'],
+ ['20', '21', '22', '23', '24', '25', '26'],
+ ['27', '28', '29', '30', '01', '02', '03'],
+ ['04', '05', '06', '07', '08', '09', '10']
+ ]);
+ });
+
+ it('renders the week numbers correctly', function() {
+ expect(getWeeks()).toEqual(['35', '36', '37', '38', '39', '40']);
+ });
+ });
+
+ describe('showWeeks', function() {
+ beforeEach(function() {
+ $rootScope.datepickerOptions = {
+ showWeeks: false
+ };
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+ });
+
+ it('hides week numbers based on variable', function() {
+ expect(getLabelsRow().find('th').length).toEqual(7);
+ var tr = element.find('tbody').find('tr');
+ for (var i = 0; i < 5; i++) {
+ expect(tr.eq(i).find('td').length).toEqual(7);
+ }
+ });
+ });
+
+ describe('minDate', function() {
+ beforeEach(function() {
+ $rootScope.datepickerOptions = {
+ minDate: new Date('September 12, 2010')
+ };
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+ });
+
+ it('disables appropriate days in current month', function() {
+ var buttons = getAllOptionsEl();
+ angular.forEach(buttons, function(button, index) {
+ expect(angular.element(button).prop('disabled')).toBe(index < 14);
+ });
+ });
+
+ it('disables appropriate days when min date changes', function() {
+ $rootScope.datepickerOptions.minDate = new Date('September 5, 2010');
+ $rootScope.$digest();
+
+ var buttons = getAllOptionsEl();
+ angular.forEach(buttons, function(button, index) {
+ expect(angular.element(button).prop('disabled')).toBe(index < 7);
+ });
+ });
+
+ it('invalidates when model is a disabled date', function() {
+ $rootScope.datepickerOptions.minDate = new Date('September 5, 2010');
+ $rootScope.date = new Date('September 2, 2010');
+ $rootScope.$digest();
+ expect(element.hasClass('ng-invalid')).toBeTruthy();
+ expect(element.hasClass('ng-invalid-date-disabled')).toBeTruthy();
+ });
+
+ it('disables all days in previous month', function() {
+ clickPreviousButton();
+ var buttons = getAllOptionsEl();
+ angular.forEach(buttons, function(button, index) {
+ expect(angular.element(button).prop('disabled')).toBe(true);
+ });
+ });
+
+ it('disables no days in next month', function() {
+ clickNextButton();
+ var buttons = getAllOptionsEl();
+ angular.forEach(buttons, function(button, index) {
+ expect(angular.element(button).prop('disabled')).toBe(false);
+ });
+ });
+
+ it('disables appropriate months in current year', function() {
+ clickTitleButton();
+ var buttons = getAllOptionsEl();
+ angular.forEach(buttons, function(button, index) {
+ expect(angular.element(button).prop('disabled')).toBe(index < 8);
+ });
+ });
+
+ it('disables all months in previous year', function() {
+ clickTitleButton();
+ clickPreviousButton();
+ var buttons = getAllOptionsEl();
+ angular.forEach(buttons, function(button, index) {
+ expect(angular.element(button).prop('disabled')).toBe(true);
+ });
+ });
+
+ it('disables no months in next year', function() {
+ clickTitleButton();
+ clickNextButton();
+ var buttons = getAllOptionsEl();
+ angular.forEach(buttons, function(button, index) {
+ expect(angular.element(button).prop('disabled')).toBe(false);
+ });
+ });
+
+ it('enables everything before if it is cleared', function() {
+ $rootScope.datepickerOptions.minDate = null;
+ $rootScope.date = new Date('December 20, 1949');
+ $rootScope.$digest();
+
+ clickTitleButton();
+ var buttons = getAllOptionsEl();
+ angular.forEach(buttons, function(button, index) {
+ expect(angular.element(button).prop('disabled')).toBe(false);
+ });
+ });
+
+ it('accepts literals, \'yyyy-MM-dd\' case', function() {
+ $rootScope.datepickerOptions.minDate = '2010-09-05';
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+ var buttons = getAllOptionsEl();
+ angular.forEach(buttons, function(button, index) {
+ expect(angular.element(button).prop('disabled')).toBe(index < 7);
+ });
+ });
+ });
+
+ describe('maxDate', function() {
+ beforeEach(function() {
+ $rootScope.datepickerOptions = {
+ maxDate: new Date('September 25, 2010')
+ };
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+ });
+
+ it('disables appropriate days in current month', function() {
+ var buttons = getAllOptionsEl();
+ angular.forEach(buttons, function(button, index) {
+ expect(angular.element(button).prop('disabled')).toBe(index > 27);
+ });
+ });
+
+ it('disables appropriate days when max date changes', function() {
+ $rootScope.datepickerOptions.maxDate = new Date('September 18, 2010');
+ $rootScope.$digest();
+
+ var buttons = getAllOptionsEl();
+ angular.forEach(buttons, function(button, index) {
+ expect(angular.element(button).prop('disabled')).toBe(index > 20);
+ });
+ });
+
+ it('invalidates when model is a disabled date', function() {
+ $rootScope.datepickerOptions.maxDate = new Date('September 18, 2010');
+ $rootScope.$digest();
+ expect(element.hasClass('ng-invalid')).toBeTruthy();
+ expect(element.hasClass('ng-invalid-date-disabled')).toBeTruthy();
+ });
+
+ it('disables no days in previous month', function() {
+ clickPreviousButton();
+ var buttons = getAllOptionsEl();
+ angular.forEach(buttons, function(button, index) {
+ expect(angular.element(button).prop('disabled')).toBe(false);
+ });
+ });
+
+ it('disables all days in next month', function() {
+ clickNextButton();
+ var buttons = getAllOptionsEl();
+ angular.forEach(buttons, function(button, index) {
+ expect(angular.element(button).prop('disabled')).toBe(true);
+ });
+ });
+
+ it('disables appropriate months in current year', function() {
+ clickTitleButton();
+ var buttons = getAllOptionsEl();
+ angular.forEach(buttons, function(button, index) {
+ expect(angular.element(button).prop('disabled')).toBe(index > 8);
+ });
+ });
+
+ it('disables no months in previous year', function() {
+ clickTitleButton();
+ clickPreviousButton();
+ var buttons = getAllOptionsEl();
+ angular.forEach(buttons, function(button, index) {
+ expect(angular.element(button).prop('disabled')).toBe(false);
+ });
+ });
+
+ it('disables all months in next year', function() {
+ clickTitleButton();
+ clickNextButton();
+ var buttons = getAllOptionsEl();
+ angular.forEach(buttons, function(button, index) {
+ expect(angular.element(button).prop('disabled')).toBe(true);
+ });
+ });
+
+ it('enables everything after if it is cleared', function() {
+ $rootScope.datepickerOptions.maxDate = null;
+ $rootScope.$digest();
+ var buttons = getAllOptionsEl();
+ angular.forEach(buttons, function(button, index) {
+ expect(angular.element(button).prop('disabled')).toBe(false);
+ });
+ });
+ });
+
+ describe('formatting', function() {
+ beforeEach(function() {
+ $rootScope.datepickerOptions = {
+ formatDay: 'd',
+ formatDayHeader: 'EEEE',
+ formatDayTitle: 'MMMM, yy',
+ formatMonth: 'MMM',
+ formatMonthTitle: 'yy',
+ formatYear: 'yy',
+ yearColumns: 4,
+ yearRows: 3
+ };
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+ });
+
+ it('changes the title format in `day` mode', function() {
+ expect(getTitle()).toBe('September, 10');
+ });
+
+ it('changes the title & months format in `month` mode', function() {
+ clickTitleButton();
+
+ expect(getTitle()).toBe('10');
+ expect(getOptions()).toEqual([
+ ['Jan', 'Feb', 'Mar'],
+ ['Apr', 'May', 'Jun'],
+ ['Jul', 'Aug', 'Sep'],
+ ['Oct', 'Nov', 'Dec']
+ ]);
+ });
+
+ it('changes the title, year format & range in `year` mode', function() {
+ clickTitleButton();
+ clickTitleButton();
+
+ expect(getTitle()).toBe('05 - 16');
+ expect(getOptions()).toEqual([
+ ['05', '06', '07', '08'],
+ ['09', '10', '11', '12'],
+ ['13', '14', '15', '16']
+ ]);
+ });
+
+ it('shows day labels', function() {
+ expect(getLabels(true)).toEqual(['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']);
+ });
+
+ it('changes the day format', function() {
+ expect(getOptions(true)).toEqual([
+ ['29', '30', '31', '1', '2', '3', '4'],
+ ['5', '6', '7', '8', '9', '10', '11'],
+ ['12', '13', '14', '15', '16', '17', '18'],
+ ['19', '20', '21', '22', '23', '24', '25'],
+ ['26', '27', '28', '29', '30', '1', '2'],
+ ['3', '4', '5', '6', '7', '8', '9']
+ ]);
+ });
+ });
+ });
+
+ describe('attribute `starting-day`', function() {
beforeEach(function() {
$rootScope.startingDay = 1;
element = $compile('')($rootScope);
@@ -909,7 +1203,7 @@ describe('datepicker', function() {
});
});
- describe('`min-date` attribute', function () {
+ describe('`min-date` attribute', function() {
beforeEach(function() {
$rootScope.mindate = new Date('September 12, 2010');
element = $compile('')($rootScope);
@@ -1088,7 +1382,7 @@ describe('datepicker', function() {
});
});
- describe('date-disabled expression', function () {
+ describe('date-disabled expression', function() {
beforeEach(function() {
$rootScope.dateDisabledHandler = jasmine.createSpy('dateDisabledHandler');
element = $compile('')($rootScope);