diff --git a/src/components/datepicker/calendar-theme.scss b/src/components/datepicker/calendar-theme.scss index b2862b05c79..c8da1fff066 100644 --- a/src/components/datepicker/calendar-theme.scss +++ b/src/components/datepicker/calendar-theme.scss @@ -16,6 +16,10 @@ .md-calendar-date.md-calendar-date-today { color: '{{primary-500}}'; // blue-500 + + &.md-calendar-date-disabled { + color: '{{primary-500-0.6}}'; + } } // The CSS class `md-focus` is used instead of real browser focus for accessibility reasons @@ -39,4 +43,8 @@ } } + .md-calendar-date-disabled, + .md-calendar-month-label-disabled { + color: '{{background-400}}'; // grey-400 + } } diff --git a/src/components/datepicker/calendar.js b/src/components/datepicker/calendar.js index cf52f3068b3..6b0249360eb 100644 --- a/src/components/datepicker/calendar.js +++ b/src/components/datepicker/calendar.js @@ -34,12 +34,19 @@ */ var TBODY_HEIGHT = 265; + /** + * Height of a calendar month with a single row. This is needed to calculate the offset for + * rendering an extra month in virtual-repeat that only contains one row. + */ + var TBODY_SINGLE_ROW_HEIGHT = 45; + function calendarDirective() { return { template: '' + '
' + - '' + + '' + '' + '' + '' + '', - scope: {}, + scope: { + minDate: '=mdMinDate', + maxDate: '=mdMaxDate', + }, require: ['ngModel', 'mdCalendar'], controller: CalendarCtrl, controllerAs: 'ctrl', @@ -87,6 +97,15 @@ */ this.items = {length: 2000}; + if (this.maxDate && this.minDate) { + // Limit the number of months if min and max dates are set. + var numMonths = $$mdDateUtil.getMonthDistance(this.minDate, this.maxDate) + 1; + numMonths = Math.max(numMonths, 1); + // Add an additional month as the final dummy month for rendering purposes. + numMonths += 1; + this.items.length = numMonths; + } + /** @final {!angular.$animate} */ this.$animate = $animate; @@ -123,9 +142,19 @@ /** @final {Date} */ this.today = this.dateUtil.createDateAtMidnight(); - // Set the first renderable date once for all calendar instances. - firstRenderableDate = - firstRenderableDate || this.dateUtil.incrementMonths(this.today, -this.items.length / 2); + /** @type {Date} */ + this.firstRenderableDate = this.dateUtil.incrementMonths(this.today, -this.items.length / 2); + + if (this.minDate && this.minDate > this.firstRenderableDate) { + this.firstRenderableDate = this.minDate; + } else if (this.maxDate) { + // Calculate the difference between the start date and max date. + // Subtract 1 because it's an inclusive difference and 1 for the final dummy month. + // + var monthDifference = this.items.length - 2; + this.firstRenderableDate = this.dateUtil.incrementMonths(this.maxDate, -(this.items.length - 2)); + } + /** @final {number} Unique ID for this calendar instance. */ this.id = nextUniqueId++; @@ -279,6 +308,7 @@ // Selection isn't occuring, so the key event is either navigation or nothing. var date = self.getFocusDateFromKeyEvent(event); if (date) { + date = self.boundDateByMinAndMax(date); event.preventDefault(); event.stopPropagation(); @@ -324,7 +354,8 @@ * @returns {number} */ CalendarCtrl.prototype.getSelectedMonthIndex = function() { - return this.dateUtil.getMonthDistance(firstRenderableDate, this.selectedDate || this.today); + return this.dateUtil.getMonthDistance(this.firstRenderableDate, + this.selectedDate || this.today); }; /** @@ -336,7 +367,7 @@ return; } - var monthDistance = this.dateUtil.getMonthDistance(firstRenderableDate, date); + var monthDistance = this.dateUtil.getMonthDistance(this.firstRenderableDate, date); this.calendarScroller.scrollTop = monthDistance * TBODY_HEIGHT; }; @@ -372,6 +403,23 @@ } }; + /** + * If a date exceeds minDate or maxDate, returns date matching minDate or maxDate, respectively. + * Otherwise, returns the date. + * @param {Date} date + * @return {Date} + */ + CalendarCtrl.prototype.boundDateByMinAndMax = function(date) { + var boundDate = date; + if (this.minDate && date < this.minDate) { + boundDate = new Date(this.minDate.getTime()); + } + if (this.maxDate && date > this.maxDate) { + boundDate = new Date(this.maxDate.getTime()); + } + return boundDate; + }; + /*** Updating the displayed / selected date ***/ /** diff --git a/src/components/datepicker/calendar.scss b/src/components/datepicker/calendar.scss index a2f5f435278..0238f02d0d6 100644 --- a/src/components/datepicker/calendar.scss +++ b/src/components/datepicker/calendar.scss @@ -12,7 +12,6 @@ $md-calendar-width: (7 * $md-calendar-cell-size) + (2 * $md-calendar-side-paddin $md-calendar-height: ($md-calendar-weeks-to-show * $md-calendar-cell-size) + $md-calendar-header-height; - // Styles for date cells, including day-of-the-week header cells. @mixin md-calendar-cell() { height: $md-calendar-cell-size; @@ -88,6 +87,10 @@ md-calendar { // A single date cell in the calendar table. .md-calendar-date { @include md-calendar-cell(); + + &.md-calendar-date-disabled { + cursor: default; + } } // Circle element inside of every date cell used to indicate selection or focus. @@ -97,11 +100,13 @@ md-calendar { border-radius: 50%; display: inline-block; - cursor: pointer; - width: $md-calendar-cell-emphasis-size; height: $md-calendar-cell-emphasis-size; line-height: $md-calendar-cell-emphasis-size; + + .md-calendar-date:not(.md-disabled) & { + cursor: pointer; + } } // The label above each month (containing the month name and the year, e.g. "Jun 2014"). diff --git a/src/components/datepicker/calendar.spec.js b/src/components/datepicker/calendar.spec.js index 107bd2e8fe2..e1ef2aaa57d 100644 --- a/src/components/datepicker/calendar.spec.js +++ b/src/components/datepicker/calendar.spec.js @@ -50,6 +50,23 @@ describe('md-calendar', function() { } } + /** + * Finds a month `tbody` in the calendar element given a date. + */ + function findMonthElement(date) { + var months = element.querySelectorAll('[md-calendar-month]'); + var monthHeader = dateLocale.monthHeaderFormatter(date); + var month; + + for (var i = 0; i < months.length; i++) { + month = months[i]; + if (month.querySelector('tr:first-child td:first-child').textContent === monthHeader) { + return month; + } + } + return null; + } + /** * Gets the month label for a given date cell. * @param {HTMLElement|DocumentView} cell @@ -63,7 +80,8 @@ describe('md-calendar', function() { /** Creates and compiles an md-calendar element. */ function createElement(parentScope) { var directiveScope = parentScope || $rootScope.$new(); - var template = ''; + var template = ''; var attachedElement = angular.element(template); document.body.appendChild(attachedElement[0]); var newElement = $compile(attachedElement)(directiveScope); @@ -135,7 +153,7 @@ describe('md-calendar', function() { ngElement = createElement(pageScope); element = ngElement[0]; - scope = ngElement.scope(); + scope = ngElement.isolateScope(); controller = ngElement.controller('mdCalendar'); })); @@ -227,6 +245,40 @@ describe('md-calendar', function() { var monthHeader = monthElement.querySelector('tr'); expect(monthHeader.textContent).toEqual('Junz 2014'); }); + + it('should update the model on cell click', function() { + spyOn(scope, '$emit'); + var date = new Date(2014, MAY, 30); + var monthElement = monthCtrl.buildCalendarForMonth(date); + var expectedDate = new Date(2014, MAY, 5); + findDateElement(monthElement, 5).click(); + expect(pageScope.myDate).toBeSameDayAs(expectedDate); + expect(scope.$emit).toHaveBeenCalledWith('md-calendar-change', expectedDate); + }); + + it('should disable any dates outside the min/max date range', function() { + pageScope.minDate = new Date(2014, JUN, 10); + pageScope.maxDate = new Date(2014, JUN, 20); + pageScope.$apply(); + + var monthElement = monthCtrl.buildCalendarForMonth(new Date(2014, JUN, 15)); + expect(findDateElement(monthElement, 5)).toHaveClass('md-calendar-date-disabled'); + expect(findDateElement(monthElement, 10)).not.toHaveClass('md-calendar-date-disabled'); + expect(findDateElement(monthElement, 20)).not.toHaveClass('md-calendar-date-disabled'); + expect(findDateElement(monthElement, 25)).toHaveClass('md-calendar-date-disabled'); + }); + + it('should not respond to disabled cell clicks', function() { + var initialDate = new Date(2014, JUN, 15); + pageScope.myDate = initialDate; + pageScope.minDate = new Date(2014, JUN, 10); + pageScope.maxDate = new Date(2014, JUN, 20); + pageScope.$apply(); + + var monthElement = monthCtrl.buildCalendarForMonth(pageScope.myDate); + findDateElement(monthElement, 5).click(); + expect(pageScope.myDate).toBeSameDayAs(initialDate); + }); }); it('should highlight today', function() { @@ -325,6 +377,41 @@ describe('md-calendar', function() { expect(controller.selectedDate).toBeSameDayAs(new Date(2014, MAR, 1)); }); + it('should restrict date navigation to min/max dates', function() { + pageScope.minDate = new Date(2014, FEB, 5); + pageScope.maxDate = new Date(2014, FEB, 10); + pageScope.myDate = new Date(2014, FEB, 8); + applyDateChange(); + + var selectedDate = element.querySelector('.md-calendar-selected-date'); + selectedDate.focus(); + + dispatchKeyEvent(keyCodes.UP_ARROW); + expect(getFocusedDateElement().textContent).toBe('5'); + expect(getMonthLabelForDateCell(getFocusedDateElement())).toBe('Feb 2014'); + + dispatchKeyEvent(keyCodes.LEFT_ARROW); + expect(getFocusedDateElement().textContent).toBe('5'); + expect(getMonthLabelForDateCell(getFocusedDateElement())).toBe('Feb 2014'); + + dispatchKeyEvent(keyCodes.DOWN_ARROW); + expect(getFocusedDateElement().textContent).toBe('10'); + expect(getMonthLabelForDateCell(getFocusedDateElement())).toBe('Feb 2014'); + + dispatchKeyEvent(keyCodes.RIGHT_ARROW); + expect(getFocusedDateElement().textContent).toBe('10'); + expect(getMonthLabelForDateCell(getFocusedDateElement())).toBe('Feb 2014'); + + dispatchKeyEvent(keyCodes.UP_ARROW, {meta: true}); + expect(getFocusedDateElement().textContent).toBe('5'); + expect(getMonthLabelForDateCell(getFocusedDateElement())).toBe('Feb 2014'); + + dispatchKeyEvent(keyCodes.DOWN_ARROW, {meta: true}); + expect(getFocusedDateElement().textContent).toBe('10'); + expect(getMonthLabelForDateCell(getFocusedDateElement())).toBe('Feb 2014'); + + }); + it('should fire an event when escape is pressed', function() { var escapeHandler = jasmine.createSpy('escapeHandler'); pageScope.$on('md-calendar-close', escapeHandler); @@ -354,4 +441,44 @@ describe('md-calendar', function() { controller.changeDisplayDate(laterDate); expect(controller.displayDate).toBeSameDayAs(laterDate); }); + + it('should not render any months before the min date', function() { + ngElement.remove(); + var newScope = $rootScope.$new(); + newScope.minDate = new Date(2014, JUN, 5); + newScope.myDate = new Date(2014, JUN, 15); + newScope.$apply(); + element = createElement(newScope)[0]; + + expect(findMonthElement(new Date(2014, JUL, 1))).not.toBeNull(); + expect(findMonthElement(new Date(2014, JUN, 1))).not.toBeNull(); + expect(findMonthElement(new Date(2014, MAY, 1))).toBeNull(); + }); + + it('should render one single-row month of disabled cells after the max date', function() { + ngElement.remove(); + var newScope = $rootScope.$new(); + newScope.myDate = new Date(2014, APR, 15); + newScope.maxDate = new Date(2014, APR, 30); + newScope.$apply(); + element = createElement(newScope)[0]; + + expect(findMonthElement(new Date(2014, MAR, 1))).not.toBeNull(); + expect(findMonthElement(new Date(2014, APR, 1))).not.toBeNull(); + + // First date of May 2014 on Thursday (i.e. has 3 dates on the first row). + var nextMonth = findMonthElement(new Date(2014, MAY, 1)); + expect(nextMonth).not.toBeNull(); + expect(nextMonth.querySelector('.md-calendar-month-label')).toHaveClass( + 'md-calendar-month-label-disabled'); + expect(nextMonth.querySelectorAll('tr').length).toBe(1); + + var dates = nextMonth.querySelectorAll('.md-calendar-date'); + for (var i = 0; i < dates.length; i++) { + date = dates[i]; + if (date.textContent) { + expect(date).toHaveClass('md-calendar-date-disabled'); + } + } + }); }); diff --git a/src/components/datepicker/calendarMonth.js b/src/components/datepicker/calendarMonth.js index 9e506045c4e..723eabdc5df 100644 --- a/src/components/datepicker/calendarMonth.js +++ b/src/components/datepicker/calendarMonth.js @@ -74,8 +74,7 @@ /** Generate and append the content for this month to the directive element. */ CalendarMonthCtrl.prototype.generateContent = function() { var calendarCtrl = this.calendarCtrl; - var offset = (-calendarCtrl.items.length / 2) + this.offset; - var date = this.dateUtil.incrementMonths(calendarCtrl.today, offset); + var date = this.dateUtil.incrementMonths(calendarCtrl.firstRenderableDate, this.offset); this.$element.empty(); this.$element.append(this.buildCalendarForMonth(date)); @@ -104,16 +103,9 @@ cell.setAttribute('role', 'gridcell'); if (opt_date) { - // Add a indicator for select, hover, and focus states. - var selectionIndicator = document.createElement('span'); - cell.appendChild(selectionIndicator); - selectionIndicator.classList.add('md-calendar-date-selection-indicator'); - selectionIndicator.textContent = this.dateLocale.dates[opt_date.getDate()]; - cell.setAttribute('tabindex', '-1'); cell.setAttribute('aria-label', this.dateLocale.longDateFormatter(opt_date)); cell.id = calendarCtrl.getDateId(opt_date); - cell.addEventListener('click', calendarCtrl.cellClickHandler); // Use `data-timestamp` attribute because IE10 does not support the `dataset` property. cell.setAttribute('data-timestamp', opt_date.getTime()); @@ -130,8 +122,24 @@ cell.setAttribute('aria-selected', 'true'); } - if (calendarCtrl.focusDate && this.dateUtil.isSameDay(opt_date, calendarCtrl.focusDate)) { - this.focusAfterAppend = cell; + var cellText = this.dateLocale.dates[opt_date.getDate()]; + + if (this.dateUtil.isDateWithinRange(opt_date, + this.calendarCtrl.minDate, this.calendarCtrl.maxDate)) { + // Add a indicator for select, hover, and focus states. + var selectionIndicator = document.createElement('span'); + cell.appendChild(selectionIndicator); + selectionIndicator.classList.add('md-calendar-date-selection-indicator'); + selectionIndicator.textContent = cellText; + + cell.addEventListener('click', calendarCtrl.cellClickHandler); + + if (calendarCtrl.focusDate && this.dateUtil.isSameDay(opt_date, calendarCtrl.focusDate)) { + this.focusAfterAppend = cell; + } + } else { + cell.classList.add('md-calendar-date-disabled'); + cell.textContent = cellText; } } @@ -174,26 +182,38 @@ var row = this.buildDateRow(rowNumber); monthBody.appendChild(row); + // If this is the final month in the list of items, only the first week should render, + // so we should return immediately after the first row is complete and has been + // attached to the body. + var isFinalMonth = this.offset === this.calendarCtrl.items.length - 1; + // Add a label for the month. If the month starts on a Sun/Mon/Tues, the month label // goes on a row above the first of the month. Otherwise, the month label takes up the first // two cells of the first row. var blankCellOffset = 0; var monthLabelCell = document.createElement('td'); monthLabelCell.classList.add('md-calendar-month-label'); + // If the entire month is after the max date, render the label as a disabled state. + if (this.calendarCtrl.maxDate && firstDayOfMonth > this.calendarCtrl.maxDate) { + monthLabelCell.classList.add('md-calendar-month-label-disabled'); + } + monthLabelCell.textContent = this.dateLocale.monthHeaderFormatter(date); if (firstDayOfTheWeek <= 2) { monthLabelCell.setAttribute('colspan', '7'); var monthLabelRow = this.buildDateRow(); monthLabelRow.appendChild(monthLabelCell); monthBody.insertBefore(monthLabelRow, row); + + if (isFinalMonth) { + return monthBody; + } } else { blankCellOffset = 2; monthLabelCell.setAttribute('colspan', '2'); row.appendChild(monthLabelCell); } - monthLabelCell.textContent = this.dateLocale.monthHeaderFormatter(date); - // Add a blank cell for each day of the week that occurs before the first of the month. // For example, if the first day of the month is a Tuesday, add blank cells for Sun and Mon. // The blankCellOffset is needed in cases where the first N cells are used by the month label. @@ -208,6 +228,10 @@ for (var d = 1; d <= numberOfDaysInMonth; d++) { // If we've reached the end of the week, start a new row. if (dayOfWeek === 7) { + // We've finished the first row, so we're done if this is the final month. + if (isFinalMonth) { + return monthBody; + } dayOfWeek = 0; rowNumber++; row = this.buildDateRow(rowNumber); diff --git a/src/components/datepicker/datePicker.js b/src/components/datepicker/datePicker.js index 249a7e51c0a..87d1f84bdd3 100644 --- a/src/components/datepicker/datePicker.js +++ b/src/components/datepicker/datePicker.js @@ -22,6 +22,8 @@ * * @param {Date} ng-model The component's model. Expects a JavaScript Date object. * @param {expression=} ng-change Expression evaluated when the model value changes. + * @param {expression=} md-min-date Expression representing a min date (inclusive). + * @param {expression=} md-max-date Expression representing a max date (inclusive). * @param {boolean=} disabled Whether the datepicker is disabled. * * @description @@ -65,11 +67,15 @@ '' + '
' + '' + + 'md-min-date="ctrl.minDate" md-max-date="ctrl.maxDate"' + + 'ng-model="ctrl.date" ng-if="ctrl.isCalendarOpen">' + + '' + '
' + '', require: ['ngModel', 'mdDatepicker'], scope: { + minDate: '=mdMinDate', + maxDate: '=mdMaxDate', placeholder: '@mdPlaceholder' }, controller: DatePickerCtrl, @@ -127,6 +133,9 @@ /** @type {HTMLInputElement} */ this.inputElement = $element[0].querySelector('input'); + /** @final {!angular.JQLite} */ + this.ngInputElement = angular.element(this.inputElement); + /** @type {HTMLElement} */ this.inputContainer = $element[0].querySelector('.md-datepicker-input-container'); @@ -227,10 +236,9 @@ self.inputContainer.classList.remove(INVALID_CLASS); }); - var ngElement = angular.element(self.inputElement); - ngElement.on('input', angular.bind(self, self.resizeInputElement)); + self.ngInputElement.on('input', angular.bind(self, self.resizeInputElement)); // TODO(chenmike): Add ability for users to specify this interval. - ngElement.on('input', self.$mdUtil.debounce(self.handleInputEvent, + self.ngInputElement.on('input', self.$mdUtil.debounce(self.handleInputEvent, DEFAULT_DEBOUNCE_INTERVAL, self)); }; @@ -241,7 +249,7 @@ var keyCodes = this.$mdConstant.KEY_CODE; // Add event listener through angular so that we can triggerHandler in unit tests. - angular.element(self.inputElement).on('keydown', function(event) { + self.ngInputElement.on('keydown', function(event) { if (event.altKey && event.keyCode == keyCodes.DOWN_ARROW) { self.openCalendarPane(event); $scope.$digest(); @@ -298,8 +306,11 @@ DatePickerCtrl.prototype.handleInputEvent = function() { var inputString = this.inputElement.value; var parsedDate = this.dateLocale.parseDate(inputString); + this.dateUtil.setDateTimeToMidnight(parsedDate); - if (this.dateUtil.isValidDate(parsedDate) && this.dateLocale.isDateComplete(inputString)) { + if (this.dateUtil.isValidDate(parsedDate) && + this.dateLocale.isDateComplete(inputString) && + this.dateUtil.isDateWithinRange(parsedDate, this.minDate, this.maxDate)) { this.ngModelCtrl.$setViewValue(parsedDate); this.date = parsedDate; this.inputContainer.classList.remove(INVALID_CLASS); diff --git a/src/components/datepicker/datePicker.spec.js b/src/components/datepicker/datePicker.spec.js index 557e9a4fe05..2128d2af102 100644 --- a/src/components/datepicker/datePicker.spec.js +++ b/src/components/datepicker/datePicker.spec.js @@ -24,7 +24,12 @@ describe('md-date-picker', function() { pageScope.myDate = initialDate; pageScope.isDisabled = false; - var template = ''; + var template = '' + + ''; ngElement = $compile(template)(pageScope); $rootScope.$apply(); @@ -33,6 +38,14 @@ describe('md-date-picker', function() { element = ngElement[0]; })); + /** + * Populates the inputElement with a value and triggers the input events. + */ + function populateInputElement(inputString) { + controller.ngInputElement.val(inputString).triggerHandler('input'); + $timeout.flush(); + } + it('should set initial value from ng-model', function() { expect(controller.inputElement.value).toBe(dateLocale.formatDate(initialDate)); }); @@ -53,7 +66,7 @@ describe('md-date-picker', function() { }); it('should open and close the floating calendar pane element via keyboard', function() { - angular.element(controller.inputElement).triggerHandler({ + controller.ngInputElement.triggerHandler({ type: 'keydown', altKey: true, keyCode: keyCodes.DOWN_ARROW @@ -98,28 +111,35 @@ describe('md-date-picker', function() { describe('input event', function() { it('should update the model value when user enters a valid date', function() { var expectedDate = new Date(2015, JUN, 1); - controller.inputElement.value = '6/1/2015'; - angular.element(controller.inputElement).triggerHandler('input'); - $timeout.flush(); + populateInputElement('6/1/2015'); expect(controller.ngModelCtrl.$modelValue).toEqual(expectedDate); }); it('should not update the model value when user enters an invalid date', function() { - controller.inputElement.value = '7'; - angular.element(controller.inputElement).triggerHandler('input'); - $timeout.flush(); + populateInputElement('7'); expect(controller.ngModelCtrl.$modelValue).toEqual(initialDate); }); + it('should not update the model value when input is outside min/max bounds', function() { + pageScope.minDate = new Date(2014, JUN, 1); + pageScope.maxDate = new Date(2014, JUN, 3); + pageScope.$apply(); + + populateInputElement('5/30/2014'); + expect(controller.ngModelCtrl.$modelValue).toEqual(initialDate); + + populateInputElement('6/4/2014'); + expect(controller.ngModelCtrl.$modelValue).toEqual(initialDate); + + populateInputElement('6/2/2014'); + expect(controller.ngModelCtrl.$modelValue).toEqual(new Date(2014, JUN, 2)); + }); + it('should add and remove the invalid class', function() { - controller.inputElement.value = '6/1/2015'; - angular.element(controller.inputElement).triggerHandler('input'); - $timeout.flush(); + populateInputElement('6/1/2015'); expect(controller.inputContainer).not.toHaveClass('md-datepicker-invalid'); - controller.inputElement.value = '7'; - angular.element(controller.inputElement).triggerHandler('input'); - $timeout.flush(); + populateInputElement('7'); expect(controller.inputContainer).toHaveClass('md-datepicker-invalid'); }); }); @@ -152,9 +172,7 @@ describe('md-date-picker', function() { }); it('should remove the invalid state if present', function() { - controller.inputElement.value = '7'; - angular.element(controller.inputElement).triggerHandler('input'); - $timeout.flush(); + populateInputElement('7'); expect(controller.inputContainer).toHaveClass('md-datepicker-invalid'); controller.openCalendarPane({ diff --git a/src/components/datepicker/dateUtil.js b/src/components/datepicker/dateUtil.js index 97a2451cc6c..e97b24edc2f 100644 --- a/src/components/datepicker/dateUtil.js +++ b/src/components/datepicker/dateUtil.js @@ -22,7 +22,9 @@ isSameDay: isSameDay, getMonthDistance: getMonthDistance, isValidDate: isValidDate, - createDateAtMidnight: createDateAtMidnight + setDateTimeToMidnight: setDateTimeToMidnight, + createDateAtMidnight: createDateAtMidnight, + isDateWithinRange: isDateWithinRange }; /** @@ -188,6 +190,14 @@ return date != null && date.getTime && !isNaN(date.getTime()); } + /** + * Sets a date's time to midnight. + * @param {Date} date + */ + function setDateTimeToMidnight(date) { + date.setHours(0, 0, 0, 0); + } + /** * Creates a date with the time set to midnight. * Drop-in replacement for two forms of the Date constructor: @@ -203,8 +213,20 @@ } else { date = new Date(opt_value); } - date.setHours(0, 0, 0, 0); + setDateTimeToMidnight(date); return date; } + + /** + * Checks if a date is within a min and max range. + * If minDate or maxDate are not dates, they are ignored. + * @param {Date} date + * @param {Date} minDate + * @param {Date} maxDate + */ + function isDateWithinRange(date, minDate, maxDate) { + return (!angular.isDate(minDate) || minDate <= date) && + (!angular.isDate(maxDate) || maxDate >= date); + } }); })(); diff --git a/src/components/datepicker/demoBasicUsage/index.html b/src/components/datepicker/demoBasicUsage/index.html index f168923d96c..588ce5110a6 100644 --- a/src/components/datepicker/demoBasicUsage/index.html +++ b/src/components/datepicker/demoBasicUsage/index.html @@ -7,5 +7,8 @@

Standard date-picker

Disabled date-picker

+

Date-picker with min date and max date

+ diff --git a/src/components/datepicker/demoBasicUsage/script.js b/src/components/datepicker/demoBasicUsage/script.js index b2143d230c4..2881b5c9dd2 100644 --- a/src/components/datepicker/demoBasicUsage/script.js +++ b/src/components/datepicker/demoBasicUsage/script.js @@ -2,10 +2,13 @@ angular.module('datepickerBasicUsage', ['ngMaterial']) .controller('AppCtrl', function($scope) { $scope.myDate = new Date(); - $scope.adjustMonth = function(delta) { - $scope.myDate = new Date( - $scope.myDate.getFullYear(), - $scope.myDate.getMonth() + delta, - $scope.myDate.getDate()); - }; + $scope.minDate = new Date( + $scope.myDate.getFullYear(), + $scope.myDate.getMonth() - 2, + $scope.myDate.getDate()); + + $scope.maxDate = new Date( + $scope.myDate.getFullYear(), + $scope.myDate.getMonth() + 2, + $scope.myDate.getDate()); }); diff --git a/src/components/virtualRepeat/virtual-repeater.js b/src/components/virtualRepeat/virtual-repeater.js index 8f79d38eb4d..4d5888f0244 100644 --- a/src/components/virtualRepeat/virtual-repeater.js +++ b/src/components/virtualRepeat/virtual-repeater.js @@ -97,6 +97,9 @@ function VirtualRepeatContainerController($$rAF, $scope, $element, $attrs) { this.autoShrinkMin = parseInt(this.$attrs.mdAutoShrinkMin, 10) || 0; /** @type {?number} Original container size when shrank */ this.originalSize = null; + /** @type {number} Amount to offset the total scroll size by. */ + this.offsetSize = parseInt(this.$attrs.mdOffsetSize, 10) || 0; + this.scroller = $element[0].getElementsByClassName('md-virtual-repeat-scroller')[0]; this.sizer = this.scroller.getElementsByClassName('md-virtual-repeat-sizer')[0]; @@ -229,9 +232,10 @@ VirtualRepeatContainerController.prototype.autoShrink_ = function(size) { /** * Sets the scrollHeight or scrollWidth. Called by the repeater based on * its item count and item size. - * @param {number} size The new size. + * @param {number} itemsSize The total size of the items. */ -VirtualRepeatContainerController.prototype.setScrollSize = function(size) { +VirtualRepeatContainerController.prototype.setScrollSize = function(itemsSize) { + var size = itemsSize + this.offsetSize; if (this.scrollSize === size) return; this.sizeScroller_(size);