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

310 lines (258 loc) 10.5 kB
(function() { 'use strict'; angular.module('material.components.datepicker') .directive('mdCalendarMonth', calendarDirective); /** * Height of one calendar month tbody. This must be made known to the virtual-repeat and is * subsequently used for scrolling to specific months. */ 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; /** Private directive that represents a list of months inside the calendar. */ function calendarDirective() { return { template: '<table aria-hidden="true" class="md-calendar-day-header"><thead></thead></table>' + '<div class="md-calendar-scroll-mask">' + '<md-virtual-repeat-container class="md-calendar-scroll-container" ' + 'md-offset-size="' + (TBODY_SINGLE_ROW_HEIGHT - TBODY_HEIGHT) + '">' + '<table role="grid" tabindex="0" class="md-calendar" aria-readonly="true">' + '<tbody ' + 'md-calendar-month-body ' + 'role="rowgroup" ' + 'md-virtual-repeat="i in monthCtrl.items" ' + 'md-month-offset="$index" ' + 'class="md-calendar-month" ' + 'md-start-index="monthCtrl.getSelectedMonthIndex()" ' + 'md-item-size="' + TBODY_HEIGHT + '">' + // The <tr> ensures that the <tbody> will always have the // proper height, even if it's empty. If it's content is // compiled, the <tr> will be overwritten. '<tr aria-hidden="true" style="height:' + TBODY_HEIGHT + 'px;"></tr>' + '</tbody>' + '</table>' + '</md-virtual-repeat-container>' + '</div>', require: ['^^mdCalendar', 'mdCalendarMonth'], controller: CalendarMonthCtrl, controllerAs: 'monthCtrl', bindToController: true, link: function(scope, element, attrs, controllers) { var calendarCtrl = controllers[0]; var monthCtrl = controllers[1]; monthCtrl.initialize(calendarCtrl); } }; } /** * Controller for the calendar month component. * @ngInject @constructor */ function CalendarMonthCtrl($element, $scope, $animate, $q, $$mdDateUtil, $mdDateLocale) { /** @final {!angular.JQLite} */ this.$element = $element; /** @final {!angular.Scope} */ this.$scope = $scope; /** @final {!angular.$animate} */ this.$animate = $animate; /** @final {!angular.$q} */ this.$q = $q; /** @final */ this.dateUtil = $$mdDateUtil; /** @final */ this.dateLocale = $mdDateLocale; /** @final {HTMLElement} */ this.calendarScroller = $element[0].querySelector('.md-virtual-repeat-scroller'); /** @type {boolean} */ this.isInitialized = false; /** @type {boolean} */ this.isMonthTransitionInProgress = false; var self = this; /** * Handles a click event on a date cell. * Created here so that every cell can use the same function instance. * @this {HTMLTableCellElement} The cell that was clicked. */ this.cellClickHandler = function() { var timestamp = $$mdDateUtil.getTimestampFromNode(this); self.$scope.$apply(function() { self.calendarCtrl.setNgModelValue(timestamp); }); }; /** * Handles click events on the month headers. Switches * the calendar to the year view. * @this {HTMLTableCellElement} The cell that was clicked. */ this.headerClickHandler = function() { self.calendarCtrl.setCurrentView('year', $$mdDateUtil.getTimestampFromNode(this)); }; } /*** Initialization ***/ /** * Initialize the controller by saving a reference to the calendar and * setting up the object that will be iterated by the virtual repeater. */ CalendarMonthCtrl.prototype.initialize = function(calendarCtrl) { /** * Dummy array-like object for virtual-repeat to iterate over. The length is the total * number of months that can be viewed. We add 2 months: one to include the current month * and one for the last dummy month. * * This is shorter than ideal because of a (potential) Firefox bug * https://bugzilla.mozilla.org/show_bug.cgi?id=1181658. */ this.items = { length: this.dateUtil.getMonthDistance( calendarCtrl.firstRenderableDate, calendarCtrl.lastRenderableDate ) + 2 }; this.calendarCtrl = calendarCtrl; this.attachScopeListeners(); calendarCtrl.updateVirtualRepeat(); // Fire the initial render, since we might have missed it the first time it fired. calendarCtrl.ngModelCtrl && calendarCtrl.ngModelCtrl.$render(); }; /** * Gets the "index" of the currently selected date as it would be in the virtual-repeat. * @returns {number} */ CalendarMonthCtrl.prototype.getSelectedMonthIndex = function() { var calendarCtrl = this.calendarCtrl; return this.dateUtil.getMonthDistance( calendarCtrl.firstRenderableDate, calendarCtrl.displayDate || calendarCtrl.selectedDate || calendarCtrl.today ); }; /** * Change the selected date in the calendar (ngModel value has already been changed). * @param {Date} date */ CalendarMonthCtrl.prototype.changeSelectedDate = function(date) { var self = this; var calendarCtrl = self.calendarCtrl; var previousSelectedDate = calendarCtrl.selectedDate; calendarCtrl.selectedDate = date; this.changeDisplayDate(date).then(function() { var selectedDateClass = calendarCtrl.SELECTED_DATE_CLASS; var namespace = 'month'; // Remove the selected class from the previously selected date, if any. if (previousSelectedDate) { var prevDateCell = document.getElementById(calendarCtrl.getDateId(previousSelectedDate, namespace)); if (prevDateCell) { prevDateCell.classList.remove(selectedDateClass); prevDateCell.setAttribute('aria-selected', 'false'); } } // Apply the select class to the new selected date if it is set. if (date) { var dateCell = document.getElementById(calendarCtrl.getDateId(date, namespace)); if (dateCell) { dateCell.classList.add(selectedDateClass); dateCell.setAttribute('aria-selected', 'true'); } } }); }; /** * Change the date that is being shown in the calendar. If the given date is in a different * month, the displayed month will be transitioned. * @param {Date} date */ CalendarMonthCtrl.prototype.changeDisplayDate = function(date) { // Initialization is deferred until this function is called because we want to reflect // the starting value of ngModel. if (!this.isInitialized) { this.buildWeekHeader(); this.calendarCtrl.hideVerticalScrollbar(this); this.isInitialized = true; return this.$q.when(); } // If trying to show an invalid date or a transition is in progress, do nothing. if (!this.dateUtil.isValidDate(date) || this.isMonthTransitionInProgress) { return this.$q.when(); } this.isMonthTransitionInProgress = true; var animationPromise = this.animateDateChange(date); this.calendarCtrl.displayDate = date; var self = this; animationPromise.then(function() { self.isMonthTransitionInProgress = false; }); return animationPromise; }; /** * Animates the transition from the calendar's current month to the given month. * @param {Date} date * @returns {angular.$q.Promise} The animation promise. */ CalendarMonthCtrl.prototype.animateDateChange = function(date) { if (this.dateUtil.isValidDate(date)) { var monthDistance = this.dateUtil.getMonthDistance(this.calendarCtrl.firstRenderableDate, date); this.calendarScroller.scrollTop = monthDistance * TBODY_HEIGHT; } return this.$q.when(); }; /** * Builds and appends a day-of-the-week header to the calendar. * This should only need to be called once during initialization. */ CalendarMonthCtrl.prototype.buildWeekHeader = function() { var firstDayOfWeek = this.dateLocale.firstDayOfWeek; var shortDays = this.dateLocale.shortDays; var row = document.createElement('tr'); for (var i = 0; i < 7; i++) { var th = document.createElement('th'); th.textContent = shortDays[(i + firstDayOfWeek) % 7]; row.appendChild(th); } this.$element.find('thead').append(row); }; /** * Attaches listeners for the scope events that are broadcast by the calendar. */ CalendarMonthCtrl.prototype.attachScopeListeners = function() { var self = this; self.$scope.$on('md-calendar-parent-changed', function(event, value) { self.changeSelectedDate(value); }); self.$scope.$on('md-calendar-parent-action', angular.bind(this, this.handleKeyEvent)); }; /** * Handles the month-specific keyboard interactions. * @param {Object} event Scope event object passed by the calendar. * @param {String} action Action, corresponding to the key that was pressed. */ CalendarMonthCtrl.prototype.handleKeyEvent = function(event, action) { var calendarCtrl = this.calendarCtrl; var displayDate = calendarCtrl.displayDate; if (action === 'select') { calendarCtrl.setNgModelValue(displayDate); } else { var date = null; var dateUtil = this.dateUtil; switch (action) { case 'move-right': date = dateUtil.incrementDays(displayDate, 1); break; case 'move-left': date = dateUtil.incrementDays(displayDate, -1); break; case 'move-page-down': date = dateUtil.incrementMonths(displayDate, 1); break; case 'move-page-up': date = dateUtil.incrementMonths(displayDate, -1); break; case 'move-row-down': date = dateUtil.incrementDays(displayDate, 7); break; case 'move-row-up': date = dateUtil.incrementDays(displayDate, -7); break; case 'start': date = dateUtil.getFirstDateOfMonth(displayDate); break; case 'end': date = dateUtil.getLastDateOfMonth(displayDate); break; } if (date) { date = this.dateUtil.clampDate(date, calendarCtrl.minDate, calendarCtrl.maxDate); this.changeDisplayDate(date).then(function() { calendarCtrl.focus(date); }); } } }; })();