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);