From dab18336e4d3459cd027d475ed5c535153ced0b4 Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Sat, 3 Aug 2013 14:38:12 +0300 Subject: [PATCH] feat(datepikcer): `ngModelController` plug & new `datepikcerPopup` * `ngModelController` integration * `datepikcerPopup` directive ti use with inputs * invalid & disabled validation * add `min` / `max` into configuration Closes #612 --- src/datepicker/datepicker.js | 486 +++++++++++----- src/datepicker/docs/demo.html | 16 +- src/datepicker/docs/demo.js | 13 +- src/datepicker/test/datepicker.spec.js | 770 ++++++++++++++++++------- template/datepicker/datepicker.html | 4 +- template/datepicker/popup.html | 12 + 6 files changed, 959 insertions(+), 342 deletions(-) create mode 100644 template/datepicker/popup.html diff --git a/src/datepicker/datepicker.js b/src/datepicker/datepicker.js index fdfd63e8c6..2b89878a28 100644 --- a/src/datepicker/datepicker.js +++ b/src/datepicker/datepicker.js @@ -1,4 +1,4 @@ -angular.module('ui.bootstrap.datepicker', []) +angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position']) .constant('datepickerConfig', { dayFormat: 'dd', @@ -9,31 +9,139 @@ angular.module('ui.bootstrap.datepicker', []) monthTitleFormat: 'yyyy', showWeeks: true, startingDay: 0, - yearRange: 20 + yearRange: 20, + minDate: null, + maxDate: null }) -.directive( 'datepicker', ['dateFilter', '$parse', 'datepickerConfig', function (dateFilter, $parse, datepickerConfig) { +.controller('DatepickerController', ['$scope', '$attrs', 'dateFilter', 'datepickerConfig', function($scope, $attrs, dateFilter, dtConfig) { + var format = { + day: getValue($attrs.dayFormat, dtConfig.dayFormat), + month: getValue($attrs.monthFormat, dtConfig.monthFormat), + year: getValue($attrs.yearFormat, dtConfig.yearFormat), + dayHeader: getValue($attrs.dayHeaderFormat, dtConfig.dayHeaderFormat), + dayTitle: getValue($attrs.dayTitleFormat, dtConfig.dayTitleFormat), + monthTitle: getValue($attrs.monthTitleFormat, dtConfig.monthTitleFormat) + }, + startingDay = getValue($attrs.startingDay, dtConfig.startingDay), + yearRange = getValue($attrs.yearRange, dtConfig.yearRange); + + this.minDate = dtConfig.minDate ? new Date(dtConfig.minDate) : null; + this.maxDate = dtConfig.maxDate ? new Date(dtConfig.maxDate) : null; + + function getValue(value, defaultValue) { + return angular.isDefined(value) ? $scope.$parent.$eval(value) : defaultValue; + } + + function getDaysInMonth( year, month ) { + return new Date(year, month, 0).getDate(); + } + + function getDates(startDate, n) { + var dates = new Array(n); + var current = startDate, i = 0; + while (i < n) { + dates[i++] = new Date(current); + current.setDate( current.getDate() + 1 ); + } + return dates; + } + + function makeDate(date, format, isSelected, isSecondary) { + return { date: date, label: dateFilter(date, format), selected: !!isSelected, secondary: !!isSecondary }; + } + + this.modes = [ + { + name: 'day', + getVisibleDates: function(date, selected) { + var year = date.getFullYear(), month = date.getMonth(), firstDayOfMonth = new Date(year, month, 1); + var difference = startingDay - firstDayOfMonth.getDay(), + numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference, + firstDate = new Date(firstDayOfMonth), numDates = 0; + + if ( numDisplayedFromPreviousMonth > 0 ) { + firstDate.setDate( - numDisplayedFromPreviousMonth + 1 ); + numDates += numDisplayedFromPreviousMonth; // Previous + } + numDates += getDaysInMonth(year, month + 1); // Current + numDates += (7 - numDates % 7) % 7; // Next + + var days = getDates(firstDate, numDates), labels = new Array(7); + for (var i = 0; i < numDates; i ++) { + var dt = new Date(days[i]); + days[i] = makeDate(dt, format.day, (selected && selected.getDate() === dt.getDate() && selected.getMonth() === dt.getMonth() && selected.getFullYear() === dt.getFullYear()), dt.getMonth() !== month); + } + for (var j = 0; j < 7; j++) { + labels[j] = dateFilter(days[j].date, format.dayHeader); + } + return { objects: days, title: dateFilter(date, format.dayTitle), labels: labels }; + }, + compare: function(date1, date2) { + return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) ); + }, + split: 7, + step: { months: 1 } + }, + { + name: 'month', + getVisibleDates: function(date, selected) { + var months = new Array(12), year = date.getFullYear(); + for ( var i = 0; i < 12; i++ ) { + var dt = new Date(year, i, 1); + months[i] = makeDate(dt, format.month, (selected && selected.getMonth() === i && selected.getFullYear() === year)); + } + return { objects: months, title: dateFilter(date, format.monthTitle) }; + }, + compare: function(date1, date2) { + return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() ); + }, + split: 3, + step: { years: 1 } + }, + { + name: 'year', + getVisibleDates: function(date, selected) { + var years = new Array(yearRange), year = date.getFullYear(), startYear = parseInt((year - 1) / yearRange, 10) * yearRange + 1; + for ( var i = 0; i < yearRange; i++ ) { + var dt = new Date(startYear + i, 0, 1); + years[i] = makeDate(dt, format.year, (selected && selected.getFullYear() === dt.getFullYear())); + } + return { objects: years, title: [years[0].label, years[yearRange - 1].label].join(' - ') }; + }, + compare: function(date1, date2) { + return date1.getFullYear() - date2.getFullYear(); + }, + split: 5, + step: { years: yearRange } + } + ]; + + this.isDisabled = function(date, mode) { + var currentMode = this.modes[mode || 0]; + return ((this.minDate && currentMode.compare(date, this.minDate) < 0) || (this.maxDate && currentMode.compare(date, this.maxDate) > 0) || ($scope.dateDisabled && $scope.dateDisabled({date: date, mode: currentMode.name}))); + }; +}]) + +.directive( 'datepicker', ['dateFilter', '$parse', 'datepickerConfig', '$log', function (dateFilter, $parse, datepickerConfig, $log) { return { restrict: 'EA', replace: true, + templateUrl: 'template/datepicker/datepicker.html', scope: { - model: '=ngModel', dateDisabled: '&' }, - templateUrl: 'template/datepicker/datepicker.html', - link: function(scope, element, attrs) { - scope.mode = 'day'; // Initial mode + require: ['datepicker', '?^ngModel'], + controller: 'DatepickerController', + link: function(scope, element, attrs, ctrls) { + var datepickerCtrl = ctrls[0], ngModel = ctrls[1]; + + if (!ngModel) { + return; // do nothing if no ng-model + } // Configuration parameters - var selected = new Date(), showWeeks, minDate, maxDate, format = {}; - format.day = angular.isDefined(attrs.dayFormat) ? scope.$eval(attrs.dayFormat) : datepickerConfig.dayFormat; - format.month = angular.isDefined(attrs.monthFormat) ? scope.$eval(attrs.monthFormat) : datepickerConfig.monthFormat; - format.year = angular.isDefined(attrs.yearFormat) ? scope.$eval(attrs.yearFormat) : datepickerConfig.yearFormat; - format.dayHeader = angular.isDefined(attrs.dayHeaderFormat) ? scope.$eval(attrs.dayHeaderFormat) : datepickerConfig.dayHeaderFormat; - format.dayTitle = angular.isDefined(attrs.dayTitleFormat) ? scope.$eval(attrs.dayTitleFormat) : datepickerConfig.dayTitleFormat; - format.monthTitle = angular.isDefined(attrs.monthTitleFormat) ? scope.$eval(attrs.monthTitleFormat) : datepickerConfig.monthTitleFormat; - var startingDay = angular.isDefined(attrs.startingDay) ? scope.$eval(attrs.startingDay) : datepickerConfig.startingDay; - var yearRange = angular.isDefined(attrs.yearRange) ? scope.$eval(attrs.yearRange) : datepickerConfig.yearRange; + var mode = 0, selected = new Date(), showWeeks = datepickerConfig.showWeeks; if (attrs.showWeeks) { scope.$parent.$watch($parse(attrs.showWeeks), function(value) { @@ -41,174 +149,282 @@ angular.module('ui.bootstrap.datepicker', []) updateShowWeekNumbers(); }); } else { - showWeeks = datepickerConfig.showWeeks; updateShowWeekNumbers(); } if (attrs.min) { scope.$parent.$watch($parse(attrs.min), function(value) { - minDate = value ? new Date(value) : null; + datepickerCtrl.minDate = value ? new Date(value) : null; refill(); }); } if (attrs.max) { scope.$parent.$watch($parse(attrs.max), function(value) { - maxDate = value ? new Date(value) : null; + datepickerCtrl.maxDate = value ? new Date(value) : null; refill(); }); } - function updateCalendar (rows, labels, title) { - scope.rows = rows; - scope.labels = labels; - scope.title = title; + function updateShowWeekNumbers() { + scope.showWeekNumbers = mode === 0 && showWeeks; } - // Define whether the week number are visible - function updateShowWeekNumbers() { - scope.showWeekNumbers = ( scope.mode === 'day' && showWeeks ); + // Split array into smaller arrays + function split(arr, size) { + var arrays = []; + while (arr.length > 0) { + arrays.push(arr.splice(0, size)); + } + return arrays; } - function compare( date1, date2 ) { - if ( scope.mode === 'year') { - return date2.getFullYear() - date1.getFullYear(); - } else if ( scope.mode === 'month' ) { - return new Date( date2.getFullYear(), date2.getMonth() ) - new Date( date1.getFullYear(), date1.getMonth() ); - } else if ( scope.mode === 'day' ) { - return (new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) - new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) ); + function refill( updateSelected ) { + var date = null, valid = true; + + if ( ngModel.$modelValue ) { + date = new Date( ngModel.$modelValue ); + + if ( isNaN(date) ) { + valid = false; + $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); + } else if ( updateSelected ) { + selected = date; + } } + ngModel.$setValidity('date', valid); + + var currentMode = datepickerCtrl.modes[mode], data = currentMode.getVisibleDates(selected, date); + angular.forEach(data.objects, function(obj) { + obj.disabled = datepickerCtrl.isDisabled(obj.date, mode); + }); + + ngModel.$setValidity('date-disabled', (!date || !datepickerCtrl.isDisabled(date))); + + scope.rows = split(data.objects, currentMode.split); + scope.labels = data.labels || []; + scope.title = data.title; } - function isDisabled(date) { - return ((minDate && compare(date, minDate) > 0) || (maxDate && compare(date, maxDate) < 0) || (scope.dateDisabled && scope.dateDisabled({ date: date, mode: scope.mode }))); + function setMode(value) { + mode = value; + updateShowWeekNumbers(); + refill(); } - // Split array into smaller arrays - var split = function(a, size) { - var arrays = []; - while (a.length > 0) { - arrays.push(a.splice(0, size)); + ngModel.$render = function() { + refill( true ); + }; + + scope.select = function( date ) { + if ( mode === 0 ) { + var dt = new Date( ngModel.$modelValue ); + dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() ); + ngModel.$setViewValue( dt ); + refill( true ); + } else { + selected = date; + setMode( mode - 1 ); } - return arrays; }; - var getDaysInMonth = function( year, month ) { - return new Date(year, month + 1, 0).getDate(); + scope.move = function(direction) { + var step = datepickerCtrl.modes[mode].step; + selected.setMonth( selected.getMonth() + direction * (step.months || 0) ); + selected.setFullYear( selected.getFullYear() + direction * (step.years || 0) ); + refill(); + }; + scope.toggleMode = function() { + setMode( (mode + 1) % datepickerCtrl.modes.length ); + }; + scope.getWeekNumber = function(row) { + return ( mode === 0 && scope.showWeekNumbers && row.length === 7 ) ? getISO8601WeekNumber(row[0].date) : null; }; - var fill = { - day: function() { - var days = [], labels = [], lastDate = null; + function getISO8601WeekNumber(date) { + var checkDate = new Date(date); + checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday + var time = checkDate.getTime(); + checkDate.setMonth(0); // Compare with Jan 1 + checkDate.setDate(1); + return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; + } + } + }; +}]) - function addDays( dt, n, isCurrentMonth ) { - for (var i =0; i < n; i ++) { - days.push( {date: new Date(dt), isCurrent: isCurrentMonth, isSelected: isSelected(dt), label: dateFilter(dt, format.day), disabled: isDisabled(dt) } ); - dt.setDate( dt.getDate() + 1 ); - } - lastDate = dt; - } +.constant('datepickerPopupConfig', { + dateFormat: 'yyyy-MM-dd', + closeOnDateSelection: true +}) + +.directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'datepickerPopupConfig', +function ($compile, $parse, $document, $position, dateFilter, datepickerPopupConfig) { + return { + restrict: 'EA', + require: 'ngModel', + link: function(originalScope, element, attrs, ngModel) { - var d = new Date(selected); - d.setDate(1); + var closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection; + var dateFormat = attrs.datepickerPopup || datepickerPopupConfig.dateFormat; - var difference = startingDay - d.getDay(); - var numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference; + // create a child scope for the datepicker directive so we are not polluting original scope + var scope = originalScope.$new(); + originalScope.$on('$destroy', function() { + scope.$destroy(); + }); - if ( numDisplayedFromPreviousMonth > 0 ) { - d.setDate( - numDisplayedFromPreviousMonth + 1 ); - addDays(d, numDisplayedFromPreviousMonth, false); - } - addDays(lastDate || d, getDaysInMonth(selected.getFullYear(), selected.getMonth()), true); - addDays(lastDate, (7 - days.length % 7) % 7, false); + function formatDate(value) { + return (value) ? dateFilter(value, dateFormat) : null; + } + ngModel.$formatters.push(formatDate); - // Day labels - for (i = 0; i < 7; i++) { - labels.push( dateFilter(days[i].date, format.dayHeader) ); - } - updateCalendar( split( days, 7 ), labels, dateFilter(selected, format.dayTitle) ); - }, - month: function() { - var months = [], i = 0, year = selected.getFullYear(); - while ( i < 12 ) { - var dt = new Date(year, i++, 1); - months.push( {date: dt, isCurrent: true, isSelected: isSelected(dt), label: dateFilter(dt, format.month), disabled: isDisabled(dt)} ); - } - updateCalendar( split( months, 3 ), [], dateFilter(selected, format.monthTitle) ); - }, - year: function() { - var years = [], year = parseInt((selected.getFullYear() - 1) / yearRange, 10) * yearRange + 1; - for ( var i = 0; i < yearRange; i++ ) { - var dt = new Date(year + i, 0, 1); - years.push( {date: dt, isCurrent: true, isSelected: isSelected(dt), label: dateFilter(dt, format.year), disabled: isDisabled(dt)} ); + // TODO: reverse from dateFilter string to Date object + function parseDate(value) { + if ( value ) { + var date = new Date(value); + if (!isNaN(date)) { + return date; } - var title = years[0].label + ' - ' + years[years.length - 1].label; - updateCalendar( split( years, 5 ), [], title ); + } + return value; + } + ngModel.$parsers.push(parseDate); + + var getIsOpen, setIsOpen; + if ( attrs.open ) { + getIsOpen = $parse(attrs.open); + setIsOpen = getIsOpen.assign; + + originalScope.$watch(getIsOpen, function updateOpen(value) { + scope.isOpen = !! value; + }); + } + scope.isOpen = getIsOpen ? getIsOpen(originalScope) : false; // Initial state + + function setOpen( value ) { + if (setIsOpen) { + setIsOpen(originalScope, !!value); + } else { + scope.isOpen = !!value; + } + } + + var documentClickBind = function(event) { + if (scope.isOpen && event.target !== element[0]) { + scope.$apply(function() { + setOpen(false); + }); } }; - var refill = function() { - fill[scope.mode](); + + var elementFocusBind = function() { + scope.$apply(function() { + setOpen( true ); + }); }; - var isSelected = function( dt ) { - if ( scope.model && scope.model.getFullYear() === dt.getFullYear() ) { - if ( scope.mode === 'year' ) { - return true; - } - if ( scope.model.getMonth() === dt.getMonth() ) { - return ( scope.mode === 'month' || (scope.mode === 'day' && scope.model.getDate() === dt.getDate()) ); - } + + // popup element used to display calendar + var popupEl = angular.element(''); + popupEl.attr({ + 'ng-model': 'date', + 'ng-change': 'dateSelection()' + }); + var datepickerEl = popupEl.find('datepicker'); + if (attrs.datepickerOptions) { + datepickerEl.attr(angular.extend({}, originalScope.$eval(attrs.datepickerOptions))); + } + + var $setModelValue = $parse(attrs.ngModel).assign; + + // Inner change + scope.dateSelection = function() { + $setModelValue(originalScope, scope.date); + if (closeOnDateSelection) { + setOpen( false ); } - return false; }; - scope.$watch('model', function ( dt, olddt ) { - if ( angular.isDate(dt) ) { - selected = new Date(dt); - } + // Outter change + scope.$watch(function() { + return ngModel.$modelValue; + }, function(value) { + if (angular.isString(value)) { + var date = parseDate(value); - if ( ! angular.equals(dt, olddt) ) { - refill(); + if (value && !date) { + $setModelValue(originalScope, null); + throw new Error(value + ' cannot be parsed to a date object.'); + } else { + value = date; + } } + scope.date = value; + updatePosition(); }); - scope.$watch('mode', function() { - updateShowWeekNumbers(); - refill(); - }); - scope.select = function( dt ) { - selected = new Date(dt); + function addWatchableAttribute(attribute, scopeProperty, datepickerAttribute) { + if (attribute) { + originalScope.$watch($parse(attribute), function(value){ + scope[scopeProperty] = value; + }); + datepickerEl.attr(datepickerAttribute || scopeProperty, scopeProperty); + } + } + addWatchableAttribute(attrs.min, 'min'); + addWatchableAttribute(attrs.max, 'max'); + if (attrs.showWeeks) { + addWatchableAttribute(attrs.showWeeks, 'showWeeks', 'show-weeks'); + } else { + scope.showWeeks = true; + datepickerEl.attr('show-weeks', 'showWeeks'); + } + if (attrs.dateDisabled) { + datepickerEl.attr('date-disabled', attrs.dateDisabled); + } + + function updatePosition() { + scope.position = $position.position(element); + scope.position.top = scope.position.top + element.prop('offsetHeight'); + } - if ( scope.mode === 'year' ) { - scope.mode = 'month'; - selected.setFullYear( dt.getFullYear() ); - } else if ( scope.mode === 'month' ) { - scope.mode = 'day'; - selected.setMonth( dt.getMonth() ); - } else if ( scope.mode === 'day' ) { - scope.model = new Date(selected); + scope.$watch('isOpen', function(value) { + if (value) { + updatePosition(); + $document.bind('click', documentClickBind); + element.unbind('focus', elementFocusBind); + element.focus(); + } else { + $document.unbind('click', documentClickBind); + element.bind('focus', elementFocusBind); } - }; - scope.move = function(step) { - if (scope.mode === 'day') { - selected.setMonth( selected.getMonth() + step ); - } else if (scope.mode === 'month') { - selected.setFullYear( selected.getFullYear() + step ); - } else if (scope.mode === 'year') { - selected.setFullYear( selected.getFullYear() + step * yearRange ); + + if ( setIsOpen ) { + setIsOpen(originalScope, value); } - refill(); + }); + + scope.today = function() { + $setModelValue(originalScope, new Date()); }; - scope.toggleMode = function() { - scope.mode = ( scope.mode === 'day' ) ? 'month' : ( scope.mode === 'month' ) ? 'year' : 'day'; + scope.clear = function() { + $setModelValue(originalScope, null); }; - scope.getWeekNumber = function(row) { - if ( scope.mode !== 'day' || ! scope.showWeekNumbers || row.length !== 7 ) { - return; - } - var index = ( startingDay > 4 ) ? 11 - startingDay : 4 - startingDay; // Thursday - var d = new Date( row[ index ].date ); - d.setHours(0, 0, 0); - return Math.ceil((((d - new Date(d.getFullYear(), 0, 1)) / 86400000) + 1) / 7); // 86400000 = 1000*60*60*24; - }; + element.after($compile(popupEl)(scope)); + } + }; +}]) + +.directive('datepickerPopupWrap', [function() { + return { + restrict:'E', + replace: true, + transclude: true, + templateUrl: 'template/datepicker/popup.html', + link:function (scope, element, attrs) { + element.bind('click', function(event) { + event.preventDefault(); + event.stopPropagation(); + }); } }; }]); \ No newline at end of file diff --git a/src/datepicker/docs/demo.html b/src/datepicker/docs/demo.html index fed55e9725..526b2ab62b 100644 --- a/src/datepicker/docs/demo.html +++ b/src/datepicker/docs/demo.html @@ -1,9 +1,19 @@
-
Selected date is: {{dt | date:'fullDate' }}
+
+ +
+ +
+ + +
+ +
- + + - +
\ No newline at end of file diff --git a/src/datepicker/docs/demo.js b/src/datepicker/docs/demo.js index cf7a4df191..18eb51ec83 100644 --- a/src/datepicker/docs/demo.js +++ b/src/datepicker/docs/demo.js @@ -1,4 +1,4 @@ -var DatepickerDemoCtrl = function ($scope) { +var DatepickerDemoCtrl = function ($scope, $timeout) { $scope.today = function() { $scope.dt = new Date(); }; @@ -22,4 +22,15 @@ var DatepickerDemoCtrl = function ($scope) { $scope.minDate = ( $scope.minDate ) ? null : new Date(); }; $scope.toggleMin(); + + $scope.open = function() { + $timeout(function() { + $scope.opened = true; + }); + }; + + $scope.dateOptions = { + 'year-format': "'yy'", + 'starting-day': 1 + }; }; diff --git a/src/datepicker/test/datepicker.spec.js b/src/datepicker/test/datepicker.spec.js index 7e1a77f680..79ae2c0e10 100644 --- a/src/datepicker/test/datepicker.spec.js +++ b/src/datepicker/test/datepicker.spec.js @@ -2,11 +2,12 @@ describe('datepicker directive', function () { var $rootScope, element; beforeEach(module('ui.bootstrap.datepicker')); beforeEach(module('template/datepicker/datepicker.html')); + beforeEach(module('template/datepicker/popup.html')); beforeEach(inject(function(_$compile_, _$rootScope_) { $compile = _$compile_; $rootScope = _$rootScope_; $rootScope.date = new Date("September 30, 2010 15:30:00"); - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); })); @@ -59,19 +60,54 @@ describe('datepicker directive', function () { return weeks; } - function getOptions(rowIndex) { - var cols = element.find('tbody').find('tr').eq(rowIndex).find('td'); - var days = []; - for (var i = 1, n = cols.length; i < n; i++) { - days.push( cols.eq(i).find('button').text() ); + function getOptions() { + var tr = element.find('tbody').find('tr'); + var rows = []; + + for (var j = 0, numRows = tr.length; j < numRows; j++) { + var cols = tr.eq(j).find('td'), days = []; + for (var i = 1, n = cols.length; i < n; i++) { + days.push( cols.eq(i).find('button').text() ); + } + rows.push(days); } - return days; + return rows; } - function getOptionsEl(rowIndex, colIndex) { + function _getOptionEl(rowIndex, colIndex) { return element.find('tbody').find('tr').eq(rowIndex).find('td').eq(colIndex + 1); } + function clickOption(rowIndex, colIndex) { + _getOptionEl(rowIndex, colIndex).find('button').click(); + } + + function isDisabledOption(rowIndex, colIndex) { + return _getOptionEl(rowIndex, colIndex).find('button').prop('disabled'); + } + + function getAllOptionsEl() { + var tr = element.find('tbody').find('tr'), rows = []; + for (var i = 0; i < tr.length; i++) { + var td = tr.eq(i).find('td'), cols = []; + for (var j = 0; j < td.length; j++) { + cols.push( td.eq(j + 1) ); + } + rows.push(cols); + } + return rows; + } + + function expectSelectedElement( row, col ) { + var options = getAllOptionsEl(); + for (var i = 0, n = options.length; i < n; i ++) { + var optionsRow = options[i]; + for (var j = 0; j < optionsRow.length; j ++) { + expect(optionsRow[j].find('button').hasClass('btn-info')).toBe( i === row && j === col ); + } + } + } + it('is a `` element', function() { expect(element.prop('tagName')).toBe('TABLE'); expect(element.find('thead').find('tr').length).toBe(2); @@ -87,15 +123,17 @@ describe('datepicker directive', function () { }); it('renders the calendar days correctly', function() { - expect(getOptions(0)).toEqual(['29', '30', '31', '01', '02', '03', '04']); - expect(getOptions(1)).toEqual(['05', '06', '07', '08', '09', '10', '11']); - expect(getOptions(2)).toEqual(['12', '13', '14', '15', '16', '17', '18']); - expect(getOptions(3)).toEqual(['19', '20', '21', '22', '23', '24', '25']); - expect(getOptions(4)).toEqual(['26', '27', '28', '29', '30', '01', '02']); + expect(getOptions()).toEqual([ + ['29', '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'] + ]); }); - it('renders the week numbers correctly', function() { - expect(getWeeks()).toEqual(['35', '36', '37', '38', '39']); + it('renders the week numbers based on ISO 8601', function() { + expect(getWeeks()).toEqual(['34', '35', '36', '37', '38']); }); it('value is correct', function() { @@ -103,37 +141,36 @@ describe('datepicker directive', function () { }); it('has `selected` only the correct day', function() { - for (var i = 0; i < 5; i ++) { - for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').hasClass('btn-info')).toBe( ( i === 4 && j === 4) ); - } - } + expectSelectedElement( 4, 4 ); }); - it('has no `selected` day when model is nulled', function() { + it('has no `selected` day when model is cleared', function() { $rootScope.date = null; $rootScope.$digest(); expect($rootScope.date).toBe(null); + expectSelectedElement( null, null ); + }); - for (var i = 0; i < 5; i ++) { - for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').hasClass('btn-info')).toBe( false ); - } - } + it('does not change current view when model is cleared', function() { + $rootScope.date = null; + $rootScope.$digest(); + + expect($rootScope.date).toBe(null); + expect(getTitle()).toBe('September 2010'); }); it('`disables` visible dates from other months', function() { + var options = getAllOptionsEl(); for (var i = 0; i < 5; i ++) { for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').find('span').hasClass('muted')).toBe( ((i === 0 && j < 3) || (i === 4 && j > 4)) ); + expect(options[i][j].find('button').find('span').hasClass('muted')).toBe( ((i === 0 && j < 3) || (i === 4 && j > 4)) ); } } }); it('updates the model when a day is clicked', function() { - var el = getOptionsEl(2, 3).find('button'); - el.click(); + clickOption(2, 3); expect($rootScope.date).toEqual(new Date('September 15, 2010 15:30:00')); }); @@ -142,24 +179,22 @@ describe('datepicker directive', function () { expect(getTitle()).toBe('August 2010'); expect(getLabels()).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']); - expect(getOptions(0)).toEqual(['01', '02', '03', '04', '05', '06', '07']); - expect(getOptions(1)).toEqual(['08', '09', '10', '11', '12', '13', '14']); - expect(getOptions(2)).toEqual(['15', '16', '17', '18', '19', '20', '21']); - expect(getOptions(3)).toEqual(['22', '23', '24', '25', '26', '27', '28']); - expect(getOptions(4)).toEqual(['29', '30', '31', '01', '02', '03', '04']); - - for (var i = 0; i < 5; i ++) { - for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').hasClass('btn-info')).toBe( false ); - } - } + expect(getOptions()).toEqual([ + ['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', '31', '01', '02', '03', '04'] + ]); + + expectSelectedElement( null, null ); }); it('updates the model only when when a day is clicked in the `previous` month', function() { clickPreviousButton(); expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00')); - getOptionsEl(2, 3).find('button').click(); + clickOption(2, 3); expect($rootScope.date).toEqual(new Date('August 18, 2010 15:30:00')); }); @@ -168,61 +203,90 @@ describe('datepicker directive', function () { expect(getTitle()).toBe('October 2010'); expect(getLabels()).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']); - expect(getOptions(0)).toEqual(['26', '27', '28', '29', '30', '01', '02']); - expect(getOptions(1)).toEqual(['03', '04', '05', '06', '07', '08', '09']); - expect(getOptions(2)).toEqual(['10', '11', '12', '13', '14', '15', '16']); - expect(getOptions(3)).toEqual(['17', '18', '19', '20', '21', '22', '23']); - expect(getOptions(4)).toEqual(['24', '25', '26', '27', '28', '29', '30']); - - for (var i = 0; i < 5; i ++) { - for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').hasClass('btn-info')).toBe( (i === 0 && j === 4) ); - } - } + expect(getOptions()).toEqual([ + ['26', '27', '28', '29', '30', '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'], + ['31', '01', '02', '03', '04', '05', '06'] + ]); + + expectSelectedElement( 0, 4 ); }); it('updates the model only when when a day is clicked in the `next` month', function() { clickNextButton(); expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00')); - getOptionsEl(2, 3).find('button').click(); + clickOption(2, 3); expect($rootScope.date).toEqual(new Date('October 13, 2010 15:30:00')); }); it('updates the calendar when a day of another month is selected', function() { - getOptionsEl(4, 5).find('button').click(); + clickOption(4, 5); expect($rootScope.date).toEqual(new Date('October 01, 2010 15:30:00')); expect(getTitle()).toBe('October 2010'); expect(getLabels()).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']); - expect(getOptions(0)).toEqual(['26', '27', '28', '29', '30', '01', '02']); - expect(getOptions(1)).toEqual(['03', '04', '05', '06', '07', '08', '09']); - expect(getOptions(2)).toEqual(['10', '11', '12', '13', '14', '15', '16']); - expect(getOptions(3)).toEqual(['17', '18', '19', '20', '21', '22', '23']); - expect(getOptions(4)).toEqual(['24', '25', '26', '27', '28', '29', '30']); - - for (var i = 0; i < 5; i ++) { - for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').hasClass('btn-info')).toBe( (i === 0 && j === 5) ); - } - } + expect(getOptions()).toEqual([ + ['26', '27', '28', '29', '30', '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'], + ['31', '01', '02', '03', '04', '05', '06'] + ]); + + expectSelectedElement( 0, 5 ); }); - it('updates calendar when `model` changes', function() { - $rootScope.date = new Date('November 7, 2005 23:30:00'); - $rootScope.$digest(); + describe('when `model` changes', function () { + function testCalendar() { + expect(getTitle()).toBe('November 2005'); + expect(getOptions()).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'] + ]); + + expectSelectedElement( 1, 1 ); + } - expect(getTitle()).toBe('November 2005'); - expect(getOptions(0)).toEqual(['30', '31', '01', '02', '03', '04', '05']); - expect(getOptions(1)).toEqual(['06', '07', '08', '09', '10', '11', '12']); - expect(getOptions(2)).toEqual(['13', '14', '15', '16', '17', '18', '19']); - expect(getOptions(3)).toEqual(['20', '21', '22', '23', '24', '25', '26']); - expect(getOptions(4)).toEqual(['27', '28', '29', '30', '01', '02', '03']); + describe('to a Date object', function() { + it('updates', function() { + $rootScope.date = new Date('November 7, 2005 23:30:00'); + $rootScope.$digest(); + testCalendar(); + expect(angular.isDate($rootScope.date)).toBe(true); + }); + }); - for (var i = 0; i < 5; i ++) { - for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').hasClass('btn-info')).toBe( (i === 1 && j === 1) ); - } - } + describe('not to a Date object', function() { + + it('to a Number, it updates calendar', function() { + $rootScope.date = parseInt((new Date('November 7, 2005 23:30:00')).getTime(), 10); + $rootScope.$digest(); + testCalendar(); + expect(angular.isNumber($rootScope.date)).toBe(true); + }); + + it('to a string that can be parsed by Date, it updates calendar', function() { + $rootScope.date = 'November 7, 2005 23:30:00'; + $rootScope.$digest(); + testCalendar(); + expect(angular.isString($rootScope.date)).toBe(true); + }); + + it('to a string that cannot be parsed by Date, it gets invalid', function() { + $rootScope.date = 'pizza'; + $rootScope.$digest(); + expect(element.hasClass('ng-invalid')).toBeTruthy(); + expect(element.hasClass('ng-invalid-date')).toBeTruthy(); + expect($rootScope.date).toBe('pizza'); + }); + }); }); it('loops between different modes', function() { @@ -249,10 +313,12 @@ describe('datepicker directive', function () { it('shows months as options', function() { expect(getLabels()).toEqual([]); - expect(getOptions(0)).toEqual(['January', 'February', 'March']); - expect(getOptions(1)).toEqual(['April', 'May', 'June']); - expect(getOptions(2)).toEqual(['July', 'August', 'September']); - expect(getOptions(3)).toEqual(['October', 'November', 'December']); + expect(getOptions()).toEqual([ + ['January', 'February', 'March'], + ['April', 'May', 'June'], + ['July', 'August', 'September'], + ['October', 'November', 'December'] + ]); }); it('does not change the model', function() { @@ -260,11 +326,7 @@ describe('datepicker directive', function () { }); it('has `selected` only the correct month', function() { - for (var i = 0; i < 4; i ++) { - for (var j = 0; j < 3; j ++) { - expect(getOptionsEl(i, j).find('button').hasClass('btn-info')).toBe( ( i === 2 && j === 2) ); - } - } + expectSelectedElement( 2, 2 ); }); it('moves to the previous year when `previous` button is clicked', function() { @@ -272,16 +334,14 @@ describe('datepicker directive', function () { expect(getTitle()).toBe('2009'); expect(getLabels()).toEqual([]); - expect(getOptions(0)).toEqual(['January', 'February', 'March']); - expect(getOptions(1)).toEqual(['April', 'May', 'June']); - expect(getOptions(2)).toEqual(['July', 'August', 'September']); - expect(getOptions(3)).toEqual(['October', 'November', 'December']); - - for (var i = 0; i < 5; i ++) { - for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').hasClass('btn-info')).toBe( false ); - } - } + expect(getOptions()).toEqual([ + ['January', 'February', 'March'], + ['April', 'May', 'June'], + ['July', 'August', 'September'], + ['October', 'November', 'December'] + ]); + + expectSelectedElement( null, null ); }); it('moves to the next year when `next` button is clicked', function() { @@ -289,31 +349,33 @@ describe('datepicker directive', function () { expect(getTitle()).toBe('2011'); expect(getLabels()).toEqual([]); - expect(getOptions(0)).toEqual(['January', 'February', 'March']); - expect(getOptions(1)).toEqual(['April', 'May', 'June']); - expect(getOptions(2)).toEqual(['July', 'August', 'September']); - expect(getOptions(3)).toEqual(['October', 'November', 'December']); - - for (var i = 0; i < 4; i ++) { - for (var j = 0; j < 3; j ++) { - expect(getOptionsEl(i, j).find('button').hasClass('btn-info')).toBe( false ); - } - } + expect(getOptions()).toEqual([ + ['January', 'February', 'March'], + ['April', 'May', 'June'], + ['July', 'August', 'September'], + ['October', 'November', 'December'] + ]); + + expectSelectedElement( null, null ); }); it('renders correctly when a month is clicked', function() { clickPreviousButton(5); expect(getTitle()).toBe('2005'); - var monthNovEl = getOptionsEl(3, 1).find('button'); - monthNovEl.click(); + clickOption(3, 1); expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00')); expect(getTitle()).toBe('November 2005'); - expect(getOptions(0)).toEqual(['30', '31', '01', '02', '03', '04', '05']); - expect(getOptions(1)).toEqual(['06', '07', '08', '09', '10', '11', '12']); - expect(getOptions(2)).toEqual(['13', '14', '15', '16', '17', '18', '19']); - expect(getOptions(3)).toEqual(['20', '21', '22', '23', '24', '25', '26']); - expect(getOptions(4)).toEqual(['27', '28', '29', '30', '01', '02', '03']); + expect(getOptions()).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'] + ]); + + clickOption(2, 3); + expect($rootScope.date).toEqual(new Date('November 16, 2005 15:30:00')); }); }); @@ -328,10 +390,12 @@ describe('datepicker directive', function () { it('shows years as options', function() { expect(getLabels()).toEqual([]); - expect(getOptions(0)).toEqual(['2001', '2002', '2003', '2004', '2005']); - expect(getOptions(1)).toEqual(['2006', '2007', '2008', '2009', '2010']); - expect(getOptions(2)).toEqual(['2011', '2012', '2013', '2014', '2015']); - expect(getOptions(3)).toEqual(['2016', '2017', '2018', '2019', '2020']); + expect(getOptions()).toEqual([ + ['2001', '2002', '2003', '2004', '2005'], + ['2006', '2007', '2008', '2009', '2010'], + ['2011', '2012', '2013', '2014', '2015'], + ['2016', '2017', '2018', '2019', '2020'] + ]); }); it('does not change the model', function() { @@ -339,11 +403,7 @@ describe('datepicker directive', function () { }); it('has `selected` only the selected year', function() { - for (var i = 0; i < 4; i ++) { - for (var j = 0; j < 5; j ++) { - expect(getOptionsEl(i, j).find('button').hasClass('btn-info')).toBe( ( i === 1 && j === 4) ); - } - } + expectSelectedElement( 1, 4 ); }); it('moves to the previous year set when `previous` button is clicked', function() { @@ -351,16 +411,13 @@ describe('datepicker directive', function () { expect(getTitle()).toBe('1981 - 2000'); expect(getLabels()).toEqual([]); - expect(getOptions(0)).toEqual(['1981', '1982', '1983', '1984', '1985']); - expect(getOptions(1)).toEqual(['1986', '1987', '1988', '1989', '1990']); - expect(getOptions(2)).toEqual(['1991', '1992', '1993', '1994', '1995']); - expect(getOptions(3)).toEqual(['1996', '1997', '1998', '1999', '2000']); - - for (var i = 0; i < 4; i ++) { - for (var j = 0; j < 5; j ++) { - expect(getOptionsEl(i, j).find('button').hasClass('btn-info')).toBe( false ); - } - } + expect(getOptions()).toEqual([ + ['1981', '1982', '1983', '1984', '1985'], + ['1986', '1987', '1988', '1989', '1990'], + ['1991', '1992', '1993', '1994', '1995'], + ['1996', '1997', '1998', '1999', '2000'] + ]); + expectSelectedElement( null, null ); }); it('moves to the next year set when `next` button is clicked', function() { @@ -368,22 +425,21 @@ describe('datepicker directive', function () { expect(getTitle()).toBe('2021 - 2040'); expect(getLabels()).toEqual([]); - expect(getOptions(0)).toEqual(['2021', '2022', '2023', '2024', '2025']); - expect(getOptions(1)).toEqual(['2026', '2027', '2028', '2029', '2030']); - expect(getOptions(2)).toEqual(['2031', '2032', '2033', '2034', '2035']); - expect(getOptions(3)).toEqual(['2036', '2037', '2038', '2039', '2040']); - - for (var i = 0; i < 4; i ++) { - for (var j = 0; j < 5; j ++) { - expect(getOptionsEl(i, j).find('button').hasClass('btn-info')).toBe( false ); - } - } + expect(getOptions()).toEqual([ + ['2021', '2022', '2023', '2024', '2025'], + ['2026', '2027', '2028', '2029', '2030'], + ['2031', '2032', '2033', '2034', '2035'], + ['2036', '2037', '2038', '2039', '2040'] + ]); + + expectSelectedElement( null, null ); }); }); describe('attribute `starting-day`', function () { beforeEach(function() { - element = $compile('')($rootScope); + $rootScope.startingDay = 1; + element = $compile('')($rootScope); $rootScope.$digest(); }); @@ -392,11 +448,13 @@ describe('datepicker directive', function () { }); it('renders the calendar days correctly', function() { - expect(getOptions(0)).toEqual(['30', '31', '01', '02', '03', '04', '05']); - expect(getOptions(1)).toEqual(['06', '07', '08', '09', '10', '11', '12']); - expect(getOptions(2)).toEqual(['13', '14', '15', '16', '17', '18', '19']); - expect(getOptions(3)).toEqual(['20', '21', '22', '23', '24', '25', '26']); - expect(getOptions(4)).toEqual(['27', '28', '29', '30', '01', '02', '03']); + expect(getOptions()).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'] + ]); }); it('renders the week numbers correctly', function() { @@ -408,7 +466,7 @@ describe('datepicker directive', function () { var weekHeader, weekElement; beforeEach(function() { $rootScope.showWeeks = false; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); weekHeader = getLabelsRow().find('th').eq(0); @@ -439,14 +497,14 @@ describe('datepicker directive', function () { describe('min attribute', function () { beforeEach(function() { $rootScope.mindate = new Date("September 12, 2010"); - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); }); it('disables appropriate days in current month', function() { for (var i = 0; i < 5; i ++) { for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( (i < 2) ); + expect(isDisabledOption(i, j)).toBe( (i < 2) ); } } }); @@ -456,16 +514,24 @@ describe('datepicker directive', function () { $rootScope.$digest(); for (var i = 0; i < 5; i ++) { for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( (i < 1) ); + expect(isDisabledOption(i, j)).toBe( (i < 1) ); } } }); + it('invalidates when model is a disabled date', function() { + $rootScope.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(); for (var i = 0; i < 5; i ++) { for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( true ); + expect(isDisabledOption(i, j)).toBe( true ); } } }); @@ -474,7 +540,7 @@ describe('datepicker directive', function () { clickNextButton(); for (var i = 0; i < 5; i ++) { for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( false ); + expect(isDisabledOption(i, j)).toBe( false ); } } }); @@ -483,7 +549,7 @@ describe('datepicker directive', function () { clickTitleButton(); for (var i = 0; i < 4; i ++) { for (var j = 0; j < 3; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( (i < 2 || (i === 2 && j < 2)) ); + expect(isDisabledOption(i, j)).toBe( (i < 2 || (i === 2 && j < 2)) ); } } }); @@ -493,7 +559,7 @@ describe('datepicker directive', function () { clickPreviousButton(); for (var i = 0; i < 4; i ++) { for (var j = 0; j < 3; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( true ); + expect(isDisabledOption(i, j)).toBe( true ); } } }); @@ -503,7 +569,7 @@ describe('datepicker directive', function () { clickNextButton(); for (var i = 0; i < 4; i ++) { for (var j = 0; j < 3; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( false ); + expect(isDisabledOption(i, j)).toBe( false ); } } }); @@ -516,7 +582,7 @@ describe('datepicker directive', function () { clickTitleButton(); for (var i = 0; i < 4; i ++) { for (var j = 0; j < 3; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( false ); + expect(isDisabledOption(i, j)).toBe( false ); } } }); @@ -526,14 +592,14 @@ describe('datepicker directive', function () { describe('max attribute', function () { beforeEach(function() { $rootScope.maxdate = new Date("September 25, 2010"); - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); }); it('disables appropriate days in current month', function() { for (var i = 0; i < 5; i ++) { for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( (i === 4) ); + expect(isDisabledOption(i, j)).toBe( (i === 4) ); } } }); @@ -543,16 +609,23 @@ describe('datepicker directive', function () { $rootScope.$digest(); for (var i = 0; i < 5; i ++) { for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( (i > 2) ); + expect(isDisabledOption(i, j)).toBe( (i > 2) ); } } }); + it('invalidates when model is a disabled date', function() { + $rootScope.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(); for (var i = 0; i < 5; i ++) { for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( false ); + expect(isDisabledOption(i, j)).toBe( false ); } } }); @@ -561,7 +634,7 @@ describe('datepicker directive', function () { clickNextButton(); for (var i = 0; i < 5; i ++) { for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( true ); + expect(isDisabledOption(i, j)).toBe( true ); } } }); @@ -570,7 +643,7 @@ describe('datepicker directive', function () { clickTitleButton(); for (var i = 0; i < 4; i ++) { for (var j = 0; j < 3; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( (i > 2 || (i === 2 && j > 2)) ); + expect(isDisabledOption(i, j)).toBe( (i > 2 || (i === 2 && j > 2)) ); } } }); @@ -580,7 +653,7 @@ describe('datepicker directive', function () { clickPreviousButton(); for (var i = 0; i < 4; i ++) { for (var j = 0; j < 3; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( false ); + expect(isDisabledOption(i, j)).toBe( false ); } } }); @@ -590,7 +663,7 @@ describe('datepicker directive', function () { clickNextButton(); for (var i = 0; i < 4; i ++) { for (var j = 0; j < 3; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( true ); + expect(isDisabledOption(i, j)).toBe( true ); } } }); @@ -600,7 +673,7 @@ describe('datepicker directive', function () { $rootScope.$digest(); for (var i = 0; i < 5; i ++) { for (var j = 0; j < 7; j ++) { - expect(getOptionsEl(i, j).find('button').prop('disabled')).toBe( false ); + expect(isDisabledOption(i, j)).toBe( false ); } } }); @@ -609,28 +682,31 @@ describe('datepicker directive', function () { describe('date-disabled expression', function () { beforeEach(function() { $rootScope.dateDisabledHandler = jasmine.createSpy('dateDisabledHandler'); - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); }); - it('executes the dateDisabled expression for each visible date', function() { - expect($rootScope.dateDisabledHandler.calls.length).toEqual(35); + it('executes the dateDisabled expression for each visible day plus one for validation', function() { + expect($rootScope.dateDisabledHandler.calls.length).toEqual(35 + 1); }); - it('executes the dateDisabled expression for each visible date & each month when mode changes', function() { + it('executes the dateDisabled expression for each visible month plus one for validation', function() { + $rootScope.dateDisabledHandler.reset(); clickTitleButton(); - expect($rootScope.dateDisabledHandler.calls.length).toEqual(35 + 12); + expect($rootScope.dateDisabledHandler.calls.length).toEqual(12 + 1); }); - it('executes the dateDisabled expression for each visible date, month & year when mode changes', function() { - clickTitleButton(2); - expect($rootScope.dateDisabledHandler.calls.length).toEqual(35 + 12 + 20); + it('executes the dateDisabled expression for each visible year plus one for validation', function() { + clickTitleButton(); + $rootScope.dateDisabledHandler.reset(); + clickTitleButton(); + expect($rootScope.dateDisabledHandler.calls.length).toEqual(20 + 1); }); }); describe('formatting attributes', function () { beforeEach(function() { - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); }); @@ -642,18 +718,22 @@ describe('datepicker directive', function () { clickTitleButton(); expect(getTitle()).toBe('10'); - expect(getOptions(0)).toEqual(['Jan', 'Feb', 'Mar']); - expect(getOptions(1)).toEqual(['Apr', 'May', 'Jun']); - expect(getOptions(2)).toEqual(['Jul', 'Aug', 'Sep']); - expect(getOptions(3)).toEqual(['Oct', 'Nov', 'Dec']); + 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(2); expect(getTitle()).toBe('01 - 10'); - expect(getOptions(0)).toEqual(['01', '02', '03', '04', '05']); - expect(getOptions(1)).toEqual(['06', '07', '08', '09', '10']); + expect(getOptions()).toEqual([ + ['01', '02', '03', '04', '05'], + ['06', '07', '08', '09', '10'] + ]); }); it('shows day labels', function() { @@ -661,15 +741,19 @@ describe('datepicker directive', function () { }); it('changes the day format', function() { - expect(getOptions(0)).toEqual(['29', '30', '31', '1', '2', '3', '4']); - expect(getOptions(1)).toEqual(['5', '6', '7', '8', '9', '10', '11']); - expect(getOptions(4)).toEqual(['26', '27', '28', '29', '30', '1', '2']); + expect(getOptions()).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'] + ]); }); }); describe('setting datepickerConfig', function() { var originalConfig = {}; - beforeEach(inject(function(_$compile_, _$rootScope_, datepickerConfig) { + beforeEach(inject(function(datepickerConfig) { angular.extend(originalConfig, datepickerConfig); datepickerConfig.startingDay = 6; datepickerConfig.showWeeks = false; @@ -681,7 +765,7 @@ describe('datepicker directive', function () { datepickerConfig.dayTitleFormat = 'MMMM, yy'; datepickerConfig.monthTitleFormat = 'yy'; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); })); afterEach(inject(function(datepickerConfig) { @@ -689,7 +773,7 @@ describe('datepicker directive', function () { angular.extend(datepickerConfig, originalConfig); })); - it('changes the title format in day mode', function() { + it('changes the title format in `day` mode', function() { expect(getTitle()).toBe('September, 10'); }); @@ -697,35 +781,319 @@ describe('datepicker directive', function () { clickTitleButton(); expect(getTitle()).toBe('10'); - expect(getOptions(0)).toEqual(['Jan', 'Feb', 'Mar']); - expect(getOptions(1)).toEqual(['Apr', 'May', 'Jun']); - expect(getOptions(2)).toEqual(['Jul', 'Aug', 'Sep']); - expect(getOptions(3)).toEqual(['Oct', 'Nov', 'Dec']); + 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(2); expect(getTitle()).toBe('01 - 10'); - expect(getOptions(0)).toEqual(['01', '02', '03', '04', '05']); - expect(getOptions(1)).toEqual(['06', '07', '08', '09', '10']); + expect(getOptions()).toEqual([ + ['01', '02', '03', '04', '05'], + ['06', '07', '08', '09', '10'] + ]); }); it('changes the `starting-day` & day headers & format', function() { expect(getLabels()).toEqual(['Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']); - expect(getOptions(0)).toEqual(['28', '29', '30', '31', '1', '2', '3']); - expect(getOptions(1)).toEqual(['4', '5', '6', '7', '8', '9', '10']); - expect(getOptions(4)).toEqual(['25', '26', '27', '28', '29', '30', '1']); + expect(getOptions()).toEqual([ + ['28', '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'] + ]); }); it('changes initial visibility for weeks', function() { expect(getLabelsRow().find('th').eq(0).css('display')).toBe('none'); + var tr = element.find('tbody').find('tr'); for (var i = 0; i < 5; i++) { - expect(element.find('tbody').find('tr').eq(i).find('td').eq(0).css('display')).toBe('none'); + expect(tr.eq(i).find('td').eq(0).css('display')).toBe('none'); } }); }); + + describe('controller', function () { + var ctrl, $attrs; + beforeEach(inject(function($controller) { + $rootScope.dateDisabled = null; + $attrs = {}; + ctrl = $controller('DatepickerController', { $scope: $rootScope, $attrs: $attrs }); + })); + + describe('modes', function() { + var currentMode; + + it('to be an array', function() { + expect(ctrl.modes.length).toBe(3); + }); + + describe('`day`', function() { + beforeEach(inject(function() { + currentMode = ctrl.modes[0]; + })); + + it('has the appropriate name', function() { + expect(currentMode.name).toBe('day'); + }); + + it('returns the correct date objects', function() { + var objs = currentMode.getVisibleDates(new Date('September 1, 2010'), new Date('September 30, 2010')).objects; + expect(objs.length).toBe(35); + expect(objs[1].selected).toBeFalsy(); + expect(objs[32].selected).toBeTruthy(); + }); + + it('can compare two dates', function() { + expect(currentMode.compare(new Date('September 30, 2010'), new Date('September 1, 2010'))).toBeGreaterThan(0); + expect(currentMode.compare(new Date('September 1, 2010'), new Date('September 30, 2010'))).toBeLessThan(0); + expect(currentMode.compare(new Date('September 30, 2010 15:30:00'), new Date('September 30, 2010 20:30:00'))).toBe(0); + }); + }); + + describe('`month`', function() { + beforeEach(inject(function() { + currentMode = ctrl.modes[1]; + })); + + it('has the appropriate name', function() { + expect(currentMode.name).toBe('month'); + }); + + it('returns the correct date objects', function() { + var objs = currentMode.getVisibleDates(new Date('September 1, 2010'), new Date('September 30, 2010')).objects; + expect(objs.length).toBe(12); + expect(objs[1].selected).toBeFalsy(); + expect(objs[8].selected).toBeTruthy(); + }); + + it('can compare two dates', function() { + expect(currentMode.compare(new Date('October 30, 2010'), new Date('September 01, 2010'))).toBeGreaterThan(0); + expect(currentMode.compare(new Date('September 01, 2010'), new Date('October 30, 2010'))).toBeLessThan(0); + expect(currentMode.compare(new Date('September 01, 2010'), new Date('September 30, 2010'))).toBe(0); + }); + }); + + describe('`year`', function() { + beforeEach(inject(function() { + currentMode = ctrl.modes[2]; + })); + + it('has the appropriate name', function() { + expect(currentMode.name).toBe('year'); + }); + + it('returns the correct date objects', function() { + var objs = currentMode.getVisibleDates(new Date('September 1, 2010'), new Date('September 01, 2010')).objects; + expect(objs.length).toBe(20); + expect(objs[1].selected).toBeFalsy(); + expect(objs[9].selected).toBeTruthy(); + }); + + it('can compare two dates', function() { + expect(currentMode.compare(new Date('September 1, 2011'), new Date('October 30, 2010'))).toBeGreaterThan(0); + expect(currentMode.compare(new Date('October 30, 2010'), new Date('September 1, 2011'))).toBeLessThan(0); + expect(currentMode.compare(new Date('November 9, 2010'), new Date('September 30, 2010'))).toBe(0); + }); + }); + }); + + describe('`isDisabled` function', function() { + var date = new Date("September 30, 2010 15:30:00"); + + it('to return false if no limit is set', function() { + expect(ctrl.isDisabled(date, 0)).toBeFalsy(); + }); + + it('to handle correctly the `min` date', function() { + ctrl.minDate = new Date('October 1, 2010'); + expect(ctrl.isDisabled(date, 0)).toBeTruthy(); + expect(ctrl.isDisabled(date)).toBeTruthy(); + + ctrl.minDate = new Date('September 1, 2010'); + expect(ctrl.isDisabled(date, 0)).toBeFalsy(); + }); + + it('to handle correctly the `max` date', function() { + ctrl.maxDate = new Date('October 1, 2010'); + expect(ctrl.isDisabled(date, 0)).toBeFalsy(); + + ctrl.maxDate = new Date('September 1, 2010'); + expect(ctrl.isDisabled(date, 0)).toBeTruthy(); + expect(ctrl.isDisabled(date)).toBeTruthy(); + }); + + it('to handle correctly the scope `dateDisabled` expression', function() { + $rootScope.dateDisabled = function() { + return false; + }; + $rootScope.$digest(); + expect(ctrl.isDisabled(date, 0)).toBeFalsy(); + + $rootScope.dateDisabled = function() { + return true; + }; + $rootScope.$digest(); + expect(ctrl.isDisabled(date, 0)).toBeTruthy(); + }); + }); + }); + + describe('as popup', function () { + var divElement, inputEl, dropdownEl, changeInputValueTo, $document; + + function assignElements(wrapElement) { + inputEl = wrapElement.find('input'); + dropdownEl = wrapElement.find('ul'); + element = dropdownEl.find('table'); + } + + beforeEach(inject(function(_$document_, $sniffer) { + $document = _$document_; + $rootScope.date = new Date("September 30, 2010 15:30:00"); + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + + changeInputValueTo = function (el, value) { + el.val(value); + el.trigger($sniffer.hasEvent('input') ? 'input' : 'change'); + $rootScope.$digest(); + }; + })); + + it('to display the correct value in input', function() { + expect(inputEl.val()).toBe('2010-09-30'); + }); + + it('does not to display datepicker initially', function() { + expect(dropdownEl.css('display')).toBe('none'); + }); + + it('displays datepicker on input focus', function() { + inputEl.focus(); + expect(dropdownEl.css('display')).not.toBe('none'); + }); + + it('renders the calendar correctly', function() { + expect(getLabelsRow().css('display')).not.toBe('none'); + expect(getLabels()).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']); + expect(getOptions()).toEqual([ + ['29', '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'] + ]); + }); + + it('updates the input when a day is clicked', function() { + clickOption(2, 3); + expect(inputEl.val()).toBe('2010-09-15'); + expect($rootScope.date).toEqual(new Date('September 15, 2010 15:30:00')); + }); + + it('updates the input correctly when model changes', function() { + $rootScope.date = new Date("January 10, 1983 10:00:00"); + $rootScope.$digest(); + expect(inputEl.val()).toBe('1983-01-10'); + }); + + it('closes the dropdown when a day is clicked', function() { + inputEl.focus(); + expect(dropdownEl.css('display')).not.toBe('none'); + + clickOption(2, 3); + expect(dropdownEl.css('display')).toBe('none'); + }); + + it('updates the model when input value changes', function() { + changeInputValueTo(inputEl, 'March 5, 1980'); + expect($rootScope.date.getFullYear()).toEqual(1980); + expect($rootScope.date.getMonth()).toEqual(2); + expect($rootScope.date.getDate()).toEqual(5); + }); + + it('closes when click outside of calendar', function() { + $document.find('body').click(); + expect(dropdownEl.css('display')).toBe('none'); + }); + + describe('toggles programatically by `open` attribute', function () { + beforeEach(inject(function() { + $rootScope.open = true; + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + })); + + it('to display initially', function() { + expect(dropdownEl.css('display')).not.toBe('none'); + }); + + it('to close / open from scope variable', function() { + expect(dropdownEl.css('display')).not.toBe('none'); + $rootScope.open = false; + $rootScope.$digest(); + expect(dropdownEl.css('display')).toBe('none'); + + $rootScope.open = true; + $rootScope.$digest(); + expect(dropdownEl.css('display')).not.toBe('none'); + }); + }); + + describe('custom format', function () { + beforeEach(inject(function() { + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + })); + + it('to display the correct value in input', function() { + expect(inputEl.val()).toBe('30-September-2010'); + }); + + it('updates the input when a day is clicked', function() { + clickOption(2, 3); + expect(inputEl.val()).toBe('15-September-2010'); + expect($rootScope.date).toEqual(new Date('September 15, 2010 15:30:00')); + }); + + it('updates the input correctly when model changes', function() { + $rootScope.date = new Date("January 10, 1983 10:00:00"); + $rootScope.$digest(); + expect(inputEl.val()).toBe('10-January-1983'); + }); + }); + + describe('use with ng-required directive', function() { + beforeEach(inject(function() { + $rootScope.date = ''; + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + })); + + it('should be invalid initially', function() { + expect(inputEl.hasClass('ng-invalid')).toBeTruthy(); + }); + it('should be valid if model has been specified', function() { + $rootScope.date = new Date(); + $rootScope.$digest(); + expect(inputEl.hasClass('ng-valid')).toBeTruthy(); + }); + }); + + }); + }); describe('datepicker directive with empty initial state', function () { @@ -736,7 +1104,7 @@ describe('datepicker directive with empty initial state', function () { $compile = _$compile_; $rootScope = _$rootScope_; $rootScope.date = null; - element = $compile('')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); })); diff --git a/template/datepicker/datepicker.html b/template/datepicker/datepicker.html index 708f4f7577..bc54142737 100644 --- a/template/datepicker/datepicker.html +++ b/template/datepicker/datepicker.html @@ -1,4 +1,4 @@ -
+
@@ -14,7 +14,7 @@ diff --git a/template/datepicker/popup.html b/template/datepicker/popup.html new file mode 100644 index 0000000000..77b04a263e --- /dev/null +++ b/template/datepicker/popup.html @@ -0,0 +1,12 @@ + \ No newline at end of file
{{ getWeekNumber(row) }} - +