UNPKG

angular-material-npfixed

Version:

The Angular Material project is an implementation of Material Design in Angular.js. This project provides a set of reusable, well-tested, and accessible Material Design UI components. Angular Material is supported internally at Google by the Angular.js, M

301 lines (253 loc) 11.4 kB
(function() { 'use strict'; angular.module('material.components.datepicker') .directive('mdCalendarMonthBody', mdCalendarMonthBodyDirective); /** * Private directive consumed by md-calendar-month. Having this directive lets the calender use * md-virtual-repeat and also cleanly separates the month DOM construction functions from * the rest of the calendar controller logic. * @ngInject */ function mdCalendarMonthBodyDirective($compile, $$mdSvgRegistry) { var ARROW_ICON = $compile('<md-icon md-svg-src="' + $$mdSvgRegistry.mdTabsArrow + '"></md-icon>')({})[0]; return { require: ['^^mdCalendar', '^^mdCalendarMonth', 'mdCalendarMonthBody'], scope: { offset: '=mdMonthOffset' }, controller: CalendarMonthBodyCtrl, controllerAs: 'mdMonthBodyCtrl', bindToController: true, link: function(scope, element, attrs, controllers) { var calendarCtrl = controllers[0]; var monthCtrl = controllers[1]; var monthBodyCtrl = controllers[2]; monthBodyCtrl.calendarCtrl = calendarCtrl; monthBodyCtrl.monthCtrl = monthCtrl; monthBodyCtrl.arrowIcon = ARROW_ICON.cloneNode(true); // The virtual-repeat re-uses the same DOM elements, so there are only a limited number // of repeated items that are linked, and then those elements have their bindings updated. // Since the months are not generated by bindings, we simply regenerate the entire thing // when the binding (offset) changes. scope.$watch(function() { return monthBodyCtrl.offset; }, function(offset) { if (angular.isNumber(offset)) { monthBodyCtrl.generateContent(); } }); } }; } /** * Controller for a single calendar month. * @ngInject @constructor */ function CalendarMonthBodyCtrl($element, $$mdDateUtil, $mdDateLocale) { /** @final {!angular.JQLite} */ this.$element = $element; /** @final */ this.dateUtil = $$mdDateUtil; /** @final */ this.dateLocale = $mdDateLocale; /** @type {Object} Reference to the month view. */ this.monthCtrl = null; /** @type {Object} Reference to the calendar. */ this.calendarCtrl = null; /** * Number of months from the start of the month "items" that the currently rendered month * occurs. Set via angular data binding. * @type {number} */ this.offset = null; /** * Date cell to focus after appending the month to the document. * @type {HTMLElement} */ this.focusAfterAppend = null; } /** Generate and append the content for this month to the directive element. */ CalendarMonthBodyCtrl.prototype.generateContent = function() { var date = this.dateUtil.incrementMonths(this.calendarCtrl.firstRenderableDate, this.offset); this.$element .empty() .append(this.buildCalendarForMonth(date)); if (this.focusAfterAppend) { this.focusAfterAppend.classList.add(this.calendarCtrl.FOCUSED_DATE_CLASS); this.focusAfterAppend.focus(); this.focusAfterAppend = null; } }; /** * Creates a single cell to contain a date in the calendar with all appropriate * attributes and classes added. If a date is given, the cell content will be set * based on the date. * @param {Date=} opt_date * @returns {HTMLElement} */ CalendarMonthBodyCtrl.prototype.buildDateCell = function(opt_date) { var monthCtrl = this.monthCtrl; var calendarCtrl = this.calendarCtrl; // TODO(jelbourn): cloneNode is likely a faster way of doing this. var cell = document.createElement('td'); cell.tabIndex = -1; cell.classList.add('md-calendar-date'); cell.setAttribute('role', 'gridcell'); if (opt_date) { cell.setAttribute('tabindex', '-1'); cell.setAttribute('aria-label', this.dateLocale.longDateFormatter(opt_date)); cell.id = calendarCtrl.getDateId(opt_date, 'month'); // Use `data-timestamp` attribute because IE10 does not support the `dataset` property. cell.setAttribute('data-timestamp', opt_date.getTime()); // TODO(jelourn): Doing these comparisons for class addition during generation might be slow. // It may be better to finish the construction and then query the node and add the class. if (this.dateUtil.isSameDay(opt_date, calendarCtrl.today)) { cell.classList.add(calendarCtrl.TODAY_CLASS); } if (this.dateUtil.isValidDate(calendarCtrl.selectedDate) && this.dateUtil.isSameDay(opt_date, calendarCtrl.selectedDate)) { cell.classList.add(calendarCtrl.SELECTED_DATE_CLASS); cell.setAttribute('aria-selected', 'true'); } var cellText = this.dateLocale.dates[opt_date.getDate()]; if (this.isDateEnabled(opt_date)) { // Add a indicator for select, hover, and focus states. var selectionIndicator = document.createElement('span'); selectionIndicator.classList.add('md-calendar-date-selection-indicator'); selectionIndicator.textContent = cellText; cell.appendChild(selectionIndicator); cell.addEventListener('click', monthCtrl.cellClickHandler); if (calendarCtrl.displayDate && this.dateUtil.isSameDay(opt_date, calendarCtrl.displayDate)) { this.focusAfterAppend = cell; } } else { cell.classList.add('md-calendar-date-disabled'); cell.textContent = cellText; } } return cell; }; /** * Check whether date is in range and enabled * @param {Date=} opt_date * @return {boolean} Whether the date is enabled. */ CalendarMonthBodyCtrl.prototype.isDateEnabled = function(opt_date) { return this.dateUtil.isDateWithinRange(opt_date, this.calendarCtrl.minDate, this.calendarCtrl.maxDate) && (!angular.isFunction(this.calendarCtrl.dateFilter) || this.calendarCtrl.dateFilter(opt_date)); }; /** * Builds a `tr` element for the calendar grid. * @param rowNumber The week number within the month. * @returns {HTMLElement} */ CalendarMonthBodyCtrl.prototype.buildDateRow = function(rowNumber) { var row = document.createElement('tr'); row.setAttribute('role', 'row'); // Because of an NVDA bug (with Firefox), the row needs an aria-label in order // to prevent the entire row being read aloud when the user moves between rows. // See http://community.nvda-project.org/ticket/4643. row.setAttribute('aria-label', this.dateLocale.weekNumberFormatter(rowNumber)); return row; }; /** * Builds the <tbody> content for the given date's month. * @param {Date=} opt_dateInMonth * @returns {DocumentFragment} A document fragment containing the <tr> elements. */ CalendarMonthBodyCtrl.prototype.buildCalendarForMonth = function(opt_dateInMonth) { var date = this.dateUtil.isValidDate(opt_dateInMonth) ? opt_dateInMonth : new Date(); var firstDayOfMonth = this.dateUtil.getFirstDateOfMonth(date); var firstDayOfTheWeek = this.getLocaleDay_(firstDayOfMonth); var numberOfDaysInMonth = this.dateUtil.getNumberOfDaysInMonth(date); // Store rows for the month in a document fragment so that we can append them all at once. var monthBody = document.createDocumentFragment(); var rowNumber = 1; 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.monthCtrl.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'); var monthLabelCellContent = document.createElement('span'); monthLabelCellContent.textContent = this.dateLocale.monthHeaderFormatter(date); monthLabelCell.appendChild(monthLabelCellContent); 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'); } else { monthLabelCell.addEventListener('click', this.monthCtrl.headerClickHandler); monthLabelCell.setAttribute('data-timestamp', firstDayOfMonth.getTime()); monthLabelCell.setAttribute('aria-label', this.dateLocale.monthFormatter(date)); monthLabelCell.appendChild(this.arrowIcon.cloneNode(true)); } if (firstDayOfTheWeek <= 2) { monthLabelCell.setAttribute('colspan', '7'); var monthLabelRow = this.buildDateRow(); monthLabelRow.appendChild(monthLabelCell); monthBody.insertBefore(monthLabelRow, row); if (isFinalMonth) { return monthBody; } } else { blankCellOffset = 3; monthLabelCell.setAttribute('colspan', '3'); row.appendChild(monthLabelCell); } // 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. for (var i = blankCellOffset; i < firstDayOfTheWeek; i++) { row.appendChild(this.buildDateCell()); } // Add a cell for each day of the month, keeping track of the day of the week so that // we know when to start a new row. var dayOfWeek = firstDayOfTheWeek; var iterationDate = firstDayOfMonth; 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); monthBody.appendChild(row); } iterationDate.setDate(d); var cell = this.buildDateCell(iterationDate); row.appendChild(cell); dayOfWeek++; } // Ensure that the last row of the month has 7 cells. while (row.childNodes.length < 7) { row.appendChild(this.buildDateCell()); } // Ensure that all months have 6 rows. This is necessary for now because the virtual-repeat // requires that all items have exactly the same height. while (monthBody.childNodes.length < 6) { var whitespaceRow = this.buildDateRow(); for (var j = 0; j < 7; j++) { whitespaceRow.appendChild(this.buildDateCell()); } monthBody.appendChild(whitespaceRow); } return monthBody; }; /** * Gets the day-of-the-week index for a date for the current locale. * @private * @param {Date} date * @returns {number} The column index of the date in the calendar. */ CalendarMonthBodyCtrl.prototype.getLocaleDay_ = function(date) { return (date.getDay() + (7 - this.dateLocale.firstDayOfWeek)) % 7; }; })();